@workflow/next 5.0.0-beta.0 → 5.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
1
  # @workflow/next
2
2
 
3
- Next.js plugin for [Workflow SDK](https://useworkflow.dev).
3
+ Next.js plugin for [Workflow SDK](https://workflow-sdk.dev).
@@ -1 +1 @@
1
- {"version":3,"file":"builder-deferred.d.ts","sourceRoot":"","sources":["../src/builder-deferred.ts"],"names":[],"mappings":"AA+CA,wBAAsB,sBAAsB,iBAk+D3C"}
1
+ {"version":3,"file":"builder-deferred.d.ts","sourceRoot":"","sources":["../src/builder-deferred.ts"],"names":[],"mappings":"AAkDA,wBAAsB,sBAAsB,iBAosE3C"}
@@ -9,9 +9,11 @@ const node_fs_1 = require("node:fs");
9
9
  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
+ const enhanced_resolve_1 = __importDefault(require("enhanced-resolve"));
12
13
  const socket_server_js_1 = require("./socket-server.js");
13
14
  const step_copy_utils_js_1 = require("./step-copy-utils.js");
14
15
  const ROUTE_STUB_FILE_MARKER = 'WORKFLOW_ROUTE_STUB_FILE';
16
+ const ROUTE_STUB_MARKER_SCAN_BYTES = 4 * 1024;
15
17
  let CachedNextBuilderDeferred;
16
18
  // Create the deferred Next builder dynamically by extending the ESM BaseBuilder.
17
19
  // Exported as getNextBuilderDeferred() to allow CommonJS modules to import from
@@ -20,9 +22,46 @@ async function getNextBuilderDeferred() {
20
22
  if (CachedNextBuilderDeferred) {
21
23
  return CachedNextBuilderDeferred;
22
24
  }
23
- const { BaseBuilder: BaseBuilderClass, STEP_QUEUE_TRIGGER, WORKFLOW_QUEUE_TRIGGER, applySwcTransform, detectWorkflowPatterns, getImportPath, isWorkflowSdkFile, resolveWorkflowAliasRelativePath,
25
+ const { BaseBuilder: BaseBuilderClass, STEP_QUEUE_TRIGGER, WORKFLOW_QUEUE_TRIGGER, applySwcTransform, detectWorkflowPatterns, getImportPath, resolveWorkflowAliasRelativePath,
24
26
  // biome-ignore lint/security/noGlobalEval: Need to use eval here to avoid TypeScript from transpiling the import statement into `require()`
25
27
  } = (await eval('import("@workflow/builders")'));
28
+ // Shared resolve options matching the configuration used by the SWC
29
+ // esbuild plugin (swc-esbuild-plugin.ts) for consistent resolution
30
+ // semantics across the toolchain.
31
+ const NODE_RESOLVE_OPTIONS = {
32
+ dependencyType: 'commonjs',
33
+ modules: ['node_modules'],
34
+ exportsFields: ['exports'],
35
+ importsFields: ['imports'],
36
+ conditionNames: ['node', 'require'],
37
+ descriptionFiles: ['package.json'],
38
+ extensions: [
39
+ '.ts',
40
+ '.tsx',
41
+ '.mts',
42
+ '.cts',
43
+ '.cjs',
44
+ '.mjs',
45
+ '.js',
46
+ '.jsx',
47
+ '.json',
48
+ '.node',
49
+ ],
50
+ enforceExtensions: false,
51
+ symlinks: true,
52
+ mainFields: ['main'],
53
+ mainFiles: ['index'],
54
+ roots: [],
55
+ fullySpecified: false,
56
+ preferRelative: false,
57
+ preferAbsolute: false,
58
+ restrictions: [],
59
+ };
60
+ const NODE_ESM_RESOLVE_OPTIONS = {
61
+ ...NODE_RESOLVE_OPTIONS,
62
+ dependencyType: 'esm',
63
+ conditionNames: ['node', 'import'],
64
+ };
26
65
  class NextDeferredBuilder extends BaseBuilderClass {
27
66
  socketIO;
28
67
  discoveredWorkflowFiles = new Set();
@@ -34,6 +73,11 @@ async function getNextBuilderDeferred() {
34
73
  cacheWriteTimer = null;
35
74
  deferredRebuildTimer = null;
36
75
  lastDeferredBuildSignature = null;
76
+ // Lazily initialized resolvers for bare specifier rewriting.
77
+ // Cached to avoid re-creating on every import rewrite.
78
+ esmSyncResolver;
79
+ cjsSyncResolver;
80
+ manifestStepResolveBaseDirs = null;
37
81
  async build() {
38
82
  const outputDir = await this.findAppDirectory();
39
83
  await this.initializeDiscoveryState();
@@ -70,7 +114,9 @@ async function getNextBuilderDeferred() {
70
114
  for (let buildPass = 0; buildPass < maxBuildPasses; buildPass++) {
71
115
  const inputFiles = this.getCurrentInputFiles(implicitStepFiles);
72
116
  const buildSignature = await this.createDeferredBuildSignature(inputFiles);
73
- if (buildSignature === this.lastDeferredBuildSignature) {
117
+ const shouldForceBuildForGeneratedRoutes = await this.shouldForceBuildForGeneratedRoutes();
118
+ if (buildSignature === this.lastDeferredBuildSignature &&
119
+ !shouldForceBuildForGeneratedRoutes) {
74
120
  return;
75
121
  }
76
122
  try {
@@ -107,6 +153,53 @@ async function getNextBuilderDeferred() {
107
153
  const workflowStdlibPath = this.resolveWorkflowStdlibStepFilePath();
108
154
  return workflowStdlibPath ? [workflowStdlibPath] : [];
109
155
  }
156
+ async shouldForceBuildForGeneratedRoutes() {
157
+ const outputDir = await this.findAppDirectory();
158
+ const generatedRouteFiles = [
159
+ (0, node_path_1.join)(outputDir, '.well-known/workflow/v1/flow/route.js'),
160
+ (0, node_path_1.join)(outputDir, '.well-known/workflow/v1/step/route.js'),
161
+ (0, node_path_1.join)(outputDir, '.well-known/workflow/v1/webhook/[token]/route.js'),
162
+ ];
163
+ for (const routeFilePath of generatedRouteFiles) {
164
+ const routeState = await this.getGeneratedRouteState(routeFilePath);
165
+ if (routeState === 'missing') {
166
+ return true;
167
+ }
168
+ if (routeState === 'stub') {
169
+ return true;
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+ async getGeneratedRouteState(routeFilePath) {
175
+ let routeStats;
176
+ try {
177
+ routeStats = await (0, promises_1.stat)(routeFilePath);
178
+ }
179
+ catch {
180
+ return 'missing';
181
+ }
182
+ if (!routeStats.isFile()) {
183
+ return 'missing';
184
+ }
185
+ try {
186
+ const routeFileHandle = await (0, promises_1.open)(routeFilePath, 'r');
187
+ try {
188
+ const markerScanBuffer = Buffer.alloc(ROUTE_STUB_MARKER_SCAN_BYTES);
189
+ const { bytesRead } = await routeFileHandle.read(markerScanBuffer, 0, ROUTE_STUB_MARKER_SCAN_BYTES, 0);
190
+ const markerScanSource = markerScanBuffer.toString('utf8', 0, bytesRead);
191
+ return markerScanSource.includes(ROUTE_STUB_FILE_MARKER)
192
+ ? 'stub'
193
+ : 'generated';
194
+ }
195
+ finally {
196
+ await routeFileHandle.close();
197
+ }
198
+ }
199
+ catch {
200
+ return 'missing';
201
+ }
202
+ }
110
203
  resolveWorkflowStdlibStepFilePath() {
111
204
  let workflowCjsEntry;
112
205
  try {
@@ -175,22 +268,20 @@ async function getNextBuilderDeferred() {
175
268
  return null;
176
269
  }
177
270
  if (!validatePatterns) {
178
- const isSdkFile = isWorkflowSdkFile(filePath);
179
271
  return {
180
272
  filePath,
181
273
  hasUseWorkflow: candidates.hasWorkflowCandidate,
182
274
  hasUseStep: candidates.hasStepCandidate,
183
- hasSerde: candidates.hasSerdeCandidate && !isSdkFile,
275
+ hasSerde: candidates.hasSerdeCandidate,
184
276
  };
185
277
  }
186
278
  const source = await (0, promises_1.readFile)(filePath, 'utf-8');
187
279
  const patterns = detectWorkflowPatterns(source);
188
- const isSdkFile = isWorkflowSdkFile(filePath);
189
280
  return {
190
281
  filePath,
191
282
  hasUseWorkflow: patterns.hasUseWorkflow,
192
283
  hasUseStep: patterns.hasUseStep,
193
- hasSerde: patterns.hasSerde && !isSdkFile,
284
+ hasSerde: patterns.hasSerde,
194
285
  };
195
286
  }
196
287
  catch {
@@ -322,8 +413,11 @@ async function getNextBuilderDeferred() {
322
413
  routeFileName: tempRouteFileName,
323
414
  discoveredEntries,
324
415
  };
325
- const { manifest: stepsManifest } = await this.buildStepsFunction(options);
326
416
  const workflowsBundle = await this.buildWorkflowsFunction(options);
417
+ const { manifest: stepsManifest } = await this.buildStepsFunction({
418
+ ...options,
419
+ additionalStepSourceManifest: workflowsBundle?.manifest,
420
+ });
327
421
  await this.buildWebhookRoute({
328
422
  workflowGeneratedDir,
329
423
  routeFileName: tempRouteFileName,
@@ -451,11 +545,7 @@ async function getNextBuilderDeferred() {
451
545
  if (hasCacheTrackingChange) {
452
546
  this.scheduleWorkflowsCacheWrite();
453
547
  }
454
- if (hasWorkflow ||
455
- hasStep ||
456
- hasSerde ||
457
- hasCacheTrackingChange ||
458
- wasTrackedDependency) {
548
+ if (hasCacheTrackingChange || wasTrackedDependency) {
459
549
  this.scheduleDeferredRebuild();
460
550
  }
461
551
  },
@@ -470,7 +560,8 @@ async function getNextBuilderDeferred() {
470
560
  return;
471
561
  }
472
562
  await this.loadWorkflowsCache();
473
- await this.loadDiscoveredEntriesFromInputGraph();
563
+ // Deferred mode must not run eager input-graph discovery; entries are
564
+ // discovered via loader->socket notifications during Next's build.
474
565
  this.cacheInitialized = true;
475
566
  }
476
567
  getDistDir() {
@@ -483,9 +574,38 @@ async function getNextBuilderDeferred() {
483
574
  return (0, node_path_1.join)(this.config.workingDir, this.getDistDir(), 'cache', 'workflow-socket.json');
484
575
  }
485
576
  normalizeDiscoveredFilePath(filePath) {
486
- return (0, node_path_1.isAbsolute)(filePath)
577
+ const resolvedPath = (0, node_path_1.isAbsolute)(filePath)
487
578
  ? filePath
488
579
  : (0, node_path_1.resolve)(this.config.workingDir, filePath);
580
+ try {
581
+ return (0, node_fs_1.realpathSync)(resolvedPath);
582
+ }
583
+ catch {
584
+ return resolvedPath;
585
+ }
586
+ }
587
+ getManifestStepResolveBaseDirs() {
588
+ if (this.manifestStepResolveBaseDirs) {
589
+ return this.manifestStepResolveBaseDirs;
590
+ }
591
+ const resolveBaseDirs = new Set();
592
+ const addResolveBaseDir = (baseDir) => {
593
+ resolveBaseDirs.add(this.normalizeDiscoveredFilePath(baseDir));
594
+ };
595
+ if (this.config.projectRoot) {
596
+ addResolveBaseDir(this.config.projectRoot);
597
+ }
598
+ let currentResolveDir = this.config.workingDir;
599
+ while (currentResolveDir) {
600
+ addResolveBaseDir(currentResolveDir);
601
+ const parentResolveDir = (0, node_path_1.dirname)(currentResolveDir);
602
+ if (parentResolveDir === currentResolveDir) {
603
+ break;
604
+ }
605
+ currentResolveDir = parentResolveDir;
606
+ }
607
+ this.manifestStepResolveBaseDirs = Array.from(resolveBaseDirs);
608
+ return this.manifestStepResolveBaseDirs;
489
609
  }
490
610
  async filterExistingFiles(filePaths) {
491
611
  const normalizedFilePaths = Array.from(new Set(filePaths.map((filePath) => this.normalizeDiscoveredFilePath(filePath)))).sort();
@@ -683,41 +803,6 @@ async function getNextBuilderDeferred() {
683
803
  this.discoveredSerdeFiles.add(filePath);
684
804
  }
685
805
  }
686
- async loadDiscoveredEntriesFromInputGraph() {
687
- const inputFiles = await this.getInputFiles();
688
- if (inputFiles.length === 0) {
689
- return;
690
- }
691
- const { discoveredWorkflows, discoveredSteps, discoveredSerdeFiles } = await this.discoverEntries(inputFiles, this.config.workingDir);
692
- const { workflowFiles, stepFiles, serdeFiles } = await this.reconcileDiscoveredEntries({
693
- workflowCandidates: discoveredWorkflows,
694
- stepCandidates: discoveredSteps,
695
- serdeCandidates: discoveredSerdeFiles,
696
- validatePatterns: true,
697
- });
698
- let hasChanges = false;
699
- for (const filePath of workflowFiles) {
700
- if (!this.discoveredWorkflowFiles.has(filePath)) {
701
- this.discoveredWorkflowFiles.add(filePath);
702
- hasChanges = true;
703
- }
704
- }
705
- for (const filePath of stepFiles) {
706
- if (!this.discoveredStepFiles.has(filePath)) {
707
- this.discoveredStepFiles.add(filePath);
708
- hasChanges = true;
709
- }
710
- }
711
- for (const filePath of serdeFiles) {
712
- if (!this.discoveredSerdeFiles.has(filePath)) {
713
- this.discoveredSerdeFiles.add(filePath);
714
- hasChanges = true;
715
- }
716
- }
717
- if (hasChanges) {
718
- this.scheduleWorkflowsCacheWrite();
719
- }
720
- }
721
806
  async writeWorkflowsCache() {
722
807
  const cacheFilePath = this.getWorkflowsCacheFilePath();
723
808
  const cacheDir = (0, node_path_1.join)(this.config.workingDir, this.getDistDir(), 'cache');
@@ -752,21 +837,6 @@ async function getNextBuilderDeferred() {
752
837
  await this.writeFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, 'step/route.js'), routeStubContent);
753
838
  await this.writeFileIfChanged((0, node_path_1.join)(workflowGeneratedDir, 'webhook/[token]/route.js'), routeStubContent);
754
839
  }
755
- async getInputFiles() {
756
- const inputFiles = await super.getInputFiles();
757
- return inputFiles.filter((item) => {
758
- // Match App Router entrypoints: route.ts, page.ts, layout.ts in app/ or src/app/ directories
759
- // Matches: /app/page.ts, /app/dashboard/page.ts, /src/app/route.ts, etc.
760
- if (item.match(/(^|.*[/\\])(app|src[/\\]app)([/\\](route|page|layout)\.|[/\\].*[/\\](route|page|layout)\.)/)) {
761
- return true;
762
- }
763
- // Match Pages Router entrypoints: files in pages/ or src/pages/
764
- if (item.match(/[/\\](pages|src[/\\]pages)[/\\]/)) {
765
- return true;
766
- }
767
- return false;
768
- });
769
- }
770
840
  async writeFunctionsConfig(outputDir) {
771
841
  // we don't run this in development mode as it's not needed
772
842
  if (process.env.NODE_ENV === 'development') {
@@ -893,6 +963,25 @@ async function getNextBuilderDeferred() {
893
963
  }
894
964
  rewriteCopiedStepImportSpecifier(specifier, sourceFilePath, copiedFilePath, copiedStepFileBySourcePath) {
895
965
  if (!specifier.startsWith('.')) {
966
+ // Bare specifiers (e.g. '@workflow/serde') that are transitive
967
+ // dependencies of SDK packages can't be resolved by the bundler
968
+ // from the copied file's location (__workflow_step_files__/ inside
969
+ // the app dir) because the app doesn't directly depend on them.
970
+ //
971
+ // Only rewrite when the specifier can't be resolved from the app
972
+ // directory. If the package is a direct dependency of the app,
973
+ // the bare specifier will resolve normally and should be left as-is.
974
+ const appResolvable = this.resolveBareCopiedStepSpecifier(specifier, copiedFilePath);
975
+ if (!appResolvable) {
976
+ const resolved = this.resolveBareCopiedStepSpecifier(specifier, sourceFilePath);
977
+ if (!resolved)
978
+ return specifier;
979
+ let rewrittenPath = (0, node_path_1.relative)((0, node_path_1.dirname)(copiedFilePath), resolved).replace(/\\/g, '/');
980
+ if (!rewrittenPath.startsWith('.')) {
981
+ rewrittenPath = `./${rewrittenPath}`;
982
+ }
983
+ return rewrittenPath;
984
+ }
896
985
  return specifier;
897
986
  }
898
987
  const specifierMatch = specifier.match(/^([^?#]+)(.*)$/);
@@ -917,6 +1006,39 @@ async function getNextBuilderDeferred() {
917
1006
  }
918
1007
  return `${rewrittenPath}${suffix}`;
919
1008
  }
1009
+ /**
1010
+ * Resolves a bare specifier (e.g. '@workflow/serde', 'workflow') to an
1011
+ * absolute file path using ESM-compatible resolution semantics via
1012
+ * `enhanced-resolve`. Tries ESM conditions first (`node`, `import`),
1013
+ * falling back to CJS resolution if ESM fails.
1014
+ */
1015
+ resolveBareCopiedStepSpecifier(specifier, sourceFilePath) {
1016
+ if (!this.esmSyncResolver) {
1017
+ this.esmSyncResolver = enhanced_resolve_1.default.create.sync(NODE_ESM_RESOLVE_OPTIONS);
1018
+ }
1019
+ if (!this.cjsSyncResolver) {
1020
+ this.cjsSyncResolver =
1021
+ enhanced_resolve_1.default.create.sync(NODE_RESOLVE_OPTIONS);
1022
+ }
1023
+ const context = (0, node_path_1.dirname)(sourceFilePath);
1024
+ try {
1025
+ const resolved = this.esmSyncResolver(context, specifier);
1026
+ if (resolved)
1027
+ return resolved;
1028
+ }
1029
+ catch {
1030
+ // ESM resolution failed, try CJS
1031
+ }
1032
+ try {
1033
+ const resolved = this.cjsSyncResolver(context, specifier);
1034
+ if (resolved)
1035
+ return resolved;
1036
+ }
1037
+ catch {
1038
+ // CJS resolution also failed
1039
+ }
1040
+ return undefined;
1041
+ }
920
1042
  resolveCopiedStepImportTargetPath(targetPath) {
921
1043
  if ((0, node_fs_1.existsSync)(targetPath)) {
922
1044
  return targetPath;
@@ -975,10 +1097,14 @@ async function getNextBuilderDeferred() {
975
1097
  }
976
1098
  return Array.from(relativeSpecifiers);
977
1099
  }
978
- shouldSkipTransitiveStepFile(filePath) {
1100
+ isGeneratedWorkflowArtifact(filePath) {
979
1101
  const normalizedPath = filePath.replace(/\\/g, '/');
980
1102
  return (normalizedPath.includes('/.well-known/workflow/') ||
981
- normalizedPath.includes('/.next/') ||
1103
+ normalizedPath.includes('/.next/'));
1104
+ }
1105
+ shouldSkipTransitiveStepFile(filePath) {
1106
+ const normalizedPath = filePath.replace(/\\/g, '/');
1107
+ return (this.isGeneratedWorkflowArtifact(normalizedPath) ||
982
1108
  normalizedPath.includes('/node_modules/') ||
983
1109
  normalizedPath.includes('/.pnpm/'));
984
1110
  }
@@ -1094,9 +1220,9 @@ async function getNextBuilderDeferred() {
1094
1220
  async collectTransitiveSerdeFiles({ entryFiles, serdeFiles, }) {
1095
1221
  const normalizedEntryFiles = Array.from(new Set(entryFiles.map((entryFile) => this.normalizeDiscoveredFilePath(entryFile)))).sort();
1096
1222
  const normalizedSerdeSeedFiles = Array.from(new Set(serdeFiles.map((serdeFile) => this.normalizeDiscoveredFilePath(serdeFile)))).sort();
1097
- // Intentionally re-validate serde seeds against source + SDK filtering.
1223
+ // Intentionally re-validate serde seeds against source patterns.
1098
1224
  // This keeps previously discovered/manual seed entries from sticking when
1099
- // files no longer match serde patterns or resolve to SDK internals.
1225
+ // files no longer match serde patterns.
1100
1226
  const discoveredSerdeFiles = new Set();
1101
1227
  const queuedFiles = Array.from(new Set([...normalizedEntryFiles, ...normalizedSerdeSeedFiles]));
1102
1228
  const visitedFiles = new Set();
@@ -1131,7 +1257,7 @@ async function getNextBuilderDeferred() {
1131
1257
  };
1132
1258
  for (const serdeSeedFile of normalizedSerdeSeedFiles) {
1133
1259
  const seedPatterns = await getPatterns(serdeSeedFile);
1134
- if (seedPatterns?.hasSerde && !isWorkflowSdkFile(serdeSeedFile)) {
1260
+ if (seedPatterns?.hasSerde) {
1135
1261
  discoveredSerdeFiles.add(serdeSeedFile);
1136
1262
  }
1137
1263
  }
@@ -1155,13 +1281,37 @@ async function getNextBuilderDeferred() {
1155
1281
  queuedFiles.push(resolvedImportPath);
1156
1282
  }
1157
1283
  const importPatterns = await getPatterns(resolvedImportPath);
1158
- if (importPatterns?.hasSerde &&
1159
- !isWorkflowSdkFile(resolvedImportPath)) {
1284
+ if (importPatterns?.hasSerde) {
1160
1285
  discoveredSerdeFiles.add(resolvedImportPath);
1161
1286
  }
1162
1287
  }
1163
1288
  }
1164
- return Array.from(discoveredSerdeFiles).sort();
1289
+ // AST-level verification: run SWC detect mode on regex-matched candidates
1290
+ // to confirm they actually define serde classes. This prevents SDK internal
1291
+ // files (which match serde regex patterns but define no classes) from being
1292
+ // bundled into the workflow sandbox.
1293
+ const projectRoot = this.config.projectRoot || this.config.workingDir;
1294
+ const verifiedSerdeFiles = [];
1295
+ await Promise.all(Array.from(discoveredSerdeFiles).map(async (filePath) => {
1296
+ const source = await getSource(filePath);
1297
+ if (!source)
1298
+ return;
1299
+ try {
1300
+ const relativeFilename = await this.getRelativeFilenameForSwc(filePath);
1301
+ const { workflowManifest } = await applySwcTransform(relativeFilename, source, 'detect', filePath, projectRoot);
1302
+ // Only include files that actually define serde classes
1303
+ const hasClasses = workflowManifest.classes &&
1304
+ Object.values(workflowManifest.classes).some((entries) => Object.keys(entries).length > 0);
1305
+ if (hasClasses) {
1306
+ verifiedSerdeFiles.push(filePath);
1307
+ }
1308
+ }
1309
+ catch {
1310
+ // If detect fails, include the file to be safe
1311
+ verifiedSerdeFiles.push(filePath);
1312
+ }
1313
+ }));
1314
+ return verifiedSerdeFiles.sort();
1165
1315
  }
1166
1316
  async createResponseBuiltinsStepFile({ stepsRouteDir, }) {
1167
1317
  const copiedStepsDir = (0, node_path_1.join)(stepsRouteDir, step_copy_utils_js_1.DEFERRED_STEP_COPY_DIR_NAME);
@@ -1190,12 +1340,12 @@ async function getNextBuilderDeferred() {
1190
1340
  await this.writeFileIfChanged(responseBuiltinsFilePath, `${source}\n${sourceMapComment}\n`);
1191
1341
  return responseBuiltinsFilePath;
1192
1342
  }
1193
- async copyDiscoveredStepFiles({ stepFiles, stepsRouteDir, }) {
1343
+ async copyDiscoveredStepFiles({ stepFiles, stepsRouteDir, preserveFileNames = [], }) {
1194
1344
  const copiedStepsDir = (0, node_path_1.join)(stepsRouteDir, step_copy_utils_js_1.DEFERRED_STEP_COPY_DIR_NAME);
1195
1345
  await (0, promises_1.mkdir)(copiedStepsDir, { recursive: true });
1196
1346
  const normalizedStepFiles = Array.from(new Set(stepFiles.map((stepFile) => this.normalizeDiscoveredFilePath(stepFile)))).sort();
1197
1347
  const copiedStepFileBySourcePath = new Map();
1198
- const expectedFileNames = new Set();
1348
+ const expectedFileNames = new Set(preserveFileNames);
1199
1349
  const copiedStepFiles = [];
1200
1350
  for (const normalizedStepFile of normalizedStepFiles) {
1201
1351
  const copiedFileName = this.getStepCopyFileName(normalizedStepFile);
@@ -1271,7 +1421,24 @@ async function getNextBuilderDeferred() {
1271
1421
  }));
1272
1422
  return workflowManifest;
1273
1423
  }
1274
- async buildStepsFunction({ workflowGeneratedDir, routeFileName = 'route.js', discoveredEntries, }) {
1424
+ async collectManifestStepSourceFiles(manifest) {
1425
+ const manifestStepEntries = Object.keys(manifest.steps || {});
1426
+ if (manifestStepEntries.length === 0) {
1427
+ return [];
1428
+ }
1429
+ const resolveBaseDirs = this.getManifestStepResolveBaseDirs();
1430
+ const candidateFiles = manifestStepEntries
1431
+ .flatMap((stepEntry) => {
1432
+ if ((0, node_path_1.isAbsolute)(stepEntry)) {
1433
+ return [this.normalizeDiscoveredFilePath(stepEntry)];
1434
+ }
1435
+ return resolveBaseDirs.map((baseDir) => this.normalizeDiscoveredFilePath((0, node_path_1.resolve)(baseDir, stepEntry)));
1436
+ })
1437
+ .filter((candidateFile) => !this.isGeneratedWorkflowArtifact(candidateFile));
1438
+ const existingCandidates = await this.filterExistingFiles(candidateFiles);
1439
+ return Array.from(new Set(existingCandidates)).sort();
1440
+ }
1441
+ async buildStepsFunction({ workflowGeneratedDir, routeFileName = 'route.js', discoveredEntries, additionalStepSourceManifest, }) {
1275
1442
  const stepsRouteDir = (0, node_path_1.join)(workflowGeneratedDir, 'step');
1276
1443
  await (0, promises_1.mkdir)(stepsRouteDir, { recursive: true });
1277
1444
  const discovered = discoveredEntries;
@@ -1285,22 +1452,36 @@ async function getNextBuilderDeferred() {
1285
1452
  const serdeFiles = [...discovered.discoveredSerdeFiles].sort();
1286
1453
  const stepFileSet = new Set(stepFiles);
1287
1454
  const serdeOnlyFiles = serdeFiles.filter((file) => !stepFileSet.has(file));
1455
+ const additionalManifestStepFiles = additionalStepSourceManifest
1456
+ ? await this.collectManifestStepSourceFiles(additionalStepSourceManifest)
1457
+ : [];
1458
+ const stepFilesWithManifestSources = Array.from(new Set([...stepFiles, ...additionalManifestStepFiles])).sort();
1459
+ const responseBuiltinsStepFilePath = await this.createResponseBuiltinsStepFile({
1460
+ stepsRouteDir,
1461
+ });
1462
+ const manifestStepFiles = Array.from(new Set([...stepFilesWithManifestSources, responseBuiltinsStepFilePath])).sort();
1463
+ const manifest = await this.createDeferredStepsManifest({
1464
+ stepFiles: manifestStepFiles,
1465
+ workflowFiles,
1466
+ serdeOnlyFiles,
1467
+ });
1468
+ const manifestDiscoveredStepFiles = await this.collectManifestStepSourceFiles(manifest);
1288
1469
  // Copy all discovered step sources so they are transformed in step mode.
1289
1470
  // Importing raw node_modules files directly can bypass loader transforms,
1290
1471
  // which prevents step registrars from being emitted.
1291
- const copiedStepSourceFiles = stepFiles;
1472
+ const copiedStepSourceFiles = Array.from(new Set([
1473
+ ...stepFilesWithManifestSources,
1474
+ ...manifestDiscoveredStepFiles,
1475
+ ])).sort();
1292
1476
  const copiedDiscoveredStepFiles = await this.copyDiscoveredStepFiles({
1293
1477
  stepFiles: copiedStepSourceFiles,
1294
1478
  stepsRouteDir,
1295
- });
1296
- const responseBuiltinsStepFilePath = await this.createResponseBuiltinsStepFile({
1297
- stepsRouteDir,
1479
+ preserveFileNames: [(0, node_path_1.basename)(responseBuiltinsStepFilePath)],
1298
1480
  });
1299
1481
  const copiedStepFiles = [
1300
1482
  responseBuiltinsStepFilePath,
1301
1483
  ...copiedDiscoveredStepFiles,
1302
1484
  ];
1303
- const manifestStepFiles = Array.from(new Set([...stepFiles, responseBuiltinsStepFilePath])).sort();
1304
1485
  const stepRouteFile = (0, node_path_1.join)(stepsRouteDir, routeFileName);
1305
1486
  const copiedStepImports = copiedStepFiles
1306
1487
  .map((copiedStepFile) => {
@@ -1331,11 +1512,6 @@ async function getNextBuilderDeferred() {
1331
1512
  .filter(Boolean)
1332
1513
  .join('\n');
1333
1514
  await this.writeFileIfChanged(stepRouteFile, routeContents);
1334
- const manifest = await this.createDeferredStepsManifest({
1335
- stepFiles: manifestStepFiles,
1336
- workflowFiles,
1337
- serdeOnlyFiles,
1338
- });
1339
1515
  return {
1340
1516
  context: undefined,
1341
1517
  manifest,