@squirrel-forge/sass-base64-loader
Version:
A node sass function for importing files as base64 encoded strings built for the new sass js api.
441 lines (375 loc) • 13.5 kB
JavaScript
/**
* Requires
*/
const fs = require( 'fs' );
const path = require( 'path' );
const sass = require( 'sass' );
/**
* Require optional
* @private
* @param {string} name - Module name
* @param {string} version - Version for fatal notice
* @param {boolean} fatal - Throw if not available
* @throws Error
* @return {null|*} - Module if available
*/
function requireOptional( name, version, fatal = false ) {
let module = null;
try {
module = require( name );
} catch ( err ) {
if ( fatal ) throw new Error( `Requires ${name}@${version}` );
}
return module;
}
/**
* String is url
* @private
* @param {string} str - Possible url string
* @return {boolean} - True if valid url
*/
function isUrl( str ) {
let url;
try {
url = new URL( str );
} catch ( e ) {
return false;
}
return url.protocol === 'http:' || url.protocol === 'https:';
}
/**
* Get from cache
* @private
* @param {string} source - Source
* @param {Object} cache - Cache object
* @return {null|string} - Cached string if available
*/
function _internal_base64load_fromCache( source, cache ) {
const value = cache.get ? cache.get( source ) : cache[ source ];
if ( value ) return value;
return null;
}
/**
* Write to cache
* @private
* @param {string} source - Source
* @param {string} value - Value to save
* @param {Object} cache - Cache object
* @return {void}
*/
function _internal_base64load_toCache( source, value, cache ) {
if ( cache.set ) {
cache.set( source, value );
} else {
cache[ source ] = value;
}
}
/**
* @typedef {Object} Base64loadFileResult
* @property {string} file - File or url
* @property {Buffer} file_buffer - File buffer instance
* @property {null|string} mimetype - File mimetype
*/
/**
* Resolve source sync
* @private
* @param {string} source - Source
* @param {Object|Base64loadOptions} options - Loader options
* @throws Error
* @return {Object|Base64loadFileResult} - File result
*/
function _internal_base64load_resolve_source_sync( source, options ) {
// Resolve local file path
const cwd = options.cwd || process.cwd();
// By default assume absolute path
let file = source;
// Resolve relative path
if ( !source.startsWith( path.sep ) ) {
file = path.resolve( cwd, source );
}
// Check exists
let exists;
try {
exists = fs.lstatSync( file ).isFile();
} catch ( e ) {
exists = false;
}
if ( !exists ) {
throw new Error( `base64load(${source},$mimetype) File not found: ${file}` );
}
// Read file
const file_buffer = fs.readFileSync( file );
return { file : file, file_buffer : file_buffer, mimetype : null };
}
/**
* Get mimetype from info
* @private
* @param {string} source - Source
* @param {null|string} mime - Mimetype
* @param {null|Buffer} file_buffer - File buffer
* @param {null|string} file - File path
* @throws Error
* @return {Promise<string>} - Mimetype string
*/
async function _internal_base64load_get_mimetype_async( source, mime, file_buffer = null, file = null ) {
// Detect mimetype
if ( ( file || file_buffer ) && ( !mime || !mime.length ) ) {
// Get file-type or throw with requirement
const fileType = requireOptional( 'file-type', '^16.5.3', true );
// Check buffer
if ( file_buffer ) {
const buffer_type = await fileType.fromBuffer( file_buffer );
if ( buffer_type && buffer_type.mime ) {
mime = buffer_type.mime;
}
}
// Let's check again
if ( file && ( !mime || !mime.length ) ) {
const file_type = await fileType.fromFile( file );
if ( file_type && file_type.mime ) {
mime = file_type.mime;
}
}
}
// Fail check
if ( !mime || !mime.length ) {
throw new Error( `base64load(${source},$mimetype) Failed to detect $mimetype from $source` );
}
return mime;
}
/**
* Resolve source async
* @private
* @param {string} source - Source
* @param {null|string} mime - Mimetype
* @param {Object|Base64loadOptions} options - Loader options
* @throws Error
* @return {Promise<Object|Base64loadFileResult>} - File result
*/
async function _internal_base64load_resolve_source_async( source, mime, options ) {
let buf, file_path = null;
if ( isUrl( source ) ) {
// Remote loading must be active
if ( !options.remote ) {
throw new Error( `base64load(${source},$mimetype) To use remote url loading, set remote = true in your options` );
}
// Get node-fetch or throw with requirement
const fetch = requireOptional( 'node-fetch', '^2.6.7', true );
// Fetch file form url
let result;
try {
result = await fetch( source );
} catch ( err ) {
throw new Error( `base64load(${source},$mimetype) Internal error fetching $source: ` + err );
}
// Not a valid result
if ( !result || !result.ok ) {
const status_text = result ? ' ' + result.status + '#' + result.statusText : '';
throw new Error( `base64load(${source},$mimetype) Error${status_text} while fetching $source` );
}
// Use result and content-type and mime but only if net set allowing the argument value to prevail
buf = await result.buffer();
if ( !mime || !mime.length ) {
mime = result.headers.get( 'content-type' );
}
} else {
// Default get sync for local files
const { file, file_buffer } = _internal_base64load_resolve_source_sync( source, options );
buf = file_buffer;
file_path = file;
}
// Detect mimetype if required
mime = await _internal_base64load_get_mimetype_async( source, mime, buf, file_path );
// Return data
return { file : source, file_buffer : buf, mimetype : mime };
}
/**
* Get base64 sass string from local file
* @private
* @param {string} source - Source
* @param {null|string} mime - Mime
* @param {Base64loadOptions} options - Loader options
* @return {sass.SassString} - Sass string
*/
function _internal_base64load_sync( source, mime, options ) {
// Check cache
const cached = options.cache ? _internal_base64load_fromCache( source, options.cache ) : null;
if ( cached ) {
return new sass.SassString( cached );
}
// Resolve source
const { file_buffer } = _internal_base64load_resolve_source_sync( source, options );
// Output the result
const output = `data:${mime};base64,${file_buffer.toString( 'base64' )}`;
options.cache && _internal_base64load_toCache( source, output, options.cache );
return new sass.SassString( output );
}
/**
* Get base64 sass string from local or remote file
* @private
* @param {string} source - Source
* @param {null|string} mime - Mime
* @param {Base64loadOptions} options - Loader options
* @return {Promise<sass.SassString>} - Sass string
*/
async function _internal_base64load_async( source, mime, options ) {
// Check cache
const cached = options.cache ? _internal_base64load_fromCache( source, options.cache ) : null;
if ( cached ) {
return new sass.SassString( cached );
}
// Resolve source
const { file_buffer, mimetype } = await _internal_base64load_resolve_source_async( source, mime, options );
// Output the result
const output = `data:${mimetype};base64,${file_buffer.toString( 'base64' )}`;
options.cache && _internal_base64load_toCache( source, output, options.cache );
return new sass.SassString( output );
}
/**
* @typedef {Object} Base64loadArguments
* @property {string} source - Source
* @property {null|string} mime - Mimetype
*/
/**
* Get valid arguments from function input
* @private
* @param {Array} input - Sass function arguments
* @param {boolean} sync - Sync mode, default: true
* @throws Error
* @return {Object|Base64loadArguments} - Valid arguments
*/
function _internal_base64load_valid_arguments( input, sync = true ) {
// $source must be a string
if ( !( input[ 0 ] instanceof sass.SassString ) ) {
throw new Error( 'base64load($source,$mimetype) Invalid $source argument type' );
}
// Source must have a length
const source = input[ 0 ].assertString( 'source' ).text;
if ( !source.length ) {
throw new Error( 'base64load($source,$mimetype) Invalid $source argument' );
}
// Check source for url, requires remote and async variant
if ( sync && isUrl( source ) ) {
throw new Error( `base64load(${source},$mimetype) To use the async variant for url loading, set remote = true in your options` );
}
// $mimetype must be null or not empty in async and not empty in sync mode
const mime = input[ 1 ] === sass.sassNull ? null : input[ 1 ].assertString( 'mime' ).text;
if ( sync && ( !mime || !mime.length ) || mime && ( typeof mime !== 'string' || !mime.length ) ) {
throw new Error( `base64load(${source},$mimetype) Requires $mimetype argument` + ( sync ? ' in sync mode' : '' ) );
}
// Return parsed
return { source, mime };
}
/**
* @callback Base64loadStringCacheGetter
* @param {string} key - Cache key
* @return {null|string} - Value if available
*/
/**
* @callback Base64loadStringCacheSetter
* @param {string} key - Cache key
* @param {string} value - Cache value
* @return {void}
*/
/**
* @typedef {Object} Base64loadStringCache
* @property {Function|Base64loadStringCacheGetter} get - Get value from key
* @property {Function|Base64loadStringCacheSetter} set - Set key with value
*/
/**
* @typedef {Object} Base64loadOptions
* @property {boolean} detect - Auto detect file mimetypes, default: false
* @property {boolean} remote - Load files from http urls, default: false
* @property {null|string} cwd - Base path for resolving relative paths, default: null > process.cwd()
* @property {null|Object|Base64loadStringCache} cache - Caching object, default: {}
*/
/**
* Base64load default options
* @type {Object|Base64loadOptions}
*/
const BASE64LOAD_DEFAULT_OPTIONS = {
detect : false,
remote : false,
cwd : null,
cache : {},
};
/**
* Sass function signature
* @type {string}
*/
const SASS_FUNCTION_SIGNATURE = 'base64load($source,$mimetype:null)';
/**
* Sass base64load factory
* @param {Object|Base64loadOptions} options - Loader options
* @param {null|Object} sassOptions - Sass options to add function to
* @return {{signature: string, callback: (function(Array): sass.SassString)}} - Sass custom function plugin info
*/
module.exports = function base64Loader( options = null, sassOptions = null ) {
// Get default options
const local_options = { ...BASE64LOAD_DEFAULT_OPTIONS };
// Assign custom options
if ( options !== null && typeof options === 'object' ) {
Object.assign( local_options, options );
}
/**
* Load source sync
* @param {Array} input - Sass function arguments
* @throws Error
* @return {sass.SassString} - Encoded value
*/
const base64loadSync = function base64loadSync( input ) {
const { source, mime } = _internal_base64load_valid_arguments( input );
// Attempt load the source
const result = _internal_base64load_sync( source, mime, local_options );
// Check the result to be sure
if ( !( result instanceof sass.SassString ) ) {
throw new Error( `base64load(${source},${mime || 'null'}) Invalid result type` );
}
return result;
};
/**
* Load source async
* @param {Array} input - Sass function arguments
* @throws Error
* @return {Promise<sass.SassString>} - Encoded value
*/
const base64loadAsync = async function base64loadAsync( input ) {
const { source, mime } = _internal_base64load_valid_arguments( input, false );
// Attempt load the source
const result = await _internal_base64load_async( source, mime, local_options );
// Check the result to be sure
if ( !( result instanceof sass.SassString ) ) {
throw new Error( `base64load(${source},${mime || 'null'}) Invalid result type` );
}
return result;
};
// Output format
const handler = {
signature : SASS_FUNCTION_SIGNATURE,
callback : local_options.remote || local_options.detect ? base64loadAsync : base64loadSync,
};
// Add to sassOptions functions property
if ( sassOptions !== null ) {
// Expect at least some sort of object
if ( typeof sassOptions !== 'object' ) {
throw new Error( 'The sassOptions argument must be an object' );
}
// Create functions object if it does not exist
if ( typeof sassOptions.functions !== 'object' ) {
sassOptions.functions = {};
}
// Make sure the signature is not defined yet
if ( sassOptions.functions[ SASS_FUNCTION_SIGNATURE ] ) {
throw new Error( 'Sass function signature already defined' );
}
// Set function for signature
sassOptions.functions[ SASS_FUNCTION_SIGNATURE ] = handler.callback;
}
// Return sass custom function information
return handler;
};
/**
* Export sass function signature
* @type {string}
*/
module.exports.signature = SASS_FUNCTION_SIGNATURE;