@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.
@@ -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 inputFiles = Array.from(new Set([
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
- async buildDeferredEntriesUntilStable(inputFiles, implicitStepFiles) {
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 postBuildSignature = await this.createDeferredBuildSignature(inputFiles);
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 discoveredStepFiles = Array.from(new Set([...this.discoveredStepFiles, ...implicitStepFiles])).sort();
263
- const discoveredWorkflowFiles = Array.from(this.discoveredWorkflowFiles).sort();
264
- const trackedSerdeFiles = await this.collectTrackedSerdeFiles();
265
- const discoveredSerdeFiles = Array.from(new Set([...this.discoveredSerdeFiles, ...trackedSerdeFiles])).sort();
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
- await this.rewriteJsonFileWithStableKeyOrder(manifestBuildPath);
310
- await this.copyFileIfChanged(manifestBuildPath, manifestFilePath);
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 collectTrackedSerdeFiles() {
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 Array.from(serdeFiles);
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
- async buildStepsFunction({ inputFiles, workflowGeneratedDir, tsconfigPath, routeFileName = 'route.js', discoveredEntries, }) {
661
- // Create steps bundle
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
- return await this.createStepsBundle({
665
- // If any dynamic requires are used when bundling with ESM
666
- // esbuild will create a too dynamic wrapper around require
667
- // which turbopack/webpack fail to analyze. If we externalize
668
- // correctly this shouldn't be an issue although we might want
669
- // to use cjs as alternative to avoid
670
- format: 'esm',
671
- inputFiles,
672
- outfile: (0, node_path_1.join)(stepsRouteDir, routeFileName),
673
- externalizeNonSteps: true,
674
- tsconfigPath,
675
- discoveredEntries,
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');