Skip to content

Add subpath export resolution for package IDs - #901

Merged
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids
Feb 5, 2026
Merged

Add subpath export resolution for package IDs#901
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 30, 2026

Copy link
Copy Markdown
Member

Fixed a bug in module specifier resolution and added support for package subpath exports in workflow IDs.

What changed?

  • Fixed a caching bug in the module specifier resolution system that could cause incorrect IDs
  • Added support for subpath exports in package IDs (e.g., workflow/internal/builtins@4.0.0)
  • Improved module resolution by passing absolute file paths to the SWC transform
  • Enhanced manifest merging to properly combine results from both workflow and step bundles
  • Updated builders to return and merge manifests from both workflow and step bundles

How to test?

  1. Build a project that uses subpath exports in packages
  2. Verify that workflow IDs correctly include the subpath (e.g., workflow/internal/builtins@4.0.0)
  3. Test with a project that has multiple builds to ensure module specifier caching works correctly

Why make this change?

This change addresses an issue where the module specifier cache could return incorrect results, leading to inconsistent workflow IDs. It also adds support for packages with multiple entry points through subpath exports, ensuring that steps with the same name in different subpaths don't collide. This improves the reliability of cross-bundle references and makes the system more robust when working with complex package structures.

@changeset-bot

changeset-botBot commented Jan 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b81ae6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/sveltekitPatch
@workflow/buildersPatch
@workflow/nitroPatch
@workflow/nestPatch
@workflow/nextPatch
workflow-devkit-compiler-playgroundPatch
@workflow/astroPatch
@workflow/cliPatch
@workflow/rollupPatch
workflowPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/docs-typecheckPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production479038517
✅ 💻 Local Development438032470
✅ 📦 Local Production438032470
✅ 🐘 Local Postgres438032470
✅ 🪟 Windows470047
❌ 🌍 Community Worlds311690200
✅ 📋 Other129012141
Total20001691462315

❌ Failed Tests

🌍 Community Worlds (169 failed)

mongodb (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4304
✅ example4304
✅ express4304
✅ fastify4304
✅ hono4304
✅ nextjs-turbopack4601
✅ nextjs-webpack4601
✅ nitro4304
✅ nuxt4304
✅ sveltekit4304
✅ vite4304
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb5420
✅ redis-dev300
❌ redis5420
✅ starter-dev300
❌ starter4430
✅ turso-dev300
❌ turso5420
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4304
✅ e2e-local-postgres-nest-stable4304
✅ e2e-local-prod-nest-stable4304

📋 View full workflow run

@TooTallNate
TooTallNateforce-pushed the 01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids branch from 6cce073 to ca8bd01CompareJanuary 30, 2026 19:57
@github-actions

github-actionsBot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.041s (+3.5%)1.019s (~)0.978s101.00x
💻 LocalNitro0.043s (~)1.007s (~)0.964s101.06x
💻 LocalExpress0.044s (+4.8%)1.007s (~)0.963s101.08x
🐘 PostgresNitro0.212s (-6.9% 🟢)1.015s (~)0.803s105.19x
🐘 PostgresExpress0.290s (+4.2%)1.016s (~)0.726s107.10x
🐘 PostgresNext.js (Turbopack)0.360s (+28.8% 🔺)1.021s (~)0.660s108.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.712s (+6.4% 🔺)1.582s (-0.8%)0.870s101.00x
▲ VercelExpress0.793s (+7.0% 🔺)1.607s (~)0.814s101.11x
▲ VercelNext.js (Turbopack)0.870s (-1.1%)1.721s (-12.0% 🟢)0.851s101.22x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.094s (-0.7%)2.014s (~)0.920s101.00x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
💻 LocalExpress1.117s (~)2.007s (~)0.890s101.02x
🐘 PostgresNext.js (Turbopack)1.858s (+0.5%)2.420s (+20.1% 🔺)0.562s101.70x
🐘 PostgresExpress2.133s (-1.2%)3.014s (~)0.881s101.95x
🐘 PostgresNitro2.258s (+3.1%)3.017s (~)0.759s102.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.842s (-1.3%)3.742s (+1.1%)0.900s101.00x
▲ VercelExpress2.933s (~)3.724s (+4.6%)0.791s101.03x
▲ VercelNext.js (Turbopack)2.969s (+2.0%)3.755s (-1.8%)0.786s101.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.750s (~)11.018s (~)0.268s31.00x
💻 LocalExpress10.837s (~)11.012s (~)0.175s31.01x
💻 LocalNitro10.849s (~)11.010s (~)0.162s31.01x
🐘 PostgresNext.js (Turbopack)15.074s (-1.0%)16.033s (~)0.959s21.40x
🐘 PostgresNitro20.454s (~)21.038s (~)0.583s21.90x
🐘 PostgresExpress20.532s (~)21.032s (~)0.500s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)39.852s (+75.5% 🔺)40.274s (+71.8% 🔺)0.422s11.00x
▲ VercelNitro42.916s (+89.8% 🔺)43.893s (+88.1% 🔺)0.977s21.08x
▲ VercelExpress45.343s (+94.4% 🔺)46.170s (+88.5% 🔺)0.827s21.14x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)27.245s (~)28.038s (~)0.793s31.00x
💻 LocalNitro27.465s (~)28.022s (~)0.558s31.01x
💻 LocalExpress27.501s (~)28.025s (~)0.524s31.01x
🐘 PostgresNext.js (Turbopack)37.941s (+0.8%)38.052s (~)0.110s21.39x
🐘 PostgresNitro50.373s (~)51.096s (~)0.722s21.85x
🐘 PostgresExpress50.422s (~)51.076s (~)0.654s21.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express58.799s (+2.8%)60.259s (+4.0%)1.460s11.00x
▲ VercelNitro59.008s (+3.9%)59.284s (+3.6%)0.275s21.00x
▲ VercelNext.js (Turbopack)59.231s (+2.7%)59.787s (+2.4%)0.556s21.01x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)56.526s (~)57.053s (~)0.527s21.00x
💻 LocalNitro57.216s (~)58.049s (+0.9%)0.833s21.01x
💻 LocalExpress57.319s (~)58.040s (~)0.721s21.01x
🐘 PostgresNext.js (Turbopack)74.784s (-5.7% 🟢)75.602s (-5.0% 🟢)0.819s21.32x
🐘 PostgresNitro100.195s (~)101.169s (~)0.974s11.77x
🐘 PostgresExpress100.271s (~)101.172s (~)0.901s11.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro124.409s (+2.9%)125.651s (+2.6%)1.242s11.00x
▲ VercelNext.js (Turbopack)126.724s (-2.6%)127.329s (-2.5%)0.605s11.02x
▲ VercelExpress160.313s (+31.1% 🔺)160.881s (+30.5% 🔺)0.568s11.29x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.397s (~)2.006s (~)0.609s151.00x
💻 LocalNext.js (Turbopack)1.410s (+1.3%)2.012s (~)0.603s151.01x
💻 LocalExpress1.436s (+1.3%)2.007s (~)0.571s151.03x
🐘 PostgresNitro2.319s (-1.7%)3.014s (~)0.695s101.66x
🐘 PostgresNext.js (Turbopack)2.364s (+25.8% 🔺)2.744s (+8.8% 🔺)0.381s111.69x
🐘 PostgresExpress2.366s (-1.0%)3.013s (~)0.647s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.028s (+6.5% 🔺)3.878s (+4.9%)0.850s81.00x
▲ VercelNext.js (Turbopack)3.062s (+1.5%)3.920s (-1.9%)0.858s81.01x
▲ VercelExpress3.345s (+11.3% 🔺)4.184s (+7.4% 🔺)0.839s81.10x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.553s (-1.2%)3.047s (+0.7%)0.494s101.00x
💻 LocalNitro2.577s (+3.0%)3.017s (~)0.440s101.01x
💻 LocalExpress2.633s (+1.4%)3.022s (~)0.389s101.03x
🐘 PostgresExpress7.829s (-8.7% 🟢)8.578s (-8.0% 🟢)0.749s43.07x
🐘 PostgresNitro7.987s (+1.6%)8.594s (+0.5%)0.607s43.13x
🐘 PostgresNext.js (Turbopack)12.839s (+11.5% 🔺)13.360s (+11.1% 🔺)0.522s35.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.202s (+0.7%)3.973s (+3.3%)0.770s81.00x
▲ VercelNitro3.641s (+18.1% 🔺)4.390s (+14.8% 🔺)0.748s71.14x
▲ VercelNext.js (Turbopack)3.721s (-3.8%)4.425s (-4.6%)0.704s71.16x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)7.559s (+2.3%)8.274s (-2.2%)0.715s41.00x
💻 LocalExpress7.559s (+4.0%)8.515s (+3.1%)0.956s41.00x
💻 LocalNitro7.582s (+8.8% 🔺)8.529s (+13.7% 🔺)0.946s41.00x
🐘 PostgresNitro49.993s (+10.7% 🔺)50.305s (+8.9% 🔺)0.312s16.61x
🐘 PostgresExpress51.058s (+4.2%)51.328s (+4.0%)0.270s16.75x
🐘 PostgresNext.js (Turbopack)55.261s (+8.2% 🔺)56.160s (+9.6% 🔺)0.899s17.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.388s (-17.2% 🟢)4.017s (-12.6% 🟢)0.629s81.00x
▲ VercelExpress3.464s (+3.6%)4.103s (+8.6% 🔺)0.639s81.02x
▲ VercelNext.js (Turbopack)3.996s (+6.8% 🔺)5.070s (+14.2% 🔺)1.074s61.18x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.406s (-1.9%)2.009s (~)0.603s151.00x
💻 LocalExpress1.468s (+2.8%)2.008s (~)0.541s151.04x
💻 LocalNitro1.468s (+1.5%)2.007s (~)0.538s151.04x
🐘 PostgresNitro2.169s (-5.7% 🟢)2.598s (-3.0%)0.429s121.54x
🐘 PostgresExpress2.216s (+5.5% 🔺)2.598s (+3.2%)0.382s121.58x
🐘 PostgresNext.js (Turbopack)2.274s (+26.6% 🔺)2.755s (+36.9% 🔺)0.481s111.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.763s (~)3.700s (~)0.937s91.00x
▲ VercelNext.js (Turbopack)2.913s (+1.4%)3.795s (-0.7%)0.883s81.05x
▲ VercelExpress2.986s (+4.9%)3.868s (+2.8%)0.882s81.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.663s (+1.8%)3.010s (~)0.347s101.00x
💻 LocalNext.js (Turbopack)2.674s (-0.5%)3.034s (~)0.359s101.00x
💻 LocalExpress2.691s (~)3.012s (~)0.321s101.01x
🐘 PostgresExpress11.815s (+8.9% 🔺)12.361s (+11.5% 🔺)0.546s34.44x
🐘 PostgresNitro12.124s (+12.5% 🔺)12.721s (+15.2% 🔺)0.597s34.55x
🐘 PostgresNext.js (Turbopack)13.147s (+19.7% 🔺)13.723s (+17.4% 🔺)0.576s34.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.906s (-1.7%)3.716s (+1.8%)0.810s91.00x
▲ VercelNitro3.044s (-3.0%)3.719s (-2.6%)0.676s91.05x
▲ VercelNext.js (Turbopack)3.066s (+1.2%)3.739s (-1.9%)0.674s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express7.910s (+1.1%)8.661s (-1.7%)0.751s41.00x
💻 LocalNitro7.971s (+4.9%)8.877s (+3.3%)0.906s41.01x
💻 LocalNext.js (Turbopack)8.245s (+7.8% 🔺)9.068s (+5.2% 🔺)0.823s41.04x
🐘 PostgresNitro51.279s (~)52.131s (~)0.852s16.48x
🐘 PostgresExpress51.446s (~)52.249s (~)0.803s16.50x
🐘 PostgresNext.js (Turbopack)56.929s (+4.7%)57.297s (+3.9%)0.368s17.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.257s (+5.0%)3.868s (+2.6%)0.611s81.00x
▲ VercelExpress3.722s (+11.2% 🔺)4.332s (+10.0% 🔺)0.610s81.14x
▲ VercelNext.js (Turbopack)4.022s (+13.3% 🔺)4.880s (+12.5% 🔺)0.857s71.24x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.145s (-1.4%)1.003s (~)0.016s (-8.0% 🟢)1.028s (~)0.883s101.00x
💻 LocalNitro0.182s (+2.6%)0.992s (~)0.015s (+7.3% 🔺)1.021s (~)0.839s101.26x
💻 LocalExpress0.185s (-1.4%)0.992s (~)0.014s (-4.2%)1.021s (~)0.836s101.28x
🐘 PostgresNext.js (Turbopack)0.751s (+5.0%)0.856s (-12.4% 🟢)0.000s (NaN%)1.017s (-8.8% 🟢)0.266s105.19x
🐘 PostgresNitro2.375s (+59.7% 🔺)2.673s (+61.2% 🔺)0.000s (+Infinity% 🔺)3.018s (+50.0% 🔺)0.643s1016.41x
🐘 PostgresExpress2.428s (~)2.618s (~)0.000s (+Infinity% 🔺)3.017s (~)0.589s1016.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.068s (~)3.350s (-1.4%)0.229s (+60.6% 🔺)4.071s (+1.1%)1.003s101.00x
▲ VercelNext.js (Turbopack)3.238s (+4.7%)3.502s (+6.7% 🔺)0.270s (+46.9% 🔺)4.319s (+8.5% 🔺)1.082s101.06x
▲ VercelExpress3.349s (+9.3% 🔺)3.477s (+3.5%)0.188s (-11.9% 🟢)4.230s (+3.9%)0.881s101.09x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNitro5/12
▲ VercelNitro8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)💻 Local10/12
Nitro💻 Local10/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a caching bug in the module specifier resolution system and adds support for package subpath exports in workflow IDs. The changes improve the reliability of cross-bundle references and enable proper handling of packages with multiple entry points.

Changes:

  • Fixed module specifier cache back-filling to prevent incorrect IDs across multiple lookups
  • Added subpath export resolution to support packages like workflow/internal/builtins@4.0.0
  • Enhanced workspace package detection to filter out sibling apps in monorepos based on project dependencies
  • Updated all builders to properly merge manifests from both workflow and step bundles
  • Added Windows path normalization support in the Rust transform plugin
  • Passed absolute file paths to SWC transform for accurate module specifier resolution

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
packages/builders/src/module-specifier.tsCore changes: cache back-filling, subpath export resolution, workspace package detection improvements
packages/builders/src/apply-swc-transform.tsAdded absolutePath parameter for accurate module specifier resolution
packages/builders/src/swc-esbuild-plugin.tsPasses absolute path to transform function
packages/builders/src/base-builder.tsUpdated createWorkflowsBundle return type to include manifest
packages/sveltekit/src/builder.tsUpdated to merge manifests from both bundles
packages/nitro/src/builders.tsUpdated to merge manifests from both bundles
packages/next/src/builder.tsUpdated to merge manifests and handle optional context properties
packages/nest/src/builder.tsUpdated to merge manifests from both bundles
packages/builders/src/vercel-build-output-api.tsUpdated to merge manifests from both bundles
packages/builders/src/standalone.tsAdded mergeManifests helper and updated bundle methods
packages/swc-plugin-workflow/transform/src/naming.rsAdded Windows path normalization with tests
packages/swc-plugin-workflow/transform/src/lib.rsEnhanced builtin function comment documentation
packages/swc-plugin-workflow/spec.mdUpdated documentation for subpath exports
.changeset/afraid-candies-find.mdAdded changeset entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 37 to 327
@@ -69,9 +176,56 @@ function isInNodeModules(filePath: string): boolean {
}

/**
* Check if a file path is inside a workspace package.
* Cache for project dependencies to avoid repeated filesystem reads.
* Maps project root to set of dependency package names.
*/
const projectDepsCache = new Map<string, Set<string>>();

/**
* Get all dependencies (including devDependencies) for a project.
*/
function getProjectDependencies(projectRoot: string): Set<string> {
const cached = projectDepsCache.get(projectRoot);
if (cached) {
return cached;
}

const deps = new Set<string>();
const pkgPath = join(projectRoot, 'package.json');

if (existsSync(pkgPath)) {
try {
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content);

// Collect all dependency types
for (const depType of [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
]) {
const depObj = parsed[depType];
if (depObj && typeof depObj === 'object') {
for (const name of Object.keys(depObj)) {
deps.add(name);
}
}
}
} catch {
// Invalid JSON or file not readable
}
}

projectDepsCache.set(projectRoot, deps);
return deps;
}

/**
* Check if a file path is inside a workspace package that is a dependency of the project.
* This is a heuristic - we check if the file is in a directory with a package.json
* that has a "name" field, but is NOT in node_modules.
* that has a "name" field, is NOT in node_modules, and is listed as a dependency
* of the project.
*/
function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (isInNodeModules(filePath)) {
@@ -97,8 +251,13 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (resolve(pkgPath) === rootPkgPath) {
return false;
}
// Found a package.json that's not the root - it's a workspace package
return true;

// Found a package.json that's not the root.
// Only treat it as a workspace package if it's actually a dependency
// of the current project. This prevents sibling apps in a monorepo
// from being incorrectly treated as importable packages.
const projectDeps = getProjectDependencies(projectRoot);
return projectDeps.has(pkg.name);
}
dir = dirname(dir);
}
@@ -114,11 +273,16 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
* @returns The module specifier result
*
* @example
* // File in node_modules
* // File in node_modules (root export)
* resolveModuleSpecifier('/project/node_modules/point/dist/index.js', '/project')
* // => { moduleSpecifier: 'point@1.0.0' }
*
* @example
* // File in node_modules (subpath export)
* resolveModuleSpecifier('/project/node_modules/workflow/dist/internal/builtins.js', '/project')
* // => { moduleSpecifier: 'workflow/internal/builtins@4.0.0' }
*
* @example
* // File in workspace package
* resolveModuleSpecifier('/project/packages/shared/src/utils.ts', '/project')
* // => { moduleSpecifier: '@myorg/shared@0.0.0' }
@@ -149,9 +313,16 @@ export function resolveModuleSpecifier(
return { moduleSpecifier: undefined };
}

// Return the module specifier as "name@version"
// Resolve the export subpath (e.g., "/internal/builtins" for "workflow/internal/builtins")
const subpath = resolveExportSubpath(filePath, pkg);

// Return the module specifier as "name/subpath@version" or "name@version"
const specifier = subpath
? `${pkg.name}${subpath}@${pkg.version}`
: `${pkg.name}@${pkg.version}`;

return {
moduleSpecifier: `${pkg.name}@${pkg.version}`,
moduleSpecifier: specifier,
};
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new module specifier resolution logic, including the caching improvements, workspace package detection based on project dependencies, and subpath export resolution, lacks test coverage. Consider adding comprehensive tests to verify:

  1. The cache back-filling logic works correctly for nested directories
  2. Subpath export resolution correctly handles various export configurations (conditional exports, nested subpaths, etc.)
  3. The workspace package detection correctly filters out sibling apps in a monorepo
  4. The cache is properly invalidated when needed

Tests would help prevent regressions and document the expected behavior of these critical features.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +66
// Merge manifests from both bundles
const manifest = {
steps: { ...stepsManifest.steps, ...workflowsManifest.steps },
workflows: { ...stepsManifest.workflows, ...workflowsManifest.workflows },
classes: { ...stepsManifest.classes, ...workflowsManifest.classes },
};

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest merging order could potentially cause data loss if there are conflicting keys between the stepsManifest and workflowsManifest. The current implementation uses object spread syntax where workflowsManifest properties overwrite stepsManifest properties with the same key.

While this might be intentional, consider:

  1. Adding validation to detect and warn about conflicting IDs
  2. Documenting which manifest takes precedence and why
  3. Ensuring that the merge order is consistent across all builders (currently stepsManifest comes first in all implementations)

