From bfb2d1ec06ffcd110dca7d7121b52c7b4c764403 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 16 Mar 2026 23:18:17 +0000 Subject: [PATCH 1/6] Improve error message when modules cannot be resolved during bundling --- .changeset/improve-unresolved-module-error.md | 23 +++++ .../startDevWorker/BundleController.test.ts | 84 ++++++++++++++++++- .../src/__tests__/deploy/build.test.ts | 56 +++++++++++++ .../src/deployment-bundle/build-failures.ts | 43 ++++++++++ .../wrangler/src/deployment-bundle/bundle.ts | 2 + 5 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 .changeset/improve-unresolved-module-error.md diff --git a/.changeset/improve-unresolved-module-error.md b/.changeset/improve-unresolved-module-error.md new file mode 100644 index 0000000000..39fea1accf --- /dev/null +++ b/.changeset/improve-unresolved-module-error.md @@ -0,0 +1,23 @@ +--- +"wrangler": patch +--- + +Improve error message when modules cannot be resolved during bundling + +When a module cannot be resolved during bundling, Wrangler now suggests using the `alias` configuration option to substitute it with an alternative implementation. This replaces esbuild's default suggestion to "mark the path as external", which is not a supported option in Wrangler. + +For example, if you try to import a module that doesn't exist: + +```js +import foo from "some-missing-module"; +``` + +Wrangler will now suggest: + +``` +To fix this, you can add an entry to "alias" in your Wrangler configuration +to substitute "some-missing-module" with an alternative implementation. +See https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing +``` + +This provides actionable guidance for resolving import errors. diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts index 2eef5eab07..015b9331bf 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts @@ -1,5 +1,6 @@ import path from "node:path"; -import { seed } from "@cloudflare/workers-utils/test-helpers"; +import { normalizeString, seed } from "@cloudflare/workers-utils/test-helpers"; +import * as esbuild from "esbuild"; import dedent from "ts-dedent"; // eslint-disable-next-line no-restricted-imports import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; @@ -534,4 +535,85 @@ describe("BundleController", { retry: 5, timeout: 10_000 }, () => { `); }); }); + + describe("bundling error messages", () => { + test("should recommend alias when a non-Node module cannot be resolved", async () => { + await seed({ + "src/index.ts": dedent/* javascript */ ` + import foo from 'some-nonexistent-module'; + export default { + fetch(request, env, ctx) { + return new Response(foo) + } + } satisfies ExportedHandler + `, + }); + const config = configDefaults({ + entrypoint: path.resolve("src/index.ts"), + projectRoot: path.resolve("src"), + }); + const ev = bus.waitFor("error", (e) => e.source === "BundlerController"); + controller.onConfigUpdate({ type: "configUpdate", config }); + const error = await ev; + + const buildFailure = error.cause as esbuild.BuildFailure; + const formattedError = normalizeString( + esbuild + .formatMessagesSync(buildFailure.errors ?? [], { kind: "error" }) + .join() + .trim() + ); + + expect(formattedError).toMatchInlineSnapshot(` + "X [ERROR] Could not resolve "some-nonexistent-module" + + index.ts:1:16: + 1 │ import foo from 'some-nonexistent-module'; + ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~ + + To fix this, you can add an entry to "alias" in your Wrangler configuration + to substitute "some-nonexistent-module" with an alternative implementation. + See https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing" + `); + }); + + test("should NOT recommend alias for Node built-in modules", async () => { + await seed({ + "src/index.ts": dedent/* javascript */ ` + import fs from 'fs'; + export default { + fetch(request, env, ctx) { + return new Response(String(fs)) + } + } satisfies ExportedHandler + `, + }); + const config = configDefaults({ + entrypoint: path.resolve("src/index.ts"), + projectRoot: path.resolve("src"), + }); + const ev = bus.waitFor("error", (e) => e.source === "BundlerController"); + controller.onConfigUpdate({ type: "configUpdate", config }); + const error = await ev; + + const buildFailure = error.cause as esbuild.BuildFailure; + const formattedError = normalizeString( + esbuild + .formatMessagesSync(buildFailure.errors ?? [], { kind: "error" }) + .join() + .trim() + ); + + expect(formattedError).toMatchInlineSnapshot(` + "X [ERROR] Could not resolve "fs" + + index.ts:1:15: + 1 │ import fs from 'fs'; + ╵ ~~~~ + + The package "fs" wasn't found on the file system but is built into node. + - Add the "nodejs_compat" compatibility flag to your project." + `); + }); + }); }); diff --git a/packages/wrangler/src/__tests__/deploy/build.test.ts b/packages/wrangler/src/__tests__/deploy/build.test.ts index e268fc9504..bdd96dd791 100644 --- a/packages/wrangler/src/__tests__/deploy/build.test.ts +++ b/packages/wrangler/src/__tests__/deploy/build.test.ts @@ -657,6 +657,62 @@ describe("deploy", () => { `); }); }); + describe("unresolved module error messages", () => { + it("should recommend alias when a non-Node module cannot be resolved", async () => { + writeWranglerConfig(); + fs.writeFileSync( + "index.js", + `import foo from 'some-nonexistent-module'; +export default { fetch() { return new Response(foo); } }` + ); + + await expect( + runWrangler("deploy index.js --dry-run").catch((e) => + normalizeString( + esbuild + .formatMessagesSync(e?.errors ?? [], { kind: "error" }) + .join() + .trim() + ) + ) + ).resolves.toMatchInlineSnapshot(` + "X [ERROR] Could not resolve "some-nonexistent-module" + + index.js:1:16: + 1 │ import foo from 'some-nonexistent-module'; + ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~ + + To fix this, you can add an entry to "alias" in your Wrangler configuration + to substitute "some-nonexistent-module" with an alternative implementation. + See https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing" + `); + }); + + it("should NOT recommend alias for Node built-in modules", async () => { + writeWranglerConfig(); + fs.writeFileSync("index.js", "import fs from 'fs';"); + + await expect( + runWrangler("deploy index.js --dry-run").catch((e) => + normalizeString( + esbuild + .formatMessagesSync(e?.errors ?? [], { kind: "error" }) + .join() + .trim() + ) + ) + ).resolves.toMatchInlineSnapshot(` + "X [ERROR] Could not resolve "fs" + + index.js:1:15: + 1 │ import fs from 'fs'; + ╵ ~~~~ + + The package "fs" wasn't found on the file system but is built into node. + - Add the "nodejs_compat" compatibility flag to your project." + `); + }); + }); describe("`nodejs_compat` compatibility flag", () => { it('when absent, should warn on any "external" `node:*` imports', async ({ expect, diff --git a/packages/wrangler/src/deployment-bundle/build-failures.ts b/packages/wrangler/src/deployment-bundle/build-failures.ts index 83dcb41b6c..8258455107 100644 --- a/packages/wrangler/src/deployment-bundle/build-failures.ts +++ b/packages/wrangler/src/deployment-bundle/build-failures.ts @@ -42,6 +42,49 @@ export function rewriteNodeCompatBuildFailure( } } } +/** + * RegExp matching against esbuild's error text when it is unable to resolve + * a module. Used to detect when we should suggest the `alias` config. + */ +const couldNotResolveErrorText = /^Could not resolve "(.+?)"$/; + +/** + * Text that appears in esbuild's notes when it suggests marking a module as external. + */ +const markAsExternalNoteText = "as external to exclude it from the bundle"; + +/** + * Rewrites esbuild BuildFailures for failing to resolve modules to suggest + * using the `alias` config option in wrangler.json. + */ +export function rewriteUnresolvedModuleBuildFailure(errors: esbuild.Message[]) { + for (const error of errors) { + const match = couldNotResolveErrorText.exec(error.text); + // Note: we skip Node built-in modules since these are handled by rewriteNodeCompatBuildFailure + if (match !== null && !nodeBuiltinResolveErrorText.test(error.text)) { + const moduleName = match[1]; + const hasExternalSuggestion = error.notes?.some((note) => + note.text?.includes(markAsExternalNoteText) + ); + if (hasExternalSuggestion) { + // Filter out esbuild's "mark as external" suggestion since we provide our own + error.notes = [ + ...(error.notes ?? []).filter( + (note) => !note.text?.includes(markAsExternalNoteText) + ), + { + location: null, + text: + `To fix this, you can add an entry to "alias" in your Wrangler configuration\n` + + `to substitute "${moduleName}" with an alternative implementation.\n` + + `See https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing\n`, + }, + ]; + } + } + } +} + /** * Returns true if the passed value looks like an esbuild BuildFailure object */ diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 943f7bcdf2..1c62994048 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -13,6 +13,7 @@ import { applyMiddlewareLoaderFacade } from "./apply-middleware"; import { isBuildFailure, rewriteNodeCompatBuildFailure, + rewriteUnresolvedModuleBuildFailure, } from "./build-failures"; import { dedupeModulesByName } from "./dedupe-modules"; import { getEntryPointFromMetafile } from "./entry-point-from-metafile"; @@ -477,6 +478,7 @@ export async function bundleWorker( } catch (e) { if (isBuildFailure(e)) { rewriteNodeCompatBuildFailure(e.errors, nodejsCompatMode); + rewriteUnresolvedModuleBuildFailure(e.errors); } throw e; } From f40c0cef3b4b1515d6fb673de9f18614927ef191 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Tue, 17 Mar 2026 15:19:03 +0000 Subject: [PATCH 2/6] Simplify and clarify error message --- .changeset/improve-unresolved-module-error.md | 2 +- .../__tests__/api/startDevWorker/BundleController.test.ts | 5 ++--- packages/wrangler/src/__tests__/deploy/build.test.ts | 5 ++--- packages/wrangler/src/deployment-bundle/build-failures.ts | 5 ++--- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.changeset/improve-unresolved-module-error.md b/.changeset/improve-unresolved-module-error.md index 39fea1accf..15fc74e4d6 100644 --- a/.changeset/improve-unresolved-module-error.md +++ b/.changeset/improve-unresolved-module-error.md @@ -17,7 +17,7 @@ Wrangler will now suggest: ``` To fix this, you can add an entry to "alias" in your Wrangler configuration to substitute "some-missing-module" with an alternative implementation. -See https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing +See https://developers.cloudflare.com/workers/wrangler/configuration/#for-bundling-issues ``` This provides actionable guidance for resolving import errors. diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts index 015b9331bf..f900700b36 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts @@ -571,9 +571,8 @@ describe("BundleController", { retry: 5, timeout: 10_000 }, () => { 1 │ import foo from 'some-nonexistent-module'; ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~ - To fix this, you can add an entry to "alias" in your Wrangler configuration - to substitute "some-nonexistent-module" with an alternative implementation. - See https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing" + To fix this, you can add an entry to "alias" in your Wrangler configuration. + For more guidance see: https://developers.cloudflare.com/workers/wrangler/configuration/#bundling-issues" `); }); diff --git a/packages/wrangler/src/__tests__/deploy/build.test.ts b/packages/wrangler/src/__tests__/deploy/build.test.ts index bdd96dd791..6cd78636c4 100644 --- a/packages/wrangler/src/__tests__/deploy/build.test.ts +++ b/packages/wrangler/src/__tests__/deploy/build.test.ts @@ -682,9 +682,8 @@ export default { fetch() { return new Response(foo); } }` 1 │ import foo from 'some-nonexistent-module'; ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~ - To fix this, you can add an entry to "alias" in your Wrangler configuration - to substitute "some-nonexistent-module" with an alternative implementation. - See https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing" + To fix this, you can add an entry to "alias" in your Wrangler configuration. + For more guidance see: https://developers.cloudflare.com/workers/wrangler/configuration/#bundling-issues" `); }); diff --git a/packages/wrangler/src/deployment-bundle/build-failures.ts b/packages/wrangler/src/deployment-bundle/build-failures.ts index 8258455107..c9deb1a33b 100644 --- a/packages/wrangler/src/deployment-bundle/build-failures.ts +++ b/packages/wrangler/src/deployment-bundle/build-failures.ts @@ -75,9 +75,8 @@ export function rewriteUnresolvedModuleBuildFailure(errors: esbuild.Message[]) { { location: null, text: - `To fix this, you can add an entry to "alias" in your Wrangler configuration\n` + - `to substitute "${moduleName}" with an alternative implementation.\n` + - `See https://developers.cloudflare.com/workers/wrangler/configuration/#module-aliasing\n`, + `To fix this, you can add an entry to "alias" in your Wrangler configuration.\n` + + `For more guidance see: https://developers.cloudflare.com/workers/wrangler/configuration/#bundling-issues\n`, }, ]; } From f4334083cb8a42c4c6634dc644fc33130d80e73f Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Tue, 17 Mar 2026 15:25:42 +0000 Subject: [PATCH 3/6] remove no-longer used variable --- packages/wrangler/src/deployment-bundle/build-failures.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/wrangler/src/deployment-bundle/build-failures.ts b/packages/wrangler/src/deployment-bundle/build-failures.ts index c9deb1a33b..e283f3681d 100644 --- a/packages/wrangler/src/deployment-bundle/build-failures.ts +++ b/packages/wrangler/src/deployment-bundle/build-failures.ts @@ -62,7 +62,6 @@ export function rewriteUnresolvedModuleBuildFailure(errors: esbuild.Message[]) { const match = couldNotResolveErrorText.exec(error.text); // Note: we skip Node built-in modules since these are handled by rewriteNodeCompatBuildFailure if (match !== null && !nodeBuiltinResolveErrorText.test(error.text)) { - const moduleName = match[1]; const hasExternalSuggestion = error.notes?.some((note) => note.text?.includes(markAsExternalNoteText) ); From 10858922c32ff4a3646f48c7f8e8328eef7afa6c Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 18 Mar 2026 11:15:28 +0000 Subject: [PATCH 4/6] update outdated link in changeset --- .changeset/improve-unresolved-module-error.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/improve-unresolved-module-error.md b/.changeset/improve-unresolved-module-error.md index 15fc74e4d6..e76a77dd4e 100644 --- a/.changeset/improve-unresolved-module-error.md +++ b/.changeset/improve-unresolved-module-error.md @@ -17,7 +17,7 @@ Wrangler will now suggest: ``` To fix this, you can add an entry to "alias" in your Wrangler configuration to substitute "some-missing-module" with an alternative implementation. -See https://developers.cloudflare.com/workers/wrangler/configuration/#for-bundling-issues +See https://developers.cloudflare.com/workers/wrangler/configuration/#bundling-issues ``` This provides actionable guidance for resolving import errors. From 71ca7d2cd22dcb78eaef3b70c955b41ba05c8d76 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 18 Mar 2026 11:53:29 +0000 Subject: [PATCH 5/6] fix formatting --- .../src/__tests__/api/startDevWorker/BundleController.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts index f900700b36..3f02c8fd25 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/BundleController.test.ts @@ -539,7 +539,7 @@ describe("BundleController", { retry: 5, timeout: 10_000 }, () => { describe("bundling error messages", () => { test("should recommend alias when a non-Node module cannot be resolved", async () => { await seed({ - "src/index.ts": dedent/* javascript */ ` + "src/index.ts": dedent /* javascript */ ` import foo from 'some-nonexistent-module'; export default { fetch(request, env, ctx) { @@ -578,7 +578,7 @@ describe("BundleController", { retry: 5, timeout: 10_000 }, () => { test("should NOT recommend alias for Node built-in modules", async () => { await seed({ - "src/index.ts": dedent/* javascript */ ` + "src/index.ts": dedent /* javascript */ ` import fs from 'fs'; export default { fetch(request, env, ctx) { From 895efa429694b6ed3e8df9230271919061bfe540 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Tue, 24 Mar 2026 12:40:23 +0000 Subject: [PATCH 6/6] add missing `expect`s --- packages/wrangler/src/__tests__/deploy/build.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/wrangler/src/__tests__/deploy/build.test.ts b/packages/wrangler/src/__tests__/deploy/build.test.ts index 6cd78636c4..a8d855bee2 100644 --- a/packages/wrangler/src/__tests__/deploy/build.test.ts +++ b/packages/wrangler/src/__tests__/deploy/build.test.ts @@ -658,7 +658,9 @@ describe("deploy", () => { }); }); describe("unresolved module error messages", () => { - it("should recommend alias when a non-Node module cannot be resolved", async () => { + it("should recommend alias when a non-Node module cannot be resolved", async ({ + expect, + }) => { writeWranglerConfig(); fs.writeFileSync( "index.js", @@ -687,7 +689,9 @@ export default { fetch() { return new Response(foo); } }` `); }); - it("should NOT recommend alias for Node built-in modules", async () => { + it("should NOT recommend alias for Node built-in modules", async ({ + expect, + }) => { writeWranglerConfig(); fs.writeFileSync("index.js", "import fs from 'fs';");