minify.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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. /** @typedef {import("./index.js").EXPECTED_ANY} EXPECTED_ANY */
  6. /**
  7. * @template T
  8. * @typedef {import("./index.js").MinimizerOptions<T>} MinimizerOptions
  9. */
  10. const VLQ_BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  11. /**
  12. * Encode a single integer as Base64 VLQ as used by the source-map spec.
  13. * @param {number} value integer to encode
  14. * @returns {string} encoded VLQ characters
  15. */
  16. /* eslint-disable prefer-destructuring, no-eq-null, eqeqeq */
  17. /**
  18. * @param {number} value integer to encode
  19. * @returns {string} encoded VLQ characters
  20. */
  21. function encodeVlq(value) {
  22. let vlq = value < 0 ? -value << 1 | 1 : value << 1;
  23. let out = "";
  24. do {
  25. let digit = vlq & 0b11111;
  26. vlq >>>= 5;
  27. if (vlq > 0) {
  28. digit |= 0b100000;
  29. }
  30. out += VLQ_BASE64[digit];
  31. } while (vlq > 0);
  32. return out;
  33. }
  34. /**
  35. * Encode decoded source-map mappings (per-line arrays of segments) back into
  36. * the spec's `mappings` string.
  37. * @param {number[][][]} decoded mappings as nested arrays of segments
  38. * @returns {string} encoded `mappings` field
  39. */
  40. function encodeMappings(decoded) {
  41. let result = "";
  42. let prevSourceIdx = 0;
  43. let prevOriginalLine = 0;
  44. let prevOriginalColumn = 0;
  45. let prevNameIdx = 0;
  46. for (let line = 0; line < decoded.length; line++) {
  47. if (line > 0) {
  48. result += ";";
  49. }
  50. let prevGeneratedColumn = 0;
  51. const segments = decoded[line];
  52. for (let i = 0; i < segments.length; i++) {
  53. if (i > 0) {
  54. result += ",";
  55. }
  56. const seg = segments[i];
  57. result += encodeVlq(seg[0] - prevGeneratedColumn);
  58. prevGeneratedColumn = seg[0];
  59. if (seg.length >= 4) {
  60. result += encodeVlq(seg[1] - prevSourceIdx);
  61. prevSourceIdx = seg[1];
  62. result += encodeVlq(seg[2] - prevOriginalLine);
  63. prevOriginalLine = seg[2];
  64. result += encodeVlq(seg[3] - prevOriginalColumn);
  65. prevOriginalColumn = seg[3];
  66. if (seg.length >= 5) {
  67. result += encodeVlq(seg[4] - prevNameIdx);
  68. prevNameIdx = seg[4];
  69. }
  70. }
  71. }
  72. }
  73. return result;
  74. }
  75. /**
  76. * Compose a freshly-produced source map with the input source map fed to
  77. * the minimizer. `currentMap` represents `name → step-output` and
  78. * `prevMap` represents `original → name`; the result represents
  79. * `original → step-output`.
  80. *
  81. * TODO: replace with a webpack-sources helper once one is exposed —
  82. * `SourceMapSource` already composes one level via `innerSourceMap`,
  83. * see https://github.com/webpack/webpack-sources for the proposal to
  84. * expose it as a public `composeSourceMaps` (or n-step `SourceMapSource`).
  85. * @param {RawSourceMap | undefined} currentMap map produced by the minimizer
  86. * @param {RawSourceMap | undefined} prevMap input source map fed to the minimizer
  87. * @param {string} name name of the asset that the current map points to
  88. * @returns {RawSourceMap | undefined} composed map
  89. */
  90. function composeSourceMaps(currentMap, prevMap, name) {
  91. if (!currentMap || !prevMap) {
  92. return currentMap;
  93. }
  94. // Custom minimizers may return the map as a JSON string (e.g. terser's
  95. // default output). `TraceMap` accepts both shapes, but we still hand
  96. // back the original `currentMap` (string preserved) when the previous
  97. // map can't be combined.
  98. const {
  99. TraceMap,
  100. decodedMappings,
  101. originalPositionFor,
  102. sourceContentFor
  103. } = require("@jridgewell/trace-mapping");
  104. const current = new TraceMap(/** @type {import("@jridgewell/trace-mapping").SourceMapInput} */
  105. /** @type {unknown} */currentMap);
  106. const previous = new TraceMap(/** @type {import("@jridgewell/trace-mapping").SourceMapInput} */
  107. /** @type {unknown} */prevMap);
  108. /** @type {string[]} */
  109. const sources = [];
  110. /** @type {(string | null)[]} */
  111. const sourcesContent = [];
  112. /** @type {string[]} */
  113. const names = [];
  114. /** @type {Map<string, number>} */
  115. const sourceIdx = new Map();
  116. /** @type {Map<string, number>} */
  117. const nameIdx = new Map();
  118. /**
  119. * @param {string | null | undefined} source source identifier
  120. * @param {string | undefined} content source content (when available)
  121. * @returns {number} index assigned in the composed map
  122. */
  123. const getSourceIdx = (source, content) => {
  124. const key = source || "";
  125. let idx = sourceIdx.get(key);
  126. if (typeof idx === "undefined") {
  127. idx = sources.length;
  128. sources.push(key);
  129. sourcesContent.push(typeof content === "string" ? content : null);
  130. sourceIdx.set(key, idx);
  131. } else if (typeof content === "string" && sourcesContent[idx] === null) {
  132. sourcesContent[idx] = content;
  133. }
  134. return idx;
  135. };
  136. /**
  137. * @param {string | null | undefined} value name
  138. * @returns {number} index assigned in the composed map
  139. */
  140. const getNameIdx = value => {
  141. if (typeof value !== "string") {
  142. return -1;
  143. }
  144. let idx = nameIdx.get(value);
  145. if (typeof idx === "undefined") {
  146. idx = names.length;
  147. names.push(value);
  148. nameIdx.set(value, idx);
  149. }
  150. return idx;
  151. };
  152. const decoded = decodedMappings(current);
  153. const currentSources = current.sources.map(
  154. /**
  155. * @param {string | null} source source from current map
  156. * @returns {string} normalized source string
  157. */
  158. source => source || "");
  159. const currentNames = current.names;
  160. /** @type {number[][][]} */
  161. const composed = [];
  162. for (let line = 0; line < decoded.length; line++) {
  163. /** @type {number[][]} */
  164. const newSegments = [];
  165. for (const rawSeg of decoded[line]) {
  166. const seg = /** @type {number[]} */rawSeg;
  167. // Single-element segment is just a generated column with no source info
  168. if (seg.length < 4) {
  169. newSegments.push([seg[0]]);
  170. continue;
  171. }
  172. const sourceName = currentSources[seg[1]];
  173. const origLine = /** @type {number} */seg[2];
  174. const origCol = /** @type {number} */seg[3];
  175. const segName = seg.length >= 5 ? currentNames[seg[4]] : (/** @type {string | null} */null);
  176. // When the segment points back at our intermediate `name`, look up
  177. // the original position in the previous map and emit a mapping that
  178. // points all the way back. Otherwise keep the segment as-is.
  179. if (sourceName === name) {
  180. const orig = originalPositionFor(previous, {
  181. line: origLine + 1,
  182. column: origCol
  183. });
  184. if (typeof orig.source !== "string" || orig.line == null || orig.column == null) {
  185. continue;
  186. }
  187. const content = sourceContentFor(previous, orig.source) || undefined;
  188. const newSrcIdx = getSourceIdx(orig.source, content);
  189. const finalName = typeof orig.name === "string" && orig.name ? orig.name : segName;
  190. if (typeof finalName === "string") {
  191. newSegments.push([seg[0], newSrcIdx, orig.line - 1, orig.column, getNameIdx(finalName)]);
  192. } else {
  193. newSegments.push([seg[0], newSrcIdx, orig.line - 1, orig.column]);
  194. }
  195. } else {
  196. const content = sourceContentFor(current, sourceName) || undefined;
  197. const newSrcIdx = getSourceIdx(sourceName, content);
  198. if (typeof segName === "string") {
  199. newSegments.push([seg[0], newSrcIdx, origLine, origCol, getNameIdx(segName)]);
  200. } else {
  201. newSegments.push([seg[0], newSrcIdx, origLine, origCol]);
  202. }
  203. }
  204. }
  205. composed.push(newSegments);
  206. }
  207. const result = /** @type {RawSourceMap} */
  208. /** @type {unknown} */{
  209. version: 3,
  210. sources,
  211. names,
  212. mappings: encodeMappings(composed)
  213. };
  214. if (currentMap.file) {
  215. result.file = currentMap.file;
  216. }
  217. if (sourcesContent.some(value => typeof value === "string")) {
  218. result.sourcesContent = /** @type {string[]} */
  219. /** @type {unknown} */sourcesContent;
  220. }
  221. return result;
  222. }
  223. /* eslint-enable prefer-destructuring, no-eq-null, eqeqeq */
  224. /**
  225. * @template T
  226. * @param {import("./index.js").InternalOptions<T>} options options
  227. * @returns {Promise<MinimizedResult>} minified result
  228. */
  229. async function minify(options) {
  230. const {
  231. name,
  232. input,
  233. inputSourceMap,
  234. extractComments,
  235. module,
  236. ecma
  237. } = options;
  238. const {
  239. implementation,
  240. options: minimizerOptions
  241. } = options.minimizer;
  242. const implementations = Array.isArray(implementation) ? implementation : [implementation];
  243. /** @type {string | undefined} */
  244. let lastCode;
  245. /** @type {RawSourceMap | undefined} */
  246. let lastMap;
  247. /** @type {(Error | string)[]} */
  248. const warnings = [];
  249. /** @type {(Error | string)[]} */
  250. const errors = [];
  251. /** @type {string[]} */
  252. const extractedComments = [];
  253. /**
  254. * The options entry belonging to one minimizer: an array is parallel to the
  255. * implementations, a single object is shared by all of them.
  256. * @param {number} index index into the implementations
  257. * @returns {EXPECTED_ANY} its options
  258. */
  259. const optionsAt = index => Array.isArray(minimizerOptions) ? minimizerOptions[index] || {} : minimizerOptions || {};
  260. // Source one language embeds in another carries no filename, so it is
  261. // dispatched across every configured minimizer rather than the ones this
  262. // asset's name matched — and by what each declared, which travels as data
  263. // because a minify function reaches a worker as source.
  264. const {
  265. embedded
  266. } = options;
  267. const embeddedImplementations = embedded ? (/** @type {EXPECTED_ANY[]} */
  268. /** @type {unknown} */embedded.implementation) : [];
  269. /**
  270. * @param {number} index index into the embedded implementations
  271. * @returns {EXPECTED_ANY} its options
  272. */
  273. const embeddedOptionsAt = index => /** @type {EXPECTED_ANY[]} */(/** @type {unknown} */(embedded || {
  274. options: []
  275. }).options)[index] || {};
  276. /**
  277. * The minimizers declaring `type` among the languages they minify. Source one
  278. * language embeds in another carries no filename, so `test` / `filter` cannot
  279. * dispatch it and `getTypes` answers instead.
  280. * @param {string} type the language
  281. * @returns {number[]} indices into the implementations
  282. */
  283. const claiming = type => {
  284. const matched = [];
  285. const claims = embedded ? embedded.claims : [];
  286. for (let i = 0; i < claims.length; i++) {
  287. if (claims[i].includes(type)) matched.push(i);
  288. }
  289. return matched;
  290. };
  291. /**
  292. * Minify one body a minimizer hands out from inside what it minifies, and
  293. * hand it back for the same print. Recursion is this function calling
  294. * `minify` again, so a body that nests something of its own is reached too.
  295. * Declining leaves the body exactly as the minimizer would have written it.
  296. *
  297. * What went wrong inside is answered with the body rather than pushed onto
  298. * this run: the minimizer collects it and reports it against the asset that
  299. * embeds it, which is the only one there is.
  300. * @param {string} source the nested body
  301. * @param {{ type: string, as?: string }} info what it is, and which production of it
  302. * @returns {Promise<{ code?: string, warnings?: (Error | string)[], errors?: (Error | string)[] } | undefined>} what came of it, or undefined
  303. */
  304. const renderEmbeddedSource = async (source, {
  305. type,
  306. as
  307. }) => {
  308. const matched = claiming(type);
  309. // `claiming` answers off `embedded`, so a match means there is one.
  310. if (matched.length === 0 || embedded === undefined) return undefined;
  311. const nested = await minify({
  312. /** @type {EXPECTED_ANY} */
  313. name,
  314. input: source,
  315. inputSourceMap: undefined,
  316. extractComments: false,
  317. // What the target can read is the target's, not the asset's, so it carries
  318. // into the body too: an inline `<script>` is minified for the same engines
  319. // the document is. `module` does not — an inline script is a classic script
  320. // whatever the file embedding it is.
  321. ecma,
  322. // The nested minimizers are these indices, so `at` moves with them.
  323. embedded: {
  324. ...embedded,
  325. at: matched
  326. },
  327. minimizer: {
  328. implementation: (/** @type {EXPECTED_ANY} */
  329. matched.map(i => embeddedImplementations[i])),
  330. // `as` says which production of `type` this body is — an HTML `style=""`
  331. // is CSS, but a block's contents rather than a stylesheet. It is the
  332. // body's, not the configuration's, so it overrides.
  333. options: (/** @type {EXPECTED_ANY} */
  334. matched.map(i => as === undefined ? embeddedOptionsAt(i) : {
  335. ...embeddedOptionsAt(i),
  336. as
  337. }))
  338. }
  339. });
  340. /** @type {{ code?: string, warnings?: (Error | string)[], errors?: (Error | string)[] }} */
  341. const answer = {};
  342. if (nested.warnings && nested.warnings.length > 0) {
  343. answer.warnings = nested.warnings;
  344. }
  345. if (nested.errors && nested.errors.length > 0) {
  346. answer.errors = nested.errors;
  347. }
  348. // A body that failed keeps the text it was written with, which is what
  349. // answering without a `code` spells.
  350. if ((!nested.errors || nested.errors.length === 0) && typeof nested.code === "string") {
  351. answer.code = nested.code;
  352. }
  353. return answer;
  354. };
  355. /**
  356. * Whether `index`'s minimizer could hand out a language something configured
  357. * here claims. False means offering it would only ever reach bodies with
  358. * nowhere to go, so the option is left off and it prints as it always has.
  359. * @param {number} index index into the implementations
  360. * @returns {boolean} true when some nested language is reachable
  361. */
  362. const reachesEmbedded = index => embedded !== undefined && (embedded.offers[embedded.at[index]] || []).some(type => claiming(type).length > 0);
  363. for (let i = 0; i < implementations.length; i++) {
  364. const currentImplementation = /** @type {import("./index.js").BasicMinimizerImplementation<T> & import("./index.js").MinimizeFunctionHelpers} */
  365. implementations[i];
  366. const baseOptions = /** @type {import("./index.js").MinimizerOptions<T> & { module?: boolean, ecma?: number | string }} */
  367. optionsAt(i);
  368. const currentInput = typeof lastCode === "string" ? lastCode : input;
  369. const currentMap = typeof lastCode === "string" ? lastMap : inputSourceMap;
  370. // Overlay `module` and `ecma` without mutating the caller's options so
  371. // a single options object can be reused safely across assets.
  372. const currentOptions = /** @type {import("./index.js").MinimizerOptions<T>} */
  373. {
  374. ...baseOptions,
  375. module: baseOptions.module || module,
  376. ecma: baseOptions.ecma || ecma,
  377. // Only for a minimizer that says it reads the option: every other one is
  378. // handed its own options untouched, so nothing sees a key it does not know.
  379. ...(reachesEmbedded(i) ? {
  380. renderEmbeddedSource
  381. } : {})
  382. };
  383. const result = await currentImplementation({
  384. [name]: currentInput
  385. }, currentMap, currentOptions, extractComments);
  386. if (result.warnings && result.warnings.length > 0) {
  387. warnings.push(...result.warnings);
  388. }
  389. if (result.errors && result.errors.length > 0) {
  390. errors.push(...result.errors);
  391. }
  392. if (result.extractedComments && result.extractedComments.length > 0) {
  393. extractedComments.push(...result.extractedComments);
  394. }
  395. if (typeof result.code === "string") {
  396. lastCode = result.code;
  397. // The minimizer's output map is `name → step-output`. Chain it with
  398. // the previous accumulated map so that across an array of minimizers
  399. // the final map points back to the original sources.
  400. lastMap = composeSourceMaps(result.map, currentMap, name);
  401. }
  402. }
  403. return {
  404. code: lastCode,
  405. map: lastMap,
  406. warnings,
  407. errors,
  408. extractedComments
  409. };
  410. }
  411. /**
  412. * @param {string} options options
  413. * @returns {Promise<MinimizedResult>} minified result
  414. */
  415. async function transform(options) {
  416. // 'use strict' => this === undefined (Clean Scope)
  417. // Safer for possible security issues, albeit not critical at all here
  418. const evaluatedOptions =
  419. /**
  420. * @template T
  421. * @type {import("./index.js").InternalOptions<T>}
  422. */
  423. // eslint-disable-next-line no-new-func
  424. new Function("exports", "require", "module", "__filename", "__dirname", `'use strict'\nreturn ${options}`
  425. // eslint-disable-next-line n/exports-style
  426. )(exports, require, module, __filename, __dirname);
  427. return minify(evaluatedOptions);
  428. }
  429. module.exports = {
  430. minify,
  431. transform
  432. };