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.
Versions — @workflow/vitest@4.0.9, workflow@4.3.1, @workflow/world-local@4.1.4, Node 24 (also reproduces on 26), vitest 4.
Symptom 1 — workflow-body local helper externalized
// helper.ts
export function buildPayload(label: string) {
return { label };
}
// recipe.ts ('use workflow')
import { buildPayload } from "./helper";
export async function demo(input: { label: string }) {
"use workflow";
const p = buildPayload(input.label); // used in the workflow body
return await echo(p);
}
async function echo(p: { label: string }) {
"use step";
return p.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.
Summary — With the
@workflow/vitestplugin on Node 24, a'use workflow'integration test fails to load the step bundle when a recipe imports a plain local.tshelper used in its workflow body: the helper is externalized and Node can't resolve its named export. Working around that surfaces a second issue — arequire(esm)-in-cycle on a transitively-externalized runtime module.Versions —
@workflow/vitest@4.0.9,workflow@4.3.1,@workflow/world-local@4.1.4, Node 24 (also reproduces on 26), vitest 4.Symptom 1 — workflow-body local helper externalized
The generated
steps.mjsemitsimport { buildPayload } from "../helper.js"(note the.ts→.jsrewrite); at runtime Node throws:bundleTransitiveLocalStepDependencies(added in #1965) only bundles deps reachable from step entries, not workflow-body imports — and the vitest builder'screateStepsBundle({ externalizeNonSteps: true, rewriteTsExtensions: true, … })call doesn't pass it regardless.Symptom 2 —
require(esm)cycle on Node 24Once symptom 1 is worked around, steps then fail with:
…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 enablingbundleTransitiveLocalStepDependenciesvia a patch didn't either.Ask
Wire
bundleTransitiveLocalStepDependenciesinto the vitest builder, and/or bundle transitive local deps reachable from workflow bodies (not only step entries), so externalized.tsmodules aren't loaded through Node'srequire(esm).Notes
Reproduced consistently in a Next.js app on Node 24. Happy to put together a standalone repro repo if it would help.