refactor(next): remove step file copy mechanism from deferred builder#1796
refactor(next): remove step file copy mechanism from deferred builder#1796TooTallNate wants to merge 2 commits intomainfrom
Conversation
Step sources are now imported directly into the generated `step/route.js` using the same `getImportPath`-based logic already used for serde files. This is possible because the SWC plugin's client mode was merged into step mode (#1686), so the workflow loader always runs in step mode and transforms every file it sees, including those from packages. Removed: - `__workflow_step_files__/` per-file copies with hashed names, metadata comments, inline source maps, and bare-specifier rewriting via `enhanced-resolve` - `createResponseBuiltinsStepFile`; builtins are now imported via `require.resolve('workflow/internal/builtins')` at build time - `step-copy-utils.ts` and all copy-specific branches in `loader.ts` - `enhanced-resolve` dependency The deferred builder still removes the legacy `__workflow_step_files__/` directory on boot so upgrades leave no stale artifacts behind. Dev tests that inspected copied step file contents now inspect the manifest.json entries (which list every discovered step keyed by source path).
🦋 Changeset detectedLatest commit: c44020e The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
❌ Some benchmark jobs failed:
Check the workflow run for details. |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests▲ Vercel Production (1 failed)nextjs-turbopack (1 failed):
💻 Local Development (4 failed)nextjs-webpack-canary (2 failed):
nextjs-webpack-stable (2 failed):
📦 Local Production (2 failed)nextjs-turbopack-canary (1 failed):
nextjs-turbopack-stable (1 failed):
🐘 Local Postgres (2 failed)nextjs-turbopack-canary (1 failed):
nextjs-turbopack-stable (1 failed):
Details by Category❌ ▲ Vercel Production
❌ 💻 Local Development
❌ 📦 Local Production
❌ 🐘 Local Postgres
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
There was a problem hiding this comment.
Pull request overview
Refactors the Next.js deferred builder to stop copying step source files into a generated directory, instead importing step/serde sources directly from their original locations (mirroring existing serde handling) and simplifying the loader accordingly.
Changes:
- Remove deferred step copy utilities and copy/rewrite logic; generate
step/route.jswith direct imports (including workflow response builtins). - Simplify the Next loader by removing step-copy-file special casing and always using the actual filename/module specifier inputs.
- Update dev E2E assertions to validate manifest/route imports (rather than copied file contents) and drop the unused dependency.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
packages/next/src/builder-deferred.ts |
Removes step-copy pipeline and switches step route generation to direct imports + legacy cleanup. |
packages/next/src/loader.ts |
Deletes deferred step-copy branches and unifies transform/discovery behavior. |
packages/next/src/step-copy-utils.ts |
Deleted (no longer needed). |
packages/next/package.json |
Drops enhanced-resolve dependency. |
pnpm-lock.yaml |
Removes enhanced-resolve from @workflow/next importer. |
packages/core/e2e/dev.test.ts |
Updates dev E2E checks to assert via manifest + route import specifiers. |
.changeset/remove-step-file-copy.md |
Adds release note for the behavioral change. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | null { | ||
| let resolved: string; | ||
| try { | ||
| resolved = require.resolve('workflow/internal/builtins', { | ||
| paths: [this.config.workingDir], | ||
| }); | ||
| } catch { | ||
| return null; |
There was a problem hiding this comment.
resolveResponseBuiltinsStepSource() silently returns null when workflow/internal/builtins cannot be resolved. If the app has an incompatible workflow version (or resolution differs under pnpm), this will build a step route without registering the builtin response steps, likely causing runtime failures when Request/Response builtins are invoked. Consider failing fast with a clear error (e.g., instruct to upgrade workflow) or at least logging a warning and keeping the previous in-repo fallback behavior so builtins are always present.
| } | null { | |
| let resolved: string; | |
| try { | |
| resolved = require.resolve('workflow/internal/builtins', { | |
| paths: [this.config.workingDir], | |
| }); | |
| } catch { | |
| return null; | |
| } { | |
| let resolved: string; | |
| try { | |
| resolved = require.resolve('workflow/internal/builtins', { | |
| paths: [this.config.workingDir], | |
| }); | |
| } catch (error) { | |
| const message = | |
| error instanceof Error ? ` ${error.message}` : ''; | |
| throw new Error( | |
| `Unable to resolve "workflow/internal/builtins" from working directory ` + | |
| `"${this.config.workingDir}". Builtin Request/Response workflow steps ` + | |
| `require a compatible "workflow" package to be installed and resolvable ` + | |
| `from the application. Please ensure the app depends on a compatible ` + | |
| `"workflow" version and upgrade it if necessary.${message}` | |
| ); |
VaguelySerious
left a comment
There was a problem hiding this comment.
AI review: no blocking issues
| inputSourceMap: isDeferredStepCopyFile | ||
| ? deferredSourceMapResult.sourceMap || sourceMap | ||
| : sourceMap, | ||
| inputSourceMap: sourceMap, |
There was a problem hiding this comment.
AI Review: Note
Directly transforming compiled package sources brings back SWC's external sourcemap-resolution path. Built @workflow/ai files (and others) ship with //# sourceMappingURL=foo.js.map trailers that point at sibling .map files. SWC tries to resolve the URL against the loader's relative filename (e.g. packages/ai/dist/agent/durable-agent.js) — that path doesn't exist on disk, so SWC logs failed to read input source map: failed to find input source map file … once per file per build/dev request.
Verified locally on workbench/nextjs-turbopack:
main: onlypackages/serde/dist/index.jserrors (already imported directly).- This PR: adds errors for
packages/ai/dist/agent/{do-stream-step,durable-agent,stream-text-iterator}.jsandpackages/ai/dist/providers/mock.js.
The build still succeeds and the workflows run, so this isn't blocking, but it adds noisy red ERROR lines to every workflow request in dev and to every production build that uses such packages. The previous copy mechanism avoided this by attaching an inline base64 source map via createDeferredStepCopyInlineSourceMapComment.
Options:
- Strip
//# sourceMappingURL=trailers fromsourceForTransform(or passinputSourceMap: false) when the file has no inboundsourceMapfrom a prior loader. - Read the sibling
.mapfrom disk and pass it asinputSourceMapso SWC can chain it. - Set
filenameto the absolute path here (the SWC plugin can still receive the relative path via a different option) so the sibling.mapis resolvable.
| await this.createResponseBuiltinsStepFile({ | ||
| stepsRouteDir, | ||
| }); | ||
| const responseBuiltins = this.resolveResponseBuiltinsStepSource(); |
There was a problem hiding this comment.
AI Review: Nit
The manifest key for built-in response steps changes from app/.well-known/workflow/v1/step/__workflow_step_files__/workflow-response-builtins.ts to packages/workflow/dist/internal/builtins.js. The actual stepId values (__builtin_response_array_buffer, __builtin_response_json, __builtin_response_text) are unchanged, so runtime resolution is unaffected — but anything keying off the manifest path (tooling, tests that grep manifest keys, observability dashboards that surface step source paths) will see the path move. Probably fine, just worth noting in the changeset since the diff isn't immediately obvious.
Summary
Follow-up to #1686 (removing client transform mode). Step sources in the Next.js deferred builder are now imported directly into the generated
step/route.jsinstead of being copied into__workflow_step_files__/per-file with hashed names, metadata comments, and rewritten imports. This mirrors how serde files are already handled in the same builder.Why this is now possible
The step file copy mechanism was introduced in dc2dc6a with two stated reasons:
relativeFilenamefor deterministic step IDs — the copy carried metadata so the loader fed SWC the original relative filename.Both concerns are now moot:
getRelativeFilenameForSwc(includingresolveWorkflowAliasRelativePath) already produces stable IDs from the original path.Changes
packages/next/src/builder-deferred.ts:copyDiscoveredStepFiles,createResponseBuiltinsStepFile,rewriteRelativeImportsForCopiedStep,rewriteCopiedStepImportSpecifier,resolveBareCopiedStepSpecifier,getStepCopyFileName.buildStepsFunctionso the step route imports originals viagetImportPath(package specifier for workspace/node_modules, relative path for app files).require.resolve('workflow/internal/builtins')at build time and emit one import.__workflow_step_files__/directory rm on boot so upgrades leave no stale artifacts.packages/next/src/loader.ts:isDeferredStepCopyFilePathbranches: metadata parsing, inline-sourcemap chaining, skip-transform bypass,discoveryFilePathindirection,isGeneratedWorkflowFilecarve-out.filenamefor the SWC relative filename and module specifier.packages/next/src/step-copy-utils.ts: deleted.packages/next/package.json: dropenhanced-resolvedependency.packages/core/e2e/dev.test.ts: renamesupportsDeferredStepCopies→usesDeferredBuilder; assert step registration viamanifest.jsonentries and import specifiers in the generated route, not copied file contents.Diff stats
Verification
Local verification against
workbench/nextjs-turbopack:pnpm build(all packages): passpnpm typecheck: passpnpm -C workbench/nextjs-turbopack build: pass; generatedstep/route.jscontains direct imports (incl.workflow/internal/builtins,@workflow/ai/agent, workspace and app files);__workflow_step_files__/directory not created; manifest has 27 steps / 16 workflows / 3 classes.packages/core/e2e/e2e.test.tsagainsthttp://localhost:3000): 71/71 pass.packages/core/e2e/dev.test.ts): 6/6 pass against a fresh dev server.