@workflow/builders 5.0.0-beta.3 → 5.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/base-builder.d.ts +69 -2
  2. package/dist/base-builder.d.ts.map +1 -1
  3. package/dist/base-builder.js +368 -19
  4. package/dist/base-builder.js.map +1 -1
  5. package/dist/config-helpers.d.ts +2 -1
  6. package/dist/config-helpers.d.ts.map +1 -1
  7. package/dist/config-helpers.js +1 -0
  8. package/dist/config-helpers.js.map +1 -1
  9. package/dist/constants.d.ts +3 -13
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +3 -13
  12. package/dist/constants.js.map +1 -1
  13. package/dist/discover-entries-esbuild-plugin.d.ts.map +1 -1
  14. package/dist/discover-entries-esbuild-plugin.js +16 -1
  15. package/dist/discover-entries-esbuild-plugin.js.map +1 -1
  16. package/dist/external-package-warning.test.d.ts +2 -0
  17. package/dist/external-package-warning.test.d.ts.map +1 -0
  18. package/dist/external-package-warning.test.js +219 -0
  19. package/dist/external-package-warning.test.js.map +1 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/module-specifier.d.ts.map +1 -1
  25. package/dist/module-specifier.js +47 -9
  26. package/dist/module-specifier.js.map +1 -1
  27. package/dist/module-specifier.test.js +20 -0
  28. package/dist/module-specifier.test.js.map +1 -1
  29. package/dist/node-module-esbuild-plugin.d.ts.map +1 -1
  30. package/dist/node-module-esbuild-plugin.js +72 -22
  31. package/dist/node-module-esbuild-plugin.js.map +1 -1
  32. package/dist/node-module-esbuild-plugin.test.js +96 -0
  33. package/dist/node-module-esbuild-plugin.test.js.map +1 -1
  34. package/dist/resolve-sourcemap.test.d.ts +2 -0
  35. package/dist/resolve-sourcemap.test.d.ts.map +1 -0
  36. package/dist/resolve-sourcemap.test.js +126 -0
  37. package/dist/resolve-sourcemap.test.js.map +1 -0
  38. package/dist/standalone.d.ts +0 -3
  39. package/dist/standalone.d.ts.map +1 -1
  40. package/dist/standalone.js +10 -39
  41. package/dist/standalone.js.map +1 -1
  42. package/dist/swc-esbuild-plugin.d.ts +9 -0
  43. package/dist/swc-esbuild-plugin.d.ts.map +1 -1
  44. package/dist/swc-esbuild-plugin.js +63 -8
  45. package/dist/swc-esbuild-plugin.js.map +1 -1
  46. package/dist/swc-esbuild-plugin.test.js +279 -3
  47. package/dist/swc-esbuild-plugin.test.js.map +1 -1
  48. package/dist/types.d.ts +29 -0
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/types.js.map +1 -1
  51. package/dist/vercel-build-output-api.d.ts +0 -2
  52. package/dist/vercel-build-output-api.d.ts.map +1 -1
  53. package/dist/vercel-build-output-api.js +24 -55
  54. package/dist/vercel-build-output-api.js.map +1 -1
  55. package/package.json +5 -5
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, readFile, realpath, rename, writeFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
3
4
  import { basename, dirname, join, relative, resolve } from 'node:path';
4
5
  import { promisify } from 'node:util';
6
+ import { WorkflowBuildError } from '@workflow/errors';
5
7
  import { pluralize } from '@workflow/utils';
6
8
  import chalk from 'chalk';
7
9
  import enhancedResolveOriginal from 'enhanced-resolve';
@@ -15,9 +17,47 @@ import { getImportPath } from './module-specifier.js';
15
17
  import { createNodeModuleErrorPlugin } from './node-module-esbuild-plugin.js';
16
18
  import { createPseudoPackagePlugin } from './pseudo-package-esbuild-plugin.js';
17
19
  import { createSwcPlugin } from './swc-esbuild-plugin.js';
20
+ import { detectWorkflowPatterns } from './transform-utils.js';
18
21
  import { extractWorkflowGraphs } from './workflows-extractor.js';
