diff --git a/lib/SystemMainTemplatePlugin.js b/lib/SystemMainTemplatePlugin.js index 1c3e4c8c3e1..ff519427060 100644 --- a/lib/SystemMainTemplatePlugin.js +++ b/lib/SystemMainTemplatePlugin.js @@ -31,7 +31,9 @@ class SystemMainTemplatePlugin { const { mainTemplate, chunkTemplate } = compilation; const onRenderWithEntry = (source, chunk, hash) => { - const externals = chunk.getModules().filter(m => m.external); + const externals = chunk + .getModules() + .filter(m => m.external && m.externalType === "system"); // The name this bundle should be registered as with System const name = this.name diff --git a/lib/optimize/SplitChunksPlugin.js b/lib/optimize/SplitChunksPlugin.js index e7d560bf6ce..42f23c9ed83 100644 --- a/lib/optimize/SplitChunksPlugin.js +++ b/lib/optimize/SplitChunksPlugin.js @@ -4,13 +4,13 @@ */ "use strict"; -const crypto = require("crypto"); const SortableSet = require("../util/SortableSet"); const GraphHelpers = require("../GraphHelpers"); const { isSubset } = require("../util/SetHelpers"); const deterministicGrouping = require("../util/deterministicGrouping"); const MinMaxSizeWarning = require("./MinMaxSizeWarning"); const contextify = require("../util/identifier").contextify; +const createHash = require("../util/createHash"); /** @typedef {import("../Compiler")} Compiler */ /** @typedef {import("../Chunk")} Chunk */ @@ -21,8 +21,7 @@ const contextify = require("../util/identifier").contextify; const deterministicGroupingForModules = /** @type {function(DeterministicGroupingOptionsForModule): DeterministicGroupingGroupedItemsForModule[]} */ (deterministicGrouping); const hashFilename = name => { - return crypto - .createHash("md4") + return createHash("md4") .update(name) .digest("hex") .slice(0, 8); diff --git a/lib/util/createHash.js b/lib/util/createHash.js index 64de510da5c..d9f457e407a 100644 --- a/lib/util/createHash.js +++ b/lib/util/createHash.js @@ -29,9 +29,6 @@ class Hash { } } -exports.Hash = Hash; -/** @typedef {typeof Hash} HashConstructor */ - class BulkUpdateDecorator extends Hash { /** * @param {Hash} hash hash @@ -118,6 +115,16 @@ class DebugHash extends Hash { } } +/** @type {typeof import("crypto") | undefined} */ +let crypto = undefined; +/** @type {typeof import("./hash/md4") | undefined} */ +let createMd4 = undefined; +/** @type {typeof import("./hash/BatchedHash") | undefined} */ +let BatchedHash = undefined; + +/** @type {number} */ +const NODE_MAJOR_VERSION = parseInt(process.versions.node, 10); + /** * Creates a hash by name or function * @param {string | HashConstructor} algorithm the algorithm name or a constructor creating a hash @@ -127,11 +134,41 @@ module.exports = algorithm => { if (typeof algorithm === "function") { return new BulkUpdateDecorator(new algorithm()); } + switch (algorithm) { // TODO add non-cryptographic algorithm here case "debug": return new DebugHash(); + case "md4": + if (NODE_MAJOR_VERSION >= 18) { + if (createMd4 === undefined) { + createMd4 = require("./hash/md4"); + if (BatchedHash === undefined) { + BatchedHash = require("./hash/BatchedHash"); + } + } + return new /** @type {typeof import("./hash/BatchedHash")} */ (BatchedHash)( + createMd4() + ); + } + // If we are on Node.js < 18, fall through to the default case + // eslint-disable-next-line no-fallthrough + + case "native-md4": + if (NODE_MAJOR_VERSION >= 18) { + if (crypto === undefined) crypto = require("crypto"); + return new BulkUpdateDecorator( + /** @type {typeof import("crypto")} */ (crypto).createHash("md4") + ); + } + // If we are on Node.js < 18, fall through to the default case + // eslint-disable-next-line no-fallthrough + default: - return new BulkUpdateDecorator(require("crypto").createHash(algorithm)); + if (crypto === undefined) crypto = require("crypto"); + return new BulkUpdateDecorator(crypto.createHash(algorithm)); } }; + +module.exports.Hash = Hash; +/** @typedef {typeof Hash} HashConstructor */ diff --git a/lib/util/hash/BatchedHash.js b/lib/util/hash/BatchedHash.js new file mode 100644 index 00000000000..55830ac81b6 --- /dev/null +++ b/lib/util/hash/BatchedHash.js @@ -0,0 +1,71 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +// From Webpack 5 +// https://github.com/webpack/webpack/blob/853bfda35a0080605c09e1bdeb0103bcb9367a10/lib/util/hash/BatchedHash.js + +const { Hash } = require("../createHash"); +const MAX_SHORT_STRING = require("./wasm-hash").MAX_SHORT_STRING; + +class BatchedHash extends Hash { + constructor(hash) { + super(); + this.string = undefined; + this.encoding = undefined; + this.hash = hash; + } + + /** + * Update hash {@link https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding} + * @param {string|Buffer} data data + * @param {string=} inputEncoding data encoding + * @returns {this} updated hash + */ + update(data, inputEncoding) { + if (this.string !== undefined) { + if ( + typeof data === "string" && + inputEncoding === this.encoding && + this.string.length + data.length < MAX_SHORT_STRING + ) { + this.string += data; + return this; + } + this.hash.update(this.string, this.encoding); + this.string = undefined; + } + if (typeof data === "string") { + if ( + data.length < MAX_SHORT_STRING && + // base64 encoding is not valid since it may contain padding chars + (!inputEncoding || !inputEncoding.startsWith("ba")) + ) { + this.string = data; + this.encoding = inputEncoding; + } else { + this.hash.update(data, inputEncoding); + } + } else { + this.hash.update(data); + } + return this; + } + + /** + * Calculates the digest {@link https://nodejs.org/api/crypto.html#crypto_hash_digest_encoding} + * @param {string=} encoding encoding of the return value + * @returns {string|Buffer} digest + */ + digest(encoding) { + if (this.string !== undefined) { + this.hash.update(this.string, this.encoding); + } + return this.hash.digest(encoding); + } +} + +module.exports = BatchedHash; diff --git a/lib/util/hash/md4.js b/lib/util/hash/md4.js new file mode 100644 index 00000000000..586fb93e7c9 --- /dev/null +++ b/lib/util/hash/md4.js @@ -0,0 +1,25 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +// From Webpack 5 +// https://github.com/webpack/webpack/blob/853bfda35a0080605c09e1bdeb0103bcb9367a10/lib/util/hash/md4.js + +const create = require("./wasm-hash").create; + +//#region wasm code: md4 (../../../assembly/hash/md4.asm.ts) --initialMemory 1 +// This will only get called on Node 18+ +// eslint-disable-next-line no-undef +const md4 = new WebAssembly.Module( + Buffer.from( + // 2154 bytes + "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", + "base64" + ) +); +//#endregion + +module.exports = create.bind(null, md4, [], 64, 32); diff --git a/lib/util/hash/wasm-hash.js b/lib/util/hash/wasm-hash.js new file mode 100644 index 00000000000..ef299223078 --- /dev/null +++ b/lib/util/hash/wasm-hash.js @@ -0,0 +1,174 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +// From Webpack 5 +// https://github.com/webpack/webpack/blob/853bfda35a0080605c09e1bdeb0103bcb9367a10/lib/util/hash/wasm-hash.js + +// 65536 is the size of a wasm memory page +// 64 is the maximum chunk size for every possible wasm hash implementation +// 4 is the maximum number of bytes per char for string encoding (max is utf-8) +// ~3 makes sure that it's always a block of 4 chars, so avoid partially encoded bytes for base64 +const MAX_SHORT_STRING = Math.floor((65536 - 64) / 4) & ~3; + +class WasmHash { + /** + * @param {WebAssembly.Instance} instance wasm instance + * @param {WebAssembly.Instance[]} instancesPool pool of instances + * @param {number} chunkSize size of data chunks passed to wasm + * @param {number} digestSize size of digest returned by wasm + */ + constructor(instance, instancesPool, chunkSize, digestSize) { + const exports = /** @type {any} */ (instance.exports); + exports.init(); + this.exports = exports; + this.mem = Buffer.from(exports.memory.buffer, 0, 65536); + this.buffered = 0; + this.instancesPool = instancesPool; + this.chunkSize = chunkSize; + this.digestSize = digestSize; + } + + reset() { + this.buffered = 0; + this.exports.init(); + } + + /** + * @param {Buffer | string} data data + * @param {BufferEncoding=} encoding encoding + * @returns {this} itself + */ + update(data, encoding) { + if (typeof data === "string") { + while (data.length > MAX_SHORT_STRING) { + this._updateWithShortString(data.slice(0, MAX_SHORT_STRING), encoding); + data = data.slice(MAX_SHORT_STRING); + } + this._updateWithShortString(data, encoding); + return this; + } + this._updateWithBuffer(data); + return this; + } + + /** + * @param {string} data data + * @param {BufferEncoding | 'utf-8'} encoding encoding + * @returns {void} + */ + _updateWithShortString(data, encoding) { + const { exports, buffered, mem, chunkSize } = this; + let endPos; + if (data.length < 70) { + if (!encoding || encoding === "utf-8" || encoding === "utf8") { + endPos = buffered; + for (let i = 0; i < data.length; i++) { + const cc = data.charCodeAt(i); + if (cc < 0x80) mem[endPos++] = cc; + else if (cc < 0x800) { + mem[endPos] = (cc >> 6) | 0xc0; + mem[endPos + 1] = (cc & 0x3f) | 0x80; + endPos += 2; + } else { + // bail-out for weird chars + const slicedData = data.slice(i); + endPos += mem.write( + slicedData, + endPos, + slicedData.length, + encoding + ); + break; + } + } + } else if (encoding === "latin1") { + endPos = buffered; + for (let i = 0; i < data.length; i++) { + const cc = data.charCodeAt(i); + mem[endPos++] = cc; + } + } else { + endPos = buffered + mem.write(data, buffered, data.length, encoding); + } + } else { + endPos = buffered + mem.write(data, buffered, data.length, encoding); + } + if (endPos < chunkSize) { + this.buffered = endPos; + } else { + const l = endPos & ~(this.chunkSize - 1); + exports.update(l); + const newBuffered = endPos - l; + this.buffered = newBuffered; + if (newBuffered > 0) mem.copyWithin(0, l, endPos); + } + } + + /** + * @param {Buffer} data data + * @returns {void} + */ + _updateWithBuffer(data) { + const { exports, buffered, mem } = this; + const length = data.length; + if (buffered + length < this.chunkSize) { + data.copy(mem, buffered, 0, length); + this.buffered += length; + } else { + const l = (buffered + length) & ~(this.chunkSize - 1); + if (l > 65536) { + let i = 65536 - buffered; + data.copy(mem, buffered, 0, i); + exports.update(65536); + const stop = l - buffered - 65536; + while (i < stop) { + data.copy(mem, 0, i, i + 65536); + exports.update(65536); + i += 65536; + } + data.copy(mem, 0, i, l - buffered); + exports.update(l - buffered - i); + } else { + data.copy(mem, buffered, 0, l - buffered); + exports.update(l); + } + const newBuffered = length + buffered - l; + this.buffered = newBuffered; + if (newBuffered > 0) data.copy(mem, 0, length - newBuffered, length); + } + } + + digest(type) { + const { exports, buffered, mem, digestSize } = this; + exports.final(buffered); + this.instancesPool.push(this); + const hex = mem.toString("latin1", 0, digestSize); + if (type === "hex") return hex; + if (type === "binary" || !type) return Buffer.from(hex, "hex"); + return Buffer.from(hex, "hex").toString(type); + } +} + +const create = (wasmModule, instancesPool, chunkSize, digestSize) => { + if (instancesPool.length > 0) { + const old = instancesPool.pop(); + old.reset(); + return old; + } else { + return new WasmHash( + // This will only get called on Node 18+ + // eslint-disable-next-line no-undef + new WebAssembly.Instance(wasmModule), + instancesPool, + chunkSize, + digestSize + ); + } +}; + +module.exports.create = create; +module.exports.MAX_SHORT_STRING = MAX_SHORT_STRING; diff --git a/package.json b/package.json index dd30c9e1a0f..cfecabff9e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webpack", - "version": "4.46.0", + "version": "4.47.0", "author": "Tobias Koppers @sokra", "description": "Packs CommonJs/AMD modules for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand. Support loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.", "license": "MIT", diff --git a/test/configCases/externals/externals-system-custom/index.js b/test/configCases/externals/externals-system-custom/index.js new file mode 100644 index 00000000000..3be859684cf --- /dev/null +++ b/test/configCases/externals/externals-system-custom/index.js @@ -0,0 +1,8 @@ +/* This test verifies that webpack externals that have different to System type are properly + * accessible within System.js bundle. + */ +it("should correctly handle externals of different type", function() { + expect(require("rootExt")).toEqual("works"); + expect(require("varExt")).toEqual("works"); + expect(require("windowExt")).toEqual("works"); +}); diff --git a/test/configCases/externals/externals-system-custom/test.config.js b/test/configCases/externals/externals-system-custom/test.config.js new file mode 100644 index 00000000000..e8f50d9c4ff --- /dev/null +++ b/test/configCases/externals/externals-system-custom/test.config.js @@ -0,0 +1,16 @@ +const System = require("../../../helpers/fakeSystem"); + +module.exports = { + beforeExecute: () => { + System.init(); + }, + moduleScope(scope) { + scope.window.windowExt = "works"; + scope.rootExt = "works"; + scope.varExt = "works"; + scope.System = System; + }, + afterExecute: () => { + System.execute("(anonym)"); + } +}; diff --git a/test/configCases/externals/externals-system-custom/webpack.config.js b/test/configCases/externals/externals-system-custom/webpack.config.js new file mode 100644 index 00000000000..5147ea7d571 --- /dev/null +++ b/test/configCases/externals/externals-system-custom/webpack.config.js @@ -0,0 +1,11 @@ +module.exports = { + output: { + libraryTarget: "system" + }, + target: "web", + externals: { + rootExt: "root rootExt", + varExt: "var varExt", + windowExt: "window windowExt" + } +};