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 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 # From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files - name: Check workflow files
run: | run: |

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with: with:
persist-credentials: false persist-credentials: false
@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - 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() if: github.repository == 'nuxt/nuxt' && success()
with: with:
sarif_file: results.sarif 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). 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"} ::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. 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 ### compatibilityVersion
::important ::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. This enables early access to Nuxt features or flags.

View File

@ -11,7 +11,7 @@ links:
--- ---
::important ::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 ## Usage

View File

@ -10,7 +10,7 @@ links:
--- ---
::important ::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 `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 ::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 ## Description

View File

@ -98,6 +98,10 @@ export default defineNuxtComponent({
</script> </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 ## 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. 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", "magic-string": "^0.30.10",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"rollup": "^4.18.0", "rollup": "^4.18.0",
"vite": "5.2.13", "vite": "5.3.0",
"vue": "3.4.27" "vue": "3.4.27"
}, },
"devDependencies": { "devDependencies": {
@ -56,7 +56,7 @@
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"@types/node": "20.14.2", "@types/node": "20.14.2",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@unhead/schema": "1.9.12", "@unhead/schema": "1.9.13",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"@vitest/coverage-v8": "1.6.0", "@vitest/coverage-v8": "1.6.0",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
@ -66,7 +66,7 @@
"devalue": "5.0.0", "devalue": "5.0.0",
"eslint": "9.4.0", "eslint": "9.4.0",
"eslint-plugin-no-only-tests": "3.1.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", "eslint-typegen": "0.2.4",
"execa": "9.2.0", "execa": "9.2.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
@ -76,7 +76,7 @@
"jiti": "1.21.6", "jiti": "1.21.6",
"markdownlint-cli": "0.41.0", "markdownlint-cli": "0.41.0",
"nitropack": "2.9.6", "nitropack": "2.9.6",
"nuxi": "3.11.1", "nuxi": "3.12.0",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"nuxt-content-twoslash": "0.0.10", "nuxt-content-twoslash": "0.0.10",
"ofetch": "1.3.4", "ofetch": "1.3.4",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nuxt/kit", "name": "@nuxt/kit",
"version": "3.11.2", "version": "3.12.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/nuxt/nuxt.git", "url": "git+https://github.com/nuxt/nuxt.git",
@ -27,7 +27,7 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"c12": "^1.10.0", "c12": "^1.11.1",
"consola": "^3.2.3", "consola": "^3.2.3",
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
@ -54,9 +54,9 @@
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"nitropack": "2.9.6", "nitropack": "2.9.6",
"unbuild": "latest", "unbuild": "latest",
"vite": "5.2.13", "vite": "5.3.0",
"vitest": "1.6.0", "vitest": "1.6.0",
"webpack": "5.91.0" "webpack": "5.92.0"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "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( return ([] as Array<string | undefined>).concat(
global.__NUXT_PREPATHS__, global.__NUXT_PREPATHS__,
paths || [], paths || [],
@ -73,7 +73,7 @@ export function getModulePaths (paths?: string[] | string) {
/** @deprecated Do not use CJS utils */ /** @deprecated Do not use CJS utils */
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) { export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
return normalize(_require.resolve(id, { 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 */ /** @deprecated */
const importSources = (sources: string | string[], { lazy = false } = {}) => { const importSources = (sources: string | string[], { lazy = false } = {}) => {
return toArray(sources).map((src) => { return toArray(sources).map((src) => {
const safeVariableName = genSafeVariableName(src)
if (lazy) { 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') }).join('\n')
} }

View File

@ -7,10 +7,17 @@ import { NuxtConfigSchema } from '@nuxt/schema'
import { globby } from 'globby' import { globby } from 'globby'
import defu from 'defu' 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 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> { export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> {
// Automatically detect and import layers from `~~/layers/` directory // Automatically detect and import layers from `~~/layers/` directory
@ -40,10 +47,15 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
nuxtConfig._nuxtConfigFiles = [configFile] nuxtConfig._nuxtConfigFiles = [configFile]
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = [] const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
const processedLayers = new Set<string>()
for (const layer of layers) { for (const layer of layers) {
// Resolve `rootDir` & `srcDir` of layers // Resolve `rootDir` & `srcDir` of layers
layer.config = layer.config || {} 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 // Normalise layer directories
layer.config = await applyDefaults(layerSchema, layer.config as NuxtConfig & Record<string, JSValue>) as unknown as NuxtConfig 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 // need a name from here
if (!moduleMeta.name) { return false } if (!moduleMeta.name) { return false }
// maybe the version got attached within the installed module instance? // maybe the version got attached within the installed module instance?
const version = nuxt.options._installedModules for (const m of nuxt.options._installedModules) {
// @ts-expect-error _installedModules is not typed if (m.meta.name === moduleMeta.name && m.meta.version) {
.filter(m => m.meta.name === moduleMeta.name).map(m => m.meta.version)?.[0] return m.meta.version
if (version) { }
return version
} }
// it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance // it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance
if (hasNuxtModule(moduleMeta.name)) { if (hasNuxtModule(moduleMeta.name)) {

View File

@ -51,11 +51,12 @@ export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], op
for (const middleware of middlewares) { for (const middleware of middlewares) {
const find = app.middleware.findIndex(item => item.name === middleware.name) const find = app.middleware.findIndex(item => item.name === middleware.name)
if (find >= 0) { 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) { if (options.override === true) {
app.middleware[find] = { ...middleware } app.middleware[find] = { ...middleware }
} else { } 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 { } else {
app.middleware.push({ ...middleware }) 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[]) { export async function resolveNuxtModule (base: string, paths: string[]): Promise<string[]> {
const resolved = [] const resolved: string[] = []
const resolver = createResolver(base) const resolver = createResolver(base)
for (const path of paths) { 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 } = {}) { export async function resolveFiles (path: string, pattern: string | string[], opts: { followSymbolicLinks?: boolean } = {}) {
const files = await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true }) const files: string[] = []
return files.map(p => resolve(path, p)).filter(p => !isIgnored(p)).sort() 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 { tryResolveModule } from './internal/esm'
import { getDirectory } from './module/install' import { getDirectory } from './module/install'
import { tryUseNuxt, useNuxt } from './context' import { tryUseNuxt, useNuxt } from './context'
import { getModulePaths } from './internal/cjs' import { getNodeModulesPaths } from './internal/cjs'
import { resolveNuxtModule } from './resolve' import { resolveNuxtModule } from './resolve'
/** /**
@ -113,18 +113,55 @@ export async function updateTemplates (options?: { filter?: (template: ResolvedN
} }
export async function _generateTypes (nuxt: Nuxt) { export async function _generateTypes (nuxt: Nuxt) {
const nodeModulePaths = getModulePaths(nuxt.options.modulesDir)
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir) const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir)
const modulePaths = await resolveNuxtModule(rootDirWithSlash, const include = new Set<string>([
nuxt.options._installedModules './nuxt.d.ts',
.filter(m => m.entryPath) join(relativeRootDir, '.config/nuxt.*'),
.map(m => getDirectory(m.entryPath)), 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 isV4 = nuxt.options.future?.compatibilityVersion === 4
const hasTypescriptVersionWithModulePreserve = await readPackageJSON('typescript', { url: nuxt.options.modulesDir }) const hasTypescriptVersionWithModulePreserve = await readPackageJSON('typescript', { url: nuxt.options.modulesDir })
.then(r => r?.version && gte(r.version, '5.4.0')) .then(r => r?.version && gte(r.version, '5.4.0'))
.catch(() => isV4) .catch(() => isV4)
@ -168,23 +205,8 @@ export async function _generateTypes (nuxt: Nuxt) {
noImplicitThis: true, /* enabled with `strict` */ noImplicitThis: true, /* enabled with `strict` */
allowSyntheticDefaultImports: true, allowSyntheticDefaultImports: true,
}, },
include: [ include: [...include],
'./nuxt.d.ts', exclude: [...exclude],
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')),
],
} satisfies TSConfig) } satisfies TSConfig)
const aliases: Record<string, string> = { const aliases: Record<string, string> = {
@ -195,7 +217,9 @@ export async function _generateTypes (nuxt: Nuxt) {
// Exclude bridge alias types to support Volar // Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/] 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.compilerOptions = tsConfig.compilerOptions || {}
tsConfig.include = tsConfig.include || [] tsConfig.include = tsConfig.include || []
@ -237,12 +261,13 @@ export async function _generateTypes (nuxt: Nuxt) {
} }
} }
const references: TSReference[] = await Promise.all([ const references: TSReference[] = []
...nuxt.options.modules, await Promise.all([...nuxt.options.modules, ...nuxt.options._modules].map(async (id) => {
...nuxt.options._modules, if (typeof id !== 'string') { return }
]
.filter(f => typeof f === 'string') const pkg = await readPackageJSON(id, { url: getNodeModulesPaths(nuxt.options.modulesDir) }).catch(() => null)
.map(async id => ({ types: (await readPackageJSON(id, { url: nodeModulePaths }).catch(() => null))?.name || id }))) references.push(({ types: pkg?.name || id }))
}))
const declarations: string[] = [] const declarations: string[] = []
@ -302,7 +327,11 @@ export async function writeTypes (nuxt: Nuxt) {
} }
function renderAttrs (obj: Record<string, string>) { 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) { function renderAttr (key: string, value: string) {

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "nuxt", "name": "nuxt",
"version": "3.11.2", "version": "3.12.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/nuxt/nuxt.git", "url": "git+https://github.com/nuxt/nuxt.git",
@ -65,12 +65,12 @@
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.5.4", "@nuxt/telemetry": "^2.5.4",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.9.12", "@unhead/dom": "^1.9.13",
"@unhead/ssr": "^1.9.12", "@unhead/ssr": "^1.9.13",
"@unhead/vue": "^1.9.12", "@unhead/vue": "^1.9.13",
"@vue/shared": "^3.4.27", "@vue/shared": "^3.4.27",
"acorn": "8.11.3", "acorn": "8.11.3",
"c12": "^1.10.0", "c12": "^1.11.1",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"cookie-es": "^1.1.0", "cookie-es": "^1.1.0",
"defu": "^6.1.4", "defu": "^6.1.4",
@ -90,7 +90,7 @@
"magic-string": "^0.30.10", "magic-string": "^0.30.10",
"mlly": "^1.7.1", "mlly": "^1.7.1",
"nitropack": "^2.9.6", "nitropack": "^2.9.6",
"nuxi": "^3.11.1", "nuxi": "^3.12.0",
"nypm": "^0.3.8", "nypm": "^0.3.8",
"ofetch": "^1.3.4", "ofetch": "^1.3.4",
"ohash": "^1.1.3", "ohash": "^1.1.3",
@ -118,6 +118,7 @@
"vue-router": "^4.3.3" "vue-router": "^4.3.3"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/scripts": "0.5.1",
"@nuxt/ui-templates": "1.3.4", "@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1", "@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
@ -125,7 +126,7 @@
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"@vue/compiler-sfc": "3.4.27", "@vue/compiler-sfc": "3.4.27",
"unbuild": "latest", "unbuild": "latest",
"vite": "5.2.13", "vite": "5.3.0",
"vitest": "1.6.0" "vitest": "1.6.0"
}, },
"peerDependencies": { "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 { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue'
import { debounce } from 'perfect-debounce' import { debounce } from 'perfect-debounce'
import { hash } from 'ohash' 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 const components = import.meta.client ? new Map<string, Component>() : undefined
async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['components']) { async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['components']) {
const promises = [] const promises: Array<Promise<void>> = []
for (const component in paths) { for (const component in paths) {
if (!(components!.has(component))) { if (!(components!.has(component))) {
@ -259,7 +259,7 @@ export default defineComponent({
// should away be triggered ONE tick after re-rendering the static node // should away be triggered ONE tick after re-rendering the static node
withMemo([teleportKey.value], () => { 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 // 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)) const isKeyOdd = teleportKey.value === 0 || !!(teleportKey.value && !(teleportKey.value % 2))

View File

@ -8,7 +8,7 @@ import type {
} from 'vue' } from 'vue'
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } 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 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 { preloadRouteComponents } from '../composables/preload'
import { onNuxtReady } from '../composables/ready' import { onNuxtReady } from '../composables/ready'
import { navigateTo, useRouter } from '../composables/router' 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 * @see https://nuxt.com/docs/api/components/nuxt-link
*/ */
export interface NuxtLinkOptions extends export interface NuxtLinkOptions extends
Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>, Partial<Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>>,
Pick<NuxtLinkProps, 'prefetchedClass'> { Partial<Pick<NuxtLinkProps, 'prefetchedClass'>> {
/** /**
* The name of the component. * The name of the component.
* @default "NuxtLink" * @default "NuxtLink"
@ -124,34 +124,17 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
const router = useRouter() const router = useRouter()
const config = useRuntimeConfig() const config = useRuntimeConfig()
// Resolving `to` value from `to` and `href` props const hasTarget = computed(() => !!props.target && props.target !== '_self')
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)
})
// Lazily check whether to.value has a protocol // Lazily check whether to.value has a protocol
const isAbsoluteUrl = computed(() => typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true })) const isAbsoluteUrl = computed(() => {
const path = props.to || props.href || ''
// Resolves `to` value if it's a route location object return typeof path === 'string' && hasProtocol(path, { acceptRelative: true })
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 builtinRouterLink = resolveComponent('RouterLink') as string | typeof RouterLink const builtinRouterLink = resolveComponent('RouterLink') as string | typeof RouterLink
const useBuiltinLink = builtinRouterLink && typeof builtinRouterLink !== 'string' ? builtinRouterLink.useLink : undefined 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 // Resolving link type
const isExternal = computed<boolean>(() => { const isExternal = computed<boolean>(() => {
// External prop is explicitly set // External prop is explicitly set
@ -159,17 +142,40 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
return true return true
} }
// When `target` prop is set, link is external const path = props.to || props.href || ''
if (hasTarget.value) {
return true
}
// When `to` is a route object then it's an internal link // When `to` is a route object then it's an internal link
if (typeof to.value === 'object') { if (typeof path === 'object') {
return false 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 { return {
@ -183,10 +189,10 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
isExactActive: link?.isExactActive ?? computed(() => to.value === router.currentRoute.value.path), isExactActive: link?.isExactActive ?? computed(() => to.value === router.currentRoute.value.path),
route: link?.route ?? computed(() => router.resolve(to.value)), route: link?.route ?? computed(() => router.resolve(to.value)),
async navigate () { 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> & { } satisfies ReturnType<typeof useLink> & {
to: ComputedRef<string | RouteLocationRaw> to: ComputedRef<RouteLocationRaw>
hasTarget: ComputedRef<boolean | null | undefined> hasTarget: ComputedRef<boolean | null | undefined>
isAbsoluteUrl: ComputedRef<boolean> isAbsoluteUrl: ComputedRef<boolean>
isExternal: ComputedRef<boolean> isExternal: ComputedRef<boolean>
@ -307,10 +313,12 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
unobserve?.() unobserve?.()
unobserve = null 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([ await Promise.all([
nuxtApp.hooks.callHook('link:prefetch', path).catch(() => {}), 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 prefetched.value = true
}) })
@ -336,7 +344,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
} }
return () => { return () => {
if (!isExternal.value) { if (!isExternal.value && !hasTarget.value) {
const routerLinkProps: RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = { const routerLinkProps: RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = {
ref: elRef, ref: elRef,
to: to.value, to: to.value,
@ -408,7 +416,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
}, },
rel, rel,
target, target,
isExternal: isExternal.value, isExternal: isExternal.value || hasTarget.value,
isActive: false, isActive: false,
isExactActive: false, isExactActive: false,
}) })
@ -487,3 +495,7 @@ function isSlowConnection () {
if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true } if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true }
return false 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)) { if (import.meta.dev && !Number.isInteger(source)) {
console.warn(`The v-for range expect an integer value but got ${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++) { for (let i = 0; i < source; i++) {
array[i] = i array[i] = i
} }