19
22
  const enhancedResolve = promisify(enhancedResolveOriginal);
23
+ const require = createRequire(import.meta.url);
24
+ /**
25
+ * Legacy opt-in for source maps on the final workflow wrapper + webhook
26
+ * bundles (which default to off, unlike the step/interim workflow bundles
27
+ * that default to inline). Superseded by the `sourcemap` config option and
28
+ * the `WORKFLOW_SOURCEMAP` environment variable; kept for back-compat.
29
+ */
20
30
  const EMIT_SOURCEMAPS_FOR_DEBUGGING = process.env.WORKFLOW_EMIT_SOURCEMAPS_FOR_DEBUGGING === '1';
31
+ const VALID_SOURCEMAP_STRINGS = new Set([
32
+ 'inline',
33
+ 'linked',
34
+ 'external',
35
+ 'both',
36
+ ]);
37
+ /**
38
+ * Parse the value of the `WORKFLOW_SOURCEMAP` environment variable into a
39
+ * `SourcemapMode`. Returns `undefined` if the env var is unset, empty, or
40
+ * unrecognized (a warning is emitted for unrecognized values).
41
+ */
42
+ function parseSourcemapEnv(value) {
43
+ if (value === undefined || value === '')
44
+ return undefined;
45
+ switch (value) {
46
+ case '0':
47
+ case 'false':
48
+ return false;
49
+ case '1':
50
+ case 'true':
51
+ return true;
52
+ default:
53
+ if (VALID_SOURCEMAP_STRINGS.has(value)) {
54
+ return value;
55
+ }
56
+ console.warn(`Ignoring unrecognized WORKFLOW_SOURCEMAP=${value}. ` +
57
+ `Expected one of: true, false, 0, 1, inline, linked, external, both.`);
58
+ return undefined;
59
+ }
60
+ }
21
61
  /**
22
62
  * Normalize an array of file paths by appending the `realpath()` of each entry
23
63
  * (to handle symlinks, e.g. pnpm/workspace layouts) and deduplicating.
@@ -36,6 +76,11 @@ async function withRealpaths(entries) {
36
76
  */
37
77
  export class BaseBuilder {
38
78
  config;
79
+ /**
80
+ * Tracks which external packages have already been warned about
81
+ * to avoid duplicate warnings across multiple discoverEntries() calls.
82
+ */
83
+ warnedExternalPackages = new Set();
39
84
  constructor(config) {
40
85
  this.config = config;
41
86
  }
@@ -152,6 +197,105 @@ export class BaseBuilder {
152
197
  * (e.g., when files are added/removed during watch mode).
153
198
  */
154
199
  discoveredEntries = new WeakMap();
200
+ /**
201
+ * Pseudo-packages that should not be checked for workflow patterns.
202
+ */
203
+ static PSEUDO_PACKAGES = new Set([
204
+ 'server-only',
205
+ 'client-only',
206
+ ]);
207
+ /**
208
+ * Checks each package in externalPackages for workflow patterns and emits
209
+ * warnings if any contain "use step", "use workflow" directives, or
210
+ * serialization classes. These patterns will not be transformed by the
211
+ * workflow compiler when the package is externalized.
212
+ */
213
+ async warnAboutExternalWorkflowPackages() {
214
+ const externalPackages = this.config.externalPackages;
215
+ if (!externalPackages?.length)
216
+ return;
217
+ for (const pkg of externalPackages) {
218
+ if (BaseBuilder.PSEUDO_PACKAGES.has(pkg))
219
+ continue;
220
+ if (this.warnedExternalPackages.has(pkg))
221
+ continue;
222
+ if (pkg.startsWith('.') ||
223
+ pkg.startsWith('/') ||
224
+ pkg.startsWith('$') ||
225
+ pkg.includes('*') ||
226
+ pkg.includes(':')) {
227
+ continue;
228
+ }
229
+ try {
230
+ // Check package.json dependencies for @workflow/serde (fast path)
231
+ let hasWorkflowSerdeDep = false;
232
+ try {
233
+ const pkgJsonPath = require.resolve(`${pkg}/package.json`, {
234
+ paths: [this.config.workingDir],
235
+ });
236
+ const pkgJsonSource = await readFile(pkgJsonPath, 'utf-8');
237
+ const pkgJson = JSON.parse(pkgJsonSource);
238
+ const dependencies = typeof pkgJson.dependencies === 'object' &&
239
+ pkgJson.dependencies !== null &&
240
+ !Array.isArray(pkgJson.dependencies)
241
+ ? pkgJson.dependencies
242
+ : {};
243
+ const peerDependencies = typeof pkgJson.peerDependencies === 'object' &&
244
+ pkgJson.peerDependencies !== null &&
245
+ !Array.isArray(pkgJson.peerDependencies)
246
+ ? pkgJson.peerDependencies
247
+ : {};
248
+ hasWorkflowSerdeDep =
249
+ Object.hasOwn(dependencies, '@workflow/serde') ||
250
+ Object.hasOwn(peerDependencies, '@workflow/serde');
251
+ }
252
+ catch {
253
+ // package.json not resolvable - continue to source check
254
+ }
255
+ // Check source patterns (thorough path).
256
+ // Note: require.resolve only inspects the package's main entry point.
257
+ // If workflow constructs live in sub-paths (e.g. `my-pkg/workflows`),
258
+ // they won't be detected here. The @workflow/serde dep check above
259
+ // partially covers serde cases. This is acceptable as a best-effort
260
+ // heuristic — the primary fix is auto-removal in withWorkflow().
261
+ let hasUseStep = false;
262
+ let hasUseWorkflow = false;
263
+ let hasSerde = hasWorkflowSerdeDep;
264
+ try {
265
+ const entryPath = require.resolve(pkg, {
266
+ paths: [this.config.workingDir],
267
+ });
268
+ const source = await readFile(entryPath, 'utf-8');
269
+ const patterns = detectWorkflowPatterns(source);
270
+ hasUseStep = patterns.hasUseStep;
271
+ hasUseWorkflow = patterns.hasUseWorkflow;
272
+ if (!hasSerde) {
273
+ hasSerde = patterns.hasSerde;
274
+ }
275
+ }
276
+ catch {
277
+ // Entry file not resolvable or not readable - use what we have
278
+ }
279
+ if (!hasUseStep && !hasUseWorkflow && !hasSerde)
280
+ continue;
281
+ // Build a specific description of what was found
282
+ const issues = [];
283
+ if (hasUseWorkflow)
284
+ issues.push('"use workflow" functions');
285
+ if (hasUseStep)
286
+ issues.push('"use step" functions');
287
+ if (hasSerde)
288
+ issues.push('serialization classes');
289
+ this.warnedExternalPackages.add(pkg);
290
+ console.warn(`\n${chalk.yellow('⚠')} Warning: ${chalk.bold(`"${pkg}"`)} is listed in ${chalk.bold('externalPackages')} (${chalk.bold('serverExternalPackages')} in Next.js) but contains workflow code (${issues.join(', ')}).` +
291
+ `\n This code will ${chalk.bold('not')} be transformed by the workflow compiler, which can cause runtime failures.` +
292
+ `\n Remove ${chalk.bold(`"${pkg}"`)} from ${chalk.bold('externalPackages')} (${chalk.bold('serverExternalPackages')} in Next.js) to fix this.\n`);
293
+ }
294
+ catch {
295
+ // Best-effort: if anything goes wrong, skip this package silently
296
+ }
297
+ }
298
+ }
155
299
  async discoverEntries(inputs, outdir, tsconfigPath) {
156
300
  const previousResult = this.discoveredEntries.get(inputs);
157
301
  if (previousResult) {
@@ -198,6 +342,8 @@ export class BaseBuilder {
198
342
  }
199
343
  catch (_) { }
200
344
  this.logBaseBuilderInfo(`Discovering workflow directives`, `${Date.now() - discoverStart}ms`);
345
+ // Warn about external packages that contain workflow code
346
+ await this.warnAboutExternalWorkflowPackages();
201
347
  this.discoveredEntries.set(inputs, state);
202
348
  return state;
203
349
  }
@@ -257,7 +403,9 @@ export class BaseBuilder {
257
403
  }
258
404
  }
259
405
  if (throwOnError) {
260
- throw new Error(`Build failed during ${phase}:\n${errorMessages.join('\n')}`);
406
+ throw new WorkflowBuildError(`Build failed during ${phase}:\n${errorMessages.join('\n')}`, {
407
+ hint: `Review the esbuild errors above — they come from the ${phase} bundle. Fix the offending source files and re-run the build.`,
408
+ });
261
409
  }
262
410
  }
263
411
  if (!options?.suppressWarnings &&
@@ -293,19 +441,18 @@ export class BaseBuilder {
293
441
  * Steps have full Node.js runtime access and handle side effects, API calls, etc.
294
442
  *
295
443
  * @param externalizeNonSteps - If true, only bundles step entry points and externalizes other code
444
+ * @param bundleTransitiveLocalStepDependencies - If true, also bundles project-local files imported by step entries for direct runtime loading
296
445
  * @returns Build context (for watch mode) and the collected workflow manifest
297
446
  */
298
- async createStepsBundle({ inputFiles, format = 'esm', outfile, externalizeNonSteps, rewriteTsExtensions, tsconfigPath, discoveredEntries, }) {
447
+ async createStepsBundle({ inputFiles, format = 'esm', outfile, externalizeNonSteps, bundleTransitiveLocalStepDependencies, rewriteTsExtensions, tsconfigPath, discoveredEntries, skipEsmRequireBanner = false, }) {
299
448
  const stepsBundleStart = Date.now();
300
449
  const workflowManifest = {};
301
450
  const builtInSteps = 'workflow/internal/builtins';
302
451
  const resolvedBuiltInSteps = await enhancedResolve(dirname(outfile), 'workflow/internal/builtins').catch((err) => {
303
- throw new Error([
304
- chalk.red('Failed to resolve built-in steps sources.'),
305
- `${chalk.yellow.bold('hint:')} run \`${chalk.cyan.italic('npm install workflow')}\` to resolve this issue.`,
306
- '',
307
- `Caused by: ${chalk.red(String(err))}`,
308
- ].join('\n'));
452
+ throw new WorkflowBuildError(`Failed to resolve built-in steps sources.\n\nCaused by: ${String(err)}`, {
453
+ hint: 'run `pnpm install workflow` to resolve this issue.',
454
+ cause: err,
455
+ });
309
456
  });
310
457
  // Discovery of workflow/step/serde entries. The SDK runtime entry point
311
458
  // (workflow/runtime) is resolved inside discoverEntries() itself so that
@@ -331,6 +478,28 @@ export class BaseBuilder {
331
478
  // For workspace/node_modules packages, uses the package name so esbuild
332
479
  // will resolve through package.json exports with the appropriate conditions
333
480
  const createImport = (file) => {
481
+ const normalizedWorkspaceRoot = this.config.workingDir
482
+ .replace(/\\/g, '/')
483
+ .replace(/\/$/, '');
484
+ const normalizedWorkspaceFile = file.replace(/\\/g, '/');
485
+ // Only use relative source paths for workspace symlinks (files
486
+ // outside node_modules in a packages/*/src/ directory). For tarball-
487
+ // installed packages (files inside node_modules/), fall through to
488
+ // getImportPath which returns package specifiers — this allows the
489
+ // SWC plugin's externalizeNonSteps to work correctly.
490
+ const isWorkspaceSourceBackedPackageFile = normalizedWorkspaceFile.includes('/packages/') &&
491
+ normalizedWorkspaceFile.includes('/src/') &&
492
+ !normalizedWorkspaceFile.includes('/node_modules/') &&
493
+ !(normalizedWorkspaceFile === normalizedWorkspaceRoot ||
494
+ normalizedWorkspaceFile.startsWith(`${normalizedWorkspaceRoot}/`));
495
+ const isSourceBackedPackageFile = isWorkspaceSourceBackedPackageFile;
496
+ if (isSourceBackedPackageFile) {
497
+ let relativePath = relative(normalizedWorkspaceRoot, normalizedWorkspaceFile).replace(/\\/g, '/');
498
+ if (!relativePath.startsWith('./') && !relativePath.startsWith('../')) {
499
+ relativePath = `./${relativePath}`;
500
+ }
501
+ return `import '${relativePath}';`;
502
+ }
334
503
  const { importPath, isPackage } = getImportPath(file, this.config.workingDir);
335
504
  if (isPackage) {
336
505
  // Use package name - esbuild will resolve via package.json exports
@@ -363,8 +532,9 @@ export class BaseBuilder {
363
532
  ${stepImports}
364
533
  // Serde files for cross-context class registration
365
534
  ${serdeImports}
366
- // API entrypoint
367
- export { stepEntrypoint as POST } from 'workflow/runtime';`;
535
+ // Sentinel export so bundlers (rollup) don't tree-shake this module
536
+ // when it's imported as a side-effect-only dependency.
537
+ export const __steps_registered = true;`;
368
538
  // Bundle with esbuild and our custom SWC plugin
369
539
  const entriesToBundle = externalizeNonSteps
370
540
  ? [
@@ -383,7 +553,9 @@ export class BaseBuilder {
383
553
  ]);
384
554
  const esbuildTsconfigOptions = await getEsbuildTsconfigOptions(tsconfigPath);
385
555
  const { banner: importMetaBanner, define: importMetaDefine } = this.getCjsImportMetaPolyfill(format);
386
- const esmRequireBanner = this.getEsmRequireBanner(format);
556
+ const esmRequireBanner = skipEsmRequireBanner
557
+ ? ''
558
+ : this.getEsmRequireBanner(format);
387
559
  const esbuildCtx = await esbuild.context({
388
560
  banner: {
389
561
  js: `// biome-ignore-all lint: generated file\n/* eslint-disable */\n${importMetaBanner}${esmRequireBanner}`,
@@ -425,7 +597,7 @@ export class BaseBuilder {
425
597
  // Steps execute in Node.js context and inline sourcemaps ensure we get
426
598
  // meaningful stack traces with proper file names and line numbers when errors
427
599
  // occur in deeply nested function calls across multiple files.
428
- sourcemap: 'inline',
600
+ sourcemap: this.resolveSourcemap('inline'),
429
601
  plugins: [
430
602
  // Handle pseudo-packages like 'server-only' and 'client-only' by providing
431
603
  // empty modules. Must run first to intercept these before other resolution.
@@ -436,6 +608,7 @@ export class BaseBuilder {
436
608
  outdir: outfile ? dirname(outfile) : undefined,
437
609
  projectRoot: this.transformProjectRoot,
438
610
  workflowManifest,
611
+ bundleTransitiveLocalStepDependencies,
439
612
  rewriteTsExtensions,
440
613
  sideEffectEntries: normalizedSideEffectEntries,
441
614
  }),
@@ -564,7 +737,7 @@ export class BaseBuilder {
564
737
  // Inline source maps for better stack traces in workflow VM execution.
565
738
  // This intermediate bundle is executed via runInContext() in a VM, so we need
566
739
  // inline source maps to get meaningful stack traces instead of "evalmachine.<anonymous>".
567
- sourcemap: 'inline',
740
+ sourcemap: this.resolveSourcemap('inline'),
568
741
  // Use tsconfig for path alias resolution.
569
742
  // For symlinked configs this uses tsconfigRaw to preserve cwd-relative aliases.
570
743
  ...esbuildTsconfigOptions,
@@ -625,7 +798,9 @@ export class BaseBuilder {
625
798
  await this.createSwcGitignore();
626
799
  if (!interimBundle.outputFiles ||
627
800
  interimBundle.outputFiles.length === 0) {
628
- throw new Error('No output files generated from esbuild');
801
+ throw new WorkflowBuildError('No output files generated from esbuild', {
802
+ hint: 'This usually indicates a misconfigured entry point or an empty workflow directory. Check that your workflow files contain a `"use workflow"` or `"use step"` directive.',
803
+ });
629
804
  }
630
805
  // Serde compliance warnings: check if workflow bundle has Node.js imports
631
806
  // alongside serde-registered classes (these will fail at runtime in the sandbox)
@@ -701,7 +876,7 @@ export const POST = workflowEntrypoint(workflowCode);`;
701
876
  outfile,
702
877
  // Source maps for the final workflow bundle wrapper (not important since this code
703
878
  // doesn't run in the VM - only the intermediate bundle sourcemap is relevant)
704
- sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
879
+ sourcemap: this.resolveSourcemap(EMIT_SOURCEMAPS_FOR_DEBUGGING),
705
880
  absWorkingDir: this.config.workingDir,
706
881
  bundle: true,
707
882
  format,
@@ -717,16 +892,18 @@ export const POST = workflowEntrypoint(workflowCode);`;
717
892
  });
718
893
  this.logCreateWorkflowsBundleInfo('Created final workflow bundle', `${Date.now() - bundleStartTime}ms`);
719
894
  };
720
- await bundleFinal(interimBundle.outputFiles[0].text);
895
+ const interimBundleText = interimBundle.outputFiles[0].text;
896
+ await bundleFinal(interimBundleText);
721
897
  if (keepInterimBundleContext) {
722
898
  shouldDisposeInterimBundleCtx = false;
723
899
  return {
724
900
  manifest: workflowManifest,
725
901
  interimBundleCtx,
726
902
  bundleFinal,
903
+ interimBundleText,
727
904
  };
728
905
  }
729
- return { manifest: workflowManifest };
906
+ return { manifest: workflowManifest, interimBundleText };
730
907
  }
731
908
  catch (error) {
732
909
  shouldDisposeInterimBundleCtx = true;
@@ -743,6 +920,153 @@ export const POST = workflowEntrypoint(workflowCode);`;
743
920
  }
744
921
  }
745
922
  }
923
+ /**
924
+ * V2: Creates a combined bundle that includes both step registrations and
925
+ * workflow orchestration in a single route. The combined entrypoint executes
926
+ * steps inline when possible, reducing function invocations and queue overhead.
927
+ *
928
+ * This method reuses createStepsBundle (for step registrations) and
929
+ * createWorkflowsBundle (for workflow VM code), then combines them into
930
+ * a single route file using workflowEntrypoint().
931
+ */
932
+ async createCombinedBundle({ inputFiles, stepsOutfile, flowOutfile, format = 'esm', bundleFinalOutput = true, tsconfigPath, externalizeNonSteps, bundleTransitiveLocalStepDependencies, discoveredEntries, }) {
933
+ // 1. Build step registrations bundle (used as separate file for
934
+ // bundleFinalOutput: false, or read back for inline content when true)
935
+ const { context: stepsContext, manifest: stepsManifest } = await this.createStepsBundle({
936
+ inputFiles,
937
+ outfile: stepsOutfile,
938
+ // When bundleFinalOutput is true, use ESM for the steps bundle
939
+ // regardless of the final output format. The final esbuild pass
940
+ // converts everything to the target format. Using CJS here causes
941
+ // a module.exports collision: the steps bundle's top-level
942
+ // module.exports overwrites the combined route's module.exports
943
+ // when esbuild inlines the steps without a __commonJS wrapper.
944
+ format: bundleFinalOutput ? 'esm' : format,
945
+ externalizeNonSteps,
946
+ bundleTransitiveLocalStepDependencies,
947
+ tsconfigPath,
948
+ discoveredEntries,
949
+ // Skip the createRequire banner here — when bundleFinalOutput is true
950
+ // the outer esbuild pass will inline this bundle and add its own
951
+ // banner. Emitting it twice declares __createRequire twice.
952
+ skipEsmRequireBanner: bundleFinalOutput,
953
+ });
954
+ // 2. Build workflow VM code
955
+ const tempWorkflowOutfile = `${flowOutfile}.__wf_tmp.js`;
956
+ const workflowsResult = await this.createWorkflowsBundle({
957
+ inputFiles,
958
+ outfile: tempWorkflowOutfile,
959
+ format,
960
+ bundleFinalOutput: false,
961
+ tsconfigPath,
962
+ discoveredEntries,
963
+ });
964
+ const workflowVMCode = workflowsResult.interimBundleText;
965
+ if (!workflowVMCode) {
966
+ throw new Error('createWorkflowsBundle did not return interimBundleText');
967
+ }
968
+ // Clean up the wrapper file
969
+ try {
970
+ const { unlink } = await import('node:fs/promises');
971
+ await unlink(tempWorkflowOutfile);
972
+ }
973
+ catch {
974
+ // Ignore cleanup errors
975
+ }
976
+ // 3. Generate combined route file
977
+ const stepsRelativePath = './' + basename(stepsOutfile).replace(/\\/g, '/');
978
+ const escapedVMCode = workflowVMCode.replace(/[\\`$]/g, '\\$&');
979
+ const combinedFunctionCode = `// biome-ignore-all lint: generated file
980
+ /* eslint-disable */
981
+ import { __steps_registered } from '${stepsRelativePath}';
982
+ import { workflowEntrypoint } from 'workflow/runtime';
983
+
984
+ // Prevent rollup from tree-shaking the steps side-effect import
985
+ void __steps_registered;
986
+
987
+ const workflowCode = \`${escapedVMCode}\`;
988
+
989
+ export const POST = workflowEntrypoint(workflowCode);`;
990
+ if (!bundleFinalOutput) {
991
+ // Write directly (Next.js will bundle)
992
+ const tempPath = `${flowOutfile}.${randomUUID()}.tmp`;
993
+ await writeFile(tempPath, combinedFunctionCode);
994
+ await rename(tempPath, flowOutfile);
995
+ }
996
+ else {
997
+ // Bundle the combined code for standalone use
998
+ const bundleStartTime = Date.now();
999
+ const { banner: importMetaBanner, define: importMetaDefine } = this.getCjsImportMetaPolyfill(format);
1000
+ // ESM banner provides `require` via createRequire(import.meta.url) so
1001
+ // CJS dependencies that call require() for Node.js builtins keep working
1002
+ // in the ESM output produced by bundleFinalOutput: true.
1003
+ const finalEsmRequireBanner = this.getEsmRequireBanner(format);
1004
+ const finalResult = await esbuild.build({
1005
+ banner: {
1006
+ js: `// biome-ignore-all lint: generated file\n/* eslint-disable */\n${importMetaBanner}${finalEsmRequireBanner}`,
1007
+ },
1008
+ stdin: {
1009
+ contents: combinedFunctionCode,
1010
+ resolveDir: dirname(flowOutfile),
1011
+ sourcefile: 'virtual-entry.js',
1012
+ loader: 'js',
1013
+ },
1014
+ outfile: flowOutfile,
1015
+ absWorkingDir: this.config.workingDir,
1016
+ bundle: true,
1017
+ format,
1018
+ platform: 'node',
1019
+ target: 'es2022',
1020
+ write: true,
1021
+ keepNames: true,
1022
+ minify: false,
1023
+ define: importMetaDefine,
1024
+ external: ['@aws-sdk/credential-provider-web-identity'],
1025
+ });
1026
+ this.logEsbuildMessages(finalResult, 'combined bundle', true);
1027
+ this.logBaseBuilderInfo('Created combined bundle', `${Date.now() - bundleStartTime}ms`);
1028
+ }
1029
+ // Merge manifests
1030
+ const manifest = {
1031
+ ...stepsManifest,
1032
+ workflows: {
1033
+ ...stepsManifest.workflows,
1034
+ ...workflowsResult.manifest.workflows,
1035
+ },
1036
+ classes: {
1037
+ ...stepsManifest.classes,
1038
+ ...workflowsResult.manifest.classes,
1039
+ },
1040
+ };
1041
+ // Create a custom bundleFinal for watch mode that uses workflowEntrypoint
1042
+ const combinedBundleFinal = async (interimBundleText) => {
1043
+ const escaped = interimBundleText.replace(/[\\`$]/g, '\\$&');
1044
+ const code = `// biome-ignore-all lint: generated file
1045
+ /* eslint-disable */
1046
+ import { __steps_registered } from '${stepsRelativePath}';
1047
+ import { workflowEntrypoint } from 'workflow/runtime';
1048
+
1049
+ void __steps_registered;
1050
+
1051
+ const workflowCode = \`${escaped}\`;
1052
+
1053
+ export const POST = workflowEntrypoint(workflowCode);`;
1054
+ const outputDir = dirname(flowOutfile);
1055
+ await mkdir(outputDir, { recursive: true });
1056
+ const tempPath = `${flowOutfile}.${randomUUID()}.tmp`;
1057
+ await writeFile(tempPath, code);
1058
+ await rename(tempPath, flowOutfile);
1059
+ };
1060
+ if (this.config.watch) {
1061
+ return {
1062
+ manifest,
1063
+ stepsContext,
1064
+ interimBundleCtx: workflowsResult.interimBundleCtx,
1065
+ bundleFinal: combinedBundleFinal,
1066
+ };
1067
+ }
1068
+ return { manifest };
1069
+ }
746
1070
  /**
747
1071
  * Creates a client library bundle for workflow execution.
748
1072
  * The client library allows importing and calling workflows from application code.
@@ -850,7 +1174,10 @@ export const POST = workflowEntrypoint(workflowCode);`;
850
1174
  await mkdir(dirname(outfile), { recursive: true });
851
1175
  // Create a static route that calls resumeWebhook
852
1176
  // This route works for both Next.js and Vercel Build Output API
853
- const routeContent = `import { resumeWebhook } from 'workflow/api';
1177
+ // Bundled Build Output API webhook functions need world.ts statically
1178
+ // present so getWorldLazy() can use the global getWorld registration
1179
+ // instead of falling back to a missing sibling import("./world.js").
1180
+ const routeContent = `${bundle ? "import 'workflow/runtime';\n" : ''}import { resumeWebhook } from 'workflow/api';
854
1181
 
855
1182
  async function handler(request) {
856
1183
  const url = new URL(request.url);
@@ -919,7 +1246,7 @@ export const OPTIONS = handler;`;
919
1246
  '.mjs',
920
1247
  '.cjs',
921
1248
  ],
922
- sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
1249
+ sourcemap: this.resolveSourcemap(EMIT_SOURCEMAPS_FOR_DEBUGGING),
923
1250
  mainFields: ['module', 'main'],
924
1251
  // Don't externalize anything - bundle everything including workflow packages
925
1252
  external: [],
@@ -995,6 +1322,28 @@ export const OPTIONS = handler;`;
995
1322
  return this.resolvePath('.vercel/output/diagnostics/workflows-manifest.json');
996
1323
  }
997
1324
  }
1325
+ /**
1326
+ * Resolve the effective source map mode for a given call site. Precedence:
1327
+ * explicit `sourcemap` config > `WORKFLOW_SOURCEMAP` env var > the call
1328
+ * site's default. Returned value is passed directly to esbuild's
1329
+ * `sourcemap` option.
1330
+ */
1331
+ resolveSourcemap(defaultMode) {
1332
+ if (this.config.sourcemap !== undefined)
1333
+ return this.config.sourcemap;
1334
+ const envMode = parseSourcemapEnv(process.env.WORKFLOW_SOURCEMAP);
1335
+ if (envMode !== undefined)
1336
+ return envMode;
1337
+ return defaultMode;
1338
+ }
1339
+ /**
1340
+ * Whether the resolved source map mode emits any source maps at all.
1341
+ * Used by consumers like the Vercel builder to decide whether to include
1342
+ * the source-map-support runtime shim in generated functions.
1343
+ */
1344
+ get sourcemapsEnabled() {
1345
+ return this.resolveSourcemap(true) !== false;
1346
+ }
998
1347
  /**
999
1348
  * Creates a manifest JSON file containing step/workflow/class metadata
1000
1349
  * and graph data for visualization.