If steps and workflows are guaranteed to have non-overlapping IDs by design, this concern can be disregarded, but it would be helpful to document this assumption.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +130
function resolveExportSubpath(filePath: string, pkg: PackageInfo): string {
if (!pkg.exports || typeof pkg.exports !== 'object') {
return '';
}

// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

// Search through exports to find a matching subpath
for (const [subpath, target] of Object.entries(pkg.exports)) {
const resolvedTarget = resolveExportTarget(target);
if (
resolvedTarget &&
normalizeExportPath(resolvedTarget) === relativePath
) {
// Found a match - return the subpath without the leading "."
// e.g., "./internal/builtins" -> "/internal/builtins"
return subpath === '.' ? '' : subpath.substring(1);
}
}

return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function only handles exact export path matches and doesn't support wildcard patterns (e.g., "./internal/": "./dist/internal/.js"). This could lead to incorrect or missing subpath resolution for packages that use wildcard exports. Consider adding support for wildcard patterns by:

  1. Checking if the subpath contains an asterisk
  2. Converting it to a pattern match against the relative file path
  3. Extracting the matched portion to construct the correct subpath

If wildcard exports are not expected to be used in this codebase, this concern can be disregarded.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +114
// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function checks if the normalized file path starts with the package directory plus a forward slash (normalizedPkgDir + '/'). This check will fail for files that are exactly at the package root (e.g., when filePath equals pkg.dir), causing the function to return an empty string even though it should potentially match the root export (".").

While files at the exact package root are uncommon, consider handling this edge case explicitly:

  • Check if normalizedFilePath equals normalizedPkgDir and return the root export logic
  • Or adjust the substring logic to handle this case

This may not be a practical issue if package entry points are always in subdirectories.

Copilot uses AI. Check for mistakes.
Comment threadpackages/builders/src/module-specifier.ts
Comment threadpackages/builders/src/module-specifier.ts
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Addressed the following review comments in commit fce2e7d:

  1. Redundant package.json lookup in isWorkspacePackage - Now using pkg.dir directly from the findPackageJson result instead of walking up the directory tree again.

  2. Array exports support in resolveExportTarget - Added support for array exports by checking Array.isArray(target) and recursively resolving each element in the fallback chain, per the Node.js package exports specification.

… exports support
- Remove redundant directory walk in isWorkspacePackage by using pkg.dir from findPackageJson
- Add support for array exports in resolveExportTarget (fallback chains per Node.js spec)

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced manifest merging to properly combine results from both workflow and step bundles

Out of curiosity, what does this fix? "Enhance" sounds nice but doesn't tell me anything

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@VaguelySerious The step and workflow bundles generate their own manifests, which may have some values that do not overlap. For example, if I use a class only in step functions, but never reference it in the workflow bundle, then it would be missing from the workflow manifest. The fix was to ensure that all bundles are merged in the final form.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add subpath export resolution for package IDs by TooTallNate · Pull Request #901 · vercel/workflow · GitHub
Skip to content

Add subpath export resolution for package IDs - #901

Merged
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids
Feb 5, 2026
Merged

Add subpath export resolution for package IDs#901
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 30, 2026

Copy link
Copy Markdown
Member

Fixed a bug in module specifier resolution and added support for package subpath exports in workflow IDs.

What changed?

  • Fixed a caching bug in the module specifier resolution system that could cause incorrect IDs
  • Added support for subpath exports in package IDs (e.g., workflow/internal/builtins@4.0.0)
  • Improved module resolution by passing absolute file paths to the SWC transform
  • Enhanced manifest merging to properly combine results from both workflow and step bundles
  • Updated builders to return and merge manifests from both workflow and step bundles

How to test?

  1. Build a project that uses subpath exports in packages
  2. Verify that workflow IDs correctly include the subpath (e.g., workflow/internal/builtins@4.0.0)
  3. Test with a project that has multiple builds to ensure module specifier caching works correctly

Why make this change?

This change addresses an issue where the module specifier cache could return incorrect results, leading to inconsistent workflow IDs. It also adds support for packages with multiple entry points through subpath exports, ensuring that steps with the same name in different subpaths don't collide. This improves the reliability of cross-bundle references and makes the system more robust when working with complex package structures.

@changeset-bot

changeset-botBot commented Jan 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b81ae6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/sveltekitPatch
@workflow/buildersPatch
@workflow/nitroPatch
@workflow/nestPatch
@workflow/nextPatch
workflow-devkit-compiler-playgroundPatch
@workflow/astroPatch
@workflow/cliPatch
@workflow/rollupPatch
workflowPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/docs-typecheckPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production479038517
✅ 💻 Local Development438032470
✅ 📦 Local Production438032470
✅ 🐘 Local Postgres438032470
✅ 🪟 Windows470047
❌ 🌍 Community Worlds311690200
✅ 📋 Other129012141
Total20001691462315

❌ Failed Tests

🌍 Community Worlds (169 failed)

mongodb (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4304
✅ example4304
✅ express4304
✅ fastify4304
✅ hono4304
✅ nextjs-turbopack4601
✅ nextjs-webpack4601
✅ nitro4304
✅ nuxt4304
✅ sveltekit4304
✅ vite4304
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb5420
✅ redis-dev300
❌ redis5420
✅ starter-dev300
❌ starter4430
✅ turso-dev300
❌ turso5420
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4304
✅ e2e-local-postgres-nest-stable4304
✅ e2e-local-prod-nest-stable4304

📋 View full workflow run

@TooTallNate
TooTallNateforce-pushed the 01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids branch from 6cce073 to ca8bd01CompareJanuary 30, 2026 19:57
@github-actions

github-actionsBot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.041s (+3.5%)1.019s (~)0.978s101.00x
💻 LocalNitro0.043s (~)1.007s (~)0.964s101.06x
💻 LocalExpress0.044s (+4.8%)1.007s (~)0.963s101.08x
🐘 PostgresNitro0.212s (-6.9% 🟢)1.015s (~)0.803s105.19x
🐘 PostgresExpress0.290s (+4.2%)1.016s (~)0.726s107.10x
🐘 PostgresNext.js (Turbopack)0.360s (+28.8% 🔺)1.021s (~)0.660s108.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.712s (+6.4% 🔺)1.582s (-0.8%)0.870s101.00x
▲ VercelExpress0.793s (+7.0% 🔺)1.607s (~)0.814s101.11x
▲ VercelNext.js (Turbopack)0.870s (-1.1%)1.721s (-12.0% 🟢)0.851s101.22x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.094s (-0.7%)2.014s (~)0.920s101.00x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
💻 LocalExpress1.117s (~)2.007s (~)0.890s101.02x
🐘 PostgresNext.js (Turbopack)1.858s (+0.5%)2.420s (+20.1% 🔺)0.562s101.70x
🐘 PostgresExpress2.133s (-1.2%)3.014s (~)0.881s101.95x
🐘 PostgresNitro2.258s (+3.1%)3.017s (~)0.759s102.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.842s (-1.3%)3.742s (+1.1%)0.900s101.00x
▲ VercelExpress2.933s (~)3.724s (+4.6%)0.791s101.03x
▲ VercelNext.js (Turbopack)2.969s (+2.0%)3.755s (-1.8%)0.786s101.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.750s (~)11.018s (~)0.268s31.00x
💻 LocalExpress10.837s (~)11.012s (~)0.175s31.01x
💻 LocalNitro10.849s (~)11.010s (~)0.162s31.01x
🐘 PostgresNext.js (Turbopack)15.074s (-1.0%)16.033s (~)0.959s21.40x
🐘 PostgresNitro20.454s (~)21.038s (~)0.583s21.90x
🐘 PostgresExpress20.532s (~)21.032s (~)0.500s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)39.852s (+75.5% 🔺)40.274s (+71.8% 🔺)0.422s11.00x
▲ VercelNitro42.916s (+89.8% 🔺)43.893s (+88.1% 🔺)0.977s21.08x
▲ VercelExpress45.343s (+94.4% 🔺)46.170s (+88.5% 🔺)0.827s21.14x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)27.245s (~)28.038s (~)0.793s31.00x
💻 LocalNitro27.465s (~)28.022s (~)0.558s31.01x
💻 LocalExpress27.501s (~)28.025s (~)0.524s31.01x
🐘 PostgresNext.js (Turbopack)37.941s (+0.8%)38.052s (~)0.110s21.39x
🐘 PostgresNitro50.373s (~)51.096s (~)0.722s21.85x
🐘 PostgresExpress50.422s (~)51.076s (~)0.654s21.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express58.799s (+2.8%)60.259s (+4.0%)1.460s11.00x
▲ VercelNitro59.008s (+3.9%)59.284s (+3.6%)0.275s21.00x
▲ VercelNext.js (Turbopack)59.231s (+2.7%)59.787s (+2.4%)0.556s21.01x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)56.526s (~)57.053s (~)0.527s21.00x
💻 LocalNitro57.216s (~)58.049s (+0.9%)0.833s21.01x
💻 LocalExpress57.319s (~)58.040s (~)0.721s21.01x
🐘 PostgresNext.js (Turbopack)74.784s (-5.7% 🟢)75.602s (-5.0% 🟢)0.819s21.32x
🐘 PostgresNitro100.195s (~)101.169s (~)0.974s11.77x
🐘 PostgresExpress100.271s (~)101.172s (~)0.901s11.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro124.409s (+2.9%)125.651s (+2.6%)1.242s11.00x
▲ VercelNext.js (Turbopack)126.724s (-2.6%)127.329s (-2.5%)0.605s11.02x
▲ VercelExpress160.313s (+31.1% 🔺)160.881s (+30.5% 🔺)0.568s11.29x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.397s (~)2.006s (~)0.609s151.00x
💻 LocalNext.js (Turbopack)1.410s (+1.3%)2.012s (~)0.603s151.01x
💻 LocalExpress1.436s (+1.3%)2.007s (~)0.571s151.03x
🐘 PostgresNitro2.319s (-1.7%)3.014s (~)0.695s101.66x
🐘 PostgresNext.js (Turbopack)2.364s (+25.8% 🔺)2.744s (+8.8% 🔺)0.381s111.69x
🐘 PostgresExpress2.366s (-1.0%)3.013s (~)0.647s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.028s (+6.5% 🔺)3.878s (+4.9%)0.850s81.00x
▲ VercelNext.js (Turbopack)3.062s (+1.5%)3.920s (-1.9%)0.858s81.01x
▲ VercelExpress3.345s (+11.3% 🔺)4.184s (+7.4% 🔺)0.839s81.10x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.553s (-1.2%)3.047s (+0.7%)0.494s101.00x
💻 LocalNitro2.577s (+3.0%)3.017s (~)0.440s101.01x
💻 LocalExpress2.633s (+1.4%)3.022s (~)0.389s101.03x
🐘 PostgresExpress7.829s (-8.7% 🟢)8.578s (-8.0% 🟢)0.749s43.07x
🐘 PostgresNitro7.987s (+1.6%)8.594s (+0.5%)0.607s43.13x
🐘 PostgresNext.js (Turbopack)12.839s (+11.5% 🔺)13.360s (+11.1% 🔺)0.522s35.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.202s (+0.7%)3.973s (+3.3%)0.770s81.00x
▲ VercelNitro3.641s (+18.1% 🔺)4.390s (+14.8% 🔺)0.748s71.14x
▲ VercelNext.js (Turbopack)3.721s (-3.8%)4.425s (-4.6%)0.704s71.16x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)7.559s (+2.3%)8.274s (-2.2%)0.715s41.00x
💻 LocalExpress7.559s (+4.0%)8.515s (+3.1%)0.956s41.00x
💻 LocalNitro7.582s (+8.8% 🔺)8.529s (+13.7% 🔺)0.946s41.00x
🐘 PostgresNitro49.993s (+10.7% 🔺)50.305s (+8.9% 🔺)0.312s16.61x
🐘 PostgresExpress51.058s (+4.2%)51.328s (+4.0%)0.270s16.75x
🐘 PostgresNext.js (Turbopack)55.261s (+8.2% 🔺)56.160s (+9.6% 🔺)0.899s17.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.388s (-17.2% 🟢)4.017s (-12.6% 🟢)0.629s81.00x
▲ VercelExpress3.464s (+3.6%)4.103s (+8.6% 🔺)0.639s81.02x
▲ VercelNext.js (Turbopack)3.996s (+6.8% 🔺)5.070s (+14.2% 🔺)1.074s61.18x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.406s (-1.9%)2.009s (~)0.603s151.00x
💻 LocalExpress1.468s (+2.8%)2.008s (~)0.541s151.04x
💻 LocalNitro1.468s (+1.5%)2.007s (~)0.538s151.04x
🐘 PostgresNitro2.169s (-5.7% 🟢)2.598s (-3.0%)0.429s121.54x
🐘 PostgresExpress2.216s (+5.5% 🔺)2.598s (+3.2%)0.382s121.58x
🐘 PostgresNext.js (Turbopack)2.274s (+26.6% 🔺)2.755s (+36.9% 🔺)0.481s111.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.763s (~)3.700s (~)0.937s91.00x
▲ VercelNext.js (Turbopack)2.913s (+1.4%)3.795s (-0.7%)0.883s81.05x
▲ VercelExpress2.986s (+4.9%)3.868s (+2.8%)0.882s81.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.663s (+1.8%)3.010s (~)0.347s101.00x
💻 LocalNext.js (Turbopack)2.674s (-0.5%)3.034s (~)0.359s101.00x
💻 LocalExpress2.691s (~)3.012s (~)0.321s101.01x
🐘 PostgresExpress11.815s (+8.9% 🔺)12.361s (+11.5% 🔺)0.546s34.44x
🐘 PostgresNitro12.124s (+12.5% 🔺)12.721s (+15.2% 🔺)0.597s34.55x
🐘 PostgresNext.js (Turbopack)13.147s (+19.7% 🔺)13.723s (+17.4% 🔺)0.576s34.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.906s (-1.7%)3.716s (+1.8%)0.810s91.00x
▲ VercelNitro3.044s (-3.0%)3.719s (-2.6%)0.676s91.05x
▲ VercelNext.js (Turbopack)3.066s (+1.2%)3.739s (-1.9%)0.674s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express7.910s (+1.1%)8.661s (-1.7%)0.751s41.00x
💻 LocalNitro7.971s (+4.9%)8.877s (+3.3%)0.906s41.01x
💻 LocalNext.js (Turbopack)8.245s (+7.8% 🔺)9.068s (+5.2% 🔺)0.823s41.04x
🐘 PostgresNitro51.279s (~)52.131s (~)0.852s16.48x
🐘 PostgresExpress51.446s (~)52.249s (~)0.803s16.50x
🐘 PostgresNext.js (Turbopack)56.929s (+4.7%)57.297s (+3.9%)0.368s17.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.257s (+5.0%)3.868s (+2.6%)0.611s81.00x
▲ VercelExpress3.722s (+11.2% 🔺)4.332s (+10.0% 🔺)0.610s81.14x
▲ VercelNext.js (Turbopack)4.022s (+13.3% 🔺)4.880s (+12.5% 🔺)0.857s71.24x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.145s (-1.4%)1.003s (~)0.016s (-8.0% 🟢)1.028s (~)0.883s101.00x
💻 LocalNitro0.182s (+2.6%)0.992s (~)0.015s (+7.3% 🔺)1.021s (~)0.839s101.26x
💻 LocalExpress0.185s (-1.4%)0.992s (~)0.014s (-4.2%)1.021s (~)0.836s101.28x
🐘 PostgresNext.js (Turbopack)0.751s (+5.0%)0.856s (-12.4% 🟢)0.000s (NaN%)1.017s (-8.8% 🟢)0.266s105.19x
🐘 PostgresNitro2.375s (+59.7% 🔺)2.673s (+61.2% 🔺)0.000s (+Infinity% 🔺)3.018s (+50.0% 🔺)0.643s1016.41x
🐘 PostgresExpress2.428s (~)2.618s (~)0.000s (+Infinity% 🔺)3.017s (~)0.589s1016.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.068s (~)3.350s (-1.4%)0.229s (+60.6% 🔺)4.071s (+1.1%)1.003s101.00x
▲ VercelNext.js (Turbopack)3.238s (+4.7%)3.502s (+6.7% 🔺)0.270s (+46.9% 🔺)4.319s (+8.5% 🔺)1.082s101.06x
▲ VercelExpress3.349s (+9.3% 🔺)3.477s (+3.5%)0.188s (-11.9% 🟢)4.230s (+3.9%)0.881s101.09x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNitro5/12
▲ VercelNitro8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)💻 Local10/12
Nitro💻 Local10/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a caching bug in the module specifier resolution system and adds support for package subpath exports in workflow IDs. The changes improve the reliability of cross-bundle references and enable proper handling of packages with multiple entry points.

Changes:

  • Fixed module specifier cache back-filling to prevent incorrect IDs across multiple lookups
  • Added subpath export resolution to support packages like workflow/internal/builtins@4.0.0
  • Enhanced workspace package detection to filter out sibling apps in monorepos based on project dependencies
  • Updated all builders to properly merge manifests from both workflow and step bundles
  • Added Windows path normalization support in the Rust transform plugin
  • Passed absolute file paths to SWC transform for accurate module specifier resolution

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
packages/builders/src/module-specifier.tsCore changes: cache back-filling, subpath export resolution, workspace package detection improvements
packages/builders/src/apply-swc-transform.tsAdded absolutePath parameter for accurate module specifier resolution
packages/builders/src/swc-esbuild-plugin.tsPasses absolute path to transform function
packages/builders/src/base-builder.tsUpdated createWorkflowsBundle return type to include manifest
packages/sveltekit/src/builder.tsUpdated to merge manifests from both bundles
packages/nitro/src/builders.tsUpdated to merge manifests from both bundles
packages/next/src/builder.tsUpdated to merge manifests and handle optional context properties
packages/nest/src/builder.tsUpdated to merge manifests from both bundles
packages/builders/src/vercel-build-output-api.tsUpdated to merge manifests from both bundles
packages/builders/src/standalone.tsAdded mergeManifests helper and updated bundle methods
packages/swc-plugin-workflow/transform/src/naming.rsAdded Windows path normalization with tests
packages/swc-plugin-workflow/transform/src/lib.rsEnhanced builtin function comment documentation
packages/swc-plugin-workflow/spec.mdUpdated documentation for subpath exports
.changeset/afraid-candies-find.mdAdded changeset entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 37 to 327
@@ -69,9 +176,56 @@ function isInNodeModules(filePath: string): boolean {
}

/**
* Check if a file path is inside a workspace package.
* Cache for project dependencies to avoid repeated filesystem reads.
* Maps project root to set of dependency package names.
*/
const projectDepsCache = new Map<string, Set<string>>();

/**
* Get all dependencies (including devDependencies) for a project.
*/
function getProjectDependencies(projectRoot: string): Set<string> {
const cached = projectDepsCache.get(projectRoot);
if (cached) {
return cached;
}

const deps = new Set<string>();
const pkgPath = join(projectRoot, 'package.json');

if (existsSync(pkgPath)) {
try {
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content);

// Collect all dependency types
for (const depType of [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
]) {
const depObj = parsed[depType];
if (depObj && typeof depObj === 'object') {
for (const name of Object.keys(depObj)) {
deps.add(name);
}
}
}
} catch {
// Invalid JSON or file not readable
}
}

projectDepsCache.set(projectRoot, deps);
return deps;
}

