mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 23:22:02 +00:00
Merge branch 'nuxt:main' into main
This commit is contained in:
commit
7501792b91
2
.github/workflows/autofix-docs.yml
vendored
2
.github/workflows/autofix-docs.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
|
2
.github/workflows/autofix.yml
vendored
2
.github/workflows/autofix.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
|
2
.github/workflows/benchmark.yml
vendored
2
.github/workflows/benchmark.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
|
2
.github/workflows/changelog.yml
vendored
2
.github/workflows/changelog.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: corepack enable
|
||||
|
2
.github/workflows/check-links.yml
vendored
2
.github/workflows/check-links.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
restore-keys: cache-lychee-
|
||||
|
||||
# check links with Lychee
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Lychee link checker
|
||||
uses: lycheeverse/lychee-action@25a231001d1723960a301b7d4c82884dc7ef857d # for v1.8.0
|
||||
|
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -72,7 +72,7 @@ jobs:
|
||||
- build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -83,7 +83,7 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
|
||||
uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
|
||||
with:
|
||||
languages: javascript
|
||||
queries: +security-and-quality
|
||||
@ -95,7 +95,7 @@ jobs:
|
||||
path: packages
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
|
||||
uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
|
||||
with:
|
||||
category: "/language:javascript"
|
||||
|
||||
@ -111,7 +111,7 @@ jobs:
|
||||
module: ["bundler", "node"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -142,7 +142,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -166,7 +166,7 @@ jobs:
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -218,7 +218,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -248,7 +248,7 @@ jobs:
|
||||
TEST_PAYLOAD: ${{ matrix.payload }}
|
||||
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || matrix.payload == 'js' || runner.os == 'Windows' }}
|
||||
|
||||
- uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4.4.1
|
||||
- uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
|
||||
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@ -270,7 +270,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: corepack enable
|
||||
@ -309,7 +309,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: corepack enable
|
||||
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@ -17,6 +17,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3
|
||||
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
|
2
.github/workflows/introspect.yml
vendored
2
.github/workflows/introspect.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
|
||||
- name: Check workflow files
|
||||
run: |
|
||||
|
2
.github/workflows/release-pr.yml
vendored
2
.github/workflows/release-pr.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
ref: ${{ github.event.issue.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: corepack enable
|
||||
|
2
.github/workflows/reproduire.yml
vendored
2
.github/workflows/reproduire.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
reproduire:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp
|
||||
with:
|
||||
label: needs reproduction
|
||||
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@ -68,7 +68,7 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
|
||||
uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
|
||||
if: github.repository == 'nuxt/nuxt' && success()
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
@ -23,7 +23,7 @@ To use the latest Nuxt build and test features before their release, read about
|
||||
|
||||
Nuxt 4 is planned to be released **on or before June 14** (though obviously this is dependent on having enough time after Nitro's major release to be properly tested in the community, so be aware that this is not an exact date).
|
||||
|
||||
Until then, it is possible to test many of Nuxt 4's breaking changes on the nightly release channel.
|
||||
Until then, it is possible to test many of Nuxt 4's breaking changes from Nuxt version 3.12 or via the nightly release channel.
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=r4wFKlcJK6c" target="_blank"}
|
||||
Watch a video from Alexander Lichter showing how to opt in to Nuxt 4's breaking changes already.
|
||||
|
@ -40,7 +40,7 @@ There is also a `future` namespace for early opting-in to new features that will
|
||||
### compatibilityVersion
|
||||
|
||||
::important
|
||||
This configuration option is available in Nuxt v3.12+ or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
|
||||
This configuration option is available in Nuxt v3.12+.
|
||||
::
|
||||
|
||||
This enables early access to Nuxt features or flags.
|
||||
|
@ -11,7 +11,7 @@ links:
|
||||
---
|
||||
|
||||
::important
|
||||
This component will be available in Nuxt v3.12 or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
|
||||
This component is available in Nuxt v3.12+.
|
||||
::
|
||||
|
||||
## Usage
|
||||
|
@ -10,7 +10,7 @@ links:
|
||||
---
|
||||
|
||||
::important
|
||||
This composable will be available in Nuxt v3.12+ or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
|
||||
This composable is available in Nuxt v3.12+.
|
||||
::
|
||||
|
||||
`onPrehydrate` is a composable lifecycle hook that allows you to run a callback on the client immediately before
|
||||
|
@ -11,7 +11,7 @@ links:
|
||||
---
|
||||
|
||||
::important
|
||||
This composable will be available in Nuxt v3.12 or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
|
||||
This composable is available in Nuxt v3.12+.
|
||||
::
|
||||
|
||||
## Description
|
||||
|
@ -98,6 +98,10 @@ export default defineNuxtComponent({
|
||||
</script>
|
||||
```
|
||||
|
||||
::warning
|
||||
Possible breaking change: `head` receives the nuxt app but cannot access the component instance. If the code in your `head` tries to access the data object through `this` or `this.$data`, you will need to migrate to the `useHead` composable.
|
||||
::
|
||||
|
||||
## Title Template
|
||||
|
||||
If you want to use a function (for full control), then this cannot be set in your nuxt.config, and it is recommended instead to set it within your `/layouts` directory.
|
||||
|
@ -42,7 +42,7 @@
|
||||
"magic-string": "^0.30.10",
|
||||
"nuxt": "workspace:*",
|
||||
"rollup": "^4.18.0",
|
||||
"vite": "5.2.13",
|
||||
"vite": "5.3.0",
|
||||
"vue": "3.4.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -56,7 +56,7 @@
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/node": "20.14.2",
|
||||
"@types/semver": "7.5.8",
|
||||
"@unhead/schema": "1.9.12",
|
||||
"@unhead/schema": "1.9.13",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vitest/coverage-v8": "1.6.0",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
@ -66,7 +66,7 @@
|
||||
"devalue": "5.0.0",
|
||||
"eslint": "9.4.0",
|
||||
"eslint-plugin-no-only-tests": "3.1.0",
|
||||
"eslint-plugin-perfectionist": "2.10.0",
|
||||
"eslint-plugin-perfectionist": "2.11.0",
|
||||
"eslint-typegen": "0.2.4",
|
||||
"execa": "9.2.0",
|
||||
"fs-extra": "11.2.0",
|
||||
@ -76,7 +76,7 @@
|
||||
"jiti": "1.21.6",
|
||||
"markdownlint-cli": "0.41.0",
|
||||
"nitropack": "2.9.6",
|
||||
"nuxi": "3.11.1",
|
||||
"nuxi": "3.12.0",
|
||||
"nuxt": "workspace:*",
|
||||
"nuxt-content-twoslash": "0.0.10",
|
||||
"ofetch": "1.3.4",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nuxt/kit",
|
||||
"version": "3.11.2",
|
||||
"version": "3.12.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/nuxt.git",
|
||||
@ -27,7 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"c12": "^1.10.0",
|
||||
"c12": "^1.11.1",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3",
|
||||
@ -54,9 +54,9 @@
|
||||
"lodash-es": "4.17.21",
|
||||
"nitropack": "2.9.6",
|
||||
"unbuild": "latest",
|
||||
"vite": "5.2.13",
|
||||
"vite": "5.3.0",
|
||||
"vitest": "1.6.0",
|
||||
"webpack": "5.91.0"
|
||||
"webpack": "5.92.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
|
@ -61,7 +61,7 @@ function getRequireCacheItem (id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getModulePaths (paths?: string[] | string) {
|
||||
export function getNodeModulesPaths (paths?: string[] | string) {
|
||||
return ([] as Array<string | undefined>).concat(
|
||||
global.__NUXT_PREPATHS__,
|
||||
paths || [],
|
||||
@ -73,7 +73,7 @@ export function getModulePaths (paths?: string[] | string) {
|
||||
/** @deprecated Do not use CJS utils */
|
||||
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
|
||||
return normalize(_require.resolve(id, {
|
||||
paths: getModulePaths(opts.paths),
|
||||
paths: getNodeModulesPaths(opts.paths),
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -32,10 +32,11 @@ const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"\{(.+)\
|
||||
/** @deprecated */
|
||||
const importSources = (sources: string | string[], { lazy = false } = {}) => {
|
||||
return toArray(sources).map((src) => {
|
||||
const safeVariableName = genSafeVariableName(src)
|
||||
if (lazy) {
|
||||
return `const ${genSafeVariableName(src)} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
|
||||
return `const ${safeVariableName} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
|
||||
}
|
||||
return genImport(src, genSafeVariableName(src))
|
||||
return genImport(src, safeVariableName)
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
|
@ -7,10 +7,17 @@ import { NuxtConfigSchema } from '@nuxt/schema'
|
||||
import { globby } from 'globby'
|
||||
import defu from 'defu'
|
||||
|
||||
export interface LoadNuxtConfigOptions extends LoadConfigOptions<NuxtConfig> {}
|
||||
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
|
||||
overrides?: Exclude<LoadConfigOptions<NuxtConfig>['overrides'], Promise<any> | Function>
|
||||
}
|
||||
|
||||
const layerSchemaKeys = ['future', 'srcDir', 'rootDir', 'dir']
|
||||
const layerSchema = Object.fromEntries(Object.entries(NuxtConfigSchema).filter(([key]) => layerSchemaKeys.includes(key)))
|
||||
const layerSchema = Object.create(null)
|
||||
for (const key of layerSchemaKeys) {
|
||||
if (key in NuxtConfigSchema) {
|
||||
layerSchema[key] = NuxtConfigSchema[key]
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> {
|
||||
// Automatically detect and import layers from `~~/layers/` directory
|
||||
@ -40,10 +47,15 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
|
||||
nuxtConfig._nuxtConfigFiles = [configFile]
|
||||
|
||||
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
|
||||
const processedLayers = new Set<string>()
|
||||
for (const layer of layers) {
|
||||
// Resolve `rootDir` & `srcDir` of layers
|
||||
layer.config = layer.config || {}
|
||||
layer.config.rootDir = layer.config.rootDir ?? layer.cwd
|
||||
layer.config.rootDir = layer.config.rootDir ?? layer.cwd!
|
||||
|
||||
// Only process/resolve layers once
|
||||
if (processedLayers.has(layer.config.rootDir)) { continue }
|
||||
processedLayers.add(layer.config.rootDir)
|
||||
|
||||
// Normalise layer directories
|
||||
layer.config = await applyDefaults(layerSchema, layer.config as NuxtConfig & Record<string, JSValue>) as unknown as NuxtConfig
|
||||
|
@ -51,11 +51,10 @@ export async function getNuxtModuleVersion (module: string | NuxtModule, nuxt: N
|
||||
// need a name from here
|
||||
if (!moduleMeta.name) { return false }
|
||||
// maybe the version got attached within the installed module instance?
|
||||
const version = nuxt.options._installedModules
|
||||
// @ts-expect-error _installedModules is not typed
|
||||
.filter(m => m.meta.name === moduleMeta.name).map(m => m.meta.version)?.[0]
|
||||
if (version) {
|
||||
return version
|
||||
for (const m of nuxt.options._installedModules) {
|
||||
if (m.meta.name === moduleMeta.name && m.meta.version) {
|
||||
return m.meta.version
|
||||
}
|
||||
}
|
||||
// it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance
|
||||
if (hasNuxtModule(moduleMeta.name)) {
|
||||
|
@ -51,11 +51,12 @@ export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], op
|
||||
for (const middleware of middlewares) {
|
||||
const find = app.middleware.findIndex(item => item.name === middleware.name)
|
||||
if (find >= 0) {
|
||||
if (app.middleware[find].path === middleware.path) { continue }
|
||||
const foundPath = app.middleware[find].path
|
||||
if (foundPath === middleware.path) { continue }
|
||||
if (options.override === true) {
|
||||
app.middleware[find] = { ...middleware }
|
||||
} else {
|
||||
logger.warn(`'${middleware.name}' middleware already exists at '${app.middleware[find].path}'. You can set \`override: true\` to replace it.`)
|
||||
logger.warn(`'${middleware.name}' middleware already exists at '${foundPath}'. You can set \`override: true\` to replace it.`)
|
||||
}
|
||||
} else {
|
||||
app.middleware.push({ ...middleware })
|
||||
|
@ -168,8 +168,8 @@ export function createResolver (base: string | URL): Resolver {
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveNuxtModule (base: string, paths: string[]) {
|
||||
const resolved = []
|
||||
export async function resolveNuxtModule (base: string, paths: string[]): Promise<string[]> {
|
||||
const resolved: string[] = []
|
||||
const resolver = createResolver(base)
|
||||
|
||||
for (const path of paths) {
|
||||
@ -209,6 +209,12 @@ function existsInVFS (path: string, nuxt = tryUseNuxt()) {
|
||||
}
|
||||
|
||||
export async function resolveFiles (path: string, pattern: string | string[], opts: { followSymbolicLinks?: boolean } = {}) {
|
||||
const files = await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true })
|
||||
return files.map(p => resolve(path, p)).filter(p => !isIgnored(p)).sort()
|
||||
const files: string[] = []
|
||||
for (const file of await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true })) {
|
||||
const p = resolve(path, file)
|
||||
if (!isIgnored(p)) {
|
||||
files.push(p)
|
||||
}
|
||||
}
|
||||
return files.sort()
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { readPackageJSON } from 'pkg-types'
|
||||
import { tryResolveModule } from './internal/esm'
|
||||
import { getDirectory } from './module/install'
|
||||
import { tryUseNuxt, useNuxt } from './context'
|
||||
import { getModulePaths } from './internal/cjs'
|
||||
import { getNodeModulesPaths } from './internal/cjs'
|
||||
import { resolveNuxtModule } from './resolve'
|
||||
|
||||
/**
|
||||
@ -113,18 +113,55 @@ export async function updateTemplates (options?: { filter?: (template: ResolvedN
|
||||
}
|
||||
|
||||
export async function _generateTypes (nuxt: Nuxt) {
|
||||
const nodeModulePaths = getModulePaths(nuxt.options.modulesDir)
|
||||
|
||||
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
|
||||
const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir)
|
||||
|
||||
const modulePaths = await resolveNuxtModule(rootDirWithSlash,
|
||||
nuxt.options._installedModules
|
||||
.filter(m => m.entryPath)
|
||||
.map(m => getDirectory(m.entryPath)),
|
||||
)
|
||||
const include = new Set<string>([
|
||||
'./nuxt.d.ts',
|
||||
join(relativeRootDir, '.config/nuxt.*'),
|
||||
join(relativeRootDir, '**/*'),
|
||||
])
|
||||
|
||||
if (nuxt.options.srcDir !== nuxt.options.rootDir) {
|
||||
include.add(join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*'))
|
||||
}
|
||||
|
||||
if (nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir) {
|
||||
include.add(join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*'))
|
||||
}
|
||||
|
||||
for (const layer of nuxt.options._layers) {
|
||||
const srcOrCwd = layer.config.srcDir ?? layer.cwd
|
||||
if (!srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules')) {
|
||||
include.add(join(relative(nuxt.options.buildDir, srcOrCwd), '**/*'))
|
||||
}
|
||||
}
|
||||
|
||||
const exclude = new Set<string>([
|
||||
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
|
||||
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')),
|
||||
])
|
||||
|
||||
for (const dir of nuxt.options.modulesDir) {
|
||||
exclude.add(relativeWithDot(nuxt.options.buildDir, dir))
|
||||
}
|
||||
|
||||
const moduleEntryPaths: string[] = []
|
||||
for (const m of nuxt.options._installedModules) {
|
||||
if (m.entryPath) {
|
||||
moduleEntryPaths.push(getDirectory(m.entryPath))
|
||||
}
|
||||
}
|
||||
|
||||
const modulePaths = await resolveNuxtModule(rootDirWithSlash, moduleEntryPaths)
|
||||
|
||||
for (const path of modulePaths) {
|
||||
const relative = relativeWithDot(nuxt.options.buildDir, path)
|
||||
include.add(join(relative, 'runtime'))
|
||||
exclude.add(join(relative, 'runtime/server'))
|
||||
}
|
||||
|
||||
const isV4 = nuxt.options.future?.compatibilityVersion === 4
|
||||
|
||||
const hasTypescriptVersionWithModulePreserve = await readPackageJSON('typescript', { url: nuxt.options.modulesDir })
|
||||
.then(r => r?.version && gte(r.version, '5.4.0'))
|
||||
.catch(() => isV4)
|
||||
@ -168,23 +205,8 @@ export async function _generateTypes (nuxt: Nuxt) {
|
||||
noImplicitThis: true, /* enabled with `strict` */
|
||||
allowSyntheticDefaultImports: true,
|
||||
},
|
||||
include: [
|
||||
'./nuxt.d.ts',
|
||||
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '.config/nuxt.*'),
|
||||
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'),
|
||||
...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [],
|
||||
...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd)
|
||||
.filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules'))
|
||||
.map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')),
|
||||
...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : [],
|
||||
...modulePaths.map(m => join(relativeWithDot(nuxt.options.buildDir, m), 'runtime')),
|
||||
],
|
||||
exclude: [
|
||||
...nuxt.options.modulesDir.map(m => relativeWithDot(nuxt.options.buildDir, m)),
|
||||
...modulePaths.map(m => join(relativeWithDot(nuxt.options.buildDir, m), 'runtime/server')),
|
||||
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
|
||||
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')),
|
||||
],
|
||||
include: [...include],
|
||||
exclude: [...exclude],
|
||||
} satisfies TSConfig)
|
||||
|
||||
const aliases: Record<string, string> = {
|
||||
@ -195,7 +217,9 @@ export async function _generateTypes (nuxt: Nuxt) {
|
||||
// Exclude bridge alias types to support Volar
|
||||
const excludedAlias = [/^@vue\/.*$/]
|
||||
|
||||
const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir
|
||||
const basePath = tsConfig.compilerOptions!.baseUrl
|
||||
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
|
||||
: nuxt.options.buildDir
|
||||
|
||||
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
|
||||
tsConfig.include = tsConfig.include || []
|
||||
@ -237,12 +261,13 @@ export async function _generateTypes (nuxt: Nuxt) {
|
||||
}
|
||||
}
|
||||
|
||||
const references: TSReference[] = await Promise.all([
|
||||
...nuxt.options.modules,
|
||||
...nuxt.options._modules,
|
||||
]
|
||||
.filter(f => typeof f === 'string')
|
||||
.map(async id => ({ types: (await readPackageJSON(id, { url: nodeModulePaths }).catch(() => null))?.name || id })))
|
||||
const references: TSReference[] = []
|
||||
await Promise.all([...nuxt.options.modules, ...nuxt.options._modules].map(async (id) => {
|
||||
if (typeof id !== 'string') { return }
|
||||
|
||||
const pkg = await readPackageJSON(id, { url: getNodeModulesPaths(nuxt.options.modulesDir) }).catch(() => null)
|
||||
references.push(({ types: pkg?.name || id }))
|
||||
}))
|
||||
|
||||
const declarations: string[] = []
|
||||
|
||||
@ -302,7 +327,11 @@ export async function writeTypes (nuxt: Nuxt) {
|
||||
}
|
||||
|
||||
function renderAttrs (obj: Record<string, string>) {
|
||||
return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ')
|
||||
const attrs: string[] = []
|
||||
for (const key in obj) {
|
||||
attrs.push(renderAttr(key, obj[key]))
|
||||
}
|
||||
return attrs.join(' ')
|
||||
}
|
||||
|
||||
function renderAttr (key: string, value: string) {
|
||||
|
@ -53,12 +53,12 @@ describe('tsConfig generation', () => {
|
||||
}))
|
||||
expect(tsConfig.exclude).toMatchInlineSnapshot(`
|
||||
[
|
||||
"../dist",
|
||||
"../modules/test/node_modules",
|
||||
"../modules/node_modules",
|
||||
"../node_modules/@some/module/node_modules",
|
||||
"../node_modules",
|
||||
"../../node_modules",
|
||||
"../dist",
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nuxt",
|
||||
"version": "3.11.2",
|
||||
"version": "3.12.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/nuxt.git",
|
||||
@ -65,12 +65,12 @@
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@nuxt/telemetry": "^2.5.4",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@unhead/dom": "^1.9.12",
|
||||
"@unhead/ssr": "^1.9.12",
|
||||
"@unhead/vue": "^1.9.12",
|
||||
"@unhead/dom": "^1.9.13",
|
||||
"@unhead/ssr": "^1.9.13",
|
||||
"@unhead/vue": "^1.9.13",
|
||||
"@vue/shared": "^3.4.27",
|
||||
"acorn": "8.11.3",
|
||||
"c12": "^1.10.0",
|
||||
"c12": "^1.11.1",
|
||||
"chokidar": "^3.6.0",
|
||||
"cookie-es": "^1.1.0",
|
||||
"defu": "^6.1.4",
|
||||
@ -90,7 +90,7 @@
|
||||
"magic-string": "^0.30.10",
|
||||
"mlly": "^1.7.1",
|
||||
"nitropack": "^2.9.6",
|
||||
"nuxi": "^3.11.1",
|
||||
"nuxi": "^3.12.0",
|
||||
"nypm": "^0.3.8",
|
||||
"ofetch": "^1.3.4",
|
||||
"ohash": "^1.1.3",
|
||||
@ -118,6 +118,7 @@
|
||||
"vue-router": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/scripts": "0.5.1",
|
||||
"@nuxt/ui-templates": "1.3.4",
|
||||
"@parcel/watcher": "2.4.1",
|
||||
"@types/estree": "1.0.5",
|
||||
@ -125,7 +126,7 @@
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vue/compiler-sfc": "3.4.27",
|
||||
"unbuild": "latest",
|
||||
"vite": "5.2.13",
|
||||
"vite": "5.3.0",
|
||||
"vitest": "1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { Component, PropType } from 'vue'
|
||||
import type { Component, PropType, VNode } from 'vue'
|
||||
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue'
|
||||
import { debounce } from 'perfect-debounce'
|
||||
import { hash } from 'ohash'
|
||||
@ -29,7 +29,7 @@ const getId = import.meta.client ? () => (id++).toString() : randomUUID
|
||||
const components = import.meta.client ? new Map<string, Component>() : undefined
|
||||
|
||||
async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['components']) {
|
||||
const promises = []
|
||||
const promises: Array<Promise<void>> = []
|
||||
|
||||
for (const component in paths) {
|
||||
if (!(components!.has(component))) {
|
||||
@ -259,7 +259,7 @@ export default defineComponent({
|
||||
|
||||
// should away be triggered ONE tick after re-rendering the static node
|
||||
withMemo([teleportKey.value], () => {
|
||||
const teleports = []
|
||||
const teleports: Array<VNode> = []
|
||||
// this is used to force trigger Teleport when vue makes the diff between old and new node
|
||||
const isKeyOdd = teleportKey.value === 0 || !!(teleportKey.value && !(teleportKey.value % 2))
|
||||
|
||||
|
@ -8,7 +8,7 @@ import type {
|
||||
} from 'vue'
|
||||
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue'
|
||||
import type { RouteLocation, RouteLocationRaw, Router, RouterLink, RouterLinkProps, useLink } from '#vue-router'
|
||||
import { hasProtocol, joinURL, parseQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo'
|
||||
import { hasProtocol, joinURL, parseQuery, withQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo'
|
||||
import { preloadRouteComponents } from '../composables/preload'
|
||||
import { onNuxtReady } from '../composables/ready'
|
||||
import { navigateTo, useRouter } from '../composables/router'
|
||||
@ -70,8 +70,8 @@ export interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
|
||||
* @see https://nuxt.com/docs/api/components/nuxt-link
|
||||
*/
|
||||
export interface NuxtLinkOptions extends
|
||||
Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>,
|
||||
Pick<NuxtLinkProps, 'prefetchedClass'> {
|
||||
Partial<Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>>,
|
||||
Partial<Pick<NuxtLinkProps, 'prefetchedClass'>> {
|
||||
/**
|
||||
* The name of the component.
|
||||
* @default "NuxtLink"
|
||||
@ -124,34 +124,17 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
const router = useRouter()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
// Resolving `to` value from `to` and `href` props
|
||||
const to: ComputedRef<string | RouteLocationRaw> = computed(() => {
|
||||
checkPropConflicts(props, 'to', 'href')
|
||||
const path = props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
|
||||
return resolveTrailingSlashBehavior(path, router.resolve)
|
||||
})
|
||||
const hasTarget = computed(() => !!props.target && props.target !== '_self')
|
||||
|
||||
// Lazily check whether to.value has a protocol
|
||||
const isAbsoluteUrl = computed(() => typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true }))
|
||||
|
||||
// Resolves `to` value if it's a route location object
|
||||
const href = computed(() => (typeof to.value === 'object'
|
||||
? router.resolve(to.value)?.href ?? null
|
||||
: (to.value && !props.external && !isAbsoluteUrl.value)
|
||||
? resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve) as string
|
||||
: to.value
|
||||
))
|
||||
const isAbsoluteUrl = computed(() => {
|
||||
const path = props.to || props.href || ''
|
||||
return typeof path === 'string' && hasProtocol(path, { acceptRelative: true })
|
||||
})
|
||||
|
||||
const builtinRouterLink = resolveComponent('RouterLink') as string | typeof RouterLink
|
||||
const useBuiltinLink = builtinRouterLink && typeof builtinRouterLink !== 'string' ? builtinRouterLink.useLink : undefined
|
||||
|
||||
const link = useBuiltinLink?.({
|
||||
...props,
|
||||
to: to.value,
|
||||
})
|
||||
|
||||
const hasTarget = computed(() => props.target && props.target !== '_self')
|
||||
|
||||
// Resolving link type
|
||||
const isExternal = computed<boolean>(() => {
|
||||
// External prop is explicitly set
|
||||
@ -159,17 +142,40 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
return true
|
||||
}
|
||||
|
||||
// When `target` prop is set, link is external
|
||||
if (hasTarget.value) {
|
||||
return true
|
||||
}
|
||||
const path = props.to || props.href || ''
|
||||
|
||||
// When `to` is a route object then it's an internal link
|
||||
if (typeof to.value === 'object') {
|
||||
if (typeof path === 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
return to.value === '' || isAbsoluteUrl.value
|
||||
return path === '' || isAbsoluteUrl.value
|
||||
})
|
||||
|
||||
// Resolving `to` value from `to` and `href` props
|
||||
const to: ComputedRef<RouteLocationRaw> = computed(() => {
|
||||
checkPropConflicts(props, 'to', 'href')
|
||||
const path = props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
|
||||
if (isExternal.value) { return path }
|
||||
return resolveTrailingSlashBehavior(path, router.resolve)
|
||||
})
|
||||
|
||||
const link = isExternal.value ? undefined : useBuiltinLink?.({ ...props, to })
|
||||
|
||||
// Resolves `to` value if it's a route location object
|
||||
const href = computed(() => {
|
||||
if (!to.value || isAbsoluteUrl.value) { return to.value as string }
|
||||
|
||||
if (isExternal.value) {
|
||||
const path = typeof to.value === 'object' ? resolveRouteObject(to.value) : to.value
|
||||
return resolveTrailingSlashBehavior(path, router.resolve /* will not be called */) as string
|
||||
}
|
||||
|
||||
if (typeof to.value === 'object') {
|
||||
return router.resolve(to.value)?.href ?? null
|
||||
}
|
||||
|
||||
return resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve /* will not be called */)
|
||||
})
|
||||
|
||||
return {
|
||||
@ -183,10 +189,10 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
isExactActive: link?.isExactActive ?? computed(() => to.value === router.currentRoute.value.path),
|
||||
route: link?.route ?? computed(() => router.resolve(to.value)),
|
||||
async navigate () {
|
||||
await navigateTo(href.value, { replace: props.replace, external: props.external })
|
||||
await navigateTo(href.value, { replace: props.replace, external: isExternal.value || hasTarget.value })
|
||||
},
|
||||
} satisfies ReturnType<typeof useLink> & {
|
||||
to: ComputedRef<string | RouteLocationRaw>
|
||||
to: ComputedRef<RouteLocationRaw>
|
||||
hasTarget: ComputedRef<boolean | null | undefined>
|
||||
isAbsoluteUrl: ComputedRef<boolean>
|
||||
isExternal: ComputedRef<boolean>
|
||||
@ -307,10 +313,12 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
unobserve?.()
|
||||
unobserve = null
|
||||
|
||||
const path = typeof to.value === 'string' ? to.value : router.resolve(to.value).fullPath
|
||||
const path = typeof to.value === 'string'
|
||||
? to.value
|
||||
: isExternal.value ? resolveRouteObject(to.value) : router.resolve(to.value).fullPath
|
||||
await Promise.all([
|
||||
nuxtApp.hooks.callHook('link:prefetch', path).catch(() => {}),
|
||||
!isExternal.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
|
||||
!isExternal.value && !hasTarget.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
|
||||
])
|
||||
prefetched.value = true
|
||||
})
|
||||
@ -336,7 +344,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!isExternal.value) {
|
||||
if (!isExternal.value && !hasTarget.value) {
|
||||
const routerLinkProps: RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = {
|
||||
ref: elRef,
|
||||
to: to.value,
|
||||
@ -408,7 +416,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
},
|
||||
rel,
|
||||
target,
|
||||
isExternal: isExternal.value,
|
||||
isExternal: isExternal.value || hasTarget.value,
|
||||
isActive: false,
|
||||
isExactActive: false,
|
||||
})
|
||||
@ -487,3 +495,7 @@ function isSlowConnection () {
|
||||
if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
function resolveRouteObject (to: Exclude<RouteLocationRaw, string>) {
|
||||
return withQuery(to.path || '', to.query || {}) + (to.hash ? '#' + to.hash : '')
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ export function vforToArray (source: any): any[] {
|
||||
if (import.meta.dev && !Number.isInteger(source)) {
|
||||
console.warn(`The v-for range expect an integer value but got ${source}.`)
|
||||
}
|
||||
const array = []
|
||||
const array: number[] = []
|
||||
for (let i = 0; i < source; i++) {
|
||||
array[i] = i
|
||||
}
|
||||
|
@ -37,3 +37,4 @@ export { reloadNuxtApp } from './chunk'
|
||||
export { useRequestURL } from './url'
|
||||
export { usePreviewMode } from './preview'
|
||||
export { useId } from './id'
|
||||
export { useRouteAnnouncer } from './route-announcer'
|
||||
|
@ -3,6 +3,7 @@ import { parse } from 'devalue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { getCurrentInstance, onServerPrefetch } from 'vue'
|
||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||
import type { NuxtPayload } from '../nuxt'
|
||||
|
||||
import { useRoute } from './router'
|
||||
import { getAppManifest, getRouteRules } from './manifest'
|
||||
@ -95,11 +96,12 @@ export async function isPrerendered (url = useRoute().path) {
|
||||
return !!rules.prerender && !rules.redirect
|
||||
}
|
||||
|
||||
let payloadCache: any = null
|
||||
let payloadCache: NuxtPayload | null = null
|
||||
|
||||
/** @since 3.4.0 */
|
||||
export async function getNuxtClientPayload () {
|
||||
if (import.meta.server) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (payloadCache) {
|
||||
return payloadCache
|
||||
@ -107,7 +109,7 @@ export async function getNuxtClientPayload () {
|
||||
|
||||
const el = document.getElementById('__NUXT_DATA__')
|
||||
if (!el) {
|
||||
return {}
|
||||
return {} as Partial<NuxtPayload>
|
||||
}
|
||||
|
||||
const inlineData = await parsePayload(el.textContent || '')
|
||||
|
@ -7,8 +7,8 @@ export const onNuxtReady = (callback: () => any) => {
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
if (nuxtApp.isHydrating) {
|
||||
nuxtApp.hooks.hookOnce('app:suspense:resolve', () => { requestIdleCallback(callback) })
|
||||
nuxtApp.hooks.hookOnce('app:suspense:resolve', () => { requestIdleCallback(() => callback()) })
|
||||
} else {
|
||||
requestIdleCallback(callback)
|
||||
requestIdleCallback(() => callback())
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ export interface NuxtSSRContext extends SSRContext {
|
||||
/** whether we are rendering an SSR error */
|
||||
error?: boolean
|
||||
nuxt: _NuxtApp
|
||||
payload: NuxtPayload
|
||||
payload: Partial<NuxtPayload>
|
||||
head: VueHeadClient<MergeHead>
|
||||
/** This is used solely to render runtime config with SPA renderer. */
|
||||
config?: Pick<RuntimeConfig, 'public' | 'app'>
|
||||
@ -558,6 +558,7 @@ export function defineAppConfig<C extends AppConfigInput> (config: C): C {
|
||||
/**
|
||||
* Configure error getter on runtime secret property access that doesn't exist on the client side
|
||||
*/
|
||||
const loggedKeys = new Set<string>()
|
||||
function wrappedConfig (runtimeConfig: Record<string, unknown>) {
|
||||
if (!import.meta.dev || import.meta.server) { return runtimeConfig }
|
||||
const keys = Object.keys(runtimeConfig).map(key => `\`${key}\``)
|
||||
@ -565,7 +566,10 @@ function wrappedConfig (runtimeConfig: Record<string, unknown>) {
|
||||
return new Proxy(runtimeConfig, {
|
||||
get (target, p, receiver) {
|
||||
if (typeof p === 'string' && p !== 'public' && !(p in target) && !p.startsWith('__v') /* vue check for reactivity, e.g. `__v_isRef` */) {
|
||||
console.warn(`[nuxt] Could not access \`${p}\`. The only available runtime config keys on the client side are ${keys.join(', ')} and ${lastKey}. See \`https://nuxt.com/docs/guide/going-further/runtime-config\` for more information.`)
|
||||
if (!loggedKeys.has(p)) {
|
||||
loggedKeys.add(p)
|
||||
console.warn(`[nuxt] Could not access \`${p}\`. The only available runtime config keys on the client side are ${keys.join(', ')} and ${lastKey}. See https://nuxt.com/docs/guide/going-further/runtime-config for more information.`)
|
||||
}
|
||||
}
|
||||
return Reflect.get(target, p, receiver)
|
||||
},
|
||||
|
@ -136,6 +136,7 @@ function createGranularWatcher () {
|
||||
console.timeEnd('[nuxt] builder:chokidar:watch')
|
||||
}
|
||||
})
|
||||
nuxt.hook('close', () => watcher?.close())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
const modules = await resolveNuxtModule(rootDirWithSlash,
|
||||
nuxt.options._installedModules
|
||||
.filter(m => m.entryPath)
|
||||
.map(m => m.entryPath),
|
||||
.map(m => m.entryPath!),
|
||||
)
|
||||
|
||||
const nitroConfig: NitroConfig = defu(nuxt.options.nitro, {
|
||||
|
@ -4,7 +4,7 @@ import ignore from 'ignore'
|
||||
import type { LoadNuxtOptions } from '@nuxt/kit'
|
||||
import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addServerPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
|
||||
import { resolvePath as _resolvePath } from 'mlly'
|
||||
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions, RuntimeConfig } from 'nuxt/schema'
|
||||
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema'
|
||||
import type { PackageJson } from 'pkg-types'
|
||||
import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
|
||||
import { hash } from 'ohash'
|
||||
@ -49,7 +49,10 @@ export function createNuxt (options: NuxtOptions): Nuxt {
|
||||
addHooks: hooks.addHooks,
|
||||
hook: hooks.hook,
|
||||
ready: () => initNuxt(nuxt),
|
||||
close: () => Promise.resolve(hooks.callHook('close', nuxt)),
|
||||
close: async () => {
|
||||
await hooks.callHook('close', nuxt)
|
||||
hooks.removeAllHooks()
|
||||
},
|
||||
vfs: {},
|
||||
apps: {},
|
||||
}
|
||||
@ -125,6 +128,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
// Add nuxt types
|
||||
nuxt.hook('prepare:types', (opts) => {
|
||||
opts.references.push({ types: 'nuxt' })
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/app-defaults.d.ts') })
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/plugins.d.ts') })
|
||||
// Add vue shim
|
||||
if (nuxt.options.typescript.shim) {
|
||||
@ -641,8 +645,22 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
||||
options._modules.push('@nuxt/telemetry')
|
||||
}
|
||||
|
||||
// Ensure we share runtime config between Nuxt and Nitro
|
||||
options.runtimeConfig = options.nitro.runtimeConfig as RuntimeConfig
|
||||
// Ensure we share key config between Nuxt and Nitro
|
||||
createPortalProperties(options.nitro.runtimeConfig, options, ['nitro.runtimeConfig', 'runtimeConfig'])
|
||||
createPortalProperties(options.nitro.routeRules, options, ['nitro.routeRules', 'routeRules'])
|
||||
|
||||
// prevent replacement of options.nitro
|
||||
const nitroOptions = options.nitro
|
||||
Object.defineProperties(options, {
|
||||
nitro: {
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
get: () => nitroOptions,
|
||||
set (value) {
|
||||
Object.assign(nitroOptions, value)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const nuxt = createNuxt(options)
|
||||
|
||||
@ -680,7 +698,7 @@ const RESTART_RE = /^(?:app|error|app\.config)\.(?:js|ts|mjs|jsx|tsx|vue)$/i
|
||||
function deduplicateArray<T = unknown> (maybeArray: T): T {
|
||||
if (!Array.isArray(maybeArray)) { return maybeArray }
|
||||
|
||||
const fresh = []
|
||||
const fresh: any[] = []
|
||||
const hashes = new Set<string>()
|
||||
for (const item of maybeArray) {
|
||||
const _hash = hash(item)
|
||||
@ -691,3 +709,31 @@ function deduplicateArray<T = unknown> (maybeArray: T): T {
|
||||
}
|
||||
return fresh as T
|
||||
}
|
||||
|
||||
function createPortalProperties (sourceValue: any, options: NuxtOptions, paths: string[]) {
|
||||
let sharedValue = sourceValue
|
||||
|
||||
for (const path of paths) {
|
||||
const segments = path.split('.')
|
||||
const key = segments.pop()!
|
||||
let parent: Record<string, any> = options
|
||||
|
||||
while (segments.length) {
|
||||
const key = segments.shift()!
|
||||
parent = parent[key] || (parent[key] = {})
|
||||
}
|
||||
|
||||
delete parent[key]
|
||||
|
||||
Object.defineProperties(parent, {
|
||||
[key]: {
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
get: () => sharedValue,
|
||||
set (value) {
|
||||
sharedValue = value
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -165,11 +165,7 @@ const getSPARenderer = lazyCachedFunction(async () => {
|
||||
const config = useRuntimeConfig(ssrContext.event)
|
||||
ssrContext.modules = ssrContext.modules || new Set<string>()
|
||||
ssrContext!.payload = {
|
||||
_errors: {},
|
||||
serverRendered: false,
|
||||
data: {},
|
||||
state: {},
|
||||
once: new Set<string>(),
|
||||
}
|
||||
ssrContext.config = {
|
||||
public: config.public,
|
||||
@ -404,7 +400,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
// 2. Styles
|
||||
head.push({ style: inlinedStyles })
|
||||
if (!isRenderingIsland || import.meta.dev) {
|
||||
const link = []
|
||||
const link: Link[] = []
|
||||
for (const style in styles) {
|
||||
const resource = styles[style]
|
||||
// Do not add links to resources that are inlined (vite v5+)
|
||||
|
@ -7,7 +7,7 @@ import escapeRE from 'escape-string-regexp'
|
||||
import { hash } from 'ohash'
|
||||
import { camelCase } from 'scule'
|
||||
import { filename } from 'pathe/utils'
|
||||
import type { NuxtTemplate } from 'nuxt/schema'
|
||||
import type { NuxtTemplate, NuxtTypeTemplate } from 'nuxt/schema'
|
||||
|
||||
import { annotatePlugins, checkForCircularDependencies } from './app'
|
||||
|
||||
@ -96,6 +96,20 @@ export const serverPluginTemplate: NuxtTemplate = {
|
||||
},
|
||||
}
|
||||
|
||||
export const appDefaults: NuxtTypeTemplate = {
|
||||
filename: 'types/app-defaults.d.ts',
|
||||
getContents: (ctx) => {
|
||||
const isV4 = ctx.nuxt.options.future.compatibilityVersion === 4
|
||||
return `
|
||||
declare module '#app/defaults' {
|
||||
type DefaultAsyncDataErrorValue = ${isV4 ? 'undefined' : 'null'}
|
||||
type DefaultAsyncDataValue = ${isV4 ? 'undefined' : 'null'}
|
||||
type DefaultErrorValue = ${isV4 ? 'undefined' : 'null'}
|
||||
type DedupeOption = ${isV4 ? '\'cancel\' | \'defer\'' : 'boolean | \'cancel\' | \'defer\''}
|
||||
}`
|
||||
},
|
||||
}
|
||||
|
||||
export const pluginsDeclaration: NuxtTemplate = {
|
||||
filename: 'types/plugins.d.ts',
|
||||
getContents: async (ctx) => {
|
||||
@ -112,8 +126,6 @@ export const pluginsDeclaration: NuxtTemplate = {
|
||||
|
||||
const pluginsName = (await annotatePlugins(ctx.nuxt, ctx.app.plugins)).filter(p => p.name).map(p => `'${p.name}'`)
|
||||
|
||||
const isV4 = ctx.nuxt.options.future.compatibilityVersion === 4
|
||||
|
||||
return `// Generated by Nuxt'
|
||||
import type { Plugin } from '#app'
|
||||
|
||||
@ -132,13 +144,6 @@ declare module '#app' {
|
||||
}
|
||||
}
|
||||
|
||||
declare module '#app/defaults' {
|
||||
type DefaultAsyncDataErrorValue = ${isV4 ? 'undefined' : 'null'}
|
||||
type DefaultAsyncDataValue = ${isV4 ? 'undefined' : 'null'}
|
||||
type DefaultErrorValue = ${isV4 ? 'undefined' : 'null'}
|
||||
type DedupeOption = ${isV4 ? '\'cancel\' | \'defer\'' : 'boolean | \'cancel\' | \'defer\''}
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties extends NuxtAppInjections { }
|
||||
}
|
||||
|
@ -109,6 +109,10 @@ const granularAppPresets: InlinePreset[] = [
|
||||
imports: ['useId'],
|
||||
from: '#app/composables/id',
|
||||
},
|
||||
{
|
||||
imports: ['useRouteAnnouncer'],
|
||||
from: '#app/composables/route-announcer',
|
||||
},
|
||||
]
|
||||
|
||||
export const scriptsStubsPreset = {
|
||||
@ -119,19 +123,21 @@ export const scriptsStubsPreset = {
|
||||
'useScript',
|
||||
'useScriptGoogleAnalytics',
|
||||
'useScriptPlausibleAnalytics',
|
||||
'useScriptClarity',
|
||||
'useScriptCloudflareWebAnalytics',
|
||||
'useScriptFathomAnalytics',
|
||||
'useScriptMatomoAnalytics',
|
||||
'useScriptGoogleTagManager',
|
||||
'useScriptGoogleAdsense',
|
||||
'useScriptSegment',
|
||||
'useScriptFacebookPixel',
|
||||
'useScriptMetaPixel',
|
||||
'useScriptXPixel',
|
||||
'useScriptIntercom',
|
||||
'useScriptHotjar',
|
||||
'useScriptStripe',
|
||||
'useScriptLemonSqueezy',
|
||||
'useScriptVimeoPlayer',
|
||||
'useScriptYouTubeIframe',
|
||||
'useScriptYouTubePlayer',
|
||||
'useScriptGoogleMaps',
|
||||
'useScriptNpm',
|
||||
],
|
||||
|
@ -275,6 +275,20 @@ export default defineNuxtModule({
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: inject routes in `200.html` in next nitro upgrade (2.9.7+) via https://github.com/unjs/nitro/pull/2517
|
||||
if (!nuxt.options.dev && !nuxt.options._prepare) {
|
||||
nuxt.hook('app:templatesGenerated', (app) => {
|
||||
const nitro = useNitro()
|
||||
if (nitro.options.prerender.crawlLinks) {
|
||||
for (const page of app.pages!) {
|
||||
if (page.path && !page.path.includes(':')) {
|
||||
nitro.options.prerender.routes.push(page.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
nuxt.hook('imports:extend', (imports) => {
|
||||
imports.push(
|
||||
{ name: 'definePageMeta', as: 'definePageMeta', from: resolve(runtimeDir, 'composables') },
|
||||
|
@ -18,11 +18,12 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
|
||||
}
|
||||
if (!ROUTE_RULE_RE.test(code)) { return null }
|
||||
|
||||
code = extractScriptContent(code) || code
|
||||
const script = extractScriptContent(code)
|
||||
code = script?.code || code
|
||||
|
||||
let rule: NitroRouteConfig | null = null
|
||||
|
||||
const js = await transform(code, { loader: 'ts' })
|
||||
const js = await transform(code, { loader: script?.loader || 'ts' })
|
||||
walk(parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
|
@ -9,6 +9,7 @@ import { filename } from 'pathe/utils'
|
||||
import { hash } from 'ohash'
|
||||
import { transform } from 'esbuild'
|
||||
import { parse } from 'acorn'
|
||||
import { walk } from 'estree-walker'
|
||||
import type { CallExpression, ExpressionStatement, ObjectExpression, Program, Property } from 'estree'
|
||||
import type { NuxtPage } from 'nuxt/schema'
|
||||
|
||||
@ -139,11 +140,12 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
|
||||
return prepareRoutes(routes)
|
||||
}
|
||||
|
||||
export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, augmentedPages = new Set<NuxtPage>()) {
|
||||
export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, augmentedPages = new Set<string>()) {
|
||||
for (const route of routes) {
|
||||
if (!augmentedPages.has(route) && route.file) {
|
||||
if (route.file && !augmentedPages.has(route.file)) {
|
||||
const fileContent = route.file in vfs ? vfs[route.file] : fs.readFileSync(await resolvePath(route.file), 'utf-8')
|
||||
Object.assign(route, await getRouteMeta(fileContent, route.file))
|
||||
augmentedPages.add(route.file)
|
||||
}
|
||||
|
||||
if (route.children && route.children.length > 0) {
|
||||
@ -153,12 +155,15 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
|
||||
return augmentedPages
|
||||
}
|
||||
|
||||
const SFC_SCRIPT_RE = /<script[^>]*>([\s\S]*?)<\/script[^>]*>/i
|
||||
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/i
|
||||
export function extractScriptContent (html: string) {
|
||||
const match = html.match(SFC_SCRIPT_RE)
|
||||
const groups = html.match(SFC_SCRIPT_RE)?.groups || {}
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1].trim()
|
||||
if (groups.content) {
|
||||
return {
|
||||
loader: groups.attrs.includes('tsx') ? 'tsx' : 'ts',
|
||||
code: groups.content.trim(),
|
||||
} as const
|
||||
}
|
||||
|
||||
return null
|
||||
@ -169,7 +174,7 @@ const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
|
||||
|
||||
const pageContentsCache: Record<string, string> = {}
|
||||
const metaCache: Record<string, Partial<Record<keyof NuxtPage, any>>> = {}
|
||||
async function getRouteMeta (contents: string, absolutePath: string): Promise<Partial<Record<keyof NuxtPage, any>>> {
|
||||
export async function getRouteMeta (contents: string, absolutePath: string): Promise<Partial<Record<keyof NuxtPage, any>>> {
|
||||
// set/update pageContentsCache, invalidate metaCache on cache mismatch
|
||||
if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) {
|
||||
pageContentsCache[absolutePath] = contents
|
||||
@ -184,81 +189,88 @@ async function getRouteMeta (contents: string, absolutePath: string): Promise<Pa
|
||||
return {}
|
||||
}
|
||||
|
||||
if (!PAGE_META_RE.test(script)) {
|
||||
if (!PAGE_META_RE.test(script.code)) {
|
||||
metaCache[absolutePath] = {}
|
||||
return {}
|
||||
}
|
||||
|
||||
const js = await transform(script, { loader: 'ts' })
|
||||
const js = await transform(script.code, { loader: script.loader })
|
||||
const ast = parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
ranges: true,
|
||||
}) as unknown as Program
|
||||
const pageMetaAST = ast.body.find(node => node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' && node.expression.callee.type === 'Identifier' && node.expression.callee.name === 'definePageMeta')
|
||||
if (!pageMetaAST) {
|
||||
metaCache[absolutePath] = {}
|
||||
return {}
|
||||
}
|
||||
|
||||
const pageMetaArgument = ((pageMetaAST as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
||||
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
|
||||
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
|
||||
const dynamicProperties = new Set<keyof NuxtPage>()
|
||||
|
||||
for (const key of extractionKeys) {
|
||||
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
|
||||
if (!property) { continue }
|
||||
let foundMeta = false
|
||||
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
|
||||
try {
|
||||
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
|
||||
} catch {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
walk(ast, {
|
||||
enter (node) {
|
||||
if (foundMeta) { return }
|
||||
|
||||
if (property.value.type === 'ArrayExpression') {
|
||||
const values = []
|
||||
for (const element of property.value.elements) {
|
||||
if (!element) {
|
||||
if (node.type !== 'ExpressionStatement' || node.expression.type !== 'CallExpression' || node.expression.callee.type !== 'Identifier' || node.expression.callee.name !== 'definePageMeta') { return }
|
||||
|
||||
foundMeta = true
|
||||
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
||||
|
||||
for (const key of extractionKeys) {
|
||||
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
|
||||
if (!property) { continue }
|
||||
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
|
||||
try {
|
||||
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
|
||||
} catch {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (property.value.type === 'ArrayExpression') {
|
||||
const values: string[] = []
|
||||
for (const element of property.value.elements) {
|
||||
if (!element) {
|
||||
continue
|
||||
}
|
||||
if (element.type !== 'Literal' || typeof element.value !== 'string') {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
values.push(element.value)
|
||||
}
|
||||
extractedMeta[key] = values
|
||||
continue
|
||||
}
|
||||
if (element.type !== 'Literal' || typeof element.value !== 'string') {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)
|
||||
|
||||
if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
values.push(element.value)
|
||||
extractedMeta[key] = property.value.value
|
||||
}
|
||||
extractedMeta[key] = values
|
||||
continue
|
||||
}
|
||||
|
||||
if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
extractedMeta[key] = property.value.value
|
||||
}
|
||||
const extraneousMetaKeys = pageMetaArgument.properties
|
||||
.filter(property => property.type === 'Property' && property.key.type === 'Identifier' && !(extractionKeys as unknown as string[]).includes(property.key.name))
|
||||
// @ts-expect-error inferred types have been filtered out
|
||||
.map(property => property.key.name)
|
||||
|
||||
const extraneousMetaKeys = pageMetaArgument.properties
|
||||
.filter(property => property.type === 'Property' && property.key.type === 'Identifier' && !(extractionKeys as unknown as string[]).includes(property.key.name))
|
||||
// @ts-expect-error inferred types have been filtered out
|
||||
.map(property => property.key.name)
|
||||
if (extraneousMetaKeys.length) {
|
||||
dynamicProperties.add('meta')
|
||||
}
|
||||
|
||||
if (extraneousMetaKeys.length) {
|
||||
dynamicProperties.add('meta')
|
||||
}
|
||||
|
||||
if (dynamicProperties.size) {
|
||||
extractedMeta.meta ??= {}
|
||||
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
||||
}
|
||||
if (dynamicProperties.size) {
|
||||
extractedMeta.meta ??= {}
|
||||
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
metaCache[absolutePath] = extractedMeta
|
||||
return extractedMeta
|
||||
@ -501,19 +513,20 @@ async function createClientPage(loader) {
|
||||
}`)
|
||||
}
|
||||
|
||||
if (route.children != null) {
|
||||
if (route.children) {
|
||||
metaRoute.children = route.children
|
||||
}
|
||||
|
||||
if (overrideMeta) {
|
||||
metaRoute.name = `${metaImportName}?.name`
|
||||
metaRoute.path = `${metaImportName}?.path ?? ''`
|
||||
if (route.meta) {
|
||||
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`
|
||||
}
|
||||
|
||||
if (overrideMeta) {
|
||||
// skip and retain fallback if marked dynamic
|
||||
// set to extracted value or fallback if none extracted
|
||||
for (const key of ['name', 'path'] satisfies NormalizedRouteKeys) {
|
||||
if (markedDynamic.has(key)) { continue }
|
||||
metaRoute[key] = route[key] ?? metaRoute[key]
|
||||
metaRoute[key] = route[key] ?? `${metaImportName}?.${key}`
|
||||
}
|
||||
|
||||
// set to extracted value or delete if none extracted
|
||||
@ -528,10 +541,6 @@ async function createClientPage(loader) {
|
||||
metaRoute[key] = route[key]
|
||||
}
|
||||
} else {
|
||||
if (route.meta != null) {
|
||||
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`
|
||||
}
|
||||
|
||||
if (route.alias != null) {
|
||||
metaRoute.alias = `${route.alias}.concat(${metaImportName}?.alias || [])`
|
||||
}
|
||||
|
@ -303,7 +303,7 @@
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name",
|
||||
"name": "mockMeta?.name ?? "index"",
|
||||
"path": ""/"",
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
|
@ -6,8 +6,9 @@ import * as VueFunctions from 'vue'
|
||||
import type { Import } from 'unimport'
|
||||
import { createUnimport } from 'unimport'
|
||||
import type { Plugin } from 'vite'
|
||||
import { registry as scriptRegistry } from '@nuxt/scripts/registry'
|
||||
import { TransformPlugin } from '../src/imports/transform'
|
||||
import { defaultPresets } from '../src/imports/presets'
|
||||
import { defaultPresets, scriptsStubsPreset } from '../src/imports/presets'
|
||||
|
||||
describe('imports:transform', () => {
|
||||
const imports: Import[] = [
|
||||
@ -193,3 +194,24 @@ describe('imports:vue', () => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('imports:nuxt/scripts', () => {
|
||||
const scripts = scriptRegistry().map(s => s.import?.name).filter(Boolean)
|
||||
const globalScripts = new Set([
|
||||
'useScript',
|
||||
'useAnalyticsPageEvent',
|
||||
'useElementScriptTrigger',
|
||||
'useConsentScriptTrigger',
|
||||
// registered separately
|
||||
'useScriptGoogleTagManager',
|
||||
'useScriptGoogleAnalytics',
|
||||
])
|
||||
it.each(scriptsStubsPreset.imports)(`should register %s from @nuxt/scripts`, (name) => {
|
||||
if (globalScripts.has(name)) { return }
|
||||
|
||||
expect(scripts).toContain(name)
|
||||
})
|
||||
it.each(scripts)(`should register %s from @nuxt/scripts`, (name) => {
|
||||
expect(scriptsStubsPreset.imports).toContain(name)
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { RouteLocation, RouteLocationRaw } from 'vue-router'
|
||||
import type { RouteLocation } from 'vue-router'
|
||||
import type { NuxtLinkOptions, NuxtLinkProps } from '../src/app/components/nuxt-link'
|
||||
import { defineNuxtLink } from '../src/app/components/nuxt-link'
|
||||
import { useRuntimeConfig } from '../src/app/nuxt'
|
||||
@ -99,7 +99,11 @@ describe('nuxt-link:isExternal', () => {
|
||||
})
|
||||
|
||||
it('returns `false` when `to` is a route location object', () => {
|
||||
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw }).type).toBe(INTERNAL)
|
||||
expect(nuxtLink({ to: { path: '/to' } }).type).toBe(INTERNAL)
|
||||
})
|
||||
|
||||
it('returns `true` when `to` has a `target`', () => {
|
||||
expect(nuxtLink({ to: { path: '/to' }, target: '_blank' }).type).toBe(EXTERNAL)
|
||||
})
|
||||
|
||||
it('honors `external` prop', () => {
|
||||
@ -122,7 +126,12 @@ describe('nuxt-link:propsOrAttributes', () => {
|
||||
})
|
||||
|
||||
it('resolves route location object', () => {
|
||||
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw, external: true }).props.href).toBe('/to')
|
||||
expect(nuxtLink({ to: { path: '/to' }, external: true }).props.href).toBe('/to')
|
||||
})
|
||||
|
||||
it('applies trailing slash behaviour', () => {
|
||||
expect(nuxtLink({ to: { path: '/to' }, external: true }, { trailingSlash: 'append' }).props.href).toBe('/to/')
|
||||
expect(nuxtLink({ to: '/to', external: true }, { trailingSlash: 'append' }).props.href).toBe('/to/')
|
||||
})
|
||||
})
|
||||
|
||||
@ -167,6 +176,8 @@ describe('nuxt-link:propsOrAttributes', () => {
|
||||
}, () => {
|
||||
expect(nuxtLink({ to: 'http://nuxtjs.org/app/about', target: '_blank' }).props.href).toBe('http://nuxtjs.org/app/about')
|
||||
expect(nuxtLink({ to: '//nuxtjs.org/app/about', target: '_blank' }).props.href).toBe('//nuxtjs.org/app/about')
|
||||
expect(nuxtLink({ to: { path: '/' }, external: true }).props.href).toBe('/')
|
||||
expect(nuxtLink({ to: '/', external: true }).props.href).toBe('/')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -209,7 +220,7 @@ describe('nuxt-link:propsOrAttributes', () => {
|
||||
describe('to', () => {
|
||||
it('forwards `to` prop', () => {
|
||||
expect(nuxtLink({ to: '/to' }).props.to).toBe('/to')
|
||||
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw }).props.to).toEqual({ to: '/to' })
|
||||
expect(nuxtLink({ to: { path: '/to' } }).props.to).toEqual({ path: '/to' })
|
||||
})
|
||||
})
|
||||
|
||||
|
148
packages/nuxt/test/page-metadata.test.ts
Normal file
148
packages/nuxt/test/page-metadata.test.ts
Normal 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)
|
||||
}
|
||||
]",
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nuxt/schema",
|
||||
"version": "3.11.2",
|
||||
"version": "3.12.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/nuxt.git",
|
||||
@ -39,13 +39,13 @@
|
||||
"@types/file-loader": "5.0.4",
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/sass-loader": "8.0.8",
|
||||
"@unhead/schema": "1.9.12",
|
||||
"@unhead/schema": "1.9.13",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "4.0.0",
|
||||
"@vue/compiler-core": "3.4.27",
|
||||
"@vue/compiler-sfc": "3.4.27",
|
||||
"@vue/language-core": "2.0.21",
|
||||
"c12": "1.10.0",
|
||||
"c12": "1.11.1",
|
||||
"esbuild-loader": "4.1.0",
|
||||
"h3": "1.11.1",
|
||||
"ignore": "5.3.1",
|
||||
@ -54,16 +54,16 @@
|
||||
"unbuild": "latest",
|
||||
"unctx": "2.3.1",
|
||||
"unenv": "1.9.0",
|
||||
"vite": "5.2.13",
|
||||
"vite": "5.3.0",
|
||||
"vue": "3.4.27",
|
||||
"vue-bundle-renderer": "2.1.0",
|
||||
"vue-loader": "17.4.2",
|
||||
"vue-router": "4.3.3",
|
||||
"webpack": "5.91.0",
|
||||
"webpack": "5.92.0",
|
||||
"webpack-dev-middleware": "7.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"compatx": "^0.1.3",
|
||||
"compatx": "^0.1.8",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.4",
|
||||
"hookable": "^5.5.3",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import { defineUntypedSchema } from 'untyped'
|
||||
import { join, relative, resolve } from 'pathe'
|
||||
import { isDebug, isDevelopment, isTest } from 'std-env'
|
||||
@ -28,7 +29,7 @@ export default defineUntypedSchema({
|
||||
*
|
||||
* We plan to improve the tooling around this feature in the future.
|
||||
*
|
||||
* @type {typeof import('compatx').DateString | Record<string, typeof import('compatx').DateString>}
|
||||
* @type {typeof import('compatx').CompatibilityDateSpec}
|
||||
*/
|
||||
compatibilityDate: undefined,
|
||||
|
||||
@ -117,7 +118,16 @@ export default defineUntypedSchema({
|
||||
}
|
||||
|
||||
const srcDir = resolve(rootDir, 'app')
|
||||
if (!existsSync(srcDir)) {
|
||||
const srcDirFiles = new Set<string>()
|
||||
if (existsSync(srcDir)) {
|
||||
const files = await readdir(srcDir).catch(() => [])
|
||||
for (const file of files) {
|
||||
if (file !== 'spa-loading-template.html' && !file.startsWith('router.options')) {
|
||||
srcDirFiles.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (srcDirFiles.size === 0) {
|
||||
for (const file of ['app.vue', 'App.vue']) {
|
||||
if (existsSync(resolve(rootDir, file))) {
|
||||
return rootDir
|
||||
|
@ -23,7 +23,10 @@ export default defineUntypedSchema({
|
||||
_nuxtConfigFiles: [],
|
||||
/** @private */
|
||||
appDir: '',
|
||||
/** @private */
|
||||
/**
|
||||
* @private
|
||||
* @type {Array<{ meta: ModuleMeta; timings?: Record<string, number | undefined>; entryPath?: string }>}
|
||||
*/
|
||||
_installedModules: [],
|
||||
/** @private */
|
||||
_modules: [],
|
||||
|
@ -141,7 +141,7 @@ export interface AppConfigInput extends CustomAppConfig {
|
||||
server?: never
|
||||
}
|
||||
|
||||
type Serializable<T> = T extends Function ? never : T extends Promise<infer U> ? Serializable<U> : T extends Record<string, any> ? { [K in keyof T]: Serializable<T[K]> } : T
|
||||
type Serializable<T> = T extends Function ? never : T extends Promise<infer U> ? Serializable<U> : T extends string & {} ? T : T extends Record<string, any> ? { [K in keyof T]: Serializable<T[K]> } : T
|
||||
|
||||
export interface NuxtAppConfig {
|
||||
head: Serializable<AppHeadMetaObject>
|
||||
|
@ -21,7 +21,7 @@
|
||||
"devDependencies": {
|
||||
"@types/html-minifier": "4.0.5",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@unocss/reset": "0.60.4",
|
||||
"@unocss/reset": "0.61.0",
|
||||
"critters": "0.0.22",
|
||||
"execa": "9.2.0",
|
||||
"globby": "14.0.1",
|
||||
@ -30,9 +30,9 @@
|
||||
"knitwork": "1.1.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"pathe": "1.1.2",
|
||||
"prettier": "3.3.1",
|
||||
"prettier": "3.3.2",
|
||||
"scule": "1.3.0",
|
||||
"unocss": "0.60.4",
|
||||
"vite": "5.2.13"
|
||||
"unocss": "0.61.0",
|
||||
"vite": "5.3.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nuxt/vite-builder",
|
||||
"version": "3.11.2",
|
||||
"version": "3.12.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/nuxt.git",
|
||||
@ -28,6 +28,7 @@
|
||||
"@types/clear": "0.1.4",
|
||||
"@types/estree": "1.0.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"rollup": "4.18.0",
|
||||
"unbuild": "latest",
|
||||
"vue": "3.4.27"
|
||||
},
|
||||
@ -62,7 +63,7 @@
|
||||
"ufo": "^1.5.3",
|
||||
"unenv": "^1.9.0",
|
||||
"unplugin": "^1.10.1",
|
||||
"vite": "^5.2.13",
|
||||
"vite": "^5.3.0",
|
||||
"vite-node": "^1.6.0",
|
||||
"vite-plugin-checker": "^0.6.4",
|
||||
"vue-bundle-renderer": "^2.1.0"
|
||||
|
@ -163,10 +163,11 @@ export async function buildClient (ctx: ViteBuildContext) {
|
||||
}
|
||||
|
||||
// We want to respect users' own rollup output options
|
||||
const fileNames = withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js'))
|
||||
clientConfig.build!.rollupOptions = defu(clientConfig.build!.rollupOptions!, {
|
||||
output: {
|
||||
chunkFileNames: ctx.nuxt.options.dev ? undefined : withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js')),
|
||||
entryFileNames: ctx.nuxt.options.dev ? 'entry.js' : withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js')),
|
||||
chunkFileNames: ctx.nuxt.options.dev ? undefined : fileNames,
|
||||
entryFileNames: ctx.nuxt.options.dev ? 'entry.js' : fileNames,
|
||||
} satisfies NonNullable<BuildOptions['rollupOptions']>['output'],
|
||||
}) as any
|
||||
|
||||
@ -228,7 +229,13 @@ export async function buildClient (ctx: ViteBuildContext) {
|
||||
})
|
||||
|
||||
const viteMiddleware = defineEventHandler(async (event) => {
|
||||
const viteRoutes = viteServer.middlewares.stack.map(m => m.route).filter(r => r.length > 1)
|
||||
const viteRoutes: string[] = []
|
||||
for (const viteRoute of viteServer.middlewares.stack) {
|
||||
const m = viteRoute.route
|
||||
if (m.length > 1) {
|
||||
viteRoutes.push(m)
|
||||
}
|
||||
}
|
||||
if (!event.path.startsWith(clientConfig.base!) && !viteRoutes.some(route => event.path.startsWith(route))) {
|
||||
// @ts-expect-error _skip_transform is a private property
|
||||
event.node.req._skip_transform = true
|
||||
|
@ -3,6 +3,8 @@ import type { Nuxt } from '@nuxt/schema'
|
||||
import type { InlineConfig as ViteConfig } from 'vite'
|
||||
import { distDir } from './dirs'
|
||||
|
||||
const lastPlugins = ['autoprefixer', 'cssnano']
|
||||
|
||||
export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
|
||||
const css: ViteConfig['css'] & { postcss: NonNullable<Exclude<NonNullable<ViteConfig['css']>['postcss'], string>> } = {
|
||||
postcss: {
|
||||
@ -10,19 +12,22 @@ export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
|
||||
},
|
||||
}
|
||||
|
||||
const lastPlugins = ['autoprefixer', 'cssnano']
|
||||
css.postcss.plugins = Object.entries(nuxt.options.postcss.plugins)
|
||||
css.postcss.plugins = []
|
||||
|
||||
const plugins = Object.entries(nuxt.options.postcss.plugins)
|
||||
.sort((a, b) => lastPlugins.indexOf(a[0]) - lastPlugins.indexOf(b[0]))
|
||||
.filter(([, opts]) => opts)
|
||||
.map(([name, opts]) => {
|
||||
|
||||
for (const [name, opts] of plugins) {
|
||||
if (opts) {
|
||||
const plugin = requireModule(name, {
|
||||
paths: [
|
||||
...nuxt.options.modulesDir,
|
||||
distDir,
|
||||
],
|
||||
})
|
||||
return plugin(opts)
|
||||
})
|
||||
css.postcss.plugins.push(plugin(opts))
|
||||
}
|
||||
}
|
||||
|
||||
return css
|
||||
}
|
||||
|
@ -238,7 +238,13 @@ export async function initViteDevBundler (ctx: ViteBuildContext, onBuild: () =>
|
||||
const { code, ids } = await bundleRequest(options, ctx.entry)
|
||||
await fse.writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs'), code, 'utf-8')
|
||||
// Have CSS in the manifest to prevent FOUC on dev SSR
|
||||
await writeManifest(ctx, ids.filter(isCSS).map(i => i.slice(1)))
|
||||
const manifestIds: string[] = []
|
||||
for (const i of ids) {
|
||||
if (isCSS(i)) {
|
||||
manifestIds.push(i.slice(1))
|
||||
}
|
||||
}
|
||||
await writeManifest(ctx, manifestIds)
|
||||
const time = (Date.now() - start)
|
||||
logger.success(`Vite server built in ${time}ms`)
|
||||
await onBuild()
|
||||
|
@ -50,11 +50,14 @@ export async function writeManifest (ctx: ViteBuildContext, css: string[] = [])
|
||||
await fse.mkdirp(serverDist)
|
||||
|
||||
if (ctx.config.build?.cssCodeSplit === false) {
|
||||
const entryCSS = Object.values(clientManifest as Record<string, { file?: string }>).find(val => (val).file?.endsWith('.css'))?.file
|
||||
if (entryCSS) {
|
||||
const key = relative(ctx.config.root!, ctx.entry)
|
||||
clientManifest[key].css ||= []
|
||||
clientManifest[key].css!.push(entryCSS)
|
||||
for (const key in clientManifest as Record<string, { file?: string }>) {
|
||||
const val = clientManifest[key]
|
||||
if (val.file?.endsWith('.css')) {
|
||||
const key = relative(ctx.config.root!, ctx.entry)
|
||||
clientManifest[key].css ||= []
|
||||
clientManifest[key].css!.push(val.file)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { transform } from 'esbuild'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
import defu from 'defu'
|
||||
import type { NuxtOptions } from 'nuxt/schema'
|
||||
import type { RenderedModule } from 'rollup'
|
||||
import type { ViteBuildContext } from '../vite'
|
||||
|
||||
export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
|
||||
@ -13,14 +14,18 @@ export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
|
||||
{
|
||||
name: 'nuxt:analyze-minify',
|
||||
async generateBundle (_opts, outputBundle) {
|
||||
for (const [_bundleId, bundle] of Object.entries(outputBundle)) {
|
||||
for (const _bundleId in outputBundle) {
|
||||
const bundle = outputBundle[_bundleId]
|
||||
if (bundle.type !== 'chunk') { continue }
|
||||
const originalEntries = Object.entries(bundle.modules)
|
||||
const minifiedEntries = await Promise.all(originalEntries.map(async ([moduleId, module]) => {
|
||||
const { code } = await transform(module.code || '', { minify: true })
|
||||
return [moduleId, { ...module, code }]
|
||||
}))
|
||||
bundle.modules = Object.fromEntries(minifiedEntries)
|
||||
const minifiedModuleEntryPromises: Array<Promise<[string, RenderedModule]>> = []
|
||||
for (const moduleId in bundle.modules) {
|
||||
const module = bundle.modules[moduleId]
|
||||
minifiedModuleEntryPromises.push(
|
||||
transform(module.code || '', { minify: true })
|
||||
.then(result => [moduleId, { ...module, code: result.code }]),
|
||||
)
|
||||
}
|
||||
bundle.modules = Object.fromEntries(await Promise.all(minifiedModuleEntryPromises))
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -23,12 +23,15 @@ const SUPPORTED_EXT_RE = /\.(?:m?[jt]sx?|vue)/
|
||||
|
||||
export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => {
|
||||
const composableMeta: Record<string, any> = {}
|
||||
const composableLengths = new Set<number>()
|
||||
const keyedFunctions = new Set<string>()
|
||||
for (const { name, ...meta } of options.composables) {
|
||||
composableMeta[name] = meta
|
||||
keyedFunctions.add(name)
|
||||
composableLengths.add(meta.argumentLength)
|
||||
}
|
||||
|
||||
const maxLength = Math.max(...options.composables.map(({ argumentLength }) => argumentLength))
|
||||
const keyedFunctions = new Set(options.composables.map(({ name }) => name))
|
||||
const maxLength = Math.max(...composableLengths)
|
||||
const KEYED_FUNCTIONS_RE = new RegExp(`\\b(${[...keyedFunctions].map(f => escapeRE(f)).join('|')})\\b`)
|
||||
|
||||
return {
|
||||
|
@ -25,7 +25,7 @@ export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boole
|
||||
resolveId: {
|
||||
enforce: 'post',
|
||||
handler (id) {
|
||||
if (id === '/__skip_vite' || !id.startsWith('/') || id.startsWith('/@fs')) { return }
|
||||
if (id === '/__skip_vite' || id[0] !== '/' || id.startsWith('/@fs')) { return }
|
||||
|
||||
if (resolveFromPublicAssets(id)) {
|
||||
return PREFIX + encodeURIComponent(id)
|
||||
|
@ -66,12 +66,12 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
||||
const { files, inBundle } = cssMap[file]
|
||||
// File has been tree-shaken out of build (or there are no styles to inline)
|
||||
if (!files.length || !inBundle) { continue }
|
||||
|
||||
const fileName = filename(file)
|
||||
const base = typeof outputOptions.assetFileNames === 'string'
|
||||
? outputOptions.assetFileNames
|
||||
: outputOptions.assetFileNames({
|
||||
type: 'asset',
|
||||
name: `${filename(file)}-styles.mjs`,
|
||||
name: `${fileName}-styles.mjs`,
|
||||
source: '',
|
||||
})
|
||||
|
||||
@ -79,7 +79,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
||||
|
||||
emitted[file] = this.emitFile({
|
||||
type: 'asset',
|
||||
name: `${filename(file)}-styles.mjs`,
|
||||
name: `${fileName}-styles.mjs`,
|
||||
source: [
|
||||
...files.map((css, i) => `import style_${i} from './${relative(baseDir, this.getFileName(css))}';`),
|
||||
`export default [${files.map((_, i) => `style_${i}`).join(', ')}]`,
|
||||
|
@ -105,7 +105,7 @@ export async function buildServer (ctx: ViteBuildContext) {
|
||||
|
||||
if (!ctx.nuxt.options.dev) {
|
||||
const nitroDependencies = await tryResolveModule('nitropack/package.json', ctx.nuxt.options.modulesDir)
|
||||
.then(r => import(r!)).then(r => Object.keys(r.dependencies || {})).catch(() => [])
|
||||
.then(r => import(r!)).then(r => r.dependencies ? Object.keys(r.dependencies) : []).catch(() => [])
|
||||
if (Array.isArray(serverConfig.ssr!.external)) {
|
||||
serverConfig.ssr!.external.push(
|
||||
// explicit dependencies we use in our ssr renderer - these can be inlined (if necessary) in the nitro build
|
||||
|
@ -10,7 +10,7 @@ interface Envs {
|
||||
|
||||
export function transpile (envs: Envs): Array<string | RegExp> {
|
||||
const nuxt = useNuxt()
|
||||
const transpile = []
|
||||
const transpile: Array<string | RegExp> = []
|
||||
|
||||
for (let pattern of nuxt.options.build.transpile) {
|
||||
if (typeof pattern === 'function') {
|
||||
|
@ -4,6 +4,7 @@ import { dirname, join, normalize, resolve } from 'pathe'
|
||||
import type { Nuxt, NuxtBuilder, ViteConfig } from '@nuxt/schema'
|
||||
import { addVitePlugin, isIgnored, logger, resolvePath } from '@nuxt/kit'
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import type { RollupReplaceOptions } from '@rollup/plugin-replace'
|
||||
import { sanitizeFilePath } from 'mlly'
|
||||
import { withoutLeadingSlash } from 'ufo'
|
||||
import { filename } from 'pathe/utils'
|
||||
@ -102,10 +103,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
||||
rootDir: nuxt.options.rootDir,
|
||||
composables: nuxt.options.optimization.keyedComposables,
|
||||
}),
|
||||
replace({
|
||||
...Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`])),
|
||||
preventAssignment: true,
|
||||
}),
|
||||
replace({ preventAssignment: true, ...globalThisReplacements }),
|
||||
virtual(nuxt.vfs),
|
||||
],
|
||||
server: {
|
||||
@ -164,10 +162,16 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
||||
await nuxt.callHook('vite:extend', ctx)
|
||||
|
||||
nuxt.hook('vite:extendConfig', (config) => {
|
||||
config.plugins!.push(replace({
|
||||
preventAssignment: true,
|
||||
...Object.fromEntries(Object.entries(config.define!).filter(([key]) => key.startsWith('import.meta.'))),
|
||||
}))
|
||||
const replaceOptions: RollupReplaceOptions = Object.create(null)
|
||||
replaceOptions.preventAssignment = true
|
||||
|
||||
for (const key in config.define!) {
|
||||
if (key.startsWith('import.meta.')) {
|
||||
replaceOptions[key] = config.define![key]
|
||||
}
|
||||
}
|
||||
|
||||
config.plugins!.push(replace(replaceOptions))
|
||||
})
|
||||
|
||||
if (!ctx.nuxt.options.dev) {
|
||||
@ -224,3 +228,5 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
||||
await buildClient(ctx)
|
||||
await buildServer(ctx)
|
||||
}
|
||||
|
||||
const globalThisReplacements = Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`]))
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nuxt/webpack-builder",
|
||||
"version": "3.11.2",
|
||||
"version": "3.12.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/nuxt.git",
|
||||
@ -62,7 +62,7 @@
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-bundle-renderer": "^2.1.0",
|
||||
"vue-loader": "^17.4.2",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack": "^5.92.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-dev-middleware": "^7.2.1",
|
||||
"webpack-hot-middleware": "^2.26.1",
|
||||
|
@ -43,24 +43,19 @@ export default class VueSSRClientPlugin {
|
||||
|
||||
const allFiles = new Set<string>()
|
||||
const asyncFiles = new Set<string>()
|
||||
|
||||
for (const asset of stats.assets!) {
|
||||
const file = asset.name
|
||||
if (!isHotUpdate(file)) {
|
||||
allFiles.add(file)
|
||||
if (initialFiles.has(file)) { continue }
|
||||
if (isJS(file) || isCSS(file)) {
|
||||
asyncFiles.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assetsMapping: Record<string, string[]> = {}
|
||||
for (const { name, chunkNames = [] } of stats.assets!) {
|
||||
if (isJS(name) && !isHotUpdate(name)) {
|
||||
|
||||
for (const { name: file, chunkNames = [] } of stats.assets!) {
|
||||
if (isHotUpdate(file)) { continue }
|
||||
allFiles.add(file)
|
||||
const isFileJS = isJS(file)
|
||||
if (!initialFiles.has(file) && (isFileJS || isCSS(file))) {
|
||||
asyncFiles.add(file)
|
||||
}
|
||||
if (isFileJS) {
|
||||
const componentHash = hash(chunkNames.join('|'))
|
||||
const map = assetsMapping[componentHash] ||= []
|
||||
map.push(name)
|
||||
map.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
|
1327
pnpm-lock.yaml
1327
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,7 @@ for PKG in packages/* ; do
|
||||
if [[ $PKG == "packages/test-utils" ]] ; then
|
||||
continue
|
||||
fi
|
||||
if [[ $p == "packages/ui-templates" ]] ; then
|
||||
if [[ $PKG == "packages/ui-templates" ]] ; then
|
||||
continue
|
||||
fi
|
||||
pushd $PKG
|
||||
|
@ -13,9 +13,9 @@ async function main () {
|
||||
const commits = await getLatestCommits().then(commits => commits.filter(
|
||||
c => config.types[c.type] && !(c.type === 'chore' && c.scope === 'deps' && !c.isBreaking),
|
||||
))
|
||||
const bumpType = await determineBumpType()
|
||||
const bumpType = await determineBumpType() || 'patch'
|
||||
|
||||
const newVersion = inc(workspace.find('nuxt').data.version, bumpType || 'patch')
|
||||
const newVersion = inc(workspace.find('nuxt').data.version, bumpType)
|
||||
const changelog = await generateMarkDown(commits, config)
|
||||
|
||||
// Create and push a branch with bumped versions if it has not already been created
|
||||
@ -44,7 +44,8 @@ async function main () {
|
||||
changelog
|
||||
.replace(/^## v.*\n/, '')
|
||||
.replace(`...${releaseBranch}`, `...v${newVersion}`)
|
||||
.replace(/### ❤️ Contributors[\s\S]*$/, ''),
|
||||
.replace(/### ❤️ Contributors[\s\S]*$/, '')
|
||||
.replace(/[\n\r]+/g, '\n'),
|
||||
'### ❤️ Contributors',
|
||||
contributors.map(c => `- ${c.name} (@${c.username})`).join('\n'),
|
||||
].join('\n')
|
||||
|
@ -166,6 +166,21 @@ describe('pages', () => {
|
||||
expect(res.headers.get('x-extend')).toEqual('added in pages:extend')
|
||||
})
|
||||
|
||||
it('preserves page metadata added in pages:extend hook', async () => {
|
||||
const html = await $fetch<string>('/some-custom-path')
|
||||
expect (html.match(/<pre>([^<]*)<\/pre>/)?.[1]?.trim().replace(/"/g, '"').replace(/>/g, '>')).toMatchInlineSnapshot(`
|
||||
"{
|
||||
"name": "some-custom-name",
|
||||
"path": "/some-custom-path",
|
||||
"validate": "() => true",
|
||||
"middleware": [
|
||||
"() => true"
|
||||
],
|
||||
"otherValue": "{\\"foo\\":\\"bar\\"}"
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
it('validates routes', async () => {
|
||||
const { status, headers } = await fetch('/forbidden')
|
||||
expect(status).toEqual(404)
|
||||
@ -745,6 +760,24 @@ describe('nuxt links', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
it('respects external links in edge cases', async () => {
|
||||
const html = await $fetch<string>('/nuxt-link/custom-external')
|
||||
const hrefs = html.match(/<a[^>]*href="([^"]+)"/g)
|
||||
expect(hrefs).toMatchInlineSnapshot(`
|
||||
[
|
||||
"<a href="https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html"",
|
||||
"<a href="https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html"",
|
||||
"<a href="/missing-page/"",
|
||||
"<a href="/missing-page/"",
|
||||
]
|
||||
`)
|
||||
|
||||
const { page, consoleLogs } = await renderPage('/nuxt-link/custom-external')
|
||||
const warnings = consoleLogs.filter(c => c.text.includes('No match found for location'))
|
||||
expect(warnings).toMatchInlineSnapshot(`[]`)
|
||||
await page.close()
|
||||
})
|
||||
|
||||
it('preserves route state', async () => {
|
||||
const { page } = await renderPage('/nuxt-link/trailing-slash')
|
||||
|
||||
|
7
test/fixtures/basic-types/nuxt.config.ts
vendored
7
test/fixtures/basic-types/nuxt.config.ts
vendored
@ -21,6 +21,13 @@ export default defineNuxtConfig({
|
||||
title: Promise.resolve('Nuxt Fixture'),
|
||||
// @ts-expect-error Functions are not allowed
|
||||
titleTemplate: title => 'test',
|
||||
meta: [
|
||||
{
|
||||
// Allows unknown property
|
||||
property: 'og:thing',
|
||||
content: '1234567890',
|
||||
},
|
||||
],
|
||||
},
|
||||
pageTransition: {
|
||||
// @ts-expect-error Functions are not allowed
|
||||
|
2
test/fixtures/basic-types/package.json
vendored
2
test/fixtures/basic-types/package.json
vendored
@ -11,7 +11,7 @@
|
||||
"devDependencies": {
|
||||
"ofetch": "latest",
|
||||
"unplugin-vue-router": "^0.7.0",
|
||||
"vitest": "1.5.3",
|
||||
"vitest": "1.6.0",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest"
|
||||
}
|
||||
|
12
test/fixtures/basic-types/types.ts
vendored
12
test/fixtures/basic-types/types.ts
vendored
@ -4,6 +4,7 @@ import type { FetchError } from 'ofetch'
|
||||
import type { NavigationFailure, RouteLocationNormalized, RouteLocationRaw, Router, useRouter as vueUseRouter } from '#vue-router'
|
||||
|
||||
import type { AppConfig, RuntimeValue, UpperSnakeCase } from 'nuxt/schema'
|
||||
import { defineNuxtModule } from 'nuxt/kit'
|
||||
import { defineNuxtConfig } from 'nuxt/config'
|
||||
import { callWithNuxt, isVue3 } from '#app'
|
||||
import type { NuxtError } from '#app'
|
||||
@ -242,6 +243,17 @@ describe('modules', () => {
|
||||
// @ts-expect-error we want to ensure we throw type error on invalid key
|
||||
defineNuxtConfig({ undeclaredKey: { other: false } })
|
||||
})
|
||||
|
||||
it('preserves options in defineNuxtModule setup without `.with()`', () => {
|
||||
defineNuxtModule<{ foo?: string, baz: number }>({
|
||||
defaults: {
|
||||
baz: 100,
|
||||
},
|
||||
setup: (resolvedOptions) => {
|
||||
expectTypeOf(resolvedOptions).toEqualTypeOf<{ foo?: string, baz: number }>()
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('nuxtApp', () => {
|
||||
|
@ -16,9 +16,15 @@ export default defineNuxtModule({
|
||||
}, {
|
||||
path: '/big-page-1',
|
||||
file: resolver.resolve('./pages/big-page.vue'),
|
||||
meta: {
|
||||
layout: false,
|
||||
},
|
||||
}, {
|
||||
path: '/big-page-2',
|
||||
file: resolver.resolve('./pages/big-page.vue'),
|
||||
meta: {
|
||||
layout: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
|
21
test/fixtures/basic/nuxt.config.ts
vendored
21
test/fixtures/basic/nuxt.config.ts
vendored
@ -1,5 +1,6 @@
|
||||
import { addBuildPlugin, addComponent } from 'nuxt/kit'
|
||||
import type { NuxtPage } from 'nuxt/schema'
|
||||
import { defu } from 'defu'
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import { withoutLeadingSlash } from 'ufo'
|
||||
|
||||
@ -88,10 +89,17 @@ export default defineNuxtConfig({
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
needsFallback: undefined,
|
||||
testConfig: 123,
|
||||
},
|
||||
},
|
||||
modules: [
|
||||
function (_options, nuxt) {
|
||||
// ensure setting `runtimeConfig` also sets `nitro.runtimeConfig`
|
||||
nuxt.options.runtimeConfig = defu(nuxt.options.runtimeConfig, {
|
||||
public: {
|
||||
testConfig: 123,
|
||||
},
|
||||
})
|
||||
},
|
||||
function (_options, nuxt) {
|
||||
nuxt.hook('modules:done', () => {
|
||||
// @ts-expect-error not valid nuxt option
|
||||
@ -151,6 +159,17 @@ export default defineNuxtConfig({
|
||||
internalParent!.children = newPages
|
||||
})
|
||||
},
|
||||
function (_options, nuxt) {
|
||||
// to check that page metadata is preserved
|
||||
nuxt.hook('pages:extend', (pages) => {
|
||||
const customName = pages.find(page => page.name === 'some-custom-name')
|
||||
if (!customName) { throw new Error('Page with custom name not found') }
|
||||
if (customName.path !== '/some-custom-path') { throw new Error('Page path not extracted') }
|
||||
|
||||
customName.meta ||= {}
|
||||
customName.meta.someProp = true
|
||||
})
|
||||
},
|
||||
// To test falsy module values
|
||||
undefined,
|
||||
],
|
||||
|
36
test/fixtures/basic/pages/meta.vue
vendored
Normal file
36
test/fixtures/basic/pages/meta.vue
vendored
Normal 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>
|
53
test/fixtures/basic/pages/nuxt-link/custom-external.vue
vendored
Normal file
53
test/fixtures/basic/pages/nuxt-link/custom-external.vue
vendored
Normal 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>
|
9
test/fixtures/basic/pages/tsx-page-meta.vue
vendored
Normal file
9
test/fixtures/basic/pages/tsx-page-meta.vue
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<PageContent />
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
definePageMeta({})
|
||||
defineRouteRules({})
|
||||
const PageContent = () => (<div>Home Page</div>)
|
||||
</script>
|
@ -53,6 +53,7 @@ describe('app config', () => {
|
||||
describe('composables', () => {
|
||||
it('are all tested', () => {
|
||||
const testedComposables: string[] = [
|
||||
'useRouteAnnouncer',
|
||||
'clearNuxtData',
|
||||
'refreshNuxtData',
|
||||
'useAsyncData',
|
||||
|
Loading…
Reference in New Issue
Block a user