workflow-nest build --module commonjs writes a steps bundle that declares require twice, so importing it throws.
$ npx @workflow/nest build --module commonjs
✓ Compiled workflows in 177ms (20 steps, 2 workflows)
[@workflow/nest] Built local workflow bundles
$ sed -n '1,6p' .nestjs/workflow/steps.mjs
import { createRequire as __bundled_createRequire } from 'node:module';
const require = __bundled_createRequire(import.meta.url);
// biome-ignore-all lint: generated file
/* eslint-disable */
import { createRequire as __createRequire } from "node:module";
var require = __createRequire(import.meta.url);
$ node -e "import('./.nestjs/workflow/steps.mjs')"
SyntaxError: Identifier 'require' has already been declared
The build itself reports success, so the failure shows up later and indirectly: the flow route answers 500, the queue retries the message, and the run stays pending. It took us a while to connect the two.
Where the two declarations come from
Line 2 is the shim that #rewriteStepsBundleForCjs() prepends in packages/nest/src/builder.ts. Line 6 is the esbuild banner @workflow/builders adds so bundled CJS code works in an ESM output.
createStepsBundle already has an option for this, skipEsmRequireBanner, and its comment describes the same hazard. The comment is about the __createRequire identifier being declared twice. In this case the two helper identifiers differ (__bundled_createRequire and __createRequire), but both statements declare a variable named require, and const plus var in one module scope is a syntax error.
NestLocalBuilder never passes that option, so it keeps its default of false. createCombinedBundle does pass it (skipEsmRequireBanner: bundleFinalOutput), which is why the --vercel output is unaffected. All four functions it emits pass node --check. Only the local bundle breaks.
Versions
workflow, @workflow/nest and @workflow/builders are all 5.0.0-beta.44. @nestjs/core 11.1.19, Node 24.16.0. Nest project compiling to CommonJS, WorkflowModule.forRoot({ moduleType: 'commonjs', distDir: 'dist' }).
Workaround
We remove the prepended shim after the build and keep the esbuild banner, which provides the same binding. It works, but it is a post-processing step over generated output, so we would rather drop it.
--module es6 is not an alternative here
We checked, since that would sidestep the whole rewrite. It does avoid the duplicate declaration, and the bundle then holds one require, from the esbuild banner alone. It does not load either, for an unrelated reason. With es6 the bundle externalises to the .ts sources:
import { getWorkflowServices } from "../../src/workflows/workflow-context.ts";
import { CommercetoolsEnvironment } from "../../src/commerceToolsApi.service.ts";
import { POSTBACK_STATUSES } from "../../src/better-payment/webhook/postback-config.ts";
Node has to strip the types from those files at load time, and one of them declares an enum:
SyntaxError: TypeScript enum is not supported in strip-only mode
Nothing about that file is unusual, so any CommonJS Nest project should hit this as soon as a step imports from a source file containing an enum. Handling it would need --experimental-transform-types at runtime, which we would rather not require.
Where we might still be wrong
Is NestLocalBuilder meant to be driven through the CLI for a CommonJS project, or should the local bundles come from somewhere else? If there is a third option we have missed, we would be glad to hear it.
workflow-nest build --module commonjswrites a steps bundle that declaresrequiretwice, so importing it throws.The build itself reports success, so the failure shows up later and indirectly: the flow route answers 500, the queue retries the message, and the run stays
pending. It took us a while to connect the two.Where the two declarations come from
Line 2 is the shim that
#rewriteStepsBundleForCjs()prepends inpackages/nest/src/builder.ts. Line 6 is the esbuild banner@workflow/buildersadds so bundled CJS code works in an ESM output.createStepsBundlealready has an option for this,skipEsmRequireBanner, and its comment describes the same hazard. The comment is about the__createRequireidentifier being declared twice. In this case the two helper identifiers differ (__bundled_createRequireand__createRequire), but both statements declare a variable namedrequire, andconstplusvarin one module scope is a syntax error.NestLocalBuildernever passes that option, so it keeps its default offalse.createCombinedBundledoes pass it (skipEsmRequireBanner: bundleFinalOutput), which is why the--verceloutput is unaffected. All four functions it emits passnode --check. Only the local bundle breaks.Versions
workflow,@workflow/nestand@workflow/buildersare all5.0.0-beta.44.@nestjs/core11.1.19, Node 24.16.0. Nest project compiling to CommonJS,WorkflowModule.forRoot({ moduleType: 'commonjs', distDir: 'dist' }).Workaround
We remove the prepended shim after the build and keep the esbuild banner, which provides the same binding. It works, but it is a post-processing step over generated output, so we would rather drop it.
--module es6is not an alternative hereWe checked, since that would sidestep the whole rewrite. It does avoid the duplicate declaration, and the bundle then holds one
require, from the esbuild banner alone. It does not load either, for an unrelated reason. Withes6the bundle externalises to the.tssources:Node has to strip the types from those files at load time, and one of them declares an enum:
Nothing about that file is unusual, so any CommonJS Nest project should hit this as soon as a step imports from a source file containing an enum. Handling it would need
--experimental-transform-typesat runtime, which we would rather not require.Where we might still be wrong
Is
NestLocalBuildermeant to be driven through the CLI for a CommonJS project, or should the local bundles come from somewhere else? If there is a third option we have missed, we would be glad to hear it.