AssignLibraryPlugin.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { ConcatSource } = require("webpack-sources");
  7. const { UsageState } = require("../ExportsInfo");
  8. const RuntimeGlobals = require("../RuntimeGlobals");
  9. const Template = require("../Template");
  10. const { propertyAccess } = require("../util/property");
  11. const { getEntryRuntime } = require("../util/runtime");
  12. const AbstractLibraryPlugin = require("./AbstractLibraryPlugin");
  13. /** @typedef {import("webpack-sources").Source} Source */
  14. /** @typedef {import("../../declarations/WebpackOptions").LibraryOptions} LibraryOptions */
  15. /** @typedef {import("../../declarations/WebpackOptions").LibraryType} LibraryType */
  16. /** @typedef {import("../../declarations/WebpackOptions").LibraryExport} LibraryExport */
  17. /** @typedef {import("../Chunk")} Chunk */
  18. /** @typedef {import("../Compilation")} Compilation */
  19. /** @typedef {import("../Compilation").ChunkHashContext} ChunkHashContext */
  20. /** @typedef {import("../Module")} Module */
  21. /** @typedef {import("../Module").RuntimeRequirements} RuntimeRequirements */
  22. /** @typedef {import("../ExportsInfo").ExportInfoName} ExportInfoName */
  23. /** @typedef {import("../javascript/JavascriptModulesPlugin").RenderContext} RenderContext */
  24. /** @typedef {import("../javascript/JavascriptModulesPlugin").StartupRenderContext} StartupRenderContext */
  25. /** @typedef {import("../util/Hash")} Hash */
  26. /**
  27. * Defines the shared type used by this module.
  28. * @template T
  29. * @typedef {import("./AbstractLibraryPlugin").LibraryContext<T>} LibraryContext<T>
  30. */
  31. const KEYWORD_REGEX =
  32. /^(?:await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|super|switch|static|this|throw|try|true|typeof|var|void|while|with|yield)$/;
  33. const IDENTIFIER_REGEX =
  34. /^[\p{L}\p{Nl}$_][\p{L}\p{Nl}$\p{Mn}\p{Mc}\p{Nd}\p{Pc}]*$/iu;
  35. /**
  36. * Validates the library name by checking for keywords and valid characters
  37. * @param {string} name name to be validated
  38. * @returns {boolean} true, when valid
  39. */
  40. const isNameValid = (name) =>
  41. !KEYWORD_REGEX.test(name) && IDENTIFIER_REGEX.test(name);
  42. /**
  43. * Returns code to access the accessor while initializing.
  44. * @param {string[]} accessor variable plus properties
  45. * @param {number} existingLength items of accessor that are existing already
  46. * @param {boolean=} initLast if the last property should also be initialized to an object
  47. * @returns {string} code to access the accessor while initializing
  48. */
  49. const accessWithInit = (accessor, existingLength, initLast = false) => {
  50. // This generates for [a, b, c, d]:
  51. // (((a = typeof a === "undefined" ? {} : a).b = a.b || {}).c = a.b.c || {}).d
  52. const base = accessor[0];
  53. if (accessor.length === 1 && !initLast) return base;
  54. let current =
  55. existingLength > 0
  56. ? base
  57. : `(${base} = typeof ${base} === "undefined" ? {} : ${base})`;
  58. // i is the current position in accessor that has been printed
  59. let i = 1;
  60. // all properties printed so far (excluding base)
  61. /** @type {string[] | undefined} */
  62. let propsSoFar;
  63. // if there is existingLength, print all properties until this position as property access
  64. if (existingLength > i) {
  65. propsSoFar = accessor.slice(1, existingLength);
  66. i = existingLength;
  67. current += propertyAccess(propsSoFar);
  68. } else {
  69. propsSoFar = [];
  70. }
  71. // all remaining properties (except the last one when initLast is not set)
  72. // should be printed as initializer
  73. const initUntil = initLast ? accessor.length : accessor.length - 1;
  74. for (; i < initUntil; i++) {
  75. const prop = accessor[i];
  76. propsSoFar.push(prop);
  77. current = `(${current}${propertyAccess([prop])} = ${base}${propertyAccess(
  78. propsSoFar
  79. )} || {})`;
  80. }
  81. // print the last property as property access if not yet printed
  82. if (i < accessor.length) {
  83. current = `${current}${propertyAccess([accessor[accessor.length - 1]])}`;
  84. }
  85. return current;
  86. };
  87. /** @typedef {string[] | "global"} LibraryPrefix */
  88. /**
  89. * Defines the assign library plugin options type used by this module.
  90. * @typedef {object} AssignLibraryPluginOptions
  91. * @property {LibraryType} type
  92. * @property {LibraryPrefix} prefix name prefix
  93. * @property {string | false} declare declare name as variable
  94. * @property {"error" | "static" | "copy" | "assign"} unnamed behavior for unnamed library name
  95. * @property {"copy" | "assign"=} named behavior for named library name
  96. */
  97. /** @typedef {string | string[]} LibraryName */
  98. /**
  99. * Defines the assign library plugin parsed type used by this module.
  100. * @typedef {object} AssignLibraryPluginParsed
  101. * @property {LibraryName} name
  102. * @property {LibraryExport=} export
  103. */
  104. /**
  105. * Represents the assign library plugin runtime component.
  106. * @typedef {AssignLibraryPluginParsed} T
  107. * @extends {AbstractLibraryPlugin<AssignLibraryPluginParsed>}
  108. */
  109. class AssignLibraryPlugin extends AbstractLibraryPlugin {
  110. /**
  111. * Creates an instance of AssignLibraryPlugin.
  112. * @param {AssignLibraryPluginOptions} options the plugin options
  113. */
  114. constructor(options) {
  115. super({
  116. pluginName: "AssignLibraryPlugin",
  117. type: options.type
  118. });
  119. /** @type {AssignLibraryPluginOptions["prefix"]} */
  120. this.prefix = options.prefix;
  121. /** @type {AssignLibraryPluginOptions["declare"]} */
  122. this.declare = options.declare;
  123. /** @type {AssignLibraryPluginOptions["unnamed"]} */
  124. this.unnamed = options.unnamed;
  125. /** @type {AssignLibraryPluginOptions["named"]} */
  126. this.named = options.named || "assign";
  127. }
  128. /**
  129. * Returns preprocess as needed by overriding.
  130. * @param {LibraryOptions} library normalized library option
  131. * @returns {T} preprocess as needed by overriding
  132. */
  133. parseOptions(library) {
  134. const { name } = library;
  135. if (this.unnamed === "error") {
  136. if (typeof name !== "string" && !Array.isArray(name)) {
  137. throw new Error(
  138. `Library name must be a string or string array. ${AbstractLibraryPlugin.COMMON_LIBRARY_NAME_MESSAGE}`
  139. );
  140. }
  141. } else if (name && typeof name !== "string" && !Array.isArray(name)) {
  142. throw new Error(
  143. `Library name must be a string, string array or unset. ${AbstractLibraryPlugin.COMMON_LIBRARY_NAME_MESSAGE}`
  144. );
  145. }
  146. const _name = /** @type {LibraryName} */ (name);
  147. return {
  148. name: _name,
  149. export: library.export
  150. };
  151. }
  152. /**
  153. * Finish entry module.
  154. * @param {Module} module the exporting entry module
  155. * @param {string} entryName the name of the entrypoint
  156. * @param {LibraryContext<T>} libraryContext context
  157. * @returns {void}
  158. */
  159. finishEntryModule(
  160. module,
  161. entryName,
  162. { options, compilation, compilation: { moduleGraph } }
  163. ) {
  164. const runtime = getEntryRuntime(compilation, entryName);
  165. if (options.export) {
  166. const exportsInfo = moduleGraph.getExportInfo(
  167. module,
  168. Array.isArray(options.export) ? options.export[0] : options.export
  169. );
  170. exportsInfo.setUsed(UsageState.Used, runtime);
  171. exportsInfo.canMangleUse = false;
  172. } else {
  173. const exportsInfo = moduleGraph.getExportsInfo(module);
  174. exportsInfo.setUsedInUnknownWay(runtime);
  175. }
  176. moduleGraph.addExtraReason(module, "used as library export");
  177. }
  178. /**
  179. * Returns the prefix.
  180. * @param {Compilation} compilation the compilation
  181. * @returns {LibraryPrefix} the prefix
  182. */
  183. _getPrefix(compilation) {
  184. return this.prefix === "global"
  185. ? [compilation.runtimeTemplate.globalObject]
  186. : this.prefix;
  187. }
  188. /**
  189. * Get resolved full name.
  190. * @param {AssignLibraryPluginParsed} options the library options
  191. * @param {Chunk} chunk the chunk
  192. * @param {Compilation} compilation the compilation
  193. * @returns {string[]} the resolved full name
  194. */
  195. _getResolvedFullName(options, chunk, compilation) {
  196. const prefix = this._getPrefix(compilation);
  197. const fullName = options.name
  198. ? [
  199. ...prefix,
  200. ...(Array.isArray(options.name) ? options.name : [options.name])
  201. ]
  202. : /** @type {string[]} */ (prefix);
  203. return fullName.map((n) =>
  204. compilation.getPath(n, {
  205. chunk
  206. })
  207. );
  208. }
  209. /**
  210. * Returns source with library export.
  211. * @param {Source} source source
  212. * @param {RenderContext} renderContext render context
  213. * @param {LibraryContext<T>} libraryContext context
  214. * @returns {Source} source with library export
  215. */
  216. render(source, { chunk }, { options, compilation }) {
  217. const fullNameResolved = this._getResolvedFullName(
  218. options,
  219. chunk,
  220. compilation
  221. );
  222. if (this.declare) {
  223. const base = fullNameResolved[0];
  224. if (!isNameValid(base)) {
  225. throw new Error(
  226. `Library name base (${base}) must be a valid identifier when using a var declaring library type. Either use a valid identifier (e. g. ${Template.toIdentifier(
  227. base
  228. )}) or use a different library type (e. g. 'type: "global"', which assign a property on the global scope instead of declaring a variable). ${
  229. AbstractLibraryPlugin.COMMON_LIBRARY_NAME_MESSAGE
  230. }`
  231. );
  232. }
  233. source = new ConcatSource(`${this.declare} ${base};\n`, source);
  234. }
  235. return source;
  236. }
  237. /**
  238. * Embed in runtime bailout.
  239. * @param {Module} module the exporting entry module
  240. * @param {RenderContext} renderContext render context
  241. * @param {LibraryContext<T>} libraryContext context
  242. * @returns {string | undefined} bailout reason
  243. */
  244. embedInRuntimeBailout(
  245. module,
  246. { chunk, codeGenerationResults },
  247. { options, compilation }
  248. ) {
  249. const { data } = codeGenerationResults.get(module, chunk.runtime);
  250. const topLevelDeclarations =
  251. (data && data.get("topLevelDeclarations")) ||
  252. (module.buildInfo && module.buildInfo.topLevelDeclarations);
  253. if (!topLevelDeclarations) {
  254. return "it doesn't tell about top level declarations.";
  255. }
  256. const fullNameResolved = this._getResolvedFullName(
  257. options,
  258. chunk,
  259. compilation
  260. );
  261. const base = fullNameResolved[0];
  262. if (topLevelDeclarations.has(base)) {
  263. return `it declares '${base}' on top-level, which conflicts with the current library output.`;
  264. }
  265. }
  266. /**
  267. * Strict runtime bailout.
  268. * @param {RenderContext} renderContext render context
  269. * @param {LibraryContext<T>} libraryContext context
  270. * @returns {string | undefined} bailout reason
  271. */
  272. strictRuntimeBailout({ chunk }, { options, compilation }) {
  273. if (
  274. this.declare ||
  275. this.prefix === "global" ||
  276. this.prefix.length > 0 ||
  277. !options.name
  278. ) {
  279. return;
  280. }
  281. return "a global variable is assign and maybe created";
  282. }
  283. /**
  284. * Renders source with library export.
  285. * @param {Source} source source
  286. * @param {Module} module module
  287. * @param {StartupRenderContext} renderContext render context
  288. * @param {LibraryContext<T>} libraryContext context
  289. * @returns {Source} source with library export
  290. */
  291. renderStartup(
  292. source,
  293. module,
  294. { moduleGraph, chunk },
  295. { options, compilation }
  296. ) {
  297. const fullNameResolved = this._getResolvedFullName(
  298. options,
  299. chunk,
  300. compilation
  301. );
  302. const staticExports = this.unnamed === "static";
  303. const exportAccess = options.export
  304. ? propertyAccess(
  305. Array.isArray(options.export) ? options.export : [options.export]
  306. )
  307. : "";
  308. const result = new ConcatSource(source);
  309. if (staticExports) {
  310. const exportsInfo = moduleGraph.getExportsInfo(module);
  311. const exportTarget = accessWithInit(
  312. fullNameResolved,
  313. this._getPrefix(compilation).length,
  314. true
  315. );
  316. /** @type {ExportInfoName[]} */
  317. const provided = [];
  318. for (const exportInfo of exportsInfo.orderedExports) {
  319. if (!exportInfo.provided) continue;
  320. const nameAccess = propertyAccess([exportInfo.name]);
  321. result.add(
  322. `${exportTarget}${nameAccess} = ${RuntimeGlobals.exports}${exportAccess}${nameAccess};\n`
  323. );
  324. provided.push(exportInfo.name);
  325. }
  326. const webpackExportTarget = accessWithInit(
  327. fullNameResolved,
  328. this._getPrefix(compilation).length,
  329. true
  330. );
  331. /** @type {string} */
  332. let exports = RuntimeGlobals.exports;
  333. if (exportAccess) {
  334. result.add(
  335. `var __webpack_exports_export__ = ${RuntimeGlobals.exports}${exportAccess};\n`
  336. );
  337. exports = "__webpack_exports_export__";
  338. }
  339. result.add(`for(var __webpack_i__ in ${exports}) {\n`);
  340. const hasProvided = provided.length > 0;
  341. if (hasProvided) {
  342. result.add(
  343. ` if (${JSON.stringify(provided)}.indexOf(__webpack_i__) === -1) {\n`
  344. );
  345. }
  346. result.add(
  347. ` ${
  348. hasProvided ? " " : ""
  349. }${webpackExportTarget}[__webpack_i__] = ${exports}[__webpack_i__];\n`
  350. );
  351. if (hasProvided) {
  352. result.add(" }\n");
  353. }
  354. result.add("}\n");
  355. result.add(
  356. `Object.defineProperty(${exportTarget}, "__esModule", { value: true });\n`
  357. );
  358. } else if (options.name ? this.named === "copy" : this.unnamed === "copy") {
  359. result.add(
  360. `var __webpack_export_target__ = ${accessWithInit(
  361. fullNameResolved,
  362. this._getPrefix(compilation).length,
  363. true
  364. )};\n`
  365. );
  366. /** @type {string} */
  367. let exports = RuntimeGlobals.exports;
  368. if (exportAccess) {
  369. result.add(
  370. `var __webpack_exports_export__ = ${RuntimeGlobals.exports}${exportAccess};\n`
  371. );
  372. exports = "__webpack_exports_export__";
  373. }
  374. result.add(
  375. `for(var __webpack_i__ in ${exports}) __webpack_export_target__[__webpack_i__] = ${exports}[__webpack_i__];\n`
  376. );
  377. result.add(
  378. `if(${exports}.__esModule) Object.defineProperty(__webpack_export_target__, "__esModule", { value: true });\n`
  379. );
  380. } else {
  381. result.add(
  382. `${accessWithInit(
  383. fullNameResolved,
  384. this._getPrefix(compilation).length,
  385. false
  386. )} = ${RuntimeGlobals.exports}${exportAccess};\n`
  387. );
  388. }
  389. return result;
  390. }
  391. /**
  392. * Processes the provided chunk.
  393. * @param {Chunk} chunk the chunk
  394. * @param {RuntimeRequirements} set runtime requirements
  395. * @param {LibraryContext<T>} libraryContext context
  396. * @returns {void}
  397. */
  398. runtimeRequirements(chunk, set, libraryContext) {
  399. set.add(RuntimeGlobals.exports);
  400. }
  401. /**
  402. * Processes the provided chunk.
  403. * @param {Chunk} chunk the chunk
  404. * @param {Hash} hash hash
  405. * @param {ChunkHashContext} chunkHashContext chunk hash context
  406. * @param {LibraryContext<T>} libraryContext context
  407. * @returns {void}
  408. */
  409. chunkHash(chunk, hash, chunkHashContext, { options, compilation }) {
  410. hash.update("AssignLibraryPlugin");
  411. const fullNameResolved = this._getResolvedFullName(
  412. options,
  413. chunk,
  414. compilation
  415. );
  416. if (options.name ? this.named === "copy" : this.unnamed === "copy") {
  417. hash.update("copy");
  418. }
  419. if (this.declare) {
  420. hash.update(this.declare);
  421. }
  422. hash.update(fullNameResolved.join("."));
  423. if (options.export) {
  424. hash.update(`${options.export}`);
  425. }
  426. }
  427. }
  428. module.exports = AssignLibraryPlugin;