/**
* Check if a file path is inside a workspace package that is a dependency of the project.
* This is a heuristic - we check if the file is in a directory with a package.json
* that has a "name" field, but is NOT in node_modules.
* that has a "name" field, is NOT in node_modules, and is listed as a dependency
* of the project.
*/
function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (isInNodeModules(filePath)) {
@@ -97,8 +251,13 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (resolve(pkgPath) === rootPkgPath) {
return false;
}
// Found a package.json that's not the root - it's a workspace package
return true;

// Found a package.json that's not the root.
// Only treat it as a workspace package if it's actually a dependency
// of the current project. This prevents sibling apps in a monorepo
// from being incorrectly treated as importable packages.
const projectDeps = getProjectDependencies(projectRoot);
return projectDeps.has(pkg.name);
}
dir = dirname(dir);
}
@@ -114,11 +273,16 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
* @returns The module specifier result
*
* @example
* // File in node_modules
* // File in node_modules (root export)
* resolveModuleSpecifier('/project/node_modules/point/dist/index.js', '/project')
* // => { moduleSpecifier: 'point@1.0.0' }
*
* @example
* // File in node_modules (subpath export)
* resolveModuleSpecifier('/project/node_modules/workflow/dist/internal/builtins.js', '/project')
* // => { moduleSpecifier: 'workflow/internal/builtins@4.0.0' }
*
* @example
* // File in workspace package
* resolveModuleSpecifier('/project/packages/shared/src/utils.ts', '/project')
* // => { moduleSpecifier: '@myorg/shared@0.0.0' }
@@ -149,9 +313,16 @@ export function resolveModuleSpecifier(
return { moduleSpecifier: undefined };
}

// Return the module specifier as "name@version"
// Resolve the export subpath (e.g., "/internal/builtins" for "workflow/internal/builtins")
const subpath = resolveExportSubpath(filePath, pkg);

// Return the module specifier as "name/subpath@version" or "name@version"
const specifier = subpath
? `${pkg.name}${subpath}@${pkg.version}`
: `${pkg.name}@${pkg.version}`;

return {
moduleSpecifier: `${pkg.name}@${pkg.version}`,
moduleSpecifier: specifier,
};
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new module specifier resolution logic, including the caching improvements, workspace package detection based on project dependencies, and subpath export resolution, lacks test coverage. Consider adding comprehensive tests to verify:

  1. The cache back-filling logic works correctly for nested directories
  2. Subpath export resolution correctly handles various export configurations (conditional exports, nested subpaths, etc.)
  3. The workspace package detection correctly filters out sibling apps in a monorepo
  4. The cache is properly invalidated when needed

Tests would help prevent regressions and document the expected behavior of these critical features.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +66
// Merge manifests from both bundles
const manifest = {
steps: { ...stepsManifest.steps, ...workflowsManifest.steps },
workflows: { ...stepsManifest.workflows, ...workflowsManifest.workflows },
classes: { ...stepsManifest.classes, ...workflowsManifest.classes },
};

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest merging order could potentially cause data loss if there are conflicting keys between the stepsManifest and workflowsManifest. The current implementation uses object spread syntax where workflowsManifest properties overwrite stepsManifest properties with the same key.

While this might be intentional, consider:

  1. Adding validation to detect and warn about conflicting IDs
  2. Documenting which manifest takes precedence and why
  3. Ensuring that the merge order is consistent across all builders (currently stepsManifest comes first in all implementations)

If steps and workflows are guaranteed to have non-overlapping IDs by design, this concern can be disregarded, but it would be helpful to document this assumption.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +130
function resolveExportSubpath(filePath: string, pkg: PackageInfo): string {
if (!pkg.exports || typeof pkg.exports !== 'object') {
return '';
}

// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

// Search through exports to find a matching subpath
for (const [subpath, target] of Object.entries(pkg.exports)) {
const resolvedTarget = resolveExportTarget(target);
if (
resolvedTarget &&
normalizeExportPath(resolvedTarget) === relativePath
) {
// Found a match - return the subpath without the leading "."
// e.g., "./internal/builtins" -> "/internal/builtins"
return subpath === '.' ? '' : subpath.substring(1);
}
}

return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function only handles exact export path matches and doesn't support wildcard patterns (e.g., "./internal/": "./dist/internal/.js"). This could lead to incorrect or missing subpath resolution for packages that use wildcard exports. Consider adding support for wildcard patterns by:

  1. Checking if the subpath contains an asterisk
  2. Converting it to a pattern match against the relative file path
  3. Extracting the matched portion to construct the correct subpath

If wildcard exports are not expected to be used in this codebase, this concern can be disregarded.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +114
// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function checks if the normalized file path starts with the package directory plus a forward slash (normalizedPkgDir + '/'). This check will fail for files that are exactly at the package root (e.g., when filePath equals pkg.dir), causing the function to return an empty string even though it should potentially match the root export (".").

While files at the exact package root are uncommon, consider handling this edge case explicitly:

  • Check if normalizedFilePath equals normalizedPkgDir and return the root export logic
  • Or adjust the substring logic to handle this case

This may not be a practical issue if package entry points are always in subdirectories.

Copilot uses AI. Check for mistakes.
Comment threadpackages/builders/src/module-specifier.ts
Comment threadpackages/builders/src/module-specifier.ts
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Addressed the following review comments in commit fce2e7d:

  1. Redundant package.json lookup in isWorkspacePackage - Now using pkg.dir directly from the findPackageJson result instead of walking up the directory tree again.

  2. Array exports support in resolveExportTarget - Added support for array exports by checking Array.isArray(target) and recursively resolving each element in the fallback chain, per the Node.js package exports specification.

… exports support
- Remove redundant directory walk in isWorkspacePackage by using pkg.dir from findPackageJson
- Add support for array exports in resolveExportTarget (fallback chains per Node.js spec)

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced manifest merging to properly combine results from both workflow and step bundles

Out of curiosity, what does this fix? "Enhance" sounds nice but doesn't tell me anything

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@VaguelySerious The step and workflow bundles generate their own manifests, which may have some values that do not overlap. For example, if I use a class only in step functions, but never reference it in the workflow bundle, then it would be missing from the workflow manifest. The fix was to ensure that all bundles are merged in the final form.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add subpath export resolution for package IDs by TooTallNate · Pull Request #901 · vercel/workflow · GitHub
Skip to content

Add subpath export resolution for package IDs - #901

Merged
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids
Feb 5, 2026
Merged

Add subpath export resolution for package IDs#901
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 30, 2026

Copy link
Copy Markdown
Member

Fixed a bug in module specifier resolution and added support for package subpath exports in workflow IDs.

What changed?

  • Fixed a caching bug in the module specifier resolution system that could cause incorrect IDs
  • Added support for subpath exports in package IDs (e.g., workflow/internal/builtins@4.0.0)
  • Improved module resolution by passing absolute file paths to the SWC transform
  • Enhanced manifest merging to properly combine results from both workflow and step bundles
  • Updated builders to return and merge manifests from both workflow and step bundles

How to test?

  1. Build a project that uses subpath exports in packages
  2. Verify that workflow IDs correctly include the subpath (e.g., workflow/internal/builtins@4.0.0)
  3. Test with a project that has multiple builds to ensure module specifier caching works correctly

Why make this change?

This change addresses an issue where the module specifier cache could return incorrect results, leading to inconsistent workflow IDs. It also adds support for packages with multiple entry points through subpath exports, ensuring that steps with the same name in different subpaths don't collide. This improves the reliability of cross-bundle references and makes the system more robust when working with complex package structures.

@changeset-bot

changeset-botBot commented Jan 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b81ae6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/sveltekitPatch
@workflow/buildersPatch
@workflow/nitroPatch
@workflow/nestPatch
@workflow/nextPatch
workflow-devkit-compiler-playgroundPatch
@workflow/astroPatch
@workflow/cliPatch
@workflow/rollupPatch
workflowPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/docs-typecheckPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production479038517
✅ 💻 Local Development438032470
✅ 📦 Local Production438032470
✅ 🐘 Local Postgres438032470
✅ 🪟 Windows470047
❌ 🌍 Community Worlds311690200
✅ 📋 Other129012141
Total20001691462315

❌ Failed Tests

🌍 Community Worlds (169 failed)

mongodb (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4304
✅ example4304
✅ express4304
✅ fastify4304
✅ hono4304
✅ nextjs-turbopack4601
✅ nextjs-webpack4601
✅ nitro4304
✅ nuxt4304
✅ sveltekit4304
✅ vite4304
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb5420
✅ redis-dev300
❌ redis5420
✅ starter-dev300
❌ starter4430
✅ turso-dev300
❌ turso5420
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4304
✅ e2e-local-postgres-nest-stable4304
✅ e2e-local-prod-nest-stable4304

📋 View full workflow run

@TooTallNate
TooTallNateforce-pushed the 01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids branch from 6cce073 to ca8bd01CompareJanuary 30, 2026 19:57
@github-actions

github-actionsBot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.041s (+3.5%)1.019s (~)0.978s101.00x
💻 LocalNitro0.043s (~)1.007s (~)0.964s101.06x
💻 LocalExpress0.044s (+4.8%)1.007s (~)0.963s101.08x
🐘 PostgresNitro0.212s (-6.9% 🟢)1.015s (~)0.803s105.19x
🐘 PostgresExpress0.290s (+4.2%)1.016s (~)0.726s107.10x
🐘 PostgresNext.js (Turbopack)0.360s (+28.8% 🔺)1.021s (~)0.660s108.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.712s (+6.4% 🔺)1.582s (-0.8%)0.870s101.00x
▲ VercelExpress0.793s (+7.0% 🔺)1.607s (~)0.814s101.11x
▲ VercelNext.js (Turbopack)0.870s (-1.1%)1.721s (-12.0% 🟢)0.851s101.22x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.094s (-0.7%)2.014s (~)0.920s101.00x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
💻 LocalExpress1.117s (~)2.007s (~)0.890s101.02x
🐘 PostgresNext.js (Turbopack)1.858s (+0.5%)2.420s (+20.1% 🔺)0.562s101.70x
🐘 PostgresExpress2.133s (-1.2%)3.014s (~)0.881s101.95x
🐘 PostgresNitro2.258s (+3.1%)3.017s (~)0.759s102.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.842s (-1.3%)3.742s (+1.1%)0.900s101.00x
▲ VercelExpress2.933s (~)3.724s (+4.6%)0.791s101.03x
▲ VercelNext.js (Turbopack)2.969s (+2.0%)3.755s (-1.8%)0.786s101.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.750s (~)11.018s (~)0.268s31.00x
💻 LocalExpress10.837s (~)11.012s (~)0.175s31.01x
💻 LocalNitro10.849s (~)11.010s (~)0.162s31.01x
🐘 PostgresNext.js (Turbopack)15.074s (-1.0%)16.033s (~)0.959s21.40x
🐘 PostgresNitro20.454s (~)21.038s (~)0.583s21.90x
🐘 PostgresExpress20.532s (~)21.032s (~)0.500s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)39.852s (+75.5% 🔺)40.274s (+71.8% 🔺)0.422s11.00x
▲ VercelNitro42.916s (+89.8% 🔺)43.893s (+88.1% 🔺)0.977s21.08x
▲ VercelExpress45.343s (+94.4% 🔺)46.170s (+88.5% 🔺)0.827s21.14x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)27.245s (~)28.038s (~)0.793s31.00x
💻 LocalNitro27.465s (~)28.022s (~)0.558s31.01x
💻 LocalExpress27.501s (~)28.025s (~)0.524s31.01x
🐘 PostgresNext.js (Turbopack)37.941s (+0.8%)38.052s (~)0.110s21.39x
🐘 PostgresNitro50.373s (~)51.096s (~)0.722s21.85x
🐘 PostgresExpress50.422s (~)51.076s (~)0.654s21.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express58.799s (+2.8%)60.259s (+4.0%)1.460s11.00x
▲ VercelNitro59.008s (+3.9%)59.284s (+3.6%)0.275s21.00x
▲ VercelNext.js (Turbopack)59.231s (+2.7%)59.787s (+2.4%)0.556s21.01x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)56.526s (~)57.053s (~)0.527s21.00x
💻 LocalNitro57.216s (~)58.049s (+0.9%)0.833s21.01x
💻 LocalExpress57.319s (~)58.040s (~)0.721s21.01x
🐘 PostgresNext.js (Turbopack)74.784s (-5.7% 🟢)75.602s (-5.0% 🟢)0.819s21.32x
🐘 PostgresNitro100.195s (~)101.169s (~)0.974s11.77x
🐘 PostgresExpress100.271s (~)101.172s (~)0.901s11.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro124.409s (+2.9%)125.651s (+2.6%)1.242s11.00x
▲ VercelNext.js (Turbopack)126.724s (-2.6%)127.329s (-2.5%)0.605s11.02x
▲ VercelExpress160.313s (+31.1% 🔺)160.881s (+30.5% 🔺)0.568s11.29x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.397s (~)2.006s (~)0.609s151.00x
💻 LocalNext.js (Turbopack)1.410s (+1.3%)2.012s (~)0.603s151.01x
💻 LocalExpress1.436s (+1.3%)2.007s (~)0.571s151.03x
🐘 PostgresNitro2.319s (-1.7%)3.014s (~)0.695s101.66x
🐘 PostgresNext.js (Turbopack)2.364s (+25.8% 🔺)2.744s (+8.8% 🔺)0.381s111.69x
🐘 PostgresExpress2.366s (-1.0%)3.013s (~)0.647s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.028s (+6.5% 🔺)3.878s (+4.9%)0.850s81.00x
▲ VercelNext.js (Turbopack)3.062s (+1.5%)3.920s (-1.9%)0.858s81.01x
▲ VercelExpress3.345s (+11.3% 🔺)4.184s (+7.4% 🔺)0.839s81.10x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.553s (-1.2%)3.047s (+0.7%)0.494s101.00x
💻 LocalNitro2.577s (+3.0%)3.017s (~)0.440s101.01x
💻 LocalExpress2.633s (+1.4%)3.022s (~)0.389s101.03x
🐘 PostgresExpress7.829s (-8.7% 🟢)8.578s (-8.0% 🟢)0.749s43.07x
🐘 PostgresNitro7.987s (+1.6%)8.594s (+0.5%)0.607s43.13x
🐘 PostgresNext.js (Turbopack)12.839s (+11.5% 🔺)13.360s (+11.1% 🔺)0.522s35.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.202s (+0.7%)3.973s (+3.3%)0.770s81.00x
▲ VercelNitro3.641s (+18.1% 🔺)4.390s (+14.8% 🔺)0.748s71.14x
▲ VercelNext.js (Turbopack)3.721s (-3.8%)4.425s (-4.6%)0.704s71.16x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)7.559s (+2.3%)8.274s (-2.2%)0.715s41.00x
💻 LocalExpress7.559s (+4.0%)8.515s (+3.1%)0.956s41.00x
💻 LocalNitro7.582s (+8.8% 🔺)8.529s (+13.7% 🔺)0.946s41.00x
🐘 PostgresNitro49.993s (+10.7% 🔺)50.305s (+8.9% 🔺)0.312s16.61x
🐘 PostgresExpress51.058s (+4.2%)51.328s (+4.0%)0.270s16.75x
🐘 PostgresNext.js (Turbopack)55.261s (+8.2% 🔺)56.160s (+9.6% 🔺)0.899s17.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.388s (-17.2% 🟢)4.017s (-12.6% 🟢)0.629s81.00x
▲ VercelExpress3.464s (+3.6%)4.103s (+8.6% 🔺)0.639s81.02x
▲ VercelNext.js (Turbopack)3.996s (+6.8% 🔺)5.070s (+14.2% 🔺)1.074s61.18x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.406s (-1.9%)2.009s (~)0.603s151.00x
💻 LocalExpress1.468s (+2.8%)2.008s (~)0.541s151.04x
💻 LocalNitro1.468s (+1.5%)2.007s (~)0.538s151.04x
🐘 PostgresNitro2.169s (-5.7% 🟢)2.598s (-3.0%)0.429s121.54x
🐘 PostgresExpress2.216s (+5.5% 🔺)2.598s (+3.2%)0.382s121.58x
🐘 PostgresNext.js (Turbopack)2.274s (+26.6% 🔺)2.755s (+36.9% 🔺)0.481s111.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.763s (~)3.700s (~)0.937s91.00x
▲ VercelNext.js (Turbopack)2.913s (+1.4%)3.795s (-0.7%)0.883s81.05x
▲ VercelExpress2.986s (+4.9%)3.868s (+2.8%)0.882s81.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.663s (+1.8%)3.010s (~)0.347s101.00x
💻 LocalNext.js (Turbopack)2.674s (-0.5%)3.034s (~)0.359s101.00x
💻 LocalExpress2.691s (~)3.012s (~)0.321s101.01x
🐘 PostgresExpress11.815s (+8.9% 🔺)12.361s (+11.5% 🔺)0.546s34.44x
🐘 PostgresNitro12.124s (+12.5% 🔺)12.721s (+15.2% 🔺)0.597s34.55x
🐘 PostgresNext.js (Turbopack)13.147s (+19.7% 🔺)13.723s (+17.4% 🔺)0.576s34.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.906s (-1.7%)3.716s (+1.8%)0.810s91.00x
▲ VercelNitro3.044s (-3.0%)3.719s (-2.6%)0.676s91.05x
▲ VercelNext.js (Turbopack)3.066s (+1.2%)3.739s (-1.9%)0.674s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express7.910s (+1.1%)8.661s (-1.7%)0.751s41.00x
💻 LocalNitro7.971s (+4.9%)8.877s (+3.3%)0.906s41.01x
💻 LocalNext.js (Turbopack)8.245s (+7.8% 🔺)9.068s (+5.2% 🔺)0.823s41.04x
🐘 PostgresNitro51.279s (~)52.131s (~)0.852s16.48x
🐘 PostgresExpress51.446s (~)52.249s (~)0.803s16.50x
🐘 PostgresNext.js (Turbopack)56.929s (+4.7%)57.297s (+3.9%)0.368s17.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.257s (+5.0%)3.868s (+2.6%)0.611s81.00x
▲ VercelExpress3.722s (+11.2% 🔺)4.332s (+10.0% 🔺)0.610s81.14x
▲ VercelNext.js (Turbopack)4.022s (+13.3% 🔺)4.880s (+12.5% 🔺)0.857s71.24x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.145s (-1.4%)1.003s (~)0.016s (-8.0% 🟢)1.028s (~)0.883s101.00x
💻 LocalNitro0.182s (+2.6%)0.992s (~)0.015s (+7.3% 🔺)1.021s (~)0.839s101.26x
💻 LocalExpress0.185s (-1.4%)0.992s (~)0.014s (-4.2%)1.021s (~)0.836s101.28x
🐘 PostgresNext.js (Turbopack)0.751s (+5.0%)0.856s (-12.4% 🟢)0.000s (NaN%)1.017s (-8.8% 🟢)0.266s105.19x
🐘 PostgresNitro2.375s (+59.7% 🔺)2.673s (+61.2% 🔺)0.000s (+Infinity% 🔺)3.018s (+50.0% 🔺)0.643s1016.41x
🐘 PostgresExpress2.428s (~)2.618s (~)0.000s (+Infinity% 🔺)3.017s (~)0.589s1016.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.068s (~)3.350s (-1.4%)0.229s (+60.6% 🔺)4.071s (+1.1%)1.003s101.00x
▲ VercelNext.js (Turbopack)3.238s (+4.7%)3.502s (+6.7% 🔺)0.270s (+46.9% 🔺)4.319s (+8.5% 🔺)1.082s101.06x
▲ VercelExpress3.349s (+9.3% 🔺)3.477s (+3.5%)0.188s (-11.9% 🟢)4.230s (+3.9%)0.881s101.09x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNitro5/12
▲ VercelNitro8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)💻 Local10/12
Nitro💻 Local10/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a caching bug in the module specifier resolution system and adds support for package subpath exports in workflow IDs. The changes improve the reliability of cross-bundle references and enable proper handling of packages with multiple entry points.

Changes:

  • Fixed module specifier cache back-filling to prevent incorrect IDs across multiple lookups
  • Added subpath export resolution to support packages like workflow/internal/builtins@4.0.0
  • Enhanced workspace package detection to filter out sibling apps in monorepos based on project dependencies
  • Updated all builders to properly merge manifests from both workflow and step bundles
  • Added Windows path normalization support in the Rust transform plugin
  • Passed absolute file paths to SWC transform for accurate module specifier resolution

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
packages/builders/src/module-specifier.tsCore changes: cache back-filling, subpath export resolution, workspace package detection improvements
packages/builders/src/apply-swc-transform.tsAdded absolutePath parameter for accurate module specifier resolution
packages/builders/src/swc-esbuild-plugin.tsPasses absolute path to transform function
packages/builders/src/base-builder.tsUpdated createWorkflowsBundle return type to include manifest
packages/sveltekit/src/builder.tsUpdated to merge manifests from both bundles
packages/nitro/src/builders.tsUpdated to merge manifests from both bundles
packages/next/src/builder.tsUpdated to merge manifests and handle optional context properties
packages/nest/src/builder.tsUpdated to merge manifests from both bundles
packages/builders/src/vercel-build-output-api.tsUpdated to merge manifests from both bundles
packages/builders/src/standalone.tsAdded mergeManifests helper and updated bundle methods
packages/swc-plugin-workflow/transform/src/naming.rsAdded Windows path normalization with tests
packages/swc-plugin-workflow/transform/src/lib.rsEnhanced builtin function comment documentation
packages/swc-plugin-workflow/spec.mdUpdated documentation for subpath exports
.changeset/afraid-candies-find.mdAdded changeset entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 37 to 327
@@ -69,9 +176,56 @@ function isInNodeModules(filePath: string): boolean {
}

/**
* Check if a file path is inside a workspace package.
* Cache for project dependencies to avoid repeated filesystem reads.
* Maps project root to set of dependency package names.
*/
const projectDepsCache = new Map<string, Set<string>>();

/**
* Get all dependencies (including devDependencies) for a project.
*/
function getProjectDependencies(projectRoot: string): Set<string> {
const cached = projectDepsCache.get(projectRoot);
if (cached) {
return cached;
}

const deps = new Set<string>();
const pkgPath = join(projectRoot, 'package.json');

if (existsSync(pkgPath)) {
try {
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content);

// Collect all dependency types
for (const depType of [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
]) {
const depObj = parsed[depType];
if (depObj && typeof depObj === 'object') {
for (const name of Object.keys(depObj)) {
deps.add(name);
}
}
}
} catch {
// Invalid JSON or file not readable
}
}

projectDepsCache.set(projectRoot, deps);
return deps;
}

/**
* Check if a file path is inside a workspace package that is a dependency of the project.
* This is a heuristic - we check if the file is in a directory with a package.json
* that has a "name" field, but is NOT in node_modules.
* that has a "name" field, is NOT in node_modules, and is listed as a dependency
* of the project.
*/
function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (isInNodeModules(filePath)) {
@@ -97,8 +251,13 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (resolve(pkgPath) === rootPkgPath) {
return false;
}
// Found a package.json that's not the root - it's a workspace package
return true;

// Found a package.json that's not the root.
// Only treat it as a workspace package if it's actually a dependency
// of the current project. This prevents sibling apps in a monorepo
// from being incorrectly treated as importable packages.
const projectDeps = getProjectDependencies(projectRoot);
return projectDeps.has(pkg.name);
}
dir = dirname(dir);
}
@@ -114,11 +273,16 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
* @returns The module specifier result
*
* @example
* // File in node_modules
* // File in node_modules (root export)
* resolveModuleSpecifier('/project/node_modules/point/dist/index.js', '/project')
* // => { moduleSpecifier: 'point@1.0.0' }
*
* @example
* // File in node_modules (subpath export)
* resolveModuleSpecifier('/project/node_modules/workflow/dist/internal/builtins.js', '/project')
* // => { moduleSpecifier: 'workflow/internal/builtins@4.0.0' }
*
* @example
* // File in workspace package
* resolveModuleSpecifier('/project/packages/shared/src/utils.ts', '/project')
* // => { moduleSpecifier: '@myorg/shared@0.0.0' }
@@ -149,9 +313,16 @@ export function resolveModuleSpecifier(
return { moduleSpecifier: undefined };
}

// Return the module specifier as "name@version"
// Resolve the export subpath (e.g., "/internal/builtins" for "workflow/internal/builtins")
const subpath = resolveExportSubpath(filePath, pkg);

// Return the module specifier as "name/subpath@version" or "name@version"
const specifier = subpath
? `${pkg.name}${subpath}@${pkg.version}`
: `${pkg.name}@${pkg.version}`;

return {
moduleSpecifier: `${pkg.name}@${pkg.version}`,
moduleSpecifier: specifier,
};
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new module specifier resolution logic, including the caching improvements, workspace package detection based on project dependencies, and subpath export resolution, lacks test coverage. Consider adding comprehensive tests to verify:

  1. The cache back-filling logic works correctly for nested directories
  2. Subpath export resolution correctly handles various export configurations (conditional exports, nested subpaths, etc.)
  3. The workspace package detection correctly filters out sibling apps in a monorepo
  4. The cache is properly invalidated when needed

Tests would help prevent regressions and document the expected behavior of these critical features.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +66
// Merge manifests from both bundles
const manifest = {
steps: { ...stepsManifest.steps, ...workflowsManifest.steps },
workflows: { ...stepsManifest.workflows, ...workflowsManifest.workflows },
classes: { ...stepsManifest.classes, ...workflowsManifest.classes },
};

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest merging order could potentially cause data loss if there are conflicting keys between the stepsManifest and workflowsManifest. The current implementation uses object spread syntax where workflowsManifest properties overwrite stepsManifest properties with the same key.

While this might be intentional, consider:

  1. Adding validation to detect and warn about conflicting IDs
  2. Documenting which manifest takes precedence and why
  3. Ensuring that the merge order is consistent across all builders (currently stepsManifest comes first in all implementations)

If steps and workflows are guaranteed to have non-overlapping IDs by design, this concern can be disregarded, but it would be helpful to document this assumption.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +130
function resolveExportSubpath(filePath: string, pkg: PackageInfo): string {
if (!pkg.exports || typeof pkg.exports !== 'object') {
return '';
}

// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

// Search through exports to find a matching subpath
for (const [subpath, target] of Object.entries(pkg.exports)) {
const resolvedTarget = resolveExportTarget(target);
if (
resolvedTarget &&
normalizeExportPath(resolvedTarget) === relativePath
) {
// Found a match - return the subpath without the leading "."
// e.g., "./internal/builtins" -> "/internal/builtins"
return subpath === '.' ? '' : subpath.substring(1);
}
}

return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function only handles exact export path matches and doesn't support wildcard patterns (e.g., "./internal/": "./dist/internal/.js"). This could lead to incorrect or missing subpath resolution for packages that use wildcard exports. Consider adding support for wildcard patterns by:

  1. Checking if the subpath contains an asterisk
  2. Converting it to a pattern match against the relative file path
  3. Extracting the matched portion to construct the correct subpath

If wildcard exports are not expected to be used in this codebase, this concern can be disregarded.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +114
// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function checks if the normalized file path starts with the package directory plus a forward slash (normalizedPkgDir + '/'). This check will fail for files that are exactly at the package root (e.g., when filePath equals pkg.dir), causing the function to return an empty string even though it should potentially match the root export (".").

While files at the exact package root are uncommon, consider handling this edge case explicitly:

  • Check if normalizedFilePath equals normalizedPkgDir and return the root export logic
  • Or adjust the substring logic to handle this case

This may not be a practical issue if package entry points are always in subdirectories.

Copilot uses AI. Check for mistakes.
Comment threadpackages/builders/src/module-specifier.ts
Comment threadpackages/builders/src/module-specifier.ts
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Addressed the following review comments in commit fce2e7d:

  1. Redundant package.json lookup in isWorkspacePackage - Now using pkg.dir directly from the findPackageJson result instead of walking up the directory tree again.

  2. Array exports support in resolveExportTarget - Added support for array exports by checking Array.isArray(target) and recursively resolving each element in the fallback chain, per the Node.js package exports specification.

… exports support
- Remove redundant directory walk in isWorkspacePackage by using pkg.dir from findPackageJson
- Add support for array exports in resolveExportTarget (fallback chains per Node.js spec)

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced manifest merging to properly combine results from both workflow and step bundles

Out of curiosity, what does this fix? "Enhance" sounds nice but doesn't tell me anything

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@VaguelySerious The step and workflow bundles generate their own manifests, which may have some values that do not overlap. For example, if I use a class only in step functions, but never reference it in the workflow bundle, then it would be missing from the workflow manifest. The fix was to ensure that all bundles are merged in the final form.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add subpath export resolution for package IDs by TooTallNate · Pull Request #901 · vercel/workflow · GitHub
Skip to content

Add subpath export resolution for package IDs - #901

Merged
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids
Feb 5, 2026
Merged

Add subpath export resolution for package IDs#901
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 30, 2026

Copy link
Copy Markdown
Member

Fixed a bug in module specifier resolution and added support for package subpath exports in workflow IDs.

What changed?

  • Fixed a caching bug in the module specifier resolution system that could cause incorrect IDs
  • Added support for subpath exports in package IDs (e.g., workflow/internal/builtins@4.0.0)
  • Improved module resolution by passing absolute file paths to the SWC transform
  • Enhanced manifest merging to properly combine results from both workflow and step bundles
  • Updated builders to return and merge manifests from both workflow and step bundles

How to test?

  1. Build a project that uses subpath exports in packages
  2. Verify that workflow IDs correctly include the subpath (e.g., workflow/internal/builtins@4.0.0)
  3. Test with a project that has multiple builds to ensure module specifier caching works correctly

Why make this change?

This change addresses an issue where the module specifier cache could return incorrect results, leading to inconsistent workflow IDs. It also adds support for packages with multiple entry points through subpath exports, ensuring that steps with the same name in different subpaths don't collide. This improves the reliability of cross-bundle references and makes the system more robust when working with complex package structures.

@changeset-bot

changeset-botBot commented Jan 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b81ae6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/sveltekitPatch
@workflow/buildersPatch
@workflow/nitroPatch
@workflow/nestPatch
@workflow/nextPatch
workflow-devkit-compiler-playgroundPatch
@workflow/astroPatch
@workflow/cliPatch
@workflow/rollupPatch
workflowPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/docs-typecheckPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production479038517
✅ 💻 Local Development438032470
✅ 📦 Local Production438032470
✅ 🐘 Local Postgres438032470
✅ 🪟 Windows470047
❌ 🌍 Community Worlds311690200
✅ 📋 Other129012141
Total20001691462315

❌ Failed Tests

🌍 Community Worlds (169 failed)

mongodb (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4304
✅ example4304
✅ express4304
✅ fastify4304
✅ hono4304
✅ nextjs-turbopack4601
✅ nextjs-webpack4601
✅ nitro4304
✅ nuxt4304
✅ sveltekit4304
✅ vite4304
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb5420
✅ redis-dev300
❌ redis5420
✅ starter-dev300
❌ starter4430
✅ turso-dev300
❌ turso5420
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4304
✅ e2e-local-postgres-nest-stable4304
✅ e2e-local-prod-nest-stable4304

📋 View full workflow run

@TooTallNate
TooTallNateforce-pushed the 01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids branch from 6cce073 to ca8bd01CompareJanuary 30, 2026 19:57
@github-actions

github-actionsBot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.041s (+3.5%)1.019s (~)0.978s101.00x
💻 LocalNitro0.043s (~)1.007s (~)0.964s101.06x
💻 LocalExpress0.044s (+4.8%)1.007s (~)0.963s101.08x
🐘 PostgresNitro0.212s (-6.9% 🟢)1.015s (~)0.803s105.19x
🐘 PostgresExpress0.290s (+4.2%)1.016s (~)0.726s107.10x
🐘 PostgresNext.js (Turbopack)0.360s (+28.8% 🔺)1.021s (~)0.660s108.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.712s (+6.4% 🔺)1.582s (-0.8%)0.870s101.00x
▲ VercelExpress0.793s (+7.0% 🔺)1.607s (~)0.814s101.11x
▲ VercelNext.js (Turbopack)0.870s (-1.1%)1.721s (-12.0% 🟢)0.851s101.22x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.094s (-0.7%)2.014s (~)0.920s101.00x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
💻 LocalExpress1.117s (~)2.007s (~)0.890s101.02x
🐘 PostgresNext.js (Turbopack)1.858s (+0.5%)2.420s (+20.1% 🔺)0.562s101.70x
🐘 PostgresExpress2.133s (-1.2%)3.014s (~)0.881s101.95x
🐘 PostgresNitro2.258s (+3.1%)3.017s (~)0.759s102.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.842s (-1.3%)3.742s (+1.1%)0.900s101.00x
▲ VercelExpress2.933s (~)3.724s (+4.6%)0.791s101.03x
▲ VercelNext.js (Turbopack)2.969s (+2.0%)3.755s (-1.8%)0.786s101.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.750s (~)11.018s (~)0.268s31.00x
💻 LocalExpress10.837s (~)11.012s (~)0.175s31.01x
💻 LocalNitro10.849s (~)11.010s (~)0.162s31.01x
🐘 PostgresNext.js (Turbopack)15.074s (-1.0%)16.033s (~)0.959s21.40x
🐘 PostgresNitro20.454s (~)21.038s (~)0.583s21.90x
🐘 PostgresExpress20.532s (~)21.032s (~)0.500s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)39.852s (+75.5% 🔺)40.274s (+71.8% 🔺)0.422s11.00x
▲ VercelNitro42.916s (+89.8% 🔺)43.893s (+88.1% 🔺)0.977s21.08x
▲ VercelExpress45.343s (+94.4% 🔺)46.170s (+88.5% 🔺)0.827s21.14x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)27.245s (~)28.038s (~)0.793s31.00x
💻 LocalNitro27.465s (~)28.022s (~)0.558s31.01x
💻 LocalExpress27.501s (~)28.025s (~)0.524s31.01x
🐘 PostgresNext.js (Turbopack)37.941s (+0.8%)38.052s (~)0.110s21.39x
🐘 PostgresNitro50.373s (~)51.096s (~)0.722s21.85x
🐘 PostgresExpress50.422s (~)51.076s (~)0.654s21.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express58.799s (+2.8%)60.259s (+4.0%)1.460s11.00x
▲ VercelNitro59.008s (+3.9%)59.284s (+3.6%)0.275s21.00x
▲ VercelNext.js (Turbopack)59.231s (+2.7%)59.787s (+2.4%)0.556s21.01x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)56.526s (~)57.053s (~)0.527s21.00x
💻 LocalNitro57.216s (~)58.049s (+0.9%)0.833s21.01x
💻 LocalExpress57.319s (~)58.040s (~)0.721s21.01x
🐘 PostgresNext.js (Turbopack)74.784s (-5.7% 🟢)75.602s (-5.0% 🟢)0.819s21.32x
🐘 PostgresNitro100.195s (~)101.169s (~)0.974s11.77x
🐘 PostgresExpress100.271s (~)101.172s (~)0.901s11.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro124.409s (+2.9%)125.651s (+2.6%)1.242s11.00x
▲ VercelNext.js (Turbopack)126.724s (-2.6%)127.329s (-2.5%)0.605s11.02x
▲ VercelExpress160.313s (+31.1% 🔺)160.881s (+30.5% 🔺)0.568s11.29x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.397s (~)2.006s (~)0.609s151.00x
💻 LocalNext.js (Turbopack)1.410s (+1.3%)2.012s (~)0.603s151.01x
💻 LocalExpress1.436s (+1.3%)2.007s (~)0.571s151.03x
🐘 PostgresNitro2.319s (-1.7%)3.014s (~)0.695s101.66x
🐘 PostgresNext.js (Turbopack)2.364s (+25.8% 🔺)2.744s (+8.8% 🔺)0.381s111.69x
🐘 PostgresExpress2.366s (-1.0%)3.013s (~)0.647s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.028s (+6.5% 🔺)3.878s (+4.9%)0.850s81.00x
▲ VercelNext.js (Turbopack)3.062s (+1.5%)3.920s (-1.9%)0.858s81.01x
▲ VercelExpress3.345s (+11.3% 🔺)4.184s (+7.4% 🔺)0.839s81.10x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.553s (-1.2%)3.047s (+0.7%)0.494s101.00x
💻 LocalNitro2.577s (+3.0%)3.017s (~)0.440s101.01x
💻 LocalExpress2.633s (+1.4%)3.022s (~)0.389s101.03x
🐘 PostgresExpress7.829s (-8.7% 🟢)8.578s (-8.0% 🟢)0.749s43.07x
🐘 PostgresNitro7.987s (+1.6%)8.594s (+0.5%)0.607s43.13x
🐘 PostgresNext.js (Turbopack)12.839s (+11.5% 🔺)13.360s (+11.1% 🔺)0.522s35.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.202s (+0.7%)3.973s (+3.3%)0.770s81.00x
▲ VercelNitro3.641s (+18.1% 🔺)4.390s (+14.8% 🔺)0.748s71.14x
▲ VercelNext.js (Turbopack)3.721s (-3.8%)4.425s (-4.6%)0.704s71.16x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)7.559s (+2.3%)8.274s (-2.2%)0.715s41.00x
💻 LocalExpress7.559s (+4.0%)8.515s (+3.1%)0.956s41.00x
💻 LocalNitro7.582s (+8.8% 🔺)8.529s (+13.7% 🔺)0.946s41.00x
🐘 PostgresNitro49.993s (+10.7% 🔺)50.305s (+8.9% 🔺)0.312s16.61x
🐘 PostgresExpress51.058s (+4.2%)51.328s (+4.0%)0.270s16.75x
🐘 PostgresNext.js (Turbopack)55.261s (+8.2% 🔺)56.160s (+9.6% 🔺)0.899s17.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.388s (-17.2% 🟢)4.017s (-12.6% 🟢)0.629s81.00x
▲ VercelExpress3.464s (+3.6%)4.103s (+8.6% 🔺)0.639s81.02x
▲ VercelNext.js (Turbopack)3.996s (+6.8% 🔺)5.070s (+14.2% 🔺)1.074s61.18x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.406s (-1.9%)2.009s (~)0.603s151.00x
💻 LocalExpress1.468s (+2.8%)2.008s (~)0.541s151.04x
💻 LocalNitro1.468s (+1.5%)2.007s (~)0.538s151.04x
🐘 PostgresNitro2.169s (-5.7% 🟢)2.598s (-3.0%)0.429s121.54x
🐘 PostgresExpress2.216s (+5.5% 🔺)2.598s (+3.2%)0.382s121.58x
🐘 PostgresNext.js (Turbopack)2.274s (+26.6% 🔺)2.755s (+36.9% 🔺)0.481s111.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.763s (~)3.700s (~)0.937s91.00x
▲ VercelNext.js (Turbopack)2.913s (+1.4%)3.795s (-0.7%)0.883s81.05x
▲ VercelExpress2.986s (+4.9%)3.868s (+2.8%)0.882s81.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.663s (+1.8%)3.010s (~)0.347s101.00x
💻 LocalNext.js (Turbopack)2.674s (-0.5%)3.034s (~)0.359s101.00x
💻 LocalExpress2.691s (~)3.012s (~)0.321s101.01x
🐘 PostgresExpress11.815s (+8.9% 🔺)12.361s (+11.5% 🔺)0.546s34.44x
🐘 PostgresNitro12.124s (+12.5% 🔺)12.721s (+15.2% 🔺)0.597s34.55x
🐘 PostgresNext.js (Turbopack)13.147s (+19.7% 🔺)13.723s (+17.4% 🔺)0.576s34.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.906s (-1.7%)3.716s (+1.8%)0.810s91.00x
▲ VercelNitro3.044s (-3.0%)3.719s (-2.6%)0.676s91.05x
▲ VercelNext.js (Turbopack)3.066s (+1.2%)3.739s (-1.9%)0.674s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express7.910s (+1.1%)8.661s (-1.7%)0.751s41.00x
💻 LocalNitro7.971s (+4.9%)8.877s (+3.3%)0.906s41.01x
💻 LocalNext.js (Turbopack)8.245s (+7.8% 🔺)9.068s (+5.2% 🔺)0.823s41.04x
🐘 PostgresNitro51.279s (~)52.131s (~)0.852s16.48x
🐘 PostgresExpress51.446s (~)52.249s (~)0.803s16.50x
🐘 PostgresNext.js (Turbopack)56.929s (+4.7%)57.297s (+3.9%)0.368s17.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.257s (+5.0%)3.868s (+2.6%)0.611s81.00x
▲ VercelExpress3.722s (+11.2% 🔺)4.332s (+10.0% 🔺)0.610s81.14x
▲ VercelNext.js (Turbopack)4.022s (+13.3% 🔺)4.880s (+12.5% 🔺)0.857s71.24x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.145s (-1.4%)1.003s (~)0.016s (-8.0% 🟢)1.028s (~)0.883s101.00x
💻 LocalNitro0.182s (+2.6%)0.992s (~)0.015s (+7.3% 🔺)1.021s (~)0.839s101.26x
💻 LocalExpress0.185s (-1.4%)0.992s (~)0.014s (-4.2%)1.021s (~)0.836s101.28x
🐘 PostgresNext.js (Turbopack)0.751s (+5.0%)0.856s (-12.4% 🟢)0.000s (NaN%)1.017s (-8.8% 🟢)0.266s105.19x
🐘 PostgresNitro2.375s (+59.7% 🔺)2.673s (+61.2% 🔺)0.000s (+Infinity% 🔺)3.018s (+50.0% 🔺)0.643s1016.41x
🐘 PostgresExpress2.428s (~)2.618s (~)0.000s (+Infinity% 🔺)3.017s (~)0.589s1016.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.068s (~)3.350s (-1.4%)0.229s (+60.6% 🔺)4.071s (+1.1%)1.003s101.00x
▲ VercelNext.js (Turbopack)3.238s (+4.7%)3.502s (+6.7% 🔺)0.270s (+46.9% 🔺)4.319s (+8.5% 🔺)1.082s101.06x
▲ VercelExpress3.349s (+9.3% 🔺)3.477s (+3.5%)0.188s (-11.9% 🟢)4.230s (+3.9%)0.881s101.09x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNitro5/12
▲ VercelNitro8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)💻 Local10/12
Nitro💻 Local10/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a caching bug in the module specifier resolution system and adds support for package subpath exports in workflow IDs. The changes improve the reliability of cross-bundle references and enable proper handling of packages with multiple entry points.

