mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-27 01:22:49 +00:00
335 lines
11 KiB
JavaScript
335 lines
11 KiB
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
|
|
}; |