UNPKG

@squirrel-forge/sass-package-importer

Version:

A simple node sass package importer built for the new sass js api.

249 lines (205 loc) 8.49 kB
/** * Requires */ const fs = require( 'fs' ); const path = require( 'path' ); const { pathToFileURL } = require( 'url' ); /** * @typedef {Object} PackageImporterOptions * @property {boolean} strict - Require a package directory and a package.json if there is no module target info, default: false * @property {null|string} cwd - Set the working directory for the importer, default: null > process.cwd() * @property {string} prefix - Package import prefix, default: ~ * @property {Array<string>} ext - Array of acceptable extensions, defaults: see PACKAGE_IMPORTER_DEFAULT_OPTIONS.ext * @property {Array<string>} keys - Array of possible package keys to check for a file reference, defaults: see PACKAGE_IMPORTER_DEFAULT_OPTIONS.keys * @property {Array<string>} paths - Array of possible package locations, relative or absolute paths, defaults see PACKAGE_IMPORTER_DEFAULT_OPTIONS.paths */ /** * Resolve package entry if possible * @private * @param {string} module_name - Module name * @param {string} module_path - Module path * @param {Object|PackageImporterOptions} options - Importer options * @throws Error * @return {Object|{key: string, source: string}} - Empty object if not found or key and source */ function _packageImporter_get_pkg_entry( module_name, module_path, options ) { // Attempt to load the module package let pkg = null, error = null; try { pkg = require( path.join( module_path, 'package.json' ) ); } catch ( e ) { error = e; } // No valid package if ( error || ( pkg === null || typeof pkg !== 'object' ) ) { // In loose mode let just pray there is an index file sass can load if ( !options.strict ) { return null; } // In strict we fail throw new Error( 'SassPackageImporter failed to load package.json for: ' + module_name + ' in: ' + module_path ); } // If we do not find a fitting key, // it's the same as if there was no package and we pray that sass finds and index file let found = null; // Let see if we can find an appropriate source for ( let i = 0; i < options.keys.length; i++ ) { const k = options.keys[ i ]; // Key exists and has a possible value if ( typeof pkg[ k ] === 'string' && pkg[ k ].length ) { const ext = path.extname( pkg[ k ] ); // Skip possibly incompatible extension that's not empty but also not included in our options list if ( ext && ext.length && !options.ext.includes( ext ) ) { continue; } // If we found a possible entry well take it and stop checking found = pkg[ k ]; break; } } return found; } /** * Check if given path exists * @private * @param {string} check_path - Path to check * @param {string} module_name - Module name * @param {Object|PackageImporterOptions} options - Importer options * @return {null|string} - Path string if it exists */ function _packageImporter_get_pkg_path( check_path, module_name, options ) { // Resolve absolute or relative path const check = check_path.startsWith( path.sep ) ? path.resolve( check_path, module_name ) : path.resolve( options.cwd || process.cwd(), check_path, module_name ); // Return path if it exists if ( fs.lstatSync( check ).isDirectory() ) { return check; } return null; } /** * Get package info from string * @private * @param {string} input - Input string * @param {Object|PackageImporterOptions} options - Importer options * @throws Error * @return {{module_name, module_path: string, module_target: (null|string)}} - Package info object */ function _packageImporter_get_pkg_info( input, options ) { // Module target is the path after the module name let module_target = null, module_name = input; // Split it up to see what's what, also remove empties const p = input.split( path.sep ).filter( ( v ) => { return !!v.length; } ); // If we have more than one segment if ( p.length > 1 ) { // The first is part of the module name for sure module_name = p.shift(); // If the first started with an @ it's an org // And if present, the second element is also part of the module name if ( p.length && module_name.charAt( 0 ) === '@' ) { module_name += path.sep + p.shift(); } // Anything we have left now is part of the module target path if ( p.length ) { module_target = p.join( path.sep ); } } // Resolve the actual module path and return data let module_path; for ( let i = 0; i < options.paths.length; i++ ) { module_path = _packageImporter_get_pkg_path( options.paths[ i ], module_name, options ); if ( module_path ) break; } // Fail in strict if ( !module_path ) { if ( options.strict ) throw new Error( 'SassPackageImporter failed to resolve an existing path for: ' + module_name ); // Or allow sass to check, but it will likely not find anything either module_path = path.join( options.paths[ 0 ], module_name ); } return { module_name, module_path, module_target }; } /** * Package importer default options * @private * @type {Object|PackageImporterOptions} */ const PACKAGE_IMPORTER_DEFAULT_OPTIONS = { strict : false, cwd : null, prefix : '~', ext : [ '.scss', '.sass', '.css' ], keys : [ 'scss', 'sass', 'style', 'css', 'main.scss', 'main.sass', 'main.style', 'main.css', 'main' ], paths : [ 'node_modules' ], }; /** * @typedef {Object} SassFileImporter * @property {Function|SassFileImporterFinder} findFileUrl - File url finder */ /** * @typedef {Function} SassFileImporterFinder * @param {string} url - Sass import url * @return {null|URL} - URL instance of package source, null if not found */ /** * Get package importer object for sass api * @public * @param {Object|PackageImporterOptions} options - Importer options * @param {null|Object} sassOptions - Sass options to add importer to * @return {Object|SassFileImporter} - Importer object to use in sass options */ module.exports = function packageImporter( options = null, sassOptions = null ) { // Get default options const local_options = { ...PACKAGE_IMPORTER_DEFAULT_OPTIONS }; // Assign custom options if ( options !== null && typeof options === 'object' ) { Object.assign( local_options, options ); } // Module context const context = { /** * Detect and find node_modules package url * @type {SassFileImporter} * @public * @param {string} url - Import url * @return {null|URL} - Resolved url or null */ findFileUrl( url ) { // Skip if not the appropriate prefix if ( !url.startsWith( local_options.prefix ) ) return null; // Get possible module name and path const { module_name, module_path, module_target, } = _packageImporter_get_pkg_info( url.substring( local_options.prefix.length ), local_options ); // If it's just the module name, let fetch the source from the package let source = module_target; if ( !source ) { // Check if we can select an appropriate entry from the package source = _packageImporter_get_pkg_entry( module_name, module_path, local_options ); } // We have nothing in the package or in the original path, let's hope for the best if ( !source || !source.length ) { source = ''; } // Return a local file url for sass to process return new URL( pathToFileURL( path.join( module_path, source ) ) ); } }; // Add to sassOptions importers 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 importers array if it does not exist if ( !( sassOptions.importers instanceof Array ) ) { sassOptions.importers = []; } // Append module context sassOptions.importers.push( context ); } // Return module context return context; };