From 5fb99e26071245d2aa1bdeda953468ab7d931a47 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 4 Aug 2023 14:40:46 +0000 Subject: [PATCH 01/21] feat: generate `InternalFetch` signature --- src/build.ts | 41 +++++++++++++++++++++++++++++++++++++++++ src/types/fetch.ts | 4 ++++ 2 files changed, 45 insertions(+) diff --git a/src/build.ts b/src/build.ts index ab5ece3ba4..cdbca35060 100644 --- a/src/build.ts +++ b/src/build.ts @@ -83,10 +83,18 @@ export async function writeTypes(nitro: Nitro) { Partial> > = {}; + const fetchSignatures = { + exact: [] as string[], + dynamic: [] as string[], + globs: [] as string[], + } + const eventHandlerImports = new Set() + const typesDir = resolve(nitro.options.buildDir, "types"); const middleware = [...nitro.scannedHandlers, ...nitro.options.handlers]; + let i = 1 for (const mw of middleware) { if (typeof mw.handler !== "string" || !mw.route) { continue; @@ -100,6 +108,28 @@ export async function writeTypes(nitro: Nitro) { routeTypes[mw.route] = {}; } + const eventHandlerType = `EventHandler${i++}` + eventHandlerImports.add(`type ${eventHandlerType} = typeof import('${relativePath}').default`) + + // const isOptionsOptional + const isMethodOptional = !mw.method || mw.method.toUpperCase() === 'GET' + const excludedMethods = middleware.filter(other => other.method && other.route === mw.route && other !== mw).flatMap(m => [m.method.toUpperCase(), m.method.toLowerCase()].map(m => `'${m}'`)).join(' | ') + + const methodType = mw.method ? mw.method.toUpperCase() === 'PATCH' ? 'PATCH' : [mw.method.toUpperCase(), mw.method.toLowerCase()].map(m => `'${m}'`).join(' | ') : (excludedMethods ? `Exclude` : 'DefaultMethod') + + // TODO: 1. Fine-tune matching algorithm? + // TODO: 2. infer returns when we provide typed input + // TODO: 3. require options object when we provide typed input + + const group = mw.route.includes('**:') ? 'globs' : mw.route.includes(':') ? 'dynamic' : 'exact' + const routeType = mw.route.includes(':') + ? `\`${mw.route.replace(/(\*\*)?:[^/]+/g, '${string}')}\`` + : `'${mw.route}'` + + fetchSignatures[group].push( + ` (url: ${routeType}, options?: BaseFetchOptions & { method${isMethodOptional ? '?' : ''}: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : {})): ${eventHandlerType} extends EventHandler ? Promise>>> : Promise + `) + const method = mw.method || "default"; if (!routeTypes[mw.route][method]) { routeTypes[mw.route][method] = []; @@ -135,6 +165,11 @@ export async function writeTypes(nitro: Nitro) { const routes = [ "// Generated by nitro", "import type { Serialize, Simplify } from 'nitropack'", + "import type { EventHandler, HTTPMethod } from 'h3'", + "import type { FetchOptions } from 'ofetch'", + "type DefaultMethod = HTTPMethod | Lowercase>", + "type BaseFetchOptions = Omit", + ...eventHandlerImports, "declare module 'nitropack' {", " type Awaited = T extends PromiseLike ? Awaited : T", " interface InternalApi {", @@ -148,6 +183,12 @@ export async function writeTypes(nitro: Nitro) { ].join("\n") ), " }", + " interface InternalFetch {", + ...fetchSignatures.exact, + ...fetchSignatures.dynamic, + ...fetchSignatures.globs, + " }", + "", "}", // Makes this a module for augmentation purposes "export {}", diff --git a/src/types/fetch.ts b/src/types/fetch.ts index d28d5cf7e3..975dc8665d 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -6,6 +6,10 @@ import type { MatchedRoutes } from "./utils"; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface InternalApi {} +export interface InternalFetch { + (request: Exclude | (string & {}), opts?: FetchOptions): unknown +} + export type NitroFetchRequest = | Exclude | Exclude From 44e000a33fcab1826fab962597c995f39a4021b4 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 4 Aug 2023 15:50:23 +0000 Subject: [PATCH 02/21] feat: generate `InternalFetch` signatures --- src/build.ts | 32 ++++++++++++++++---------------- src/runtime/app.ts | 2 +- src/runtime/client.ts | 2 +- src/types/fetch.ts | 30 ++++-------------------------- src/types/h3.ts | 2 +- test/fixture/types.ts | 12 +++--------- 6 files changed, 26 insertions(+), 54 deletions(-) diff --git a/src/build.ts b/src/build.ts index cdbca35060..6b5ca65dd4 100644 --- a/src/build.ts +++ b/src/build.ts @@ -77,17 +77,15 @@ export async function build(nitro: Nitro) { : _build(nitro, rollupConfig); } +const DYNAMIC_PARAM_RE = /(\*\*)(?!:)|((\*\*)?:[^/]+)/g + export async function writeTypes(nitro: Nitro) { const routeTypes: Record< string, Partial> > = {}; - const fetchSignatures = { - exact: [] as string[], - dynamic: [] as string[], - globs: [] as string[], - } + const fetchSignatures = [] as Array<[route: string, type: string]> const eventHandlerImports = new Set() const typesDir = resolve(nitro.options.buildDir, "types"); @@ -99,7 +97,7 @@ export async function writeTypes(nitro: Nitro) { if (typeof mw.handler !== "string" || !mw.route) { continue; } - const relativePath = relative(typesDir, mw.handler).replace( + const relativePath = relative(typesDir, resolveAlias(mw.handler, nitro.options.alias)).replace( /\.[a-z]+$/, "" ); @@ -118,17 +116,19 @@ export async function writeTypes(nitro: Nitro) { const methodType = mw.method ? mw.method.toUpperCase() === 'PATCH' ? 'PATCH' : [mw.method.toUpperCase(), mw.method.toLowerCase()].map(m => `'${m}'`).join(' | ') : (excludedMethods ? `Exclude` : 'DefaultMethod') // TODO: 1. Fine-tune matching algorithm? + // TODO: merge return types // TODO: 2. infer returns when we provide typed input // TODO: 3. require options object when we provide typed input - const group = mw.route.includes('**:') ? 'globs' : mw.route.includes(':') ? 'dynamic' : 'exact' - const routeType = mw.route.includes(':') - ? `\`${mw.route.replace(/(\*\*)?:[^/]+/g, '${string}')}\`` - : `'${mw.route}'` + const routeType = DYNAMIC_PARAM_RE.test(mw.route) + ? `\`${mw.route.replace(DYNAMIC_PARAM_RE, '${string}')}\`` + : `'${mw.route}' | \`${mw.route}?$\{string}\`` - fetchSignatures[group].push( - ` (url: ${routeType}, options?: BaseFetchOptions & { method${isMethodOptional ? '?' : ''}: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : {})): ${eventHandlerType} extends EventHandler ? Promise>>> : Promise - `) + fetchSignatures.push([ + mw.route, + ` ? Simplify>> : unknown>(url: ${routeType}, options?: BaseFetchOptions & { method${isMethodOptional ? '?' : ''}: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : {})): Promise + ` + ]) const method = mw.method || "default"; if (!routeTypes[mw.route][method]) { @@ -184,9 +184,9 @@ export async function writeTypes(nitro: Nitro) { ), " }", " interface InternalFetch {", - ...fetchSignatures.exact, - ...fetchSignatures.dynamic, - ...fetchSignatures.globs, + ...fetchSignatures.sort(([a], [b]) => { + return b.replace(DYNAMIC_PARAM_RE, '____').localeCompare(a.replace(DYNAMIC_PARAM_RE, '____')) + }).map(([route, type]) => type), " }", "", "}", diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 1574749f58..417c7bee16 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -110,7 +110,7 @@ function createNitroApp(): NitroApp { event.$fetch = ((req, init) => fetchWithEvent(event, req, init as RequestInit, { fetch: $fetch, - })) as $Fetch; + })) as $Fetch; // https://github.com/unjs/nitro/issues/1420 event.waitUntil = (promise) => { diff --git a/src/runtime/client.ts b/src/runtime/client.ts index 849dc854e0..34ea1b9d46 100644 --- a/src/runtime/client.ts +++ b/src/runtime/client.ts @@ -3,5 +3,5 @@ import { $fetch } from "ofetch"; import { $Fetch, NitroFetchRequest } from "../types"; if (!globalThis.$fetch) { - globalThis.$fetch = $fetch as $Fetch; + globalThis.$fetch = $fetch as $Fetch; } diff --git a/src/types/fetch.ts b/src/types/fetch.ts index 975dc8665d..275415b57b 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -7,7 +7,7 @@ import type { MatchedRoutes } from "./utils"; export interface InternalApi {} export interface InternalFetch { - (request: Exclude | (string & {}), opts?: FetchOptions): unknown + (request: Exclude | (string & {}), opts?: FetchOptions): Promise } export type NitroFetchRequest = @@ -72,31 +72,9 @@ export type ExtractedRouteMethod< ? Lowercase : "get"; -export interface $Fetch< - DefaultT = unknown, - DefaultR extends NitroFetchRequest = NitroFetchRequest, -> { - < - T = DefaultT, - R extends NitroFetchRequest = DefaultR, - O extends NitroFetchOptions = NitroFetchOptions, - >( - request: R, - opts?: O - ): Promise>>; - raw< - T = DefaultT, - R extends NitroFetchRequest = DefaultR, - O extends NitroFetchOptions = NitroFetchOptions, - >( - request: R, - opts?: O - ): Promise< - FetchResponse>> - >; - create( - defaults: FetchOptions - ): $Fetch; +export interface $Fetch extends InternalFetch { + raw: InternalFetch; + create(defaults: FetchOptions): InternalFetch; } declare global { diff --git a/src/types/h3.ts b/src/types/h3.ts index 115226aae4..66000014d5 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -6,7 +6,7 @@ export type H3EventFetch = ( init?: RequestInit ) => Promise; -export type H3Event$Fetch = $Fetch; +export type H3Event$Fetch = $Fetch; declare module "h3" { interface H3Event { diff --git a/test/fixture/types.ts b/test/fixture/types.ts index ff6d4a09ca..e8acbe34a1 100644 --- a/test/fixture/types.ts +++ b/test/fixture/types.ts @@ -98,16 +98,12 @@ describe("API routes", () => { }); it("generates types for routes matching prefix", () => { - expectTypeOf($fetch("/api/hey/**")).toEqualTypeOf>(); expectTypeOf($fetch("/api/param/{id}/**")).toEqualTypeOf>(); expectTypeOf( $fetch("/api/typed/user/{someUserId}/post/{somePostId}/**") ).toEqualTypeOf< Promise<{ internalApiKey: "/api/typed/user/:userId/post/:postId" }> >(); - expectTypeOf($fetch("/api/typed/user/john/post/coffee/**")).toEqualTypeOf< - Promise<{ internalApiKey: "/api/typed/user/john/post/coffee" }> - >(); expectTypeOf( $fetch(`/api/typed/user/${dynamicString}/post/${dynamicString}/**`) ).toEqualTypeOf< @@ -144,16 +140,14 @@ describe("API routes", () => { $fetch("/api/typed/todos/firstTodo/comments/foo") ).toEqualTypeOf< Promise< - | { internalApiKey: "/api/typed/todos/**" } - | { internalApiKey: "/api/typed/todos/:todoId/comments/**:commentId" } + { internalApiKey: "/api/typed/todos/:todoId/comments/**:commentId" } > >(); expectTypeOf( $fetch(`/api/typed/todos/firstTodo/comments/${dynamicString}`) ).toEqualTypeOf< Promise< - | { internalApiKey: "/api/typed/todos/**" } - | { internalApiKey: "/api/typed/todos/:todoId/comments/**:commentId" } + { internalApiKey: "/api/typed/todos/:todoId/comments/**:commentId" } > >(); expectTypeOf( @@ -212,7 +206,7 @@ describe("API routes", () => { >(); expectTypeOf($fetch("/api/serialized/void")).toEqualTypeOf< - Promise + Promise >(); expectTypeOf($fetch("/api/serialized/null")).toEqualTypeOf>(); From f3c5c7705d3524b5a41b02449ad36c30df2b7fdb Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 4 Aug 2023 15:58:13 +0000 Subject: [PATCH 03/21] style: lint --- src/build.ts | 67 +++++++++++++++++++++++++++++++--------------- src/types/fetch.ts | 6 ++++- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/build.ts b/src/build.ts index 6b5ca65dd4..b0538c272e 100644 --- a/src/build.ts +++ b/src/build.ts @@ -77,7 +77,7 @@ export async function build(nitro: Nitro) { : _build(nitro, rollupConfig); } -const DYNAMIC_PARAM_RE = /(\*\*)(?!:)|((\*\*)?:[^/]+)/g +const DYNAMIC_PARAM_RE = /(\*\*)(?!:)|((\*\*)?:[^/]+)/g; export async function writeTypes(nitro: Nitro) { const routeTypes: Record< @@ -85,35 +85,52 @@ export async function writeTypes(nitro: Nitro) { Partial> > = {}; - const fetchSignatures = [] as Array<[route: string, type: string]> - const eventHandlerImports = new Set() + const fetchSignatures = [] as Array<[route: string, type: string]>; + const eventHandlerImports = new Set(); const typesDir = resolve(nitro.options.buildDir, "types"); const middleware = [...nitro.scannedHandlers, ...nitro.options.handlers]; - let i = 1 + let i = 1; for (const mw of middleware) { if (typeof mw.handler !== "string" || !mw.route) { continue; } - const relativePath = relative(typesDir, resolveAlias(mw.handler, nitro.options.alias)).replace( - /\.[a-z]+$/, - "" - ); + const relativePath = relative( + typesDir, + resolveAlias(mw.handler, nitro.options.alias) + ).replace(/\.[a-z]+$/, ""); if (!routeTypes[mw.route]) { routeTypes[mw.route] = {}; } - const eventHandlerType = `EventHandler${i++}` - eventHandlerImports.add(`type ${eventHandlerType} = typeof import('${relativePath}').default`) + const eventHandlerType = `EventHandler${i++}`; + eventHandlerImports.add( + `type ${eventHandlerType} = typeof import('${relativePath}').default` + ); // const isOptionsOptional - const isMethodOptional = !mw.method || mw.method.toUpperCase() === 'GET' - const excludedMethods = middleware.filter(other => other.method && other.route === mw.route && other !== mw).flatMap(m => [m.method.toUpperCase(), m.method.toLowerCase()].map(m => `'${m}'`)).join(' | ') - - const methodType = mw.method ? mw.method.toUpperCase() === 'PATCH' ? 'PATCH' : [mw.method.toUpperCase(), mw.method.toLowerCase()].map(m => `'${m}'`).join(' | ') : (excludedMethods ? `Exclude` : 'DefaultMethod') + const isMethodOptional = !mw.method || mw.method.toUpperCase() === "GET"; + const excludedMethods = middleware + .filter( + (other) => other.method && other.route === mw.route && other !== mw + ) + .flatMap((m) => + [m.method.toUpperCase(), m.method.toLowerCase()].map((m) => `'${m}'`) + ) + .join(" | "); + + const methodType = mw.method + ? mw.method.toUpperCase() === "PATCH" + ? "PATCH" + : [mw.method.toUpperCase(), mw.method.toLowerCase()] + .map((m) => `'${m}'`) + .join(" | ") + : excludedMethods + ? `Exclude` + : "DefaultMethod"; // TODO: 1. Fine-tune matching algorithm? // TODO: merge return types @@ -121,14 +138,16 @@ export async function writeTypes(nitro: Nitro) { // TODO: 3. require options object when we provide typed input const routeType = DYNAMIC_PARAM_RE.test(mw.route) - ? `\`${mw.route.replace(DYNAMIC_PARAM_RE, '${string}')}\`` - : `'${mw.route}' | \`${mw.route}?$\{string}\`` + ? `\`${mw.route.replace(DYNAMIC_PARAM_RE, `\${string}`)}\`` + : `'${mw.route}' | \`${mw.route}?$\{string}\``; fetchSignatures.push([ mw.route, - ` ? Simplify>> : unknown>(url: ${routeType}, options?: BaseFetchOptions & { method${isMethodOptional ? '?' : ''}: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : {})): Promise - ` - ]) + ` ? Simplify>> : unknown>(url: ${routeType}, options?: BaseFetchOptions & { method${ + isMethodOptional ? "?" : "" + }: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : {})): Promise + `, + ]); const method = mw.method || "default"; if (!routeTypes[mw.route][method]) { @@ -184,9 +203,13 @@ export async function writeTypes(nitro: Nitro) { ), " }", " interface InternalFetch {", - ...fetchSignatures.sort(([a], [b]) => { - return b.replace(DYNAMIC_PARAM_RE, '____').localeCompare(a.replace(DYNAMIC_PARAM_RE, '____')) - }).map(([route, type]) => type), + ...fetchSignatures + .sort(([a], [b]) => { + return b + .replace(DYNAMIC_PARAM_RE, "____") + .localeCompare(a.replace(DYNAMIC_PARAM_RE, "____")); + }) + .map(([route, type]) => type), " }", "", "}", diff --git a/src/types/fetch.ts b/src/types/fetch.ts index 275415b57b..42c40e5940 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -7,7 +7,11 @@ import type { MatchedRoutes } from "./utils"; export interface InternalApi {} export interface InternalFetch { - (request: Exclude | (string & {}), opts?: FetchOptions): Promise + // eslint-disable-next-line @typescript-eslint/ban-types + ( + request: Exclude | (string & {}), + opts?: FetchOptions + ): Promise; } export type NitroFetchRequest = From b63b567887d33ff3da51f1d2c7090d0bb85f8bd9 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 4 Aug 2023 17:12:00 +0100 Subject: [PATCH 04/21] style: lint more --- src/build.ts | 15 ++++++++------- src/types/fetch.ts | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/build.ts b/src/build.ts index b0538c272e..8dc36f5720 100644 --- a/src/build.ts +++ b/src/build.ts @@ -122,16 +122,17 @@ export async function writeTypes(nitro: Nitro) { ) .join(" | "); - const methodType = mw.method - ? mw.method.toUpperCase() === "PATCH" - ? "PATCH" - : [mw.method.toUpperCase(), mw.method.toLowerCase()] - .map((m) => `'${m}'`) - .join(" | ") - : excludedMethods + const defaultMethod = excludedMethods ? `Exclude` : "DefaultMethod"; + const methodType = mw.method + ? [mw.method.toUpperCase(), mw.method.toLowerCase()] + .filter((m) => m !== "patch") + .map((m) => `'${m}'`) + .join(" | ") + : defaultMethod; + // TODO: 1. Fine-tune matching algorithm? // TODO: merge return types // TODO: 2. infer returns when we provide typed input diff --git a/src/types/fetch.ts b/src/types/fetch.ts index 42c40e5940..6a47ecf329 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -7,8 +7,8 @@ import type { MatchedRoutes } from "./utils"; export interface InternalApi {} export interface InternalFetch { - // eslint-disable-next-line @typescript-eslint/ban-types ( + // eslint-disable-next-line @typescript-eslint/ban-types request: Exclude | (string & {}), opts?: FetchOptions ): Promise; From 7d58869c16fc2e37fe5d1d5e38b13b9e3da25889 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 7 Aug 2023 10:56:47 +0100 Subject: [PATCH 05/21] fix: require options when using non-default method --- src/build.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/build.ts b/src/build.ts index fcf9a4e7fa..cd39d01bc5 100644 --- a/src/build.ts +++ b/src/build.ts @@ -149,9 +149,11 @@ export async function writeTypes(nitro: Nitro) { fetchSignatures.push([ mw.route, - ` ? Simplify>> : unknown>(url: ${routeType}, options?: BaseFetchOptions & { method${ + ` ? Simplify>> : unknown>(url: ${routeType}, options${ isMethodOptional ? "?" : "" - }: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : {})): Promise + }: BaseFetchOptions & { method${ + isMethodOptional ? "?" : "" + }: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : EventHandlerRequest)): Promise `, ]); @@ -220,7 +222,7 @@ export async function writeTypes(nitro: Nitro) { const routes = [ "// Generated by nitro", "import type { Serialize, Simplify } from 'nitropack'", - "import type { EventHandler, HTTPMethod } from 'h3'", + "import type { EventHandler, EventHandlerRequest, HTTPMethod } from 'h3'", "import type { FetchOptions } from 'ofetch'", "type DefaultMethod = HTTPMethod | Lowercase>", "type BaseFetchOptions = Omit", From 1f87d093f79aca604221a142fdaf4554e72a4e2c Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 7 Aug 2023 15:33:16 +0000 Subject: [PATCH 06/21] fix: restore generic defaults for `$Fetch` --- src/types/fetch.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types/fetch.ts b/src/types/fetch.ts index 6a47ecf329..cc4841bbe4 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -6,10 +6,10 @@ import type { MatchedRoutes } from "./utils"; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface InternalApi {} -export interface InternalFetch { - ( +export interface InternalFetch | (string & {})> { + ( // eslint-disable-next-line @typescript-eslint/ban-types - request: Exclude | (string & {}), + request: R, opts?: FetchOptions ): Promise; } @@ -76,9 +76,9 @@ export type ExtractedRouteMethod< ? Lowercase : "get"; -export interface $Fetch extends InternalFetch { - raw: InternalFetch; - create(defaults: FetchOptions): InternalFetch; +export interface $Fetch | (string & {})> extends InternalFetch { + raw: InternalFetch; + create(defaults: FetchOptions): InternalFetch; } declare global { From b4d2b00500e3794997bd748756a08bdc766f97c4 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 7 Aug 2023 15:41:15 +0000 Subject: [PATCH 07/21] fix: request should extend url/requestinfo --- src/types/fetch.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/types/fetch.ts b/src/types/fetch.ts index cc4841bbe4..1aa9291f09 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -6,9 +6,8 @@ import type { MatchedRoutes } from "./utils"; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface InternalApi {} -export interface InternalFetch | (string & {})> { - ( - // eslint-disable-next-line @typescript-eslint/ban-types +export interface InternalFetch | RequestInfo | URL> { + ( request: R, opts?: FetchOptions ): Promise; @@ -17,8 +16,8 @@ export interface InternalFetch | Exclude - // eslint-disable-next-line @typescript-eslint/ban-types - | (string & {}); + | RequestInfo + | URL; export type MiddlewareOf< Route extends string, @@ -76,7 +75,7 @@ export type ExtractedRouteMethod< ? Lowercase : "get"; -export interface $Fetch | (string & {})> extends InternalFetch { +export interface $Fetch | RequestInfo | URL> extends InternalFetch { raw: InternalFetch; create(defaults: FetchOptions): InternalFetch; } From 4fd5be5edac5e862f38a3df648421ff99876dea4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 15:42:30 +0000 Subject: [PATCH 08/21] chore: apply automated lint fixes --- src/types/fetch.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/types/fetch.ts b/src/types/fetch.ts index 1aa9291f09..8aee6fca61 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -6,7 +6,13 @@ import type { MatchedRoutes } from "./utils"; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface InternalApi {} -export interface InternalFetch | RequestInfo | URL> { +export interface InternalFetch< + DefaultResponse = unknown, + DefaultFetchRequest extends RequestInfo | URL = + | Exclude + | RequestInfo + | URL, +> { ( request: R, opts?: FetchOptions @@ -75,7 +81,13 @@ export type ExtractedRouteMethod< ? Lowercase : "get"; -export interface $Fetch | RequestInfo | URL> extends InternalFetch { +export interface $Fetch< + DefaultResponse = unknown, + DefaultFetchRequest extends RequestInfo | URL = + | Exclude + | RequestInfo + | URL, +> extends InternalFetch { raw: InternalFetch; create(defaults: FetchOptions): InternalFetch; } From 46751d7cd510dcca65b6b091924d40def2b5f39a Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 7 Aug 2023 15:46:33 +0000 Subject: [PATCH 09/21] fix: revert changes to generics --- src/runtime/app.ts | 2 +- src/runtime/client.ts | 2 +- src/types/h3.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/runtime/app.ts b/src/runtime/app.ts index 8b6ecf307d..9348a48d5b 100644 --- a/src/runtime/app.ts +++ b/src/runtime/app.ts @@ -130,7 +130,7 @@ function createNitroApp(): NitroApp { event.$fetch = ((req, init) => fetchWithEvent(event, req, init as RequestInit, { fetch: $fetch, - })) as $Fetch; + })) as $Fetch; // https://github.com/unjs/nitro/issues/1420 event.waitUntil = (promise) => { diff --git a/src/runtime/client.ts b/src/runtime/client.ts index 34ea1b9d46..849dc854e0 100644 --- a/src/runtime/client.ts +++ b/src/runtime/client.ts @@ -3,5 +3,5 @@ import { $fetch } from "ofetch"; import { $Fetch, NitroFetchRequest } from "../types"; if (!globalThis.$fetch) { - globalThis.$fetch = $fetch as $Fetch; + globalThis.$fetch = $fetch as $Fetch; } diff --git a/src/types/h3.ts b/src/types/h3.ts index 66000014d5..115226aae4 100644 --- a/src/types/h3.ts +++ b/src/types/h3.ts @@ -6,7 +6,7 @@ export type H3EventFetch = ( init?: RequestInit ) => Promise; -export type H3Event$Fetch = $Fetch; +export type H3Event$Fetch = $Fetch; declare module "h3" { interface H3Event { From e2f769ad741919c557034feaeb1486a2f0835e7b Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 7 Aug 2023 22:26:16 +0100 Subject: [PATCH 10/21] test: add failing tests --- test/fixture/routes/typed-routes.ts | 1 + test/fixture/types.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 test/fixture/routes/typed-routes.ts diff --git a/test/fixture/routes/typed-routes.ts b/test/fixture/routes/typed-routes.ts new file mode 100644 index 0000000000..ef48a305b3 --- /dev/null +++ b/test/fixture/routes/typed-routes.ts @@ -0,0 +1 @@ +export default eventHandler<{ query: { id: string } }, string>(() => 'foo'); diff --git a/test/fixture/types.ts b/test/fixture/types.ts index e8acbe34a1..464ffd940d 100644 --- a/test/fixture/types.ts +++ b/test/fixture/types.ts @@ -13,6 +13,19 @@ describe("API routes", () => { // eslint-disable-next-line @typescript-eslint/no-inferrable-types const dynamicString: string = ""; + it("requires correct options for typed routes", () => { + // @ts-expect-error should be a POST request + $fetch("/api/upload"); + // @ts-expect-error `query.id` is required + $fetch("/typed-routes"); + // @ts-expect-error `query.id` is required + $fetch("/typed-routes", {}); + // @ts-expect-error `query.id` should be a string + $fetch("/typed-routes", { query: { id: 42 } }); + + expectTypeOf($fetch("/typed-routes", { query: { id: 'string' } })).toEqualTypeOf>(); + }); + it("generates types for middleware, unknown and manual typed routes", () => { expectTypeOf($fetch("/")).toEqualTypeOf>(); // middleware expectTypeOf($fetch("/api/unknown")).toEqualTypeOf>(); From c50183667f19f6089f86e929d28d82faf48215a4 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 8 Aug 2023 17:13:18 +0100 Subject: [PATCH 11/21] fix: refine extends and skip private routes --- src/build.ts | 6 +++++- src/types/fetch.ts | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/build.ts b/src/build.ts index cd39d01bc5..4d142b6178 100644 --- a/src/build.ts +++ b/src/build.ts @@ -99,7 +99,11 @@ export async function writeTypes(nitro: Nitro) { let i = 1; for (const mw of middleware) { - if (typeof mw.handler !== "string" || !mw.route) { + if ( + typeof mw.handler !== "string" || + !mw.route || + /^(\/_|\/api\/_)/.test(mw.route) + ) { continue; } const relativePath = relative( diff --git a/src/types/fetch.ts b/src/types/fetch.ts index 8aee6fca61..1f41413981 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -8,21 +8,23 @@ export interface InternalApi {} export interface InternalFetch< DefaultResponse = unknown, - DefaultFetchRequest extends RequestInfo | URL = + DefaultFetchRequest extends string | Request | URL = | Exclude - | RequestInfo | URL, > { - ( - request: R, - opts?: FetchOptions + ( + url: unknown extends T + ? R + : + | R + // eslint-disable-next-line @typescript-eslint/ban-types + | (string & {}) ): Promise; } export type NitroFetchRequest = - | Exclude + | keyof InternalApi | Exclude - | RequestInfo | URL; export type MiddlewareOf< @@ -83,9 +85,8 @@ export type ExtractedRouteMethod< export interface $Fetch< DefaultResponse = unknown, - DefaultFetchRequest extends RequestInfo | URL = + DefaultFetchRequest extends string | Request | URL = | Exclude - | RequestInfo | URL, > extends InternalFetch { raw: InternalFetch; From 8b124b45cfe4f657a51659ee6b518253d45b60e8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 8 Aug 2023 21:58:13 +0100 Subject: [PATCH 12/21] fix: disallow unmatched urls --- src/build.ts | 15 +++++---------- src/types/fetch.ts | 6 +++++- test/fixture/types.ts | 18 ++++++++++-------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/build.ts b/src/build.ts index 4d142b6178..3909bc4a55 100644 --- a/src/build.ts +++ b/src/build.ts @@ -142,22 +142,17 @@ export async function writeTypes(nitro: Nitro) { .join(" | ") : defaultMethod; - // TODO: 1. Fine-tune matching algorithm? - // TODO: merge return types - // TODO: 2. infer returns when we provide typed input - // TODO: 3. require options object when we provide typed input - const routeType = DYNAMIC_PARAM_RE.test(mw.route) ? `\`${mw.route.replace(DYNAMIC_PARAM_RE, `\${string}`)}\`` : `'${mw.route}' | \`${mw.route}?$\{string}\``; fetchSignatures.push([ mw.route, - ` ? Simplify>> : unknown>(url: ${routeType}, options${ - isMethodOptional ? "?" : "" - }: BaseFetchOptions & { method${ - isMethodOptional ? "?" : "" - }: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : EventHandlerRequest)): Promise + ` ? Simplify>> : unknown>( + url: ${routeType}, + options${isMethodOptional ? "?" : ""}: + BaseFetchOptions & { method${isMethodOptional ? "?" : ""}: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : EventHandlerRequest) + ): Promise `, ]); diff --git a/src/types/fetch.ts b/src/types/fetch.ts index 1f41413981..2aec560f85 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -14,7 +14,11 @@ export interface InternalFetch< > { ( url: unknown extends T - ? R + ? R extends string + ? string extends keyof InternalApi[MatchedRoutes] + ? R + : never + : R : | R // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/test/fixture/types.ts b/test/fixture/types.ts index 464ffd940d..862c3adc24 100644 --- a/test/fixture/types.ts +++ b/test/fixture/types.ts @@ -13,21 +13,23 @@ describe("API routes", () => { // eslint-disable-next-line @typescript-eslint/no-inferrable-types const dynamicString: string = ""; - it("requires correct options for typed routes", () => { + it("requires correct options for typed routes", async () => { // @ts-expect-error should be a POST request - $fetch("/api/upload"); + await $fetch("/api/upload"); + // TODO: @ts-expect-error `query.id` is required + await $fetch("/typed-routes"); // @ts-expect-error `query.id` is required - $fetch("/typed-routes"); - // @ts-expect-error `query.id` is required - $fetch("/typed-routes", {}); + await $fetch("/typed-routes", {}); // @ts-expect-error `query.id` should be a string - $fetch("/typed-routes", { query: { id: 42 } }); + await $fetch("/typed-routes", { query: { id: 42 } }); expectTypeOf($fetch("/typed-routes", { query: { id: 'string' } })).toEqualTypeOf>(); }); - it("generates types for middleware, unknown and manual typed routes", () => { - expectTypeOf($fetch("/")).toEqualTypeOf>(); // middleware + it("generates types for unknown and manual typed routes", () => { + // @ts-expect-error No route matching this path exists + $fetch("/"); + $fetch("https://test.com/"); expectTypeOf($fetch("/api/unknown")).toEqualTypeOf>(); expectTypeOf($fetch("/test")).toEqualTypeOf< Promise From cf64788e1b7ecb674e207587b2827e577ae0a0a8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 9 Aug 2023 08:53:15 +0100 Subject: [PATCH 13/21] fix: correctly type `$fetch.{raw/create}` --- src/build.ts | 6 +++--- src/types/fetch.ts | 25 ++++++++++++++++++++++--- test/fixture/types.ts | 30 +++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/build.ts b/src/build.ts index 3909bc4a55..b160755e3e 100644 --- a/src/build.ts +++ b/src/build.ts @@ -152,7 +152,7 @@ export async function writeTypes(nitro: Nitro) { url: ${routeType}, options${isMethodOptional ? "?" : ""}: BaseFetchOptions & { method${isMethodOptional ? "?" : ""}: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : EventHandlerRequest) - ): Promise + ): true extends Raw ? Promise> : Promise `, ]); @@ -222,7 +222,7 @@ export async function writeTypes(nitro: Nitro) { "// Generated by nitro", "import type { Serialize, Simplify } from 'nitropack'", "import type { EventHandler, EventHandlerRequest, HTTPMethod } from 'h3'", - "import type { FetchOptions } from 'ofetch'", + "import type { FetchOptions, FetchResponse } from 'ofetch'", "type DefaultMethod = HTTPMethod | Lowercase>", "type BaseFetchOptions = Omit", ...eventHandlerImports, @@ -239,7 +239,7 @@ export async function writeTypes(nitro: Nitro) { ].join("\n") ), " }", - " interface InternalFetch {", + " interface InternalFetch {", ...fetchSignatures .sort(([a], [b]) => { return b diff --git a/src/types/fetch.ts b/src/types/fetch.ts index 2aec560f85..9a0b9a7f7b 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -11,6 +11,7 @@ export interface InternalFetch< DefaultFetchRequest extends string | Request | URL = | Exclude | URL, + Raw extends boolean = false, > { ( url: unknown extends T @@ -23,7 +24,19 @@ export interface InternalFetch< | R // eslint-disable-next-line @typescript-eslint/ban-types | (string & {}) - ): Promise; + ): true extends Raw ? Promise> : Promise; +} + +export interface ExternalFetch< + DefaultResponse = unknown, + DefaultFetchRequest extends string | Request | URL = + | FetchRequest + | URL, + Raw extends boolean = false, +> { + ( + url: R, + ): true extends Raw ? Promise> : Promise; } export type NitroFetchRequest = @@ -31,6 +44,7 @@ export type NitroFetchRequest = | Exclude | URL; +/** @deprecated */ export type MiddlewareOf< Route extends string, Method extends RouterMethod | "default", @@ -38,6 +52,7 @@ export type MiddlewareOf< ? Exclude][Method], Error | void> : never; +/** @deprecated */ export type TypedInternalResponse< Route, Default = unknown, @@ -56,6 +71,7 @@ export type TypedInternalResponse< // Extracts the available http methods based on the route. // Defaults to all methods if there aren't any methods available or if there is a catch-all route. +/** @deprecated */ export type AvailableRouterMethod = R extends string ? keyof InternalApi[MatchedRoutes] extends undefined @@ -70,6 +86,7 @@ export type AvailableRouterMethod = // Argumented fetch options to include the correct request methods. // This overrides the default, which is only narrowed to a string. +/** @deprecated */ export interface NitroFetchOptions< R extends NitroFetchRequest, M extends AvailableRouterMethod = AvailableRouterMethod, @@ -78,6 +95,7 @@ export interface NitroFetchOptions< } // Extract the route method from options which might be undefined or without a method parameter. +/** @deprecated */ export type ExtractedRouteMethod< R extends NitroFetchRequest, O extends NitroFetchOptions, @@ -93,8 +111,9 @@ export interface $Fetch< | Exclude | URL, > extends InternalFetch { - raw: InternalFetch; - create(defaults: FetchOptions): InternalFetch; + raw: InternalFetch; + create(defaults: Omit): InternalFetch; + create(defaults: FetchOptions): ExternalFetch; } declare global { diff --git a/test/fixture/types.ts b/test/fixture/types.ts index 862c3adc24..06f6d64417 100644 --- a/test/fixture/types.ts +++ b/test/fixture/types.ts @@ -1,6 +1,7 @@ import { expectTypeOf } from "expect-type"; import { describe, it } from "vitest"; -import { $Fetch } from "../.."; +import type { FetchResponse } from "ofetch" +import type { $Fetch } from "../.."; import { defineNitroConfig } from "../../src/config"; interface TestResponse { @@ -247,6 +248,33 @@ describe("API routes", () => { Promise<[string, string]> >(); }); + + it('types event.$fetch', () => { + const event = useEvent(); + expectTypeOf(event.$fetch("/api/serialized/tuple")).toEqualTypeOf< + Promise<[string, string]> + >(); + }) + + it('produces correct $fetch.raw', async () => { + const r = await $fetch.raw("/api/serialized/tuple") + expectTypeOf($fetch.raw("/api/serialized/tuple")).toEqualTypeOf< + Promise> + >(); + }) + + it('produces correctly typed new instance with $fetch.create', () => { + const newBase = $fetch.create({ + baseURL: 'https://test.com' + }) + expectTypeOf(newBase("/api/serialized/tuple")).toEqualTypeOf>(); + const sameBase = $fetch.create({ + headers: { Authorization: 'Bearer 123' } + }) + expectTypeOf(sameBase("/api/serialized/tuple")).toEqualTypeOf< + Promise<[string, string]> + >(); + }) }); describe("defineNitroConfig", () => { From 7b3ac3053cf4cf4a4f4284820d442fca1e3ca3bb Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 9 Aug 2023 08:55:41 +0100 Subject: [PATCH 14/21] test: update test with TODO comments for unions --- test/fixture/types.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/test/fixture/types.ts b/test/fixture/types.ts index 06f6d64417..7dbdaf5556 100644 --- a/test/fixture/types.ts +++ b/test/fixture/types.ts @@ -81,7 +81,8 @@ describe("API routes", () => { >(); expectTypeOf($fetch(`/api/typed/user/${dynamicString}`)).toEqualTypeOf< Promise< - | { internalApiKey: "/api/typed/user/john" } + // TODO: reenable deep merging of return types + // | { internalApiKey: "/api/typed/user/john" } | { internalApiKey: "/api/typed/user/:userId" } > >(); @@ -89,7 +90,8 @@ describe("API routes", () => { $fetch(`/api/typed/user/john/post/${dynamicString}`) ).toEqualTypeOf< Promise< - | { internalApiKey: "/api/typed/user/john/post/coffee" } + // TODO: reenable deep merging of return types + // | { internalApiKey: "/api/typed/user/john/post/coffee" } | { internalApiKey: "/api/typed/user/john/post/:postId" } > >(); @@ -98,17 +100,19 @@ describe("API routes", () => { ).toEqualTypeOf< Promise< | { internalApiKey: "/api/typed/user/:userId/post/:postId" } - | { internalApiKey: "/api/typed/user/:userId/post/firstPost" } + // TODO: reenable deep merging of return types + // | { internalApiKey: "/api/typed/user/:userId/post/firstPost" } > >(); expectTypeOf( $fetch(`/api/typed/user/${dynamicString}/post/${dynamicString}`) ).toEqualTypeOf< Promise< - | { internalApiKey: "/api/typed/user/john/post/coffee" } - | { internalApiKey: "/api/typed/user/john/post/:postId" } + // TODO: reenable deep merging of return types + // | { internalApiKey: "/api/typed/user/john/post/coffee" } + // | { internalApiKey: "/api/typed/user/john/post/:postId" } | { internalApiKey: "/api/typed/user/:userId/post/:postId" } - | { internalApiKey: "/api/typed/user/:userId/post/firstPost" } + // | { internalApiKey: "/api/typed/user/:userId/post/firstPost" } > >(); }); @@ -124,10 +128,11 @@ describe("API routes", () => { $fetch(`/api/typed/user/${dynamicString}/post/${dynamicString}/**`) ).toEqualTypeOf< Promise< - | { internalApiKey: "/api/typed/user/john/post/coffee" } - | { internalApiKey: "/api/typed/user/john/post/:postId" } + // TODO: reenable deep merging of return types + // | { internalApiKey: "/api/typed/user/john/post/coffee" } + // | { internalApiKey: "/api/typed/user/john/post/:postId" } | { internalApiKey: "/api/typed/user/:userId/post/:postId" } - | { internalApiKey: "/api/typed/user/:userId/post/firstPost" } + // | { internalApiKey: "/api/typed/user/:userId/post/firstPost" } > >(); }); @@ -171,7 +176,8 @@ describe("API routes", () => { ).toEqualTypeOf< Promise< | { internalApiKey: "/api/typed/todos/**" } - | { internalApiKey: "/api/typed/todos/:todoId/comments/**:commentId" } + // TODO: reenable deep merging of return types + // | { internalApiKey: "/api/typed/todos/:todoId/comments/**:commentId" } > >(); expectTypeOf( @@ -179,7 +185,8 @@ describe("API routes", () => { ).toEqualTypeOf< Promise< | { internalApiKey: "/api/typed/catchall/:slug/**:another" } - | { internalApiKey: "/api/typed/catchall/some/**:test" } + // TODO: reenable deep merging of return types + // | { internalApiKey: "/api/typed/catchall/some/**:test" } > >(); expectTypeOf($fetch("/api/typed/catchall/some/foo/bar/baz")).toEqualTypeOf< From 3a9ba1878ec43e5a5f51b900c170bfef256598d8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 07:56:47 +0000 Subject: [PATCH 15/21] chore: apply automated lint fixes --- src/build.ts | 4 +++- src/types/fetch.ts | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/build.ts b/src/build.ts index b160755e3e..0e3e51e14d 100644 --- a/src/build.ts +++ b/src/build.ts @@ -151,7 +151,9 @@ export async function writeTypes(nitro: Nitro) { ` ? Simplify>> : unknown>( url: ${routeType}, options${isMethodOptional ? "?" : ""}: - BaseFetchOptions & { method${isMethodOptional ? "?" : ""}: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : EventHandlerRequest) + BaseFetchOptions & { method${ + isMethodOptional ? "?" : "" + }: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : EventHandlerRequest) ): true extends Raw ? Promise> : Promise `, ]); diff --git a/src/types/fetch.ts b/src/types/fetch.ts index 9a0b9a7f7b..c391cb7880 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -29,13 +29,11 @@ export interface InternalFetch< export interface ExternalFetch< DefaultResponse = unknown, - DefaultFetchRequest extends string | Request | URL = - | FetchRequest - | URL, + DefaultFetchRequest extends string | Request | URL = FetchRequest | URL, Raw extends boolean = false, > { ( - url: R, + url: R ): true extends Raw ? Promise> : Promise; } @@ -112,7 +110,9 @@ export interface $Fetch< | URL, > extends InternalFetch { raw: InternalFetch; - create(defaults: Omit): InternalFetch; + create( + defaults: Omit + ): InternalFetch; create(defaults: FetchOptions): ExternalFetch; } From 357e1e499d762b5d5ff3914fad77fb83f89eb08d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 9 Aug 2023 09:16:20 +0100 Subject: [PATCH 16/21] fix: add back string types for `NitroFetchRequest` --- src/types/fetch.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/fetch.ts b/src/types/fetch.ts index c391cb7880..d45d09f607 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -40,6 +40,8 @@ export interface ExternalFetch< export type NitroFetchRequest = | keyof InternalApi | Exclude + // eslint-disable-next-line @typescript-eslint/ban-types + | (string & {}) | URL; /** @deprecated */ From 729030746b246529a0a353b93cf809f04d10abce Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 9 Aug 2023 09:23:01 +0100 Subject: [PATCH 17/21] test: add additional case --- test/fixture/types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/fixture/types.ts b/test/fixture/types.ts index 7dbdaf5556..2d287f303c 100644 --- a/test/fixture/types.ts +++ b/test/fixture/types.ts @@ -263,11 +263,13 @@ describe("API routes", () => { >(); }) - it('produces correct $fetch.raw', async () => { - const r = await $fetch.raw("/api/serialized/tuple") + it('produces correct $fetch.raw', () => { expectTypeOf($fetch.raw("/api/serialized/tuple")).toEqualTypeOf< Promise> >(); + expectTypeOf($fetch.raw("/unknown")).toEqualTypeOf< + Promise> + >(); }) it('produces correctly typed new instance with $fetch.create', () => { From 17ba1b39e7c460a1eb53c628618acc44a1e690a8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 9 Aug 2023 15:30:57 +0100 Subject: [PATCH 18/21] fix: reuse type --- src/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build.ts b/src/build.ts index 0e3e51e14d..5420fc4d7a 100644 --- a/src/build.ts +++ b/src/build.ts @@ -163,7 +163,7 @@ export async function writeTypes(nitro: Nitro) { routeTypes[mw.route][method] = []; } routeTypes[mw.route][method].push( - `Simplify>>>` + `Simplify>>>` ); } From fd7d1fcfa5fba6d1e4630b8cae1cee7fcd0a0938 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 10 Aug 2023 15:12:09 +0000 Subject: [PATCH 19/21] fix: improve type safety with optional options param --- src/build.ts | 45 +++++++++++++++++++++++++++++++++++-------- src/types/fetch.ts | 19 ++++++++---------- test/fixture/types.ts | 10 +++++----- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/build.ts b/src/build.ts index 5420fc4d7a..65a42af572 100644 --- a/src/build.ts +++ b/src/build.ts @@ -119,6 +119,15 @@ export async function writeTypes(nitro: Nitro) { eventHandlerImports.add( `type ${eventHandlerType} = typeof import('${relativePath}').default` ); + eventHandlerImports.add( + `type ${eventHandlerType}Output = ${eventHandlerType} extends EventHandler ? Simplify>> : unknown` + ) + eventHandlerImports.add( + `type ${eventHandlerType}Input = ${eventHandlerType} extends EventHandler ? Input : EventHandlerRequest` + ) + + const Output = `${eventHandlerType}Output`; + const Input = `${eventHandlerType}Input`; // const isOptionsOptional const isMethodOptional = !mw.method || mw.method.toUpperCase() === "GET"; @@ -148,20 +157,26 @@ export async function writeTypes(nitro: Nitro) { fetchSignatures.push([ mw.route, - ` ? Simplify>> : unknown>( + ` ( url: ${routeType}, - options${isMethodOptional ? "?" : ""}: - BaseFetchOptions & { method${ + options: BaseFetchOptions & { method${ isMethodOptional ? "?" : "" - }: ${methodType} } & (${eventHandlerType} extends EventHandler ? Input : EventHandlerRequest) + }: ${methodType} } & ${Input} ): true extends Raw ? Promise> : Promise - `, + ${isMethodOptional +? ` + ( + url: IsOptional<${Input}> extends true ? ${routeType} : never, + options?: BaseFetchOptions & { method${ + isMethodOptional ? "?" : "" + }: ${methodType} } & ${Input} + ): true extends Raw ? Promise> : Promise + ` +: ''}` ]); const method = mw.method || "default"; - if (!routeTypes[mw.route][method]) { - routeTypes[mw.route][method] = []; - } + routeTypes[mw.route][method] ||= []; routeTypes[mw.route][method].push( `Simplify>>>` ); @@ -226,6 +241,20 @@ export async function writeTypes(nitro: Nitro) { "import type { EventHandler, EventHandlerRequest, HTTPMethod } from 'h3'", "import type { FetchOptions, FetchResponse } from 'ofetch'", "type DefaultMethod = HTTPMethod | Lowercase>", + `type IsOptional> = + 'body' extends keyof T + ? 'query' extends keyof T + ? undefined extends T['body'] & T['query'] + ? true + : false + : T['body'] extends undefined + ? true + : false + : 'query' extends keyof T + ? T['query'] extends undefined + ? true + : false + : true`, "type BaseFetchOptions = Omit", ...eventHandlerImports, "declare module 'nitropack' {", diff --git a/src/types/fetch.ts b/src/types/fetch.ts index d45d09f607..ee2af8cd56 100644 --- a/src/types/fetch.ts +++ b/src/types/fetch.ts @@ -13,17 +13,14 @@ export interface InternalFetch< | URL, Raw extends boolean = false, > { - ( - url: unknown extends T - ? R extends string - ? string extends keyof InternalApi[MatchedRoutes] - ? R - : never - : R - : - | R - // eslint-disable-next-line @typescript-eslint/ban-types - | (string & {}) + ( + url: MatchedRoutes extends never ? R : never, + options?: FetchOptions + ): true extends Raw ? Promise> : Promise; + + ( + url: R, + options?: FetchOptions ): true extends Raw ? Promise> : Promise; } diff --git a/test/fixture/types.ts b/test/fixture/types.ts index 2d287f303c..957df15778 100644 --- a/test/fixture/types.ts +++ b/test/fixture/types.ts @@ -17,7 +17,7 @@ describe("API routes", () => { it("requires correct options for typed routes", async () => { // @ts-expect-error should be a POST request await $fetch("/api/upload"); - // TODO: @ts-expect-error `query.id` is required + // @ts-expect-error `query.id` is required await $fetch("/typed-routes"); // @ts-expect-error `query.id` is required await $fetch("/typed-routes", {}); @@ -28,9 +28,9 @@ describe("API routes", () => { }); it("generates types for unknown and manual typed routes", () => { - // @ts-expect-error No route matching this path exists - $fetch("/"); - $fetch("https://test.com/"); + // @ts-expect-error this is wrongly detected as a matched route + $fetch("/") + expectTypeOf($fetch("https://test.com/")).toEqualTypeOf>(); expectTypeOf($fetch("/api/unknown")).toEqualTypeOf>(); expectTypeOf($fetch("/test")).toEqualTypeOf< Promise @@ -82,7 +82,7 @@ describe("API routes", () => { expectTypeOf($fetch(`/api/typed/user/${dynamicString}`)).toEqualTypeOf< Promise< // TODO: reenable deep merging of return types - // | { internalApiKey: "/api/typed/user/john" } + // | { internalApiKey: "/api/typed/user/john" } | { internalApiKey: "/api/typed/user/:userId" } > >(); From 854a5e3ce940e3bc0537c3ace74dad188eb705df Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 10 Aug 2023 15:37:31 +0000 Subject: [PATCH 20/21] fix: improve error message when no route is matched --- src/build.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/build.ts b/src/build.ts index 65a42af572..5ece4a4aa6 100644 --- a/src/build.ts +++ b/src/build.ts @@ -237,7 +237,7 @@ export async function writeTypes(nitro: Nitro) { const routes = [ "// Generated by nitro", - "import type { Serialize, Simplify } from 'nitropack'", + "import type { MatchedRoutes, Serialize, Simplify } from 'nitropack'", "import type { EventHandler, EventHandlerRequest, HTTPMethod } from 'h3'", "import type { FetchOptions, FetchResponse } from 'ofetch'", "type DefaultMethod = HTTPMethod | Lowercase>", @@ -278,6 +278,9 @@ export async function writeTypes(nitro: Nitro) { .localeCompare(a.replace(DYNAMIC_PARAM_RE, "____")); }) .map(([route, type]) => type), + ` + (message: 'Could not match any route because of missing or incorrect method, query or body options for that route.', _?: any): never + `, " }", "", "}", From 2cc871fe1904fdcbcdc5abc787eacebf7e6850ef Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 15:39:24 +0000 Subject: [PATCH 21/21] chore: apply automated lint fixes --- src/build.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/build.ts b/src/build.ts index 5ece4a4aa6..e3bd405ad8 100644 --- a/src/build.ts +++ b/src/build.ts @@ -121,10 +121,10 @@ export async function writeTypes(nitro: Nitro) { ); eventHandlerImports.add( `type ${eventHandlerType}Output = ${eventHandlerType} extends EventHandler ? Simplify>> : unknown` - ) + ); eventHandlerImports.add( `type ${eventHandlerType}Input = ${eventHandlerType} extends EventHandler ? Input : EventHandlerRequest` - ) + ); const Output = `${eventHandlerType}Output`; const Input = `${eventHandlerType}Input`; @@ -160,19 +160,21 @@ export async function writeTypes(nitro: Nitro) { ` ( url: ${routeType}, options: BaseFetchOptions & { method${ - isMethodOptional ? "?" : "" - }: ${methodType} } & ${Input} + isMethodOptional ? "?" : "" + }: ${methodType} } & ${Input} ): true extends Raw ? Promise> : Promise - ${isMethodOptional -? ` + ${ + isMethodOptional + ? ` ( url: IsOptional<${Input}> extends true ? ${routeType} : never, options?: BaseFetchOptions & { method${ - isMethodOptional ? "?" : "" - }: ${methodType} } & ${Input} + isMethodOptional ? "?" : "" + }: ${methodType} } & ${Input} ): true extends Raw ? Promise> : Promise ` -: ''}` + : "" + }`, ]); const method = mw.method || "default";