UNPKG

postcss-smart-asset

Version:
487 lines (380 loc) 13.9 kB
/*! postcss-smart-asset v3.1.0 by Sebastian Software <s.werner@sebastian-software.de> */ 'use strict'; 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