| 1 | "use strict";
|
|---|
| 2 |
|
|---|
| 3 | /** @typedef {import("./index.js").MinimizedResult} MinimizedResult */
|
|---|
| 4 | /** @typedef {import("./index.js").CustomOptions} CustomOptions */
|
|---|
| 5 | /** @typedef {import("./index.js").RawSourceMap} RawSourceMap */
|
|---|
| 6 | /**
|
|---|
| 7 | * @template T
|
|---|
| 8 | * @typedef {import("./index.js").MinimizerOptions<T>} MinimizerOptions
|
|---|
| 9 | */
|
|---|
| 10 |
|
|---|
| 11 | const VLQ_BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|---|
| 12 |
|
|---|
| 13 | /**
|
|---|
| 14 | * Encode a single integer as Base64 VLQ as used by the source-map spec.
|
|---|
| 15 | * @param {number} value integer to encode
|
|---|
| 16 | * @returns {string} encoded VLQ characters
|
|---|
| 17 | */
|
|---|
| 18 | /* eslint-disable prefer-destructuring, no-eq-null, eqeqeq */
|
|---|
| 19 | /**
|
|---|
| 20 | * @param {number} value integer to encode
|
|---|
| 21 | * @returns {string} encoded VLQ characters
|
|---|
| 22 | */
|
|---|
| 23 | function encodeVlq(value) {
|
|---|
| 24 | let vlq = value < 0 ? -value << 1 | 1 : value << 1;
|
|---|
| 25 | let out = "";
|
|---|
| 26 | do {
|
|---|
| 27 | let digit = vlq & 0b11111;
|
|---|
| 28 | vlq >>>= 5;
|
|---|
| 29 | if (vlq > 0) {
|
|---|
| 30 | digit |= 0b100000;
|
|---|
| 31 | }
|
|---|
| 32 | out += VLQ_BASE64[digit];
|
|---|
| 33 | } while (vlq > 0);
|
|---|
| 34 | return out;
|
|---|
| 35 | }
|
|---|
| 36 |
|
|---|
| 37 | /**
|
|---|
| 38 | * Encode decoded source-map mappings (per-line arrays of segments) back into
|
|---|
| 39 | * the spec's `mappings` string.
|
|---|
| 40 | * @param {number[][][]} decoded mappings as nested arrays of segments
|
|---|
| 41 | * @returns {string} encoded `mappings` field
|
|---|
| 42 | */
|
|---|
| 43 | function encodeMappings(decoded) {
|
|---|
| 44 | let result = "";
|
|---|
| 45 | let prevSourceIdx = 0;
|
|---|
| 46 | let prevOriginalLine = 0;
|
|---|
| 47 | let prevOriginalColumn = 0;
|
|---|
| 48 | let prevNameIdx = 0;
|
|---|
| 49 | for (let line = 0; line < decoded.length; line++) {
|
|---|
| 50 | if (line > 0) {
|
|---|
| 51 | result += ";";
|
|---|
| 52 | }
|
|---|
| 53 | let prevGeneratedColumn = 0;
|
|---|
| 54 | const segments = decoded[line];
|
|---|
| 55 | for (let i = 0; i < segments.length; i++) {
|
|---|
| 56 | if (i > 0) {
|
|---|
| 57 | result += ",";
|
|---|
| 58 | }
|
|---|
| 59 | const seg = segments[i];
|
|---|
| 60 | result += encodeVlq(seg[0] - prevGeneratedColumn);
|
|---|
| 61 | prevGeneratedColumn = seg[0];
|
|---|
| 62 | if (seg.length >= 4) {
|
|---|
| 63 | result += encodeVlq(seg[1] - prevSourceIdx);
|
|---|
| 64 | prevSourceIdx = seg[1];
|
|---|
| 65 | result += encodeVlq(seg[2] - prevOriginalLine);
|
|---|
| 66 | prevOriginalLine = seg[2];
|
|---|
| 67 | result += encodeVlq(seg[3] - prevOriginalColumn);
|
|---|
| 68 | prevOriginalColumn = seg[3];
|
|---|
| 69 | if (seg.length >= 5) {
|
|---|
| 70 | result += encodeVlq(seg[4] - prevNameIdx);
|
|---|
| 71 | prevNameIdx = seg[4];
|
|---|
| 72 | }
|
|---|
| 73 | }
|
|---|
| 74 | }
|
|---|
| 75 | }
|
|---|
| 76 | return result;
|
|---|
| 77 | }
|
|---|
| 78 |
|
|---|
| 79 | /**
|
|---|
| 80 | * Compose a freshly-produced source map with the input source map fed to
|
|---|
| 81 | * the minimizer. `currentMap` represents `name → step-output` and
|
|---|
| 82 | * `prevMap` represents `original → name`; the result represents
|
|---|
| 83 | * `original → step-output`.
|
|---|
| 84 | *
|
|---|
| 85 | * TODO: replace with a webpack-sources helper once one is exposed —
|
|---|
| 86 | * `SourceMapSource` already composes one level via `innerSourceMap`,
|
|---|
| 87 | * see https://github.com/webpack/webpack-sources for the proposal to
|
|---|
| 88 | * expose it as a public `composeSourceMaps` (or n-step `SourceMapSource`).
|
|---|
| 89 | * @param {RawSourceMap | undefined} currentMap map produced by the minimizer
|
|---|
| 90 | * @param {RawSourceMap | undefined} prevMap input source map fed to the minimizer
|
|---|
| 91 | * @param {string} name name of the asset that the current map points to
|
|---|
| 92 | * @returns {RawSourceMap | undefined} composed map
|
|---|
| 93 | */
|
|---|
| 94 | function composeSourceMaps(currentMap, prevMap, name) {
|
|---|
| 95 | if (!currentMap || !prevMap) {
|
|---|
| 96 | return currentMap;
|
|---|
| 97 | }
|
|---|
| 98 |
|
|---|
| 99 | // Custom minimizers may return the map as a JSON string (e.g. terser's
|
|---|
| 100 | // default output). `TraceMap` accepts both shapes, but we still hand
|
|---|
| 101 | // back the original `currentMap` (string preserved) when the previous
|
|---|
| 102 | // map can't be combined.
|
|---|
| 103 | const {
|
|---|
| 104 | TraceMap,
|
|---|
| 105 | decodedMappings,
|
|---|
| 106 | originalPositionFor,
|
|---|
| 107 | sourceContentFor
|
|---|
| 108 | } = require("@jridgewell/trace-mapping");
|
|---|
| 109 | const current = new TraceMap(/** @type {import("@jridgewell/trace-mapping").SourceMapInput} */
|
|---|
| 110 | /** @type {unknown} */currentMap);
|
|---|
| 111 | const previous = new TraceMap(/** @type {import("@jridgewell/trace-mapping").SourceMapInput} */
|
|---|
| 112 | /** @type {unknown} */prevMap);
|
|---|
| 113 |
|
|---|
| 114 | /** @type {string[]} */
|
|---|
| 115 | const sources = [];
|
|---|
| 116 | /** @type {(string | null)[]} */
|
|---|
| 117 | const sourcesContent = [];
|
|---|
| 118 | /** @type {string[]} */
|
|---|
| 119 | const names = [];
|
|---|
| 120 | /** @type {Map<string, number>} */
|
|---|
| 121 | const sourceIdx = new Map();
|
|---|
| 122 | /** @type {Map<string, number>} */
|
|---|
| 123 | const nameIdx = new Map();
|
|---|
| 124 |
|
|---|
| 125 | /**
|
|---|
| 126 | * @param {string | null | undefined} source source identifier
|
|---|
| 127 | * @param {string | undefined} content source content (when available)
|
|---|
| 128 | * @returns {number} index assigned in the composed map
|
|---|
| 129 | */
|
|---|
| 130 | const getSourceIdx = (source, content) => {
|
|---|
| 131 | const key = source || "";
|
|---|
| 132 | let idx = sourceIdx.get(key);
|
|---|
| 133 | if (typeof idx === "undefined") {
|
|---|
| 134 | idx = sources.length;
|
|---|
| 135 | sources.push(key);
|
|---|
| 136 | sourcesContent.push(typeof content === "string" ? content : null);
|
|---|
| 137 | sourceIdx.set(key, idx);
|
|---|
| 138 | } else if (typeof content === "string" && sourcesContent[idx] === null) {
|
|---|
| 139 | sourcesContent[idx] = content;
|
|---|
| 140 | }
|
|---|
| 141 | return idx;
|
|---|
| 142 | };
|
|---|
| 143 |
|
|---|
| 144 | /**
|
|---|
| 145 | * @param {string | null | undefined} value name
|
|---|
| 146 | * @returns {number} index assigned in the composed map
|
|---|
| 147 | */
|
|---|
| 148 | const getNameIdx = value => {
|
|---|
| 149 | if (typeof value !== "string") {
|
|---|
| 150 | return -1;
|
|---|
| 151 | }
|
|---|
| 152 | let idx = nameIdx.get(value);
|
|---|
| 153 | if (typeof idx === "undefined") {
|
|---|
| 154 | idx = names.length;
|
|---|
| 155 | names.push(value);
|
|---|
| 156 | nameIdx.set(value, idx);
|
|---|
| 157 | }
|
|---|
| 158 | return idx;
|
|---|
| 159 | };
|
|---|
| 160 | const decoded = decodedMappings(current);
|
|---|
| 161 | const currentSources = current.sources.map(
|
|---|
| 162 | /**
|
|---|
| 163 | * @param {string | null} source source from current map
|
|---|
| 164 | * @returns {string} normalized source string
|
|---|
| 165 | */
|
|---|
| 166 | source => source || "");
|
|---|
| 167 | const currentNames = current.names;
|
|---|
| 168 |
|
|---|
| 169 | /** @type {number[][][]} */
|
|---|
| 170 | const composed = [];
|
|---|
| 171 | for (let line = 0; line < decoded.length; line++) {
|
|---|
| 172 | /** @type {number[][]} */
|
|---|
| 173 | const newSegments = [];
|
|---|
| 174 | for (const rawSeg of decoded[line]) {
|
|---|
| 175 | const seg = /** @type {number[]} */rawSeg;
|
|---|
| 176 |
|
|---|
| 177 | // Single-element segment is just a generated column with no source info
|
|---|
| 178 | if (seg.length < 4) {
|
|---|
| 179 | newSegments.push([seg[0]]);
|
|---|
| 180 | continue;
|
|---|
| 181 | }
|
|---|
| 182 | const sourceName = currentSources[seg[1]];
|
|---|
| 183 | const origLine = /** @type {number} */seg[2];
|
|---|
| 184 | const origCol = /** @type {number} */seg[3];
|
|---|
| 185 | const segName = seg.length >= 5 ? currentNames[seg[4]] : (/** @type {string | null} */null);
|
|---|
| 186 |
|
|---|
| 187 | // When the segment points back at our intermediate `name`, look up
|
|---|
| 188 | // the original position in the previous map and emit a mapping that
|
|---|
| 189 | // points all the way back. Otherwise keep the segment as-is.
|
|---|
| 190 | if (sourceName === name) {
|
|---|
| 191 | const orig = originalPositionFor(previous, {
|
|---|
| 192 | line: origLine + 1,
|
|---|
| 193 | column: origCol
|
|---|
| 194 | });
|
|---|
| 195 | if (typeof orig.source !== "string" || orig.line == null || orig.column == null) {
|
|---|
| 196 | continue;
|
|---|
| 197 | }
|
|---|
| 198 | const content = sourceContentFor(previous, orig.source) || undefined;
|
|---|
| 199 | const newSrcIdx = getSourceIdx(orig.source, content);
|
|---|
| 200 | const finalName = typeof orig.name === "string" && orig.name ? orig.name : segName;
|
|---|
| 201 | if (typeof finalName === "string") {
|
|---|
| 202 | newSegments.push([seg[0], newSrcIdx, orig.line - 1, orig.column, getNameIdx(finalName)]);
|
|---|
| 203 | } else {
|
|---|
| 204 | newSegments.push([seg[0], newSrcIdx, orig.line - 1, orig.column]);
|
|---|
| 205 | }
|
|---|
| 206 | } else {
|
|---|
| 207 | const content = sourceContentFor(current, sourceName) || undefined;
|
|---|
| 208 | const newSrcIdx = getSourceIdx(sourceName, content);
|
|---|
| 209 | if (typeof segName === "string") {
|
|---|
| 210 | newSegments.push([seg[0], newSrcIdx, origLine, origCol, getNameIdx(segName)]);
|
|---|
| 211 | } else {
|
|---|
| 212 | newSegments.push([seg[0], newSrcIdx, origLine, origCol]);
|
|---|
| 213 | }
|
|---|
| 214 | }
|
|---|
| 215 | }
|
|---|
| 216 | composed.push(newSegments);
|
|---|
| 217 | }
|
|---|
| 218 | const result = /** @type {RawSourceMap} */
|
|---|
| 219 |
|
|---|
| 220 | /** @type {unknown} */{
|
|---|
| 221 | version: 3,
|
|---|
| 222 | sources,
|
|---|
| 223 | names,
|
|---|
| 224 | mappings: encodeMappings(composed)
|
|---|
| 225 | };
|
|---|
| 226 | if (currentMap.file) {
|
|---|
| 227 | result.file = currentMap.file;
|
|---|
| 228 | }
|
|---|
| 229 | if (sourcesContent.some(value => typeof value === "string")) {
|
|---|
| 230 | result.sourcesContent = /** @type {string[]} */
|
|---|
| 231 | /** @type {unknown} */sourcesContent;
|
|---|
| 232 | }
|
|---|
| 233 | return result;
|
|---|
| 234 | }
|
|---|
| 235 | /* eslint-enable prefer-destructuring, no-eq-null, eqeqeq */
|
|---|
| 236 |
|
|---|
| 237 | /**
|
|---|
| 238 | * @template T
|
|---|
| 239 | * @param {import("./index.js").InternalOptions<T>} options options
|
|---|
| 240 | * @returns {Promise<MinimizedResult>} minified result
|
|---|
| 241 | */
|
|---|
| 242 | async function minify(options) {
|
|---|
| 243 | const {
|
|---|
| 244 | name,
|
|---|
| 245 | input,
|
|---|
| 246 | inputSourceMap,
|
|---|
| 247 | extractComments,
|
|---|
| 248 | module,
|
|---|
| 249 | ecma
|
|---|
| 250 | } = options;
|
|---|
| 251 | const {
|
|---|
| 252 | implementation,
|
|---|
| 253 | options: minimizerOptions
|
|---|
| 254 | } = options.minimizer;
|
|---|
| 255 | const implementations = Array.isArray(implementation) ? implementation : [implementation];
|
|---|
| 256 |
|
|---|
| 257 | /** @type {string | undefined} */
|
|---|
| 258 | let lastCode;
|
|---|
| 259 | /** @type {RawSourceMap | undefined} */
|
|---|
| 260 | let lastMap;
|
|---|
| 261 | /** @type {(Error | string)[]} */
|
|---|
| 262 | const warnings = [];
|
|---|
| 263 | /** @type {(Error | string)[]} */
|
|---|
| 264 | const errors = [];
|
|---|
| 265 | /** @type {string[]} */
|
|---|
| 266 | const extractedComments = [];
|
|---|
| 267 | for (let i = 0; i < implementations.length; i++) {
|
|---|
| 268 | const currentImplementation = /** @type {import("./index.js").BasicMinimizerImplementation<T> & import("./index.js").MinimizeFunctionHelpers} */
|
|---|
| 269 | implementations[i];
|
|---|
| 270 | const baseOptions = /** @type {import("./index.js").MinimizerOptions<T> & { module?: boolean, ecma?: number | string }} */
|
|---|
| 271 |
|
|---|
| 272 | Array.isArray(minimizerOptions) ? minimizerOptions[i] || {} : minimizerOptions || {};
|
|---|
| 273 | const currentInput = typeof lastCode === "string" ? lastCode : input;
|
|---|
| 274 | const currentMap = typeof lastCode === "string" ? lastMap : inputSourceMap;
|
|---|
| 275 |
|
|---|
| 276 | // Overlay `module` and `ecma` without mutating the caller's options so
|
|---|
| 277 | // a single options object can be reused safely across assets.
|
|---|
| 278 | const currentOptions = /** @type {import("./index.js").MinimizerOptions<T>} */
|
|---|
| 279 | {
|
|---|
| 280 | ...baseOptions,
|
|---|
| 281 | module: baseOptions.module || module,
|
|---|
| 282 | ecma: baseOptions.ecma || ecma
|
|---|
| 283 | };
|
|---|
| 284 | const result = await currentImplementation({
|
|---|
| 285 | [name]: currentInput
|
|---|
| 286 | }, currentMap, currentOptions, extractComments);
|
|---|
| 287 | if (result.warnings && result.warnings.length > 0) {
|
|---|
| 288 | warnings.push(...result.warnings);
|
|---|
| 289 | }
|
|---|
| 290 | if (result.errors && result.errors.length > 0) {
|
|---|
| 291 | errors.push(...result.errors);
|
|---|
| 292 | }
|
|---|
| 293 | if (result.extractedComments && result.extractedComments.length > 0) {
|
|---|
| 294 | extractedComments.push(...result.extractedComments);
|
|---|
| 295 | }
|
|---|
| 296 | if (typeof result.code === "string") {
|
|---|
| 297 | lastCode = result.code;
|
|---|
| 298 | // The minimizer's output map is `name → step-output`. Chain it with
|
|---|
| 299 | // the previous accumulated map so that across an array of minimizers
|
|---|
| 300 | // the final map points back to the original sources.
|
|---|
| 301 | lastMap = composeSourceMaps(result.map, currentMap, name);
|
|---|
| 302 | }
|
|---|
| 303 | }
|
|---|
| 304 | return {
|
|---|
| 305 | code: lastCode,
|
|---|
| 306 | map: lastMap,
|
|---|
| 307 | warnings,
|
|---|
| 308 | errors,
|
|---|
| 309 | extractedComments
|
|---|
| 310 | };
|
|---|
| 311 | }
|
|---|
| 312 |
|
|---|
| 313 | /**
|
|---|
| 314 | * @param {string} options options
|
|---|
| 315 | * @returns {Promise<MinimizedResult>} minified result
|
|---|
| 316 | */
|
|---|
| 317 | async function transform(options) {
|
|---|
| 318 | // 'use strict' => this === undefined (Clean Scope)
|
|---|
| 319 | // Safer for possible security issues, albeit not critical at all here
|
|---|
| 320 |
|
|---|
| 321 | const evaluatedOptions =
|
|---|
| 322 | /**
|
|---|
| 323 | * @template T
|
|---|
| 324 | * @type {import("./index.js").InternalOptions<T>}
|
|---|
| 325 | */
|
|---|
| 326 |
|
|---|
| 327 | // eslint-disable-next-line no-new-func
|
|---|
| 328 | new Function("exports", "require", "module", "__filename", "__dirname", `'use strict'\nreturn ${options}`) // eslint-disable-next-line n/exports-style
|
|---|
| 329 | (exports, require, module, __filename, __dirname);
|
|---|
| 330 | return minify(evaluatedOptions);
|
|---|
| 331 | }
|
|---|
| 332 | module.exports = {
|
|---|
| 333 | minify,
|
|---|
| 334 | transform
|
|---|
| 335 | }; |
|---|