Changes:

  • Fixed module specifier cache back-filling to prevent incorrect IDs across multiple lookups
  • Added subpath export resolution to support packages like workflow/internal/builtins@4.0.0
  • Enhanced workspace package detection to filter out sibling apps in monorepos based on project dependencies
  • Updated all builders to properly merge manifests from both workflow and step bundles
  • Added Windows path normalization support in the Rust transform plugin
  • Passed absolute file paths to SWC transform for accurate module specifier resolution

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
packages/builders/src/module-specifier.tsCore changes: cache back-filling, subpath export resolution, workspace package detection improvements
packages/builders/src/apply-swc-transform.tsAdded absolutePath parameter for accurate module specifier resolution
packages/builders/src/swc-esbuild-plugin.tsPasses absolute path to transform function
packages/builders/src/base-builder.tsUpdated createWorkflowsBundle return type to include manifest
packages/sveltekit/src/builder.tsUpdated to merge manifests from both bundles
packages/nitro/src/builders.tsUpdated to merge manifests from both bundles
packages/next/src/builder.tsUpdated to merge manifests and handle optional context properties
packages/nest/src/builder.tsUpdated to merge manifests from both bundles
packages/builders/src/vercel-build-output-api.tsUpdated to merge manifests from both bundles
packages/builders/src/standalone.tsAdded mergeManifests helper and updated bundle methods
packages/swc-plugin-workflow/transform/src/naming.rsAdded Windows path normalization with tests
packages/swc-plugin-workflow/transform/src/lib.rsEnhanced builtin function comment documentation
packages/swc-plugin-workflow/spec.mdUpdated documentation for subpath exports
.changeset/afraid-candies-find.mdAdded changeset entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 37 to 327
@@ -69,9 +176,56 @@ function isInNodeModules(filePath: string): boolean {
}

/**
* Check if a file path is inside a workspace package.
* Cache for project dependencies to avoid repeated filesystem reads.
* Maps project root to set of dependency package names.
*/
const projectDepsCache = new Map<string, Set<string>>();

/**
* Get all dependencies (including devDependencies) for a project.
*/
function getProjectDependencies(projectRoot: string): Set<string> {
const cached = projectDepsCache.get(projectRoot);
if (cached) {
return cached;
}

const deps = new Set<string>();
const pkgPath = join(projectRoot, 'package.json');

if (existsSync(pkgPath)) {
try {
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content);

// Collect all dependency types
for (const depType of [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
]) {
const depObj = parsed[depType];
if (depObj && typeof depObj === 'object') {
for (const name of Object.keys(depObj)) {
deps.add(name);
}
}
}
} catch {
// Invalid JSON or file not readable
}
}

projectDepsCache.set(projectRoot, deps);
return deps;
}

/**
* Check if a file path is inside a workspace package that is a dependency of the project.
* This is a heuristic - we check if the file is in a directory with a package.json
* that has a "name" field, but is NOT in node_modules.
* that has a "name" field, is NOT in node_modules, and is listed as a dependency
* of the project.
*/
function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (isInNodeModules(filePath)) {
@@ -97,8 +251,13 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (resolve(pkgPath) === rootPkgPath) {
return false;
}
// Found a package.json that's not the root - it's a workspace package
return true;

// Found a package.json that's not the root.
// Only treat it as a workspace package if it's actually a dependency
// of the current project. This prevents sibling apps in a monorepo
// from being incorrectly treated as importable packages.
const projectDeps = getProjectDependencies(projectRoot);
return projectDeps.has(pkg.name);
}
dir = dirname(dir);
}
@@ -114,11 +273,16 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
* @returns The module specifier result
*
* @example
* // File in node_modules
* // File in node_modules (root export)
* resolveModuleSpecifier('/project/node_modules/point/dist/index.js', '/project')
* // => { moduleSpecifier: 'point@1.0.0' }
*
* @example
* // File in node_modules (subpath export)
* resolveModuleSpecifier('/project/node_modules/workflow/dist/internal/builtins.js', '/project')
* // => { moduleSpecifier: 'workflow/internal/builtins@4.0.0' }
*
* @example
* // File in workspace package
* resolveModuleSpecifier('/project/packages/shared/src/utils.ts', '/project')
* // => { moduleSpecifier: '@myorg/shared@0.0.0' }
@@ -149,9 +313,16 @@ export function resolveModuleSpecifier(
return { moduleSpecifier: undefined };
}

// Return the module specifier as "name@version"
// Resolve the export subpath (e.g., "/internal/builtins" for "workflow/internal/builtins")
const subpath = resolveExportSubpath(filePath, pkg);

// Return the module specifier as "name/subpath@version" or "name@version"
const specifier = subpath
? `${pkg.name}${subpath}@${pkg.version}`
: `${pkg.name}@${pkg.version}`;

return {
moduleSpecifier: `${pkg.name}@${pkg.version}`,
moduleSpecifier: specifier,
};
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new module specifier resolution logic, including the caching improvements, workspace package detection based on project dependencies, and subpath export resolution, lacks test coverage. Consider adding comprehensive tests to verify:

  1. The cache back-filling logic works correctly for nested directories
  2. Subpath export resolution correctly handles various export configurations (conditional exports, nested subpaths, etc.)
  3. The workspace package detection correctly filters out sibling apps in a monorepo
  4. The cache is properly invalidated when needed

Tests would help prevent regressions and document the expected behavior of these critical features.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +66
// Merge manifests from both bundles
const manifest = {
steps: { ...stepsManifest.steps, ...workflowsManifest.steps },
workflows: { ...stepsManifest.workflows, ...workflowsManifest.workflows },
classes: { ...stepsManifest.classes, ...workflowsManifest.classes },
};

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest merging order could potentially cause data loss if there are conflicting keys between the stepsManifest and workflowsManifest. The current implementation uses object spread syntax where workflowsManifest properties overwrite stepsManifest properties with the same key.

While this might be intentional, consider:

  1. Adding validation to detect and warn about conflicting IDs
  2. Documenting which manifest takes precedence and why
  3. Ensuring that the merge order is consistent across all builders (currently stepsManifest comes first in all implementations)

If steps and workflows are guaranteed to have non-overlapping IDs by design, this concern can be disregarded, but it would be helpful to document this assumption.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +130
function resolveExportSubpath(filePath: string, pkg: PackageInfo): string {
if (!pkg.exports || typeof pkg.exports !== 'object') {
return '';
}

// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

// Search through exports to find a matching subpath
for (const [subpath, target] of Object.entries(pkg.exports)) {
const resolvedTarget = resolveExportTarget(target);
if (
resolvedTarget &&
normalizeExportPath(resolvedTarget) === relativePath
) {
// Found a match - return the subpath without the leading "."
// e.g., "./internal/builtins" -> "/internal/builtins"
return subpath === '.' ? '' : subpath.substring(1);
}
}

return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function only handles exact export path matches and doesn't support wildcard patterns (e.g., "./internal/": "./dist/internal/.js"). This could lead to incorrect or missing subpath resolution for packages that use wildcard exports. Consider adding support for wildcard patterns by:

  1. Checking if the subpath contains an asterisk
  2. Converting it to a pattern match against the relative file path
  3. Extracting the matched portion to construct the correct subpath

If wildcard exports are not expected to be used in this codebase, this concern can be disregarded.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +114
// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function checks if the normalized file path starts with the package directory plus a forward slash (normalizedPkgDir + '/'). This check will fail for files that are exactly at the package root (e.g., when filePath equals pkg.dir), causing the function to return an empty string even though it should potentially match the root export (".").

While files at the exact package root are uncommon, consider handling this edge case explicitly:

  • Check if normalizedFilePath equals normalizedPkgDir and return the root export logic
  • Or adjust the substring logic to handle this case

This may not be a practical issue if package entry points are always in subdirectories.

Copilot uses AI. Check for mistakes.
Comment threadpackages/builders/src/module-specifier.ts
Comment threadpackages/builders/src/module-specifier.ts
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Addressed the following review comments in commit fce2e7d:

  1. Redundant package.json lookup in isWorkspacePackage - Now using pkg.dir directly from the findPackageJson result instead of walking up the directory tree again.

  2. Array exports support in resolveExportTarget - Added support for array exports by checking Array.isArray(target) and recursively resolving each element in the fallback chain, per the Node.js package exports specification.

… exports support
- Remove redundant directory walk in isWorkspacePackage by using pkg.dir from findPackageJson
- Add support for array exports in resolveExportTarget (fallback chains per Node.js spec)

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced manifest merging to properly combine results from both workflow and step bundles

Out of curiosity, what does this fix? "Enhance" sounds nice but doesn't tell me anything

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@VaguelySerious The step and workflow bundles generate their own manifests, which may have some values that do not overlap. For example, if I use a class only in step functions, but never reference it in the workflow bundle, then it would be missing from the workflow manifest. The fix was to ensure that all bundles are merged in the final form.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add subpath export resolution for package IDs by TooTallNate · Pull Request #901 · vercel/workflow · GitHub
Skip to content

Add subpath export resolution for package IDs - #901

Merged
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids
Feb 5, 2026
Merged

Add subpath export resolution for package IDs#901
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 30, 2026

Copy link
Copy Markdown
Member

Fixed a bug in module specifier resolution and added support for package subpath exports in workflow IDs.

What changed?

  • Fixed a caching bug in the module specifier resolution system that could cause incorrect IDs
  • Added support for subpath exports in package IDs (e.g., workflow/internal/builtins@4.0.0)
  • Improved module resolution by passing absolute file paths to the SWC transform
  • Enhanced manifest merging to properly combine results from both workflow and step bundles
  • Updated builders to return and merge manifests from both workflow and step bundles

How to test?

  1. Build a project that uses subpath exports in packages
  2. Verify that workflow IDs correctly include the subpath (e.g., workflow/internal/builtins@4.0.0)
  3. Test with a project that has multiple builds to ensure module specifier caching works correctly

Why make this change?

This change addresses an issue where the module specifier cache could return incorrect results, leading to inconsistent workflow IDs. It also adds support for packages with multiple entry points through subpath exports, ensuring that steps with the same name in different subpaths don't collide. This improves the reliability of cross-bundle references and makes the system more robust when working with complex package structures.

@changeset-bot

changeset-botBot commented Jan 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b81ae6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/sveltekitPatch
@workflow/buildersPatch
@workflow/nitroPatch
@workflow/nestPatch
@workflow/nextPatch
workflow-devkit-compiler-playgroundPatch
@workflow/astroPatch
@workflow/cliPatch
@workflow/rollupPatch
workflowPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/docs-typecheckPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production479038517
✅ 💻 Local Development438032470
✅ 📦 Local Production438032470
✅ 🐘 Local Postgres438032470
✅ 🪟 Windows470047
❌ 🌍 Community Worlds311690200
✅ 📋 Other129012141
Total20001691462315

❌ Failed Tests

🌍 Community Worlds (169 failed)

mongodb (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4304
✅ example4304
✅ express4304
✅ fastify4304
✅ hono4304
✅ nextjs-turbopack4601
✅ nextjs-webpack4601
✅ nitro4304
✅ nuxt4304
✅ sveltekit4304
✅ vite4304
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb5420
✅ redis-dev300
❌ redis5420
✅ starter-dev300
❌ starter4430
✅ turso-dev300
❌ turso5420
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4304
✅ e2e-local-postgres-nest-stable4304
✅ e2e-local-prod-nest-stable4304

📋 View full workflow run

@TooTallNate
TooTallNateforce-pushed the 01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids branch from 6cce073 to ca8bd01CompareJanuary 30, 2026 19:57
@github-actions

github-actionsBot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.041s (+3.5%)1.019s (~)0.978s101.00x
💻 LocalNitro0.043s (~)1.007s (~)0.964s101.06x
💻 LocalExpress0.044s (+4.8%)1.007s (~)0.963s101.08x
🐘 PostgresNitro0.212s (-6.9% 🟢)1.015s (~)0.803s105.19x
🐘 PostgresExpress0.290s (+4.2%)1.016s (~)0.726s107.10x
🐘 PostgresNext.js (Turbopack)0.360s (+28.8% 🔺)1.021s (~)0.660s108.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.712s (+6.4% 🔺)1.582s (-0.8%)0.870s101.00x
▲ VercelExpress0.793s (+7.0% 🔺)1.607s (~)0.814s101.11x
▲ VercelNext.js (Turbopack)0.870s (-1.1%)1.721s (-12.0% 🟢)0.851s101.22x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.094s (-0.7%)2.014s (~)0.920s101.00x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
💻 LocalExpress1.117s (~)2.007s (~)0.890s101.02x
🐘 PostgresNext.js (Turbopack)1.858s (+0.5%)2.420s (+20.1% 🔺)0.562s101.70x
🐘 PostgresExpress2.133s (-1.2%)3.014s (~)0.881s101.95x
🐘 PostgresNitro2.258s (+3.1%)3.017s (~)0.759s102.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.842s (-1.3%)3.742s (+1.1%)0.900s101.00x
▲ VercelExpress2.933s (~)3.724s (+4.6%)0.791s101.03x
▲ VercelNext.js (Turbopack)2.969s (+2.0%)3.755s (-1.8%)0.786s101.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.750s (~)11.018s (~)0.268s31.00x
💻 LocalExpress10.837s (~)11.012s (~)0.175s31.01x
💻 LocalNitro10.849s (~)11.010s (~)0.162s31.01x
🐘 PostgresNext.js (Turbopack)15.074s (-1.0%)16.033s (~)0.959s21.40x
🐘 PostgresNitro20.454s (~)21.038s (~)0.583s21.90x
🐘 PostgresExpress20.532s (~)21.032s (~)0.500s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)39.852s (+75.5% 🔺)40.274s (+71.8% 🔺)0.422s11.00x
▲ VercelNitro42.916s (+89.8% 🔺)43.893s (+88.1% 🔺)0.977s21.08x
▲ VercelExpress45.343s (+94.4% 🔺)46.170s (+88.5% 🔺)0.827s21.14x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)27.245s (~)28.038s (~)0.793s31.00x
💻 LocalNitro27.465s (~)28.022s (~)0.558s31.01x
💻 LocalExpress27.501s (~)28.025s (~)0.524s31.01x
🐘 PostgresNext.js (Turbopack)37.941s (+0.8%)38.052s (~)0.110s21.39x
🐘 PostgresNitro50.373s (~)51.096s (~)0.722s21.85x
🐘 PostgresExpress50.422s (~)51.076s (~)0.654s21.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express58.799s (+2.8%)60.259s (+4.0%)1.460s11.00x
▲ VercelNitro59.008s (+3.9%)59.284s (+3.6%)0.275s21.00x
▲ VercelNext.js (Turbopack)59.231s (+2.7%)59.787s (+2.4%)0.556s21.01x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)56.526s (~)57.053s (~)0.527s21.00x
💻 LocalNitro57.216s (~)58.049s (+0.9%)0.833s21.01x
💻 LocalExpress57.319s (~)58.040s (~)0.721s21.01x
🐘 PostgresNext.js (Turbopack)74.784s (-5.7% 🟢)75.602s (-5.0% 🟢)0.819s21.32x
🐘 PostgresNitro100.195s (~)101.169s (~)0.974s11.77x
🐘 PostgresExpress100.271s (~)101.172s (~)0.901s11.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro124.409s (+2.9%)125.651s (+2.6%)1.242s11.00x
▲ VercelNext.js (Turbopack)126.724s (-2.6%)127.329s (-2.5%)0.605s11.02x
▲ VercelExpress160.313s (+31.1% 🔺)160.881s (+30.5% 🔺)0.568s11.29x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.397s (~)2.006s (~)0.609s151.00x
💻 LocalNext.js (Turbopack)1.410s (+1.3%)2.012s (~)0.603s151.01x
💻 LocalExpress1.436s (+1.3%)2.007s (~)0.571s151.03x
🐘 PostgresNitro2.319s (-1.7%)3.014s (~)0.695s101.66x
🐘 PostgresNext.js (Turbopack)2.364s (+25.8% 🔺)2.744s (+8.8% 🔺)0.381s111.69x
🐘 PostgresExpress2.366s (-1.0%)3.013s (~)0.647s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.028s (+6.5% 🔺)3.878s (+4.9%)0.850s81.00x
▲ VercelNext.js (Turbopack)3.062s (+1.5%)3.920s (-1.9%)0.858s81.01x
▲ VercelExpress3.345s (+11.3% 🔺)4.184s (+7.4% 🔺)0.839s81.10x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.553s (-1.2%)3.047s (+0.7%)0.494s101.00x
💻 LocalNitro2.577s (+3.0%)3.017s (~)0.440s101.01x
💻 LocalExpress2.633s (+1.4%)3.022s (~)0.389s101.03x
🐘 PostgresExpress7.829s (-8.7% 🟢)8.578s (-8.0% 🟢)0.749s43.07x
🐘 PostgresNitro7.987s (+1.6%)8.594s (+0.5%)0.607s43.13x
🐘 PostgresNext.js (Turbopack)12.839s (+11.5% 🔺)13.360s (+11.1% 🔺)0.522s35.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.202s (+0.7%)3.973s (+3.3%)0.770s81.00x
▲ VercelNitro3.641s (+18.1% 🔺)4.390s (+14.8% 🔺)0.748s71.14x
▲ VercelNext.js (Turbopack)3.721s (-3.8%)4.425s (-4.6%)0.704s71.16x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)7.559s (+2.3%)8.274s (-2.2%)0.715s41.00x
💻 LocalExpress7.559s (+4.0%)8.515s (+3.1%)0.956s41.00x
💻 LocalNitro7.582s (+8.8% 🔺)8.529s (+13.7% 🔺)0.946s41.00x
🐘 PostgresNitro49.993s (+10.7% 🔺)50.305s (+8.9% 🔺)0.312s16.61x
🐘 PostgresExpress51.058s (+4.2%)51.328s (+4.0%)0.270s16.75x
🐘 PostgresNext.js (Turbopack)55.261s (+8.2% 🔺)56.160s (+9.6% 🔺)0.899s17.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.388s (-17.2% 🟢)4.017s (-12.6% 🟢)0.629s81.00x
▲ VercelExpress3.464s (+3.6%)4.103s (+8.6% 🔺)0.639s81.02x
▲ VercelNext.js (Turbopack)3.996s (+6.8% 🔺)5.070s (+14.2% 🔺)1.074s61.18x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.406s (-1.9%)2.009s (~)0.603s151.00x
💻 LocalExpress1.468s (+2.8%)2.008s (~)0.541s151.04x
💻 LocalNitro1.468s (+1.5%)2.007s (~)0.538s151.04x
🐘 PostgresNitro2.169s (-5.7% 🟢)2.598s (-3.0%)0.429s121.54x
🐘 PostgresExpress2.216s (+5.5% 🔺)2.598s (+3.2%)0.382s121.58x
🐘 PostgresNext.js (Turbopack)2.274s (+26.6% 🔺)2.755s (+36.9% 🔺)0.481s111.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.763s (~)3.700s (~)0.937s91.00x
▲ VercelNext.js (Turbopack)2.913s (+1.4%)3.795s (-0.7%)0.883s81.05x
▲ VercelExpress2.986s (+4.9%)3.868s (+2.8%)0.882s81.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.663s (+1.8%)3.010s (~)0.347s101.00x
💻 LocalNext.js (Turbopack)2.674s (-0.5%)3.034s (~)0.359s101.00x
💻 LocalExpress2.691s (~)3.012s (~)0.321s101.01x
🐘 PostgresExpress11.815s (+8.9% 🔺)12.361s (+11.5% 🔺)0.546s34.44x
🐘 PostgresNitro12.124s (+12.5% 🔺)12.721s (+15.2% 🔺)0.597s34.55x
🐘 PostgresNext.js (Turbopack)13.147s (+19.7% 🔺)13.723s (+17.4% 🔺)0.576s34.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.906s (-1.7%)3.716s (+1.8%)0.810s91.00x
▲ VercelNitro3.044s (-3.0%)3.719s (-2.6%)0.676s91.05x
▲ VercelNext.js (Turbopack)3.066s (+1.2%)3.739s (-1.9%)0.674s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express7.910s (+1.1%)8.661s (-1.7%)0.751s41.00x
💻 LocalNitro7.971s (+4.9%)8.877s (+3.3%)0.906s41.01x
💻 LocalNext.js (Turbopack)8.245s (+7.8% 🔺)9.068s (+5.2% 🔺)0.823s41.04x
🐘 PostgresNitro51.279s (~)52.131s (~)0.852s16.48x
🐘 PostgresExpress51.446s (~)52.249s (~)0.803s16.50x
🐘 PostgresNext.js (Turbopack)56.929s (+4.7%)57.297s (+3.9%)0.368s17.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.257s (+5.0%)3.868s (+2.6%)0.611s81.00x
▲ VercelExpress3.722s (+11.2% 🔺)4.332s (+10.0% 🔺)0.610s81.14x
▲ VercelNext.js (Turbopack)4.022s (+13.3% 🔺)4.880s (+12.5% 🔺)0.857s71.24x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.145s (-1.4%)1.003s (~)0.016s (-8.0% 🟢)1.028s (~)0.883s101.00x
💻 LocalNitro0.182s (+2.6%)0.992s (~)0.015s (+7.3% 🔺)1.021s (~)0.839s101.26x
💻 LocalExpress0.185s (-1.4%)0.992s (~)0.014s (-4.2%)1.021s (~)0.836s101.28x
🐘 PostgresNext.js (Turbopack)0.751s (+5.0%)0.856s (-12.4% 🟢)0.000s (NaN%)1.017s (-8.8% 🟢)0.266s105.19x
🐘 PostgresNitro2.375s (+59.7% 🔺)2.673s (+61.2% 🔺)0.000s (+Infinity% 🔺)3.018s (+50.0% 🔺)0.643s1016.41x
🐘 PostgresExpress2.428s (~)2.618s (~)0.000s (+Infinity% 🔺)3.017s (~)0.589s1016.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.068s (~)3.350s (-1.4%)0.229s (+60.6% 🔺)4.071s (+1.1%)1.003s101.00x
▲ VercelNext.js (Turbopack)3.238s (+4.7%)3.502s (+6.7% 🔺)0.270s (+46.9% 🔺)4.319s (+8.5% 🔺)1.082s101.06x
▲ VercelExpress3.349s (+9.3% 🔺)3.477s (+3.5%)0.188s (-11.9% 🟢)4.230s (+3.9%)0.881s101.09x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNitro5/12
▲ VercelNitro8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)💻 Local10/12
Nitro💻 Local10/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a caching bug in the module specifier resolution system and adds support for package subpath exports in workflow IDs. The changes improve the reliability of cross-bundle references and enable proper handling of packages with multiple entry points.

