Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/calm-counters-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

Allow `wrangler dev` to start module Workers without a default export

Wrangler now skips default-entrypoint middleware for Workers that only export named entrypoints. This avoids generating a middleware facade with an invalid default import.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset exposes internal implementation
The second paragraph discusses the middleware facade and generated imports. REVIEW.md requires changesets to describe user-facing impact instead of internal implementation.

34 changes: 33 additions & 1 deletion packages/wrangler/e2e/multiworker-dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,35 @@ describe("multiworker", () => {
);
});

it("can fetch named entrypoint on b through a and do RPC", async ({
it("can call and prefix logs from a named-only RPC entrypoint", async ({
expect,
}) => {
await baseSeed(b, {
"src/index.ts": dedent /* javascript */ `
import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers";

class Counter extends RpcTarget {
#value = 0;

increment(amount) {
this.#value += amount;
console.log("incremented counter", this.#value);
return this.#value;
}

get value() {
return this.#value;
}
}

export class CounterService extends WorkerEntrypoint {
async newCounter() {
return new Counter();
}
}
`,
});

const workerA = helper.runLongLived(
`wrangler dev -c wrangler.toml -c ${b}/wrangler.toml`,
{ cwd: a }
Expand All @@ -228,6 +254,12 @@ describe("multiworker", () => {
await waitForLong(
async () => await expect(fetchText(`${url}/count`)).resolves.toBe("6")
);
await waitFor(() => {
const logLine = workerA.currentOutput
.split("\n")
.find((line) => line.includes("incremented counter 6"));
expect(logLine).toContain(`[${workerName2}]`);
});
});

it("can access service props through a binding", async ({ expect }) => {
Expand Down
46 changes: 45 additions & 1 deletion packages/wrangler/src/__tests__/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from "node:path";
import { runInTempDir } from "@cloudflare/workers-utils/test-helpers";
import dedent from "ts-dedent";
import { beforeEach, describe, it, vi } from "vitest";
import { startWorker } from "../api/startDevWorker";
import { DevEnv, startWorker } from "../api/startDevWorker";
import { mockConsoleMethods } from "./helpers/mock-console";
import { runWrangler } from "./helpers/run-wrangler";

Expand Down Expand Up @@ -35,6 +35,50 @@ describe("middleware", () => {
});

describe("module workers", () => {
it("should start a Worker with only named entrypoints", async () => {
const scriptContent = `
import { RpcTarget, WorkerEntrypoint } from "cloudflare:workers";

class Counter extends RpcTarget {
increment() {
return 1;
}
}

export class CounterService extends WorkerEntrypoint {
async newCounter() {
return new Counter();
}
}
`;
fs.writeFileSync("index.js", scriptContent);

const devEnv = new DevEnv();
// `worker.ready` only waits for the proxy server, which stays available
// after recoverable build failures. Wait for the Worker runtime to reload
// so the test verifies that the user module was bundled successfully.
const buildFinished = new Promise<void>((resolve, reject) => {
devEnv.once("reloadComplete", () => resolve());
devEnv.once("buildFailed", () => {
reject(new Error("Worker build failed"));
});
});
const worker = await devEnv.startWorker({
entrypoint: "index.js",
dev: {
server: { hostname: "127.0.0.1", port: 0 },
inspector: false,
},
});

try {
await buildFinished;
await worker.ready;
} finally {
await worker.dispose();
}
});

it("should register a middleware and intercept", async ({ expect }) => {
const scriptContent = `
const middleware = async (request, env, _ctx, middlewareCtx) => {
Expand Down
12 changes: 12 additions & 0 deletions packages/wrangler/src/deployment-bundle/apply-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export async function applyMiddlewareLoaderFacade(
tmpDirPath: string,
middleware: MiddlewareLoader[]
): Promise<{ entry: Entry; inject?: string[] }> {
// Module middleware only wraps the default entrypoint. Named entrypoints are
// re-exported unchanged, so a module with only named exports has nothing to
// wrap. Skipping the facade also avoids generating an invalid default import.
// Synthesized module entries (eg pages) may have empty exports but still need middleware.
if (
entry.format === "modules" &&
entry.exports.length > 0 &&
!entry.exports.includes("default")
) {
return { entry };
Comment thread
petebacondarwin marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}

// Firstly we need to insert the middleware array into the project,
// and then we load the middleware - this insertion and loading is
// different for each format.
Expand Down
32 changes: 19 additions & 13 deletions packages/wrangler/src/deployment-bundle/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export async function bundleWorker(

// At this point, we take the opportunity to "wrap" the worker with middleware.
const middlewareToLoad: MiddlewareLoader[] = [];
const middlewareConfig: Record<string, Record<string, unknown>> = {};

if (
targetConsumer === "dev" &&
Expand Down Expand Up @@ -272,16 +273,20 @@ export async function bundleWorker(
inject.push(checkedFetchFileToInject);
}

// When multiple workers are running we need some way to disambiguate logs between them. Inject a patched version of `globalThis.console` that prefixes logs with the worker name
// When multiple workers are running we need some way to disambiguate logs
// between them. This patch only has a side effect, so inject it independently
// of the middleware facade, which may be skipped for named-only Workers.
if (getFlag("MULTIWORKER")) {
middlewareToLoad.push({
name: "patch-console-prefix",
path: "templates/middleware/middleware-patch-console-prefix.ts",
supports: ["modules", "service-worker"],
config: {
prefix: chalk.blue(`[${entry.name}]`),
},
});
const name = "patch-console-prefix";
inject.push(
path.resolve(
getBasePath(),
"templates/middleware/middleware-patch-console-prefix.ts"
)
);
middlewareConfig[name] = {
prefix: chalk.blue(`[${entry.name}]`),
};
}
// Check that the current worker format is supported by all the active middleware
for (const middleware of middlewareToLoad) {
Expand Down Expand Up @@ -420,13 +425,14 @@ export async function bundleWorker(
cloudflareInternalPlugin,
buildResultPlugin,
...(plugins || []),
configProviderPlugin(
Object.fromEntries(
configProviderPlugin({
...Object.fromEntries(
middlewareToLoad
.filter((m) => m.config !== undefined)
.map((m) => [m.name, m.config] as [string, Record<string, unknown>])
)
),
),
...middlewareConfig,
}),
],
...(jsxFactory && { jsxFactory }),
...(jsxFragment && { jsxFragment }),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/// <reference path="middleware-patch-console-prefix.d.ts"/>

import { prefix } from "config:middleware/patch-console-prefix";
import type { Middleware } from "./common";

// Directly patch console methods to add worker prefix.
// We capture the original method once and replace with a wrapper.
Expand All @@ -13,9 +12,3 @@ import type { Middleware } from "./common";
},
});
});

const passthrough: Middleware = (request, env, _ctx, middlewareCtx) => {
return middlewareCtx.next(request, env);
};

export default passthrough;
Loading