Support framework base paths outside Next.js - #2771

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

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

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo 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 beforehandle: 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-v3sveltekitastronuxt (nitropack v2 / legacy BOA)
full workflow run completes (queue → flow fn → steps)addTen=42 awaited over HTTP✅ run completed✅ run completedprobes only (queue path shares the astro-verified BOA function code)
runtime base path global inside flow fnmetadata.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 → base404served at both paths, like all Astro routes302 → 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-botBot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e4db2b

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

This PR includes changesets to release 16 packages
NameType
@workflow/buildersPatch
@workflow/astroPatch
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/vitestPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch
@workflow/webPatch

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 Jul 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/astro/src/builder.ts
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-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 1e4db2b · Tue, 14 Jul 2026 23:17:09 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1341 (-2.7%)1628 🔴1713 🔴3659 🔴30
TTFShook + stream1510 (+10%)1893 🔴2040 🔴2279 🔴30
STSO1020 steps (1-20)308 (+2.7%)311 🔴355 🔴974 🔴19
STSO1020 steps (101-120)428 (-1.5%)462 🔴501 🔴593 🔴19
STSO1020 steps (1001-1020)908 (+0.6%)916 🔴1098 🔴1115 🔴19
WOstream1341 (-2.7%)16281713365930
WOhook + stream1510 (+10%)18932040227930
SLstream4291 (-15%)5074 🔴5632 🔴5794 🔴30
SLhook + stream5070 (+0.6%)5601 🔴5776 🔴6467 🔴30
📜 Previous results (4)

7f3d6b9

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1235 (+5.7%)1666 🔴1685 🔴2108 🔴30
TTFShook + stream1424 (+3.5%)1893 🔴1931 🔴2021 🔴30
STSO1020 steps (1-20)246 (-9.5%)269 🔴306 🔴321 🔴19
STSO1020 steps (101-120)386 (-7.3%)436 🔴483 🔴504 🔴19
STSO1020 steps (1001-1020)1078 (±0%)924 🔴1265 🔴4600 🔴19
WOstream1235 (+5.7%)16661685210830
WOhook + stream1424 (+3.5%)18931931202130
SLstream4027 (-18%)5053 🔴5713 🔴6063 🔴30
SLhook + stream5055 (+0.6%)5705 🔴5820 🔴5976 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1601 (+37%)1595 🔴1665 🔴1899 🔴30
TTFShook + stream1814 (+32%)1877 🔴1901 🔴2008 🔴30
STSO1020 steps (1-20)262 (-3.5%)304 🔴356 🔴410 🔴19
STSO1020 steps (101-120)427 (+2.5%)442 🔴599 🔴619 🔴19
STSO1020 steps (1001-1020)1129 (+4.8%)1006 🔴1336 🔴4632 🔴19
WOstream1601 (+37%)15951665189930
WOhook + stream1814 (+32%)18771901200830
SLstream4663 (-5.3%)4741 🔴4829 🔴5472 🔴30
SLhook + stream4510 (-10%)4565 🔴4726 🔴5563 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1248 (+12%)1664 🔴1699 🔴1788 🔴30
STSO1020 steps (1-20)280 (+2.8%)295 🔴402 🔴472 🔴19
STSO1020 steps (101-120)395 (-4.0%)467 🔴488 🔴495 🔴19
STSO1020 steps (1001-1020)857 (-1.9%)905 🔴982 🔴1016 🔴19
WOstream1248 (+12%)16641699178830
SLstream4640 (-0.7%)5744 🔴5872 🔴6056 🔴30

