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

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,17 @@ 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
- await Promise.all(staleArtifactPaths.map((stalePath) => (0, promises_1.rm)(stalePath, { recursive: true, force: true })));
443
454
  await Promise.all([
455
+ ...staleArtifactPaths.map((stalePath) => (0, promises_1.rm)(stalePath, { recursive: true, force: true })),
456
+ (0, socket_server_js_1.cleanupStaleSocketInfoFiles)((0, node_path_1.join)(this.config.workingDir, this.getDistDir())),
444
457
  this.removeStaleDeferredTempFiles(flowRouteDir),
445
- this.removeStaleDeferredTempFiles(stepRouteDir),
446
458
  this.removeStaleDeferredTempFiles(webhookRouteDir),
447
459
  ]);
448
460
  }
@@ -503,7 +515,11 @@ async function getNextBuilderDeferred() {
503
515
  if (hasCacheTrackingChange) {
504
516
  this.scheduleWorkflowsCacheWrite();
505
517
  }
506
- if (hasCacheTrackingChange || wasTrackedDependency) {
518
+ if (hasWorkflow ||
519
+ hasStep ||
520
+ hasSerde ||
521
+ hasCacheTrackingChange ||
522
+ wasTrackedDependency) {
507
523
  this.scheduleDeferredRebuild();
508
524
  }
509
525
  },
@@ -529,41 +545,68 @@ async function getNextBuilderDeferred() {
529
545
  return (0, node_path_1.join)(this.config.workingDir, this.getDistDir(), 'cache', 'workflows.json');
530
546
  }
531
547
  getSocketInfoFilePath() {
532
- return (0, node_path_1.join)(this.config.workingDir, this.getDistDir(), 'cache', 'workflow-socket.json');
548
+ return (0, node_path_1.join)(this.config.workingDir, this.getDistDir(), socket_server_js_1.SOCKET_INFO_FILENAME);
533
549
  }
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;
550
+ findPackageJsonPath(filePath) {
551
+ let currentDir = (0, node_path_1.dirname)(filePath);
552
+ let previousDir = '';
553
+ while (currentDir !== previousDir) {
554
+ const packageJsonPath = (0, node_path_1.join)(currentDir, 'package.json');
555
+ if ((0, node_fs_1.existsSync)(packageJsonPath)) {
556
+ return packageJsonPath;
557
+ }
558
+ previousDir = currentDir;
559
+ currentDir = (0, node_path_1.dirname)(currentDir);
543
560
  }
561
+ return null;
544
562
  }
545
- getManifestStepResolveBaseDirs() {
546
- if (this.manifestStepResolveBaseDirs) {
547
- return this.manifestStepResolveBaseDirs;
563
+ shouldPreferSourceBackedPackagePath(filePath) {
564
+ const normalizedPath = filePath.replace(/\\/g, '/');
565
+ // Only prefer source for workspace packages (not in node_modules).
566
+ // For tarball-installed packages, using source-backed paths causes
567
+ // esbuild to bundle the full source tree (including world.ts with
568
+ // process.cwd()) instead of externalizing properly.
569
+ if (normalizedPath.includes('/packages/') &&
570
+ !normalizedPath.includes('/node_modules/')) {
571
+ return true;
548
572
  }
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);
573
+ if (!normalizedPath.includes('/node_modules/')) {
574
+ return false;
555
575
  }
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;
576
+ const packageJsonPath = this.findPackageJsonPath(filePath);
577
+ if (!packageJsonPath) {
578
+ return false;
579
+ }
580
+ try {
581
+ const packageJson = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, 'utf-8'));
582
+ return (packageJson.name === 'workflow' ||
583
+ (typeof packageJson.name === 'string' &&
584
+ packageJson.name.startsWith('@workflow/')));
585
+ }
586
+ catch {
587
+ return false;
564
588
  }
565
- this.manifestStepResolveBaseDirs = Array.from(resolveBaseDirs);
566
- return this.manifestStepResolveBaseDirs;
589
+ }
590
+ resolveSourceBackedPackagePath(filePath) {
591
+ const normalizedPath = filePath.replace(/\\/g, '/');
592
+ if (!normalizedPath.includes('/dist/')) {
593
+ return filePath;
594
+ }
595
+ const sourceCandidate = normalizedPath.replace('/dist/', '/src/');
596
+ const resolvedSourceCandidate = this.resolveCopiedStepImportTargetPath(sourceCandidate);
597
+ if (!(0, node_fs_1.existsSync)(resolvedSourceCandidate) ||
598
+ !this.shouldPreferSourceBackedPackagePath(filePath)) {
599
+ return filePath;
600
+ }
601
+ return (0, node_fs_1.existsSync)(resolvedSourceCandidate)
602
+ ? resolvedSourceCandidate
603
+ : filePath;
604
+ }
605
+ normalizeDiscoveredFilePath(filePath) {
606
+ const absolutePath = (0, node_path_1.isAbsolute)(filePath)
607
+ ? filePath
608
+ : (0, node_path_1.resolve)(this.config.workingDir, filePath);
609
+ return this.resolveSourceBackedPackagePath(absolutePath);
567
610
  }
