@workflow/builders 4.0.1-beta.9 → 5.0.0-beta.0
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/README.md +1 -1
- package/dist/apply-swc-transform.d.ts +19 -4
- package/dist/apply-swc-transform.d.ts.map +1 -1
- package/dist/apply-swc-transform.js +62 -7
- package/dist/apply-swc-transform.js.map +1 -1
- package/dist/base-builder.d.ts +77 -22
- package/dist/base-builder.d.ts.map +1 -1
- package/dist/base-builder.js +667 -202
- 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 +32 -0
- package/dist/config-helpers.d.ts.map +1 -1
- package/dist/config-helpers.js +61 -0
- package/dist/config-helpers.js.map +1 -1
- package/dist/constants.d.ts +2 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -4
- package/dist/constants.js.map +1 -1
- package/dist/discover-entries-esbuild-plugin.d.ts +5 -6
- package/dist/discover-entries-esbuild-plugin.d.ts.map +1 -1
- package/dist/discover-entries-esbuild-plugin.js +98 -30
- package/dist/discover-entries-esbuild-plugin.js.map +1 -1
- package/dist/discover-entries-esbuild-plugin.test.d.ts +2 -0
- package/dist/discover-entries-esbuild-plugin.test.d.ts.map +1 -0
- package/dist/discover-entries-esbuild-plugin.test.js +110 -0
- package/dist/discover-entries-esbuild-plugin.test.js.map +1 -0
- package/dist/esbuild-tsconfig.d.ts +13 -0
- package/dist/esbuild-tsconfig.d.ts.map +1 -0
- package/dist/esbuild-tsconfig.js +25 -0
- package/dist/esbuild-tsconfig.js.map +1 -0
- package/dist/esbuild-tsconfig.test.d.ts +2 -0
- package/dist/esbuild-tsconfig.test.d.ts.map +1 -0
- package/dist/esbuild-tsconfig.test.js +94 -0
- package/dist/esbuild-tsconfig.test.js.map +1 -0
- 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 +121 -0
- package/dist/get-input-files.test.js.map +1 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -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 +240 -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 +364 -7
- package/dist/node-module-esbuild-plugin.js.map +1 -1
- package/dist/node-module-esbuild-plugin.test.js +461 -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/serde-checker.d.ts +70 -0
- package/dist/serde-checker.d.ts.map +1 -0
- package/dist/serde-checker.js +118 -0
- package/dist/serde-checker.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 +23 -2
- package/dist/swc-esbuild-plugin.d.ts.map +1 -1
- package/dist/swc-esbuild-plugin.js +133 -43
- package/dist/swc-esbuild-plugin.js.map +1 -1
- package/dist/swc-esbuild-plugin.test.d.ts +2 -0
- package/dist/swc-esbuild-plugin.test.d.ts.map +1 -0
- package/dist/swc-esbuild-plugin.test.js +493 -0
- package/dist/swc-esbuild-plugin.test.js.map +1 -0
- 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 +302 -0
- package/dist/transform-utils.test.js.map +1 -0
- package/dist/types.d.ts +31 -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 +38 -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 +65 -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 +53 -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 +14 -13
package/dist/base-builder.js
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
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 { getEsbuildTsconfigOptions } from './esbuild-tsconfig.js';
|
|
14
|
+
import { getImportPath } from './module-specifier.js';
|
|
11
15
|
import { createNodeModuleErrorPlugin } from './node-module-esbuild-plugin.js';
|
|
16
|
+
import { createPseudoPackagePlugin } from './pseudo-package-esbuild-plugin.js';
|
|
12
17
|
import { createSwcPlugin } from './swc-esbuild-plugin.js';
|
|
18
|
+
import { extractWorkflowGraphs } from './workflows-extractor.js';
|
|
13
19
|
const enhancedResolve = promisify(enhancedResolveOriginal);
|
|
14
20
|
const EMIT_SOURCEMAPS_FOR_DEBUGGING = process.env.WORKFLOW_EMIT_SOURCEMAPS_FOR_DEBUGGING === '1';
|
|
21
|
+
/**
|
|
22
|
+
* Normalize an array of file paths by appending the `realpath()` of each entry
|
|
23
|
+
* (to handle symlinks, e.g. pnpm/workspace layouts) and deduplicating.
|
|
24
|
+
*/
|
|
25
|
+
async function withRealpaths(entries) {
|
|
26
|
+
return Array.from(new Set((await Promise.all(entries.map(async (entry) => {
|
|
27
|
+
const resolved = await realpath(entry).catch(() => undefined);
|
|
28
|
+
return resolved ? [entry, resolved] : [entry];
|
|
29
|
+
}))).flat()));
|
|
30
|
+
}
|
|
15
31
|
/**
|
|
16
32
|
* Base class for workflow builders. Provides common build logic for transforming
|
|
17
33
|
* workflow source files into deployable bundles using esbuild and SWC.
|
|
@@ -23,35 +39,63 @@ export class BaseBuilder {
|
|
|
23
39
|
constructor(config) {
|
|
24
40
|
this.config = config;
|
|
25
41
|
}
|
|
42
|
+
get transformProjectRoot() {
|
|
43
|
+
return this.config.projectRoot || this.config.workingDir;
|
|
44
|
+
}
|
|
26
45
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
46
|
+
* Whether informational BaseBuilder logs should be printed.
|
|
47
|
+
* Subclasses can override this to silence progress logs while keeping warnings/errors.
|
|
29
48
|
*/
|
|
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
|
-
}
|
|
49
|
+
get shouldLogBaseBuilderInfo() {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
logBaseBuilderInfo(...args) {
|
|
53
|
+
if (this.shouldLogBaseBuilderInfo) {
|
|
54
|
+
console.log(...args);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
logCreateWorkflowsBundleInfo(...args) {
|
|
58
|
+
if (!this.config.suppressCreateWorkflowsBundleLogs) {
|
|
59
|
+
this.logBaseBuilderInfo(...args);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
logCreateWebhookBundleInfo(...args) {
|
|
63
|
+
if (!this.config.suppressCreateWebhookBundleLogs) {
|
|
64
|
+
this.logBaseBuilderInfo(...args);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
logCreateManifestInfo(...args) {
|
|
68
|
+
if (!this.config.suppressCreateManifestLogs) {
|
|
69
|
+
this.logBaseBuilderInfo(...args);
|
|
53
70
|
}
|
|
54
|
-
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* When outputting CJS, esbuild replaces `import.meta` with an empty object,
|
|
74
|
+
* making `import.meta.url` (and `import.meta.resolve`) undefined. This method
|
|
75
|
+
* returns banner code and `define` entries that polyfill them using CJS
|
|
76
|
+
* equivalents (`__filename`, `require.resolve`) so user code (e.g. Prisma)
|
|
77
|
+
* that relies on `import.meta.url` works correctly in bundled CJS output.
|
|
78
|
+
*/
|
|
79
|
+
getCjsImportMetaPolyfill(format) {
|
|
80
|
+
if (format !== 'cjs')
|
|
81
|
+
return { banner: '', define: {} };
|
|
82
|
+
return {
|
|
83
|
+
banner: 'var __import_meta_url = typeof __filename !== "undefined" ? require("url").pathToFileURL(__filename).href : undefined;\n' +
|
|
84
|
+
'var __import_meta_resolve = typeof require !== "undefined" && typeof __filename !== "undefined" ' +
|
|
85
|
+
'? (s) => require("url").pathToFileURL(require.resolve(s)).href : undefined;\n',
|
|
86
|
+
define: {
|
|
87
|
+
'import.meta.url': '__import_meta_url',
|
|
88
|
+
'import.meta.resolve': '__import_meta_resolve',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Finds tsconfig.json/jsconfig.json for the project.
|
|
94
|
+
* Used by esbuild to properly resolve module imports during bundling.
|
|
95
|
+
*/
|
|
96
|
+
async findTsConfigPath() {
|
|
97
|
+
const cwd = this.config.workingDir || process.cwd();
|
|
98
|
+
return findUp(['tsconfig.json', 'jsconfig.json'], { cwd });
|
|
55
99
|
}
|
|
56
100
|
/**
|
|
57
101
|
* Discovers all source files in the configured directories.
|
|
@@ -59,24 +103,36 @@ export class BaseBuilder {
|
|
|
59
103
|
* and dependency directories.
|
|
60
104
|
*/
|
|
61
105
|
async getInputFiles() {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
106
|
+
const ignore = [
|
|
107
|
+
'**/node_modules/**',
|
|
108
|
+
'**/.git/**',
|
|
109
|
+
'**/.next/**',
|
|
110
|
+
'**/.nuxt/**',
|
|
111
|
+
'**/.output/**',
|
|
112
|
+
'**/.vercel/**',
|
|
113
|
+
'**/.workflow-data/**',
|
|
114
|
+
'**/.workflow-vitest/**',
|
|
115
|
+
'**/.well-known/workflow/**',
|
|
116
|
+
'**/.svelte-kit/**',
|
|
117
|
+
'**/.turbo/**',
|
|
118
|
+
'**/.cache/**',
|
|
119
|
+
'**/.yarn/**',
|
|
120
|
+
'**/.pnpm-store/**',
|
|
121
|
+
];
|
|
122
|
+
// Use relative patterns with `cwd` per directory so that `dot: true`
|
|
123
|
+
// applies consistently to both the search pattern *and* the ignore
|
|
124
|
+
// patterns. When absolute patterns are used with tinyglobby, the `**`
|
|
125
|
+
// in ignore patterns does not match dot-prefixed path segments.
|
|
126
|
+
const results = await Promise.all(this.config.dirs.map((dir) => {
|
|
127
|
+
const cwd = resolve(this.config.workingDir, dir);
|
|
128
|
+
return glob(['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], {
|
|
129
|
+
cwd,
|
|
130
|
+
ignore,
|
|
131
|
+
absolute: true,
|
|
132
|
+
dot: true,
|
|
133
|
+
});
|
|
134
|
+
}));
|
|
135
|
+
return results.flat();
|
|
80
136
|
}
|
|
81
137
|
/**
|
|
82
138
|
* Caches discovered workflow entries by input array reference.
|
|
@@ -85,49 +141,76 @@ export class BaseBuilder {
|
|
|
85
141
|
* (e.g., when files are added/removed during watch mode).
|
|
86
142
|
*/
|
|
87
143
|
discoveredEntries = new WeakMap();
|
|
88
|
-
async discoverEntries(inputs, outdir) {
|
|
144
|
+
async discoverEntries(inputs, outdir, tsconfigPath) {
|
|
89
145
|
const previousResult = this.discoveredEntries.get(inputs);
|
|
90
146
|
if (previousResult) {
|
|
91
147
|
return previousResult;
|
|
92
148
|
}
|
|
93
149
|
const state = {
|
|
94
|
-
discoveredSteps:
|
|
95
|
-
discoveredWorkflows:
|
|
150
|
+
discoveredSteps: new Set(),
|
|
151
|
+
discoveredWorkflows: new Set(),
|
|
152
|
+
discoveredSerdeFiles: new Set(),
|
|
96
153
|
};
|
|
97
154
|
const discoverStart = Date.now();
|
|
155
|
+
const effectiveTsconfigPath = tsconfigPath ?? (await this.findTsConfigPath());
|
|
156
|
+
const esbuildTsconfigOptions = await getEsbuildTsconfigOptions(effectiveTsconfigPath);
|
|
98
157
|
try {
|
|
99
158
|
await esbuild.build({
|
|
100
159
|
treeShaking: true,
|
|
101
160
|
entryPoints: inputs,
|
|
102
|
-
plugins: [
|
|
161
|
+
plugins: [
|
|
162
|
+
createDiscoverEntriesPlugin(state, this.transformProjectRoot),
|
|
163
|
+
],
|
|
103
164
|
platform: 'node',
|
|
104
165
|
write: false,
|
|
105
166
|
outdir,
|
|
106
167
|
bundle: true,
|
|
107
|
-
sourcemap:
|
|
168
|
+
sourcemap: false,
|
|
108
169
|
absWorkingDir: this.config.workingDir,
|
|
109
170
|
logLevel: 'silent',
|
|
171
|
+
...esbuildTsconfigOptions,
|
|
172
|
+
// External packages that should not be bundled during discovery
|
|
173
|
+
external: this.config.externalPackages || [],
|
|
110
174
|
});
|
|
111
175
|
}
|
|
112
176
|
catch (_) { }
|
|
113
|
-
|
|
177
|
+
this.logBaseBuilderInfo(`Discovering workflow directives`, `${Date.now() - discoverStart}ms`);
|
|
114
178
|
this.discoveredEntries.set(inputs, state);
|
|
115
179
|
return state;
|
|
116
180
|
}
|
|
117
181
|
/**
|
|
118
182
|
* Writes debug information to a JSON file for troubleshooting build issues.
|
|
119
|
-
*
|
|
183
|
+
* Uses atomic write (temp file + rename) to prevent race conditions when
|
|
184
|
+
* multiple builds run concurrently.
|
|
120
185
|
*/
|
|
121
186
|
async writeDebugFile(outfile, debugData, merge) {
|
|
187
|
+
const prefix = this.config.debugFilePrefix || '';
|
|
188
|
+
const targetPath = `${dirname(outfile)}/${prefix}${basename(outfile)}.debug.json`;
|
|
189
|
+
let existing = {};
|
|
122
190
|
try {
|
|
123
|
-
let existing = {};
|
|
124
191
|
if (merge) {
|
|
125
|
-
|
|
192
|
+
try {
|
|
193
|
+
const content = await readFile(targetPath, 'utf8');
|
|
194
|
+
existing = JSON.parse(content);
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
// File doesn't exist yet or is corrupted - start fresh.
|
|
198
|
+
// Don't log error for ENOENT (file not found) as that's expected on first run.
|
|
199
|
+
if (e.code !== 'ENOENT') {
|
|
200
|
+
console.warn('Error reading debug file, starting fresh:', e);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
126
203
|
}
|
|
127
|
-
|
|
204
|
+
const mergedData = JSON.stringify({
|
|
128
205
|
...existing,
|
|
129
206
|
...debugData,
|
|
130
|
-
}, null, 2)
|
|
207
|
+
}, null, 2);
|
|
208
|
+
// Write atomically: write to temp file, then rename.
|
|
209
|
+
// rename() is atomic on POSIX systems and provides best-effort atomicity on Windows.
|
|
210
|
+
// Prevents race conditions where concurrent builds read partially-written files.
|
|
211
|
+
const tempPath = `${targetPath}.${randomUUID()}.tmp`;
|
|
212
|
+
await writeFile(tempPath, mergedData);
|
|
213
|
+
await rename(tempPath, targetPath);
|
|
131
214
|
}
|
|
132
215
|
catch (error) {
|
|
133
216
|
console.warn('Failed to write debug file:', error);
|
|
@@ -137,7 +220,7 @@ export class BaseBuilder {
|
|
|
137
220
|
* Logs and optionally throws on esbuild errors and warnings.
|
|
138
221
|
* @param throwOnError - If true, throws an error when esbuild errors are present
|
|
139
222
|
*/
|
|
140
|
-
logEsbuildMessages(result, phase, throwOnError = true) {
|
|
223
|
+
logEsbuildMessages(result, phase, throwOnError = true, options) {
|
|
141
224
|
if (result.errors && result.errors.length > 0) {
|
|
142
225
|
console.error(`❌ esbuild errors in ${phase}:`);
|
|
143
226
|
const errorMessages = [];
|
|
@@ -154,7 +237,9 @@ export class BaseBuilder {
|
|
|
154
237
|
throw new Error(`Build failed during ${phase}:\n${errorMessages.join('\n')}`);
|
|
155
238
|
}
|
|
156
239
|
}
|
|
157
|
-
if (
|
|
240
|
+
if (!options?.suppressWarnings &&
|
|
241
|
+
result.warnings &&
|
|
242
|
+
result.warnings.length > 0) {
|
|
158
243
|
console.warn(`! esbuild warnings in ${phase}:`);
|
|
159
244
|
for (const warning of result.warnings) {
|
|
160
245
|
console.warn(` ${warning.text}`);
|
|
@@ -164,18 +249,48 @@ export class BaseBuilder {
|
|
|
164
249
|
}
|
|
165
250
|
}
|
|
166
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Converts an absolute file path to a normalized relative path for the manifest.
|
|
254
|
+
*/
|
|
255
|
+
getRelativeFilepath(absolutePath) {
|
|
256
|
+
const normalizedFile = absolutePath.replace(/\\/g, '/');
|
|
257
|
+
const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
|
|
258
|
+
let relativePath = relative(normalizedWorkingDir, normalizedFile).replace(/\\/g, '/');
|
|
259
|
+
// Handle files discovered outside the working directory
|
|
260
|
+
if (relativePath.startsWith('../')) {
|
|
261
|
+
relativePath = relativePath
|
|
262
|
+
.split('/')
|
|
263
|
+
.filter((part) => part !== '..')
|
|
264
|
+
.join('/');
|
|
265
|
+
}
|
|
266
|
+
return relativePath;
|
|
267
|
+
}
|
|
167
268
|
/**
|
|
168
269
|
* Creates a bundle for workflow step functions.
|
|
169
270
|
* Steps have full Node.js runtime access and handle side effects, API calls, etc.
|
|
170
271
|
*
|
|
171
272
|
* @param externalizeNonSteps - If true, only bundles step entry points and externalizes other code
|
|
273
|
+
* @returns Build context (for watch mode) and the collected workflow manifest
|
|
172
274
|
*/
|
|
173
|
-
async createStepsBundle({ inputFiles, format = 'cjs', outfile, externalizeNonSteps,
|
|
275
|
+
async createStepsBundle({ inputFiles, format = 'cjs', outfile, externalizeNonSteps, rewriteTsExtensions, tsconfigPath, discoveredEntries, }) {
|
|
174
276
|
// These need to handle watching for dev to scan for
|
|
175
277
|
// new entries and changes to existing ones
|
|
176
|
-
const
|
|
278
|
+
const discovered = discoveredEntries ??
|
|
279
|
+
(await this.discoverEntries(inputFiles, dirname(outfile), tsconfigPath));
|
|
280
|
+
const stepFiles = [...discovered.discoveredSteps].sort();
|
|
281
|
+
const workflowFiles = [...discovered.discoveredWorkflows].sort();
|
|
282
|
+
const serdeFiles = [...discovered.discoveredSerdeFiles].sort();
|
|
283
|
+
// Include serde files that aren't already step files for cross-context class registration.
|
|
284
|
+
// Classes need to be registered in the step bundle so they can be deserialized
|
|
285
|
+
// when receiving data from workflows and serialized when returning data to workflows.
|
|
286
|
+
const stepFilesSet = new Set(stepFiles);
|
|
287
|
+
const serdeOnlyFiles = serdeFiles.filter((f) => !stepFilesSet.has(f));
|
|
177
288
|
// log the step files for debugging
|
|
178
|
-
await this.writeDebugFile(outfile, {
|
|
289
|
+
await this.writeDebugFile(outfile, {
|
|
290
|
+
stepFiles,
|
|
291
|
+
workflowFiles,
|
|
292
|
+
serdeOnlyFiles,
|
|
293
|
+
});
|
|
179
294
|
const stepsBundleStart = Date.now();
|
|
180
295
|
const workflowManifest = {};
|
|
181
296
|
const builtInSteps = 'workflow/internal/builtins';
|
|
@@ -187,34 +302,65 @@ export class BaseBuilder {
|
|
|
187
302
|
`Caused by: ${chalk.red(String(err))}`,
|
|
188
303
|
].join('\n'));
|
|
189
304
|
});
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
305
|
+
// Helper to create import statement from file path
|
|
306
|
+
// For workspace/node_modules packages, uses the package name so esbuild
|
|
307
|
+
// will resolve through package.json exports with the appropriate conditions
|
|
308
|
+
const createImport = (file) => {
|
|
309
|
+
const { importPath, isPackage } = getImportPath(file, this.config.workingDir);
|
|
310
|
+
if (isPackage) {
|
|
311
|
+
// Use package name - esbuild will resolve via package.json exports
|
|
312
|
+
return `import '${importPath}';`;
|
|
313
|
+
}
|
|
314
|
+
// Local app file - use relative path
|
|
194
315
|
// Normalize both paths to forward slashes before calling relative()
|
|
195
316
|
// This is critical on Windows where relative() can produce unexpected results with mixed path formats
|
|
196
317
|
const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
|
|
197
318
|
const normalizedFile = file.replace(/\\/g, '/');
|
|
198
319
|
// Calculate relative path from working directory to the file
|
|
199
320
|
let relativePath = relative(normalizedWorkingDir, normalizedFile).replace(/\\/g, '/');
|
|
200
|
-
// Ensure relative paths start with ./ so esbuild resolves them correctly
|
|
201
|
-
|
|
321
|
+
// Ensure relative paths start with ./ so esbuild resolves them correctly.
|
|
322
|
+
// Paths like ".output/..." are not valid relative specifiers and must
|
|
323
|
+
// become "./.output/...".
|
|
324
|
+
if (!relativePath.startsWith('./') && !relativePath.startsWith('../')) {
|
|
202
325
|
relativePath = `./${relativePath}`;
|
|
203
326
|
}
|
|
204
327
|
return `import '${relativePath}';`;
|
|
205
|
-
}
|
|
206
|
-
|
|
328
|
+
};
|
|
329
|
+
// Create a virtual entry that imports all files. All step definitions
|
|
330
|
+
// will get registered thanks to the swc transform.
|
|
331
|
+
const stepImports = stepFiles.map(createImport).join('\n');
|
|
332
|
+
// Include serde-only files for class registration side effects
|
|
333
|
+
const serdeImports = serdeOnlyFiles.map(createImport).join('\n');
|
|
207
334
|
const entryContent = `
|
|
208
335
|
// Built in steps
|
|
209
336
|
import '${builtInSteps}';
|
|
210
337
|
// User steps
|
|
211
|
-
${
|
|
338
|
+
${stepImports}
|
|
339
|
+
// Serde files for cross-context class registration
|
|
340
|
+
${serdeImports}
|
|
212
341
|
// API entrypoint
|
|
213
342
|
export { stepEntrypoint as POST } from 'workflow/runtime';`;
|
|
214
343
|
// Bundle with esbuild and our custom SWC plugin
|
|
344
|
+
const entriesToBundle = externalizeNonSteps
|
|
345
|
+
? [
|
|
346
|
+
...stepFiles,
|
|
347
|
+
...serdeFiles,
|
|
348
|
+
...(resolvedBuiltInSteps ? [resolvedBuiltInSteps] : []),
|
|
349
|
+
]
|
|
350
|
+
: undefined;
|
|
351
|
+
const normalizedEntriesToBundle = entriesToBundle
|
|
352
|
+
? await withRealpaths(entriesToBundle)
|
|
353
|
+
: undefined;
|
|
354
|
+
const normalizedSideEffectEntries = await withRealpaths([
|
|
355
|
+
...stepFiles,
|
|
356
|
+
...serdeOnlyFiles,
|
|
357
|
+
...(resolvedBuiltInSteps ? [resolvedBuiltInSteps] : []),
|
|
358
|
+
]);
|
|
359
|
+
const esbuildTsconfigOptions = await getEsbuildTsconfigOptions(tsconfigPath);
|
|
360
|
+
const { banner: importMetaBanner, define: importMetaDefine } = this.getCjsImportMetaPolyfill(format);
|
|
215
361
|
const esbuildCtx = await esbuild.context({
|
|
216
362
|
banner: {
|
|
217
|
-
js:
|
|
363
|
+
js: `// biome-ignore-all lint: generated file\n/* eslint-disable */\n${importMetaBanner}`,
|
|
218
364
|
},
|
|
219
365
|
stdin: {
|
|
220
366
|
contents: entryContent,
|
|
@@ -233,42 +379,76 @@ export class BaseBuilder {
|
|
|
233
379
|
treeShaking: true,
|
|
234
380
|
keepNames: true,
|
|
235
381
|
minify: false,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
382
|
+
jsx: 'preserve',
|
|
383
|
+
logLevel: 'error',
|
|
384
|
+
// Use tsconfig for path alias resolution.
|
|
385
|
+
// For symlinked configs this uses tsconfigRaw to preserve cwd-relative aliases.
|
|
386
|
+
...esbuildTsconfigOptions,
|
|
387
|
+
define: importMetaDefine,
|
|
388
|
+
resolveExtensions: [
|
|
389
|
+
'.ts',
|
|
390
|
+
'.tsx',
|
|
391
|
+
'.mts',
|
|
392
|
+
'.cts',
|
|
393
|
+
'.js',
|
|
394
|
+
'.jsx',
|
|
395
|
+
'.mjs',
|
|
396
|
+
'.cjs',
|
|
397
|
+
],
|
|
398
|
+
// Inline source maps for better stack traces in step execution.
|
|
399
|
+
// Steps execute in Node.js context and inline sourcemaps ensure we get
|
|
400
|
+
// meaningful stack traces with proper file names and line numbers when errors
|
|
401
|
+
// occur in deeply nested function calls across multiple files.
|
|
402
|
+
sourcemap: 'inline',
|
|
239
403
|
plugins: [
|
|
404
|
+
// Handle pseudo-packages like 'server-only' and 'client-only' by providing
|
|
405
|
+
// empty modules. Must run first to intercept these before other resolution.
|
|
406
|
+
createPseudoPackagePlugin(),
|
|
240
407
|
createSwcPlugin({
|
|
241
408
|
mode: 'step',
|
|
242
|
-
entriesToBundle:
|
|
243
|
-
? [
|
|
244
|
-
...stepFiles,
|
|
245
|
-
...(resolvedBuiltInSteps ? [resolvedBuiltInSteps] : []),
|
|
246
|
-
]
|
|
247
|
-
: undefined,
|
|
409
|
+
entriesToBundle: normalizedEntriesToBundle,
|
|
248
410
|
outdir: outfile ? dirname(outfile) : undefined,
|
|
249
|
-
|
|
250
|
-
tsPaths,
|
|
411
|
+
projectRoot: this.transformProjectRoot,
|
|
251
412
|
workflowManifest,
|
|
413
|
+
rewriteTsExtensions,
|
|
414
|
+
sideEffectEntries: normalizedSideEffectEntries,
|
|
252
415
|
}),
|
|
253
416
|
],
|
|
254
417
|
// Plugin should catch most things, but this lets users hard override
|
|
255
418
|
// if the plugin misses anything that should be externalized
|
|
256
|
-
external: this.config.externalPackages || [],
|
|
419
|
+
external: ['bun', 'bun:*', ...(this.config.externalPackages || [])],
|
|
257
420
|
});
|
|
258
421
|
const stepsResult = await esbuildCtx.rebuild();
|
|
259
422
|
this.logEsbuildMessages(stepsResult, 'steps bundle creation');
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
await
|
|
423
|
+
this.logBaseBuilderInfo('Created steps bundle', `${Date.now() - stepsBundleStart}ms`);
|
|
424
|
+
// Handle workflow-only files that may have been tree-shaken from the bundle.
|
|
425
|
+
// These files have no steps, so esbuild removes them, but we still need their
|
|
426
|
+
// workflow metadata for the manifest. Transform them separately.
|
|
427
|
+
const workflowOnlyFiles = workflowFiles.filter((f) => !stepFiles.includes(f));
|
|
428
|
+
await Promise.all(workflowOnlyFiles.map(async (workflowFile) => {
|
|
429
|
+
try {
|
|
430
|
+
const source = await readFile(workflowFile, 'utf8');
|
|
431
|
+
const relativeFilepath = this.getRelativeFilepath(workflowFile);
|
|
432
|
+
const { workflowManifest: fileManifest } = await applySwcTransform(relativeFilepath, source, 'workflow', workflowFile, this.transformProjectRoot);
|
|
433
|
+
if (fileManifest.workflows) {
|
|
434
|
+
workflowManifest.workflows = Object.assign(workflowManifest.workflows || {}, fileManifest.workflows);
|
|
435
|
+
}
|
|
436
|
+
if (fileManifest.classes) {
|
|
437
|
+
workflowManifest.classes = Object.assign(workflowManifest.classes || {}, fileManifest.classes);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
// Log warning but continue - don't fail build for workflow-only file issues
|
|
442
|
+
console.warn(`Warning: Failed to extract workflow metadata from ${workflowFile}:`, error instanceof Error ? error.message : String(error));
|
|
443
|
+
}
|
|
444
|
+
}));
|
|
266
445
|
// Create .gitignore in .swc directory
|
|
267
446
|
await this.createSwcGitignore();
|
|
268
447
|
if (this.config.watch) {
|
|
269
|
-
return esbuildCtx;
|
|
448
|
+
return { context: esbuildCtx, manifest: workflowManifest };
|
|
270
449
|
}
|
|
271
450
|
await esbuildCtx.dispose();
|
|
451
|
+
return { context: undefined, manifest: workflowManifest };
|
|
272
452
|
}
|
|
273
453
|
/**
|
|
274
454
|
* Creates a bundle for workflow orchestration functions.
|
|
@@ -276,30 +456,59 @@ export class BaseBuilder {
|
|
|
276
456
|
*
|
|
277
457
|
* @param bundleFinalOutput - If false, skips the final bundling step (used by Next.js)
|
|
278
458
|
*/
|
|
279
|
-
async createWorkflowsBundle({ inputFiles, format = 'cjs', outfile, bundleFinalOutput = true,
|
|
280
|
-
const
|
|
459
|
+
async createWorkflowsBundle({ inputFiles, format = 'cjs', outfile, bundleFinalOutput = true, keepInterimBundleContext = this.config.watch, tsconfigPath, discoveredEntries, }) {
|
|
460
|
+
const discovered = discoveredEntries ??
|
|
461
|
+
(await this.discoverEntries(inputFiles, dirname(outfile), tsconfigPath));
|
|
462
|
+
const workflowFiles = [...discovered.discoveredWorkflows].sort();
|
|
463
|
+
const serdeFiles = [...discovered.discoveredSerdeFiles].sort();
|
|
464
|
+
// Include serde files that aren't already workflow files for cross-context class registration.
|
|
465
|
+
// Classes need to be registered in the workflow bundle so they can be deserialized
|
|
466
|
+
// when receiving data from steps or when serializing data to send to steps.
|
|
467
|
+
const workflowFilesSet = new Set(workflowFiles);
|
|
468
|
+
const serdeOnlyFiles = serdeFiles.filter((f) => !workflowFilesSet.has(f));
|
|
281
469
|
// 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
|
-
|
|
470
|
+
await this.writeDebugFile(outfile, { workflowFiles, serdeOnlyFiles });
|
|
471
|
+
// Helper to create import statement from file path
|
|
472
|
+
// For packages, uses the package name so esbuild will resolve through
|
|
473
|
+
// package.json exports with conditions: ['workflow']
|
|
474
|
+
const createImport = (file) => {
|
|
475
|
+
const { importPath, isPackage } = getImportPath(file, this.config.workingDir);
|
|
476
|
+
if (isPackage) {
|
|
477
|
+
// Use package name - esbuild will resolve via package.json exports
|
|
478
|
+
// and apply the 'workflow' condition
|
|
479
|
+
return `import '${importPath}';`;
|
|
480
|
+
}
|
|
481
|
+
// Local app file - use relative path
|
|
482
|
+
// Normalize both paths to forward slashes before calling relative()
|
|
483
|
+
// This is critical on Windows where relative() can produce unexpected results with mixed path formats
|
|
484
|
+
const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
|
|
485
|
+
const normalizedFile = file.replace(/\\/g, '/');
|
|
486
|
+
// Calculate relative path from working directory to the file
|
|
487
|
+
let relativePath = relative(normalizedWorkingDir, normalizedFile).replace(/\\/g, '/');
|
|
488
|
+
// Ensure relative paths start with ./ so esbuild resolves them correctly.
|
|
489
|
+
// Paths like ".output/..." are not valid relative specifiers and must
|
|
490
|
+
// become "./.output/...".
|
|
491
|
+
if (!relativePath.startsWith('./') && !relativePath.startsWith('../')) {
|
|
492
|
+
relativePath = `./${relativePath}`;
|
|
493
|
+
}
|
|
494
|
+
return `import '${relativePath}';`;
|
|
495
|
+
};
|
|
496
|
+
// Create a virtual entry that imports all workflow files
|
|
497
|
+
// The SWC plugin in workflow mode emits `globalThis.__private_workflows.set(workflowId, fn)`
|
|
498
|
+
// calls directly, so we just need to import the files (Map is initialized via banner)
|
|
499
|
+
const workflowImports = workflowFiles.map(createImport).join('\n');
|
|
500
|
+
// Include serde-only files for class registration side effects
|
|
501
|
+
const serdeImports = serdeOnlyFiles.map(createImport).join('\n');
|
|
502
|
+
const imports = serdeImports
|
|
503
|
+
? `${workflowImports}\n// Serde files for cross-context class registration\n${serdeImports}`
|
|
504
|
+
: workflowImports;
|
|
301
505
|
const bundleStartTime = Date.now();
|
|
302
506
|
const workflowManifest = {};
|
|
507
|
+
const esbuildTsconfigOptions = await getEsbuildTsconfigOptions(tsconfigPath);
|
|
508
|
+
const normalizedWorkflowSideEffectEntries = await withRealpaths([
|
|
509
|
+
...workflowFiles,
|
|
510
|
+
...serdeOnlyFiles,
|
|
511
|
+
]);
|
|
303
512
|
// Bundle with esbuild and our custom SWC plugin in workflow mode.
|
|
304
513
|
// this bundle will be run inside a vm isolate
|
|
305
514
|
const interimBundleCtx = await esbuild.context({
|
|
@@ -320,106 +529,192 @@ export class BaseBuilder {
|
|
|
320
529
|
treeShaking: true,
|
|
321
530
|
keepNames: true,
|
|
322
531
|
minify: false,
|
|
532
|
+
// Initialize the workflow registry at the very top of the bundle
|
|
533
|
+
// This must be in banner (not the virtual entry) because esbuild's bundling
|
|
534
|
+
// can reorder code, and the .set() calls need the Map to exist first
|
|
535
|
+
banner: {
|
|
536
|
+
js: 'globalThis.__private_workflows = new Map();',
|
|
537
|
+
},
|
|
323
538
|
// Inline source maps for better stack traces in workflow VM execution.
|
|
324
539
|
// This intermediate bundle is executed via runInContext() in a VM, so we need
|
|
325
540
|
// inline source maps to get meaningful stack traces instead of "evalmachine.<anonymous>".
|
|
326
541
|
sourcemap: 'inline',
|
|
327
|
-
|
|
542
|
+
// Use tsconfig for path alias resolution.
|
|
543
|
+
// For symlinked configs this uses tsconfigRaw to preserve cwd-relative aliases.
|
|
544
|
+
...esbuildTsconfigOptions,
|
|
545
|
+
resolveExtensions: [
|
|
546
|
+
'.ts',
|
|
547
|
+
'.tsx',
|
|
548
|
+
'.mts',
|
|
549
|
+
'.cts',
|
|
550
|
+
'.js',
|
|
551
|
+
'.jsx',
|
|
552
|
+
'.mjs',
|
|
553
|
+
'.cjs',
|
|
554
|
+
],
|
|
328
555
|
plugins: [
|
|
556
|
+
// Handle pseudo-packages like 'server-only' and 'client-only' by providing
|
|
557
|
+
// empty modules. Must run first to intercept these before other resolution.
|
|
558
|
+
createPseudoPackagePlugin(),
|
|
329
559
|
createSwcPlugin({
|
|
330
560
|
mode: 'workflow',
|
|
331
|
-
|
|
332
|
-
tsPaths,
|
|
561
|
+
projectRoot: this.transformProjectRoot,
|
|
333
562
|
workflowManifest,
|
|
563
|
+
sideEffectEntries: normalizedWorkflowSideEffectEntries,
|
|
334
564
|
}),
|
|
335
565
|
// This plugin must run after the swc plugin to ensure dead code elimination
|
|
336
566
|
// happens first, preventing false positives on Node.js imports in unused code paths
|
|
337
567
|
createNodeModuleErrorPlugin(),
|
|
338
568
|
],
|
|
569
|
+
// NOTE: We intentionally do NOT use the external option here for workflow bundles.
|
|
570
|
+
// When packages are marked external with format: 'cjs', esbuild generates require() calls.
|
|
571
|
+
// However, the workflow VM (vm.runInContext) does not have require() defined - it only
|
|
572
|
+
// provides module.exports and exports. External packages would fail at runtime with:
|
|
573
|
+
// ReferenceError: require is not defined
|
|
574
|
+
// Instead, we bundle everything and rely on:
|
|
575
|
+
// - createPseudoPackagePlugin() to handle server-only/client-only with empty modules
|
|
576
|
+
// - createNodeModuleErrorPlugin() to catch Node.js builtin imports at build time
|
|
339
577
|
});
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
578
|
+
let shouldDisposeInterimBundleCtx = !keepInterimBundleContext;
|
|
579
|
+
try {
|
|
580
|
+
const interimBundle = await interimBundleCtx.rebuild();
|
|
581
|
+
this.logEsbuildMessages(interimBundle, 'intermediate workflow bundle', true, {
|
|
582
|
+
suppressWarnings: this.config.suppressCreateWorkflowsBundleWarnings,
|
|
583
|
+
});
|
|
584
|
+
this.logCreateWorkflowsBundleInfo('Created intermediate workflow bundle', `${Date.now() - bundleStartTime}ms`);
|
|
585
|
+
if (this.config.workflowManifestPath) {
|
|
586
|
+
const resolvedPath = resolve(process.cwd(), this.config.workflowManifestPath);
|
|
587
|
+
let prefix = '';
|
|
588
|
+
if (resolvedPath.endsWith('.cjs')) {
|
|
589
|
+
prefix = 'module.exports = ';
|
|
590
|
+
}
|
|
591
|
+
else if (resolvedPath.endsWith('.js') ||
|
|
592
|
+
resolvedPath.endsWith('.mjs')) {
|
|
593
|
+
prefix = 'export default ';
|
|
594
|
+
}
|
|
595
|
+
await mkdir(dirname(resolvedPath), { recursive: true });
|
|
596
|
+
await writeFile(resolvedPath, prefix + JSON.stringify(workflowManifest, null, 2));
|
|
352
597
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
598
|
+
// Create .gitignore in .swc directory
|
|
599
|
+
await this.createSwcGitignore();
|
|
600
|
+
if (!interimBundle.outputFiles ||
|
|
601
|
+
interimBundle.outputFiles.length === 0) {
|
|
602
|
+
throw new Error('No output files generated from esbuild');
|
|
356
603
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
604
|
+
// Serde compliance warnings: check if workflow bundle has Node.js imports
|
|
605
|
+
// alongside serde-registered classes (these will fail at runtime in the sandbox)
|
|
606
|
+
if (workflowManifest.classes &&
|
|
607
|
+
Object.keys(workflowManifest.classes).length > 0) {
|
|
608
|
+
const { analyzeSerdeCompliance } = await import('./serde-checker.js');
|
|
609
|
+
const bundleText = interimBundle.outputFiles[0].text;
|
|
610
|
+
const serdeResult = analyzeSerdeCompliance({
|
|
611
|
+
sourceCode: '',
|
|
612
|
+
workflowCode: bundleText,
|
|
613
|
+
manifest: workflowManifest,
|
|
614
|
+
});
|
|
615
|
+
// De-dupe warnings: group identical issues across classes
|
|
616
|
+
const issuesToClasses = new Map();
|
|
617
|
+
for (const cls of serdeResult.classes) {
|
|
618
|
+
if (!cls.compliant) {
|
|
619
|
+
for (const issue of cls.issues) {
|
|
620
|
+
let affectedClasses = issuesToClasses.get(issue);
|
|
621
|
+
if (!affectedClasses) {
|
|
622
|
+
affectedClasses = new Set();
|
|
623
|
+
issuesToClasses.set(issue, affectedClasses);
|
|
624
|
+
}
|
|
625
|
+
affectedClasses.add(cls.className);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const [issue, affectedClasses] of issuesToClasses) {
|
|
630
|
+
const classNames = [...affectedClasses];
|
|
631
|
+
const classLabel = classNames.length === 1
|
|
632
|
+
? `class "${classNames[0]}"`
|
|
633
|
+
: `classes ${classNames.map((name) => `"${name}"`).join(', ')}`;
|
|
634
|
+
console.warn(chalk.yellow(`⚠ Serde warning for ${classLabel}: `) + issue);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const bundleFinal = async (interimBundle) => {
|
|
638
|
+
const workflowBundleCode = interimBundle;
|
|
639
|
+
const workflowFunctionCode = `// biome-ignore-all lint: generated file
|
|
368
640
|
/* eslint-disable */
|
|
369
641
|
import { workflowEntrypoint } from 'workflow/runtime';
|
|
370
642
|
|
|
371
643
|
const workflowCode = \`${workflowBundleCode.replace(/[\\`$]/g, '\\$&')}\`;
|
|
372
644
|
|
|
373
645
|
export const POST = workflowEntrypoint(workflowCode);`;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
646
|
+
// we skip the final bundling step for Next.js so it can bundle itself
|
|
647
|
+
if (!bundleFinalOutput) {
|
|
648
|
+
if (!outfile) {
|
|
649
|
+
throw new Error(`Invariant: missing outfile for workflow bundle`);
|
|
650
|
+
}
|
|
651
|
+
// Ensure the output directory exists
|
|
652
|
+
const outputDir = dirname(outfile);
|
|
653
|
+
await mkdir(outputDir, { recursive: true });
|
|
654
|
+
// Atomic write: write to temp file then rename to prevent
|
|
655
|
+
// file watchers from reading partial file during write
|
|
656
|
+
const tempPath = `${outfile}.${randomUUID()}.tmp`;
|
|
657
|
+
await writeFile(tempPath, workflowFunctionCode);
|
|
658
|
+
await rename(tempPath, outfile);
|
|
659
|
+
return;
|
|
378
660
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
await
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
external: ['@aws-sdk/credential-provider-web-identity'],
|
|
411
|
-
});
|
|
412
|
-
this.logEsbuildMessages(finalWorkflowResult, 'final workflow bundle');
|
|
413
|
-
console.log('Created final workflow bundle', `${Date.now() - bundleStartTime}ms`);
|
|
414
|
-
};
|
|
415
|
-
await bundleFinal(interimBundle.outputFiles[0].text);
|
|
416
|
-
if (this.config.watch) {
|
|
417
|
-
return {
|
|
418
|
-
interimBundleCtx,
|
|
419
|
-
bundleFinal,
|
|
661
|
+
const bundleStartTime = Date.now();
|
|
662
|
+
// Now bundle this so we can resolve the @workflow/core dependency
|
|
663
|
+
// we could remove this if we do nft tracing or similar instead
|
|
664
|
+
const finalWorkflowResult = await esbuild.build({
|
|
665
|
+
banner: {
|
|
666
|
+
js: '// biome-ignore-all lint: generated file\n/* eslint-disable */\n',
|
|
667
|
+
},
|
|
668
|
+
stdin: {
|
|
669
|
+
contents: workflowFunctionCode,
|
|
670
|
+
resolveDir: this.config.workingDir,
|
|
671
|
+
sourcefile: 'virtual-entry.js',
|
|
672
|
+
loader: 'js',
|
|
673
|
+
},
|
|
674
|
+
outfile,
|
|
675
|
+
// Source maps for the final workflow bundle wrapper (not important since this code
|
|
676
|
+
// doesn't run in the VM - only the intermediate bundle sourcemap is relevant)
|
|
677
|
+
sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
|
|
678
|
+
absWorkingDir: this.config.workingDir,
|
|
679
|
+
bundle: true,
|
|
680
|
+
format,
|
|
681
|
+
platform: 'node',
|
|
682
|
+
target: 'es2022',
|
|
683
|
+
write: true,
|
|
684
|
+
keepNames: true,
|
|
685
|
+
minify: false,
|
|
686
|
+
external: ['@aws-sdk/credential-provider-web-identity'],
|
|
687
|
+
});
|
|
688
|
+
this.logEsbuildMessages(finalWorkflowResult, 'final workflow bundle', true, {
|
|
689
|
+
suppressWarnings: this.config.suppressCreateWorkflowsBundleWarnings,
|
|
690
|
+
});
|
|
691
|
+
this.logCreateWorkflowsBundleInfo('Created final workflow bundle', `${Date.now() - bundleStartTime}ms`);
|
|
420
692
|
};
|
|
693
|
+
await bundleFinal(interimBundle.outputFiles[0].text);
|
|
694
|
+
if (keepInterimBundleContext) {
|
|
695
|
+
shouldDisposeInterimBundleCtx = false;
|
|
696
|
+
return {
|
|
697
|
+
manifest: workflowManifest,
|
|
698
|
+
interimBundleCtx,
|
|
699
|
+
bundleFinal,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return { manifest: workflowManifest };
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
shouldDisposeInterimBundleCtx = true;
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
if (shouldDisposeInterimBundleCtx) {
|
|
710
|
+
try {
|
|
711
|
+
await interimBundleCtx.dispose();
|
|
712
|
+
}
|
|
713
|
+
catch (disposeError) {
|
|
714
|
+
console.warn('Warning: Failed to dispose workflow bundle context', disposeError);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
421
717
|
}
|
|
422
|
-
await interimBundleCtx.dispose();
|
|
423
718
|
}
|
|
424
719
|
/**
|
|
425
720
|
* Creates a client library bundle for workflow execution.
|
|
@@ -431,23 +726,58 @@ export const POST = workflowEntrypoint(workflowCode);`;
|
|
|
431
726
|
// Silently exit since no client bundle was requested
|
|
432
727
|
return;
|
|
433
728
|
}
|
|
434
|
-
|
|
435
|
-
|
|
729
|
+
this.logBaseBuilderInfo('Generating a client library at', this.config.clientBundlePath);
|
|
730
|
+
this.logBaseBuilderInfo('NOTE: The recommended way to use workflow with a framework like NextJS is using the loader/plugin with webpack/turbobpack/rollup');
|
|
436
731
|
// Ensure we have the directory for the client bundle
|
|
437
732
|
const outputDir = dirname(this.config.clientBundlePath);
|
|
438
733
|
await mkdir(outputDir, { recursive: true });
|
|
439
734
|
const inputFiles = await this.getInputFiles();
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
735
|
+
// Discover serde files from the input files' dependency tree for cross-context class registration.
|
|
736
|
+
// Classes need to be registered in the client bundle so they can be serialized
|
|
737
|
+
// when passing data to workflows via start() and deserialized when receiving workflow results.
|
|
738
|
+
const { discoveredSerdeFiles } = await this.discoverEntries(inputFiles, outputDir);
|
|
739
|
+
// Identify serde files that aren't in the inputFiles (deduplicated)
|
|
740
|
+
const inputFilesNormalized = new Set(inputFiles.map((f) => f.replace(/\\/g, '/')));
|
|
741
|
+
const serdeOnlyFiles = [...discoveredSerdeFiles].filter((f) => !inputFilesNormalized.has(f));
|
|
742
|
+
// Re-exports for input files (user's workflow/step definitions).
|
|
743
|
+
// These must use valid relative specifiers because some frameworks pass
|
|
744
|
+
// generated files like ".output/server/index.mjs" as input files.
|
|
745
|
+
const reexports = inputFiles
|
|
746
|
+
.map((file) => {
|
|
747
|
+
const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
|
|
748
|
+
let relativePath = relative(normalizedWorkingDir, file).replace(/\\/g, '/');
|
|
749
|
+
if (!relativePath.startsWith('./') && !relativePath.startsWith('../')) {
|
|
750
|
+
relativePath = `./${relativePath}`;
|
|
751
|
+
}
|
|
752
|
+
return `export * from '${relativePath}';`;
|
|
753
|
+
})
|
|
443
754
|
.join('\n');
|
|
755
|
+
// Side-effect imports for serde files not in inputFiles (for class registration)
|
|
756
|
+
const serdeImports = serdeOnlyFiles
|
|
757
|
+
.map((file) => {
|
|
758
|
+
const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
|
|
759
|
+
let relativePath = relative(normalizedWorkingDir, file).replace(/\\/g, '/');
|
|
760
|
+
if (!relativePath.startsWith('./') && !relativePath.startsWith('../')) {
|
|
761
|
+
relativePath = `./${relativePath}`;
|
|
762
|
+
}
|
|
763
|
+
return `import '${relativePath}';`;
|
|
764
|
+
})
|
|
765
|
+
.join('\n');
|
|
766
|
+
// Combine: serde imports (for registration side effects) + re-exports
|
|
767
|
+
const entryContent = serdeImports
|
|
768
|
+
? `// Serde files for cross-context class registration\n${serdeImports}\n${reexports}`
|
|
769
|
+
: reexports;
|
|
444
770
|
// Bundle with esbuild and our custom SWC plugin
|
|
771
|
+
const normalizedClientSideEffectEntries = await withRealpaths([
|
|
772
|
+
...inputFiles,
|
|
773
|
+
...serdeOnlyFiles,
|
|
774
|
+
]);
|
|
445
775
|
const clientResult = await esbuild.build({
|
|
446
776
|
banner: {
|
|
447
777
|
js: '// biome-ignore-all lint: generated file\n/* eslint-disable */\n',
|
|
448
778
|
},
|
|
449
779
|
stdin: {
|
|
450
|
-
contents:
|
|
780
|
+
contents: entryContent,
|
|
451
781
|
resolveDir: this.config.workingDir,
|
|
452
782
|
sourcefile: 'virtual-entry.js',
|
|
453
783
|
loader: 'js',
|
|
@@ -456,12 +786,28 @@ export const POST = workflowEntrypoint(workflowCode);`;
|
|
|
456
786
|
bundle: true,
|
|
457
787
|
format: 'esm',
|
|
458
788
|
platform: 'node',
|
|
789
|
+
jsx: 'preserve',
|
|
459
790
|
target: 'es2022',
|
|
460
791
|
write: true,
|
|
461
792
|
treeShaking: true,
|
|
462
793
|
external: ['@workflow/core'],
|
|
463
|
-
resolveExtensions: [
|
|
464
|
-
|
|
794
|
+
resolveExtensions: [
|
|
795
|
+
'.ts',
|
|
796
|
+
'.tsx',
|
|
797
|
+
'.mts',
|
|
798
|
+
'.cts',
|
|
799
|
+
'.js',
|
|
800
|
+
'.jsx',
|
|
801
|
+
'.mjs',
|
|
802
|
+
'.cjs',
|
|
803
|
+
],
|
|
804
|
+
plugins: [
|
|
805
|
+
createSwcPlugin({
|
|
806
|
+
mode: 'client',
|
|
807
|
+
projectRoot: this.transformProjectRoot,
|
|
808
|
+
sideEffectEntries: normalizedClientSideEffectEntries,
|
|
809
|
+
}),
|
|
810
|
+
],
|
|
465
811
|
});
|
|
466
812
|
this.logEsbuildMessages(clientResult, 'client library bundle');
|
|
467
813
|
// Create .gitignore in .swc directory
|
|
@@ -473,7 +819,7 @@ export const POST = workflowEntrypoint(workflowCode);`;
|
|
|
473
819
|
* @param bundle - If true, bundles dependencies (needed for Build Output API)
|
|
474
820
|
*/
|
|
475
821
|
async createWebhookBundle({ outfile, bundle = false, }) {
|
|
476
|
-
|
|
822
|
+
this.logCreateWebhookBundleInfo('Creating webhook route');
|
|
477
823
|
await mkdir(dirname(outfile), { recursive: true });
|
|
478
824
|
// Create a static route that calls resumeWebhook
|
|
479
825
|
// This route works for both Next.js and Vercel Build Output API
|
|
@@ -512,10 +858,12 @@ export const OPTIONS = handler;`;
|
|
|
512
858
|
return;
|
|
513
859
|
}
|
|
514
860
|
// For Build Output API, bundle with esbuild to resolve imports
|
|
861
|
+
const webhookFormat = 'cjs';
|
|
862
|
+
const { banner: webhookImportMetaBanner, define: webhookImportMetaDefine } = this.getCjsImportMetaPolyfill(webhookFormat);
|
|
515
863
|
const webhookBundleStart = Date.now();
|
|
516
864
|
const result = await esbuild.build({
|
|
517
865
|
banner: {
|
|
518
|
-
js:
|
|
866
|
+
js: `// biome-ignore-all lint: generated file\n/* eslint-disable */\n${webhookImportMetaBanner}`,
|
|
519
867
|
},
|
|
520
868
|
stdin: {
|
|
521
869
|
contents: routeContent,
|
|
@@ -526,7 +874,8 @@ export const OPTIONS = handler;`;
|
|
|
526
874
|
outfile,
|
|
527
875
|
absWorkingDir: this.config.workingDir,
|
|
528
876
|
bundle: true,
|
|
529
|
-
|
|
877
|
+
jsx: 'preserve',
|
|
878
|
+
format: webhookFormat,
|
|
530
879
|
platform: 'node',
|
|
531
880
|
conditions: ['import', 'module', 'node', 'default'],
|
|
532
881
|
target: 'es2022',
|
|
@@ -534,14 +883,24 @@ export const OPTIONS = handler;`;
|
|
|
534
883
|
treeShaking: true,
|
|
535
884
|
keepNames: true,
|
|
536
885
|
minify: false,
|
|
537
|
-
|
|
538
|
-
|
|
886
|
+
define: webhookImportMetaDefine,
|
|
887
|
+
resolveExtensions: [
|
|
888
|
+
'.ts',
|
|
889
|
+
'.tsx',
|
|
890
|
+
'.mts',
|
|
891
|
+
'.cts',
|
|
892
|
+
'.js',
|
|
893
|
+
'.jsx',
|
|
894
|
+
'.mjs',
|
|
895
|
+
'.cjs',
|
|
896
|
+
],
|
|
897
|
+
sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
|
|
539
898
|
mainFields: ['module', 'main'],
|
|
540
899
|
// Don't externalize anything - bundle everything including workflow packages
|
|
541
900
|
external: [],
|
|
542
901
|
});
|
|
543
902
|
this.logEsbuildMessages(result, 'webhook bundle creation');
|
|
544
|
-
|
|
903
|
+
this.logCreateWebhookBundleInfo('Created webhook bundle', `${Date.now() - webhookBundleStart}ms`);
|
|
545
904
|
}
|
|
546
905
|
/**
|
|
547
906
|
* Creates a package.json file with the specified module type.
|
|
@@ -560,6 +919,9 @@ export const OPTIONS = handler;`;
|
|
|
560
919
|
launcherType: config.launcherType ?? 'Nodejs',
|
|
561
920
|
architecture: config.architecture ?? 'arm64',
|
|
562
921
|
shouldAddHelpers: config.shouldAddHelpers ?? true,
|
|
922
|
+
...(config.maxDuration !== undefined && {
|
|
923
|
+
maxDuration: config.maxDuration,
|
|
924
|
+
}),
|
|
563
925
|
...(config.shouldAddSourcemapSupport !== undefined && {
|
|
564
926
|
shouldAddSourcemapSupport: config.shouldAddSourcemapSupport,
|
|
565
927
|
}),
|
|
@@ -589,5 +951,108 @@ export const OPTIONS = handler;`;
|
|
|
589
951
|
// We're intentionally silently ignoring this error - creating .gitignore isn't critical
|
|
590
952
|
}
|
|
591
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Whether the manifest should be exposed as a public HTTP route.
|
|
956
|
+
* Controlled by the `WORKFLOW_PUBLIC_MANIFEST` environment variable.
|
|
957
|
+
*/
|
|
958
|
+
get shouldExposePublicManifest() {
|
|
959
|
+
return process.env.WORKFLOW_PUBLIC_MANIFEST === '1';
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Whether diagnostics artifacts should be emitted to Vercel output.
|
|
963
|
+
* This is enabled when the resolved world target is Vercel.
|
|
964
|
+
*/
|
|
965
|
+
get shouldEmitVercelDiagnostics() {
|
|
966
|
+
return (usesVercelWorld() || this.config.buildTarget === 'vercel-build-output-api');
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Creates a manifest JSON file containing step/workflow/class metadata
|
|
970
|
+
* and graph data for visualization.
|
|
971
|
+
*
|
|
972
|
+
* @returns The manifest JSON string, or undefined if manifest creation failed.
|
|
973
|
+
*/
|
|
974
|
+
async createManifest({ workflowBundlePath, manifestDir, manifest, }) {
|
|
975
|
+
const buildStart = Date.now();
|
|
976
|
+
this.logCreateManifestInfo('Creating manifest...');
|
|
977
|
+
try {
|
|
978
|
+
const workflowGraphs = await extractWorkflowGraphs(workflowBundlePath);
|
|
979
|
+
const steps = this.convertStepsManifest(manifest.steps);
|
|
980
|
+
const workflows = this.convertWorkflowsManifest(manifest.workflows, workflowGraphs);
|
|
981
|
+
const classes = this.convertClassesManifest(manifest.classes);
|
|
982
|
+
const output = { version: '1.0.0', steps, workflows, classes };
|
|
983
|
+
const manifestJson = JSON.stringify(output, null, 2);
|
|
984
|
+
await mkdir(manifestDir, { recursive: true });
|
|
985
|
+
await writeFile(join(manifestDir, 'manifest.json'), manifestJson);
|
|
986
|
+
if (this.shouldEmitVercelDiagnostics) {
|
|
987
|
+
const diagnosticsManifestPath = this.resolvePath('.vercel/output/diagnostics/workflows-manifest.json');
|
|
988
|
+
await this.ensureDirectory(diagnosticsManifestPath);
|
|
989
|
+
await writeFile(diagnosticsManifestPath, manifestJson);
|
|
990
|
+
}
|
|
991
|
+
const stepCount = Object.values(steps).reduce((acc, s) => acc + Object.keys(s).length, 0);
|
|
992
|
+
const workflowCount = Object.values(workflows).reduce((acc, w) => acc + Object.keys(w).length, 0);
|
|
993
|
+
const classCount = Object.values(classes).reduce((acc, c) => acc + Object.keys(c).length, 0);
|
|
994
|
+
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`);
|
|
995
|
+
return manifestJson;
|
|
996
|
+
}
|
|
997
|
+
catch (error) {
|
|
998
|
+
console.warn('Failed to create manifest:', error instanceof Error ? error.message : String(error));
|
|
999
|
+
return undefined;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
convertStepsManifest(steps) {
|
|
1003
|
+
const result = {};
|
|
1004
|
+
if (!steps)
|
|
1005
|
+
return result;
|
|
1006
|
+
for (const [filePath, entries] of Object.entries(steps)) {
|
|
1007
|
+
result[filePath] = {};
|
|
1008
|
+
for (const [name, data] of Object.entries(entries)) {
|
|
1009
|
+
result[filePath][name] = { stepId: data.stepId };
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
convertWorkflowsManifest(workflows, graphs) {
|
|
1015
|
+
const result = {};
|
|
1016
|
+
if (!workflows)
|
|
1017
|
+
return result;
|
|
1018
|
+
// Build a normalized lookup for graphs since the graph extractor uses
|
|
1019
|
+
// paths from workflowId (e.g. "./workflows/hello-agent") while the
|
|
1020
|
+
// manifest uses source file paths (e.g. "workflows/hello-agent.ts").
|
|
1021
|
+
// Normalize by stripping leading "./" and file extensions.
|
|
1022
|
+
const normalizedGraphs = new Map();
|
|
1023
|
+
for (const [graphPath, graphEntries] of Object.entries(graphs)) {
|
|
1024
|
+
const normalized = graphPath
|
|
1025
|
+
.replace(/^\.\//, '')
|
|
1026
|
+
.replace(/\.[^/.]+$/, '');
|
|
1027
|
+
normalizedGraphs.set(normalized, graphEntries);
|
|
1028
|
+
}
|
|
1029
|
+
for (const [filePath, entries] of Object.entries(workflows)) {
|
|
1030
|
+
result[filePath] = {};
|
|
1031
|
+
// Normalize the manifest file path for lookup
|
|
1032
|
+
const normalizedFilePath = filePath
|
|
1033
|
+
.replace(/^\.\//, '')
|
|
1034
|
+
.replace(/\.[^/.]+$/, '');
|
|
1035
|
+
const graphEntries = graphs[filePath] || normalizedGraphs.get(normalizedFilePath);
|
|
1036
|
+
for (const [name, data] of Object.entries(entries)) {
|
|
1037
|
+
result[filePath][name] = {
|
|
1038
|
+
workflowId: data.workflowId,
|
|
1039
|
+
graph: graphEntries?.[name]?.graph || { nodes: [], edges: [] },
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return result;
|
|
1044
|
+
}
|
|
1045
|
+
convertClassesManifest(classes) {
|
|
1046
|
+
const result = {};
|
|
1047
|
+
if (!classes)
|
|
1048
|
+
return result;
|
|
1049
|
+
for (const [filePath, entries] of Object.entries(classes)) {
|
|
1050
|
+
result[filePath] = {};
|
|
1051
|
+
for (const [name, data] of Object.entries(entries)) {
|
|
1052
|
+
result[filePath][name] = { classId: data.classId };
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return result;
|
|
1056
|
+
}
|
|
592
1057
|
}
|
|
593
1058
|
//# sourceMappingURL=base-builder.js.map
|