@workflow/builders 4.0.1-beta.8 → 4.0.1
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 +64 -9
- 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 +669 -201
- 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 +3 -4
- package/dist/discover-entries-esbuild-plugin.d.ts.map +1 -1
- package/dist/discover-entries-esbuild-plugin.js +73 -28
- 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 +95 -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 +283 -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,7 +141,7 @@ 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;
|
|
@@ -93,41 +149,68 @@ export class BaseBuilder {
|
|
|
93
149
|
const state = {
|
|
94
150
|
discoveredSteps: [],
|
|
95
151
|
discoveredWorkflows: [],
|
|
152
|
+
discoveredSerdeFiles: [],
|
|
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,103 +529,192 @@ export class BaseBuilder {
|
|
|
320
529
|
treeShaking: true,
|
|
321
530
|
keepNames: true,
|
|
322
531
|
minify: false,
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
},
|
|
538
|
+
// Inline source maps for better stack traces in workflow VM execution.
|
|
539
|
+
// This intermediate bundle is executed via runInContext() in a VM, so we need
|
|
540
|
+
// inline source maps to get meaningful stack traces instead of "evalmachine.<anonymous>".
|
|
541
|
+
sourcemap: 'inline',
|
|
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
|
+
],
|
|
326
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(),
|
|
327
559
|
createSwcPlugin({
|
|
328
560
|
mode: 'workflow',
|
|
329
|
-
|
|
330
|
-
tsPaths,
|
|
561
|
+
projectRoot: this.transformProjectRoot,
|
|
331
562
|
workflowManifest,
|
|
563
|
+
sideEffectEntries: normalizedWorkflowSideEffectEntries,
|
|
332
564
|
}),
|
|
333
565
|
// This plugin must run after the swc plugin to ensure dead code elimination
|
|
334
566
|
// happens first, preventing false positives on Node.js imports in unused code paths
|
|
335
567
|
createNodeModuleErrorPlugin(),
|
|
336
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
|
|
337
577
|
});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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));
|
|
350
597
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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');
|
|
354
603
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
366
640
|
/* eslint-disable */
|
|
367
641
|
import { workflowEntrypoint } from 'workflow/runtime';
|
|
368
642
|
|
|
369
643
|
const workflowCode = \`${workflowBundleCode.replace(/[\\`$]/g, '\\$&')}\`;
|
|
370
644
|
|
|
371
645
|
export const POST = workflowEntrypoint(workflowCode);`;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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;
|
|
376
660
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
await
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
this.logEsbuildMessages(finalWorkflowResult, 'final workflow bundle');
|
|
410
|
-
console.log('Created final workflow bundle', `${Date.now() - bundleStartTime}ms`);
|
|
411
|
-
};
|
|
412
|
-
await bundleFinal(interimBundle.outputFiles[0].text);
|
|
413
|
-
if (this.config.watch) {
|
|
414
|
-
return {
|
|
415
|
-
interimBundleCtx,
|
|
416
|
-
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`);
|
|
417
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
|
+
}
|
|
418
717
|
}
|
|
419
|
-
await interimBundleCtx.dispose();
|
|
420
718
|
}
|
|
421
719
|
/**
|
|
422
720
|
* Creates a client library bundle for workflow execution.
|
|
@@ -428,23 +726,58 @@ export const POST = workflowEntrypoint(workflowCode);`;
|
|
|
428
726
|
// Silently exit since no client bundle was requested
|
|
429
727
|
return;
|
|
430
728
|
}
|
|
431
|
-
|
|
432
|
-
|
|
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');
|
|
433
731
|
// Ensure we have the directory for the client bundle
|
|
434
732
|
const outputDir = dirname(this.config.clientBundlePath);
|
|
435
733
|
await mkdir(outputDir, { recursive: true });
|
|
436
734
|
const inputFiles = await this.getInputFiles();
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
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
|
+
})
|
|
440
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;
|
|
441
770
|
// Bundle with esbuild and our custom SWC plugin
|
|
771
|
+
const normalizedClientSideEffectEntries = await withRealpaths([
|
|
772
|
+
...inputFiles,
|
|
773
|
+
...serdeOnlyFiles,
|
|
774
|
+
]);
|
|
442
775
|
const clientResult = await esbuild.build({
|
|
443
776
|
banner: {
|
|
444
777
|
js: '// biome-ignore-all lint: generated file\n/* eslint-disable */\n',
|
|
445
778
|
},
|
|
446
779
|
stdin: {
|
|
447
|
-
contents:
|
|
780
|
+
contents: entryContent,
|
|
448
781
|
resolveDir: this.config.workingDir,
|
|
449
782
|
sourcefile: 'virtual-entry.js',
|
|
450
783
|
loader: 'js',
|
|
@@ -453,12 +786,28 @@ export const POST = workflowEntrypoint(workflowCode);`;
|
|
|
453
786
|
bundle: true,
|
|
454
787
|
format: 'esm',
|
|
455
788
|
platform: 'node',
|
|
789
|
+
jsx: 'preserve',
|
|
456
790
|
target: 'es2022',
|
|
457
791
|
write: true,
|
|
458
792
|
treeShaking: true,
|
|
459
793
|
external: ['@workflow/core'],
|
|
460
|
-
resolveExtensions: [
|
|
461
|
-
|
|
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
|
+
],
|
|
462
811
|
});
|
|
463
812
|
this.logEsbuildMessages(clientResult, 'client library bundle');
|
|
464
813
|
// Create .gitignore in .swc directory
|
|
@@ -470,7 +819,7 @@ export const POST = workflowEntrypoint(workflowCode);`;
|
|
|
470
819
|
* @param bundle - If true, bundles dependencies (needed for Build Output API)
|
|
471
820
|
*/
|
|
472
821
|
async createWebhookBundle({ outfile, bundle = false, }) {
|
|
473
|
-
|
|
822
|
+
this.logCreateWebhookBundleInfo('Creating webhook route');
|
|
474
823
|
await mkdir(dirname(outfile), { recursive: true });
|
|
475
824
|
// Create a static route that calls resumeWebhook
|
|
476
825
|
// This route works for both Next.js and Vercel Build Output API
|
|
@@ -509,10 +858,12 @@ export const OPTIONS = handler;`;
|
|
|
509
858
|
return;
|
|
510
859
|
}
|
|
511
860
|
// For Build Output API, bundle with esbuild to resolve imports
|
|
861
|
+
const webhookFormat = 'cjs';
|
|
862
|
+
const { banner: webhookImportMetaBanner, define: webhookImportMetaDefine } = this.getCjsImportMetaPolyfill(webhookFormat);
|
|
512
863
|
const webhookBundleStart = Date.now();
|
|
513
864
|
const result = await esbuild.build({
|
|
514
865
|
banner: {
|
|
515
|
-
js:
|
|
866
|
+
js: `// biome-ignore-all lint: generated file\n/* eslint-disable */\n${webhookImportMetaBanner}`,
|
|
516
867
|
},
|
|
517
868
|
stdin: {
|
|
518
869
|
contents: routeContent,
|
|
@@ -523,7 +874,8 @@ export const OPTIONS = handler;`;
|
|
|
523
874
|
outfile,
|
|
524
875
|
absWorkingDir: this.config.workingDir,
|
|
525
876
|
bundle: true,
|
|
526
|
-
|
|
877
|
+
jsx: 'preserve',
|
|
878
|
+
format: webhookFormat,
|
|
527
879
|
platform: 'node',
|
|
528
880
|
conditions: ['import', 'module', 'node', 'default'],
|
|
529
881
|
target: 'es2022',
|
|
@@ -531,14 +883,24 @@ export const OPTIONS = handler;`;
|
|
|
531
883
|
treeShaking: true,
|
|
532
884
|
keepNames: true,
|
|
533
885
|
minify: false,
|
|
534
|
-
|
|
535
|
-
|
|
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,
|
|
536
898
|
mainFields: ['module', 'main'],
|
|
537
899
|
// Don't externalize anything - bundle everything including workflow packages
|
|
538
900
|
external: [],
|
|
539
901
|
});
|
|
540
902
|
this.logEsbuildMessages(result, 'webhook bundle creation');
|
|
541
|
-
|
|
903
|
+
this.logCreateWebhookBundleInfo('Created webhook bundle', `${Date.now() - webhookBundleStart}ms`);
|
|
542
904
|
}
|
|
543
905
|
/**
|
|
544
906
|
* Creates a package.json file with the specified module type.
|
|
@@ -557,6 +919,9 @@ export const OPTIONS = handler;`;
|
|
|
557
919
|
launcherType: config.launcherType ?? 'Nodejs',
|
|
558
920
|
architecture: config.architecture ?? 'arm64',
|
|
559
921
|
shouldAddHelpers: config.shouldAddHelpers ?? true,
|
|
922
|
+
...(config.maxDuration !== undefined && {
|
|
923
|
+
maxDuration: config.maxDuration,
|
|
924
|
+
}),
|
|
560
925
|
...(config.shouldAddSourcemapSupport !== undefined && {
|
|
561
926
|
shouldAddSourcemapSupport: config.shouldAddSourcemapSupport,
|
|
562
927
|
}),
|
|
@@ -586,5 +951,108 @@ export const OPTIONS = handler;`;
|
|
|
586
951
|
// We're intentionally silently ignoring this error - creating .gitignore isn't critical
|
|
587
952
|
}
|
|
588
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
|
+
}
|
|
589
1057
|
}
|
|
590
1058
|
//# sourceMappingURL=base-builder.js.map
|