You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
375 lines
12 KiB
375 lines
12 KiB
/* |
|
MIT License http://www.opensource.org/licenses/mit-license.php |
|
Author Tobias Koppers @sokra |
|
*/ |
|
|
|
"use strict"; |
|
|
|
const { ConcatSource } = require("webpack-sources"); |
|
const { UsageState } = require("../ExportsInfo"); |
|
const Template = require("../Template"); |
|
const propertyAccess = require("../util/propertyAccess"); |
|
const { getEntryRuntime } = require("../util/runtime"); |
|
const AbstractLibraryPlugin = require("./AbstractLibraryPlugin"); |
|
|
|
/** @typedef {import("webpack-sources").Source} Source */ |
|
/** @typedef {import("../../declarations/WebpackOptions").LibraryOptions} LibraryOptions */ |
|
/** @typedef {import("../../declarations/WebpackOptions").LibraryType} LibraryType */ |
|
/** @typedef {import("../Chunk")} Chunk */ |
|
/** @typedef {import("../Compilation").ChunkHashContext} ChunkHashContext */ |
|
/** @typedef {import("../Compiler")} Compiler */ |
|
/** @typedef {import("../Module")} Module */ |
|
/** @typedef {import("../javascript/JavascriptModulesPlugin").RenderContext} RenderContext */ |
|
/** @typedef {import("../javascript/JavascriptModulesPlugin").StartupRenderContext} StartupRenderContext */ |
|
/** @typedef {import("../util/Hash")} Hash */ |
|
/** @template T @typedef {import("./AbstractLibraryPlugin").LibraryContext<T>} LibraryContext<T> */ |
|
|
|
const KEYWORD_REGEX = |
|
/^(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)$/; |
|
const IDENTIFIER_REGEX = |
|
/^[\p{L}\p{Nl}$_][\p{L}\p{Nl}$\p{Mn}\p{Mc}\p{Nd}\p{Pc}]*$/iu; |
|
|
|
/** |
|
* Validates the library name by checking for keywords and valid characters |
|
* @param {string} name name to be validated |
|
* @returns {boolean} true, when valid |
|
*/ |
|
const isNameValid = name => { |
|
return !KEYWORD_REGEX.test(name) && IDENTIFIER_REGEX.test(name); |
|
}; |
|
|
|
/** |
|
* @param {string[]} accessor variable plus properties |
|
* @param {number} existingLength items of accessor that are existing already |
|
* @param {boolean=} initLast if the last property should also be initialized to an object |
|
* @returns {string} code to access the accessor while initializing |
|
*/ |
|
const accessWithInit = (accessor, existingLength, initLast = false) => { |
|
// This generates for [a, b, c, d]: |
|
// (((a = typeof a === "undefined" ? {} : a).b = a.b || {}).c = a.b.c || {}).d |
|
const base = accessor[0]; |
|
if (accessor.length === 1 && !initLast) return base; |
|
let current = |
|
existingLength > 0 |
|
? base |
|
: `(${base} = typeof ${base} === "undefined" ? {} : ${base})`; |
|
|
|
// i is the current position in accessor that has been printed |
|
let i = 1; |
|
|
|
// all properties printed so far (excluding base) |
|
let propsSoFar; |
|
|
|
// if there is existingLength, print all properties until this position as property access |
|
if (existingLength > i) { |
|
propsSoFar = accessor.slice(1, existingLength); |
|
i = existingLength; |
|
current += propertyAccess(propsSoFar); |
|
} else { |
|
propsSoFar = []; |
|
} |
|
|
|
// all remaining properties (except the last one when initLast is not set) |
|
// should be printed as initializer |
|
const initUntil = initLast ? accessor.length : accessor.length - 1; |
|
for (; i < initUntil; i++) { |
|
const prop = accessor[i]; |
|
propsSoFar.push(prop); |
|
current = `(${current}${propertyAccess([prop])} = ${base}${propertyAccess( |
|
propsSoFar |
|
)} || {})`; |
|
} |
|
|
|
// print the last property as property access if not yet printed |
|
if (i < accessor.length) |
|
current = `${current}${propertyAccess([accessor[accessor.length - 1]])}`; |
|
|
|
return current; |
|
}; |
|
|
|
/** |
|
* @typedef {Object} AssignLibraryPluginOptions |
|
* @property {LibraryType} type |
|
* @property {string[] | "global"} prefix name prefix |
|
* @property {string | false} declare declare name as variable |
|
* @property {"error"|"static"|"copy"|"assign"} unnamed behavior for unnamed library name |
|
* @property {"copy"|"assign"=} named behavior for named library name |
|
*/ |
|
|
|
/** |
|
* @typedef {Object} AssignLibraryPluginParsed |
|
* @property {string | string[]} name |
|
* @property {string | string[] | undefined} export |
|
*/ |
|
|
|
/** |
|
* @typedef {AssignLibraryPluginParsed} T |
|
* @extends {AbstractLibraryPlugin<AssignLibraryPluginParsed>} |
|
*/ |
|
class AssignLibraryPlugin extends AbstractLibraryPlugin { |
|
/** |
|
* @param {AssignLibraryPluginOptions} options the plugin options |
|
*/ |
|
constructor(options) { |
|
super({ |
|
pluginName: "AssignLibraryPlugin", |
|
type: options.type |
|
}); |
|
this.prefix = options.prefix; |
|
this.declare = options.declare; |
|
this.unnamed = options.unnamed; |
|
this.named = options.named || "assign"; |
|
} |
|
|
|
/** |
|
* @param {LibraryOptions} library normalized library option |
|
* @returns {T | false} preprocess as needed by overriding |
|
*/ |
|
parseOptions(library) { |
|
const { name } = library; |
|
if (this.unnamed === "error") { |
|
if (typeof name !== "string" && !Array.isArray(name)) { |
|
throw new Error( |
|
`Library name must be a string or string array. ${AbstractLibraryPlugin.COMMON_LIBRARY_NAME_MESSAGE}` |
|
); |
|
} |
|
} else { |
|
if (name && typeof name !== "string" && !Array.isArray(name)) { |
|
throw new Error( |
|
`Library name must be a string, string array or unset. ${AbstractLibraryPlugin.COMMON_LIBRARY_NAME_MESSAGE}` |
|
); |
|
} |
|
} |
|
return { |
|
name: /** @type {string|string[]=} */ (name), |
|
export: library.export |
|
}; |
|
} |
|
|
|
/** |
|
* @param {Module} module the exporting entry module |
|
* @param {string} entryName the name of the entrypoint |
|
* @param {LibraryContext<T>} libraryContext context |
|
* @returns {void} |
|
*/ |
|
finishEntryModule( |
|
module, |
|
entryName, |
|
{ options, compilation, compilation: { moduleGraph } } |
|
) { |
|
const runtime = getEntryRuntime(compilation, entryName); |
|
if (options.export) { |
|
const exportsInfo = moduleGraph.getExportInfo( |
|
module, |
|
Array.isArray(options.export) ? options.export[0] : options.export |
|
); |
|
exportsInfo.setUsed(UsageState.Used, runtime); |
|
exportsInfo.canMangleUse = false; |
|
} else { |
|
const exportsInfo = moduleGraph.getExportsInfo(module); |
|
exportsInfo.setUsedInUnknownWay(runtime); |
|
} |
|
moduleGraph.addExtraReason(module, "used as library export"); |
|
} |
|
|
|
_getPrefix(compilation) { |
|
return this.prefix === "global" |
|
? [compilation.runtimeTemplate.globalObject] |
|
: this.prefix; |
|
} |
|
|
|
_getResolvedFullName(options, chunk, compilation) { |
|
const prefix = this._getPrefix(compilation); |
|
const fullName = options.name ? prefix.concat(options.name) : prefix; |
|
return fullName.map(n => |
|
compilation.getPath(n, { |
|
chunk |
|
}) |
|
); |
|
} |
|
|
|
/** |
|
* @param {Source} source source |
|
* @param {RenderContext} renderContext render context |
|
* @param {LibraryContext<T>} libraryContext context |
|
* @returns {Source} source with library export |
|
*/ |
|
render(source, { chunk }, { options, compilation }) { |
|
const fullNameResolved = this._getResolvedFullName( |
|
options, |
|
chunk, |
|
compilation |
|
); |
|
if (this.declare) { |
|
const base = fullNameResolved[0]; |
|
if (!isNameValid(base)) { |
|
throw new Error( |
|
`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( |
|
base |
|
)}) or use a different library type (e. g. 'type: "global"', which assign a property on the global scope instead of declaring a variable). ${ |
|
AbstractLibraryPlugin.COMMON_LIBRARY_NAME_MESSAGE |
|
}` |
|
); |
|
} |
|
source = new ConcatSource(`${this.declare} ${base};\n`, source); |
|
} |
|
return source; |
|
} |
|
|
|
/** |
|
* @param {Module} module the exporting entry module |
|
* @param {RenderContext} renderContext render context |
|
* @param {LibraryContext<T>} libraryContext context |
|
* @returns {string | undefined} bailout reason |
|
*/ |
|
embedInRuntimeBailout( |
|
module, |
|
{ chunk, codeGenerationResults }, |
|
{ options, compilation } |
|
) { |
|
const { data } = codeGenerationResults.get(module, chunk.runtime); |
|
const topLevelDeclarations = |
|
(data && data.get("topLevelDeclarations")) || |
|
(module.buildInfo && module.buildInfo.topLevelDeclarations); |
|
if (!topLevelDeclarations) |
|
return "it doesn't tell about top level declarations."; |
|
const fullNameResolved = this._getResolvedFullName( |
|
options, |
|
chunk, |
|
compilation |
|
); |
|
const base = fullNameResolved[0]; |
|
if (topLevelDeclarations.has(base)) |
|
return `it declares '${base}' on top-level, which conflicts with the current library output.`; |
|
} |
|
|
|
/** |
|
* @param {RenderContext} renderContext render context |
|
* @param {LibraryContext<T>} libraryContext context |
|
* @returns {string | undefined} bailout reason |
|
*/ |
|
strictRuntimeBailout({ chunk }, { options, compilation }) { |
|
if ( |
|
this.declare || |
|
this.prefix === "global" || |
|
this.prefix.length > 0 || |
|
!options.name |
|
) { |
|
return; |
|
} |
|
return "a global variable is assign and maybe created"; |
|
} |
|
|
|
/** |
|
* @param {Source} source source |
|
* @param {Module} module module |
|
* @param {StartupRenderContext} renderContext render context |
|
* @param {LibraryContext<T>} libraryContext context |
|
* @returns {Source} source with library export |
|
*/ |
|
renderStartup( |
|
source, |
|
module, |
|
{ moduleGraph, chunk }, |
|
{ options, compilation } |
|
) { |
|
const fullNameResolved = this._getResolvedFullName( |
|
options, |
|
chunk, |
|
compilation |
|
); |
|
const staticExports = this.unnamed === "static"; |
|
const exportAccess = options.export |
|
? propertyAccess( |
|
Array.isArray(options.export) ? options.export : [options.export] |
|
) |
|
: ""; |
|
const result = new ConcatSource(source); |
|
if (staticExports) { |
|
const exportsInfo = moduleGraph.getExportsInfo(module); |
|
const exportTarget = accessWithInit( |
|
fullNameResolved, |
|
this._getPrefix(compilation).length, |
|
true |
|
); |
|
for (const exportInfo of exportsInfo.orderedExports) { |
|
if (!exportInfo.provided) continue; |
|
const nameAccess = propertyAccess([exportInfo.name]); |
|
result.add( |
|
`${exportTarget}${nameAccess} = __webpack_exports__${exportAccess}${nameAccess};\n` |
|
); |
|
} |
|
result.add( |
|
`Object.defineProperty(${exportTarget}, "__esModule", { value: true });\n` |
|
); |
|
} else if (options.name ? this.named === "copy" : this.unnamed === "copy") { |
|
result.add( |
|
`var __webpack_export_target__ = ${accessWithInit( |
|
fullNameResolved, |
|
this._getPrefix(compilation).length, |
|
true |
|
)};\n` |
|
); |
|
let exports = "__webpack_exports__"; |
|
if (exportAccess) { |
|
result.add( |
|
`var __webpack_exports_export__ = __webpack_exports__${exportAccess};\n` |
|
); |
|
exports = "__webpack_exports_export__"; |
|
} |
|
result.add( |
|
`for(var i in ${exports}) __webpack_export_target__[i] = ${exports}[i];\n` |
|
); |
|
result.add( |
|
`if(${exports}.__esModule) Object.defineProperty(__webpack_export_target__, "__esModule", { value: true });\n` |
|
); |
|
} else { |
|
result.add( |
|
`${accessWithInit( |
|
fullNameResolved, |
|
this._getPrefix(compilation).length, |
|
false |
|
)} = __webpack_exports__${exportAccess};\n` |
|
); |
|
} |
|
return result; |
|
} |
|
|
|
/** |
|
* @param {Chunk} chunk the chunk |
|
* @param {Set<string>} set runtime requirements |
|
* @param {LibraryContext<T>} libraryContext context |
|
* @returns {void} |
|
*/ |
|
runtimeRequirements(chunk, set, libraryContext) { |
|
// we don't need to return exports from runtime |
|
} |
|
|
|
/** |
|
* @param {Chunk} chunk the chunk |
|
* @param {Hash} hash hash |
|
* @param {ChunkHashContext} chunkHashContext chunk hash context |
|
* @param {LibraryContext<T>} libraryContext context |
|
* @returns {void} |
|
*/ |
|
chunkHash(chunk, hash, chunkHashContext, { options, compilation }) { |
|
hash.update("AssignLibraryPlugin"); |
|
const fullNameResolved = this._getResolvedFullName( |
|
options, |
|
chunk, |
|
compilation |
|
); |
|
if (options.name ? this.named === "copy" : this.unnamed === "copy") { |
|
hash.update("copy"); |
|
} |
|
if (this.declare) { |
|
hash.update(this.declare); |
|
} |
|
hash.update(fullNameResolved.join(".")); |
|
if (options.export) { |
|
hash.update(`${options.export}`); |
|
} |
|
} |
|
} |
|
|
|
module.exports = AssignLibraryPlugin;
|
|
|