You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
@workflow/vitest: workflow-body local .ts imports are externalized and fail to load on Node 24 (+ a require(esm) cycle) #2289
Summary — With the @workflow/vitest plugin on Node 24, a 'use workflow' integration test fails to load the step bundle when a recipe imports a plain local .ts helper used in its workflow body: the helper is externalized and Node can't resolve its named export. Working around that surfaces a second issue — a require(esm)-in-cycle on a transitively-externalized runtime module.
Symptom 1 — workflow-body local helper externalized
// helper.tsexportfunctionbuildPayload(label: string){return{ label };}// recipe.ts ('use workflow')import{buildPayload}from"./helper";exportasyncfunctiondemo(input: {label: string}){"use workflow";constp=buildPayload(input.label);// used in the workflow bodyreturnawaitecho(p);}asyncfunctionecho(p: {label: string}){"use step";returnp.label;}
The generated steps.mjs emits import { buildPayload } from "../helper.js" (note the .ts→.js rewrite); at runtime Node throws:
SyntaxError: The requested module '../helper.js' does not provide an export named 'buildPayload'
bundleTransitiveLocalStepDependencies (added in #1965) only bundles deps reachable from step entries, not workflow-body imports — and the vitest builder's createStepsBundle({ externalizeNonSteps: true, rewriteTsExtensions: true, … }) call doesn't pass it regardless.
Symptom 2 — require(esm) cycle on Node 24
Once symptom 1 is worked around, steps then fail with:
Cannot require() ES Module .../runtime.ts in a cycle. A cycle involving require(esm) is not allowed
to maintain invariants mandated by the ECMAScript specification. Try making at least part of the
dependency in the graph lazily loaded.
…when a step dynamically imports a runtime module that sits in an import cycle. Node 24's loader rejects require() of an ESM module mid-cycle (these cycles are fine under esbuild/webpack in production). Lazy-loading / inlining the cycle members on our side didn't break it, and enabling bundleTransitiveLocalStepDependencies via a patch didn't either.
Ask
Wire bundleTransitiveLocalStepDependencies into the vitest builder, and/or bundle transitive local deps reachable from workflow bodies (not only step entries), so externalized .ts modules aren't loaded through Node's require(esm).
Notes
Reproduced consistently in a Next.js app on Node 24. Happy to put together a standalone repro repo if it would help.
Reproduced in a standalone vitest project on Node 24 with the versions from the report (@workflow/vitest@4.0.9, workflow@4.3.1): a 'use workflow' recipe importing a plain local .ts helper produces a step bundle with import { buildPayload } from "../helper.js", which fails at run dispatch (ERR_MODULE_NOT_FOUND in a fresh project; the missing-named-export variant you saw occurs when a compiled helper.js neighbor exists on disk). The beta channel reproduces the same root cause without the .js rewrite: the helper is externalized as a raw .ts specifier, which only loads via Node's erasable-only type stripping — a helper containing an enum fails with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, and a helper importing another local module extensionless fails with ERR_MODULE_NOT_FOUND.
Root cause: the vitest builder's generated bundles are imported directly by Node's native ESM loader in the test worker (no downstream bundler), but project-local imports of step files were externalized instead of bundled — bundleTransitiveLocalStepDependencies was not wired into the vitest builder, as you suspected.
For symptom 2: I reproduced a related failure with a step dynamically importing a local module in an import cycle (fails without the fix, passes with it), but not the exact require(esm)-in-cycle message — that shape likely depends on your specific module graph. If it persists after the fix below, a standalone repro would help.
PR: #2351 — enables bundleTransitiveLocalStepDependencies in the vitest builder so local helpers (whether used in workflow bodies or step bodies) are inlined into the step bundle. npm packages stay external, so vi.mock() of third-party packages keeps working. CI is green.
To see if this fix solves your issue, could you try to install the pre-release in your package.json? Just replace the deps like so:
Activity
fix(vitest): bundle transitive local step dependencies
VaguelySerious commentedon Jun 11, 2026
(AI)
Reproduction
Reproduced in a standalone vitest project on Node 24 with the versions from the report (
@workflow/vitest@4.0.9,workflow@4.3.1): a'use workflow'recipe importing a plain local.tshelper produces a step bundle withimport { buildPayload } from "../helper.js", which fails at run dispatch (ERR_MODULE_NOT_FOUNDin a fresh project; the missing-named-export variant you saw occurs when a compiledhelper.jsneighbor exists on disk). The beta channel reproduces the same root cause without the.jsrewrite: the helper is externalized as a raw.tsspecifier, which only loads via Node's erasable-only type stripping — a helper containing anenumfails withERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, and a helper importing another local module extensionless fails withERR_MODULE_NOT_FOUND.Root cause: the vitest builder's generated bundles are imported directly by Node's native ESM loader in the test worker (no downstream bundler), but project-local imports of step files were externalized instead of bundled —
bundleTransitiveLocalStepDependencieswas not wired into the vitest builder, as you suspected.For symptom 2: I reproduced a related failure with a step dynamically importing a local module in an import cycle (fails without the fix, passes with it), but not the exact
require(esm)-in-cycle message — that shape likely depends on your specific module graph. If it persists after the fix below, a standalone repro would help.Related issues
bundleTransitiveLocalStepDependencies(Fix Nitro dev loading of local step dependencies #1965); this fix wires the same option into the vitest builderFix
PR: #2351 — enables
bundleTransitiveLocalStepDependenciesin the vitest builder so local helpers (whether used in workflow bodies or step bodies) are inlined into the step bundle. npm packages stay external, sovi.mock()of third-party packages keeps working. CI is green.To see if this fix solves your issue, could you try to install the pre-release in your package.json? Just replace the deps like so:
{ "dependencies": { "workflow": "https://workflow-tarballs-git-peter-issue-2289.labs.vercel.dev/workflow.tgz", "@workflow/world-local": "https://workflow-tarballs-git-peter-issue-2289.labs.vercel.dev/workflow-world-local.tgz" }, "devDependencies": { "@workflow/vitest": "https://workflow-tarballs-git-peter-issue-2289.labs.vercel.dev/workflow-vitest.tgz" } }Note this pre-release is built from the v5 beta channel (
main), so it includes 5.0 beta changes relative to your 4.x versions.