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

@VaguelySeriousVaguelySerious 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 bothwebpackIgnore 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

OptionWhy not (here)
Keep #2752's build-time aliasIt'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 coreOnly 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 entryNicer 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 resolutionCleanest 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

Pagev5
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.

VaguelySeriousand 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

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
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7519011328651
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
✅ express126028
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@VaguelySerious
VaguelySeriousforce-pushed the peter/revert-static-world-target branch from 003f800 to 164e7d3CompareJuly 29, 2026 06:14
Base automatically changed from peter/revert-static-world-target to mainJuly 29, 2026 15:55
Sign up for freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@VaguelySeriousVaguelySerious 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 bothwebpackIgnore 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

OptionWhy not (here)
Keep #2752's build-time aliasIt'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 coreOnly 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 entryNicer 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 resolutionCleanest 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

Pagev5
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.

VaguelySeriousand 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

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
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7519011328651
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
✅ express126028
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@VaguelySerious
VaguelySeriousforce-pushed the peter/revert-static-world-target branch from 003f800 to 164e7d3CompareJuly 29, 2026 06:14
Base automatically changed from peter/revert-static-world-target to mainJuly 29, 2026 15:55
Sign up for freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@VaguelySeriousVaguelySerious 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 bothwebpackIgnore 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

OptionWhy not (here)
Keep #2752's build-time aliasIt'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 coreOnly 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 entryNicer 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 resolutionCleanest 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

Pagev5
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.

VaguelySeriousand 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

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
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7519011328651
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
✅ express126028
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@VaguelySerious
VaguelySeriousforce-pushed the peter/revert-static-world-target branch from 003f800 to 164e7d3CompareJuly 29, 2026 06:14
Base automatically changed from peter/revert-static-world-target to mainJuly 29, 2026 15:55
Sign up for freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@VaguelySeriousVaguelySerious 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 bothwebpackIgnore 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

OptionWhy not (here)
Keep #2752's build-time aliasIt'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 coreOnly 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 entryNicer 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 resolutionCleanest 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

Pagev5
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.

VaguelySeriousand 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

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
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7519011328651
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
✅ express126028
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@VaguelySerious
VaguelySeriousforce-pushed the peter/revert-static-world-target branch from 003f800 to 164e7d3CompareJuly 29, 2026 06:14
Base automatically changed from peter/revert-static-world-target to mainJuly 29, 2026 15:55
Sign up for freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@VaguelySeriousVaguelySerious 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 bothwebpackIgnore 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

OptionWhy not (here)
Keep #2752's build-time aliasIt'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 coreOnly 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 entryNicer 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 resolutionCleanest 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

Pagev5
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.

VaguelySeriousand 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

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
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7519011328651
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
✅ express126028
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@VaguelySerious
VaguelySeriousforce-pushed the peter/revert-static-world-target branch from 003f800 to 164e7d3CompareJuly 29, 2026 06:14
Base automatically changed from peter/revert-static-world-target to mainJuly 29, 2026 15:55
Sign up for freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@VaguelySeriousVaguelySerious 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 bothwebpackIgnore 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

OptionWhy not (here)
Keep #2752's build-time aliasIt'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 coreOnly 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 entryNicer 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 resolutionCleanest 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

Pagev5
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.

VaguelySeriousand 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

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
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7519011328651
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
✅ express126028
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@VaguelySerious
VaguelySeriousforce-pushed the peter/revert-static-world-target branch from 003f800 to 164e7d3CompareJuly 29, 2026 06:14
Base automatically changed from peter/revert-static-world-target to mainJuly 29, 2026 15:55
Sign up for freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@VaguelySeriousVaguelySerious 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 bothwebpackIgnore 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

OptionWhy not (here)
Keep #2752's build-time aliasIt'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 coreOnly 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 entryNicer 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 resolutionCleanest 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

Pagev5
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.

VaguelySeriousand 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

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
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7519011328651
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
✅ express126028
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@VaguelySerious
VaguelySeriousforce-pushed the peter/revert-static-world-target branch from 003f800 to 164e7d3CompareJuly 29, 2026 06:14
Base automatically changed from peter/revert-static-world-target to mainJuly 29, 2026 15:55
Sign up for freeto 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

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@VaguelySeriousVaguelySerious 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 bothwebpackIgnore 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

OptionWhy not (here)
Keep #2752's build-time aliasIt'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 coreOnly 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 entryNicer 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 resolutionCleanest 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

Pagev5
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.

VaguelySeriousand 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

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
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7519011328651
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
✅ express126028
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@VaguelySerious
VaguelySeriousforce-pushed the peter/revert-static-world-target branch from 003f800 to 164e7d3CompareJuly 29, 2026 06:14
Base automatically changed from peter/revert-static-world-target to mainJuly 29, 2026 15:55
Sign up for freeto 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

@VaguelySerious