568
611
  async filterExistingFiles(filePaths) {
569
612
  const normalizedFilePaths = Array.from(new Set(filePaths.map((filePath) => this.normalizeDiscoveredFilePath(filePath)))).sort();
@@ -683,9 +726,12 @@ async function getNextBuilderDeferred() {
683
726
  }
684
727
  const normalizedSourcePath = this.normalizeDiscoveredFilePath(resolvedSourcePath);
685
728
  const normalizedSourcePathForCheck = normalizedSourcePath.replace(/\\/g, '/');
729
+ const isSourceBackedPackagePath = this.shouldPreferSourceBackedPackagePath(normalizedSourcePath);
686
730
  if (normalizedSourcePathForCheck.includes('/.well-known/workflow/') ||
687
- normalizedSourcePathForCheck.includes('/node_modules/') ||
688
- normalizedSourcePathForCheck.includes('/.pnpm/') ||
731
+ (!isSourceBackedPackagePath &&
732
+ normalizedSourcePathForCheck.includes('/node_modules/')) ||
733
+ (!isSourceBackedPackagePath &&
734
+ normalizedSourcePathForCheck.includes('/.pnpm/')) ||
689
735
  normalizedSourcePathForCheck.includes('/.next/') ||
690
736
  normalizedSourcePathForCheck.endsWith('/virtual-entry.js')) {
691
737
  continue;
@@ -707,9 +753,6 @@ async function getNextBuilderDeferred() {
707
753
  }, 50);
708
754
  }
709
755
  scheduleDeferredRebuild() {
710
- if (!this.config.watch) {
711
- return;
712
- }
713
756
  if (this.deferredRebuildTimer) {
714
757
  clearTimeout(this.deferredRebuildTimer);
715
758
  }
@@ -784,35 +827,61 @@ async function getNextBuilderDeferred() {
784
827
  ].join('\n');
785
828
  const workflowGeneratedDir = (0, node_path_1.join)(outputDir, '.well-known/workflow/v1');
786
829
  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
830
  await (0, promises_1.mkdir)((0, node_path_1.join)(workflowGeneratedDir, 'webhook/[token]'), {
789
831
  recursive: true,
790
832
  });
791
833
  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.
834
+ // V2: Only flow + webhook stubs needed (no separate step route).
835
+ // Stubs are replaced by generated output once discovery finishes.
794
836
  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
837
  await this.writeFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, 'webhook/[token]/route.js'), routeStubContent);
797
838
  }
