terser-webpack-plugin
Version:
Minimizer plugin for webpack
335 lines (315 loc) • 11.3 kB
JavaScript
"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<T>} 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<string, number>} */
const sourceIdx = new Map();
/** @type {Map<string, number>} */
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<T>} options options
* @returns {Promise<MinimizedResult>} 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<T> & import("./index.js").MinimizeFunctionHelpers} */
implementations[i];
const baseOptions = /** @type {import("./index.js").MinimizerOptions<T> & { 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<T>} */
{
...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<MinimizedResult>} 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<T>}
*/
// 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
};