diff --git a/.changeset/four-planes-attend.md b/.changeset/four-planes-attend.md new file mode 100644 index 0000000000..f1f6013d2c --- /dev/null +++ b/.changeset/four-planes-attend.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Fix source phase imports in bundled and non-bundled Workers + +Wrangler now preserves `import source` syntax when it runs esbuild, including module format detection and bundled deploy output. This fixes both `--no-bundle` and bundled deployments for Workers that import WebAssembly using source phase imports. diff --git a/packages/wrangler/src/__tests__/deploy/build.test.ts b/packages/wrangler/src/__tests__/deploy/build.test.ts index e268fc9504..491176e0cb 100644 --- a/packages/wrangler/src/__tests__/deploy/build.test.ts +++ b/packages/wrangler/src/__tests__/deploy/build.test.ts @@ -1153,6 +1153,18 @@ describe("deploy", () => { await runWrangler("deploy index.js --dry-run --outdir dist"); expect(fs.readFileSync("dist/index.js", "utf-8")).toMatch(scriptContent); }); + + it("should preserve source phase imports without error", async ({ + expect, + }) => { + writeWranglerConfig(); + const scriptContent = `import source mod from './mod.wasm'; +export default { fetch() { return new Response(mod); } };`; + fs.writeFileSync("index.js", scriptContent); + fs.writeFileSync("mod.wasm", ""); + await runWrangler("deploy index.js --no-bundle --dry-run --outdir dist"); + expect(fs.readFileSync("dist/index.js", "utf-8")).toMatch(scriptContent); + }); }); describe("--no-bundle --minify", () => { it("should warn that no-bundle and minify can't be used together", async ({ diff --git a/packages/wrangler/src/__tests__/deploy/formats.test.ts b/packages/wrangler/src/__tests__/deploy/formats.test.ts index c316c6e95f..1779c29902 100644 --- a/packages/wrangler/src/__tests__/deploy/formats.test.ts +++ b/packages/wrangler/src/__tests__/deploy/formats.test.ts @@ -931,6 +931,52 @@ export default{ } `); }); + + it("should copy source phase wasm imports to --outdir if specified", async ({ + expect, + }) => { + writeWranglerConfig(); + fs.writeFileSync( + "./index.js", + ` +import txt from './textfile.txt'; +import source hello from './hello.wasm'; +export default { + async fetch() { + return new Response(txt + hello); + } +} +` + ); + fs.writeFileSync("./textfile.txt", "Hello, World!"); + fs.writeFileSync("./hello.wasm", "Hello wasm World!"); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedModules: { + "./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt": + "Hello, World!", + "./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm": + "Hello wasm World!", + }, + }); + await runWrangler("deploy index.js --outdir some-dir"); + + expect(fs.existsSync("some-dir/index.js")).toBe(true); + expect(fs.existsSync("some-dir/index.js.map")).toBe(true); + expect(fs.readFileSync("some-dir/index.js", "utf8")).toContain( + 'import source hello from "./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm";' + ); + expect( + fs.existsSync( + "some-dir/0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt" + ) + ).toBe(true); + expect( + fs.existsSync( + "some-dir/d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm" + ) + ).toBe(true); + }); }); describe("--outfile", () => { it("should generate worker bundle at --outfile if specified", async ({ @@ -1056,6 +1102,56 @@ export default{ `); }); + it("should include source phase wasm imports in the worker bundle", async ({ + expect, + }) => { + writeWranglerConfig(); + fs.writeFileSync( + "./index.js", + ` +import txt from './textfile.txt'; +import source hello from './hello.wasm'; +export default { + async fetch() { + return new Response(txt + hello); + } +} +` + ); + fs.writeFileSync("./textfile.txt", "Hello, World!"); + fs.writeFileSync("./hello.wasm", "Hello wasm World!"); + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedModules: { + "./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt": + "Hello, World!", + "./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm": + "Hello wasm World!", + }, + }); + await runWrangler("deploy index.js --outfile some-dir/worker.bundle"); + + expect(fs.existsSync("some-dir/worker.bundle")).toBe(true); + expect( + fs + .readFileSync("some-dir/worker.bundle", "utf8") + .replace( + /------formdata-undici-0.[0-9]*/g, + "------formdata-undici-0.test" + ) + .replace(/wrangler_(.+?)_default/g, "wrangler_default") + ).toContain( + dedent` + // index.js + import txt from "./0a0a9f2a6772942557ab5355d76af442f8f65e01-textfile.txt"; + import source hello from "./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm"; + ` + ); + expect(fs.readFileSync("some-dir/worker.bundle", "utf8")).toContain( + 'Content-Disposition: form-data; name="./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm"; filename="./d025a03cd31e98e96fb5bd5bce87f9bca4e8ce2c-hello.wasm"' + ); + }); + it("should include bindings in the worker bundle", async ({ expect }) => { writeWranglerConfig({ kv_namespaces: [{ binding: "KV", id: "kv-namespace-id" }], diff --git a/packages/wrangler/src/__tests__/guess-worker-format.test.ts b/packages/wrangler/src/__tests__/guess-worker-format.test.ts index 651d1baf82..fd628e9425 100644 --- a/packages/wrangler/src/__tests__/guess-worker-format.test.ts +++ b/packages/wrangler/src/__tests__/guess-worker-format.test.ts @@ -113,4 +113,21 @@ describe("guess worker format", () => { ); expect(guess.exports).toStrictEqual(["Hello", "default"]); }); + + it("should detect a modules worker that uses source phase imports", async ({ + expect, + }) => { + await writeFile("./mod.wasm", ""); + await writeFile( + "./index.js", + `import source mod from './mod.wasm'; +export default { fetch() { return new Response(mod); } };` + ); + const guess = await guessWorkerFormat( + path.join(process.cwd(), "./index.js"), + process.cwd(), + undefined + ); + expect(guess.format).toBe("modules"); + }); }); diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 943f7bcdf2..67964780d9 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -46,6 +46,7 @@ export const COMMON_ESBUILD_OPTIONS = { // v8 supports es2024 features as of 11.9 // workerd uses [v8 version 14.2 as of 2025-10-17](https://developers.cloudflare.com/workers/platform/changelog/#2025-10-17) target: "es2024", + supported: { "import-source": true }, loader: { ".js": "jsx", ".mjs": "jsx", ".cjs": "jsx" }, } as const; @@ -384,6 +385,7 @@ export async function bundleWorker( : undefined, format: entry.format === "modules" ? "esm" : "iife", target: COMMON_ESBUILD_OPTIONS.target, + supported: COMMON_ESBUILD_OPTIONS.supported, sourcemap: sourcemap ?? true, // Include a reference to the output folder in the sourcemap. // This is omitted by default, but we need it to properly resolve source paths in error output.