@workflow/next 5.0.0-beta.4 → 5.0.0-beta.5

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.
@@ -7,10 +7,10 @@ exports.getNextBuilderDeferred = getNextBuilderDeferred;
7
7
  const node_crypto_1 = require("node:crypto");
8
8
  const node_fs_1 = require("node:fs");
9
9
  const promises_1 = require("node:fs/promises");
10
+ const node_module_1 = require("node:module");
10
11
  const node_os_1 = __importDefault(require("node:os"));
11
12
  const node_path_1 = require("node:path");
12
13
  const socket_server_js_1 = require("./socket-server.js");
13
- const DEFERRED_STEP_COPY_DIR_NAME = '__workflow_step_files__';
14
14
  const ROUTE_STUB_FILE_MARKER = 'WORKFLOW_ROUTE_STUB_FILE';
15
15
  const ROUTE_STUB_MARKER_SCAN_BYTES = 4 * 1024;
16
16
  let CachedNextBuilderDeferred;
@@ -21,7 +21,13 @@ async function getNextBuilderDeferred() {
21
21
  if (CachedNextBuilderDeferred) {
22
22
  return CachedNextBuilderDeferred;
23
23
  }
24
- const { BaseBuilder: BaseBuilderClass, STEP_QUEUE_TRIGGER, WORKFLOW_QUEUE_TRIGGER, applySwcTransform, detectWorkflowPatterns, getImportPath, resolveWorkflowAliasRelativePath,
24
+ // V2: STEP_QUEUE_TRIGGER, getImportPath, and enhanced-resolve infrastructure
25
+ // were removed because the V2 combined handler eliminates the separate step
26
+ // route/topic. The step copy import rewriting (getRelativeImportSpecifier,
27
+ // getStepCopyFileName, rewriteRelativeImportsForCopiedStep) from main was also
28
+ // removed — V2 doesn't use step copies. If step copy support is needed, it
29
+ // should land as a complete feature set.
30
+ const { BaseBuilder: BaseBuilderClass, WORKFLOW_QUEUE_TRIGGER, detectWorkflowPatterns, applySwcTransform, resolveWorkflowAliasRelativePath,
25
31
  // biome-ignore lint/security/noGlobalEval: Need to use eval here to avoid TypeScript from transpiling the import statement into `require()`
26
32
  } = (await eval('import("@workflow/builders")'));
27
33
  class NextDeferredBuilder extends BaseBuilderClass {
@@ -35,7 +41,6 @@ async function getNextBuilderDeferred() {
35
41
  cacheWriteTimer = null;
36
42
  deferredRebuildTimer = null;
37
43
  lastDeferredBuildSignature = null;
38
- manifestStepResolveBaseDirs = null;
39
44
  async build() {
40
45
  const outputDir = await this.findAppDirectory();
41
46
  await this.initializeDiscoveryState();
@@ -99,6 +104,14 @@ async function getNextBuilderDeferred() {
99
104
  }
100
105
  }
101
106
  this.lastDeferredBuildSignature = buildSignature;
107
+ if (!this.config.watch) {
108
+ // Production builds can persist newly discovered deferred-entry files to
109
+ // the cache after the first pass completes. Reload that cache before we
110
+ // decide whether the input signature stabilized so staged tarball builds
111
+ // can immediately replay with the expanded step set.
112
+ await new Promise((resolve) => setTimeout(resolve, 250));
113
+ await this.loadWorkflowsCache();
114
+ }
102
115
  const postBuildInputFiles = this.getCurrentInputFiles(implicitStepFiles);
103
116
  const postBuildSignature = await this.createDeferredBuildSignature(postBuildInputFiles);
104
117
  if (postBuildSignature === buildSignature) {
@@ -115,15 +128,11 @@ async function getNextBuilderDeferred() {
115
128
  const outputDir = await this.findAppDirectory();
116
129
  const generatedRouteFiles = [
117
130
  (0, node_path_1.join)(outputDir, '.well-known/workflow/v1/flow/route.js'),
118
- (0, node_path_1.join)(outputDir, '.well-known/workflow/v1/step/route.js'),
119
131
  (0, node_path_1.join)(outputDir, '.well-known/workflow/v1/webhook/[token]/route.js'),
120
132
  ];
121
133
  for (const routeFilePath of generatedRouteFiles) {
122
134
  const routeState = await this.getGeneratedRouteState(routeFilePath);
123
- if (routeState === 'missing') {
124
- return true;
125
- }
126
- if (routeState === 'stub') {
135
+ if (routeState === 'missing' || routeState === 'stub') {
127
136
  return true;
128
137
  }
129
138
  }
@@ -341,18 +350,17 @@ async function getNextBuilderDeferred() {
341
350
  ...this.discoveredSerdeFiles,
342
351
  ...trackedDiscoveredEntries.discoveredSerdeFiles,
343
352
  ])).sort();
344
- const discoveredStepFiles = await this.filterExistingFiles(discoveredStepFileCandidates);
345
353
  const discoveredWorkflowFiles = await this.filterExistingFiles(discoveredWorkflowFileCandidates);
354
+ const existingStepFileCandidates = await this.filterExistingFiles(discoveredStepFileCandidates);
355
+ const discoveredStepFiles = await this.collectTransitiveStepFiles({
356
+ entryFiles: [...existingStepFileCandidates, ...discoveredWorkflowFiles],
357
+ stepFiles: existingStepFileCandidates,
358
+ });
346
359
  const existingSerdeFileCandidates = await this.filterExistingFiles(discoveredSerdeFileCandidates);
347
360
  const discoveredSerdeFiles = await this.collectTransitiveSerdeFiles({
348
361
  entryFiles: [...discoveredStepFiles, ...discoveredWorkflowFiles],
349
362
  serdeFiles: existingSerdeFileCandidates,
350
363
  });
351
- const discoveredEntries = {
352
- discoveredSteps: new Set(discoveredStepFiles),
353
- discoveredWorkflows: new Set(discoveredWorkflowFiles),
354
- discoveredSerdeFiles: new Set(discoveredSerdeFiles),
355
- };
356
364
  const existingInputFiles = await this.filterExistingFiles(inputFiles);
357
365
  const buildInputFiles = Array.from(new Set([
358
366
  ...existingInputFiles,
@@ -360,38 +368,42 @@ async function getNextBuilderDeferred() {
360
368
  ...discoveredWorkflowFiles,
361
369
  ...discoveredSerdeFiles,
362
370
  ])).sort();
371
+ const discoveredEntries = {
372
+ discoveredSteps: new Set(discoveredStepFiles),
373
+ discoveredWorkflows: new Set(discoveredWorkflowFiles),
374
+ discoveredSerdeFiles: new Set(discoveredSerdeFiles),
375
+ };
363
376
  // Ensure output directories exist
364
377
  await (0, promises_1.mkdir)(workflowGeneratedDir, { recursive: true });
365
378
  await this.writeFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, '.gitignore'), '*');
366
379
  const tsconfigPath = await this.findTsConfigPath();
367
- const options = {
380
+ // V2: Build combined route (replaces separate step + flow routes)
381
+ const flowRouteDir = (0, node_path_1.join)(workflowGeneratedDir, 'flow');
382
+ await (0, promises_1.mkdir)(flowRouteDir, { recursive: true });
383
+ // Write step registrations to final name directly (not temp) so the
384
+ // import path in the flow route is correct. The flow route uses temp
385
+ // naming to avoid HMR churn via copyFileIfChanged, but the step
386
+ // registrations file is a side-effect import and doesn't need that.
387
+ const stepsOutfile = (0, node_path_1.join)(flowRouteDir, '__step_registrations.js');
388
+ const combinedResult = await this.createCombinedBundle({
389
+ format: 'esm',
368
390
  inputFiles: buildInputFiles,
369
- workflowGeneratedDir,
391
+ stepsOutfile,
392
+ flowOutfile: (0, node_path_1.join)(flowRouteDir, tempRouteFileName),
393
+ bundleFinalOutput: false,
394
+ externalizeNonSteps: true,
370
395
  tsconfigPath,
371
- routeFileName: tempRouteFileName,
372
396
  discoveredEntries,
373
- };
374
- const workflowsBundle = await this.buildWorkflowsFunction(options);
375
- const { manifest: stepsManifest } = await this.buildStepsFunction({
376
- ...options,
377
- additionalStepSourceManifest: workflowsBundle?.manifest,
378
397
  });
379
398
  await this.buildWebhookRoute({
380
399
  workflowGeneratedDir,
381
400
  routeFileName: tempRouteFileName,
382
401
  });
383
402
  await this.refreshTrackedDependencyFiles(workflowGeneratedDir, tempRouteFileName);
384
- // Merge manifests from both bundles
385
403
  const manifest = {
386
- steps: { ...stepsManifest.steps, ...workflowsBundle?.manifest?.steps },
387
- workflows: {
388
- ...stepsManifest.workflows,
389
- ...workflowsBundle?.manifest?.workflows,
390
- },
391
- classes: {
392
- ...stepsManifest.classes,
393
- ...workflowsBundle?.manifest?.classes,
394
- },
404
+ steps: { ...combinedResult?.manifest?.steps },
405
+ workflows: { ...combinedResult?.manifest?.workflows },
406
+ classes: { ...combinedResult?.manifest?.classes },
395
407
  };
396
408
  const manifestFilePath = (0, node_path_1.join)(workflowGeneratedDir, 'manifest.json');
397
409
  const manifestBuildPath = (0, node_path_1.join)(manifestBuildDir, 'manifest.json');
@@ -410,8 +422,8 @@ async function getNextBuilderDeferred() {
410
422
  await (0, promises_1.rm)(manifestFilePath, { force: true });
411
423
  }
412
424
  await this.writeFunctionsConfig(outputDir);
425
+ // V2: Combined route (flow) — step registrations already at final path
413
426
  await this.copyFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, `flow/${tempRouteFileName}`), (0, node_path_1.join)(workflowGeneratedDir, 'flow/route.js'));
414
- await this.copyFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, `step/${tempRouteFileName}`), (0, node_path_1.join)(workflowGeneratedDir, 'step/route.js'));
415
427
  await this.copyFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, `webhook/[token]/${tempRouteFileName}`), (0, node_path_1.join)(workflowGeneratedDir, 'webhook/[token]/route.js'));
