diff --git a/.changeset/fix-multiworker-build-errors.md b/.changeset/fix-multiworker-build-errors.md new file mode 100644 index 0000000000..e5cfcf09ea --- /dev/null +++ b/.changeset/fix-multiworker-build-errors.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Display build errors for auxiliary workers in multi-worker mode + +Previously, when running `wrangler dev` with multiple `-c` config flags (multi-worker mode), build errors from auxiliary/secondary workers were only logged at debug level, causing Wrangler to silently hang. Build errors from all workers are now displayed at error level so you can see what went wrong and fix it. diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/DevEnv.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/DevEnv.test.ts new file mode 100644 index 0000000000..3179032a7f --- /dev/null +++ b/packages/wrangler/src/__tests__/api/startDevWorker/DevEnv.test.ts @@ -0,0 +1,99 @@ +import { describe, test } from "vitest"; +import { DevEnv } from "../../../api/startDevWorker/DevEnv"; +import { mockConsoleMethods } from "../../helpers/mock-console"; + +describe("DevEnv", () => { + const std = mockConsoleMethods(); + + describe("handleErrorEvent", () => { + test("should format esbuild BuildFailure errors nicely for BundlerController", ({ + expect, + }) => { + const devEnv = new DevEnv(); + + // Create an esbuild-like BuildFailure with errors and warnings arrays + const buildFailure = Object.assign(new Error("Build failed"), { + errors: [ + { + id: "", + pluginName: "", + text: 'Could not resolve "some-missing-module"', + location: null, + notes: [], + detail: undefined, + }, + ], + warnings: [], + }); + + devEnv.dispatch({ + type: "error", + reason: "Failed to construct initial bundle", + cause: buildFailure, + source: "BundlerController", + data: undefined, + }); + + expect(std.err).toContain("Build failed with 1 error"); + expect(std.err).toContain('Could not resolve "some-missing-module"'); + + void devEnv.teardown(); + }); + + test("should format esbuild BuildFailure from cause for BundlerController", ({ + expect, + }) => { + const devEnv = new DevEnv(); + + // Create an esbuild-like BuildFailure nested in cause + const innerFailure = Object.assign(new Error("Build failed"), { + errors: [ + { + id: "", + pluginName: "", + text: "Syntax error in worker code", + location: null, + notes: [], + detail: undefined, + }, + ], + warnings: [], + }); + const outerError = new Error("Initial build failed."); + outerError.cause = innerFailure; + + devEnv.dispatch({ + type: "error", + reason: "Failed to construct initial bundle", + cause: outerError, + source: "BundlerController", + data: undefined, + }); + + expect(std.err).toContain("Build failed with 1 error"); + expect(std.err).toContain("Syntax error in worker code"); + + void devEnv.teardown(); + }); + + test("should log non-esbuild BundlerController errors with just the message", ({ + expect, + }) => { + const devEnv = new DevEnv(); + + const buildError = new Error("Custom build command failed"); + + devEnv.dispatch({ + type: "error", + reason: "Custom build failed", + cause: buildError, + source: "BundlerController", + data: { config: undefined, filePath: "src/index.ts" }, + }); + + expect(std.err).toContain("Custom build command failed"); + + void devEnv.teardown(); + }); + }); +}); diff --git a/packages/wrangler/src/api/startDevWorker/BundlerController.ts b/packages/wrangler/src/api/startDevWorker/BundlerController.ts index 5e34762bfb..e2771d0bd8 100644 --- a/packages/wrangler/src/api/startDevWorker/BundlerController.ts +++ b/packages/wrangler/src/api/startDevWorker/BundlerController.ts @@ -173,7 +173,6 @@ export class BundlerController extends Controller { entrypointSource: readFileSync(entrypointPath, "utf8"), }); } catch (err) { - logger.error("Custom build failed:", err); this.emitErrorEvent({ type: "error", reason: "Custom build failed", @@ -335,9 +334,6 @@ export class BundlerController extends Controller { try { this.#tmpDir = getWranglerTmpDir(event.config.projectRoot, "dev"); } catch (e) { - logger.error( - "Failed to create temporary directory to store built files." - ); this.emitErrorEvent({ type: "error", reason: "Failed to create temporary directory to store built files.", @@ -348,7 +344,6 @@ export class BundlerController extends Controller { } void this.#startCustomBuild(event.config).catch((err) => { - logger.error("Failed to run custom build:", err); this.emitErrorEvent({ type: "error", reason: "Failed to run custom build", @@ -358,7 +353,6 @@ export class BundlerController extends Controller { }); }); void this.#startBundle(event.config).catch((err) => { - logger.error("Failed to start bundler:", err); this.emitErrorEvent({ type: "error", reason: "Failed to start bundler", @@ -368,7 +362,6 @@ export class BundlerController extends Controller { }); }); void this.#ensureWatchingAssets(event.config).catch((err) => { - logger.error("Failed to watch assets:", err); this.emitErrorEvent({ type: "error", reason: "Failed to watch assets", diff --git a/packages/wrangler/src/api/startDevWorker/DevEnv.ts b/packages/wrangler/src/api/startDevWorker/DevEnv.ts index eab5cf530f..36bc0f937b 100644 --- a/packages/wrangler/src/api/startDevWorker/DevEnv.ts +++ b/packages/wrangler/src/api/startDevWorker/DevEnv.ts @@ -2,7 +2,11 @@ import assert from "node:assert"; import { EventEmitter } from "node:events"; import { ParseError, UserError } from "@cloudflare/workers-utils"; import { MiniflareCoreError } from "miniflare"; -import { logger, runWithLogLevel } from "../../logger"; +import { + isBuildFailure, + isBuildFailureFromCause, +} from "../../deployment-bundle/build-failures"; +import { logBuildFailure, logger, runWithLogLevel } from "../../logger"; import { BundlerController } from "./BundlerController"; import { ConfigController } from "./ConfigController"; import { LocalRuntimeController } from "./LocalRuntimeController"; @@ -156,6 +160,16 @@ export class DevEnv extends EventEmitter implements ControllerBus { ) { logger.error(event.cause); } + // Build errors are recoverable by fixing the code and saving + else if (event.source === "BundlerController") { + if (isBuildFailure(event.cause)) { + logBuildFailure(event.cause.errors, event.cause.warnings); + } else if (isBuildFailureFromCause(event.cause)) { + logBuildFailure(event.cause.cause.errors, event.cause.cause.warnings); + } else { + logger.error(event.cause.message); + } + } // if other knowable + recoverable errors occur, handle them here else { // otherwise, re-emit the unknowable errors to the top-level