mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-18 14:41:25 +00:00
Merge branch 'main' into docs/kit
This commit is contained in:
commit
2350a1f75e
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -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:
|
||||
|
2
.github/ISSUE_TEMPLATE/z-bug-report-2.yml
vendored
2
.github/ISSUE_TEMPLATE/z-bug-report-2.yml
vendored
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -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') || '.'
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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: {
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}"` : ''
|
||||
}
|
@ -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"
|
||||
},
|
||||
|
@ -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'
|
||||
|
@ -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)) {
|
||||
|
153
packages/nuxt/src/app/components/nuxt-layout.ts
Normal file
153
packages/nuxt/src/app/components/nuxt-layout.ts
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
@ -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<
|
||||
|
@ -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.')
|
||||
|
@ -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)))
|
||||
|
@ -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') {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 () => {}'
|
||||
|
@ -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, {
|
||||
|
@ -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) {
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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')
|
||||
|
@ -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",
|
||||
|
@ -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>}
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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=\\"[{"count":3}]\\"></div><p>hello world !!!</p><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"hello\\" nuxt-ssr-slot-data=\\"[{"t":0},{"t":1},{"t":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=\\"[{"t":"fall"},{"t":"back"}]\\"><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=\\"[{"count":3}]\\"></div><p>hello world !!!</p><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"hello\\" nuxt-ssr-slot-data=\\"[{"t":0},{"t":1},{"t":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=\\"[{"t":"fall"},{"t":"back"}]\\"><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": {},
|
||||
}
|
||||
`)
|
||||
|
@ -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"')
|
||||
|
2
test/fixtures/basic-types/types.ts
vendored
2
test/fixtures/basic-types/types.ts
vendored
@ -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' })
|
||||
|
@ -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>
|
||||
|
26
test/fixtures/basic/pages/server-components/lazy/end.vue
vendored
Normal file
26
test/fixtures/basic/pages/server-components/lazy/end.vue
vendored
Normal 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>
|
10
test/fixtures/basic/pages/server-components/lazy/start.vue
vendored
Normal file
10
test/fixtures/basic/pages/server-components/lazy/start.vue
vendored
Normal 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>
|
12
test/fixtures/basic/plugins/server-only.server.ts
vendored
Normal file
12
test/fixtures/basic/plugins/server-only.server.ts
vendored
Normal 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
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user