Skip to content

Support framework base paths outside Next.js#2771

Open
NathanColosimo wants to merge 41 commits into
mainfrom
nathanc/fix-framework-base-path
Open

Support framework base paths outside Next.js#2771
NathanColosimo wants to merge 41 commits into
mainfrom
nathanc/fix-framework-base-path

Conversation

@NathanColosimo

@NathanColosimo NathanColosimo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Description

Builds on #2732 (now merged; this PR targets main directly).

Adds non-Next base path support using framework-native config only (kit.paths.base, Astro base, Nitro baseURL, Nuxt app.baseURL). There is no WORKFLOW_BASE_PATH env var and no new user-facing Workflow option.

Every behavior in this PR was verified as needed — via local falsification or Vercel preview deployments — or removed. Preview deploys surfaced that queue-triggered workflow execution was broken on Vercel for every framework with a base path: the queue trigger config carries no path — Vercel invokes a triggered function at its function-directory path — and the adapters emit the flow function at the root-relative route id, which the base-mounted server inside rejects (deliveries retried forever, runs stayed pending).

The fix follows one principle everywhere: the flow function lives below the base path, so its directory path (= what the queue invokes, = the public URL) matches the route the server inside actually mounts. No request rewriting, no queue-header sniffing:

  • builders (Build Output API): workflow functions are emitted below the base path so public URLs match function paths directly; route src/dest both carry the base path. Root-relative URLs match no function and 404 naturally (no 404 block routes — Vercel's router re-matches rewritten paths against them, which corrupts legitimate responses).
  • nitro: reads baseURL; keeps internal routes/functionRules unprefixed (Nitro applies baseURL when serving). On Vercel, the compiled hook moves the generated flow function below the base path and repoints Nitro's generated flow/webhook HTTP routes at the catch-all server function (the per-route dests don't resolve — platform 404s). The base-path plugin only injects the runtime global.
  • sveltekit: reads kit.paths.base from the svelte config; the existing beforeExit function patch now recreates the flow function below the base path (the adapter emits it at the root-relative route id).
  • astro: reads config.base; splices workflow routes before handle: filesystem (post-filesystem rewrites don't resolve functions) and injects the runtime base path global into the SSR build.
  • nuxt: no changes needed — Nuxt natively lowers app.baseURL into Nitro's baseURL (verified via instrumented builds; the previous propagation code was removed as dead).
  • There is no explicit root-URL blocking anywhere: workflow routes inherit each framework's own base path semantics, the same as any user route in the app. SvelteKit 404s root-relative URLs, Nitro/Nuxt production redirects them to the base path, and Astro + Nitro dev serve them at both paths (verified those frameworks dual-mount the user's own routes too).
  • Workbenches opt into base-path E2E with WORKFLOW_E2E_BASE_PATH=/app; getDeploymentUrl() in the e2e utils appends it to DEPLOYMENT_URL, so the entire existing e2e suite runs against the base-path deployment unchanged — no dedicated base-path tests.

The shared helpers live in @workflow/builders because — unlike Next.js, which owns its whole routing surface (@workflow/next only forwards nextConfig.basePath) — these integrations generate the routing artifacts themselves, and Astro, Nitro, and SvelteKit share the same normalization, Build Output API prefixing, and generated-code snippets (runtime global, queue-delivery detection).

How did you test your changes?

Vercel preview deployments (vercel deploy with WORKFLOW_E2E_BASE_PATH=/app WORKFLOW_PUBLIC_MANIFEST=1), probed through deployment protection via trusted-sources OIDC:

nitro-v3 sveltekit astro nuxt (nitropack v2 / legacy BOA)
full workflow run completes (queue → flow fn → steps) addTen=42 awaited over HTTP ✅ run completed ✅ run completed probes only (queue path shares the astro-verified BOA function code)
runtime base path global inside flow fn metadata.url ends in /app ✅ health reports /app/...
webhook resume at base-prefixed URL ✅ handler reached ✅ real resume: 202 → run completed ✅ handler reached ✅ handler reached
root-relative workflow URLs (framework-native semantics) 302 → base 404 served at both paths, like all Astro routes 302 → base
public manifest below base path ✅ 200 ✅ 200 ✅ 200 ✅ 200
control deploy without base path unchanged

