Skip to content

@workflow/vitest: workflow-body local .ts imports are externalized and fail to load on Node 24 (+ a require(esm) cycle) #2289

@kevinhofmaenner

Description

@kevinhofmaenner

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.

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions