Merge branch 'nuxt:main' into main

This commit is contained in:
David Nahodyl 2024-06-14 09:11:28 -04:00 committed by GitHub
commit 7501792b91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 1822 additions and 695 deletions

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:

View File

@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- run: corepack enable

View File

@ -25,7 +25,7 @@ jobs:
restore-keys: cache-lychee-
# check links with Lychee
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Lychee link checker
uses: lycheeverse/lychee-action@25a231001d1723960a301b7d4c82884dc7ef857d # for v1.8.0

View File

@ -35,7 +35,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -72,7 +72,7 @@ jobs:
- build
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -83,7 +83,7 @@ jobs:
run: pnpm install
- name: Initialize CodeQL
uses: github/codeql-action/init@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
with:
languages: javascript
queries: +security-and-quality
@ -95,7 +95,7 @@ jobs:
path: packages
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
with:
category: "/language:javascript"
@ -111,7 +111,7 @@ jobs:
module: ["bundler", "node"]
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -142,7 +142,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -166,7 +166,7 @@ jobs:
needs:
- build
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -218,7 +218,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -248,7 +248,7 @@ jobs:
TEST_PAYLOAD: ${{ matrix.payload }}
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || matrix.payload == 'js' || runner.os == 'Windows' }}
- uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4.4.1
- uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
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 }}
@ -270,7 +270,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- run: corepack enable
@ -309,7 +309,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- run: corepack enable

View File

@ -17,6 +17,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: 'Dependency Review'
uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files
run: |

View File

@ -29,7 +29,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.issue.pull_request.head.sha }}
fetch-depth: 0

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- run: corepack enable

View File

@ -10,7 +10,7 @@ jobs:
reproduire:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp
with:
label: needs reproduction

View File

@ -32,7 +32,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
persist-credentials: false
@ -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@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
if: github.repository == 'nuxt/nuxt' && success()
with:
sarif_file: results.sarif

View File

@ -23,7 +23,7 @@ To use the latest Nuxt build and test features before their release, read about
Nuxt 4 is planned to be released **on or before June 14** (though obviously this is dependent on having enough time after Nitro's major release to be properly tested in the community, so be aware that this is not an exact date).
Until then, it is possible to test many of Nuxt 4's breaking changes on the nightly release channel.
Until then, it is possible to test many of Nuxt 4's breaking changes from Nuxt version 3.12 or via the nightly release channel.
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=r4wFKlcJK6c" target="_blank"}
Watch a video from Alexander Lichter showing how to opt in to Nuxt 4's breaking changes already.

View File

@ -40,7 +40,7 @@ There is also a `future` namespace for early opting-in to new features that will
### compatibilityVersion
::important
This configuration option is available in Nuxt v3.12+ or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
This configuration option is available in Nuxt v3.12+.
::
This enables early access to Nuxt features or flags.

View File

@ -11,7 +11,7 @@ links:
---
::important
This component will be available in Nuxt v3.12 or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
This component is available in Nuxt v3.12+.
::
## Usage

View File

@ -10,7 +10,7 @@ links:
---
::important
This composable will be available in Nuxt v3.12+ or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
This composable is available in Nuxt v3.12+.
::
`onPrehydrate` is a composable lifecycle hook that allows you to run a callback on the client immediately before

View File

@ -11,7 +11,7 @@ links:
---
::important
This composable will be available in Nuxt v3.12 or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
This composable is available in Nuxt v3.12+.
::
## Description

View File

@ -98,6 +98,10 @@ export default defineNuxtComponent({
</script>
```
::warning
Possible breaking change: `head` receives the nuxt app but cannot access the component instance. If the code in your `head` tries to access the data object through `this` or `this.$data`, you will need to migrate to the `useHead` composable.
::
## Title Template
If you want to use a function (for full control), then this cannot be set in your nuxt.config, and it is recommended instead to set it within your `/layouts` directory.

View File

@ -42,7 +42,7 @@
"magic-string": "^0.30.10",
"nuxt": "workspace:*",
"rollup": "^4.18.0",
"vite": "5.2.13",
"vite": "5.3.0",
"vue": "3.4.27"
},
"devDependencies": {
@ -56,7 +56,7 @@
"@types/fs-extra": "11.0.4",
"@types/node": "20.14.2",
"@types/semver": "7.5.8",
"@unhead/schema": "1.9.12",
"@unhead/schema": "1.9.13",
"@vitejs/plugin-vue": "5.0.4",
"@vitest/coverage-v8": "1.6.0",
"@vue/test-utils": "2.4.6",
@ -66,7 +66,7 @@
"devalue": "5.0.0",
"eslint": "9.4.0",
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-perfectionist": "2.10.0",
"eslint-plugin-perfectionist": "2.11.0",
"eslint-typegen": "0.2.4",
"execa": "9.2.0",
"fs-extra": "11.2.0",
@ -76,7 +76,7 @@
"jiti": "1.21.6",
"markdownlint-cli": "0.41.0",
"nitropack": "2.9.6",
"nuxi": "3.11.1",
"nuxi": "3.12.0",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.0.10",
"ofetch": "1.3.4",

View File

@ -1,6 +1,6 @@
{
"name": "@nuxt/kit",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -27,7 +27,7 @@
},
"dependencies": {
"@nuxt/schema": "workspace:*",
"c12": "^1.10.0",
"c12": "^1.11.1",
"consola": "^3.2.3",
"defu": "^6.1.4",
"destr": "^2.0.3",
@ -54,9 +54,9 @@
"lodash-es": "4.17.21",
"nitropack": "2.9.6",
"unbuild": "latest",
"vite": "5.2.13",
"vite": "5.3.0",
"vitest": "1.6.0",
"webpack": "5.91.0"
"webpack": "5.92.0"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"

View File

@ -61,7 +61,7 @@ function getRequireCacheItem (id: string) {
}
}
export function getModulePaths (paths?: string[] | string) {
export function getNodeModulesPaths (paths?: string[] | string) {
return ([] as Array<string | undefined>).concat(
global.__NUXT_PREPATHS__,
paths || [],
@ -73,7 +73,7 @@ export function getModulePaths (paths?: string[] | string) {
/** @deprecated Do not use CJS utils */
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
return normalize(_require.resolve(id, {
paths: getModulePaths(opts.paths),
paths: getNodeModulesPaths(opts.paths),
}))
}

View File

@ -32,10 +32,11 @@ const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"\{(.+)\
/** @deprecated */
const importSources = (sources: string | string[], { lazy = false } = {}) => {
return toArray(sources).map((src) => {
const safeVariableName = genSafeVariableName(src)
if (lazy) {
return `const ${genSafeVariableName(src)} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
return `const ${safeVariableName} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
}
return genImport(src, genSafeVariableName(src))
return genImport(src, safeVariableName)
}).join('\n')
}

View File

@ -7,10 +7,17 @@ import { NuxtConfigSchema } from '@nuxt/schema'
import { globby } from 'globby'
import defu from 'defu'
export interface LoadNuxtConfigOptions extends LoadConfigOptions<NuxtConfig> {}
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
overrides?: Exclude<LoadConfigOptions<NuxtConfig>['overrides'], Promise<any> | Function>
}
const layerSchemaKeys = ['future', 'srcDir', 'rootDir', 'dir']
const layerSchema = Object.fromEntries(Object.entries(NuxtConfigSchema).filter(([key]) => layerSchemaKeys.includes(key)))
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
@ -40,10 +47,15 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
nuxtConfig._nuxtConfigFiles = [configFile]
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
const processedLayers = new Set<string>()
for (const layer of layers) {
// Resolve `rootDir` & `srcDir` of layers
layer.config = layer.config || {}
layer.config.rootDir = layer.config.rootDir ?? layer.cwd
layer.config.rootDir = layer.config.rootDir ?? layer.cwd!
// Only process/resolve layers once
if (processedLayers.has(layer.config.rootDir)) { continue }
processedLayers.add(layer.config.rootDir)
// Normalise layer directories
layer.config = await applyDefaults(layerSchema, layer.config as NuxtConfig & Record<string, JSValue>) as unknown as NuxtConfig

View File

@ -51,11 +51,10 @@ export async function getNuxtModuleVersion (module: string | NuxtModule, nuxt: N
// need a name from here
if (!moduleMeta.name) { return false }
// maybe the version got attached within the installed module instance?
const version = nuxt.options._installedModules
// @ts-expect-error _installedModules is not typed
.filter(m => m.meta.name === moduleMeta.name).map(m => m.meta.version)?.[0]
if (version) {
return version
for (const m of nuxt.options._installedModules) {
if (m.meta.name === moduleMeta.name && m.meta.version) {
return m.meta.version
}
}
// it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance
if (hasNuxtModule(moduleMeta.name)) {

View File

@ -51,11 +51,12 @@ export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], op
for (const middleware of middlewares) {
const find = app.middleware.findIndex(item => item.name === middleware.name)
if (find >= 0) {
if (app.middleware[find].path === middleware.path) { continue }
const foundPath = app.middleware[find].path
if (foundPath === middleware.path) { continue }
if (options.override === true) {
app.middleware[find] = { ...middleware }
} else {
logger.warn(`'${middleware.name}' middleware already exists at '${app.middleware[find].path}'. You can set \`override: true\` to replace it.`)
logger.warn(`'${middleware.name}' middleware already exists at '${foundPath}'. You can set \`override: true\` to replace it.`)
}
} else {
app.middleware.push({ ...middleware })

View File

@ -168,8 +168,8 @@ export function createResolver (base: string | URL): Resolver {
}
}
export async function resolveNuxtModule (base: string, paths: string[]) {
const resolved = []
export async function resolveNuxtModule (base: string, paths: string[]): Promise<string[]> {
const resolved: string[] = []
const resolver = createResolver(base)
for (const path of paths) {
@ -209,6 +209,12 @@ function existsInVFS (path: string, nuxt = tryUseNuxt()) {
}
export async function resolveFiles (path: string, pattern: string | string[], opts: { followSymbolicLinks?: boolean } = {}) {
const files = await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true })
return files.map(p => resolve(path, p)).filter(p => !isIgnored(p)).sort()
const files: string[] = []
for (const file of await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true })) {
const p = resolve(path, file)
if (!isIgnored(p)) {
files.push(p)
}
}
return files.sort()
}