Negative results that shaped the final code (each intermediate variant was deployed and probed): Nitro's native routes platform-404 the base-prefixed flow/webhook URLs; queue deliveries 302/404-looped on Nitro/SvelteKit while the flow function lived at the root-relative path (runs stuck pending) and succeeded once relocated below the base path; 404 block routes stamped their status onto legitimate rewritten requests; Astro and Nitro dev dual-mount every route (root and base), so explicit root 404s made workflow routes stricter than the user's own routes and were removed.

Local falsification (disable the code → observe the behavior): Nuxt propagation (works with it deleted → removed), SvelteKit base-prefixed vc-config path (never exists → removed), Astro/Nitro-dev root guards (frameworks dual-mount user routes identically → removed).

Local builds + probes with and without WORKFLOW_E2E_BASE_PATH=/app for sveltekit, astro, nitro-v3, nuxt (prod) and vite, astro (dev): base-prefixed health 200, root non-200, no-base builds byte-identical to main (no base-path code injected into generated routes).

Unit tests: @workflow/builders, @workflow/nitro, @workflow/sveltekit. biome check, tsc, and changeset status pass.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes (run git commit --signoff on your commits)
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@changeset-bot

changeset-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7f3d6b9

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

This PR includes changesets to release 16 packages
Name Type
@workflow/builders Patch
@workflow/astro Patch
@workflow/nitro Patch
@workflow/sveltekit Patch
@workflow/cli Patch
@workflow/nest Patch
@workflow/next Patch
@workflow/nuxt Patch
@workflow/rollup Patch
@workflow/vite Patch
@workflow/vitest Patch
workflow Patch
@workflow/world-testing Patch
@workflow/core Patch
@workflow/web-shared Patch
@workflow/web 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

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Jul 10, 2026 5:30am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jul 10, 2026 5:30am
example-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workbench-astro-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workbench-express-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workbench-fastify-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workbench-hono-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workbench-nitro-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workbench-nuxt-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workbench-sveltekit-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workbench-vite-workflow Ready Ready Preview, Comment Jul 10, 2026 5:30am
workflow-docs Ready Ready Preview, Comment, Open in v0 Jul 10, 2026 5:30am
workflow-swc-playground Ready Ready Preview, Comment Jul 10, 2026 5:30am
workflow-tarballs Ready Ready Preview, Comment Jul 10, 2026 5:30am
workflow-web Ready Ready Preview, Comment Jul 10, 2026 5:30am

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1453 0 230 1683
✅ 💻 Local Development 1617 0 219 1836
✅ 📦 Local Production 1617 0 219 1836
✅ 🐘 Local Postgres 1617 0 219 1836
✅ 🪟 Windows 153 0 0 153
✅ 📋 Other 894 0 177 1071
Total 7351 0 1064 8415

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 126 0 27
✅ example 126 0 27
✅ express 126 0 27
✅ fastify 126 0 27
✅ hono 126 0 27
✅ nextjs-turbopack 150 0 3
✅ nextjs-webpack 150 0 3
✅ nitro 126 0 27
✅ nuxt 126 0 27
✅ sveltekit 145 0 8
✅ vite 126 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 153 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 25
✅ e2e-local-dev-tanstack-start- 128 0 25
✅ e2e-local-postgres-nest-stable 128 0 25
✅ e2e-local-postgres-tanstack-start- 128 0 25
✅ e2e-local-prod-nest-stable 128 0 25
✅ e2e-local-prod-tanstack-start- 128 0 25
✅ e2e-vercel-prod-tanstack-start 126 0 27

📋 View full workflow run

Comment thread packages/astro/src/builder.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Refactor to the minimal change set while keeping behavior:

- builders: share createBuildOutputApiWorkflowRoutes /
  createBuildOutputApiRootBlockRoutes so Astro reuses them instead of
  duplicating route regexes; merge the two new test files into one and
  drop test-only exports (createBuildOutputApiRoutes,
  createBasePathRouteRegexPrefix no longer re-exported)
- astro: make the handler guard private, reuse shared routes, only
  inject the base path Vite plugin when a base is configured; drop the
  package test script and test file (covered by builders tests)
- sveltekit: revert the test-only flow-route extraction; keep the
  svelte.config loader + its test
- nitro: only register the Vercel output patch hook, static manifest
  copy, and base path runtime plugin when a baseURL is configured so
  default deployments are untouched; consolidate the two file-fixture
  tests into one and revert unrelated test churn
- pass undefined (not '') basePath when no base path is configured so
  generated route files are identical to main for default apps
- workbenches: drop redundant turbo.json env overrides (root turbo.json
  already declares them; verified via turbo --dry), compact the E2E env
  boilerplate, and revert the TanStack type-fix commit (builds fine
  without it)

Verified locally with WORKFLOW_E2E_BASE_PATH=/app builds + HTTP probes
for SvelteKit, Astro, Nitro v3, Nuxt, and Vite/TanStack (prod and dev
servers): base-prefixed health/manifest respond 200 with the prefixed
endpoint path, root workflow routes are blocked, and default (no base
path) builds are unchanged.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…outing

Every piece of the base path support is now either verified as needed via
local falsification or Vercel preview deployments, or removed.

Fixed (found via preview deploys — queue-triggered workflow execution was
broken on Vercel for every framework with a base path; runs stayed pending
forever while deliveries retried):

- nitro: Vercel queue triggers invoke the flow function with the
  root-relative route path, which the baseURL-mounted Nitro app answers
  with a 302 redirect. The base path plugin now rewrites queue deliveries
  (identified by their CloudEvents/queue headers) to the base-prefixed
  path before routing. Nitro's generated flow/webhook routes point at
  per-route functions that Vercel's router fails to resolve — they are
  repointed at the catch-all server function.
- sveltekit: same delivery failure (the SvelteKit server mounted below
  kit.paths.base 404s the root-relative trigger path). The flow function
  handler is wrapped to rewrite queue deliveries to the base-prefixed
  path; plain HTTP requests are untouched.
- astro/Build Output API: workflow functions are now emitted BELOW the
  base path so public URLs match function paths directly, and Astro
  splices the routes before `handle: filesystem` (post-filesystem
  rewrites don't resolve; verified on previews). The root-block 404
  routes are gone entirely — root-relative URLs match no function and
  404/redirect naturally (verified per framework).

Removed as unnecessary (verified):

- Nuxt baseURL propagation (both mechanisms): Nuxt natively lowers
  app.baseURL into nitro.options.baseURL (verified via instrumented
  builds); reverted the nuxt module change and dropped the
  options.app fallback.
- SvelteKit base-prefixed .vc-config patching: the adapter emits the
  flow function at the root-relative route-id path even with a base
  path; the extra path never exists.
- Nitro static manifest copy: the manifest route resolves natively
  through the catch-all server function (verified 200 on preview).
- createBuildOutputApiRootBlockRoutes and its tests.

Kept after failing to falsify:

- Astro handler guard (Astro node server serves workflow routes at both
  root and base without it) and dev middleware (Astro dev normalizes
  request.url to include the base, blinding the guard).
- Vite dev root-block branch (Nitro dev serves handlers at both root
  and base-prefixed paths).

Also gates the base path e2e health test to local deployments, matching
the existing suite contract: direct HTTP access to the flow endpoint has
never been guaranteed on Vercel (sveltekit on main has the same platform
404; queue-based health is the Vercel path).

Verified on Vercel preview deployments with WORKFLOW_E2E_BASE_PATH=/app:
full workflow runs complete on nitro-v3 (addTen=42, metadata.url ends in
/app), sveltekit (run completed including webhook resume at the
base-prefixed URL), and astro (run completed); root-relative workflow
URLs are blocked or redirected on all of them; public manifests serve
below the base path.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
- Remove the getDeploymentUrl unit tests and the local-only base path
  health e2e test (the behavior is covered by every existing e2e test
  running against a base-path deployment via getDeploymentUrl).
- Document why the base path helpers live in @workflow/builders: Next.js
  owns its entire routing surface so @workflow/next only forwards
  nextConfig.basePath, while the other integrations generate the routing
  artifacts themselves and share this logic.
- Share the queue-delivery detection between the Nitro plugin and the
  SvelteKit flow wrapper via QUEUE_DELIVERY_HEADERS_GUARD_CODE instead of
  duplicating the header contract in two generated-code strings, and use
  WORKFLOW_ROUTE_BASE/joinWorkflowBasePath in the SvelteKit wrapper
  instead of hand-rolled path strings.
- Make createBasePathRouteRegexPrefix private to the Build Output API
  module (its only consumer).
- Consolidate the builders base path tests to three focused cases and
  merge the two Nitro internal-routes tests into one.

Re-verified the regenerated code on Vercel previews with
WORKFLOW_E2E_BASE_PATH=/app: nitro-v3 full loop returns addTen=42 and
sveltekit runs execute (queue deliveries rewritten by the shared guard).

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
A base path should just change where routes are mounted — routes that
aren't mounted at the root shouldn't need explicit 404 code. The three
remaining pieces of explicit 404 code existed because Astro (prod and
dev) and Nitro dev serve routes at BOTH the root-relative and
base-prefixed paths. Verified that this dual-mounting applies to the
user's own routes too (e.g. /api/... responds at both paths with a base
path configured), so blocking only the workflow routes made them
artificially stricter than every other route in the same app.

