@elora-cloud/elora-cli
Version:
elora build cli 前端打包脚手架
209 lines (204 loc) • 9.33 kB
JavaScript
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 };