Changes:

  • Fixed module specifier cache back-filling to prevent incorrect IDs across multiple lookups
  • Added subpath export resolution to support packages like workflow/internal/builtins@4.0.0
  • Enhanced workspace package detection to filter out sibling apps in monorepos based on project dependencies
  • Updated all builders to properly merge manifests from both workflow and step bundles
  • Added Windows path normalization support in the Rust transform plugin
  • Passed absolute file paths to SWC transform for accurate module specifier resolution

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
packages/builders/src/module-specifier.tsCore changes: cache back-filling, subpath export resolution, workspace package detection improvements
packages/builders/src/apply-swc-transform.tsAdded absolutePath parameter for accurate module specifier resolution
packages/builders/src/swc-esbuild-plugin.tsPasses absolute path to transform function
packages/builders/src/base-builder.tsUpdated createWorkflowsBundle return type to include manifest
packages/sveltekit/src/builder.tsUpdated to merge manifests from both bundles
packages/nitro/src/builders.tsUpdated to merge manifests from both bundles
packages/next/src/builder.tsUpdated to merge manifests and handle optional context properties
packages/nest/src/builder.tsUpdated to merge manifests from both bundles
packages/builders/src/vercel-build-output-api.tsUpdated to merge manifests from both bundles
packages/builders/src/standalone.tsAdded mergeManifests helper and updated bundle methods
packages/swc-plugin-workflow/transform/src/naming.rsAdded Windows path normalization with tests
packages/swc-plugin-workflow/transform/src/lib.rsEnhanced builtin function comment documentation
packages/swc-plugin-workflow/spec.mdUpdated documentation for subpath exports
.changeset/afraid-candies-find.mdAdded changeset entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 37 to 327
@@ -69,9 +176,56 @@ function isInNodeModules(filePath: string): boolean {
}

/**
* Check if a file path is inside a workspace package.
* Cache for project dependencies to avoid repeated filesystem reads.
* Maps project root to set of dependency package names.
*/
const projectDepsCache = new Map<string, Set<string>>();

/**
* Get all dependencies (including devDependencies) for a project.
*/
function getProjectDependencies(projectRoot: string): Set<string> {
const cached = projectDepsCache.get(projectRoot);
if (cached) {
return cached;
}

const deps = new Set<string>();
const pkgPath = join(projectRoot, 'package.json');

if (existsSync(pkgPath)) {
try {
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content);

// Collect all dependency types
for (const depType of [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
]) {
const depObj = parsed[depType];
if (depObj && typeof depObj === 'object') {
for (const name of Object.keys(depObj)) {
deps.add(name);
}
}
}
} catch {
// Invalid JSON or file not readable
}
}

projectDepsCache.set(projectRoot, deps);
return deps;
}

/**
* Check if a file path is inside a workspace package that is a dependency of the project.
* This is a heuristic - we check if the file is in a directory with a package.json
* that has a "name" field, but is NOT in node_modules.
* that has a "name" field, is NOT in node_modules, and is listed as a dependency
* of the project.
*/
function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (isInNodeModules(filePath)) {
@@ -97,8 +251,13 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (resolve(pkgPath) === rootPkgPath) {
return false;
}
// Found a package.json that's not the root - it's a workspace package
return true;

// Found a package.json that's not the root.
// Only treat it as a workspace package if it's actually a dependency
// of the current project. This prevents sibling apps in a monorepo
// from being incorrectly treated as importable packages.
const projectDeps = getProjectDependencies(projectRoot);
return projectDeps.has(pkg.name);
}
dir = dirname(dir);
}
@@ -114,11 +273,16 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
* @returns The module specifier result
*
* @example
* // File in node_modules
* // File in node_modules (root export)
* resolveModuleSpecifier('/project/node_modules/point/dist/index.js', '/project')
* // => { moduleSpecifier: 'point@1.0.0' }
*
* @example
* // File in node_modules (subpath export)
* resolveModuleSpecifier('/project/node_modules/workflow/dist/internal/builtins.js', '/project')
* // => { moduleSpecifier: 'workflow/internal/builtins@4.0.0' }
*
* @example
* // File in workspace package
* resolveModuleSpecifier('/project/packages/shared/src/utils.ts', '/project')
* // => { moduleSpecifier: '@myorg/shared@0.0.0' }
@@ -149,9 +313,16 @@ export function resolveModuleSpecifier(
return { moduleSpecifier: undefined };
}

// Return the module specifier as "name@version"
// Resolve the export subpath (e.g., "/internal/builtins" for "workflow/internal/builtins")
const subpath = resolveExportSubpath(filePath, pkg);

// Return the module specifier as "name/subpath@version" or "name@version"
const specifier = subpath
? `${pkg.name}${subpath}@${pkg.version}`
: `${pkg.name}@${pkg.version}`;

return {
moduleSpecifier: `${pkg.name}@${pkg.version}`,
moduleSpecifier: specifier,
};
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new module specifier resolution logic, including the caching improvements, workspace package detection based on project dependencies, and subpath export resolution, lacks test coverage. Consider adding comprehensive tests to verify:

  1. The cache back-filling logic works correctly for nested directories
  2. Subpath export resolution correctly handles various export configurations (conditional exports, nested subpaths, etc.)
  3. The workspace package detection correctly filters out sibling apps in a monorepo
  4. The cache is properly invalidated when needed

Tests would help prevent regressions and document the expected behavior of these critical features.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +66
// Merge manifests from both bundles
const manifest = {
steps: { ...stepsManifest.steps, ...workflowsManifest.steps },
workflows: { ...stepsManifest.workflows, ...workflowsManifest.workflows },
classes: { ...stepsManifest.classes, ...workflowsManifest.classes },
};

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest merging order could potentially cause data loss if there are conflicting keys between the stepsManifest and workflowsManifest. The current implementation uses object spread syntax where workflowsManifest properties overwrite stepsManifest properties with the same key.

While this might be intentional, consider:

  1. Adding validation to detect and warn about conflicting IDs
  2. Documenting which manifest takes precedence and why
  3. Ensuring that the merge order is consistent across all builders (currently stepsManifest comes first in all implementations)

If steps and workflows are guaranteed to have non-overlapping IDs by design, this concern can be disregarded, but it would be helpful to document this assumption.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +130
function resolveExportSubpath(filePath: string, pkg: PackageInfo): string {
if (!pkg.exports || typeof pkg.exports !== 'object') {
return '';
}

// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

// Search through exports to find a matching subpath
for (const [subpath, target] of Object.entries(pkg.exports)) {
const resolvedTarget = resolveExportTarget(target);
if (
resolvedTarget &&
normalizeExportPath(resolvedTarget) === relativePath
) {
// Found a match - return the subpath without the leading "."
// e.g., "./internal/builtins" -> "/internal/builtins"
return subpath === '.' ? '' : subpath.substring(1);
}
}

return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function only handles exact export path matches and doesn't support wildcard patterns (e.g., "./internal/": "./dist/internal/.js"). This could lead to incorrect or missing subpath resolution for packages that use wildcard exports. Consider adding support for wildcard patterns by:

  1. Checking if the subpath contains an asterisk
  2. Converting it to a pattern match against the relative file path
  3. Extracting the matched portion to construct the correct subpath

If wildcard exports are not expected to be used in this codebase, this concern can be disregarded.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +114
// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function checks if the normalized file path starts with the package directory plus a forward slash (normalizedPkgDir + '/'). This check will fail for files that are exactly at the package root (e.g., when filePath equals pkg.dir), causing the function to return an empty string even though it should potentially match the root export (".").

While files at the exact package root are uncommon, consider handling this edge case explicitly:

  • Check if normalizedFilePath equals normalizedPkgDir and return the root export logic
  • Or adjust the substring logic to handle this case

This may not be a practical issue if package entry points are always in subdirectories.

Copilot uses AI. Check for mistakes.
Comment threadpackages/builders/src/module-specifier.ts
Comment threadpackages/builders/src/module-specifier.ts
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Addressed the following review comments in commit fce2e7d:

  1. Redundant package.json lookup in isWorkspacePackage - Now using pkg.dir directly from the findPackageJson result instead of walking up the directory tree again.

  2. Array exports support in resolveExportTarget - Added support for array exports by checking Array.isArray(target) and recursively resolving each element in the fallback chain, per the Node.js package exports specification.

… exports support
- Remove redundant directory walk in isWorkspacePackage by using pkg.dir from findPackageJson
- Add support for array exports in resolveExportTarget (fallback chains per Node.js spec)

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced manifest merging to properly combine results from both workflow and step bundles

Out of curiosity, what does this fix? "Enhance" sounds nice but doesn't tell me anything

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@VaguelySerious The step and workflow bundles generate their own manifests, which may have some values that do not overlap. For example, if I use a class only in step functions, but never reference it in the workflow bundle, then it would be missing from the workflow manifest. The fix was to ensure that all bundles are merged in the final form.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add subpath export resolution for package IDs by TooTallNate · Pull Request #901 · vercel/workflow · GitHub
Skip to content

Add subpath export resolution for package IDs - #901

Merged
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids
Feb 5, 2026
Merged

Add subpath export resolution for package IDs#901
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 30, 2026

Copy link
Copy Markdown
Member

Fixed a bug in module specifier resolution and added support for package subpath exports in workflow IDs.

What changed?

  • Fixed a caching bug in the module specifier resolution system that could cause incorrect IDs
  • Added support for subpath exports in package IDs (e.g., workflow/internal/builtins@4.0.0)
  • Improved module resolution by passing absolute file paths to the SWC transform
  • Enhanced manifest merging to properly combine results from both workflow and step bundles
  • Updated builders to return and merge manifests from both workflow and step bundles

How to test?

  1. Build a project that uses subpath exports in packages
  2. Verify that workflow IDs correctly include the subpath (e.g., workflow/internal/builtins@4.0.0)
  3. Test with a project that has multiple builds to ensure module specifier caching works correctly

Why make this change?

This change addresses an issue where the module specifier cache could return incorrect results, leading to inconsistent workflow IDs. It also adds support for packages with multiple entry points through subpath exports, ensuring that steps with the same name in different subpaths don't collide. This improves the reliability of cross-bundle references and makes the system more robust when working with complex package structures.

@changeset-bot

changeset-botBot commented Jan 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b81ae6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/sveltekitPatch
@workflow/buildersPatch
@workflow/nitroPatch
@workflow/nestPatch
@workflow/nextPatch
workflow-devkit-compiler-playgroundPatch
@workflow/astroPatch
@workflow/cliPatch
@workflow/rollupPatch
workflowPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/docs-typecheckPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production479038517
✅ 💻 Local Development438032470
✅ 📦 Local Production438032470
✅ 🐘 Local Postgres438032470
✅ 🪟 Windows470047
❌ 🌍 Community Worlds311690200
✅ 📋 Other129012141
Total20001691462315

❌ Failed Tests

🌍 Community Worlds (169 failed)

mongodb (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4304
✅ example4304
✅ express4304
✅ fastify4304
✅ hono4304
✅ nextjs-turbopack4601
✅ nextjs-webpack4601
✅ nitro4304
✅ nuxt4304
✅ sveltekit4304
✅ vite4304
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb5420
✅ redis-dev300
❌ redis5420
✅ starter-dev300
❌ starter4430
✅ turso-dev300
❌ turso5420
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4304
✅ e2e-local-postgres-nest-stable4304
✅ e2e-local-prod-nest-stable4304

📋 View full workflow run

@TooTallNate
TooTallNateforce-pushed the 01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids branch from 6cce073 to ca8bd01CompareJanuary 30, 2026 19:57
@github-actions

github-actionsBot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.041s (+3.5%)1.019s (~)0.978s101.00x
💻 LocalNitro0.043s (~)1.007s (~)0.964s101.06x
💻 LocalExpress0.044s (+4.8%)1.007s (~)0.963s101.08x
🐘 PostgresNitro0.212s (-6.9% 🟢)1.015s (~)0.803s105.19x
🐘 PostgresExpress0.290s (+4.2%)1.016s (~)0.726s107.10x
🐘 PostgresNext.js (Turbopack)0.360s (+28.8% 🔺)1.021s (~)0.660s108.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.712s (+6.4% 🔺)1.582s (-0.8%)0.870s101.00x
▲ VercelExpress0.793s (+7.0% 🔺)1.607s (~)0.814s101.11x
▲ VercelNext.js (Turbopack)0.870s (-1.1%)1.721s (-12.0% 🟢)0.851s101.22x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.094s (-0.7%)2.014s (~)0.920s101.00x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
💻 LocalExpress1.117s (~)2.007s (~)0.890s101.02x
🐘 PostgresNext.js (Turbopack)1.858s (+0.5%)2.420s (+20.1% 🔺)0.562s101.70x
🐘 PostgresExpress2.133s (-1.2%)3.014s (~)0.881s101.95x
🐘 PostgresNitro2.258s (+3.1%)3.017s (~)0.759s102.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.842s (-1.3%)3.742s (+1.1%)0.900s101.00x
▲ VercelExpress2.933s (~)3.724s (+4.6%)0.791s101.03x
▲ VercelNext.js (Turbopack)2.969s (+2.0%)3.755s (-1.8%)0.786s101.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.750s (~)11.018s (~)0.268s31.00x
💻 LocalExpress10.837s (~)11.012s (~)0.175s31.01x
💻 LocalNitro10.849s (~)11.010s (~)0.162s31.01x
🐘 PostgresNext.js (Turbopack)15.074s (-1.0%)16.033s (~)0.959s21.40x
🐘 PostgresNitro20.454s (~)21.038s (~)0.583s21.90x
🐘 PostgresExpress20.532s (~)21.032s (~)0.500s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)39.852s (+75.5% 🔺)40.274s (+71.8% 🔺)0.422s11.00x
▲ VercelNitro42.916s (+89.8% 🔺)43.893s (+88.1% 🔺)0.977s21.08x
▲ VercelExpress45.343s (+94.4% 🔺)46.170s (+88.5% 🔺)0.827s21.14x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)27.245s (~)28.038s (~)0.793s31.00x
💻 LocalNitro27.465s (~)28.022s (~)0.558s31.01x
💻 LocalExpress27.501s (~)28.025s (~)0.524s31.01x
🐘 PostgresNext.js (Turbopack)37.941s (+0.8%)38.052s (~)0.110s21.39x
🐘 PostgresNitro50.373s (~)51.096s (~)0.722s21.85x
🐘 PostgresExpress50.422s (~)51.076s (~)0.654s21.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express58.799s (+2.8%)60.259s (+4.0%)1.460s11.00x
▲ VercelNitro59.008s (+3.9%)59.284s (+3.6%)0.275s21.00x
▲ VercelNext.js (Turbopack)59.231s (+2.7%)59.787s (+2.4%)0.556s21.01x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)56.526s (~)57.053s (~)0.527s21.00x
💻 LocalNitro57.216s (~)58.049s (+0.9%)0.833s21.01x
💻 LocalExpress57.319s (~)58.040s (~)0.721s21.01x
🐘 PostgresNext.js (Turbopack)74.784s (-5.7% 🟢)75.602s (-5.0% 🟢)0.819s21.32x
🐘 PostgresNitro100.195s (~)101.169s (~)0.974s11.77x
🐘 PostgresExpress100.271s (~)101.172s (~)0.901s11.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro124.409s (+2.9%)125.651s (+2.6%)1.242s11.00x
▲ VercelNext.js (Turbopack)126.724s (-2.6%)127.329s (-2.5%)0.605s11.02x
▲ VercelExpress160.313s (+31.1% 🔺)160.881s (+30.5% 🔺)0.568s11.29x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.397s (~)2.006s (~)0.609s151.00x
💻 LocalNext.js (Turbopack)1.410s (+1.3%)2.012s (~)0.603s151.01x
💻 LocalExpress1.436s (+1.3%)2.007s (~)0.571s151.03x
🐘 PostgresNitro2.319s (-1.7%)3.014s (~)0.695s101.66x
🐘 PostgresNext.js (Turbopack)2.364s (+25.8% 🔺)2.744s (+8.8% 🔺)0.381s111.69x
🐘 PostgresExpress2.366s (-1.0%)3.013s (~)0.647s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.028s (+6.5% 🔺)3.878s (+4.9%)0.850s81.00x
▲ VercelNext.js (Turbopack)3.062s (+1.5%)3.920s (-1.9%)0.858s81.01x
▲ VercelExpress3.345s (+11.3% 🔺)4.184s (+7.4% 🔺)0.839s81.10x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.553s (-1.2%)3.047s (+0.7%)0.494s101.00x
💻 LocalNitro2.577s (+3.0%)3.017s (~)0.440s101.01x
💻 LocalExpress2.633s (+1.4%)3.022s (~)0.389s101.03x
🐘 PostgresExpress7.829s (-8.7% 🟢)8.578s (-8.0% 🟢)0.749s43.07x
🐘 PostgresNitro7.987s (+1.6%)8.594s (+0.5%)0.607s43.13x
🐘 PostgresNext.js (Turbopack)12.839s (+11.5% 🔺)13.360s (+11.1% 🔺)0.522s35.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.202s (+0.7%)3.973s (+3.3%)0.770s81.00x
▲ VercelNitro3.641s (+18.1% 🔺)4.390s (+14.8% 🔺)0.748s71.14x
▲ VercelNext.js (Turbopack)3.721s (-3.8%)4.425s (-4.6%)0.704s71.16x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)7.559s (+2.3%)8.274s (-2.2%)0.715s41.00x
💻 LocalExpress7.559s (+4.0%)8.515s (+3.1%)0.956s41.00x
💻 LocalNitro7.582s (+8.8% 🔺)8.529s (+13.7% 🔺)0.946s41.00x
🐘 PostgresNitro49.993s (+10.7% 🔺)50.305s (+8.9% 🔺)0.312s16.61x
🐘 PostgresExpress51.058s (+4.2%)51.328s (+4.0%)0.270s16.75x
🐘 PostgresNext.js (Turbopack)55.261s (+8.2% 🔺)56.160s (+9.6% 🔺)0.899s17.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.388s (-17.2% 🟢)4.017s (-12.6% 🟢)0.629s81.00x
▲ VercelExpress3.464s (+3.6%)4.103s (+8.6% 🔺)0.639s81.02x
▲ VercelNext.js (Turbopack)3.996s (+6.8% 🔺)5.070s (+14.2% 🔺)1.074s61.18x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.406s (-1.9%)2.009s (~)0.603s151.00x
💻 LocalExpress1.468s (+2.8%)2.008s (~)0.541s151.04x
💻 LocalNitro1.468s (+1.5%)2.007s (~)0.538s151.04x
🐘 PostgresNitro2.169s (-5.7% 🟢)2.598s (-3.0%)0.429s121.54x
🐘 PostgresExpress2.216s (+5.5% 🔺)2.598s (+3.2%)0.382s121.58x
🐘 PostgresNext.js (Turbopack)2.274s (+26.6% 🔺)2.755s (+36.9% 🔺)0.481s111.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.763s (~)3.700s (~)0.937s91.00x
▲ VercelNext.js (Turbopack)2.913s (+1.4%)3.795s (-0.7%)0.883s81.05x
▲ VercelExpress2.986s (+4.9%)3.868s (+2.8%)0.882s81.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.663s (+1.8%)3.010s (~)0.347s101.00x
💻 LocalNext.js (Turbopack)2.674s (-0.5%)3.034s (~)0.359s101.00x
💻 LocalExpress2.691s (~)3.012s (~)0.321s101.01x
🐘 PostgresExpress11.815s (+8.9% 🔺)12.361s (+11.5% 🔺)0.546s34.44x
🐘 PostgresNitro12.124s (+12.5% 🔺)12.721s (+15.2% 🔺)0.597s34.55x
🐘 PostgresNext.js (Turbopack)13.147s (+19.7% 🔺)13.723s (+17.4% 🔺)0.576s34.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.906s (-1.7%)3.716s (+1.8%)0.810s91.00x
▲ VercelNitro3.044s (-3.0%)3.719s (-2.6%)0.676s91.05x
▲ VercelNext.js (Turbopack)3.066s (+1.2%)3.739s (-1.9%)0.674s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express7.910s (+1.1%)8.661s (-1.7%)0.751s41.00x
💻 LocalNitro7.971s (+4.9%)8.877s (+3.3%)0.906s41.01x
💻 LocalNext.js (Turbopack)8.245s (+7.8% 🔺)9.068s (+5.2% 🔺)0.823s41.04x
🐘 PostgresNitro51.279s (~)52.131s (~)0.852s16.48x
🐘 PostgresExpress51.446s (~)52.249s (~)0.803s16.50x
🐘 PostgresNext.js (Turbopack)56.929s (+4.7%)57.297s (+3.9%)0.368s17.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.257s (+5.0%)3.868s (+2.6%)0.611s81.00x
▲ VercelExpress3.722s (+11.2% 🔺)4.332s (+10.0% 🔺)0.610s81.14x
▲ VercelNext.js (Turbopack)4.022s (+13.3% 🔺)4.880s (+12.5% 🔺)0.857s71.24x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.145s (-1.4%)1.003s (~)0.016s (-8.0% 🟢)1.028s (~)0.883s101.00x
💻 LocalNitro0.182s (+2.6%)0.992s (~)0.015s (+7.3% 🔺)1.021s (~)0.839s101.26x
💻 LocalExpress0.185s (-1.4%)0.992s (~)0.014s (-4.2%)1.021s (~)0.836s101.28x
🐘 PostgresNext.js (Turbopack)0.751s (+5.0%)0.856s (-12.4% 🟢)0.000s (NaN%)1.017s (-8.8% 🟢)0.266s105.19x
🐘 PostgresNitro2.375s (+59.7% 🔺)2.673s (+61.2% 🔺)0.000s (+Infinity% 🔺)3.018s (+50.0% 🔺)0.643s1016.41x
🐘 PostgresExpress2.428s (~)2.618s (~)0.000s (+Infinity% 🔺)3.017s (~)0.589s1016.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.068s (~)3.350s (-1.4%)0.229s (+60.6% 🔺)4.071s (+1.1%)1.003s101.00x
▲ VercelNext.js (Turbopack)3.238s (+4.7%)3.502s (+6.7% 🔺)0.270s (+46.9% 🔺)4.319s (+8.5% 🔺)1.082s101.06x
▲ VercelExpress3.349s (+9.3% 🔺)3.477s (+3.5%)0.188s (-11.9% 🟢)4.230s (+3.9%)0.881s101.09x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNitro5/12
▲ VercelNitro8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)💻 Local10/12
Nitro💻 Local10/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a caching bug in the module specifier resolution system and adds support for package subpath exports in workflow IDs. The changes improve the reliability of cross-bundle references and enable proper handling of packages with multiple entry points.

