packages/minifier-css/minifier.js
import path from 'path';
import url from 'url';
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 {String[]} Array containing the minified CSS.
*/
minifyCss(cssText) {
return Promise.await(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 resource = url.parse(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 (resource.protocol !== null ||
resource.href.startsWith('//') ||
resource.href.startsWith('#')) {
continue;
}
// Rewrite relative paths (that refers to the internal application tree)
// to absolute paths (addressable from the public build).
let absolutePath = isRelative(resource.path)
? pathJoin(basePath, resource.path)
: resource.path;
if (resource.hash) {
absolutePath += resource.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)));