Description
When a workflow or step file imports a local .ts module that itself imports another local .ts module, the workflow dev bundle still externalizes part of that local dependency graph. At runtime in Nuxt dev, Node ends up loading those externalized files directly under native ESM, and their own extensionless relative imports fail.
This is related to #278 (which fixed direct imports) but appears to remain unresolved for transitive local TypeScript dependencies.
Current status
Still reproducible as of March 26, 2026 with:
workflow: 4.2.0-beta.72
@workflow/nitro: 4.0.1-beta.67
@workflow/nuxt: 4.0.1-beta.56
nuxt: 4.4.2
- Node:
24
- macOS
Why this crashes in Nuxt dev
workflow/nuxt already performs a local workflow build in dev via @workflow/nitro. The problem is not the absence of a dev bundling pass.
The problem is that the generated dev step bundle is still not fully self-contained for some transitive local TS dependencies. It externalizes local .ts modules, and once Node loads those files directly under native ESM, their own relative imports must be fully specified.
So the failure mode is:
workflow dev build -> .nuxt/workflow/steps.mjs with externalized local TS modules
Node ESM loads those externalized files directly
an extensionless relative import inside one of those files fails
Depending on Node version, the exact crash point can differ:
- On some versions, loading the externalized
.ts file itself fails.
- On newer Node versions, the
.ts file can load, but its own extensionless relative imports still fail under native ESM.
Reproduction
https://github.com/okueng/workflow-transitive-dep-bug
git clone https://github.com/okueng/workflow-transitive-dep-bug
cd workflow-transitive-dep-bug
pnpm install
pnpm dev
# visit any page -> error
File structure
server/workflows/my-workflow.ts -> imports ../../shared/constants
shared/constants.ts -> imports ./helpers
shared/helpers.ts -> leaf module
Expected
Both constants.ts and helpers.ts should be bundled into the generated workflow step bundle so that .nuxt/workflow/steps.mjs is self-contained for local runtime dependencies.
Actual
The generated .nuxt/workflow/steps.mjs still contains externalized local TS imports. Example output from a current reproduction:
import { CATEGORIES } from "../../shared/constants.ts";
And in a larger real app on current versions, emitted lines look like:
import { PathwayTypeValues } from "../../shared/domain/pathways.ts";
import { getStaticFxRates } from "../../shared/utils/staticFxRates.ts";
import { resolvePathwayEquations, resolveDerivedVariables } from "../../shared/calc/equations.ts";
Those externalized files then execute under native Node ESM. If one of them contains an extensionless relative import such as ./helpers, dev crashes with an error in this family:
Error: Cannot find module '/path/to/shared/helpers' imported from /path/to/shared/constants.ts
Likely fix area
The bug still appears to be in how @workflow/builders discovers and retains the local transitive dependency graph for step bundles.
The earlier analysis still seems directionally correct:
discover-entries-esbuild-plugin.js likely does not resolve extensionless TS imports correctly during discovery.
- Its
onResolve filter appears too restrictive for extensionless relative specifiers.
- The later externalization logic appears to miss at least some reverse transitive relationships from step entrypoints to local helper modules.
Suggested fix
The previously proposed patch still seems like the right place to investigate:
--- a/dist/discover-entries-esbuild-plugin.js
+++ b/dist/discover-entries-esbuild-plugin.js
-const enhancedResolve = promisify(enhancedResolveOriginal);
+const enhancedResolve = promisify(enhancedResolveOriginal.create({
+ extensions: ['.ts', '.tsx', '.mts', '.cts', '.cjs', '.mjs', '.js', '.jsx', '.json', '.node'],
+ mainFields: ['main'],
+ mainFiles: ['index'],
+ conditionNames: ['node', 'import'],
+}));
- build.onResolve({ filter: jsTsRegex }, async (args) => {
+ build.onResolve({ filter: /^[./]/ }, async (args) => {
--- a/dist/swc-esbuild-plugin.js
+++ b/dist/swc-esbuild-plugin.js
if (parentHasChild(normalizedResolvedPath, normalizedEntry)) {
return null;
}
+ if (parentHasChild(normalizedEntry, normalizedResolvedPath)) {
+ return null;
+ }
The onResolve filter uses /^[./]/ rather than /.*/ to avoid pulling node_modules into the import graph and over-bundling package dependencies.
If helpful, I can convert this into a PR against the current @workflow/builders source rather than the built dist/ output.
Description
When a workflow or step file imports a local
.tsmodule that itself imports another local.tsmodule, the workflow dev bundle still externalizes part of that local dependency graph. At runtime in Nuxt dev, Node ends up loading those externalized files directly under native ESM, and their own extensionless relative imports fail.This is related to #278 (which fixed direct imports) but appears to remain unresolved for transitive local TypeScript dependencies.
Current status
Still reproducible as of March 26, 2026 with:
workflow:4.2.0-beta.72@workflow/nitro:4.0.1-beta.67@workflow/nuxt:4.0.1-beta.56nuxt:4.4.224Why this crashes in Nuxt dev
workflow/nuxtalready performs a local workflow build in dev via@workflow/nitro. The problem is not the absence of a dev bundling pass.The problem is that the generated dev step bundle is still not fully self-contained for some transitive local TS dependencies. It externalizes local
.tsmodules, and once Node loads those files directly under native ESM, their own relative imports must be fully specified.So the failure mode is:
Depending on Node version, the exact crash point can differ:
.tsfile itself fails..tsfile can load, but its own extensionless relative imports still fail under native ESM.Reproduction
https://github.com/okueng/workflow-transitive-dep-bug
File structure
Expected
Both
constants.tsandhelpers.tsshould be bundled into the generated workflow step bundle so that.nuxt/workflow/steps.mjsis self-contained for local runtime dependencies.Actual
The generated
.nuxt/workflow/steps.mjsstill contains externalized local TS imports. Example output from a current reproduction:And in a larger real app on current versions, emitted lines look like:
Those externalized files then execute under native Node ESM. If one of them contains an extensionless relative import such as
./helpers, dev crashes with an error in this family:Likely fix area
The bug still appears to be in how
@workflow/buildersdiscovers and retains the local transitive dependency graph for step bundles.The earlier analysis still seems directionally correct:
discover-entries-esbuild-plugin.jslikely does not resolve extensionless TS imports correctly during discovery.onResolvefilter appears too restrictive for extensionless relative specifiers.Suggested fix
The previously proposed patch still seems like the right place to investigate:
The
onResolvefilter uses/^[./]/rather than/.*/to avoid pullingnode_modulesinto the import graph and over-bundling package dependencies.If helpful, I can convert this into a PR against the current
@workflow/builderssource rather than the builtdist/output.