@workflow/next 4.0.1-beta.51 → 4.0.1-beta.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder-deferred.d.ts.map +1 -1
- package/dist/builder-deferred.js +561 -36
- package/dist/builder-deferred.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +7 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +164 -75
- package/dist/loader.js.map +1 -1
- package/dist/step-copy-utils.d.ts +19 -0
- package/dist/step-copy-utils.d.ts.map +1 -0
- package/dist/step-copy-utils.js +101 -0
- package/dist/step-copy-utils.js.map +1 -0
- package/docs/api-reference/index.mdx +4 -0
- package/docs/api-reference/with-workflow.mdx +4 -0
- package/docs/next.mdx +7 -0
- package/package.json +3 -3
- package/dist/swc-cache.d.ts +0 -8
- package/dist/swc-cache.d.ts.map +0 -1
- package/dist/swc-cache.js +0 -53
- package/dist/swc-cache.js.map +0 -1
package/dist/builder-deferred.js
CHANGED
|
@@ -10,6 +10,7 @@ const promises_1 = require("node:fs/promises");
|
|
|
10
10
|
const node_os_1 = __importDefault(require("node:os"));
|
|
11
11
|
const node_path_1 = require("node:path");
|
|
12
12
|
const socket_server_js_1 = require("./socket-server.js");
|
|
13
|
+
const step_copy_utils_js_1 = require("./step-copy-utils.js");
|
|
13
14
|
const ROUTE_STUB_FILE_MARKER = 'WORKFLOW_ROUTE_STUB_FILE';
|
|
14
15
|
let CachedNextBuilderDeferred;
|
|
15
16
|
// Create the deferred Next builder dynamically by extending the ESM BaseBuilder.
|
|
@@ -19,7 +20,7 @@ async function getNextBuilderDeferred() {
|
|
|
19
20
|
if (CachedNextBuilderDeferred) {
|
|
20
21
|
return CachedNextBuilderDeferred;
|
|
21
22
|
}
|
|
22
|
-
const { BaseBuilder: BaseBuilderClass, STEP_QUEUE_TRIGGER, WORKFLOW_QUEUE_TRIGGER, detectWorkflowPatterns, isWorkflowSdkFile,
|
|
23
|
+
const { BaseBuilder: BaseBuilderClass, STEP_QUEUE_TRIGGER, WORKFLOW_QUEUE_TRIGGER, applySwcTransform, detectWorkflowPatterns, getImportPath, isWorkflowSdkFile, resolveWorkflowAliasRelativePath,
|
|
23
24
|
// biome-ignore lint/security/noGlobalEval: Need to use eval here to avoid TypeScript from transpiling the import statement into `require()`
|
|
24
25
|
} = (await eval('import("@workflow/builders")'));
|
|
25
26
|
class NextDeferredBuilder extends BaseBuilderClass {
|
|
@@ -35,6 +36,7 @@ async function getNextBuilderDeferred() {
|
|
|
35
36
|
async build() {
|
|
36
37
|
const outputDir = await this.findAppDirectory();
|
|
37
38
|
await this.initializeDiscoveryState();
|
|
39
|
+
await this.cleanupGeneratedArtifactsOnBoot(outputDir);
|
|
38
40
|
await this.writeStubFiles(outputDir);
|
|
39
41
|
await this.createDiscoverySocketServer();
|
|
40
42
|
}
|
|
@@ -42,12 +44,7 @@ async function getNextBuilderDeferred() {
|
|
|
42
44
|
await this.initializeDiscoveryState();
|
|
43
45
|
await this.validateDiscoveredEntryFiles();
|
|
44
46
|
const implicitStepFiles = await this.resolveImplicitStepFiles();
|
|
45
|
-
const
|
|
46
|
-
...this.discoveredWorkflowFiles,
|
|
47
|
-
...this.discoveredStepFiles,
|
|
48
|
-
...implicitStepFiles,
|
|
49
|
-
])).sort();
|
|
50
|
-
const pendingBuild = this.deferredBuildQueue.then(() => this.buildDeferredEntriesUntilStable(inputFiles, implicitStepFiles));
|
|
47
|
+
const pendingBuild = this.deferredBuildQueue.then(() => this.buildDeferredEntriesUntilStable(implicitStepFiles));
|
|
51
48
|
// Keep the queue chain alive even when the current build fails so future
|
|
52
49
|
// callbacks can enqueue another attempt without triggering unhandled
|
|
53
50
|
// rejection warnings.
|
|
@@ -56,12 +53,21 @@ async function getNextBuilderDeferred() {
|
|
|
56
53
|
});
|
|
57
54
|
await pendingBuild;
|
|
58
55
|
}
|
|
59
|
-
|
|
56
|
+
getCurrentInputFiles(implicitStepFiles) {
|
|
57
|
+
return Array.from(new Set([
|
|
58
|
+
...this.discoveredWorkflowFiles,
|
|
59
|
+
...this.discoveredStepFiles,
|
|
60
|
+
...this.discoveredSerdeFiles,
|
|
61
|
+
...implicitStepFiles,
|
|
62
|
+
])).sort();
|
|
63
|
+
}
|
|
64
|
+
async buildDeferredEntriesUntilStable(implicitStepFiles) {
|
|
60
65
|
// A successful build can discover additional transitive dependency files
|
|
61
66
|
// (via source maps), which changes the signature and may require one more
|
|
62
67
|
// build pass to include newly discovered serde files.
|
|
63
68
|
const maxBuildPasses = 3;
|
|
64
69
|
for (let buildPass = 0; buildPass < maxBuildPasses; buildPass++) {
|
|
70
|
+
const inputFiles = this.getCurrentInputFiles(implicitStepFiles);
|
|
65
71
|
const buildSignature = await this.createDeferredBuildSignature(inputFiles);
|
|
66
72
|
if (buildSignature === this.lastDeferredBuildSignature) {
|
|
67
73
|
return;
|
|
@@ -87,7 +93,8 @@ async function getNextBuilderDeferred() {
|
|
|
87
93
|
if (!didBuildSucceed) {
|
|
88
94
|
return;
|
|
89
95
|
}
|
|
90
|
-
const
|
|
96
|
+
const postBuildInputFiles = this.getCurrentInputFiles(implicitStepFiles);
|
|
97
|
+
const postBuildSignature = await this.createDeferredBuildSignature(postBuildInputFiles);
|
|
91
98
|
if (postBuildSignature === buildSignature) {
|
|
92
99
|
return;
|
|
93
100
|
}
|
|
@@ -259,21 +266,41 @@ async function getNextBuilderDeferred() {
|
|
|
259
266
|
await (0, promises_1.mkdir)(cacheDir, { recursive: true });
|
|
260
267
|
const manifestBuildDir = (0, node_path_1.join)(cacheDir, 'workflow-generated-manifest');
|
|
261
268
|
const tempRouteFileName = 'route.js.temp';
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
const trackedDiscoveredEntries = await this.collectTrackedDiscoveredEntries();
|
|
270
|
+
const discoveredStepFiles = Array.from(new Set([
|
|
271
|
+
...this.discoveredStepFiles,
|
|
272
|
+
...trackedDiscoveredEntries.discoveredSteps,
|
|
273
|
+
...implicitStepFiles,
|
|
274
|
+
])).sort();
|
|
275
|
+
const discoveredWorkflowFiles = Array.from(new Set([
|
|
276
|
+
...this.discoveredWorkflowFiles,
|
|
277
|
+
...trackedDiscoveredEntries.discoveredWorkflows,
|
|
278
|
+
])).sort();
|
|
279
|
+
const discoveredSerdeFileCandidates = Array.from(new Set([
|
|
280
|
+
...this.discoveredSerdeFiles,
|
|
281
|
+
...trackedDiscoveredEntries.discoveredSerdeFiles,
|
|
282
|
+
])).sort();
|
|
283
|
+
const discoveredSerdeFiles = await this.collectTransitiveSerdeFiles({
|
|
284
|
+
entryFiles: [...discoveredStepFiles, ...discoveredWorkflowFiles],
|
|
285
|
+
serdeFiles: discoveredSerdeFileCandidates,
|
|
286
|
+
});
|
|
266
287
|
const discoveredEntries = {
|
|
267
288
|
discoveredSteps: discoveredStepFiles,
|
|
268
289
|
discoveredWorkflows: discoveredWorkflowFiles,
|
|
269
290
|
discoveredSerdeFiles,
|
|
270
291
|
};
|
|
292
|
+
const buildInputFiles = Array.from(new Set([
|
|
293
|
+
...inputFiles,
|
|
294
|
+
...discoveredStepFiles,
|
|
295
|
+
...discoveredWorkflowFiles,
|
|
296
|
+
...discoveredSerdeFiles,
|
|
297
|
+
])).sort();
|
|
271
298
|
// Ensure output directories exist
|
|
272
299
|
await (0, promises_1.mkdir)(workflowGeneratedDir, { recursive: true });
|
|
273
300
|
await this.writeFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, '.gitignore'), '*');
|
|
274
301
|
const tsconfigPath = await this.findTsConfigPath();
|
|
275
302
|
const options = {
|
|
276
|
-
inputFiles,
|
|
303
|
+
inputFiles: buildInputFiles,
|
|
277
304
|
workflowGeneratedDir,
|
|
278
305
|
tsconfigPath,
|
|
279
306
|
routeFileName: tempRouteFileName,
|
|
@@ -306,8 +333,14 @@ async function getNextBuilderDeferred() {
|
|
|
306
333
|
manifestDir: manifestBuildDir,
|
|
307
334
|
manifest,
|
|
308
335
|
});
|
|
309
|
-
|
|
310
|
-
|
|
336
|
+
if (manifestJson) {
|
|
337
|
+
await this.rewriteJsonFileWithStableKeyOrder(manifestBuildPath);
|
|
338
|
+
await this.copyFileIfChanged(manifestBuildPath, manifestFilePath);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
await (0, promises_1.rm)(manifestBuildPath, { force: true });
|
|
342
|
+
await (0, promises_1.rm)(manifestFilePath, { force: true });
|
|
343
|
+
}
|
|
311
344
|
await this.writeFunctionsConfig(outputDir);
|
|
312
345
|
await this.copyFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, `flow/${tempRouteFileName}`), (0, node_path_1.join)(workflowGeneratedDir, 'flow/route.js'));
|
|
313
346
|
await this.copyFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, `step/${tempRouteFileName}`), (0, node_path_1.join)(workflowGeneratedDir, 'step/route.js'));
|
|
@@ -322,6 +355,41 @@ async function getNextBuilderDeferred() {
|
|
|
322
355
|
// Notify deferred entry loaders waiting on route.js stubs.
|
|
323
356
|
this.socketIO?.emit('build-complete');
|
|
324
357
|
}
|
|
358
|
+
async cleanupGeneratedArtifactsOnBoot(outputDir) {
|
|
359
|
+
const workflowGeneratedDir = (0, node_path_1.join)(outputDir, '.well-known/workflow/v1');
|
|
360
|
+
const flowRouteDir = (0, node_path_1.join)(workflowGeneratedDir, 'flow');
|
|
361
|
+
const stepRouteDir = (0, node_path_1.join)(workflowGeneratedDir, 'step');
|
|
362
|
+
const webhookRouteDir = (0, node_path_1.join)(workflowGeneratedDir, 'webhook/[token]');
|
|
363
|
+
const staleArtifactPaths = [
|
|
364
|
+
(0, node_path_1.join)(flowRouteDir, 'route.js.temp'),
|
|
365
|
+
(0, node_path_1.join)(flowRouteDir, 'route.js.temp.debug.json'),
|
|
366
|
+
(0, node_path_1.join)(flowRouteDir, 'route.js.debug.json'),
|
|
367
|
+
(0, node_path_1.join)(stepRouteDir, 'route.js.temp'),
|
|
368
|
+
(0, node_path_1.join)(stepRouteDir, 'route.js.temp.debug.json'),
|
|
369
|
+
(0, node_path_1.join)(stepRouteDir, 'route.js.debug.json'),
|
|
370
|
+
(0, node_path_1.join)(stepRouteDir, step_copy_utils_js_1.DEFERRED_STEP_COPY_DIR_NAME),
|
|
371
|
+
(0, node_path_1.join)(webhookRouteDir, 'route.js.temp'),
|
|
372
|
+
(0, node_path_1.join)(workflowGeneratedDir, 'manifest.json'),
|
|
373
|
+
];
|
|
374
|
+
await Promise.all(staleArtifactPaths.map((stalePath) => (0, promises_1.rm)(stalePath, { recursive: true, force: true })));
|
|
375
|
+
await Promise.all([
|
|
376
|
+
this.removeStaleDeferredTempFiles(flowRouteDir),
|
|
377
|
+
this.removeStaleDeferredTempFiles(stepRouteDir),
|
|
378
|
+
this.removeStaleDeferredTempFiles(webhookRouteDir),
|
|
379
|
+
]);
|
|
380
|
+
}
|
|
381
|
+
async removeStaleDeferredTempFiles(routeDir) {
|
|
382
|
+
const routeEntries = await (0, promises_1.readdir)(routeDir, {
|
|
383
|
+
withFileTypes: true,
|
|
384
|
+
}).catch(() => []);
|
|
385
|
+
await Promise.all(routeEntries
|
|
386
|
+
.filter((entry) => entry.isFile() &&
|
|
387
|
+
entry.name.startsWith('route.js.') &&
|
|
388
|
+
entry.name.endsWith('.tmp'))
|
|
389
|
+
.map((entry) => (0, promises_1.rm)((0, node_path_1.join)(routeDir, entry.name), {
|
|
390
|
+
force: true,
|
|
391
|
+
})));
|
|
392
|
+
}
|
|
325
393
|
async createDiscoverySocketServer() {
|
|
326
394
|
if (this.socketIO || process.env.WORKFLOW_SOCKET_PORT) {
|
|
327
395
|
return;
|
|
@@ -406,17 +474,25 @@ async function getNextBuilderDeferred() {
|
|
|
406
474
|
}
|
|
407
475
|
return signatureHash.digest('hex');
|
|
408
476
|
}
|
|
409
|
-
async
|
|
477
|
+
async collectTrackedDiscoveredEntries() {
|
|
410
478
|
if (this.trackedDependencyFiles.size === 0) {
|
|
411
|
-
return
|
|
479
|
+
return {
|
|
480
|
+
discoveredSteps: [],
|
|
481
|
+
discoveredWorkflows: [],
|
|
482
|
+
discoveredSerdeFiles: [],
|
|
483
|
+
};
|
|
412
484
|
}
|
|
413
|
-
const { serdeFiles } = await this.reconcileDiscoveredEntries({
|
|
414
|
-
workflowCandidates:
|
|
415
|
-
stepCandidates:
|
|
485
|
+
const { workflowFiles, stepFiles, serdeFiles } = await this.reconcileDiscoveredEntries({
|
|
486
|
+
workflowCandidates: this.trackedDependencyFiles,
|
|
487
|
+
stepCandidates: this.trackedDependencyFiles,
|
|
416
488
|
serdeCandidates: this.trackedDependencyFiles,
|
|
417
489
|
validatePatterns: true,
|
|
418
490
|
});
|
|
419
|
-
return
|
|
491
|
+
return {
|
|
492
|
+
discoveredSteps: Array.from(stepFiles).sort(),
|
|
493
|
+
discoveredWorkflows: Array.from(workflowFiles).sort(),
|
|
494
|
+
discoveredSerdeFiles: Array.from(serdeFiles).sort(),
|
|
495
|
+
};
|
|
420
496
|
}
|
|
421
497
|
async refreshTrackedDependencyFiles(workflowGeneratedDir, routeFileName) {
|
|
422
498
|
const bundleFiles = [
|
|
@@ -657,23 +733,472 @@ async function getNextBuilderDeferred() {
|
|
|
657
733
|
// Manifest may not exist (e.g. manifest generation failed); ignore.
|
|
658
734
|
}
|
|
659
735
|
}
|
|
660
|
-
|
|
661
|
-
|
|
736
|
+
mergeWorkflowManifest(target, source) {
|
|
737
|
+
if (source.steps) {
|
|
738
|
+
target.steps = Object.assign(target.steps || {}, source.steps);
|
|
739
|
+
}
|
|
740
|
+
if (source.workflows) {
|
|
741
|
+
target.workflows = Object.assign(target.workflows || {}, source.workflows);
|
|
742
|
+
}
|
|
743
|
+
if (source.classes) {
|
|
744
|
+
target.classes = Object.assign(target.classes || {}, source.classes);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async getRelativeFilenameForSwc(filePath) {
|
|
748
|
+
const workingDir = this.config.workingDir;
|
|
749
|
+
const normalizedWorkingDir = workingDir
|
|
750
|
+
.replace(/\\/g, '/')
|
|
751
|
+
.replace(/\/$/, '');
|
|
752
|
+
const normalizedFilepath = filePath.replace(/\\/g, '/');
|
|
753
|
+
// Windows fix: Use case-insensitive comparison to work around drive letter casing issues.
|
|
754
|
+
const lowerWd = normalizedWorkingDir.toLowerCase();
|
|
755
|
+
const lowerPath = normalizedFilepath.toLowerCase();
|
|
756
|
+
let relativeFilename;
|
|
757
|
+
if (lowerPath.startsWith(`${lowerWd}/`)) {
|
|
758
|
+
relativeFilename = normalizedFilepath.substring(normalizedWorkingDir.length + 1);
|
|
759
|
+
}
|
|
760
|
+
else if (lowerPath === lowerWd) {
|
|
761
|
+
relativeFilename = '.';
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
relativeFilename = (0, node_path_1.relative)(workingDir, filePath).replace(/\\/g, '/');
|
|
765
|
+
if (relativeFilename.startsWith('../')) {
|
|
766
|
+
const aliasedRelativePath = await resolveWorkflowAliasRelativePath(filePath, workingDir);
|
|
767
|
+
if (aliasedRelativePath) {
|
|
768
|
+
relativeFilename = aliasedRelativePath;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
relativeFilename = relativeFilename
|
|
772
|
+
.split('/')
|
|
773
|
+
.filter((part) => part !== '..')
|
|
774
|
+
.join('/');
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (relativeFilename.includes(':') || relativeFilename.startsWith('/')) {
|
|
779
|
+
relativeFilename = (0, node_path_1.basename)(normalizedFilepath);
|
|
780
|
+
}
|
|
781
|
+
return relativeFilename;
|
|
782
|
+
}
|
|
783
|
+
getRelativeImportSpecifier(fromFilePath, toFilePath) {
|
|
784
|
+
let relativePath = (0, node_path_1.relative)((0, node_path_1.dirname)(fromFilePath), toFilePath).replace(/\\/g, '/');
|
|
785
|
+
if (!relativePath.startsWith('.')) {
|
|
786
|
+
relativePath = `./${relativePath}`;
|
|
787
|
+
}
|
|
788
|
+
return relativePath;
|
|
789
|
+
}
|
|
790
|
+
getStepCopyFileName(filePath) {
|
|
791
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
792
|
+
const hash = (0, node_crypto_1.createHash)('sha256').update(normalizedPath).digest('hex');
|
|
793
|
+
const extension = (0, node_path_1.extname)(normalizedPath);
|
|
794
|
+
return `${hash.slice(0, 16)}${extension || '.js'}`;
|
|
795
|
+
}
|
|
796
|
+
rewriteCopiedStepImportSpecifier(specifier, sourceFilePath, copiedFilePath, copiedStepFileBySourcePath) {
|
|
797
|
+
if (!specifier.startsWith('.')) {
|
|
798
|
+
return specifier;
|
|
799
|
+
}
|
|
800
|
+
const specifierMatch = specifier.match(/^([^?#]+)(.*)$/);
|
|
801
|
+
const importPath = specifierMatch?.[1] ?? specifier;
|
|
802
|
+
const suffix = specifierMatch?.[2] ?? '';
|
|
803
|
+
const absoluteTargetPath = (0, node_path_1.resolve)((0, node_path_1.dirname)(sourceFilePath), importPath);
|
|
804
|
+
const resolvedTargetPath = this.resolveCopiedStepImportTargetPath(absoluteTargetPath);
|
|
805
|
+
const normalizedTargetPath = resolvedTargetPath.replace(/\\/g, '/');
|
|
806
|
+
const copiedTargetPath = copiedStepFileBySourcePath.get(normalizedTargetPath) ||
|
|
807
|
+
(() => {
|
|
808
|
+
try {
|
|
809
|
+
const realTargetPath = (0, node_fs_1.realpathSync)(resolvedTargetPath).replace(/\\/g, '/');
|
|
810
|
+
return copiedStepFileBySourcePath.get(realTargetPath);
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
})();
|
|
816
|
+
let rewrittenPath = (0, node_path_1.relative)((0, node_path_1.dirname)(copiedFilePath), copiedTargetPath || resolvedTargetPath).replace(/\\/g, '/');
|
|
817
|
+
if (!rewrittenPath.startsWith('.')) {
|
|
818
|
+
rewrittenPath = `./${rewrittenPath}`;
|
|
819
|
+
}
|
|
820
|
+
return `${rewrittenPath}${suffix}`;
|
|
821
|
+
}
|
|
822
|
+
resolveCopiedStepImportTargetPath(targetPath) {
|
|
823
|
+
if ((0, node_fs_1.existsSync)(targetPath)) {
|
|
824
|
+
return targetPath;
|
|
825
|
+
}
|
|
826
|
+
const extensionMatch = targetPath.match(/(\.[^./\\]+)$/);
|
|
827
|
+
const extension = extensionMatch?.[1]?.toLowerCase();
|
|
828
|
+
if (!extension) {
|
|
829
|
+
return targetPath;
|
|
830
|
+
}
|
|
831
|
+
const extensionFallbacks = extension === '.js'
|
|
832
|
+
? ['.ts', '.tsx', '.mts', '.cts']
|
|
833
|
+
: extension === '.mjs'
|
|
834
|
+
? ['.mts']
|
|
835
|
+
: extension === '.cjs'
|
|
836
|
+
? ['.cts']
|
|
837
|
+
: extension === '.jsx'
|
|
838
|
+
? ['.tsx']
|
|
839
|
+
: [];
|
|
840
|
+
if (extensionFallbacks.length === 0) {
|
|
841
|
+
return targetPath;
|
|
842
|
+
}
|
|
843
|
+
const targetWithoutExtension = targetPath.slice(0, -extension.length);
|
|
844
|
+
for (const fallbackExtension of extensionFallbacks) {
|
|
845
|
+
const fallbackPath = `${targetWithoutExtension}${fallbackExtension}`;
|
|
846
|
+
if ((0, node_fs_1.existsSync)(fallbackPath)) {
|
|
847
|
+
return fallbackPath;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return targetPath;
|
|
851
|
+
}
|
|
852
|
+
rewriteRelativeImportsForCopiedStep(source, sourceFilePath, copiedFilePath, copiedStepFileBySourcePath) {
|
|
853
|
+
const rewriteSpecifier = (specifier) => this.rewriteCopiedStepImportSpecifier(specifier, sourceFilePath, copiedFilePath, copiedStepFileBySourcePath);
|
|
854
|
+
const rewritePattern = (currentSource, pattern) => currentSource.replace(pattern, (_match, prefix, specifier, suffix) => `${prefix}${rewriteSpecifier(specifier)}${suffix}`);
|
|
855
|
+
let rewrittenSource = source;
|
|
856
|
+
rewrittenSource = rewritePattern(rewrittenSource, /(from\s+['"])([^'"]+)(['"])/g);
|
|
857
|
+
rewrittenSource = rewritePattern(rewrittenSource, /(import\s+['"])([^'"]+)(['"])/g);
|
|
858
|
+
rewrittenSource = rewritePattern(rewrittenSource, /(import\(\s*['"])([^'"]+)(['"]\s*\))/g);
|
|
859
|
+
rewrittenSource = rewritePattern(rewrittenSource, /(require\(\s*['"])([^'"]+)(['"]\s*\))/g);
|
|
860
|
+
return rewrittenSource;
|
|
861
|
+
}
|
|
862
|
+
extractRelativeImportSpecifiers(source) {
|
|
863
|
+
const relativeSpecifiers = new Set();
|
|
864
|
+
const importPatterns = [
|
|
865
|
+
/from\s+['"]([^'"]+)['"]/g,
|
|
866
|
+
/import\s+['"]([^'"]+)['"]/g,
|
|
867
|
+
/import\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
868
|
+
/require\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
869
|
+
];
|
|
870
|
+
for (const importPattern of importPatterns) {
|
|
871
|
+
for (const match of source.matchAll(importPattern)) {
|
|
872
|
+
const specifier = match[1];
|
|
873
|
+
if (specifier?.startsWith('.')) {
|
|
874
|
+
relativeSpecifiers.add(specifier);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return Array.from(relativeSpecifiers);
|
|
879
|
+
}
|
|
880
|
+
shouldSkipTransitiveStepFile(filePath) {
|
|
881
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
882
|
+
return (normalizedPath.includes('/.well-known/workflow/') ||
|
|
883
|
+
normalizedPath.includes('/.next/') ||
|
|
884
|
+
normalizedPath.includes('/node_modules/') ||
|
|
885
|
+
normalizedPath.includes('/.pnpm/'));
|
|
886
|
+
}
|
|
887
|
+
async resolveTransitiveStepImportTargetPath(sourceFilePath, specifier) {
|
|
888
|
+
const specifierMatch = specifier.match(/^([^?#]+)(.*)$/);
|
|
889
|
+
const importPath = specifierMatch?.[1] ?? specifier;
|
|
890
|
+
const absoluteTargetPath = (0, node_path_1.resolve)((0, node_path_1.dirname)(sourceFilePath), importPath);
|
|
891
|
+
const candidatePaths = new Set([
|
|
892
|
+
this.resolveCopiedStepImportTargetPath(absoluteTargetPath),
|
|
893
|
+
]);
|
|
894
|
+
if (!(0, node_path_1.extname)(absoluteTargetPath)) {
|
|
895
|
+
const extensionCandidates = [
|
|
896
|
+
'.ts',
|
|
897
|
+
'.tsx',
|
|
898
|
+
'.mts',
|
|
899
|
+
'.cts',
|
|
900
|
+
'.js',
|
|
901
|
+
'.jsx',
|
|
902
|
+
'.mjs',
|
|
903
|
+
'.cjs',
|
|
904
|
+
];
|
|
905
|
+
for (const extensionCandidate of extensionCandidates) {
|
|
906
|
+
candidatePaths.add(`${absoluteTargetPath}${extensionCandidate}`);
|
|
907
|
+
candidatePaths.add((0, node_path_1.join)(absoluteTargetPath, `index${extensionCandidate}`));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
for (const candidatePath of candidatePaths) {
|
|
911
|
+
const resolvedPath = this.resolveCopiedStepImportTargetPath(candidatePath);
|
|
912
|
+
const normalizedResolvedPath = this.normalizeDiscoveredFilePath(resolvedPath);
|
|
913
|
+
if (this.shouldSkipTransitiveStepFile(normalizedResolvedPath)) {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
const fileStats = await (0, promises_1.stat)(normalizedResolvedPath);
|
|
918
|
+
if (fileStats.isFile()) {
|
|
919
|
+
return normalizedResolvedPath;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
// Try the next candidate path.
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
async collectTransitiveStepFiles(stepFiles) {
|
|
929
|
+
const normalizedStepFiles = Array.from(new Set(stepFiles.map((stepFile) => this.normalizeDiscoveredFilePath(stepFile)))).sort();
|
|
930
|
+
const discoveredStepFiles = new Set(normalizedStepFiles);
|
|
931
|
+
const queuedFiles = [...normalizedStepFiles];
|
|
932
|
+
const visitedFiles = new Set();
|
|
933
|
+
const sourceCache = new Map();
|
|
934
|
+
const patternCache = new Map();
|
|
935
|
+
const getSource = async (filePath) => {
|
|
936
|
+
if (sourceCache.has(filePath)) {
|
|
937
|
+
return sourceCache.get(filePath) ?? null;
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
const source = await (0, promises_1.readFile)(filePath, 'utf-8');
|
|
941
|
+
sourceCache.set(filePath, source);
|
|
942
|
+
return source;
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
sourceCache.set(filePath, null);
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
const getPatterns = async (filePath) => {
|
|
950
|
+
if (patternCache.has(filePath)) {
|
|
951
|
+
return patternCache.get(filePath) ?? null;
|
|
952
|
+
}
|
|
953
|
+
const source = await getSource(filePath);
|
|
954
|
+
if (source === null) {
|
|
955
|
+
patternCache.set(filePath, null);
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
const patterns = detectWorkflowPatterns(source);
|
|
959
|
+
patternCache.set(filePath, patterns);
|
|
960
|
+
return patterns;
|
|
961
|
+
};
|
|
962
|
+
while (queuedFiles.length > 0) {
|
|
963
|
+
const currentFile = queuedFiles.pop();
|
|
964
|
+
if (!currentFile || visitedFiles.has(currentFile)) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
visitedFiles.add(currentFile);
|
|
968
|
+
const currentSource = await getSource(currentFile);
|
|
969
|
+
if (currentSource === null) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const relativeImportSpecifiers = this.extractRelativeImportSpecifiers(currentSource);
|
|
973
|
+
for (const specifier of relativeImportSpecifiers) {
|
|
974
|
+
const resolvedImportPath = await this.resolveTransitiveStepImportTargetPath(currentFile, specifier);
|
|
975
|
+
if (!resolvedImportPath) {
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (!visitedFiles.has(resolvedImportPath)) {
|
|
979
|
+
queuedFiles.push(resolvedImportPath);
|
|
980
|
+
}
|
|
981
|
+
const importPatterns = await getPatterns(resolvedImportPath);
|
|
982
|
+
if (importPatterns?.hasUseStep) {
|
|
983
|
+
discoveredStepFiles.add(resolvedImportPath);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return Array.from(discoveredStepFiles).sort();
|
|
988
|
+
}
|
|
989
|
+
async collectTransitiveSerdeFiles({ entryFiles, serdeFiles, }) {
|
|
990
|
+
const normalizedEntryFiles = Array.from(new Set(entryFiles.map((entryFile) => this.normalizeDiscoveredFilePath(entryFile)))).sort();
|
|
991
|
+
const discoveredSerdeFiles = new Set(serdeFiles.map((serdeFile) => this.normalizeDiscoveredFilePath(serdeFile)));
|
|
992
|
+
const queuedFiles = Array.from(new Set([...normalizedEntryFiles, ...discoveredSerdeFiles]));
|
|
993
|
+
const visitedFiles = new Set();
|
|
994
|
+
const sourceCache = new Map();
|
|
995
|
+
const patternCache = new Map();
|
|
996
|
+
const getSource = async (filePath) => {
|
|
997
|
+
if (sourceCache.has(filePath)) {
|
|
998
|
+
return sourceCache.get(filePath) ?? null;
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const source = await (0, promises_1.readFile)(filePath, 'utf-8');
|
|
1002
|
+
sourceCache.set(filePath, source);
|
|
1003
|
+
return source;
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
sourceCache.set(filePath, null);
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
const getPatterns = async (filePath) => {
|
|
1011
|
+
if (patternCache.has(filePath)) {
|
|
1012
|
+
return patternCache.get(filePath) ?? null;
|
|
1013
|
+
}
|
|
1014
|
+
const source = await getSource(filePath);
|
|
1015
|
+
if (source === null) {
|
|
1016
|
+
patternCache.set(filePath, null);
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
const patterns = detectWorkflowPatterns(source);
|
|
1020
|
+
patternCache.set(filePath, patterns);
|
|
1021
|
+
return patterns;
|
|
1022
|
+
};
|
|
1023
|
+
while (queuedFiles.length > 0) {
|
|
1024
|
+
const currentFile = queuedFiles.pop();
|
|
1025
|
+
if (!currentFile || visitedFiles.has(currentFile)) {
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
visitedFiles.add(currentFile);
|
|
1029
|
+
const currentSource = await getSource(currentFile);
|
|
1030
|
+
if (currentSource === null) {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
const relativeImportSpecifiers = this.extractRelativeImportSpecifiers(currentSource);
|
|
1034
|
+
for (const specifier of relativeImportSpecifiers) {
|
|
1035
|
+
const resolvedImportPath = await this.resolveTransitiveStepImportTargetPath(currentFile, specifier);
|
|
1036
|
+
if (!resolvedImportPath) {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (!visitedFiles.has(resolvedImportPath)) {
|
|
1040
|
+
queuedFiles.push(resolvedImportPath);
|
|
1041
|
+
}
|
|
1042
|
+
const importPatterns = await getPatterns(resolvedImportPath);
|
|
1043
|
+
if (importPatterns?.hasSerde &&
|
|
1044
|
+
!isWorkflowSdkFile(resolvedImportPath)) {
|
|
1045
|
+
discoveredSerdeFiles.add(resolvedImportPath);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return Array.from(discoveredSerdeFiles).sort();
|
|
1050
|
+
}
|
|
1051
|
+
resolveBuiltInStepFilePath() {
|
|
1052
|
+
try {
|
|
1053
|
+
return this.normalizeDiscoveredFilePath(require.resolve('workflow/internal/builtins', {
|
|
1054
|
+
paths: [this.config.workingDir],
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
catch {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async copyDiscoveredStepFiles({ stepFiles, stepsRouteDir, }) {
|
|
1062
|
+
const copiedStepsDir = (0, node_path_1.join)(stepsRouteDir, step_copy_utils_js_1.DEFERRED_STEP_COPY_DIR_NAME);
|
|
1063
|
+
await (0, promises_1.mkdir)(copiedStepsDir, { recursive: true });
|
|
1064
|
+
const normalizedStepFiles = Array.from(new Set(stepFiles.map((stepFile) => this.normalizeDiscoveredFilePath(stepFile)))).sort();
|
|
1065
|
+
const copiedStepFileBySourcePath = new Map();
|
|
1066
|
+
const expectedFileNames = new Set();
|
|
1067
|
+
const copiedStepFiles = [];
|
|
1068
|
+
for (const normalizedStepFile of normalizedStepFiles) {
|
|
1069
|
+
const copiedFileName = this.getStepCopyFileName(normalizedStepFile);
|
|
1070
|
+
const copiedFilePath = (0, node_path_1.join)(copiedStepsDir, copiedFileName);
|
|
1071
|
+
const normalizedPathKey = normalizedStepFile.replace(/\\/g, '/');
|
|
1072
|
+
copiedStepFileBySourcePath.set(normalizedPathKey, copiedFilePath);
|
|
1073
|
+
try {
|
|
1074
|
+
const realPathKey = (0, node_fs_1.realpathSync)(normalizedStepFile).replace(/\\/g, '/');
|
|
1075
|
+
copiedStepFileBySourcePath.set(realPathKey, copiedFilePath);
|
|
1076
|
+
}
|
|
1077
|
+
catch {
|
|
1078
|
+
// Keep best-effort mapping when source cannot be realpath-resolved.
|
|
1079
|
+
}
|
|
1080
|
+
expectedFileNames.add(copiedFileName);
|
|
1081
|
+
copiedStepFiles.push(copiedFilePath);
|
|
1082
|
+
}
|
|
1083
|
+
for (const normalizedStepFile of normalizedStepFiles) {
|
|
1084
|
+
const source = await (0, promises_1.readFile)(normalizedStepFile, 'utf-8');
|
|
1085
|
+
const copiedFilePath = copiedStepFileBySourcePath.get(normalizedStepFile.replace(/\\/g, '/'));
|
|
1086
|
+
if (!copiedFilePath) {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const rewrittenSource = this.rewriteRelativeImportsForCopiedStep(source, normalizedStepFile, copiedFilePath, copiedStepFileBySourcePath);
|
|
1090
|
+
const metadataComment = (0, step_copy_utils_js_1.createDeferredStepSourceMetadataComment)({
|
|
1091
|
+
relativeFilename: await this.getRelativeFilenameForSwc(normalizedStepFile),
|
|
1092
|
+
absolutePath: normalizedStepFile.replace(/\\/g, '/'),
|
|
1093
|
+
});
|
|
1094
|
+
const sourceMapComment = (0, step_copy_utils_js_1.createDeferredStepCopyInlineSourceMapComment)({
|
|
1095
|
+
sourcePath: normalizedStepFile,
|
|
1096
|
+
sourceContent: source,
|
|
1097
|
+
generatedContent: rewrittenSource,
|
|
1098
|
+
});
|
|
1099
|
+
const copiedSource = `${metadataComment}\n${rewrittenSource}\n${sourceMapComment}`;
|
|
1100
|
+
await this.writeFileIfChanged(copiedFilePath, copiedSource);
|
|
1101
|
+
}
|
|
1102
|
+
const existingEntries = await (0, promises_1.readdir)(copiedStepsDir, {
|
|
1103
|
+
withFileTypes: true,
|
|
1104
|
+
}).catch(() => []);
|
|
1105
|
+
await Promise.all(existingEntries
|
|
1106
|
+
.filter((entry) => !expectedFileNames.has(entry.name))
|
|
1107
|
+
.map((entry) => (0, promises_1.rm)((0, node_path_1.join)(copiedStepsDir, entry.name), {
|
|
1108
|
+
recursive: true,
|
|
1109
|
+
force: true,
|
|
1110
|
+
})));
|
|
1111
|
+
return copiedStepFiles;
|
|
1112
|
+
}
|
|
1113
|
+
async createDeferredStepsManifest({ stepFiles, workflowFiles, serdeOnlyFiles, }) {
|
|
1114
|
+
const workflowManifest = {};
|
|
1115
|
+
const filesForStepTransform = Array.from(new Set([...stepFiles, ...serdeOnlyFiles])).sort();
|
|
1116
|
+
await Promise.all(filesForStepTransform.map(async (stepFile) => {
|
|
1117
|
+
const source = await (0, promises_1.readFile)(stepFile, 'utf-8');
|
|
1118
|
+
const relativeFilename = await this.getRelativeFilenameForSwc(stepFile);
|
|
1119
|
+
const { workflowManifest: fileManifest } = await applySwcTransform(relativeFilename, source, 'step', stepFile);
|
|
1120
|
+
this.mergeWorkflowManifest(workflowManifest, fileManifest);
|
|
1121
|
+
}));
|
|
1122
|
+
const stepFileSet = new Set(stepFiles);
|
|
1123
|
+
const workflowOnlyFiles = workflowFiles
|
|
1124
|
+
.filter((workflowFile) => !stepFileSet.has(workflowFile))
|
|
1125
|
+
.sort();
|
|
1126
|
+
await Promise.all(workflowOnlyFiles.map(async (workflowFile) => {
|
|
1127
|
+
try {
|
|
1128
|
+
const source = await (0, promises_1.readFile)(workflowFile, 'utf-8');
|
|
1129
|
+
const relativeFilename = await this.getRelativeFilenameForSwc(workflowFile);
|
|
1130
|
+
const { workflowManifest: fileManifest } = await applySwcTransform(relativeFilename, source, 'workflow', workflowFile);
|
|
1131
|
+
this.mergeWorkflowManifest(workflowManifest, {
|
|
1132
|
+
workflows: fileManifest.workflows,
|
|
1133
|
+
classes: fileManifest.classes,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
catch (error) {
|
|
1137
|
+
console.log(`Warning: Failed to extract workflow metadata from ${workflowFile}:`, error instanceof Error ? error.message : String(error));
|
|
1138
|
+
}
|
|
1139
|
+
}));
|
|
1140
|
+
return workflowManifest;
|
|
1141
|
+
}
|
|
1142
|
+
async buildStepsFunction({ workflowGeneratedDir, routeFileName = 'route.js', discoveredEntries, }) {
|
|
662
1143
|
const stepsRouteDir = (0, node_path_1.join)(workflowGeneratedDir, 'step');
|
|
663
1144
|
await (0, promises_1.mkdir)(stepsRouteDir, { recursive: true });
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1145
|
+
const discovered = discoveredEntries;
|
|
1146
|
+
const stepFiles = await this.collectTransitiveStepFiles([...discovered.discoveredSteps].sort());
|
|
1147
|
+
const workflowFiles = [...discovered.discoveredWorkflows].sort();
|
|
1148
|
+
const serdeFiles = [...discovered.discoveredSerdeFiles].sort();
|
|
1149
|
+
const stepFileSet = new Set(stepFiles);
|
|
1150
|
+
const serdeOnlyFiles = serdeFiles.filter((file) => !stepFileSet.has(file));
|
|
1151
|
+
const builtInStepFilePath = this.resolveBuiltInStepFilePath();
|
|
1152
|
+
const copiedStepSourceFiles = builtInStepFilePath
|
|
1153
|
+
? [
|
|
1154
|
+
builtInStepFilePath,
|
|
1155
|
+
...stepFiles.filter((stepFile) => stepFile !== builtInStepFilePath),
|
|
1156
|
+
]
|
|
1157
|
+
: stepFiles;
|
|
1158
|
+
const copiedStepFiles = await this.copyDiscoveredStepFiles({
|
|
1159
|
+
stepFiles: copiedStepSourceFiles,
|
|
1160
|
+
stepsRouteDir,
|
|
676
1161
|
});
|
|
1162
|
+
const stepRouteFile = (0, node_path_1.join)(stepsRouteDir, routeFileName);
|
|
1163
|
+
const copiedStepImports = copiedStepFiles
|
|
1164
|
+
.map((copiedStepFile) => {
|
|
1165
|
+
const importSpecifier = this.getRelativeImportSpecifier(stepRouteFile, copiedStepFile);
|
|
1166
|
+
return `import '${importSpecifier}';`;
|
|
1167
|
+
})
|
|
1168
|
+
.join('\n');
|
|
1169
|
+
const serdeImports = serdeOnlyFiles
|
|
1170
|
+
.map((serdeFile) => {
|
|
1171
|
+
const normalizedSerdeFile = this.normalizeDiscoveredFilePath(serdeFile);
|
|
1172
|
+
const { importPath, isPackage } = getImportPath(normalizedSerdeFile, this.config.workingDir);
|
|
1173
|
+
if (isPackage) {
|
|
1174
|
+
return `import '${importPath}';`;
|
|
1175
|
+
}
|
|
1176
|
+
const importSpecifier = this.getRelativeImportSpecifier(stepRouteFile, normalizedSerdeFile);
|
|
1177
|
+
return `import '${importSpecifier}';`;
|
|
1178
|
+
})
|
|
1179
|
+
.join('\n');
|
|
1180
|
+
const routeContents = [
|
|
1181
|
+
'// biome-ignore-all lint: generated file',
|
|
1182
|
+
'/* eslint-disable */',
|
|
1183
|
+
builtInStepFilePath ? '' : "import 'workflow/internal/builtins';",
|
|
1184
|
+
copiedStepImports,
|
|
1185
|
+
serdeImports
|
|
1186
|
+
? `// Serde files for cross-context class registration\n${serdeImports}`
|
|
1187
|
+
: '',
|
|
1188
|
+
"export { stepEntrypoint as POST } from 'workflow/runtime';",
|
|
1189
|
+
]
|
|
1190
|
+
.filter(Boolean)
|
|
1191
|
+
.join('\n');
|
|
1192
|
+
await this.writeFileIfChanged(stepRouteFile, routeContents);
|
|
1193
|
+
const manifest = await this.createDeferredStepsManifest({
|
|
1194
|
+
stepFiles: copiedStepSourceFiles,
|
|
1195
|
+
workflowFiles,
|
|
1196
|
+
serdeOnlyFiles,
|
|
1197
|
+
});
|
|
1198
|
+
return {
|
|
1199
|
+
context: undefined,
|
|
1200
|
+
manifest,
|
|
1201
|
+
};
|
|
677
1202
|
}
|
|
678
1203
|
async buildWorkflowsFunction({ inputFiles, workflowGeneratedDir, tsconfigPath, routeFileName = 'route.js', discoveredEntries, }) {
|
|
679
1204
|
const workflowsRouteDir = (0, node_path_1.join)(workflowGeneratedDir, 'flow');
|