Skip to content

[wrangler] Support named-only module Workers in dev - #15611

Open
taylorlee wants to merge 3 commits into
mainfrom
tlee/no-default-export-local-dev
Open

taylorlee wants to merge 3 commits into
mainfrom
tlee/no-default-export-local-dev

Conversation

@taylorlee

@taylorlee taylorlee commented Sep 11, 2026

Copy link
Copy Markdown
Member

The middleware facade only wraps the default entrypoint and re-exports named entrypoints unchanged, so skipping it for named-only modules preserves existing behavior while avoiding an invalid default import.

The regression test waits for the Worker runtime to reload and fails on build errors because worker.ready only indicates that the proxy server started and can resolve after a recoverable bundling failure.

Followup to #15518


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: I have a docs pr drafted but wanted to fix any known issues in tooling here first. Docs will also only simplify the superfluous default exports for entrypoint examples, nothing needed to document for this to start working.

A picture of a cute animal (not mandatory, but encouraged)


Devin Review

@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e53c7b5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
wrangler Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Sep 11, 2026
@workers-devprod
workers-devprod requested review from a team and petebacondarwin and removed request for a team September 11, 2026 17:41
@workers-devprod

workers-devprod commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/calm-counters-develop.md: [@cloudflare/wrangler]
  • packages/wrangler/e2e/multiworker-dev.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/middleware.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/deployment-bundle/apply-middleware.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/deployment-bundle/bundle.ts: [@cloudflare/wrangler]
  • packages/wrangler/templates/middleware/middleware-patch-console-prefix.ts: [@cloudflare/wrangler]

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

I'm Bonk, and I've done a quick review of your PR.

Adds named-only module Worker support in local development.

  1. P2: Named-only Workers lose multi-worker console prefixes. Posted one suggestion comment.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Sep 11, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15611

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15611

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15611

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15611

@cloudflare/containers-shared

npm i https://pkg.pr.new/@cloudflare/containers-shared@15611

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15611

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15611

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15611

miniflare

npm i https://pkg.pr.new/miniflare@15611

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15611

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15611

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15611

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15611

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15611

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15611

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15611

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15611

wrangler

npm i https://pkg.pr.new/wrangler@15611

commit: e53c7b5

@petebacondarwin

Copy link
Copy Markdown
Contributor

packages/wrangler/src/deployment-bundle/apply-middleware.ts:R32-38

Export changes break watched rebuilds

After a running Worker adds or removes its default export, entry.exports remains the startup snapshot during watched rebuilds. The stale value regenerates the invalid facade or skips required development middleware, so the edited Worker cannot reload correctly.

Wrangler computes entry.exports while resolving the entrypoint, before starting the esbuild watch context. Each source edit rebuilds through #startBundle with that same Entry; esbuild does not rerun the facade-selection code with updated exports. Removing the default export therefore leaves "default" in the snapshot and recreates the facade that this change intends to avoid. Adding a default export to a named-only Worker leaves the snapshot named-only and keeps request-body draining, scheduled testing, and local error middleware disabled.

Example: Start wrangler dev with export default { fetch() {} }, then replace it with export class Api extends WorkerEntrypoint {}. The watch rebuild still treats the module as having a default export and fails on the generated default import instead of loading Api.

Recommended fix: Derive facade eligibility from each build's current export metadata, or restart/reconfigure the bundle when the entrypoint's export shape changes. Add watch-mode tests for both default-to-named-only and named-only-to-default edits.

Comment thread .changeset/calm-counters-develop.md Outdated

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed! Amended that commit.

The middleware facade only wraps the default entrypoint and re-exports named entrypoints unchanged, so skipping it for named-only modules preserves existing behavior while avoiding an invalid default import.

The regression test waits for the Worker runtime to reload and fails on build errors because worker.ready only indicates that the proxy server started and can resolve after a recoverable bundling failure.
Move console-prefix from request middleware to an esbuild inject.

The console-prefix module was previously loaded through the middleware
facade only for its import-time side effect. Its default export was a
passthrough that did not alter request handling.

This is a noop refactor for default exports because injecting the module
directly still applies the same patch.