Removed:
- the Astro generated-handler base path guard (generated route files are
  identical to main again)
- the Astro dev-server middleware that 404'd root-relative workflow
  routes (the base path Vite plugin is banner-only now)
- the root-blocking branch in the Nitro Vite dev middleware; the file is
  restored to main's structure, with the existing 404-empty-body
  workaround extended to cover the base-prefixed alias (Nitro dev serves
  both)

Root-relative workflow URLs now behave exactly like the framework treats
any other route: SvelteKit 404s them, Nitro/Nuxt production redirects to
the base path, and Astro + Nitro dev serve them at both paths.

Re-verified: local astro prod + vite dev probes (base works, aliases
match user-route behavior, webhook 404 workaround still applies on the
base-prefixed path), and an Astro preview deployment with
WORKFLOW_E2E_BASE_PATH=/app where addTenWorkflow completed.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
- Changeset: drop @workflow/nuxt and workflow — neither package is
  modified anymore.
- @workflow/builders: stop exporting getBuildOutputStaticManifestDir and
  getBuildOutputFunctionsPrefix from the package index — they have no
  consumers outside the Build Output API module and its tests.
- Nitro tests: merge the two base path plugin tests into one (the
  generated-plugin assertions are the only automated guard for the
  Vercel queue-delivery fix, since CI cannot deploy-test base paths) and
  drop the baseURL-forwarding test (it only echoed config plumbing).
- SvelteKit: rename createQueueBasePathEntryCode to
  createQueueDeliveryEntryCode and document the actual reason it exists:
  Vercel queue triggers invoke the function at its function-directory
  path (the root-relative route id), not the public base-prefixed URL,
  so the base-mounted server inside would 404 every delivery.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…iveries

The queue trigger config carries no path — Vercel invokes a triggered
function at its function-directory path. So the right fix for base paths
is to put the function where the server inside it expects requests: below
the base path, matching the public URL. This is the same principle the
Build Output API builder already uses, now applied uniformly:

- sveltekit: the beforeExit hook recreates the flow function below the
  base path (instead of in place) — the handler wrapper that rewrote
  queue deliveries is deleted.
- nitro: the compiled hook moves the generated flow function below the
  base path — the base path plugin is global-injection-only again and
  the queue-delivery request rewrite is deleted.
- builders: QUEUE_DELIVERY_HEADERS_GUARD_CODE is deleted — nothing
  sniffs queue-delivery headers anymore.

Verified on Vercel preview deployments with WORKFLOW_E2E_BASE_PATH=/app:
nitro-v3 full loop returns addTen=42 (and base health 200 via the
catch-all), sveltekit runs execute end to end. Local builds without a
base path produce byte-identical output to main.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Full-file review pass:

