mirror of https://github.com/webpack/webpack.git
				
				
				
			Merge pull request #12896 from webpack/feature/define-cache-version
DefinePlugin takes care of module invalidation when values have changed
This commit is contained in:
		
						commit
						5af7e54e7d
					
				|  | @ -772,6 +772,8 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si | |||
| 		if (compiler.contextTimestamps) { | ||||
| 			this.fileSystemInfo.addContextTimestamps(compiler.contextTimestamps); | ||||
| 		} | ||||
| 		/** @type {Map<string, string>} */ | ||||
| 		this.valueCacheVersions = new Map(); | ||||
| 		this.requestShortener = compiler.requestShortener; | ||||
| 		this.compilerPath = compiler.compilerPath; | ||||
| 
 | ||||
|  | @ -1225,7 +1227,8 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si | |||
| 
 | ||||
| 		module.needBuild( | ||||
| 			{ | ||||
| 				fileSystemInfo: this.fileSystemInfo | ||||
| 				fileSystemInfo: this.fileSystemInfo, | ||||
| 				valueCacheVersions: this.valueCacheVersions | ||||
| 			}, | ||||
| 			(err, needBuild) => { | ||||
| 				if (err) return callback(err); | ||||
|  |  | |||
|  | @ -6,55 +6,126 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| const RuntimeGlobals = require("./RuntimeGlobals"); | ||||
| const WebpackError = require("./WebpackError"); | ||||
| const ConstDependency = require("./dependencies/ConstDependency"); | ||||
| const BasicEvaluatedExpression = require("./javascript/BasicEvaluatedExpression"); | ||||
| const { | ||||
| 	approve, | ||||
| 	evaluateToString, | ||||
| 	toConstantDependency | ||||
| } = require("./javascript/JavascriptParserHelpers"); | ||||
| 
 | ||||
| /** @typedef {import("estree").Expression} Expression */ | ||||
| /** @typedef {import("./Compiler")} Compiler */ | ||||
| /** @typedef {import("./NormalModule")} NormalModule */ | ||||
| /** @typedef {import("./RuntimeTemplate")} RuntimeTemplate */ | ||||
| /** @typedef {import("./javascript/JavascriptParser")} JavascriptParser */ | ||||
| 
 | ||||
| /** @typedef {null|undefined|RegExp|Function|string|number|boolean|bigint|undefined} CodeValuePrimitive */ | ||||
| /** @typedef {RecursiveArrayOrRecord<CodeValuePrimitive|RuntimeValue>} CodeValue */ | ||||
| 
 | ||||
| /** | ||||
|  * @typedef {Object} RuntimeValueOptions | ||||
|  * @property {string[]=} fileDependencies | ||||
|  * @property {string[]=} contextDependencies | ||||
|  * @property {string[]=} missingDependencies | ||||
|  * @property {string[]=} buildDependencies | ||||
|  * @property {string|function(): string=} version | ||||
|  */ | ||||
| 
 | ||||
| class RuntimeValue { | ||||
| 	constructor(fn, fileDependencies) { | ||||
| 	/** | ||||
| 	 * @param {function({ module: NormalModule, key: string, readonly version: string | undefined }): CodeValuePrimitive} fn generator function | ||||
| 	 * @param {true | string[] | RuntimeValueOptions=} options options | ||||
| 	 */ | ||||
| 	constructor(fn, options) { | ||||
| 		this.fn = fn; | ||||
| 		this.fileDependencies = fileDependencies || []; | ||||
| 		if (Array.isArray(options)) { | ||||
| 			options = { | ||||
| 				fileDependencies: options | ||||
| 			}; | ||||
| 		} | ||||
| 		this.options = options || {}; | ||||
| 	} | ||||
| 
 | ||||
| 	exec(parser) { | ||||
| 	get fileDependencies() { | ||||
| 		return this.options === true ? true : this.options.fileDependencies; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * @param {JavascriptParser} parser the parser | ||||
| 	 * @param {Map<string, string>} valueCacheVersions valueCacheVersions | ||||
| 	 * @param {string} key the defined key | ||||
| 	 * @returns {CodeValuePrimitive} code | ||||
| 	 */ | ||||
| 	exec(parser, valueCacheVersions, key) { | ||||
| 		const buildInfo = parser.state.module.buildInfo; | ||||
| 		if (this.fileDependencies === true) { | ||||
| 		if (this.options === true) { | ||||
| 			buildInfo.cacheable = false; | ||||
| 		} else { | ||||
| 			for (const fileDependency of this.fileDependencies) { | ||||
| 				buildInfo.fileDependencies.add(fileDependency); | ||||
| 			if (this.options.fileDependencies) { | ||||
| 				for (const dep of this.options.fileDependencies) { | ||||
| 					buildInfo.fileDependencies.add(dep); | ||||
| 				} | ||||
| 			} | ||||
| 			if (this.options.contextDependencies) { | ||||
| 				for (const dep of this.options.contextDependencies) { | ||||
| 					buildInfo.contextDependencies.add(dep); | ||||
| 				} | ||||
| 			} | ||||
| 			if (this.options.missingDependencies) { | ||||
| 				for (const dep of this.options.missingDependencies) { | ||||
| 					buildInfo.missingDependencies.add(dep); | ||||
| 				} | ||||
| 			} | ||||
| 			if (this.options.buildDependencies) { | ||||
| 				for (const dep of this.options.buildDependencies) { | ||||
| 					buildInfo.buildDependencies.add(dep); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return this.fn({ module: parser.state.module }); | ||||
| 		return this.fn({ | ||||
| 			module: parser.state.module, | ||||
| 			key, | ||||
| 			get version() { | ||||
| 				return valueCacheVersions.get(VALUE_DEP_PREFIX + key); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	getCacheVersion() { | ||||
| 		return this.options === true | ||||
| 			? undefined | ||||
| 			: (typeof this.options.version === "function" | ||||
| 					? this.options.version() | ||||
| 					: this.options.version) || "unset"; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {any[]|{[k: string]: any}} obj obj | ||||
|  * @param {JavascriptParser} parser Parser | ||||
|  * @param {Map<string, string>} valueCacheVersions valueCacheVersions | ||||
|  * @param {string} key the defined key | ||||
|  * @param {RuntimeTemplate} runtimeTemplate the runtime template | ||||
|  * @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded) | ||||
|  * @returns {string} code converted to string that evaluates | ||||
|  */ | ||||
| const stringifyObj = (obj, parser, runtimeTemplate, asiSafe) => { | ||||
| const stringifyObj = ( | ||||
| 	obj, | ||||
| 	parser, | ||||
| 	valueCacheVersions, | ||||
| 	key, | ||||
| 	runtimeTemplate, | ||||
| 	asiSafe | ||||
| ) => { | ||||
| 	let code; | ||||
| 	let arr = Array.isArray(obj); | ||||
| 	if (arr) { | ||||
| 		code = `[${obj | ||||
| 			.map(code => toCode(code, parser, runtimeTemplate, null)) | ||||
| 			.map(code => | ||||
| 				toCode(code, parser, valueCacheVersions, key, runtimeTemplate, null) | ||||
| 			) | ||||
| 			.join(",")}]`;
 | ||||
| 	} else { | ||||
| 		code = `{${Object.keys(obj) | ||||
|  | @ -63,7 +134,7 @@ const stringifyObj = (obj, parser, runtimeTemplate, asiSafe) => { | |||
| 				return ( | ||||
| 					JSON.stringify(key) + | ||||
| 					":" + | ||||
| 					toCode(code, parser, runtimeTemplate, null) | ||||
| 					toCode(code, parser, valueCacheVersions, key, runtimeTemplate, null) | ||||
| 				); | ||||
| 			}) | ||||
| 			.join(",")}}`;
 | ||||
|  | @ -85,11 +156,20 @@ const stringifyObj = (obj, parser, runtimeTemplate, asiSafe) => { | |||
|  * Convert code to a string that evaluates | ||||
|  * @param {CodeValue} code Code to evaluate | ||||
|  * @param {JavascriptParser} parser Parser | ||||
|  * @param {Map<string, string>} valueCacheVersions valueCacheVersions | ||||
|  * @param {string} key the defined key | ||||
|  * @param {RuntimeTemplate} runtimeTemplate the runtime template | ||||
|  * @param {boolean|undefined|null=} asiSafe asi safe (undefined: unknown, null: unneeded) | ||||
|  * @returns {string} code converted to string that evaluates | ||||
|  */ | ||||
| const toCode = (code, parser, runtimeTemplate, asiSafe) => { | ||||
| const toCode = ( | ||||
| 	code, | ||||
| 	parser, | ||||
| 	valueCacheVersions, | ||||
| 	key, | ||||
| 	runtimeTemplate, | ||||
| 	asiSafe | ||||
| ) => { | ||||
| 	if (code === null) { | ||||
| 		return "null"; | ||||
| 	} | ||||
|  | @ -100,7 +180,14 @@ const toCode = (code, parser, runtimeTemplate, asiSafe) => { | |||
| 		return "-0"; | ||||
| 	} | ||||
| 	if (code instanceof RuntimeValue) { | ||||
| 		return toCode(code.exec(parser), parser, runtimeTemplate, asiSafe); | ||||
| 		return toCode( | ||||
| 			code.exec(parser, valueCacheVersions, key), | ||||
| 			parser, | ||||
| 			valueCacheVersions, | ||||
| 			key, | ||||
| 			runtimeTemplate, | ||||
| 			asiSafe | ||||
| 		); | ||||
| 	} | ||||
| 	if (code instanceof RegExp && code.toString) { | ||||
| 		return code.toString(); | ||||
|  | @ -109,7 +196,14 @@ const toCode = (code, parser, runtimeTemplate, asiSafe) => { | |||
| 		return "(" + code.toString() + ")"; | ||||
| 	} | ||||
| 	if (typeof code === "object") { | ||||
| 		return stringifyObj(code, parser, runtimeTemplate, asiSafe); | ||||
| 		return stringifyObj( | ||||
| 			code, | ||||
| 			parser, | ||||
| 			valueCacheVersions, | ||||
| 			key, | ||||
| 			runtimeTemplate, | ||||
| 			asiSafe | ||||
| 		); | ||||
| 	} | ||||
| 	if (typeof code === "bigint") { | ||||
| 		return runtimeTemplate.supportsBigIntLiteral() | ||||
|  | @ -119,6 +213,41 @@ const toCode = (code, parser, runtimeTemplate, asiSafe) => { | |||
| 	return code + ""; | ||||
| }; | ||||
| 
 | ||||
| const toCacheVersion = code => { | ||||
| 	if (code === null) { | ||||
| 		return "null"; | ||||
| 	} | ||||
| 	if (code === undefined) { | ||||
| 		return "undefined"; | ||||
| 	} | ||||
| 	if (Object.is(code, -0)) { | ||||
| 		return "-0"; | ||||
| 	} | ||||
| 	if (code instanceof RuntimeValue) { | ||||
| 		return code.getCacheVersion(); | ||||
| 	} | ||||
| 	if (code instanceof RegExp && code.toString) { | ||||
| 		return code.toString(); | ||||
| 	} | ||||
| 	if (typeof code === "function" && code.toString) { | ||||
| 		return "(" + code.toString() + ")"; | ||||
| 	} | ||||
| 	if (typeof code === "object") { | ||||
| 		const items = Object.keys(code).map(key => ({ | ||||
| 			key, | ||||
| 			value: toCacheVersion(code[key]) | ||||
| 		})); | ||||
| 		if (items.some(({ value }) => value === undefined)) return undefined; | ||||
| 		return `{${items.map(({ key, value }) => `${key}: ${value}`).join(", ")}}`; | ||||
| 	} | ||||
| 	if (typeof code === "bigint") { | ||||
| 		return `${code}n`; | ||||
| 	} | ||||
| 	return code + ""; | ||||
| }; | ||||
| 
 | ||||
| const VALUE_DEP_PREFIX = "webpack/DefinePlugin "; | ||||
| 
 | ||||
| class DefinePlugin { | ||||
| 	/** | ||||
| 	 * Create a new define plugin | ||||
|  | @ -128,8 +257,13 @@ class DefinePlugin { | |||
| 		this.definitions = definitions; | ||||
| 	} | ||||
| 
 | ||||
| 	static runtimeValue(fn, fileDependencies) { | ||||
| 		return new RuntimeValue(fn, fileDependencies); | ||||
| 	/** | ||||
| 	 * @param {function({ module: NormalModule, key: string, readonly version: string | undefined }): CodeValuePrimitive} fn generator function | ||||
| 	 * @param {true | string[] | RuntimeValueOptions=} options options | ||||
| 	 * @returns {RuntimeValue} runtime value | ||||
| 	 */ | ||||
| 	static runtimeValue(fn, options) { | ||||
| 		return new RuntimeValue(fn, options); | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
|  | @ -154,6 +288,21 @@ class DefinePlugin { | |||
| 				 * @returns {void} | ||||
| 				 */ | ||||
| 				const handler = parser => { | ||||
| 					const addValueDependency = key => { | ||||
| 						const { buildInfo } = parser.state.module; | ||||
| 						if (!buildInfo.valueDependencies) | ||||
| 							buildInfo.valueDependencies = new Map(); | ||||
| 						buildInfo.valueDependencies.set( | ||||
| 							VALUE_DEP_PREFIX + key, | ||||
| 							compilation.valueCacheVersions.get(VALUE_DEP_PREFIX + key) | ||||
| 						); | ||||
| 					}; | ||||
| 
 | ||||
| 					const withValueDependency = (key, fn) => (...args) => { | ||||
| 						addValueDependency(key); | ||||
| 						return fn(...args); | ||||
| 					}; | ||||
| 
 | ||||
| 					/** | ||||
| 					 * Walk definitions | ||||
| 					 * @param {Object} definitions Definitions map | ||||
|  | @ -188,7 +337,10 @@ class DefinePlugin { | |||
| 						const splittedKey = key.split("."); | ||||
| 						splittedKey.slice(1).forEach((_, i) => { | ||||
| 							const fullKey = prefix + splittedKey.slice(0, i + 1).join("."); | ||||
| 							parser.hooks.canRename.for(fullKey).tap("DefinePlugin", approve); | ||||
| 							parser.hooks.canRename.for(fullKey).tap("DefinePlugin", () => { | ||||
| 								addValueDependency(key); | ||||
| 								return true; | ||||
| 							}); | ||||
| 						}); | ||||
| 					}; | ||||
| 
 | ||||
|  | @ -199,12 +351,16 @@ class DefinePlugin { | |||
| 					 * @returns {void} | ||||
| 					 */ | ||||
| 					const applyDefine = (key, code) => { | ||||
| 						const originalKey = key; | ||||
| 						const isTypeof = /^typeof\s+/.test(key); | ||||
| 						if (isTypeof) key = key.replace(/^typeof\s+/, ""); | ||||
| 						let recurse = false; | ||||
| 						let recurseTypeof = false; | ||||
| 						if (!isTypeof) { | ||||
| 							parser.hooks.canRename.for(key).tap("DefinePlugin", approve); | ||||
| 							parser.hooks.canRename.for(key).tap("DefinePlugin", () => { | ||||
| 								addValueDependency(originalKey); | ||||
| 								return true; | ||||
| 							}); | ||||
| 							parser.hooks.evaluateIdentifier | ||||
| 								.for(key) | ||||
| 								.tap("DefinePlugin", expr => { | ||||
|  | @ -217,18 +373,29 @@ class DefinePlugin { | |||
| 									 * }); | ||||
| 									 */ | ||||
| 									if (recurse) return; | ||||
| 									addValueDependency(originalKey); | ||||
| 									recurse = true; | ||||
| 									const res = parser.evaluate( | ||||
| 										toCode(code, parser, runtimeTemplate, null) | ||||
| 										toCode( | ||||
| 											code, | ||||
| 											parser, | ||||
| 											compilation.valueCacheVersions, | ||||
| 											key, | ||||
| 											runtimeTemplate, | ||||
| 											null | ||||
| 										) | ||||
| 									); | ||||
| 									recurse = false; | ||||
| 									res.setRange(expr.range); | ||||
| 									return res; | ||||
| 								}); | ||||
| 							parser.hooks.expression.for(key).tap("DefinePlugin", expr => { | ||||
| 								addValueDependency(originalKey); | ||||
| 								const strCode = toCode( | ||||
| 									code, | ||||
| 									parser, | ||||
| 									compilation.valueCacheVersions, | ||||
| 									originalKey, | ||||
| 									runtimeTemplate, | ||||
| 									!parser.isAsiPosition(expr.range[0]) | ||||
| 								); | ||||
|  | @ -256,22 +423,36 @@ class DefinePlugin { | |||
| 							 */ | ||||
| 							if (recurseTypeof) return; | ||||
| 							recurseTypeof = true; | ||||
| 							addValueDependency(originalKey); | ||||
| 							const codeCode = toCode( | ||||
| 								code, | ||||
| 								parser, | ||||
| 								compilation.valueCacheVersions, | ||||
| 								originalKey, | ||||
| 								runtimeTemplate, | ||||
| 								null | ||||
| 							); | ||||
| 							const typeofCode = isTypeof | ||||
| 								? toCode(code, parser, runtimeTemplate, null) | ||||
| 								: "typeof (" + | ||||
| 								  toCode(code, parser, runtimeTemplate, null) + | ||||
| 								  ")"; | ||||
| 								? codeCode | ||||
| 								: "typeof (" + codeCode + ")"; | ||||
| 							const res = parser.evaluate(typeofCode); | ||||
| 							recurseTypeof = false; | ||||
| 							res.setRange(expr.range); | ||||
| 							return res; | ||||
| 						}); | ||||
| 						parser.hooks.typeof.for(key).tap("DefinePlugin", expr => { | ||||
| 							addValueDependency(originalKey); | ||||
| 							const codeCode = toCode( | ||||
| 								code, | ||||
| 								parser, | ||||
| 								compilation.valueCacheVersions, | ||||
| 								originalKey, | ||||
| 								runtimeTemplate, | ||||
| 								null | ||||
| 							); | ||||
| 							const typeofCode = isTypeof | ||||
| 								? toCode(code, parser, runtimeTemplate, null) | ||||
| 								: "typeof (" + | ||||
| 								  toCode(code, parser, runtimeTemplate, null) + | ||||
| 								  ")"; | ||||
| 								? codeCode | ||||
| 								: "typeof (" + codeCode + ")"; | ||||
| 							const res = parser.evaluate(typeofCode); | ||||
| 							if (!res.isString()) return; | ||||
| 							return toConstantDependency( | ||||
|  | @ -288,22 +469,32 @@ class DefinePlugin { | |||
| 					 * @returns {void} | ||||
| 					 */ | ||||
| 					const applyObjectDefine = (key, obj) => { | ||||
| 						parser.hooks.canRename.for(key).tap("DefinePlugin", approve); | ||||
| 						parser.hooks.canRename.for(key).tap("DefinePlugin", () => { | ||||
| 							addValueDependency(key); | ||||
| 							return true; | ||||
| 						}); | ||||
| 						parser.hooks.evaluateIdentifier | ||||
| 							.for(key) | ||||
| 							.tap("DefinePlugin", expr => | ||||
| 								new BasicEvaluatedExpression() | ||||
| 							.tap("DefinePlugin", expr => { | ||||
| 								addValueDependency(key); | ||||
| 								return new BasicEvaluatedExpression() | ||||
| 									.setTruthy() | ||||
| 									.setSideEffects(false) | ||||
| 									.setRange(expr.range) | ||||
| 							); | ||||
| 									.setRange(expr.range); | ||||
| 							}); | ||||
| 						parser.hooks.evaluateTypeof | ||||
| 							.for(key) | ||||
| 							.tap("DefinePlugin", evaluateToString("object")); | ||||
| 							.tap( | ||||
| 								"DefinePlugin", | ||||
| 								withValueDependency(key, evaluateToString("object")) | ||||
| 							); | ||||
| 						parser.hooks.expression.for(key).tap("DefinePlugin", expr => { | ||||
| 							addValueDependency(key); | ||||
| 							const strCode = stringifyObj( | ||||
| 								obj, | ||||
| 								parser, | ||||
| 								compilation.valueCacheVersions, | ||||
| 								key, | ||||
| 								runtimeTemplate, | ||||
| 								!parser.isAsiPosition(expr.range[0]) | ||||
| 							); | ||||
|  | @ -324,7 +515,10 @@ class DefinePlugin { | |||
| 							.for(key) | ||||
| 							.tap( | ||||
| 								"DefinePlugin", | ||||
| 								toConstantDependency(parser, JSON.stringify("object")) | ||||
| 								withValueDependency( | ||||
| 									key, | ||||
| 									toConstantDependency(parser, JSON.stringify("object")) | ||||
| 								) | ||||
| 							); | ||||
| 					}; | ||||
| 
 | ||||
|  | @ -340,6 +534,41 @@ class DefinePlugin { | |||
| 				normalModuleFactory.hooks.parser | ||||
| 					.for("javascript/esm") | ||||
| 					.tap("DefinePlugin", handler); | ||||
| 
 | ||||
| 				/** | ||||
| 				 * Walk definitions | ||||
| 				 * @param {Object} definitions Definitions map | ||||
| 				 * @param {string} prefix Prefix string | ||||
| 				 * @returns {void} | ||||
| 				 */ | ||||
| 				const walkDefinitionsForValues = (definitions, prefix) => { | ||||
| 					Object.keys(definitions).forEach(key => { | ||||
| 						const code = definitions[key]; | ||||
| 						const version = toCacheVersion(code); | ||||
| 						const name = VALUE_DEP_PREFIX + prefix + key; | ||||
| 						const oldVersion = compilation.valueCacheVersions.get(name); | ||||
| 						if (oldVersion === undefined) { | ||||
| 							compilation.valueCacheVersions.set(name, version); | ||||
| 						} else if (oldVersion !== version) { | ||||
| 							const warning = new WebpackError( | ||||
| 								`DefinePlugin\nConflicting values for '${prefix + key}'` | ||||
| 							); | ||||
| 							warning.details = `'${oldVersion}' !== '${version}'`; | ||||
| 							warning.hideStack = true; | ||||
| 							compilation.warnings.push(warning); | ||||
| 						} | ||||
| 						if ( | ||||
| 							code && | ||||
| 							typeof code === "object" && | ||||
| 							!(code instanceof RuntimeValue) && | ||||
| 							!(code instanceof RegExp) | ||||
| 						) { | ||||
| 							walkDefinitionsForValues(code, prefix + key + "."); | ||||
| 						} | ||||
| 					}); | ||||
| 				}; | ||||
| 
 | ||||
| 				walkDefinitionsForValues(definitions, ""); | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
|  |  | |||
|  | @ -94,6 +94,7 @@ const makeSerializable = require("./util/makeSerializable"); | |||
| /** | ||||
|  * @typedef {Object} NeedBuildContext | ||||
|  * @property {FileSystemInfo} fileSystemInfo | ||||
|  * @property {Map<string, string>} valueCacheVersions | ||||
|  */ | ||||
| 
 | ||||
| /** @typedef {KnownBuildMeta & Record<string, any>} BuildMeta */ | ||||
|  |  | |||
|  | @ -832,6 +832,7 @@ class NormalModule extends Module { | |||
| 			contextDependencies: undefined, | ||||
| 			missingDependencies: undefined, | ||||
| 			buildDependencies: undefined, | ||||
| 			valueDependencies: undefined, | ||||
| 			hash: undefined, | ||||
| 			assets: undefined, | ||||
| 			assetsInfo: undefined | ||||
|  | @ -1102,7 +1103,7 @@ class NormalModule extends Module { | |||
| 	 * @param {function(WebpackError=, boolean=): void} callback callback function, returns true, if the module needs a rebuild | ||||
| 	 * @returns {void} | ||||
| 	 */ | ||||
| 	needBuild({ fileSystemInfo }, callback) { | ||||
| 	needBuild({ fileSystemInfo, valueCacheVersions }, callback) { | ||||
| 		// build if enforced
 | ||||
| 		if (this._forceBuild) return callback(null, true); | ||||
| 
 | ||||
|  | @ -1115,6 +1116,16 @@ class NormalModule extends Module { | |||
| 		// build when there is no snapshot to check
 | ||||
| 		if (!this.buildInfo.snapshot) return callback(null, true); | ||||
| 
 | ||||
| 		// build when valueDependencies have changed
 | ||||
| 		if (this.buildInfo.valueDependencies) { | ||||
| 			if (!valueCacheVersions) return callback(null, true); | ||||
| 			for (const [key, value] of this.buildInfo.valueDependencies) { | ||||
| 				if (value === undefined) return callback(null, true); | ||||
| 				const current = valueCacheVersions.get(key); | ||||
| 				if (value !== current) return callback(null, true); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// check snapshot for validity
 | ||||
| 		fileSystemInfo.checkSnapshotValid(this.buildInfo.snapshot, (err, valid) => { | ||||
| 			callback(err, !valid); | ||||
|  |  | |||
|  | @ -122,6 +122,9 @@ describe("BuildDependencies", () => { | |||
| 		await exec("5"); | ||||
| 		const now3 = Date.now(); | ||||
| 		await exec("6"); | ||||
| 		await exec("7", { | ||||
| 			definedValue: "other" | ||||
| 		}); | ||||
| 		let now4, now5; | ||||
| 		if (supportsEsm) { | ||||
| 			fs.writeFileSync( | ||||
|  | @ -129,21 +132,27 @@ describe("BuildDependencies", () => { | |||
| 				"module.exports = Date.now();" | ||||
| 			); | ||||
| 			now4 = Date.now(); | ||||
| 			await exec("7"); | ||||
| 			await exec("8", { | ||||
| 				definedValue: "other" | ||||
| 			}); | ||||
| 			fs.writeFileSync( | ||||
| 				path.resolve(inputDirectory, "esm-async-dependency.mjs"), | ||||
| 				"export default Date.now();" | ||||
| 			); | ||||
| 			now5 = Date.now(); | ||||
| 			await exec("8"); | ||||
| 
 | ||||
| 			await exec("9", { | ||||
| 				definedValue: "other" | ||||
| 			}); | ||||
| 		} | ||||
| 		const results = Array.from({ length: supportsEsm ? 9 : 7 }).map((_, i) => | ||||
| 		const results = Array.from({ length: supportsEsm ? 10 : 8 }).map((_, i) => | ||||
| 			require(`./js/buildDeps/${i}/main.js`) | ||||
| 		); | ||||
| 		for (const r of results) { | ||||
| 			expect(typeof r.loader).toBe("number"); | ||||
| 			expect(typeof r.config).toBe("number"); | ||||
| 			expect(typeof r.uncached).toBe("number"); | ||||
| 			expect(typeof r.definedValue).toBe("string"); | ||||
| 		} | ||||
| 		let result = results.shift(); | ||||
| 		expect(result.loader).toBe(0); | ||||
|  | @ -163,14 +172,16 @@ describe("BuildDependencies", () => { | |||
| 		expect(result.esmConfig).toBe(1); | ||||
| 		expect(result.uncached).toBe(1); | ||||
| 		// 2 -> 3 should stay cached
 | ||||
| 		let prevResult = result; | ||||
| 		result = results.shift(); | ||||
| 		expect(result.loader).toBe(result.loader); | ||||
| 		expect(result.loader).toBe(prevResult.loader); | ||||
| 		expect(result.config).toBe(1); | ||||
| 		expect(result.esmConfig).toBe(1); | ||||
| 		expect(result.uncached).toBe(1); | ||||
| 		// 3 -> 4 should stay cached
 | ||||
| 		prevResult = result; | ||||
| 		result = results.shift(); | ||||
| 		expect(result.loader).toBe(result.loader); | ||||
| 		expect(result.loader).toBe(prevResult.loader); | ||||
| 		expect(result.config).toBe(1); | ||||
| 		expect(result.esmConfig).toBe(1); | ||||
| 		expect(result.uncached).toBe(1); | ||||
|  | @ -181,18 +192,25 @@ describe("BuildDependencies", () => { | |||
| 		expect(result.esmConfig).toBe(1); | ||||
| 		expect(result.uncached).toBe(result.config); | ||||
| 		// 5 -> 6 should stay cached, but uncacheable module still rebuilds
 | ||||
| 		prevResult = result; | ||||
| 		result = results.shift(); | ||||
| 		expect(result.loader).toBe(result.loader); | ||||
| 		expect(result.config).toBe(result.config); | ||||
| 		expect(result.loader).toBe(prevResult.loader); | ||||
| 		expect(result.config).toBe(prevResult.config); | ||||
| 		expect(result.uncached).toBeGreaterThan(now3); | ||||
| 		// 6 -> 7 should stay cached, except the updated defined value
 | ||||
| 		prevResult = result; | ||||
| 		result = results.shift(); | ||||
| 		expect(result.loader).toBe(prevResult.loader); | ||||
| 		expect(result.config).toBe(prevResult.config); | ||||
| 		expect(result.definedValue).toBe("other"); | ||||
| 		if (supportsEsm) { | ||||
| 			// 6 -> 7 should be invalidated
 | ||||
| 			// 7 -> 8 should be invalidated
 | ||||
| 			result = results.shift(); | ||||
| 			expect(result.loader).toBeGreaterThan(now4); | ||||
| 			expect(result.config).toBeGreaterThan(now4); | ||||
| 			expect(result.esmConfig).toBeGreaterThan(now4); | ||||
| 			expect(result.uncached).toBeGreaterThan(now4); | ||||
| 			// 7 -> 8 should be invalidated
 | ||||
| 			// 8 -> 9 should be invalidated
 | ||||
| 			result = results.shift(); | ||||
| 			expect(result.loader).toBeGreaterThan(now5); | ||||
| 			expect(result.config).toBeGreaterThan(now5); | ||||
|  |  | |||
|  | @ -0,0 +1 @@ | |||
| module.exports = DEFINED_VALUE; | ||||
|  | @ -5,5 +5,6 @@ module.exports = { | |||
| 	config: VALUE, | ||||
| 	esmConfig: VALUE2, | ||||
| 	esmAsyncConfig: VALUE3, | ||||
| 	uncached: require("./module") | ||||
| 	uncached: require("./module"), | ||||
| 	definedValue: require("./definedValue") | ||||
| }; | ||||
|  |  | |||
|  | @ -29,13 +29,23 @@ function run({ default: value2, asyncDep: value3 }) { | |||
| 			}, | ||||
| 			plugins: [ | ||||
| 				new webpack.DefinePlugin({ | ||||
| 					VALUE: JSON.stringify(value), | ||||
| 					VALUE2: JSON.stringify(value2), | ||||
| 					VALUE3: JSON.stringify(value3), | ||||
| 					VALUE: webpack.DefinePlugin.runtimeValue( | ||||
| 						() => JSON.stringify(value), | ||||
| 						{ version: "no" } | ||||
| 					), | ||||
| 					VALUE2: webpack.DefinePlugin.runtimeValue( | ||||
| 						() => JSON.stringify(value2), | ||||
| 						{ version: "no" } | ||||
| 					), | ||||
| 					VALUE3: webpack.DefinePlugin.runtimeValue( | ||||
| 						() => JSON.stringify(value3), | ||||
| 						{ version: "no" } | ||||
| 					), | ||||
| 					VALUE_UNCACHEABLE: webpack.DefinePlugin.runtimeValue( | ||||
| 						() => JSON.stringify(value), | ||||
| 						true | ||||
| 					) | ||||
| 					), | ||||
| 					DEFINED_VALUE: JSON.stringify(options.definedValue || "value") | ||||
| 				}) | ||||
| 			], | ||||
| 			infrastructureLogging: { | ||||
|  | @ -75,6 +85,7 @@ function run({ default: value2, asyncDep: value3 }) { | |||
| 					console.log("OK"); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				console.log(stats.toString()); | ||||
| 				process.exitCode = 0; | ||||
| 				console.log("OK"); | ||||
| 			} | ||||
|  |  | |||
|  | @ -1,23 +1,52 @@ | |||
| it("should be able to use dynamic defines in watch mode", function() { | ||||
| it("should be able to use dynamic defines in watch mode", function () { | ||||
| 	const module = require("./module"); | ||||
| 	expect(module).toEqual(nsObj({ | ||||
| 		default: WATCH_STEP, | ||||
| 		type: "string" | ||||
| 	})); | ||||
| 	expect(module).toEqual( | ||||
| 		nsObj({ | ||||
| 			default: WATCH_STEP, | ||||
| 			type: "string" | ||||
| 		}) | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| it("should not update a define when dependencies list is missing", function() { | ||||
| it("should not update a define when dependencies list is missing", function () { | ||||
| 	const module2 = require("./module2"); | ||||
| 	expect(module2).toEqual(nsObj({ | ||||
| 		default: "0", | ||||
| 		type: "string" | ||||
| 	})); | ||||
| 	expect(module2).toEqual( | ||||
| 		nsObj({ | ||||
| 			default: "0", | ||||
| 			type: "string" | ||||
| 		}) | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| it("should update always when fileDependencies is true", function() { | ||||
| it("should update always when fileDependencies is true", function () { | ||||
| 	const module3 = require("./module3"); | ||||
| 	expect(module3).toEqual(nsObj({ | ||||
| 		default: WATCH_STEP, | ||||
| 		type: "string" | ||||
| 	})); | ||||
| 	expect(module3).toEqual( | ||||
| 		nsObj({ | ||||
| 			default: WATCH_STEP, | ||||
| 			type: "string" | ||||
| 		}) | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| it("should allow to use an options object with fileDependencies", function () { | ||||
| 	const module4 = require("./module4"); | ||||
| 	expect(module4).toEqual( | ||||
| 		nsObj({ | ||||
| 			default: WATCH_STEP, | ||||
| 			type: "string" | ||||
| 		}) | ||||
| 	); | ||||
| }); | ||||
| 
 | ||||
| it("should allow to use an options object with dynamic version", function () { | ||||
| 	const module5 = require("./module5"); | ||||
| 	expect(module5).toEqual( | ||||
| 		nsObj({ | ||||
| 			default: { | ||||
| 				version: WATCH_STEP, | ||||
| 				key: "TEST_VALUE5" | ||||
| 			}, | ||||
| 			type: "object" | ||||
| 		}) | ||||
| 	); | ||||
| }); | ||||
|  |  | |||
|  | @ -0,0 +1,2 @@ | |||
| export default TEST_VALUE4; | ||||
| export const type = typeof TEST_VALUE4; | ||||
|  | @ -0,0 +1,2 @@ | |||
| export default TEST_VALUE5; | ||||
| export const type = typeof TEST_VALUE5; | ||||
|  | @ -17,7 +17,23 @@ module.exports = { | |||
| 			}, []), | ||||
| 			TEST_VALUE3: webpack.DefinePlugin.runtimeValue(() => { | ||||
| 				return JSON.stringify(fs.readFileSync(valueFile, "utf-8").trim()); | ||||
| 			}, true) | ||||
| 			}, true), | ||||
| 			TEST_VALUE4: webpack.DefinePlugin.runtimeValue( | ||||
| 				() => { | ||||
| 					return JSON.stringify(fs.readFileSync(valueFile, "utf-8").trim()); | ||||
| 				}, | ||||
| 				{ | ||||
| 					fileDependencies: [valueFile] | ||||
| 				} | ||||
| 			), | ||||
| 			TEST_VALUE5: webpack.DefinePlugin.runtimeValue( | ||||
| 				({ version, key }) => { | ||||
| 					return JSON.stringify({ version, key }); | ||||
| 				}, | ||||
| 				{ | ||||
| 					version: () => fs.readFileSync(valueFile, "utf-8").trim() | ||||
| 				} | ||||
| 			) | ||||
| 		}) | ||||
| 	] | ||||
| }; | ||||
|  |  | |||
|  | @ -1193,6 +1193,15 @@ type CodeValue = | |||
| 			| RegExp | ||||
| 			| RuntimeValue | ||||
| 	  >[]; | ||||
| type CodeValuePrimitive = | ||||
| 	| undefined | ||||
| 	| null | ||||
| 	| string | ||||
| 	| number | ||||
| 	| bigint | ||||
| 	| boolean | ||||
| 	| Function | ||||
| 	| RegExp; | ||||
| declare interface Comparator<T> { | ||||
| 	(arg0: T, arg1: T): 0 | 1 | -1; | ||||
| } | ||||
|  | @ -1338,6 +1347,7 @@ declare class Compilation { | |||
| 	resolverFactory: ResolverFactory; | ||||
| 	inputFileSystem: InputFileSystem; | ||||
| 	fileSystemInfo: FileSystemInfo; | ||||
| 	valueCacheVersions: Map<string, string>; | ||||
| 	requestShortener: RequestShortener; | ||||
| 	compilerPath: string; | ||||
| 	logger: WebpackLogger; | ||||
|  | @ -2311,7 +2321,14 @@ declare class DefinePlugin { | |||
| 	 * Apply the plugin | ||||
| 	 */ | ||||
| 	apply(compiler: Compiler): void; | ||||
| 	static runtimeValue(fn?: any, fileDependencies?: any): RuntimeValue; | ||||
| 	static runtimeValue( | ||||
| 		fn: (arg0: { | ||||
| 			module: NormalModule; | ||||
| 			key: string; | ||||
| 			readonly version?: string; | ||||
| 		}) => CodeValuePrimitive, | ||||
| 		options?: true | string[] | RuntimeValueOptions | ||||
| 	): RuntimeValue; | ||||
| } | ||||
| declare class DelegatedPlugin { | ||||
| 	constructor(options?: any); | ||||
|  | @ -6399,6 +6416,7 @@ declare class NaturalModuleIdsPlugin { | |||
| } | ||||
| declare interface NeedBuildContext { | ||||
| 	fileSystemInfo: FileSystemInfo; | ||||
| 	valueCacheVersions: Map<string, string>; | ||||
| } | ||||
| declare class NoEmitOnErrorsPlugin { | ||||
| 	constructor(); | ||||
|  | @ -9569,9 +9587,26 @@ declare abstract class RuntimeTemplate { | |||
| 	}): string; | ||||
| } | ||||
| declare abstract class RuntimeValue { | ||||
| 	fn: any; | ||||
| 	fileDependencies: any; | ||||
| 	exec(parser?: any): any; | ||||
| 	fn: (arg0: { | ||||
| 		module: NormalModule; | ||||
| 		key: string; | ||||
| 		readonly version?: string; | ||||
| 	}) => CodeValuePrimitive; | ||||
| 	options: true | RuntimeValueOptions; | ||||
| 	readonly fileDependencies?: true | string[]; | ||||
| 	exec( | ||||
| 		parser: JavascriptParser, | ||||
| 		valueCacheVersions: Map<string, string>, | ||||
| 		key: string | ||||
| 	): CodeValuePrimitive; | ||||
| 	getCacheVersion(): undefined | string; | ||||
| } | ||||
| declare interface RuntimeValueOptions { | ||||
| 	fileDependencies?: string[]; | ||||
| 	contextDependencies?: string[]; | ||||
| 	missingDependencies?: string[]; | ||||
| 	buildDependencies?: string[]; | ||||
| 	version?: string | (() => string); | ||||
| } | ||||
| type Schema = | ||||
| 	| (JSONSchema4 & Extend) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue