Template.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { ConcatSource, PrefixSource } = require("webpack-sources");
  7. const { WEBPACK_MODULE_TYPE_RUNTIME } = require("./ModuleTypeConstants");
  8. const RuntimeGlobals = require("./RuntimeGlobals");
  9. /** @import { Source } from "webpack-sources" */
  10. /**
  11. * @import {
  12. * OutputNormalizedWithDefaults as OutputOptions
  13. * } from "./config/defaults"
  14. */
  15. /** @import Chunk from "./Chunk" */
  16. /** @import ChunkGraph, { ModuleId } from "./ChunkGraph" */
  17. /** @import CodeGenerationResults from "./CodeGenerationResults" */
  18. /** @import { AssetInfo, PathData } from "./Compilation" */
  19. /** @import DependencyTemplates from "./DependencyTemplates" */
  20. /** @import Module from "./Module" */
  21. /** @import ModuleGraph from "./ModuleGraph" */
  22. /** @import ModuleTemplate from "./ModuleTemplate" */
  23. /** @import RuntimeModule from "./RuntimeModule" */
  24. /** @import RuntimeTemplate from "./RuntimeTemplate" */
  25. /**
  26. * @import {
  27. * ChunkRenderContext,
  28. * RenderContext
  29. * } from "./javascript/JavascriptModulesPlugin"
  30. */
  31. const START_LOWERCASE_ALPHABET_CODE = "a".charCodeAt(0);
  32. const START_UPPERCASE_ALPHABET_CODE = "A".charCodeAt(0);
  33. const DELTA_A_TO_Z = "z".charCodeAt(0) - START_LOWERCASE_ALPHABET_CODE + 1;
  34. const NUMBER_OF_IDENTIFIER_START_CHARS = DELTA_A_TO_Z * 2 + 2; // a-z A-Z _ $
  35. const NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS =
  36. NUMBER_OF_IDENTIFIER_START_CHARS + 10; // a-z A-Z _ $ 0-9
  37. const FUNCTION_CONTENT_REGEX = /^function\s?\(\)\s?\{\r?\n?|\r?\n?\}$/g;
  38. // JSDoc type annotations exist only to type the runtime template; strip them so
  39. // they are never emitted into the bundle. Whole-line blocks drop the line too.
  40. const JSDOC_LINE_REGEX = /^[ \t]*\/\*\*(?:[^*]|\*(?!\/))*\*\/[ \t]*\r?\n/gm;
  41. const JSDOC_INLINE_REGEX = /\/\*\*(?:[^*]|\*(?!\/))*\*\/[ \t]*/g;
  42. const INDENT_MULTILINE_REGEX = /^\t/gm;
  43. // Start of every non-blank line after the first. A lookahead keeps the next
  44. // character out of the match, so the replacement neither captures nor re-emits it.
  45. const LINE_START_REGEX = /\n(?=[^\n])/g;
  46. const LINE_SEPARATOR_REGEX = /\r?\n/g;
  47. const IDENTIFIER_NAME_REPLACE_REGEX = /^([^a-z$_])/i;
  48. const IDENTIFIER_ALPHA_NUMERIC_NAME_REPLACE_REGEX = /[^a-z0-9$]+/gi;
  49. const COMMENT_END_REGEX = /\*\//g;
  50. const PATH_NAME_NORMALIZE_REPLACE_REGEX = /[^a-z0-9_!§$()=\-^°]+/gi;
  51. const MATCH_PADDED_HYPHENS_REPLACE_REGEX = /^-|-$/g;
  52. /**
  53. * Decimal digit count of a non-negative integer, without allocating a string.
  54. * @param {number} n non-negative integer
  55. * @returns {number} number of decimal digits
  56. */
  57. const numberLength = (n) => {
  58. if (n < 10) return 1;
  59. if (n < 100) return 2;
  60. if (n < 1000) return 3;
  61. if (n < 10000) return 4;
  62. if (n < 100000) return 5;
  63. if (n < 1000000) return 6;
  64. if (n < 10000000) return 7;
  65. return String(n).length;
  66. };
  67. /**
  68. * Defines the render manifest options type used by this module.
  69. * @typedef {object} RenderManifestOptions
  70. * @property {Chunk} chunk the chunk used to render
  71. * @property {string} hash
  72. * @property {string} fullHash
  73. * @property {OutputOptions} outputOptions
  74. * @property {CodeGenerationResults} codeGenerationResults
  75. * @property {{ javascript: ModuleTemplate }} moduleTemplates
  76. * @property {DependencyTemplates} dependencyTemplates
  77. * @property {RuntimeTemplate} runtimeTemplate
  78. * @property {ModuleGraph} moduleGraph
  79. * @property {ChunkGraph} chunkGraph
  80. */
  81. /** @typedef {RenderManifestEntryTemplated | RenderManifestEntryStatic} RenderManifestEntry */
  82. /**
  83. * Defines the render manifest entry templated type used by this module.
  84. * @typedef {object} RenderManifestEntryTemplated
  85. * @property {() => Source} render
  86. * @property {string | import("./TemplatedPathPlugin").TemplatePathFn<EXPECTED_ANY>} filenameTemplate
  87. * @property {PathData=} pathOptions
  88. * @property {AssetInfo=} info
  89. * @property {string} identifier
  90. * @property {string=} hash
  91. * @property {boolean=} auxiliary
  92. */
  93. /**
  94. * Defines the render manifest entry static type used by this module.
  95. * @typedef {object} RenderManifestEntryStatic
  96. * @property {() => Source} render
  97. * @property {string} filename
  98. * @property {AssetInfo} info
  99. * @property {string} identifier
  100. * @property {string=} hash
  101. * @property {boolean=} auxiliary
  102. */
  103. /**
  104. * Defines the module filter predicate type used by this module.
  105. * @typedef {(module: Module) => boolean} ModuleFilterPredicate
  106. */
  107. /**
  108. * Represents the template runtime component.
  109. * @typedef {object} Stringable
  110. * @property {() => string} toString
  111. */
  112. class Template {
  113. /**
  114. * Gets function content.
  115. * @param {Stringable} fn a runtime function (.runtime.js) "template"
  116. * @returns {string} the updated and normalized function string
  117. */
  118. static getFunctionContent(fn) {
  119. return fn
  120. .toString()
  121. .replace(JSDOC_LINE_REGEX, "")
  122. .replace(JSDOC_INLINE_REGEX, "")
  123. .replace(FUNCTION_CONTENT_REGEX, "")
  124. .replace(INDENT_MULTILINE_REGEX, "")
  125. .replace(LINE_SEPARATOR_REGEX, "\n");
  126. }
  127. /**
  128. * Returns created identifier.
  129. * @param {string} str the string converted to identifier
  130. * @returns {string} created identifier
  131. */
  132. static toIdentifier(str) {
  133. if (typeof str !== "string") return "";
  134. return str
  135. .replace(IDENTIFIER_NAME_REPLACE_REGEX, "_$1")
  136. .replace(IDENTIFIER_ALPHA_NUMERIC_NAME_REPLACE_REGEX, "_");
  137. }
  138. /**
  139. * Returns a commented version of string.
  140. * @param {string} str string to be converted to commented in bundle code
  141. * @returns {string} returns a commented version of string
  142. */
  143. static toComment(str) {
  144. if (!str) return "";
  145. return `/*! ${str.includes("*/") ? str.replace(COMMENT_END_REGEX, "* /") : str} */`;
  146. }
  147. /**
  148. * Returns a commented version of string.
  149. * @param {string} str string to be converted to "normal comment"
  150. * @returns {string} returns a commented version of string
  151. */
  152. static toNormalComment(str) {
  153. if (!str) return "";
  154. return `/* ${str.includes("*/") ? str.replace(COMMENT_END_REGEX, "* /") : str} */`;
  155. }
  156. /**
  157. * Returns normalized bundle-safe path.
  158. * @param {string} str string path to be normalized
  159. * @returns {string} normalized bundle-safe path
  160. */
  161. static toPath(str) {
  162. if (typeof str !== "string") return "";
  163. return str
  164. .replace(PATH_NAME_NORMALIZE_REPLACE_REGEX, "-")
  165. .replace(MATCH_PADDED_HYPHENS_REPLACE_REGEX, "");
  166. }
  167. // map number to a single character a-z, A-Z or multiple characters if number is too big
  168. /**
  169. * Number to identifier.
  170. * @param {number} n number to convert to ident
  171. * @returns {string} returns single character ident
  172. */
  173. static numberToIdentifier(n) {
  174. if (n >= NUMBER_OF_IDENTIFIER_START_CHARS) {
  175. // use multiple letters
  176. return (
  177. Template.numberToIdentifier(n % NUMBER_OF_IDENTIFIER_START_CHARS) +
  178. Template.numberToIdentifierContinuation(
  179. Math.floor(n / NUMBER_OF_IDENTIFIER_START_CHARS)
  180. )
  181. );
  182. }
  183. // lower case
  184. if (n < DELTA_A_TO_Z) {
  185. return String.fromCharCode(START_LOWERCASE_ALPHABET_CODE + n);
  186. }
  187. n -= DELTA_A_TO_Z;
  188. // upper case
  189. if (n < DELTA_A_TO_Z) {
  190. return String.fromCharCode(START_UPPERCASE_ALPHABET_CODE + n);
  191. }
  192. if (n === DELTA_A_TO_Z) return "_";
  193. return "$";
  194. }
  195. /**
  196. * Number to identifier continuation.
  197. * @param {number} n number to convert to ident
  198. * @returns {string} returns single character ident
  199. */
  200. static numberToIdentifierContinuation(n) {
  201. if (n >= NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS) {
  202. // use multiple letters
  203. return (
  204. Template.numberToIdentifierContinuation(
  205. n % NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS
  206. ) +
  207. Template.numberToIdentifierContinuation(
  208. Math.floor(n / NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS)
  209. )
  210. );
  211. }
  212. // lower case
  213. if (n < DELTA_A_TO_Z) {
  214. return String.fromCharCode(START_LOWERCASE_ALPHABET_CODE + n);
  215. }
  216. n -= DELTA_A_TO_Z;
  217. // upper case
  218. if (n < DELTA_A_TO_Z) {
  219. return String.fromCharCode(START_UPPERCASE_ALPHABET_CODE + n);
  220. }
  221. n -= DELTA_A_TO_Z;
  222. // numbers
  223. if (n < 10) {
  224. return `${n}`;
  225. }
  226. if (n === 10) return "_";
  227. return "$";
  228. }
  229. /**
  230. * Returns converted identity.
  231. * @param {string | string[]} s string to convert to identity
  232. * @returns {string} converted identity
  233. */
  234. static indent(s) {
  235. if (Array.isArray(s)) {
  236. return s.map(Template.indent).join("\n");
  237. }
  238. const str = s.trimEnd();
  239. if (!str) return "";
  240. const ind = str[0] === "\n" ? "" : "\t";
  241. return ind + str.replace(LINE_START_REGEX, "\n\t");
  242. }
  243. /**
  244. * Returns new prefix string.
  245. * @param {string | string[]} s string to create prefix for
  246. * @param {string} prefix prefix to compose
  247. * @returns {string} returns new prefix string
  248. */
  249. static prefix(s, prefix) {
  250. const str = Template.asString(s).trim();
  251. if (!str) return "";
  252. const ind = str[0] === "\n" ? "" : prefix;
  253. // Keeps the capture group: `prefix` is caller-supplied and may itself hold
  254. // `$&`/`$1`, which expand differently against a zero-width lookahead.
  255. return ind + str.replace(/\n([^\n])/g, `\n${prefix}$1`);
  256. }
  257. /**
  258. * Returns a single string from array.
  259. * @param {string | string[]} str string or string collection
  260. * @returns {string} returns a single string from array
  261. */
  262. static asString(str) {
  263. if (Array.isArray(str)) {
  264. return str.join("\n");
  265. }
  266. return str;
  267. }
  268. /**
  269. * Defines the with id type used by this module.
  270. * @typedef {object} WithId
  271. * @property {string | number} id
  272. */
  273. /**
  274. * Gets modules array bounds.
  275. * @param {WithId[]} modules a collection of modules to get array bounds for
  276. * @returns {[number, number] | false} returns the upper and lower array bounds
  277. * or false if not every module has a number based id
  278. */
  279. static getModulesArrayBounds(modules) {
  280. let maxId = -Infinity;
  281. let minId = Infinity;
  282. for (const module of modules) {
  283. const moduleId = module.id;
  284. if (typeof moduleId !== "number") return false;
  285. if (maxId < moduleId) maxId = moduleId;
  286. if (minId > moduleId) minId = moduleId;
  287. }
  288. if (minId < 16 + String(minId).length) {
  289. // add minId x ',' instead of 'Array(minId).concat(…)'
  290. minId = 0;
  291. }
  292. // start with -1 because the first module needs no comma
  293. let objectOverhead = -1;
  294. for (const module of modules) {
  295. // module id digits + colon + comma; ids are non-negative integers here
  296. // (non-number ids already returned false above), so count digits
  297. // arithmetically instead of allocating a string per module.
  298. const id = /** @type {number} */ (module.id);
  299. objectOverhead += numberLength(id) + 2;
  300. }
  301. // number of commas, or when starting non-zero the length of Array(minId).concat()
  302. const arrayOverhead =
  303. minId === 0 ? maxId : 16 + numberLength(minId) + maxId;
  304. return arrayOverhead < objectOverhead ? [minId, maxId] : false;
  305. }
  306. /**
  307. * Renders chunk modules.
  308. * @param {ChunkRenderContext} renderContext render context
  309. * @param {Module[]} modules modules to render (should be ordered by identifier)
  310. * @param {(module: Module, renderInArray?: boolean) => Source | null} renderModule function to render a module
  311. * @param {string=} prefix applying prefix strings
  312. * @returns {Source | null} rendered chunk modules in a Source object or null if no modules
  313. */
  314. static renderChunkModules(renderContext, modules, renderModule, prefix = "") {
  315. const { chunkGraph } = renderContext;
  316. const source = new ConcatSource();
  317. if (modules.length === 0) {
  318. return null;
  319. }
  320. /** @type {{ id: ModuleId, module: Module }[]} */
  321. const modulesWithId = modules.map((m) => ({
  322. id: /** @type {ModuleId} */ (chunkGraph.getModuleId(m)),
  323. module: m
  324. }));
  325. const bounds = Template.getModulesArrayBounds(modulesWithId);
  326. const renderInObject = bounds === false;
  327. /** @type {{ id: ModuleId, source: Source | "false" }[]} */
  328. const allModules = modulesWithId.map(({ id, module }) => ({
  329. id,
  330. source: renderModule(module, renderInObject) || "false"
  331. }));
  332. if (bounds) {
  333. // Render a spare array
  334. const minId = bounds[0];
  335. const maxId = bounds[1];
  336. if (minId !== 0) {
  337. source.add(`Array(${minId}).concat(`);
  338. }
  339. source.add("[\n");
  340. /** @type {Map<ModuleId, { id: ModuleId, source: Source | "false" }>} */
  341. const modules = new Map();
  342. for (const module of allModules) {
  343. modules.set(module.id, module);
  344. }
  345. for (let idx = minId; idx <= maxId; idx++) {
  346. const module = modules.get(idx);
  347. if (idx !== minId) {
  348. source.add(",\n");
  349. }
  350. source.add(`/* ${idx} */`);
  351. if (module) {
  352. source.add("\n");
  353. source.add(module.source);
  354. }
  355. }
  356. source.add(`\n${prefix}]`);
  357. if (minId !== 0) {
  358. source.add(")");
  359. }
  360. } else {
  361. // Render an object
  362. source.add("{\n");
  363. for (let i = 0; i < allModules.length; i++) {
  364. const module = allModules[i];
  365. if (i !== 0) {
  366. source.add(",\n");
  367. }
  368. source.add(
  369. `\n/***/ ${JSON.stringify(module.id)}${renderContext.runtimeTemplate.supportsMethodShorthand() && module.source !== "false" ? "" : ":"}\n`
  370. );
  371. source.add(module.source);
  372. }
  373. source.add(`\n\n${prefix}}`);
  374. }
  375. return source;
  376. }
  377. /**
  378. * Renders runtime modules.
  379. * @param {RuntimeModule[]} runtimeModules array of runtime modules in order
  380. * @param {RenderContext & { codeGenerationResults?: CodeGenerationResults }} renderContext render context
  381. * @returns {Source} rendered runtime modules in a Source object
  382. */
  383. static renderRuntimeModules(runtimeModules, renderContext) {
  384. const source = new ConcatSource();
  385. for (const module of runtimeModules) {
  386. const codeGenerationResults = renderContext.codeGenerationResults;
  387. /** @type {undefined | Source} */
  388. let runtimeSource;
  389. if (codeGenerationResults) {
  390. runtimeSource = codeGenerationResults.getSource(
  391. module,
  392. renderContext.chunk.runtime,
  393. WEBPACK_MODULE_TYPE_RUNTIME
  394. );
  395. } else {
  396. const codeGenResult = module.codeGeneration({
  397. chunkGraph: renderContext.chunkGraph,
  398. dependencyTemplates: renderContext.dependencyTemplates,
  399. moduleGraph: renderContext.moduleGraph,
  400. runtimeTemplate: renderContext.runtimeTemplate,
  401. runtime: renderContext.chunk.runtime,
  402. runtimes: [renderContext.chunk.runtime],
  403. codeGenerationResults
  404. });
  405. if (!codeGenResult) continue;
  406. runtimeSource = codeGenResult.sources.get("runtime");
  407. }
  408. if (runtimeSource) {
  409. source.add(`${Template.toNormalComment(module.identifier())}\n`);
  410. if (!module.shouldIsolate()) {
  411. source.add(runtimeSource);
  412. source.add("\n\n");
  413. } else if (renderContext.runtimeTemplate.supportsArrowFunction()) {
  414. source.add("(() => {\n");
  415. source.add(new PrefixSource("\t", runtimeSource));
  416. source.add("\n})();\n\n");
  417. } else {
  418. source.add("!function() {\n");
  419. source.add(new PrefixSource("\t", runtimeSource));
  420. source.add("\n}();\n\n");
  421. }
  422. }
  423. }
  424. return source;
  425. }
  426. /**
  427. * Renders chunk runtime modules.
  428. * @param {RuntimeModule[]} runtimeModules array of runtime modules in order
  429. * @param {RenderContext} renderContext render context
  430. * @returns {Source} rendered chunk runtime modules in a Source object
  431. */
  432. static renderChunkRuntimeModules(runtimeModules, renderContext) {
  433. return new PrefixSource(
  434. "/******/ ",
  435. new ConcatSource(
  436. `function(${RuntimeGlobals.require}) { // webpackRuntimeModules\n`,
  437. this.renderRuntimeModules(runtimeModules, renderContext),
  438. "}\n"
  439. )
  440. );
  441. }
  442. }
  443. Template.NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS =
  444. NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS;
  445. Template.NUMBER_OF_IDENTIFIER_START_CHARS = NUMBER_OF_IDENTIFIER_START_CHARS;
  446. module.exports = Template;