diff --git a/.gitignore b/.gitignore index c5ae6bb95e..e459b9980c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ dist .nuxt* .output .gen +nuxt.d.ts # Junit reports reports diff --git a/packages/bridge/src/nitro.ts b/packages/bridge/src/nitro.ts index a018ea2f2f..1848ff839a 100644 --- a/packages/bridge/src/nitro.ts +++ b/packages/bridge/src/nitro.ts @@ -123,6 +123,11 @@ export function setupNitroBridge () { 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 // @ts-ignore nuxt.options.build._minifyServer = false diff --git a/packages/nitro/src/build.ts b/packages/nitro/src/build.ts index f4dc5aba4e..b02a4542fb 100644 --- a/packages/nitro/src/build.ts +++ b/packages/nitro/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/packages/nitro/types/fetch.d.ts b/packages/nitro/types/fetch.d.ts new file mode 100644 index 0000000000..b530117b09 --- /dev/null +++ b/packages/nitro/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/packages/nitro/types/index.d.ts b/packages/nitro/types/index.d.ts index 30743bebb4..e7e8da3a9e 100644 --- a/packages/nitro/types/index.d.ts +++ b/packages/nitro/types/index.d.ts @@ -7,4 +7,5 @@ declare module '@nuxt/kit' { } } +export * from './fetch' export * from '../dist' diff --git a/packages/nitro/types/shims.d.ts b/packages/nitro/types/shims.d.ts index baff617ec2..b385af6cf0 100644 --- a/packages/nitro/types/shims.d.ts +++ b/packages/nitro/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 {} diff --git a/packages/nuxt3/src/app/types/shims.d.ts b/packages/nuxt3/src/app/types/shims.d.ts index 88170bff26..9c5b1cf4ad 100644 --- a/packages/nuxt3/src/app/types/shims.d.ts +++ b/packages/nuxt3/src/app/types/shims.d.ts @@ -1,15 +1,8 @@ import { Component } from '@vue/runtime-core' -import { $Fetch } from 'ohmyfetch' import { NuxtApp } from '../nuxt' declare global { - // eslint-disable-next-line no-var - var $fetch: $Fetch - namespace NodeJS { - interface Global { - $fetch: $Fetch - } interface Process { browser: boolean client: boolean diff --git a/packages/nuxt3/src/core/nitro.ts b/packages/nuxt3/src/core/nitro.ts index 2b87a53b21..21057dae65 100644 --- a/packages/nuxt3/src/core/nitro.ts +++ b/packages/nuxt3/src/core/nitro.ts @@ -26,6 +26,11 @@ export function initNitro (nuxt: Nuxt) { nuxt.hook('close', () => nitroDevContext._internal.hooks.callHook('close')) 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) nuxt.hook('app:resolve', (app) => { app.plugins.push({ src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.mjs') }) diff --git a/playground/nuxt.d.ts b/playground/nuxt.d.ts deleted file mode 100644 index a4e90216c2..0000000000 --- a/playground/nuxt.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file is auto generated by `nuxt prepare` -// Please do not manually modify this file. - -/// -/// -/// -export {}