416
428
  // Expose manifest as a static file when WORKFLOW_PUBLIC_MANIFEST=1.
417
429
  // Next.js serves files from public/ at the root URL.
@@ -432,17 +444,16 @@ async function getNextBuilderDeferred() {
432
444
  (0, node_path_1.join)(flowRouteDir, 'route.js.temp'),
433
445
  (0, node_path_1.join)(flowRouteDir, 'route.js.temp.debug.json'),
434
446
  (0, node_path_1.join)(flowRouteDir, 'route.js.debug.json'),
435
- (0, node_path_1.join)(stepRouteDir, 'route.js.temp'),
436
- (0, node_path_1.join)(stepRouteDir, 'route.js.temp.debug.json'),
437
- (0, node_path_1.join)(stepRouteDir, 'route.js.debug.json'),
438
- (0, node_path_1.join)(stepRouteDir, DEFERRED_STEP_COPY_DIR_NAME),
447
+ (0, node_path_1.join)(flowRouteDir, '__step_registrations.route.js.temp'),
448
+ (0, node_path_1.join)(flowRouteDir, '__step_registrations.route.js.temp.debug.json'),
449
+ // V2: clean up stale V1 step route directory
450
+ stepRouteDir,
439
451
  (0, node_path_1.join)(webhookRouteDir, 'route.js.temp'),
440
452
  (0, node_path_1.join)(workflowGeneratedDir, 'manifest.json'),
441
453
  ];
