AssignLibraryPlugin.js 14 KB

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