minify.js 11 KB

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