442
454
  await Promise.all(staleArtifactPaths.map((stalePath) => (0, promises_1.rm)(stalePath, { recursive: true, force: true })));
443
455
  await Promise.all([
444
456
  this.removeStaleDeferredTempFiles(flowRouteDir),
445
- this.removeStaleDeferredTempFiles(stepRouteDir),
446
457
  this.removeStaleDeferredTempFiles(webhookRouteDir),
447
458
  ]);
448
459
  }
@@ -503,7 +514,11 @@ async function getNextBuilderDeferred() {
503
514
  if (hasCacheTrackingChange) {
504
515
  this.scheduleWorkflowsCacheWrite();
505
516
  }
506
- if (hasCacheTrackingChange || wasTrackedDependency) {
517
+ if (hasWorkflow ||
518
+ hasStep ||
519
+ hasSerde ||
520
+ hasCacheTrackingChange ||
521
+ wasTrackedDependency) {
507
522
  this.scheduleDeferredRebuild();
508
523
  }
509
524
  },
@@ -531,39 +546,66 @@ async function getNextBuilderDeferred() {
531
546
  getSocketInfoFilePath() {
532
547
  return (0, node_path_1.join)(this.config.workingDir, this.getDistDir(), 'cache', 'workflow-socket.json');
533
548
  }
534
- normalizeDiscoveredFilePath(filePath) {
535
- const resolvedPath = (0, node_path_1.isAbsolute)(filePath)
536
- ? filePath
537
- : (0, node_path_1.resolve)(this.config.workingDir, filePath);
538
- try {
539
- return (0, node_fs_1.realpathSync)(resolvedPath);
540
- }
541
- catch {
542
- return resolvedPath;
549
+ findPackageJsonPath(filePath) {
550
+ let currentDir = (0, node_path_1.dirname)(filePath);
551
+ let previousDir = '';
552
+ while (currentDir !== previousDir) {
553
+ const packageJsonPath = (0, node_path_1.join)(currentDir, 'package.json');
554
+ if ((0, node_fs_1.existsSync)(packageJsonPath)) {
555
+ return packageJsonPath;
556
+ }
557
+ previousDir = currentDir;
558
+ currentDir = (0, node_path_1.dirname)(currentDir);
543
559
  }
560
+ return null;
544
561
  }
545
- getManifestStepResolveBaseDirs() {
546
- if (this.manifestStepResolveBaseDirs) {
547
- return this.manifestStepResolveBaseDirs;
562
+ shouldPreferSourceBackedPackagePath(filePath) {
563
+ const normalizedPath = filePath.replace(/\\/g, '/');
564
+ // Only prefer source for workspace packages (not in node_modules).
565
+ // For tarball-installed packages, using source-backed paths causes
566
+ // esbuild to bundle the full source tree (including world.ts with
567
+ // process.cwd()) instead of externalizing properly.
568
+ if (normalizedPath.includes('/packages/') &&
569
+ !normalizedPath.includes('/node_modules/')) {
570
+ return true;
548
571
  }
549
- const resolveBaseDirs = new Set();
550
- const addResolveBaseDir = (baseDir) => {
551
- resolveBaseDirs.add(this.normalizeDiscoveredFilePath(baseDir));
552
- };
553
- if (this.config.projectRoot) {
554
- addResolveBaseDir(this.config.projectRoot);
572
+ if (!normalizedPath.includes('/node_modules/')) {
573
+ return false;
555
574
  }
556
- let currentResolveDir = this.config.workingDir;
557
- while (currentResolveDir) {
558
- addResolveBaseDir(currentResolveDir);
559
- const parentResolveDir = (0, node_path_1.dirname)(currentResolveDir);
560
- if (parentResolveDir === currentResolveDir) {
561
- break;
562
- }
563
- currentResolveDir = parentResolveDir;
575
+ const packageJsonPath = this.findPackageJsonPath(filePath);
576
+ if (!packageJsonPath) {
577
+ return false;
578
+ }
579
+ try {
580
+ const packageJson = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, 'utf-8'));
581
+ return (packageJson.name === 'workflow' ||
582
+ (typeof packageJson.name === 'string' &&
583
+ packageJson.name.startsWith('@workflow/')));
584
+ }
585
+ catch {
586
+ return false;
564
587
  }
565
- this.manifestStepResolveBaseDirs = Array.from(resolveBaseDirs);
566
- return this.manifestStepResolveBaseDirs;
588
+ }
589
+ resolveSourceBackedPackagePath(filePath) {
590
+ const normalizedPath = filePath.replace(/\\/g, '/');
591
+ if (!normalizedPath.includes('/dist/')) {
592
+ return filePath;
593
+ }
594
+ const sourceCandidate = normalizedPath.replace('/dist/', '/src/');
595
+ const resolvedSourceCandidate = this.resolveCopiedStepImportTargetPath(sourceCandidate);
596
+ if (!(0, node_fs_1.existsSync)(resolvedSourceCandidate) ||
597
+ !this.shouldPreferSourceBackedPackagePath(filePath)) {
598
+ return filePath;
599
+ }
600
+ return (0, node_fs_1.existsSync)(resolvedSourceCandidate)
601
+ ? resolvedSourceCandidate
602
+ : filePath;
603
+ }
604
+ normalizeDiscoveredFilePath(filePath) {
605
+ const absolutePath = (0, node_path_1.isAbsolute)(filePath)
606
+ ? filePath
607
+ : (0, node_path_1.resolve)(this.config.workingDir, filePath);
608
+ return this.resolveSourceBackedPackagePath(absolutePath);
567
609
  }
568
610
  async filterExistingFiles(filePaths) {
569
611
  const normalizedFilePaths = Array.from(new Set(filePaths.map((filePath) => this.normalizeDiscoveredFilePath(filePath)))).sort();
@@ -683,9 +725,12 @@ async function getNextBuilderDeferred() {
683
725
  }
684
726
  const normalizedSourcePath = this.normalizeDiscoveredFilePath(resolvedSourcePath);
685
727
  const normalizedSourcePathForCheck = normalizedSourcePath.replace(/\\/g, '/');
728
+ const isSourceBackedPackagePath = this.shouldPreferSourceBackedPackagePath(normalizedSourcePath);
686
729
  if (normalizedSourcePathForCheck.includes('/.well-known/workflow/') ||
687
- normalizedSourcePathForCheck.includes('/node_modules/') ||
688
- normalizedSourcePathForCheck.includes('/.pnpm/') ||
730
+ (!isSourceBackedPackagePath &&
731
+ normalizedSourcePathForCheck.includes('/node_modules/')) ||
732
+ (!isSourceBackedPackagePath &&
733
+ normalizedSourcePathForCheck.includes('/.pnpm/')) ||
689
734
  normalizedSourcePathForCheck.includes('/.next/') ||
690
735
  normalizedSourcePathForCheck.endsWith('/virtual-entry.js')) {
691
736
  continue;
@@ -707,9 +752,6 @@ async function getNextBuilderDeferred() {
707
752
  }, 50);
708
753
  }
709
754
  scheduleDeferredRebuild() {
710
- if (!this.config.watch) {
711
- return;
712
- }
713
755
  if (this.deferredRebuildTimer) {
714
756
  clearTimeout(this.deferredRebuildTimer);
715
757
  }
@@ -784,35 +826,61 @@ async function getNextBuilderDeferred() {
784
826
  ].join('\n');
785
827
  const workflowGeneratedDir = (0, node_path_1.join)(outputDir, '.well-known/workflow/v1');
786
828
  await (0, promises_1.mkdir)((0, node_path_1.join)(workflowGeneratedDir, 'flow'), { recursive: true });
787
- await (0, promises_1.mkdir)((0, node_path_1.join)(workflowGeneratedDir, 'step'), { recursive: true });
788
829
  await (0, promises_1.mkdir)((0, node_path_1.join)(workflowGeneratedDir, 'webhook/[token]'), {
789
830
  recursive: true,
790
831
  });
791
832
  await this.writeFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, '.gitignore'), '*');
792
- // route.js stubs are replaced by generated route.js output once discovery
793
- // finishes and a deferred build completes.
833
+ // V2: Only flow + webhook stubs needed (no separate step route).
834
+ // Stubs are replaced by generated output once discovery finishes.
794
835
  await this.writeFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, 'flow/route.js'), routeStubContent);
795
- await this.writeFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, 'step/route.js'), routeStubContent);
796
836
  await this.writeFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, 'webhook/[token]/route.js'), routeStubContent);