Changes:

  • Fixed module specifier cache back-filling to prevent incorrect IDs across multiple lookups
  • Added subpath export resolution to support packages like workflow/internal/builtins@4.0.0
  • Enhanced workspace package detection to filter out sibling apps in monorepos based on project dependencies
  • Updated all builders to properly merge manifests from both workflow and step bundles
  • Added Windows path normalization support in the Rust transform plugin
  • Passed absolute file paths to SWC transform for accurate module specifier resolution

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
packages/builders/src/module-specifier.tsCore changes: cache back-filling, subpath export resolution, workspace package detection improvements
packages/builders/src/apply-swc-transform.tsAdded absolutePath parameter for accurate module specifier resolution
packages/builders/src/swc-esbuild-plugin.tsPasses absolute path to transform function
packages/builders/src/base-builder.tsUpdated createWorkflowsBundle return type to include manifest
packages/sveltekit/src/builder.tsUpdated to merge manifests from both bundles
packages/nitro/src/builders.tsUpdated to merge manifests from both bundles
packages/next/src/builder.tsUpdated to merge manifests and handle optional context properties
packages/nest/src/builder.tsUpdated to merge manifests from both bundles
packages/builders/src/vercel-build-output-api.tsUpdated to merge manifests from both bundles
packages/builders/src/standalone.tsAdded mergeManifests helper and updated bundle methods
packages/swc-plugin-workflow/transform/src/naming.rsAdded Windows path normalization with tests
packages/swc-plugin-workflow/transform/src/lib.rsEnhanced builtin function comment documentation
packages/swc-plugin-workflow/spec.mdUpdated documentation for subpath exports
.changeset/afraid-candies-find.mdAdded changeset entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 37 to 327
@@ -69,9 +176,56 @@ function isInNodeModules(filePath: string): boolean {
}

/**
* Check if a file path is inside a workspace package.
* Cache for project dependencies to avoid repeated filesystem reads.
* Maps project root to set of dependency package names.
*/
const projectDepsCache = new Map<string, Set<string>>();

/**
* Get all dependencies (including devDependencies) for a project.
*/
function getProjectDependencies(projectRoot: string): Set<string> {
const cached = projectDepsCache.get(projectRoot);
if (cached) {
return cached;
}

const deps = new Set<string>();
const pkgPath = join(projectRoot, 'package.json');

if (existsSync(pkgPath)) {
try {
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content);

// Collect all dependency types
for (const depType of [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
]) {
const depObj = parsed[depType];
if (depObj && typeof depObj === 'object') {
for (const name of Object.keys(depObj)) {
deps.add(name);
}
}
}
} catch {
// Invalid JSON or file not readable
}
}

projectDepsCache.set(projectRoot, deps);
return deps;
}

/**
* Check if a file path is inside a workspace package that is a dependency of the project.
* This is a heuristic - we check if the file is in a directory with a package.json
* that has a "name" field, but is NOT in node_modules.
* that has a "name" field, is NOT in node_modules, and is listed as a dependency
* of the project.
*/
function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (isInNodeModules(filePath)) {
@@ -97,8 +251,13 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (resolve(pkgPath) === rootPkgPath) {
return false;
}
// Found a package.json that's not the root - it's a workspace package
return true;

// Found a package.json that's not the root.
// Only treat it as a workspace package if it's actually a dependency
// of the current project. This prevents sibling apps in a monorepo
// from being incorrectly treated as importable packages.
const projectDeps = getProjectDependencies(projectRoot);
return projectDeps.has(pkg.name);
}
dir = dirname(dir);
}
@@ -114,11 +273,16 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
* @returns The module specifier result
*
* @example
* // File in node_modules
* // File in node_modules (root export)
* resolveModuleSpecifier('/project/node_modules/point/dist/index.js', '/project')
* // => { moduleSpecifier: 'point@1.0.0' }
*
* @example
* // File in node_modules (subpath export)
* resolveModuleSpecifier('/project/node_modules/workflow/dist/internal/builtins.js', '/project')
* // => { moduleSpecifier: 'workflow/internal/builtins@4.0.0' }
*
* @example
* // File in workspace package
* resolveModuleSpecifier('/project/packages/shared/src/utils.ts', '/project')
* // => { moduleSpecifier: '@myorg/shared@0.0.0' }
@@ -149,9 +313,16 @@ export function resolveModuleSpecifier(
return { moduleSpecifier: undefined };
}

// Return the module specifier as "name@version"
// Resolve the export subpath (e.g., "/internal/builtins" for "workflow/internal/builtins")
const subpath = resolveExportSubpath(filePath, pkg);

// Return the module specifier as "name/subpath@version" or "name@version"
const specifier = subpath
? `${pkg.name}${subpath}@${pkg.version}`
: `${pkg.name}@${pkg.version}`;

return {
moduleSpecifier: `${pkg.name}@${pkg.version}`,
moduleSpecifier: specifier,
};
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new module specifier resolution logic, including the caching improvements, workspace package detection based on project dependencies, and subpath export resolution, lacks test coverage. Consider adding comprehensive tests to verify:

  1. The cache back-filling logic works correctly for nested directories
  2. Subpath export resolution correctly handles various export configurations (conditional exports, nested subpaths, etc.)
  3. The workspace package detection correctly filters out sibling apps in a monorepo
  4. The cache is properly invalidated when needed

Tests would help prevent regressions and document the expected behavior of these critical features.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +66
// Merge manifests from both bundles
const manifest = {
steps: { ...stepsManifest.steps, ...workflowsManifest.steps },
workflows: { ...stepsManifest.workflows, ...workflowsManifest.workflows },
classes: { ...stepsManifest.classes, ...workflowsManifest.classes },
};

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest merging order could potentially cause data loss if there are conflicting keys between the stepsManifest and workflowsManifest. The current implementation uses object spread syntax where workflowsManifest properties overwrite stepsManifest properties with the same key.

While this might be intentional, consider:

  1. Adding validation to detect and warn about conflicting IDs
  2. Documenting which manifest takes precedence and why
  3. Ensuring that the merge order is consistent across all builders (currently stepsManifest comes first in all implementations)