839
+ async getInputFiles() {
840
+ // Read Next.js's app-paths-manifest.json from a previous build to
841
+ // determine which files are actual route entrypoints. This avoids
842
+ // predicting Next.js conventions with regexes and instead reads
843
+ // from Next.js's own output.
844
+ const nextDir = (0, node_path_1.join)(this.config.workingDir, '.next');
845
+ const manifestPath = (0, node_path_1.join)(nextDir, 'app-paths-manifest.json');
846
+ try {
847
+ const manifestContent = (0, node_fs_1.readFileSync)(manifestPath, 'utf-8');
848
+ const manifest = JSON.parse(manifestContent);
849
+ // The manifest maps route paths to their source files.
850
+ // Extract the source file paths and resolve them.
851
+ const manifestFiles = new Set();
852
+ for (const sourcePath of Object.values(manifest)) {
853
+ const resolved = (0, node_path_1.resolve)(nextDir, 'server', sourcePath);
854
+ // The manifest points to built output; find the source file
855
+ // by matching against the base builder's full file list.
856
+ manifestFiles.add(resolved);
857
+ }
858
+ // Use the manifest route paths to filter the input files.
859
+ // A file is included if it matches a known route segment from
860
+ // the manifest (e.g., app/api/route contains 'app/api/route').
861
+ const inputFiles = await super.getInputFiles();
862
+ const routeSegments = Object.keys(manifest).map((route) => route.replace(/^\//, '').replace(/\/route$/, ''));
863
+ return inputFiles.filter((item) => routeSegments.some((segment) => item.includes(segment)));
864
+ }
865
+ catch {
866
+ // No manifest from a previous build — fall back to the base
867
+ // builder's full file scan. This is safe but slower; subsequent
868
+ // builds will use the manifest.
869
+ return super.getInputFiles();
870
+ }
871
+ }
798
872
  async writeFunctionsConfig(outputDir) {
799
873
  // we don't run this in development mode as it's not needed
800
874
  if (process.env.NODE_ENV === 'development') {
801
875
  return;
802
876
  }
877
+ // V2: Single combined trigger handles both workflow and step execution
803
878
  const generatedConfig = {
804
879
  version: '0',
805
- steps: {
806
- maxDuration: 'max',
807
- experimentalTriggers: [STEP_QUEUE_TRIGGER],
808
- },
809
880
  workflows: {
810
881
  maxDuration: 'max',
811
882
  experimentalTriggers: [WORKFLOW_QUEUE_TRIGGER],
812
883
  },
813
884
  };
814
- // We write this file to the generated directory for
815
- // the Next.js builder to consume
816
885
  await this.writeFileIfChanged((0, node_path_1.join)(outputDir, '.well-known/workflow/v1/config.json'), JSON.stringify(generatedConfig, null, 2));
817
886
  }
818
887
  async writeFileIfChanged(filePath, contents) {
@@ -859,16 +928,56 @@ async function getNextBuilderDeferred() {
859
928
  // Manifest may not exist (e.g. manifest generation failed); ignore.
860
929
  }
861
930
  }
862
- mergeWorkflowManifest(target, source) {
863
- if (source.steps) {
864
- target.steps = Object.assign(target.steps || {}, source.steps);
931
+ resolveCopiedStepImportTargetPath(targetPath) {
932
+ if ((0, node_fs_1.existsSync)(targetPath)) {
933
+ return targetPath;
934
+ }
935
+ const extensionMatch = targetPath.match(/(\.[^./\\]+)$/);
936
+ const extension = extensionMatch?.[1]?.toLowerCase();
937
+ if (!extension) {
938
+ return targetPath;
939
+ }
940
+ const extensionFallbacks = extension === '.js'
941
+ ? ['.ts', '.tsx', '.mts', '.cts']
942
+ : extension === '.mjs'
943
+ ? ['.mts']
944
+ : extension === '.cjs'
945
+ ? ['.cts']
946
+ : extension === '.jsx'
947
+ ? ['.tsx']
948
+ : [];
949
+ if (extensionFallbacks.length === 0) {
950
+ return targetPath;
865
951
  }
866
- if (source.workflows) {
867
- target.workflows = Object.assign(target.workflows || {}, source.workflows);
952
+ const targetWithoutExtension = targetPath.slice(0, -extension.length);
953
+ for (const fallbackExtension of extensionFallbacks) {
954
+ const fallbackPath = `${targetWithoutExtension}${fallbackExtension}`;
955
+ if ((0, node_fs_1.existsSync)(fallbackPath)) {
956
+ return fallbackPath;
957
+ }
868
958
  }
869
- if (source.classes) {
870
- target.classes = Object.assign(target.classes || {}, source.classes);
959
+ return targetPath;
960
+ }
961
+ extractRelativeImportSpecifiers(source) {
962
+ return this.extractImportSpecifiers(source).filter((specifier) => specifier.startsWith('.'));
963
+ }
964
+ extractImportSpecifiers(source) {
965
+ const relativeSpecifiers = new Set();
966
+ const importPatterns = [
967
+ /from\s+['"]([^'"]+)['"]/g,
968
+ /import\s+['"]([^'"]+)['"]/g,
969
+ /import\(\s*['"]([^'"]+)['"]\s*\)/g,
970
+ /require\(\s*['"]([^'"]+)['"]\s*\)/g,
971
+ ];
972
+ for (const importPattern of importPatterns) {
973
+ for (const match of source.matchAll(importPattern)) {
974
+ const specifier = match[1];
975
+ if (specifier) {
976
+ relativeSpecifiers.add(specifier);
977
+ }
978
+ }
871
979
  }
980
+ return Array.from(relativeSpecifiers);
872
981
  }
873
982
  async getRelativeFilenameForSwc(filePath) {
874
983
  const workingDir = this.config.workingDir;
@@ -906,13 +1015,6 @@ async function getNextBuilderDeferred() {
906
1015
  }
907
1016
  return relativeFilename;
908
1017
  }
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
1018
  resolveImportTargetWithExtensionFallbacks(targetPath) {
917
1019
  if ((0, node_fs_1.existsSync)(targetPath)) {
918
1020
  return targetPath;
@@ -943,38 +1045,35 @@ async function getNextBuilderDeferred() {
943
1045
  }
944
1046
  return targetPath;
945
1047
  }
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
1048
  shouldSkipTransitiveStepFile(filePath) {
970
1049
  const normalizedPath = filePath.replace(/\\/g, '/');
971
- return (this.isGeneratedWorkflowArtifact(normalizedPath) ||
972
- normalizedPath.includes('/node_modules/') ||
973
- normalizedPath.includes('/.pnpm/'));
1050
+ const isSourceBackedPackagePath = this.shouldPreferSourceBackedPackagePath(filePath);
1051
+ return (normalizedPath.includes('/.well-known/workflow/') ||
1052
+ normalizedPath.includes('/.next/') ||
1053
+ (!isSourceBackedPackagePath &&
1054
+ (normalizedPath.includes('/node_modules/') ||
1055
+ normalizedPath.includes('/.pnpm/'))));
974
1056
  }
975
1057
  async resolveTransitiveStepImportTargetPath(sourceFilePath, specifier) {
976
1058
  const specifierMatch = specifier.match(/^([^?#]+)(.*)$/);
977
1059
  const importPath = specifierMatch?.[1] ?? specifier;
1060
+ if (!importPath.startsWith('.')) {
1061
+ if (importPath !== 'workflow' && !importPath.startsWith('@workflow/')) {
1062
+ return null;
1063
+ }
1064
+ try {
1065
+ const resolvedPath = (0, node_module_1.createRequire)(sourceFilePath).resolve(importPath);
1066
+ const normalizedResolvedPath = this.normalizeDiscoveredFilePath(resolvedPath);
1067
+ if (this.shouldSkipTransitiveStepFile(normalizedResolvedPath)) {
1068
+ return null;
1069
+ }
1070
+ const fileStats = await (0, promises_1.stat)(normalizedResolvedPath);
1071
+ return fileStats.isFile() ? normalizedResolvedPath : null;
1072
+ }
1073
+ catch {
1074
+ return null;
1075
+ }
1076
+ }
978
1077
  const absoluteTargetPath = (0, node_path_1.resolve)((0, node_path_1.dirname)(sourceFilePath), importPath);
979
1078
  const candidatePaths = new Set([
980
1079
  this.resolveImportTargetWithExtensionFallbacks(absoluteTargetPath),
@@ -1013,13 +1112,11 @@ async function getNextBuilderDeferred() {
1013
1112
  }
1014
1113
  return null;
1015
1114
  }
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];
1115
+ async collectTransitiveStepFiles({ entryFiles, stepFiles, }) {
1116
+ const normalizedEntryFiles = Array.from(new Set(entryFiles.map((entryFile) => this.normalizeDiscoveredFilePath(entryFile)))).sort();
1117
+ const normalizedStepSeedFiles = Array.from(new Set(stepFiles.map((stepFile) => this.normalizeDiscoveredFilePath(stepFile)))).sort();
1118
+ const discoveredStepFiles = new Set(normalizedStepSeedFiles);
1119
+ const queuedFiles = Array.from(new Set([...normalizedEntryFiles, ...normalizedStepSeedFiles]));
1023
1120
  const visitedFiles = new Set();
1024
1121
  const sourceCache = new Map();
1025
1122
  const patternCache = new Map();
@@ -1060,12 +1157,8 @@ async function getNextBuilderDeferred() {
1060
1157
  if (currentSource === null) {
1061
1158
  continue;
1062
1159
  }
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) {
1160
+ const importSpecifiers = this.extractImportSpecifiers(currentSource);
1161
+ for (const specifier of importSpecifiers) {
1069
1162
  const resolvedImportPath = await this.resolveTransitiveStepImportTargetPath(currentFile, specifier);
1070
1163
  if (!resolvedImportPath) {
1071
1164
  continue;
@@ -1177,173 +1270,6 @@ async function getNextBuilderDeferred() {
1177
1270
  }));
1178
1271
  return verifiedSerdeFiles.sort();
1179
1272
  }
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
1273
  async buildWebhookRoute({ workflowGeneratedDir, routeFileName = 'route.js', }) {
1348
1274
  const webhookRouteFile = (0, node_path_1.join)(workflowGeneratedDir, `webhook/[token]/${routeFileName}`);
1349
1275
  await this.createWebhookBundle({