ConcatenationScope.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const {
  7. DEFAULT_EXPORT,
  8. NAMESPACE_OBJECT_EXPORT
  9. } = require("./util/concatenate");
  10. /** @import { Source } from "webpack-sources" */
  11. /** @import Module from "./Module" */
  12. /**
  13. * @import {
  14. * ConcatenatedModuleInfo,
  15. * ModuleInfo,
  16. * ExportName as Ids
  17. * } from "./optimize/ConcatenatedModule"
  18. */
  19. /** @import { Range } from "./javascript/JavascriptParser" */
  20. /** @typedef {{ start: number, end: number, name: string }} ModuleReferenceMatch */
  21. const MODULE_REFERENCE_REGEXP =
  22. /^__WEBPACK_MODULE_REFERENCE__(\d+)_([\da-f]+|ns)(_call)?(_directImport)?(_deferredImport)?(_mangleableNs)?(_moduleExportsAccess)?(?:_asiSafe(\d))?__$/;
  23. // unanchored variant: wrapped CJS bodies are not re-parsed, so their
  24. // references must be found textually
  25. const MODULE_REFERENCE_SCAN_REGEXP = new RegExp(
  26. MODULE_REFERENCE_REGEXP.source.slice(1, -1),
  27. "g"
  28. );
  29. /** @type {WeakMap<Source, ModuleReferenceMatch[]>} */
  30. const moduleReferencesCache = new WeakMap();
  31. /**
  32. * Encodes how a concatenated module reference should be interpreted when it is
  33. * later reconstructed from its placeholder identifier.
  34. * @typedef {object} ModuleReferenceOptions
  35. * @property {Ids} ids the properties or exports selected from the referenced module
  36. * @property {boolean} call true, when this referenced export is called
  37. * @property {boolean} directImport true, when this referenced export is directly imported (not via property access)
  38. * @property {boolean} deferredImport true, when this referenced export is deferred
  39. * @property {boolean} mangleableNamespace true, when a whole-namespace reference may use a decoupled namespace object that keeps the original export names
  40. * @property {boolean} moduleExportsAccess true, when the reference resolves to the module's own exports value with plain property access, skipping ESM interop (how `require()` and a wrapped module's side-effect init see it)
  41. * @property {boolean | undefined} asiSafe if the position is ASI safe or unknown
  42. */
  43. /**
  44. * Tracks the symbols and cross-module references needed while rendering a
  45. * concatenated module.
  46. */
  47. class ConcatenationScope {
  48. /**
  49. * Creates the mutable scope object used while rendering a concatenated
  50. * module and its cross-module references.
  51. * @param {ModuleInfo[] | Map<Module, ModuleInfo>} modulesMap all module info by module
  52. * @param {ConcatenatedModuleInfo} currentModule the current module info
  53. * @param {Set<string>} usedNames all used names
  54. */
  55. constructor(modulesMap, currentModule, usedNames) {
  56. /** @type {ConcatenatedModuleInfo} */
  57. this._currentModule = currentModule;
  58. if (Array.isArray(modulesMap)) {
  59. /** @type {Map<Module, ConcatenatedModuleInfo>} */
  60. const map = new Map();
  61. for (const info of modulesMap) {
  62. map.set(info.module, /** @type {ConcatenatedModuleInfo} */ (info));
  63. }
  64. modulesMap = map;
  65. }
  66. /** @type {Set<string>} */
  67. this.usedNames = usedNames;
  68. /** @type {Map<Module, ModuleInfo>} */
  69. this._modulesMap = modulesMap;
  70. // written and read by dependency templates within a single code
  71. // generation pass, never across passes
  72. /** @type {Range[] | undefined} */
  73. this._replacedRequireRanges = undefined;
  74. }
  75. /**
  76. * Checks whether a module participates in the current concatenation scope.
  77. * @param {Module} module the referenced module
  78. * @returns {boolean} true, when it's in the scope
  79. */
  80. isModuleInScope(module) {
  81. return this._modulesMap.has(module);
  82. }
  83. /**
  84. * Whether an in-scope module's body renders inside the lazy wrapper.
  85. * @param {Module} module the referenced module
  86. * @returns {boolean} true, when it's wrapped
  87. */
  88. isModuleWrapped(module) {
  89. const info = this._modulesMap.get(module);
  90. return info !== undefined && info.wrapped;
  91. }
  92. /**
  93. * Marks a wrapped module as eagerly imported, so its accessor is called at its
  94. * own slot in evaluation order instead of from inside the importing body.
  95. * @param {Module} module the referenced module
  96. */
  97. registerEagerModule(module) {
  98. const info = this._modulesMap.get(module);
  99. if (info !== undefined) info.eager = true;
  100. }
  101. /**
  102. * Whether the current module runs inside the lazy wrapper. Exports then live
  103. * on a real exports object, not hoisted bindings, so templates emit runtime
  104. * code.
  105. * @returns {boolean} true, when the current module is wrapped
  106. */
  107. isWrapped() {
  108. return this._currentModule.wrapped;
  109. }
  110. /**
  111. * Records a `require(...)` call that was replaced as a whole by a
  112. * concatenation reference, so nothing else rewrites a range inside it.
  113. * @param {Range} range source range of the replaced call
  114. */
  115. registerReplacedRequire(range) {
  116. if (this._replacedRequireRanges === undefined) {
  117. this._replacedRequireRanges = [];
  118. }
  119. this._replacedRequireRanges.push(range);
  120. }
  121. /**
  122. * Checks whether an offset sits inside an already-replaced `require(...)` call.
  123. * Containment, not exact match: `new require(...)` is replaced from `new`.
  124. * @param {number} start start offset to test
  125. * @returns {boolean} true, when the offset was replaced already
  126. */
  127. isInsideReplacedRequire(start) {
  128. const ranges = this._replacedRequireRanges;
  129. if (ranges === undefined) return false;
  130. for (const [rangeStart, rangeEnd] of ranges) {
  131. if (start >= rangeStart && start < rangeEnd) return true;
  132. }
  133. return false;
  134. }
  135. /**
  136. * Records the symbol that should be used when the current module exports a
  137. * named binding.
  138. * @param {string} exportName name of the export
  139. * @param {string} symbol identifier of the export in source code
  140. */
  141. registerExport(exportName, symbol) {
  142. if (!this._currentModule.exportMap) {
  143. this._currentModule.exportMap = new Map();
  144. }
  145. if (!this._currentModule.exportMap.has(exportName)) {
  146. this._currentModule.exportMap.set(exportName, symbol);
  147. }
  148. }
  149. /**
  150. * Records a raw expression that can be used to reference an export without
  151. * going through the normal symbol map.
  152. * @param {string} exportName name of the export
  153. * @param {string} expression expression to be used
  154. */
  155. registerRawExport(exportName, expression) {
  156. if (!this._currentModule.rawExportMap) {
  157. this._currentModule.rawExportMap = new Map();
  158. }
  159. if (!this._currentModule.rawExportMap.has(exportName)) {
  160. this._currentModule.rawExportMap.set(exportName, expression);
  161. }
  162. }
  163. /**
  164. * Returns the raw expression registered for an export, if one exists.
  165. * @param {string} exportName name of the export
  166. * @returns {string | undefined} the expression of the export
  167. */
  168. getRawExport(exportName) {
  169. if (!this._currentModule.rawExportMap) {
  170. return undefined;
  171. }
  172. return this._currentModule.rawExportMap.get(exportName);
  173. }
  174. /**
  175. * Replaces the raw expression for an export only when that export already
  176. * has an entry in the raw export map.
  177. * @param {string} exportName name of the export
  178. * @param {string} expression expression to be used
  179. */
  180. setRawExportMap(exportName, expression) {
  181. if (!this._currentModule.rawExportMap) {
  182. this._currentModule.rawExportMap = new Map();
  183. }
  184. if (this._currentModule.rawExportMap.has(exportName)) {
  185. this._currentModule.rawExportMap.set(exportName, expression);
  186. }
  187. }
  188. /**
  189. * Records the symbol that should be used for the synthetic namespace export.
  190. * @param {string} symbol identifier of the export in source code
  191. */
  192. registerNamespaceExport(symbol) {
  193. this._currentModule.namespaceExportSymbol = symbol;
  194. }
  195. /**
  196. * Encodes a reference to another concatenated module as a placeholder
  197. * identifier that can be parsed later during code generation.
  198. * @param {Module} module the referenced module
  199. * @param {Partial<ModuleReferenceOptions>} options options
  200. * @returns {string} the reference as identifier
  201. */
  202. createModuleReference(
  203. module,
  204. {
  205. ids = undefined,
  206. call = false,
  207. directImport = false,
  208. deferredImport = false,
  209. mangleableNamespace = false,
  210. moduleExportsAccess = false,
  211. asiSafe = false
  212. }
  213. ) {
  214. const info = /** @type {ModuleInfo} */ (this._modulesMap.get(module));
  215. const callFlag = call ? "_call" : "";
  216. const directImportFlag = directImport ? "_directImport" : "";
  217. const deferredImportFlag = deferredImport ? "_deferredImport" : "";
  218. const mangleableNamespaceFlag = mangleableNamespace ? "_mangleableNs" : "";
  219. const moduleExportsAccessFlag = moduleExportsAccess
  220. ? "_moduleExportsAccess"
  221. : "";
  222. const asiSafeFlag = asiSafe
  223. ? "_asiSafe1"
  224. : asiSafe === false
  225. ? "_asiSafe0"
  226. : "";
  227. const exportData = ids
  228. ? Buffer.from(JSON.stringify(ids), "utf8").toString("hex")
  229. : "ns";
  230. // a "._" is appended to allow "delete ...", which would cause a SyntaxError in strict mode
  231. return `__WEBPACK_MODULE_REFERENCE__${info.index}_${exportData}${callFlag}${directImportFlag}${deferredImportFlag}${mangleableNamespaceFlag}${moduleExportsAccessFlag}${asiSafeFlag}__._`;
  232. }
  233. /**
  234. * Checks whether an identifier is one of webpack's encoded concatenation
  235. * module references.
  236. * @param {string} name the identifier
  237. * @returns {boolean} true, when it's an module reference
  238. */
  239. static isModuleReference(name) {
  240. return MODULE_REFERENCE_REGEXP.test(name);
  241. }
  242. /**
  243. * Finds all encoded module references in a generated source.
  244. * @param {Source} source generated source
  245. * @returns {ModuleReferenceMatch[]} reference token positions (end is exclusive, excludes the appended "._")
  246. */
  247. static findModuleReferences(source) {
  248. const cached = moduleReferencesCache.get(source);
  249. if (cached !== undefined) return cached;
  250. const code = source.source().toString();
  251. /** @type {ModuleReferenceMatch[]} */
  252. const result = [];
  253. MODULE_REFERENCE_SCAN_REGEXP.lastIndex = 0;
  254. let match = MODULE_REFERENCE_SCAN_REGEXP.exec(code);
  255. while (match !== null) {
  256. result.push({
  257. start: match.index,
  258. end: match.index + match[0].length,
  259. name: match[0]
  260. });
  261. match = MODULE_REFERENCE_SCAN_REGEXP.exec(code);
  262. }
  263. moduleReferencesCache.set(source, result);
  264. return result;
  265. }
  266. /**
  267. * Parses an encoded module reference back into its module index and
  268. * reference flags.
  269. * @param {string} name the identifier
  270. * @returns {ModuleReferenceOptions & { index: number } | null} parsed options and index
  271. */
  272. static matchModuleReference(name) {
  273. const match = MODULE_REFERENCE_REGEXP.exec(name);
  274. if (!match) return null;
  275. const index = Number(match[1]);
  276. const asiSafe = match[8];
  277. return {
  278. index,
  279. ids:
  280. match[2] === "ns"
  281. ? []
  282. : JSON.parse(Buffer.from(match[2], "hex").toString("utf8")),
  283. call: Boolean(match[3]),
  284. directImport: Boolean(match[4]),
  285. deferredImport: Boolean(match[5]),
  286. mangleableNamespace: Boolean(match[6]),
  287. moduleExportsAccess: Boolean(match[7]),
  288. asiSafe: asiSafe ? asiSafe === "1" : undefined
  289. };
  290. }
  291. }
  292. ConcatenationScope.DEFAULT_EXPORT = DEFAULT_EXPORT;
  293. ConcatenationScope.NAMESPACE_OBJECT_EXPORT = NAMESPACE_OBJECT_EXPORT;
  294. module.exports = ConcatenationScope;