This is a fix for named-only workers, which were never able to load the
middleware previously anyway.
Re-evaluate entrypoint exports and restart the watcher when middleware
facade eligibility changes. Restarting updates esbuild's immutable
entrypoint graph without sacrificing incremental rebuilds for ordinary edits.
@taylorlee
taylorlee force-pushed the tlee/no-default-export-local-dev branch from 226a196 to e53c7b5 Compare September 17, 2026 23:56
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/calm-counters-develop.md: [@cloudflare/wrangler]
  • packages/wrangler/e2e/multiworker-dev.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/api/startDevWorker/BundleController.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/middleware.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/BundlerController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/deployment-bundle/apply-middleware.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/deployment-bundle/bundle.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/use-esbuild.ts: [@cloudflare/wrangler]
  • packages/wrangler/templates/middleware/middleware-patch-console-prefix.ts: [@cloudflare/wrangler]

@taylorlee

Copy link
Copy Markdown
Member Author

Export changes break watched rebuilds
...
Recommended fix: Derive facade eligibility from each build's current export metadata, or restart/reconfigure the bundle when the entrypoint's export shape changes. Add watch-mode tests for both default-to-named-only and named-only-to-default edits.

Good catch! I pushed a new commit to address that. I went with the restart-on-changes approach because that seems to play better with the watchers and incremental builds. Added both of those tests.

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 5 new potential issues.

Devin Review

Comment on lines +129 to +134
exportShapeChanged =
shouldApplyMiddlewareLoaderFacade(entry) !==
shouldApplyMiddlewareLoaderFacade({
format: entry.format,
exports: currentExports,
});

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.

🟡 Named entrypoint edits keep stale names

When a rebuild changes named exports without crossing the default-export boundary, exportShapeChanged remains false. entry.exports retains removed names and omits new RPC entrypoints in the reloaded runtime.

Learn more

Miniflare receives the runtime entrypoint list from buildSourceOptions. This plugin compares only whether the middleware facade applies, not whether the export names changed. A successful rebuild from export class Api {} to export class Admin {} therefore updates the bundle source but leaves entry.exports as ["Api"]. The runtime reload advertises an entrypoint that no longer exists and omits Admin.

Example: Start a named-only Worker exporting Api, then rename that class to Admin. The rebuilt source exports Admin, but service bindings still receive Api as the configured entrypoint name.

Recommended fix: Compare the complete discovered export list with entry.exports and refresh the bundler state whenever it changes. Keep the middleware-facade predicate only for deciding whether facade regeneration is necessary, not for deciding whether runtime export metadata must change.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

targetConsumer,
testScheduled,
plugins: [
exportShapePlugin,

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.

🟡 Alternate rebuilds keep stale exports

When no_bundle or a custom build changes exports, exportShapePlugin never refreshes entry.exports. Custom builds can select the stale facade, while either mode exposes stale runtime entrypoints.

Learn more

The new plugin is installed only when build calls bundleWorker. A normal no_bundle build bypasses that call, and runCustomBuild calls bundleWorker without the plugin. Both watch paths reuse the exports discovered before watching started. The new middleware decision now depends on that stale list, and Miniflare also consumes the same list.

Example: A custom build initially emits a default Worker, then emits only CounterService after an input edit. Wrangler still treats the output as default-exporting, generates a facade with a default import, and the rebuild fails instead of loading CounterService.

Recommended fix: Re-run export discovery after each successful custom build and each watched no_bundle entrypoint change. Recreate the facade when its applicability changes, and always publish the refreshed Entry.exports with the rebuilt bundle.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +2 to +7
"wrangler": patch
---

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

Previously, `wrangler dev` failed to start module Workers that exported only named entrypoints, such as RPC services, unless an unused default export was added. These Workers now start normally in local development without that workaround.

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.

📝 Info: Patch classification matches the fix

The patch changeset describes the restored ability to start named-only module Workers without exposing implementation details.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +276 to +289
// 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}]`),
};

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.

📝 Info: Console prefix no longer depends on facade

Independent injection preserves multiworker console prefixes when named-only modules skip the request-middleware facade. middlewareConfig still supplies the injected module's prefix.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +32 to +37
return !(
entry.format === "modules" &&
entry.exports.length > 0 &&
!entry.exports.includes("default")
);
}

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.

🔍 Wildcard re-exports remain untested

entry.exports.length can miss wildcard re-exports. Test a named-only Worker using export * to verify the facade is skipped.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

3 participants