From 396cf777636e04c3b0e82c28a652da00672ba728 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Mon, 17 Aug 2026 12:31:30 -0700 Subject: [PATCH 01/10] fix: break the type cycle between middleware data and downstream validators --- .changeset/middleware-handler-type-cycle.md | 5 ++ agent-feedback/dx.md | 6 -- .../.marko-run/routes.d.ts | 87 +++++++++++++++++++ .../__snapshots__/dev.expected.md | 5 ++ .../__snapshots__/preview.expected.md | 5 ++ .../src/routes/api/procs/$pid/+handler.ts | 10 +++ .../routes/api/workspaces/$id/+middleware.ts | 3 + .../$id/review/$commentId/+handler.ts | 13 +++ .../params-validator-types/test.config.ts | 15 ++++ .../tsconfig.check.json | 11 +++ .../params-validator-types/tsconfig.json | 4 + packages/run/src/__tests__/typecheck.test.ts | 37 ++++++++ packages/run/src/runtime/types.ts | 28 +++--- 13 files changed, 205 insertions(+), 24 deletions(-) create mode 100644 .changeset/middleware-handler-type-cycle.md create mode 100644 packages/run/src/__tests__/fixtures/params-validator-types/.marko-run/routes.d.ts create mode 100644 packages/run/src/__tests__/fixtures/params-validator-types/__snapshots__/dev.expected.md create mode 100644 packages/run/src/__tests__/fixtures/params-validator-types/__snapshots__/preview.expected.md create mode 100644 packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts create mode 100644 packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts create mode 100644 packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts create mode 100644 packages/run/src/__tests__/fixtures/params-validator-types/test.config.ts create mode 100644 packages/run/src/__tests__/fixtures/params-validator-types/tsconfig.check.json create mode 100644 packages/run/src/__tests__/fixtures/params-validator-types/tsconfig.json diff --git a/.changeset/middleware-handler-type-cycle.md b/.changeset/middleware-handler-type-cycle.md new file mode 100644 index 00000000..0abe659b --- /dev/null +++ b/.changeset/middleware-handler-type-cycle.md @@ -0,0 +1,5 @@ +--- +"@marko/run": patch +--- + +Fix `Run.DELETE({ params(...) }, handler)` and similar losing its typing (TS7022/TS2554) when a parent-segment middleware passes context-derived data to `next`. diff --git a/agent-feedback/dx.md b/agent-feedback/dx.md index 5b7304f2..852912d4 100644 --- a/agent-feedback/dx.md +++ b/agent-feedback/dx.md @@ -209,9 +209,3 @@ CodeRabbit's pre-merge checks (configured in the organization UI; the repo has n `cspell.json` › `words` | 2026-08-11 | impact:med | effort:low `pnpm run lint` fails on a clean `main` with a single cspell error: `agent-feedback/cleanup.md` uses "macrotask", which is not in `cspell.json`'s `words` list. The word arrived with 445114c (#256) and the check covers `**/*.{md,ts,marko}`, so every branch cut from main inherits a red lint until the word is added. Re-verify: `npx cspell "**/*.{md,ts,marko}" --no-progress` on main reports one issue. - -## A `params` validator on a route with two `$` segments loses its typing - -`packages/run/src/runtime/types.ts` › `RouteForFileDef` | 2026-08-17 | impact:low | effort:med - -In an app with `api/workspaces/$id/+middleware.ts` (a `Run.ALL` middleware calling `next({ workspace })`) and a leaf `api/workspaces/$id/review/$commentId/+handler.ts` exporting `Run.DELETE({ params({ id, commentId }) { … } }, (ctx) => …)`, `tsc` reports TS7031 (`id` / `commentId` implicitly any) on the validator's destructured argument and TS2554 ("Expected 1 arguments, but got 2") on the `Run.DELETE` call, so the two-argument form is unusable there. The identical shape on a one-segment route with no parent middleware (`api/procs/$pid/+handler.ts`, `Run.DELETE({ params({ pid }) … }, handler)`) type-checks. Something in the merge of the parent middleware's options with the leaf's `params` (`MergedRouteOptionsForFile` → `RouteForFileDef.params`) collapses when the path has two params, since the workaround is simply parsing `ctx.params.commentId` inside the handler. Repro: the two files above in a fixture under `packages/run/src/__tests__/fixtures/`, then `tsc` on it; a passing fixture with the validator form is the fix's test. diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/.marko-run/routes.d.ts b/packages/run/src/__tests__/fixtures/params-validator-types/.marko-run/routes.d.ts new file mode 100644 index 00000000..b917c161 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/params-validator-types/.marko-run/routes.d.ts @@ -0,0 +1,87 @@ +/* + WARNING: This file is automatically generated and any changes made to it will be overwritten without warning. + Do NOT manually edit this file or your changes will be lost. +*/ + +import { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform } from "@marko/run/namespace"; +import type * as $ from "@marko/run"; + + +declare module "@marko/run" { + interface App extends $.DefineRoutes<{ + "/api/procs/$pid": [H1]; + "/api/workspaces/$id/review/$commentId": [M1, H2]; + }> {} +} + +type M1 = $.Middleware<"M1", typeof import("../src/routes/api/workspaces/$id/+middleware")>; +declare module "../src/routes/api/workspaces/$id/+middleware" { + const Run: $.Namespace; + namespace Run { + type Context = $.ContextForFile; + } + + /** @deprecated use `Run` namespace instead */ + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = $.Routes["/api/workspaces/$id/review/$commentId"]; + export type Context = $.MultiRouteContext; + export type Handler = $.HandlerLike; + export type GET = $.HandlerLike; + export type HEAD = $.HandlerLike; + export type POST = $.HandlerLike; + export type PUT = $.HandlerLike; + export type DELETE = $.HandlerLike; + export type PATCH = $.HandlerLike; + export type OPTIONS = $.HandlerLike; + export type QUERY = $.HandlerLike; + } +} + +type H1 = $.Handler<"H1", typeof import("../src/routes/api/procs/$pid/+handler")>; +declare module "../src/routes/api/procs/$pid/+handler" { + const Run: $.Namespace

; + namespace Run { + type Context = $.ContextForFile

; + } + + /** @deprecated use `Run` namespace instead */ + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = $.Routes["/api/procs/$pid"]; + export type Context = $.MultiRouteContext; + export type Handler = $.HandlerLike; + export type GET = $.HandlerLike; + export type HEAD = $.HandlerLike; + export type POST = $.HandlerLike; + export type PUT = $.HandlerLike; + export type DELETE = $.HandlerLike; + export type PATCH = $.HandlerLike; + export type OPTIONS = $.HandlerLike; + export type QUERY = $.HandlerLike; + } +} + +type H2 = $.Handler<"H2", typeof import("../src/routes/api/workspaces/$id/review/$commentId/+handler")>; +declare module "../src/routes/api/workspaces/$id/review/$commentId/+handler" { + const Run: $.Namespace

; + namespace Run { + type Context = $.ContextForFile

; + } + + /** @deprecated use `Run` namespace instead */ + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = $.Routes["/api/workspaces/$id/review/$commentId"]; + export type Context = $.MultiRouteContext; + export type Handler = $.HandlerLike; + export type GET = $.HandlerLike; + export type HEAD = $.HandlerLike; + export type POST = $.HandlerLike; + export type PUT = $.HandlerLike; + export type DELETE = $.HandlerLike; + export type PATCH = $.HandlerLike; + export type OPTIONS = $.HandlerLike; + export type QUERY = $.HandlerLike; + } +} diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/__snapshots__/dev.expected.md b/packages/run/src/__tests__/fixtures/params-validator-types/__snapshots__/dev.expected.md new file mode 100644 index 00000000..df97ae65 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/params-validator-types/__snapshots__/dev.expected.md @@ -0,0 +1,5 @@ +# Loading + +# Step 0 +ctx=>deleteComment(ctx) + diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/__snapshots__/preview.expected.md b/packages/run/src/__tests__/fixtures/params-validator-types/__snapshots__/preview.expected.md new file mode 100644 index 00000000..df97ae65 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/params-validator-types/__snapshots__/preview.expected.md @@ -0,0 +1,5 @@ +# Loading + +# Step 0 +ctx=>deleteComment(ctx) + diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts new file mode 100644 index 00000000..55831c10 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts @@ -0,0 +1,10 @@ +export const DELETE = Run.DELETE( + { + params({ pid }) { + return { pid }; + }, + }, + (ctx) => { + return Response.json({ pid: ctx.params.pid }); + }, +); diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts new file mode 100644 index 00000000..ddf06e38 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts @@ -0,0 +1,3 @@ +export default Run.ALL((ctx, next) => { + return next({ workspace: `ws-${ctx.params.id}` }); +}); diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts new file mode 100644 index 00000000..432a0094 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts @@ -0,0 +1,13 @@ +export const DELETE = Run.DELETE( + { + params({ id, commentId }) { + return { id, commentId: Number(commentId) }; + }, + }, + (ctx) => { + return Response.json({ + id: ctx.params.id, + commentId: ctx.params.commentId, + }); + }, +); diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/test.config.ts b/packages/run/src/__tests__/fixtures/params-validator-types/test.config.ts new file mode 100644 index 00000000..7951a292 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/params-validator-types/test.config.ts @@ -0,0 +1,15 @@ +import assert from "assert"; + +import type { Step, StepContext } from "../../main.test"; + +export const path = "/api/procs/42"; + +async function deleteComment({ page }: StepContext) { + const res = await page.fetch( + new URL("/api/workspaces/w1/review/7", page.url()).href, + { method: "DELETE" }, + ); + assert.deepEqual(await res.json(), { id: "w1", commentId: 7 }); +} + +export const steps: Step[] = [(ctx) => deleteComment(ctx)]; diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/tsconfig.check.json b/packages/run/src/__tests__/fixtures/params-validator-types/tsconfig.check.json new file mode 100644 index 00000000..be7e4d43 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/params-validator-types/tsconfig.check.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "baseUrl": ".", + "paths": { + "@marko/run": ["../../../runtime/index.ts"], + "@marko/run/namespace": ["../../../runtime/namespace.ts"] + } + } +} diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/tsconfig.json b/packages/run/src/__tests__/fixtures/params-validator-types/tsconfig.json new file mode 100644 index 00000000..03ff88a5 --- /dev/null +++ b/packages/run/src/__tests__/fixtures/params-validator-types/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig-base.json", + "include": ["src/**/*", ".marko-run/*"], +} \ No newline at end of file diff --git a/packages/run/src/__tests__/typecheck.test.ts b/packages/run/src/__tests__/typecheck.test.ts index 41d46982..8b455295 100644 --- a/packages/run/src/__tests__/typecheck.test.ts +++ b/packages/run/src/__tests__/typecheck.test.ts @@ -57,3 +57,40 @@ describe("test sources type-check", () => { ); }); }); + +describe("route type generation type-checks", () => { + // A parent-segment middleware that derives `next(data)` from its context + // plus a downstream handler with a `params` validator once made the two + // module types circular (TS7022); this fixture compiles both against the + // workspace source types. + it("should type-check the params-validator-types fixture", function () { + this.timeout(120000); + const result = spawnSync( + process.execPath, + [ + require.resolve("typescript/lib/tsc.js"), + "-p", + path.join( + __dirname, + "fixtures", + "params-validator-types", + "tsconfig.check.json", + ), + "--pretty", + "false", + ], + { encoding: "utf-8", timeout: 90000 }, + ); + + const localErrors = (result.stdout || "") + .split("\n") + // The compile pulls in the whole workspace graph, which carries known + // declaration-level noise; only errors in the fixture's sources or its + // generated route types indicate a regression here. + .filter((line) => + /params-validator-types[/\\](src|\.marko-run)[/\\]/.test(line), + ); + + assert.deepEqual(localErrors, []); + }); +}); diff --git a/packages/run/src/runtime/types.ts b/packages/run/src/runtime/types.ts index 9a77f580..28e0a104 100644 --- a/packages/run/src/runtime/types.ts +++ b/packages/run/src/runtime/types.ts @@ -477,18 +477,9 @@ type DefineHandlerOptions = [Verb] extends [ ] ? HandlerOptionsWithoutBody : HandlerOptionsWithBody; -type TypesFromHandlerFilesWithLocal< - Files extends File[], - Verb extends HttpVerb, - Id extends ID, - Options, -> = { - [I in keyof Files]: Files[I] extends { - id: Id; - } - ? HandlerTypes - : TypesFromHandlerFile; -}; +// Only files that run before the local one contribute options: a later +// file's type may itself depend on this file's (via upstream data), and +// including it makes the two inferences circular. type RouteFileGroupOptionsWithLocal< Group extends RouteFileGroup, Verb extends HttpVerb, @@ -496,12 +487,13 @@ type RouteFileGroupOptionsWithLocal< Options, > = MergeHandlerOptionsTuple< MapTuple< - TypesFromHandlerFilesWithLocal< - [...Group["middleware"], Group["handler"]], - Verb, - Id, - Options - >, + [ + ...TypesFromHandlerFiles< + TakeUntil<[...Group["middleware"], Group["handler"]], Id>, + Verb + >, + HandlerTypes, + ], "options", Empty > From 448bd7529cc102a327a6d5b596c82add98336284 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Mon, 17 Aug 2026 12:46:57 -0700 Subject: [PATCH 02/10] revert: restore the full downstream options merge; pin the type cycle as a repro --- .changeset/middleware-handler-type-cycle.md | 5 ---- agent-feedback/dx.md | 6 +++++ packages/run/src/__tests__/typecheck.test.ts | 25 +++++++++-------- packages/run/src/runtime/types.ts | 28 +++++++++++++------- 4 files changed, 38 insertions(+), 26 deletions(-) delete mode 100644 .changeset/middleware-handler-type-cycle.md diff --git a/.changeset/middleware-handler-type-cycle.md b/.changeset/middleware-handler-type-cycle.md deleted file mode 100644 index 0abe659b..00000000 --- a/.changeset/middleware-handler-type-cycle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@marko/run": patch ---- - -Fix `Run.DELETE({ params(...) }, handler)` and similar losing its typing (TS7022/TS2554) when a parent-segment middleware passes context-derived data to `next`. diff --git a/agent-feedback/dx.md b/agent-feedback/dx.md index 852912d4..5b7304f2 100644 --- a/agent-feedback/dx.md +++ b/agent-feedback/dx.md @@ -209,3 +209,9 @@ CodeRabbit's pre-merge checks (configured in the organization UI; the repo has n `cspell.json` › `words` | 2026-08-11 | impact:med | effort:low `pnpm run lint` fails on a clean `main` with a single cspell error: `agent-feedback/cleanup.md` uses "macrotask", which is not in `cspell.json`'s `words` list. The word arrived with 445114c (#256) and the check covers `**/*.{md,ts,marko}`, so every branch cut from main inherits a red lint until the word is added. Re-verify: `npx cspell "**/*.{md,ts,marko}" --no-progress` on main reports one issue. + +## A `params` validator on a route with two `$` segments loses its typing + +`packages/run/src/runtime/types.ts` › `RouteForFileDef` | 2026-08-17 | impact:low | effort:med + +In an app with `api/workspaces/$id/+middleware.ts` (a `Run.ALL` middleware calling `next({ workspace })`) and a leaf `api/workspaces/$id/review/$commentId/+handler.ts` exporting `Run.DELETE({ params({ id, commentId }) { … } }, (ctx) => …)`, `tsc` reports TS7031 (`id` / `commentId` implicitly any) on the validator's destructured argument and TS2554 ("Expected 1 arguments, but got 2") on the `Run.DELETE` call, so the two-argument form is unusable there. The identical shape on a one-segment route with no parent middleware (`api/procs/$pid/+handler.ts`, `Run.DELETE({ params({ pid }) … }, handler)`) type-checks. Something in the merge of the parent middleware's options with the leaf's `params` (`MergedRouteOptionsForFile` → `RouteForFileDef.params`) collapses when the path has two params, since the workaround is simply parsing `ctx.params.commentId` inside the handler. Repro: the two files above in a fixture under `packages/run/src/__tests__/fixtures/`, then `tsc` on it; a passing fixture with the validator form is the fix's test. diff --git a/packages/run/src/__tests__/typecheck.test.ts b/packages/run/src/__tests__/typecheck.test.ts index 8b455295..4dc20367 100644 --- a/packages/run/src/__tests__/typecheck.test.ts +++ b/packages/run/src/__tests__/typecheck.test.ts @@ -59,11 +59,13 @@ describe("test sources type-check", () => { }); describe("route type generation type-checks", () => { - // A parent-segment middleware that derives `next(data)` from its context - // plus a downstream handler with a `params` validator once made the two - // module types circular (TS7022); this fixture compiles both against the - // workspace source types. - it("should type-check the params-validator-types fixture", function () { + // Known limitation, reproduced by the params-validator-types fixture: a + // parent-segment middleware deriving `next(data)` from its context plus a + // downstream handler with a `params` validator makes the two module types + // mutually dependent, and TypeScript collapses both to `any` (TS7022). + // This pins the failure; when it starts failing, the cycle got fixed and + // the assertion should flip to expect no fixture-local errors. + it("still reproduces the middleware/validator type cycle", function () { this.timeout(120000); const result = spawnSync( process.execPath, @@ -85,12 +87,13 @@ describe("route type generation type-checks", () => { const localErrors = (result.stdout || "") .split("\n") // The compile pulls in the whole workspace graph, which carries known - // declaration-level noise; only errors in the fixture's sources or its - // generated route types indicate a regression here. - .filter((line) => - /params-validator-types[/\\](src|\.marko-run)[/\\]/.test(line), - ); + // declaration-level noise; only errors in the fixture's sources are + // the reproduction. + .filter((line) => /params-validator-types[/\\]src[/\\]/.test(line)); - assert.deepEqual(localErrors, []); + assert.ok( + localErrors.some((line) => line.includes("error TS7022")), + `expected the TS7022 cycle, got:\n${localErrors.join("\n") || "(no fixture-local errors — the cycle may be fixed!)"}`, + ); }); }); diff --git a/packages/run/src/runtime/types.ts b/packages/run/src/runtime/types.ts index 28e0a104..9a77f580 100644 --- a/packages/run/src/runtime/types.ts +++ b/packages/run/src/runtime/types.ts @@ -477,9 +477,18 @@ type DefineHandlerOptions = [Verb] extends [ ] ? HandlerOptionsWithoutBody : HandlerOptionsWithBody; -// Only files that run before the local one contribute options: a later -// file's type may itself depend on this file's (via upstream data), and -// including it makes the two inferences circular. +type TypesFromHandlerFilesWithLocal< + Files extends File[], + Verb extends HttpVerb, + Id extends ID, + Options, +> = { + [I in keyof Files]: Files[I] extends { + id: Id; + } + ? HandlerTypes + : TypesFromHandlerFile; +}; type RouteFileGroupOptionsWithLocal< Group extends RouteFileGroup, Verb extends HttpVerb, @@ -487,13 +496,12 @@ type RouteFileGroupOptionsWithLocal< Options, > = MergeHandlerOptionsTuple< MapTuple< - [ - ...TypesFromHandlerFiles< - TakeUntil<[...Group["middleware"], Group["handler"]], Id>, - Verb - >, - HandlerTypes, - ], + TypesFromHandlerFilesWithLocal< + [...Group["middleware"], Group["handler"]], + Verb, + Id, + Options + >, "options", Empty > From ee1df50d177486d588c05ccd2eda52696ceb9481 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Mon, 17 Aug 2026 16:55:42 -0700 Subject: [PATCH 03/10] fix: keep middleware and validator types across the route module cycle --- .changeset/middleware-validator-type-cycle.md | 5 + agent-feedback/dx.md | 6 - .../routes/api/workspaces/$id/+middleware.ts | 6 + .../$id/review/$commentId/+handler.ts | 7 + packages/run/src/__tests__/typecheck.test.ts | 19 +-- packages/run/src/runtime/types.ts | 155 +++++++++++++++++- 6 files changed, 180 insertions(+), 18 deletions(-) create mode 100644 .changeset/middleware-validator-type-cycle.md diff --git a/.changeset/middleware-validator-type-cycle.md b/.changeset/middleware-validator-type-cycle.md new file mode 100644 index 00000000..cd633673 --- /dev/null +++ b/.changeset/middleware-validator-type-cycle.md @@ -0,0 +1,5 @@ +--- +"@marko/run": patch +--- + +Fix route handler and middleware types collapsing to `any` when a middleware derives `next()` data from its context while a downstream handler declares validators. diff --git a/agent-feedback/dx.md b/agent-feedback/dx.md index 5b7304f2..852912d4 100644 --- a/agent-feedback/dx.md +++ b/agent-feedback/dx.md @@ -209,9 +209,3 @@ CodeRabbit's pre-merge checks (configured in the organization UI; the repo has n `cspell.json` › `words` | 2026-08-11 | impact:med | effort:low `pnpm run lint` fails on a clean `main` with a single cspell error: `agent-feedback/cleanup.md` uses "macrotask", which is not in `cspell.json`'s `words` list. The word arrived with 445114c (#256) and the check covers `**/*.{md,ts,marko}`, so every branch cut from main inherits a red lint until the word is added. Re-verify: `npx cspell "**/*.{md,ts,marko}" --no-progress` on main reports one issue. - -## A `params` validator on a route with two `$` segments loses its typing - -`packages/run/src/runtime/types.ts` › `RouteForFileDef` | 2026-08-17 | impact:low | effort:med - -In an app with `api/workspaces/$id/+middleware.ts` (a `Run.ALL` middleware calling `next({ workspace })`) and a leaf `api/workspaces/$id/review/$commentId/+handler.ts` exporting `Run.DELETE({ params({ id, commentId }) { … } }, (ctx) => …)`, `tsc` reports TS7031 (`id` / `commentId` implicitly any) on the validator's destructured argument and TS2554 ("Expected 1 arguments, but got 2") on the `Run.DELETE` call, so the two-argument form is unusable there. The identical shape on a one-segment route with no parent middleware (`api/procs/$pid/+handler.ts`, `Run.DELETE({ params({ pid }) … }, handler)`) type-checks. Something in the merge of the parent middleware's options with the leaf's `params` (`MergedRouteOptionsForFile` → `RouteForFileDef.params`) collapses when the path has two params, since the workaround is simply parsing `ctx.params.commentId` inside the handler. Repro: the two files above in a fixture under `packages/run/src/__tests__/fixtures/`, then `tsc` on it; a passing fixture with the validator form is the fix's test. diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts index ddf06e38..075ddb54 100644 --- a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts @@ -1,3 +1,9 @@ export default Run.ALL((ctx, next) => { + if ("commentId" in ctx.params) { + const merged: number = ctx.params.commentId; + // @ts-expect-error the DELETE validator types commentId as number + const bad: boolean = ctx.params.commentId; + void merged, void bad; + } return next({ workspace: `ws-${ctx.params.id}` }); }); diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts index 432a0094..2274dbee 100644 --- a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts @@ -5,6 +5,13 @@ export const DELETE = Run.DELETE( }, }, (ctx) => { + const validated: number = ctx.params.commentId; + const upstream: string = ctx.data.workspace; + // @ts-expect-error the validator types commentId as number + const badParam: boolean = ctx.params.commentId; + // @ts-expect-error the middleware data types workspace as string + const badData: boolean = ctx.data.workspace; + void validated, void upstream, void badParam, void badData; return Response.json({ id: ctx.params.id, commentId: ctx.params.commentId, diff --git a/packages/run/src/__tests__/typecheck.test.ts b/packages/run/src/__tests__/typecheck.test.ts index 4dc20367..62cfc626 100644 --- a/packages/run/src/__tests__/typecheck.test.ts +++ b/packages/run/src/__tests__/typecheck.test.ts @@ -59,13 +59,13 @@ describe("test sources type-check", () => { }); describe("route type generation type-checks", () => { - // Known limitation, reproduced by the params-validator-types fixture: a - // parent-segment middleware deriving `next(data)` from its context plus a - // downstream handler with a `params` validator makes the two module types - // mutually dependent, and TypeScript collapses both to `any` (TS7022). - // This pins the failure; when it starts failing, the cycle got fixed and - // the assertion should flip to expect no fixture-local errors. - it("still reproduces the middleware/validator type cycle", function () { + // The params-validator-types fixture pairs a parent-segment middleware + // deriving `next(data)` from its context with a downstream handler whose + // `params` validator the middleware's context must reflect — the shape + // that used to collapse both modules to `any` (TS7022). Its sources carry + // `@ts-expect-error` probes, so this compile fails on a missing error too + // (types silently widening back to `any` would surface as TS2578). + it("keeps middleware and validator types across the module cycle", function () { this.timeout(120000); const result = spawnSync( process.execPath, @@ -91,9 +91,6 @@ describe("route type generation type-checks", () => { // the reproduction. .filter((line) => /params-validator-types[/\\]src[/\\]/.test(line)); - assert.ok( - localErrors.some((line) => line.includes("error TS7022")), - `expected the TS7022 cycle, got:\n${localErrors.join("\n") || "(no fixture-local errors — the cycle may be fixed!)"}`, - ); + assert.deepEqual(localErrors, []); }); }); diff --git a/packages/run/src/runtime/types.ts b/packages/run/src/runtime/types.ts index 9a77f580..c4623c9a 100644 --- a/packages/run/src/runtime/types.ts +++ b/packages/run/src/runtime/types.ts @@ -661,6 +661,157 @@ export type DefineHandler = { >; }; +// The file's own options are the only contributor consulted, so resolving +// this context never forces another file's inferred export type. A standalone +// interface (not Context>) keeps every member lazy: a constrained +// type argument would be structurally compared against Route, forcing `data` +// (and with it the upstream middleware's inferred export) while that export +// is still resolving. +interface LeafContext< + F extends File, + Path extends string, + Verb extends HttpVerb, + Options, +> { + readonly route: Path; + readonly method: Verb; + readonly meta: Path extends keyof AppPaths + ? Verb extends keyof AppPaths[Path]["verbs"] + ? AppPaths[Path]["verbs"][Verb]["def"]["meta"] + : Empty + : Empty; + readonly params: Validation extends { + params: infer T; + } + ? T + : PathParams; + readonly search: Validation extends { + search: infer T; + } + ? T + : undefined; + readonly body: Verb extends HttpVerbWithBody + ? Validation extends { + json: infer T; + } + ? Promise + : Validation extends { + form: infer T; + } + ? Promise + : undefined + : undefined; + readonly data: GetUpstreamData, Verb> extends [ + infer T extends Record, + ] + ? T + : Record; + readonly url: URL; + readonly request: Request; + readonly platform: Platform; + readonly parent: Context | undefined; + serializedGlobals: Record; + fetch( + resource: string | URL | Request, + init?: RequestInit, + ): Promise; + render( + template: Marko.Template, + input: T, + init?: ResponseInit, + ): Response; + redirect(to: string | URL, status?: number): Response; + back(fallback?: string | URL, status?: number): Response; +} +type LeafContextForFile< + F extends File, + Verb extends HttpVerbOrAll, + Options, +> = Union<{ + [Path in PathsForFile]: Union<{ + [V in VerbsForPath]: V extends HttpVerb + ? LeafContext + : never; + }>; +}>; +// Only a template consumes a handler's inferred data, so template-less routes +// can use the leaf form; routes with a template keep full body inference. +type FileHasTemplate = + true extends Union<{ + [P in PathsForFile]: AppPaths[P]["files"]["template"] extends [] + ? false + : true; + }> + ? true + : false; +// The `=> any` return is what lets an upstream middleware resolve this +// export without typing the handler body (TypeScript skips computing a +// source function's return type against an `any`-returning target); a +// checked return type here would re-create the cycle. +type LeafHandlerFunction = (ctx: Ctx, next: NextFunction) => any; +type LeafHandlerArray = readonly ( + | { + [__run__.TYPES]: { + options: any; + data: any; + }; + } + | LeafHandlerFunction +)[]; +export type DefineLeafHandler = { + ( + handlers: LeafHandlerArray< + LeafContextForFile> + > & + Handlers, + ): Typed< + NormalizedHandlerFunction>, + HandlerTypes< + LeafContextForFile>, + Verb, + ComposedHandlerOptions, + Empty + > + >; + < + const Options extends DefineHandlerOptions, + const Handlers extends readonly unknown[], + >( + options: Exact>, + handlers: LeafHandlerArray> & Handlers, + ): Typed< + NormalizedHandlerFunction< + Verb, + MergeHandlerOptions, Options> + >, + HandlerTypes< + LeafContextForFile, + Verb, + MergeHandlerOptions, Options>, + Empty + > + >; + ( + handler: LeafHandlerFunction>, + ): Typed< + NormalizedHandlerFunction, + HandlerTypes, Verb, {}, Empty> + >; + >( + options: Exact>, + ): Typed< + NormalizedHandlerFunction, + HandlerTypes, Verb, Options, Empty> + >; + >( + options: Exact>, + handler: LeafHandlerFunction>>, + ): Typed< + NormalizedHandlerFunction, + HandlerTypes, Verb, Options, Empty> + >; +}; + export type GlobalDefineHandler = { ( handlers: HandlerArray & Handlers, @@ -943,7 +1094,9 @@ export type Namespace = Typed< } : F["type"] extends "handler" ? { - [Verb in HttpVerb]: DefineHandler; + [Verb in HttpVerb]: FileHasTemplate extends true + ? DefineHandler + : DefineLeafHandler; } : Empty) & NamespaceVerb, From a6457777068a5ea513b70241481e3536f10b0ec9 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Mon, 17 Aug 2026 18:08:07 -0700 Subject: [PATCH 04/10] feat: file-wide options export merged route-wide without the module type cycle --- .changeset/middleware-validator-type-cycle.md | 4 +- .../routes/api/workspaces/$id/+middleware.ts | 6 + .../$id/review/$commentId/+handler.ts | 41 +-- packages/run/src/runtime/internal.ts | 13 +- packages/run/src/runtime/types.ts | 242 ++++-------------- .../build-routes.expected.routes.md | 70 ++--- .../flat-routes.expected.routes.md | 38 +-- .../__snapshots__/get-post.expected.routes.md | 12 +- .../meta-files-with-verbs.expected.routes.md | 14 +- .../optional-types.expected.routes.md | 31 ++- .../query-verb.expected.routes.md | 5 +- packages/run/src/vite/codegen/index.ts | 13 +- 12 files changed, 201 insertions(+), 288 deletions(-) diff --git a/.changeset/middleware-validator-type-cycle.md b/.changeset/middleware-validator-type-cycle.md index cd633673..48f85281 100644 --- a/.changeset/middleware-validator-type-cycle.md +++ b/.changeset/middleware-validator-type-cycle.md @@ -1,5 +1,5 @@ --- -"@marko/run": patch +"@marko/run": minor --- -Fix route handler and middleware types collapsing to `any` when a middleware derives `next()` data from its context while a downstream handler declares validators. +Add a file-wide `export const options = Run.options({...})` for route validators, merged across the whole route and visible in every file's context — including through the middleware/handler shapes whose inline options previously collapsed the route's types to `any`. diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts index 075ddb54..6fc94fa1 100644 --- a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts @@ -1,3 +1,9 @@ +export const options = Run.options({ + search({ limit }) { + return { limit: limit ? Number(limit) : 0 }; + }, +}); + export default Run.ALL((ctx, next) => { if ("commentId" in ctx.params) { const merged: number = ctx.params.commentId; diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts index 2274dbee..ef3ba355 100644 --- a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts @@ -1,20 +1,23 @@ -export const DELETE = Run.DELETE( - { - params({ id, commentId }) { - return { id, commentId: Number(commentId) }; - }, +export const options = Run.options({ + params({ id, commentId }) { + return { id, commentId: Number(commentId) }; }, - (ctx) => { - const validated: number = ctx.params.commentId; - const upstream: string = ctx.data.workspace; - // @ts-expect-error the validator types commentId as number - const badParam: boolean = ctx.params.commentId; - // @ts-expect-error the middleware data types workspace as string - const badData: boolean = ctx.data.workspace; - void validated, void upstream, void badParam, void badData; - return Response.json({ - id: ctx.params.id, - commentId: ctx.params.commentId, - }); - }, -); +}); + +export const DELETE = Run.DELETE((ctx) => { + const validated: number = ctx.params.commentId; + const upstream: string = ctx.data.workspace; + const fromMiddleware: number = ctx.search.limit; + // @ts-expect-error the validator types commentId as number + const badParam: boolean = ctx.params.commentId; + // @ts-expect-error the middleware data types workspace as string + const badData: boolean = ctx.data.workspace; + // @ts-expect-error the middleware search validator types limit as number + const badSearch: boolean = ctx.search.limit; + void validated, void upstream, void fromMiddleware, void badParam; + void badData, void badSearch; + return Response.json({ + id: ctx.params.id, + commentId: ctx.params.commentId, + }); +}); diff --git a/packages/run/src/runtime/internal.ts b/packages/run/src/runtime/internal.ts index 090d1441..4972d002 100644 --- a/packages/run/src/runtime/internal.ts +++ b/packages/run/src/runtime/internal.ts @@ -59,6 +59,7 @@ globalThis.MarkoRun ??= { globalThis.Run ??= { href, + options: (options: HandlerOptions) => options, ALL: createDefineHandler("ALL"), ...Object.fromEntries( httpVerbs.map((v) => { @@ -462,11 +463,17 @@ type MergeOptionsInput = // first source is often a `+middleware` options object shared across routes. // With a `verb`, inputs stamped with a verb that would not run for it are // skipped; GET-stamped inputs still apply to HEAD, mirroring `call()`. -export function mergeOptions(items: MergeOptionsInput[], verb?: HttpVerb) { +export function mergeOptions( + items: (MergeOptionsInput | undefined)[], + verb?: HttpVerb, +) { const merged: HandlerOptions = {}; for (const item of items) { let options: HandlerOptions; - if (typeof item === "object") { + // A missing `options` export reaches the per-route merge as undefined. + if (!item) { + continue; + } else if (typeof item === "object") { options = item; } else if ("options" in item) { if (verb && item.verb) { @@ -508,7 +515,7 @@ export function mergeOptions(items: MergeOptionsInput[], verb?: HttpVerb) { export function normalizeOptions( verb: HttpVerb, - ...items: MergeOptionsInput[] + ...items: (MergeOptionsInput | undefined)[] ) { const merged = mergeOptions(items, verb); diff --git a/packages/run/src/runtime/types.ts b/packages/run/src/runtime/types.ts index c4623c9a..300b6b5b 100644 --- a/packages/run/src/runtime/types.ts +++ b/packages/run/src/runtime/types.ts @@ -303,16 +303,26 @@ type RouteFileGroupMeta< Group extends RouteFileGroup, Verb extends HttpVerb, > = Fallback>, Empty>; +// A file's `options` export is read off the module directly, so consuming it +// never resolves the file's inferred handler consts — the property that lets +// every other file's context include it without a type cycle. +type FileExportOptions = F["module"] extends { + options: infer O; +} + ? O + : never; type RouteFileGroupOptions< Group extends RouteFileGroup, Verb extends HttpVerb, > = MergeHandlerOptionsTuple< - MapTuple< - TypesFromHandlerFiles<[...Group["middleware"], Group["handler"]], Verb>, - "options", - Empty - > + FileOptionsTuple<[...Group["middleware"], Group["handler"]], Verb> >; +type FileOptionsTuple = { + [I in keyof Files]: MergeHandlerOptions< + FileExportOptions, + Fallback["options"], never> + >; +}; type RouteFileGroupData< Group extends RouteFileGroup, Verb extends HttpVerb, @@ -477,43 +487,37 @@ type DefineHandlerOptions = [Verb] extends [ ] ? HandlerOptionsWithoutBody : HandlerOptionsWithBody; -type TypesFromHandlerFilesWithLocal< - Files extends File[], - Verb extends HttpVerb, - Id extends ID, - Options, -> = { - [I in keyof Files]: Files[I] extends { - id: Id; - } - ? HandlerTypes - : TypesFromHandlerFile; -}; +// Merged options as seen from one file's own context. Other files contribute +// only their `options` export: reaching into their inline options would +// resolve their inferred handler consts, whose bodies may in turn read this +// file's context — the mutual inference TypeScript collapses to `any` +// (TS7022). The local file's inline options are already in hand as `Options`, +// so they merge without touching any export. type RouteFileGroupOptionsWithLocal< Group extends RouteFileGroup, - Verb extends HttpVerb, Id extends ID, Options, > = MergeHandlerOptionsTuple< - MapTuple< - TypesFromHandlerFilesWithLocal< - [...Group["middleware"], Group["handler"]], - Verb, - Id, - Options - >, - "options", - Empty + FileOptionsTupleWithLocal< + [...Group["middleware"], Group["handler"]], + Id, + Options > >; +type FileOptionsTupleWithLocal = { + [I in keyof Files]: Files[I] extends { + id: Id; + } + ? MergeHandlerOptions, Options> + : FileExportOptions; +}; type MergedRouteOptionsForFile< Path extends keyof AppPaths, - Verb extends HttpVerb, Id extends ID, Options, > = Fallback< Validation< - RouteFileGroupOptionsWithLocal + RouteFileGroupOptionsWithLocal >, Empty >; @@ -528,22 +532,22 @@ export interface RouteForFileDef< meta: Verb extends keyof AppPaths[Path]["verbs"] ? AppPaths[Path]["verbs"][Verb]["def"]["meta"] : Empty; - params: MergedRouteOptionsForFile extends { + params: MergedRouteOptionsForFile extends { params: infer T; } ? T : PathParams; - search: MergedRouteOptionsForFile extends { + search: MergedRouteOptionsForFile extends { search: infer T; } ? T : undefined; body: Verb extends HttpVerbWithBody - ? MergedRouteOptionsForFile extends { + ? MergedRouteOptionsForFile extends { json: infer T; } ? Promise - : MergedRouteOptionsForFile extends { + : MergedRouteOptionsForFile extends { form: infer T; } ? Promise @@ -661,157 +665,6 @@ export type DefineHandler = { >; }; -// The file's own options are the only contributor consulted, so resolving -// this context never forces another file's inferred export type. A standalone -// interface (not Context>) keeps every member lazy: a constrained -// type argument would be structurally compared against Route, forcing `data` -// (and with it the upstream middleware's inferred export) while that export -// is still resolving. -interface LeafContext< - F extends File, - Path extends string, - Verb extends HttpVerb, - Options, -> { - readonly route: Path; - readonly method: Verb; - readonly meta: Path extends keyof AppPaths - ? Verb extends keyof AppPaths[Path]["verbs"] - ? AppPaths[Path]["verbs"][Verb]["def"]["meta"] - : Empty - : Empty; - readonly params: Validation extends { - params: infer T; - } - ? T - : PathParams; - readonly search: Validation extends { - search: infer T; - } - ? T - : undefined; - readonly body: Verb extends HttpVerbWithBody - ? Validation extends { - json: infer T; - } - ? Promise - : Validation extends { - form: infer T; - } - ? Promise - : undefined - : undefined; - readonly data: GetUpstreamData, Verb> extends [ - infer T extends Record, - ] - ? T - : Record; - readonly url: URL; - readonly request: Request; - readonly platform: Platform; - readonly parent: Context | undefined; - serializedGlobals: Record; - fetch( - resource: string | URL | Request, - init?: RequestInit, - ): Promise; - render( - template: Marko.Template, - input: T, - init?: ResponseInit, - ): Response; - redirect(to: string | URL, status?: number): Response; - back(fallback?: string | URL, status?: number): Response; -} -type LeafContextForFile< - F extends File, - Verb extends HttpVerbOrAll, - Options, -> = Union<{ - [Path in PathsForFile]: Union<{ - [V in VerbsForPath]: V extends HttpVerb - ? LeafContext - : never; - }>; -}>; -// Only a template consumes a handler's inferred data, so template-less routes -// can use the leaf form; routes with a template keep full body inference. -type FileHasTemplate = - true extends Union<{ - [P in PathsForFile]: AppPaths[P]["files"]["template"] extends [] - ? false - : true; - }> - ? true - : false; -// The `=> any` return is what lets an upstream middleware resolve this -// export without typing the handler body (TypeScript skips computing a -// source function's return type against an `any`-returning target); a -// checked return type here would re-create the cycle. -type LeafHandlerFunction = (ctx: Ctx, next: NextFunction) => any; -type LeafHandlerArray = readonly ( - | { - [__run__.TYPES]: { - options: any; - data: any; - }; - } - | LeafHandlerFunction -)[]; -export type DefineLeafHandler = { - ( - handlers: LeafHandlerArray< - LeafContextForFile> - > & - Handlers, - ): Typed< - NormalizedHandlerFunction>, - HandlerTypes< - LeafContextForFile>, - Verb, - ComposedHandlerOptions, - Empty - > - >; - < - const Options extends DefineHandlerOptions, - const Handlers extends readonly unknown[], - >( - options: Exact>, - handlers: LeafHandlerArray> & Handlers, - ): Typed< - NormalizedHandlerFunction< - Verb, - MergeHandlerOptions, Options> - >, - HandlerTypes< - LeafContextForFile, - Verb, - MergeHandlerOptions, Options>, - Empty - > - >; - ( - handler: LeafHandlerFunction>, - ): Typed< - NormalizedHandlerFunction, - HandlerTypes, Verb, {}, Empty> - >; - >( - options: Exact>, - ): Typed< - NormalizedHandlerFunction, - HandlerTypes, Verb, Options, Empty> - >; - >( - options: Exact>, - handler: LeafHandlerFunction>>, - ): Typed< - NormalizedHandlerFunction, - HandlerTypes, Verb, Options, Empty> - >; -}; - export type GlobalDefineHandler = { ( handlers: HandlerArray & Handlers, @@ -1086,17 +939,30 @@ export type NamespaceVerb = { }; export type GlobalNamespace = { [Verb in HttpVerbOrAll]: GlobalDefineHandler; -} & NamespaceVerb; +} & NamespaceVerb & { + options: DefineFileOptions; + }; +// File-wide options declared as `export const options = Run.options({...})`. +// The context-free constraint keeps the export's type independent of every +// route context, which is what lets other files' contexts consume it while +// their own types are still resolving. +export type DefineFileOptions = < + const O extends HandlerOptionsWithBody, +>( + options: Exact>, +) => O; export type Namespace = Typed< (F["type"] extends "middleware" ? { [Verb in HttpVerbOrAll]: DefineHandler; + } & { + options: DefineFileOptions; } : F["type"] extends "handler" ? { - [Verb in HttpVerb]: FileHasTemplate extends true - ? DefineHandler - : DefineLeafHandler; + [Verb in HttpVerb]: DefineHandler; + } & { + options: DefineFileOptions; } : Empty) & NamespaceVerb, diff --git a/packages/run/src/vite/__tests__/fixtures/build-routes/__snapshots__/build-routes.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/build-routes/__snapshots__/build-routes.expected.routes.md index 055ac7e2..07a74505 100644 --- a/packages/run/src/vite/__tests__/fixtures/build-routes/__snapshots__/build-routes.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/build-routes/__snapshots__/build-routes.expected.routes.md @@ -3,15 +3,19 @@ ## Middleware ```js import { normalizeHandler } from "virtual:marko-run/runtime/internal"; -import middleware4 from "./src/routes/+middleware.ts"; -import middleware5 from "./src/routes/_protected/+middleware.ts"; -import middleware7 from "./src/routes/_protected/_home/+middleware.ts"; -import middleware13 from "./src/routes/_protected/_home/notes/$id/+middleware.ts"; +import middleware4, * as middlewareModule4 from "./src/routes/+middleware.ts"; +import middleware5, * as middlewareModule5 from "./src/routes/_protected/+middleware.ts"; +import middleware7, * as middlewareModule7 from "./src/routes/_protected/_home/+middleware.ts"; +import middleware13, * as middlewareModule13 from "./src/routes/_protected/_home/notes/$id/+middleware.ts"; export const mware4 = normalizeHandler(middleware4); +export const mwareOptions4 = middlewareModule4.options; export const mware5 = normalizeHandler(middleware5); +export const mwareOptions5 = middlewareModule5.options; export const mware7 = normalizeHandler(middleware7); +export const mwareOptions7 = middlewareModule7.options; export const mware13 = normalizeHandler(middleware13); +export const mwareOptions13 = middlewareModule13.options; ``` --- @@ -32,11 +36,11 @@ import Page from "../../src/routes/_protected/_home/+page.marko"; ### Handler ```js import { call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4, mware5, mware7 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mwareOptions4, mware5, mwareOptions5, mware7, mwareOptions7 } from "virtual:marko-run/__marko-run__middleware.js"; import page from "./dist/.marko-run/index.marko"; -export const get3_options = normalizeOptions('GET', mware4, mware5, mware7); -export const head3_options = normalizeOptions('HEAD', mware4, mware5, mware7); +export const get3_options = normalizeOptions('GET', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7); +export const head3_options = normalizeOptions('HEAD', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7); export function get3(context) { const __page = (data) => render(context, page, {}, data); @@ -67,8 +71,9 @@ import Page from "../../src/routes/_protected/_home/new/+page.marko"; ### Handler ```js import { normalizeHandler, normalizeMeta, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4, mware5, mware7 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mwareOptions4, mware5, mwareOptions5, mware7, mwareOptions7 } from "virtual:marko-run/__marko-run__middleware.js"; import { POST } from "./src/routes/_protected/_home/new/+handler.ts"; +import * as handlerModule from "./src/routes/_protected/_home/new/+handler.ts"; import page from "./dist/.marko-run/new.marko"; import meta4 from "./src/routes/_protected/_home/new/+meta.json"; @@ -76,9 +81,9 @@ const postHandler = normalizeHandler(POST, 'POST'); export const { GET: get4_meta, GET: head4_meta, POST: post4_meta } = normalizeMeta(meta4); -export const get4_options = normalizeOptions('GET', mware4, mware5, mware7); -export const head4_options = normalizeOptions('HEAD', mware4, mware5, mware7); -export const post4_options = normalizeOptions('POST', mware4, mware5, mware7, postHandler); +export const get4_options = normalizeOptions('GET', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7); +export const head4_options = normalizeOptions('HEAD', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7); +export const post4_options = normalizeOptions('POST', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, handlerModule.options, postHandler); export function get4(context) { const __page = (data) => render(context, page, {}, data); @@ -117,19 +122,20 @@ import Page from "../../src/routes/_protected/_home/notes/$id/+page.marko"; ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4, mware5, mware7, mware13 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mwareOptions4, mware5, mwareOptions5, mware7, mwareOptions7, mware13, mwareOptions13 } from "virtual:marko-run/__marko-run__middleware.js"; import { PUT, POST, DELETE } from "./src/routes/_protected/_home/notes/$id/+handler.ts"; +import * as handlerModule from "./src/routes/_protected/_home/notes/$id/+handler.ts"; import page from "./dist/.marko-run/notes.$.marko"; const putHandler = normalizeHandler(PUT, 'PUT'); const postHandler = normalizeHandler(POST, 'POST'); const deleteHandler = normalizeHandler(DELETE, 'DELETE'); -export const get5_options = normalizeOptions('GET', mware4, mware5, mware7, mware13); -export const head5_options = normalizeOptions('HEAD', mware4, mware5, mware7, mware13); -export const post5_options = normalizeOptions('POST', mware4, mware5, mware7, mware13, postHandler); -export const put5_options = normalizeOptions('PUT', mware4, mware5, mware7, mware13, putHandler); -export const delete5_options = normalizeOptions('DELETE', mware4, mware5, mware7, mware13, deleteHandler); +export const get5_options = normalizeOptions('GET', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13); +export const head5_options = normalizeOptions('HEAD', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13); +export const post5_options = normalizeOptions('POST', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, postHandler); +export const put5_options = normalizeOptions('PUT', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, putHandler); +export const delete5_options = normalizeOptions('DELETE', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, deleteHandler); export function get5(context) { const __page = (data) => render(context, page, {}, data); @@ -174,8 +180,9 @@ export function delete5(context) { ### Handler ```js import { normalizeHandler, normalizeMeta, call, normalizeOptions, noContent } from "virtual:marko-run/runtime/internal"; -import { mware4, mware5, mware7, mware13 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mwareOptions4, mware5, mwareOptions5, mware7, mwareOptions7, mware13, mwareOptions13 } from "virtual:marko-run/__marko-run__middleware.js"; import { PUT, POST, DELETE } from "./src/routes/_protected/_home/notes/$id/comments/+handler.ts"; +import * as handlerModule from "./src/routes/_protected/_home/notes/$id/comments/+handler.ts"; import meta6 from "./src/routes/_protected/_home/notes/$id/comments/+meta.ts"; const putHandler = normalizeHandler(PUT, 'PUT'); @@ -184,9 +191,9 @@ const deleteHandler = normalizeHandler(DELETE, 'DELETE'); export const { POST: post6_meta, PUT: put6_meta, DELETE: delete6_meta } = normalizeMeta(meta6); -export const post6_options = normalizeOptions('POST', mware4, mware5, mware7, mware13, postHandler); -export const put6_options = normalizeOptions('PUT', mware4, mware5, mware7, mware13, putHandler); -export const delete6_options = normalizeOptions('DELETE', mware4, mware5, mware7, mware13, deleteHandler); +export const post6_options = normalizeOptions('POST', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, postHandler); +export const put6_options = normalizeOptions('PUT', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, putHandler); +export const delete6_options = normalizeOptions('DELETE', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, deleteHandler); export function post6(context) { const __postHandler = (data) => call(postHandler, noContent, context, data); @@ -218,13 +225,14 @@ export function delete6(context) { ### Handler ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mwareOptions4 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/callback/oauth2/+handler.ts"; +import * as handlerModule from "./src/routes/callback/oauth2/+handler.ts"; const getHandler = normalizeHandler(GET, 'GET'); -export const get7_options = normalizeOptions('GET', mware4, getHandler); -export const head7_options = normalizeOptions('HEAD', mware4); +export const get7_options = normalizeOptions('GET', mwareOptions4, mware4, handlerModule.options, getHandler); +export const head7_options = normalizeOptions('HEAD', mwareOptions4, mware4); export function get7(context) { const __getHandler = (data) => call(getHandler, noContent, context, data); @@ -250,15 +258,16 @@ import Page from "../../src/routes/my/+page.marko"; ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mwareOptions4 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET, HEAD } from "./src/routes/my/+handler.ts"; +import * as handlerModule from "./src/routes/my/+handler.ts"; import page from "./dist/.marko-run/my.marko"; const getHandler = normalizeHandler(GET, 'GET'); const headHandler = normalizeHandler(HEAD, 'HEAD'); -export const get8_options = normalizeOptions('GET', mware4, getHandler); -export const head8_options = normalizeOptions('HEAD', mware4, headHandler); +export const get8_options = normalizeOptions('GET', mwareOptions4, mware4, handlerModule.options, getHandler); +export const head8_options = normalizeOptions('HEAD', mwareOptions4, mware4, handlerModule.options, headHandler); export function get8(context) { const __page = (data) => render(context, page, {}, data); @@ -278,13 +287,14 @@ export function head8(context) { ### Handler ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mwareOptions4 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/$$match/+handler.ts"; +import * as handlerModule from "./src/routes/$$match/+handler.ts"; const getHandler = normalizeHandler(GET, 'GET'); -export const get9_options = normalizeOptions('GET', mware4, getHandler); -export const head9_options = normalizeOptions('HEAD', mware4); +export const get9_options = normalizeOptions('GET', mwareOptions4, mware4, handlerModule.options, getHandler); +export const head9_options = normalizeOptions('HEAD', mwareOptions4, mware4); export function get9(context) { const __getHandler = (data) => call(getHandler, noContent, context, data); diff --git a/packages/run/src/vite/__tests__/fixtures/flat-routes/__snapshots__/flat-routes.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/flat-routes/__snapshots__/flat-routes.expected.routes.md index fce44cf5..bcab63a8 100644 --- a/packages/run/src/vite/__tests__/fixtures/flat-routes/__snapshots__/flat-routes.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/flat-routes/__snapshots__/flat-routes.expected.routes.md @@ -3,9 +3,10 @@ ## Middleware ```js import { normalizeHandler } from "virtual:marko-run/runtime/internal"; -import middleware3 from "./src/routes/$id,a.d+middleware.marko"; +import middleware3, * as middlewareModule3 from "./src/routes/$id,a.d+middleware.marko"; export const mware3 = normalizeHandler(middleware3); +export const mwareOptions3 = middlewareModule3.options; ``` --- @@ -46,14 +47,15 @@ import Page from "../../src/routes/foo,$id,$$rest,+page.marko"; ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; +import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; import page from "./dist/.marko-run/foo.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get2_options = normalizeOptions('GET', getHandler); +export const get2_options = normalizeOptions('GET', handlerModule.options, getHandler); export const head2_options = {}; -export const post2_options = normalizeOptions('POST', postHandler); +export const post2_options = normalizeOptions('POST', handlerModule.options, postHandler); export function get2(context) { const __page = (data) => render(context, page, {}, data); @@ -81,11 +83,11 @@ import Page from "../../src/routes/foo,$id,$$rest,+page.marko"; ### Handler ```js import { call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware3 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware3, mwareOptions3 } from "virtual:marko-run/__marko-run__middleware.js"; import page from "./dist/.marko-run/$.marko"; -export const get3_options = normalizeOptions('GET', mware3); -export const head3_options = normalizeOptions('HEAD', mware3); +export const get3_options = normalizeOptions('GET', mwareOptions3, mware3); +export const head3_options = normalizeOptions('HEAD', mwareOptions3, mware3); export function get3(context) { const __page = (data) => render(context, page, {}, data); @@ -128,13 +130,14 @@ export function head4(context) { ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; +import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get5_options = normalizeOptions('GET', getHandler); +export const get5_options = normalizeOptions('GET', handlerModule.options, getHandler); export const head5_options = {}; -export const post5_options = normalizeOptions('POST', postHandler); +export const post5_options = normalizeOptions('POST', handlerModule.options, postHandler); export function get5(context) { return call(getHandler, noContent, context); @@ -154,15 +157,16 @@ export function post5(context) { ### Handler ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware3 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware3, mwareOptions3 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; +import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get6_options = normalizeOptions('GET', mware3, getHandler); -export const head6_options = normalizeOptions('HEAD', mware3); -export const post6_options = normalizeOptions('POST', mware3, postHandler); +export const get6_options = normalizeOptions('GET', mwareOptions3, mware3, handlerModule.options, getHandler); +export const head6_options = normalizeOptions('HEAD', mwareOptions3, mware3); +export const post6_options = normalizeOptions('POST', mwareOptions3, mware3, handlerModule.options, postHandler); export function get6(context) { const __getHandler = (data) => call(getHandler, noContent, context, data); @@ -185,13 +189,14 @@ export function post6(context) { ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; +import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get7_options = normalizeOptions('GET', getHandler); +export const get7_options = normalizeOptions('GET', handlerModule.options, getHandler); export const head7_options = {}; -export const post7_options = normalizeOptions('POST', postHandler); +export const post7_options = normalizeOptions('POST', handlerModule.options, postHandler); export function get7(context) { return call(getHandler, noContent, context); @@ -212,13 +217,14 @@ export function post7(context) { ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; +import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get8_options = normalizeOptions('GET', getHandler); +export const get8_options = normalizeOptions('GET', handlerModule.options, getHandler); export const head8_options = {}; -export const post8_options = normalizeOptions('POST', postHandler); +export const post8_options = normalizeOptions('POST', handlerModule.options, postHandler); export function get8(context) { return call(getHandler, noContent, context); diff --git a/packages/run/src/vite/__tests__/fixtures/get-post/__snapshots__/get-post.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/get-post/__snapshots__/get-post.expected.routes.md index 63075b24..2a98cf57 100644 --- a/packages/run/src/vite/__tests__/fixtures/get-post/__snapshots__/get-post.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/get-post/__snapshots__/get-post.expected.routes.md @@ -3,9 +3,10 @@ ## Middleware ```js import { normalizeHandler } from "virtual:marko-run/runtime/internal"; -import middleware3 from "./src/routes/+middleware.ts"; +import middleware3, * as middlewareModule3 from "./src/routes/+middleware.ts"; export const mware3 = normalizeHandler(middleware3); +export const mwareOptions3 = middlewareModule3.options; ``` --- @@ -20,16 +21,17 @@ import Page from "../../src/routes/+page.marko"; ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware3 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware3, mwareOptions3 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET, POST } from "./src/routes/+handler.marko"; +import * as handlerModule from "./src/routes/+handler.marko"; import page from "./dist/.marko-run/index.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get1_options = normalizeOptions('GET', mware3, getHandler); -export const head1_options = normalizeOptions('HEAD', mware3); -export const post1_options = normalizeOptions('POST', mware3, postHandler); +export const get1_options = normalizeOptions('GET', mwareOptions3, mware3, handlerModule.options, getHandler); +export const head1_options = normalizeOptions('HEAD', mwareOptions3, mware3); +export const post1_options = normalizeOptions('POST', mwareOptions3, mware3, handlerModule.options, postHandler); export function get1(context) { const __page = (data) => render(context, page, {}, data); diff --git a/packages/run/src/vite/__tests__/fixtures/meta-files-with-verbs/__snapshots__/meta-files-with-verbs.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/meta-files-with-verbs/__snapshots__/meta-files-with-verbs.expected.routes.md index 4ec4a205..9f8b8c05 100644 --- a/packages/run/src/vite/__tests__/fixtures/meta-files-with-verbs/__snapshots__/meta-files-with-verbs.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/meta-files-with-verbs/__snapshots__/meta-files-with-verbs.expected.routes.md @@ -6,6 +6,7 @@ ```js import { normalizeHandler, normalizeMeta, call, normalizeOptions, noContent } from "virtual:marko-run/runtime/internal"; import { PUT, POST, DELETE } from "./src/routes/foo/bar/+handler.ts"; +import * as handlerModule from "./src/routes/foo/bar/+handler.ts"; import meta1 from "./src/routes/foo/bar/+meta.ts"; const putHandler = normalizeHandler(PUT, 'PUT'); @@ -14,9 +15,9 @@ const deleteHandler = normalizeHandler(DELETE, 'DELETE'); export const { POST: post1_meta, PUT: put1_meta, DELETE: delete1_meta } = normalizeMeta(meta1); -export const post1_options = normalizeOptions('POST', postHandler); -export const put1_options = normalizeOptions('PUT', putHandler); -export const delete1_options = normalizeOptions('DELETE', deleteHandler); +export const post1_options = normalizeOptions('POST', handlerModule.options, postHandler); +export const put1_options = normalizeOptions('PUT', handlerModule.options, putHandler); +export const delete1_options = normalizeOptions('DELETE', handlerModule.options, deleteHandler); export function post1(context) { return call(postHandler, noContent, context); @@ -43,6 +44,7 @@ import Page from "../../src/routes/foo/baz/+page.marko"; ```js import { normalizeHandler, normalizeMeta, call, normalizeOptions, render, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { PUT, POST, DELETE } from "./src/routes/foo/baz/+handler.ts"; +import * as handlerModule from "./src/routes/foo/baz/+handler.ts"; import page from "./dist/.marko-run/foo.baz.marko"; import meta2 from "./src/routes/foo/baz/+meta.json"; @@ -54,9 +56,9 @@ export const { GET: get2_meta, GET: head2_meta, POST: post2_meta, PUT: put2_meta export const get2_options = {}; export const head2_options = {}; -export const post2_options = normalizeOptions('POST', postHandler); -export const put2_options = normalizeOptions('PUT', putHandler); -export const delete2_options = normalizeOptions('DELETE', deleteHandler); +export const post2_options = normalizeOptions('POST', handlerModule.options, postHandler); +export const put2_options = normalizeOptions('PUT', handlerModule.options, putHandler); +export const delete2_options = normalizeOptions('DELETE', handlerModule.options, deleteHandler); export function get2(context) { return render(context, page, {}); diff --git a/packages/run/src/vite/__tests__/fixtures/optional-types/__snapshots__/optional-types.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/optional-types/__snapshots__/optional-types.expected.routes.md index 5f29a24c..05bceee5 100644 --- a/packages/run/src/vite/__tests__/fixtures/optional-types/__snapshots__/optional-types.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/optional-types/__snapshots__/optional-types.expected.routes.md @@ -3,9 +3,10 @@ ## Middleware ```js import { normalizeHandler } from "virtual:marko-run/runtime/internal"; -import middleware2 from "./src/routes/+middleware.ts"; +import middleware2, * as middlewareModule2 from "./src/routes/+middleware.ts"; export const mware2 = normalizeHandler(middleware2); +export const mwareOptions2 = middlewareModule2.options; ``` --- @@ -23,14 +24,15 @@ import Page from "../../src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+page.marko" ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware2 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware2, mwareOptions2 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; +import * as handlerModule from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; import page from "./dist/.marko-run/aaa.$.marko"; const getHandler = normalizeHandler(GET, 'GET'); -export const get1_options = normalizeOptions('GET', mware2, getHandler); -export const head1_options = normalizeOptions('HEAD', mware2); +export const get1_options = normalizeOptions('GET', mwareOptions2, mware2, handlerModule.options, getHandler); +export const head1_options = normalizeOptions('HEAD', mwareOptions2, mware2); export function get1(context) { const __page = (data) => render(context, page, {}, data); @@ -57,14 +59,15 @@ import Page from "../../src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+page.marko" ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware2 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware2, mwareOptions2 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; +import * as handlerModule from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; import page from "./dist/.marko-run/aaa.$.bbb.$.marko"; const getHandler = normalizeHandler(GET, 'GET'); -export const get2_options = normalizeOptions('GET', mware2, getHandler); -export const head2_options = normalizeOptions('HEAD', mware2); +export const get2_options = normalizeOptions('GET', mwareOptions2, mware2, handlerModule.options, getHandler); +export const head2_options = normalizeOptions('HEAD', mwareOptions2, mware2); export function get2(context) { const __page = (data) => render(context, page, {}, data); @@ -91,14 +94,15 @@ import Page from "../../src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+page.marko" ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware2 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware2, mwareOptions2 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; +import * as handlerModule from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; import page from "./dist/.marko-run/aaa.$.bbb.$.ccc.$.marko"; const getHandler = normalizeHandler(GET, 'GET'); -export const get3_options = normalizeOptions('GET', mware2, getHandler); -export const head3_options = normalizeOptions('HEAD', mware2); +export const get3_options = normalizeOptions('GET', mwareOptions2, mware2, handlerModule.options, getHandler); +export const head3_options = normalizeOptions('HEAD', mwareOptions2, mware2); export function get3(context) { const __page = (data) => render(context, page, {}, data); @@ -125,14 +129,15 @@ import Page from "../../src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+page.marko" ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware2 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware2, mwareOptions2 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; +import * as handlerModule from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; import page from "./dist/.marko-run/aaa.$.ccc.$.marko"; const getHandler = normalizeHandler(GET, 'GET'); -export const get4_options = normalizeOptions('GET', mware2, getHandler); -export const head4_options = normalizeOptions('HEAD', mware2); +export const get4_options = normalizeOptions('GET', mwareOptions2, mware2, handlerModule.options, getHandler); +export const head4_options = normalizeOptions('HEAD', mwareOptions2, mware2); export function get4(context) { const __page = (data) => render(context, page, {}, data); diff --git a/packages/run/src/vite/__tests__/fixtures/query-verb/__snapshots__/query-verb.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/query-verb/__snapshots__/query-verb.expected.routes.md index 11b27180..4442993e 100644 --- a/packages/run/src/vite/__tests__/fixtures/query-verb/__snapshots__/query-verb.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/query-verb/__snapshots__/query-verb.expected.routes.md @@ -12,6 +12,7 @@ import Page from "../../src/routes/+page.marko"; ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { QUERY, POST } from "./src/routes/+handler.ts"; +import * as handlerModule from "./src/routes/+handler.ts"; import page from "./dist/.marko-run/index.marko"; const queryHandler = normalizeHandler(QUERY, 'QUERY'); @@ -19,8 +20,8 @@ const postHandler = normalizeHandler(POST, 'POST'); export const get1_options = {}; export const head1_options = {}; -export const post1_options = normalizeOptions('POST', postHandler); -export const query1_options = normalizeOptions('QUERY', queryHandler); +export const post1_options = normalizeOptions('POST', handlerModule.options, postHandler); +export const query1_options = normalizeOptions('QUERY', handlerModule.options, queryHandler); export function get1(context) { return render(context, page, {}); diff --git a/packages/run/src/vite/codegen/index.ts b/packages/run/src/vite/codegen/index.ts index f622833a..379dc758 100644 --- a/packages/run/src/vite/codegen/index.ts +++ b/packages/run/src/vite/codegen/index.ts @@ -120,7 +120,10 @@ export function renderRouteEntry(route: Route, rootDir: string): string { } if (middleware.length) { - const names = middleware.map((m) => `mware${m.id}`); + const names = middleware.flatMap((m) => [ + `mware${m.id}`, + `mwareOptions${m.id}`, + ]); imports.writeLines( `import { ${names.join( ", ", @@ -141,6 +144,7 @@ export function renderRouteEntry(route: Route, rootDir: string): string { } imports.writeLines( `import { ${names.join(", ")} } from "${normalizedRelativePath(rootDir, handler.filePath)}";`, + `import * as handlerModule from "${normalizedRelativePath(rootDir, handler.filePath)}";`, ); } @@ -347,10 +351,11 @@ export function renderMiddleware( for (const { id, filePath } of middleware) { const importName = `middleware${id}`; imports.writeLines( - `import ${importName} from "${normalizedRelativePath(rootDir, filePath)}";`, + `import ${importName}, * as middlewareModule${id} from "${normalizedRelativePath(rootDir, filePath)}";`, ); writer.writeLines( `export const mware${id} = normalizeHandler(${importName});`, + `export const mwareOptions${id} = middlewareModule${id}.options;`, ); } @@ -635,10 +640,10 @@ function writeRouteOptions(writer: Writer, route: Route, verb: HttpVerb): void { writer.write(`normalizeOptions('${verb.toUpperCase()}'`); for (const { id } of route.middleware) { - writer.write(`, mware${id}`); + writer.write(`, mwareOptions${id}, mware${id}`); } if (hasHandler) { - writer.write(`, ${verb}Handler`); + writer.write(`, handlerModule.options, ${verb}Handler`); } writer.write(");"); } else { From c41d0cdbe8f5bc17b3c76d2fa5b355ff52c5d54d Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Mon, 17 Aug 2026 18:42:48 -0700 Subject: [PATCH 05/10] fix: infer route types across the middleware/validator module cycle --- .changeset/middleware-validator-type-cycle.md | 4 +- .../routes/api/workspaces/$id/+middleware.ts | 29 +- .../$id/review/$commentId/+handler.ts | 45 ++-- packages/run/src/__tests__/typecheck.test.ts | 12 +- packages/run/src/runtime/internal.ts | 13 +- packages/run/src/runtime/types.ts | 247 ++++++++++++------ .../build-routes.expected.routes.md | 70 +++-- .../flat-routes.expected.routes.md | 38 ++- .../__snapshots__/get-post.expected.routes.md | 12 +- .../meta-files-with-verbs.expected.routes.md | 14 +- .../optional-types.expected.routes.md | 31 +-- .../query-verb.expected.routes.md | 5 +- packages/run/src/vite/codegen/index.ts | 13 +- 13 files changed, 287 insertions(+), 246 deletions(-) diff --git a/.changeset/middleware-validator-type-cycle.md b/.changeset/middleware-validator-type-cycle.md index 48f85281..9218aadc 100644 --- a/.changeset/middleware-validator-type-cycle.md +++ b/.changeset/middleware-validator-type-cycle.md @@ -1,5 +1,5 @@ --- -"@marko/run": minor +"@marko/run": patch --- -Add a file-wide `export const options = Run.options({...})` for route validators, merged across the whole route and visible in every file's context — including through the middleware/handler shapes whose inline options previously collapsed the route's types to `any`. +Fix route middleware and handler types collapsing to `any` when a middleware derives `next()` data from its context while a downstream handler declares validators. diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts index 6fc94fa1..6dbe77bb 100644 --- a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/+middleware.ts @@ -1,15 +1,16 @@ -export const options = Run.options({ - search({ limit }) { - return { limit: limit ? Number(limit) : 0 }; +export default Run.ALL( + { + search({ limit }) { + return { limit: limit ? Number(limit) : 0 }; + }, }, -}); - -export default Run.ALL((ctx, next) => { - if ("commentId" in ctx.params) { - const merged: number = ctx.params.commentId; - // @ts-expect-error the DELETE validator types commentId as number - const bad: boolean = ctx.params.commentId; - void merged, void bad; - } - return next({ workspace: `ws-${ctx.params.id}` }); -}); + (ctx, next) => { + if ("commentId" in ctx.params) { + const merged: number = ctx.params.commentId; + // @ts-expect-error the DELETE validator types commentId as number + const bad: boolean = ctx.params.commentId; + void merged, void bad; + } + return next({ workspace: `ws-${ctx.params.id}` }); + }, +); diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts index ef3ba355..762eb1db 100644 --- a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts @@ -1,23 +1,24 @@ -export const options = Run.options({ - params({ id, commentId }) { - return { id, commentId: Number(commentId) }; +export const DELETE = Run.DELETE( + { + params({ id, commentId }) { + return { id, commentId: Number(commentId) }; + }, }, -}); - -export const DELETE = Run.DELETE((ctx) => { - const validated: number = ctx.params.commentId; - const upstream: string = ctx.data.workspace; - const fromMiddleware: number = ctx.search.limit; - // @ts-expect-error the validator types commentId as number - const badParam: boolean = ctx.params.commentId; - // @ts-expect-error the middleware data types workspace as string - const badData: boolean = ctx.data.workspace; - // @ts-expect-error the middleware search validator types limit as number - const badSearch: boolean = ctx.search.limit; - void validated, void upstream, void fromMiddleware, void badParam; - void badData, void badSearch; - return Response.json({ - id: ctx.params.id, - commentId: ctx.params.commentId, - }); -}); + (ctx) => { + const validated: number = ctx.params.commentId; + const upstream: string = ctx.data.workspace; + const fromMiddleware: number = ctx.search.limit; + // @ts-expect-error the validator types commentId as number + const badParam: boolean = ctx.params.commentId; + // @ts-expect-error the middleware data types workspace as string + const badData: boolean = ctx.data.workspace; + // @ts-expect-error the middleware search validator types limit as number + const badSearch: boolean = ctx.search.limit; + void validated, void upstream, void fromMiddleware, void badParam; + void badData, void badSearch; + return Response.json({ + id: ctx.params.id, + commentId: ctx.params.commentId, + }); + }, +); diff --git a/packages/run/src/__tests__/typecheck.test.ts b/packages/run/src/__tests__/typecheck.test.ts index 62cfc626..16ce9df1 100644 --- a/packages/run/src/__tests__/typecheck.test.ts +++ b/packages/run/src/__tests__/typecheck.test.ts @@ -59,12 +59,12 @@ describe("test sources type-check", () => { }); describe("route type generation type-checks", () => { - // The params-validator-types fixture pairs a parent-segment middleware - // deriving `next(data)` from its context with a downstream handler whose - // `params` validator the middleware's context must reflect — the shape - // that used to collapse both modules to `any` (TS7022). Its sources carry - // `@ts-expect-error` probes, so this compile fails on a missing error too - // (types silently widening back to `any` would surface as TS2578). + // The params-validator-types fixture pairs a parent-segment middleware — + // deriving `next(data)` from its context, with its own inline validator — + // with a downstream handler whose validator the middleware's context must + // reflect: the mutual inference that used to collapse both modules to + // `any` (TS7022). Its sources carry `@ts-expect-error` probes, so this + // compile also fails if the types silently widen back to `any` (TS2578). it("keeps middleware and validator types across the module cycle", function () { this.timeout(120000); const result = spawnSync( diff --git a/packages/run/src/runtime/internal.ts b/packages/run/src/runtime/internal.ts index 4972d002..090d1441 100644 --- a/packages/run/src/runtime/internal.ts +++ b/packages/run/src/runtime/internal.ts @@ -59,7 +59,6 @@ globalThis.MarkoRun ??= { globalThis.Run ??= { href, - options: (options: HandlerOptions) => options, ALL: createDefineHandler("ALL"), ...Object.fromEntries( httpVerbs.map((v) => { @@ -463,17 +462,11 @@ type MergeOptionsInput = // first source is often a `+middleware` options object shared across routes. // With a `verb`, inputs stamped with a verb that would not run for it are // skipped; GET-stamped inputs still apply to HEAD, mirroring `call()`. -export function mergeOptions( - items: (MergeOptionsInput | undefined)[], - verb?: HttpVerb, -) { +export function mergeOptions(items: MergeOptionsInput[], verb?: HttpVerb) { const merged: HandlerOptions = {}; for (const item of items) { let options: HandlerOptions; - // A missing `options` export reaches the per-route merge as undefined. - if (!item) { - continue; - } else if (typeof item === "object") { + if (typeof item === "object") { options = item; } else if ("options" in item) { if (verb && item.verb) { @@ -515,7 +508,7 @@ export function mergeOptions( export function normalizeOptions( verb: HttpVerb, - ...items: (MergeOptionsInput | undefined)[] + ...items: MergeOptionsInput[] ) { const merged = mergeOptions(items, verb); diff --git a/packages/run/src/runtime/types.ts b/packages/run/src/runtime/types.ts index 300b6b5b..8e91159f 100644 --- a/packages/run/src/runtime/types.ts +++ b/packages/run/src/runtime/types.ts @@ -303,26 +303,16 @@ type RouteFileGroupMeta< Group extends RouteFileGroup, Verb extends HttpVerb, > = Fallback>, Empty>; -// A file's `options` export is read off the module directly, so consuming it -// never resolves the file's inferred handler consts — the property that lets -// every other file's context include it without a type cycle. -type FileExportOptions = F["module"] extends { - options: infer O; -} - ? O - : never; type RouteFileGroupOptions< Group extends RouteFileGroup, Verb extends HttpVerb, > = MergeHandlerOptionsTuple< - FileOptionsTuple<[...Group["middleware"], Group["handler"]], Verb> + MapTuple< + TypesFromHandlerFiles<[...Group["middleware"], Group["handler"]], Verb>, + "options", + Empty + > >; -type FileOptionsTuple = { - [I in keyof Files]: MergeHandlerOptions< - FileExportOptions, - Fallback["options"], never> - >; -}; type RouteFileGroupData< Group extends RouteFileGroup, Verb extends HttpVerb, @@ -487,37 +477,43 @@ type DefineHandlerOptions = [Verb] extends [ ] ? HandlerOptionsWithoutBody : HandlerOptionsWithBody; -// Merged options as seen from one file's own context. Other files contribute -// only their `options` export: reaching into their inline options would -// resolve their inferred handler consts, whose bodies may in turn read this -// file's context — the mutual inference TypeScript collapses to `any` -// (TS7022). The local file's inline options are already in hand as `Options`, -// so they merge without touching any export. +type TypesFromHandlerFilesWithLocal< + Files extends File[], + Verb extends HttpVerb, + Id extends ID, + Options, +> = { + [I in keyof Files]: Files[I] extends { + id: Id; + } + ? HandlerTypes + : TypesFromHandlerFile; +}; type RouteFileGroupOptionsWithLocal< Group extends RouteFileGroup, + Verb extends HttpVerb, Id extends ID, Options, > = MergeHandlerOptionsTuple< - FileOptionsTupleWithLocal< - [...Group["middleware"], Group["handler"]], - Id, - Options + MapTuple< + TypesFromHandlerFilesWithLocal< + [...Group["middleware"], Group["handler"]], + Verb, + Id, + Options + >, + "options", + Empty > >; -type FileOptionsTupleWithLocal = { - [I in keyof Files]: Files[I] extends { - id: Id; - } - ? MergeHandlerOptions, Options> - : FileExportOptions; -}; type MergedRouteOptionsForFile< Path extends keyof AppPaths, + Verb extends HttpVerb, Id extends ID, Options, > = Fallback< Validation< - RouteFileGroupOptionsWithLocal + RouteFileGroupOptionsWithLocal >, Empty >; @@ -532,22 +528,22 @@ export interface RouteForFileDef< meta: Verb extends keyof AppPaths[Path]["verbs"] ? AppPaths[Path]["verbs"][Verb]["def"]["meta"] : Empty; - params: MergedRouteOptionsForFile extends { + params: MergedRouteOptionsForFile extends { params: infer T; } ? T : PathParams; - search: MergedRouteOptionsForFile extends { + search: MergedRouteOptionsForFile extends { search: infer T; } ? T : undefined; body: Verb extends HttpVerbWithBody - ? MergedRouteOptionsForFile extends { + ? MergedRouteOptionsForFile extends { json: infer T; } ? Promise - : MergedRouteOptionsForFile extends { + : MergedRouteOptionsForFile extends { form: infer T; } ? Promise @@ -559,6 +555,42 @@ export interface RouteForFileDef< ? T : Record; } +// Context> would work structurally, but Context's +// `T extends Route` constraint check compares every member eagerly — forcing +// `data`, and with it another file's export, while that export may still be +// resolving. A standalone interface keeps each member lazy: nothing resolves +// until user code reads it, by which point the exports it needs resolve +// body-free. +interface FileContext< + F extends File, + Path extends keyof AppPaths, + Verb extends HttpVerb, + Options, +> { + readonly route: Path; + readonly method: Verb; + readonly meta: RouteForFileDef["meta"]; + readonly params: RouteForFileDef["params"]; + readonly search: RouteForFileDef["search"]; + readonly body: RouteForFileDef["body"]; + readonly data: RouteForFileDef["data"]; + readonly url: URL; + readonly request: Request; + readonly platform: Platform; + readonly parent: Context | undefined; + serializedGlobals: Record; + fetch( + resource: string | URL | Request, + init?: RequestInit, + ): Promise; + render( + template: Marko.Template, + input: T, + init?: ResponseInit, + ): Response; + redirect(to: string | URL, status?: number): Response; + back(fallback?: string | URL, status?: number): Response; +} type ContextForFileWithOptions< F extends File, Verb extends HttpVerbOrAll, @@ -566,24 +598,80 @@ type ContextForFileWithOptions< > = Union<{ [Path in PathsForFile]: Union<{ [V in VerbsForPath]: V extends HttpVerb - ? Context> + ? FileContext : never; }>; }>; +// Inference captures the whole function type `H` rather than its return: a +// function type's return resolves lazily, and the `any`-returning constraint +// target never asks for it, so another file can resolve this export — and +// read its options — without this handler's body ever being typed. The +// return type still reaches the export through `InferredReturn`, a +// conditional that only types the body once something reads the data it +// carries. +type InferredHandlerFunction = (ctx: Ctx, next: NextFunction) => any; +type InferredHandlerArray = readonly ( + | { + [__run__.TYPES]: { + options: any; + data: any; + }; + } + | InferredHandlerFunction +)[]; +type InferredReturn = H extends (...args: any[]) => infer R ? R : never; +// The data a captured function type carries, deferred: nothing types the +// function's body until something actually reads the route's data. +type InferredHandlerData = HandlerFuncData>; +// The Typed payload is spelled as an object literal on purpose: a deferred +// conditional (data derived from an uninstantiated function's return) stays +// lazy inside a structural member, but is forced when passed as a direct +// type argument to a named generic like HandlerTypes — which would type the +// handler body while another file's export is still resolving. +type InferredHandlerTypes = { + context: Ctx; + verb: Verb; + options: Options; + data: InferredHandlerData extends infer Data + ? [Data] extends [never] + ? Empty + : Data extends Record + ? Data + : Empty + : never; + passthrough: [InferredHandlerData] extends [never] ? false : true; +}; +type ComposedHandlerTypes< + Ctx, + Verb extends HttpVerbOrAll, + Options, + Handlers extends readonly unknown[], +> = { + context: Ctx; + verb: Verb; + options: Options; + data: ComposedHandlerData extends infer Data + ? [Data] extends [never] + ? Empty + : Data extends Record + ? Data + : Empty + : never; + passthrough: [ComposedHandlerData] extends [never] ? false : true; +}; export type DefineHandler = { - ( - handlers: HandlerArray< - ContextForFileWithOptions & {}, - Return + ( + handlers: InferredHandlerArray< + ContextForFileWithOptions & {} > & Handlers, ): Typed< NormalizedHandlerFunction>, - HandlerTypes< + ComposedHandlerTypes< ContextForFileWithOptions & {}, Verb, ComposedHandlerOptions, - ComposedHandlerData + Handlers > >; < @@ -592,15 +680,13 @@ export type DefineHandler = { ContextForFileWithOptions & {} >, const Handlers extends readonly unknown[], - Return extends unknown[], >( options: Exact< Options, DefineHandlerOptions & {}> >, - handlers: HandlerArray< - ContextForFileWithOptions & {}, - Return + handlers: InferredHandlerArray< + ContextForFileWithOptions & {} > & Handlers, ): Typed< @@ -608,23 +694,27 @@ export type DefineHandler = { Verb, MergeHandlerOptions, Options> >, - HandlerTypes< + ComposedHandlerTypes< ContextForFileWithOptions & {}, Verb, MergeHandlerOptions, Options>, - ComposedHandlerData + Handlers > >; - ( - handler: HandlerFunction< - ContextForFileWithOptions & {}, - Return + < + H extends InferredHandlerFunction< + ContextForFileWithOptions & {} >, - ): NormalizedHandler< - ContextForFileWithOptions & {}, - Verb, - Return, - {} + >( + handler: H, + ): Typed< + NormalizedHandlerFunction, + InferredHandlerTypes< + ContextForFileWithOptions & {}, + Verb, + {}, + H + > >; < const Options extends DefineHandlerOptions< @@ -647,21 +737,23 @@ export type DefineHandler = { Verb, ContextForFileWithOptions & {} >, - Return, + H extends InferredHandlerFunction< + NoInfer> & {} + >, >( options: Exact< Options, DefineHandlerOptions & {}> >, - handler: HandlerFunction< - NoInfer> & {}, - Return - >, - ): NormalizedHandler< - ContextForFileWithOptions & {}, - Verb, - Return, - Options + handler: H, + ): Typed< + NormalizedHandlerFunction, + InferredHandlerTypes< + ContextForFileWithOptions & {}, + Verb, + Options, + H + > >; }; @@ -939,30 +1031,15 @@ export type NamespaceVerb = { }; export type GlobalNamespace = { [Verb in HttpVerbOrAll]: GlobalDefineHandler; -} & NamespaceVerb & { - options: DefineFileOptions; - }; -// File-wide options declared as `export const options = Run.options({...})`. -// The context-free constraint keeps the export's type independent of every -// route context, which is what lets other files' contexts consume it while -// their own types are still resolving. -export type DefineFileOptions = < - const O extends HandlerOptionsWithBody, ->( - options: Exact>, -) => O; +} & NamespaceVerb; export type Namespace = Typed< (F["type"] extends "middleware" ? { [Verb in HttpVerbOrAll]: DefineHandler; - } & { - options: DefineFileOptions; } : F["type"] extends "handler" ? { [Verb in HttpVerb]: DefineHandler; - } & { - options: DefineFileOptions; } : Empty) & NamespaceVerb, diff --git a/packages/run/src/vite/__tests__/fixtures/build-routes/__snapshots__/build-routes.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/build-routes/__snapshots__/build-routes.expected.routes.md index 07a74505..055ac7e2 100644 --- a/packages/run/src/vite/__tests__/fixtures/build-routes/__snapshots__/build-routes.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/build-routes/__snapshots__/build-routes.expected.routes.md @@ -3,19 +3,15 @@ ## Middleware ```js import { normalizeHandler } from "virtual:marko-run/runtime/internal"; -import middleware4, * as middlewareModule4 from "./src/routes/+middleware.ts"; -import middleware5, * as middlewareModule5 from "./src/routes/_protected/+middleware.ts"; -import middleware7, * as middlewareModule7 from "./src/routes/_protected/_home/+middleware.ts"; -import middleware13, * as middlewareModule13 from "./src/routes/_protected/_home/notes/$id/+middleware.ts"; +import middleware4 from "./src/routes/+middleware.ts"; +import middleware5 from "./src/routes/_protected/+middleware.ts"; +import middleware7 from "./src/routes/_protected/_home/+middleware.ts"; +import middleware13 from "./src/routes/_protected/_home/notes/$id/+middleware.ts"; export const mware4 = normalizeHandler(middleware4); -export const mwareOptions4 = middlewareModule4.options; export const mware5 = normalizeHandler(middleware5); -export const mwareOptions5 = middlewareModule5.options; export const mware7 = normalizeHandler(middleware7); -export const mwareOptions7 = middlewareModule7.options; export const mware13 = normalizeHandler(middleware13); -export const mwareOptions13 = middlewareModule13.options; ``` --- @@ -36,11 +32,11 @@ import Page from "../../src/routes/_protected/_home/+page.marko"; ### Handler ```js import { call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4, mwareOptions4, mware5, mwareOptions5, mware7, mwareOptions7 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mware5, mware7 } from "virtual:marko-run/__marko-run__middleware.js"; import page from "./dist/.marko-run/index.marko"; -export const get3_options = normalizeOptions('GET', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7); -export const head3_options = normalizeOptions('HEAD', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7); +export const get3_options = normalizeOptions('GET', mware4, mware5, mware7); +export const head3_options = normalizeOptions('HEAD', mware4, mware5, mware7); export function get3(context) { const __page = (data) => render(context, page, {}, data); @@ -71,9 +67,8 @@ import Page from "../../src/routes/_protected/_home/new/+page.marko"; ### Handler ```js import { normalizeHandler, normalizeMeta, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4, mwareOptions4, mware5, mwareOptions5, mware7, mwareOptions7 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mware5, mware7 } from "virtual:marko-run/__marko-run__middleware.js"; import { POST } from "./src/routes/_protected/_home/new/+handler.ts"; -import * as handlerModule from "./src/routes/_protected/_home/new/+handler.ts"; import page from "./dist/.marko-run/new.marko"; import meta4 from "./src/routes/_protected/_home/new/+meta.json"; @@ -81,9 +76,9 @@ const postHandler = normalizeHandler(POST, 'POST'); export const { GET: get4_meta, GET: head4_meta, POST: post4_meta } = normalizeMeta(meta4); -export const get4_options = normalizeOptions('GET', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7); -export const head4_options = normalizeOptions('HEAD', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7); -export const post4_options = normalizeOptions('POST', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, handlerModule.options, postHandler); +export const get4_options = normalizeOptions('GET', mware4, mware5, mware7); +export const head4_options = normalizeOptions('HEAD', mware4, mware5, mware7); +export const post4_options = normalizeOptions('POST', mware4, mware5, mware7, postHandler); export function get4(context) { const __page = (data) => render(context, page, {}, data); @@ -122,20 +117,19 @@ import Page from "../../src/routes/_protected/_home/notes/$id/+page.marko"; ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4, mwareOptions4, mware5, mwareOptions5, mware7, mwareOptions7, mware13, mwareOptions13 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mware5, mware7, mware13 } from "virtual:marko-run/__marko-run__middleware.js"; import { PUT, POST, DELETE } from "./src/routes/_protected/_home/notes/$id/+handler.ts"; -import * as handlerModule from "./src/routes/_protected/_home/notes/$id/+handler.ts"; import page from "./dist/.marko-run/notes.$.marko"; const putHandler = normalizeHandler(PUT, 'PUT'); const postHandler = normalizeHandler(POST, 'POST'); const deleteHandler = normalizeHandler(DELETE, 'DELETE'); -export const get5_options = normalizeOptions('GET', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13); -export const head5_options = normalizeOptions('HEAD', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13); -export const post5_options = normalizeOptions('POST', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, postHandler); -export const put5_options = normalizeOptions('PUT', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, putHandler); -export const delete5_options = normalizeOptions('DELETE', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, deleteHandler); +export const get5_options = normalizeOptions('GET', mware4, mware5, mware7, mware13); +export const head5_options = normalizeOptions('HEAD', mware4, mware5, mware7, mware13); +export const post5_options = normalizeOptions('POST', mware4, mware5, mware7, mware13, postHandler); +export const put5_options = normalizeOptions('PUT', mware4, mware5, mware7, mware13, putHandler); +export const delete5_options = normalizeOptions('DELETE', mware4, mware5, mware7, mware13, deleteHandler); export function get5(context) { const __page = (data) => render(context, page, {}, data); @@ -180,9 +174,8 @@ export function delete5(context) { ### Handler ```js import { normalizeHandler, normalizeMeta, call, normalizeOptions, noContent } from "virtual:marko-run/runtime/internal"; -import { mware4, mwareOptions4, mware5, mwareOptions5, mware7, mwareOptions7, mware13, mwareOptions13 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4, mware5, mware7, mware13 } from "virtual:marko-run/__marko-run__middleware.js"; import { PUT, POST, DELETE } from "./src/routes/_protected/_home/notes/$id/comments/+handler.ts"; -import * as handlerModule from "./src/routes/_protected/_home/notes/$id/comments/+handler.ts"; import meta6 from "./src/routes/_protected/_home/notes/$id/comments/+meta.ts"; const putHandler = normalizeHandler(PUT, 'PUT'); @@ -191,9 +184,9 @@ const deleteHandler = normalizeHandler(DELETE, 'DELETE'); export const { POST: post6_meta, PUT: put6_meta, DELETE: delete6_meta } = normalizeMeta(meta6); -export const post6_options = normalizeOptions('POST', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, postHandler); -export const put6_options = normalizeOptions('PUT', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, putHandler); -export const delete6_options = normalizeOptions('DELETE', mwareOptions4, mware4, mwareOptions5, mware5, mwareOptions7, mware7, mwareOptions13, mware13, handlerModule.options, deleteHandler); +export const post6_options = normalizeOptions('POST', mware4, mware5, mware7, mware13, postHandler); +export const put6_options = normalizeOptions('PUT', mware4, mware5, mware7, mware13, putHandler); +export const delete6_options = normalizeOptions('DELETE', mware4, mware5, mware7, mware13, deleteHandler); export function post6(context) { const __postHandler = (data) => call(postHandler, noContent, context, data); @@ -225,14 +218,13 @@ export function delete6(context) { ### Handler ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4, mwareOptions4 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/callback/oauth2/+handler.ts"; -import * as handlerModule from "./src/routes/callback/oauth2/+handler.ts"; const getHandler = normalizeHandler(GET, 'GET'); -export const get7_options = normalizeOptions('GET', mwareOptions4, mware4, handlerModule.options, getHandler); -export const head7_options = normalizeOptions('HEAD', mwareOptions4, mware4); +export const get7_options = normalizeOptions('GET', mware4, getHandler); +export const head7_options = normalizeOptions('HEAD', mware4); export function get7(context) { const __getHandler = (data) => call(getHandler, noContent, context, data); @@ -258,16 +250,15 @@ import Page from "../../src/routes/my/+page.marko"; ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4, mwareOptions4 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET, HEAD } from "./src/routes/my/+handler.ts"; -import * as handlerModule from "./src/routes/my/+handler.ts"; import page from "./dist/.marko-run/my.marko"; const getHandler = normalizeHandler(GET, 'GET'); const headHandler = normalizeHandler(HEAD, 'HEAD'); -export const get8_options = normalizeOptions('GET', mwareOptions4, mware4, handlerModule.options, getHandler); -export const head8_options = normalizeOptions('HEAD', mwareOptions4, mware4, handlerModule.options, headHandler); +export const get8_options = normalizeOptions('GET', mware4, getHandler); +export const head8_options = normalizeOptions('HEAD', mware4, headHandler); export function get8(context) { const __page = (data) => render(context, page, {}, data); @@ -287,14 +278,13 @@ export function head8(context) { ### Handler ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware4, mwareOptions4 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware4 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/$$match/+handler.ts"; -import * as handlerModule from "./src/routes/$$match/+handler.ts"; const getHandler = normalizeHandler(GET, 'GET'); -export const get9_options = normalizeOptions('GET', mwareOptions4, mware4, handlerModule.options, getHandler); -export const head9_options = normalizeOptions('HEAD', mwareOptions4, mware4); +export const get9_options = normalizeOptions('GET', mware4, getHandler); +export const head9_options = normalizeOptions('HEAD', mware4); export function get9(context) { const __getHandler = (data) => call(getHandler, noContent, context, data); diff --git a/packages/run/src/vite/__tests__/fixtures/flat-routes/__snapshots__/flat-routes.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/flat-routes/__snapshots__/flat-routes.expected.routes.md index bcab63a8..fce44cf5 100644 --- a/packages/run/src/vite/__tests__/fixtures/flat-routes/__snapshots__/flat-routes.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/flat-routes/__snapshots__/flat-routes.expected.routes.md @@ -3,10 +3,9 @@ ## Middleware ```js import { normalizeHandler } from "virtual:marko-run/runtime/internal"; -import middleware3, * as middlewareModule3 from "./src/routes/$id,a.d+middleware.marko"; +import middleware3 from "./src/routes/$id,a.d+middleware.marko"; export const mware3 = normalizeHandler(middleware3); -export const mwareOptions3 = middlewareModule3.options; ``` --- @@ -47,15 +46,14 @@ import Page from "../../src/routes/foo,$id,$$rest,+page.marko"; ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; -import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; import page from "./dist/.marko-run/foo.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get2_options = normalizeOptions('GET', handlerModule.options, getHandler); +export const get2_options = normalizeOptions('GET', getHandler); export const head2_options = {}; -export const post2_options = normalizeOptions('POST', handlerModule.options, postHandler); +export const post2_options = normalizeOptions('POST', postHandler); export function get2(context) { const __page = (data) => render(context, page, {}, data); @@ -83,11 +81,11 @@ import Page from "../../src/routes/foo,$id,$$rest,+page.marko"; ### Handler ```js import { call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware3, mwareOptions3 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware3 } from "virtual:marko-run/__marko-run__middleware.js"; import page from "./dist/.marko-run/$.marko"; -export const get3_options = normalizeOptions('GET', mwareOptions3, mware3); -export const head3_options = normalizeOptions('HEAD', mwareOptions3, mware3); +export const get3_options = normalizeOptions('GET', mware3); +export const head3_options = normalizeOptions('HEAD', mware3); export function get3(context) { const __page = (data) => render(context, page, {}, data); @@ -130,14 +128,13 @@ export function head4(context) { ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; -import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get5_options = normalizeOptions('GET', handlerModule.options, getHandler); +export const get5_options = normalizeOptions('GET', getHandler); export const head5_options = {}; -export const post5_options = normalizeOptions('POST', handlerModule.options, postHandler); +export const post5_options = normalizeOptions('POST', postHandler); export function get5(context) { return call(getHandler, noContent, context); @@ -157,16 +154,15 @@ export function post5(context) { ### Handler ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware3, mwareOptions3 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware3 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; -import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get6_options = normalizeOptions('GET', mwareOptions3, mware3, handlerModule.options, getHandler); -export const head6_options = normalizeOptions('HEAD', mwareOptions3, mware3); -export const post6_options = normalizeOptions('POST', mwareOptions3, mware3, handlerModule.options, postHandler); +export const get6_options = normalizeOptions('GET', mware3, getHandler); +export const head6_options = normalizeOptions('HEAD', mware3); +export const post6_options = normalizeOptions('POST', mware3, postHandler); export function get6(context) { const __getHandler = (data) => call(getHandler, noContent, context, data); @@ -189,14 +185,13 @@ export function post6(context) { ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; -import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get7_options = normalizeOptions('GET', handlerModule.options, getHandler); +export const get7_options = normalizeOptions('GET', getHandler); export const head7_options = {}; -export const post7_options = normalizeOptions('POST', handlerModule.options, postHandler); +export const post7_options = normalizeOptions('POST', postHandler); export function get7(context) { return call(getHandler, noContent, context); @@ -217,14 +212,13 @@ export function post7(context) { ```js import { normalizeHandler, call, normalizeOptions, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { GET, POST } from "./src/routes/foo,(a,b).(c,d)+handler.marko"; -import * as handlerModule from "./src/routes/foo,(a,b).(c,d)+handler.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get8_options = normalizeOptions('GET', handlerModule.options, getHandler); +export const get8_options = normalizeOptions('GET', getHandler); export const head8_options = {}; -export const post8_options = normalizeOptions('POST', handlerModule.options, postHandler); +export const post8_options = normalizeOptions('POST', postHandler); export function get8(context) { return call(getHandler, noContent, context); diff --git a/packages/run/src/vite/__tests__/fixtures/get-post/__snapshots__/get-post.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/get-post/__snapshots__/get-post.expected.routes.md index 2a98cf57..63075b24 100644 --- a/packages/run/src/vite/__tests__/fixtures/get-post/__snapshots__/get-post.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/get-post/__snapshots__/get-post.expected.routes.md @@ -3,10 +3,9 @@ ## Middleware ```js import { normalizeHandler } from "virtual:marko-run/runtime/internal"; -import middleware3, * as middlewareModule3 from "./src/routes/+middleware.ts"; +import middleware3 from "./src/routes/+middleware.ts"; export const mware3 = normalizeHandler(middleware3); -export const mwareOptions3 = middlewareModule3.options; ``` --- @@ -21,17 +20,16 @@ import Page from "../../src/routes/+page.marko"; ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware3, mwareOptions3 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware3 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET, POST } from "./src/routes/+handler.marko"; -import * as handlerModule from "./src/routes/+handler.marko"; import page from "./dist/.marko-run/index.marko"; const getHandler = normalizeHandler(GET, 'GET'); const postHandler = normalizeHandler(POST, 'POST'); -export const get1_options = normalizeOptions('GET', mwareOptions3, mware3, handlerModule.options, getHandler); -export const head1_options = normalizeOptions('HEAD', mwareOptions3, mware3); -export const post1_options = normalizeOptions('POST', mwareOptions3, mware3, handlerModule.options, postHandler); +export const get1_options = normalizeOptions('GET', mware3, getHandler); +export const head1_options = normalizeOptions('HEAD', mware3); +export const post1_options = normalizeOptions('POST', mware3, postHandler); export function get1(context) { const __page = (data) => render(context, page, {}, data); diff --git a/packages/run/src/vite/__tests__/fixtures/meta-files-with-verbs/__snapshots__/meta-files-with-verbs.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/meta-files-with-verbs/__snapshots__/meta-files-with-verbs.expected.routes.md index 9f8b8c05..4ec4a205 100644 --- a/packages/run/src/vite/__tests__/fixtures/meta-files-with-verbs/__snapshots__/meta-files-with-verbs.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/meta-files-with-verbs/__snapshots__/meta-files-with-verbs.expected.routes.md @@ -6,7 +6,6 @@ ```js import { normalizeHandler, normalizeMeta, call, normalizeOptions, noContent } from "virtual:marko-run/runtime/internal"; import { PUT, POST, DELETE } from "./src/routes/foo/bar/+handler.ts"; -import * as handlerModule from "./src/routes/foo/bar/+handler.ts"; import meta1 from "./src/routes/foo/bar/+meta.ts"; const putHandler = normalizeHandler(PUT, 'PUT'); @@ -15,9 +14,9 @@ const deleteHandler = normalizeHandler(DELETE, 'DELETE'); export const { POST: post1_meta, PUT: put1_meta, DELETE: delete1_meta } = normalizeMeta(meta1); -export const post1_options = normalizeOptions('POST', handlerModule.options, postHandler); -export const put1_options = normalizeOptions('PUT', handlerModule.options, putHandler); -export const delete1_options = normalizeOptions('DELETE', handlerModule.options, deleteHandler); +export const post1_options = normalizeOptions('POST', postHandler); +export const put1_options = normalizeOptions('PUT', putHandler); +export const delete1_options = normalizeOptions('DELETE', deleteHandler); export function post1(context) { return call(postHandler, noContent, context); @@ -44,7 +43,6 @@ import Page from "../../src/routes/foo/baz/+page.marko"; ```js import { normalizeHandler, normalizeMeta, call, normalizeOptions, render, noContent, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { PUT, POST, DELETE } from "./src/routes/foo/baz/+handler.ts"; -import * as handlerModule from "./src/routes/foo/baz/+handler.ts"; import page from "./dist/.marko-run/foo.baz.marko"; import meta2 from "./src/routes/foo/baz/+meta.json"; @@ -56,9 +54,9 @@ export const { GET: get2_meta, GET: head2_meta, POST: post2_meta, PUT: put2_meta export const get2_options = {}; export const head2_options = {}; -export const post2_options = normalizeOptions('POST', handlerModule.options, postHandler); -export const put2_options = normalizeOptions('PUT', handlerModule.options, putHandler); -export const delete2_options = normalizeOptions('DELETE', handlerModule.options, deleteHandler); +export const post2_options = normalizeOptions('POST', postHandler); +export const put2_options = normalizeOptions('PUT', putHandler); +export const delete2_options = normalizeOptions('DELETE', deleteHandler); export function get2(context) { return render(context, page, {}); diff --git a/packages/run/src/vite/__tests__/fixtures/optional-types/__snapshots__/optional-types.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/optional-types/__snapshots__/optional-types.expected.routes.md index 05bceee5..5f29a24c 100644 --- a/packages/run/src/vite/__tests__/fixtures/optional-types/__snapshots__/optional-types.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/optional-types/__snapshots__/optional-types.expected.routes.md @@ -3,10 +3,9 @@ ## Middleware ```js import { normalizeHandler } from "virtual:marko-run/runtime/internal"; -import middleware2, * as middlewareModule2 from "./src/routes/+middleware.ts"; +import middleware2 from "./src/routes/+middleware.ts"; export const mware2 = normalizeHandler(middleware2); -export const mwareOptions2 = middlewareModule2.options; ``` --- @@ -24,15 +23,14 @@ import Page from "../../src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+page.marko" ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware2, mwareOptions2 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware2 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; -import * as handlerModule from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; import page from "./dist/.marko-run/aaa.$.marko"; const getHandler = normalizeHandler(GET, 'GET'); -export const get1_options = normalizeOptions('GET', mwareOptions2, mware2, handlerModule.options, getHandler); -export const head1_options = normalizeOptions('HEAD', mwareOptions2, mware2); +export const get1_options = normalizeOptions('GET', mware2, getHandler); +export const head1_options = normalizeOptions('HEAD', mware2); export function get1(context) { const __page = (data) => render(context, page, {}, data); @@ -59,15 +57,14 @@ import Page from "../../src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+page.marko" ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware2, mwareOptions2 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware2 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; -import * as handlerModule from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; import page from "./dist/.marko-run/aaa.$.bbb.$.marko"; const getHandler = normalizeHandler(GET, 'GET'); -export const get2_options = normalizeOptions('GET', mwareOptions2, mware2, handlerModule.options, getHandler); -export const head2_options = normalizeOptions('HEAD', mwareOptions2, mware2); +export const get2_options = normalizeOptions('GET', mware2, getHandler); +export const head2_options = normalizeOptions('HEAD', mware2); export function get2(context) { const __page = (data) => render(context, page, {}, data); @@ -94,15 +91,14 @@ import Page from "../../src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+page.marko" ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware2, mwareOptions2 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware2 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; -import * as handlerModule from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; import page from "./dist/.marko-run/aaa.$.bbb.$.ccc.$.marko"; const getHandler = normalizeHandler(GET, 'GET'); -export const get3_options = normalizeOptions('GET', mwareOptions2, mware2, handlerModule.options, getHandler); -export const head3_options = normalizeOptions('HEAD', mwareOptions2, mware2); +export const get3_options = normalizeOptions('GET', mware2, getHandler); +export const head3_options = normalizeOptions('HEAD', mware2); export function get3(context) { const __page = (data) => render(context, page, {}, data); @@ -129,15 +125,14 @@ import Page from "../../src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+page.marko" ### Handler ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; -import { mware2, mwareOptions2 } from "virtual:marko-run/__marko-run__middleware.js"; +import { mware2 } from "virtual:marko-run/__marko-run__middleware.js"; import { GET } from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; -import * as handlerModule from "./src/routes/aaa.$aId.(,bbb.$bId).(,ccc.$cId)/+handler.ts"; import page from "./dist/.marko-run/aaa.$.ccc.$.marko"; const getHandler = normalizeHandler(GET, 'GET'); -export const get4_options = normalizeOptions('GET', mwareOptions2, mware2, handlerModule.options, getHandler); -export const head4_options = normalizeOptions('HEAD', mwareOptions2, mware2); +export const get4_options = normalizeOptions('GET', mware2, getHandler); +export const head4_options = normalizeOptions('HEAD', mware2); export function get4(context) { const __page = (data) => render(context, page, {}, data); diff --git a/packages/run/src/vite/__tests__/fixtures/query-verb/__snapshots__/query-verb.expected.routes.md b/packages/run/src/vite/__tests__/fixtures/query-verb/__snapshots__/query-verb.expected.routes.md index 4442993e..11b27180 100644 --- a/packages/run/src/vite/__tests__/fixtures/query-verb/__snapshots__/query-verb.expected.routes.md +++ b/packages/run/src/vite/__tests__/fixtures/query-verb/__snapshots__/query-verb.expected.routes.md @@ -12,7 +12,6 @@ import Page from "../../src/routes/+page.marko"; ```js import { normalizeHandler, call, normalizeOptions, render, stripResponseBody } from "virtual:marko-run/runtime/internal"; import { QUERY, POST } from "./src/routes/+handler.ts"; -import * as handlerModule from "./src/routes/+handler.ts"; import page from "./dist/.marko-run/index.marko"; const queryHandler = normalizeHandler(QUERY, 'QUERY'); @@ -20,8 +19,8 @@ const postHandler = normalizeHandler(POST, 'POST'); export const get1_options = {}; export const head1_options = {}; -export const post1_options = normalizeOptions('POST', handlerModule.options, postHandler); -export const query1_options = normalizeOptions('QUERY', handlerModule.options, queryHandler); +export const post1_options = normalizeOptions('POST', postHandler); +export const query1_options = normalizeOptions('QUERY', queryHandler); export function get1(context) { return render(context, page, {}); diff --git a/packages/run/src/vite/codegen/index.ts b/packages/run/src/vite/codegen/index.ts index 379dc758..f622833a 100644 --- a/packages/run/src/vite/codegen/index.ts +++ b/packages/run/src/vite/codegen/index.ts @@ -120,10 +120,7 @@ export function renderRouteEntry(route: Route, rootDir: string): string { } if (middleware.length) { - const names = middleware.flatMap((m) => [ - `mware${m.id}`, - `mwareOptions${m.id}`, - ]); + const names = middleware.map((m) => `mware${m.id}`); imports.writeLines( `import { ${names.join( ", ", @@ -144,7 +141,6 @@ export function renderRouteEntry(route: Route, rootDir: string): string { } imports.writeLines( `import { ${names.join(", ")} } from "${normalizedRelativePath(rootDir, handler.filePath)}";`, - `import * as handlerModule from "${normalizedRelativePath(rootDir, handler.filePath)}";`, ); } @@ -351,11 +347,10 @@ export function renderMiddleware( for (const { id, filePath } of middleware) { const importName = `middleware${id}`; imports.writeLines( - `import ${importName}, * as middlewareModule${id} from "${normalizedRelativePath(rootDir, filePath)}";`, + `import ${importName} from "${normalizedRelativePath(rootDir, filePath)}";`, ); writer.writeLines( `export const mware${id} = normalizeHandler(${importName});`, - `export const mwareOptions${id} = middlewareModule${id}.options;`, ); } @@ -640,10 +635,10 @@ function writeRouteOptions(writer: Writer, route: Route, verb: HttpVerb): void { writer.write(`normalizeOptions('${verb.toUpperCase()}'`); for (const { id } of route.middleware) { - writer.write(`, mwareOptions${id}, mware${id}`); + writer.write(`, mware${id}`); } if (hasHandler) { - writer.write(`, handlerModule.options, ${verb}Handler`); + writer.write(`, ${verb}Handler`); } writer.write(");"); } else { From f31766d286c9e5d712538c26107fe7caa5b39ace Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Mon, 17 Aug 2026 22:11:20 -0700 Subject: [PATCH 06/10] fix: check handler returns where no middleware entangles, name capture types for declaration emit --- .../src/routes/api/procs/$pid/+handler.ts | 12 ++ packages/run/src/runtime/index.ts | 9 + packages/run/src/runtime/types.ts | 179 ++++++++++++++---- 3 files changed, 168 insertions(+), 32 deletions(-) diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts index 55831c10..29fb064a 100644 --- a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts @@ -8,3 +8,15 @@ export const DELETE = Run.DELETE( return Response.json({ pid: ctx.params.pid }); }, ); + +// This route has no middleware upstream, so its namespace resolves to the +// return-checking CheckedDefineHandler. +export const validReturns = [ + Run.GET(() => new Response("ok")), + Run.GET(async () => Response.json({ ok: true })), + Run.GET((ctx, next) => next()), +]; +// @ts-expect-error a handler cannot return a bare number +export const badNumber = Run.GET(() => 42); +// @ts-expect-error a handler cannot return a bare object in place of a Response +export const badObject = Run.GET(() => ({ plain: "object" })); diff --git a/packages/run/src/runtime/index.ts b/packages/run/src/runtime/index.ts index 3cfe955b..a50ff637 100644 --- a/packages/run/src/runtime/index.ts +++ b/packages/run/src/runtime/index.ts @@ -76,14 +76,23 @@ export type { } from "./legacy-types"; export type { App, + CheckedDefineHandler, + ComposedHandlerTypes, Context, ContextForFile, + DefineHandler, DefineRoutes, Empty, Fetch, + FileContext, GetContext, Handler, HandlerTypes, + InferredHandlerArray, + InferredHandlerData, + InferredHandlerFunction, + InferredHandlerTypes, + InferredReturn, Invoke, LayoutInput, Match, diff --git a/packages/run/src/runtime/types.ts b/packages/run/src/runtime/types.ts index 8e91159f..50d16083 100644 --- a/packages/run/src/runtime/types.ts +++ b/packages/run/src/runtime/types.ts @@ -472,6 +472,11 @@ type ComposedHandlerData = MergeTuple<{ [K in keyof Handlers]: HandlerValueData; }>; type Exact = T & Record, never>; +// A function is assignable to the all-optional options shape (and Exact has +// no keys to poison on it), which let a mistyped `Run.GET(() => ...)` match +// the options-only overload and skip return checking. Every function has +// `apply`, so this bars callables while leaving plain option objects alone. +type NotAFunction = { apply?: never }; type DefineHandlerOptions = [Verb] extends [ HttpVerbWithoutBody, ] @@ -561,7 +566,7 @@ export interface RouteForFileDef< // resolving. A standalone interface keeps each member lazy: nothing resolves // until user code reads it, by which point the exports it needs resolve // body-free. -interface FileContext< +export interface FileContext< F extends File, Path extends keyof AppPaths, Verb extends HttpVerb, @@ -609,8 +614,11 @@ type ContextForFileWithOptions< // return type still reaches the export through `InferredReturn`, a // conditional that only types the body once something reads the data it // carries. -type InferredHandlerFunction = (ctx: Ctx, next: NextFunction) => any; -type InferredHandlerArray = readonly ( +export type InferredHandlerFunction = ( + ctx: Ctx, + next: NextFunction, +) => any; +export type InferredHandlerArray = readonly ( | { [__run__.TYPES]: { options: any; @@ -619,17 +627,24 @@ type InferredHandlerArray = readonly ( } | InferredHandlerFunction )[]; -type InferredReturn = H extends (...args: any[]) => infer R ? R : never; +export type InferredReturn = H extends (...args: any[]) => infer R + ? R + : never; // The data a captured function type carries, deferred: nothing types the // function's body until something actually reads the route's data. -type InferredHandlerData = HandlerFuncData>; +export type InferredHandlerData = HandlerFuncData>; // The Typed payload is spelled as an object literal on purpose: a deferred // conditional (data derived from an uninstantiated function's return) stays // lazy inside a structural member, but is forced when passed as a direct // type argument to a named generic like HandlerTypes — which would type the // handler body while another file's export is still resolving. -type InferredHandlerTypes = { - context: Ctx; +export type InferredHandlerTypes< + F extends File, + Verb extends HttpVerbOrAll, + Options, + H, +> = { + context: ContextForFileWithOptions & {}; verb: Verb; options: Options; data: InferredHandlerData extends infer Data @@ -641,13 +656,13 @@ type InferredHandlerTypes = { : never; passthrough: [InferredHandlerData] extends [never] ? false : true; }; -type ComposedHandlerTypes< - Ctx, +export type ComposedHandlerTypes< + F extends File, Verb extends HttpVerbOrAll, Options, Handlers extends readonly unknown[], > = { - context: Ctx; + context: ContextForFileWithOptions & {}; verb: Verb; options: Options; data: ComposedHandlerData extends infer Data @@ -659,19 +674,33 @@ type ComposedHandlerTypes< : never; passthrough: [ComposedHandlerData] extends [never] ? false : true; }; -export type DefineHandler = { - ( - handlers: InferredHandlerArray< - ContextForFileWithOptions & {} +// When no middleware runs upstream of a handler file, nothing can resolve +// its export while another export is mid-flight, so the original +// return-inferring overloads are safe — and they check the handler's return +// type, which the deferred form cannot (a checked return target forces the +// body while an entangled export resolves). Namespace picks per file. +type FileHasUpstream = + true extends Union<{ + [P in PathsForFile]: AppPaths[P]["files"]["middleware"] extends [] + ? false + : true; + }> + ? true + : false; +export type CheckedDefineHandler = { + ( + handlers: HandlerArray< + ContextForFileWithOptions & {}, + Return > & Handlers, ): Typed< NormalizedHandlerFunction>, - ComposedHandlerTypes< + HandlerTypes< ContextForFileWithOptions & {}, Verb, ComposedHandlerOptions, - Handlers + ComposedHandlerData > >; < @@ -680,11 +709,103 @@ export type DefineHandler = { ContextForFileWithOptions & {} >, const Handlers extends readonly unknown[], + Return extends unknown[], + >( + options: Exact< + Options, + DefineHandlerOptions & {}> + > & + NotAFunction, + handlers: HandlerArray< + ContextForFileWithOptions & {}, + Return + > & + Handlers, + ): Typed< + NormalizedHandlerFunction< + Verb, + MergeHandlerOptions, Options> + >, + HandlerTypes< + ContextForFileWithOptions & {}, + Verb, + MergeHandlerOptions, Options>, + ComposedHandlerData + > + >; + ( + handler: HandlerFunction< + ContextForFileWithOptions & {}, + Return + >, + ): NormalizedHandler< + ContextForFileWithOptions & {}, + Verb, + Return, + {} + >; + < + const Options extends DefineHandlerOptions< + Verb, + ContextForFileWithOptions & {} + >, + >( + options: Exact< + Options, + DefineHandlerOptions & {}> + > & + NotAFunction, + ): NormalizedHandler< + ContextForFileWithOptions & {}, + Verb, + {}, + Options + >; + < + const Options extends DefineHandlerOptions< + Verb, + ContextForFileWithOptions & {} + >, + Return, >( options: Exact< Options, DefineHandlerOptions & {}> + > & + NotAFunction, + handler: HandlerFunction< + NoInfer> & {}, + Return + >, + ): NormalizedHandler< + ContextForFileWithOptions & {}, + Verb, + Return, + Options + >; +}; +export type DefineHandler = { + ( + handlers: InferredHandlerArray< + ContextForFileWithOptions & {} + > & + Handlers, + ): Typed< + NormalizedHandlerFunction>, + ComposedHandlerTypes, Handlers> + >; + < + const Options extends DefineHandlerOptions< + Verb, + ContextForFileWithOptions & {} >, + const Handlers extends readonly unknown[], + >( + options: Exact< + Options, + DefineHandlerOptions & {}> + > & + NotAFunction, handlers: InferredHandlerArray< ContextForFileWithOptions & {} > & @@ -695,7 +816,7 @@ export type DefineHandler = { MergeHandlerOptions, Options> >, ComposedHandlerTypes< - ContextForFileWithOptions & {}, + F, Verb, MergeHandlerOptions, Options>, Handlers @@ -709,12 +830,7 @@ export type DefineHandler = { handler: H, ): Typed< NormalizedHandlerFunction, - InferredHandlerTypes< - ContextForFileWithOptions & {}, - Verb, - {}, - H - > + InferredHandlerTypes >; < const Options extends DefineHandlerOptions< @@ -725,7 +841,8 @@ export type DefineHandler = { options: Exact< Options, DefineHandlerOptions & {}> - >, + > & + NotAFunction, ): NormalizedHandler< ContextForFileWithOptions & {}, Verb, @@ -744,16 +861,12 @@ export type DefineHandler = { options: Exact< Options, DefineHandlerOptions & {}> - >, + > & + NotAFunction, handler: H, ): Typed< NormalizedHandlerFunction, - InferredHandlerTypes< - ContextForFileWithOptions & {}, - Verb, - Options, - H - > + InferredHandlerTypes >; }; @@ -1039,7 +1152,9 @@ export type Namespace = Typed< } : F["type"] extends "handler" ? { - [Verb in HttpVerb]: DefineHandler; + [Verb in HttpVerb]: FileHasUpstream extends true + ? DefineHandler + : CheckedDefineHandler; } : Empty) & NamespaceVerb, From d24226c418d9c2ea9bf150149c39a0ae0813038b Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Tue, 18 Aug 2026 10:06:08 -0700 Subject: [PATCH 07/10] test: pin context helper compatibility and route-level validated params --- .../workspaces/$id/review/$commentId/+handler.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts index 762eb1db..7ee11261 100644 --- a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/workspaces/$id/review/$commentId/+handler.ts @@ -1,3 +1,5 @@ +import type { Context, GetContext } from "@marko/run"; + export const DELETE = Run.DELETE( { params({ id, commentId }) { @@ -5,6 +7,13 @@ export const DELETE = Run.DELETE( }, }, (ctx) => { + const asPlainContext: Context = ctx; + const asScopedContext: GetContext<"/api/workspaces/$id/review/$commentId"> = + ctx; + // @ts-expect-error the deprecated MarkoRun.Context types params as raw + // strings, so a context whose validator produced a number never fit it + const asLegacyContext: MarkoRun.Context = ctx; + void asPlainContext, void asScopedContext, void asLegacyContext; const validated: number = ctx.params.commentId; const upstream: string = ctx.data.workspace; const fromMiddleware: number = ctx.search.limit; @@ -22,3 +31,7 @@ export const DELETE = Run.DELETE( }); }, ); + +export const routeLevel: number = null as unknown as Run.Context["params"]["commentId"]; +// @ts-expect-error route-level params keep the validated number type +export const routeLevelBad: boolean = null as unknown as Run.Context["params"]["commentId"]; From 3091eaa0ca6b5d459b8291f32dbcffba2c9ee49e Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Tue, 18 Aug 2026 11:08:14 -0700 Subject: [PATCH 08/10] refactor: one deferred DefineHandler parameterized by its return target --- packages/run/src/runtime/index.ts | 10 +- packages/run/src/runtime/types.ts | 270 ++++++++---------------------- 2 files changed, 72 insertions(+), 208 deletions(-) diff --git a/packages/run/src/runtime/index.ts b/packages/run/src/runtime/index.ts index a50ff637..55baf1b7 100644 --- a/packages/run/src/runtime/index.ts +++ b/packages/run/src/runtime/index.ts @@ -76,10 +76,11 @@ export type { } from "./legacy-types"; export type { App, - CheckedDefineHandler, - ComposedHandlerTypes, Context, + ContextBase, ContextForFile, + DefinedHandler, + DefinedHandlerTypes, DefineHandler, DefineRoutes, Empty, @@ -88,11 +89,6 @@ export type { GetContext, Handler, HandlerTypes, - InferredHandlerArray, - InferredHandlerData, - InferredHandlerFunction, - InferredHandlerTypes, - InferredReturn, Invoke, LayoutInput, Match, diff --git a/packages/run/src/runtime/types.ts b/packages/run/src/runtime/types.ts index 50d16083..826613bc 100644 --- a/packages/run/src/runtime/types.ts +++ b/packages/run/src/runtime/types.ts @@ -571,7 +571,7 @@ export interface FileContext< Path extends keyof AppPaths, Verb extends HttpVerb, Options, -> { +> extends ContextBase { readonly route: Path; readonly method: Verb; readonly meta: RouteForFileDef["meta"]; @@ -579,22 +579,6 @@ export interface FileContext< readonly search: RouteForFileDef["search"]; readonly body: RouteForFileDef["body"]; readonly data: RouteForFileDef["data"]; - readonly url: URL; - readonly request: Request; - readonly platform: Platform; - readonly parent: Context | undefined; - serializedGlobals: Record; - fetch( - resource: string | URL | Request, - init?: RequestInit, - ): Promise; - render( - template: Marko.Template, - input: T, - init?: ResponseInit, - ): Response; - redirect(to: string | URL, status?: number): Response; - back(fallback?: string | URL, status?: number): Response; } type ContextForFileWithOptions< F extends File, @@ -607,78 +591,68 @@ type ContextForFileWithOptions< : never; }>; }>; -// Inference captures the whole function type `H` rather than its return: a -// function type's return resolves lazily, and the `any`-returning constraint +// ── Deferred handler definition ───────────────────────────────────────── +// Inference captures whole function types rather than their returns: a +// function type's return resolves lazily, and an `any`-returning constraint // target never asks for it, so another file can resolve this export — and -// read its options — without this handler's body ever being typed. The -// return type still reaches the export through `InferredReturn`, a -// conditional that only types the body once something reads the data it -// carries. -export type InferredHandlerFunction = ( +// read its merged options — without this file's body ever being typed. The +// body's contribution (data passed through `next()`) reaches the export as +// conditionals over the captured types, typed only once something reads the +// data they carry. +// +// `Return` selects the constraint's return target: `any` keeps the export +// resolvable body-free, required wherever another route file's inference can +// entangle this one; `HandlerReturn` checks the return position like an +// ordinary generic call, safe only when nothing else can be mid-resolution. +// Namespace picks per file. +type DefinedHandlerFunction = ( ctx: Ctx, next: NextFunction, -) => any; -export type InferredHandlerArray = readonly ( +) => Return; +type DefinedHandlerArray = readonly ( | { [__run__.TYPES]: { options: any; data: any; }; } - | InferredHandlerFunction + | DefinedHandlerFunction )[]; -export type InferredReturn = H extends (...args: any[]) => infer R - ? R - : never; -// The data a captured function type carries, deferred: nothing types the -// function's body until something actually reads the route's data. -export type InferredHandlerData = HandlerFuncData>; -// The Typed payload is spelled as an object literal on purpose: a deferred -// conditional (data derived from an uninstantiated function's return) stays -// lazy inside a structural member, but is forced when passed as a direct -// type argument to a named generic like HandlerTypes — which would type the -// handler body while another file's export is still resolving. -export type InferredHandlerTypes< +// Spelled as an object literal on purpose: a deferred conditional (data +// derived from an uncaptured function's return) stays lazy as a structural +// member, but is forced when passed as a direct type argument to a named +// generic like HandlerTypes — which would type the body while another file's +// export is still resolving. +export type DefinedHandlerTypes< F extends File, Verb extends HttpVerbOrAll, Options, - H, + Handlers extends readonly unknown[], > = { context: ContextForFileWithOptions & {}; verb: Verb; options: Options; - data: InferredHandlerData extends infer Data + data: ComposedHandlerData extends infer Data ? [Data] extends [never] ? Empty : Data extends Record ? Data : Empty : never; - passthrough: [InferredHandlerData] extends [never] ? false : true; + passthrough: [ComposedHandlerData] extends [never] ? false : true; }; -export type ComposedHandlerTypes< +export type DefinedHandler< F extends File, Verb extends HttpVerbOrAll, Options, Handlers extends readonly unknown[], -> = { - context: ContextForFileWithOptions & {}; - verb: Verb; - options: Options; - data: ComposedHandlerData extends infer Data - ? [Data] extends [never] - ? Empty - : Data extends Record - ? Data - : Empty - : never; - passthrough: [ComposedHandlerData] extends [never] ? false : true; -}; -// When no middleware runs upstream of a handler file, nothing can resolve -// its export while another export is mid-flight, so the original -// return-inferring overloads are safe — and they check the handler's return -// type, which the deferred form cannot (a checked return target forces the -// body while an entangled export resolves). Namespace picks per file. +> = Typed< + NormalizedHandlerFunction, + DefinedHandlerTypes +>; +// A handler file with no middleware on any of its routes can never be +// resolved while another export is in flight, so it gets the return-checked +// form. type FileHasUpstream = true extends Union<{ [P in PathsForFile]: AppPaths[P]["files"]["middleware"] extends [] @@ -687,151 +661,49 @@ type FileHasUpstream = }> ? true : false; -export type CheckedDefineHandler = { - ( - handlers: HandlerArray< +export type DefineHandler< + F extends File, + Verb extends HttpVerbOrAll, + Return = any, +> = { + ( + handlers: DefinedHandlerArray< ContextForFileWithOptions & {}, Return > & Handlers, - ): Typed< - NormalizedHandlerFunction>, - HandlerTypes< - ContextForFileWithOptions & {}, - Verb, - ComposedHandlerOptions, - ComposedHandlerData - > - >; + ): DefinedHandler, Handlers>; < const Options extends DefineHandlerOptions< Verb, ContextForFileWithOptions & {} >, const Handlers extends readonly unknown[], - Return extends unknown[], >( options: Exact< Options, DefineHandlerOptions & {}> > & NotAFunction, - handlers: HandlerArray< + handlers: DefinedHandlerArray< ContextForFileWithOptions & {}, Return > & Handlers, - ): Typed< - NormalizedHandlerFunction< - Verb, - MergeHandlerOptions, Options> - >, - HandlerTypes< - ContextForFileWithOptions & {}, - Verb, - MergeHandlerOptions, Options>, - ComposedHandlerData - > - >; - ( - handler: HandlerFunction< - ContextForFileWithOptions & {}, - Return - >, - ): NormalizedHandler< - ContextForFileWithOptions & {}, - Verb, - Return, - {} - >; - < - const Options extends DefineHandlerOptions< - Verb, - ContextForFileWithOptions & {} - >, - >( - options: Exact< - Options, - DefineHandlerOptions & {}> - > & - NotAFunction, - ): NormalizedHandler< - ContextForFileWithOptions & {}, + ): DefinedHandler< + F, Verb, - {}, - Options + MergeHandlerOptions, Options>, + Handlers >; < - const Options extends DefineHandlerOptions< - Verb, - ContextForFileWithOptions & {} - >, - Return, - >( - options: Exact< - Options, - DefineHandlerOptions & {}> - > & - NotAFunction, - handler: HandlerFunction< - NoInfer> & {}, + H extends DefinedHandlerFunction< + ContextForFileWithOptions & {}, Return >, - ): NormalizedHandler< - ContextForFileWithOptions & {}, - Verb, - Return, - Options - >; -}; -export type DefineHandler = { - ( - handlers: InferredHandlerArray< - ContextForFileWithOptions & {} - > & - Handlers, - ): Typed< - NormalizedHandlerFunction>, - ComposedHandlerTypes, Handlers> - >; - < - const Options extends DefineHandlerOptions< - Verb, - ContextForFileWithOptions & {} - >, - const Handlers extends readonly unknown[], - >( - options: Exact< - Options, - DefineHandlerOptions & {}> - > & - NotAFunction, - handlers: InferredHandlerArray< - ContextForFileWithOptions & {} - > & - Handlers, - ): Typed< - NormalizedHandlerFunction< - Verb, - MergeHandlerOptions, Options> - >, - ComposedHandlerTypes< - F, - Verb, - MergeHandlerOptions, Options>, - Handlers - > - >; - < - H extends InferredHandlerFunction< - ContextForFileWithOptions & {} - >, >( handler: H, - ): Typed< - NormalizedHandlerFunction, - InferredHandlerTypes - >; + ): DefinedHandler; < const Options extends DefineHandlerOptions< Verb, @@ -843,19 +715,15 @@ export type DefineHandler = { DefineHandlerOptions & {}> > & NotAFunction, - ): NormalizedHandler< - ContextForFileWithOptions & {}, - Verb, - {}, - Options - >; + ): DefinedHandler; < const Options extends DefineHandlerOptions< Verb, ContextForFileWithOptions & {} >, - H extends InferredHandlerFunction< - NoInfer> & {} + H extends DefinedHandlerFunction< + NoInfer> & {}, + Return >, >( options: Exact< @@ -864,12 +732,8 @@ export type DefineHandler = { > & NotAFunction, handler: H, - ): Typed< - NormalizedHandlerFunction, - InferredHandlerTypes - >; + ): DefinedHandler; }; - export type GlobalDefineHandler = { ( handlers: HandlerArray & Handlers, @@ -1154,7 +1018,7 @@ export type Namespace = Typed< ? { [Verb in HttpVerb]: FileHasUpstream extends true ? DefineHandler - : CheckedDefineHandler; + : DefineHandler; } : Empty) & NamespaceVerb, @@ -1184,14 +1048,9 @@ export type DefineRoutes = { >; }; export interface Platform {} -export interface Context { - readonly route: T["path"]; - readonly method: T["method"]; - readonly meta: T["meta"]; - readonly params: T["params"]; - readonly search: T["search"]; - readonly body: T["body"]; - readonly data: T["data"]; +// The route-independent half of a context, shared so FileContext cannot +// drift from Context. +export interface ContextBase { readonly url: URL; readonly request: Request; readonly platform: Platform; @@ -1209,6 +1068,15 @@ export interface Context { redirect(to: string | URL, status?: number): Response; back(fallback?: string | URL, status?: number): Response; } +export interface Context extends ContextBase { + readonly route: T["path"]; + readonly method: T["method"]; + readonly meta: T["meta"]; + readonly params: T["params"]; + readonly search: T["search"]; + readonly body: T["body"]; + readonly data: T["data"]; +} export type GetContext< Scope extends keyof AppPaths | `*` | `/${string}*` | object = "*", Verb extends From 88fd81f085cfa1bc7cd58caec7f86d352ff71c77 Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Tue, 18 Aug 2026 11:47:00 -0700 Subject: [PATCH 09/10] refactor: tighten handler define aliases and drop redundant intersections --- packages/run/src/runtime/types.ts | 54 ++++++++++++------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/packages/run/src/runtime/types.ts b/packages/run/src/runtime/types.ts index 826613bc..392cfb36 100644 --- a/packages/run/src/runtime/types.ts +++ b/packages/run/src/runtime/types.ts @@ -605,6 +605,15 @@ type ContextForFileWithOptions< // entangle this one; `HandlerReturn` checks the return position like an // ordinary generic call, safe only when nothing else can be mid-resolution. // Namespace picks per file. +type FileHandlerOptions< + F extends File, + Verb extends HttpVerbOrAll, +> = DefineHandlerOptions>; +type OptionsArg = Exact< + Options, + FileHandlerOptions +> & + NotAFunction; type DefinedHandlerFunction = ( ctx: Ctx, next: NextFunction, @@ -629,7 +638,7 @@ export type DefinedHandlerTypes< Options, Handlers extends readonly unknown[], > = { - context: ContextForFileWithOptions & {}; + context: ContextForFileWithOptions; verb: Verb; options: Options; data: ComposedHandlerData extends infer Data @@ -668,25 +677,18 @@ export type DefineHandler< > = { ( handlers: DefinedHandlerArray< - ContextForFileWithOptions & {}, + ContextForFileWithOptions, Return > & Handlers, ): DefinedHandler, Handlers>; < - const Options extends DefineHandlerOptions< - Verb, - ContextForFileWithOptions & {} - >, + const Options extends FileHandlerOptions, const Handlers extends readonly unknown[], >( - options: Exact< - Options, - DefineHandlerOptions & {}> - > & - NotAFunction, + options: OptionsArg, handlers: DefinedHandlerArray< - ContextForFileWithOptions & {}, + ContextForFileWithOptions, Return > & Handlers, @@ -698,39 +700,23 @@ export type DefineHandler< >; < H extends DefinedHandlerFunction< - ContextForFileWithOptions & {}, + ContextForFileWithOptions, Return >, >( handler: H, ): DefinedHandler; - < - const Options extends DefineHandlerOptions< - Verb, - ContextForFileWithOptions & {} - >, - >( - options: Exact< - Options, - DefineHandlerOptions & {}> - > & - NotAFunction, + >( + options: OptionsArg, ): DefinedHandler; < - const Options extends DefineHandlerOptions< - Verb, - ContextForFileWithOptions & {} - >, + const Options extends FileHandlerOptions, H extends DefinedHandlerFunction< - NoInfer> & {}, + NoInfer>, Return >, >( - options: Exact< - Options, - DefineHandlerOptions & {}> - > & - NotAFunction, + options: OptionsArg, handler: H, ): DefinedHandler; }; From c99cfc6ba60697866241b08856413ef2a74a82ce Mon Sep 17 00:00:00 2001 From: Ryan Turnquist Date: Tue, 18 Aug 2026 12:19:17 -0700 Subject: [PATCH 10/10] docs: align type comments with the comment conventions --- .../src/routes/api/procs/$pid/+handler.ts | 3 +- packages/run/src/__tests__/typecheck.test.ts | 9 +-- packages/run/src/runtime/types.ts | 71 ++++++++++--------- 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts index 29fb064a..cd14bed2 100644 --- a/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts +++ b/packages/run/src/__tests__/fixtures/params-validator-types/src/routes/api/procs/$pid/+handler.ts @@ -9,8 +9,7 @@ export const DELETE = Run.DELETE( }, ); -// This route has no middleware upstream, so its namespace resolves to the -// return-checking CheckedDefineHandler. +// No middleware runs upstream, so this file's handlers get return checking. export const validReturns = [ Run.GET(() => new Response("ok")), Run.GET(async () => Response.json({ ok: true })), diff --git a/packages/run/src/__tests__/typecheck.test.ts b/packages/run/src/__tests__/typecheck.test.ts index 16ce9df1..61658a1f 100644 --- a/packages/run/src/__tests__/typecheck.test.ts +++ b/packages/run/src/__tests__/typecheck.test.ts @@ -59,12 +59,9 @@ describe("test sources type-check", () => { }); describe("route type generation type-checks", () => { - // The params-validator-types fixture pairs a parent-segment middleware — - // deriving `next(data)` from its context, with its own inline validator — - // with a downstream handler whose validator the middleware's context must - // reflect: the mutual inference that used to collapse both modules to - // `any` (TS7022). Its sources carry `@ts-expect-error` probes, so this - // compile also fails if the types silently widen back to `any` (TS2578). + // The fixture's middleware and handler each need the other's inferred + // module type — the shape that collapses both to `any` (TS7022). Its + // `@ts-expect-error` probes make silent widening fail this compile too. it("keeps middleware and validator types across the module cycle", function () { this.timeout(120000); const result = spawnSync( diff --git a/packages/run/src/runtime/types.ts b/packages/run/src/runtime/types.ts index 392cfb36..d36a288f 100644 --- a/packages/run/src/runtime/types.ts +++ b/packages/run/src/runtime/types.ts @@ -472,10 +472,11 @@ type ComposedHandlerData = MergeTuple<{ [K in keyof Handlers]: HandlerValueData; }>; type Exact = T & Record, never>; -// A function is assignable to the all-optional options shape (and Exact has -// no keys to poison on it), which let a mistyped `Run.GET(() => ...)` match -// the options-only overload and skip return checking. Every function has -// `apply`, so this bars callables while leaving plain option objects alone. +/** + * Bars a function from matching the all-optional options parameter — every + * function has `apply` — so a mistyped handler argument fails the overload + * instead of being accepted as an options object. + */ type NotAFunction = { apply?: never }; type DefineHandlerOptions = [Verb] extends [ HttpVerbWithoutBody, @@ -560,12 +561,13 @@ export interface RouteForFileDef< ? T : Record; } -// Context> would work structurally, but Context's -// `T extends Route` constraint check compares every member eagerly — forcing -// `data`, and with it another file's export, while that export may still be -// resolving. A standalone interface keeps each member lazy: nothing resolves -// until user code reads it, by which point the exports it needs resolve -// body-free. +/** + * A file's view of one route context. Deliberately not `Context<...>`: its + * constraint check forces every member — including `data`, and with it + * another file's possibly mid-resolution export. A standalone interface + * resolves each member only when read, after the exports it needs can + * resolve body-free. + */ export interface FileContext< F extends File, Path extends keyof AppPaths, @@ -591,20 +593,6 @@ type ContextForFileWithOptions< : never; }>; }>; -// ── Deferred handler definition ───────────────────────────────────────── -// Inference captures whole function types rather than their returns: a -// function type's return resolves lazily, and an `any`-returning constraint -// target never asks for it, so another file can resolve this export — and -// read its merged options — without this file's body ever being typed. The -// body's contribution (data passed through `next()`) reaches the export as -// conditionals over the captured types, typed only once something reads the -// data they carry. -// -// `Return` selects the constraint's return target: `any` keeps the export -// resolvable body-free, required wherever another route file's inference can -// entangle this one; `HandlerReturn` checks the return position like an -// ordinary generic call, safe only when nothing else can be mid-resolution. -// Namespace picks per file. type FileHandlerOptions< F extends File, Verb extends HttpVerbOrAll, @@ -627,11 +615,12 @@ type DefinedHandlerArray = readonly ( } | DefinedHandlerFunction )[]; -// Spelled as an object literal on purpose: a deferred conditional (data -// derived from an uncaptured function's return) stays lazy as a structural -// member, but is forced when passed as a direct type argument to a named -// generic like HandlerTypes — which would type the body while another file's -// export is still resolving. +/** + * Spelled as an object literal: a deferred conditional stays lazy as a + * structural member but is forced as a direct type argument to a named + * generic like HandlerTypes, which would type the handler body while another + * file's export is still resolving. + */ export type DefinedHandlerTypes< F extends File, Verb extends HttpVerbOrAll, @@ -659,9 +648,12 @@ export type DefinedHandler< NormalizedHandlerFunction, DefinedHandlerTypes >; -// A handler file with no middleware on any of its routes can never be -// resolved while another export is in flight, so it gets the return-checked -// form. +/** + * True when middleware shares any of the file's routes, so resolving its + * export while another is mid-flight is possible. Namespace resolves this — + * not a `Return` default — because the deferred form needs the literal `any` + * type for the checker to skip return comparison. + */ type FileHasUpstream = true extends Union<{ [P in PathsForFile]: AppPaths[P]["files"]["middleware"] extends [] @@ -670,6 +662,18 @@ type FileHasUpstream = }> ? true : false; +/** + * Captures whole function types rather than their returns: a function type's + * return resolves lazily and an `any` return target never asks for it, so + * other files can read this export's merged options without its body ever + * being typed. Body-derived data reaches the export as conditionals typed + * only when something reads them. + * + * `Return` is the callbacks' return target: `any` keeps the export + * resolvable body-free, required wherever another route file's inference can + * entangle this one; `HandlerReturn` checks the return position, safe only + * when nothing else can be mid-resolution. + */ export type DefineHandler< F extends File, Verb extends HttpVerbOrAll, @@ -1034,8 +1038,7 @@ export type DefineRoutes = { >; }; export interface Platform {} -// The route-independent half of a context, shared so FileContext cannot -// drift from Context. +/** The route-independent half of a context, shared with FileContext. */ export interface ContextBase { readonly url: URL; readonly request: Request;