UNPKG

postcss-smart-asset

Version:
474 lines (371 loc) 13.2 kB
/*! postcss-smart-asset v3.1.0 by Sebastian Software <s.werner@sebastian-software.de> */ import path from 'path'; import 'postcss'; import 'core-js/modules/es.error.cause.js'; import fs, { promises } from 'fs'; import { getHashedName } from 'asset-hash'; import mime from 'mime/lite'; import url from 'url'; import minimatch from 'minimatch'; /** * Normalizing result url, before replace decl value */ const normalize = assetUrl => { assetUrl = path.normalize(assetUrl); if (path.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.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.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.parse(assetUrl); const pathname = !isUrlWithoutPathname(assetUrl) ? parsedUrl.pathname : null; const absolutePath = pathname ? path.resolve(path.join(dir.file, pathname)) : getPathDeclFile(decl); return { url: assetUrl, originUrl: assetUrl, pathname, absolutePath: absolutePath || dir.from, relativePath: absolutePath ? path.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.existsSync); if (!filePath) { warn(`Can't read file '${paths.join()}', ignoring`); return; } return { path: filePath, mimeType: mime.getType(filePath) }; }; const getHashName = (file, options) => 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.parse(assetRelativePath); const fileName = path.parse(asset.relativePath).name; pathObj.name = `${fileName}~${pathObj.name}`; delete pathObj.base; // otherwise it would override name assetRelativePath = path.format(pathObj); } const targetDir = getTargetDir(dir); const newAssetBaseDir = getAssetsPath(targetDir, options.assetsPath); const newAssetPath = path.join(newAssetBaseDir, assetRelativePath); const newRelativeAssetPath = normalize(path.relative(targetDir, newAssetPath)); await promises.mkdir(path.dirname(newAssetPath), { recursive: true }); await 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 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.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.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.relative(process.cwd(), asset.absolutePath); if (typeof pattern === "string") { pattern = minimatch.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.dirname(opts.from) : "."; const to = opts.to ? path.dirname(opts.to) : from; return declProcessor(from, to, options, result, decl); } })); // PostCSS v8 marker const postcss = true; export { index as default, postcss }; //# sourceMappingURL=index.esm.js.map