797
837
  }
838
+ async getInputFiles() {
839
+ // Read Next.js's app-paths-manifest.json from a previous build to
840
+ // determine which files are actual route entrypoints. This avoids
841
+ // predicting Next.js conventions with regexes and instead reads
842
+ // from Next.js's own output.
843
+ const nextDir = (0, node_path_1.join)(this.config.workingDir, '.next');
844
+ const manifestPath = (0, node_path_1.join)(nextDir, 'app-paths-manifest.json');
845
+ try {
846
+ const manifestContent = (0, node_fs_1.readFileSync)(manifestPath, 'utf-8');
847
+ const manifest = JSON.parse(manifestContent);
848
+ // The manifest maps route paths to their source files.
849
+ // Extract the source file paths and resolve them.
850
+ const manifestFiles = new Set();
851
+ for (const sourcePath of Object.values(manifest)) {
852
+ const resolved = (0, node_path_1.resolve)(nextDir, 'server', sourcePath);
853
+ // The manifest points to built output; find the source file
854
+ // by matching against the base builder's full file list.
855
+ manifestFiles.add(resolved);
856
+ }
857
+ // Use the manifest route paths to filter the input files.
858
+ // A file is included if it matches a known route segment from
859
+ // the manifest (e.g., app/api/route contains 'app/api/route').
860
+ const inputFiles = await super.getInputFiles();
861
+ const routeSegments = Object.keys(manifest).map((route) => route.replace(/^\//, '').replace(/\/route$/, ''));
862
+ return inputFiles.filter((item) => routeSegments.some((segment) => item.includes(segment)));
863
+ }
864
+ catch {
865
+ // No manifest from a previous build — fall back to the base
866
+ // builder's full file scan. This is safe but slower; subsequent
867
+ // builds will use the manifest.
868
+ return super.getInputFiles();
869
+ }
870
+ }
798
871
  async writeFunctionsConfig(outputDir) {
799
872
  // we don't run this in development mode as it's not needed
800
873
  if (process.env.NODE_ENV === 'development') {
801
874
  return;
802
875
  }
876
+ // V2: Single combined trigger handles both workflow and step execution
803
877
  const generatedConfig = {
804
878
  version: '0',
805
- steps: {
806
- maxDuration: 'max',
807
- experimentalTriggers: [STEP_QUEUE_TRIGGER],
808
- },
809
879
  workflows: {
810
880
  maxDuration: 'max',
811
881
  experimentalTriggers: [WORKFLOW_QUEUE_TRIGGER],
812
882
  },
813
883
  };
814
- // We write this file to the generated directory for
815
- // the Next.js builder to consume
816
884
  await this.writeFileIfChanged((0, node_path_1.join)(outputDir, '.well-known/workflow/v1/config.json'), JSON.stringify(generatedConfig, null, 2));
817
885
  }
818
886
  async writeFileIfChanged(filePath, contents) {
@@ -859,16 +927,56 @@ async function getNextBuilderDeferred() {
859
927
  // Manifest may not exist (e.g. manifest generation failed); ignore.
860
928
  }
861
929
  }
862
- mergeWorkflowManifest(target, source) {
863
- if (source.steps) {
864
- target.steps = Object.assign(target.steps || {}, source.steps);
930
+ resolveCopiedStepImportTargetPath(targetPath) {
931
+ if ((0, node_fs_1.existsSync)(targetPath)) {
932
+ return targetPath;
933
+ }
934
+ const extensionMatch = targetPath.match(/(\.[^./\\]+)$/);
935
+ const extension = extensionMatch?.[1]?.toLowerCase();
936
+ if (!extension) {
937
+ return targetPath;
938
+ }
939
+ const extensionFallbacks = extension === '.js'
940
+ ? ['.ts', '.tsx', '.mts', '.cts']
941
+ : extension === '.mjs'
942
+ ? ['.mts']
943
+ : extension === '.cjs'
944
+ ? ['.cts']
945
+ : extension === '.jsx'
946
+ ? ['.tsx']
947
+ : [];
948
+ if (extensionFallbacks.length === 0) {
949
+ return targetPath;
865
950
  }
866
- if (source.workflows) {
867
- target.workflows = Object.assign(target.workflows || {}, source.workflows);
951
+ const targetWithoutExtension = targetPath.slice(0, -extension.length);
952
+ for (const fallbackExtension of extensionFallbacks) {
953
+ const fallbackPath = `${targetWithoutExtension}${fallbackExtension}`;
954
+ if ((0, node_fs_1.existsSync)(fallbackPath)) {
955
+ return fallbackPath;
956
+ }
868
957
  }
869
- if (source.classes) {
870
- target.classes = Object.assign(target.classes || {}, source.classes);
958
+ return targetPath;
959
+ }
960
+ extractRelativeImportSpecifiers(source) {
961
+ return this.extractImportSpecifiers(source).filter((specifier) => specifier.startsWith('.'));
962
+ }
963
+ extractImportSpecifiers(source) {
964
+ const relativeSpecifiers = new Set();
965
+ const importPatterns = [
966
+ /from\s+['"]([^'"]+)['"]/g,
967
+ /import\s+['"]([^'"]+)['"]/g,
968
+ /import\(\s*['"]([^'"]+)['"]\s*\)/g,
969
+ /require\(\s*['"]([^'"]+)['"]\s*\)/g,
970
+ ];
971
+ for (const importPattern of importPatterns) {
972
+ for (const match of source.matchAll(importPattern)) {
973
+ const specifier = match[1];
974
+ if (specifier) {
975
+ relativeSpecifiers.add(specifier);
976
+ }
977
+ }
871
978
  }
979
+ return Array.from(relativeSpecifiers);
872
980
  }
873
981
  async getRelativeFilenameForSwc(filePath) {
874
982
  const workingDir = this.config.workingDir;
@@ -906,13 +1014,6 @@ async function getNextBuilderDeferred() {
906
1014
  }
907
1015
  return relativeFilename;
908
1016
  }
909
- getRelativeImportSpecifier(fromFilePath, toFilePath) {
910
- let relativePath = (0, node_path_1.relative)((0, node_path_1.dirname)(fromFilePath), toFilePath).replace(/\\/g, '/');
911
- if (!relativePath.startsWith('.')) {
912
- relativePath = `./${relativePath}`;
913
- }
914
- return relativePath;
915
- }
916
1017
  resolveImportTargetWithExtensionFallbacks(targetPath) {
917
1018
  if ((0, node_fs_1.existsSync)(targetPath)) {
918
1019
  return targetPath;
@@ -943,38 +1044,35 @@ async function getNextBuilderDeferred() {
943
1044
  }
944
1045
  return targetPath;
945
1046
  }
946
- extractRelativeImportSpecifiers(source) {
947
- const relativeSpecifiers = new Set();
948
- const importPatterns = [
949
- /from\s+['"]([^'"]+)['"]/g,
950
- /import\s+['"]([^'"]+)['"]/g,
951
- /import\(\s*['"]([^'"]+)['"]\s*\)/g,
952
- /require\(\s*['"]([^'"]+)['"]\s*\)/g,
953
- ];
954
- for (const importPattern of importPatterns) {
955
- for (const match of source.matchAll(importPattern)) {
956
- const specifier = match[1];
957
- if (specifier?.startsWith('.')) {
958
- relativeSpecifiers.add(specifier);
959
- }
960
- }
961
- }
962
- return Array.from(relativeSpecifiers);
963
- }
964
- isGeneratedWorkflowArtifact(filePath) {
965
- const normalizedPath = filePath.replace(/\\/g, '/');
966
- return (normalizedPath.includes('/.well-known/workflow/') ||
967
- normalizedPath.includes('/.next/'));
968
- }
969
1047
  shouldSkipTransitiveStepFile(filePath) {
970
1048
  const normalizedPath = filePath.replace(/\\/g, '/');
971
- return (this.isGeneratedWorkflowArtifact(normalizedPath) ||
972
- normalizedPath.includes('/node_modules/') ||
973
- normalizedPath.includes('/.pnpm/'));
1049
+ const isSourceBackedPackagePath = this.shouldPreferSourceBackedPackagePath(filePath);
1050
+ return (normalizedPath.includes('/.well-known/workflow/') ||
1051
+ normalizedPath.includes('/.next/') ||
1052
+ (!isSourceBackedPackagePath &&
1053
+ (normalizedPath.includes('/node_modules/') ||
1054
+ normalizedPath.includes('/.pnpm/'))));
974
1055
  }
975
1056
  async resolveTransitiveStepImportTargetPath(sourceFilePath, specifier) {
976
1057
  const specifierMatch = specifier.match(/^([^?#]+)(.*)$/);
977
1058
  const importPath = specifierMatch?.[1] ?? specifier;
1059
+ if (!importPath.startsWith('.')) {
1060
+ if (importPath !== 'workflow' && !importPath.startsWith('@workflow/')) {
1061
+ return null;
1062
+ }
1063
+ try {
1064
+ const resolvedPath = (0, node_module_1.createRequire)(sourceFilePath).resolve(importPath);
1065
+ const normalizedResolvedPath = this.normalizeDiscoveredFilePath(resolvedPath);
1066
+ if (this.shouldSkipTransitiveStepFile(normalizedResolvedPath)) {
1067
+ return null;
1068
+ }
1069
+ const fileStats = await (0, promises_1.stat)(normalizedResolvedPath);
1070
+ return fileStats.isFile() ? normalizedResolvedPath : null;
1071
+ }
1072
+ catch {
1073
+ return null;
1074
+ }
1075
+ }
978
1076
  const absoluteTargetPath = (0, node_path_1.resolve)((0, node_path_1.dirname)(sourceFilePath), importPath);
979
1077
  const candidatePaths = new Set([
980
1078
  this.resolveImportTargetWithExtensionFallbacks(absoluteTargetPath),
@@ -1013,13 +1111,11 @@ async function getNextBuilderDeferred() {
1013
1111
  }
1014
1112
  return null;
1015
1113
  }
1016
- async collectTransitiveStepFiles({ stepFiles, seedFiles = [], }) {
1017
- const normalizedSeedFiles = Array.from(new Set([...stepFiles, ...seedFiles].map((stepFile) => this.normalizeDiscoveredFilePath(stepFile)))).sort();
1018
- // Intentionally re-validate step seeds against current file contents
1019
- // instead of blindly trusting callers. This prevents stale/manual seed
1020
- // paths from persisting when files no longer contain "use step".
1021
- const discoveredStepFiles = new Set();
1022
- const queuedFiles = [...normalizedSeedFiles];
1114
+ async collectTransitiveStepFiles({ entryFiles, stepFiles, }) {
1115
+ const normalizedEntryFiles = Array.from(new Set(entryFiles.map((entryFile) => this.normalizeDiscoveredFilePath(entryFile)))).sort();
1116
+ const normalizedStepSeedFiles = Array.from(new Set(stepFiles.map((stepFile) => this.normalizeDiscoveredFilePath(stepFile)))).sort();
1117
+ const discoveredStepFiles = new Set(normalizedStepSeedFiles);
1118
+ const queuedFiles = Array.from(new Set([...normalizedEntryFiles, ...normalizedStepSeedFiles]));
1023
1119
  const visitedFiles = new Set();
1024
1120
  const sourceCache = new Map();
1025
1121
  const patternCache = new Map();
@@ -1060,12 +1156,8 @@ async function getNextBuilderDeferred() {
1060
1156
  if (currentSource === null) {
1061
1157
  continue;
1062
1158
  }
1063
- const currentPatterns = await getPatterns(currentFile);
1064
- if (currentPatterns?.hasUseStep) {
1065
- discoveredStepFiles.add(currentFile);
1066
- }
1067
- const relativeImportSpecifiers = this.extractRelativeImportSpecifiers(currentSource);
1068
- for (const specifier of relativeImportSpecifiers) {
1159
+ const importSpecifiers = this.extractImportSpecifiers(currentSource);
1160
+ for (const specifier of importSpecifiers) {
1069
1161
  const resolvedImportPath = await this.resolveTransitiveStepImportTargetPath(currentFile, specifier);
1070
1162
  if (!resolvedImportPath) {
1071
1163
  continue;
@@ -1177,173 +1269,6 @@ async function getNextBuilderDeferred() {
1177
1269
  }));
1178
1270
  return verifiedSerdeFiles.sort();
1179
1271
  }
1180
- async createDeferredStepsManifest({ stepFiles, workflowFiles, serdeOnlyFiles, }) {
1181
- const workflowManifest = {};
1182
- const filesForStepTransform = Array.from(new Set([...stepFiles, ...serdeOnlyFiles])).sort();
1183
- await Promise.all(filesForStepTransform.map(async (stepFile) => {
1184
- const source = await (0, promises_1.readFile)(stepFile, 'utf-8');
1185
- const relativeFilename = await this.getRelativeFilenameForSwc(stepFile);
1186
- const { workflowManifest: fileManifest } = await applySwcTransform(relativeFilename, source, 'step', stepFile, this.config.projectRoot || this.config.workingDir);
1187
- this.mergeWorkflowManifest(workflowManifest, fileManifest);
1188
- }));
1189
- const stepFileSet = new Set(stepFiles);
1190
- const workflowOnlyFiles = workflowFiles
1191
- .filter((workflowFile) => !stepFileSet.has(workflowFile))
1192
- .sort();
1193
- await Promise.all(workflowOnlyFiles.map(async (workflowFile) => {
1194
- try {
1195
- const source = await (0, promises_1.readFile)(workflowFile, 'utf-8');
1196
- const relativeFilename = await this.getRelativeFilenameForSwc(workflowFile);
1197
- const { workflowManifest: fileManifest } = await applySwcTransform(relativeFilename, source, 'workflow', workflowFile, this.config.projectRoot || this.config.workingDir);
1198
- this.mergeWorkflowManifest(workflowManifest, {
1199
- workflows: fileManifest.workflows,
1200
- classes: fileManifest.classes,
1201
- });
1202
- }
1203
- catch (error) {
1204
- console.log(`Warning: Failed to extract workflow metadata from ${workflowFile}:`, error instanceof Error ? error.message : String(error));
1205
- }
1206
- }));
1207
- return workflowManifest;
1208
- }
1209
- async collectManifestStepSourceFiles(manifest) {
1210
- const manifestStepEntries = Object.keys(manifest.steps || {});
1211
- if (manifestStepEntries.length === 0) {
1212
- return [];
1213
- }
1214
- const resolveBaseDirs = this.getManifestStepResolveBaseDirs();
1215
- const candidateFiles = manifestStepEntries
1216
- .flatMap((stepEntry) => {
1217
- if ((0, node_path_1.isAbsolute)(stepEntry)) {
1218
- return [this.normalizeDiscoveredFilePath(stepEntry)];
1219
- }
1220
- return resolveBaseDirs.map((baseDir) => this.normalizeDiscoveredFilePath((0, node_path_1.resolve)(baseDir, stepEntry)));
1221
- })
1222
- .filter((candidateFile) => !this.isGeneratedWorkflowArtifact(candidateFile));
1223
- const existingCandidates = await this.filterExistingFiles(candidateFiles);
1224
- return Array.from(new Set(existingCandidates)).sort();
1225
- }
1226
- /**
1227
- * Resolves the path to the workflow SDK's internal response builtins
1228
- * step file. Returns the file as a pair of (absolute path, import
1229
- * specifier) so the caller can include it in both the manifest
1230
- * (absolute path) and the generated step/route.js (as an import).
1231
- */
1232
- resolveResponseBuiltinsStepSource() {
1233
- let resolved;
1234
- try {
1235
- resolved = require.resolve('workflow/internal/builtins', {
1236
- paths: [this.config.workingDir],
1237
- });
1238
- }
1239
- catch {
1240
- return null;
1241
- }
1242
- return {
1243
- absolutePath: this.normalizeDiscoveredFilePath(resolved),
1244
- importPath: 'workflow/internal/builtins',
1245
- };
1246
- }
1247
- /**
1248
- * Builds the import specifier for a given step/serde source file as it
1249
- * should appear in the generated step/route.js. Package-provided files
1250
- * are imported via their package specifier so the bundler resolves them
1251
- * from the app's dependency graph. Local files are imported via a
1252
- * relative path from the generated route.
1253
- */
1254
- getStepRouteImportSpecifier(stepRouteFile, sourceFilePath) {
1255
- const { importPath, isPackage } = getImportPath(sourceFilePath, this.config.workingDir);
1256
- if (isPackage) {
1257
- return importPath;
1258
- }
1259
- return this.getRelativeImportSpecifier(stepRouteFile, sourceFilePath);
1260
- }
1261
- async buildStepsFunction({ workflowGeneratedDir, routeFileName = 'route.js', discoveredEntries, additionalStepSourceManifest, }) {
1262
- const stepsRouteDir = (0, node_path_1.join)(workflowGeneratedDir, 'step');
1263
- await (0, promises_1.mkdir)(stepsRouteDir, { recursive: true });
1264
- const discovered = discoveredEntries;
1265
- const workflowFiles = [...discovered.discoveredWorkflows].sort();
1266
- const stepFiles = await this.collectTransitiveStepFiles({
1267
- stepFiles: [...discovered.discoveredSteps].sort(),
1268
- // Workflow transforms can inline step IDs and remove runtime imports,
1269
- // so seed transitive traversal with workflow files too.
1270
- seedFiles: workflowFiles,
1271
- });
1272
- const serdeFiles = [...discovered.discoveredSerdeFiles].sort();
1273
- const stepFileSet = new Set(stepFiles);
1274
- const serdeOnlyFiles = serdeFiles.filter((file) => !stepFileSet.has(file));
1275
- const additionalManifestStepFiles = additionalStepSourceManifest
1276
- ? await this.collectManifestStepSourceFiles(additionalStepSourceManifest)
1277
- : [];
1278
- const stepFilesWithManifestSources = Array.from(new Set([...stepFiles, ...additionalManifestStepFiles])).sort();
1279
- const responseBuiltins = this.resolveResponseBuiltinsStepSource();
1280
- const manifestStepFiles = Array.from(new Set([
1281
- ...stepFilesWithManifestSources,
1282
- ...(responseBuiltins ? [responseBuiltins.absolutePath] : []),
1283
- ])).sort();
1284
- const manifest = await this.createDeferredStepsManifest({
1285
- stepFiles: manifestStepFiles,
1286
- workflowFiles,
1287
- serdeOnlyFiles,
1288
- });
1289
- const manifestDiscoveredStepFiles = await this.collectManifestStepSourceFiles(manifest);
1290
- // Step source files are imported directly from their original
1291
- // locations. The workflow loader transforms every file it sees in
1292
- // step mode, so package-provided sources register their step
1293
- // functions as module side effects just like local sources do.
1294
- const stepSourceFiles = Array.from(new Set([
1295
- ...stepFilesWithManifestSources,
1296
- ...manifestDiscoveredStepFiles,
1297
- ])).sort();
1298
- const stepRouteFile = (0, node_path_1.join)(stepsRouteDir, routeFileName);
1299
- const stepImportSpecifiers = new Set();
1300
- if (responseBuiltins) {
1301
- stepImportSpecifiers.add(responseBuiltins.importPath);
1302
- }
1303
- for (const stepSourceFile of stepSourceFiles) {
1304
- stepImportSpecifiers.add(this.getStepRouteImportSpecifier(stepRouteFile, stepSourceFile));
1305
- }
1306
- const stepImports = Array.from(stepImportSpecifiers)
1307
- .map((specifier) => `import '${specifier}';`)
1308
- .join('\n');
1309
- const serdeImports = serdeOnlyFiles
1310
- .map((serdeFile) => {
1311
- const normalizedSerdeFile = this.normalizeDiscoveredFilePath(serdeFile);
1312
- return `import '${this.getStepRouteImportSpecifier(stepRouteFile, normalizedSerdeFile)}';`;
1313
- })
1314
- .join('\n');
1315
- const routeContents = [
1316
- '// biome-ignore-all lint: generated file',
1317
- '/* eslint-disable */',
1318
- stepImports,
1319
- serdeImports
1320
- ? `// Serde files for cross-context class registration\n${serdeImports}`
1321
- : '',
1322
- "export { stepEntrypoint as POST } from 'workflow/runtime';",
1323
- ]
1324
- .filter(Boolean)
1325
- .join('\n');
1326
- await this.writeFileIfChanged(stepRouteFile, routeContents);
1327
- return {
1328
- context: undefined,
1329
- manifest,
1330
- };
1331
- }
1332
- async buildWorkflowsFunction({ inputFiles, workflowGeneratedDir, tsconfigPath, routeFileName = 'route.js', discoveredEntries, }) {
1333
- const workflowsRouteDir = (0, node_path_1.join)(workflowGeneratedDir, 'flow');
1334
- await (0, promises_1.mkdir)(workflowsRouteDir, { recursive: true });
1335
- return await this.createWorkflowsBundle({
1336
- format: 'esm',
1337
- outfile: (0, node_path_1.join)(workflowsRouteDir, routeFileName),
1338
- bundleFinalOutput: false,
1339
- // Deferred builds do not reuse the interim esbuild context. Dispose it
1340
- // after each pass to avoid leaking contexts during watch-mode rebuilds.
1341
- keepInterimBundleContext: false,
1342
- inputFiles,
1343
- tsconfigPath,
1344
- discoveredEntries,
1345
- });
1346
- }
1347
1272
  async buildWebhookRoute({ workflowGeneratedDir, routeFileName = 'route.js', }) {
1348
1273
  const webhookRouteFile = (0, node_path_1.join)(workflowGeneratedDir, `webhook/[token]/${routeFileName}`);
1349
1274
  await this.createWebhookBundle({