View File

@ -11,7 +11,7 @@ import { readPackageJSON } from 'pkg-types'
import { tryResolveModule } from './internal/esm'
import { getDirectory } from './module/install'
import { tryUseNuxt, useNuxt } from './context'
import { getModulePaths } from './internal/cjs'
import { getNodeModulesPaths } from './internal/cjs'
import { resolveNuxtModule } from './resolve'
/**
@ -113,18 +113,55 @@ export async function updateTemplates (options?: { filter?: (template: ResolvedN
}
export async function _generateTypes (nuxt: Nuxt) {
const nodeModulePaths = getModulePaths(nuxt.options.modulesDir)
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir)
const modulePaths = await resolveNuxtModule(rootDirWithSlash,
nuxt.options._installedModules
.filter(m => m.entryPath)
.map(m => getDirectory(m.entryPath)),
)
const include = new Set<string>([
'./nuxt.d.ts',
join(relativeRootDir, '.config/nuxt.*'),
join(relativeRootDir, '**/*'),
])
if (nuxt.options.srcDir !== nuxt.options.rootDir) {
include.add(join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*'))
}
if (nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir) {
include.add(join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*'))
}
for (const layer of nuxt.options._layers) {
const srcOrCwd = layer.config.srcDir ?? layer.cwd
if (!srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules')) {
include.add(join(relative(nuxt.options.buildDir, srcOrCwd), '**/*'))
}
}
const exclude = new Set<string>([
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')),
])
for (const dir of nuxt.options.modulesDir) {
exclude.add(relativeWithDot(nuxt.options.buildDir, dir))
}
const moduleEntryPaths: string[] = []
for (const m of nuxt.options._installedModules) {
if (m.entryPath) {
moduleEntryPaths.push(getDirectory(m.entryPath))
}
}
const modulePaths = await resolveNuxtModule(rootDirWithSlash, moduleEntryPaths)
for (const path of modulePaths) {
const relative = relativeWithDot(nuxt.options.buildDir, path)
include.add(join(relative, 'runtime'))
exclude.add(join(relative, 'runtime/server'))
}
const isV4 = nuxt.options.future?.compatibilityVersion === 4
const hasTypescriptVersionWithModulePreserve = await readPackageJSON('typescript', { url: nuxt.options.modulesDir })
.then(r => r?.version && gte(r.version, '5.4.0'))
.catch(() => isV4)
@ -168,23 +205,8 @@ export async function _generateTypes (nuxt: Nuxt) {
noImplicitThis: true, /* enabled with `strict` */
allowSyntheticDefaultImports: true,
},
include: [
'./nuxt.d.ts',
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '.config/nuxt.*'),
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'),
...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [],
...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd)
.filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules'))
.map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')),
...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : [],
...modulePaths.map(m => join(relativeWithDot(nuxt.options.buildDir, m), 'runtime')),
],
exclude: [
...nuxt.options.modulesDir.map(m => relativeWithDot(nuxt.options.buildDir, m)),
...modulePaths.map(m => join(relativeWithDot(nuxt.options.buildDir, m), 'runtime/server')),
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')),
],
include: [...include],
exclude: [...exclude],
} satisfies TSConfig)
const aliases: Record<string, string> = {
@ -195,7 +217,9 @@ export async function _generateTypes (nuxt: Nuxt) {
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/]
const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir
const basePath = tsConfig.compilerOptions!.baseUrl
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
: nuxt.options.buildDir
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
tsConfig.include = tsConfig.include || []
@ -237,12 +261,13 @@ export async function _generateTypes (nuxt: Nuxt) {
}
}
const references: TSReference[] = await Promise.all([
...nuxt.options.modules,
...nuxt.options._modules,
]
.filter(f => typeof f === 'string')
.map(async id => ({ types: (await readPackageJSON(id, { url: nodeModulePaths }).catch(() => null))?.name || id })))
const references: TSReference[] = []
await Promise.all([...nuxt.options.modules, ...nuxt.options._modules].map(async (id) => {
if (typeof id !== 'string') { return }
const pkg = await readPackageJSON(id, { url: getNodeModulesPaths(nuxt.options.modulesDir) }).catch(() => null)
references.push(({ types: pkg?.name || id }))
}))
const declarations: string[] = []
@ -302,7 +327,11 @@ export async function writeTypes (nuxt: Nuxt) {
}
function renderAttrs (obj: Record<string, string>) {
return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ')
const attrs: string[] = []
for (const key in obj) {
attrs.push(renderAttr(key, obj[key]))
}
return attrs.join(' ')
}
function renderAttr (key: string, value: string) {

View File

@ -53,12 +53,12 @@ describe('tsConfig generation', () => {
}))
expect(tsConfig.exclude).toMatchInlineSnapshot(`
[
"../dist",
"../modules/test/node_modules",
"../modules/node_modules",
"../node_modules/@some/module/node_modules",
"../node_modules",
"../../node_modules",
"../dist",
]
`)
})

View File

