postcss-smart-asset
Version:
PostCSS plugin to rebase or inline on url().
487 lines (380 loc) • 13.9 kB
JavaScript
/*! postcss-smart-asset v3.1.0 by Sebastian Software <s.werner@sebastian-software.de> */
;
Object.defineProperty(exports, '__esModule', { value: true });
var path = require('path');
require('postcss');
require('core-js/modules/es.error.cause.js');
var fs = require('fs');
var assetHash = require('asset-hash');
var mime = require('mime/lite');
var url = require('url');
var minimatch = require('minimatch');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
var mime__default = /*#__PURE__*/_interopDefaultLegacy(mime);
var url__default = /*#__PURE__*/_interopDefaultLegacy(url);
var minimatch__default = /*#__PURE__*/_interopDefaultLegacy(minimatch);
/**
* Normalizing result url, before replace decl value
*/
const normalize = assetUrl => {
assetUrl = path__default.normalize(assetUrl);
if (path__default.sep === "\\") {
assetUrl = assetUrl.replace(/\\/g, "/");
}
if (assetUrl.charAt(0) !== "." && assetUrl.charAt(0) !== "/") {
assetUrl = `./${assetUrl}`;
}
return assetUrl;
};
const isUrlWithoutPathname = assetUrl => assetUrl[0] === "#" || assetUrl.indexOf("%23") === 0 || assetUrl.indexOf("data:") === 0 || /^[a-z]+:\/\//.test(assetUrl);
/**
* Check if url is absolute, hash or data-uri
*/
const isUrlShouldBeIgnored = (assetUrl, options) => isUrlWithoutPathname(assetUrl) || assetUrl[0] === "/" && !options.basePath;
const getAssetsPath = (baseDir, assetsPath, relative) => path__default.resolve(baseDir, assetsPath || "", relative || "");
/**
* Target path, output base dir
*/
const getTargetDir = dir => dir.from !== dir.to ? dir.to : process.cwd();
/**
* Stylesheet file path from decl
*/
const getPathDeclFile = decl => decl.source && decl.source.input && decl.source.input.file;
/**
* Stylesheet file dir from decl
*/
const getDirDeclFile = decl => {
const filename = getPathDeclFile(decl);
return filename ? path__default.dirname(filename) : process.cwd();
};
/**
* Returns paths list, where we can find assets file
*
* @param basePath - base paths where trying search to assets file
* @param dirFrom
* @param relPath - relative asset path
*/
const getPathByBasePath = (basePath, dirFrom, relPath) => {
if (relPath[0] === "/") {
relPath = `.${relPath}`;
}
basePath = !Array.isArray(basePath) ? [basePath] : basePath;
return basePath.map(pathItem => getAssetsPath(dirFrom, pathItem, relPath));
};
/**
* Preparing asset paths and data
*/
const prepareAsset = (assetUrl, dir, decl) => {
const parsedUrl = url__default.parse(assetUrl);
const pathname = !isUrlWithoutPathname(assetUrl) ? parsedUrl.pathname : null;
const absolutePath = pathname ? path__default.resolve(path__default.join(dir.file, pathname)) : getPathDeclFile(decl);
return {
url: assetUrl,
originUrl: assetUrl,
pathname,
absolutePath: absolutePath || dir.from,
relativePath: absolutePath ? path__default.relative(dir.from, absolutePath) : ".",
search: parsedUrl.search || "",
hash: parsedUrl.hash || ""
};
};
const getFile = (asset, options, dir, warn) => {
const paths = options.basePath ? getPathByBasePath(options.basePath, dir.from, asset.pathname) : [asset.absolutePath];
const filePath = paths.find(fs__default.existsSync);
if (!filePath) {
warn(`Can't read file '${paths.join()}', ignoring`);
return;
}
return {
path: filePath,
mimeType: mime__default.getType(filePath)
};
};
const getHashName = (file, options) => assetHash.getHashedName(file.path, options);
/**
* Copy images from readed from url() to an specific assets destination
* (`assetsPath`) and fix url() according to that path.
* You can rename the assets by a hash or keep the real filename.
*
* Option assetsPath is require and is relative to the css destination (`to`)
*/
async function copyAsset(asset, dir, options, decl, warn, result, addDependency) {
if (!options.assetsPath && dir.from === dir.to) {
warn("Option `to` of postcss is required, ignoring");
return;
}
const file = getFile(asset, options, dir, warn);
if (!file) {
return;
}
addDependency(file.path);
let assetRelativePath = options.useHash ? await getHashName(file, options.hashOptions) : asset.relativePath;
if (options.useHash && options.keepName) {
const pathObj = path__default.parse(assetRelativePath);
const fileName = path__default.parse(asset.relativePath).name;
pathObj.name = `${fileName}~${pathObj.name}`;
delete pathObj.base; // otherwise it would override name
assetRelativePath = path__default.format(pathObj);
}
const targetDir = getTargetDir(dir);
const newAssetBaseDir = getAssetsPath(targetDir, options.assetsPath);
const newAssetPath = path__default.join(newAssetBaseDir, assetRelativePath);
const newRelativeAssetPath = normalize(path__default.relative(targetDir, newAssetPath));
await fs.promises.mkdir(path__default.dirname(newAssetPath), {
recursive: true
});
await fs.promises.copyFile(file.path, newAssetPath);
return `${newRelativeAssetPath}${asset.search}${asset.hash}`;
}
/**
* Transform url() based on a custom callback
*/
function customAsset(asset, dir, options) {
return options.url.apply(null, arguments);
}
/**
* Optimize encoding SVG files (IE9+, Android 3+)
*
* @see https://codepen.io/tigt/post/optimizing-svgs-in-data-uris
*/
const optimizedSvgEncode = svgContent => {
const result = encodeURIComponent(svgContent).replace(/%3D/g, "=").replace(/%3A/g, ":").replace(/%2F/g, "/").replace(/%22/g, "'").replace(/%2C/g, ",").replace(/%3B/g, ";"); // Lowercase the hex-escapes for better gzipping
return result.replace(/(%[\dA-Z]{2})/g, (matched, AZ) => AZ.toLowerCase());
};
/**
* Encoding file contents to string
*/
var encodeFile = (async (file, encodeType, shouldOptimizeSvgEncode) => {
const dataMime = `data:${file.mimeType}`;
const contents = await fs.promises.readFile(file.path);
if (encodeType === "base64") {
return `${dataMime};base64,${contents.toString("base64")}`;
}
const encodeFunc = encodeType === "encodeURI" ? encodeURI : encodeURIComponent;
const content = contents.toString("utf8") // removing new lines
.replace(/\n+/g, "");
let encodedStr = shouldOptimizeSvgEncode && encodeType === "encodeURIComponent" ? optimizedSvgEncode(content) : encodeFunc(content);
encodedStr = encodedStr.replace(/%20/g, " ").replace(/#/g, "%23");
return `${dataMime},${encodedStr}`;
});
/**
* Fix url() according to source (`from`) or destination (`to`)
*/
function rebaseAsset(asset, dir) {
const rebasedUrl = normalize(path__default.relative(dir.to, asset.absolutePath));
return `${rebasedUrl}${asset.search}${asset.hash}`;
}
function inlineFallback(originUrl, dir, options) {
if (typeof options.fallback === "function") {
return options.fallback.apply(null, arguments);
}
switch (options.fallback) {
case "copy":
return copyAsset(...arguments);
case "rebase":
return rebaseAsset(...arguments);
}
}
/**
* Inline image in url()
*/
/* eslint-disable complexity */
async function inlineAsset(asset, dir, options, decl, warn, result, addDependency) {
const file = getFile(asset, options, dir, warn);
if (!file) {
return;
}
if (!file.mimeType) {
warn(`Unable to find asset mime-type for ${file.path}`);
return;
}
const maxSize = (options.maxSize || 0) * 1024;
if (maxSize) {
const stats = fs__default.statSync(file.path);
if (stats.size >= maxSize) {
return inlineFallback.apply(this, arguments);
}
}
const isSvg = file.mimeType === "image/svg+xml";
const defaultEncodeType = isSvg ? "encodeURIComponent" : "base64";
const encodeType = options.encodeType || defaultEncodeType; // Warn for svg with hashes/fragments
if (isSvg && asset.hash && !options.ignoreFragmentWarning) {
// eslint-disable-next-line max-len
warn(`Image type is svg and link contains #.
PostCSS Smart Asset can't handle SVG fragments.
SVG file fully inlined. ${file.path}`);
}
addDependency(file.path);
const optimizeSvgEncode = isSvg && options.optimizeSvgEncode;
const encodedStr = await encodeFile(file, encodeType, optimizeSvgEncode);
const resultValue = options.includeUriFragment && asset.hash ? `${encodedStr}${asset.hash}` : encodedStr; // wrap url by quotes if percent-encoded svg
return isSvg && encodeType !== "base64" ? `"${resultValue}"` : resultValue;
}
/**
* Returns whether the given asset matches the given pattern
Allways returns true if the given pattern is empty
*
* @param asset the processed asset
* @param pattern A minimatch string,
* regular expression or function to test the asset
*/
const matchesFilter = (asset, pattern) => {
const relativeToRoot = path__default.relative(process.cwd(), asset.absolutePath);
if (typeof pattern === "string") {
pattern = minimatch__default.filter(pattern);
return pattern(relativeToRoot);
}
if (pattern instanceof RegExp) {
return pattern.test(relativeToRoot);
}
if (pattern instanceof Function) {
return pattern(asset);
}
return true;
};
/**
* Matching single option
*/
const matchOption = (asset, option) => {
const matched = matchesFilter(asset, option.filter);
if (!matched) {
return false;
}
return typeof option.url === "function" || !isUrlShouldBeIgnored(asset.url, option);
};
const isMultiOption = option => option.multi && typeof option.url === "function";
/**
* Matching options by asset
*/
const matchOptions = (asset, options) => {
if (!options) {
return;
}
if (Array.isArray(options)) {
const optionIndex = options.findIndex(option => matchOption(asset, option));
if (optionIndex < 0) {
return;
}
const matchedOption = options[optionIndex]; // if founded option is last
if (optionIndex === options.length - 1) {
return matchedOption;
}
const extendOptions = options.slice(optionIndex + 1).filter(option => (isMultiOption(matchedOption) || isMultiOption(option)) && matchOption(asset, option));
return extendOptions.length > 0 ? [matchedOption].concat(extendOptions) : matchedOption;
}
if (matchOption(asset, options)) {
return options;
}
};
const modeMap = {
copy: copyAsset,
custom: customAsset,
inline: inlineAsset,
rebase: rebaseAsset
};
/**
* Restricted modes
*/
const PROCESS_TYPES = new Set(["rebase", "inline", "copy", "custom"]);
const getUrlProcessorType = optionUrl => typeof optionUrl === "function" ? "custom" : optionUrl || "rebase";
function getUrlProcessor(optionUrl) {
const mode = getUrlProcessorType(optionUrl);
if (!PROCESS_TYPES.has(mode)) {
throw new Error(`Unknown mode for postcss-url: ${mode}`);
}
return modeMap[mode];
}
const wrapUrlProcessor = (urlProcessor, result, decl) => {
const warn = message => decl.warn(result, message);
const addDependency = file => result.messages.push({
type: "dependency",
file,
parent: getPathDeclFile(decl)
});
return (asset, dir, option) => urlProcessor(asset, dir, option, decl, warn, result, addDependency);
};
const replaceUrl = (url, dir, options, result, decl) => {
const asset = prepareAsset(url, dir, decl);
const matchedOptions = matchOptions(asset, options);
if (!matchedOptions) {
return;
}
const process = option => {
const wrappedUrlProcessor = wrapUrlProcessor(getUrlProcessor(option.url), result, decl);
return wrappedUrlProcessor(asset, dir, option);
};
if (Array.isArray(matchedOptions)) {
matchedOptions.forEach(option => {
asset.url = process(option);
});
} else {
asset.url = process(matchedOptions);
}
return asset.url;
};
const WITH_QUOTES = /^["']/;
function buildResult(newUrl, matched, before, after) {
if (!newUrl) {
return matched;
}
if (WITH_QUOTES.test(newUrl) && WITH_QUOTES.test(after)) {
before = before.slice(0, -1);
after = after.slice(1);
}
return `${before}${newUrl}${after}`;
} // Tracks visited declarations
const processTracker = new Set();
const declProcessor = (from, to, options, result, decl) => {
if (processTracker.has(decl)) {
return;
}
const dir = {
from,
to,
file: getDirDeclFile(decl)
};
const pattern = /(url\(\s*["']?)([^"')]+)(["']?\s*\))/g;
if (!pattern) {
return;
}
const matches = decl.value.match(pattern);
if (!matches) {
return;
}
return Promise.all(matches.map((singleMatch, index) => {
const [matched, before, url, after] = /(url\(\s*["']?)([^"')]+)(["']?\s*\))/.exec(singleMatch);
const replacement = replaceUrl(url, dir, options, result, decl);
if (replacement) {
if (replacement.then) {
return replacement.then(resolved => // const fullReplacement = resolved == null ? null : `${before}${resolved}${after}`
// return fullReplacement
buildResult(resolved, singleMatch, before, after));
} // const fullReplacement = `${before}${replacement}${after}`
return buildResult(replacement, singleMatch, before, after);
}
return null;
})).then(values => {
processTracker.add(decl);
decl.value = decl.value.replace(pattern, match => {
const replacement = values.shift();
return replacement == null ? match : replacement;
});
});
};
var index = ((options = {}) => ({
postcssPlugin: "postcss-smart-asset",
Declaration(decl, {
result
}) {
const opts = result.opts;
const from = opts.from ? path__default.dirname(opts.from) : ".";
const to = opts.to ? path__default.dirname(opts.to) : from;
return declProcessor(from, to, options, result, decl);
}
})); // PostCSS v8 marker
const postcss = true;
exports["default"] = index;
exports.postcss = postcss;
//# sourceMappingURL=index.cjs.js.map