If steps and workflows are guaranteed to have non-overlapping IDs by design, this concern can be disregarded, but it would be helpful to document this assumption.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +130
function resolveExportSubpath(filePath: string, pkg: PackageInfo): string {
if (!pkg.exports || typeof pkg.exports !== 'object') {
return '';
}

// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

// Search through exports to find a matching subpath
for (const [subpath, target] of Object.entries(pkg.exports)) {
const resolvedTarget = resolveExportTarget(target);
if (
resolvedTarget &&
normalizeExportPath(resolvedTarget) === relativePath
) {
// Found a match - return the subpath without the leading "."
// e.g., "./internal/builtins" -> "/internal/builtins"
return subpath === '.' ? '' : subpath.substring(1);
}
}

return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function only handles exact export path matches and doesn't support wildcard patterns (e.g., "./internal/": "./dist/internal/.js"). This could lead to incorrect or missing subpath resolution for packages that use wildcard exports. Consider adding support for wildcard patterns by:

  1. Checking if the subpath contains an asterisk
  2. Converting it to a pattern match against the relative file path
  3. Extracting the matched portion to construct the correct subpath

If wildcard exports are not expected to be used in this codebase, this concern can be disregarded.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +114
// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function checks if the normalized file path starts with the package directory plus a forward slash (normalizedPkgDir + '/'). This check will fail for files that are exactly at the package root (e.g., when filePath equals pkg.dir), causing the function to return an empty string even though it should potentially match the root export (".").

While files at the exact package root are uncommon, consider handling this edge case explicitly:

  • Check if normalizedFilePath equals normalizedPkgDir and return the root export logic
  • Or adjust the substring logic to handle this case

This may not be a practical issue if package entry points are always in subdirectories.

Copilot uses AI. Check for mistakes.
Comment threadpackages/builders/src/module-specifier.ts
Comment threadpackages/builders/src/module-specifier.ts
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Addressed the following review comments in commit fce2e7d:

  1. Redundant package.json lookup in isWorkspacePackage - Now using pkg.dir directly from the findPackageJson result instead of walking up the directory tree again.

  2. Array exports support in resolveExportTarget - Added support for array exports by checking Array.isArray(target) and recursively resolving each element in the fallback chain, per the Node.js package exports specification.

… exports support
- Remove redundant directory walk in isWorkspacePackage by using pkg.dir from findPackageJson
- Add support for array exports in resolveExportTarget (fallback chains per Node.js spec)

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced manifest merging to properly combine results from both workflow and step bundles

Out of curiosity, what does this fix? "Enhance" sounds nice but doesn't tell me anything

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@VaguelySerious The step and workflow bundles generate their own manifests, which may have some values that do not overlap. For example, if I use a class only in step functions, but never reference it in the workflow bundle, then it would be missing from the workflow manifest. The fix was to ensure that all bundles are merged in the final form.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Add subpath export resolution for package IDs by TooTallNate · Pull Request #901 · vercel/workflow · GitHub
Skip to content

Add subpath export resolution for package IDs - #901

Merged
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids
Feb 5, 2026
Merged

Add subpath export resolution for package IDs#901
TooTallNate merged 8 commits into
mainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 30, 2026

Copy link
Copy Markdown
Member

Fixed a bug in module specifier resolution and added support for package subpath exports in workflow IDs.

What changed?

  • Fixed a caching bug in the module specifier resolution system that could cause incorrect IDs
  • Added support for subpath exports in package IDs (e.g., workflow/internal/builtins@4.0.0)
  • Improved module resolution by passing absolute file paths to the SWC transform
  • Enhanced manifest merging to properly combine results from both workflow and step bundles
  • Updated builders to return and merge manifests from both workflow and step bundles

How to test?

  1. Build a project that uses subpath exports in packages
  2. Verify that workflow IDs correctly include the subpath (e.g., workflow/internal/builtins@4.0.0)
  3. Test with a project that has multiple builds to ensure module specifier caching works correctly

Why make this change?

This change addresses an issue where the module specifier cache could return incorrect results, leading to inconsistent workflow IDs. It also adds support for packages with multiple entry points through subpath exports, ensuring that steps with the same name in different subpaths don't collide. This improves the reliability of cross-bundle references and makes the system more robust when working with complex package structures.

@changeset-bot

changeset-botBot commented Jan 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b81ae6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/sveltekitPatch
@workflow/buildersPatch
@workflow/nitroPatch
@workflow/nestPatch
@workflow/nextPatch
workflow-devkit-compiler-playgroundPatch
@workflow/astroPatch
@workflow/cliPatch
@workflow/rollupPatch
workflowPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/docs-typecheckPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production479038517
✅ 💻 Local Development438032470
✅ 📦 Local Production438032470
✅ 🐘 Local Postgres438032470
✅ 🪟 Windows470047
❌ 🌍 Community Worlds311690200
✅ 📋 Other129012141
Total20001691462315

❌ Failed Tests

🌍 Community Worlds (169 failed)

mongodb (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (43 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (42 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro4304
✅ example4304
✅ express4304
✅ fastify4304
✅ hono4304
✅ nextjs-turbopack4601
✅ nextjs-webpack4601
✅ nitro4304
✅ nuxt4304
✅ sveltekit4304
✅ vite4304
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable4304
✅ express-stable4304
✅ fastify-stable4304
✅ hono-stable4304
✅ nextjs-turbopack-stable4700
✅ nextjs-webpack-stable4700
✅ nitro-stable4304
✅ nuxt-stable4304
✅ sveltekit-stable4304
✅ vite-stable4304
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4700
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb5420
✅ redis-dev300
❌ redis5420
✅ starter-dev300
❌ starter4430
✅ turso-dev300
❌ turso5420
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable4304
✅ e2e-local-postgres-nest-stable4304
✅ e2e-local-prod-nest-stable4304

📋 View full workflow run

@TooTallNate
TooTallNateforce-pushed the 01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids branch from 6cce073 to ca8bd01CompareJanuary 30, 2026 19:57
@github-actions

github-actionsBot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.041s (+3.5%)1.019s (~)0.978s101.00x
💻 LocalNitro0.043s (~)1.007s (~)0.964s101.06x
💻 LocalExpress0.044s (+4.8%)1.007s (~)0.963s101.08x
🐘 PostgresNitro0.212s (-6.9% 🟢)1.015s (~)0.803s105.19x
🐘 PostgresExpress0.290s (+4.2%)1.016s (~)0.726s107.10x
🐘 PostgresNext.js (Turbopack)0.360s (+28.8% 🔺)1.021s (~)0.660s108.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.712s (+6.4% 🔺)1.582s (-0.8%)0.870s101.00x
▲ VercelExpress0.793s (+7.0% 🔺)1.607s (~)0.814s101.11x
▲ VercelNext.js (Turbopack)0.870s (-1.1%)1.721s (-12.0% 🟢)0.851s101.22x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.094s (-0.7%)2.014s (~)0.920s101.00x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
💻 LocalExpress1.117s (~)2.007s (~)0.890s101.02x
🐘 PostgresNext.js (Turbopack)1.858s (+0.5%)2.420s (+20.1% 🔺)0.562s101.70x
🐘 PostgresExpress2.133s (-1.2%)3.014s (~)0.881s101.95x
🐘 PostgresNitro2.258s (+3.1%)3.017s (~)0.759s102.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.842s (-1.3%)3.742s (+1.1%)0.900s101.00x
▲ VercelExpress2.933s (~)3.724s (+4.6%)0.791s101.03x
▲ VercelNext.js (Turbopack)2.969s (+2.0%)3.755s (-1.8%)0.786s101.04x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.750s (~)11.018s (~)0.268s31.00x
💻 LocalExpress10.837s (~)11.012s (~)0.175s31.01x
💻 LocalNitro10.849s (~)11.010s (~)0.162s31.01x
🐘 PostgresNext.js (Turbopack)15.074s (-1.0%)16.033s (~)0.959s21.40x
🐘 PostgresNitro20.454s (~)21.038s (~)0.583s21.90x
🐘 PostgresExpress20.532s (~)21.032s (~)0.500s21.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)39.852s (+75.5% 🔺)40.274s (+71.8% 🔺)0.422s11.00x
▲ VercelNitro42.916s (+89.8% 🔺)43.893s (+88.1% 🔺)0.977s21.08x
▲ VercelExpress45.343s (+94.4% 🔺)46.170s (+88.5% 🔺)0.827s21.14x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)27.245s (~)28.038s (~)0.793s31.00x
💻 LocalNitro27.465s (~)28.022s (~)0.558s31.01x
💻 LocalExpress27.501s (~)28.025s (~)0.524s31.01x
🐘 PostgresNext.js (Turbopack)37.941s (+0.8%)38.052s (~)0.110s21.39x
🐘 PostgresNitro50.373s (~)51.096s (~)0.722s21.85x
🐘 PostgresExpress50.422s (~)51.076s (~)0.654s21.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express58.799s (+2.8%)60.259s (+4.0%)1.460s11.00x
▲ VercelNitro59.008s (+3.9%)59.284s (+3.6%)0.275s21.00x
▲ VercelNext.js (Turbopack)59.231s (+2.7%)59.787s (+2.4%)0.556s21.01x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)56.526s (~)57.053s (~)0.527s21.00x
💻 LocalNitro57.216s (~)58.049s (+0.9%)0.833s21.01x
💻 LocalExpress57.319s (~)58.040s (~)0.721s21.01x
🐘 PostgresNext.js (Turbopack)74.784s (-5.7% 🟢)75.602s (-5.0% 🟢)0.819s21.32x
🐘 PostgresNitro100.195s (~)101.169s (~)0.974s11.77x
🐘 PostgresExpress100.271s (~)101.172s (~)0.901s11.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro124.409s (+2.9%)125.651s (+2.6%)1.242s11.00x
▲ VercelNext.js (Turbopack)126.724s (-2.6%)127.329s (-2.5%)0.605s11.02x
▲ VercelExpress160.313s (+31.1% 🔺)160.881s (+30.5% 🔺)0.568s11.29x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.397s (~)2.006s (~)0.609s151.00x
💻 LocalNext.js (Turbopack)1.410s (+1.3%)2.012s (~)0.603s151.01x
💻 LocalExpress1.436s (+1.3%)2.007s (~)0.571s151.03x
🐘 PostgresNitro2.319s (-1.7%)3.014s (~)0.695s101.66x
🐘 PostgresNext.js (Turbopack)2.364s (+25.8% 🔺)2.744s (+8.8% 🔺)0.381s111.69x
🐘 PostgresExpress2.366s (-1.0%)3.013s (~)0.647s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.028s (+6.5% 🔺)3.878s (+4.9%)0.850s81.00x
▲ VercelNext.js (Turbopack)3.062s (+1.5%)3.920s (-1.9%)0.858s81.01x
▲ VercelExpress3.345s (+11.3% 🔺)4.184s (+7.4% 🔺)0.839s81.10x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.553s (-1.2%)3.047s (+0.7%)0.494s101.00x
💻 LocalNitro2.577s (+3.0%)3.017s (~)0.440s101.01x
💻 LocalExpress2.633s (+1.4%)3.022s (~)0.389s101.03x
🐘 PostgresExpress7.829s (-8.7% 🟢)8.578s (-8.0% 🟢)0.749s43.07x
🐘 PostgresNitro7.987s (+1.6%)8.594s (+0.5%)0.607s43.13x
🐘 PostgresNext.js (Turbopack)12.839s (+11.5% 🔺)13.360s (+11.1% 🔺)0.522s35.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.202s (+0.7%)3.973s (+3.3%)0.770s81.00x
▲ VercelNitro3.641s (+18.1% 🔺)4.390s (+14.8% 🔺)0.748s71.14x
▲ VercelNext.js (Turbopack)3.721s (-3.8%)4.425s (-4.6%)0.704s71.16x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)7.559s (+2.3%)8.274s (-2.2%)0.715s41.00x
💻 LocalExpress7.559s (+4.0%)8.515s (+3.1%)0.956s41.00x
💻 LocalNitro7.582s (+8.8% 🔺)8.529s (+13.7% 🔺)0.946s41.00x
🐘 PostgresNitro49.993s (+10.7% 🔺)50.305s (+8.9% 🔺)0.312s16.61x
🐘 PostgresExpress51.058s (+4.2%)51.328s (+4.0%)0.270s16.75x
🐘 PostgresNext.js (Turbopack)55.261s (+8.2% 🔺)56.160s (+9.6% 🔺)0.899s17.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.388s (-17.2% 🟢)4.017s (-12.6% 🟢)0.629s81.00x
▲ VercelExpress3.464s (+3.6%)4.103s (+8.6% 🔺)0.639s81.02x
▲ VercelNext.js (Turbopack)3.996s (+6.8% 🔺)5.070s (+14.2% 🔺)1.074s61.18x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.406s (-1.9%)2.009s (~)0.603s151.00x
💻 LocalExpress1.468s (+2.8%)2.008s (~)0.541s151.04x
💻 LocalNitro1.468s (+1.5%)2.007s (~)0.538s151.04x
🐘 PostgresNitro2.169s (-5.7% 🟢)2.598s (-3.0%)0.429s121.54x
🐘 PostgresExpress2.216s (+5.5% 🔺)2.598s (+3.2%)0.382s121.58x
🐘 PostgresNext.js (Turbopack)2.274s (+26.6% 🔺)2.755s (+36.9% 🔺)0.481s111.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.763s (~)3.700s (~)0.937s91.00x
▲ VercelNext.js (Turbopack)2.913s (+1.4%)3.795s (-0.7%)0.883s81.05x
▲ VercelExpress2.986s (+4.9%)3.868s (+2.8%)0.882s81.08x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro2.663s (+1.8%)3.010s (~)0.347s101.00x
💻 LocalNext.js (Turbopack)2.674s (-0.5%)3.034s (~)0.359s101.00x
💻 LocalExpress2.691s (~)3.012s (~)0.321s101.01x
🐘 PostgresExpress11.815s (+8.9% 🔺)12.361s (+11.5% 🔺)0.546s34.44x
🐘 PostgresNitro12.124s (+12.5% 🔺)12.721s (+15.2% 🔺)0.597s34.55x
🐘 PostgresNext.js (Turbopack)13.147s (+19.7% 🔺)13.723s (+17.4% 🔺)0.576s34.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.906s (-1.7%)3.716s (+1.8%)0.810s91.00x
▲ VercelNitro3.044s (-3.0%)3.719s (-2.6%)0.676s91.05x
▲ VercelNext.js (Turbopack)3.066s (+1.2%)3.739s (-1.9%)0.674s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express7.910s (+1.1%)8.661s (-1.7%)0.751s41.00x
💻 LocalNitro7.971s (+4.9%)8.877s (+3.3%)0.906s41.01x
💻 LocalNext.js (Turbopack)8.245s (+7.8% 🔺)9.068s (+5.2% 🔺)0.823s41.04x
🐘 PostgresNitro51.279s (~)52.131s (~)0.852s16.48x
🐘 PostgresExpress51.446s (~)52.249s (~)0.803s16.50x
🐘 PostgresNext.js (Turbopack)56.929s (+4.7%)57.297s (+3.9%)0.368s17.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.257s (+5.0%)3.868s (+2.6%)0.611s81.00x
▲ VercelExpress3.722s (+11.2% 🔺)4.332s (+10.0% 🔺)0.610s81.14x
▲ VercelNext.js (Turbopack)4.022s (+13.3% 🔺)4.880s (+12.5% 🔺)0.857s71.24x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.145s (-1.4%)1.003s (~)0.016s (-8.0% 🟢)1.028s (~)0.883s101.00x
💻 LocalNitro0.182s (+2.6%)0.992s (~)0.015s (+7.3% 🔺)1.021s (~)0.839s101.26x
💻 LocalExpress0.185s (-1.4%)0.992s (~)0.014s (-4.2%)1.021s (~)0.836s101.28x
🐘 PostgresNext.js (Turbopack)0.751s (+5.0%)0.856s (-12.4% 🟢)0.000s (NaN%)1.017s (-8.8% 🟢)0.266s105.19x
🐘 PostgresNitro2.375s (+59.7% 🔺)2.673s (+61.2% 🔺)0.000s (+Infinity% 🔺)3.018s (+50.0% 🔺)0.643s1016.41x
🐘 PostgresExpress2.428s (~)2.618s (~)0.000s (+Infinity% 🔺)3.017s (~)0.589s1016.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.068s (~)3.350s (-1.4%)0.229s (+60.6% 🔺)4.071s (+1.1%)1.003s101.00x
▲ VercelNext.js (Turbopack)3.238s (+4.7%)3.502s (+6.7% 🔺)0.270s (+46.9% 🔺)4.319s (+8.5% 🔺)1.082s101.06x
▲ VercelExpress3.349s (+9.3% 🔺)3.477s (+3.5%)0.188s (-11.9% 🟢)4.230s (+3.9%)0.881s101.09x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)9/12
🐘 PostgresNitro5/12
▲ VercelNitro8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local10/12
Next.js (Turbopack)💻 Local10/12
Nitro💻 Local10/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a caching bug in the module specifier resolution system and adds support for package subpath exports in workflow IDs. The changes improve the reliability of cross-bundle references and enable proper handling of packages with multiple entry points.

Changes:

  • Fixed module specifier cache back-filling to prevent incorrect IDs across multiple lookups
  • Added subpath export resolution to support packages like workflow/internal/builtins@4.0.0
  • Enhanced workspace package detection to filter out sibling apps in monorepos based on project dependencies
  • Updated all builders to properly merge manifests from both workflow and step bundles
  • Added Windows path normalization support in the Rust transform plugin
  • Passed absolute file paths to SWC transform for accurate module specifier resolution

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
packages/builders/src/module-specifier.tsCore changes: cache back-filling, subpath export resolution, workspace package detection improvements
packages/builders/src/apply-swc-transform.tsAdded absolutePath parameter for accurate module specifier resolution
packages/builders/src/swc-esbuild-plugin.tsPasses absolute path to transform function
packages/builders/src/base-builder.tsUpdated createWorkflowsBundle return type to include manifest
packages/sveltekit/src/builder.tsUpdated to merge manifests from both bundles
packages/nitro/src/builders.tsUpdated to merge manifests from both bundles
packages/next/src/builder.tsUpdated to merge manifests and handle optional context properties
packages/nest/src/builder.tsUpdated to merge manifests from both bundles
packages/builders/src/vercel-build-output-api.tsUpdated to merge manifests from both bundles
packages/builders/src/standalone.tsAdded mergeManifests helper and updated bundle methods
packages/swc-plugin-workflow/transform/src/naming.rsAdded Windows path normalization with tests
packages/swc-plugin-workflow/transform/src/lib.rsEnhanced builtin function comment documentation
packages/swc-plugin-workflow/spec.mdUpdated documentation for subpath exports
.changeset/afraid-candies-find.mdAdded changeset entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 37 to 327
@@ -69,9 +176,56 @@ function isInNodeModules(filePath: string): boolean {
}

/**
* Check if a file path is inside a workspace package.
* Cache for project dependencies to avoid repeated filesystem reads.
* Maps project root to set of dependency package names.
*/
const projectDepsCache = new Map<string, Set<string>>();

/**
* Get all dependencies (including devDependencies) for a project.
*/
function getProjectDependencies(projectRoot: string): Set<string> {
const cached = projectDepsCache.get(projectRoot);
if (cached) {
return cached;
}

const deps = new Set<string>();
const pkgPath = join(projectRoot, 'package.json');

if (existsSync(pkgPath)) {
try {
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content);

// Collect all dependency types
for (const depType of [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
]) {
const depObj = parsed[depType];
if (depObj && typeof depObj === 'object') {
for (const name of Object.keys(depObj)) {
deps.add(name);
}
}
}
} catch {
// Invalid JSON or file not readable
}
}

projectDepsCache.set(projectRoot, deps);
return deps;
}

/**
* Check if a file path is inside a workspace package that is a dependency of the project.
* This is a heuristic - we check if the file is in a directory with a package.json
* that has a "name" field, but is NOT in node_modules.
* that has a "name" field, is NOT in node_modules, and is listed as a dependency
* of the project.
*/
function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (isInNodeModules(filePath)) {
@@ -97,8 +251,13 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
if (resolve(pkgPath) === rootPkgPath) {
return false;
}
// Found a package.json that's not the root - it's a workspace package
return true;

// Found a package.json that's not the root.
// Only treat it as a workspace package if it's actually a dependency
// of the current project. This prevents sibling apps in a monorepo
// from being incorrectly treated as importable packages.
const projectDeps = getProjectDependencies(projectRoot);
return projectDeps.has(pkg.name);
}
dir = dirname(dir);
}
@@ -114,11 +273,16 @@ function isWorkspacePackage(filePath: string, projectRoot: string): boolean {
* @returns The module specifier result
*
* @example
* // File in node_modules
* // File in node_modules (root export)
* resolveModuleSpecifier('/project/node_modules/point/dist/index.js', '/project')
* // => { moduleSpecifier: 'point@1.0.0' }
*
* @example
* // File in node_modules (subpath export)
* resolveModuleSpecifier('/project/node_modules/workflow/dist/internal/builtins.js', '/project')
* // => { moduleSpecifier: 'workflow/internal/builtins@4.0.0' }
*
* @example
* // File in workspace package
* resolveModuleSpecifier('/project/packages/shared/src/utils.ts', '/project')
* // => { moduleSpecifier: '@myorg/shared@0.0.0' }
@@ -149,9 +313,16 @@ export function resolveModuleSpecifier(
return { moduleSpecifier: undefined };
}

// Return the module specifier as "name@version"
// Resolve the export subpath (e.g., "/internal/builtins" for "workflow/internal/builtins")
const subpath = resolveExportSubpath(filePath, pkg);

// Return the module specifier as "name/subpath@version" or "name@version"
const specifier = subpath
? `${pkg.name}${subpath}@${pkg.version}`
: `${pkg.name}@${pkg.version}`;

return {
moduleSpecifier: `${pkg.name}@${pkg.version}`,
moduleSpecifier: specifier,
};
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new module specifier resolution logic, including the caching improvements, workspace package detection based on project dependencies, and subpath export resolution, lacks test coverage. Consider adding comprehensive tests to verify:

  1. The cache back-filling logic works correctly for nested directories
  2. Subpath export resolution correctly handles various export configurations (conditional exports, nested subpaths, etc.)
  3. The workspace package detection correctly filters out sibling apps in a monorepo
  4. The cache is properly invalidated when needed

Tests would help prevent regressions and document the expected behavior of these critical features.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +66
// Merge manifests from both bundles
const manifest = {
steps: { ...stepsManifest.steps, ...workflowsManifest.steps },
workflows: { ...stepsManifest.workflows, ...workflowsManifest.workflows },
classes: { ...stepsManifest.classes, ...workflowsManifest.classes },
};

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest merging order could potentially cause data loss if there are conflicting keys between the stepsManifest and workflowsManifest. The current implementation uses object spread syntax where workflowsManifest properties overwrite stepsManifest properties with the same key.

While this might be intentional, consider:

  1. Adding validation to detect and warn about conflicting IDs
  2. Documenting which manifest takes precedence and why
  3. Ensuring that the merge order is consistent across all builders (currently stepsManifest comes first in all implementations)

If steps and workflows are guaranteed to have non-overlapping IDs by design, this concern can be disregarded, but it would be helpful to document this assumption.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +130
function resolveExportSubpath(filePath: string, pkg: PackageInfo): string {
if (!pkg.exports || typeof pkg.exports !== 'object') {
return '';
}

// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

// Search through exports to find a matching subpath
for (const [subpath, target] of Object.entries(pkg.exports)) {
const resolvedTarget = resolveExportTarget(target);
if (
resolvedTarget &&
normalizeExportPath(resolvedTarget) === relativePath
) {
// Found a match - return the subpath without the leading "."
// e.g., "./internal/builtins" -> "/internal/builtins"
return subpath === '.' ? '' : subpath.substring(1);
}
}

return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function only handles exact export path matches and doesn't support wildcard patterns (e.g., "./internal/": "./dist/internal/.js"). This could lead to incorrect or missing subpath resolution for packages that use wildcard exports. Consider adding support for wildcard patterns by:

  1. Checking if the subpath contains an asterisk
  2. Converting it to a pattern match against the relative file path
  3. Extracting the matched portion to construct the correct subpath

If wildcard exports are not expected to be used in this codebase, this concern can be disregarded.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +114
// Get the relative path from package root to the file
const normalizedFilePath = filePath.replace(/\\/g, '/');
const normalizedPkgDir = pkg.dir.replace(/\\/g, '/');
const relativePath = normalizedFilePath.startsWith(normalizedPkgDir + '/')
? './' + normalizedFilePath.substring(normalizedPkgDir.length + 1)
: null;

if (!relativePath) {
return '';
}

CopilotAIFeb 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveExportSubpath function checks if the normalized file path starts with the package directory plus a forward slash (normalizedPkgDir + '/'). This check will fail for files that are exactly at the package root (e.g., when filePath equals pkg.dir), causing the function to return an empty string even though it should potentially match the root export (".").

While files at the exact package root are uncommon, consider handling this edge case explicitly:

  • Check if normalizedFilePath equals normalizedPkgDir and return the root export logic
  • Or adjust the substring logic to handle this case

This may not be a practical issue if package entry points are always in subdirectories.

Copilot uses AI. Check for mistakes.
Comment threadpackages/builders/src/module-specifier.ts
Comment threadpackages/builders/src/module-specifier.ts
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

Addressed the following review comments in commit fce2e7d:

  1. Redundant package.json lookup in isWorkspacePackage - Now using pkg.dir directly from the findPackageJson result instead of walking up the directory tree again.

  2. Array exports support in resolveExportTarget - Added support for array exports by checking Array.isArray(target) and recursively resolving each element in the fallback chain, per the Node.js package exports specification.

… exports support
- Remove redundant directory walk in isWorkspacePackage by using pkg.dir from findPackageJson
- Add support for array exports in resolveExportTarget (fallback chains per Node.js spec)

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced manifest merging to properly combine results from both workflow and step bundles

Out of curiosity, what does this fix? "Enhance" sounds nice but doesn't tell me anything

@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@VaguelySerious The step and workflow bundles generate their own manifests, which may have some values that do not overlap. For example, if I use a class only in step functions, but never reference it in the workflow bundle, then it would be missing from the workflow manifest. The fix was to ensure that all bundles are merged in the final form.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious