diff --git a/lib/config/defaults.js b/lib/config/defaults.js index aeeece583..e50e5d0e7 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -1138,10 +1138,12 @@ const applyOutputDefaults = ( break; } if ( - tp.require === null || - tp.nodeBuiltins === null || - tp.document === null || - tp.importScripts === null + (tp.require === null || + tp.nodeBuiltins === null || + tp.document === null || + tp.importScripts === null) && + output.module && + environment.dynamicImport ) { return "universal"; } @@ -1163,9 +1165,11 @@ const applyOutputDefaults = ( break; } if ( - tp.require === null || - tp.nodeBuiltins === null || - tp.importScriptsInWorker === null + (tp.require === null || + tp.nodeBuiltins === null || + tp.importScriptsInWorker === null) && + output.module && + environment.dynamicImport ) { return "universal"; } diff --git a/lib/javascript/EnableChunkLoadingPlugin.js b/lib/javascript/EnableChunkLoadingPlugin.js index 014c44e02..4e7263a53 100644 --- a/lib/javascript/EnableChunkLoadingPlugin.js +++ b/lib/javascript/EnableChunkLoadingPlugin.js @@ -101,14 +101,12 @@ class EnableChunkLoadingPlugin { }).apply(compiler); break; } - case "import": { + case "import": + case "universal": { const ModuleChunkLoadingPlugin = require("../esm/ModuleChunkLoadingPlugin"); new ModuleChunkLoadingPlugin().apply(compiler); break; } - case "universal": - // TODO implement universal chunk loading - throw new Error("Universal Chunk Loading is not implemented yet"); default: throw new Error(`Unsupported chunk loading type ${type}. Plugins which provide custom chunk loading types must call EnableChunkLoadingPlugin.setEnabled(compiler, type) to disable this error.`); diff --git a/test/configCases/target/universal/file.png b/test/configCases/target/universal/file.png new file mode 100644 index 000000000..fb53b9ded Binary files /dev/null and b/test/configCases/target/universal/file.png differ diff --git a/test/configCases/target/universal/index.js b/test/configCases/target/universal/index.js new file mode 100644 index 000000000..0d68af7df --- /dev/null +++ b/test/configCases/target/universal/index.js @@ -0,0 +1,22 @@ +import value from "./separate"; +import { test as t } from "external-self"; + +it("should compile", () => { + expect(value).toBe(42); +}); + +it("should circular depend on itself external", () => { + expect(test()).toBe(42); + expect(t()).toBe(42); +}); + +it("work with URL", () => { + const url = new URL("./file.png", import.meta.url); + expect(/[a-f0-9]{20}\.png/.test(url)).toBe(true); +}); + +function test() { + return 42; +} + +export { test }; diff --git a/test/configCases/target/universal/separate.js b/test/configCases/target/universal/separate.js new file mode 100644 index 000000000..7a4e8a723 --- /dev/null +++ b/test/configCases/target/universal/separate.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/configCases/target/universal/test.config.js b/test/configCases/target/universal/test.config.js new file mode 100644 index 000000000..b15222e44 --- /dev/null +++ b/test/configCases/target/universal/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle: function () { + return ["./runtime.mjs", "./separate.mjs", "./main.mjs"]; + } +}; diff --git a/test/configCases/target/universal/webpack.config.js b/test/configCases/target/universal/webpack.config.js new file mode 100644 index 000000000..386112ee0 --- /dev/null +++ b/test/configCases/target/universal/webpack.config.js @@ -0,0 +1,30 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + output: { + filename: "[name].mjs", + library: { + type: "module" + } + }, + target: ["web", "node"], + experiments: { + outputModule: true + }, + optimization: { + minimize: true, + runtimeChunk: "single", + splitChunks: { + cacheGroups: { + separate: { + test: /separate/, + chunks: "all", + filename: "separate.mjs", + enforce: true + } + } + } + }, + externals: { + "external-self": "./main.mjs" + } +}; diff --git a/test/configCases/wasm/universal/index.js b/test/configCases/wasm/universal/index.js index b4dcd1014..1f57a507e 100644 --- a/test/configCases/wasm/universal/index.js +++ b/test/configCases/wasm/universal/index.js @@ -11,3 +11,17 @@ it("should allow to run a WebAssembly module (direct)", function() { expect(result).toEqual(42); }); }); + +it("should allow to run a WebAssembly module (in Worker)", async function() { + const worker = new Worker(new URL("./worker.js", import.meta.url), { + type: "module" + }); + worker.postMessage("ok"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("data: 42, thanks"); + await worker.terminate(); +}); diff --git a/test/configCases/wasm/universal/worker.js b/test/configCases/wasm/universal/worker.js index e69de29bb..18cefef96 100644 --- a/test/configCases/wasm/universal/worker.js +++ b/test/configCases/wasm/universal/worker.js @@ -0,0 +1,4 @@ +self.onmessage = async event => { + const { run } = await import("./module"); + postMessage(`data: ${run()}, thanks`); +}; diff --git a/test/configCases/worker/universal/index.js b/test/configCases/worker/universal/index.js new file mode 100644 index 000000000..d88ba1b50 --- /dev/null +++ b/test/configCases/worker/universal/index.js @@ -0,0 +1,18 @@ +it("should allow to create a WebWorker", async () => { + const worker = new Worker(new URL("./worker.js", import.meta.url), { + type: "module" + }); + worker.postMessage("ok"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("data: OK, thanks"); + await worker.terminate(); +}); + +it("should allow to share chunks", async () => { + const { upper } = await import("./module"); + expect(upper("ok")).toBe("OK"); +}); diff --git a/test/configCases/worker/universal/module.js b/test/configCases/worker/universal/module.js new file mode 100644 index 000000000..3a0b527ff --- /dev/null +++ b/test/configCases/worker/universal/module.js @@ -0,0 +1,3 @@ +export function upper(str) { + return str.toUpperCase(); +} diff --git a/test/configCases/worker/universal/test.config.js b/test/configCases/worker/universal/test.config.js new file mode 100644 index 000000000..221e5e155 --- /dev/null +++ b/test/configCases/worker/universal/test.config.js @@ -0,0 +1,10 @@ +module.exports = { + moduleScope(scope, options) { + if (options.name.includes("node")) { + delete scope.Worker; + } + }, + findBundle: function (i, options) { + return ["web-main.mjs"]; + } +}; diff --git a/test/configCases/worker/universal/test.filter.js b/test/configCases/worker/universal/test.filter.js new file mode 100644 index 000000000..f74eb03f0 --- /dev/null +++ b/test/configCases/worker/universal/test.filter.js @@ -0,0 +1,5 @@ +const supportsWorker = require("../../../helpers/supportsWorker"); + +module.exports = function (config) { + return supportsWorker(); +}; diff --git a/test/configCases/worker/universal/webpack.config.js b/test/configCases/worker/universal/webpack.config.js new file mode 100644 index 000000000..583e26deb --- /dev/null +++ b/test/configCases/worker/universal/webpack.config.js @@ -0,0 +1,13 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = [ + { + name: "web", + target: ["web", "node"], + output: { + filename: "web-[name].mjs" + }, + experiments: { + outputModule: true + } + } +]; diff --git a/test/configCases/worker/universal/worker.js b/test/configCases/worker/universal/worker.js new file mode 100644 index 000000000..4f730feb8 --- /dev/null +++ b/test/configCases/worker/universal/worker.js @@ -0,0 +1,4 @@ +self.onmessage = async event => { + const { upper } = await import("./module"); + postMessage(`data: ${upper(event.data)}, thanks`); +}; diff --git a/test/helpers/createFakeWorker.js b/test/helpers/createFakeWorker.js index a9c2172bc..a0ebc24c9 100644 --- a/test/helpers/createFakeWorker.js +++ b/test/helpers/createFakeWorker.js @@ -2,22 +2,32 @@ const path = require("path"); module.exports = ({ outputDirectory }) => class Worker { - constructor(url, options = {}) { - expect(url).toBeInstanceOf(URL); - expect(url.origin).toBe("https://test.cases"); - expect(url.pathname.startsWith("/path/")).toBe(true); - this.url = url; - const file = url.pathname.slice(6); + constructor(resource, options = {}) { + expect(resource).toBeInstanceOf(URL); + + const isFileURL = /^file:/i.test(resource); + + if (!isFileURL) { + expect(resource.origin).toBe("https://test.cases"); + expect(resource.pathname.startsWith("/path/")).toBe(true); + } + + this.url = resource; + const file = isFileURL + ? resource + : path.resolve(outputDirectory, resource.pathname.slice(6)); + const workerBootstrap = ` const { parentPort } = require("worker_threads"); -const { URL } = require("url"); +const { URL, fileURLToPath } = require("url"); const path = require("path"); const fs = require("fs"); global.self = global; self.URL = URL; -self.location = new URL(${JSON.stringify(url.toString())}); +self.location = new URL(${JSON.stringify(resource.toString())}); const urlToPath = url => { - if(url.startsWith("https://test.cases/path/")) url = url.slice(24); + if (/^file:/i.test(url)) return fileURLToPath(url); + if (url.startsWith("https://test.cases/path/")) url = url.slice(24); return path.resolve(${JSON.stringify(outputDirectory)}, \`./\${url}\`); }; self.importScripts = url => { @@ -35,8 +45,10 @@ self.fetch = async url => { ) ); return { + headers: { get(name) { } }, status: 200, ok: true, + arrayBuffer() { return buffer; }, json: async () => JSON.parse(buffer.toString("utf-8")) }; } catch(err) { @@ -49,15 +61,26 @@ self.fetch = async url => { throw err; } }; -parentPort.on("message", data => { - if(self.onmessage) self.onmessage({ - data - }); -}); + self.postMessage = data => { parentPort.postMessage(data); }; -require(${JSON.stringify(path.resolve(outputDirectory, file))}); +if (${options.type === "module"}) { + import(${JSON.stringify(file)}).then(() => { + parentPort.on("message", data => { + if(self.onmessage) self.onmessage({ + data + }); + }); + }); +} else { + parentPort.on("message", data => { + if(self.onmessage) self.onmessage({ + data + }); + }); + require(${JSON.stringify(file)}); +} `; this.worker = new (require("worker_threads").Worker)(workerBootstrap, { eval: true