Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add subpath export resolution for package IDs#901

Merged
TooTallNate merged 8 commits intomainfrom
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 intomainfrom
01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids

Conversation

@TooTallNate
Copy link
Member

@TooTallNate TooTallNate commented Jan 30, 2026

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/[email protected])
  • 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/[email protected])
  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
Copy link

changeset-bot bot commented Jan 30, 2026

🦋 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
Name Type
@workflow/swc-plugin Patch
@workflow/sveltekit Patch
@workflow/builders Patch
@workflow/nitro Patch
@workflow/nest Patch
@workflow/next Patch
workflow-devkit-compiler-playground Patch
@workflow/astro Patch
@workflow/cli Patch
@workflow/rollup Patch
workflow Patch
@workflow/vite Patch
@workflow/nuxt Patch
@workflow/docs-typecheck Patch
@workflow/world-testing Patch
@workflow/core Patch
@workflow/web-shared Patch

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
Copy link
Contributor

vercel bot commented Jan 30, 2026

@github-actions
Copy link
Contributor

github-actions bot commented Jan 30, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 479 0 38 517
✅ 💻 Local Development 438 0 32 470
✅ 📦 Local Production 438 0 32 470
✅ 🐘 Local Postgres 438 0 32 470
✅ 🪟 Windows 47 0 0 47
❌ 🌍 Community Worlds 31 169 0 200
✅ 📋 Other 129 0 12 141
Total 2000 169 146 2315

❌ 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
App Passed Failed Skipped
✅ astro 43 0 4
✅ example 43 0 4
✅ express 43 0 4
✅ fastify 43 0 4
✅ hono 43 0 4
✅ nextjs-turbopack 46 0 1
✅ nextjs-webpack 46 0 1
✅ nitro 43 0 4
✅ nuxt 43 0 4
✅ sveltekit 43 0 4
✅ vite 43 0 4
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 43 0 4
✅ express-stable 43 0 4
✅ fastify-stable 43 0 4
✅ hono-stable 43 0 4
✅ nextjs-turbopack-stable 47 0 0
✅ nextjs-webpack-stable 47 0 0
✅ nitro-stable 43 0 4
✅ nuxt-stable 43 0 4
✅ sveltekit-stable 43 0 4
✅ vite-stable 43 0 4
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 43 0 4
✅ express-stable 43 0 4
✅ fastify-stable 43 0 4
✅ hono-stable 43 0 4
✅ nextjs-turbopack-stable 47 0 0
✅ nextjs-webpack-stable 47 0 0
✅ nitro-stable 43 0 4
✅ nuxt-stable 43 0 4
✅ sveltekit-stable 43 0 4
✅ vite-stable 43 0 4
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 43 0 4
✅ express-stable 43 0 4
✅ fastify-stable 43 0 4
✅ hono-stable 43 0 4
✅ nextjs-turbopack-stable 47 0 0
✅ nextjs-webpack-stable 47 0 0
✅ nitro-stable 43 0 4
✅ nuxt-stable 43 0 4
✅ sveltekit-stable 43 0 4
✅ vite-stable 43 0 4
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 47 0 0
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 0
❌ mongodb 5 42 0
✅ redis-dev 3 0 0
❌ redis 5 42 0
✅ starter-dev 3 0 0
❌ starter 4 43 0
✅ turso-dev 3 0 0
❌ turso 5 42 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 43 0 4
✅ e2e-local-postgres-nest-stable 43 0 4
✅ e2e-local-prod-nest-stable 43 0 4

📋 View full workflow run

@TooTallNate TooTallNate force-pushed the 01-30-fix_module_specifier_cache_bug_and_add_subpath_export_resolution_for_package_ids branch from 6cce073 to ca8bd01 Compare January 30, 2026 19:57
@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 0.041s (+3.5%) 1.019s (~) 0.978s 10 1.00x
💻 Local Nitro 0.043s (~) 1.007s (~) 0.964s 10 1.06x
💻 Local Express 0.044s (+4.8%) 1.007s (~) 0.963s 10 1.08x
🐘 Postgres Nitro 0.212s (-6.9% 🟢) 1.015s (~) 0.803s 10 5.19x
🐘 Postgres Express 0.290s (+4.2%) 1.016s (~) 0.726s 10 7.10x
🐘 Postgres Next.js (Turbopack) 0.360s (+28.8% 🔺) 1.021s (~) 0.660s 10 8.81x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.712s (+6.4% 🔺) 1.582s (-0.8%) 0.870s 10 1.00x
▲ Vercel Express 0.793s (+7.0% 🔺) 1.607s (~) 0.814s 10 1.11x
▲ Vercel Next.js (Turbopack) 0.870s (-1.1%) 1.721s (-12.0% 🟢) 0.851s 10 1.22x

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

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 1.094s (-0.7%) 2.014s (~) 0.920s 10 1.00x
💻 Local Nitro 1.115s (~) 2.007s (~) 0.892s 10 1.02x
💻 Local Express 1.117s (~) 2.007s (~) 0.890s 10 1.02x
🐘 Postgres Next.js (Turbopack) 1.858s (+0.5%) 2.420s (+20.1% 🔺) 0.562s 10 1.70x
🐘 Postgres Express 2.133s (-1.2%) 3.014s (~) 0.881s 10 1.95x
🐘 Postgres Nitro 2.258s (+3.1%) 3.017s (~) 0.759s 10 2.06x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.842s (-1.3%) 3.742s (+1.1%) 0.900s 10 1.00x
▲ Vercel Express 2.933s (~) 3.724s (+4.6%) 0.791s 10 1.03x
▲ Vercel Next.js (Turbopack) 2.969s (+2.0%) 3.755s (-1.8%) 0.786s 10 1.04x

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

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 10.750s (~) 11.018s (~) 0.268s 3 1.00x
💻 Local Express 10.837s (~) 11.012s (~) 0.175s 3 1.01x
💻 Local Nitro 10.849s (~) 11.010s (~) 0.162s 3 1.01x
🐘 Postgres Next.js (Turbopack) 15.074s (-1.0%) 16.033s (~) 0.959s 2 1.40x
🐘 Postgres Nitro 20.454s (~) 21.038s (~) 0.583s 2 1.90x
🐘 Postgres Express 20.532s (~) 21.032s (~) 0.500s 2 1.91x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 39.852s (+75.5% 🔺) 40.274s (+71.8% 🔺) 0.422s 1 1.00x
▲ Vercel Nitro 42.916s (+89.8% 🔺) 43.893s (+88.1% 🔺) 0.977s 2 1.08x
▲ Vercel Express 45.343s (+94.4% 🔺) 46.170s (+88.5% 🔺) 0.827s 2 1.14x

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

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 27.245s (~) 28.038s (~) 0.793s 3 1.00x
💻 Local Nitro 27.465s (~) 28.022s (~) 0.558s 3 1.01x
💻 Local Express 27.501s (~) 28.025s (~) 0.524s 3 1.01x
🐘 Postgres Next.js (Turbopack) 37.941s (+0.8%) 38.052s (~) 0.110s 2 1.39x
🐘 Postgres Nitro 50.373s (~) 51.096s (~) 0.722s 2 1.85x
🐘 Postgres Express 50.422s (~) 51.076s (~) 0.654s 2 1.85x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 58.799s (+2.8%) 60.259s (+4.0%) 1.460s 1 1.00x
▲ Vercel Nitro 59.008s (+3.9%) 59.284s (+3.6%) 0.275s 2 1.00x
▲ Vercel Next.js (Turbopack) 59.231s (+2.7%) 59.787s (+2.4%) 0.556s 2 1.01x

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

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 56.526s (~) 57.053s (~) 0.527s 2 1.00x
💻 Local Nitro 57.216s (~) 58.049s (+0.9%) 0.833s 2 1.01x
💻 Local Express 57.319s (~) 58.040s (~) 0.721s 2 1.01x
🐘 Postgres Next.js (Turbopack) 74.784s (-5.7% 🟢) 75.602s (-5.0% 🟢) 0.819s 2 1.32x
🐘 Postgres Nitro 100.195s (~) 101.169s (~) 0.974s 1 1.77x
🐘 Postgres Express 100.271s (~) 101.172s (~) 0.901s 1 1.77x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 124.409s (+2.9%) 125.651s (+2.6%) 1.242s 1 1.00x
▲ Vercel Next.js (Turbopack) 126.724s (-2.6%) 127.329s (-2.5%) 0.605s 1 1.02x
▲ Vercel Express 160.313s (+31.1% 🔺) 160.881s (+30.5% 🔺) 0.568s 1 1.29x

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

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.397s (~) 2.006s (~) 0.609s 15 1.00x
💻 Local Next.js (Turbopack) 1.410s (+1.3%) 2.012s (~) 0.603s 15 1.01x
💻 Local Express 1.436s (+1.3%) 2.007s (~) 0.571s 15 1.03x
🐘 Postgres Nitro 2.319s (-1.7%) 3.014s (~) 0.695s 10 1.66x
🐘 Postgres Next.js (Turbopack) 2.364s (+25.8% 🔺) 2.744s (+8.8% 🔺) 0.381s 11 1.69x
🐘 Postgres Express 2.366s (-1.0%) 3.013s (~) 0.647s 10 1.69x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.028s (+6.5% 🔺) 3.878s (+4.9%) 0.850s 8 1.00x
▲ Vercel Next.js (Turbopack) 3.062s (+1.5%) 3.920s (-1.9%) 0.858s 8 1.01x
▲ Vercel Express 3.345s (+11.3% 🔺) 4.184s (+7.4% 🔺) 0.839s 8 1.10x

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

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 2.553s (-1.2%) 3.047s (+0.7%) 0.494s 10 1.00x
💻 Local Nitro 2.577s (+3.0%) 3.017s (~) 0.440s 10 1.01x
💻 Local Express 2.633s (+1.4%) 3.022s (~) 0.389s 10 1.03x
🐘 Postgres Express 7.829s (-8.7% 🟢) 8.578s (-8.0% 🟢) 0.749s 4 3.07x
🐘 Postgres Nitro 7.987s (+1.6%) 8.594s (+0.5%) 0.607s 4 3.13x
🐘 Postgres Next.js (Turbopack) 12.839s (+11.5% 🔺) 13.360s (+11.1% 🔺) 0.522s 3 5.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.202s (+0.7%) 3.973s (+3.3%) 0.770s 8 1.00x
▲ Vercel Nitro 3.641s (+18.1% 🔺) 4.390s (+14.8% 🔺) 0.748s 7 1.14x
▲ Vercel Next.js (Turbopack) 3.721s (-3.8%) 4.425s (-4.6%) 0.704s 7 1.16x

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

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 7.559s (+2.3%) 8.274s (-2.2%) 0.715s 4 1.00x
💻 Local Express 7.559s (+4.0%) 8.515s (+3.1%) 0.956s 4 1.00x
💻 Local Nitro 7.582s (+8.8% 🔺) 8.529s (+13.7% 🔺) 0.946s 4 1.00x
🐘 Postgres Nitro 49.993s (+10.7% 🔺) 50.305s (+8.9% 🔺) 0.312s 1 6.61x
🐘 Postgres Express 51.058s (+4.2%) 51.328s (+4.0%) 0.270s 1 6.75x
🐘 Postgres Next.js (Turbopack) 55.261s (+8.2% 🔺) 56.160s (+9.6% 🔺) 0.899s 1 7.31x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.388s (-17.2% 🟢) 4.017s (-12.6% 🟢) 0.629s 8 1.00x
▲ Vercel Express 3.464s (+3.6%) 4.103s (+8.6% 🔺) 0.639s 8 1.02x
▲ Vercel Next.js (Turbopack) 3.996s (+6.8% 🔺) 5.070s (+14.2% 🔺) 1.074s 6 1.18x

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

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 1.406s (-1.9%) 2.009s (~) 0.603s 15 1.00x
💻 Local Express 1.468s (+2.8%) 2.008s (~) 0.541s 15 1.04x
💻 Local Nitro 1.468s (+1.5%) 2.007s (~) 0.538s 15 1.04x
🐘 Postgres Nitro 2.169s (-5.7% 🟢) 2.598s (-3.0%) 0.429s 12 1.54x
🐘 Postgres Express 2.216s (+5.5% 🔺) 2.598s (+3.2%) 0.382s 12 1.58x
🐘 Postgres Next.js (Turbopack) 2.274s (+26.6% 🔺) 2.755s (+36.9% 🔺) 0.481s 11 1.62x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.763s (~) 3.700s (~) 0.937s 9 1.00x
▲ Vercel Next.js (Turbopack) 2.913s (+1.4%) 3.795s (-0.7%) 0.883s 8 1.05x
▲ Vercel Express 2.986s (+4.9%) 3.868s (+2.8%) 0.882s 8 1.08x

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

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 2.663s (+1.8%) 3.010s (~) 0.347s 10 1.00x
💻 Local Next.js (Turbopack) 2.674s (-0.5%) 3.034s (~) 0.359s 10 1.00x
💻 Local Express 2.691s (~) 3.012s (~) 0.321s 10 1.01x
🐘 Postgres Express 11.815s (+8.9% 🔺) 12.361s (+11.5% 🔺) 0.546s 3 4.44x
🐘 Postgres Nitro 12.124s (+12.5% 🔺) 12.721s (+15.2% 🔺) 0.597s 3 4.55x
🐘 Postgres Next.js (Turbopack) 13.147s (+19.7% 🔺) 13.723s (+17.4% 🔺) 0.576s 3 4.94x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.906s (-1.7%) 3.716s (+1.8%) 0.810s 9 1.00x
▲ Vercel Nitro 3.044s (-3.0%) 3.719s (-2.6%) 0.676s 9 1.05x
▲ Vercel Next.js (Turbopack) 3.066s (+1.2%) 3.739s (-1.9%) 0.674s 9 1.06x

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

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 7.910s (+1.1%) 8.661s (-1.7%) 0.751s 4 1.00x
💻 Local Nitro 7.971s (+4.9%) 8.877s (+3.3%) 0.906s 4 1.01x
💻 Local Next.js (Turbopack) 8.245s (+7.8% 🔺) 9.068s (+5.2% 🔺) 0.823s 4 1.04x
🐘 Postgres Nitro 51.279s (~) 52.131s (~) 0.852s 1 6.48x
🐘 Postgres Express 51.446s (~) 52.249s (~) 0.803s 1 6.50x
🐘 Postgres Next.js (Turbopack) 56.929s (+4.7%) 57.297s (+3.9%) 0.368s 1 7.20x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.257s (+5.0%) 3.868s (+2.6%) 0.611s 8 1.00x
▲ Vercel Express 3.722s (+11.2% 🔺) 4.332s (+10.0% 🔺) 0.610s 8 1.14x
▲ Vercel Next.js (Turbopack) 4.022s (+13.3% 🔺) 4.880s (+12.5% 🔺) 0.857s 7 1.24x

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

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 0.145s (-1.4%) 1.003s (~) 0.016s (-8.0% 🟢) 1.028s (~) 0.883s 10 1.00x
💻 Local Nitro 0.182s (+2.6%) 0.992s (~) 0.015s (+7.3% 🔺) 1.021s (~) 0.839s 10 1.26x
💻 Local Express 0.185s (-1.4%) 0.992s (~) 0.014s (-4.2%) 1.021s (~) 0.836s 10 1.28x
🐘 Postgres Next.js (Turbopack) 0.751s (+5.0%) 0.856s (-12.4% 🟢) 0.000s (NaN%) 1.017s (-8.8% 🟢) 0.266s 10 5.19x
🐘 Postgres Nitro 2.375s (+59.7% 🔺) 2.673s (+61.2% 🔺) 0.000s (+Infinity% 🔺) 3.018s (+50.0% 🔺) 0.643s 10 16.41x
🐘 Postgres Express 2.428s (~) 2.618s (~) 0.000s (+Infinity% 🔺) 3.017s (~) 0.589s 10 16.78x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.068s (~) 3.350s (-1.4%) 0.229s (+60.6% 🔺) 4.071s (+1.1%) 1.003s 10 1.00x
▲ Vercel Next.js (Turbopack) 3.238s (+4.7%) 3.502s (+6.7% 🔺) 0.270s (+46.9% 🔺) 4.319s (+8.5% 🔺) 1.082s 10 1.06x
▲ Vercel Express 3.349s (+9.3% 🔺) 3.477s (+3.5%) 0.188s (-11.9% 🟢) 4.230s (+3.9%) 0.881s 10 1.09x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Next.js (Turbopack) 9/12
🐘 Postgres Nitro 5/12
▲ Vercel Nitro 8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 💻 Local 10/12
Next.js (Turbopack) 💻 Local 10/12
Nitro 💻 Local 10/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