@ -1,6 +1,6 @@
{
"name": "nuxt",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -65,12 +65,12 @@
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.5.4",
"@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.9.12",
"@unhead/ssr": "^1.9.12",
"@unhead/vue": "^1.9.12",
"@unhead/dom": "^1.9.13",
"@unhead/ssr": "^1.9.13",
"@unhead/vue": "^1.9.13",
"@vue/shared": "^3.4.27",
"acorn": "8.11.3",
"c12": "^1.10.0",
"c12": "^1.11.1",
"chokidar": "^3.6.0",
"cookie-es": "^1.1.0",
"defu": "^6.1.4",
@ -90,7 +90,7 @@
"magic-string": "^0.30.10",
"mlly": "^1.7.1",
"nitropack": "^2.9.6",
"nuxi": "^3.11.1",
"nuxi": "^3.12.0",
"nypm": "^0.3.8",
"ofetch": "^1.3.4",
"ohash": "^1.1.3",
@ -118,6 +118,7 @@
"vue-router": "^4.3.3"
},
"devDependencies": {
"@nuxt/scripts": "0.5.1",
"@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5",
@ -125,7 +126,7 @@
"@vitejs/plugin-vue": "5.0.4",
"@vue/compiler-sfc": "3.4.27",
"unbuild": "latest",
"vite": "5.2.13",
"vite": "5.3.0",
"vitest": "1.6.0"
},
"peerDependencies": {

View File

@ -1,4 +1,4 @@
import type { Component, PropType } from 'vue'
import type { Component, PropType, VNode } from 'vue'
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
@ -29,7 +29,7 @@ const getId = import.meta.client ? () => (id++).toString() : randomUUID
const components = import.meta.client ? new Map<string, Component>() : undefined
async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['components']) {
const promises = []
const promises: Array<Promise<void>> = []
for (const component in paths) {
if (!(components!.has(component))) {
@ -259,7 +259,7 @@ export default defineComponent({
// should away be triggered ONE tick after re-rendering the static node
withMemo([teleportKey.value], () => {
const teleports = []
const teleports: Array<VNode> = []
// this is used to force trigger Teleport when vue makes the diff between old and new node
const isKeyOdd = teleportKey.value === 0 || !!(teleportKey.value && !(teleportKey.value % 2))

View File

@ -8,7 +8,7 @@ import type {
} from 'vue'
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue'
import type { RouteLocation, RouteLocationRaw, Router, RouterLink, RouterLinkProps, useLink } from '#vue-router'
import { hasProtocol, joinURL, parseQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo'
import { hasProtocol, joinURL, parseQuery, withQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo'
import { preloadRouteComponents } from '../composables/preload'
import { onNuxtReady } from '../composables/ready'
import { navigateTo, useRouter } from '../composables/router'
@ -70,8 +70,8 @@ export interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
* @see https://nuxt.com/docs/api/components/nuxt-link
*/
export interface NuxtLinkOptions extends
Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>,
Pick<NuxtLinkProps, 'prefetchedClass'> {
Partial<Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>>,
Partial<Pick<NuxtLinkProps, 'prefetchedClass'>> {
/**
* The name of the component.
* @default "NuxtLink"
@ -124,34 +124,17 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
const router = useRouter()
const config = useRuntimeConfig()
// Resolving `to` value from `to` and `href` props
const to: ComputedRef<string | RouteLocationRaw> = computed(() => {
checkPropConflicts(props, 'to', 'href')
const path = props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
return resolveTrailingSlashBehavior(path, router.resolve)
})
const hasTarget = computed(() => !!props.target && props.target !== '_self')
// Lazily check whether to.value has a protocol
const isAbsoluteUrl = computed(() => typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true }))
// Resolves `to` value if it's a route location object
const href = computed(() => (typeof to.value === 'object'
? router.resolve(to.value)?.href ?? null
: (to.value && !props.external && !isAbsoluteUrl.value)
? resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve) as string
: to.value
))
const isAbsoluteUrl = computed(() => {
const path = props.to || props.href || ''
return typeof path === 'string' && hasProtocol(path, { acceptRelative: true })
})
const builtinRouterLink = resolveComponent('RouterLink') as string | typeof RouterLink
const useBuiltinLink = builtinRouterLink && typeof builtinRouterLink !== 'string' ? builtinRouterLink.useLink : undefined
const link = useBuiltinLink?.({
...props,
to: to.value,
})
const hasTarget = computed(() => props.target && props.target !== '_self')
// Resolving link type
const isExternal = computed<boolean>(() => {
// External prop is explicitly set
@ -159,17 +142,40 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
return true
}
// When `target` prop is set, link is external
if (hasTarget.value) {
return true
}
const path = props.to || props.href || ''
// When `to` is a route object then it's an internal link
if (typeof to.value === 'object') {
if (typeof path === 'object') {
return false
}
return to.value === '' || isAbsoluteUrl.value
return path === '' || isAbsoluteUrl.value
})
// Resolving `to` value from `to` and `href` props
const to: ComputedRef<RouteLocationRaw> = computed(() => {
checkPropConflicts(props, 'to', 'href')
const path = props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
if (isExternal.value) { return path }
return resolveTrailingSlashBehavior(path, router.resolve)
})
const link = isExternal.value ? undefined : useBuiltinLink?.({ ...props, to })
// Resolves `to` value if it's a route location object
const href = computed(() => {
if (!to.value || isAbsoluteUrl.value) { return to.value as string }
if (isExternal.value) {
const path = typeof to.value === 'object' ? resolveRouteObject(to.value) : to.value
return resolveTrailingSlashBehavior(path, router.resolve /* will not be called */) as string
}
if (typeof to.value === 'object') {
return router.resolve(to.value)?.href ?? null
}
return resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve /* will not be called */)
})
return {
@ -183,10 +189,10 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
isExactActive: link?.isExactActive ?? computed(() => to.value === router.currentRoute.value.path),
route: link?.route ?? computed(() => router.resolve(to.value)),
async navigate () {
await navigateTo(href.value, { replace: props.replace, external: props.external })
await navigateTo(href.value, { replace: props.replace, external: isExternal.value || hasTarget.value })
},
} satisfies ReturnType<typeof useLink> & {
to: ComputedRef<string | RouteLocationRaw>
to: ComputedRef<RouteLocationRaw>
hasTarget: ComputedRef<boolean | null | undefined>
isAbsoluteUrl: ComputedRef<boolean>
isExternal: ComputedRef<boolean>
@ -307,10 +313,12 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
unobserve?.()
unobserve = null
const path = typeof to.value === 'string' ? to.value : router.resolve(to.value).fullPath
const path = typeof to.value === 'string'
? to.value
: isExternal.value ? resolveRouteObject(to.value) : router.resolve(to.value).fullPath
await Promise.all([
nuxtApp.hooks.callHook('link:prefetch', path).catch(() => {}),
!isExternal.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
!isExternal.value && !hasTarget.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
])
prefetched.value = true
})
@ -336,7 +344,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
}
return () => {
if (!isExternal.value) {
if (!isExternal.value && !hasTarget.value) {
const routerLinkProps: RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = {
ref: elRef,
to: to.value,
@ -408,7 +416,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
},
rel,
target,
isExternal: isExternal.value,
isExternal: isExternal.value || hasTarget.value,
isActive: false,
isExactActive: false,
})
@ -487,3 +495,7 @@ function isSlowConnection () {
if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true }
return false
}
function resolveRouteObject (to: Exclude<RouteLocationRaw, string>) {
return withQuery(to.path || '', to.query || {}) + (to.hash ? '#' + to.hash : '')
}

View File

@ -86,7 +86,7 @@ export function vforToArray (source: any): any[] {
if (import.meta.dev && !Number.isInteger(source)) {
console.warn(`The v-for range expect an integer value but got ${source}.`)
}
const array = []
const array: number[] = []
for (let i = 0; i < source; i++) {
array[i] = i
}

View File

@ -37,3 +37,4 @@ export { reloadNuxtApp } from './chunk'
export { useRequestURL } from './url'
export { usePreviewMode } from './preview'
export { useId } from './id'
export { useRouteAnnouncer } from './route-announcer'

View File

@ -3,6 +3,7 @@ import { parse } from 'devalue'
import { useHead } from '@unhead/vue'
import { getCurrentInstance, onServerPrefetch } from 'vue'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import type { NuxtPayload } from '../nuxt'
import { useRoute } from './router'
import { getAppManifest, getRouteRules } from './manifest'
@ -95,11 +96,12 @@ export async function isPrerendered (url = useRoute().path) {
return !!rules.prerender && !rules.redirect
}
let payloadCache: any = null
let payloadCache: NuxtPayload | null = null
/** @since 3.4.0 */
export async function getNuxtClientPayload () {
if (import.meta.server) {
return
return null
}
if (payloadCache) {
return payloadCache
@ -107,7 +109,7 @@ export async function getNuxtClientPayload () {
const el = document.getElementById('__NUXT_DATA__')
if (!el) {
return {}
return {} as Partial<NuxtPayload>
}
const inlineData = await parsePayload(el.textContent || '')

View File

@ -7,8 +7,8 @@ export const onNuxtReady = (callback: () => any) => {
const nuxtApp = useNuxtApp()
if (nuxtApp.isHydrating) {
nuxtApp.hooks.hookOnce('app:suspense:resolve', () => { requestIdleCallback(callback) })
nuxtApp.hooks.hookOnce('app:suspense:resolve', () => { requestIdleCallback(() => callback()) })
} else {
requestIdleCallback(callback)
requestIdleCallback(() => callback())
}
}

View File

@ -69,7 +69,7 @@ export interface NuxtSSRContext extends SSRContext {
/** whether we are rendering an SSR error */
error?: boolean
nuxt: _NuxtApp
payload: NuxtPayload
payload: Partial<NuxtPayload>
head: VueHeadClient<MergeHead>
/** This is used solely to render runtime config with SPA renderer. */
config?: Pick<RuntimeConfig, 'public' | 'app'>
@ -558,6 +558,7 @@ export function defineAppConfig<C extends AppConfigInput> (config: C): C {
/**
* Configure error getter on runtime secret property access that doesn't exist on the client side
*/
const loggedKeys = new Set<string>()
function wrappedConfig (runtimeConfig: Record<string, unknown>) {
if (!import.meta.dev || import.meta.server) { return runtimeConfig }
const keys = Object.keys(runtimeConfig).map(key => `\`${key}\``)
@ -565,7 +566,10 @@ function wrappedConfig (runtimeConfig: Record<string, unknown>) {
return new Proxy(runtimeConfig, {
get (target, p, receiver) {
if (typeof p === 'string' && p !== 'public' && !(p in target) && !p.startsWith('__v') /* vue check for reactivity, e.g. `__v_isRef` */) {
console.warn(`[nuxt] Could not access \`${p}\`. The only available runtime config keys on the client side are ${keys.join(', ')} and ${lastKey}. See \`https://nuxt.com/docs/guide/going-further/runtime-config\` for more information.`)
if (!loggedKeys.has(p)) {
loggedKeys.add(p)
console.warn(`[nuxt] Could not access \`${p}\`. The only available runtime config keys on the client side are ${keys.join(', ')} and ${lastKey}. See https://nuxt.com/docs/guide/going-further/runtime-config for more information.`)
}
}
return Reflect.get(target, p, receiver)
},

View File

@ -136,6 +136,7 @@ function createGranularWatcher () {
console.timeEnd('[nuxt] builder:chokidar:watch')
}
})
nuxt.hook('close', () => watcher?.close())
}
}

View File

@ -43,7 +43,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
const modules = await resolveNuxtModule(rootDirWithSlash,
nuxt.options._installedModules
.filter(m => m.entryPath)
.map(m => m.entryPath),
.map(m => m.entryPath!),
)
const nitroConfig: NitroConfig = defu(nuxt.options.nitro, {

View File

@ -4,7 +4,7 @@ import ignore from 'ignore'
import type { LoadNuxtOptions } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addServerPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
import { resolvePath as _resolvePath } from 'mlly'
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions, RuntimeConfig } from 'nuxt/schema'
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema'
import type { PackageJson } from 'pkg-types'
import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
import { hash } from 'ohash'
@ -49,7 +49,10 @@ export function createNuxt (options: NuxtOptions): Nuxt {
addHooks: hooks.addHooks,
hook: hooks.hook,
ready: () => initNuxt(nuxt),
close: () => Promise.resolve(hooks.callHook('close', nuxt)),
close: async () => {
await hooks.callHook('close', nuxt)
hooks.removeAllHooks()
},
vfs: {},
apps: {},
}
@ -125,6 +128,7 @@ async function initNuxt (nuxt: Nuxt) {
// Add nuxt types
nuxt.hook('prepare:types', (opts) => {
opts.references.push({ types: 'nuxt' })
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/app-defaults.d.ts') })
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/plugins.d.ts') })
// Add vue shim
if (nuxt.options.typescript.shim) {
@ -641,8 +645,22 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
options._modules.push('@nuxt/telemetry')
}
// Ensure we share runtime config between Nuxt and Nitro
options.runtimeConfig = options.nitro.runtimeConfig as RuntimeConfig
// Ensure we share key config between Nuxt and Nitro
createPortalProperties(options.nitro.runtimeConfig, options, ['nitro.runtimeConfig', 'runtimeConfig'])
createPortalProperties(options.nitro.routeRules, options, ['nitro.routeRules', 'routeRules'])
// prevent replacement of options.nitro
const nitroOptions = options.nitro
Object.defineProperties(options, {
nitro: {
configurable: false,
enumerable: true,
get: () => nitroOptions,
set (value) {
Object.assign(nitroOptions, value)
},
},
})
const nuxt = createNuxt(options)
@ -680,7 +698,7 @@ const RESTART_RE = /^(?:app|error|app\.config)\.(?:js|ts|mjs|jsx|tsx|vue)$/i
function deduplicateArray<T = unknown> (maybeArray: T): T {
if (!Array.isArray(maybeArray)) { return maybeArray }
const fresh = []
const fresh: any[] = []
const hashes = new Set<string>()
for (const item of maybeArray) {
const _hash = hash(item)
@ -691,3 +709,31 @@ function deduplicateArray<T = unknown> (maybeArray: T): T {
}
return fresh as T
}
function createPortalProperties (sourceValue: any, options: NuxtOptions, paths: string[]) {
let sharedValue = sourceValue
for (const path of paths) {
const segments = path.split('.')
const key = segments.pop()!
let parent: Record<string, any> = options
while (segments.length) {
const key = segments.shift()!
parent = parent[key] || (parent[key] = {})
}
delete parent[key]
Object.defineProperties(parent, {
[key]: {
configurable: false,
enumerable: true,
get: () => sharedValue,
set (value) {
sharedValue = value
},
},
})
}
}

View File

@ -165,11 +165,7 @@ const getSPARenderer = lazyCachedFunction(async () => {
const config = useRuntimeConfig(ssrContext.event)
ssrContext.modules = ssrContext.modules || new Set<string>()
ssrContext!.payload = {
_errors: {},
serverRendered: false,
data: {},
state: {},
once: new Set<string>(),
}
ssrContext.config = {
public: config.public,
@ -404,7 +400,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// 2. Styles
head.push({ style: inlinedStyles })
if (!isRenderingIsland || import.meta.dev) {
const link = []
const link: Link[] = []
for (const style in styles) {
const resource = styles[style]
// Do not add links to resources that are inlined (vite v5+)

View File

@ -7,7 +7,7 @@ import escapeRE from 'escape-string-regexp'
import { hash } from 'ohash'
import { camelCase } from 'scule'
import { filename } from 'pathe/utils'
import type { NuxtTemplate } from 'nuxt/schema'
import type { NuxtTemplate, NuxtTypeTemplate } from 'nuxt/schema'
import { annotatePlugins, checkForCircularDependencies } from './app'
@ -96,6 +96,20 @@ export const serverPluginTemplate: NuxtTemplate = {
},
}
export const appDefaults: NuxtTypeTemplate = {
filename: 'types/app-defaults.d.ts',
getContents: (ctx) => {
const isV4 = ctx.nuxt.options.future.compatibilityVersion === 4
return `
declare module '#app/defaults' {
type DefaultAsyncDataErrorValue = ${isV4 ? 'undefined' : 'null'}
type DefaultAsyncDataValue = ${isV4 ? 'undefined' : 'null'}
type DefaultErrorValue = ${isV4 ? 'undefined' : 'null'}
type DedupeOption = ${isV4 ? '\'cancel\' | \'defer\'' : 'boolean | \'cancel\' | \'defer\''}
}`
},
}
export const pluginsDeclaration: NuxtTemplate = {
filename: 'types/plugins.d.ts',
getContents: async (ctx) => {
@ -112,8 +126,6 @@ export const pluginsDeclaration: NuxtTemplate = {
const pluginsName = (await annotatePlugins(ctx.nuxt, ctx.app.plugins)).filter(p => p.name).map(p => `'${p.name}'`)
const isV4 = ctx.nuxt.options.future.compatibilityVersion === 4
return `// Generated by Nuxt'
import type { Plugin } from '#app'
@ -132,13 +144,6 @@ declare module '#app' {
}
}
declare module '#app/defaults' {
type DefaultAsyncDataErrorValue = ${isV4 ? 'undefined' : 'null'}
type DefaultAsyncDataValue = ${isV4 ? 'undefined' : 'null'}
type DefaultErrorValue = ${isV4 ? 'undefined' : 'null'}
type DedupeOption = ${isV4 ? '\'cancel\' | \'defer\'' : 'boolean | \'cancel\' | \'defer\''}
}
declare module 'vue' {
interface ComponentCustomProperties extends NuxtAppInjections { }
}

View File

@ -109,6 +109,10 @@ const granularAppPresets: InlinePreset[] = [
imports: ['useId'],
from: '#app/composables/id',
},
{
imports: ['useRouteAnnouncer'],
from: '#app/composables/route-announcer',
},
]
export const scriptsStubsPreset = {
@ -119,19 +123,21 @@ export const scriptsStubsPreset = {
'useScript',
'useScriptGoogleAnalytics',
'useScriptPlausibleAnalytics',
'useScriptClarity',
'useScriptCloudflareWebAnalytics',
'useScriptFathomAnalytics',
'useScriptMatomoAnalytics',
'useScriptGoogleTagManager',
'useScriptGoogleAdsense',
'useScriptSegment',
'useScriptFacebookPixel',
'useScriptMetaPixel',
'useScriptXPixel',
'useScriptIntercom',
'useScriptHotjar',
'useScriptStripe',
'useScriptLemonSqueezy',
'useScriptVimeoPlayer',
'useScriptYouTubeIframe',
'useScriptYouTubePlayer',
'useScriptGoogleMaps',
'useScriptNpm',
],

View File

@ -275,6 +275,20 @@ export default defineNuxtModule({
}
})
// TODO: inject routes in `200.html` in next nitro upgrade (2.9.7+) via https://github.com/unjs/nitro/pull/2517
if (!nuxt.options.dev && !nuxt.options._prepare) {
nuxt.hook('app:templatesGenerated', (app) => {
const nitro = useNitro()
if (nitro.options.prerender.crawlLinks) {
for (const page of app.pages!) {
if (page.path && !page.path.includes(':')) {
nitro.options.prerender.routes.push(page.path)
}
}
}
})
}
nuxt.hook('imports:extend', (imports) => {
imports.push(
{ name: 'definePageMeta', as: 'definePageMeta', from: resolve(runtimeDir, 'composables') },

View File

@ -18,11 +18,12 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
}
if (!ROUTE_RULE_RE.test(code)) { return null }
code = extractScriptContent(code) || code
const script = extractScriptContent(code)
code = script?.code || code
let rule: NitroRouteConfig | null = null
const js = await transform(code, { loader: 'ts' })
const js = await transform(code, { loader: script?.loader || 'ts' })
walk(parse(js.code, {
sourceType: 'module',
ecmaVersion: 'latest',

View File

@ -9,6 +9,7 @@ import { filename } from 'pathe/utils'
import { hash } from 'ohash'
import { transform } from 'esbuild'
import { parse } from 'acorn'
import { walk } from 'estree-walker'
import type { CallExpression, ExpressionStatement, ObjectExpression, Program, Property } from 'estree'
import type { NuxtPage } from 'nuxt/schema'
@ -139,11 +140,12 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
return prepareRoutes(routes)
}
export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, augmentedPages = new Set<NuxtPage>()) {
export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, augmentedPages = new Set<string>()) {
for (const route of routes) {
if (!augmentedPages.has(route) && route.file) {
if (route.file && !augmentedPages.has(route.file)) {
const fileContent = route.file in vfs ? vfs[route.file] : fs.readFileSync(await resolvePath(route.file), 'utf-8')
Object.assign(route, await getRouteMeta(fileContent, route.file))
augmentedPages.add(route.file)
}
if (route.children && route.children.length > 0) {
@ -153,12 +155,15 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
return augmentedPages
}
const SFC_SCRIPT_RE = /<script[^>]*>([\s\S]*?)<\/script[^>]*>/i
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/i
export function extractScriptContent (html: string) {
const match = html.match(SFC_SCRIPT_RE)
const groups = html.match(SFC_SCRIPT_RE)?.groups || {}
if (match && match[1]) {
return match[1].trim()
if (groups.content) {
return {
loader: groups.attrs.includes('tsx') ? 'tsx' : 'ts',
code: groups.content.trim(),
} as const
}
return null
@ -169,7 +174,7 @@ const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
const pageContentsCache: Record<string, string> = {}
const metaCache: Record<string, Partial<Record<keyof NuxtPage, any>>> = {}
async function getRouteMeta (contents: string, absolutePath: string): Promise<Partial<Record<keyof NuxtPage, any>>> {
export async function getRouteMeta (contents: string, absolutePath: string): Promise<Partial<Record<keyof NuxtPage, any>>> {
// set/update pageContentsCache, invalidate metaCache on cache mismatch
if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) {
pageContentsCache[absolutePath] = contents
@ -184,81 +189,88 @@ async function getRouteMeta (contents: string, absolutePath: string): Promise<Pa
return {}
}
if (!PAGE_META_RE.test(script)) {
if (!PAGE_META_RE.test(script.code)) {
metaCache[absolutePath] = {}
return {}
}
const js = await transform(script, { loader: 'ts' })
const js = await transform(script.code, { loader: script.loader })
const ast = parse(js.code, {
sourceType: 'module',
ecmaVersion: 'latest',
ranges: true,
}) as unknown as Program
const pageMetaAST = ast.body.find(node => node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' && node.expression.callee.type === 'Identifier' && node.expression.callee.name === 'definePageMeta')
if (!pageMetaAST) {
metaCache[absolutePath] = {}
return {}
}
const pageMetaArgument = ((pageMetaAST as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
const dynamicProperties = new Set<keyof NuxtPage>()
for (const key of extractionKeys) {
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
if (!property) { continue }
let foundMeta = false
if (property.value.type === 'ObjectExpression') {
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
try {
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
} catch {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)
dynamicProperties.add(key)
continue
}
}
walk(ast, {
enter (node) {
if (foundMeta) { return }
if (property.value.type === 'ArrayExpression') {
const values = []
for (const element of property.value.elements) {
if (!element) {
if (node.type !== 'ExpressionStatement' || node.expression.type !== 'CallExpression' || node.expression.callee.type !== 'Identifier' || node.expression.callee.name !== 'definePageMeta') { return }
foundMeta = true
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
for (const key of extractionKeys) {
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
if (!property) { continue }
if (property.value.type === 'ObjectExpression') {
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
try {
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
} catch {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)
dynamicProperties.add(key)
continue
}
}
if (property.value.type === 'ArrayExpression') {
const values: string[] = []
for (const element of property.value.elements) {
if (!element) {
continue
}
if (element.type !== 'Literal' || typeof element.value !== 'string') {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)
dynamicProperties.add(key)
continue
}
values.push(element.value)
}
extractedMeta[key] = values
continue
}
if (element.type !== 'Literal' || typeof element.value !== 'string') {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)
if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
dynamicProperties.add(key)
continue
}
values.push(element.value)
extractedMeta[key] = property.value.value
}
extractedMeta[key] = values
continue
}
if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
dynamicProperties.add(key)
continue
}
extractedMeta[key] = property.value.value
}
const extraneousMetaKeys = pageMetaArgument.properties
.filter(property => property.type === 'Property' && property.key.type === 'Identifier' && !(extractionKeys as unknown as string[]).includes(property.key.name))
// @ts-expect-error inferred types have been filtered out
.map(property => property.key.name)
const extraneousMetaKeys = pageMetaArgument.properties
.filter(property => property.type === 'Property' && property.key.type === 'Identifier' && !(extractionKeys as unknown as string[]).includes(property.key.name))
// @ts-expect-error inferred types have been filtered out
.map(property => property.key.name)
if (extraneousMetaKeys.length) {
dynamicProperties.add('meta')
}
if (extraneousMetaKeys.length) {
dynamicProperties.add('meta')
}
if (dynamicProperties.size) {
extractedMeta.meta ??= {}
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
}
if (dynamicProperties.size) {
extractedMeta.meta ??= {}
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
}
},
})
metaCache[absolutePath] = extractedMeta
return extractedMeta
@ -501,19 +513,20 @@ async function createClientPage(loader) {
}`)
}
if (route.children != null) {
if (route.children) {
metaRoute.children = route.children
}
if (overrideMeta) {
metaRoute.name = `${metaImportName}?.name`
metaRoute.path = `${metaImportName}?.path ?? ''`
if (route.meta) {
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`
}
if (overrideMeta) {
// skip and retain fallback if marked dynamic
// set to extracted value or fallback if none extracted
for (const key of ['name', 'path'] satisfies NormalizedRouteKeys) {
if (markedDynamic.has(key)) { continue }
metaRoute[key] = route[key] ?? metaRoute[key]
metaRoute[key] = route[key] ?? `${metaImportName}?.${key}`
}
// set to extracted value or delete if none extracted
@ -528,10 +541,6 @@ async function createClientPage(loader) {
metaRoute[key] = route[key]
}
} else {
if (route.meta != null) {
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`
}
if (route.alias != null) {
metaRoute.alias = `${route.alias}.concat(${metaImportName}?.alias || [])`
}

View File

@ -303,7 +303,7 @@
"alias": "mockMeta?.alias || []",
"component": "() => import("pages/index.vue").then(m => m.default || m)",
"meta": "mockMeta || {}",
"name": "mockMeta?.name",
"name": "mockMeta?.name ?? "index"",
"path": ""/"",
"redirect": "mockMeta?.redirect",
},

View File

@ -6,8 +6,9 @@ import * as VueFunctions from 'vue'
import type { Import } from 'unimport'
import { createUnimport } from 'unimport'
import type { Plugin } from 'vite'
import { registry as scriptRegistry } from '@nuxt/scripts/registry'
import { TransformPlugin } from '../src/imports/transform'
import { defaultPresets } from '../src/imports/presets'
import { defaultPresets, scriptsStubsPreset } from '../src/imports/presets'
describe('imports:transform', () => {
const imports: Import[] = [
@ -193,3 +194,24 @@ describe('imports:vue', () => {
})
}
})
describe('imports:nuxt/scripts', () => {
const scripts = scriptRegistry().map(s => s.import?.name).filter(Boolean)
const globalScripts = new Set([
'useScript',
'useAnalyticsPageEvent',
'useElementScriptTrigger',
'useConsentScriptTrigger',
// registered separately
'useScriptGoogleTagManager',
'useScriptGoogleAnalytics',
])
it.each(scriptsStubsPreset.imports)(`should register %s from @nuxt/scripts`, (name) => {
if (globalScripts.has(name)) { return }
expect(scripts).toContain(name)
})
it.each(scripts)(`should register %s from @nuxt/scripts`, (name) => {
expect(scriptsStubsPreset.imports).toContain(name)
})
})

View File

@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import type { RouteLocation, RouteLocationRaw } from 'vue-router'
import type { RouteLocation } from 'vue-router'
import type { NuxtLinkOptions, NuxtLinkProps } from '../src/app/components/nuxt-link'
import { defineNuxtLink } from '../src/app/components/nuxt-link'
import { useRuntimeConfig } from '../src/app/nuxt'
@ -99,7 +99,11 @@ describe('nuxt-link:isExternal', () => {
})
it('returns `false` when `to` is a route location object', () => {
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw }).type).toBe(INTERNAL)
expect(nuxtLink({ to: { path: '/to' } }).type).toBe(INTERNAL)
})
it('returns `true` when `to` has a `target`', () => {
expect(nuxtLink({ to: { path: '/to' }, target: '_blank' }).type).toBe(EXTERNAL)
})
it('honors `external` prop', () => {
@ -122,7 +126,12 @@ describe('nuxt-link:propsOrAttributes', () => {
})
it('resolves route location object', () => {
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw, external: true }).props.href).toBe('/to')
expect(nuxtLink({ to: { path: '/to' }, external: true }).props.href).toBe('/to')
})
it('applies trailing slash behaviour', () => {
expect(nuxtLink({ to: { path: '/to' }, external: true }, { trailingSlash: 'append' }).props.href).toBe('/to/')
expect(nuxtLink({ to: '/to', external: true }, { trailingSlash: 'append' }).props.href).toBe('/to/')
})
})
@ -167,6 +176,8 @@ describe('nuxt-link:propsOrAttributes', () => {
}, () => {
expect(nuxtLink({ to: 'http://nuxtjs.org/app/about', target: '_blank' }).props.href).toBe('http://nuxtjs.org/app/about')
expect(nuxtLink({ to: '//nuxtjs.org/app/about', target: '_blank' }).props.href).toBe('//nuxtjs.org/app/about')
expect(nuxtLink({ to: { path: '/' }, external: true }).props.href).toBe('/')
expect(nuxtLink({ to: '/', external: true }).props.href).toBe('/')
})
})
})
@ -209,7 +220,7 @@ describe('nuxt-link:propsOrAttributes', () => {
describe('to', () => {
it('forwards `to` prop', () => {
expect(nuxtLink({ to: '/to' }).props.to).toBe('/to')
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw }).props.to).toEqual({ to: '/to' })
expect(nuxtLink({ to: { path: '/to' } }).props.to).toEqual({ path: '/to' })
})
})

View File

@ -0,0 +1,148 @@
import { describe, expect, it } from 'vitest'
import { getRouteMeta, normalizeRoutes } from '../src/pages/utils'
import type { NuxtPage } from '../schema'
const filePath = '/app/pages/index.vue'
describe('page metadata', () => {
it('should not extract metadata from empty files', async () => {
expect(await getRouteMeta('', filePath)).toEqual({})
expect(await getRouteMeta('<template><div>Hi</div></template>', filePath)).toEqual({})
})
it('should use and invalidate cache', async () => {
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()
})
it('should extract serialisable metadata', async () => {
const meta = await getRouteMeta(`
<script setup>
definePageMeta({
name: 'some-custom-name',
path: '/some-custom-path',
validate: () => true,
middleware: [
function () {},
],
otherValue: {
foo: 'bar',
},
})
</script>
`, filePath)
expect(meta).toMatchInlineSnapshot(`
{
"meta": {
"__nuxt_dynamic_meta_key": Set {
"meta",
},
},
"name": "some-custom-name",
"path": "/some-custom-path",
}
`)
})
it('should extract serialisable metadata in options api', async () => {
const meta = await getRouteMeta(`
<script>
export default {
setup() {
definePageMeta({
name: 'some-custom-name',
path: '/some-custom-path',
middleware: (from, to) => console.warn('middleware'),
})
},
};
</script>
`, filePath)
expect(meta).toMatchInlineSnapshot(`
{
"meta": {
"__nuxt_dynamic_meta_key": Set {
"meta",
},
},
"name": "some-custom-name",
"path": "/some-custom-path",
}
`)
})
})
describe('normalizeRoutes', () => {
it('should produce valid route objects when used with extracted meta', async () => {
const page: NuxtPage = { path: '/', file: filePath }
Object.assign(page, await getRouteMeta(`
<script setup>
definePageMeta({
name: 'some-custom-name',
path: ref('/some-custom-path'), /* dynamic */
validate: () => true,
redirect: '/',
middleware: [
function () {},
],
otherValue: {
foo: 'bar',
},
})
</script>
`, filePath))
page.meta ||= {}
page.meta.layout = 'test'
page.meta.foo = 'bar'
const { routes, imports } = normalizeRoutes([page], new Set(), true)
expect({ routes, imports }).toMatchInlineSnapshot(`
{
"imports": Set {
"import { default as indexN6pT4Un8hYMeta } from "/app/pages/index.vue?macro=true";",
},
"routes": "[
{
name: "some-custom-name",
path: indexN6pT4Un8hYMeta?.path ?? "/",
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
redirect: "/",
component: () => import("/app/pages/index.vue").then(m => m.default || m)
}
]",
}
`)
})
it('should produce valid route objects when used without extracted meta', () => {
const page: NuxtPage = { path: '/', file: filePath }
page.meta ||= {}
page.meta.layout = 'test'
page.meta.foo = 'bar'
const { routes, imports } = normalizeRoutes([page], new Set())
expect({ routes, imports }).toMatchInlineSnapshot(`
{
"imports": Set {
"import { default as indexN6pT4Un8hYMeta } from "/app/pages/index.vue?macro=true";",
},
"routes": "[
{
name: indexN6pT4Un8hYMeta?.name ?? undefined,
path: indexN6pT4Un8hYMeta?.path ?? "/",
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
alias: indexN6pT4Un8hYMeta?.alias || [],
redirect: indexN6pT4Un8hYMeta?.redirect,
component: () => import("/app/pages/index.vue").then(m => m.default || m)
}
]",
}
`)
})
})

View File

@ -1,6 +1,6 @@
{
"name": "@nuxt/schema",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -39,13 +39,13 @@
"@types/file-loader": "5.0.4",
"@types/pug": "2.0.10",
"@types/sass-loader": "8.0.8",
"@unhead/schema": "1.9.12",
"@unhead/schema": "1.9.13",
"@vitejs/plugin-vue": "5.0.4",
"@vitejs/plugin-vue-jsx": "4.0.0",
"@vue/compiler-core": "3.4.27",
"@vue/compiler-sfc": "3.4.27",
"@vue/language-core": "2.0.21",
"c12": "1.10.0",
"c12": "1.11.1",
"esbuild-loader": "4.1.0",
"h3": "1.11.1",
"ignore": "5.3.1",
@ -54,16 +54,16 @@
"unbuild": "latest",
"unctx": "2.3.1",
"unenv": "1.9.0",
"vite": "5.2.13",
"vite": "5.3.0",
"vue": "3.4.27",
"vue-bundle-renderer": "2.1.0",
"vue-loader": "17.4.2",
"vue-router": "4.3.3",
"webpack": "5.91.0",
"webpack": "5.92.0",
"webpack-dev-middleware": "7.2.1"
},
"dependencies": {
"compatx": "^0.1.3",
"compatx": "^0.1.8",
"consola": "^3.2.3",
"defu": "^6.1.4",
"hookable": "^5.5.3",

View File

@ -1,4 +1,5 @@
import { existsSync } from 'node:fs'
import { readdir } from 'node:fs/promises'
import { defineUntypedSchema } from 'untyped'
import { join, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env'
@ -28,7 +29,7 @@ export default defineUntypedSchema({
*
* We plan to improve the tooling around this feature in the future.
*
* @type {typeof import('compatx').DateString | Record<string, typeof import('compatx').DateString>}
* @type {typeof import('compatx').CompatibilityDateSpec}
*/
compatibilityDate: undefined,
@ -117,7 +118,16 @@ export default defineUntypedSchema({
}
const srcDir = resolve(rootDir, 'app')
if (!existsSync(srcDir)) {
const srcDirFiles = new Set<string>()
if (existsSync(srcDir)) {
const files = await readdir(srcDir).catch(() => [])
for (const file of files) {
if (file !== 'spa-loading-template.html' && !file.startsWith('router.options')) {
srcDirFiles.add(file)
}
}
}
if (srcDirFiles.size === 0) {
for (const file of ['app.vue', 'App.vue']) {
if (existsSync(resolve(rootDir, file))) {
return rootDir

View File

@ -23,7 +23,10 @@ export default defineUntypedSchema({
_nuxtConfigFiles: [],
/** @private */
appDir: '',
/** @private */
/**
* @private
* @type {Array<{ meta: ModuleMeta; timings?: Record<string, number | undefined>; entryPath?: string }>}
*/
_installedModules: [],
/** @private */
_modules: [],

View File

@ -141,7 +141,7 @@ export interface AppConfigInput extends CustomAppConfig {
server?: never
}
type Serializable<T> = T extends Function ? never : T extends Promise<infer U> ? Serializable<U> : T extends Record<string, any> ? { [K in keyof T]: Serializable<T[K]> } : T
type Serializable<T> = T extends Function ? never : T extends Promise<infer U> ? Serializable<U> : T extends string & {} ? T : T extends Record<string, any> ? { [K in keyof T]: Serializable<T[K]> } : T
export interface NuxtAppConfig {
head: Serializable<AppHeadMetaObject>

View File

@ -21,7 +21,7 @@
"devDependencies": {
"@types/html-minifier": "4.0.5",
"@types/lodash-es": "4.17.12",
"@unocss/reset": "0.60.4",
"@unocss/reset": "0.61.0",
"critters": "0.0.22",
"execa": "9.2.0",
"globby": "14.0.1",
@ -30,9 +30,9 @@
"knitwork": "1.1.0",
"lodash-es": "4.17.21",
"pathe": "1.1.2",
"prettier": "3.3.1",
"prettier": "3.3.2",
"scule": "1.3.0",
"unocss": "0.60.4",
"vite": "5.2.13"
"unocss": "0.61.0",
"vite": "5.3.0"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@nuxt/vite-builder",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -28,6 +28,7 @@
"@types/clear": "0.1.4",
"@types/estree": "1.0.5",
"@types/fs-extra": "11.0.4",
"rollup": "4.18.0",
"unbuild": "latest",
"vue": "3.4.27"
},
@ -62,7 +63,7 @@
"ufo": "^1.5.3",
"unenv": "^1.9.0",
"unplugin": "^1.10.1",
"vite": "^5.2.13",
"vite": "^5.3.0",
"vite-node": "^1.6.0",
"vite-plugin-checker": "^0.6.4",
"vue-bundle-renderer": "^2.1.0"

View File

@ -163,10 +163,11 @@ export async function buildClient (ctx: ViteBuildContext) {
}
// We want to respect users' own rollup output options
const fileNames = withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js'))
clientConfig.build!.rollupOptions = defu(clientConfig.build!.rollupOptions!, {
output: {
chunkFileNames: ctx.nuxt.options.dev ? undefined : withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js')),
entryFileNames: ctx.nuxt.options.dev ? 'entry.js' : withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js')),
chunkFileNames: ctx.nuxt.options.dev ? undefined : fileNames,
entryFileNames: ctx.nuxt.options.dev ? 'entry.js' : fileNames,
} satisfies NonNullable<BuildOptions['rollupOptions']>['output'],
}) as any
@ -228,7 +229,13 @@ export async function buildClient (ctx: ViteBuildContext) {
})
const viteMiddleware = defineEventHandler(async (event) => {
const viteRoutes = viteServer.middlewares.stack.map(m => m.route).filter(r => r.length > 1)
const viteRoutes: string[] = []
for (const viteRoute of viteServer.middlewares.stack) {
const m = viteRoute.route
if (m.length > 1) {
viteRoutes.push(m)
}
}
if (!event.path.startsWith(clientConfig.base!) && !viteRoutes.some(route => event.path.startsWith(route))) {
// @ts-expect-error _skip_transform is a private property
event.node.req._skip_transform = true

View File

@ -3,6 +3,8 @@ import type { Nuxt } from '@nuxt/schema'
import type { InlineConfig as ViteConfig } from 'vite'
import { distDir } from './dirs'
const lastPlugins = ['autoprefixer', 'cssnano']
export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
const css: ViteConfig['css'] & { postcss: NonNullable<Exclude<NonNullable<ViteConfig['css']>['postcss'], string>> } = {
postcss: {
@ -10,19 +12,22 @@ export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
},
}
const lastPlugins = ['autoprefixer', 'cssnano']
css.postcss.plugins = Object.entries(nuxt.options.postcss.plugins)
css.postcss.plugins = []
const plugins = Object.entries(nuxt.options.postcss.plugins)
.sort((a, b) => lastPlugins.indexOf(a[0]) - lastPlugins.indexOf(b[0]))
.filter(([, opts]) => opts)
.map(([name, opts]) => {
for (const [name, opts] of plugins) {
if (opts) {
const plugin = requireModule(name, {
paths: [
...nuxt.options.modulesDir,
distDir,
],
})
return plugin(opts)
})
css.postcss.plugins.push(plugin(opts))
}
}
return css
}

View File

@ -238,7 +238,13 @@ export async function initViteDevBundler (ctx: ViteBuildContext, onBuild: () =>
const { code, ids } = await bundleRequest(options, ctx.entry)
await fse.writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs'), code, 'utf-8')
// Have CSS in the manifest to prevent FOUC on dev SSR
await writeManifest(ctx, ids.filter(isCSS).map(i => i.slice(1)))
const manifestIds: string[] = []
for (const i of ids) {
if (isCSS(i)) {
manifestIds.push(i.slice(1))
}
}
await writeManifest(ctx, manifestIds)
const time = (Date.now() - start)
logger.success(`Vite server built in ${time}ms`)
await onBuild()

View File

@ -50,11 +50,14 @@ export async function writeManifest (ctx: ViteBuildContext, css: string[] = [])
await fse.mkdirp(serverDist)
if (ctx.config.build?.cssCodeSplit === false) {
const entryCSS = Object.values(clientManifest as Record<string, { file?: string }>).find(val => (val).file?.endsWith('.css'))?.file
if (entryCSS) {
const key = relative(ctx.config.root!, ctx.entry)
clientManifest[key].css ||= []
clientManifest[key].css!.push(entryCSS)
for (const key in clientManifest as Record<string, { file?: string }>) {
const val = clientManifest[key]
if (val.file?.endsWith('.css')) {
const key = relative(ctx.config.root!, ctx.entry)
clientManifest[key].css ||= []
clientManifest[key].css!.push(val.file)
break
}
}
}

View File

@ -3,6 +3,7 @@ import { transform } from 'esbuild'
import { visualizer } from 'rollup-plugin-visualizer'
import defu from 'defu'
import type { NuxtOptions } from 'nuxt/schema'
import type { RenderedModule } from 'rollup'
import type { ViteBuildContext } from '../vite'
export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
@ -13,14 +14,18 @@ export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
{
name: 'nuxt:analyze-minify',
async generateBundle (_opts, outputBundle) {
for (const [_bundleId, bundle] of Object.entries(outputBundle)) {
for (const _bundleId in outputBundle) {
const bundle = outputBundle[_bundleId]
if (bundle.type !== 'chunk') { continue }
const originalEntries = Object.entries(bundle.modules)
const minifiedEntries = await Promise.all(originalEntries.map(async ([moduleId, module]) => {
const { code } = await transform(module.code || '', { minify: true })
return [moduleId, { ...module, code }]
}))
bundle.modules = Object.fromEntries(minifiedEntries)
const minifiedModuleEntryPromises: Array<Promise<[string, RenderedModule]>> = []
for (const moduleId in bundle.modules) {
const module = bundle.modules[moduleId]
minifiedModuleEntryPromises.push(
transform(module.code || '', { minify: true })
.then(result => [moduleId, { ...module, code: result.code }]),
)
}
bundle.modules = Object.fromEntries(await Promise.all(minifiedModuleEntryPromises))
}
},
},

View File

@ -23,12 +23,15 @@ const SUPPORTED_EXT_RE = /\.(?:m?[jt]sx?|vue)/
export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => {
const composableMeta: Record<string, any> = {}
const composableLengths = new Set<number>()
const keyedFunctions = new Set<string>()
for (const { name, ...meta } of options.composables) {
composableMeta[name] = meta
keyedFunctions.add(name)
composableLengths.add(meta.argumentLength)
}
const maxLength = Math.max(...options.composables.map(({ argumentLength }) => argumentLength))
const keyedFunctions = new Set(options.composables.map(({ name }) => name))
const maxLength = Math.max(...composableLengths)
const KEYED_FUNCTIONS_RE = new RegExp(`\\b(${[...keyedFunctions].map(f => escapeRE(f)).join('|')})\\b`)
return {

View File

@ -25,7 +25,7 @@ export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boole
resolveId: {
enforce: 'post',
handler (id) {
if (id === '/__skip_vite' || !id.startsWith('/') || id.startsWith('/@fs')) { return }
if (id === '/__skip_vite' || id[0] !== '/' || id.startsWith('/@fs')) { return }
if (resolveFromPublicAssets(id)) {
return PREFIX + encodeURIComponent(id)

View File

@ -66,12 +66,12 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
const { files, inBundle } = cssMap[file]
// File has been tree-shaken out of build (or there are no styles to inline)
if (!files.length || !inBundle) { continue }
const fileName = filename(file)
const base = typeof outputOptions.assetFileNames === 'string'
? outputOptions.assetFileNames
: outputOptions.assetFileNames({
type: 'asset',
name: `${filename(file)}-styles.mjs`,
name: `${fileName}-styles.mjs`,
source: '',
})
@ -79,7 +79,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
emitted[file] = this.emitFile({
type: 'asset',
name: `${filename(file)}-styles.mjs`,
name: `${fileName}-styles.mjs`,
source: [
...files.map((css, i) => `import style_${i} from './${relative(baseDir, this.getFileName(css))}';`),
`export default [${files.map((_, i) => `style_${i}`).join(', ')}]`,

View File

@ -105,7 +105,7 @@ export async function buildServer (ctx: ViteBuildContext) {
if (!ctx.nuxt.options.dev) {
const nitroDependencies = await tryResolveModule('nitropack/package.json', ctx.nuxt.options.modulesDir)
.then(r => import(r!)).then(r => Object.keys(r.dependencies || {})).catch(() => [])
.then(r => import(r!)).then(r => r.dependencies ? Object.keys(r.dependencies) : []).catch(() => [])
if (Array.isArray(serverConfig.ssr!.external)) {
serverConfig.ssr!.external.push(
// explicit dependencies we use in our ssr renderer - these can be inlined (if necessary) in the nitro build

View File

@ -10,7 +10,7 @@ interface Envs {
export function transpile (envs: Envs): Array<string | RegExp> {
const nuxt = useNuxt()
const transpile = []
const transpile: Array<string | RegExp> = []
for (let pattern of nuxt.options.build.transpile) {
if (typeof pattern === 'function') {

View File

@ -4,6 +4,7 @@ import { dirname, join, normalize, resolve } from 'pathe'
import type { Nuxt, NuxtBuilder, ViteConfig } from '@nuxt/schema'
import { addVitePlugin, isIgnored, logger, resolvePath } from '@nuxt/kit'
import replace from '@rollup/plugin-replace'
import type { RollupReplaceOptions } from '@rollup/plugin-replace'
import { sanitizeFilePath } from 'mlly'
import { withoutLeadingSlash } from 'ufo'
import { filename } from 'pathe/utils'
@ -102,10 +103,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
rootDir: nuxt.options.rootDir,
composables: nuxt.options.optimization.keyedComposables,
}),
replace({
...Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`])),
preventAssignment: true,
}),
replace({ preventAssignment: true, ...globalThisReplacements }),
virtual(nuxt.vfs),
],
server: {
@ -164,10 +162,16 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
await nuxt.callHook('vite:extend', ctx)
nuxt.hook('vite:extendConfig', (config) => {
config.plugins!.push(replace({
preventAssignment: true,
...Object.fromEntries(Object.entries(config.define!).filter(([key]) => key.startsWith('import.meta.'))),
}))
const replaceOptions: RollupReplaceOptions = Object.create(null)
replaceOptions.preventAssignment = true
for (const key in config.define!) {
if (key.startsWith('import.meta.')) {
replaceOptions[key] = config.define![key]
}
}
config.plugins!.push(replace(replaceOptions))
})
if (!ctx.nuxt.options.dev) {
@ -224,3 +228,5 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
await buildClient(ctx)
await buildServer(ctx)
}
const globalThisReplacements = Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`]))

View File

@ -1,6 +1,6 @@
{
"name": "@nuxt/webpack-builder",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -62,7 +62,7 @@
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.0",
"vue-loader": "^17.4.2",
"webpack": "^5.91.0",
"webpack": "^5.92.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-middleware": "^7.2.1",
"webpack-hot-middleware": "^2.26.1",

View File

@ -43,24 +43,19 @@ export default class VueSSRClientPlugin {
const allFiles = new Set<string>()
const asyncFiles = new Set<string>()
for (const asset of stats.assets!) {
const file = asset.name
if (!isHotUpdate(file)) {
allFiles.add(file)
if (initialFiles.has(file)) { continue }
if (isJS(file) || isCSS(file)) {
asyncFiles.add(file)
}
}
}
const assetsMapping: Record<string, string[]> = {}
for (const { name, chunkNames = [] } of stats.assets!) {
if (isJS(name) && !isHotUpdate(name)) {
for (const { name: file, chunkNames = [] } of stats.assets!) {
if (isHotUpdate(file)) { continue }
allFiles.add(file)
const isFileJS = isJS(file)
if (!initialFiles.has(file) && (isFileJS || isCSS(file))) {
asyncFiles.add(file)
}
if (isFileJS) {
const componentHash = hash(chunkNames.join('|'))
const map = assetsMapping[componentHash] ||= []
map.push(name)
map.push(file)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ for PKG in packages/* ; do
if [[ $PKG == "packages/test-utils" ]] ; then
continue
fi
if [[ $p == "packages/ui-templates" ]] ; then
if [[ $PKG == "packages/ui-templates" ]] ; then
continue
fi
pushd $PKG

View File

@ -13,9 +13,9 @@ async function main () {
const commits = await getLatestCommits().then(commits => commits.filter(
c => config.types[c.type] && !(c.type === 'chore' && c.scope === 'deps' && !c.isBreaking),
))
const bumpType = await determineBumpType()
const bumpType = await determineBumpType() || 'patch'
const newVersion = inc(workspace.find('nuxt').data.version, bumpType || 'patch')
const newVersion = inc(workspace.find('nuxt').data.version, bumpType)
const changelog = await generateMarkDown(commits, config)
// Create and push a branch with bumped versions if it has not already been created
@ -44,7 +44,8 @@ async function main () {
changelog
.replace(/^## v.*\n/, '')
.replace(`...${releaseBranch}`, `...v${newVersion}`)
.replace(/### ❤️ Contributors[\s\S]*$/, ''),
.replace(/### ❤️ Contributors[\s\S]*$/, '')
.replace(/[\n\r]+/g, '\n'),
'### ❤️ Contributors',
contributors.map(c => `- ${c.name} (@${c.username})`).join('\n'),
].join('\n')

View File

@ -166,6 +166,21 @@ describe('pages', () => {
expect(res.headers.get('x-extend')).toEqual('added in pages:extend')
})
it('preserves page metadata added in pages:extend hook', async () => {
const html = await $fetch<string>('/some-custom-path')
expect (html.match(/<pre>([^<]*)<\/pre>/)?.[1]?.trim().replace(/&quot;/g, '"').replace(/&gt;/g, '>')).toMatchInlineSnapshot(`
"{
"name": "some-custom-name",
"path": "/some-custom-path",
"validate": "() => true",
"middleware": [
"() => true"
],
"otherValue": "{\\"foo\\":\\"bar\\"}"
}"
`)
})
it('validates routes', async () => {
const { status, headers } = await fetch('/forbidden')
expect(status).toEqual(404)
@ -745,6 +760,24 @@ describe('nuxt links', () => {
`)
})
it('respects external links in edge cases', async () => {
const html = await $fetch<string>('/nuxt-link/custom-external')
const hrefs = html.match(/<a[^>]*href="([^"]+)"/g)
expect(hrefs).toMatchInlineSnapshot(`
[
"<a href="https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html"",
"<a href="https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html"",
"<a href="/missing-page/"",
"<a href="/missing-page/"",
]
`)
const { page, consoleLogs } = await renderPage('/nuxt-link/custom-external')
const warnings = consoleLogs.filter(c => c.text.includes('No match found for location'))
expect(warnings).toMatchInlineSnapshot(`[]`)
await page.close()
})
it('preserves route state', async () => {
const { page } = await renderPage('/nuxt-link/trailing-slash')

View File

@ -21,6 +21,13 @@ export default defineNuxtConfig({
title: Promise.resolve('Nuxt Fixture'),
// @ts-expect-error Functions are not allowed
titleTemplate: title => 'test',
meta: [
{
// Allows unknown property
property: 'og:thing',
content: '1234567890',
},
],
},
pageTransition: {
// @ts-expect-error Functions are not allowed

View File

@ -11,7 +11,7 @@
"devDependencies": {
"ofetch": "latest",
"unplugin-vue-router": "^0.7.0",
"vitest": "1.5.3",
"vitest": "1.6.0",
"vue": "latest",
"vue-router": "latest"
}

View File

@ -4,6 +4,7 @@ import type { FetchError } from 'ofetch'
import type { NavigationFailure, RouteLocationNormalized, RouteLocationRaw, Router, useRouter as vueUseRouter } from '#vue-router'
import type { AppConfig, RuntimeValue, UpperSnakeCase } from 'nuxt/schema'
import { defineNuxtModule } from 'nuxt/kit'
import { defineNuxtConfig } from 'nuxt/config'
import { callWithNuxt, isVue3 } from '#app'
import type { NuxtError } from '#app'
@ -242,6 +243,17 @@ describe('modules', () => {
// @ts-expect-error we want to ensure we throw type error on invalid key
defineNuxtConfig({ undeclaredKey: { other: false } })
})
it('preserves options in defineNuxtModule setup without `.with()`', () => {
defineNuxtModule<{ foo?: string, baz: number }>({
defaults: {
baz: 100,
},
setup: (resolvedOptions) => {
expectTypeOf(resolvedOptions).toEqualTypeOf<{ foo?: string, baz: number }>()
},
})
})
})
describe('nuxtApp', () => {

View File

@ -16,9 +16,15 @@ export default defineNuxtModule({
}, {
path: '/big-page-1',
file: resolver.resolve('./pages/big-page.vue'),
meta: {
layout: false,
},
}, {
path: '/big-page-2',
file: resolver.resolve('./pages/big-page.vue'),
meta: {
layout: false,
},
})
})
},

View File

@ -1,5 +1,6 @@
import { addBuildPlugin, addComponent } from 'nuxt/kit'
import type { NuxtPage } from 'nuxt/schema'
import { defu } from 'defu'
import { createUnplugin } from 'unplugin'
import { withoutLeadingSlash } from 'ufo'
@ -88,10 +89,17 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
needsFallback: undefined,
testConfig: 123,
},
},
modules: [
function (_options, nuxt) {
// ensure setting `runtimeConfig` also sets `nitro.runtimeConfig`
nuxt.options.runtimeConfig = defu(nuxt.options.runtimeConfig, {
public: {
testConfig: 123,
},
})
},
function (_options, nuxt) {
nuxt.hook('modules:done', () => {
// @ts-expect-error not valid nuxt option
@ -151,6 +159,17 @@ export default defineNuxtConfig({
internalParent!.children = newPages
})
},
function (_options, nuxt) {
// to check that page metadata is preserved
nuxt.hook('pages:extend', (pages) => {
const customName = pages.find(page => page.name === 'some-custom-name')
if (!customName) { throw new Error('Page with custom name not found') }
if (customName.path !== '/some-custom-path') { throw new Error('Page path not extracted') }
customName.meta ||= {}
customName.meta.someProp = true
})
},
// To test falsy module values
undefined,
],

36
test/fixtures/basic/pages/meta.vue vendored Normal file
View File

@ -0,0 +1,36 @@
<script setup lang="ts">
definePageMeta({
name: 'some-custom-name',
path: '/some-custom-path',
validate: () => true,
middleware: [() => true],
otherValue: {
foo: 'bar',
},
})
const serialisedMeta: Record<string, string> = {}
const meta = useRoute().meta
for (const key in meta) {
if (Array.isArray(meta[key])) {
serialisedMeta[key] = meta[key].map((fn: Function) => fn.toString())
continue
}
if (typeof meta[key] === 'string') {
serialisedMeta[key] = meta[key]
continue
}
if (typeof meta[key] === 'object') {
serialisedMeta[key] = JSON.stringify(meta[key])
continue
}
if (typeof meta[key] === 'function') {
serialisedMeta[key] = meta[key].toString()
continue
}
}
</script>
<template>
<pre>{{ serialisedMeta }}</pre>
</template>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
const MyLink = defineNuxtLink({
componentName: 'MyLink',
trailingSlash: 'append',
})
</script>
<template>
<div>
<div>
<MyLink to="https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html">
Trailing slashes should not be applied to implicit external links
</MyLink>
</div>
<div>
<NuxtLink
:to="{ path: 'https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html' }"
external
>
External links within route objects should be respected and not have trailing slashes applied
</NuxtLink>
</div>
<div>
<MyLink
:to="{ path: '/missing-page' }"
external
>
External links within route objects should be respected and have trailing slashes applied
</MyLink>
</div>
<div>
<MyLink
to="/missing-page"
external
>
External links should be respected and have trailing slashes applied
</MyLink>
</div>
<div>
<NuxtLink
custom
to="https://google.com"
external
>
<template #default="{ navigate }">
<button @click="navigate()">
Using navigate() with external link should work
</button>
</template>
</NuxtLink>
</div>
</div>
</template>

View File

@ -0,0 +1,9 @@
<template>
<PageContent />
</template>
<script setup lang="tsx">
definePageMeta({})
defineRouteRules({})
const PageContent = () => (<div>Home Page</div>)
</script>

View File

@ -53,6 +53,7 @@ describe('app config', () => {
describe('composables', () => {
it('are all tested', () => {
const testedComposables: string[] = [
'useRouteAnnouncer',
'clearNuxtData',
'refreshNuxtData',
'useAsyncData',