Merge branch 'main' into payload-extraction

This commit is contained in:
Saeid Zareie 2025-02-07 00:00:25 +03:30 committed by GitHub
commit 66c3549e1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
66 changed files with 1505 additions and 1537 deletions

View File

@ -18,8 +18,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"

View File

@ -14,8 +14,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"

View File

@ -25,8 +25,8 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"

View File

@ -38,8 +38,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -78,7 +78,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5
uses: github/codeql-action/init@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
with:
config: |
paths:
@ -95,7 +95,7 @@ jobs:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5
uses: github/codeql-action/analyze@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
with:
category: "/language:${{ matrix.language }}"
@ -112,8 +112,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -143,8 +143,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -170,8 +170,8 @@ jobs:
- build
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -195,8 +195,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -220,8 +220,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -280,8 +280,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: ${{ matrix.node }}
cache: "pnpm"
@ -332,8 +332,8 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -364,8 +364,8 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"

View File

@ -29,7 +29,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Lychee link checker
uses: lycheeverse/lychee-action@f796c8b7d468feb9b8c0a46da3fac0af6874d374 # for v1.8.0
uses: lycheeverse/lychee-action@f613c4a64e50d792e0b31ec34bbcbba12263c6a6 # for v1.8.0
with:
# arguments with file types to check
args: >-

View File

@ -22,8 +22,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"

View File

@ -26,8 +26,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"

View File

@ -22,8 +22,8 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
registry-url: "https://registry.npmjs.org/"

View File

@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
if: github.repository == 'nuxt/nuxt' && success()
with:
sarif_file: results.sarif

View File

@ -176,7 +176,7 @@ Now, before navigation to that page can complete, the `auth` route middleware wi
:link-example{to="/docs/examples/routing/middleware"}
## Setting Middleware At Build Time
## Setting Middleware at Build Time
Instead of using `definePageMeta` on each page, you can add named route middleware within the `pages:extend` hook.

View File

@ -190,7 +190,7 @@ This configuration will observe when the element enters the viewport and also li
### Enable Cross-origin Prefetch
To enable cross-origin prefetching, you can set the `crossOriginPrefetch` option in your `nuxt.config`. This will enabled cross-origin prefetch using the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API).
To enable cross-origin prefetching, you can set the `crossOriginPrefetch` option in your `nuxt.config`. This will enable cross-origin prefetching using the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API).
```ts [nuxt.config.ts]
export default defineNuxtConfig({

View File

@ -34,4 +34,7 @@ exclude = [
# single-quotes are required for regexp
'(https?:\/\/github\.com\/)(.*\/)(generate)',
"https://github.com/nuxt-contrib/vue3-ssr-starter/generate",
# excluded URLs from test suite
"http://auth.com",
"http://example2.com/",
]

View File

@ -1,5 +1,6 @@
// For pnpm typecheck:docs to generate correct types
import { fileURLToPath } from 'node:url'
import { addPluginTemplate, addRouteMiddleware } from 'nuxt/kit'
export default defineNuxtConfig({
@ -17,6 +18,9 @@ export default defineNuxtConfig({
},
],
pages: process.env.DOCS_TYPECHECK === 'true',
dir: {
app: fileURLToPath(new URL('./test/runtime/app', import.meta.url)),
},
typescript: {
shim: process.env.DOCS_TYPECHECK === 'true',
hoist: ['@vitejs/plugin-vue', 'vue-router'],

View File

@ -39,13 +39,13 @@
"resolutions": {
"@babel/core": "7.26.7",
"@babel/helper-plugin-utils": "7.26.5",
"@nuxt/cli": "3.20.0",
"@nuxt/cli": "3.21.1",
"@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
"@types/node": "22.10.10",
"@types/node": "22.13.1",
"@unhead/dom": "1.11.18",
"@unhead/schema": "1.11.18",
"@unhead/shared": "1.11.18",
@ -58,18 +58,18 @@
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"jiti": "2.4.2",
"magic-string": "^0.30.17",
"nitro": "npm:nitro-nightly@3.0.0-beta-28938837.19ec5395",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"nuxt": "workspace:*",
"ohash": "1.1.4",
"postcss": "8.5.1",
"rollup": "4.32.0",
"rollup": "4.34.4",
"send": ">=1.1.0",
"typescript": "5.7.3",
"ufo": "1.5.4",
"unbuild": "3.3.1",
"unhead": "1.11.18",
"unimport": "4.0.0",
"vite": "6.0.11",
"vite": "6.1.0",
"vue": "3.5.13"
},
"devDependencies": {
@ -77,8 +77,8 @@
"@babel/core": "7.26.7",
"@babel/helper-plugin-utils": "7.26.5",
"@codspeed/vitest-plugin": "4.0.0",
"@nuxt/cli": "3.20.0",
"@nuxt/eslint-config": "0.7.5",
"@nuxt/cli": "3.21.1",
"@nuxt/eslint-config": "1.0.0",
"@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*",
"@nuxt/test-utils": "3.15.4",
@ -86,11 +86,11 @@
"@testing-library/vue": "8.1.0",
"@types/babel__core": "7.20.5",
"@types/babel__helper-plugin-utils": "7.10.3",
"@types/node": "22.10.10",
"@types/node": "22.13.1",
"@types/semver": "7.5.8",
"@unhead/schema": "1.11.18",
"@unhead/vue": "1.11.18",
"@vitest/coverage-v8": "3.0.4",
"@vitest/coverage-v8": "3.0.5",
"@vue/test-utils": "2.4.6",
"acorn": "8.14.0",
"autoprefixer": "10.4.20",
@ -102,40 +102,40 @@
"devalue": "5.1.1",
"eslint": "9.19.0",
"eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "4.7.0",
"eslint-plugin-perfectionist": "4.8.0",
"eslint-typegen": "1.0.0",
"estree-walker": "3.0.3",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"happy-dom": "16.7.2",
"happy-dom": "17.0.0",
"installed-check": "9.3.0",
"jiti": "2.4.2",
"knip": "5.43.3",
"knip": "5.43.6",
"magic-string": "0.30.17",
"markdownlint-cli": "0.44.0",
"memfs": "4.17.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28938837.19ec5395",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.2",
"ofetch": "1.4.1",
"pathe": "2.0.2",
"pkg-pr-new": "0.0.39",
"playwright-core": "1.50.0",
"rollup": "4.32.0",
"semver": "7.6.3",
"sherif": "1.2.0",
"playwright-core": "1.50.1",
"rollup": "4.34.4",
"semver": "7.7.1",
"sherif": "1.3.0",
"std-env": "3.8.0",
"tinyexec": "0.3.2",
"tinyglobby": "0.2.10",
"ts-blank-space": "0.5.0",
"ts-blank-space": "0.5.1",
"typescript": "5.7.3",
"ufo": "1.5.4",
"unbuild": "3.3.1",
"vitest": "3.0.4",
"vitest": "3.0.5",
"vitest-environment-nuxt": "1.0.1",
"vue": "3.5.13",
"vue-tsc": "2.2.0",
"webpack": "5.97.1"
},
"packageManager": "pnpm@9.15.4",
"packageManager": "pnpm@10.2.0",
"version": ""
}

View File

@ -27,7 +27,6 @@
"test:attw": "attw --pack"
},
"dependencies": {
"@nuxt/schema": "workspace:*",
"c12": "^2.0.1",
"consola": "^3.4.0",
"defu": "^6.1.4",
@ -42,7 +41,7 @@
"pathe": "^2.0.2",
"pkg-types": "^1.3.1",
"scule": "^1.3.0",
"semver": "^7.6.3",
"semver": "^7.7.1",
"std-env": "^3.8.0",
"ufo": "^1.5.4",
"unctx": "^2.4.1",
@ -50,12 +49,13 @@
"untyped": "^1.5.2"
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"@rspack/core": "1.2.2",
"@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28938837.19ec5395",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"unbuild": "3.3.1",
"vite": "6.0.11",
"vitest": "3.0.4",
"vite": "6.1.0",
"vitest": "3.0.5",
"webpack": "5.97.1"
},
"engines": {

View File

@ -1,27 +1,21 @@
import { existsSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
import type { JSValue } from 'untyped'
import { applyDefaults } from 'untyped'
import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
import { loadConfig } from 'c12'
import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
import { NuxtConfigSchema } from '@nuxt/schema'
import { globby } from 'globby'
import defu from 'defu'
import { join } from 'pathe'
import { isWindows } from 'std-env'
import { tryResolveModule } from '../internal/esm'
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
overrides?: Exclude<LoadConfigOptions<NuxtConfig>['overrides'], Promise<any> | Function>
}
const layerSchemaKeys = ['future', 'srcDir', 'rootDir', 'serverDir', 'dir']
const layerSchema = Object.create(null)
for (const key of layerSchemaKeys) {
if (key in NuxtConfigSchema) {
layerSchema[key] = NuxtConfigSchema[key]
}
}
export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> {
// Automatically detect and import layers from `~~/layers/` directory
opts.overrides = defu(opts.overrides, {
@ -54,6 +48,16 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
nuxtConfig.buildDir = join(nuxtConfig.rootDir!, 'node_modules/.cache/nuxt/.nuxt')
}
const NuxtConfigSchema = await loadNuxtSchema(nuxtConfig.rootDir || cwd || process.cwd())
const layerSchemaKeys = ['future', 'srcDir', 'rootDir', 'serverDir', 'dir']
const layerSchema = Object.create(null)
for (const key of layerSchemaKeys) {
if (key in NuxtConfigSchema) {
layerSchema[key] = NuxtConfigSchema[key]
}
}
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
const processedLayers = new Set<string>()
for (const layer of layers) {
@ -89,3 +93,13 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
// Resolve and apply defaults
return await applyDefaults(NuxtConfigSchema, nuxtConfig as NuxtConfig & Record<string, JSValue>) as unknown as NuxtOptions
}
async function loadNuxtSchema (cwd: string) {
const paths = [cwd]
const nuxtPath = await tryResolveModule('nuxt', cwd) ?? await tryResolveModule('nuxt-nightly', cwd)
if (nuxtPath) {
paths.unshift(nuxtPath)
}
const schemaPath = await tryResolveModule('@nuxt/schema', paths) ?? '@nuxt/schema'
return await import(isWindows ? pathToFileURL(schemaPath).href : schemaPath).then(r => r.NuxtConfigSchema)
}

View File

@ -120,7 +120,7 @@ function _defineNuxtModule<
// Measure setup time
if (setupTime > 5000 && uniqueKey !== '@nuxt/telemetry') {
logger.warn(`Slow module \`${uniqueKey || '<no name>'}\` took \`${setupTime}ms\` to setup.`)
} else if (nuxt.options.debug) {
} else if (nuxt.options.debug && nuxt.options.debug.modules) {
logger.info(`Module \`${uniqueKey || '<no name>'}\` took \`${setupTime}ms\` to setup.`)
}

