feat(nitro): automatically type middleware/api routes (#708)

This commit is contained in:
Daniel Roe 2021-10-11 14:29:02 +01:00 committed by GitHub
parent 18cf0ba865
commit b005b2403f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 84 additions and 29 deletions

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ dist
.nuxt* .nuxt*
.output .output
.gen .gen
nuxt.d.ts
# Junit reports # Junit reports
reports reports

View File

@ -123,6 +123,11 @@ export function setupNitroBridge () {
nitroDevContext.middleware.push(...middleware) nitroDevContext.middleware.push(...middleware)
}) })
// Add typed route responses
nuxt.hook('prepare:types', (opts) => {
opts.references.push({ path: resolve(nuxt.options.buildDir, 'nitro.d.ts') })
})
// nuxt build/dev // nuxt build/dev
// @ts-ignore // @ts-ignore
nuxt.options.build._minifyServer = false nuxt.options.build._minifyServer = false

View File

@ -1,4 +1,4 @@
import { resolve, join } from 'pathe' import { relative, resolve, join } from 'pathe'
import consola from 'consola' import consola from 'consola'
import { rollup, watch as rollupWatch } from 'rollup' import { rollup, watch as rollupWatch } from 'rollup'
import fse from 'fs-extra' import fse from 'fs-extra'
@ -55,9 +55,38 @@ export async function build (nitroContext: NitroContext) {
nitroContext.rollupConfig = getRollupConfig(nitroContext) nitroContext.rollupConfig = getRollupConfig(nitroContext)
await nitroContext._internal.hooks.callHook('nitro:rollup:before', nitroContext) await nitroContext._internal.hooks.callHook('nitro:rollup:before', nitroContext)
await writeTypes(nitroContext)
return nitroContext._nuxt.dev ? _watch(nitroContext) : _build(nitroContext) return nitroContext._nuxt.dev ? _watch(nitroContext) : _build(nitroContext)
} }
async function writeTypes (nitroContext: NitroContext) {
const routeTypes: Record<string, string[]> = {}
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<typeof import('${relativePath}').default>`)
}
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) { async function _build (nitroContext: NitroContext) {
nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir) nitroContext.scannedMiddleware = await scanMiddleware(nitroContext._nuxt.serverDir)
@ -117,6 +146,7 @@ async function _watch (nitroContext: NitroContext) {
nitroContext.scannedMiddleware = middleware nitroContext.scannedMiddleware = middleware
if (['add', 'addDir'].includes(event)) { if (['add', 'addDir'].includes(event)) {
watcher.close() watcher.close()
writeTypes(nitroContext).catch(console.error)
watcher = startRollupWatcher(nitroContext) watcher = startRollupWatcher(nitroContext)
} }
} }

41
packages/nitro/types/fetch.d.ts vendored Normal file
View File

@ -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> = C extends Record<any, any> ? C[keyof C] : never
export declare type MatchedRoutes<Route extends string> = ValueOf<{
// exact match, prefix match or root middleware
[key in keyof InternalApi]: Route extends key | `${key}/${string}` | '/' ? key : never
}>
export declare type MiddlewareOf<Route extends string> = Exclude<InternalApi[MatchedRoutes<Route>], Error | void>
export declare type TypedInternalResponse<Route, Default> =
Default extends string | boolean | number | null | void | object
// Allow user overrides
? Default
: Route extends string
? MiddlewareOf<Route> extends never
// Bail if only types are Error or void (for example, from middleware)
? Default
: MiddlewareOf<Route>
: Default
export declare interface $Fetch {
<T = unknown, R extends FetchRequest = FetchRequest> (request: R, opts?: FetchOptions): Promise<TypedInternalResponse<R, T>>
raw<T = unknown, R extends FetchRequest = FetchRequest> (request: R, opts?: FetchOptions): Promise<FetchResponse<TypedInternalResponse<R, T>>>
}
declare global {
// eslint-disable-next-line no-var
var $fetch: $Fetch
namespace NodeJS {
interface Global {
$fetch: $Fetch
}
}
}
export default {}

View File

@ -7,4 +7,5 @@ declare module '@nuxt/kit' {
} }
} }
export * from './fetch'
export * from '../dist' export * from '../dist'

View File

@ -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' { declare module '#storage' {
import type { Storage } from 'unstorage' import type { Storage } from 'unstorage'
export const storage: Storage export const storage: Storage
@ -21,5 +9,3 @@ declare module '#assets' {
export function statAsset(id: string): Promise<AssetMeta> export function statAsset(id: string): Promise<AssetMeta>
export function getKeys() : Promise<string[]> export function getKeys() : Promise<string[]>
} }
export default {}

View File

@ -1,15 +1,8 @@
import { Component } from '@vue/runtime-core' import { Component } from '@vue/runtime-core'
import { $Fetch } from 'ohmyfetch'
import { NuxtApp } from '../nuxt' import { NuxtApp } from '../nuxt'
declare global { declare global {
// eslint-disable-next-line no-var
var $fetch: $Fetch
namespace NodeJS { namespace NodeJS {
interface Global {
$fetch: $Fetch
}
interface Process { interface Process {
browser: boolean browser: boolean
client: boolean client: boolean

View File

@ -26,6 +26,11 @@ export function initNitro (nuxt: Nuxt) {
nuxt.hook('close', () => nitroDevContext._internal.hooks.callHook('close')) nuxt.hook('close', () => nitroDevContext._internal.hooks.callHook('close'))
nitroDevContext._internal.hooks.hook('nitro:document', template => nuxt.callHook('nitro:document', template)) nitroDevContext._internal.hooks.hook('nitro:document', template => nuxt.callHook('nitro:document', template))
// Add typed route responses
nuxt.hook('prepare:types', (opts) => {
opts.references.push({ path: resolve(nuxt.options.buildDir, 'nitro.d.ts') })
})
// Add nitro client plugin (to inject $fetch helper) // Add nitro client plugin (to inject $fetch helper)
nuxt.hook('app:resolve', (app) => { nuxt.hook('app:resolve', (app) => {
app.plugins.push({ src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.mjs') }) app.plugins.push({ src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.mjs') })

View File

@ -1,7 +0,0 @@
// This file is auto generated by `nuxt prepare`
// Please do not manually modify this file.
/// <reference types="nuxt3" />
/// <reference path=".nuxt/components.d.ts" />
/// <reference path=".nuxt/auto-imports.d.ts" />
export {}