View File

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

View File

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

View File

@ -69,7 +69,7 @@ export interface NuxtSSRContext extends SSRContext {
/** whether we are rendering an SSR error */ /** whether we are rendering an SSR error */
error?: boolean error?: boolean
nuxt: _NuxtApp nuxt: _NuxtApp
payload: NuxtPayload payload: Partial<NuxtPayload>
head: VueHeadClient<MergeHead> head: VueHeadClient<MergeHead>
/** This is used solely to render runtime config with SPA renderer. */ /** This is used solely to render runtime config with SPA renderer. */
config?: Pick<RuntimeConfig, 'public' | 'app'> 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 * 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>) { function wrappedConfig (runtimeConfig: Record<string, unknown>) {
if (!import.meta.dev || import.meta.server) { return runtimeConfig } if (!import.meta.dev || import.meta.server) { return runtimeConfig }
const keys = Object.keys(runtimeConfig).map(key => `\`${key}\``) const keys = Object.keys(runtimeConfig).map(key => `\`${key}\``)
@ -565,7 +566,10 @@ function wrappedConfig (runtimeConfig: Record<string, unknown>) {
return new Proxy(runtimeConfig, { return new Proxy(runtimeConfig, {
get (target, p, receiver) { get (target, p, receiver) {
if (typeof p === 'string' && p !== 'public' && !(p in target) && !p.startsWith('__v') /* vue check for reactivity, e.g. `__v_isRef` */) { 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) return Reflect.get(target, p, receiver)
}, },

View File

@ -136,6 +136,7 @@ function createGranularWatcher () {
console.timeEnd('[nuxt] builder:chokidar:watch') 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, const modules = await resolveNuxtModule(rootDirWithSlash,
nuxt.options._installedModules nuxt.options._installedModules
.filter(m => m.entryPath) .filter(m => m.entryPath)
.map(m => m.entryPath), .map(m => m.entryPath!),
) )
const nitroConfig: NitroConfig = defu(nuxt.options.nitro, { const nitroConfig: NitroConfig = defu(nuxt.options.nitro, {

View File

@ -4,7 +4,7 @@ import ignore from 'ignore'
import type { LoadNuxtOptions } from '@nuxt/kit' 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 { 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 { 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 type { PackageJson } from 'pkg-types'
import { readPackageJSON, resolvePackageJSON } from 'pkg-types' import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
import { hash } from 'ohash' import { hash } from 'ohash'
@ -49,7 +49,10 @@ export function createNuxt (options: NuxtOptions): Nuxt {
addHooks: hooks.addHooks, addHooks: hooks.addHooks,
hook: hooks.hook, hook: hooks.hook,
ready: () => initNuxt(nuxt), ready: () => initNuxt(nuxt),
close: () => Promise.resolve(hooks.callHook('close', nuxt)), close: async () => {
await hooks.callHook('close', nuxt)
hooks.removeAllHooks()
},
vfs: {}, vfs: {},
apps: {}, apps: {},
} }
@ -125,6 +128,7 @@ async function initNuxt (nuxt: Nuxt) {
// Add nuxt types // Add nuxt types
nuxt.hook('prepare:types', (opts) => { nuxt.hook('prepare:types', (opts) => {
opts.references.push({ types: 'nuxt' }) 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') }) opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/plugins.d.ts') })
// Add vue shim // Add vue shim
if (nuxt.options.typescript.shim) { if (nuxt.options.typescript.shim) {
@ -641,8 +645,22 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
options._modules.push('@nuxt/telemetry') options._modules.push('@nuxt/telemetry')
} }
// Ensure we share runtime config between Nuxt and Nitro // Ensure we share key config between Nuxt and Nitro
options.runtimeConfig = options.nitro.runtimeConfig as RuntimeConfig 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) 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 { function deduplicateArray<T = unknown> (maybeArray: T): T {
if (!Array.isArray(maybeArray)) { return maybeArray } if (!Array.isArray(maybeArray)) { return maybeArray }
const fresh = [] const fresh: any[] = []
const hashes = new Set<string>() const hashes = new Set<string>()
for (const item of maybeArray) { for (const item of maybeArray) {
const _hash = hash(item) const _hash = hash(item)
@ -691,3 +709,31 @@ function deduplicateArray<T = unknown> (maybeArray: T): T {
} }
return fresh as 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) const config = useRuntimeConfig(ssrContext.event)
ssrContext.modules = ssrContext.modules || new Set<string>() ssrContext.modules = ssrContext.modules || new Set<string>()
ssrContext!.payload = { ssrContext!.payload = {
_errors: {},
serverRendered: false, serverRendered: false,
data: {},
state: {},
once: new Set<string>(),
} }
ssrContext.config = { ssrContext.config = {
public: config.public, public: config.public,
@ -404,7 +400,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// 2. Styles // 2. Styles
head.push({ style: inlinedStyles }) head.push({ style: inlinedStyles })
if (!isRenderingIsland || import.meta.dev) { if (!isRenderingIsland || import.meta.dev) {
const link = [] const link: Link[] = []
for (const style in styles) { for (const style in styles) {
const resource = styles[style] const resource = styles[style]
// Do not add links to resources that are inlined (vite v5+) // 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 { hash } from 'ohash'
import { camelCase } from 'scule' import { camelCase } from 'scule'
import { filename } from 'pathe/utils' import { filename } from 'pathe/utils'
import type { NuxtTemplate } from 'nuxt/schema' import type { NuxtTemplate, NuxtTypeTemplate } from 'nuxt/schema'
import { annotatePlugins, checkForCircularDependencies } from './app' 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 = { export const pluginsDeclaration: NuxtTemplate = {
filename: 'types/plugins.d.ts', filename: 'types/plugins.d.ts',
getContents: async (ctx) => { 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 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' return `// Generated by Nuxt'
import type { Plugin } from '#app' 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' { declare module 'vue' {
interface ComponentCustomProperties extends NuxtAppInjections { } interface ComponentCustomProperties extends NuxtAppInjections { }
} }

View File

@ -109,6 +109,10 @@ const granularAppPresets: InlinePreset[] = [
imports: ['useId'], imports: ['useId'],
from: '#app/composables/id', from: '#app/composables/id',
}, },
{
imports: ['useRouteAnnouncer'],
from: '#app/composables/route-announcer',
},
] ]
export const scriptsStubsPreset = { export const scriptsStubsPreset = {
@ -119,19 +123,21 @@ export const scriptsStubsPreset = {
'useScript', 'useScript',
'useScriptGoogleAnalytics', 'useScriptGoogleAnalytics',
'useScriptPlausibleAnalytics', 'useScriptPlausibleAnalytics',
'useScriptClarity',
'useScriptCloudflareWebAnalytics', 'useScriptCloudflareWebAnalytics',
'useScriptFathomAnalytics', 'useScriptFathomAnalytics',
'useScriptMatomoAnalytics', 'useScriptMatomoAnalytics',
'useScriptGoogleTagManager', 'useScriptGoogleTagManager',
'useScriptGoogleAdsense',
'useScriptSegment', 'useScriptSegment',
'useScriptFacebookPixel', 'useScriptMetaPixel',
'useScriptXPixel', 'useScriptXPixel',
'useScriptIntercom', 'useScriptIntercom',
'useScriptHotjar', 'useScriptHotjar',
'useScriptStripe', 'useScriptStripe',
'useScriptLemonSqueezy', 'useScriptLemonSqueezy',
'useScriptVimeoPlayer', 'useScriptVimeoPlayer',
'useScriptYouTubeIframe', 'useScriptYouTubePlayer',
'useScriptGoogleMaps', 'useScriptGoogleMaps',
'useScriptNpm', '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) => { nuxt.hook('imports:extend', (imports) => {
imports.push( imports.push(
{ name: 'definePageMeta', as: 'definePageMeta', from: resolve(runtimeDir, 'composables') }, { 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 } 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 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, { walk(parse(js.code, {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 'latest', ecmaVersion: 'latest',

View File

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

View File

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

View File

@ -6,8 +6,9 @@ import * as VueFunctions from 'vue'
import type { Import } from 'unimport' import type { Import } from 'unimport'
import { createUnimport } from 'unimport' import { createUnimport } from 'unimport'
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
import { registry as scriptRegistry } from '@nuxt/scripts/registry'
import { TransformPlugin } from '../src/imports/transform' import { TransformPlugin } from '../src/imports/transform'
import { defaultPresets } from '../src/imports/presets' import { defaultPresets, scriptsStubsPreset } from '../src/imports/presets'
describe('imports:transform', () => { describe('imports:transform', () => {
const imports: Import[] = [ 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 { 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 type { NuxtLinkOptions, NuxtLinkProps } from '../src/app/components/nuxt-link'
import { defineNuxtLink } from '../src/app/components/nuxt-link' import { defineNuxtLink } from '../src/app/components/nuxt-link'
import { useRuntimeConfig } from '../src/app/nuxt' import { useRuntimeConfig } from '../src/app/nuxt'
@ -99,7 +99,11 @@ describe('nuxt-link:isExternal', () => {
}) })
it('returns `false` when `to` is a route location object', () => { 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', () => { it('honors `external` prop', () => {
@ -122,7 +126,12 @@ describe('nuxt-link:propsOrAttributes', () => {
}) })
it('resolves route location object', () => { 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: '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: '//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', () => { describe('to', () => {
it('forwards `to` prop', () => { it('forwards `to` prop', () => {
expect(nuxtLink({ to: '/to' }).props.to).toBe('/to') 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", "name": "@nuxt/schema",
"version": "3.11.2", "version": "3.12.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/nuxt/nuxt.git", "url": "git+https://github.com/nuxt/nuxt.git",
@ -39,13 +39,13 @@
"@types/file-loader": "5.0.4", "@types/file-loader": "5.0.4",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/sass-loader": "8.0.8", "@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": "5.0.4",
"@vitejs/plugin-vue-jsx": "4.0.0", "@vitejs/plugin-vue-jsx": "4.0.0",
"@vue/compiler-core": "3.4.27", "@vue/compiler-core": "3.4.27",
"@vue/compiler-sfc": "3.4.27", "@vue/compiler-sfc": "3.4.27",
"@vue/language-core": "2.0.21", "@vue/language-core": "2.0.21",
"c12": "1.10.0", "c12": "1.11.1",
"esbuild-loader": "4.1.0", "esbuild-loader": "4.1.0",
"h3": "1.11.1", "h3": "1.11.1",
"ignore": "5.3.1", "ignore": "5.3.1",
@ -54,16 +54,16 @@
"unbuild": "latest", "unbuild": "latest",
"unctx": "2.3.1", "unctx": "2.3.1",
"unenv": "1.9.0", "unenv": "1.9.0",
"vite": "5.2.13", "vite": "5.3.0",
"vue": "3.4.27", "vue": "3.4.27",
"vue-bundle-renderer": "2.1.0", "vue-bundle-renderer": "2.1.0",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue-router": "4.3.3", "vue-router": "4.3.3",
"webpack": "5.91.0", "webpack": "5.92.0",
"webpack-dev-middleware": "7.2.1" "webpack-dev-middleware": "7.2.1"
}, },
"dependencies": { "dependencies": {
"compatx": "^0.1.3", "compatx": "^0.1.8",
"consola": "^3.2.3", "consola": "^3.2.3",
"defu": "^6.1.4", "defu": "^6.1.4",
"hookable": "^5.5.3", "hookable": "^5.5.3",

View File

@ -1,4 +1,5 @@
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { readdir } from 'node:fs/promises'
import { defineUntypedSchema } from 'untyped' import { defineUntypedSchema } from 'untyped'
import { join, relative, resolve } from 'pathe' import { join, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env' 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. * 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, compatibilityDate: undefined,
@ -117,7 +118,16 @@ export default defineUntypedSchema({
} }
const srcDir = resolve(rootDir, 'app') 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']) { for (const file of ['app.vue', 'App.vue']) {
if (existsSync(resolve(rootDir, file))) { if (existsSync(resolve(rootDir, file))) {
return rootDir return rootDir

View File

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

View File

@ -141,7 +141,7 @@ export interface AppConfigInput extends CustomAppConfig {
server?: never 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 { export interface NuxtAppConfig {
head: Serializable<AppHeadMetaObject> head: Serializable<AppHeadMetaObject>

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@nuxt/vite-builder", "name": "@nuxt/vite-builder",
"version": "3.11.2", "version": "3.12.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/nuxt/nuxt.git", "url": "git+https://github.com/nuxt/nuxt.git",
@ -28,6 +28,7 @@
"@types/clear": "0.1.4", "@types/clear": "0.1.4",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"rollup": "4.18.0",
"unbuild": "latest", "unbuild": "latest",
"vue": "3.4.27" "vue": "3.4.27"
}, },
@ -62,7 +63,7 @@
"ufo": "^1.5.3", "ufo": "^1.5.3",
"unenv": "^1.9.0", "unenv": "^1.9.0",
"unplugin": "^1.10.1", "unplugin": "^1.10.1",
"vite": "^5.2.13", "vite": "^5.3.0",
"vite-node": "^1.6.0", "vite-node": "^1.6.0",
"vite-plugin-checker": "^0.6.4", "vite-plugin-checker": "^0.6.4",
"vue-bundle-renderer": "^2.1.0" "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 // 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!, { clientConfig.build!.rollupOptions = defu(clientConfig.build!.rollupOptions!, {
output: { output: {
chunkFileNames: ctx.nuxt.options.dev ? undefined : withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js')), chunkFileNames: ctx.nuxt.options.dev ? undefined : fileNames,
entryFileNames: ctx.nuxt.options.dev ? 'entry.js' : withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js')), entryFileNames: ctx.nuxt.options.dev ? 'entry.js' : fileNames,
} satisfies NonNullable<BuildOptions['rollupOptions']>['output'], } satisfies NonNullable<BuildOptions['rollupOptions']>['output'],
}) as any }) as any
@ -228,7 +229,13 @@ export async function buildClient (ctx: ViteBuildContext) {
}) })
const viteMiddleware = defineEventHandler(async (event) => { 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))) { if (!event.path.startsWith(clientConfig.base!) && !viteRoutes.some(route => event.path.startsWith(route))) {
// @ts-expect-error _skip_transform is a private property // @ts-expect-error _skip_transform is a private property
event.node.req._skip_transform = true 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 type { InlineConfig as ViteConfig } from 'vite'
import { distDir } from './dirs' import { distDir } from './dirs'
const lastPlugins = ['autoprefixer', 'cssnano']
export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] { export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
const css: ViteConfig['css'] & { postcss: NonNullable<Exclude<NonNullable<ViteConfig['css']>['postcss'], string>> } = { const css: ViteConfig['css'] & { postcss: NonNullable<Exclude<NonNullable<ViteConfig['css']>['postcss'], string>> } = {
postcss: { postcss: {
@ -10,19 +12,22 @@ export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
}, },
} }
const lastPlugins = ['autoprefixer', 'cssnano'] css.postcss.plugins = []
css.postcss.plugins = Object.entries(nuxt.options.postcss.plugins)
const plugins = Object.entries(nuxt.options.postcss.plugins)
.sort((a, b) => lastPlugins.indexOf(a[0]) - lastPlugins.indexOf(b[0])) .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, { const plugin = requireModule(name, {
paths: [ paths: [
...nuxt.options.modulesDir, ...nuxt.options.modulesDir,
distDir, distDir,
], ],
}) })
return plugin(opts) css.postcss.plugins.push(plugin(opts))
}) }
}
return css return css
} }

View File

@ -238,7 +238,13 @@ export async function initViteDevBundler (ctx: ViteBuildContext, onBuild: () =>
const { code, ids } = await bundleRequest(options, ctx.entry) const { code, ids } = await bundleRequest(options, ctx.entry)
await fse.writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs'), code, 'utf-8') 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 // 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) const time = (Date.now() - start)
logger.success(`Vite server built in ${time}ms`) logger.success(`Vite server built in ${time}ms`)
await onBuild() await onBuild()

View File

@ -50,11 +50,14 @@ export async function writeManifest (ctx: ViteBuildContext, css: string[] = [])
await fse.mkdirp(serverDist) await fse.mkdirp(serverDist)
if (ctx.config.build?.cssCodeSplit === false) { if (ctx.config.build?.cssCodeSplit === false) {
const entryCSS = Object.values(clientManifest as Record<string, { file?: string }>).find(val => (val).file?.endsWith('.css'))?.file for (const key in clientManifest as Record<string, { file?: string }>) {
if (entryCSS) { const val = clientManifest[key]
const key = relative(ctx.config.root!, ctx.entry) if (val.file?.endsWith('.css')) {
clientManifest[key].css ||= [] const key = relative(ctx.config.root!, ctx.entry)
clientManifest[key].css!.push(entryCSS) 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 { visualizer } from 'rollup-plugin-visualizer'
import defu from 'defu' import defu from 'defu'
import type { NuxtOptions } from 'nuxt/schema' import type { NuxtOptions } from 'nuxt/schema'
import type { RenderedModule } from 'rollup'
import type { ViteBuildContext } from '../vite' import type { ViteBuildContext } from '../vite'
export function analyzePlugin (ctx: ViteBuildContext): Plugin[] { export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
@ -13,14 +14,18 @@ export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
{ {
name: 'nuxt:analyze-minify', name: 'nuxt:analyze-minify',
async generateBundle (_opts, outputBundle) { 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 } if (bundle.type !== 'chunk') { continue }
const originalEntries = Object.entries(bundle.modules) const minifiedModuleEntryPromises: Array<Promise<[string, RenderedModule]>> = []
const minifiedEntries = await Promise.all(originalEntries.map(async ([moduleId, module]) => { for (const moduleId in bundle.modules) {
const { code } = await transform(module.code || '', { minify: true }) const module = bundle.modules[moduleId]
return [moduleId, { ...module, code }] minifiedModuleEntryPromises.push(
})) transform(module.code || '', { minify: true })
bundle.modules = Object.fromEntries(minifiedEntries) .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) => { export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => {
const composableMeta: Record<string, any> = {} const composableMeta: Record<string, any> = {}
const composableLengths = new Set<number>()
const keyedFunctions = new Set<string>()
for (const { name, ...meta } of options.composables) { for (const { name, ...meta } of options.composables) {
composableMeta[name] = meta composableMeta[name] = meta
keyedFunctions.add(name)
composableLengths.add(meta.argumentLength)
} }
const maxLength = Math.max(...options.composables.map(({ argumentLength }) => argumentLength)) const maxLength = Math.max(...composableLengths)
const keyedFunctions = new Set(options.composables.map(({ name }) => name))
const KEYED_FUNCTIONS_RE = new RegExp(`\\b(${[...keyedFunctions].map(f => escapeRE(f)).join('|')})\\b`) const KEYED_FUNCTIONS_RE = new RegExp(`\\b(${[...keyedFunctions].map(f => escapeRE(f)).join('|')})\\b`)
return { return {

View File

@ -25,7 +25,7 @@ export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boole
resolveId: { resolveId: {
enforce: 'post', enforce: 'post',
handler (id) { 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)) { if (resolveFromPublicAssets(id)) {
return PREFIX + encodeURIComponent(id) return PREFIX + encodeURIComponent(id)

View File

@ -66,12 +66,12 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
const { files, inBundle } = cssMap[file] const { files, inBundle } = cssMap[file]
// File has been tree-shaken out of build (or there are no styles to inline) // File has been tree-shaken out of build (or there are no styles to inline)
if (!files.length || !inBundle) { continue } if (!files.length || !inBundle) { continue }
const fileName = filename(file)
const base = typeof outputOptions.assetFileNames === 'string' const base = typeof outputOptions.assetFileNames === 'string'
? outputOptions.assetFileNames ? outputOptions.assetFileNames
: outputOptions.assetFileNames({ : outputOptions.assetFileNames({
type: 'asset', type: 'asset',
name: `${filename(file)}-styles.mjs`, name: `${fileName}-styles.mjs`,
source: '', source: '',
}) })
@ -79,7 +79,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
emitted[file] = this.emitFile({ emitted[file] = this.emitFile({
type: 'asset', type: 'asset',
name: `${filename(file)}-styles.mjs`, name: `${fileName}-styles.mjs`,
source: [ source: [
...files.map((css, i) => `import style_${i} from './${relative(baseDir, this.getFileName(css))}';`), ...files.map((css, i) => `import style_${i} from './${relative(baseDir, this.getFileName(css))}';`),
`export default [${files.map((_, i) => `style_${i}`).join(', ')}]`, `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) { if (!ctx.nuxt.options.dev) {
const nitroDependencies = await tryResolveModule('nitropack/package.json', ctx.nuxt.options.modulesDir) 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)) { if (Array.isArray(serverConfig.ssr!.external)) {
serverConfig.ssr!.external.push( serverConfig.ssr!.external.push(
// explicit dependencies we use in our ssr renderer - these can be inlined (if necessary) in the nitro build // 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> { export function transpile (envs: Envs): Array<string | RegExp> {
const nuxt = useNuxt() const nuxt = useNuxt()
const transpile = [] const transpile: Array<string | RegExp> = []
for (let pattern of nuxt.options.build.transpile) { for (let pattern of nuxt.options.build.transpile) {
if (typeof pattern === 'function') { 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 type { Nuxt, NuxtBuilder, ViteConfig } from '@nuxt/schema'
import { addVitePlugin, isIgnored, logger, resolvePath } from '@nuxt/kit' import { addVitePlugin, isIgnored, logger, resolvePath } from '@nuxt/kit'
import replace from '@rollup/plugin-replace' import replace from '@rollup/plugin-replace'
import type { RollupReplaceOptions } from '@rollup/plugin-replace'
import { sanitizeFilePath } from 'mlly' import { sanitizeFilePath } from 'mlly'
import { withoutLeadingSlash } from 'ufo' import { withoutLeadingSlash } from 'ufo'
import { filename } from 'pathe/utils' import { filename } from 'pathe/utils'
@ -102,10 +103,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
rootDir: nuxt.options.rootDir, rootDir: nuxt.options.rootDir,
composables: nuxt.options.optimization.keyedComposables, composables: nuxt.options.optimization.keyedComposables,
}), }),
replace({ replace({ preventAssignment: true, ...globalThisReplacements }),
...Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`])),
preventAssignment: true,
}),
virtual(nuxt.vfs), virtual(nuxt.vfs),
], ],
server: { server: {
@ -164,10 +162,16 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
await nuxt.callHook('vite:extend', ctx) await nuxt.callHook('vite:extend', ctx)
nuxt.hook('vite:extendConfig', (config) => { nuxt.hook('vite:extendConfig', (config) => {
config.plugins!.push(replace({ const replaceOptions: RollupReplaceOptions = Object.create(null)
preventAssignment: true, replaceOptions.preventAssignment = true
...Object.fromEntries(Object.entries(config.define!).filter(([key]) => key.startsWith('import.meta.'))),
})) 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) { if (!ctx.nuxt.options.dev) {
@ -224,3 +228,5 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
await buildClient(ctx) await buildClient(ctx)
await buildServer(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", "name": "@nuxt/webpack-builder",
"version": "3.11.2", "version": "3.12.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/nuxt/nuxt.git", "url": "git+https://github.com/nuxt/nuxt.git",
@ -62,7 +62,7 @@
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.0", "vue-bundle-renderer": "^2.1.0",
"vue-loader": "^17.4.2", "vue-loader": "^17.4.2",
"webpack": "^5.91.0", "webpack": "^5.92.0",
"webpack-bundle-analyzer": "^4.10.2", "webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-middleware": "^7.2.1", "webpack-dev-middleware": "^7.2.1",
"webpack-hot-middleware": "^2.26.1", "webpack-hot-middleware": "^2.26.1",

View File

@ -43,24 +43,19 @@ export default class VueSSRClientPlugin {
const allFiles = new Set<string>() const allFiles = new Set<string>()
const asyncFiles = 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[]> = {} 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 componentHash = hash(chunkNames.join('|'))
const map = assetsMapping[componentHash] ||= [] 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 if [[ $PKG == "packages/test-utils" ]] ; then
continue continue
fi fi
if [[ $p == "packages/ui-templates" ]] ; then if [[ $PKG == "packages/ui-templates" ]] ; then
continue continue
fi fi
pushd $PKG pushd $PKG

View File

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

View File

@ -166,6 +166,21 @@ describe('pages', () => {
expect(res.headers.get('x-extend')).toEqual('added in pages:extend') 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 () => { it('validates routes', async () => {
const { status, headers } = await fetch('/forbidden') const { status, headers } = await fetch('/forbidden')
expect(status).toEqual(404) 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 () => { it('preserves route state', async () => {
const { page } = await renderPage('/nuxt-link/trailing-slash') const { page } = await renderPage('/nuxt-link/trailing-slash')

View File

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

View File

@ -11,7 +11,7 @@
"devDependencies": { "devDependencies": {
"ofetch": "latest", "ofetch": "latest",
"unplugin-vue-router": "^0.7.0", "unplugin-vue-router": "^0.7.0",
"vitest": "1.5.3", "vitest": "1.6.0",
"vue": "latest", "vue": "latest",
"vue-router": "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 { NavigationFailure, RouteLocationNormalized, RouteLocationRaw, Router, useRouter as vueUseRouter } from '#vue-router'
import type { AppConfig, RuntimeValue, UpperSnakeCase } from 'nuxt/schema' import type { AppConfig, RuntimeValue, UpperSnakeCase } from 'nuxt/schema'
import { defineNuxtModule } from 'nuxt/kit'
import { defineNuxtConfig } from 'nuxt/config' import { defineNuxtConfig } from 'nuxt/config'
import { callWithNuxt, isVue3 } from '#app' import { callWithNuxt, isVue3 } from '#app'
import type { NuxtError } 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 // @ts-expect-error we want to ensure we throw type error on invalid key
defineNuxtConfig({ undeclaredKey: { other: false } }) 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', () => { describe('nuxtApp', () => {

View File

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

View File

@ -1,5 +1,6 @@
import { addBuildPlugin, addComponent } from 'nuxt/kit' import { addBuildPlugin, addComponent } from 'nuxt/kit'
import type { NuxtPage } from 'nuxt/schema' import type { NuxtPage } from 'nuxt/schema'
import { defu } from 'defu'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import { withoutLeadingSlash } from 'ufo' import { withoutLeadingSlash } from 'ufo'
@ -88,10 +89,17 @@ export default defineNuxtConfig({
runtimeConfig: { runtimeConfig: {
public: { public: {
needsFallback: undefined, needsFallback: undefined,
testConfig: 123,
}, },
}, },
modules: [ modules: [
function (_options, nuxt) {
// ensure setting `runtimeConfig` also sets `nitro.runtimeConfig`
nuxt.options.runtimeConfig = defu(nuxt.options.runtimeConfig, {
public: {
testConfig: 123,
},
})
},
function (_options, nuxt) { function (_options, nuxt) {
nuxt.hook('modules:done', () => { nuxt.hook('modules:done', () => {
// @ts-expect-error not valid nuxt option // @ts-expect-error not valid nuxt option
@ -151,6 +159,17 @@ export default defineNuxtConfig({
internalParent!.children = newPages 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 // To test falsy module values
undefined, 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', () => { describe('composables', () => {
it('are all tested', () => { it('are all tested', () => {
const testedComposables: string[] = [ const testedComposables: string[] = [
'useRouteAnnouncer',
'clearNuxtData', 'clearNuxtData',
'refreshNuxtData', 'refreshNuxtData',
'useAsyncData', 'useAsyncData',