- astro: unify the Vercel route splice — routes always go before
  `handle: filesystem` (post-filesystem rewrites never resolved the
  functions; verified the no-base preview deployment serves webhook +
  manifest identically, and HTTP flow access on Vercel was already
  non-functional on main — it's queue-only, like SvelteKit).
- nitro: revert the route-constant refactor churn in index.ts (paths are
  internal literals again, matching main) and the unrelated vi.stubEnv
  test rewrite; collapse the dev-middleware path check to a single
  substring match (both path aliases contain the workflow marker),
  removing the route-prefix state.
- builders: inline the two single-use path helpers into the Build Output
  builder; drop their test block.
- e2e: collapse getDeploymentUrl to a single function.

nitro's Vercel output is byte-identical before/after the churn revert;
astro local prod with a base path still serves base-prefixed health.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…-path-local

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>

# Conflicts:
#	packages/next/src/index.ts
#	packages/utils/src/index.ts
… nathanc/fix-framework-base-path

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>

# Conflicts:
#	packages/astro/src/builder.ts
#	packages/astro/src/plugin.ts
#	packages/core/e2e/bench.bench.ts
#	packages/nitro/src/builders.ts
#	packages/nitro/src/index.test.ts
#	packages/nitro/src/index.ts
#	packages/nitro/src/vite.ts
#	packages/sveltekit/src/builder.ts
#	packages/sveltekit/src/plugin.ts
#	workbench/express/nitro.config.ts
#	workbench/fastify/nitro.config.ts
#	workbench/hono/nitro.config.ts
#	workbench/nitro-v3/nitro.config.ts
#	workbench/tanstack-start/vite.config.ts
#	workbench/vite/vite.config.ts
Also addresses the #2732 review nit about the base path symbol: the
Symbol.for('@workflow/core/basePath') string lives in three coordinated
places (canonical reader in @workflow/utils workflow-routes, the CJS
copy in @workflow/next, and the generated-code emitter in
@workflow/builders) — each now carries a cross-reference comment so a
drift is caught in review instead of silently breaking propagation.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 7f3d6b9 · Fri, 10 Jul 2026 05:47:31 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1235 (+5.7%) 1666 🔴 1685 🔴 2108 🔴 30
TTFS hook + stream 1424 (+3.5%) 1893 🔴 1931 🔴 2021 🔴 30
STSO 1020 steps (1-20) 246 (-9.5%) 269 🔴 306 🔴 321 🔴 19
STSO 1020 steps (101-120) 386 (-7.3%) 436 🔴 483 🔴 504 🔴 19
STSO 1020 steps (1001-1020) 1078 (±0%) 924 🔴 1265 🔴 4600 🔴 19
WO stream 1235 (+5.7%) 1666 1685 2108 30
WO hook + stream 1424 (+3.5%) 1893 1931 2021 30
SL stream 4027 (-18%) 5053 🔴 5713 🔴 6063 🔴 30
SL hook + stream 5055 (+0.6%) 5705 🔴 5820 🔴 5976 🔴 30
📜 Previous results (3)

41d2013

Fri, 10 Jul 2026 05:24:49 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1601 (+37%) 1595 🔴 1665 🔴 1899 🔴 30
TTFS hook + stream 1814 (+32%) 1877 🔴 1901 🔴 2008 🔴 30
STSO 1020 steps (1-20) 262 (-3.5%) 304 🔴 356 🔴 410 🔴 19
STSO 1020 steps (101-120) 427 (+2.5%) 442 🔴 599 🔴 619 🔴 19
STSO 1020 steps (1001-1020) 1129 (+4.8%) 1006 🔴 1336 🔴 4632 🔴 19
WO stream 1601 (+37%) 1595 1665 1899 30
WO hook + stream 1814 (+32%) 1877 1901 2008 30
SL stream 4663 (-5.3%) 4741 🔴 4829 🔴 5472 🔴 30
SL hook + stream 4510 (-10%) 4565 🔴 4726 🔴 5563 🔴 30

41d2013

Thu, 09 Jul 2026 23:44:19 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1248 (+12%) 1664 🔴 1699 🔴 1788 🔴 30
STSO 1020 steps (1-20) 280 (+2.8%) 295 🔴 402 🔴 472 🔴 19
STSO 1020 steps (101-120) 395 (-4.0%) 467 🔴 488 🔴 495 🔴 19
STSO 1020 steps (1001-1020) 857 (-1.9%) 905 🔴 982 🔴 1016 🔴 19
WO stream 1248 (+12%) 1664 1699 1788 30
SL stream 4640 (-0.7%) 5744 🔴 5872 🔴 6056 🔴 30

f5a13cd

Thu, 09 Jul 2026 23:22:00 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1333 (+20%) 1665 🔴 1719 🔴 1811 🔴 30
TTFS hook + stream 1686 (+31%) 1922 🔴 2083 🔴 5721 🔴 30
STSO 1020 steps (1-20) 268 (-1.5%) 321 🔴 335 🔴 386 🔴 19
STSO 1020 steps (101-120) 404 (-1.6%) 449 🔴 475 🔴 482 🔴 19
STSO 1020 steps (1001-1020) 1057 (+21%) 1008 🔴 1262 🔴 4275 🔴 19
WO stream 1333 (+20%) 1665 1719 1811 30
WO hook + stream 1686 (+31%) 1922 2083 5721 30
SL stream 4573 (-2.1%) 5489 🔴 5817 🔴 6011 🔴 30
SL hook + stream 5061 (+0.6%) 5582 🔴 5730 🔴 6125 🔴 30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

…ndings

The adapter-node chunk patch skipped any chunk matching
/\b(const|let|var)\s+__(file|dir)name\b/ — but $ is not a regex word
character, so rollup-renamed declarations like `__filename$1` (produced
when adapter-node re-bundles the intermediate server output and our
banner's declaration collides) satisfied the check. Chunks that declared
only a renamed binding while a bundled CJS dependency referenced the
bare `__filename` were skipped, and the production server crashed at
boot (observed on main with the TypeScript compiler bundled via
cosmiconfig through @workflow/world-postgres).

Anchor both regexes with (?![\w$]) so renamed identifiers no longer
match. Verified: the sveltekit workbench production server now boots and
serves health checks (with and without a base path), and queue
deliveries from start() succeed.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
The plugin is only the delivery mechanism for setWorkflowBasePath into a
bundle without a boot hook; a runtime env var would be a second config
source that drifts from config.base, and a build-time define can't reach
@workflow/utils since Astro externalizes it in SSR output.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…ename bindings"

Split into #2799 together with the underlying fix that keeps the
TypeScript compiler out of the SvelteKit server bundle — that crash is a
main regression independent of base path support.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
The flow function needs no route: it's an exact static path that Vercel
serves via filesystem matching (and with a base path the function lives
below it, so the public URL is the function path — main's implicit
behavior, just relocated). Only the webhook needs a route, because
[token] is a dynamic segment that arbitrary token URLs can't
filesystem-match. The flow route entry was a leftover from the PR's
earlier root-blocking design.

Verified on an astro preview deployment with WORKFLOW_E2E_BASE_PATH=/app:
webhook handler reachable at the base URL, manifest 200, root 404, and
addTenWorkflow ran to completed.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Nitro v3 types baseURL on NitroOptions (with NITRO_APP_BASE_URL docs);
the cast and its comment claiming otherwise were wrong.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
- Delete getNitroBasePath (alias of normalizeWorkflowBasePath over
  nitro.options.baseURL), joinWorkflowBasePath (normalize + concat, all
  call sites already normalized), createBasePathRouteRegexPrefix (one
  caller), and astro's prependBanner (one caller; matches sveltekit's
  inline style).
- Nitro setup computes the base path once and passes it down; the
  runtime plugin registration is inlined at that point;
  patchNativeVercelWorkflowRoutes takes the normalized base path and
  fails loudly (no existsSync guard) if the flow function is missing.
- SvelteKit config loader is one function: existsSync instead of
  try/catch, no function-config branch (svelte configs are object
  exports), required workingDir argument.
- createBuildOutputApiWebhookRoute takes a required string | undefined.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
The test hardcoded ${process.cwd()}/workbench/express, assuming vitest
runs from the repo root — but the package test script (added in this PR;
these tests never ran in CI before) runs from packages/nitro, making the
path nonexistent. Linux accidentally passed via require.resolve ancestor
walking; Windows fell through to the bare-specifier fallback. Resolve the
workbench path relative to the test file instead.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
The test resolved @workflow/world-postgres from a real workbench, which
requires that package's dist to exist — but the unit test job builds only
each tested package's own dependency graph (build-packages: false), in
arbitrary order, so the test raced world-postgres's build. Linux happened
to win the race, Windows didn't. Resolve against a fixture app with the
world installed instead; verified passing with world-postgres/dist
deleted.

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
# Conflicts:
#	packages/core/e2e/bench.bench.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
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.

1 participant