View File

@ -0,0 +1,76 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { useRuntimeConfig } from './runtime-config'
const { useNuxt, klona } = vi.hoisted(() => ({ useNuxt: vi.fn(), klona: vi.fn() }))
vi.mock('./context', () => ({ useNuxt }))
vi.mock('klona', () => ({ klona }))
const testCases = [
{
description:
'should return runtime config with environment variables applied',
runtimeConfig: {
apiUrl: 'http://localhost',
authUrl: 'http://auth.com',
},
envExpansion: true,
env: {
NITRO_API_URL: 'http://example.com',
},
expected: {
apiUrl: 'http://example.com',
authUrl: 'http://auth.com',
},
},
{
description: 'should expand environment variables in strings',
runtimeConfig: {
apiUrl: '{{BASE_URL}}/api',
mail: '{{MAIL_SCHEME}}://{{MAIL_HOST}}:{{MAIL_PORT}}',
},
envExpansion: true,
env: {
BASE_URL: 'http://example.com',
MAIL_SCHEME: 'http',
MAIL_HOST: 'localhost',
MAIL_PORT: '3366',
},
expected: {
apiUrl: 'http://example.com/api',
mail: 'http://localhost:3366',
},
},
{
description:
'should not expand environment variables if envExpansion is false',
runtimeConfig: {
apiUrl: '{{BASE_URL}}/api',
someUrl: '',
},
envExpansion: false,
env: {
BASE_URL: 'http://example1.com',
NITRO_NOT_API_URL: 'http://example2.com',
NUXT_SOME_URL: 'http://example3.com',
},
expected: {
apiUrl: '{{BASE_URL}}/api',
someUrl: 'http://example3.com',
},
},
]
describe('useRuntimeConfig', () => {
afterEach(() => {
vi.unstubAllEnvs()
})
it.each(testCases)('$description', ({ runtimeConfig, envExpansion, env, expected }) => {
useNuxt.mockReturnValue({ options: { nitro: { runtimeConfig, experimental: { envExpansion } } } })
klona.mockReturnValue(runtimeConfig)
Object.entries(env).forEach(([key, value]) => vi.stubEnv(key, value))
expect(useRuntimeConfig()).toEqual(expected)
})
})

View File

