feat(nuxt): upgrade nitro + reduce node-specific usage (#22515)

Co-authored-by: Heb <xsh4k3@gmail.com>
This commit is contained in:
pooya parsa 2023-08-23 09:30:53 +02:00 committed by GitHub
parent 7b35a1fe4f
commit a2f2a4748e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 367 additions and 364 deletions

View File

@ -14,7 +14,7 @@ Nuxt automatically scans files inside these directories to register API and serv
Each file should export a default function defined with `defineEventHandler()` or `eventHandler()` (alias).
The handler can directly return JSON data, a `Promise` or use `event.node.res.end()` to send a response.
The handler can directly return JSON data, a `Promise`, or use `event.node.res.end()` to send a response.
**Example:** Create the `/api/hello` route with `server/api/hello.ts` file:

View File

@ -12,7 +12,7 @@ Within your pages, components, and plugins you can use `useRequestEvent` to acce
const event = useRequestEvent()
// Get the URL
const url = event.node.req.url
const url = event.path
```
::alert{icon=👉}

View File

@ -64,7 +64,7 @@
"happy-dom": "10.10.4",
"jiti": "1.19.3",
"markdownlint-cli": "^0.33.0",
"nitropack": "2.5.2",
"nitropack": "2.6.0",
"nuxi": "workspace:*",
"nuxt": "workspace:*",
"nuxt-vitest": "0.10.2",

View File

@ -44,7 +44,7 @@
"@types/lodash-es": "4.17.8",
"@types/semver": "7.5.0",
"lodash-es": "4.17.21",
"nitropack": "2.5.2",
"nitropack": "2.6.0",
"unbuild": "latest",
"vite": "4.4.9",
"vitest": "0.33.0",

View File

@ -41,7 +41,7 @@
"listhen": "1.3.0",
"mlly": "1.4.0",
"mri": "1.2.0",
"nitropack": "2.5.2",
"nitropack": "2.6.0",
"ohash": "1.1.3",
"pathe": "1.1.1",
"perfect-debounce": "1.0.0",

View File

@ -1,6 +1,6 @@
import { promises as fsp } from 'node:fs'
import { join, resolve } from 'pathe'
import { createApp, eventHandler, lazyEventHandler, toNodeListener } from 'h3'
import { createApp, eventHandler, lazyEventHandler, send, toNodeListener } from 'h3'
import { listen } from 'listhen'
import type { NuxtAnalyzeMeta } from '@nuxt/schema'
import { defu } from 'defu'
@ -77,7 +77,7 @@ export default defineNuxtCommand({
const serveFile = (filePath: string) => lazyEventHandler(async () => {
const contents = await fsp.readFile(filePath, 'utf-8')
return eventHandler((event) => { event.node.res.end(contents) })
return eventHandler(event => send(event, contents))
})
console.info('Starting stats server...')

View File

@ -81,7 +81,7 @@
"knitwork": "^1.0.0",
"magic-string": "^0.30.2",
"mlly": "^1.4.0",
"nitropack": "^2.5.2",
"nitropack": "^2.6.0",
"nuxi": "workspace:../nuxi",
"nypm": "^0.3.0",
"ofetch": "^1.1.1",

View File

@ -2,7 +2,7 @@ import type { Ref } from 'vue'
import { getCurrentInstance, nextTick, onUnmounted, ref, toRaw, watch } from 'vue'
import type { CookieParseOptions, CookieSerializeOptions } from 'cookie-es'
import { parse, serialize } from 'cookie-es'
import { deleteCookie, getCookie, setCookie } from 'h3'
import { deleteCookie, getCookie, getRequestHeader, setCookie } from 'h3'
import type { H3Event } from 'h3'
import destr from 'destr'
import { isEqual } from 'ohash'
@ -80,7 +80,7 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
function readRawCookies (opts: CookieOptions = {}): Record<string, string> | undefined {
if (import.meta.server) {
return parse(useRequestEvent()?.node.req.headers.cookie || '', opts)
return parse(getRequestHeader(useRequestEvent(), 'cookie') || '', opts)
} else if (import.meta.client) {
return parse(document.cookie, opts)
}

View File

@ -1,5 +1,5 @@
import type { H3Event } from 'h3'
import { setResponseStatus as _setResponseStatus } from 'h3'
import { setResponseStatus as _setResponseStatus, getRequestHeaders } from 'h3'
import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt'
@ -7,7 +7,8 @@ export function useRequestHeaders<K extends string = string> (include: K[]): { [
export function useRequestHeaders (): Readonly<Record<string, string>>
export function useRequestHeaders (include?: any[]) {
if (import.meta.client) { return {} }
const headers = useNuxtApp().ssrContext?.event.node.req.headers ?? {}
const event = useNuxtApp().ssrContext?.event
const headers = event ? getRequestHeaders(event) : {}
if (!include) { return headers }
return Object.fromEntries(include.map(key => key.toLowerCase()).filter(key => headers[key]).map(key => [key, headers[key]]))
}

View File

@ -35,7 +35,6 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
console.warn(`[nuxt] Could not load custom \`spaLoadingTemplate\` path as it does not exist: \`${spaLoadingTemplate}\`.`)
}
// @ts-expect-error `typescriptBundlerResolution` coming in next nitro version
const nitroConfig: NitroConfig = defu(_nitroConfig, {
debug: nuxt.options.debug,
rootDir: nuxt.options.rootDir,
@ -44,9 +43,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
dev: nuxt.options.dev,
buildDir: nuxt.options.buildDir,
experimental: {
// @ts-expect-error `typescriptBundlerResolution` coming in next nitro version
typescriptBundlerResolution: nuxt.options.experimental.typescriptBundlerResolution || nuxt.options.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler' || _nitroConfig.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler',
asyncContext: nuxt.options.experimental.asyncContext
asyncContext: nuxt.options.experimental.asyncContext,
typescriptBundlerResolution: nuxt.options.experimental.typescriptBundlerResolution || nuxt.options.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler' || _nitroConfig.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler'
},
imports: {
autoImport: nuxt.options.imports.autoImport as boolean,

View File

@ -1,7 +1,7 @@
import { joinURL, withQuery } from 'ufo'
import type { NitroErrorHandler } from 'nitropack'
import type { H3Error } from 'h3'
import { getRequestHeaders, setResponseHeader, setResponseStatus } from 'h3'
import { getRequestHeaders, send, setResponseHeader, setResponseStatus } from 'h3'
import { useNitroApp, useRuntimeConfig } from '#internal/nitro'
import { isJsonRequest, normalizeError } from '#internal/nitro/utils'
@ -11,7 +11,7 @@ export default <NitroErrorHandler> async function errorhandler (error: H3Error,
// Create an error object
const errorObject = {
url: event.node.req.url,
url: event.path,
statusCode,
statusMessage,
message,
@ -41,12 +41,11 @@ export default <NitroErrorHandler> async function errorhandler (error: H3Error,
// JSON response
if (isJsonRequest(event)) {
setResponseHeader(event, 'Content-Type', 'application/json')
event.node.res.end(JSON.stringify(errorObject))
return
return send(event, JSON.stringify(errorObject))
}
// HTML response (via SSR)
const isErrorPage = event.node.req.url?.startsWith('/__nuxt_error')
const isErrorPage = event.path.startsWith('/__nuxt_error')
const res = !isErrorPage
? await useNitroApp().localFetch(withQuery(joinURL(useRuntimeConfig().app.baseURL, '/__nuxt_error'), errorObject), {
headers: getRequestHeaders(event) as Record<string, string>,
@ -67,8 +66,7 @@ export default <NitroErrorHandler> async function errorhandler (error: H3Error,
}
if (event.handled) { return }
setResponseHeader(event, 'Content-Type', 'text/html;charset=UTF-8')
event.node.res.end(template(errorObject))
return
return send(event, template(errorObject))
}
const html = await res.text()
@ -79,5 +77,5 @@ export default <NitroErrorHandler> async function errorhandler (error: H3Error,
}
setResponseStatus(event, res.status && res.status !== 200 ? res.status : undefined, res.statusText)
event.node.res.end(html)
return send(event, html)
}

View File

@ -9,7 +9,7 @@ import {
import type { RenderResponse } from 'nitropack'
import type { Manifest } from 'vite'
import type { H3Event } from 'h3'
import { appendResponseHeader, createError, getQuery, readBody, writeEarlyHints } from 'h3'
import { appendResponseHeader, createError, getQuery, getResponseStatus, getResponseStatusText, readBody, writeEarlyHints } from 'h3'
import devalue from '@nuxt/devalue'
import { stringify, uneval } from 'devalue'
import destr from 'destr'
@ -170,16 +170,16 @@ const islandPropCache = import.meta.prerender ? useStorage('internal:nuxt:preren
async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
// TODO: Strict validation for url
let url = event.node.req.url || ''
if (import.meta.prerender && event.node.req.url && await islandPropCache!.hasItem(event.node.req.url)) {
let url = event.path || ''
if (import.meta.prerender && event.path && await islandPropCache!.hasItem(event.path)) {
// rehydrate props from cache so we can rerender island if cache does not have it any more
url = await islandPropCache!.getItem(event.node.req.url) as string
url = await islandPropCache!.getItem(event.path) as string
}
url = url.substring('/__nuxt_island'.length + 1) || ''
const [componentName, hashId] = url.split('?')[0].split('_')
// TODO: Validate context
const context = event.node.req.method === 'GET' ? getQuery(event) : await readBody(event)
const context = event.method === 'GET' ? getQuery(event) : await readBody(event)
const ctx: NuxtIslandContext = {
url: '/',
@ -202,7 +202,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
const nitroApp = useNitroApp()
// Whether we're rendering an error page
const ssrError = event.node.req.url?.startsWith('/__nuxt_error')
const ssrError = event.path.startsWith('/__nuxt_error')
? getQuery(event) as unknown as Exclude<NuxtPayload['error'], Error>
: null
@ -210,7 +210,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
ssrError.statusCode = parseInt(ssrError.statusCode as any)
}
if (ssrError && event.node.req.socket.readyState !== 'readOnly' /* direct request */) {
if (ssrError && !('__unenv__' in event.node.req) /* allow internal fetch */) {
throw createError({
statusCode: 404,
statusMessage: 'Page Not Found: /__nuxt_error'
@ -218,21 +218,23 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
}
// Check for island component rendering
const islandContext = (process.env.NUXT_COMPONENT_ISLANDS && event.node.req.url?.startsWith('/__nuxt_island'))
const islandContext = (process.env.NUXT_COMPONENT_ISLANDS && event.path.startsWith('/__nuxt_island'))
? await getIslandContext(event)
: undefined
if (import.meta.prerender && islandContext && event.node.req.url && await islandCache!.hasItem(event.node.req.url)) {
return islandCache!.getItem(event.node.req.url) as Promise<Partial<RenderResponse>>
if (import.meta.prerender && islandContext && event.path && await islandCache!.hasItem(event.path)) {
return islandCache!.getItem(event.path) as Promise<Partial<RenderResponse>>
}
// Request url
let url = ssrError?.url as string || islandContext?.url || event.node.req.url!
let url = ssrError?.url as string || islandContext?.url || event.path
// Whether we are rendering payload route
const isRenderingPayload = PAYLOAD_URL_RE.test(url) && !islandContext
if (isRenderingPayload) {
url = url.substring(0, url.lastIndexOf('/')) || '/'
event._path = url
event.node.req.url = url
if (import.meta.prerender && await payloadCache!.hasItem(url)) {
return payloadCache!.getItem(url) as Promise<Partial<RenderResponse>>
@ -435,8 +437,8 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
const response = {
body: JSON.stringify(islandResponse, null, 2),
statusCode: event.node.res.statusCode,
statusMessage: event.node.res.statusMessage,
statusCode: getResponseStatus(event),
statusMessage: getResponseStatusText(event),
headers: {
'content-type': 'application/json;charset=utf-8',
'x-powered-by': 'Nuxt'
@ -444,7 +446,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
} satisfies RenderResponse
if (import.meta.prerender) {
await islandCache!.setItem(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, response)
await islandPropCache!.setItem(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, event.node.req.url!)
await islandPropCache!.setItem(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, event.path)
}
return response
}
@ -452,8 +454,8 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Construct HTML response
const response = {
body: renderHTMLDocument(htmlContext),
statusCode: event.node.res.statusCode,
statusMessage: event.node.res.statusMessage,
statusCode: getResponseStatus(event),
statusMessage: getResponseStatusText(event),
headers: {
'content-type': 'text/html;charset=utf-8',
'x-powered-by': 'Nuxt'
@ -511,8 +513,8 @@ function renderPayloadResponse (ssrContext: NuxtSSRContext) {
body: process.env.NUXT_JSON_PAYLOADS
? stringify(splitPayload(ssrContext).payload, ssrContext._payloadReducers)
: `export default ${devalue(splitPayload(ssrContext).payload)}`,
statusCode: ssrContext.event.node.res.statusCode,
statusMessage: ssrContext.event.node.res.statusMessage,
statusCode: getResponseStatus(ssrContext.event),
statusMessage: getResponseStatusText(ssrContext.event),
headers: {
'content-type': process.env.NUXT_JSON_PAYLOADS ? 'application/json;charset=utf-8' : 'text/javascript;charset=utf-8',
'x-powered-by': 'Nuxt'

View File

@ -37,7 +37,7 @@
"esbuild-loader": "4.0.1",
"h3": "1.8.0",
"ignore": "5.2.4",
"nitropack": "2.5.2",
"nitropack": "2.6.0",
"unbuild": "latest",
"unctx": "2.3.1",
"vite": "4.4.9",

View File

@ -163,18 +163,17 @@ export async function buildClient (ctx: ViteBuildContext) {
})
const viteMiddleware = defineEventHandler(async (event) => {
// Workaround: vite devmiddleware modifies req.url
const originalURL = event.node.req.url!
const viteRoutes = viteServer.middlewares.stack.map(m => m.route).filter(r => r.length > 1)
if (!originalURL.startsWith(clientConfig.base!) && !viteRoutes.some(route => originalURL.startsWith(route))) {
if (!event.path.startsWith(clientConfig.base!) && !viteRoutes.some(route => event.path.startsWith(route))) {
// @ts-expect-error _skip_transform is a private property
event.node.req._skip_transform = true
}
// Workaround: vite devmiddleware modifies req.url
const _originalPath = event.node.req.url
await new Promise((resolve, reject) => {
viteServer.middlewares.handle(event.node.req, event.node.res, (err: Error) => {
event.node.req.url = originalURL
event.node.req.url = _originalPath
return err ? reject(err) : resolve(null)
})
})

View File

@ -141,7 +141,7 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
}
return eventHandler(async (event) => {
const moduleId = decodeURI(event.node.req.url!).substring(1)
const moduleId = decodeURI(event.path).substring(1)
if (moduleId === '/') {
throw createError({ statusCode: 400 })
}

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
for (const outputDir of ['.output', '.output-inline']) {
it('default client bundle size', async () => {
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"95.7k"')
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"96.4k"')
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
[
"_nuxt/entry.js",
@ -32,10 +32,10 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.6k"')
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"305k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2348k"')
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"1822k"')
const packages = modules.files
.filter(m => m.endsWith('package.json'))
@ -55,33 +55,12 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"@vue/runtime-dom",
"@vue/server-renderer",
"@vue/shared",
"cookie-es",
"debug",
"defu",
"destr",
"devalue",
"estree-walker",
"h3",
"has-flag",
"hookable",
"http-graceful-shutdown",
"iron-webcrypto",
"klona",
"ms",
"node-fetch-native",
"ofetch",
"ohash",
"pathe",
"radix3",
"scule",
"source-map-js",
"supports-color",
"ufo",
"uncrypto",
"unctx",
"unenv",
"unhead",
"unstorage",
"vue",
"vue-bundle-renderer",
]
@ -92,10 +71,10 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output-inline/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"370k"')
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"611k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"613k"')
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"70.9k"')
const packages = modules.files
.filter(m => m.endsWith('package.json'))
@ -106,31 +85,9 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"@unhead/dom",
"@unhead/shared",
"@unhead/ssr",
"cookie-es",
"debug",
"defu",
"destr",
"devalue",
"h3",
"has-flag",
"hookable",
"http-graceful-shutdown",
"iron-webcrypto",
"klona",
"ms",
"node-fetch-native",
"ofetch",
"ohash",
"pathe",
"radix3",
"scule",
"supports-color",
"ufo",
"uncrypto",
"unctx",
"unenv",
"unhead",
"unstorage",
]
`)
})