Merge branch 'main' into add-middleware

This commit is contained in:
Saeid Zareie 2025-01-29 10:27:36 +03:30 committed by GitHub
commit be28c16104
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 1725 additions and 1290 deletions

View File

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

View File

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

View File

@ -1,52 +0,0 @@
name: benchmark
on:
workflow_dispatch:
# pull_request:
# paths-ignore:
# - "docs/**"
# - "*.md"
# branches:
# - main
# - "!v[0-9]*"
# https://github.com/vitejs/vite/blob/main/.github/workflows/ci.yml
env:
# 7 GiB by default on GitHub, setting to 6 GiB
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
NODE_OPTIONS: --max-old-space-size=6144
# Remove default permissions of GITHUB_TOKEN for security
# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: ${{ github.event_name != 'push' }}
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build (stub)
run: pnpm dev:prepare
- name: Build
run: pnpm build
- name: Run benchmarks
uses: CodSpeedHQ/action@513a19673a831f139e8717bf45ead67e47f00044 # v3.2.0
with:
run: pnpm vitest bench
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@ -26,7 +26,7 @@ jobs:
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"

View File

@ -39,7 +39,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.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@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
uses: github/codeql-action/init@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3.28.6
with:
config: |
paths:
@ -95,7 +95,7 @@ jobs:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
uses: github/codeql-action/analyze@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3.28.6
with:
category: "/language:${{ matrix.language }}"
@ -113,7 +113,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -144,7 +144,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -171,7 +171,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -196,7 +196,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -213,6 +213,34 @@ jobs:
- name: Check bundle size
run: pnpm vitest run bundle
test-benchmark:
runs-on: ubuntu-latest
needs:
- build
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Restore dist cache
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: dist
path: packages
- name: Run benchmarks
uses: CodSpeedHQ/action@513a19673a831f139e8717bf45ead67e47f00044 # v3.2.0
with:
run: pnpm vitest bench
token: ${{ secrets.CODSPEED_TOKEN }}
test-fixtures:
runs-on: ${{ matrix.os }}
needs:
@ -253,7 +281,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: ${{ matrix.node }}
cache: "pnpm"
@ -280,7 +308,7 @@ jobs:
TEST_PAYLOAD: ${{ matrix.payload }}
SKIP_BUNDLE_SIZE: true
- uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
- uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -305,7 +333,7 @@ jobs:
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"
@ -337,7 +365,7 @@ jobs:
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
with:
node-version: lts/*
cache: "pnpm"

View File

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

View File

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

View File

@ -23,7 +23,7 @@ jobs:
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.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@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
uses: github/codeql-action/upload-sarif@17a820bf2e43b47be2c72b39cc905417bc1ab6d0 # v3.28.6
if: github.repository == 'nuxt/nuxt' && success()
with:
sarif_file: results.sarif

View File

@ -126,6 +126,26 @@ This will produce three files:
The `200.html` and `404.html` might be useful for the hosting provider you are using.
#### Skipping Client Fallback Generation
When prerendering a client-rendered app, Nuxt will generate `index.html`, `200.html` and `404.html` files by default. However, if you need to prevent any (or all) of these files from being generated in your build, you can use the `'prerender:generate'` hook from [Nitro](/docs/getting-started/prerendering#prerendergenerate-nitro-hook).
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
ssr: false,
nitro: {
hooks: {
'prerender:generate'(route) {
const routesToSkip = ['/index.html', '/200.html', '/404.html']
if (routesToSkip.includes(route.route)) {
route.skip = true
}
}
}
}
})
```
## Hybrid Rendering
Hybrid rendering allows different caching rules per route using **Route Rules** and decides how the server should respond to a new request on a given URL.

View File

@ -37,13 +37,15 @@
"typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs --languages html"
},
"resolutions": {
"@babel/core": "7.26.7",
"@babel/helper-plugin-utils": "7.26.5",
"@nuxt/cli": "3.20.0",
"@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
"@types/node": "22.10.7",
"@types/node": "22.12.0",
"@unhead/dom": "1.11.18",
"@unhead/schema": "1.11.18",
"@unhead/shared": "1.11.18",
@ -53,27 +55,28 @@
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13",
"c12": "2.0.1",
"h3": "npm:h3-nightly@1.13.1-20250110-173418-de24917",
"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-28968142.ce23e942",
"nuxt": "workspace:*",
"ohash": "1.1.4",
"postcss": "8.5.1",
"rollup": "4.31.0",
"rollup": "4.32.1",
"send": ">=1.1.0",
"typescript": "5.7.3",
"ufo": "1.5.4",
"unbuild": "3.3.1",
"unhead": "1.11.18",
"unimport": "3.14.6",
"unimport": "4.0.0",
"vite": "6.0.11",
"vue": "3.5.13"
},
"devDependencies": {
"@arethetypeswrong/cli": "0.17.3",
"@babel/core": "7.26.0",
"@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/kit": "workspace:*",
@ -83,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.7",
"@types/node": "22.12.0",
"@types/semver": "7.5.8",
"@unhead/schema": "1.11.18",
"@unhead/vue": "1.11.18",
"@vitest/coverage-v8": "3.0.3",
"@vitest/coverage-v8": "3.0.4",
"@vue/test-utils": "2.4.6",
"acorn": "8.14.0",
"autoprefixer": "10.4.20",
@ -97,29 +100,29 @@
"cssnano": "7.0.6",
"destr": "2.0.3",
"devalue": "5.1.1",
"eslint": "9.18.0",
"eslint": "9.19.0",
"eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "4.7.0",
"eslint-typegen": "1.0.0",
"estree-walker": "3.0.3",
"h3": "npm:h3-nightly@1.13.1-20250110-173418-de24917",
"happy-dom": "16.7.1",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"happy-dom": "16.7.3",
"installed-check": "9.3.0",
"jiti": "2.4.2",
"knip": "5.42.2",
"knip": "5.43.6",
"magic-string": "0.30.17",
"markdownlint-cli": "0.43.0",
"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-28968142.ce23e942",
"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.49.1",
"rollup": "4.31.0",
"playwright-core": "1.50.0",
"rollup": "4.32.1",
"semver": "7.6.3",
"sherif": "1.1.1",
"sherif": "1.2.0",
"std-env": "3.8.0",
"tinyexec": "0.3.2",
"tinyglobby": "0.2.10",
@ -127,7 +130,7 @@
"typescript": "5.7.3",
"ufo": "1.5.4",
"unbuild": "3.3.1",
"vitest": "3.0.3",
"vitest": "3.0.4",
"vitest-environment-nuxt": "1.0.1",
"vue": "3.5.13",
"vue-tsc": "2.2.0",

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",
@ -46,19 +45,20 @@
"std-env": "^3.8.0",
"ufo": "^1.5.4",
"unctx": "^2.4.1",
"unimport": "^3.14.6",
"unimport": "^4.0.0",
"untyped": "^1.5.2"
},
"devDependencies": {
"@rspack/core": "1.2.0",
"@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-28968142.ce23e942",
"unbuild": "3.3.1",
"vite": "6.0.11",
"vitest": "3.0.3",
"vitest": "3.0.4",
"webpack": "5.97.1"
},
"engines": {
"node": ">=18.0.0"
"node": ">=18.12.0"
}
}

View File

@ -1,7 +1,6 @@
import { kebabCase, pascalCase } from 'scule'
import type { Component, ComponentsDir } from '@nuxt/schema'
import { useNuxt } from './context'
import { checkNuxtVersion } from './compatibility'
import { logger } from './logger'
import { MODE_RE } from './utils'
@ -10,9 +9,6 @@ import { MODE_RE } from './utils'
*/
export function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) {
const nuxt = useNuxt()
if (!checkNuxtVersion('>=2.13', nuxt)) {
throw new Error(`\`addComponentsDir\` requires Nuxt 2.13 or higher.`)
}
nuxt.options.components ||= []
dir.priority ||= 0
nuxt.hook('components:dirs', (dirs) => { dirs[opts.prepend ? 'unshift' : 'push'](dir) })
@ -27,10 +23,6 @@ export type AddComponentOptions = { name: string, filePath: string } & Partial<E
*/
export function addComponent (opts: AddComponentOptions) {
const nuxt = useNuxt()
if (!checkNuxtVersion('>=2.13', nuxt)) {
throw new Error(`\`addComponent\` requires Nuxt 2.13 or higher.`)
}
nuxt.options.components ||= []
if (!opts.mode) {

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

@ -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

@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url'
import { bench, describe } from 'vitest'
import { join, normalize } from 'pathe'
import { withoutTrailingSlash } from 'ufo'
import { loadNuxtConfig } from '../src'
import { loadNuxtConfig } from '@nuxt/kit'
const fixtures = {
'empty directory': 'node_modules/fixture',
@ -16,7 +16,7 @@ describe('loadNuxtConfig', () => {
for (const fixture in fixtures) {
const relativeDir = join('../../..', fixtures[fixture as keyof typeof fixtures])
const path = withoutTrailingSlash(normalize(fileURLToPath(new URL(relativeDir, import.meta.url))))
bench(fixture, async () => {
bench(`loadNuxtConfig in the ${fixture}`, async () => {
await loadNuxtConfig({ cwd: path })
})
}

View File

@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url'
import { afterAll, bench, describe } from 'vitest'
import { join, normalize } from 'pathe'
import { withoutTrailingSlash } from 'ufo'
import { loadNuxt, writeTypes } from '../src'
import { loadNuxt, writeTypes } from '@nuxt/kit'
describe('writeTypes', async () => {
const relativeDir = join('../../..', 'test/fixtures/basic-types')
@ -13,7 +13,7 @@ describe('writeTypes', async () => {
await nuxt.close()
})
bench('write types', async () => {
bench('writeTypes in the basic-types fixture', async () => {
await writeTypes(nuxt)
})
})

View File

@ -90,7 +90,7 @@
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"globby": "^14.0.2",
"h3": "npm:h3-nightly@1.13.1-20250110-173418-de24917",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"hookable": "^5.5.3",
"ignore": "^7.0.3",
"impound": "^0.2.0",
@ -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-28968142.ce23e942",
"nypm": "^0.5.2",
"ofetch": "^1.4.1",
"ohash": "^1.1.4",
"pathe": "^2.0.2",
@ -119,9 +119,9 @@
"unctx": "^2.4.1",
"unenv": "^1.10.0",
"unhead": "^1.11.18",
"unimport": "^3.14.6",
"unimport": "^4.0.0",
"unplugin": "^2.1.2",
"unplugin-vue-router": "^0.11.0",
"unplugin-vue-router": "^0.11.2",
"unstorage": "^1.14.4",
"untyped": "^1.5.2",
"vue": "^3.5.13",
@ -131,13 +131,13 @@
},
"devDependencies": {
"@nuxt/scripts": "0.9.5",
"@parcel/watcher": "2.5.0",
"@parcel/watcher": "2.5.1",
"@types/estree": "1.0.6",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
"unbuild": "3.3.1",
"vite": "6.0.11",
"vitest": "3.0.3"
"vitest": "3.0.4"
},
"peerDependencies": {
"@parcel/watcher": "^2.1.0",

View File

@ -132,10 +132,11 @@ export default defineComponent({
ssrHTML.value = getFragmentHTML(instance.vnode.el, true)?.join('') || ''
const key = `${props.name}_${hashId.value}`
nuxtApp.payload.data[key] ||= {}
nuxtApp.payload.data[key].html = ssrHTML.value
// clear all data-island-uid to avoid conflicts when saving into payloads
nuxtApp.payload.data[key].html = ssrHTML.value.replaceAll(new RegExp(`data-island-uid="${ssrHTML.value.match(SSR_UID_RE)?.[1] || ''}"`, 'g'), `data-island-uid=""`)
}
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId())
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] || getId())
const availableSlots = computed(() => [...ssrHTML.value.matchAll(SLOTNAME_RE)].map(m => m[1]))
const html = computed(() => {
const currentSlots = Object.keys(slots)

View File

@ -1,12 +1,12 @@
import type { DefineComponent, MaybeRef, VNode } from 'vue'
import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue'
import { Suspense, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import type { PageMeta } from '../../pages/runtime/composables'
import { useRoute, useRouter } from '../composables/router'
import { useNuxtApp } from '../nuxt'
import { _wrapIf } from './utils'
import { _wrapInTransition } from './utils'
import { LayoutMetaSymbol, PageRouteSymbol } from './injections'
// @ts-expect-error virtual file
@ -80,7 +80,7 @@ export default defineComponent({
const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition
// We avoid rendering layout transition if there is no layout to render
return _wrapIf(Transition, hasLayout && transitionProps, {
return _wrapInTransition(hasLayout && transitionProps, {
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
default: () => h(
LayoutProvider,

View File

@ -1,5 +1,5 @@
import { createStaticVNode, h } from 'vue'
import type { Component, RendererNode, VNode } from 'vue'
import { Transition, createStaticVNode, h } from 'vue'
import type { RendererNode, VNode } from 'vue'
// eslint-disable-next-line
import { isString, isPromise, isArray, isObject } from '@vue/shared'
import type { RouteLocationNormalized } from 'vue-router'
@ -10,9 +10,8 @@ import { START_LOCATION } from '#build/pages'
* Internal utility
* @private
*/
export const _wrapIf = (component: Component, props: any, slots: any) => {
props = props === true ? {} : props
return { default: () => props ? h(component, props, slots) : slots.default?.() }
export const _wrapInTransition = (props: any, children: any) => {
return { default: () => import.meta.client && props ? h(Transition, props === true ? {} : props, children) : children.default?.() }
}
const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g

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

@ -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

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']) {
@ -661,7 +662,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 })
}

View File

@ -43,8 +43,8 @@ export function walk (ast: Program | Node, callback: Partial<WalkOptions>) {
export function parseAndWalk (code: string, sourceFilename: string, callback: WalkerCallback): Program
export function parseAndWalk (code: string, sourceFilename: string, object: Partial<WalkOptions>): Program
export function parseAndWalk (code: string, _sourceFilename: string, callback: Partial<WalkOptions> | WalkerCallback) {
const ast = parse (code, { sourceType: 'module', ecmaVersion: 'latest', locations: true })
export function parseAndWalk (code: string, sourceFilename: string, callback: Partial<WalkOptions> | WalkerCallback) {
const ast = parse (code, { sourceType: 'module', ecmaVersion: 'latest', locations: true, sourceFile: sourceFilename })
walk(ast, typeof callback === 'function' ? { enter: callback } : callback)
return ast
}

View File

@ -1,4 +1,4 @@
import { Fragment, Suspense, Transition, defineComponent, h, inject, nextTick, ref, watch } from 'vue'
import { Fragment, Suspense, defineComponent, h, inject, nextTick, ref, watch } from 'vue'
import type { KeepAliveProps, TransitionProps, VNode } from 'vue'
import { RouterView } from 'vue-router'
import { defu } from 'defu'
@ -9,7 +9,7 @@ import type { RouterViewSlotProps } from './utils'
import { RouteProvider } from '#app/components/route-provider'
import { useNuxtApp } from '#app/nuxt'
import { useRouter } from '#app/composables/router'
import { _wrapIf } from '#app/components/utils'
import { _wrapInTransition } from '#app/components/utils'
import { LayoutMetaSymbol, PageRouteSymbol } from '#app/components/injections'
// @ts-expect-error virtual file
import { appKeepalive as defaultKeepaliveConfig, appPageTransition as defaultPageTransition } from '#build/nuxt.config.mjs'
@ -101,8 +101,30 @@ export default defineComponent({
nuxtApp.callHook('page:loading:end')
pageLoadingEndHookAlreadyCalled = true
}
previousPageKey = key
if (import.meta.server) {
vnode = h(Suspense, {
suspensible: true,
}, {
default: () => {
const providerVNode = h(RouteProvider, {
key: key || undefined,
vnode: slots.default ? h(Fragment, undefined, slots.default(routeProps)) : routeProps.Component,
route: routeProps.route,
renderKey: key || undefined,
vnodeRef: pageRef,
})
return providerVNode
},
})
return vnode
}
// Client side rendering
const hasTransition = !!(props.transition ?? routeProps.route.meta.pageTransition ?? defaultPageTransition)
const transitionProps = hasTransition && _mergeTransitionProps([
props.transition,
@ -112,7 +134,7 @@ export default defineComponent({
].filter(Boolean))
const keepaliveConfig = props.keepalive ?? routeProps.route.meta.keepalive ?? (defaultKeepaliveConfig as KeepAliveProps)
vnode = _wrapIf(Transition, hasTransition && transitionProps,
vnode = _wrapInTransition(hasTransition && transitionProps,
wrapInKeepAlive(keepaliveConfig, h(Suspense, {
suspensible: true,
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
@ -134,7 +156,7 @@ export default defineComponent({
trackRootNodes: hasTransition,
vnodeRef: pageRef,
})
if (import.meta.client && keepaliveConfig) {
if (keepaliveConfig) {
(providerVNode.type as any).name = (routeProps.Component.type as any).name || (routeProps.Component.type as any).__name || 'RouteProvider'
}
return providerVNode

View File

@ -11,6 +11,7 @@ import { transform } from 'esbuild'
import type { Property } from 'estree'
import type { NuxtPage } from 'nuxt/schema'
import { klona } from 'klona'
import { parseAndWalk, withLocations } from '../core/utils/parse'
import { getLoader, uniqueBy } from '../core/utils'
import { logger, toArray } from '../utils'
@ -215,7 +216,7 @@ export async function getRouteMeta (contents: string, absolutePath: string, extr
}
if (absolutePath in metaCache && metaCache[absolutePath]) {
return metaCache[absolutePath]
return klona(metaCache[absolutePath])
}
const loader = getLoader(absolutePath)
@ -314,7 +315,7 @@ export async function getRouteMeta (contents: string, absolutePath: string, extr
}
metaCache[absolutePath] = extractedMeta
return extractedMeta
return klona(extractedMeta)
}
const COLON_RE = /:/g

View File

@ -2,13 +2,13 @@ import { fileURLToPath } from 'node:url'
import { bench, describe } from 'vitest'
import { normalize } from 'pathe'
import { withoutTrailingSlash } from 'ufo'
import { loadNuxt } from '../src'
import { loadNuxt } from 'nuxt'
const emptyDir = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../node_modules/fixture', import.meta.url))))
const basicTestFixtureDir = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../test/fixtures/basic', import.meta.url))))
describe('loadNuxt', () => {
bench('empty directory', async () => {
bench('loadNuxt in an empty directory', async () => {
const nuxt = await loadNuxt({
cwd: emptyDir,
ready: true,
@ -16,7 +16,7 @@ describe('loadNuxt', () => {
await nuxt.close()
})
bench('basic test fixture', async () => {
bench('loadNuxt in the basic test fixture', async () => {
const nuxt = await loadNuxt({
cwd: basicTestFixtureDir,
ready: true,

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { type MockedFunction, describe, expect, it, vi } from 'vitest'
import { compileScript, parse } from '@vue/compiler-sfc'
import * as Parser from 'acorn'
import { klona } from 'klona'
import { transform as esbuildTransform } from 'esbuild'
import { PageMetaPlugin } from '../src/pages/plugins/page-meta'
import { getRouteMeta, normalizeRoutes } from '../src/pages/utils'
@ -8,6 +9,8 @@ import type { NuxtPage } from '../schema'
const filePath = '/app/pages/index.vue'
vi.mock('klona', { spy: true })
describe('page metadata', () => {
it('should not extract metadata from empty files', async () => {
expect(await getRouteMeta('', filePath)).toEqual({})
@ -62,11 +65,20 @@ definePageMeta({ name: 'bar' })
})
it('should use and invalidate cache', async () => {
const _klona = klona as unknown as MockedFunction<typeof klona>
_klona.mockImplementation(obj => obj)
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
const meta = await getRouteMeta(fileContents, filePath)
expect(meta === await getRouteMeta(fileContents, filePath)).toBeTruthy()
expect(meta === await getRouteMeta(fileContents, '/app/pages/other.vue')).toBeFalsy()
expect(meta === await getRouteMeta('<template><div>Hi</div></template>' + fileContents, filePath)).toBeFalsy()
_klona.mockReset()
})
it('should not share state between page metadata', async () => {
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
const meta = await getRouteMeta(fileContents, filePath)
expect(meta === await getRouteMeta(fileContents, filePath)).toBeFalsy()
})
it('should extract serialisable metadata', async () => {

View File

@ -834,3 +834,41 @@ describe('pages:pathToNitroGlob', () => {
expect(pathToNitroGlob(path)).to.equal(expected)
})
})
describe('page:extends', () => {
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
it('should preserve distinct metadata for multiple routes referencing the same file', async () => {
const files: NuxtPage[] = [
{ path: 'home', file: `pages/index.vue` },
{ path: 'home1', file: `pages/index.vue`, meta: { test: true } },
{ path: 'home2', file: `pages/index.vue`, meta: { snap: true } },
]
const vfs = Object.fromEntries(
files.map(file => [file.file, `
<script setup lang="ts">
definePageMeta({
hello: 'world'
})
</script>
`]),
) as Record<string, string>
await augmentPages(files, vfs)
expect(files).toEqual([
{
path: 'home',
file: `pages/index.vue`,
meta: { [DYNAMIC_META_KEY]: new Set(['meta']) },
},
{
path: 'home1',
file: `pages/index.vue`,
meta: { [DYNAMIC_META_KEY]: new Set(['meta']), test: true },
},
{
path: 'home2',
file: `pages/index.vue`,
meta: { [DYNAMIC_META_KEY]: new Set(['meta']), snap: true },
},
])
})
})

View File

@ -32,7 +32,7 @@
"dependencies": {
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
"@nuxt/kit": "workspace:*",
"@rspack/core": "^1.2.0",
"@rspack/core": "^1.2.2",
"autoprefixer": "^10.4.20",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
@ -43,7 +43,7 @@
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.2",
"globby": "^14.0.2",
"h3": "npm:h3-nightly@1.13.1-20250110-173418-de24917",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"jiti": "^2.4.2",
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
@ -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.31.0",
"rollup": "4.32.1",
"unbuild": "3.3.1",
"vue": "3.5.13"
},

View File

@ -48,17 +48,17 @@
"compatx": "0.1.8",
"esbuild-loader": "4.2.2",
"file-loader": "6.2.0",
"h3": "npm:h3-nightly@1.13.1-20250110-173418-de24917",
"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-28968142.ce23e942",
"ofetch": "1.4.1",
"pkg-types": "1.3.1",
"sass-loader": "16.0.4",
"scule": "1.3.0",
"unbuild": "3.3.1",
"unctx": "2.4.1",
"unimport": "3.14.6",
"unimport": "4.0.0",
"untyped": "1.5.2",
"vite": "6.0.11",
"vue": "3.5.13",

View File

@ -39,5 +39,13 @@ export default defineUntypedSchema({
* @type {(data: { loading?: string }) => string}
*/
loadingTemplate,
/**
* Set CORS options for the dev server
* @type {typeof import('h3').H3CorsOptions}
*/
cors: {
origin: [/^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/],
},
},
})

View File

@ -17,7 +17,7 @@
"prerender": "pnpm build && jiti ./lib/prerender"
},
"devDependencies": {
"@unocss/reset": "65.4.2",
"@unocss/reset": "65.4.3",
"beasties": "0.2.0",
"html-validate": "9.1.3",
"htmlnano": "2.1.1",
@ -29,7 +29,7 @@
"svgo": "3.3.2",
"tinyexec": "0.3.2",
"tinyglobby": "0.2.10",
"unocss": "65.4.2",
"unocss": "65.4.3",
"vite": "6.0.11"
}
}

View File

@ -26,7 +26,7 @@
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"rollup": "4.31.0",
"rollup": "4.32.1",
"unbuild": "3.3.1",
"vue": "3.5.13"
},
@ -41,8 +41,9 @@
"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.13.1-20250110-173418-de24917",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"jiti": "^2.4.2",
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
@ -56,7 +57,7 @@
"unenv": "^1.10.0",
"unplugin": "^2.1.2",
"vite": "^6.0.11",
"vite-node": "^3.0.3",
"vite-node": "^3.0.4",
"vite-plugin-checker": "^0.8.0",
"vue-bundle-renderer": "^2.1.1"
},

View File

@ -9,7 +9,7 @@ import { getPort } from 'get-port-please'
import { joinURL, withoutLeadingSlash } from 'ufo'
import { defu } from 'defu'
import { env, nodeless } from 'unenv'
import { appendCorsHeaders, appendCorsPreflightHeaders, defineEventHandler } from 'h3'
import { defineEventHandler, handleCors, setHeader } from 'h3'
import type { ViteConfig } from '@nuxt/schema'
import type { ViteBuildContext } from './vite'
import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
@ -255,11 +255,11 @@ export async function buildClient (ctx: ViteBuildContext) {
// @ts-expect-error _skip_transform is a private property
event.node.req._skip_transform = true
} else if (!useViteCors) {
if (event.method === 'OPTIONS') {
appendCorsPreflightHeaders(event, {})
const isPreflight = handleCors(event, ctx.nuxt.options.devServer.cors)
if (isPreflight) {
return null
}
appendCorsHeaders(event, {})
setHeader(event, 'Vary', 'Origin')
}
// Workaround: vite devmiddleware modifies req.url

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

@ -42,7 +42,7 @@
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.2",
"globby": "^14.0.2",
"h3": "npm:h3-nightly@1.13.1-20250110-173418-de24917",
"h3": "npm:h3-nightly@1.14.0-20250122-114730-3f9e703",
"jiti": "^2.4.2",
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
@ -73,11 +73,11 @@
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"@rspack/core": "1.2.0",
"@rspack/core": "1.2.2",
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.31.0",
"rollup": "4.32.1",
"unbuild": "3.3.1",
"vue": "3.5.13"
},

View File

@ -1,6 +1,7 @@
import pify from 'pify'
import { resolve } from 'pathe'
import { defineEventHandler, fromNodeMiddleware } 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'
import webpackHotMiddleware from 'webpack-hot-middleware'
@ -125,7 +126,7 @@ async function createDevMiddleware (compiler: Compiler) {
})
// Register devMiddleware on server
const devHandler = wdmToH3Handler(devMiddleware)
const devHandler = wdmToH3Handler(devMiddleware, nuxt.options.devServer.cors)
const hotHandler = fromNodeMiddleware(hotMiddleware)
await nuxt.callHook('server:devHandler', defineEventHandler(async (event) => {
const body = await devHandler(event)
@ -139,8 +140,20 @@ async function createDevMiddleware (compiler: Compiler) {
}
// TODO: implement upstream in `webpack-dev-middleware`
function wdmToH3Handler (devMiddleware: webpackDevMiddleware.API<IncomingMessage, ServerResponse>) {
function wdmToH3Handler (devMiddleware: webpackDevMiddleware.API<IncomingMessage, ServerResponse>, corsOptions: H3CorsOptions) {
return defineEventHandler(async (event) => {
const isPreflight = handleCors(event, corsOptions)
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 = {
...event.context.webpack,
devMiddleware: devMiddleware.context,

File diff suppressed because it is too large Load Diff

View File

@ -61,7 +61,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"209k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1398k"`)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1402k"`)
const packages = modules.files
.filter(m => m.endsWith('package.json'))
@ -127,10 +127,10 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(pagesRootDir, '.output/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"303k"`)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"301k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1398k"`)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1402k"`)
const packages = modules.files
.filter(m => m.endsWith('package.json'))

View File

@ -1,10 +1,10 @@
import { resolve } from 'pathe'
import { configDefaults, coverageConfigDefaults, defineConfig } from 'vitest/config'
import { isWindows } from 'std-env'
// import codspeedPlugin from '@codspeed/vitest-plugin'
import codspeedPlugin from '@codspeed/vitest-plugin'
export default defineConfig({
// plugins: [codspeedPlugin()],
plugins: [codspeedPlugin()],
resolve: {
alias: {
'#build/nuxt.config.mjs': resolve('./test/mocks/nuxt-config'),