@workflow/builders 4.0.1-beta.6 → 4.0.1-beta.61

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