@ -94,7 +94,7 @@ function applyEnv (
return obj
}
const envExpandRx = /\{\{(.*?)\}\}/g
const envExpandRx = /\{\{([^{}]*)\}\}/g
function _expandFromEnv (value: string, env: Record<string, any> = process.env) {
return value.replace(envExpandRx, (match, key) => {

View File

@ -1,17 +1,25 @@
import { fileURLToPath } from 'node:url'
import { afterAll, bench, describe } from 'vitest'
import { join, normalize } from 'pathe'
import { rm } from 'node:fs/promises'
import { afterAll, beforeAll, bench, describe } from 'vitest'
import { join, normalize, resolve } from 'pathe'
import { withoutTrailingSlash } from 'ufo'
import { loadNuxt, writeTypes } from '@nuxt/kit'
import type { Nuxt } from 'nuxt/schema'
describe('writeTypes', async () => {
describe('writeTypes', () => {
const relativeDir = join('../../..', 'test/fixtures/basic-types')
const path = withoutTrailingSlash(normalize(fileURLToPath(new URL(relativeDir, import.meta.url))))
const nuxt = await loadNuxt({ cwd: path })
let nuxt: Nuxt
beforeAll(async () => {
nuxt = await loadNuxt({ cwd: path })
await rm(resolve(path, '.nuxt'), { recursive: true, force: true })
}, 20_000)
afterAll(async () => {
await nuxt.close()
})
}, 20_000)
bench('writeTypes in the basic-types fixture', async () => {
await writeTypes(nuxt)

View File

@ -64,9 +64,9 @@
"test:attw": "attw --pack"
},
"dependencies": {
"@nuxt/cli": "^3.20.0",
"@nuxt/cli": "^3.21.1",
"@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.7.0",
"@nuxt/devtools": "^2.0.0",
"@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.4",
@ -100,8 +100,8 @@
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"nanotar": "^0.2.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28938837.19ec5395",
"nypm": "^0.5.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"nypm": "^0.5.2",
"ofetch": "^1.4.1",
"ohash": "^1.1.4",
"pathe": "^2.0.2",
@ -109,7 +109,7 @@
"pkg-types": "^1.3.1",
"radix3": "^1.1.2",
"scule": "^1.3.0",
"semver": "^7.6.3",
"semver": "^7.7.1",
"std-env": "^3.8.0",
"strip-literal": "^3.0.0",
"tinyglobby": "0.2.10",
@ -136,8 +136,8 @@
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
"unbuild": "3.3.1",
"vite": "6.0.11",
"vitest": "3.0.4"
"vite": "6.1.0",
"vitest": "3.0.5"
},
"peerDependencies": {
"@parcel/watcher": "^2.1.0",

View File

@ -1,4 +1,4 @@
import type { DefineComponent, MaybeRef, VNode } from 'vue'
import type { DefineComponent, ExtractPublicPropTypes, MaybeRef, PropType, VNode } from 'vue'
import { Suspense, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
@ -30,19 +30,23 @@ const LayoutLoader = defineComponent({
},
})
// props are moved outside of defineComponent to later explicitly assert the prop types
// this avoids type loss/simplification resulting in things like MaybeRef<string | false>, keeping type hints for layout names
const nuxtLayoutProps = {
name: {
type: [String, Boolean, Object] as PropType<unknown extends PageMeta['layout'] ? MaybeRef<string | false> : PageMeta['layout']>,
default: null,
},
fallback: {
type: [String, Object] as PropType<unknown extends PageMeta['layout'] ? MaybeRef<string> : PageMeta['layout']>,
default: null,
},
}
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,
},
fallback: {
type: [String, Object] as unknown as () => unknown extends PageMeta['layout'] ? MaybeRef<string> : PageMeta['layout'],
default: null,
},
},
props: nuxtLayoutProps,
setup (props, context) {
const nuxtApp = useNuxtApp()
// Need to ensure (if we are not a child of `<NuxtPage>`) that we use synchronous route (not deferred)
@ -95,9 +99,7 @@ export default defineComponent({
}).default()
}
},
}) as unknown as DefineComponent<{
name?: (unknown extends PageMeta['layout'] ? MaybeRef<string | false> : PageMeta['layout']) | undefined
}>
}) as DefineComponent<ExtractPublicPropTypes<typeof nuxtLayoutProps>>
const LayoutProvider = defineComponent({
name: 'NuxtLayoutProvider',

View File

@ -3,7 +3,8 @@ import type { Ref, VNode } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { PageRouteSymbol } from './injections'
export const RouteProvider = defineComponent({
export const defineRouteProvider = (name = 'RouteProvider') => defineComponent({
name,
props: {
vnode: {
type: Object as () => VNode,
@ -55,3 +56,5 @@ export const RouteProvider = defineComponent({
}
},
})
export const RouteProvider = defineRouteProvider()

View File

@ -18,7 +18,7 @@ export const useRouter: typeof _useRouter = () => {
/** @since 3.0.0 */
export const useRoute: typeof _useRoute = () => {
if (import.meta.dev && isProcessingMiddleware()) {
if (import.meta.dev && !getCurrentInstance() && isProcessingMiddleware()) {
console.warn('[nuxt] Calling `useRoute` within middleware may lead to misleading results. Instead, use the (to, from) arguments passed to the middleware to access the new and old routes.')
}
if (hasInjectionContext()) {

View File

@ -8,7 +8,7 @@ export { defineNuxtLink } from './components/index'
export type { NuxtLinkOptions, NuxtLinkProps } from './components/index'
export { _getAppConfig, updateAppConfig, useAppConfig } from './config'
export { cancelIdleCallback, requestIdleCallback } from './compat/idle-callback'
export type { NuxtAppLiterals, NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext, PageMeta } from './types'
export type { NuxtAppLiterals, NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext, PageMeta, NuxtPageProps } from './types'
export const isVue2 = false
export const isVue3 = true

View File

@ -2,7 +2,7 @@ import { createDebugger } from 'hookable'
import { defineNuxtPlugin } from '../nuxt'
export default defineNuxtPlugin({
name: 'nuxt:debug',
name: 'nuxt:debug:hooks',
enforce: 'pre',
setup (nuxtApp) {
createDebugger(nuxtApp.hooks, { tag: 'nuxt-app' })

View File

@ -1,4 +1,4 @@
export type { PageMeta } from '../pages/runtime/index'
export type { PageMeta, NuxtPageProps } from '../pages/runtime/index'
export interface NuxtAppLiterals {
[key: string]: string

View File

@ -88,7 +88,7 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
const perf = performance.now() - start
const setupTime = Math.round((perf * 100)) / 100
if (nuxt.options.debug || setupTime > 500) {
if ((nuxt.options.debug && nuxt.options.debug.templates) || setupTime > 500) {
logger.info(`Compiled \`${template.filename}\` in ${setupTime}ms`)
}

View File

@ -108,6 +108,18 @@ function createWatcher () {
ignored: [isIgnored, /[\\/]node_modules[\\/]/],
})
const restartPaths = new Set<string>()
const srcDir = nuxt.options.srcDir.replace(/\/?$/, '/')
for (const pattern of nuxt.options.watch) {
if (typeof pattern !== 'string') { continue }
const path = resolve(nuxt.options.srcDir, pattern)
if (!path.startsWith(srcDir)) {
restartPaths.add(path)
}
}
watcher.add([...restartPaths])
watcher.on('all', (event, path) => {
if (event === 'all' || event === 'ready' || event === 'error' || event === 'raw') {
return
@ -121,7 +133,7 @@ function createGranularWatcher () {
const nuxt = useNuxt()
const isIgnored = createIsIgnored(nuxt)
if (nuxt.options.debug) {
if (nuxt.options.debug && nuxt.options.debug.watchers) {
// eslint-disable-next-line no-console
console.time('[nuxt] builder:chokidar:watch')
}
@ -166,7 +178,7 @@ function createGranularWatcher () {
})
watcher.on('ready', () => {
pending--
if (nuxt.options.debug && !pending) {
if (nuxt.options.debug && nuxt.options.debug.watchers && !pending) {
// eslint-disable-next-line no-console
console.timeEnd('[nuxt] builder:chokidar:watch')
}
@ -177,7 +189,7 @@ function createGranularWatcher () {
async function createParcelWatcher () {
const nuxt = useNuxt()
if (nuxt.options.debug) {
if (nuxt.options.debug && nuxt.options.debug.watchers) {
// eslint-disable-next-line no-console
console.time('[nuxt] builder:parcel:watch')
}
@ -203,7 +215,7 @@ async function createParcelWatcher () {
],
})
watcher.then((subscription) => {
if (nuxt.options.debug) {
if (nuxt.options.debug && nuxt.options.debug.watchers) {
// eslint-disable-next-line no-console
console.timeEnd('[nuxt] builder:parcel:watch')
}

View File

@ -52,7 +52,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
const isNuxtV4 = nuxt.options.future?.compatibilityVersion === 4
const nitroConfig: NitroConfig = defu(nuxt.options.nitro, {
debug: nuxt.options.debug,
debug: nuxt.options.debug ? nuxt.options.debug.nitro : false,
rootDir: nuxt.options.rootDir,
workspaceDir: nuxt.options.workspaceDir,
srcDir: nuxt.options.serverDir,

View File

@ -83,7 +83,6 @@ const nightlies = {
export const keyDependencies = [
'@nuxt/kit',
'@nuxt/schema',
]
let warnedAboutCompatDate = false
@ -414,7 +413,7 @@ async function initNuxt (nuxt: Nuxt) {
await nuxt.callHook('modules:before')
const modulesToInstall = new Map<string | NuxtModule, Record<string, any>>()
const watchedPaths = new Set<string>()
const modulePaths = new Set<string>()
const specifiedModules = new Set<string>()
for (const _mod of nuxt.options.modules) {
@ -432,13 +431,15 @@ async function initNuxt (nuxt: Nuxt) {
`${modulesDir}/*/index{${nuxt.options.extensions.join(',')}}`,
])
for (const mod of layerModules) {
watchedPaths.add(mod)
modulePaths.add(mod)
if (specifiedModules.has(mod)) { continue }
specifiedModules.add(mod)
modulesToInstall.set(mod, {})
}
}
nuxt.options.watch.push(...modulePaths)
// Register user and then ad-hoc modules
for (const key of ['modules', '_modules'] as const) {
for (const item of nuxt.options[key as 'modules']) {
@ -554,9 +555,13 @@ async function initNuxt (nuxt: Nuxt) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/preload.server'))
}
// Add nuxt app debugger
if (nuxt.options.debug) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/debug'))
// Add nuxt app hooks debugger
if (
nuxt.options.debug
&& nuxt.options.debug.hooks
&& (nuxt.options.debug.hooks === true || nuxt.options.debug.hooks.client)
) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/debug-hooks'))
}
// Add experimental Chrome devtools timings support
@ -661,7 +666,7 @@ export default defineNuxtPlugin({
nuxt.hooks.hook('builder:watch', (event, relativePath) => {
const path = resolve(nuxt.options.srcDir, relativePath)
// Local module patterns
if (watchedPaths.has(path)) {
if (modulePaths.has(path)) {
return nuxt.callHook('restart', { hard: true })
}
@ -739,7 +744,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
: options.devtools?.enabled !== false // enabled by default unless explicitly disabled
if (isDevToolsEnabled) {
if (!options._modules.some(m => m === '@nuxt/devtools' || m === '@nuxt/devtools-edge')) {
if (!options._modules.some(m => m === '@nuxt/devtools' || m === '@nuxt/devtools-nightly' || m === '@nuxt/devtools-edge')) {
options._modules.push('@nuxt/devtools')
}
}
@ -815,7 +820,11 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
nuxt.hooks.addHooks(opts.overrides.hooks)
}
if (nuxt.options.debug) {
if (
nuxt.options.debug
&& nuxt.options.debug.hooks
&& (nuxt.options.debug.hooks === true || nuxt.options.debug.hooks.server)
) {
createDebugger(nuxt.hooks, { tag: 'nuxt' })
}

View File

@ -14,7 +14,18 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
name: 'nuxt:resolve-bare-imports',
enforce: 'post',
configResolved (config) {
conditions = config.mode === 'test' ? [...config.resolve.conditions, 'import', 'require'] : config.resolve.conditions
const resolvedConditions = new Set([nuxt.options.dev ? 'development' : 'production', ...config.resolve.conditions])
if (resolvedConditions.has('browser')) {
resolvedConditions.add('web')
resolvedConditions.add('import')
resolvedConditions.add('module')
resolvedConditions.add('default')
}
if (config.mode === 'test') {
resolvedConditions.add('import')
resolvedConditions.add('require')
}
conditions = [...resolvedConditions]
},
async resolveId (id, importer) {
if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:') && !importer.startsWith('\0virtual:')) || exclude.some(e => id.startsWith(e))) {

View File

@ -674,7 +674,7 @@ function getServerComponentHTML (body: string): string {
const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/
const SSR_CLIENT_TELEPORT_MARKER = /^uid=([^;]*);client=(.*)$/
const SSR_CLIENT_SLOT_MARKER = /^island-slot=[^;]*;(.*)$/
const SSR_CLIENT_SLOT_MARKER = /^island-slot=([^;]*);(.*)$/
function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] {
if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.slots).length) { return undefined }
@ -698,21 +698,21 @@ function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandRespons
response[clientUid] = {
...component,
html,
slots: getComponentSlotTeleport(ssrContext.teleports ?? {}),
slots: getComponentSlotTeleport(clientUid, ssrContext.teleports ?? {}),
}
}
return response
}
function getComponentSlotTeleport (teleports: Record<string, string>) {
function getComponentSlotTeleport (clientUid: string, teleports: Record<string, string>) {
const entries = Object.entries(teleports)
const slots: Record<string, string> = {}
for (const [key, value] of entries) {
const match = key.match(SSR_CLIENT_SLOT_MARKER)
if (match) {
const [, slot] = match
if (!slot) { continue }
const [, id, slot] = match
if (!slot || clientUid !== id) { continue }
slots[slot] = value
}
}

View File

@ -174,7 +174,7 @@ export default defineNuxtModule({
const options: TypedRouterOptions = {
routesFolder: [],
dts: resolve(nuxt.options.buildDir, declarationFile),
logs: nuxt.options.debug,
logs: nuxt.options.debug && nuxt.options.debug.router,
async beforeWriteFiles (rootPage) {
rootPage.children.forEach(child => child.delete())
const pages = nuxt.apps.default?.pages || await resolvePagesRoutes(nuxt)

View File

@ -1,2 +1,3 @@
export { definePageMeta, defineRouteRules } from './composables'
export type { PageMeta } from './composables'
export type { NuxtPageProps } from './page'

View File

@ -1,12 +1,12 @@
import { Fragment, Suspense, defineComponent, h, inject, nextTick, ref, watch } from 'vue'
import type { KeepAliveProps, TransitionProps, VNode } from 'vue'
import type { AllowedComponentProps, Component, ComponentCustomProps, ComponentPublicInstance, KeepAliveProps, Slot, TransitionProps, VNode, VNodeProps } from 'vue'
import { RouterView } from 'vue-router'
import { defu } from 'defu'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouterViewProps } from 'vue-router'
import { generateRouteKey, toArray, wrapInKeepAlive } from './utils'
import type { RouterViewSlotProps } from './utils'
import { RouteProvider } from '#app/components/route-provider'
import { RouteProvider, defineRouteProvider } from '#app/components/route-provider'
import { useNuxtApp } from '#app/nuxt'
import { useRouter } from '#app/composables/router'
import { _wrapInTransition } from '#app/components/utils'
@ -14,6 +14,23 @@ import { LayoutMetaSymbol, PageRouteSymbol } from '#app/components/injections'
// @ts-expect-error virtual file
import { appKeepalive as defaultKeepaliveConfig, appPageTransition as defaultPageTransition } from '#build/nuxt.config.mjs'
export interface NuxtPageProps extends RouterViewProps {
/**
* Define global transitions for all pages rendered with the `NuxtPage` component.
*/
transition?: boolean | TransitionProps
/**
* Control state preservation of pages rendered with the `NuxtPage` component.
*/
keepalive?: boolean | KeepAliveProps
/**
* Control when the `NuxtPage` component is re-rendered.
*/
pageKey?: string | ((route: RouteLocationNormalizedLoaded) => string)
}
export default defineComponent({
name: 'NuxtPage',
inheritAttrs: false,
@ -66,6 +83,9 @@ export default defineComponent({
nuxtApp._isNuxtPageUsed = true
}
let pageLoadingEndHookAlreadyCalled = false
const routerProviderLookup = new WeakMap<Component, ReturnType<typeof defineRouteProvider> | undefined>()
return () => {
return h(RouterView, { name: props.name, route: props.route, ...attrs }, {
default: (routeProps: RouterViewSlotProps) => {
@ -111,7 +131,7 @@ export default defineComponent({
default: () => {
const providerVNode = h(RouteProvider, {
key: key || undefined,
vnode: slots.default ? h(Fragment, undefined, slots.default(routeProps)) : routeProps.Component,
vnode: slots.default ? normalizeSlot(slots.default, routeProps) : routeProps.Component,
route: routeProps.route,
renderKey: key || undefined,
vnodeRef: pageRef,
@ -124,7 +144,6 @@ export default defineComponent({
}
// Client side rendering
const hasTransition = !!(props.transition ?? routeProps.route.meta.pageTransition ?? defaultPageTransition)
const transitionProps = hasTransition && _mergeTransitionProps([
props.transition,
@ -148,18 +167,28 @@ export default defineComponent({
},
}, {
default: () => {
const providerVNode = h(RouteProvider, {
const routeProviderProps = {
key: key || undefined,
vnode: slots.default ? h(Fragment, undefined, slots.default(routeProps)) : routeProps.Component,
vnode: slots.default ? normalizeSlot(slots.default, routeProps) : routeProps.Component,
route: routeProps.route,
renderKey: key || undefined,
trackRootNodes: hasTransition,
vnodeRef: pageRef,
})
if (keepaliveConfig) {
(providerVNode.type as any).name = (routeProps.Component.type as any).name || (routeProps.Component.type as any).__name || 'RouteProvider'
}
return providerVNode
if (!keepaliveConfig) {
return h(RouteProvider, routeProviderProps)
}
const routerComponentType = routeProps.Component.type as any
let PageRouteProvider = routerProviderLookup.get(routerComponentType)
if (!PageRouteProvider) {
PageRouteProvider = defineRouteProvider(routerComponentType.name || routerComponentType.__name)
routerProviderLookup.set(routerComponentType, PageRouteProvider)
}
return h(PageRouteProvider, routeProviderProps)
},
}),
)).default()
@ -169,7 +198,24 @@ export default defineComponent({
})
}
},
})
}) as unknown as {
new(): {
$props: AllowedComponentProps &
ComponentCustomProps &
VNodeProps &
NuxtPageProps
$slots: {
default?: (routeProps: RouterViewSlotProps) => VNode[]
}
// expose
/**
* Reference to the page component instance
*/
pageRef: Element | ComponentPublicInstance | null
}
}
function _mergeTransitionProps (routeProps: TransitionProps[]): TransitionProps {
const _props: TransitionProps[] = routeProps.map(prop => ({
@ -198,3 +244,8 @@ function hasChildrenRoutes (fork: RouteLocationNormalizedLoaded | null, newRoute
const index = newRoute.matched.findIndex(m => m.components?.default === Component?.type)
return index < newRoute.matched.length - 1
}
function normalizeSlot (slot: Slot, data: RouterViewSlotProps) {
const slotContent = slot(data)
return slotContent.length === 1 ? h(slotContent[0]!) : h(Fragment, undefined, slotContent)
}

View File

@ -0,0 +1,33 @@
import { fileURLToPath } from 'node:url'
import { rm } from 'node:fs/promises'
import { beforeAll, bench, describe } from 'vitest'
import { join, normalize } from 'pathe'
import { withoutTrailingSlash } from 'ufo'
import { build, loadNuxt } from 'nuxt'
const basicTestFixtureDir = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../test/fixtures/basic', import.meta.url))))
describe('build', () => {
beforeAll(async () => {
await rm(join(basicTestFixtureDir, '.nuxt'), { recursive: true, force: true })
})
bench('initial dev server build in the basic test fixture', async () => {
const nuxt = await loadNuxt({
cwd: basicTestFixtureDir,
ready: true,
overrides: {
dev: true,
sourcemap: false,
builder: {
bundle: () => Promise.resolve(),
},
},
})
await new Promise<void>((resolve) => {
nuxt.hook('build:done', () => resolve())
build(nuxt)
})
await nuxt.close()
})
})

View File

@ -57,8 +57,8 @@ describe('islandTransform - server and island components', () => {
<script setup lang="ts">
const someData = 'some data'
</script>`
, 'hello.server.vue')
</script>`,
'hello.server.vue')
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
"<template>
@ -130,8 +130,8 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
<script setup lang="ts">
const someData = 'some data'
</script>`
, 'hello.server.vue')
</script>`,
'hello.server.vue')
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
"<template>
@ -182,8 +182,8 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
const message = "Hello World";
</script>
`
, 'hello.server.vue')
`,
'hello.server.vue')
expect(normalizeLineEndings(result)).toMatchInlineSnapshot(`
"<template>

View File

@ -1,6 +1,7 @@
import { fileURLToPath } from 'node:url'
import { bench, describe } from 'vitest'
import { normalize } from 'pathe'
import { rm } from 'node:fs/promises'
import { beforeAll, bench, describe } from 'vitest'
import { join, normalize } from 'pathe'
import { withoutTrailingSlash } from 'ufo'
import { loadNuxt } from 'nuxt'
@ -8,6 +9,13 @@ const emptyDir = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../
const basicTestFixtureDir = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../test/fixtures/basic', import.meta.url))))
describe('loadNuxt', () => {
beforeAll(async () => {
await Promise.all([
rm(join(emptyDir, '.nuxt'), { recursive: true, force: true }),
rm(join(basicTestFixtureDir, '.nuxt'), { recursive: true, force: true }),
])
})
bench('loadNuxt in an empty directory', async () => {
const nuxt = await loadNuxt({
cwd: emptyDir,

View File

@ -75,7 +75,7 @@
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.32.0",
"rollup": "4.34.4",
"unbuild": "3.3.1",
"vue": "3.5.13"
},

View File

@ -51,7 +51,7 @@
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"hookable": "5.5.3",
"ignore": "7.0.3",
"nitro": "npm:nitro-nightly@3.0.0-beta-28938837.19ec5395",
"nitro": "npm:nitro-nightly@3.0.0-beta-28969273.f7aa9de6",
"ofetch": "1.4.1",
"pkg-types": "1.3.1",
"sass-loader": "16.0.4",
@ -60,7 +60,7 @@
"unctx": "2.4.1",
"unimport": "4.0.0",
"untyped": "1.5.2",
"vite": "6.0.11",
"vite": "6.1.0",
"vue": "3.5.13",
"vue-bundle-renderer": "2.1.1",
"vue-loader": "17.4.2",

View File

@ -314,7 +314,7 @@ export default defineUntypedSchema({
* animation: loader 400ms linear infinite;
* }
*
* \@-webkit-keyframes loader {
* @-webkit-keyframes loader {
* 0% {
* -webkit-transform: translate(-50%, -50%) rotate(0deg);
* }
@ -322,7 +322,7 @@ export default defineUntypedSchema({
* -webkit-transform: translate(-50%, -50%) rotate(360deg);
* }
* }
* \@keyframes loader {
* @keyframes loader {
* 0% {
* transform: translate(-50%, -50%) rotate(0deg);
* }

View File

@ -8,6 +8,7 @@ import { defu } from 'defu'
import { findWorkspaceDir } from 'pkg-types'
import type { RuntimeConfig } from '../types/config'
import type { NuxtDebugOptions } from '../types/debug'
export default defineUntypedSchema({
/**
@ -264,9 +265,32 @@ export default defineUntypedSchema({
* At the moment, it prints out hook names and timings on the server, and
* logs hook arguments as well in the browser.
*
* You can also set this to an object to enable specific debug options.
*
* @type {boolean | (typeof import('../src/types/debug').NuxtDebugOptions) | undefined}
*/
debug: {
$resolve: val => val ?? isDebug,
$resolve: (val: boolean | NuxtDebugOptions | undefined) => {
val ??= isDebug
if (val === false) {
return val
}
if (val === true) {
return {
templates: true,
modules: true,
watchers: true,
hooks: {
client: true,
server: true,
},
nitro: true,
router: true,
hydration: true,
} satisfies Required<NuxtDebugOptions>
}
return val
},
},
/**

View File

@ -2,6 +2,7 @@ import { consola } from 'consola'
import { resolve } from 'pathe'
import { isTest } from 'std-env'
import { defineUntypedSchema } from 'untyped'
import type { NuxtDebugOptions } from '../types/debug'
export default defineUntypedSchema({
/**
@ -20,9 +21,10 @@ export default defineUntypedSchema({
},
define: {
$resolve: async (val: Record<string, any> | undefined, get) => {
const [isDev, isDebug] = await Promise.all([get('dev'), get('debug')]) as [boolean, boolean]
const [isDev, debug] = await Promise.all([get('dev'), get('debug')]) as [boolean, boolean | NuxtDebugOptions]
return {
'__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': isDebug,
'__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': Boolean(debug && (debug === true || debug.hydration)),
'process.dev': isDev,
'import.meta.dev': isDev,
'process.test': isTest,

View File

@ -8,6 +8,7 @@ export type { AppHeadMetaObject, MetaObject, MetaObjectRaw, HeadAugmentations }
export type { ModuleDefinition, ModuleMeta, ModuleOptions, ModuleSetupInstallResult, ModuleSetupReturn, NuxtModule, ResolvedModuleOptions } from './types/module'
export type { Nuxt, NuxtApp, NuxtPlugin, NuxtPluginTemplate, NuxtTemplate, NuxtTypeTemplate, NuxtServerTemplate, ResolvedNuxtTemplate } from './types/nuxt'
export type { RouterConfig, RouterConfigSerializable, RouterOptions } from './types/router'
export type { NuxtDebugOptions } from './types/debug'
// Schema
export { default as NuxtConfigSchema } from './config/index'

View File

@ -76,9 +76,10 @@ export interface NuxtBuilder {
}
// Normalized Nuxt options available as `nuxt.options.*`
export interface NuxtOptions extends Omit<ConfigSchema, 'vue' | 'sourcemap' | 'builder' | 'postcss' | 'webpack'> {
export interface NuxtOptions extends Omit<ConfigSchema, 'vue' | 'sourcemap' | 'debug' | 'builder' | 'postcss' | 'webpack'> {
vue: Omit<ConfigSchema['vue'], 'config'> & { config?: Partial<Filter<VueAppConfig, string | boolean>> }
sourcemap: Required<Exclude<ConfigSchema['sourcemap'], boolean>>
debug: Required<Exclude<ConfigSchema['debug'], true>>
builder: '@nuxt/vite-builder' | '@nuxt/webpack-builder' | '@nuxt/rspack-builder' | NuxtBuilder
postcss: Omit<ConfigSchema['postcss'], 'order'> & { order: Exclude<ConfigSchema['postcss']['order'], string> }
webpack: ConfigSchema['webpack'] & {

View File

@ -0,0 +1,21 @@
import type { NitroOptions } from 'nitro/types'
export interface NuxtDebugOptions {
/** Debug for Nuxt templates */
templates?: boolean
/** Debug for modules setup timings */
modules?: boolean
/** Debug for file watchers */
watchers?: boolean
/** Debug options for Nitro */
nitro?: NitroOptions['debug']
/** Debug for production hydration mismatch */
hydration?: boolean
/** Debug for Vue Router */
router?: boolean
/** Debug for hooks, can be set to `true` or an object with `server` and `client` keys */
hooks?: boolean | {
server?: boolean
client?: boolean
}
}

View File

@ -19,7 +19,7 @@
"devDependencies": {
"@unocss/reset": "65.4.3",
"beasties": "0.2.0",
"html-validate": "9.1.3",
"html-validate": "9.2.1",
"htmlnano": "2.1.1",
"jiti": "2.4.2",
"knitwork": "1.2.0",
@ -30,6 +30,6 @@
"tinyexec": "0.3.2",
"tinyglobby": "0.2.10",
"unocss": "65.4.3",
"vite": "6.0.11"
"vite": "6.1.0"
}
}

View File

@ -44,7 +44,7 @@
<h2 class="font-semibold text-base mt-1">Modules</h2>
<p class="text-sm text-gray-700 dark:text-gray-200 group-hover:dark:text-gray-100">Discover our list of modules to supercharge your Nuxt project.</p>
</a>
<a href="https://nuxt.com/modules?utm_source=nuxt-welcome" target="_blank" class="relative flex flex-col gap-1 border p-6 rounded-lg border-gray-200 dark:border-white/10 dark:bg-white/5 bg-gray-50/10 group hover:dark:border-[#00DC82] hover:border-[#00DC82] transition-all">
<a href="https://nuxt.com/docs/examples?utm_source=nuxt-welcome" target="_blank" class="relative flex flex-col gap-1 border p-6 rounded-lg border-gray-200 dark:border-white/10 dark:bg-white/5 bg-gray-50/10 group hover:dark:border-[#00DC82] hover:border-[#00DC82] transition-all">
<div class="w-[32px] h-[32px] bg-[#00DC82]/5 flex items-center justify-center border rounded border-[#00DC82] transition-all dark:border-[#00DC82]/50 group-hover:dark:border-[#00DC82]/80 dark:bg-[#020420] text-[#00DC82] dark:text-[#00DC82]">
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M224 56v144a8 8 0 0 1-8 8H40a8 8 0 0 1-8-8V56a8 8 0 0 1 8-8h176a8 8 0 0 1 8 8Z" opacity=".2"/><path fill="currentColor" d="M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16Zm0 160H40V56h176v144ZM80 84a12 12 0 1 1-12-12a12 12 0 0 1 12 12Zm40 0a12 12 0 1 1-12-12a12 12 0 0 1 12 12Z"/></svg>
</div>
@ -93,6 +93,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584l-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/></svg>
</a>
</li>
<li>
<a
href="https://go.nuxt.com/bluesky"
target="_blank"
class="focus-visible:ring-2 text-gray-500 hover:text-[#020420] dark:text-gray-400 dark:hover:text-white"
>
<span class="sr-only">Nuxt Bluesky</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565C.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479c.815 2.736 3.713 3.66 6.383 3.364q.204-.03.415-.056q-.207.033-.415.056c-3.912.58-7.387 2.005-2.83 7.078c5.013 5.19 6.87-1.113 7.823-4.308c.953 3.195 2.05 9.271 7.733 4.308c4.267-4.308 1.172-6.498-2.74-7.078a9 9 0 0 1-.415-.056q.21.026.415.056c2.67.297 5.568-.628 6.383-3.364c.246-.828.624-5.79.624-6.478c0-.69-.139-1.861-.902-2.206c-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8"/></svg>
</a>
</li>
<li>
<a
href="https://go.nuxt.com/linkedin"

View File

@ -26,7 +26,7 @@
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"rollup": "4.32.0",
"rollup": "4.34.4",
"unbuild": "3.3.1",
"vue": "3.5.13"
},
@ -41,6 +41,7 @@
"defu": "^6.1.4",
"esbuild": "^0.24.2",
"escape-string-regexp": "^5.0.0",
"externality": "^1.0.2",
"get-port-please": "^3.1.2",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"jiti": "^2.4.2",
@ -50,13 +51,13 @@
"pathe": "^2.0.2",
"pkg-types": "^1.3.1",
"postcss": "^8.5.1",
"rollup-plugin-visualizer": "^5.13.1",
"rollup-plugin-visualizer": "^5.14.0",
"std-env": "^3.8.0",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^2.1.2",
"vite": "^6.0.11",
"vite-node": "^3.0.4",
"vite": "^6.1.0",
"vite-node": "^3.0.5",
"vite-plugin-checker": "^0.8.0",
"vue-bundle-renderer": "^2.1.1"
},

View File

@ -131,19 +131,6 @@ export async function buildClient (ctx: ViteBuildContext) {
},
},
plugins: [
{
name: 'nuxt:import-conditions',
enforce: 'post',
config (_config, env) {
if (env.mode !== 'test') {
return {
resolve: {
conditions: [ctx.nuxt.options.dev ? 'development' : 'production', 'web', 'browser', 'import', 'module', 'default'],
},
}
}
},
},
devStyleSSRPlugin({
srcDir: ctx.nuxt.options.srcDir,
buildAssetsURL: joinURL(ctx.nuxt.options.app.baseURL, ctx.nuxt.options.app.buildAssetsDir),

View File

@ -0,0 +1,36 @@
import type { ExternalsOptions } from 'externality'
import { ExternalsDefaults, isExternal } from 'externality'
import type { ViteDevServer } from 'vite'
import escapeStringRegexp from 'escape-string-regexp'
import { withTrailingSlash } from 'ufo'
import type { Nuxt } from 'nuxt/schema'
import { resolve } from 'pathe'
import { toArray } from '.'
export function createIsExternal (viteServer: ViteDevServer, nuxt: Nuxt) {
const externalOpts: ExternalsOptions = {
inline: [
/virtual:/,
/\.ts$/,
...ExternalsDefaults.inline || [],
...(
viteServer.config.ssr.noExternal && viteServer.config.ssr.noExternal !== true
? toArray(viteServer.config.ssr.noExternal)
: []
),
],
external: [
'#shared',
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(nuxt.options.rootDir, nuxt.options.dir.shared)))),
...(viteServer.config.ssr.external as string[]) || [],
/node_modules/,
],
resolve: {
modules: nuxt.options.modulesDir,
type: 'module',
extensions: ['.ts', '.js', '.json', '.vue', '.mjs', '.jsx', '.tsx', '.wasm'],
},
}
return (id: string) => isExternal(id, nuxt.options.rootDir, externalOpts)
}

View File

@ -8,9 +8,11 @@ import { isFileServingAllowed } from 'vite'
import type { ModuleNode, Plugin as VitePlugin } from 'vite'
import { getQuery } from 'ufo'
import { normalizeViteManifest } from 'vue-bundle-renderer'
import { resolve as resolveModule } from 'mlly'
import { distDir } from './dirs'
import type { ViteBuildContext } from './vite'
import { isCSS } from './utils'
import { createIsExternal } from './utils/external'
// TODO: Remove this in favor of registerViteNodeMiddleware
// after Nitropack or h3 allows adding middleware after setup
@ -126,6 +128,15 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
},
})
const isExternal = createIsExternal(viteServer, ctx.nuxt)
node.shouldExternalize = async (id: string) => {
const result = await isExternal(id)
if (result?.external) {
return resolveModule(result.id, { url: ctx.nuxt.options.modulesDir }).catch(() => false)
}
return false
}
return eventHandler(async (event) => {
const moduleId = decodeURI(event.path).substring(1)
if (moduleId === '/') {

View File

@ -85,7 +85,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
// https://github.com/vitejs/vite/tree/main/packages/vite/src/node/build.ts#L464-L478
assetFileNames: nuxt.options.dev
? undefined
: chunk => withoutLeadingSlash(join(nuxt.options.app.buildAssetsDir, `${sanitizeFilePath(filename(chunk.name!))}.[hash].[ext]`)),
: chunk => withoutLeadingSlash(join(nuxt.options.app.buildAssetsDir, `${sanitizeFilePath(filename(chunk.names[0]!))}.[hash].[ext]`)),
},
},
watch: {

View File

@ -77,7 +77,7 @@
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.32.0",
"rollup": "4.34.4",
"unbuild": "3.3.1",
"vue": "3.5.13"
},

View File

@ -33,6 +33,6 @@ export function vue (ctx: WebpackConfigContext) {
ctx.config.plugins!.push(new webpack.DefinePlugin({
'__VUE_OPTIONS_API__': 'true',
'__VUE_PROD_DEVTOOLS__': 'false',
'__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': ctx.nuxt.options.debug,
'__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': ctx.nuxt.options.debug && ctx.nuxt.options.debug.hydration,
}))
}

View File

@ -1,6 +1,6 @@
import pify from 'pify'
import { resolve } from 'pathe'
import { defineEventHandler, fromNodeMiddleware, handleCors, setHeader } from 'h3'
import { createError, defineEventHandler, fromNodeMiddleware, getRequestHeader, handleCors, setHeader } from 'h3'
import type { H3CorsOptions } from 'h3'
import type { IncomingMessage, MultiWatching, ServerResponse } from 'webpack-dev-middleware'
import webpackDevMiddleware from 'webpack-dev-middleware'
@ -146,6 +146,12 @@ function wdmToH3Handler (devMiddleware: webpackDevMiddleware.API<IncomingMessage
if (isPreflight) {
return null
}
// disallow cross-site requests in no-cors mode
if (getRequestHeader(event, 'sec-fetch-mode') === 'no-cors' && getRequestHeader(event, 'sec-fetch-site') === 'cross-site') {
throw createError({ statusCode: 403 })
}
setHeader(event, 'Vary', 'Origin')
event.context.webpack = {

File diff suppressed because it is too large Load Diff

View File

@ -1943,6 +1943,7 @@ describe('server components/islands', () => {
// test islands mounted client side with slot
await page.locator('#show-island').click()
expect(await page.locator('#island-mounted-client-side').innerHTML()).toContain('Interactive testing slot post SSR')
expect(await page.locator('#island-mounted-client-side').innerHTML()).toContain('Sugar Counter')
// test islands wrapped with client-only
expect(await page.locator('#wrapped-client-only').innerHTML()).toContain('Was router enabled')

View File

@ -23,8 +23,8 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const [clientStats, clientStatsInlined] = await Promise.all((['.output', '.output-inline'])
.map(outputDir => analyzeSizes(['**/*.js'], join(rootDir, outputDir, 'public'))))
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"116k"`)
expect.soft(roundToKilobytes(clientStatsInlined!.totalBytes)).toMatchInlineSnapshot(`"116k"`)
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"115k"`)
expect.soft(roundToKilobytes(clientStatsInlined!.totalBytes)).toMatchInlineSnapshot(`"115k"`)
const files = new Set([...clientStats!.files, ...clientStatsInlined!.files].map(f => f.replace(/\..*\.js/, '.js')))

View File

@ -10,8 +10,8 @@
},
"devDependencies": {
"ofetch": "latest",
"unplugin-vue-router": "^0.10.7",
"vitest": "1.6.0",
"unplugin-vue-router": "latest",
"vitest": "latest",
"vue": "latest",
"vue-router": "latest"
},

View File

@ -258,7 +258,7 @@ describe('typed router integration', () => {
})
describe('layouts', () => {
it('recognizes named layouts', () => {
it('definePageMeta recognizes named layouts', () => {
definePageMeta({ layout: 'custom' })
definePageMeta({ layout: 'pascal-case' })
definePageMeta({ layout: 'override' })
@ -266,11 +266,14 @@ describe('layouts', () => {
definePageMeta({ layout: 'invalid-layout' })
})
it('allows typing layouts', () => {
it('NuxtLayout recognizes named layouts', () => {
h(NuxtLayout, { name: 'custom' })
// @ts-expect-error Invalid layout
h(NuxtLayout, { name: 'invalid-layout' })
h(NuxtLayout, { fallback: 'custom' })
// @ts-expect-error Invalid layout
h(NuxtLayout, { fallback: 'invalid-layout' })
})
})

View File

@ -661,14 +661,13 @@ describe('routing utilities: `encodeURL`', () => {
})
describe('routing utilities: `useRoute`', () => {
it('should show provide a mock route', () => {
it('should provide a route', () => {
expect(useRoute()).toMatchObject({
fullPath: '/',
hash: '',
href: '/',
matched: [],
matched: expect.arrayContaining([]),
meta: {},
name: undefined,
name: 'catchall',
params: {},
path: '/',
query: {},

View File

@ -0,0 +1,97 @@
/// <reference path="../fixtures/basic/.nuxt/nuxt.d.ts" />
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { NuxtLayout, NuxtPage } from '#components'
describe('NuxtPage should work with keepalive options', () => {
let visits = 0
const router = useRouter()
beforeEach(() => {
visits = 0
router.addRoute({
name: 'home',
path: '/home',
component: defineComponent({
name: 'home',
setup () {
visits++
return () => h('div', 'home')
},
}),
})
})
afterEach(() => {
router.removeRoute('home')
})
// include/exclude/boolean
it('should reload setup every time a page is visited, without keepalive', async () => {
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(2)
el.unmount()
})
it('should not remount a page when keepalive is enabled', async () => {
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage, { keepalive: true }) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(1)
el.unmount()
})
it('should not remount a page when keepalive is granularly enabled (with include)', async () => {
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage, { keepalive: { include: ['home'] } }) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(1)
el.unmount()
})
it('should not remount a page when keepalive is granularly enabled (with exclude)', async () => {
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage, { keepalive: { exclude: ['catchall'] } }) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(1)
el.unmount()
})
it('should not remount a page when keepalive options are modified', async () => {
const pages = ref('home')
const el = await mountSuspended({
setup () {
return () => h(NuxtLayout, {}, { default: () => h(NuxtPage, { keepalive: { include: pages.value } }) })
},
})
await navigateTo('/home')
await navigateTo('/')
await navigateTo('/home')
pages.value = 'home,catchall'
await navigateTo('/')
await navigateTo('/home')
expect(visits).toBe(1)
el.unmount()
})
})

View File

@ -0,0 +1,17 @@
import type { RouterOptions } from 'nuxt/schema'
import { defineComponent } from 'vue'
export default <RouterOptions> {
routes (_routes) {
return [
{
name: 'catchall',
path: '/:catchAll(.*)*',
component: defineComponent({
name: 'catchall',
setup: () => () => ({}),
}),
},
]
},
}

View File

@ -13,6 +13,7 @@ export default defineVitestConfig({
environmentOptions: {
nuxt: {
overrides: {
pages: true,
runtimeConfig: {
app: {
buildId: 'override',