Copy link
Contributor

Copilot AI left a comment

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/[email protected]
  • 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
File Description
packages/builders/src/module-specifier.ts Core changes: cache back-filling, subpath export resolution, workspace package detection improvements
packages/builders/src/apply-swc-transform.ts Added absolutePath parameter for accurate module specifier resolution
packages/builders/src/swc-esbuild-plugin.ts Passes absolute path to transform function
packages/builders/src/base-builder.ts Updated createWorkflowsBundle return type to include manifest
packages/sveltekit/src/builder.ts Updated to merge manifests from both bundles
packages/nitro/src/builders.ts Updated to merge manifests from both bundles
packages/next/src/builder.ts Updated to merge manifests and handle optional context properties
packages/nest/src/builder.ts Updated to merge manifests from both bundles
packages/builders/src/vercel-build-output-api.ts Updated to merge manifests from both bundles
packages/builders/src/standalone.ts Added mergeManifests helper and updated bundle methods
packages/swc-plugin-workflow/transform/src/naming.rs Added Windows path normalization with tests
packages/swc-plugin-workflow/transform/src/lib.rs Enhanced builtin function comment documentation
packages/swc-plugin-workflow/spec.md Updated documentation for subpath exports
.changeset/afraid-candies-find.md Added 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: '[email protected]' }
*
* @example
* // File in node_modules (subpath export)
* resolveModuleSpecifier('/project/node_modules/workflow/dist/internal/builtins.js', '/project')
* // => { moduleSpecifier: 'workflow/internal/[email protected]' }
*
* @example
* // File in workspace package
* resolveModuleSpecifier('/project/packages/shared/src/utils.ts', '/project')
* // => { moduleSpecifier: '@myorg/[email protected]' }
@@ -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,
};
}
Copy link

Copilot AI Feb 4, 2026

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 },
};
Copy link

Copilot AI Feb 4, 2026

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 '';
}
Copy link

Copilot AI Feb 4, 2026

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 '';
}
Copy link

Copilot AI Feb 4, 2026

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.
@TooTallNate
Copy link
Member Author

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)
Copy link
Member

@VaguelySerious VaguelySerious left a comment

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
Member Author

@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 free to 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