Skip to content

fix(core): actionable errors when a custom world package can't load - #3143

Draft
VaguelySerious wants to merge 2 commits into
mainfrom
peter/harden-dynamic-world-resolution
Draft

fix(core): actionable errors when a custom world package can't load#3143
VaguelySerious wants to merge 2 commits into
mainfrom
peter/harden-dynamic-world-resolution

Conversation

@VaguelySerious

@VaguelySerious VaguelySerious commented Jul 27, 2026

Copy link
Copy Markdown
Member

Draft — proposal, stacked on #3142. Base is peter/revert-static-world-target; review that PR first.

#2752 was shipped to fix the o2flow workflow@5.0.0-beta.26 incident: sandboxDoneHook.resume() from a plain Next.js API route failed with

Cannot find module as expression is too dynamic

The static world-target injection was one way to make that go away, but it wasn't the cause. The cause, as documented in #3001's regression test:

  1. defineHook came from the root workflow entry, which did not carry the @workflow/core/runtime/world-init side-effect import (only workflow/api did).
  2. Turbopack tree-shook world.ts — and its globalThis[GetWorldFnKey] ??= getWorld registration — out of the isolated route bundle.
  3. getWorldLazy() fell through to a last-resort await import(['./world', 'js'].join('.')) — an obfuscated specifier with no ignore comments, which Turbopack compiles into a throwing stub.

Steps 1–3 are all fixed by machinery that survives the revert: world-init is imported by workflow, workflow/api, and workflow/runtime; the obfuscated fallback is gone and getWorldLazy() now throws world runtime was not initialized instead; and #3001's route-bundle-isolation.test.ts builds a real Turbopack bundle and asserts the o2flow failure can't come back. o2flow itself runs on the Vercel world, which core imports statically, so it never touches dynamic resolution at all.

So this PR does not try to re-solve o2flow. It covers what the revert genuinely gives up: apps on a custom world package, where the world is resolved at runtime and is not part of the host bundle.

What this changes

createWorld()'s custom-world path now reports failures usefully instead of leaking whatever the loader threw:

  • Package not resolvable from the running output (self-contained output like Nitro's .output/server, an esbuild bundle, a container image built without the world package) — previously a bare ERR_MODULE_NOT_FOUND for a path the user never wrote.
  • A bundler replaced the dynamic import with a stub — the o2flow signature, plus webpack's "the request of a dependency is an expression". Detected explicitly and called out, because the error is otherwise indistinguishable from a typo.

Both now name the specifier, list every resolution attempt, and give the fix — a static import plus setWorld() — with the original error preserved as cause. The Invalid target world module error also keeps its cause now, so "resolved but exported the wrong shape" stops looking like "couldn't resolve".

Also added:

  • world-bundler-safety.test.ts — source-level guard that every non-literal import()/require() in world.ts keeps both webpackIgnore and turbopackIgnore. Those comments are the only thing preventing a bundler stub, they are invisible to typecheck, and no test that runs under Node's own loader can catch their loss. Also asserts removeComments stays off so they survive into dist/.
  • world-resolution.test.ts — loads a world by absolute path, and asserts the two failure messages above (the stub case is simulated with a module that throws Turbopack's error text).
  • A docs section on registering a world explicitly with setWorld(), and when you need to.

Alternatives considered

Option Why not (here)
Keep #2752's build-time alias It's what this stack is reverting: a world-target module plus alias wiring in seven framework integrations, and it made the world package a static dependency of every host bundle (which is what dragged the TypeScript compiler and pg-native into the SvelteKit server output and needed stub aliases of its own).
Literal import() per known world in core Only works for a closed set. Community worlds (Turso, Redis, self-hosted) are the case that needs it, and an unresolvable literal specifier is a hard build error for everyone who hasn't installed that package.
world-package/register side-effect entry Nicer ergonomics than setWorld(), but it's additive sugar over the same mechanism and needs a change in every world package. Worth doing separately if we like the direction.
Require setWorld() for all custom worlds, drop dynamic resolution Cleanest end state and removes the last expression import() from core, but it breaks every app currently setting WORKFLOW_TARGET_WORLD to a package. Only worth it as a v5 breaking change, and this PR's error message is the migration path for it.

Happy to take this in any of those directions — the point of the draft is the diagnosis above, not the specific patch.

Docs Preview

Page v5
Configuration → Worlds (new "Registering a World explicitly" section) preview

Verification

  • pnpm typecheck green; @workflow/core unit tests green (1580 passed, 3 expected-fail)
  • verified the new coverage bites: against this branch's base world.ts, 3 of the 4 world-resolution.test.ts cases fail (the absolute-path load passes on both). world-bundler-safety.test.ts passes on both by design — it's a guard against future regressions, not a repro.

VaguelySerious and others added 2 commits July 27, 2026 15:19
This reverts the static world-target injection and restores runtime
resolution of the world package from `WORKFLOW_TARGET_WORLD`.

Because 141 commits have landed on top of #2752, this is a surgical
revert rather than a mechanical one. Removed: the `world-target` modules
in core/builders/utils, the bundler aliases and `define`s wired through
next/nitro/nuxt/astro/sveltekit/rollup/nest, the
`@workflow/core/runtime/world-target` export, and the
pg-native/node-compat-banner helpers that only existed to support the
statically bundled world.

Kept (later work that builds on #2752):
- `createWorldFromModule` / `WorldFactoryModule` in core (used by
  world-testing and the nitro dev handler)
- `world-init` / `world-init-stub` / `getWorldLazy` and their
  registration of `getWorld` on globalThis, plus the #3001 isolated
  route bundle regression test
- #2804 (web), #2806 (cli dynamic community backends), #3112 (world
  factory alias removal), #2988 (nest vercel builder), #2925/#2908
  (nitro), #2799 (sveltekit typescript stub is dropped with the
  injection it existed for), next basePath support, and the windows /
  canary test timeout tuning

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to the #2752 revert. `WORKFLOW_TARGET_WORLD` resolves a custom
world package from the app root at runtime, which fails in two ways whose
raw errors say nothing about workflows or about the fix:

- the package isn't resolvable from the running output (self-contained
  server bundles, container images built without it) -> ERR_MODULE_NOT_FOUND