f5a13cd

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1333 (+20%)1665 🔴1719 🔴1811 🔴30
TTFShook + stream1686 (+31%)1922 🔴2083 🔴5721 🔴30
STSO1020 steps (1-20)268 (-1.5%)321 🔴335 🔴386 🔴19
STSO1020 steps (101-120)404 (-1.6%)449 🔴475 🔴482 🔴19
STSO1020 steps (1001-1020)1057 (+21%)1008 🔴1262 🔴4275 🔴19
WOstream1333 (+20%)16651719181130
WOhook + stream1686 (+31%)19222083572130
SLstream4573 (-2.1%)5489 🔴5817 🔴6011 🔴30
SLhook + stream5061 (+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>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
# Conflicts:
#	packages/builders/src/vercel-build-output-api.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
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.

1 participant

@NathanColosimo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Support framework base paths outside Next.js - #2771

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

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

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo 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 beforehandle: 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-v3sveltekitastronuxt (nitropack v2 / legacy BOA)
full workflow run completes (queue → flow fn → steps)addTen=42 awaited over HTTP✅ run completed✅ run completedprobes only (queue path shares the astro-verified BOA function code)
runtime base path global inside flow fnmetadata.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 → base404served at both paths, like all Astro routes302 → 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-botBot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e4db2b

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

This PR includes changesets to release 16 packages
NameType
@workflow/buildersPatch
@workflow/astroPatch
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/vitestPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch
@workflow/webPatch

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 Jul 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/astro/src/builder.ts
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-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 1e4db2b · Tue, 14 Jul 2026 23:17:09 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1341 (-2.7%)1628 🔴1713 🔴3659 🔴30
TTFShook + stream1510 (+10%)1893 🔴2040 🔴2279 🔴30
STSO1020 steps (1-20)308 (+2.7%)311 🔴355 🔴974 🔴19
STSO1020 steps (101-120)428 (-1.5%)462 🔴501 🔴593 🔴19
STSO1020 steps (1001-1020)908 (+0.6%)916 🔴1098 🔴1115 🔴19
WOstream1341 (-2.7%)16281713365930
WOhook + stream1510 (+10%)18932040227930
SLstream4291 (-15%)5074 🔴5632 🔴5794 🔴30
SLhook + stream5070 (+0.6%)5601 🔴5776 🔴6467 🔴30
📜 Previous results (4)

7f3d6b9

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1235 (+5.7%)1666 🔴1685 🔴2108 🔴30
TTFShook + stream1424 (+3.5%)1893 🔴1931 🔴2021 🔴30
STSO1020 steps (1-20)246 (-9.5%)269 🔴306 🔴321 🔴19
STSO1020 steps (101-120)386 (-7.3%)436 🔴483 🔴504 🔴19
STSO1020 steps (1001-1020)1078 (±0%)924 🔴1265 🔴4600 🔴19
WOstream1235 (+5.7%)16661685210830
WOhook + stream1424 (+3.5%)18931931202130
SLstream4027 (-18%)5053 🔴5713 🔴6063 🔴30
SLhook + stream5055 (+0.6%)5705 🔴5820 🔴5976 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1601 (+37%)1595 🔴1665 🔴1899 🔴30
TTFShook + stream1814 (+32%)1877 🔴1901 🔴2008 🔴30
STSO1020 steps (1-20)262 (-3.5%)304 🔴356 🔴410 🔴19
STSO1020 steps (101-120)427 (+2.5%)442 🔴599 🔴619 🔴19
STSO1020 steps (1001-1020)1129 (+4.8%)1006 🔴1336 🔴4632 🔴19
WOstream1601 (+37%)15951665189930
WOhook + stream1814 (+32%)18771901200830
SLstream4663 (-5.3%)4741 🔴4829 🔴5472 🔴30
SLhook + stream4510 (-10%)4565 🔴4726 🔴5563 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1248 (+12%)1664 🔴1699 🔴1788 🔴30
STSO1020 steps (1-20)280 (+2.8%)295 🔴402 🔴472 🔴19
STSO1020 steps (101-120)395 (-4.0%)467 🔴488 🔴495 🔴19
STSO1020 steps (1001-1020)857 (-1.9%)905 🔴982 🔴1016 🔴19
WOstream1248 (+12%)16641699178830
SLstream4640 (-0.7%)5744 🔴5872 🔴6056 🔴30

f5a13cd

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1333 (+20%)1665 🔴1719 🔴1811 🔴30
TTFShook + stream1686 (+31%)1922 🔴2083 🔴5721 🔴30
STSO1020 steps (1-20)268 (-1.5%)321 🔴335 🔴386 🔴19
STSO1020 steps (101-120)404 (-1.6%)449 🔴475 🔴482 🔴19
STSO1020 steps (1001-1020)1057 (+21%)1008 🔴1262 🔴4275 🔴19
WOstream1333 (+20%)16651719181130
WOhook + stream1686 (+31%)19222083572130
SLstream4573 (-2.1%)5489 🔴5817 🔴6011 🔴30
SLhook + stream5061 (+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>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
# Conflicts:
#	packages/builders/src/vercel-build-output-api.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
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.

1 participant

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

Support framework base paths outside Next.js - #2771

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

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

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo 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 beforehandle: 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-v3sveltekitastronuxt (nitropack v2 / legacy BOA)
full workflow run completes (queue → flow fn → steps)addTen=42 awaited over HTTP✅ run completed✅ run completedprobes only (queue path shares the astro-verified BOA function code)
runtime base path global inside flow fnmetadata.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 → base404served at both paths, like all Astro routes302 → 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-botBot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e4db2b

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

This PR includes changesets to release 16 packages
NameType
@workflow/buildersPatch
@workflow/astroPatch
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/vitestPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch
@workflow/webPatch

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 Jul 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/astro/src/builder.ts
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-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 1e4db2b · Tue, 14 Jul 2026 23:17:09 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1341 (-2.7%)1628 🔴1713 🔴3659 🔴30
TTFShook + stream1510 (+10%)1893 🔴2040 🔴2279 🔴30
STSO1020 steps (1-20)308 (+2.7%)311 🔴355 🔴974 🔴19
STSO1020 steps (101-120)428 (-1.5%)462 🔴501 🔴593 🔴19
STSO1020 steps (1001-1020)908 (+0.6%)916 🔴1098 🔴1115 🔴19
WOstream1341 (-2.7%)16281713365930
WOhook + stream1510 (+10%)18932040227930
SLstream4291 (-15%)5074 🔴5632 🔴5794 🔴30
SLhook + stream5070 (+0.6%)5601 🔴5776 🔴6467 🔴30
📜 Previous results (4)

7f3d6b9

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1235 (+5.7%)1666 🔴1685 🔴2108 🔴30
TTFShook + stream1424 (+3.5%)1893 🔴1931 🔴2021 🔴30
STSO1020 steps (1-20)246 (-9.5%)269 🔴306 🔴321 🔴19
STSO1020 steps (101-120)386 (-7.3%)436 🔴483 🔴504 🔴19
STSO1020 steps (1001-1020)1078 (±0%)924 🔴1265 🔴4600 🔴19
WOstream1235 (+5.7%)16661685210830
WOhook + stream1424 (+3.5%)18931931202130
SLstream4027 (-18%)5053 🔴5713 🔴6063 🔴30
SLhook + stream5055 (+0.6%)5705 🔴5820 🔴5976 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1601 (+37%)1595 🔴1665 🔴1899 🔴30
TTFShook + stream1814 (+32%)1877 🔴1901 🔴2008 🔴30
STSO1020 steps (1-20)262 (-3.5%)304 🔴356 🔴410 🔴19
STSO1020 steps (101-120)427 (+2.5%)442 🔴599 🔴619 🔴19
STSO1020 steps (1001-1020)1129 (+4.8%)1006 🔴1336 🔴4632 🔴19
WOstream1601 (+37%)15951665189930
WOhook + stream1814 (+32%)18771901200830
SLstream4663 (-5.3%)4741 🔴4829 🔴5472 🔴30
SLhook + stream4510 (-10%)4565 🔴4726 🔴5563 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1248 (+12%)1664 🔴1699 🔴1788 🔴30
STSO1020 steps (1-20)280 (+2.8%)295 🔴402 🔴472 🔴19
STSO1020 steps (101-120)395 (-4.0%)467 🔴488 🔴495 🔴19
STSO1020 steps (1001-1020)857 (-1.9%)905 🔴982 🔴1016 🔴19
WOstream1248 (+12%)16641699178830
SLstream4640 (-0.7%)5744 🔴5872 🔴6056 🔴30

f5a13cd

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1333 (+20%)1665 🔴1719 🔴1811 🔴30
TTFShook + stream1686 (+31%)1922 🔴2083 🔴5721 🔴30
STSO1020 steps (1-20)268 (-1.5%)321 🔴335 🔴386 🔴19
STSO1020 steps (101-120)404 (-1.6%)449 🔴475 🔴482 🔴19
STSO1020 steps (1001-1020)1057 (+21%)1008 🔴1262 🔴4275 🔴19
WOstream1333 (+20%)16651719181130
WOhook + stream1686 (+31%)19222083572130
SLstream4573 (-2.1%)5489 🔴5817 🔴6011 🔴30
SLhook + stream5061 (+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>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
# Conflicts:
#	packages/builders/src/vercel-build-output-api.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
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.

1 participant

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

Support framework base paths outside Next.js - #2771

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

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

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo 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 beforehandle: 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-v3sveltekitastronuxt (nitropack v2 / legacy BOA)
full workflow run completes (queue → flow fn → steps)addTen=42 awaited over HTTP✅ run completed✅ run completedprobes only (queue path shares the astro-verified BOA function code)
runtime base path global inside flow fnmetadata.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 → base404served at both paths, like all Astro routes302 → 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-botBot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e4db2b

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

This PR includes changesets to release 16 packages
NameType
@workflow/buildersPatch
@workflow/astroPatch
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/vitestPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch
@workflow/webPatch

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 Jul 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/astro/src/builder.ts
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-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 1e4db2b · Tue, 14 Jul 2026 23:17:09 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1341 (-2.7%)1628 🔴1713 🔴3659 🔴30
TTFShook + stream1510 (+10%)1893 🔴2040 🔴2279 🔴30
STSO1020 steps (1-20)308 (+2.7%)311 🔴355 🔴974 🔴19
STSO1020 steps (101-120)428 (-1.5%)462 🔴501 🔴593 🔴19
STSO1020 steps (1001-1020)908 (+0.6%)916 🔴1098 🔴1115 🔴19
WOstream1341 (-2.7%)16281713365930
WOhook + stream1510 (+10%)18932040227930
SLstream4291 (-15%)5074 🔴5632 🔴5794 🔴30
SLhook + stream5070 (+0.6%)5601 🔴5776 🔴6467 🔴30
📜 Previous results (4)

7f3d6b9

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1235 (+5.7%)1666 🔴1685 🔴2108 🔴30
TTFShook + stream1424 (+3.5%)1893 🔴1931 🔴2021 🔴30
STSO1020 steps (1-20)246 (-9.5%)269 🔴306 🔴321 🔴19
STSO1020 steps (101-120)386 (-7.3%)436 🔴483 🔴504 🔴19
STSO1020 steps (1001-1020)1078 (±0%)924 🔴1265 🔴4600 🔴19
WOstream1235 (+5.7%)16661685210830
WOhook + stream1424 (+3.5%)18931931202130
SLstream4027 (-18%)5053 🔴5713 🔴6063 🔴30
SLhook + stream5055 (+0.6%)5705 🔴5820 🔴5976 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1601 (+37%)1595 🔴1665 🔴1899 🔴30
TTFShook + stream1814 (+32%)1877 🔴1901 🔴2008 🔴30
STSO1020 steps (1-20)262 (-3.5%)304 🔴356 🔴410 🔴19
STSO1020 steps (101-120)427 (+2.5%)442 🔴599 🔴619 🔴19
STSO1020 steps (1001-1020)1129 (+4.8%)1006 🔴1336 🔴4632 🔴19
WOstream1601 (+37%)15951665189930
WOhook + stream1814 (+32%)18771901200830
SLstream4663 (-5.3%)4741 🔴4829 🔴5472 🔴30
SLhook + stream4510 (-10%)4565 🔴4726 🔴5563 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1248 (+12%)1664 🔴1699 🔴1788 🔴30
STSO1020 steps (1-20)280 (+2.8%)295 🔴402 🔴472 🔴19
STSO1020 steps (101-120)395 (-4.0%)467 🔴488 🔴495 🔴19
STSO1020 steps (1001-1020)857 (-1.9%)905 🔴982 🔴1016 🔴19
WOstream1248 (+12%)16641699178830
SLstream4640 (-0.7%)5744 🔴5872 🔴6056 🔴30

f5a13cd

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1333 (+20%)1665 🔴1719 🔴1811 🔴30
TTFShook + stream1686 (+31%)1922 🔴2083 🔴5721 🔴30
STSO1020 steps (1-20)268 (-1.5%)321 🔴335 🔴386 🔴19
STSO1020 steps (101-120)404 (-1.6%)449 🔴475 🔴482 🔴19
STSO1020 steps (1001-1020)1057 (+21%)1008 🔴1262 🔴4275 🔴19
WOstream1333 (+20%)16651719181130
WOhook + stream1686 (+31%)19222083572130
SLstream4573 (-2.1%)5489 🔴5817 🔴6011 🔴30
SLhook + stream5061 (+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>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
# Conflicts:
#	packages/builders/src/vercel-build-output-api.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
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.

1 participant

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

Support framework base paths outside Next.js - #2771

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

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

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo 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 beforehandle: 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-v3sveltekitastronuxt (nitropack v2 / legacy BOA)
full workflow run completes (queue → flow fn → steps)addTen=42 awaited over HTTP✅ run completed✅ run completedprobes only (queue path shares the astro-verified BOA function code)
runtime base path global inside flow fnmetadata.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 → base404served at both paths, like all Astro routes302 → 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-botBot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e4db2b

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

This PR includes changesets to release 16 packages
NameType
@workflow/buildersPatch
@workflow/astroPatch
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/vitestPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch
@workflow/webPatch

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 Jul 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/astro/src/builder.ts
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-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 1e4db2b · Tue, 14 Jul 2026 23:17:09 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1341 (-2.7%)1628 🔴1713 🔴3659 🔴30
TTFShook + stream1510 (+10%)1893 🔴2040 🔴2279 🔴30
STSO1020 steps (1-20)308 (+2.7%)311 🔴355 🔴974 🔴19
STSO1020 steps (101-120)428 (-1.5%)462 🔴501 🔴593 🔴19
STSO1020 steps (1001-1020)908 (+0.6%)916 🔴1098 🔴1115 🔴19
WOstream1341 (-2.7%)16281713365930
WOhook + stream1510 (+10%)18932040227930
SLstream4291 (-15%)5074 🔴5632 🔴5794 🔴30
SLhook + stream5070 (+0.6%)5601 🔴5776 🔴6467 🔴30
📜 Previous results (4)

7f3d6b9

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1235 (+5.7%)1666 🔴1685 🔴2108 🔴30
TTFShook + stream1424 (+3.5%)1893 🔴1931 🔴2021 🔴30
STSO1020 steps (1-20)246 (-9.5%)269 🔴306 🔴321 🔴19
STSO1020 steps (101-120)386 (-7.3%)436 🔴483 🔴504 🔴19
STSO1020 steps (1001-1020)1078 (±0%)924 🔴1265 🔴4600 🔴19
WOstream1235 (+5.7%)16661685210830
WOhook + stream1424 (+3.5%)18931931202130
SLstream4027 (-18%)5053 🔴5713 🔴6063 🔴30
SLhook + stream5055 (+0.6%)5705 🔴5820 🔴5976 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1601 (+37%)1595 🔴1665 🔴1899 🔴30
TTFShook + stream1814 (+32%)1877 🔴1901 🔴2008 🔴30
STSO1020 steps (1-20)262 (-3.5%)304 🔴356 🔴410 🔴19
STSO1020 steps (101-120)427 (+2.5%)442 🔴599 🔴619 🔴19
STSO1020 steps (1001-1020)1129 (+4.8%)1006 🔴1336 🔴4632 🔴19
WOstream1601 (+37%)15951665189930
WOhook + stream1814 (+32%)18771901200830
SLstream4663 (-5.3%)4741 🔴4829 🔴5472 🔴30
SLhook + stream4510 (-10%)4565 🔴4726 🔴5563 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1248 (+12%)1664 🔴1699 🔴1788 🔴30
STSO1020 steps (1-20)280 (+2.8%)295 🔴402 🔴472 🔴19
STSO1020 steps (101-120)395 (-4.0%)467 🔴488 🔴495 🔴19
STSO1020 steps (1001-1020)857 (-1.9%)905 🔴982 🔴1016 🔴19
WOstream1248 (+12%)16641699178830
SLstream4640 (-0.7%)5744 🔴5872 🔴6056 🔴30

f5a13cd

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1333 (+20%)1665 🔴1719 🔴1811 🔴30
TTFShook + stream1686 (+31%)1922 🔴2083 🔴5721 🔴30
STSO1020 steps (1-20)268 (-1.5%)321 🔴335 🔴386 🔴19
STSO1020 steps (101-120)404 (-1.6%)449 🔴475 🔴482 🔴19
STSO1020 steps (1001-1020)1057 (+21%)1008 🔴1262 🔴4275 🔴19
WOstream1333 (+20%)16651719181130
WOhook + stream1686 (+31%)19222083572130
SLstream4573 (-2.1%)5489 🔴5817 🔴6011 🔴30
SLhook + stream5061 (+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>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
# Conflicts:
#	packages/builders/src/vercel-build-output-api.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
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.

1 participant

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

Support framework base paths outside Next.js - #2771

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

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

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo 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 beforehandle: 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-v3sveltekitastronuxt (nitropack v2 / legacy BOA)
full workflow run completes (queue → flow fn → steps)addTen=42 awaited over HTTP✅ run completed✅ run completedprobes only (queue path shares the astro-verified BOA function code)
runtime base path global inside flow fnmetadata.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 → base404served at both paths, like all Astro routes302 → 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-botBot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e4db2b

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

This PR includes changesets to release 16 packages
NameType
@workflow/buildersPatch
@workflow/astroPatch
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/vitestPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch
@workflow/webPatch

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 Jul 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/astro/src/builder.ts
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-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 1e4db2b · Tue, 14 Jul 2026 23:17:09 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1341 (-2.7%)1628 🔴1713 🔴3659 🔴30
TTFShook + stream1510 (+10%)1893 🔴2040 🔴2279 🔴30
STSO1020 steps (1-20)308 (+2.7%)311 🔴355 🔴974 🔴19
STSO1020 steps (101-120)428 (-1.5%)462 🔴501 🔴593 🔴19
STSO1020 steps (1001-1020)908 (+0.6%)916 🔴1098 🔴1115 🔴19
WOstream1341 (-2.7%)16281713365930
WOhook + stream1510 (+10%)18932040227930
SLstream4291 (-15%)5074 🔴5632 🔴5794 🔴30
SLhook + stream5070 (+0.6%)5601 🔴5776 🔴6467 🔴30
📜 Previous results (4)

7f3d6b9

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1235 (+5.7%)1666 🔴1685 🔴2108 🔴30
TTFShook + stream1424 (+3.5%)1893 🔴1931 🔴2021 🔴30
STSO1020 steps (1-20)246 (-9.5%)269 🔴306 🔴321 🔴19
STSO1020 steps (101-120)386 (-7.3%)436 🔴483 🔴504 🔴19
STSO1020 steps (1001-1020)1078 (±0%)924 🔴1265 🔴4600 🔴19
WOstream1235 (+5.7%)16661685210830
WOhook + stream1424 (+3.5%)18931931202130
SLstream4027 (-18%)5053 🔴5713 🔴6063 🔴30
SLhook + stream5055 (+0.6%)5705 🔴5820 🔴5976 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1601 (+37%)1595 🔴1665 🔴1899 🔴30
TTFShook + stream1814 (+32%)1877 🔴1901 🔴2008 🔴30
STSO1020 steps (1-20)262 (-3.5%)304 🔴356 🔴410 🔴19
STSO1020 steps (101-120)427 (+2.5%)442 🔴599 🔴619 🔴19
STSO1020 steps (1001-1020)1129 (+4.8%)1006 🔴1336 🔴4632 🔴19
WOstream1601 (+37%)15951665189930
WOhook + stream1814 (+32%)18771901200830
SLstream4663 (-5.3%)4741 🔴4829 🔴5472 🔴30
SLhook + stream4510 (-10%)4565 🔴4726 🔴5563 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1248 (+12%)1664 🔴1699 🔴1788 🔴30
STSO1020 steps (1-20)280 (+2.8%)295 🔴402 🔴472 🔴19
STSO1020 steps (101-120)395 (-4.0%)467 🔴488 🔴495 🔴19
STSO1020 steps (1001-1020)857 (-1.9%)905 🔴982 🔴1016 🔴19
WOstream1248 (+12%)16641699178830
SLstream4640 (-0.7%)5744 🔴5872 🔴6056 🔴30

f5a13cd

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1333 (+20%)1665 🔴1719 🔴1811 🔴30
TTFShook + stream1686 (+31%)1922 🔴2083 🔴5721 🔴30
STSO1020 steps (1-20)268 (-1.5%)321 🔴335 🔴386 🔴19
STSO1020 steps (101-120)404 (-1.6%)449 🔴475 🔴482 🔴19
STSO1020 steps (1001-1020)1057 (+21%)1008 🔴1262 🔴4275 🔴19
WOstream1333 (+20%)16651719181130
WOhook + stream1686 (+31%)19222083572130
SLstream4573 (-2.1%)5489 🔴5817 🔴6011 🔴30
SLhook + stream5061 (+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>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
# Conflicts:
#	packages/builders/src/vercel-build-output-api.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
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.

1 participant

@NathanColosimo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support framework base paths outside Next.js - #2771

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

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

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo 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 beforehandle: 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-v3sveltekitastronuxt (nitropack v2 / legacy BOA)
full workflow run completes (queue → flow fn → steps)addTen=42 awaited over HTTP✅ run completed✅ run completedprobes only (queue path shares the astro-verified BOA function code)
runtime base path global inside flow fnmetadata.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 → base404served at both paths, like all Astro routes302 → 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-botBot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e4db2b

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

This PR includes changesets to release 16 packages
NameType
@workflow/buildersPatch
@workflow/astroPatch
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/vitestPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch
@workflow/webPatch

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 Jul 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/astro/src/builder.ts
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-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 1e4db2b · Tue, 14 Jul 2026 23:17:09 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1341 (-2.7%)1628 🔴1713 🔴3659 🔴30
TTFShook + stream1510 (+10%)1893 🔴2040 🔴2279 🔴30
STSO1020 steps (1-20)308 (+2.7%)311 🔴355 🔴974 🔴19
STSO1020 steps (101-120)428 (-1.5%)462 🔴501 🔴593 🔴19
STSO1020 steps (1001-1020)908 (+0.6%)916 🔴1098 🔴1115 🔴19
WOstream1341 (-2.7%)16281713365930
WOhook + stream1510 (+10%)18932040227930
SLstream4291 (-15%)5074 🔴5632 🔴5794 🔴30
SLhook + stream5070 (+0.6%)5601 🔴5776 🔴6467 🔴30
📜 Previous results (4)

7f3d6b9

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1235 (+5.7%)1666 🔴1685 🔴2108 🔴30
TTFShook + stream1424 (+3.5%)1893 🔴1931 🔴2021 🔴30
STSO1020 steps (1-20)246 (-9.5%)269 🔴306 🔴321 🔴19
STSO1020 steps (101-120)386 (-7.3%)436 🔴483 🔴504 🔴19
STSO1020 steps (1001-1020)1078 (±0%)924 🔴1265 🔴4600 🔴19
WOstream1235 (+5.7%)16661685210830
WOhook + stream1424 (+3.5%)18931931202130
SLstream4027 (-18%)5053 🔴5713 🔴6063 🔴30
SLhook + stream5055 (+0.6%)5705 🔴5820 🔴5976 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1601 (+37%)1595 🔴1665 🔴1899 🔴30
TTFShook + stream1814 (+32%)1877 🔴1901 🔴2008 🔴30
STSO1020 steps (1-20)262 (-3.5%)304 🔴356 🔴410 🔴19
STSO1020 steps (101-120)427 (+2.5%)442 🔴599 🔴619 🔴19
STSO1020 steps (1001-1020)1129 (+4.8%)1006 🔴1336 🔴4632 🔴19
WOstream1601 (+37%)15951665189930
WOhook + stream1814 (+32%)18771901200830
SLstream4663 (-5.3%)4741 🔴4829 🔴5472 🔴30
SLhook + stream4510 (-10%)4565 🔴4726 🔴5563 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1248 (+12%)1664 🔴1699 🔴1788 🔴30
STSO1020 steps (1-20)280 (+2.8%)295 🔴402 🔴472 🔴19
STSO1020 steps (101-120)395 (-4.0%)467 🔴488 🔴495 🔴19
STSO1020 steps (1001-1020)857 (-1.9%)905 🔴982 🔴1016 🔴19
WOstream1248 (+12%)16641699178830
SLstream4640 (-0.7%)5744 🔴5872 🔴6056 🔴30

f5a13cd

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1333 (+20%)1665 🔴1719 🔴1811 🔴30
TTFShook + stream1686 (+31%)1922 🔴2083 🔴5721 🔴30
STSO1020 steps (1-20)268 (-1.5%)321 🔴335 🔴386 🔴19
STSO1020 steps (101-120)404 (-1.6%)449 🔴475 🔴482 🔴19
STSO1020 steps (1001-1020)1057 (+21%)1008 🔴1262 🔴4275 🔴19
WOstream1333 (+20%)16651719181130
WOhook + stream1686 (+31%)19222083572130
SLstream4573 (-2.1%)5489 🔴5817 🔴6011 🔴30
SLhook + stream5061 (+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>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
# Conflicts:
#	packages/builders/src/vercel-build-output-api.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
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.

1 participant

@NathanColosimo
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Support framework base paths outside Next.js - #2771

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

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

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo 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 beforehandle: 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-v3sveltekitastronuxt (nitropack v2 / legacy BOA)
full workflow run completes (queue → flow fn → steps)addTen=42 awaited over HTTP✅ run completed✅ run completedprobes only (queue path shares the astro-verified BOA function code)
runtime base path global inside flow fnmetadata.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 → base404served at both paths, like all Astro routes302 → 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-botBot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e4db2b

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

This PR includes changesets to release 16 packages
NameType
@workflow/buildersPatch
@workflow/astroPatch
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/vitePatch
@workflow/vitestPatch
workflowPatch
@workflow/world-testingPatch
@workflow/corePatch
@workflow/web-sharedPatch
@workflow/webPatch

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 Jul 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

Comment threadpackages/astro/src/builder.ts
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-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 1e4db2b · Tue, 14 Jul 2026 23:17:09 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1341 (-2.7%)1628 🔴1713 🔴3659 🔴30
TTFShook + stream1510 (+10%)1893 🔴2040 🔴2279 🔴30
STSO1020 steps (1-20)308 (+2.7%)311 🔴355 🔴974 🔴19
STSO1020 steps (101-120)428 (-1.5%)462 🔴501 🔴593 🔴19
STSO1020 steps (1001-1020)908 (+0.6%)916 🔴1098 🔴1115 🔴19
WOstream1341 (-2.7%)16281713365930
WOhook + stream1510 (+10%)18932040227930
SLstream4291 (-15%)5074 🔴5632 🔴5794 🔴30
SLhook + stream5070 (+0.6%)5601 🔴5776 🔴6467 🔴30
📜 Previous results (4)

7f3d6b9

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1235 (+5.7%)1666 🔴1685 🔴2108 🔴30
TTFShook + stream1424 (+3.5%)1893 🔴1931 🔴2021 🔴30
STSO1020 steps (1-20)246 (-9.5%)269 🔴306 🔴321 🔴19
STSO1020 steps (101-120)386 (-7.3%)436 🔴483 🔴504 🔴19
STSO1020 steps (1001-1020)1078 (±0%)924 🔴1265 🔴4600 🔴19
WOstream1235 (+5.7%)16661685210830
WOhook + stream1424 (+3.5%)18931931202130
SLstream4027 (-18%)5053 🔴5713 🔴6063 🔴30
SLhook + stream5055 (+0.6%)5705 🔴5820 🔴5976 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1601 (+37%)1595 🔴1665 🔴1899 🔴30
TTFShook + stream1814 (+32%)1877 🔴1901 🔴2008 🔴30
STSO1020 steps (1-20)262 (-3.5%)304 🔴356 🔴410 🔴19
STSO1020 steps (101-120)427 (+2.5%)442 🔴599 🔴619 🔴19
STSO1020 steps (1001-1020)1129 (+4.8%)1006 🔴1336 🔴4632 🔴19
WOstream1601 (+37%)15951665189930
WOhook + stream1814 (+32%)18771901200830
SLstream4663 (-5.3%)4741 🔴4829 🔴5472 🔴30
SLhook + stream4510 (-10%)4565 🔴4726 🔴5563 🔴30

41d2013

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1248 (+12%)1664 🔴1699 🔴1788 🔴30
STSO1020 steps (1-20)280 (+2.8%)295 🔴402 🔴472 🔴19
STSO1020 steps (101-120)395 (-4.0%)467 🔴488 🔴495 🔴19
STSO1020 steps (1001-1020)857 (-1.9%)905 🔴982 🔴1016 🔴19
WOstream1248 (+12%)16641699178830
SLstream4640 (-0.7%)5744 🔴5872 🔴6056 🔴30

f5a13cd

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

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1333 (+20%)1665 🔴1719 🔴1811 🔴30
TTFShook + stream1686 (+31%)1922 🔴2083 🔴5721 🔴30
STSO1020 steps (1-20)268 (-1.5%)321 🔴335 🔴386 🔴19
STSO1020 steps (101-120)404 (-1.6%)449 🔴475 🔴482 🔴19
STSO1020 steps (1001-1020)1057 (+21%)1008 🔴1262 🔴4275 🔴19
WOstream1333 (+20%)16651719181130
WOhook + stream1686 (+31%)19222083572130
SLstream4573 (-2.1%)5489 🔴5817 🔴6011 🔴30
SLhook + stream5061 (+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>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
# Conflicts:
#	packages/builders/src/vercel-build-output-api.ts
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
…-base-path
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
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.

1 participant

@NathanColosimo