/
githubmirror
/
meteor
Обзор
Документация
Войти
/
githubmirror
/
meteor
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
devel
packages/minifier-css/minifier.js
299 строк
10 KB
italo jose
refactor: replace Node.js 'url' module with the native URL API across various packages and tools
23 мар 2026, 23:05
23 мар 2026, 23:05
3b66476
Код
Авторство
О чём код?
import path from 'path'; import postcss from 'postcss'; import cssnano from 'cssnano'; const CssTools = { /** * Parse the incoming CSS string; return a CSS AST. * * @param {string} cssText The CSS string to be parsed. * @param {Object} options Options to pass to the PostCSS parser. * @return {postcss#Root} PostCSS Root AST. */ parseCss(cssText, options = {}) { // This function previously used the `css-parse` npm package, which // set the name of the css file being parsed using { source: 'filename' }. // If included, we'll convert this to the `postcss` equivalent, to maintain // backwards compatibility. if (options.source) { options.from = options.source; delete options.source; } return postcss.parse(cssText, options); }, /** * Using the incoming CSS AST, create and return a new object with the * generated CSS string, and optional sourcemap details. * * @param {postcss#Root} cssAst PostCSS Root AST. * @param {Object} options Options to pass to the PostCSS parser. * @return {Object} Format: { code: 'css string', map: 'sourcemap deatils' }. */ stringifyCss(cssAst, options = {}) { // This function previously used the `css-stringify` npm package, which // controlled sourcemap generation by passing in { sourcemap: true }. // If included, we'll convert this to the `postcss` equivalent, to maintain // backwards compatibility. if (options.sourcemap) { options.map = { inline: false, annotation: false, sourcesContent: false, }; delete options.sourcemap; } // explicitly set from to undefined to prevent postcss warnings if (!options.from){ options.from = void 0; } transformResult = cssAst.toResult(options); return { code: transformResult.css, map: transformResult.map ? transformResult.map.toJSON() : null, }; }, /** * Minify the passed in CSS string. * * @param {string} cssText CSS string to minify. * @return {Promise<String[]>} Array containing the minified CSS. */ minifyCss(cssText) { return CssTools.minifyCssAsync(cssText); }, /** * Minify the passed in CSS string. * * @param {string} cssText CSS string to minify. * @return {Promise<String[]>} Array containing the minified CSS. */ async minifyCssAsync(cssText) { return await postcss([cssnano({ safe: true })]) .process(cssText, { from: void 0, }) .then((result) => [result.css]); }, /** * Merge multiple CSS AST's into one. * * @param {postcss#Root[]} cssAsts Array of PostCSS Root objects. * @callback warnCb Callback used to handle warning messages. * @return {postcss#Root} PostCSS Root object. */ mergeCssAsts(cssAsts, warnCb) { const rulesPredicate = (rules, exclude = false) => { if (! Array.isArray(rules)) { rules = [rules]; } return node => { // PostCSS AtRule nodes have `type: 'atrule'` and a descriptive name, // e.g. 'import' or 'charset', while Comment nodes have type only. const nodeMatchesRule = rules.includes(node.name || node.type); return exclude ? !nodeMatchesRule : nodeMatchesRule; } }; // Simple concatenation of CSS files would break @import rules // located in the beginning of a file. Before concatenation, pull // @import rules to the beginning of a new syntax tree so they always // precede other rules. const newAst = postcss.root(); cssAsts.forEach((ast) => { if (ast.nodes) { // Pick only the imports from the beginning of file ignoring @charset // rules as every file is assumed to be in UTF-8. const charsetRules = ast.nodes.filter(rulesPredicate('charset')); if (charsetRules.some((rule) => { // According to MDN, only 'UTF-8' and "UTF-8" are the correct // encoding directives representing UTF-8. return ! /^(['"])UTF-8\1$/.test(rule.params); })) { warnCb( ast.filename, '@charset rules in this file will be ignored as UTF-8 is the ' + 'only encoding supported' ); } ast.nodes = ast.nodes.filter(rulesPredicate('charset', true)); let importCount = 0; for (let i = 0; i < ast.nodes.length; i++) { if (! rulesPredicate(['import', 'comment'])(ast.nodes[i])) { importCount = i; break; } } CssTools.rewriteCssUrls(ast); const imports = ast.nodes.splice(0, importCount); newAst.nodes.push(...imports); // If there are imports left in the middle of a file, warn users as it // might be a potential bug (imports are only valid at the beginning of // a file). if (ast.nodes.some(rulesPredicate('import'))) { warnCb( ast.filename, 'There are some @import rules in the middle of a file. This ' + 'might be a bug, as imports are only valid at the beginning of ' + 'a file.' ); } } }); // Now we can put the rest of CSS rules into new AST. cssAsts.forEach((ast) => { if (ast.nodes) { newAst.nodes.push(...ast.nodes); } }); return newAst; }, /** * We are looking for all relative urls defined with the `url()` functional * notation and rewriting them to the equivalent absolute url using the * `source` path provided by postcss. For performance reasons this function * acts by side effect by modifying the given AST without doing a deep copy. * * @param {postcss#Root} ast PostCSS Root object. * @return Modifies the ast param in place. */ rewriteCssUrls(ast) { const mergedCssPath = '/'; rewriteRules(ast.nodes, mergedCssPath); } }; if (typeof Profile !== 'undefined') { [ 'parseCss', 'stringifyCss', 'minifyCss', 'minifyCssAsync', 'mergeCssAsts', 'rewriteCssUrls', ].forEach(funcName => { CssTools[funcName] = Profile(`CssTools.${funcName}`, CssTools[funcName]); }); } export { CssTools }; const hasOwn = Object.prototype.hasOwnProperty; const rewriteRules = (rules, mergedCssPath) => { rules.forEach((rule) => { // Recurse if there are sub-rules. An example: // @media (...) { // .rule { url(...); } // } if (hasOwn.call(rule, 'nodes')) { rewriteRules(rule.nodes, mergedCssPath); } const appDir = process.cwd(); const sourceFile = rule.source.input.file; const sourceFileFromAppRoot = sourceFile ? sourceFile.replace(appDir, '') : ''; let basePath = pathJoin('/', pathDirname(sourceFileFromAppRoot)); // Set the correct basePath based on how the linked asset will be served. // XXX This is wrong. We are coupling the information about how files will // be served by the web server to the information how they were stored // originally on the filesystem in the project structure. Ideally, there // should be some module that tells us precisely how each asset will be // served but for now we are just assuming that everything that comes from // a folder starting with "/packages/" is served on the same path as // it was on the filesystem and everything else is served on root "/". if (! basePath.match(/^\/?packages\//i)) { basePath = "/"; } let value = rule.value; // Match css values containing some functional calls to `url(URI)` where // URI is optionally quoted. // Note that a css value can contains other elements, for instance: // background: top center url("background.png") black; // or even multiple url(), for instance for multiple backgrounds. var cssUrlRegex = /url\s*\(\s*(['"]?)(.+?)\1\s*\)/gi; let parts; while (parts = cssUrlRegex.exec(value)) { const oldCssUrl = parts[0]; const quote = parts[1]; const cssUrlValue = parts[2]; // We don't rewrite URLs starting with a protocol definition such as // http, https, or data, or those with network-path references // i.e. //img.domain.com/cat.gif if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(cssUrlValue) || cssUrlValue.startsWith('//') || cssUrlValue.startsWith('#')) { continue; } // Rewrite relative paths (that refers to the internal application tree) // to absolute paths (addressable from the public build). const hashIndex = cssUrlValue.indexOf('#'); const hash = hashIndex >= 0 ? cssUrlValue.slice(hashIndex) : ''; const pathPart = hashIndex >= 0 ? cssUrlValue.slice(0, hashIndex) : cssUrlValue; let absolutePath = isRelative(pathPart) ? pathJoin(basePath, pathPart) : pathPart; if (hash) { absolutePath += hash; } // We used to finish the rewriting process at the absolute path step // above. But it didn't work in case the Meteor application was deployed // under a sub-path (eg `ROOT_URL=http://localhost:3000/myapp meteor`) // in which case the resources linked in the merged CSS file would miss // the `myapp/` prefix. Since this path prefix is only known at launch // time (rather than build time) we can't use absolute paths to link // resources in the generated CSS. // // Instead we transform absolute paths to make them relative to the // merged CSS, leaving to the browser the responsibility to calculate // the final resource links (by adding the application deployment // prefix, here `myapp/`, if applicable). const relativeToMergedCss = pathRelative(mergedCssPath, absolutePath); const newCssUrl = `url(${quote}${relativeToMergedCss}${quote})`; value = value.replace(oldCssUrl, newCssUrl); } rule.value = value; }); }; const isRelative = path => path && path.charAt(0) !== '/'; // These are duplicates of functions in tools/files.js, because we don't have // a good way of exporting them into packages. // XXX deduplicate files.js into a package at some point so that we can use it // in core const toOSPath = p => process.platform === 'win32' ? p.replace(/\//g, '\\') : p; const toStandardPath = p => process.platform === 'win32' ? p.replace(/\\/g, '/') : p; const pathJoin = (a, b) => toStandardPath(path.join(toOSPath(a), toOSPath(b))); const pathDirname = p => toStandardPath(path.dirname(toOSPath(p))); const pathRelative = (p1, p2) => toStandardPath(path.relative(toOSPath(p1), toOSPath(p2)));