UNPKG

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