fix(next): skip eager workflow discovery in deferred mode with explicit dirs#1585
fix(next): skip eager workflow discovery in deferred mode with explicit dirs#1585
Conversation
…re explicit In deferred build mode, workflow discovery should only happen through: 1. Cache from previous builds 2. Loader socket notifications when Next.js loads files When users specify explicit workflow directories (workflows.dirs option), the dirsAreEntrypoints flag is false, indicating these are actual workflow directories, not entrypoints to trace from. Previously, loadDiscoveredEntriesFromInputGraph() was always called during initialization, performing eager discovery that defeats the purpose of both: - Deferred mode (which should discover on-demand) - The workflows.dirs optimization (which avoids scanning the entire app) This fix conditionally skips eager discovery when dirsAreEntrypoints is false, allowing deferred mode to work as intended with explicit workflow directories.
🦋 Changeset detectedLatest commit: 9e7015b The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 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: Nitro | Next.js (Turbopack) workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro 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 (66 failed)astro (1 failed):
express (1 failed):
fastify (1 failed):
hono (1 failed):
nextjs-turbopack (61 failed):
sveltekit (1 failed):
📦 Local Production (93 failed)nextjs-turbopack-canary (38 failed):
nextjs-turbopack-stable (55 failed):
🐘 Local Postgres (93 failed)nextjs-turbopack-canary (38 failed):
nextjs-turbopack-stable (55 failed):
🪟 Windows (55 failed)nextjs-turbopack (55 failed):
🌍 Community Worlds (141 failed)mongodb-dev (1 failed):
mongodb (41 failed):
redis-dev (1 failed):
redis (41 failed):
turso-dev (1 failed):
turso (56 failed):
Details by Category❌ ▲ Vercel Production
✅ 💻 Local Development
❌ 📦 Local Production
❌ 🐘 Local Postgres
❌ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
The @workflow/ai package was exporting compiled JS which strips 'use step' directives. This caused internal step functions like closeStream to not be registered in deferred mode. Solution: Add 'workflow' export condition that points to the TypeScript source, similar to how @workflow/core and workflow packages work. This ensures the workflow bundler processes the source with directives intact.
The src folder is not published with the package, so pointing the workflow export to src/agent/durable-agent.ts causes resolution errors. The compiled JS preserves 'use step' directives, so we can safely point the workflow export to the dist file. This matches the pattern where workflow and default exports are the same (unlike @workflow/core which has separate workflow-specific builds).
The extractBundleSourceFiles method was excluding ALL node_modules files from tracked dependency discovery. This prevented step functions in @workflow/ai from being discovered after bundling. Solution: Allow @workflow/* scoped packages to be tracked as dependencies, while still excluding other node_modules. This ensures internal step functions in workflow SDK packages are properly discovered and registered.
Next.js doesn't process node_modules files with custom loaders by default. Adding @workflow/ai to transpilePackages ensures our loader processes it, allowing step functions like closeStream to be discovered via socket notifications.
In production builds, socket notifications don't trigger rebuilds because scheduleDeferredRebuild() is a no-op when watch is false. Without initial discovery, there are no files to bundle, so the system can't bootstrap. Solution: Perform discovery in two cases: 1. Production builds (always, because socket rebuilds don't work) 2. First dev build when cache is empty (to bootstrap) In dev mode with cache, skip discovery and rely on socket notifications. This provides the optimization for the common case (dev with cache) while ensuring production builds and first builds work correctly.
VaguelySerious
left a comment
There was a problem hiding this comment.
AI review: blocking issues found
1. Fix test to use mocking instead of dynamic import (avoids VM error) 2. Include discoveredSerdeFiles in cache check 3. Apply @workflow package allowlist to shouldSkipTransitiveStepFile 4. Add comment about @workflow/ai being optional dependency 5. Make changeset more concise
The previous test mocked the entire module and re-implemented initializeDiscoveryState in the mock, so it tested mock behavior rather than production code. New approach: Spy on BaseBuilder.discoverEntries to verify when discovery is called, while testing the real initializeDiscoveryState implementation. This ensures tests will fail if the production code changes incorrectly.
…b.com:vercel/workflow into cursor/workflow-discovery-deferred-build-2820
Problem
Workflow discovery was happening on every build in deferred mode, even in dev mode with an existing cache. This defeats the optimization purpose of deferred mode.
Root Cause
The deferred builder was calling
loadDiscoveredEntriesFromInputGraph()unconditionally during initialization, performing eager discovery on every build regardless of context.Solution
Made discovery conditional based on build context:
Skip discovery when:
watch: true) AND cache existsPerform discovery when:
Additional fixes:
@workflow/*packages in tracked dependency discovery (bothextractBundleSourceFilesandshouldSkipTransitiveStepFile)@workflow/aitotranspilePackagesso loader processes it (optional dependency, Next.js handles gracefully if not installed)"workflow"export condition to@workflow/ai/agent(points to same dist file as default - forward compatibility for potential future workflow-specific builds)Changes
packages/next/src/builder-deferred.ts: Conditional discovery based on watch mode and cache, consistent @workflow package filteringpackages/next/src/index.ts: Add @workflow/ai to transpilePackages with explanatory commentpackages/ai/package.json: Added workflow export conditionpackages/next/src/builder-deferred.test.ts: Mocked tests avoiding dynamic import issuesImpact
This provides the optimization for the common dev workflow:
Related
Slack Thread