@workflow/builders 4.0.1-beta.6 → 4.0.1-beta.60
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.
- package/dist/apply-swc-transform.d.ts +14 -4
- package/dist/apply-swc-transform.d.ts.map +1 -1
- package/dist/apply-swc-transform.js +59 -9
- package/dist/apply-swc-transform.js.map +1 -1
- package/dist/base-builder.d.ts +65 -22
- package/dist/base-builder.d.ts.map +1 -1
- package/dist/base-builder.js +482 -136
- package/dist/base-builder.js.map +1 -1
- package/dist/build-queue.d.ts +18 -0
- package/dist/build-queue.d.ts.map +1 -0
- package/dist/build-queue.js +26 -0
- package/dist/build-queue.js.map +1 -0
- package/dist/config-helpers.d.ts +31 -0
- package/dist/config-helpers.d.ts.map +1 -1
- package/dist/config-helpers.js +60 -0
- package/dist/config-helpers.js.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +2 -2
- package/dist/discover-entries-esbuild-plugin.d.ts +2 -3
- package/dist/discover-entries-esbuild-plugin.d.ts.map +1 -1
- package/dist/discover-entries-esbuild-plugin.js +71 -26
- package/dist/discover-entries-esbuild-plugin.js.map +1 -1
- package/dist/get-input-files.test.d.ts +2 -0
- package/dist/get-input-files.test.d.ts.map +1 -0
- package/dist/get-input-files.test.js +119 -0
- package/dist/get-input-files.test.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/module-specifier.d.ts +85 -0
- package/dist/module-specifier.d.ts.map +1 -0
- package/dist/module-specifier.js +448 -0
- package/dist/module-specifier.js.map +1 -0
- package/dist/module-specifier.test.d.ts +2 -0
- package/dist/module-specifier.test.d.ts.map +1 -0
- package/dist/module-specifier.test.js +211 -0
- package/dist/module-specifier.test.js.map +1 -0
- package/dist/node-module-esbuild-plugin.d.ts +37 -0
- package/dist/node-module-esbuild-plugin.d.ts.map +1 -1
- package/dist/node-module-esbuild-plugin.js +286 -7
- package/dist/node-module-esbuild-plugin.js.map +1 -1
- package/dist/node-module-esbuild-plugin.test.js +347 -146
- package/dist/node-module-esbuild-plugin.test.js.map +1 -1
- package/dist/pseudo-package-esbuild-plugin.d.ts +20 -0
- package/dist/pseudo-package-esbuild-plugin.d.ts.map +1 -0
- package/dist/pseudo-package-esbuild-plugin.js +47 -0
- package/dist/pseudo-package-esbuild-plugin.js.map +1 -0
- package/dist/pseudo-package-esbuild-plugin.test.d.ts +2 -0
- package/dist/pseudo-package-esbuild-plugin.test.d.ts.map +1 -0
- package/dist/pseudo-package-esbuild-plugin.test.js +315 -0
- package/dist/pseudo-package-esbuild-plugin.test.js.map +1 -0
- package/dist/request-converter.d.ts +3 -0
- package/dist/request-converter.d.ts.map +1 -0
- package/dist/request-converter.js +14 -0
- package/dist/request-converter.js.map +1 -0
- package/dist/standalone.d.ts +3 -0
- package/dist/standalone.d.ts.map +1 -1
- package/dist/standalone.js +37 -13
- package/dist/standalone.js.map +1 -1
- package/dist/swc-esbuild-plugin.d.ts +0 -2
- package/dist/swc-esbuild-plugin.d.ts.map +1 -1
- package/dist/swc-esbuild-plugin.js +33 -15
- package/dist/swc-esbuild-plugin.js.map +1 -1
- package/dist/transform-utils.d.ts +68 -0
- package/dist/transform-utils.d.ts.map +1 -0
- package/dist/transform-utils.js +102 -0
- package/dist/transform-utils.js.map +1 -0
- package/dist/transform-utils.test.d.ts +2 -0
- package/dist/transform-utils.test.d.ts.map +1 -0
- package/dist/transform-utils.test.js +283 -0
- package/dist/transform-utils.test.js.map +1 -0
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/vercel-build-output-api.d.ts.map +1 -1
- package/dist/vercel-build-output-api.js +36 -14
- package/dist/vercel-build-output-api.js.map +1 -1
- package/dist/workflow-alias.d.ts +3 -0
- package/dist/workflow-alias.d.ts.map +1 -0
- package/dist/workflow-alias.js +46 -0
- package/dist/workflow-alias.js.map +1 -0
- package/dist/workflow-alias.test.d.ts +2 -0
- package/dist/workflow-alias.test.d.ts.map +1 -0
- package/dist/workflow-alias.test.js +46 -0
- package/dist/workflow-alias.test.js.map +1 -0
- package/dist/workflows-extractor.d.ts +92 -0
- package/dist/workflows-extractor.d.ts.map +1 -0
- package/dist/workflows-extractor.js +1476 -0
- package/dist/workflows-extractor.js.map +1 -0
- package/package.json +12 -11
package/dist/base-builder.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
28
|
-
*
|
|
32
|
+
* Whether informational BaseBuilder logs should be printed.
|
|
33
|
+
* Subclasses can override this to silence progress logs while keeping warnings/errors.
|
|
29
34
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
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, {
|
|
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
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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:
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
await
|
|
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,
|
|
280
|
-
const
|
|
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
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
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:
|
|
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: [
|
|
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
|
-
|
|
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:
|
|
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: [
|
|
535
|
-
|
|
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
|
-
|
|
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
|