From 3163fa217ed16e672c12d4dde3304a683fddb7cb Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 11 Oct 2021 14:29:02 +0100 Subject: [PATCH] feat(nitro): automatically type middleware/api routes (#708) --- src/build.ts | 32 +++++++++++++++++++++++++++++++- src/types/fetch.d.ts | 41 +++++++++++++++++++++++++++++++++++++++++ src/types/index.d.ts | 1 + src/types/shims.d.ts | 14 -------------- 4 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 src/types/fetch.d.ts diff --git a/src/build.ts b/src/build.ts index f4dc5aba4e..b02a4542fb 100644 --- a/src/build.ts +++ b/src/build.ts @@ -1,4 +1,4 @@ -import { resolve, join } from 'pathe' +import { relative, resolve, join } from 'pathe' import consola from 'consola' import { rollup, watch as rollupWatch } from 'rollup' import fse from 'fs-extra' @@ -55,9 +55,38 @@ export async function build (nitroContext: NitroContext) { nitroContext.rollupConfig = getRollupConfig(nitroContext) await nitroContext._internal.hooks.callHook('nitro:rollup:before', nitroContext) + await writeTypes(nitroContext) return nitroContext._nuxt.dev ? _watch(nitroContext) : _build(nitroContext) } +async function writeTypes (nitroContext: NitroContext) { + const routeTypes: Record = {} + + const middleware = [ + ...nitroContext.scannedMiddleware, + ...nitroContext.middleware + ] + + for (const mw of middleware) { + if (typeof mw.handle !== 'string') { continue } + const relativePath = relative(nitroContext._nuxt.buildDir, mw.handle).replace(/\.[a-z]+$/, '') + routeTypes[mw.route] = routeTypes[mw.route] || [] + routeTypes[mw.route].push(`ReturnType`) + } + + const lines = [ + 'declare module \'@nuxt/nitro\' {', + ' interface InternalApi {', + ...Object.entries(routeTypes).map(([path, types]) => ` '${path}': ${types.join(' | ')}`), + ' }', + '}', + // Makes this a module for augmentation purposes + 'export {}' + ] + + await writeFile(join(nitroContext._nuxt.buildDir, 'nitro.d.ts'), lines.join('\n')) +} + async function _build (nitroContext: NitroContext) { nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir) @@ -117,6 +146,7 @@ async function _watch (nitroContext: NitroContext) { nitroContext.scannedMiddleware = middleware if (['add', 'addDir'].includes(event)) { watcher.close() + writeTypes(nitroContext).catch(console.error) watcher = startRollupWatcher(nitroContext) } } diff --git a/src/types/fetch.d.ts b/src/types/fetch.d.ts new file mode 100644 index 0000000000..b530117b09 --- /dev/null +++ b/src/types/fetch.d.ts @@ -0,0 +1,41 @@ +import type { FetchRequest, FetchOptions, FetchResponse } from 'ohmyfetch' + +// An interface to extend in a local project +export declare interface InternalApi { } + +export declare type ValueOf = C extends Record ? C[keyof C] : never + +export declare type MatchedRoutes = ValueOf<{ + // exact match, prefix match or root middleware + [key in keyof InternalApi]: Route extends key | `${key}/${string}` | '/' ? key : never +}> + +export declare type MiddlewareOf = Exclude], Error | void> + +export declare type TypedInternalResponse = + Default extends string | boolean | number | null | void | object + // Allow user overrides + ? Default + : Route extends string + ? MiddlewareOf extends never + // Bail if only types are Error or void (for example, from middleware) + ? Default + : MiddlewareOf + : Default + +export declare interface $Fetch { + (request: R, opts?: FetchOptions): Promise> + raw (request: R, opts?: FetchOptions): Promise>> +} + +declare global { + // eslint-disable-next-line no-var + var $fetch: $Fetch + namespace NodeJS { + interface Global { + $fetch: $Fetch + } + } +} + +export default {} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 30743bebb4..e7e8da3a9e 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -7,4 +7,5 @@ declare module '@nuxt/kit' { } } +export * from './fetch' export * from '../dist' diff --git a/src/types/shims.d.ts b/src/types/shims.d.ts index baff617ec2..b385af6cf0 100644 --- a/src/types/shims.d.ts +++ b/src/types/shims.d.ts @@ -1,15 +1,3 @@ -declare global { - import type { $Fetch } from 'ohmyfetch' - - // eslint-disable-next-line no-var - var $fetch: $Fetch - namespace NodeJS { - interface Global { - $fetch: $Fetch - } - } -} - declare module '#storage' { import type { Storage } from 'unstorage' export const storage: Storage @@ -21,5 +9,3 @@ declare module '#assets' { export function statAsset(id: string): Promise export function getKeys() : Promise } - -export default {}