Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/improve-unresolved-module-error.md
Original file line number Diff line number Diff line change
@@ -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/#bundling-issues
```

This provides actionable guidance for resolving import errors.
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -534,4 +535,84 @@ 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.
For more guidance see: https://developers.cloudflare.com/workers/wrangler/configuration/#bundling-issues"
`);
});

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."
`);
});
});
});
59 changes: 59 additions & 0 deletions packages/wrangler/src/__tests__/deploy/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,65 @@ describe("deploy", () => {
`);
});
});
describe("unresolved module error messages", () => {
it("should recommend alias when a non-Node module cannot be resolved", async ({
expect,
}) => {
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.
For more guidance see: https://developers.cloudflare.com/workers/wrangler/configuration/#bundling-issues"
`);
});

it("should NOT recommend alias for Node built-in modules", async ({
expect,
}) => {
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,
Expand Down
41 changes: 41 additions & 0 deletions packages/wrangler/src/deployment-bundle/build-failures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,47 @@ 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 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` +
`For more guidance see: https://developers.cloudflare.com/workers/wrangler/configuration/#bundling-issues\n`,
},
];
}
}
}
}

/**
* Returns true if the passed value looks like an esbuild BuildFailure object
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/deployment-bundle/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -477,6 +478,7 @@ export async function bundleWorker(
} catch (e) {
if (isBuildFailure(e)) {
rewriteNodeCompatBuildFailure(e.errors, nodejsCompatMode);
rewriteUnresolvedModuleBuildFailure(e.errors);
}
throw e;
}
Expand Down
Loading