Merge branch 'main' into docs/kit

This commit is contained in:
Andrey Yolkin 2023-08-01 15:29:25 +03:00 committed by GitHub
commit 2350a1f75e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 744 additions and 541 deletions

View File

@ -10,7 +10,7 @@ body:
Please use a template below to create a minimal reproduction
👉 https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz
👉 https://codesandbox.io/p/github/nuxt/starter/v3-codesandbox
👉 https://codesandbox.io/s/github/nuxt/starter/v3-codesandbox
- type: textarea
id: bug-env
attributes:

View File

@ -10,7 +10,7 @@ body:
Please use a template below to create a minimal reproduction
👉 https://stackblitz.com/github/nuxt/starter/tree/v2
👉 https://codesandbox.io/p/github/nuxt/starter/v2
👉 https://codesandbox.io/s/github/nuxt/starter/v2
- type: textarea
id: bug-env
attributes:

View File

@ -31,7 +31,7 @@ Start with one of our starters and themes directly by opening [nuxt.new](https:/
::details
:summary[Additional notes for an optimal setup:]
- **Node.js**: Make sure to use an even numbered version (18, 20, etc)
- **Nuxtr**: Install the community-developed [Nuxtr extension](https://marketplace.visualstudio.com/items?itemName=Nuxtr.nuxtr-vscode)
- **Volar**: Either enable [**Take Over Mode**](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) (recommended) or add the [TypeScript Vue Plugin](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin)
If you have enabled **Take Over Mode** or installed the **TypeScript Vue Plugin (Volar)**, you can disable generating the shim for `*.vue` files in your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) file:

View File

@ -42,6 +42,7 @@ You may need to update the config below with a path to your web browser. For mor
"type": "node",
"request": "launch",
"name": "server: nuxt",
"outputCapture": "std",
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
"args": [
"dev"
@ -60,6 +61,12 @@ You may need to update the config below with a path to your web browser. For mor
}
```
If you prefer your usual browser extensions, add this to the _chrome_ configuration above:
```json5
"userDataDir": false,
```
### Example JetBrains IDEs Debug Configuration
You can also debug your Nuxt app in JetBrains IDEs such as IntelliJ IDEA, WebStorm, or PhpStorm.

View File

@ -33,16 +33,16 @@ If your issue concerns Vue 3 or Vite, please try to reproduce it first with the
**Nuxt 3**:
:button-link[Nuxt 3 on StackBlitz]{href="https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz" blank .mr-2}
:button-link[Nuxt 3 on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt/starter/v3-codesandbox" blank}
:button-link[Nuxt 3 on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt/starter/v3-codesandbox" blank}
**Nuxt Bridge**:
:button-link[Nuxt Bridge on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt/starter/v2-bridge-codesandbox" blank}
:button-link[Nuxt Bridge on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt/starter/v2-bridge-codesandbox" blank}
**Vue 3**:
:button-link[Vue 3 SSR on StackBlitz]{href="https://stackblitz.com/github/nuxt-contrib/vue3-ssr-starter/tree/main?terminal=dev" blank .mr-2}
:button-link[Vue 3 SSR on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt-contrib/vue3-ssr-starter/main" blank .mr-2}
:button-link[Vue 3 SSR on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt-contrib/vue3-ssr-starter/main" blank .mr-2}
:button-link[Vue 3 SSR Template]{href="https://github.com/nuxt-contrib/vue3-ssr-starter/generate" blank}
Once you've reproduced the issue, remove as much code from your reproduction as you can (while still recreating the bug). The time spent making the reproduction as minimal as possible will make a huge difference to whoever sets out to fix the issue.

View File

@ -84,7 +84,7 @@
"vue-router": "4.2.4",
"vue-tsc": "1.8.8"
},
"packageManager": "pnpm@8.6.10",
"packageManager": "pnpm@8.6.11",
"engines": {
"node": "^14.18.0 || >=16.10.0"
}

View File

@ -34,6 +34,7 @@
"pkg-types": "^1.0.3",
"scule": "^1.0.0",
"semver": "^7.5.4",
"ufo": "^1.2.0",
"unctx": "^2.3.1",
"unimport": "^3.1.0",
"untyped": "^1.4.0"

View File

@ -60,15 +60,19 @@ function getRequireCacheItem (id: string) {
}
}
export function getModulePaths (paths?: string[] | string) {
return ([] as Array<string | undefined>).concat(
global.__NUXT_PREPATHS__,
paths || [],
process.cwd(),
global.__NUXT_PATHS__
).filter(Boolean) as string[]
}
/** @deprecated Do not use CJS utils */
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
return normalize(_require.resolve(id, {
paths: ([] as Array<string | undefined>).concat(
global.__NUXT_PREPATHS__,
opts.paths || [],
process.cwd(),
global.__NUXT_PATHS__
).filter(Boolean) as string[]
paths: getModulePaths(opts.paths)
}))
}

View File

@ -1,8 +1,15 @@
import { existsSync } from 'node:fs'
import { basename, parse, resolve } from 'pathe'
import { existsSync, promises as fsp } from 'node:fs'
import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
import hash from 'hash-sum'
import type { NuxtTemplate, ResolvedNuxtTemplate } from '@nuxt/schema'
import type { Nuxt, NuxtTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema'
import { withTrailingSlash } from 'ufo'
import { defu } from 'defu'
import type { TSConfig } from 'pkg-types'
import { readPackageJSON } from 'pkg-types'
import { tryResolveModule } from './internal/esm'
import { tryUseNuxt, useNuxt } from './context'
import { getModulePaths } from './internal/cjs'
/**
* Renders given template using lodash template during build into the project buildDir
@ -101,3 +108,160 @@ export function normalizeTemplate (template: NuxtTemplate<any> | string): Resolv
export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate<any>) => boolean }) {
return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options)
}
export async function writeTypes (nuxt: Nuxt) {
const modulePaths = getModulePaths(nuxt.options.modulesDir)
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, {
compilerOptions: {
forceConsistentCasingInFileNames: true,
jsx: 'preserve',
target: 'ESNext',
module: 'ESNext',
moduleResolution: nuxt.options.experimental?.typescriptBundlerResolution ? 'Bundler' : 'Node',
skipLibCheck: true,
strict: nuxt.options.typescript?.strict ?? true,
allowJs: true,
noEmit: true,
resolveJsonModule: true,
allowSyntheticDefaultImports: true,
types: ['node'],
paths: {}
},
include: [
'./nuxt.d.ts',
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'),
...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [],
...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd)
.filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules'))
.map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')),
...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : []
],
exclude: [
...nuxt.options.modulesDir.map(m => relativeWithDot(nuxt.options.buildDir, m)),
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist'))
]
} satisfies TSConfig)
const aliases: Record<string, string> = {
...nuxt.options.alias,
'#build': nuxt.options.buildDir
}
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/]
const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
tsConfig.include = tsConfig.include || []
for (const alias in aliases) {
if (excludedAlias.some(re => re.test(alias))) {
continue
}
let absolutePath = resolve(basePath, aliases[alias])
let stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */)
if (!stats) {
const resolvedModule = await tryResolveModule(aliases[alias], nuxt.options.modulesDir)
if (resolvedModule) {
absolutePath = resolvedModule
stats = await fsp.stat(resolvedModule).catch(() => null)
}
}
const relativePath = relativeWithDot(nuxt.options.buildDir, absolutePath)
if (stats?.isDirectory()) {
tsConfig.compilerOptions.paths[alias] = [relativePath]
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${relativePath}/*`]
if (!absolutePath.startsWith(rootDirWithSlash)) {
tsConfig.include.push(relativePath)
}
} else {
const path = stats?.isFile()
// remove extension
? relativePath.replace(/(?<=\w)\.\w+$/g, '')
// non-existent file probably shouldn't be resolved
: aliases[alias]
tsConfig.compilerOptions.paths[alias] = [path]
if (!absolutePath.startsWith(rootDirWithSlash)) {
tsConfig.include.push(path)
}
}
}
const references: TSReference[] = await Promise.all([
...nuxt.options.modules,
...nuxt.options._modules
]
.filter(f => typeof f === 'string')
.map(async id => ({ types: (await readPackageJSON(id, { url: modulePaths }).catch(() => null))?.name || id })))
if (nuxt.options.experimental?.reactivityTransform) {
references.push({ types: 'vue/macros-global' })
}
const declarations: string[] = []
await nuxt.callHook('prepare:types', { references, declarations, tsConfig })
for (const alias in tsConfig.compilerOptions!.paths) {
const paths = tsConfig.compilerOptions!.paths[alias]
tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => {
if (!isAbsolute(path)) { return path }
const stats = await fsp.stat(path).catch(() => null /* file does not exist */)
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */ : path)
}))
}
tsConfig.include = [...new Set(tsConfig.include.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
tsConfig.exclude = [...new Set(tsConfig.exclude!.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
const declaration = [
...references.map((ref) => {
if ('path' in ref && isAbsolute(ref.path)) {
ref.path = relative(nuxt.options.buildDir, ref.path)
}
return `/// <reference ${renderAttrs(ref)} />`
}),
...declarations,
'',
'export {}',
''
].join('\n')
async function writeFile () {
const GeneratedBy = '// Generated by nuxi'
const tsConfigPath = resolve(nuxt.options.buildDir, 'tsconfig.json')
await fsp.mkdir(nuxt.options.buildDir, { recursive: true })
await fsp.writeFile(tsConfigPath, GeneratedBy + '\n' + JSON.stringify(tsConfig, null, 2))
const declarationPath = resolve(nuxt.options.buildDir, 'nuxt.d.ts')
await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration)
}
// This is needed for Nuxt 2 which clears the build directory again before building
// https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144
// @ts-expect-error TODO: Nuxt 2 hook
nuxt.hook('builder:prepared', writeFile)
await writeFile()
}
function renderAttrs (obj: Record<string, string>) {
return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ')
}
function renderAttr (key: string, value: string) {
return value ? `${key}="${value}"` : ''
}
function relativeWithDot (from: string, to: string) {
return relative(from, to).replace(/^([^.])/, './$1') || '.'
}

View File

@ -1,6 +1,8 @@
import { relative, resolve } from 'pathe'
import { consola } from 'consola'
import { writeTypes } from '../utils/prepare'
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
import { loadKit } from '../utils/kit'
import { clearBuildDir } from '../utils/fs'
import { overrideEnv } from '../utils/env'
@ -19,7 +21,7 @@ export default defineNuxtCommand({
const rootDir = resolve(args._[0] || '.')
showVersions(rootDir)
const { loadNuxt, buildNuxt, useNitro } = await loadKit(rootDir)
const { loadNuxt, buildNuxt, useNitro, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const nuxt = await loadNuxt({
rootDir,

View File

@ -7,8 +7,10 @@ import type { Nuxt } from '@nuxt/schema'
import { consola } from 'consola'
import { withTrailingSlash } from 'ufo'
import { setupDotenv } from 'c12'
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
import { showBanner, showVersions } from '../utils/banner'
import { writeTypes } from '../utils/prepare'
import { loadKit } from '../utils/kit'
import { importModule } from '../utils/esm'
import { overrideEnv } from '../utils/env'
@ -30,7 +32,7 @@ export default defineNuxtCommand({
await setupDotenv({ cwd: rootDir, fileName: args.dotenv })
const { loadNuxt, loadNuxtConfig, buildNuxt } = await loadKit(rootDir)
const { loadNuxt, loadNuxtConfig, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const config = await loadNuxtConfig({
cwd: rootDir,

View File

@ -1,8 +1,10 @@
import { relative, resolve } from 'pathe'
import { consola } from 'consola'
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
import { clearBuildDir } from '../utils/fs'
import { loadKit } from '../utils/kit'
import { writeTypes } from '../utils/prepare'
import { defineNuxtCommand } from './index'
export default defineNuxtCommand({
@ -15,7 +17,7 @@ export default defineNuxtCommand({
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
const rootDir = resolve(args._[0] || '.')
const { loadNuxt, buildNuxt } = await loadKit(rootDir)
const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const nuxt = await loadNuxt({
rootDir,
overrides: {

View File

@ -1,9 +1,10 @@
import { execa } from 'execa'
import { resolve } from 'pathe'
import { tryResolveModule } from '../utils/esm'
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
import { tryResolveModule } from '../utils/esm'
import { loadKit } from '../utils/kit'
import { writeTypes } from '../utils/prepare'
import { defineNuxtCommand } from './index'
export default defineNuxtCommand({
@ -16,7 +17,7 @@ export default defineNuxtCommand({
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
const rootDir = resolve(args._[0] || '.')
const { loadNuxt, buildNuxt } = await loadKit(rootDir)
const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const nuxt = await loadNuxt({
rootDir,
overrides: {

View File

@ -1,7 +1,7 @@
import { createRequire } from 'node:module'
import { dirname, normalize } from 'pathe'
import { normalize } from 'pathe'
export function getModulePaths (paths?: string | string[]): string[] {
function getModulePaths (paths?: string | string[]): string[] {
return ([] as Array<string | undefined>)
.concat(
global.__NUXT_PREPATHS__,
@ -25,11 +25,3 @@ function requireModule (id: string, paths?: string | string[]) {
export function tryRequireModule (id: string, paths?: string | string[]) {
try { return requireModule(id, paths) } catch { return null }
}
export function getNearestPackage (id: string, paths?: string | string[]) {
while (dirname(id) !== id) {
try { return requireModule(id + '/package.json', paths) } catch {}
id = dirname(id)
}
return null
}

View File

@ -1,141 +0,0 @@
import { promises as fsp } from 'node:fs'
import { isAbsolute, join, relative, resolve } from 'pathe'
import type { Nuxt, TSReference } from '@nuxt/schema'
import { defu } from 'defu'
import type { TSConfig } from 'pkg-types'
import { withTrailingSlash } from 'ufo'
import { getModulePaths, getNearestPackage } from './cjs'
export const writeTypes = async (nuxt: Nuxt) => {
const modulePaths = getModulePaths(nuxt.options.modulesDir)
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, {
compilerOptions: {
forceConsistentCasingInFileNames: true,
jsx: 'preserve',
target: 'ESNext',
module: 'ESNext',
moduleResolution: nuxt.options.experimental?.typescriptBundlerResolution ? 'Bundler' : 'Node',
skipLibCheck: true,
strict: nuxt.options.typescript?.strict ?? true,
allowJs: true,
// TODO: remove by default in 3.7
baseUrl: nuxt.options.srcDir,
noEmit: true,
resolveJsonModule: true,
allowSyntheticDefaultImports: true,
types: ['node'],
paths: {}
},
include: [
'./nuxt.d.ts',
join(relative(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'),
...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [],
...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd)
.filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules'))
.map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')),
...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : []
],
exclude: [
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
relative(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist'))
]
} satisfies TSConfig)
const aliases: Record<string, string> = {
...nuxt.options.alias,
'#build': nuxt.options.buildDir
}
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/]
const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
tsConfig.include = tsConfig.include || []
for (const alias in aliases) {
if (excludedAlias.some(re => re.test(alias))) {
continue
}
const absolutePath = resolve(basePath, aliases[alias])
const stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */)
if (stats?.isDirectory()) {
tsConfig.compilerOptions.paths[alias] = [absolutePath]
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`]
if (!absolutePath.startsWith(rootDirWithSlash)) {
tsConfig.include.push(absolutePath)
tsConfig.include.push(`${absolutePath}/*`)
}
} else {
const path = stats?.isFile()
? absolutePath.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */
: absolutePath
tsConfig.compilerOptions.paths[alias] = [path]
if (!absolutePath.startsWith(rootDirWithSlash)) {
tsConfig.include.push(path)
}
}
}
const references: TSReference[] = [
...nuxt.options.modules,
...nuxt.options._modules
]
.filter(f => typeof f === 'string')
.map(id => ({ types: getNearestPackage(id, modulePaths)?.name || id }))
if (nuxt.options.experimental?.reactivityTransform) {
references.push({ types: 'vue/macros-global' })
}
const declarations: string[] = []
await nuxt.callHook('prepare:types', { references, declarations, tsConfig })
const declaration = [
...references.map((ref) => {
if ('path' in ref && isAbsolute(ref.path)) {
ref.path = relative(nuxt.options.buildDir, ref.path)
}
return `/// <reference ${renderAttrs(ref)} />`
}),
...declarations,
'',
'export {}',
''
].join('\n')
async function writeFile () {
const GeneratedBy = '// Generated by nuxi'
const tsConfigPath = resolve(nuxt.options.buildDir, 'tsconfig.json')
await fsp.mkdir(nuxt.options.buildDir, { recursive: true })
await fsp.writeFile(tsConfigPath, GeneratedBy + '\n' + JSON.stringify(tsConfig, null, 2))
const declarationPath = resolve(nuxt.options.buildDir, 'nuxt.d.ts')
await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration)
}
// This is needed for Nuxt 2 which clears the build directory again before building
// https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144
// @ts-expect-error TODO: Nuxt 2 hook
nuxt.hook('builder:prepared', writeFile)
await writeFile()
}
function renderAttrs (obj: Record<string, string>) {
return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ')
}
function renderAttr (key: string, value: string) {
return value ? `${key}="${value}"` : ''
}

View File

@ -58,8 +58,8 @@
"@nuxt/telemetry": "^2.3.2",
"@nuxt/ui-templates": "^1.3.1",
"@nuxt/vite-builder": "workspace:../vite",
"@unhead/ssr": "^1.1.33",
"@unhead/vue": "^1.1.33",
"@unhead/ssr": "^1.1.35",
"@unhead/vue": "^1.1.35",
"@vue/shared": "^3.3.4",
"acorn": "8.10.0",
"c12": "^1.4.2",
@ -78,7 +78,6 @@
"jiti": "^1.19.1",
"klona": "^2.0.6",
"knitwork": "^1.0.0",
"local-pkg": "^0.4.3",
"magic-string": "^0.30.2",
"mlly": "^1.4.0",
"nitropack": "^2.5.2",
@ -88,6 +87,7 @@
"ohash": "^1.1.2",
"pathe": "^1.1.1",
"perfect-debounce": "^1.0.0",
"pkg-types": "^1.0.3",
"prompts": "^2.4.2",
"scule": "^1.0.0",
"strip-literal": "^1.0.1",
@ -101,7 +101,7 @@
"unplugin-vue-router": "^0.6.4",
"untyped": "^1.4.0",
"vue": "^3.3.4",
"vue-bundle-renderer": "^1.0.3",
"vue-bundle-renderer": "^2.0.0",
"vue-devtools-stub": "^0.1.0",
"vue-router": "^4.2.4"
},

View File

@ -1,150 +1,2 @@
import type { Ref, VNode } from 'vue'
import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { _wrapIf } from './utils'
import { LayoutMetaSymbol, PageRouteSymbol } from './injections'
import { useRoute } from '#app/composables/router'
// @ts-expect-error virtual file
import { useRoute as useVueRouterRoute } from '#build/pages'
// @ts-expect-error virtual file
import layouts from '#build/layouts'
// @ts-expect-error virtual file
import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs'
import { useNuxtApp } from '#app'
// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved
const LayoutLoader = defineComponent({
name: 'LayoutLoader',
inheritAttrs: false,
props: {
name: String,
layoutProps: Object
},
async setup (props, context) {
const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r)
return () => h(LayoutComponent, props.layoutProps, context.slots)
}
})
export default defineComponent({
name: 'NuxtLayout',
inheritAttrs: false,
props: {
name: {
type: [String, Boolean, Object] as unknown as () => string | false | Ref<string | false>,
default: null
}
},
setup (props, context) {
const nuxtApp = useNuxtApp()
// Need to ensure (if we are not a child of `<NuxtPage>`) that we use synchronous route (not deferred)
const injectedRoute = inject(PageRouteSymbol)
const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute
const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default')
const layoutRef = ref()
context.expose({ layoutRef })
const done = nuxtApp.deferHydration()
return () => {
const hasLayout = layout.value && layout.value in layouts
if (process.dev && layout.value && !hasLayout && layout.value !== 'default') {
console.warn(`Invalid layout \`${layout.value}\` selected.`)
}
const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition
// We avoid rendering layout transition if there is no layout to render
return _wrapIf(Transition, hasLayout && transitionProps, {
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
default: () => h(
// @ts-expect-error seems to be an issue in vue types
LayoutProvider,
{
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
key: layout.value,
name: layout.value,
shouldProvide: !props.name,
hasTransition: !!transitionProps
}, context.slots)
})
}).default()
}
}
})
const LayoutProvider = defineComponent({
name: 'NuxtLayoutProvider',
inheritAttrs: false,
props: {
name: {
type: [String, Boolean]
},
layoutProps: {
type: Object
},
hasTransition: {
type: Boolean
},
shouldProvide: {
type: Boolean
}
},
setup (props, context) {
// Prevent reactivity when the page will be rerendered in a different suspense fork
// eslint-disable-next-line vue/no-setup-props-destructure
const name = props.name
if (props.shouldProvide) {
provide(LayoutMetaSymbol, {
isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default')
})
}
let vnode: VNode | undefined
if (process.dev && process.client) {
onMounted(() => {
nextTick(() => {
if (['#comment', '#text'].includes(vnode?.el?.nodeName)) {
if (name) {
console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`)
} else {
console.warn('[nuxt] `<NuxtLayout>` needs to be passed a single root node in its default slot.')
}
}
})
})
}
return () => {
if (!name || (typeof name === 'string' && !(name in layouts))) {
if (process.dev && process.client && props.hasTransition) {
vnode = context.slots.default?.() as VNode | undefined
return vnode
}
return context.slots.default?.()
}
if (process.dev && process.client && props.hasTransition) {
vnode = h(
// @ts-expect-error seems to be an issue in vue types
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
)
return vnode
}
return h(
// @ts-expect-error seems to be an issue in vue types
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
)
}
}
})
// TODO: remove in 4.x
export { default } from './nuxt-layout'

View File

@ -13,6 +13,9 @@ import { getFragmentHTML, getSlotProps } from './utils'
import { useNuxtApp, useRuntimeConfig } from '#app/nuxt'
import { useRequestEvent } from '#app/composables/ssr'
// @ts-expect-error virtual file
import { remoteComponentIslands } from '#build/nuxt.config.mjs'
const pKey = '_islandPromises'
const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
@ -29,6 +32,7 @@ export default defineComponent({
type: String,
required: true
},
lazy: Boolean,
props: {
type: Object,
default: () => undefined
@ -36,12 +40,17 @@ export default defineComponent({
context: {
type: Object,
default: () => ({})
},
source: {
type: String,
default: () => undefined
}
},
async setup (props, { slots }) {
const error = ref<unknown>(null)
const config = useRuntimeConfig()
const nuxtApp = useNuxtApp()
const hashId = computed(() => hash([props.name, props.props, props.context]))
const hashId = computed(() => hash([props.name, props.props, props.context, props.source]))
const instance = getCurrentInstance()!
const event = useRequestEvent()
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
@ -61,7 +70,7 @@ export default defineComponent({
}
}
const ssrHTML = ref('<div></div>')
const ssrHTML = ref<string>('')
if (process.client) {
const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('')
if (renderedHTML && nuxtApp.isHydrating) {
@ -74,7 +83,7 @@ export default defineComponent({
}
})
}
ssrHTML.value = renderedHTML ?? '<div></div>'
ssrHTML.value = renderedHTML
}
const slotProps = computed(() => getSlotProps(ssrHTML.value))
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID())
@ -100,7 +109,8 @@ export default defineComponent({
const key = `${props.name}_${hashId.value}`
if (nuxtApp.payload.data[key] && !force) { return nuxtApp.payload.data[key] }
const url = `/__nuxt_island/${key}`
const url = remoteComponentIslands && props.source ? new URL(`/__nuxt_island/${key}`, props.source).href : `/__nuxt_island/${key}`
if (process.server && process.env.prerender) {
// Hint to Nitro to prerender the island component
appendResponseHeader(event, 'x-nitro-prerender', url)
@ -130,18 +140,23 @@ export default defineComponent({
delete nuxtApp[pKey]![uid.value]
})
}
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
cHead.value.link = res.head.link
cHead.value.style = res.head.style
ssrHTML.value = res.html.replace(UID_ATTR, () => {
return `nuxt-ssr-component-uid="${getId()}"`
})
key.value++
if (process.client) {
// must await next tick for Teleport to work correctly with static node re-rendering
await nextTick()
try {
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
cHead.value.link = res.head.link
cHead.value.style = res.head.style
ssrHTML.value = res.html.replace(UID_ATTR, () => {
return `nuxt-ssr-component-uid="${getId()}"`
})
key.value++
error.value = null
if (process.client) {
// must await next tick for Teleport to work correctly with static node re-rendering
await nextTick()
}
setUid()
} catch (e) {
error.value = e
}
setUid()
}
if (import.meta.hot) {
@ -154,15 +169,19 @@ export default defineComponent({
watch(props, debounce(() => fetchComponent(), 100))
}
// TODO: allow lazy loading server islands
if (process.server || !nuxtApp.isHydrating) {
if (process.client && !nuxtApp.isHydrating && props.lazy) {
fetchComponent()
} else if (process.server || !nuxtApp.isHydrating) {
await fetchComponent()
}
return () => {
if ((!html.value || error.value) && slots.fallback) {
return [slots.fallback({ error: error.value })]
}
const nodes = [createVNode(Fragment, {
key: key.value
}, [h(createStaticVNode(html.value, 1))])]
}, [h(createStaticVNode(html.value || '<div></div>', 1))])]
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
for (const slot in slots) {
if (availableSlots.value.includes(slot)) {

View File

@ -0,0 +1,153 @@
import type { DefineComponent, MaybeRef, VNode } from 'vue'
import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { _wrapIf } from './utils'
import { LayoutMetaSymbol, PageRouteSymbol } from './injections'
import type { PageMeta } from '#app'
import { useRoute } from '#app/composables/router'
import { useNuxtApp } from '#app/nuxt'
// @ts-expect-error virtual file
import { useRoute as useVueRouterRoute } from '#build/pages'
// @ts-expect-error virtual file
import layouts from '#build/layouts'
// @ts-expect-error virtual file
import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs'
// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved
const LayoutLoader = defineComponent({
name: 'LayoutLoader',
inheritAttrs: false,
props: {
name: String,
layoutProps: Object
},
async setup (props, context) {
const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r)
return () => h(LayoutComponent, props.layoutProps, context.slots)
}
})
export default defineComponent({
name: 'NuxtLayout',
inheritAttrs: false,
props: {
name: {
type: [String, Boolean, Object] as unknown as () => unknown extends PageMeta['layout'] ? MaybeRef<string | false> : PageMeta['layout'],
default: null
}
},
setup (props, context) {
const nuxtApp = useNuxtApp()
// Need to ensure (if we are not a child of `<NuxtPage>`) that we use synchronous route (not deferred)
const injectedRoute = inject(PageRouteSymbol)
const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute
const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default')
const layoutRef = ref()
context.expose({ layoutRef })
const done = nuxtApp.deferHydration()
return () => {
const hasLayout = layout.value && layout.value in layouts
if (process.dev && layout.value && !hasLayout && layout.value !== 'default') {
console.warn(`Invalid layout \`${layout.value}\` selected.`)
}
const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition
// We avoid rendering layout transition if there is no layout to render
return _wrapIf(Transition, hasLayout && transitionProps, {
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
default: () => h(
// @ts-expect-error seems to be an issue in vue types
LayoutProvider,
{
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
key: layout.value,
name: layout.value,
shouldProvide: !props.name,
hasTransition: !!transitionProps
}, context.slots)
})
}).default()
}
}
}) as unknown as DefineComponent<{
name?: unknown extends PageMeta['layout'] ? MaybeRef<string | false> : PageMeta['layout']
}>
const LayoutProvider = defineComponent({
name: 'NuxtLayoutProvider',
inheritAttrs: false,
props: {
name: {
type: [String, Boolean]
},
layoutProps: {
type: Object
},
hasTransition: {
type: Boolean
},
shouldProvide: {
type: Boolean
}
},
setup (props, context) {
// Prevent reactivity when the page will be rerendered in a different suspense fork
// eslint-disable-next-line vue/no-setup-props-destructure
const name = props.name
if (props.shouldProvide) {
provide(LayoutMetaSymbol, {
isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default')
})
}
let vnode: VNode | undefined
if (process.dev && process.client) {
onMounted(() => {
nextTick(() => {
if (['#comment', '#text'].includes(vnode?.el?.nodeName)) {
if (name) {
console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`)
} else {
console.warn('[nuxt] `<NuxtLayout>` needs to be passed a single root node in its default slot.')
}
}
})
})
}
return () => {
if (!name || (typeof name === 'string' && !(name in layouts))) {
if (process.dev && process.client && props.hasTransition) {
vnode = context.slots.default?.() as VNode | undefined
return vnode
}
return context.slots.default?.()
}
if (process.dev && process.client && props.hasTransition) {
vnode = h(
// @ts-expect-error seems to be an issue in vue types
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
)
return vnode
}
return h(
// @ts-expect-error seems to be an issue in vue types
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
)
}
}
})

View File

@ -1,18 +1,25 @@
import type { FetchError } from 'ofetch'
import type { AvailableRouterMethod, NitroFetchOptions, NitroFetchRequest, TypedInternalResponse } from 'nitropack'
import type { FetchError, FetchOptions } from 'ofetch'
import type { NitroFetchRequest, TypedInternalResponse, AvailableRouterMethod as _AvailableRouterMethod } from 'nitropack'
import type { Ref } from 'vue'
import { computed, reactive, unref } from 'vue'
import { hash } from 'ohash'
import { useRequestFetch } from './ssr'
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom, _Transform } from './asyncData'
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData'
import { useAsyncData } from './asyncData'
export type FetchResult<ReqT extends NitroFetchRequest, M extends AvailableRouterMethod<ReqT>> = TypedInternalResponse<ReqT, unknown, M>
// support uppercase methods, detail: https://github.com/nuxt/nuxt/issues/22313
type AvailableRouterMethod<R extends NitroFetchRequest> = _AvailableRouterMethod<R> | Uppercase<_AvailableRouterMethod<R>>
export type FetchResult<ReqT extends NitroFetchRequest, M extends AvailableRouterMethod<ReqT>> = TypedInternalResponse<ReqT, unknown, Lowercase<M>>
type ComputedOptions<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends Function ? T[K] : T[K] extends Record<string, any> ? ComputedOptions<T[K]> | Ref<T[K]> | T[K] : Ref<T[K]> | T[K]
}
interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> extends FetchOptions {
method?: M;
}
type ComputedFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R>> = ComputedOptions<NitroFetchOptions<R, M>>
export interface UseFetchOptions<

View File

@ -221,7 +221,7 @@ export const abortNavigation = (err?: string | Partial<NuxtError>) => {
throw err
}
export const setPageLayout = (layout: string) => {
export const setPageLayout = (layout: unknown extends PageMeta['layout'] ? string : PageMeta['layout']) => {
if (process.server) {
if (process.dev && getCurrentInstance() && useState('_layout').value !== layout) {
console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout on the server within a component as this will cause hydration errors.')

View File

@ -10,6 +10,7 @@ import type { H3Event } from 'h3'
import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema'
import type { RenderResponse } from 'nitropack'
import type { MergeHead, VueHeadClient } from '@unhead/vue'
// eslint-disable-next-line import/no-restricted-paths
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
import type { RouteMiddleware } from '../../app'
@ -18,15 +19,6 @@ import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
type NuxtMeta = {
htmlAttrs?: string
headAttrs?: string
bodyAttrs?: string
headTags?: string
bodyScriptsPrepend?: string
bodyScripts?: string
}
type HookResult = Promise<void> | void
type AppRenderedContext = { ssrContext: NuxtApp['ssrContext'], renderResult: null | Awaited<ReturnType<ReturnType<typeof createRenderer>['renderToString']>> }
@ -59,10 +51,10 @@ export interface NuxtSSRContext extends SSRContext {
error?: boolean
nuxt: _NuxtApp
payload: NuxtPayload
head: VueHeadClient<MergeHead>
/** This is used solely to render runtime config with SPA renderer. */
config?: Pick<RuntimeConfig, 'public' | 'app'>
teleports?: Record<string, string>
renderMeta?: () => Promise<NuxtMeta> | NuxtMeta
islandContext?: NuxtIslandContext
/** @internal */
_renderResponse?: Partial<RenderResponse>
@ -163,6 +155,16 @@ export interface PluginMeta {
order?: number
}
export interface PluginEnvContext {
/**
* This enable the plugin for islands components.
* Require `experimental.componentsIslands`.
*
* @default true
*/
islands?: boolean
}
export interface ResolvedPluginMeta {
name?: string
parallel?: boolean
@ -177,6 +179,7 @@ export interface Plugin<Injections extends Record<string, unknown> = Record<stri
export interface ObjectPlugin<Injections extends Record<string, unknown> = Record<string, unknown>> extends PluginMeta {
hooks?: Partial<RuntimeNuxtHooks>
setup?: Plugin<Injections>
env?: PluginEnvContext
/**
* Execute plugin in parallel with other parallel plugins.
*
@ -326,6 +329,7 @@ export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & Ob
const parallels: Promise<any>[] = []
const errors: Error[] = []
for (const plugin of plugins) {
if (process.server && nuxtApp.ssrContext?.islandContext && plugin.env?.islands === false) { continue }
const promise = applyPlugin(nuxtApp, plugin)
if (plugin.parallel) {
parallels.push(promise.catch(e => errors.push(e)))

View File

@ -221,7 +221,7 @@ export default defineNuxtModule<ComponentsOptions>({
getComponents,
mode,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: nuxt.options.experimental.componentIslands
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands
}))
if (isServer && nuxt.options.experimental.componentIslands) {
@ -265,7 +265,7 @@ export default defineNuxtModule<ComponentsOptions>({
getComponents,
mode,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: nuxt.options.experimental.componentIslands
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands
}))
if (nuxt.options.experimental.componentIslands && mode === 'server') {

View File

@ -5,9 +5,11 @@ export const createServerComponent = (name: string) => {
return defineComponent({
name,
inheritAttrs: false,
setup (_props, { attrs, slots }) {
props: { lazy: Boolean },
setup (props, { attrs, slots }) {
return () => h(NuxtIsland, {
name,
lazy: props.lazy,
props: attrs
}, slots)
}

View File

@ -1,10 +1,10 @@
import { addDependency } from 'nypm'
import { isPackageExists } from 'local-pkg'
import { resolvePackageJSON } from 'pkg-types'
import { logger } from '@nuxt/kit'
import prompts from 'prompts'
export async function ensurePackageInstalled (rootDir: string, name: string, searchPaths?: string[]) {
if (isPackageExists(name, { paths: searchPaths })) {
if (await resolvePackageJSON(name, { url: searchPaths }).catch(() => null)) {
return true
}

View File

@ -8,8 +8,6 @@ import escapeRE from 'escape-string-regexp'
import { defu } from 'defu'
import fsExtra from 'fs-extra'
import { dynamicEventHandler } from 'h3'
import { createHeadCore } from '@unhead/vue'
import { renderSSRHead } from '@unhead/ssr'
import type { Nuxt } from 'nuxt/schema'
// @ts-expect-error TODO: add legacy type support for subpath imports
import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs'
@ -205,12 +203,6 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Resolve user-provided paths
nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!)
// Add head chunk for SPA renders
const head = createHeadCore()
head.push(nuxt.options.app.head)
const headChunk = await renderSSRHead(head)
nitroConfig.virtual!['#head-static'] = `export default ${JSON.stringify(headChunk)}`
// Add fallback server for `ssr: false`
if (!nuxt.options.ssr) {
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'

View File

@ -1,4 +1,4 @@
import { join, normalize, resolve } from 'pathe'
import { join, normalize, relative, resolve } from 'pathe'
import { createDebugger, createHooks } from 'hookable'
import type { LoadNuxtOptions } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
@ -200,7 +200,7 @@ async function initNuxt (nuxt: Nuxt) {
addComponent({
name: 'NuxtLayout',
priority: 10, // built-in that we do not expect the user to override
filePath: resolve(nuxt.options.appDir, 'components/layout')
filePath: resolve(nuxt.options.appDir, 'components/nuxt-layout')
})
// Add <NuxtErrorBoundary>
@ -349,12 +349,17 @@ async function initNuxt (nuxt: Nuxt) {
}
// User provided patterns
const layerRelativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
for (const pattern of nuxt.options.watch) {
if (typeof pattern === 'string') {
if (pattern === path) { return nuxt.callHook('restart') }
// Test (normalised) strings against absolute path and relative path to any layer `srcDir`
if (pattern === path || layerRelativePaths.includes(pattern)) { return nuxt.callHook('restart') }
continue
}
if (pattern.test(path)) { return nuxt.callHook('restart') }
// Test regular expressions against path to _any_ layer `srcDir`
if (layerRelativePaths.some(p => pattern.test(p))) {
return nuxt.callHook('restart')
}
}
// Core Nuxt files: app.vue, error.vue and app.config.ts
@ -406,6 +411,13 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
}
}
// Nuxt Webpack Builder is currently opt-in
if (options.builder === '@nuxt/webpack-builder') {
if (!await import('./features').then(r => r.ensurePackageInstalled(options.rootDir, '@nuxt/webpack-builder', options.modulesDir))) {
logger.warn('Failed to install `@nuxt/webpack-builder`, please install it manually, or change the `builder` option to vite in `nuxt.config`')
}
}
// Add core modules
options._modules.push(pagesModule, metaModule, componentsModule)
options._modules.push([importsModule, {

View File

@ -1,4 +1,10 @@
import { createRenderer, renderResourceHeaders } from 'vue-bundle-renderer/runtime'
import {
createRenderer,
getPrefetchLinks,
getPreloadLinks,
getRequestDependencies,
renderResourceHeaders
} from 'vue-bundle-renderer/runtime'
import type { RenderResponse } from 'nitropack'
import type { Manifest } from 'vite'
import type { H3Event } from 'h3'
@ -9,14 +15,17 @@ import destr from 'destr'
import { joinURL, withoutTrailingSlash } from 'ufo'
import { renderToString as _renderToString } from 'vue/server-renderer'
import { hash } from 'ohash'
import { renderSSRHead } from '@unhead/ssr'
import { defineRenderHandler, getRouteRules, useRuntimeConfig } from '#internal/nitro'
import { useNitroApp } from '#internal/nitro/app'
import type { Link, Script } from '@unhead/vue'
import { createServerHead } from '@unhead/vue'
// eslint-disable-next-line import/no-restricted-paths
import type { NuxtPayload, NuxtSSRContext } from '#app/nuxt'
// @ts-expect-error virtual file
import { appRootId, appRootTag } from '#internal/nuxt.config.mjs'
import { appHead, appRootId, appRootTag } from '#internal/nuxt.config.mjs'
// @ts-expect-error virtual file
import { buildAssetsURL, publicAssetsURL } from '#paths'
@ -71,9 +80,6 @@ const getEntryIds: () => Promise<string[]> = () => getClientManifest().then(r =>
r._globalCSS
).map(r => r.src!))
// @ts-expect-error virtual file
const getStaticRenderedHead = (): Promise<NuxtMeta> => import('#head-static').then(r => r.default || r)
// @ts-expect-error file will be produced after app build
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
@ -140,7 +146,6 @@ const getSPARenderer = lazyCachedFunction(async () => {
public: config.public,
app: config.app
}
ssrContext!.renderMeta = ssrContext!.renderMeta ?? getStaticRenderedHead
return Promise.resolve(result)
}
@ -221,6 +226,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Get route options (currently to apply `ssr: false`)
const routeOptions = getRouteRules(event)
const head = createServerHead()
head.push(appHead)
// Initialize ssr context
const ssrContext: NuxtSSRContext = {
url,
@ -231,6 +239,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
event.context.nuxt?.noSSR ||
routeOptions.ssr === false ||
(process.env.prerender ? PRERENDER_NO_SSR_ROUTES.has(url) : false),
head,
error: !!ssrError,
nuxt: undefined!, /* NuxtApp */
payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload,
@ -288,9 +297,6 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
PAYLOAD_CACHE!.set(withoutTrailingSlash(url), renderPayloadResponse(ssrContext))
}
// Render meta
const renderedMeta = await ssrContext.renderMeta?.() ?? {}
if (process.env.NUXT_INLINE_STYLES && !islandContext) {
const source = ssrContext.modules ?? ssrContext._registeredComponents
if (source) {
@ -303,45 +309,81 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Render inline styles
const inlinedStyles = (process.env.NUXT_INLINE_STYLES || Boolean(islandContext))
? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? [])
: ''
: []
const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.experimentalNoScripts
// Setup head
const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext)
// 1.Extracted payload preloading
if (_PAYLOAD_EXTRACTION) {
head.push({
link: [
process.env.NUXT_JSON_PAYLOADS
? { rel: 'preload', as: 'fetch', crossorigin: 'anonymous', href: payloadURL }
: { rel: 'modulepreload', href: payloadURL }
]
})
}
// 2. Styles
head.push({
link: Object.values(styles)
.map(resource =>
({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) })
),
style: inlinedStyles
})
if (!NO_SCRIPTS) {
// 3. Resource Hints
// TODO: add priorities based on Capo
head.push({
link: getPreloadLinks(ssrContext, renderer.rendererContext) as Link[]
})
head.push({
link: getPrefetchLinks(ssrContext, renderer.rendererContext) as Link[]
})
// 4. Payloads
head.push({
script: _PAYLOAD_EXTRACTION
? process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
}, {
// this should come before another end of body scripts
tagPosition: 'bodyClose',
tagPriority: 'high'
})
}
// 5. Scripts
if (!routeOptions.experimentalNoScripts) {
head.push({
script: Object.values(scripts).map(resource => (<Script> {
type: resource.module ? 'module' : null,
src: renderer.rendererContext.buildAssetsURL(resource.file),
defer: resource.module ? null : true,
crossorigin: ''
}))
})
}
// remove certain tags for nuxt islands
const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)
// Create render context
const htmlContext: NuxtRenderHTMLContext = {
island: Boolean(islandContext),
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
head: normalizeChunks([
renderedMeta.headTags,
process.env.NUXT_JSON_PAYLOADS
? _PAYLOAD_EXTRACTION ? `<link rel="preload" as="fetch" crossorigin="anonymous" href="${payloadURL}">` : null
: _PAYLOAD_EXTRACTION ? `<link rel="modulepreload" href="${payloadURL}">` : null,
NO_SCRIPTS ? null : _rendered.renderResourceHints(),
_rendered.renderStyles(),
inlinedStyles,
ssrContext.styles
]),
bodyAttrs: normalizeChunks([renderedMeta.bodyAttrs!]),
bodyPrepend: normalizeChunks([
renderedMeta.bodyScriptsPrepend,
ssrContext.teleports?.body
]),
htmlAttrs: [htmlAttrs],
head: normalizeChunks([headTags, ssrContext.styles]),
bodyAttrs: [bodyAttrs],
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceServerOnlyComponentsSlots(ssrContext, _rendered.html) : _rendered.html],
bodyAppend: normalizeChunks([
NO_SCRIPTS
? undefined
: (_PAYLOAD_EXTRACTION
? process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
),
routeOptions.experimentalNoScripts ? undefined : _rendered.renderScripts(),
// Note: bodyScripts may contain tags other than <script>
renderedMeta.bodyScripts
])
bodyAppend: [bodyTags]
}
// Allow hooking into the rendered result
@ -349,21 +391,21 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Response for component islands
if (process.env.NUXT_COMPONENT_ISLANDS && islandContext) {
const _tags = htmlContext.head.flatMap(head => extractHTMLTags(head))
const head: NuxtIslandResponse['head'] = {
link: _tags.filter(tag => tag.tagName === 'link' && tag.attrs.rel === 'stylesheet' && tag.attrs.href.includes('scoped') && !tag.attrs.href.includes('pages/')).map(tag => ({
key: 'island-link-' + hash(tag.attrs.href),
...tag.attrs
})),
style: _tags.filter(tag => tag.tagName === 'style' && tag.innerHTML).map(tag => ({
key: 'island-style-' + hash(tag.innerHTML),
innerHTML: tag.innerHTML
}))
const islandHead: NuxtIslandResponse['head'] = {
link: [],
style: []
}
for (const tag of await head.resolveTags()) {
if (tag.tag === 'link' && tag.props.rel === 'stylesheet' && tag.props.href.includes('scoped') && !tag.props.href.includes('pages/')) {
islandHead.link.push({ ...tag.props, key: 'island-link-' + hash(tag.props.href) })
}
if (tag.tag === 'style' && tag.innerHTML) {
islandHead.style.push({ key: 'island-style-' + hash(tag.innerHTML), innerHTML: tag.innerHTML })
}
}
const islandResponse: NuxtIslandResponse = {
id: islandContext.id,
head,
head: islandHead,
html: getServerComponentHTML(htmlContext.body),
state: ssrContext.payload.state
}
@ -429,33 +471,17 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) {
</html>`
}
// TODO: Move to external library
const HTML_TAG_RE = /<(?<tag>[a-z]+)(?<rawAttrs> [^>]*)?>(?:(?<innerHTML>[\s\S]*?)<\/\k<tag>)?/g
const HTML_TAG_ATTR_RE = /(?<name>[a-z]+)="(?<value>[^"]*)"/g
function extractHTMLTags (html: string) {
const tags: { tagName: string, attrs: Record<string, string>, innerHTML: string }[] = []
for (const tagMatch of html.matchAll(HTML_TAG_RE)) {
const attrs: Record<string, string> = {}
for (const attrMatch of tagMatch.groups!.rawAttrs?.matchAll(HTML_TAG_ATTR_RE) || []) {
attrs[attrMatch.groups!.name] = attrMatch.groups!.value
}
const innerHTML = tagMatch.groups!.innerHTML || ''
tags.push({ tagName: tagMatch.groups!.tag, attrs, innerHTML })
}
return tags
}
async function renderInlineStyles (usedModules: Set<string> | string[]) {
const styleMap = await getSSRStyles()
const inlinedStyles = new Set<string>()
for (const mod of usedModules) {
if (mod in styleMap) {
for (const style of await styleMap[mod]()) {
inlinedStyles.add(`<style>${style}</style>`)
inlinedStyles.add(style)
}
}
}
return Array.from(inlinedStyles).join('')
return Array.from(inlinedStyles).map(style => ({ innerHTML: style }))
}
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
@ -472,25 +498,41 @@ function renderPayloadResponse (ssrContext: NuxtSSRContext) {
} satisfies RenderResponse
}
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }) {
const attrs = [
'type="application/json"',
`id="${opts.id}"`,
`data-ssr="${!(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)}"`,
opts.src ? `data-src="${opts.src}"` : ''
].filter(Boolean)
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
return `<script ${attrs.join(' ')}>${contents}</script>` +
`<script>window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}</script>`
const payload: Script = {
type: 'application/json',
id: opts.id,
innerHTML: contents,
'data-ssr': !(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)
}
if (opts.src) {
payload['data-src'] = opts.src
}
return [
payload,
{
innerHTML: `window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}`
}
]
}
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }) {
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
opts.data.config = opts.ssrContext.config
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
if (_PAYLOAD_EXTRACTION) {
return `<script type="module">import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})}</script>`
return [
{
type: 'module',
innerHTML: `import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})`
}
]
}
return `<script>window.__NUXT__=${devalue(opts.data)}</script>`
return [
{
innerHTML: `window.__NUXT__=${devalue(opts.data)}`
}
]
}
function splitPayload (ssrContext: NuxtSSRContext) {

View File

@ -329,6 +329,7 @@ export const nuxtConfigTemplate = {
...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`),
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
`export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`,
`export const remoteComponentIslands = ${ctx.nuxt.options.experimental.componentIslands === 'local+remote'}`,
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`
].join('\n\n')

View File

@ -1,16 +1,11 @@
import { createHead as createClientHead, createServerHead } from '@unhead/vue'
import { renderSSRHead } from '@unhead/ssr'
import { createHead as createClientHead } from '@unhead/vue'
import { defineNuxtPlugin } from '#app/nuxt'
// @ts-expect-error untyped
import { appHead } from '#build/nuxt.config.mjs'
export default defineNuxtPlugin({
name: 'nuxt:head',
setup (nuxtApp) {
const createHead = process.server ? createServerHead : createClientHead
const head = createHead()
head.push(appHead)
const head = process.server ? nuxtApp.ssrContext!.head : createClientHead()
// nuxt.config appHead is set server-side within the renderer
nuxtApp.vueApp.use(head)
if (process.client) {
@ -28,17 +23,5 @@ export default defineNuxtPlugin({
// unpause the DOM once the mount suspense is resolved
nuxtApp.hooks.hook('app:suspense:resolve', unpauseDom)
}
if (process.server) {
nuxtApp.ssrContext!.renderMeta = async () => {
const meta = await renderSSRHead(head)
return {
...meta,
bodyScriptsPrepend: meta.bodyTagsOpen,
// resolves naming difference with NuxtMeta and Unhead
bodyScripts: meta.bodyTags
}
}
}
}
})

View File

@ -353,11 +353,11 @@ export default defineNuxtModule({
getContents: ({ app }: { app: NuxtApp }) => {
const composablesFile = resolve(runtimeDir, 'composables')
return [
'import { ComputedRef, Ref } from \'vue\'',
'import { ComputedRef, MaybeRef } from \'vue\'',
`export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`,
`declare module ${genString(composablesFile)} {`,
' interface PageMeta {',
' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>',
' layout?: MaybeRef<LayoutKey | false> | ComputedRef<LayoutKey | false>',
' }',
'}'
].join('\n')

View File

@ -30,7 +30,7 @@
"@types/file-loader": "5.0.1",
"@types/pug": "2.0.6",
"@types/sass-loader": "8.0.5",
"@unhead/schema": "1.1.33",
"@unhead/schema": "1.1.35",
"@vitejs/plugin-vue": "4.2.3",
"@vitejs/plugin-vue-jsx": "3.0.1",
"@vue/compiler-core": "3.3.4",
@ -42,7 +42,7 @@
"unctx": "2.3.1",
"vite": "4.4.7",
"vue": "3.3.4",
"vue-bundle-renderer": "1.0.3",
"vue-bundle-renderer": "2.0.0",
"vue-loader": "17.2.2",
"vue-router": "4.2.4",
"webpack": "5.88.2",

View File

@ -365,8 +365,9 @@ export default defineUntypedSchema({
/**
* The watch property lets you define patterns that will restart the Nuxt dev server when changed.
*
* It is an array of strings or regular expressions, which will be matched against the file path
* relative to the project `srcDir`.
* It is an array of strings or regular expressions. Strings should be either absolute paths or
* relative to the `srcDir` (and the `srcDir` of any layers). Regular expressions will be matched
* against the path relative to the project `srcDir` (and the `srcDir` of any layers).
*
* @type {Array<string | RegExp>}
*/

View File

@ -137,8 +137,15 @@ export default defineUntypedSchema({
/**
* Experimental component islands support with <NuxtIsland> and .island.vue files.
* @type {true | 'local' | 'local+remote' | false}
*/
componentIslands: false,
componentIslands: {
$resolve: (val) => {
if (typeof val === 'string') { return val }
if (val === true) { return 'local' }
return false
}
},
/**
* Config schema support

View File

@ -60,7 +60,7 @@
"vite": "^4.4.7",
"vite-node": "^0.33.0",
"vite-plugin-checker": "^0.6.1",
"vue-bundle-renderer": "^1.0.3"
"vue-bundle-renderer": "^2.0.0"
},
"peerDependencies": {
"vue": "^3.3.4"

View File

@ -52,7 +52,7 @@
"ufo": "^1.2.0",
"unplugin": "^1.4.0",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^1.0.3",
"vue-bundle-renderer": "^2.0.0",
"vue-loader": "^17.2.2",
"webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.9.0",

View File

@ -194,6 +194,9 @@ importers:
semver:
specifier: ^7.5.4
version: 7.5.4
ufo:
specifier: ^1.2.0
version: 1.2.0
unctx:
specifier: ^2.3.1
version: 2.3.1
@ -356,11 +359,11 @@ importers:
specifier: ^14.18.0 || >=16.10.0
version: 18.17.1
'@unhead/ssr':
specifier: ^1.1.33
version: 1.1.33
specifier: ^1.1.35
version: 1.1.35
'@unhead/vue':
specifier: ^1.1.33
version: 1.1.33(vue@3.3.4)
specifier: ^1.1.35
version: 1.1.35(vue@3.3.4)
'@vue/shared':
specifier: ^3.3.4
version: 3.3.4
@ -415,9 +418,6 @@ importers:
knitwork:
specifier: ^1.0.0
version: 1.0.0
local-pkg:
specifier: ^0.4.3
version: 0.4.3
magic-string:
specifier: ^0.30.2
version: 0.30.2
@ -445,6 +445,9 @@ importers:
perfect-debounce:
specifier: ^1.0.0
version: 1.0.0
pkg-types:
specifier: ^1.0.3
version: 1.0.3
prompts:
specifier: ^2.4.2
version: 2.4.2
@ -485,8 +488,8 @@ importers:
specifier: 3.3.4
version: 3.3.4
vue-bundle-renderer:
specifier: ^1.0.3
version: 1.0.3
specifier: ^2.0.0
version: 2.0.0
vue-devtools-stub:
specifier: ^0.1.0
version: 0.1.0
@ -565,8 +568,8 @@ importers:
specifier: 8.0.5
version: 8.0.5
'@unhead/schema':
specifier: 1.1.33
version: 1.1.33
specifier: 1.1.35
version: 1.1.35
'@vitejs/plugin-vue':
specifier: 4.2.3
version: 4.2.3(vite@4.4.7)(vue@3.3.4)
@ -601,8 +604,8 @@ importers:
specifier: 3.3.4
version: 3.3.4
vue-bundle-renderer:
specifier: 1.0.3
version: 1.0.3
specifier: 2.0.0
version: 2.0.0
vue-loader:
specifier: 17.2.2
version: 17.2.2(vue@3.3.4)(webpack@5.88.2)
@ -767,8 +770,8 @@ importers:
specifier: ^0.6.1
version: 0.6.1(eslint@8.46.0)(typescript@5.1.6)(vite@4.4.7)(vue-tsc@1.8.8)
vue-bundle-renderer:
specifier: ^1.0.3
version: 1.0.3
specifier: ^2.0.0
version: 2.0.0
devDependencies:
'@nuxt/schema':
specifier: workspace:*
@ -891,8 +894,8 @@ importers:
specifier: ^4.1.1
version: 4.1.1(file-loader@6.2.0)(webpack@5.88.2)
vue-bundle-renderer:
specifier: ^1.0.3
version: 1.0.3
specifier: ^2.0.0
version: 2.0.0
vue-loader:
specifier: ^17.2.2
version: 17.2.2(vue@3.3.4)(webpack@5.88.2)
@ -2786,41 +2789,41 @@ packages:
eslint-visitor-keys: 3.4.2
dev: true
/@unhead/dom@1.1.33:
resolution: {integrity: sha512-HKe8ppDQvFJeKkz4Hz8qmZHaEniChA3fooaE56/jW/ZQtmguU7xRU09PDm0VEV/08xiI3WRt93IYq+RtqrkzAw==}
/@unhead/dom@1.1.35:
resolution: {integrity: sha512-/VAwHHiZGHAKS9V0JaYBWxIBc8OpPMfjVk0TRcKoerFCmYRMsuWtpWauWx644j177kCbzCCT1HOA2fB7R07uXQ==}
dependencies:
'@unhead/schema': 1.1.33
'@unhead/shared': 1.1.33
'@unhead/schema': 1.1.35
'@unhead/shared': 1.1.35
dev: false
/@unhead/schema@1.1.33:
resolution: {integrity: sha512-QC73j5goOht4/sUQjADPM3Bg+WKHm5k+062Ns8tCrC8/YE10y7aeD33vpAF6z9BM4GCBODlDUlpOUuGfqovP5w==}
/@unhead/schema@1.1.35:
resolution: {integrity: sha512-hB1uHbK38+WoZn2PHRl0eJJ2Lip374+eHHxUbHY4rFQeL4mTgxAFL0KltpMZr5Eo7ZMV/zNL7LZ89KBd9L43Zg==}
dependencies:
hookable: 5.5.3
zhead: 2.0.10
/@unhead/shared@1.1.33:
resolution: {integrity: sha512-zEviDmj61MAFAMR3Ts4lHgnBFPmRaLhkwsS00l4K9nHOkghoj575cGImhzSJ863r0KDr3dVDCalgF8remhz9pg==}
/@unhead/shared@1.1.35:
resolution: {integrity: sha512-SmR2tyAVYfvN+bPp71Bp4igHpv19X6VAoVP14qq3Yqdw1nWJKknla2QEkpqAgygit9b69Gyu+Wi5WABpZKUA+A==}
dependencies:
'@unhead/schema': 1.1.33
'@unhead/schema': 1.1.35
dev: false
/@unhead/ssr@1.1.33:
resolution: {integrity: sha512-nboGQZ5X62HOZYcsdEFugLRYnz57XBoJv+7zyuH6qk8jB2ebzSnkz0cR3eawpcgIzEPi9tQ2Q3RfGuJ0m5KIhA==}
/@unhead/ssr@1.1.35:
resolution: {integrity: sha512-VFIWcqGX358v05tzEPgZ8N7YhAhrrGxeecmRVE/jHtwimKCXa/xsQnhHe5ytswDiuTCTd/qBHEqVTVg8tGseUg==}
dependencies:
'@unhead/schema': 1.1.33
'@unhead/shared': 1.1.33
'@unhead/schema': 1.1.35
'@unhead/shared': 1.1.35
dev: false
/@unhead/vue@1.1.33(vue@3.3.4):
resolution: {integrity: sha512-cbq2k8RII9gDzpGC+CMaPezid6k8b8mV2KxhrtidnsHfK/ZmEaChrVCQ06C9xTtHMHfnNbCUn9vvQh96dHHF9A==}
/@unhead/vue@1.1.35(vue@3.3.4):
resolution: {integrity: sha512-U0iM9B8pq06FS1DLK7g25+ddMqDnQkqy+fgQyC0Gv+e4m8XEKsI7JKSbzAjFnsG69orCKd8M6jvcOyst8gvn5g==}
peerDependencies:
vue: '>=2.7 || >=3'
dependencies:
'@unhead/schema': 1.1.33
'@unhead/shared': 1.1.33
'@unhead/schema': 1.1.35
'@unhead/shared': 1.1.35
hookable: 5.5.3
unhead: 1.1.33
unhead: 1.1.35
vue: 3.3.4
dev: false
@ -8659,12 +8662,12 @@ packages:
node-fetch-native: 1.2.0
pathe: 1.1.1
/unhead@1.1.33:
resolution: {integrity: sha512-Qm94ySKOPwoXubGkdkeuLr9FcCv706PSL+GEApOcupBIf8M9kkmmYmRT5dCAoQcoUJDrvpeynTxiRkfA1jNRkA==}
/unhead@1.1.35:
resolution: {integrity: sha512-YEHXxJeSM313yPRcJdBQOSCnkcck1uhg7e2ZoEO+X0KVLuhqV1iYXU+tzvLU+ZId6IZOcEVDfsJ0hHfLkM6Itw==}
dependencies:
'@unhead/dom': 1.1.33
'@unhead/schema': 1.1.33
'@unhead/shared': 1.1.33
'@unhead/dom': 1.1.35
'@unhead/schema': 1.1.35
'@unhead/shared': 1.1.35
hookable: 5.5.3
dev: false
@ -9089,8 +9092,8 @@ packages:
resolution: {integrity: sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==}
dev: false
/vue-bundle-renderer@1.0.3:
resolution: {integrity: sha512-EfjX+5TTUl70bki9hPuVp+54JiZOvFIfoWBcfXsSwLzKEiDYyHNi5iX8srnqLIv3YRnvxgbntdcG1WPq0MvffQ==}
/vue-bundle-renderer@2.0.0:
resolution: {integrity: sha512-oYATTQyh8XVkUWe2kaKxhxKVuuzK2Qcehe+yr3bGiaQAhK3ry2kYE4FWOfL+KO3hVFwCdLmzDQTzYhTi9C+R2A==}
dependencies:
ufo: 1.2.0

View File

@ -10,10 +10,12 @@ async function main () {
const date = Math.round(Date.now() / (1000 * 60))
const nuxtPkg = workspace.find('nuxt')
const nitroInfo = await $fetch('https://registry.npmjs.org/nitropack-edge')
const latestNitro = nitroInfo['dist-tags'].latest
const { version: latestNitro } = await $fetch<{ version: string }>('https://registry.npmjs.org/nitropack-edge/latest')
nuxtPkg.data.dependencies.nitropack = `npm:nitropack-edge@^${latestNitro}`
const { version: latestNuxi } = await $fetch<{ version: string }>('https://registry.npmjs.org/nuxi-ng/latest')
nuxtPkg.data.dependencies.nuxi = `npm:nuxi-ng@^${latestNuxi}`
const bumpType = await determineBumpType()
for (const pkg of workspace.packages.filter(p => !p.data.private)) {

View File

@ -1365,7 +1365,6 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
const html: string = await $fetch('/styles')
expect(html.match(/<link [^>]*href="[^"]*\.css">/g)?.filter(m => m.includes('entry'))?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(`
[
"<link rel=\\"preload\\" as=\\"style\\" href=\\"/_nuxt/entry.css\\">",
"<link rel=\\"stylesheet\\" href=\\"/_nuxt/entry.css\\">",
]
`)
@ -1419,6 +1418,43 @@ describe('server components/islands', () => {
await page.close()
})
it('lazy server components', async () => {
const page = await createPage('/server-components/lazy/start')
await page.waitForLoadState('networkidle')
await page.getByText('Go to page with lazy server component').click()
const text = await page.innerText('pre')
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id=\\"fallback\\"> Loading server component </section><section id=\\"no-fallback\\"><div></div></section>"')
expect(text).not.toContain('async component that was very long')
expect(text).toContain('Loading server component')
// Wait for all pending micro ticks to be cleared
// await page.waitForLoadState('networkidle')
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.close()
})
it('non-lazy server components', async () => {
const page = await createPage('/server-components/lazy/start')
await page.waitForLoadState('networkidle')
await page.getByText('Go to page without lazy server component').click()
const text = await page.innerText('pre')
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id=\\"fallback\\"><div nuxt-ssr-component-uid=\\"0\\"> This is a .server (20ms) async component that was very long ... <div id=\\"async-server-component-count\\">42</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div></div></section><section id=\\"no-fallback\\"><div nuxt-ssr-component-uid=\\"1\\"> This is a .server (20ms) async component that was very long ... <div id=\\"async-server-component-count\\">42</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div></div></section>"')
expect(text).toContain('async component that was very long')
// Wait for all pending micro ticks to be cleared
// await page.waitForLoadState('networkidle')
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.close()
})
it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => {
// @ts-expect-error ssssh! untyped secret property
const publicDir = useTestContext().nuxt._nitro.options.output.publicDir
@ -1649,7 +1685,7 @@ describe('component islands', () => {
"link": [],
"style": [],
},
"html": "<div nuxt-ssr-component-uid><div> count is above 2 </div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div> that was very long ... <div id=\\"long-async-component-count\\">3</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"test\\" nuxt-ssr-slot-data=\\"[{&quot;count&quot;:3}]\\"></div><p>hello world !!!</p><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"hello\\" nuxt-ssr-slot-data=\\"[{&quot;t&quot;:0},{&quot;t&quot;:1},{&quot;t&quot;:2}]\\"><div nuxt-slot-fallback-start=\\"hello\\"></div><!--[--><div style=\\"display:contents;\\"><div> fallback slot -- index: 0</div></div><div style=\\"display:contents;\\"><div> fallback slot -- index: 1</div></div><div style=\\"display:contents;\\"><div> fallback slot -- index: 2</div></div><!--]--><div nuxt-slot-fallback-end></div></div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"fallback\\" nuxt-ssr-slot-data=\\"[{&quot;t&quot;:&quot;fall&quot;},{&quot;t&quot;:&quot;back&quot;}]\\"><div nuxt-slot-fallback-start=\\"fallback\\"></div><!--[--><div style=\\"display:contents;\\"><div>fall slot -- index: 0</div><div class=\\"fallback-slot-content\\"> wonderful fallback </div></div><div style=\\"display:contents;\\"><div>back slot -- index: 1</div><div class=\\"fallback-slot-content\\"> wonderful fallback </div></div><!--]--><div nuxt-slot-fallback-end></div></div></div>",
"html": "<div nuxt-ssr-component-uid><div> count is above 2 </div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div> that was very long ... <div id=\\"long-async-component-count\\">3</div> <div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"test\\" nuxt-ssr-slot-data=\\"[{&quot;count&quot;:3}]\\"></div><p>hello world !!!</p><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"hello\\" nuxt-ssr-slot-data=\\"[{&quot;t&quot;:0},{&quot;t&quot;:1},{&quot;t&quot;:2}]\\"><div nuxt-slot-fallback-start=\\"hello\\"></div><!--[--><div style=\\"display:contents;\\"><div> fallback slot -- index: 0</div></div><div style=\\"display:contents;\\"><div> fallback slot -- index: 1</div></div><div style=\\"display:contents;\\"><div> fallback slot -- index: 2</div></div><!--]--><div nuxt-slot-fallback-end></div></div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"fallback\\" nuxt-ssr-slot-data=\\"[{&quot;t&quot;:&quot;fall&quot;},{&quot;t&quot;:&quot;back&quot;}]\\"><div nuxt-slot-fallback-start=\\"fallback\\"></div><!--[--><div style=\\"display:contents;\\"><div>fall slot -- index: 0</div><div class=\\"fallback-slot-content\\"> wonderful fallback </div></div><div style=\\"display:contents;\\"><div>back slot -- index: 1</div><div class=\\"fallback-slot-content\\"> wonderful fallback </div></div><!--]--><div nuxt-slot-fallback-end></div></div></div>",
"state": {},
}
`)

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('"97.5k"')
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.4k"')
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
[
"_nuxt/entry.js",
@ -32,7 +32,7 @@ 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.5k"')
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.4k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2330k"')

View File

@ -56,9 +56,9 @@ describe('API routes', () => {
it('works with useFetch', () => {
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
// @ts-expect-error TODO: remove when fixed upstream: https://github.com/unjs/nitro/pull/1247
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
// @ts-expect-error not a valid method
useFetch('/api/hey', { method: 'PATCH' })

View File

@ -8,6 +8,7 @@
<div id="long-async-component-count">
{{ count }}
</div>
{{ headers['custom-head'] }}
<slot name="test" :count="count" />
<p>hello world !!!</p>
<slot v-for="(t, index) in 3" name="hello" :t="t">
@ -28,8 +29,12 @@
</template>
<script setup lang="ts">
import { getResponseHeaders } from 'h3'
defineProps<{
count: number
}>()
const evt = useRequestEvent()
const headers = getResponseHeaders(evt)
const { data } = await useFetch('/api/very-long-request')
</script>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
const page = ref<HTMLDivElement | undefined>()
const mountedHTML = ref()
onMounted(() => {
mountedHTML.value = page.value?.innerHTML
})
const lazy = useRoute().query.lazy === 'true'
</script>
<template>
<div ref="page" class="end-page">
End page
<pre>{{ mountedHTML }}</pre>
<section id="fallback">
<AsyncServerComponent :lazy="lazy" :count="42">
<template #fallback>
Loading server component
</template>
</AsyncServerComponent>
</section>
<section id="no-fallback">
<AsyncServerComponent :lazy="lazy" :count="42" />
</section>
</div>
</template>

View File

@ -0,0 +1,10 @@
<template>
<div>
<NuxtLink to="/server-components/lazy/end?lazy=true">
Go to page with lazy server component
</NuxtLink>
<NuxtLink to="/server-components/lazy/end?lazy=false">
Go to page without lazy server component
</NuxtLink>
</div>
</template>

View File

@ -0,0 +1,12 @@
import { setHeader } from 'h3'
export default defineNuxtPlugin({
name: 'server-only-plugin',
setup () {
const evt = useRequestEvent()
setHeader(evt, 'custom-head', 'hello')
},
env: {
islands: false
}
})