- a bundler dropped the webpackIgnore/turbopackIgnore comments and
  replaced the dynamic import with a stub -> "expression is too dynamic",
  the o2flow beta.26 failure signature

Both now fail with the specifier, every resolution attempt made, and the
static `setWorld()` registration that fixes them, with the original error
kept as `cause`. Adds a source-level guard test asserting the ignore
comments stay attached to every non-literal import/require in world.ts,
and documents explicit world registration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 72c6113

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

This PR includes changesets to release 16 packages
Name Type
@workflow/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt 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

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Jul 27, 2026 10:29pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jul 27, 2026 10:29pm
example-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-astro-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-express-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-fastify-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-hono-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-nestjs-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-nitro-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-nuxt-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workbench-vite-workflow Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Jul 27, 2026 10:29pm
workflow-swc-playground Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workflow-tarballs Ready Ready Preview, Comment Jul 27, 2026 10:29pm
workflow-web Ready Ready Preview, Comment Jul 27, 2026 10:29pm

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
Passed Failed Skipped Total
✅ ▲ Vercel Production 1455 0 239 1694
✅ 💻 Local Development 1621 0 227 1848
✅ 📦 Local Production 1621 0 227 1848
✅ 🐘 Local Postgres 1621 0 227 1848
✅ 🪟 Windows 154 0 0 154
✅ 📋 Other 1020 0 212 1232
✅ vercel-multi-region 27 0 0 27
Total 7519 0 1132 8651
Details by Category

✅ ▲ Vercel Production

App Passed Failed Skipped
✅ astro 126 0 28
✅ example 126 0 28
✅ express 126 0 28
✅ fastify 126 0 28
✅ hono 126 0 28
✅ nextjs-turbopack 151 0 3
✅ nextjs-webpack 151 0 3
✅ nitro 126 0 28
✅ nuxt 126 0 28
✅ sveltekit 145 0 9
✅ vite 126 0 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack 154 0 0

✅ 📋 Other

App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 26
✅ e2e-local-dev-tanstack-start- 128 0 26
✅ e2e-local-postgres-nest-stable 128 0 26
✅ e2e-local-postgres-tanstack-start- 128 0 26
✅ e2e-local-prod-nest-stable 128 0 26
✅ e2e-local-prod-tanstack-start- 128 0 26
✅ e2e-vercel-prod-nest 126 0 28
✅ e2e-vercel-prod-tanstack-start 126 0 28

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

@VaguelySerious
VaguelySerious force-pushed the peter/revert-static-world-target branch from 003f800 to 164e7d3 Compare July 29, 2026 06:14
Base automatically changed from peter/revert-static-world-target to main July 29, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant