@workflow/builders 4.0.1-beta.5 → 4.0.1-beta.50

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 (88) hide show
  1. package/dist/apply-swc-transform.d.ts +14 -4
  2. package/dist/apply-swc-transform.d.ts.map +1 -1
  3. package/dist/apply-swc-transform.js +59 -9
  4. package/dist/apply-swc-transform.js.map +1 -1
  5. package/dist/base-builder.d.ts +65 -22
  6. package/dist/base-builder.d.ts.map +1 -1
  7. package/dist/base-builder.js +438 -115
  8. package/dist/base-builder.js.map +1 -1
  9. package/dist/build-queue.d.ts +18 -0
  10. package/dist/build-queue.d.ts.map +1 -0
  11. package/dist/build-queue.js +26 -0
  12. package/dist/build-queue.js.map +1 -0
  13. package/dist/config-helpers.d.ts +31 -0
  14. package/dist/config-helpers.d.ts.map +1 -1
  15. package/dist/config-helpers.js +60 -0
  16. package/dist/config-helpers.js.map +1 -1
  17. package/dist/constants.d.ts +2 -2
  18. package/dist/constants.js +2 -2
  19. package/dist/discover-entries-esbuild-plugin.d.ts +2 -3
  20. package/dist/discover-entries-esbuild-plugin.d.ts.map +1 -1
  21. package/dist/discover-entries-esbuild-plugin.js +57 -26
  22. package/dist/discover-entries-esbuild-plugin.js.map +1 -1
  23. package/dist/index.d.ts +9 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +8 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/module-specifier.d.ts +85 -0
  28. package/dist/module-specifier.d.ts.map +1 -0
  29. package/dist/module-specifier.js +437 -0
  30. package/dist/module-specifier.js.map +1 -0
  31. package/dist/module-specifier.test.d.ts +2 -0
  32. package/dist/module-specifier.test.d.ts.map +1 -0
  33. package/dist/module-specifier.test.js +169 -0
  34. package/dist/module-specifier.test.js.map +1 -0
  35. package/dist/node-module-esbuild-plugin.d.ts +37 -0
  36. package/dist/node-module-esbuild-plugin.d.ts.map +1 -1
  37. package/dist/node-module-esbuild-plugin.js +289 -9
  38. package/dist/node-module-esbuild-plugin.js.map +1 -1
  39. package/dist/node-module-esbuild-plugin.test.js +359 -88
  40. package/dist/node-module-esbuild-plugin.test.js.map +1 -1
  41. package/dist/pseudo-package-esbuild-plugin.d.ts +20 -0
  42. package/dist/pseudo-package-esbuild-plugin.d.ts.map +1 -0
  43. package/dist/pseudo-package-esbuild-plugin.js +47 -0
  44. package/dist/pseudo-package-esbuild-plugin.js.map +1 -0
  45. package/dist/pseudo-package-esbuild-plugin.test.d.ts +2 -0
  46. package/dist/pseudo-package-esbuild-plugin.test.d.ts.map +1 -0
  47. package/dist/pseudo-package-esbuild-plugin.test.js +315 -0
  48. package/dist/pseudo-package-esbuild-plugin.test.js.map +1 -0
  49. package/dist/request-converter.d.ts +3 -0
  50. package/dist/request-converter.d.ts.map +1 -0
  51. package/dist/request-converter.js +14 -0
  52. package/dist/request-converter.js.map +1 -0
  53. package/dist/standalone.d.ts +3 -0
  54. package/dist/standalone.d.ts.map +1 -1
  55. package/dist/standalone.js +37 -13
  56. package/dist/standalone.js.map +1 -1
  57. package/dist/swc-esbuild-plugin.d.ts +0 -2
  58. package/dist/swc-esbuild-plugin.d.ts.map +1 -1
  59. package/dist/swc-esbuild-plugin.js +33 -15
  60. package/dist/swc-esbuild-plugin.js.map +1 -1
  61. package/dist/transform-utils.d.ts +68 -0
  62. package/dist/transform-utils.d.ts.map +1 -0
  63. package/dist/transform-utils.js +97 -0
  64. package/dist/transform-utils.js.map +1 -0
  65. package/dist/transform-utils.test.d.ts +2 -0
  66. package/dist/transform-utils.test.d.ts.map +1 -0
  67. package/dist/transform-utils.test.js +266 -0
  68. package/dist/transform-utils.test.js.map +1 -0
  69. package/dist/types.d.ts +26 -2
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js +2 -0
  72. package/dist/types.js.map +1 -1
  73. package/dist/vercel-build-output-api.d.ts.map +1 -1
  74. package/dist/vercel-build-output-api.js +36 -14
  75. package/dist/vercel-build-output-api.js.map +1 -1
  76. package/dist/workflow-alias.d.ts +3 -0
  77. package/dist/workflow-alias.d.ts.map +1 -0
  78. package/dist/workflow-alias.js +46 -0
  79. package/dist/workflow-alias.js.map +1 -0
  80. package/dist/workflow-alias.test.d.ts +2 -0
  81. package/dist/workflow-alias.test.d.ts.map +1 -0
  82. package/dist/workflow-alias.test.js +46 -0
  83. package/dist/workflow-alias.test.js.map +1 -0
  84. package/dist/workflows-extractor.d.ts +92 -0
  85. package/dist/workflows-extractor.d.ts.map +1 -0
  86. package/dist/workflows-extractor.js +1476 -0
  87. package/dist/workflows-extractor.js.map +1 -0
  88. package/package.json +10 -9
@@ -1,15 +1,20 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { dirname, join, relative, resolve } from 'node:path';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, realpath, rename, writeFile } from 'node:fs/promises';
3
+ import { basename, dirname, join, relative, resolve } from 'node:path';
3
4
  import { promisify } from 'node:util';
5
+ import { pluralize, usesVercelWorld } from '@workflow/utils';
4
6
  import chalk from 'chalk';
5
- import { parse } from 'comment-json';
6
7
  import enhancedResolveOriginal from 'enhanced-resolve';
7
8
  import * as esbuild from 'esbuild';
8
9
  import { findUp } from 'find-up';
9
10
  import { glob } from 'tinyglobby';
11
+ import { applySwcTransform, } from './apply-swc-transform.js';
10
12
  import { createDiscoverEntriesPlugin } from './discover-entries-esbuild-plugin.js';
13
+ import { getImportPath } from './module-specifier.js';
11
14
  import { createNodeModuleErrorPlugin } from './node-module-esbuild-plugin.js';
15
+ import { createPseudoPackagePlugin } from './pseudo-package-esbuild-plugin.js';
12
16
  import { createSwcPlugin } from './swc-esbuild-plugin.js';
17
+ import { extractWorkflowGraphs } from './workflows-extractor.js';
13
18
  const enhancedResolve = promisify(enhancedResolveOriginal);
14
19
  const EMIT_SOURCEMAPS_FOR_DEBUGGING = process.env.WORKFLOW_EMIT_SOURCEMAPS_FOR_DEBUGGING === '1';
15
20
  /**
@@ -24,34 +29,39 @@ export class BaseBuilder {
24
29
  this.config = config;
25
30
  }
26
31
  /**
27
- * Extracts TypeScript path mappings and baseUrl from tsconfig.json/jsconfig.json.
28
- * Used to properly resolve module imports during bundling.
32
+ * Whether informational BaseBuilder logs should be printed.
33
+ * Subclasses can override this to silence progress logs while keeping warnings/errors.
29
34
  */
30
- async getTsConfigOptions() {
31
- const options = {};
32
- const cwd = this.config.workingDir || process.cwd();
33
- const tsJsConfig = await findUp(['tsconfig.json', 'jsconfig.json'], {
34
- cwd,
35
- });
36
- if (tsJsConfig) {
37
- try {
38
- const rawJson = await readFile(tsJsConfig, 'utf8');
39
- const parsed = parse(rawJson);
40
- if (parsed) {
41
- options.paths = parsed.compilerOptions?.paths;
42
- if (parsed.compilerOptions?.baseUrl) {
43
- options.baseUrl = resolve(cwd, parsed.compilerOptions.baseUrl);
44
- }
45
- else {
46
- options.baseUrl = cwd;
47
- }
48
- }
49
- }
50
- catch (err) {
51
- console.error(`Failed to parse ${tsJsConfig} aliases might not apply properly`, err);
52
- }
35
+ get shouldLogBaseBuilderInfo() {
36
+ return true;
37
+ }
38
+ logBaseBuilderInfo(...args) {
39
+ if (this.shouldLogBaseBuilderInfo) {
40
+ console.log(...args);
41
+ }
42
+ }
43
+ logCreateWorkflowsBundleInfo(...args) {
44
+ if (!this.config.suppressCreateWorkflowsBundleLogs) {
45
+ this.logBaseBuilderInfo(...args);
46
+ }
47
+ }
48
+ logCreateWebhookBundleInfo(...args) {
49
+ if (!this.config.suppressCreateWebhookBundleLogs) {
50
+ this.logBaseBuilderInfo(...args);
51
+ }
52
+ }
53
+ logCreateManifestInfo(...args) {
54
+ if (!this.config.suppressCreateManifestLogs) {
55
+ this.logBaseBuilderInfo(...args);
53
56
  }
54
- return options;
57
+ }
58
+ /**
59
+ * Finds tsconfig.json/jsconfig.json for the project.
60
+ * Used by esbuild to properly resolve module imports during bundling.
61
+ */
62
+ async findTsConfigPath() {
63
+ const cwd = this.config.workingDir || process.cwd();
64
+ return findUp(['tsconfig.json', 'jsconfig.json'], { cwd });
55
65
  }
56
66
  /**
57
67
  * Discovers all source files in the configured directories.
@@ -73,6 +83,7 @@ export class BaseBuilder {
73
83
  '**/.vercel/**',
74
84
  '**/.workflow-data/**',
75
85
  '**/.well-known/workflow/**',
86
+ '**/.svelte-kit/**',
76
87
  ],
77
88
  absolute: true,
78
89
  });
@@ -93,6 +104,7 @@ export class BaseBuilder {
93
104
  const state = {
94
105
  discoveredSteps: [],
95
106
  discoveredWorkflows: [],
107
+ discoveredSerdeFiles: [],
96
108
  };
97
109
  const discoverStart = Date.now();
98
110
  try {
@@ -104,30 +116,51 @@ export class BaseBuilder {
104
116
  write: false,
105
117
  outdir,
106
118
  bundle: true,
107
- sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
119
+ sourcemap: false,
108
120
  absWorkingDir: this.config.workingDir,
109
121
  logLevel: 'silent',
122
+ // External packages that should not be bundled during discovery
123
+ external: this.config.externalPackages || [],
110
124
  });
111
125
  }
112
126
  catch (_) { }
113
- console.log(`Discovering workflow directives`, `${Date.now() - discoverStart}ms`);
127
+ this.logBaseBuilderInfo(`Discovering workflow directives`, `${Date.now() - discoverStart}ms`);
114
128
  this.discoveredEntries.set(inputs, state);
115
129
  return state;
116
130
  }
117
131
  /**
118
132
  * Writes debug information to a JSON file for troubleshooting build issues.
119
- * Executes whenever called, regardless of environment variables.
133
+ * Uses atomic write (temp file + rename) to prevent race conditions when
134
+ * multiple builds run concurrently.
120
135
  */
121
136
  async writeDebugFile(outfile, debugData, merge) {
137
+ const prefix = this.config.debugFilePrefix || '';
138
+ const targetPath = `${dirname(outfile)}/${prefix}${basename(outfile)}.debug.json`;
139
+ let existing = {};
122
140
  try {
123
- let existing = {};
124
141
  if (merge) {
125
- existing = JSON.parse(await readFile(`${outfile}.debug.json`, 'utf8').catch(() => '{}'));
142
+ try {
143
+ const content = await readFile(targetPath, 'utf8');
144
+ existing = JSON.parse(content);
145
+ }
146
+ catch (e) {
147
+ // File doesn't exist yet or is corrupted - start fresh.
148
+ // Don't log error for ENOENT (file not found) as that's expected on first run.
149
+ if (e.code !== 'ENOENT') {
150
+ console.warn('Error reading debug file, starting fresh:', e);
151
+ }
152
+ }
126
153
  }
127
- await writeFile(`${outfile}.debug.json`, JSON.stringify({
154
+ const mergedData = JSON.stringify({
128
155
  ...existing,
129
156
  ...debugData,
130
- }, null, 2));
157
+ }, null, 2);
158
+ // Write atomically: write to temp file, then rename.
159
+ // rename() is atomic on POSIX systems and provides best-effort atomicity on Windows.
160
+ // Prevents race conditions where concurrent builds read partially-written files.
161
+ const tempPath = `${targetPath}.${randomUUID()}.tmp`;
162
+ await writeFile(tempPath, mergedData);
163
+ await rename(tempPath, targetPath);
131
164
  }
132
165
  catch (error) {
133
166
  console.warn('Failed to write debug file:', error);
@@ -137,7 +170,7 @@ export class BaseBuilder {
137
170
  * Logs and optionally throws on esbuild errors and warnings.
138
171
  * @param throwOnError - If true, throws an error when esbuild errors are present
139
172
  */
140
- logEsbuildMessages(result, phase, throwOnError = true) {
173
+ logEsbuildMessages(result, phase, throwOnError = true, options) {
141
174
  if (result.errors && result.errors.length > 0) {
142
175
  console.error(`❌ esbuild errors in ${phase}:`);
143
176
  const errorMessages = [];
@@ -154,7 +187,9 @@ export class BaseBuilder {
154
187
  throw new Error(`Build failed during ${phase}:\n${errorMessages.join('\n')}`);
155
188
  }
156
189
  }
157
- if (result.warnings && result.warnings.length > 0) {
190
+ if (!options?.suppressWarnings &&
191
+ result.warnings &&
192
+ result.warnings.length > 0) {
158
193
  console.warn(`! esbuild warnings in ${phase}:`);
159
194
  for (const warning of result.warnings) {
160
195
  console.warn(` ${warning.text}`);
@@ -164,18 +199,48 @@ export class BaseBuilder {
164
199
  }
165
200
  }
166
201
  }
202
+ /**
203
+ * Converts an absolute file path to a normalized relative path for the manifest.
204
+ */
205
+ getRelativeFilepath(absolutePath) {
206
+ const normalizedFile = absolutePath.replace(/\\/g, '/');
207
+ const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
208
+ let relativePath = relative(normalizedWorkingDir, normalizedFile).replace(/\\/g, '/');
209
+ // Handle files discovered outside the working directory
210
+ if (relativePath.startsWith('../')) {
211
+ relativePath = relativePath
212
+ .split('/')
213
+ .filter((part) => part !== '..')
214
+ .join('/');
215
+ }
216
+ return relativePath;
217
+ }
167
218
  /**
168
219
  * Creates a bundle for workflow step functions.
169
220
  * Steps have full Node.js runtime access and handle side effects, API calls, etc.
170
221
  *
171
222
  * @param externalizeNonSteps - If true, only bundles step entry points and externalizes other code
223
+ * @returns Build context (for watch mode) and the collected workflow manifest
172
224
  */
173
- async createStepsBundle({ inputFiles, format = 'cjs', outfile, externalizeNonSteps, tsBaseUrl, tsPaths, }) {
225
+ async createStepsBundle({ inputFiles, format = 'cjs', outfile, externalizeNonSteps, tsconfigPath, discoveredEntries, }) {
174
226
  // These need to handle watching for dev to scan for
175
227
  // new entries and changes to existing ones
176
- const { discoveredSteps: stepFiles } = await this.discoverEntries(inputFiles, dirname(outfile));
228
+ const discovered = discoveredEntries ??
229
+ (await this.discoverEntries(inputFiles, dirname(outfile)));
230
+ const stepFiles = [...discovered.discoveredSteps].sort();
231
+ const workflowFiles = [...discovered.discoveredWorkflows].sort();
232
+ const serdeFiles = [...discovered.discoveredSerdeFiles].sort();
233
+ // Include serde files that aren't already step files for cross-context class registration.
234
+ // Classes need to be registered in the step bundle so they can be deserialized
235
+ // when receiving data from workflows and serialized when returning data to workflows.
236
+ const stepFilesSet = new Set(stepFiles);
237
+ const serdeOnlyFiles = serdeFiles.filter((f) => !stepFilesSet.has(f));
177
238
  // log the step files for debugging
178
- await this.writeDebugFile(outfile, { stepFiles });
239
+ await this.writeDebugFile(outfile, {
240
+ stepFiles,
241
+ workflowFiles,
242
+ serdeOnlyFiles,
243
+ });
179
244
  const stepsBundleStart = Date.now();
180
245
  const workflowManifest = {};
181
246
  const builtInSteps = 'workflow/internal/builtins';
@@ -187,10 +252,16 @@ export class BaseBuilder {
187
252
  `Caused by: ${chalk.red(String(err))}`,
188
253
  ].join('\n'));
189
254
  });
190
- // Create a virtual entry that imports all files. All step definitions
191
- // will get registered thanks to the swc transform.
192
- const imports = stepFiles
193
- .map((file) => {
255
+ // Helper to create import statement from file path
256
+ // For workspace/node_modules packages, uses the package name so esbuild
257
+ // will resolve through package.json exports with the appropriate conditions
258
+ const createImport = (file) => {
259
+ const { importPath, isPackage } = getImportPath(file, this.config.workingDir);
260
+ if (isPackage) {
261
+ // Use package name - esbuild will resolve via package.json exports
262
+ return `import '${importPath}';`;
263
+ }
264
+ // Local app file - use relative path
194
265
  // Normalize both paths to forward slashes before calling relative()
195
266
  // This is critical on Windows where relative() can produce unexpected results with mixed path formats
196
267
  const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
@@ -202,16 +273,37 @@ export class BaseBuilder {
202
273
  relativePath = `./${relativePath}`;
203
274
  }
204
275
  return `import '${relativePath}';`;
205
- })
206
- .join('\n');
276
+ };
277
+ // Create a virtual entry that imports all files. All step definitions
278
+ // will get registered thanks to the swc transform.
279
+ const stepImports = stepFiles.map(createImport).join('\n');
280
+ // Include serde-only files for class registration side effects
281
+ const serdeImports = serdeOnlyFiles.map(createImport).join('\n');
207
282
  const entryContent = `
208
283
  // Built in steps
209
284
  import '${builtInSteps}';
210
285
  // User steps
211
- ${imports}
286
+ ${stepImports}
287
+ // Serde files for cross-context class registration
288
+ ${serdeImports}
212
289
  // API entrypoint
213
290
  export { stepEntrypoint as POST } from 'workflow/runtime';`;
214
291
  // Bundle with esbuild and our custom SWC plugin
292
+ const entriesToBundle = externalizeNonSteps
293
+ ? [
294
+ ...stepFiles,
295
+ ...serdeFiles,
296
+ ...(resolvedBuiltInSteps ? [resolvedBuiltInSteps] : []),
297
+ ]
298
+ : undefined;
299
+ const normalizedEntriesToBundle = entriesToBundle
300
+ ? Array.from(new Set((await Promise.all(entriesToBundle.map(async (entryToBundle) => {
301
+ const resolvedEntry = await realpath(entryToBundle).catch(() => undefined);
302
+ return resolvedEntry
303
+ ? [entryToBundle, resolvedEntry]
304
+ : [entryToBundle];
305
+ }))).flat()))
306
+ : undefined;
215
307
  const esbuildCtx = await esbuild.context({
216
308
  banner: {
217
309
  js: '// biome-ignore-all lint: generated file\n/* eslint-disable */\n',
@@ -233,42 +325,71 @@ export class BaseBuilder {
233
325
  treeShaking: true,
234
326
  keepNames: true,
235
327
  minify: false,
236
- resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
237
- // TODO: investigate proper source map support
238
- sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
328
+ jsx: 'preserve',
329
+ logLevel: 'error',
330
+ // Use tsconfig for path alias resolution
331
+ tsconfig: tsconfigPath,
332
+ resolveExtensions: [
333
+ '.ts',
334
+ '.tsx',
335
+ '.mts',
336
+ '.cts',
337
+ '.js',
338
+ '.jsx',
339
+ '.mjs',
340
+ '.cjs',
341
+ ],
342
+ // Inline source maps for better stack traces in step execution.
343
+ // Steps execute in Node.js context and inline sourcemaps ensure we get
344
+ // meaningful stack traces with proper file names and line numbers when errors
345
+ // occur in deeply nested function calls across multiple files.
346
+ sourcemap: 'inline',
239
347
  plugins: [
348
+ // Handle pseudo-packages like 'server-only' and 'client-only' by providing
349
+ // empty modules. Must run first to intercept these before other resolution.
350
+ createPseudoPackagePlugin(),
240
351
  createSwcPlugin({
241
352
  mode: 'step',
242
- entriesToBundle: externalizeNonSteps
243
- ? [
244
- ...stepFiles,
245
- ...(resolvedBuiltInSteps ? [resolvedBuiltInSteps] : []),
246
- ]
247
- : undefined,
353
+ entriesToBundle: normalizedEntriesToBundle,
248
354
  outdir: outfile ? dirname(outfile) : undefined,
249
- tsBaseUrl,
250
- tsPaths,
251
355
  workflowManifest,
252
356
  }),
253
357
  ],
254
358
  // Plugin should catch most things, but this lets users hard override
255
359
  // if the plugin misses anything that should be externalized
256
- external: this.config.externalPackages || [],
360
+ external: ['bun', 'bun:*', ...(this.config.externalPackages || [])],
257
361
  });
258
362
  const stepsResult = await esbuildCtx.rebuild();
259
363
  this.logEsbuildMessages(stepsResult, 'steps bundle creation');
260
- console.log('Created steps bundle', `${Date.now() - stepsBundleStart}ms`);
261
- const partialWorkflowManifest = {
262
- steps: workflowManifest.steps,
263
- };
264
- // always write to debug file
265
- await this.writeDebugFile(join(dirname(outfile), 'manifest'), partialWorkflowManifest, true);
364
+ this.logBaseBuilderInfo('Created steps bundle', `${Date.now() - stepsBundleStart}ms`);
365
+ // Handle workflow-only files that may have been tree-shaken from the bundle.
366
+ // These files have no steps, so esbuild removes them, but we still need their
367
+ // workflow metadata for the manifest. Transform them separately.
368
+ const workflowOnlyFiles = workflowFiles.filter((f) => !stepFiles.includes(f));
369
+ await Promise.all(workflowOnlyFiles.map(async (workflowFile) => {
370
+ try {
371
+ const source = await readFile(workflowFile, 'utf8');
372
+ const relativeFilepath = this.getRelativeFilepath(workflowFile);
373
+ const { workflowManifest: fileManifest } = await applySwcTransform(relativeFilepath, source, 'workflow');
374
+ if (fileManifest.workflows) {
375
+ workflowManifest.workflows = Object.assign(workflowManifest.workflows || {}, fileManifest.workflows);
376
+ }
377
+ if (fileManifest.classes) {
378
+ workflowManifest.classes = Object.assign(workflowManifest.classes || {}, fileManifest.classes);
379
+ }
380
+ }
381
+ catch (error) {
382
+ // Log warning but continue - don't fail build for workflow-only file issues
383
+ console.warn(`Warning: Failed to extract workflow metadata from ${workflowFile}:`, error instanceof Error ? error.message : String(error));
384
+ }
385
+ }));
266
386
  // Create .gitignore in .swc directory
267
387
  await this.createSwcGitignore();
268
388
  if (this.config.watch) {
269
- return esbuildCtx;
389
+ return { context: esbuildCtx, manifest: workflowManifest };
270
390
  }
271
391
  await esbuildCtx.dispose();
392
+ return { context: undefined, manifest: workflowManifest };
272
393
  }
273
394
  /**
274
395
  * Creates a bundle for workflow orchestration functions.
@@ -276,28 +397,50 @@ export class BaseBuilder {
276
397
  *
277
398
  * @param bundleFinalOutput - If false, skips the final bundling step (used by Next.js)
278
399
  */
279
- async createWorkflowsBundle({ inputFiles, format = 'cjs', outfile, bundleFinalOutput = true, tsBaseUrl, tsPaths, }) {
280
- const { discoveredWorkflows: workflowFiles } = await this.discoverEntries(inputFiles, dirname(outfile));
400
+ async createWorkflowsBundle({ inputFiles, format = 'cjs', outfile, bundleFinalOutput = true, tsconfigPath, discoveredEntries, }) {
401
+ const discovered = discoveredEntries ??
402
+ (await this.discoverEntries(inputFiles, dirname(outfile)));
403
+ const workflowFiles = [...discovered.discoveredWorkflows].sort();
404
+ const serdeFiles = [...discovered.discoveredSerdeFiles].sort();
405
+ // Include serde files that aren't already workflow files for cross-context class registration.
406
+ // Classes need to be registered in the workflow bundle so they can be deserialized
407
+ // when receiving data from steps or when serializing data to send to steps.
408
+ const workflowFilesSet = new Set(workflowFiles);
409
+ const serdeOnlyFiles = serdeFiles.filter((f) => !workflowFilesSet.has(f));
281
410
  // log the workflow files for debugging
282
- await this.writeDebugFile(outfile, { workflowFiles });
283
- // Create a virtual entry that imports all files
284
- const imports = `globalThis.__private_workflows = new Map();\n` +
285
- workflowFiles
286
- .map((file, workflowFileIdx) => {
287
- // Normalize both paths to forward slashes before calling relative()
288
- // This is critical on Windows where relative() can produce unexpected results with mixed path formats
289
- const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
290
- const normalizedFile = file.replace(/\\/g, '/');
291
- // Calculate relative path from working directory to the file
292
- let relativePath = relative(normalizedWorkingDir, normalizedFile).replace(/\\/g, '/');
293
- // Ensure relative paths start with ./ so esbuild resolves them correctly
294
- if (!relativePath.startsWith('.')) {
295
- relativePath = `./${relativePath}`;
296
- }
297
- return `import * as workflowFile${workflowFileIdx} from '${relativePath}';
298
- Object.values(workflowFile${workflowFileIdx}).map(item => item?.workflowId && globalThis.__private_workflows.set(item.workflowId, item))`;
299
- })
300
- .join('\n');
411
+ await this.writeDebugFile(outfile, { workflowFiles, serdeOnlyFiles });
412
+ // Helper to create import statement from file path
413
+ // For packages, uses the package name so esbuild will resolve through
414
+ // package.json exports with conditions: ['workflow']
415
+ const createImport = (file) => {
416
+ const { importPath, isPackage } = getImportPath(file, this.config.workingDir);
417
+ if (isPackage) {
418
+ // Use package name - esbuild will resolve via package.json exports
419
+ // and apply the 'workflow' condition
420
+ return `import '${importPath}';`;
421
+ }
422
+ // Local app file - use relative path
423
+ // Normalize both paths to forward slashes before calling relative()
424
+ // This is critical on Windows where relative() can produce unexpected results with mixed path formats
425
+ const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
426
+ const normalizedFile = file.replace(/\\/g, '/');
427
+ // Calculate relative path from working directory to the file
428
+ let relativePath = relative(normalizedWorkingDir, normalizedFile).replace(/\\/g, '/');
429
+ // Ensure relative paths start with ./ so esbuild resolves them correctly
430
+ if (!relativePath.startsWith('.')) {
431
+ relativePath = `./${relativePath}`;
432
+ }
433
+ return `import '${relativePath}';`;
434
+ };
435
+ // Create a virtual entry that imports all workflow files
436
+ // The SWC plugin in workflow mode emits `globalThis.__private_workflows.set(workflowId, fn)`
437
+ // calls directly, so we just need to import the files (Map is initialized via banner)
438
+ const workflowImports = workflowFiles.map(createImport).join('\n');
439
+ // Include serde-only files for class registration side effects
440
+ const serdeImports = serdeOnlyFiles.map(createImport).join('\n');
441
+ const imports = serdeImports
442
+ ? `${workflowImports}\n// Serde files for cross-context class registration\n${serdeImports}`
443
+ : workflowImports;
301
444
  const bundleStartTime = Date.now();
302
445
  const workflowManifest = {};
303
446
  // Bundle with esbuild and our custom SWC plugin in workflow mode.
@@ -320,28 +463,54 @@ export class BaseBuilder {
320
463
  treeShaking: true,
321
464
  keepNames: true,
322
465
  minify: false,
323
- // TODO: investigate proper source map support
324
- sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
325
- resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
466
+ // Initialize the workflow registry at the very top of the bundle
467
+ // This must be in banner (not the virtual entry) because esbuild's bundling
468
+ // can reorder code, and the .set() calls need the Map to exist first
469
+ banner: {
470
+ js: 'globalThis.__private_workflows = new Map();',
471
+ },
472
+ // Inline source maps for better stack traces in workflow VM execution.
473
+ // This intermediate bundle is executed via runInContext() in a VM, so we need
474
+ // inline source maps to get meaningful stack traces instead of "evalmachine.<anonymous>".
475
+ sourcemap: 'inline',
476
+ // Use tsconfig for path alias resolution
477
+ tsconfig: tsconfigPath,
478
+ resolveExtensions: [
479
+ '.ts',
480
+ '.tsx',
481
+ '.mts',
482
+ '.cts',
483
+ '.js',
484
+ '.jsx',
485
+ '.mjs',
486
+ '.cjs',
487
+ ],
326
488
  plugins: [
489
+ // Handle pseudo-packages like 'server-only' and 'client-only' by providing
490
+ // empty modules. Must run first to intercept these before other resolution.
491
+ createPseudoPackagePlugin(),
327
492
  createSwcPlugin({
328
493
  mode: 'workflow',
329
- tsBaseUrl,
330
- tsPaths,
331
494
  workflowManifest,
332
495
  }),
333
496
  // This plugin must run after the swc plugin to ensure dead code elimination
334
497
  // happens first, preventing false positives on Node.js imports in unused code paths
335
498
  createNodeModuleErrorPlugin(),
336
499
  ],
500
+ // NOTE: We intentionally do NOT use the external option here for workflow bundles.
501
+ // When packages are marked external with format: 'cjs', esbuild generates require() calls.
502
+ // However, the workflow VM (vm.runInContext) does not have require() defined - it only
503
+ // provides module.exports and exports. External packages would fail at runtime with:
504
+ // ReferenceError: require is not defined
505
+ // Instead, we bundle everything and rely on:
506
+ // - createPseudoPackagePlugin() to handle server-only/client-only with empty modules
507
+ // - createNodeModuleErrorPlugin() to catch Node.js builtin imports at build time
337
508
  });
338
509
  const interimBundle = await interimBundleCtx.rebuild();
339
- this.logEsbuildMessages(interimBundle, 'intermediate workflow bundle');
340
- console.log('Created intermediate workflow bundle', `${Date.now() - bundleStartTime}ms`);
341
- const partialWorkflowManifest = {
342
- workflows: workflowManifest.workflows,
343
- };
344
- await this.writeDebugFile(join(dirname(outfile), 'manifest'), partialWorkflowManifest, true);
510
+ this.logEsbuildMessages(interimBundle, 'intermediate workflow bundle', true, {
511
+ suppressWarnings: this.config.suppressCreateWorkflowsBundleWarnings,
512
+ });
513
+ this.logCreateWorkflowsBundleInfo('Created intermediate workflow bundle', `${Date.now() - bundleStartTime}ms`);
345
514
  if (this.config.workflowManifestPath) {
346
515
  const resolvedPath = resolve(process.cwd(), this.config.workflowManifestPath);
347
516
  let prefix = '';
@@ -353,7 +522,7 @@ export class BaseBuilder {
353
522
  prefix = 'export default ';
354
523
  }
355
524
  await mkdir(dirname(resolvedPath), { recursive: true });
356
- await writeFile(resolvedPath, prefix + JSON.stringify(workflowManifest.workflows, null, 2));
525
+ await writeFile(resolvedPath, prefix + JSON.stringify(workflowManifest, null, 2));
357
526
  }
358
527
  // Create .gitignore in .swc directory
359
528
  await this.createSwcGitignore();
@@ -377,7 +546,11 @@ export const POST = workflowEntrypoint(workflowCode);`;
377
546
  // Ensure the output directory exists
378
547
  const outputDir = dirname(outfile);
379
548
  await mkdir(outputDir, { recursive: true });
380
- await writeFile(outfile, workflowFunctionCode);
549
+ // Atomic write: write to temp file then rename to prevent
550
+ // file watchers from reading partial file during write
551
+ const tempPath = `${outfile}.${randomUUID()}.tmp`;
552
+ await writeFile(tempPath, workflowFunctionCode);
553
+ await rename(tempPath, outfile);
381
554
  return;
382
555
  }
383
556
  const bundleStartTime = Date.now();
@@ -394,7 +567,8 @@ export const POST = workflowEntrypoint(workflowCode);`;
394
567
  loader: 'js',
395
568
  },
396
569
  outfile,
397
- // TODO: investigate proper source map support
570
+ // Source maps for the final workflow bundle wrapper (not important since this code
571
+ // doesn't run in the VM - only the intermediate bundle sourcemap is relevant)
398
572
  sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
399
573
  absWorkingDir: this.config.workingDir,
400
574
  bundle: true,
@@ -406,17 +580,21 @@ export const POST = workflowEntrypoint(workflowCode);`;
406
580
  minify: false,
407
581
  external: ['@aws-sdk/credential-provider-web-identity'],
408
582
  });
409
- this.logEsbuildMessages(finalWorkflowResult, 'final workflow bundle');
410
- console.log('Created final workflow bundle', `${Date.now() - bundleStartTime}ms`);
583
+ this.logEsbuildMessages(finalWorkflowResult, 'final workflow bundle', true, {
584
+ suppressWarnings: this.config.suppressCreateWorkflowsBundleWarnings,
585
+ });
586
+ this.logCreateWorkflowsBundleInfo('Created final workflow bundle', `${Date.now() - bundleStartTime}ms`);
411
587
  };
412
588
  await bundleFinal(interimBundle.outputFiles[0].text);
413
589
  if (this.config.watch) {
414
590
  return {
591
+ manifest: workflowManifest,
415
592
  interimBundleCtx,
416
593
  bundleFinal,
417
594
  };
418
595
  }
419
596
  await interimBundleCtx.dispose();
597
+ return { manifest: workflowManifest };
420
598
  }
421
599
  /**
422
600
  * Creates a client library bundle for workflow execution.
@@ -428,23 +606,45 @@ export const POST = workflowEntrypoint(workflowCode);`;
428
606
  // Silently exit since no client bundle was requested
429
607
  return;
430
608
  }
431
- console.log('Generating a client library at', this.config.clientBundlePath);
432
- console.log('NOTE: The recommended way to use workflow with a framework like NextJS is using the loader/plugin with webpack/turbobpack/rollup');
609
+ this.logBaseBuilderInfo('Generating a client library at', this.config.clientBundlePath);
610
+ this.logBaseBuilderInfo('NOTE: The recommended way to use workflow with a framework like NextJS is using the loader/plugin with webpack/turbobpack/rollup');
433
611
  // Ensure we have the directory for the client bundle
434
612
  const outputDir = dirname(this.config.clientBundlePath);
435
613
  await mkdir(outputDir, { recursive: true });
436
614
  const inputFiles = await this.getInputFiles();
437
- // Create a virtual entry that imports all files
438
- const imports = inputFiles
615
+ // Discover serde files from the input files' dependency tree for cross-context class registration.
616
+ // Classes need to be registered in the client bundle so they can be serialized
617
+ // when passing data to workflows via start() and deserialized when receiving workflow results.
618
+ const { discoveredSerdeFiles } = await this.discoverEntries(inputFiles, outputDir);
619
+ // Identify serde files that aren't in the inputFiles (deduplicated)
620
+ const inputFilesNormalized = new Set(inputFiles.map((f) => f.replace(/\\/g, '/')));
621
+ const serdeOnlyFiles = discoveredSerdeFiles.filter((f) => !inputFilesNormalized.has(f));
622
+ // Re-exports for input files (user's workflow/step definitions)
623
+ const reexports = inputFiles
439
624
  .map((file) => `export * from '${file}';`)
440
625
  .join('\n');
626
+ // Side-effect imports for serde files not in inputFiles (for class registration)
627
+ const serdeImports = serdeOnlyFiles
628
+ .map((file) => {
629
+ const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
630
+ let relativePath = relative(normalizedWorkingDir, file).replace(/\\/g, '/');
631
+ if (!relativePath.startsWith('.')) {
632
+ relativePath = `./${relativePath}`;
633
+ }
634
+ return `import '${relativePath}';`;
635
+ })
636
+ .join('\n');
637
+ // Combine: serde imports (for registration side effects) + re-exports
638
+ const entryContent = serdeImports
639
+ ? `// Serde files for cross-context class registration\n${serdeImports}\n${reexports}`
640
+ : reexports;
441
641
  // Bundle with esbuild and our custom SWC plugin
442
642
  const clientResult = await esbuild.build({
443
643
  banner: {
444
644
  js: '// biome-ignore-all lint: generated file\n/* eslint-disable */\n',
445
645
  },
446
646
  stdin: {
447
- contents: imports,
647
+ contents: entryContent,
448
648
  resolveDir: this.config.workingDir,
449
649
  sourcefile: 'virtual-entry.js',
450
650
  loader: 'js',
@@ -453,11 +653,21 @@ export const POST = workflowEntrypoint(workflowCode);`;
453
653
  bundle: true,
454
654
  format: 'esm',
455
655
  platform: 'node',
656
+ jsx: 'preserve',
456
657
  target: 'es2022',
457
658
  write: true,
458
659
  treeShaking: true,
459
660
  external: ['@workflow/core'],
460
- resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
661
+ resolveExtensions: [
662
+ '.ts',
663
+ '.tsx',
664
+ '.mts',
665
+ '.cts',
666
+ '.js',
667
+ '.jsx',
668
+ '.mjs',
669
+ '.cjs',
670
+ ],
461
671
  plugins: [createSwcPlugin({ mode: 'client' })],
462
672
  });
463
673
  this.logEsbuildMessages(clientResult, 'client library bundle');
@@ -470,7 +680,7 @@ export const POST = workflowEntrypoint(workflowCode);`;
470
680
  * @param bundle - If true, bundles dependencies (needed for Build Output API)
471
681
  */
472
682
  async createWebhookBundle({ outfile, bundle = false, }) {
473
- console.log('Creating webhook route');
683
+ this.logCreateWebhookBundleInfo('Creating webhook route');
474
684
  await mkdir(dirname(outfile), { recursive: true });
475
685
  // Create a static route that calls resumeWebhook
476
686
  // This route works for both Next.js and Vercel Build Output API
@@ -512,7 +722,7 @@ export const OPTIONS = handler;`;
512
722
  const webhookBundleStart = Date.now();
513
723
  const result = await esbuild.build({
514
724
  banner: {
515
- js: '// biome-ignore-all lint: generated file\n/* eslint-disable */\n',
725
+ js: `// biome-ignore-all lint: generated file\n/* eslint-disable */`,
516
726
  },
517
727
  stdin: {
518
728
  contents: routeContent,
@@ -523,6 +733,7 @@ export const OPTIONS = handler;`;
523
733
  outfile,
524
734
  absWorkingDir: this.config.workingDir,
525
735
  bundle: true,
736
+ jsx: 'preserve',
526
737
  format: 'cjs',
527
738
  platform: 'node',
528
739
  conditions: ['import', 'module', 'node', 'default'],
@@ -531,14 +742,23 @@ export const OPTIONS = handler;`;
531
742
  treeShaking: true,
532
743
  keepNames: true,
533
744
  minify: false,
534
- resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
535
- sourcemap: false,
745
+ resolveExtensions: [
746
+ '.ts',
747
+ '.tsx',
748
+ '.mts',
749
+ '.cts',
750
+ '.js',
751
+ '.jsx',
752
+ '.mjs',
753
+ '.cjs',
754
+ ],
755
+ sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
536
756
  mainFields: ['module', 'main'],
537
757
  // Don't externalize anything - bundle everything including workflow packages
538
758
  external: [],
539
759
  });
540
760
  this.logEsbuildMessages(result, 'webhook bundle creation');
541
- console.log('Created webhook bundle', `${Date.now() - webhookBundleStart}ms`);
761
+ this.logCreateWebhookBundleInfo('Created webhook bundle', `${Date.now() - webhookBundleStart}ms`);
542
762
  }
543
763
  /**
544
764
  * Creates a package.json file with the specified module type.
@@ -586,5 +806,108 @@ export const OPTIONS = handler;`;
586
806
  // We're intentionally silently ignoring this error - creating .gitignore isn't critical
587
807
  }
588
808
  }
809
+ /**
810
+ * Whether the manifest should be exposed as a public HTTP route.
811
+ * Controlled by the `WORKFLOW_PUBLIC_MANIFEST` environment variable.
812
+ */
813
+ get shouldExposePublicManifest() {
814
+ return process.env.WORKFLOW_PUBLIC_MANIFEST === '1';
815
+ }
816
+ /**
817
+ * Whether diagnostics artifacts should be emitted to Vercel output.
818
+ * This is enabled when the resolved world target is Vercel.
819
+ */
820
+ get shouldEmitVercelDiagnostics() {
821
+ return (usesVercelWorld() || this.config.buildTarget === 'vercel-build-output-api');
822
+ }
823
+ /**
824
+ * Creates a manifest JSON file containing step/workflow/class metadata
825
+ * and graph data for visualization.
826
+ *
827
+ * @returns The manifest JSON string, or undefined if manifest creation failed.
828
+ */
829
+ async createManifest({ workflowBundlePath, manifestDir, manifest, }) {
830
+ const buildStart = Date.now();
831
+ this.logCreateManifestInfo('Creating manifest...');
832
+ try {
833
+ const workflowGraphs = await extractWorkflowGraphs(workflowBundlePath);
834
+ const steps = this.convertStepsManifest(manifest.steps);
835
+ const workflows = this.convertWorkflowsManifest(manifest.workflows, workflowGraphs);
836
+ const classes = this.convertClassesManifest(manifest.classes);
837
+ const output = { version: '1.0.0', steps, workflows, classes };
838
+ const manifestJson = JSON.stringify(output, null, 2);
839
+ await mkdir(manifestDir, { recursive: true });
840
+ await writeFile(join(manifestDir, 'manifest.json'), manifestJson);
841
+ if (this.shouldEmitVercelDiagnostics) {
842
+ const diagnosticsManifestPath = this.resolvePath('.vercel/output/diagnostics/workflows-manifest.json');
843
+ await this.ensureDirectory(diagnosticsManifestPath);
844
+ await writeFile(diagnosticsManifestPath, manifestJson);
845
+ }
846
+ const stepCount = Object.values(steps).reduce((acc, s) => acc + Object.keys(s).length, 0);
847
+ const workflowCount = Object.values(workflows).reduce((acc, w) => acc + Object.keys(w).length, 0);
848
+ const classCount = Object.values(classes).reduce((acc, c) => acc + Object.keys(c).length, 0);
849
+ this.logCreateManifestInfo(`Created manifest with ${stepCount} ${pluralize('step', 'steps', stepCount)}, ${workflowCount} ${pluralize('workflow', 'workflows', workflowCount)}, and ${classCount} ${pluralize('class', 'classes', classCount)}`, `${Date.now() - buildStart}ms`);
850
+ return manifestJson;
851
+ }
852
+ catch (error) {
853
+ console.warn('Failed to create manifest:', error instanceof Error ? error.message : String(error));
854
+ return undefined;
855
+ }
856
+ }
857
+ convertStepsManifest(steps) {
858
+ const result = {};
859
+ if (!steps)
860
+ return result;
861
+ for (const [filePath, entries] of Object.entries(steps)) {
862
+ result[filePath] = {};
863
+ for (const [name, data] of Object.entries(entries)) {
864
+ result[filePath][name] = { stepId: data.stepId };
865
+ }
866
+ }
867
+ return result;
868
+ }
869
+ convertWorkflowsManifest(workflows, graphs) {
870
+ const result = {};
871
+ if (!workflows)
872
+ return result;
873
+ // Build a normalized lookup for graphs since the graph extractor uses
874
+ // paths from workflowId (e.g. "./workflows/hello-agent") while the
875
+ // manifest uses source file paths (e.g. "workflows/hello-agent.ts").
876
+ // Normalize by stripping leading "./" and file extensions.
877
+ const normalizedGraphs = new Map();
878
+ for (const [graphPath, graphEntries] of Object.entries(graphs)) {
879
+ const normalized = graphPath
880
+ .replace(/^\.\//, '')
881
+ .replace(/\.[^/.]+$/, '');
882
+ normalizedGraphs.set(normalized, graphEntries);
883
+ }
884
+ for (const [filePath, entries] of Object.entries(workflows)) {
885
+ result[filePath] = {};
886
+ // Normalize the manifest file path for lookup
887
+ const normalizedFilePath = filePath
888
+ .replace(/^\.\//, '')
889
+ .replace(/\.[^/.]+$/, '');
890
+ const graphEntries = graphs[filePath] || normalizedGraphs.get(normalizedFilePath);
891
+ for (const [name, data] of Object.entries(entries)) {
892
+ result[filePath][name] = {
893
+ workflowId: data.workflowId,
894
+ graph: graphEntries?.[name]?.graph || { nodes: [], edges: [] },
895
+ };
896
+ }
897
+ }
898
+ return result;
899
+ }
900
+ convertClassesManifest(classes) {
901
+ const result = {};
902
+ if (!classes)
903
+ return result;
904
+ for (const [filePath, entries] of Object.entries(classes)) {
905
+ result[filePath] = {};
906
+ for (const [name, data] of Object.entries(entries)) {
907
+ result[filePath][name] = { classId: data.classId };
908
+ }
909
+ }
910
+ return result;
911
+ }
589
912
  }
590
913
  //# sourceMappingURL=base-builder.js.map