"use strict"; /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */ /** @typedef {import("./index.js").CustomOptions} CustomOptions */ /** @typedef {import("./index.js").RawSourceMap} RawSourceMap */ /** * @template T * @typedef {import("./index.js").MinimizerOptions} MinimizerOptions */ const VLQ_BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /** * Encode a single integer as Base64 VLQ as used by the source-map spec. * @param {number} value integer to encode * @returns {string} encoded VLQ characters */ /* eslint-disable prefer-destructuring, no-eq-null, eqeqeq */ /** * @param {number} value integer to encode * @returns {string} encoded VLQ characters */ function encodeVlq(value) { let vlq = value < 0 ? -value << 1 | 1 : value << 1; let out = ""; do { let digit = vlq & 0b11111; vlq >>>= 5; if (vlq > 0) { digit |= 0b100000; } out += VLQ_BASE64[digit]; } while (vlq > 0); return out; } /** * Encode decoded source-map mappings (per-line arrays of segments) back into * the spec's `mappings` string. * @param {number[][][]} decoded mappings as nested arrays of segments * @returns {string} encoded `mappings` field */ function encodeMappings(decoded) { let result = ""; let prevSourceIdx = 0; let prevOriginalLine = 0; let prevOriginalColumn = 0; let prevNameIdx = 0; for (let line = 0; line < decoded.length; line++) { if (line > 0) { result += ";"; } let prevGeneratedColumn = 0; const segments = decoded[line]; for (let i = 0; i < segments.length; i++) { if (i > 0) { result += ","; } const seg = segments[i]; result += encodeVlq(seg[0] - prevGeneratedColumn); prevGeneratedColumn = seg[0]; if (seg.length >= 4) { result += encodeVlq(seg[1] - prevSourceIdx); prevSourceIdx = seg[1]; result += encodeVlq(seg[2] - prevOriginalLine); prevOriginalLine = seg[2]; result += encodeVlq(seg[3] - prevOriginalColumn); prevOriginalColumn = seg[3]; if (seg.length >= 5) { result += encodeVlq(seg[4] - prevNameIdx); prevNameIdx = seg[4]; } } } } return result; } /** * Compose a freshly-produced source map with the input source map fed to * the minimizer. `currentMap` represents `name → step-output` and * `prevMap` represents `original → name`; the result represents * `original → step-output`. * * TODO: replace with a webpack-sources helper once one is exposed — * `SourceMapSource` already composes one level via `innerSourceMap`, * see https://github.com/webpack/webpack-sources for the proposal to * expose it as a public `composeSourceMaps` (or n-step `SourceMapSource`). * @param {RawSourceMap | undefined} currentMap map produced by the minimizer * @param {RawSourceMap | undefined} prevMap input source map fed to the minimizer * @param {string} name name of the asset that the current map points to * @returns {RawSourceMap | undefined} composed map */ function composeSourceMaps(currentMap, prevMap, name) { if (!currentMap || !prevMap) { return currentMap; } // Custom minimizers may return the map as a JSON string (e.g. terser's // default output). `TraceMap` accepts both shapes, but we still hand // back the original `currentMap` (string preserved) when the previous // map can't be combined. const { TraceMap, decodedMappings, originalPositionFor, sourceContentFor } = require("@jridgewell/trace-mapping"); const current = new TraceMap(/** @type {import("@jridgewell/trace-mapping").SourceMapInput} */ /** @type {unknown} */currentMap); const previous = new TraceMap(/** @type {import("@jridgewell/trace-mapping").SourceMapInput} */ /** @type {unknown} */prevMap); /** @type {string[]} */ const sources = []; /** @type {(string | null)[]} */ const sourcesContent = []; /** @type {string[]} */ const names = []; /** @type {Map} */ const sourceIdx = new Map(); /** @type {Map} */ const nameIdx = new Map(); /** * @param {string | null | undefined} source source identifier * @param {string | undefined} content source content (when available) * @returns {number} index assigned in the composed map */ const getSourceIdx = (source, content) => { const key = source || ""; let idx = sourceIdx.get(key); if (typeof idx === "undefined") { idx = sources.length; sources.push(key); sourcesContent.push(typeof content === "string" ? content : null); sourceIdx.set(key, idx); } else if (typeof content === "string" && sourcesContent[idx] === null) { sourcesContent[idx] = content; } return idx; }; /** * @param {string | null | undefined} value name * @returns {number} index assigned in the composed map */ const getNameIdx = value => { if (typeof value !== "string") { return -1; } let idx = nameIdx.get(value); if (typeof idx === "undefined") { idx = names.length; names.push(value); nameIdx.set(value, idx); } return idx; }; const decoded = decodedMappings(current); const currentSources = current.sources.map( /** * @param {string | null} source source from current map * @returns {string} normalized source string */ source => source || ""); const currentNames = current.names; /** @type {number[][][]} */ const composed = []; for (let line = 0; line < decoded.length; line++) { /** @type {number[][]} */ const newSegments = []; for (const rawSeg of decoded[line]) { const seg = /** @type {number[]} */rawSeg; // Single-element segment is just a generated column with no source info if (seg.length < 4) { newSegments.push([seg[0]]); continue; } const sourceName = currentSources[seg[1]]; const origLine = /** @type {number} */seg[2]; const origCol = /** @type {number} */seg[3]; const segName = seg.length >= 5 ? currentNames[seg[4]] : (/** @type {string | null} */null); // When the segment points back at our intermediate `name`, look up // the original position in the previous map and emit a mapping that // points all the way back. Otherwise keep the segment as-is. if (sourceName === name) { const orig = originalPositionFor(previous, { line: origLine + 1, column: origCol }); if (typeof orig.source !== "string" || orig.line == null || orig.column == null) { continue; } const content = sourceContentFor(previous, orig.source) || undefined; const newSrcIdx = getSourceIdx(orig.source, content); const finalName = typeof orig.name === "string" && orig.name ? orig.name : segName; if (typeof finalName === "string") { newSegments.push([seg[0], newSrcIdx, orig.line - 1, orig.column, getNameIdx(finalName)]); } else { newSegments.push([seg[0], newSrcIdx, orig.line - 1, orig.column]); } } else { const content = sourceContentFor(current, sourceName) || undefined; const newSrcIdx = getSourceIdx(sourceName, content); if (typeof segName === "string") { newSegments.push([seg[0], newSrcIdx, origLine, origCol, getNameIdx(segName)]); } else { newSegments.push([seg[0], newSrcIdx, origLine, origCol]); } } } composed.push(newSegments); } const result = /** @type {RawSourceMap} */ /** @type {unknown} */{ version: 3, sources, names, mappings: encodeMappings(composed) }; if (currentMap.file) { result.file = currentMap.file; } if (sourcesContent.some(value => typeof value === "string")) { result.sourcesContent = /** @type {string[]} */ /** @type {unknown} */sourcesContent; } return result; } /* eslint-enable prefer-destructuring, no-eq-null, eqeqeq */ /** * @template T * @param {import("./index.js").InternalOptions} options options * @returns {Promise} minified result */ async function minify(options) { const { name, input, inputSourceMap, extractComments, module, ecma } = options; const { implementation, options: minimizerOptions } = options.minimizer; const implementations = Array.isArray(implementation) ? implementation : [implementation]; /** @type {string | undefined} */ let lastCode; /** @type {RawSourceMap | undefined} */ let lastMap; /** @type {(Error | string)[]} */ const warnings = []; /** @type {(Error | string)[]} */ const errors = []; /** @type {string[]} */ const extractedComments = []; for (let i = 0; i < implementations.length; i++) { const currentImplementation = /** @type {import("./index.js").BasicMinimizerImplementation & import("./index.js").MinimizeFunctionHelpers} */ implementations[i]; const baseOptions = /** @type {import("./index.js").MinimizerOptions & { module?: boolean, ecma?: number | string }} */ Array.isArray(minimizerOptions) ? minimizerOptions[i] || {} : minimizerOptions || {}; const currentInput = typeof lastCode === "string" ? lastCode : input; const currentMap = typeof lastCode === "string" ? lastMap : inputSourceMap; // Overlay `module` and `ecma` without mutating the caller's options so // a single options object can be reused safely across assets. const currentOptions = /** @type {import("./index.js").MinimizerOptions} */ { ...baseOptions, module: baseOptions.module || module, ecma: baseOptions.ecma || ecma }; const result = await currentImplementation({ [name]: currentInput }, currentMap, currentOptions, extractComments); if (result.warnings && result.warnings.length > 0) { warnings.push(...result.warnings); } if (result.errors && result.errors.length > 0) { errors.push(...result.errors); } if (result.extractedComments && result.extractedComments.length > 0) { extractedComments.push(...result.extractedComments); } if (typeof result.code === "string") { lastCode = result.code; // The minimizer's output map is `name → step-output`. Chain it with // the previous accumulated map so that across an array of minimizers // the final map points back to the original sources. lastMap = composeSourceMaps(result.map, currentMap, name); } } return { code: lastCode, map: lastMap, warnings, errors, extractedComments }; } /** * @param {string} options options * @returns {Promise} minified result */ async function transform(options) { // 'use strict' => this === undefined (Clean Scope) // Safer for possible security issues, albeit not critical at all here const evaluatedOptions = /** * @template T * @type {import("./index.js").InternalOptions} */ // eslint-disable-next-line no-new-func new Function("exports", "require", "module", "__filename", "__dirname", `'use strict'\nreturn ${options}`) // eslint-disable-next-line n/exports-style (exports, require, module, __filename, __dirname); return minify(evaluatedOptions); } module.exports = { minify, transform };