UNPKG

@elora-cloud/elora-cli

Version:
209 lines (204 loc) 9.33 kB
import { dirname } from 'node:path'; import { createFilter } from 'rollup-pluginutils'; import dartSass from 'sass'; function scss(options = {}) { const filter = createFilter(options.include || ['/**/*.css', '/**/*.scss', '/**/*.sass'], options.exclude); const insertStyleFnName = '___$insertStylesToHeader'; const styles = {}; const fileName = options.fileName || (options.output === 'string' ? options.output : undefined); const name = options.name || 'output.css'; const prefix = options.prefix ? `${options.prefix}\n` : ''; let includePaths = options.includePaths || ['node_modules/']; // eslint-disable-next-line node/prefer-global/process includePaths.push(process.cwd()); const compileToCSS = async function (scss) { // Compile SASS to CSS if (scss.length) { includePaths = includePaths.filter((v, i, a) => a.indexOf(v) === i); try { return dartSass.compileString(prefix + scss, { ...options, loadPaths: includePaths, importer: { findFileUrl: (url, _content) => { /* If a path begins with `.`, then it's a local import and this * importer cannot handle it. This check covers both `.` and * `..`. * * Additionally, if an import path begins with `url` or `http`, * then it's a remote import, this importer also cannot handle * that. */ if (url.startsWith('.') || url.startsWith('url') || url.startsWith('http')) { /* The importer returns `null` to defer processing the import * back to the sass compiler. */ return null; } /* If the requested path begins with a `~`, we remove it. This * character is used by webpack-contrib's sass-loader to * indicate the import is from the node_modules folder. Since * this is so standard in the JS world, the importer supports * it, by removing it and ignoring it. */ const cleanUrl = url.startsWith('~') ? url.replace('~', '') : url; /* Now, the importer uses `require.resolve()` to attempt * to resolve the path to the requested file. In the case * of a standard node_modules project, this will use Node's * `require.resolve()`. In the case of a Plug 'n Play project, * this will use the `require.resolve()` provided by the * package manager. * * This statement is surrounded by a try/catch block because * if Node or the package manager cannot resolve the requested * file, they will throw an error, so the importer needs to * defer to sass, by returning `null`. * * The paths property tells `require.resolve()` where to begin * resolution (i.e. who is requesting the file). */ try { const resolved = require.resolve(cleanUrl, { paths: [prefix + scss], }); /* Since `require.resolve()` will throw an error if a file * doesn't exist. It's safe to assume the file exists and * pass it off to the sass compiler. */ return new URL(resolved); } // eslint-disable-next-line unused-imports/no-unused-vars catch (e) { /* Just because `require.resolve()` couldn't find the file * doesn't mean it doesn't exist. It may still be a local * import that just doesn't list a relative path, so defer * processing back to sass by returning `null` */ return null; } }, }, }); } catch (e) { if (options.failOnError) { throw e; } console.log(); console.log(red(`Error:\n\t${e.message}`)); if (e.message.includes('Invalid CSS')) { console.log(green('Solution:\n\t' + 'fix your Sass code')); console.log(`Line: ${e.line}`); console.log(`Column: ${e.column}`); } if (e.message.includes('sass') && e.message.includes('find module')) { console.log(green('Solution:\n\t' + 'npm install --save-dev sass')); } if (e.message.includes('node-sass') && e.message.includes('bindings')) { console.log(green('Solution:\n\t' + 'npm rebuild node-sass --force')); } console.log(); } } return { css: '', loadedUrls: [] }; }; return { name: 'scss', intro() { // eslint-disable-next-line ts/no-use-before-define return options.insert === true ? insertStyleFn.replace(/insertStyleFn/, insertStyleFnName) : ''; }, async transform(code, id) { if (!filter(id)) { return; } // Add the include path before doing any processing includePaths.push(dirname(id)); // Rebuild all scss files if anything happens to this folder // TODO: check if it's possible to get a list of all dependent scss files // and only watch those if (options.watch) { const files = Array.isArray(options.watch) ? options.watch : [options.watch]; files.forEach(file => this.addWatchFile(file)); } if (options.insert === true) { // When the 'insert' is enabled, the stylesheet will be inserted into <head/> tag. const { css } = await compileToCSS(code); return { code: `export default ${insertStyleFnName}(${JSON.stringify(css)})`, map: { mappings: '' }, }; } else if (options.output === false) { // When output is disabled, the stylesheet is exported as a string const { css } = await compileToCSS(code); return { code: `export default ${JSON.stringify(css)}`, map: { mappings: '' }, }; } // Map of every stylesheet styles[id] = code; return ''; }, async generateBundle(_opts) { // No stylesheet needed if (options.output === false || options.insert === true) { return; } // Combine all stylesheets let scss = ''; for (const id in styles) { scss += styles[id] || ''; } const compiled = await compileToCSS(scss); if (!compiled) { return; } // Emit styles through callback if (typeof options.output === 'function') { options.output(compiled.css, styles); return; } // Don't create unwanted empty stylesheets if (!compiled.css.length) { return; } // Emit styles to file this.emitFile({ type: 'asset', source: compiled.css, name, fileName, }); if (options.sourceMap && compiled.sourceMap) { this.emitFile({ type: 'asset', source: compiled.sourceMap.mappings, name: `${name}.map`, fileName: fileName && `${fileName}.map`, }); } }, }; } /** * Create a style tag and append to head tag * * @param {string} css style * @return {string} css style */ const insertStyleFn = `function insertStyleFn(css) { if (!css) { return } if (typeof window === 'undefined') { return } const style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.innerHTML = css; document.head.appendChild(style); return css }`; function red(text) { return `\x1B[1m\x1B[31m${text}\x1B[0m`; } function green(text) { return `\x1B[1m\x1B[32m${text}\x1B[0m`; } export { scss as default };