mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +00:00
Merge branch 'main' into feat/named_export_components
This commit is contained in:
commit
cf9f8ccf09
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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- 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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- 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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- 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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: corepack enable
|
||||
|
4
.github/workflows/check-links.yml
vendored
4
.github/workflows/check-links.yml
vendored
@ -25,10 +25,10 @@ jobs:
|
||||
restore-keys: cache-lychee-
|
||||
|
||||
# check links with Lychee
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- name: Lychee link checker
|
||||
uses: lycheeverse/lychee-action@054a8e8c7a88ada133165c6633a49825a32174e2 # for v1.8.0
|
||||
uses: lycheeverse/lychee-action@25a231001d1723960a301b7d4c82884dc7ef857d # for v1.8.0
|
||||
with:
|
||||
# arguments with file types to check
|
||||
args: >-
|
||||
|
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -72,7 +72,7 @@ jobs:
|
||||
- build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- 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@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4
|
||||
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
with:
|
||||
languages: javascript
|
||||
queries: +security-and-quality
|
||||
@ -95,7 +95,7 @@ jobs:
|
||||
path: packages
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4
|
||||
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
with:
|
||||
category: "/language:javascript"
|
||||
|
||||
@ -111,7 +111,7 @@ jobs:
|
||||
module: ["bundler", "node"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -142,7 +142,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -166,7 +166,7 @@ jobs:
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -218,7 +218,7 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
@ -246,9 +246,9 @@ jobs:
|
||||
TEST_CONTEXT: ${{ matrix.context }}
|
||||
TEST_V4: ${{ matrix.version == 'v4' }}
|
||||
TEST_PAYLOAD: ${{ matrix.payload }}
|
||||
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || runner.os == 'Windows' }}
|
||||
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || matrix.payload == 'js' || runner.os == 'Windows' }}
|
||||
|
||||
- uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # v4.3.1
|
||||
- uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4.4.1
|
||||
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@ -270,7 +270,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: corepack enable
|
||||
@ -309,7 +309,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2
|
||||
|
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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- 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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
|
||||
- name: Check workflow files
|
||||
run: |
|
||||
|
2
.github/workflows/nuxt2-edge.yml
vendored
2
.github/workflows/nuxt2-edge.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: '2.x'
|
||||
fetch-depth: 0 # All history
|
||||
|
4
.github/workflows/release-pr.yml
vendored
4
.github/workflows/release-pr.yml
vendored
@ -22,14 +22,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Ensure action is by maintainer
|
||||
uses: octokit/request-action@21d174fc38ff59af9cf4d7e07347d29df6dbaa99 # v2.3.0
|
||||
uses: octokit/request-action@872c5c97b3c85c23516a572f02b31401ef82415d # v2.3.1
|
||||
id: check_role
|
||||
with:
|
||||
route: GET /repos/nuxt/nuxt/collaborators/${{ github.event.comment.user.login }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.issue.number }}/merge
|
||||
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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
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@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp
|
||||
with:
|
||||
label: needs reproduction
|
||||
|
6
.github/workflows/scorecards.yml
vendored
6
.github/workflows/scorecards.yml
vendored
@ -32,12 +32,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
|
||||
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
@ -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@ccf74c947955fd1cf117aef6a0e4e66191ef6f61 # v3.25.4
|
||||
uses: github/codeql-action/upload-sarif@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
if: github.repository == 'nuxt/nuxt' && success()
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
@ -33,6 +33,7 @@ It provides a number of features that make it easy to build fast, SEO-friendly,
|
||||
- ❤️ [Contribute](#contribute)
|
||||
- 🏠 [Local Development](#local-development)
|
||||
- ⛰️ [Nuxt 2](#nuxt-2)
|
||||
- 🛟 [Professional Support](#professional-support)
|
||||
- 🔗 [Follow us](#follow-us)
|
||||
- ⚖️ [License](#license)
|
||||
|
||||
@ -104,6 +105,13 @@ Follow the docs to [Set Up Your Local Development Environment](https://nuxt.com/
|
||||
|
||||
You can find the code for Nuxt 2 on the [`2.x` branch](https://github.com/nuxt/nuxt/tree/2.x) and the documentation at [v2.nuxt.com](https://v2.nuxt.com).
|
||||
|
||||
If you expect to be using Nuxt 2 beyond the EOL (End of Life) date (June 30, 2024), and still need a maintained version that can satisfy security and browser compatibility requirements, make sure to check out [HeroDevs’ NES (Never-Ending Support) Nuxt 2](https://www.herodevs.com/support/nuxt-nes?utm_source=nuxt-github&utm_medium=nuxt-readme).
|
||||
|
||||
## <a name="professional-support">🛟 Professional Support</a>
|
||||
|
||||
- Technical audit & consulting: [Nuxt Experts](https://nuxt.com/enterprise/support)
|
||||
- Custom development & more: [Nuxt Agencies Partners](https://nuxt.com/enterprise/agencies)
|
||||
|
||||
## <a name="follow-us">🔗 Follow us</a>
|
||||
|
||||
<p valign="center">
|
||||
|
@ -10,6 +10,10 @@ If you are a module author, you can find more specific information in the [Modul
|
||||
|
||||
Nuxt offers first-class support for end-to-end and unit testing of your Nuxt application via `@nuxt/test-utils`, a library of test utilities and configuration that currently powers the [tests we use on Nuxt itself](https://github.com/nuxt/nuxt/tree/main/test) and tests throughout the module ecosystem.
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=yGzwk9xi9gU" target="_blank"}
|
||||
Watch a video from Alexander Lichter about getting started with the `@nuxt/test-utils`.
|
||||
::
|
||||
|
||||
## Installation
|
||||
|
||||
In order to allow you to manage your other testing dependencies, `@nuxt/test-utils` ships with various optional peer dependencies. For example:
|
||||
@ -160,21 +164,32 @@ export default defineVitestConfig({
|
||||
`mountSuspended` allows you to mount any Vue component within the Nuxt environment, allowing async setup and access to injections from your Nuxt plugins. For example:
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { it, expect } from 'vitest'
|
||||
declare const SomeComponent: Component
|
||||
declare const App: Component
|
||||
import type { Component } from 'vue'
|
||||
declare module '#components' {
|
||||
export const SomeComponent: Component
|
||||
}
|
||||
// ---cut---
|
||||
// tests/components/SomeComponents.nuxt.spec.ts
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||
import { SomeComponent } from '#components'
|
||||
|
||||
it('can mount some component', async () => {
|
||||
const component = await mountSuspended(SomeComponent)
|
||||
expect(component.text()).toMatchInlineSnapshot(
|
||||
'This is an auto-imported component'
|
||||
'"This is an auto-imported component"'
|
||||
)
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
```ts twoslash
|
||||
import { it, expect } from 'vitest'
|
||||
// ---cut---
|
||||
// tests/components/SomeComponents.nuxt.spec.ts
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||
import App from '~/app.vue'
|
||||
|
||||
// tests/App.nuxt.spec.ts
|
||||
it('can also mount an app', async () => {
|
||||
const component = await mountSuspended(App, { route: '/test' })
|
||||
@ -199,13 +214,15 @@ The passed in component will be rendered inside a `<div id="test-wrapper"></div>
|
||||
Examples:
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { it, expect } from 'vitest'
|
||||
declare const SomeComponent: Component
|
||||
declare const App: Component
|
||||
import type { Component } from 'vue'
|
||||
declare module '#components' {
|
||||
export const SomeComponent: Component
|
||||
}
|
||||
// ---cut---
|
||||
// tests/components/SomeComponents.nuxt.spec.ts
|
||||
import { renderSuspended } from '@nuxt/test-utils/runtime'
|
||||
import { SomeComponent } from '#components'
|
||||
import { screen } from '@testing-library/vue'
|
||||
|
||||
it('can render some component', async () => {
|
||||
@ -215,13 +232,11 @@ it('can render some component', async () => {
|
||||
```
|
||||
|
||||
```ts twoslash
|
||||
import type { Component } from 'vue'
|
||||
import { it, expect } from 'vitest'
|
||||
declare const SomeComponent: Component
|
||||
declare const App: Component
|
||||
// ---cut---
|
||||
// tests/App.nuxt.spec.ts
|
||||
import { renderSuspended } from '@nuxt/test-utils/runtime'
|
||||
import App from '~/app.vue'
|
||||
|
||||
it('can also render an app', async () => {
|
||||
const html = await renderSuspended(App, { route: '/test' })
|
||||
@ -358,7 +373,7 @@ registerEndpoint('/test/', {
|
||||
})
|
||||
```
|
||||
|
||||
> **Note**: If your requests in a component go to external API, you can use `baseURL` and then make it empty using Nuxt Environment Config (`$test`) so all your requests will go to Nitro server.
|
||||
> **Note**: If your requests in a component go to an external API, you can use `baseURL` and then make it empty using [Nuxt Environment Override Config](/docs/getting-started/configuration#environment-overrides) (`$test`) so all your requests will go to Nitro server.
|
||||
|
||||
#### Conflict with End-To-End Testing
|
||||
|
||||
|
@ -25,13 +25,17 @@ Nuxt 4 is planned to be released **on or before June 14** (though obviously this
|
||||
|
||||
Until then, it is possible to test many of Nuxt 4's breaking changes on 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.
|
||||
::
|
||||
|
||||
### Opting in to Nuxt 4
|
||||
|
||||
First, opt in to the nightly release channel [following these steps](/docs/guide/going-further/nightly-release-channel#opting-in).
|
||||
|
||||
Then you can set your `compatibilityVersion` to match Nuxt 4 behavior:
|
||||
|
||||
```ts
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
future: {
|
||||
compatibilityVersion: 4,
|
||||
@ -42,7 +46,9 @@ export default defineNuxtConfig({
|
||||
// app: 'app'
|
||||
// },
|
||||
// experimental: {
|
||||
// sharedPrerenderData: false,
|
||||
// compileTemplate: true,
|
||||
// resetAsyncDataToUndefined: true,
|
||||
// templateUtils: true,
|
||||
// relativeWatchPaths: true,
|
||||
// defaults: {
|
||||
@ -100,6 +106,7 @@ app/
|
||||
pages/
|
||||
plugins/
|
||||
utils/
|
||||
app.config.ts
|
||||
app.vue
|
||||
router.options.ts
|
||||
modules/
|
||||
@ -126,12 +133,12 @@ nuxt.config.ts
|
||||
##### Migration Steps
|
||||
|
||||
1. Create a new directory called `app/`.
|
||||
1. Move your `assets/`, `components/`, `composables/`, `layouts/`, `middleware/`, `pages/`, `plugins/` and `utils/` folders under it, as well as `app.vue`, `error.vue`. If you have an `app/router-options.ts` or `app/spa-loading-template.html`, these paths remain the same.
|
||||
1. Move your `assets/`, `components/`, `composables/`, `layouts/`, `middleware/`, `pages/`, `plugins/` and `utils/` folders under it, as well as `app.vue`, `error.vue`, `app.config.ts`. If you have an `app/router-options.ts` or `app/spa-loading-template.html`, these paths remain the same.
|
||||
1. Make sure your `nuxt.config.ts`, `modules/`, `public/` and `server/` folders remain outside the `app/` folder, in the root of your project.
|
||||
|
||||
However, migration is _not required_. If you wish to keep your current folder structure, Nuxt should auto-detect it. (If it does not, please raise an issue.) You can also force a v3 folder structure with the following configuration:
|
||||
|
||||
```ts
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
// This reverts the new srcDir default from `app` back to your root directory
|
||||
srcDir: '.',
|
||||
@ -142,6 +149,104 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
#### Shared Prerender Data
|
||||
|
||||
🚦 **Impact Level**: Medium
|
||||
|
||||
##### What Changed
|
||||
|
||||
We enabled a previously experimental feature to share data from `useAsyncData` and `useFetch` calls, across different pages. See [original PR](https://github.com/nuxt/nuxt/pull/24894).
|
||||
|
||||
##### Reasons for Change
|
||||
|
||||
This feature automatically shares payload _data_ between pages that are prerendered. This can result in a significant performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and fetch the same data in different pages.
|
||||
|
||||
For example, if your site requires a `useFetch` call for every page (for example, to get navigation data for a menu, or site settings from a CMS), this data would only be fetched once when prerendering the first page that uses it, and then cached for use when prerendering other pages.
|
||||
|
||||
##### Migration Steps
|
||||
|
||||
Make sure that any unique key of your data is always resolvable to the same data. For example, if you are using `useAsyncData` to fetch data related to a particular page, you should provide a key that uniquely matches that data. (`useFetch` should do this automatically for you.)
|
||||
|
||||
```ts [app/pages/test/[slug\\].vue]
|
||||
// This would be unsafe in a dynamic page (e.g. `[slug].vue`) because the route slug makes a difference
|
||||
// to the data fetched, but Nuxt can't know that because it's not reflected in the key.
|
||||
const route = useRoute()
|
||||
const { data } = await useAsyncData(async () => {
|
||||
return await $fetch(`/api/my-page/${route.params.slug}`)
|
||||
})
|
||||
// Instead, you should use a key that uniquely identifies the data fetched.
|
||||
const { data } = await useAsyncData(route.params.slug, async () => {
|
||||
return await $fetch(`/api/my-page/${route.params.slug}`)
|
||||
})
|
||||
```
|
||||
|
||||
Alternatively, you can disable this feature with:
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
sharedPrerenderData: false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Default `data` and `error` values in `useAsyncData` and `useFetch`
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
|
||||
##### What Changed
|
||||
|
||||
`data` and `error` objects returned from `useAsyncData` will now default to `undefined`.
|
||||
|
||||
##### Reasons for Change
|
||||
|
||||
Previously `data` was initialized to `null` but reset in `clearNuxtData` to `undefined`. `error` was initialized to `null`. This change is to bring greater consistency.
|
||||
|
||||
##### Migration Steps
|
||||
|
||||
If you encounter any issues you can revert back to the previous behavior with:
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
defaults: {
|
||||
useAsyncData: {
|
||||
value: 'null',
|
||||
errorValue: 'null'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Please report an issue if you are doing this, as we do not plan to keep this as configurable.
|
||||
|
||||
#### Respect defaults when clearing `data` in `useAsyncData` and `useFetch`
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
|
||||
##### What Changed
|
||||
|
||||
If you provide a custom `default` value for `useAsyncData`, this will now be used when calling `clear` or `clearNuxtData` and it will be reset to its default value rather than simply unset.
|
||||
|
||||
##### Reasons for Change
|
||||
|
||||
Often users set an appropriately empty value, such as an empty array, to avoid the need to check for `null`/`undefined` when iterating over it. This should be respected when resetting/clearing the data.
|
||||
|
||||
##### Migration Steps
|
||||
|
||||
If you encounter any issues you can revert back to the previous behavior, for now, with:
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
resetAsyncDataToUndefined: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Please report an issue if you are doing so, as we do not plan to keep this as configurable.
|
||||
|
||||
#### Shallow Data Reactivity in `useAsyncData` and `useFetch`
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
@ -166,7 +271,7 @@ In most cases, no migration steps are required, but if you rely on the reactivit
|
||||
+ const { data } = useFetch('/api/test', { deep: true })
|
||||
```
|
||||
1. You can change the default behavior on a project-wide basis (not recommended):
|
||||
```ts
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
defaults: {
|
||||
@ -205,6 +310,34 @@ However, if you are a module author using the `builder:watch` hook and wishing t
|
||||
})
|
||||
```
|
||||
|
||||
#### Directory index scanning
|
||||
|
||||
🚦 **Impact Level**: Medium
|
||||
|
||||
##### What Changed
|
||||
|
||||
Child folders in your `middleware/` folder are also scanned for `index` files and these are now also registered as middleware in your project.
|
||||
|
||||
##### Reasons for Change
|
||||
|
||||
Nuxt scans a number of folders automatically, including `middleware/` and `plugins/`.
|
||||
|
||||
Child folders in your `plugins/` folder are scanned for `index` files and we wanted to make this behavior consistent between scanned directories.
|
||||
|
||||
##### Migration Steps
|
||||
|
||||
Probably no migration is necessary but if you wish to revert to previous behavior you can add a hook to filter out these middleware:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
hooks: {
|
||||
'app:resolve'(app) {
|
||||
app.middleware = app.middleware.filter(mw => !/\/index\.[^/]+$/.test(mw.path))
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Template Compilation Changes
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
@ -265,6 +398,29 @@ const importSources = (sources: string | string[], { lazy = false } = {}) => {
|
||||
const importName = genSafeVariableName
|
||||
```
|
||||
|
||||
#### Removal of Experimental Features
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
|
||||
##### What Changed
|
||||
|
||||
Four experimental features are no longer configurable in Nuxt 4:
|
||||
|
||||
* `treeshakeClientOnly` will be `true` (default since v3.0)
|
||||
* `configSchema` will be `true` (default since v3.3)
|
||||
* `polyfillVueUseHead` will be `false` (default since v3.4)
|
||||
* `respectNoSSRHeader` will be `false` (default since v3.4)
|
||||
|
||||
##### Reasons for Change
|
||||
|
||||
These options have been set to their current values for some time and we do not have a reason to believe that they need to remain configurable.
|
||||
|
||||
##### Migration Steps
|
||||
|
||||
* `polyfillVueUseHead` is implementable in user-land with [this plugin](https://github.com/nuxt/nuxt/blob/f209158352b09d1986aa320e29ff36353b91c358/packages/nuxt/src/head/runtime/plugins/vueuse-head-polyfill.ts#L10-L11)
|
||||
|
||||
* `respectNoSSRHeader`is implementable in user-land with [server middleware](https://github.com/nuxt/nuxt/blob/c660b39447f0d5b8790c0826092638d321cd6821/packages/nuxt/src/core/runtime/nitro/no-ssr.ts#L8-L9)
|
||||
|
||||
## Nuxt 2 vs Nuxt 3
|
||||
|
||||
In the table below, there is a quick comparison between 3 versions of Nuxt:
|
||||
@ -272,7 +428,7 @@ In the table below, there is a quick comparison between 3 versions of Nuxt:
|
||||
Feature / Version | Nuxt 2 | Nuxt Bridge | Nuxt 3
|
||||
-------------------------|-----------------|------------------|---------
|
||||
Vue | 2 | 2 | 3
|
||||
Stability | 😊 Stable | 😌 Semi-stable | 😊 Stable
|
||||
Stability | 😊 Stable | 😊 Stable | 😊 Stable
|
||||
Performance | 🏎 Fast | ✈️ Faster | 🚀 Fastest
|
||||
Nitro Engine | ❌ | ✅ | ✅
|
||||
ESM support | 🌙 Partial | 👍 Better | ✅
|
||||
|
@ -46,6 +46,10 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=DFZI2iVCrNc" target="_blank"}
|
||||
Watch a video from Alexander Lichter about the env-aware `nuxt.config.ts`.
|
||||
::
|
||||
|
||||
::note
|
||||
If you're authoring layers, you can also use the `$meta` key to provide metadata that you or the consumers of your layer might use.
|
||||
::
|
||||
|
@ -54,6 +54,10 @@ const { data: count } = await useFetch('/api/count')
|
||||
|
||||
This composable is a wrapper around the [`useAsyncData`](/docs/api/composables/use-async-data) composable and `$fetch` utility.
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=njsGVmcWviY" target="_blank"}
|
||||
Watch the video from Alexander Lichter to avoid using `useFetch` the wrong way!
|
||||
::
|
||||
|
||||
:read-more{to="/docs/api/composables/use-fetch"}
|
||||
|
||||
:link-example{to="/docs/examples/features/data-fetching"}
|
||||
@ -93,6 +97,10 @@ The `useAsyncData` composable is responsible for wrapping async logic and return
|
||||
It's developer experience sugar for the most common use case.
|
||||
::
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=0X-aOpSGabA" target="_blank"}
|
||||
Watch a video from Alexander Lichter to dig deeper into the difference between `useFetch` and `useAsyncData`.
|
||||
::
|
||||
|
||||
There are some cases when using the [`useFetch`](/docs/api/composables/use-fetch) composable is not appropriate, for example when a CMS or a third-party provide their own query layer. In this case, you can use [`useAsyncData`](/docs/api/composables/use-async-data) to wrap your calls and still keep the benefits provided by the composable.
|
||||
|
||||
```vue [pages/users.vue]
|
||||
|
@ -8,6 +8,10 @@ Nuxt provides the [`useState`](/docs/api/composables/use-state) composable to cr
|
||||
|
||||
[`useState`](/docs/api/composables/use-state) is an SSR-friendly [`ref`](https://vuejs.org/api/reactivity-core.html#ref) replacement. Its value will be preserved after server-side rendering (during client-side hydration) and shared across all components using a unique key.
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=mv0WcBABcIk" target="_blank"}
|
||||
Watch a video from Alexander Lichter about why and when to use `useState()`.
|
||||
::
|
||||
|
||||
::important
|
||||
Because the data inside [`useState`](/docs/api/composables/use-state) will be serialized to JSON, it is important that it does not contain anything that cannot be serialized, such as classes, functions or symbols.
|
||||
::
|
||||
@ -208,6 +212,10 @@ const color = useColor() // Same as useState('color')
|
||||
</template>
|
||||
```
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=dZSNW07sO-A" target="_blank"}
|
||||
Watch a video from Daniel Roe on how to deal with global state and SSR in Nuxt.
|
||||
::
|
||||
|
||||
## Using third-party libraries
|
||||
|
||||
Nuxt **used to rely** on the Vuex library to provide global state management. If you are migrating from Nuxt 2, please head to [the migration guide](/docs/migration/configuration#vuex).
|
||||
|
@ -20,6 +20,10 @@ Using Nitro gives Nuxt superpowers:
|
||||
|
||||
Nitro is internally using [h3](https://github.com/unjs/h3), a minimal H(TTP) framework built for high performance and portability.
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=DkvgJa-X31k" target="_blank"}
|
||||
Watch a video from Alexander Lichter to understand the responsibilities of Nuxt and Nitro in your application.
|
||||
::
|
||||
|
||||
## Server Endpoints & Middleware
|
||||
|
||||
You can easily manage the server-only part of your Nuxt app, from API endpoints to middleware.
|
||||
|
@ -51,7 +51,7 @@ Read more about layers in the **Layer Author Guide**.
|
||||
Watch a video from Learn Vue about Nuxt Layers.
|
||||
::
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=fr5yo3aVkfA&t=271s" target="_blank"}
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=fr5yo3aVkfA" target="_blank"}
|
||||
Watch a video from Alexander Lichter about Nuxt Layers.
|
||||
::
|
||||
|
||||
|
@ -16,4 +16,7 @@ surround: false
|
||||
::card{icon="i-ph-star-duotone" title="Going Further" to="/docs/guide/going-further"}
|
||||
Master Nuxt with advanced concepts like experimental features, hooks, modules, and more.
|
||||
::
|
||||
::card{icon="i-ph-book-open-duotone" title="Recipes" to="/docs/guide/recipes"}
|
||||
Find solutions to common problems and learn how to implement them in your Nuxt project.
|
||||
::
|
||||
::
|
||||
|
@ -60,6 +60,10 @@ That means that (with very few exceptions) you cannot use them outside a Nuxt pl
|
||||
|
||||
If you get an error message like `Nuxt instance is unavailable` then it probably means you are calling a Nuxt composable in the wrong place in the Vue or Nuxt lifecycle.
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=ofuKRZLtOdY" target="_blank"}
|
||||
Watch a video from Alexander Lichter about handling async code in composables and fixing `Nuxt instance is unavailable` in your app.
|
||||
::
|
||||
|
||||
::read-more{to="/docs/guide/going-further/experimental-features#asynccontext" icon="i-ph-star-duotone"}
|
||||
Checkout the `asyncContext` experimental feature to use Nuxt composables in async functions.
|
||||
::
|
||||
@ -168,3 +172,7 @@ export default defineNuxtConfig({
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=FT2LQJ2NvVI" target="_blank"}
|
||||
Watch a video from Alexander Lichter on how to easily set up custom auto imports.
|
||||
::
|
||||
|
@ -65,6 +65,10 @@ This file contains the recommended basic TypeScript configuration for your proje
|
||||
|
||||
[Read more about how to extend this configuration](/docs/guide/directory-structure/tsconfig).
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://youtu.be/umLI7SlPygY" target="_blank"}
|
||||
Watch a video from Daniel Roe explaining built-in Nuxt aliases.
|
||||
::
|
||||
|
||||
::note
|
||||
Nitro also [auto-generates types](/docs/guide/concepts/server-engine#typed-api-routes) for API routes. Plus, Nuxt also generates types for globally available components and [auto-imports from your composables](/docs/guide/directory-structure/composables), plus other core functionality.
|
||||
::
|
||||
|
@ -259,7 +259,7 @@ Watch Learn Vue video about Nuxt Server Components.
|
||||
::
|
||||
|
||||
::tip{icon="i-ph-article-duotone" to="https://roe.dev/blog/nuxt-server-components" target="_blank"}
|
||||
Read Daniel Roe's guide to Nuxt server components
|
||||
Read Daniel Roe's guide to Nuxt Server Components.
|
||||
::
|
||||
|
||||
### Standalone server components
|
||||
|
@ -46,7 +46,11 @@ export default defineEventHandler(() => {
|
||||
|
||||
When starting Nuxt, the `hello` module will be registered and the `/api/hello` route will be available.
|
||||
|
||||
Local modules are registered in alphabetical order. You can change the order by adding a number to the front of each directory name:
|
||||
Modules are executed in the following sequence:
|
||||
- First, the modules defined in [`nuxt.config.ts`](/docs/api/nuxt-config#modules-1) are loaded.
|
||||
- Then, modules found in the `modules/` directory are executed, and they load in alphabetical order.
|
||||
|
||||
You can change the order of local module by adding a number to the front of each directory name:
|
||||
|
||||
```bash [Directory structure]
|
||||
modules/
|
||||
|
@ -76,6 +76,10 @@ export default defineNuxtPlugin({
|
||||
})
|
||||
```
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=2aXZyXB1QGQ" target="_blank"}
|
||||
Watch a video from Alexander Lichter about the Object Syntax for Nuxt plugins.
|
||||
::
|
||||
|
||||
::note
|
||||
If you are using the object-syntax, the properties may be statically analyzed in future to produce a more optimized build. So you should not define them at runtime. :br
|
||||
For example, setting `enforce: import.meta.server ? 'pre' : 'post'` would defeat any future optimization Nuxt is able to do for your plugins.
|
||||
|
@ -33,11 +33,25 @@ npx nuxi dev --dotenv .env.local
|
||||
|
||||
When updating `.env` in development mode, the Nuxt instance is automatically restarted to apply new values to the `process.env`.
|
||||
|
||||
## Production Preview
|
||||
## Production
|
||||
|
||||
**After your server is built**, you are responsible for setting environment variables when you run the server.
|
||||
|
||||
Your `.env` file will not be read at this point. How you do this is different for every environment.
|
||||
Your `.env` files will not be read at this point. How you do this is different for every environment.
|
||||
|
||||
This design decision was made to ensure compatibility across various deployment environments, some of which may not have a traditional file system available, such as serverless platforms or edge networks like Cloudflare Workers.
|
||||
|
||||
Since `.env` files are not used in production, you must explicitly set environment variables using the tools and methods provided by your hosting environment. Here are some common approaches:
|
||||
|
||||
* You can pass the environment variables as arguments using the terminal:
|
||||
|
||||
`$ DATABASE_HOST=mydatabaseconnectionstring node .output/server/index.mjs`
|
||||
|
||||
* You can set environment variables in shell configuration files like `.bashrc` or `.profile`.
|
||||
|
||||
* Many cloud service providers, such as Vercel, Netlify, and AWS, provide interfaces for setting environment variables via their dashboards, CLI tools or configuration files.
|
||||
|
||||
## Production Preview
|
||||
|
||||
For local production preview purpose, we recommend using [`nuxi preview`](/docs/api/commands/preview) since using this command, the `.env` file will be loaded into `process.env` for convenience. Note that this command requires dependencies to be installed in the package directory.
|
||||
|
||||
|
@ -306,6 +306,10 @@ Out of the box, this will enable typed usage of [`navigateTo`](/docs/api/utils/n
|
||||
|
||||
You can even get typed params within a page by using `const route = useRoute('route-name')`.
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=SXk-L19gTZk" target="_blank"}
|
||||
Watch a video from Daniel Roe explaining type-safe routing in Nuxt.
|
||||
::
|
||||
|
||||
## watcher
|
||||
|
||||
Set an alternative watcher that will be used as the watching service for Nuxt.
|
||||
@ -340,6 +344,10 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=1jUupYHVvrU" target="_blank"}
|
||||
Watch a video from Alexander Lichter about the experimental `sharedPrerenderData` setting.
|
||||
::
|
||||
|
||||
It is particularly important when enabling this feature to make sure that any unique key of your data
|
||||
is always resolvable to the same data. For example, if you are using `useAsyncData` to fetch
|
||||
data related to a particular page, you should provide a key that uniquely matches that data. (`useFetch`
|
||||
@ -378,6 +386,16 @@ This option allows exposing some route metadata defined in `definePageMeta` at b
|
||||
|
||||
This only works with static or strings/arrays rather than variables or conditional assignment. See [original issue](https://github.com/nuxt/nuxt/issues/24770) for more information and context.
|
||||
|
||||
<!-- You can disable this feature if it causes issues in your project.
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
scanPageMeta: false
|
||||
}
|
||||
})
|
||||
``` -->
|
||||
|
||||
## cookieStore
|
||||
|
||||
Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
|
||||
|
@ -61,6 +61,10 @@ Setting the default of `runtimeConfig` values to *differently named environment
|
||||
It is advised to use environment variables that match the structure of your `runtimeConfig` object.
|
||||
::
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://youtu.be/_FYV5WfiWvs" target="_blank"}
|
||||
Watch a video from Alexander Lichter showcasing the top mistake developers make using runtimeConfig.
|
||||
::
|
||||
|
||||
#### Example
|
||||
|
||||
```sh [.env]
|
||||
|
@ -15,6 +15,10 @@ Discover all Nuxt Kit utilities.
|
||||
|
||||
You can install the latest Nuxt Kit by adding it to the `dependencies` section of your `package.json`. However, please consider always explicitly installing the `@nuxt/kit` package even if it is already installed by Nuxt.
|
||||
|
||||
::note
|
||||
`@nuxt/kit` and `@nuxt/schema` are key dependencies for Nuxt. If you are installing it separately, make sure that the versions of `@nuxt/kit` and `@nuxt/schema` are equal to or greater than your `nuxt` version to avoid any unexpected behavior.
|
||||
::
|
||||
|
||||
```json [package.json]
|
||||
{
|
||||
"dependencies": {
|
||||
|
105
docs/2.guide/4.recipes/3.custom-usefetch.md
Normal file
105
docs/2.guide/4.recipes/3.custom-usefetch.md
Normal file
@ -0,0 +1,105 @@
|
||||
---
|
||||
navigation.title: 'Custom useFetch'
|
||||
title: Custom useFetch in Nuxt
|
||||
description: How to create a custom fetcher for calling your external API in Nuxt 3.
|
||||
---
|
||||
|
||||
When working with Nuxt, you might be making the frontend and fetching an external API, and you might want to set some default options for fetching from your API.
|
||||
|
||||
The [`$fetch`](/docs/api/utils/dollarfetch) utility function (used by the [`useFetch`](/docs/api/composables/use-fetch) composable) is intentionally not globally configurable. This is important so that fetching behavior throughout your application remains consistent, and other integrations (like modules) can rely on the behavior of core utilities like `$fetch`.
|
||||
|
||||
However, Nuxt provides a way to create a custom fetcher for your API (or multiple fetchers if you have multiple APIs to call).
|
||||
|
||||
## Custom `$fetch`
|
||||
|
||||
Let's create a custom `$fetch` instance with a [Nuxt plugin](/docs/guide/directory-structure/plugins).
|
||||
|
||||
::note
|
||||
`$fetch` is a configured instance of [ofetch](https://github.com/unjs/ofetch) which supports adding the base URL of your Nuxt server as well as direct function calls during SSR (avoiding HTTP roundtrips).
|
||||
::
|
||||
|
||||
Let's pretend here that:
|
||||
- The main API is https://api.nuxt.com
|
||||
- We are storing the JWT token in a session with [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils)
|
||||
- If the API responds with a `401` status code, we redirect the user to the `/login` page
|
||||
|
||||
```ts [plugins/api.ts]
|
||||
export default defineNuxtPlugin(() => {
|
||||
const { session } = useUserSession()
|
||||
|
||||
const api = $fetch.create({
|
||||
baseURL: 'https://api.nuxt.com',
|
||||
onRequest({ request, options, error }) {
|
||||
if (session.value?.token) {
|
||||
const headers = options.headers ||= {}
|
||||
if (Array.isArray(headers)) {
|
||||
headers.push(['Authorization', `Bearer ${session.value?.token}`])
|
||||
} else if (headers instanceof Headers) {
|
||||
headers.set('Authorization', `Bearer ${session.value?.token}`)
|
||||
} else {
|
||||
headers.Authorization = `Bearer ${session.value?.token}`
|
||||
}
|
||||
}
|
||||
},
|
||||
async onResponseError({ response }) {
|
||||
if (response.status === 401) {
|
||||
await navigateTo('/login')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Expose to useNuxtApp().$api
|
||||
return {
|
||||
provide: {
|
||||
api
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
With this Nuxt plugin, `$api` is exposed from `useNuxtApp()` to make API calls directly from the Vue components:
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup>
|
||||
const { $api } = useNuxtApp()
|
||||
const { data: modules } = await useAsyncData('modules', () => $api('/modules'))
|
||||
</script>
|
||||
```
|
||||
|
||||
::callout
|
||||
Wrapping with [`useAsyncData`](/docs/api/composables/use-async-data) **avoid double data fetching when doing server-side rendering** (server & client on hydration).
|
||||
::
|
||||
|
||||
## Custom `useFetch`
|
||||
|
||||
Now that `$api` has the logic we want, let's create a `useAPI` composable to replace the usage of `useAsyncData` + `$api`:
|
||||
|
||||
```ts [composables/useAPI.ts]
|
||||
import type { UseFetchOptions } from 'nuxt/app'
|
||||
|
||||
export function useAPI<T>(
|
||||
url: string | (() => string),
|
||||
options: Omit<UseFetchOptions<T>, 'default'> & { default: () => T | Ref<T> },
|
||||
) {
|
||||
return useFetch(url, {
|
||||
...options,
|
||||
$fetch: useNuxtApp().$api
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Let's use the new composable and have a nice and clean component:
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup>
|
||||
const { data: modules } = await useAPI('/modules')
|
||||
</script>
|
||||
```
|
||||
|
||||
::callout{icon="i-simple-icons-youtube" color="red" to="https://www.youtube.com/watch?v=jXH8Tr-exhI"}
|
||||
Watch a video about custom `$fetch` and Repository Pattern in Nuxt.
|
||||
::
|
||||
|
||||
::note
|
||||
We are currently discussing to find a cleaner way to let you create a custom fetcher, see https://github.com/nuxt/nuxt/issues/14736.
|
||||
::
|
@ -25,6 +25,22 @@ In this example, we use `<NuxtLink>` component to link to another page of the ap
|
||||
</template>
|
||||
```
|
||||
|
||||
### Passing Params to Dynamic Routes
|
||||
|
||||
In this example, we pass the `id` param to link to the route `~/pages/posts/[id].vue`.
|
||||
|
||||
```vue [pages/index.vue]
|
||||
<template>
|
||||
<NuxtLink :to="{ name: 'posts-id', params: { id: 123 } }">
|
||||
Post 123
|
||||
</NuxtLink>
|
||||
</template>
|
||||
```
|
||||
|
||||
::tip
|
||||
Check out the Pages panel in Nuxt DevTools to see the route name and the params it might take.
|
||||
::
|
||||
|
||||
### Handling 404s
|
||||
|
||||
When using `<NuxtLink>` for `/public` directory files or when pointing to a different app on the same domain, you should use the `external` prop.
|
||||
|
@ -30,6 +30,7 @@ You can pass custom HTML or components through the loading indicator's default s
|
||||
## Props
|
||||
|
||||
- `color`: The color of the loading bar. It can be set to `false` to turn off explicit color styling.
|
||||
- `errorColor`: The color of the loading bar when `error` is set to `true`.
|
||||
- `height`: Height of the loading bar, in pixels (default `3`).
|
||||
- `duration`: Duration of the loading bar, in milliseconds (default `2000`).
|
||||
- `throttle`: Throttle the appearing and hiding, in milliseconds (default `200`).
|
||||
|
@ -59,13 +59,10 @@ Use these options to set the expiration of the cookie.
|
||||
The given number will be converted to an integer by rounding down. By default, no maximum age is set.
|
||||
|
||||
`expires`: Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.1).
|
||||
By default, no expiration is set. Most clients will consider this a "non-persistent cookie" and
|
||||
will delete it on a condition like exiting a web browser application.
|
||||
By default, no expiration is set. Most clients will consider this a "non-persistent cookie" and will delete it on a condition like exiting a web browser application.
|
||||
|
||||
::note
|
||||
The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and
|
||||
`maxAge` is set, then `maxAge` takes precedence, but not all clients may obey this,
|
||||
so if both are set, they should point to the same date and time!
|
||||
The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and `maxAge` is set, then `maxAge` takes precedence, but not all clients may obey this, so if both are set, they should point to the same date and time!
|
||||
::
|
||||
|
||||
::note
|
||||
@ -74,22 +71,29 @@ If neither of `expires` and `maxAge` is set, the cookie will be session-only and
|
||||
|
||||
### `httpOnly`
|
||||
|
||||
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy,
|
||||
the `HttpOnly` attribute is set; otherwise it is not. By default, the `HttpOnly` attribute is not set.
|
||||
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy, the `HttpOnly` attribute is set; otherwise it is not. By default, the `HttpOnly` attribute is not set.
|
||||
|
||||
::warning
|
||||
Be careful when setting this to `true`, as compliant clients will not allow client-side
|
||||
JavaScript to see the cookie in `document.cookie`.
|
||||
Be careful when setting this to `true`, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
|
||||
::
|
||||
|
||||
### `secure`
|
||||
|
||||
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy,
|
||||
the `Secure` attribute is set; otherwise it is not. By default, the `Secure` attribute is not set.
|
||||
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy, the `Secure` attribute is set; otherwise it is not. By default, the `Secure` attribute is not set.
|
||||
|
||||
::warning
|
||||
Be careful when setting this to `true`, as compliant clients will not send the cookie back to
|
||||
the server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
|
||||
Be careful when setting this to `true`, as compliant clients will not send the cookie back to the server in the future if the browser does not have an HTTPS connection. This can lead to hydration errors.
|
||||
::
|
||||
|
||||
### `partitioned`
|
||||
|
||||
Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1) attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the `Partitioned` attribute is not set.
|
||||
|
||||
::note
|
||||
This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||
This also means many clients may ignore this attribute until they understand it.
|
||||
|
||||
More information can be found in the [proposal](https://github.com/privacycg/CHIPS).
|
||||
::
|
||||
|
||||
### `domain`
|
||||
@ -114,23 +118,18 @@ More information about the different enforcement levels can be found in [the spe
|
||||
|
||||
### `encode`
|
||||
|
||||
Specifies a function that will be used to encode a cookie's value. Since the value of a cookie
|
||||
has a limited character set (and must be a simple string), this function can be used to encode
|
||||
a value into a string suited for a cookie's value.
|
||||
Specifies a function that will be used to encode a cookie's value. Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to encode a value into a string suited for a cookie's value.
|
||||
|
||||
The default encoder is the `JSON.stringify` + `encodeURIComponent`.
|
||||
|
||||
### `decode`
|
||||
|
||||
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie
|
||||
has a limited character set (and must be a simple string), this function can be used to decode
|
||||
a previously encoded cookie value into a JavaScript string or other object.
|
||||
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to decode a previously encoded cookie value into a JavaScript string or other object.
|
||||
|
||||
The default decoder is `decodeURIComponent` + [destr](https://github.com/unjs/destr).
|
||||
|
||||
::note
|
||||
If an error is thrown from this function, the original, non-decoded cookie value will
|
||||
be returned as the cookie's value.
|
||||
If an error is thrown from this function, the original, non-decoded cookie value will be returned as the cookie's value.
|
||||
::
|
||||
|
||||
### `default`
|
||||
|
@ -66,6 +66,10 @@ const { data, pending, error, refresh } = await useFetch('/api/auth/login', {
|
||||
`useFetch` is a reserved function name transformed by the compiler, so you should not name your own function `useFetch`.
|
||||
::
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=njsGVmcWviY" target="_blank"}
|
||||
Watch the video from Alexander Lichter to avoid using `useFetch` the wrong way!
|
||||
::
|
||||
|
||||
:link-example{to="/docs/examples/advanced/use-custom-fetch-composable"}
|
||||
|
||||
:read-more{to="/docs/getting-started/data-fetching"}
|
||||
@ -83,6 +87,8 @@ const { data, pending, error, refresh } = await useFetch('/api/auth/login', {
|
||||
- `headers`: Request headers.
|
||||
- `baseURL`: Base URL for the request.
|
||||
- `timeout`: Milliseconds to automatically abort request
|
||||
- `cache`: Handles cache control according to [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/fetch#cache)
|
||||
- You can pass boolean to disable the cache or you can pass one of the following values: `default`, `no-store`, `reload`, `no-cache`, `force-cache`, and `only-if-cached`.
|
||||
|
||||
::note
|
||||
All fetch options can be given a `computed` or `ref` value. These will be watched and new requests made automatically with any new values if they are updated.
|
||||
|
@ -26,6 +26,11 @@ It hooks into [`page:loading:start`](/docs/api/advanced/hooks#app-hooks-runtime)
|
||||
- **type**: `Ref<boolean>`
|
||||
- **description**: The loading state
|
||||
|
||||
### `error`
|
||||
|
||||
- **type**: `Ref<boolean>`
|
||||
- **description**: The error state
|
||||
|
||||
### `progress`
|
||||
|
||||
- **type**: `Ref<number>`
|
||||
@ -39,7 +44,7 @@ Set `isLoading` to true and start to increase the `progress` value.
|
||||
|
||||
### `finish()`
|
||||
|
||||
Set the `progress` value to `100`, stop all timers and intervals then reset the loading state `500` ms later. `finish` accepts a `{ force: true }` option to skip the interval before the state is reset.
|
||||
Set the `progress` value to `100`, stop all timers and intervals then reset the loading state `500` ms later. `finish` accepts a `{ force: true }` option to skip the interval before the state is reset, and `{ error: true }` to change the loading bar color and set the error property to true.
|
||||
|
||||
### `clear()`
|
||||
|
||||
|
@ -138,6 +138,10 @@ Nuxt exposes the following properties through `ssrContext`:
|
||||
|
||||
Since [Nuxt v3.4](https://nuxt.com/blog/v3-4#payload-enhancements), it is possible to define your own reducer/reviver for types that are not supported by Nuxt.
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=8w6ffRBs8a4" target="_blank"}
|
||||
Watch a video from Alexander Lichter about serializing payloads, especially with regards to classes.
|
||||
::
|
||||
|
||||
In the example below, we define a reducer (or a serializer) and a reviver (or deserializer) for the [Luxon](https://moment.github.io/luxon/#/) DateTime class, using a payload plugin.
|
||||
|
||||
```ts [plugins/date-time-payload.ts]
|
||||
|
@ -25,6 +25,10 @@ Because the data inside `useState` will be serialized to JSON, it is important t
|
||||
`useState` is a reserved function name transformed by the compiler, so you should not name your own function `useState`.
|
||||
::
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=mv0WcBABcIk" target="_blank"}
|
||||
Watch a video from Alexander Lichter about why and when to use `useState()`.
|
||||
::
|
||||
|
||||
## Using `shallowRef`
|
||||
|
||||
If you don't need your state to be deeply reactive, you can combine `useState` with [`shallowRef`](https://vuejs.org/api/reactivity-advanced.html#shallowref). This can improve performance when your state contains large objects and arrays.
|
||||
|
@ -32,6 +32,10 @@ We'll do our best to follow our [internal issue decision making flowchart](https
|
||||
|
||||
### Send a Pull Request
|
||||
|
||||
::Tip
|
||||
On windows, you need to clone the repository with `git clone -c core.symlinks=true https://github.com/nuxt/nuxt.git` to make symlinks work.
|
||||
::
|
||||
|
||||
We always welcome pull requests! ❤️
|
||||
|
||||
#### Before You Start
|
||||
|
@ -39,11 +39,11 @@ Translations | - | [nuxt/translations#4](https://github.com/nuxt/tra
|
||||
In addition to the Nuxt framework, there are modules that are vital for the ecosystem. Their status will be updated below.
|
||||
|
||||
Module | Status | Nuxt Support | Repository | Description
|
||||
---------------|---------------------|--------------|------------|-------------------
|
||||
Scripts | April 2024 | 3.x | `nuxt/scripts` to be announced | Easy 3rd party script management. [nuxt/nuxt#22016](https://github.com/nuxt/nuxt/discussions/22016)
|
||||
------------------------------------|---------------------|--------------|------------|-------------------
|
||||
[Scripts](https://scripts.nuxt.com) | Public Preview | 3.x | [nuxt/scripts](https://github.com/nuxt/scripts) | Easy 3rd party script management.
|
||||
A11y | Planned | 3.x | `nuxt/a11y` to be announced | Accessibility hinting and utilities [nuxt/nuxt#23255](https://github.com/nuxt/nuxt/issues/23255)
|
||||
Auth | Planned | 3.x | `nuxt/auth` to be announced | Nuxt 3 support is planned after session support
|
||||
Hints | Planned | 3.x | `nuxt/hints` to be announced | Guidance and suggestions for enhancing development practices
|
||||
Auth | Planned | 3.x | `nuxt/auth` to be announced | Nuxt 3 support is planned after session support.
|
||||
Hints | Planned | 3.x | `nuxt/hints` to be announced | Guidance and suggestions for enhancing development practices.
|
||||
|
||||
## Release Cycle
|
||||
|
||||
@ -64,9 +64,9 @@ Each active version has its own nightly releases which are generated automatical
|
||||
Release | | Initial release | End Of Life | Docs
|
||||
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
|
||||
**4.x** (scheduled) | | 2024 Q2 | |
|
||||
**3.x** (stable) | <a href="https://npmjs.com/package/nuxt"><img alt="Nuxt latest 3.x version" src="https://flat.badgen.net/npm/v/nuxt?label="></a> | 2022-11-16 | TBA | [nuxt.com](/docs)
|
||||
**2.x** (maintenance) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 2.x version" src="https://flat.badgen.net/npm/v/nuxt/2x?label="></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs)
|
||||
**1.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 1.x version" src="https://flat.badgen.net/npm/v/nuxt/1x?label="></a> | 2018-01-08 | 2019-09-21 |
|
||||
**3.x** (stable) | <a href="https://npmjs.com/package/nuxt"><img alt="Nuxt latest 3.x version" src="https://flat.badgen.net/npm/v/nuxt?label=" class="not-prose"></a> | 2022-11-16 | TBA | [nuxt.com](/docs)
|
||||
**2.x** (maintenance) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 2.x version" src="https://flat.badgen.net/npm/v/nuxt/2x?label=" class="not-prose"></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs)
|
||||
**1.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 1.x version" src="https://flat.badgen.net/npm/v/nuxt/1x?label=" class="not-prose"></a> | 2018-01-08 | 2019-09-21 |
|
||||
|
||||
### Support Status
|
||||
|
||||
|
@ -68,6 +68,16 @@ navigation.icon: i-ph-notification-duotone
|
||||
::card
|
||||
---
|
||||
icon: i-simple-icons-github
|
||||
title: nuxt/scripts
|
||||
to: https://github.com/nuxt/scripts/tags
|
||||
target: _blank
|
||||
ui.icon.base: text-black dark:text-white
|
||||
---
|
||||
Nuxt Scripts releases. (Public Preview)
|
||||
::
|
||||
::card
|
||||
---
|
||||
icon: i-simple-icons-github
|
||||
title: nuxt/ui
|
||||
to: https://github.com/nuxt/ui/releases
|
||||
target: _blank
|
||||
|
31
package.json
31
package.json
@ -20,6 +20,7 @@
|
||||
"lint:knip": "pnpx knip",
|
||||
"play": "nuxi dev playground",
|
||||
"play:build": "nuxi build playground",
|
||||
"play:generate": "nuxi generate playground",
|
||||
"play:preview": "nuxi preview playground",
|
||||
"test": "pnpm test:fixtures && pnpm test:fixtures:dev && pnpm test:fixtures:webpack && pnpm test:unit && pnpm test:runtime && pnpm test:types && pnpm typecheck",
|
||||
"test:prepare": "jiti ./test/prepare.ts",
|
||||
@ -40,20 +41,20 @@
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"magic-string": "^0.30.10",
|
||||
"nuxt": "workspace:*",
|
||||
"rollup": "^4.17.2",
|
||||
"vite": "5.2.11",
|
||||
"rollup": "^4.18.0",
|
||||
"vite": "5.2.12",
|
||||
"vue": "3.4.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.2.0",
|
||||
"@nuxt/eslint-config": "0.3.12",
|
||||
"@eslint/js": "9.3.0",
|
||||
"@nuxt/eslint-config": "0.3.13",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@nuxt/test-utils": "3.12.1",
|
||||
"@nuxt/test-utils": "3.13.1",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@testing-library/vue": "8.0.3",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/eslint__js": "8.42.3",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/node": "20.12.11",
|
||||
"@types/node": "20.12.13",
|
||||
"@types/semver": "7.5.8",
|
||||
"@vitest/coverage-v8": "1.6.0",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
@ -61,25 +62,25 @@
|
||||
"changelogen": "0.5.5",
|
||||
"consola": "3.2.3",
|
||||
"devalue": "5.0.0",
|
||||
"eslint": "9.2.0",
|
||||
"eslint": "9.3.0",
|
||||
"eslint-plugin-no-only-tests": "3.1.0",
|
||||
"eslint-plugin-perfectionist": "2.10.0",
|
||||
"eslint-typegen": "0.2.4",
|
||||
"execa": "9.0.1",
|
||||
"execa": "9.1.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"globby": "14.0.1",
|
||||
"h3": "1.11.1",
|
||||
"happy-dom": "14.10.1",
|
||||
"happy-dom": "14.12.0",
|
||||
"jiti": "1.21.0",
|
||||
"markdownlint-cli": "0.40.0",
|
||||
"markdownlint-cli": "0.41.0",
|
||||
"nitropack": "2.9.6",
|
||||
"nuxi": "3.11.1",
|
||||
"nuxt": "workspace:*",
|
||||
"nuxt-content-twoslash": "0.0.10",
|
||||
"ofetch": "1.3.4",
|
||||
"pathe": "1.1.2",
|
||||
"playwright-core": "1.44.0",
|
||||
"rimraf": "5.0.5",
|
||||
"playwright-core": "1.44.1",
|
||||
"rimraf": "5.0.7",
|
||||
"semver": "7.6.2",
|
||||
"std-env": "3.7.0",
|
||||
"typescript": "5.4.5",
|
||||
@ -88,9 +89,9 @@
|
||||
"vitest-environment-nuxt": "1.0.0",
|
||||
"vue": "3.4.27",
|
||||
"vue-router": "4.3.2",
|
||||
"vue-tsc": "2.0.16"
|
||||
"vue-tsc": "2.0.19"
|
||||
},
|
||||
"packageManager": "pnpm@9.1.0",
|
||||
"packageManager": "pnpm@9.1.3",
|
||||
"engines": {
|
||||
"node": "^16.10.0 || >=18.0.0"
|
||||
},
|
||||
|
@ -44,7 +44,7 @@
|
||||
"semver": "^7.6.2",
|
||||
"ufo": "^1.5.3",
|
||||
"unctx": "^2.3.1",
|
||||
"unimport": "^3.7.1",
|
||||
"unimport": "^3.7.2",
|
||||
"untyped": "^1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -54,7 +54,7 @@
|
||||
"lodash-es": "4.17.21",
|
||||
"nitropack": "2.9.6",
|
||||
"unbuild": "latest",
|
||||
"vite": "5.2.11",
|
||||
"vite": "5.2.12",
|
||||
"vitest": "1.6.0",
|
||||
"webpack": "5.91.0"
|
||||
},
|
||||
|
@ -4,7 +4,7 @@ import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/sch
|
||||
import { useNuxt } from './context'
|
||||
|
||||
export function normalizeSemanticVersion (version: string) {
|
||||
return version.replace(/-[0-9]+\.[0-9a-f]+/, '') // Remove edge prefix
|
||||
return version.replace(/-\d+\.[0-9a-f]+/, '') // Remove edge prefix
|
||||
}
|
||||
|
||||
const builderMap = {
|
||||
|
@ -27,7 +27,7 @@ export async function compileTemplate<T> (template: NuxtTemplate<T>, ctx: any) {
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"{(.+)}"(?=,?$)/gm, r => JSON.parse(r).replace(/^{(.*)}$/, '$1'))
|
||||
const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"\{(.+)\}"(?=,?$)/gm, r => JSON.parse(r).replace(/^\{(.*)\}$/, '$1'))
|
||||
|
||||
/** @deprecated */
|
||||
const importSources = (sources: string | string[], { lazy = false } = {}) => {
|
||||
|
@ -4,6 +4,8 @@ import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
|
||||
import { loadConfig } from 'c12'
|
||||
import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
|
||||
import { NuxtConfigSchema } from '@nuxt/schema'
|
||||
import { globby } from 'globby'
|
||||
import defu from 'defu'
|
||||
|
||||
export interface LoadNuxtConfigOptions extends LoadConfigOptions<NuxtConfig> {}
|
||||
|
||||
@ -11,12 +13,19 @@ const layerSchemaKeys = ['future', 'srcDir', 'rootDir', 'dir']
|
||||
const layerSchema = Object.fromEntries(Object.entries(NuxtConfigSchema).filter(([key]) => layerSchemaKeys.includes(key)))
|
||||
|
||||
export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> {
|
||||
// Automatically detect and import layers from `~~/layers/` directory
|
||||
opts.overrides = defu(opts.overrides, {
|
||||
_extends: await globby('layers/*', {
|
||||
onlyDirectories: true,
|
||||
cwd: opts.cwd || process.cwd(),
|
||||
}),
|
||||
});
|
||||
(globalThis as any).defineNuxtConfig = (c: any) => c
|
||||
const result = await loadConfig<NuxtConfig>({
|
||||
name: 'nuxt',
|
||||
configFile: 'nuxt.config',
|
||||
rcFile: '.nuxtrc',
|
||||
extend: { extendKey: ['theme', 'extends'] },
|
||||
extend: { extendKey: ['theme', 'extends', '_extends'] },
|
||||
dotenv: true,
|
||||
globalRc: true,
|
||||
...opts,
|
||||
|
@ -73,8 +73,8 @@ export function defineNuxtModule<OptionsT extends ModuleOptions> (definition: Mo
|
||||
const key = `nuxt:module:${uniqueKey || (Math.round(Math.random() * 10000))}`
|
||||
const mark = performance.mark(key)
|
||||
const res = await module.setup?.call(null as any, _options, nuxt) ?? {}
|
||||
const perf = performance.measure(key, mark?.name) // TODO: remove when Node 14 reaches EOL
|
||||
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
|
||||
const perf = performance.measure(key, mark.name)
|
||||
const setupTime = Math.round((perf.duration * 100)) / 100
|
||||
|
||||
// Measure setup time
|
||||
if (setupTime > 5000 && uniqueKey !== '@nuxt/telemetry') {
|
||||
|
@ -3,7 +3,6 @@ import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema'
|
||||
import { useNuxt } from './context'
|
||||
import { addTemplate } from './template'
|
||||
import { resolveAlias } from './resolve'
|
||||
import { logger } from './logger'
|
||||
|
||||
/**
|
||||
* Normalize a nuxt plugin object
|
||||
@ -20,12 +19,6 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
|
||||
throw new Error('Invalid plugin. src option is required: ' + JSON.stringify(plugin))
|
||||
}
|
||||
|
||||
// TODO: only scan top-level files #18418
|
||||
const nonTopLevelPlugin = plugin.src.match(/\/plugins\/[^/]+\/index\.[^/]+$/i)
|
||||
if (nonTopLevelPlugin && nonTopLevelPlugin.length > 0 && !useNuxt().options.plugins.find(i => (typeof i === 'string' ? i : i.src).endsWith(nonTopLevelPlugin[0]))) {
|
||||
logger.warn(`[deprecation] You are using a plugin that is within a subfolder of your plugins directory without adding it to your config explicitly. You can move it to the top-level plugins directory, or include the file '~${nonTopLevelPlugin[0]}' in your plugins config (https://nuxt.com/docs/api/nuxt-config#plugins-1) to remove this warning.`)
|
||||
}
|
||||
|
||||
// Normalize full path to plugin
|
||||
plugin.src = normalize(resolveAlias(plugin.src))
|
||||
|
||||
|
@ -36,12 +36,12 @@ export function updateRuntimeConfig (runtimeConfig: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* https://github.com/unjs/nitro/blob/main/src/runtime/utils.env.ts.
|
||||
*
|
||||
*
|
||||
* These utils will be replaced by util exposed from nitropack. See https://github.com/unjs/nitro/pull/2404
|
||||
* for more context and future plans.)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
type EnvOptions = {
|
||||
@ -94,7 +94,7 @@ function applyEnv (
|
||||
return obj
|
||||
}
|
||||
|
||||
const envExpandRx = /{{(.*?)}}/g
|
||||
const envExpandRx = /\{\{(.*?)\}\}/g
|
||||
|
||||
function _expandFromEnv (value: string, env: Record<string, any> = process.env) {
|
||||
return value.replace(envExpandRx, (match, key) => {
|
||||
|
@ -202,7 +202,7 @@ export async function _generateTypes (nuxt: Nuxt) {
|
||||
} else {
|
||||
const path = stats?.isFile()
|
||||
// remove extension
|
||||
? relativePath.replace(/(?<=\w)\.\w+$/g, '')
|
||||
? relativePath.replace(/\b\.\w+$/g, '')
|
||||
// non-existent file probably shouldn't be resolved
|
||||
: aliases[alias]
|
||||
|
||||
@ -230,7 +230,7 @@ export async function _generateTypes (nuxt: Nuxt) {
|
||||
tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => {
|
||||
if (!isAbsolute(path)) { return path }
|
||||
const stats = await fsp.stat(path).catch(() => null /* file does not exist */)
|
||||
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */ : path)
|
||||
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/\b\.\w+$/g, '') /* remove extension */ : path)
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -60,14 +60,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/devalue": "^2.0.2",
|
||||
"@nuxt/devtools": "^1.2.0",
|
||||
"@nuxt/devtools": "^1.3.2",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@nuxt/telemetry": "^2.5.4",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@unhead/dom": "^1.9.10",
|
||||
"@unhead/ssr": "^1.9.10",
|
||||
"@unhead/vue": "^1.9.10",
|
||||
"@unhead/dom": "^1.9.11",
|
||||
"@unhead/ssr": "^1.9.11",
|
||||
"@unhead/vue": "^1.9.11",
|
||||
"@vue/shared": "^3.4.27",
|
||||
"acorn": "8.11.3",
|
||||
"c12": "^1.10.0",
|
||||
@ -76,7 +76,7 @@
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3",
|
||||
"devalue": "^5.0.0",
|
||||
"esbuild": "^0.21.1",
|
||||
"esbuild": "^0.21.4",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
@ -99,6 +99,7 @@
|
||||
"pkg-types": "^1.1.1",
|
||||
"radix3": "^1.1.2",
|
||||
"scule": "^1.3.0",
|
||||
"semver": "^7.6.2",
|
||||
"std-env": "^3.7.0",
|
||||
"strip-literal": "^2.1.0",
|
||||
"ufo": "^1.5.3",
|
||||
@ -106,7 +107,7 @@
|
||||
"uncrypto": "^0.1.3",
|
||||
"unctx": "^2.3.1",
|
||||
"unenv": "^1.9.0",
|
||||
"unimport": "^3.7.1",
|
||||
"unimport": "^3.7.2",
|
||||
"unplugin": "^1.10.1",
|
||||
"unplugin-vue-router": "^0.7.0",
|
||||
"unstorage": "^1.10.2",
|
||||
@ -117,13 +118,13 @@
|
||||
"vue-router": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/ui-templates": "1.3.3",
|
||||
"@nuxt/ui-templates": "1.3.4",
|
||||
"@parcel/watcher": "2.4.1",
|
||||
"@types/estree": "1.0.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"unbuild": "latest",
|
||||
"vite": "5.2.11",
|
||||
"vite": "5.2.12",
|
||||
"vitest": "1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -20,20 +20,24 @@ export default defineComponent({
|
||||
type: [String, Boolean],
|
||||
default: 'repeating-linear-gradient(to right,#00dc82 0%,#34cdfe 50%,#0047e1 100%)',
|
||||
},
|
||||
errorColor: {
|
||||
type: String,
|
||||
default: 'repeating-linear-gradient(to right,#f87171 0%,#ef4444 100%)',
|
||||
},
|
||||
estimatedProgress: {
|
||||
type: Function as unknown as () => (duration: number, elapsed: number) => number,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
setup (props, { slots, expose }) {
|
||||
const { progress, isLoading, start, finish, clear } = useLoadingIndicator({
|
||||
const { progress, isLoading, error, start, finish, clear } = useLoadingIndicator({
|
||||
duration: props.duration,
|
||||
throttle: props.throttle,
|
||||
estimatedProgress: props.estimatedProgress,
|
||||
})
|
||||
|
||||
expose({
|
||||
progress, isLoading, start, finish, clear,
|
||||
progress, isLoading, error, start, finish, clear,
|
||||
})
|
||||
|
||||
return () => h('div', {
|
||||
@ -47,7 +51,7 @@ export default defineComponent({
|
||||
width: 'auto',
|
||||
height: `${props.height}px`,
|
||||
opacity: isLoading.value ? 1 : 0,
|
||||
background: props.color || undefined,
|
||||
background: error.value ? props.errorColor : props.color || undefined,
|
||||
backgroundSize: `${(100 / progress.value) * 100}% auto`,
|
||||
transform: `scaleX(${progress.value}%)`,
|
||||
transformOrigin: 'left',
|
||||
|
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<Suspense @resolve="onResolve">
|
||||
<div v-if="abortRender" />
|
||||
<ErrorComponent
|
||||
v-if="error"
|
||||
v-else-if="error"
|
||||
:error="error"
|
||||
/>
|
||||
<IslandRenderer
|
||||
@ -53,6 +54,8 @@ if (import.meta.dev && results && results.some(i => i && 'then' in i)) {
|
||||
|
||||
// error handling
|
||||
const error = useError()
|
||||
// render an empty <div> when plugins have thrown an error but we're not yet rendering the error page
|
||||
const abortRender = import.meta.server && error.value && !nuxtApp.ssrContext.error
|
||||
onErrorCaptured((err, target, info) => {
|
||||
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
|
||||
if (import.meta.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {
|
||||
|
@ -8,7 +8,10 @@ import { createError } from './error'
|
||||
import { onNuxtReady } from './ready'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { asyncDataDefaults } from '#build/nuxt.config.mjs'
|
||||
import { asyncDataDefaults, resetAsyncDataToUndefined } from '#build/nuxt.config.mjs'
|
||||
|
||||
// TODO: temporary module for backwards compatibility
|
||||
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
|
||||
|
||||
export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
|
||||
|
||||
@ -42,7 +45,7 @@ export interface AsyncDataOptions<
|
||||
ResT,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> {
|
||||
/**
|
||||
* Whether to fetch on the server side.
|
||||
@ -117,7 +120,7 @@ export interface _AsyncData<DataT, ErrorT> {
|
||||
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
|
||||
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
|
||||
clear: () => void
|
||||
error: Ref<ErrorT | null>
|
||||
error: Ref<ErrorT | DefaultAsyncDataErrorValue>
|
||||
status: Ref<AsyncDataRequestStatus>
|
||||
}
|
||||
|
||||
@ -138,11 +141,11 @@ export function useAsyncData<
|
||||
NuxtErrorDataT = unknown,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
|
||||
/**
|
||||
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
|
||||
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
|
||||
@ -158,7 +161,7 @@ export function useAsyncData<
|
||||
> (
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
|
||||
/**
|
||||
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
|
||||
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
|
||||
@ -171,12 +174,12 @@ export function useAsyncData<
|
||||
NuxtErrorDataT = unknown,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
key: string,
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
|
||||
/**
|
||||
* Provides access to data that resolves asynchronously in an SSR-friendly composable.
|
||||
* See {@link https://nuxt.com/docs/api/composables/use-async-data}
|
||||
@ -194,14 +197,14 @@ export function useAsyncData<
|
||||
key: string,
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue>
|
||||
export function useAsyncData<
|
||||
ResT,
|
||||
NuxtErrorDataT = unknown,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | null> {
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys>, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>) | DefaultAsyncDataErrorValue> {
|
||||
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||
|
||||
@ -226,14 +229,14 @@ export function useAsyncData<
|
||||
const value = nuxtApp.ssrContext!._sharedPrerenderCache!.get(key)
|
||||
if (value) { return value as Promise<ResT> }
|
||||
|
||||
const promise = nuxtApp.runWithContext(_handler)
|
||||
const promise = Promise.resolve().then(() => nuxtApp.runWithContext(_handler))
|
||||
|
||||
nuxtApp.ssrContext!._sharedPrerenderCache!.set(key, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
// Used to get default values
|
||||
const getDefault = () => null
|
||||
const getDefault = () => asyncDataDefaults.value
|
||||
const getDefaultCachedData = () => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key]
|
||||
|
||||
// Apply defaults
|
||||
@ -250,11 +253,12 @@ export function useAsyncData<
|
||||
console.warn('[nuxt] `boolean` values are deprecated for the `dedupe` option of `useAsyncData` and will be removed in the future. Use \'cancel\' or \'defer\' instead.')
|
||||
}
|
||||
|
||||
// TODO: make more precise when v4 lands
|
||||
const hasCachedData = () => options.getCachedData!(key, nuxtApp) != null
|
||||
|
||||
// Create or use a shared asyncData entity
|
||||
if (!nuxtApp._asyncData[key] || !options.immediate) {
|
||||
nuxtApp.payload._errors[key] ??= null
|
||||
nuxtApp.payload._errors[key] ??= asyncDataDefaults.errorValue
|
||||
|
||||
const _ref = options.deep ? ref : shallowRef
|
||||
|
||||
@ -263,11 +267,15 @@ export function useAsyncData<
|
||||
pending: ref(!hasCachedData()),
|
||||
error: toRef(nuxtApp.payload._errors, key),
|
||||
status: ref('idle'),
|
||||
_default: options.default!,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Else, somehow check for conflicting keys with different defaults or fetcher
|
||||
const asyncData = { ...nuxtApp._asyncData[key] } as AsyncData<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
|
||||
const asyncData = { ...nuxtApp._asyncData[key] } as { _default?: unknown } & AsyncData<DataT | DefaultT, (NuxtErrorDataT extends Error | NuxtError ? NuxtErrorDataT : NuxtError<NuxtErrorDataT>)>
|
||||
|
||||
// Don't expose default function to end user
|
||||
delete asyncData._default
|
||||
|
||||
asyncData.refresh = asyncData.execute = (opts = {}) => {
|
||||
if (nuxtApp._asyncDataPromises[key]) {
|
||||
@ -307,7 +315,7 @@ export function useAsyncData<
|
||||
nuxtApp.payload.data[key] = result
|
||||
|
||||
asyncData.data.value = result
|
||||
asyncData.error.value = null
|
||||
asyncData.error.value = asyncDataDefaults.errorValue
|
||||
asyncData.status.value = 'success'
|
||||
})
|
||||
.catch((error: any) => {
|
||||
@ -404,11 +412,11 @@ export function useLazyAsyncData<
|
||||
DataE = Error,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
|
||||
export function useLazyAsyncData<
|
||||
ResT,
|
||||
DataE = Error,
|
||||
@ -418,18 +426,18 @@ export function useLazyAsyncData<
|
||||
> (
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
|
||||
export function useLazyAsyncData<
|
||||
ResT,
|
||||
DataE = Error,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
key: string,
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
|
||||
export function useLazyAsyncData<
|
||||
ResT,
|
||||
DataE = Error,
|
||||
@ -440,15 +448,15 @@ export function useLazyAsyncData<
|
||||
key: string,
|
||||
handler: (ctx?: NuxtApp) => Promise<ResT>,
|
||||
options?: Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue>
|
||||
|
||||
export function useLazyAsyncData<
|
||||
ResT,
|
||||
DataE = Error,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | null> {
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (...args: any[]): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, DataE | DefaultAsyncDataValue> {
|
||||
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||
const [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
|
||||
@ -463,12 +471,12 @@ export function useLazyAsyncData<
|
||||
}
|
||||
|
||||
/** @since 3.1.0 */
|
||||
export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | null> } {
|
||||
export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | DefaultAsyncDataValue> } {
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
// Initialize value when key is not already set
|
||||
if (!(key in nuxtApp.payload.data)) {
|
||||
nuxtApp.payload.data[key] = null
|
||||
nuxtApp.payload.data[key] = asyncDataDefaults.value
|
||||
}
|
||||
|
||||
return {
|
||||
@ -520,12 +528,12 @@ function clearNuxtDataByKey (nuxtApp: NuxtApp, key: string): void {
|
||||
}
|
||||
|
||||
if (key in nuxtApp.payload._errors) {
|
||||
nuxtApp.payload._errors[key] = null
|
||||
nuxtApp.payload._errors[key] = asyncDataDefaults.errorValue
|
||||
}
|
||||
|
||||
if (nuxtApp._asyncData[key]) {
|
||||
nuxtApp._asyncData[key]!.data.value = undefined
|
||||
nuxtApp._asyncData[key]!.error.value = null
|
||||
nuxtApp._asyncData[key]!.data.value = resetAsyncDataToUndefined ? undefined : nuxtApp._asyncData[key]!._default()
|
||||
nuxtApp._asyncData[key]!.error.value = asyncDataDefaults.errorValue
|
||||
nuxtApp._asyncData[key]!.pending.value = false
|
||||
nuxtApp._asyncData[key]!.status.value = 'idle'
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import { toRef } from 'vue'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
import { useRouter } from './router'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { nuxtDefaultErrorValue } from '#build/nuxt.config.mjs'
|
||||
|
||||
export const NUXT_ERROR_SIGNATURE = '__nuxt_error'
|
||||
|
||||
/** @since 3.0.0 */
|
||||
@ -47,7 +50,7 @@ export const clearError = async (options: { redirect?: string } = {}) => {
|
||||
await useRouter().replace(options.redirect)
|
||||
}
|
||||
|
||||
error.value = null
|
||||
error.value = nuxtDefaultErrorValue
|
||||
}
|
||||
|
||||
/** @since 3.0.0 */
|
||||
|
@ -8,6 +8,9 @@ import { useRequestFetch } from './ssr'
|
||||
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData'
|
||||
import { useAsyncData } from './asyncData'
|
||||
|
||||
// TODO: temporary module for backwards compatibility
|
||||
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { fetchDefaults } from '#build/nuxt.config.mjs'
|
||||
|
||||
@ -30,7 +33,7 @@ export interface UseFetchOptions<
|
||||
ResT,
|
||||
DataT = ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
R extends NitroFetchRequest = string & {},
|
||||
M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>,
|
||||
> extends Omit<AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>, 'watch'>, ComputedFetchOptions<R, M> {
|
||||
@ -54,11 +57,11 @@ export function useFetch<
|
||||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
|
||||
DataT = _ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
|
||||
/**
|
||||
* Fetch data from an API endpoint with an SSR-friendly composable.
|
||||
* See {@link https://nuxt.com/docs/api/composables/use-fetch}
|
||||
@ -77,7 +80,7 @@ export function useFetch<
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
opts?: UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
|
||||
export function useFetch<
|
||||
ResT = void,
|
||||
ErrorT = FetchError,
|
||||
@ -86,7 +89,7 @@ export function useFetch<
|
||||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
|
||||
DataT = _ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
arg1?: string | UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>,
|
||||
@ -161,8 +164,10 @@ export function useFetch<
|
||||
* @see https://github.com/unjs/ofetch/blob/bb2d72baa5d3f332a2185c20fc04e35d2c3e258d/src/fetch.ts#L152
|
||||
*/
|
||||
const timeoutLength = toValue(opts.timeout)
|
||||
let timeoutId: NodeJS.Timeout
|
||||
if (timeoutLength) {
|
||||
setTimeout(() => controller.abort(), timeoutLength)
|
||||
timeoutId = setTimeout(() => controller.abort(), timeoutLength)
|
||||
controller.signal.onabort = () => clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
let _$fetch = opts.$fetch || globalThis.$fetch
|
||||
@ -175,7 +180,7 @@ export function useFetch<
|
||||
}
|
||||
}
|
||||
|
||||
return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any) as Promise<_ResT>
|
||||
return _$fetch(_request.value, { signal: controller.signal, ..._fetchOptions } as any).finally(() => { clearTimeout(timeoutId) }) as Promise<_ResT>
|
||||
}, _asyncDataOptions)
|
||||
|
||||
return asyncData
|
||||
@ -190,11 +195,11 @@ export function useLazyFetch<
|
||||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
|
||||
DataT = _ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
|
||||
export function useLazyFetch<
|
||||
ResT = void,
|
||||
ErrorT = FetchError,
|
||||
@ -207,7 +212,7 @@ export function useLazyFetch<
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
opts?: Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | null>
|
||||
): AsyncData<PickFrom<DataT, PickKeys> | DefaultT, ErrorT | DefaultAsyncDataErrorValue>
|
||||
export function useLazyFetch<
|
||||
ResT = void,
|
||||
ErrorT = FetchError,
|
||||
@ -216,7 +221,7 @@ export function useLazyFetch<
|
||||
_ResT = ResT extends void ? FetchResult<ReqT, Method> : ResT,
|
||||
DataT = _ResT,
|
||||
PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
|
||||
DefaultT = null,
|
||||
DefaultT = DefaultAsyncDataValue,
|
||||
> (
|
||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||
arg1?: string | Omit<UseFetchOptions<_ResT, DataT, PickKeys, DefaultT, ReqT, Method>, 'lazy'>,
|
||||
|
@ -23,9 +23,10 @@ export type LoadingIndicator = {
|
||||
_cleanup: () => void
|
||||
progress: Ref<number>
|
||||
isLoading: Ref<boolean>
|
||||
error: Ref<boolean>
|
||||
start: () => void
|
||||
set: (value: number) => void
|
||||
finish: (opts?: { force?: boolean }) => void
|
||||
finish: (opts?: { force?: boolean, error?: boolean }) => void
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
@ -40,6 +41,7 @@ function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const progress = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const error = ref(false)
|
||||
let done = false
|
||||
let rafId: number
|
||||
|
||||
@ -47,7 +49,10 @@ function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
|
||||
let hideTimeout: number | NodeJS.Timeout
|
||||
let resetTimeout: number | NodeJS.Timeout
|
||||
|
||||
const start = () => set(0)
|
||||
const start = () => {
|
||||
error.value = false
|
||||
set(0)
|
||||
}
|
||||
|
||||
function set (at = 0) {
|
||||
if (nuxtApp.isHydrating) {
|
||||
@ -76,11 +81,14 @@ function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function finish (opts: { force?: boolean } = {}) {
|
||||
function finish (opts: { force?: boolean, error?: boolean } = {}) {
|
||||
progress.value = 100
|
||||
done = true
|
||||
clear()
|
||||
_clearTimeouts()
|
||||
if (opts.error) {
|
||||
error.value = true
|
||||
}
|
||||
if (opts.force) {
|
||||
progress.value = 0
|
||||
isLoading.value = false
|
||||
@ -145,6 +153,7 @@ function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
|
||||
_cleanup,
|
||||
progress: computed(() => progress.value),
|
||||
isLoading: computed(() => isLoading.value),
|
||||
error: computed(() => error.value),
|
||||
start,
|
||||
set,
|
||||
finish,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { MatcherExport, RouteMatcher } from 'radix3'
|
||||
import { createMatcherFromExport, createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '../config'
|
||||
import { useRuntimeConfig } from '../nuxt'
|
||||
// @ts-expect-error virtual file
|
||||
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
|
||||
@ -25,9 +24,7 @@ function fetchManifest () {
|
||||
if (!isAppManifestEnabled) {
|
||||
throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`')
|
||||
}
|
||||
// @ts-expect-error private property
|
||||
const buildId = useAppConfig().nuxt?.buildId
|
||||
manifest = $fetch<NuxtAppManifest>(buildAssetsURL(`builds/meta/${buildId}.json`))
|
||||
manifest = $fetch<NuxtAppManifest>(buildAssetsURL(`builds/meta/${useRuntimeConfig().app.buildId}.json`))
|
||||
manifest.then((m) => {
|
||||
matcher = createMatcherFromExport(m.matcher)
|
||||
})
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { hasProtocol, joinURL, withoutTrailingSlash } from 'ufo'
|
||||
import { parse } from 'devalue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import { getCurrentInstance, onServerPrefetch } from 'vue'
|
||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||
import { useAppConfig } from '../config'
|
||||
|
||||
import { useRoute } from './router'
|
||||
import { getAppManifest, getRouteRules } from './manifest'
|
||||
@ -17,9 +16,9 @@ interface LoadPayloadOptions {
|
||||
}
|
||||
|
||||
/** @since 3.0.0 */
|
||||
export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record<string, any> | Promise<Record<string, any>> | null {
|
||||
export async function loadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<Record<string, any> | null> {
|
||||
if (import.meta.server || !payloadExtraction) { return null }
|
||||
const payloadURL = _getPayloadURL(url, opts)
|
||||
const payloadURL = await _getPayloadURL(url, opts)
|
||||
const nuxtApp = useNuxtApp()
|
||||
const cache = nuxtApp._payloadCache = nuxtApp._payloadCache || {}
|
||||
if (payloadURL in cache) {
|
||||
@ -40,25 +39,34 @@ export function loadPayload (url: string, opts: LoadPayloadOptions = {}): Record
|
||||
return cache[payloadURL]
|
||||
}
|
||||
/** @since 3.0.0 */
|
||||
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}) {
|
||||
const payloadURL = _getPayloadURL(url, opts)
|
||||
useHead({
|
||||
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<void> {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const promise = _getPayloadURL(url, opts).then((payloadURL) => {
|
||||
nuxtApp.runWithContext(() => useHead({
|
||||
link: [
|
||||
{ rel: 'modulepreload', href: payloadURL },
|
||||
],
|
||||
}))
|
||||
})
|
||||
if (import.meta.server) {
|
||||
onServerPrefetch(() => promise)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
// --- Internal ---
|
||||
|
||||
const filename = renderJsonPayloads ? '_payload.json' : '_payload.js'
|
||||
function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
||||
async function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
||||
const u = new URL(url, 'http://localhost')
|
||||
if (u.host !== 'localhost' || hasProtocol(u.pathname, { acceptRelative: true })) {
|
||||
throw new Error('Payload URL must not include hostname: ' + url)
|
||||
}
|
||||
const hash = opts.hash || (opts.fresh ? Date.now() : (useAppConfig().nuxt as any)?.buildId)
|
||||
return joinURL(useRuntimeConfig().app.baseURL, u.pathname, filename + (hash ? `?${hash}` : ''))
|
||||
const config = useRuntimeConfig()
|
||||
const hash = opts.hash || (opts.fresh ? Date.now() : config.app.buildId)
|
||||
const cdnURL = config.app.cdnURL
|
||||
const baseOrCdnURL = cdnURL && await isPrerendered(url) ? cdnURL : config.app.baseURL
|
||||
return joinURL(baseOrCdnURL, u.pathname, filename + (hash ? `?${hash}` : ''))
|
||||
}
|
||||
|
||||
async function _importPayload (payloadURL: string) {
|
||||
|
@ -169,8 +169,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
|
||||
nuxtApp.ssrContext!._renderResponse = {
|
||||
statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302),
|
||||
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
|
||||
// do not encode as this would break some modules and some environments
|
||||
headers: { location },
|
||||
headers: { location: encodeURI(location) },
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
7
packages/nuxt/src/app/defaults.ts
Normal file
7
packages/nuxt/src/app/defaults.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// TODO: temporary module for backwards compatibility
|
||||
|
||||
export type DefaultAsyncDataErrorValue = null
|
||||
export type DefaultAsyncDataValue = null
|
||||
export type DefaultErrorValue = null
|
||||
|
||||
export {}
|
@ -1,4 +1,4 @@
|
||||
import { effectScope, getCurrentInstance, getCurrentScope, hasInjectionContext, reactive } from 'vue'
|
||||
import { effectScope, getCurrentInstance, getCurrentScope, hasInjectionContext, reactive, shallowReactive } from 'vue'
|
||||
import type { App, EffectScope, Ref, VNode, onErrorCaptured } from 'vue'
|
||||
import type { RouteLocationNormalizedLoaded } from '#vue-router'
|
||||
import type { HookCallback, Hookable } from 'hookable'
|
||||
@ -20,13 +20,15 @@ import type { LoadingIndicator } from '../app/composables/loading-indicator'
|
||||
import type { RouteAnnouncer } from '../app/composables/route-announcer'
|
||||
import type { ViewTransition } from './plugins/view-transitions.client'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { appId } from '#build/nuxt.config.mjs'
|
||||
|
||||
// TODO: temporary module for backwards compatibility
|
||||
import type { DefaultAsyncDataErrorValue, DefaultErrorValue } from '#app/defaults'
|
||||
import type { NuxtAppLiterals } from '#app'
|
||||
|
||||
// @ts-expect-error virtual import
|
||||
import { buildId } from '#build/nuxt.config.mjs'
|
||||
|
||||
function getNuxtAppCtx (appName?: string) {
|
||||
return getContext<NuxtApp>(appName || buildId || 'nuxt-app', {
|
||||
function getNuxtAppCtx (appName = appId || 'nuxt-app') {
|
||||
return getContext<NuxtApp>(appName, {
|
||||
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server,
|
||||
})
|
||||
}
|
||||
@ -92,8 +94,8 @@ export interface NuxtPayload {
|
||||
state: Record<string, any>
|
||||
once: Set<string>
|
||||
config?: Pick<RuntimeConfig, 'public' | 'app'>
|
||||
error?: NuxtError | null
|
||||
_errors: Record<string, NuxtError | null>
|
||||
error?: NuxtError | DefaultErrorValue
|
||||
_errors: Record<string, NuxtError | DefaultAsyncDataErrorValue>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
@ -120,10 +122,12 @@ interface _NuxtApp {
|
||||
_asyncDataPromises: Record<string, Promise<any> | undefined>
|
||||
/** @internal */
|
||||
_asyncData: Record<string, {
|
||||
data: Ref<any>
|
||||
data: Ref<unknown>
|
||||
pending: Ref<boolean>
|
||||
error: Ref<Error | null>
|
||||
error: Ref<Error | DefaultAsyncDataErrorValue>
|
||||
status: Ref<AsyncDataRequestStatus>
|
||||
/** @internal */
|
||||
_default: () => unknown
|
||||
} | undefined>
|
||||
|
||||
/** @internal */
|
||||
@ -244,7 +248,7 @@ export interface CreateOptions {
|
||||
export function createNuxtApp (options: CreateOptions) {
|
||||
let hydratingCount = 0
|
||||
const nuxtApp: NuxtApp = {
|
||||
name: buildId,
|
||||
_name: appId || 'nuxt-app',
|
||||
_scope: effectScope(),
|
||||
provide: undefined,
|
||||
globalName: 'nuxt',
|
||||
@ -252,12 +256,11 @@ export function createNuxtApp (options: CreateOptions) {
|
||||
get nuxt () { return __NUXT_VERSION__ },
|
||||
get vue () { return nuxtApp.vueApp.version },
|
||||
},
|
||||
payload: reactive({
|
||||
data: {},
|
||||
state: {},
|
||||
payload: shallowReactive({
|
||||
data: shallowReactive({}),
|
||||
state: reactive({}),
|
||||
once: new Set<string>(),
|
||||
_errors: {},
|
||||
...(import.meta.client ? window.__NUXT__ ?? {} : { serverRendered: true }),
|
||||
_errors: shallowReactive({}),
|
||||
}),
|
||||
static: {
|
||||
data: {},
|
||||
@ -288,11 +291,32 @@ export function createNuxtApp (options: CreateOptions) {
|
||||
}
|
||||
},
|
||||
_asyncDataPromises: {},
|
||||
_asyncData: {},
|
||||
_asyncData: shallowReactive({}),
|
||||
_payloadRevivers: {},
|
||||
...options,
|
||||
} as any as NuxtApp
|
||||
|
||||
if (import.meta.server) {
|
||||
nuxtApp.payload.serverRendered = true
|
||||
}
|
||||
|
||||
// TODO: remove/refactor in https://github.com/nuxt/nuxt/issues/25336
|
||||
if (import.meta.client && window.__NUXT__) {
|
||||
for (const key in window.__NUXT__) {
|
||||
switch (key) {
|
||||
case 'data':
|
||||
case 'state':
|
||||
case '_errors':
|
||||
// Preserve reactivity for non-rich payload support
|
||||
Object.assign(nuxtApp.payload[key], window.__NUXT__[key])
|
||||
break
|
||||
|
||||
default:
|
||||
nuxtApp.payload[key] = window.__NUXT__[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nuxtApp.hooks = createHooks<RuntimeNuxtHooks>()
|
||||
nuxtApp.hook = nuxtApp.hooks.hook
|
||||
|
||||
|
@ -1,13 +1,21 @@
|
||||
import { consola, createConsola } from 'consola'
|
||||
import { createConsola } from 'consola'
|
||||
import type { LogObject } from 'consola'
|
||||
import { parse } from 'devalue'
|
||||
|
||||
import { h } from 'vue'
|
||||
import { defineNuxtPlugin } from '../nuxt'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { devLogs, devRootDir } from '#build/nuxt.config.mjs'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const devRevivers: Record<string, (data: any) => any> = import.meta.server
|
||||
? {}
|
||||
: {
|
||||
VNode: data => h(data.type, data.props),
|
||||
URL: data => new URL(data),
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
if (import.meta.test) { return }
|
||||
|
||||
if (import.meta.server) {
|
||||
@ -23,42 +31,18 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
date: true,
|
||||
},
|
||||
})
|
||||
const hydrationLogs = new Set<string>()
|
||||
consola.wrapConsole()
|
||||
consola.addReporter({
|
||||
log (logObj) {
|
||||
try {
|
||||
hydrationLogs.add(JSON.stringify(logObj.args))
|
||||
} catch {
|
||||
// silently ignore - the worst case is a user gets log twice
|
||||
}
|
||||
},
|
||||
})
|
||||
nuxtApp.hook('dev:ssr-logs', (logs) => {
|
||||
for (const log of logs) {
|
||||
// deduplicate so we don't print out things that are logged on client
|
||||
try {
|
||||
if (!hydrationLogs.size || !hydrationLogs.has(JSON.stringify(log.args))) {
|
||||
logger.log(normalizeServerLog({ ...log }))
|
||||
}
|
||||
} catch {
|
||||
logger.log(normalizeServerLog({ ...log }))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
nuxtApp.hooks.hook('app:suspense:resolve', () => consola.restoreAll())
|
||||
nuxtApp.hooks.hookOnce('dev:ssr-logs', () => hydrationLogs.clear())
|
||||
}
|
||||
|
||||
// pass SSR logs after hydration
|
||||
nuxtApp.hooks.hook('app:suspense:resolve', async () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const content = document.getElementById('__NUXT_LOGS__')?.textContent
|
||||
const logs = content ? parse(content, nuxtApp._payloadRevivers) as LogObject[] : []
|
||||
const logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : []
|
||||
await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function normalizeFilenames (stack?: string) {
|
||||
|
@ -10,7 +10,7 @@ interface LoaderOptions {
|
||||
transform?: ComponentsOptions['transform']
|
||||
rootDir: string
|
||||
}
|
||||
const CLIENT_FALLBACK_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/
|
||||
const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/
|
||||
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
|
||||
export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions) => {
|
||||
const exclude = options.transform?.exclude || []
|
||||
@ -37,7 +37,7 @@ export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions
|
||||
|
||||
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
|
||||
count++
|
||||
if (/ :?uid=/g.test(attrs)) { return full }
|
||||
if (/ :?uid=/.test(attrs)) { return full }
|
||||
return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>`
|
||||
})
|
||||
|
||||
|
@ -25,7 +25,7 @@ interface ComponentChunkOptions {
|
||||
}
|
||||
|
||||
const SCRIPT_RE = /<script[^>]*>/g
|
||||
const HAS_SLOT_OR_CLIENT_RE = /(<slot[^>]*>)|(nuxt-client)/
|
||||
const HAS_SLOT_OR_CLIENT_RE = /<slot[^>]*>|nuxt-client/
|
||||
const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
|
||||
const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g
|
||||
const IMPORT_CODE = '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport NuxtTeleportIslandComponent from \'#app/components/nuxt-teleport-island-component\'' + '\nimport NuxtTeleportSsrSlot from \'#app/components/nuxt-teleport-island-slot\''
|
||||
|
@ -43,7 +43,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
||||
const s = new MagicString(code)
|
||||
|
||||
// replace `_resolveComponent("...")` to direct import
|
||||
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy)?([^'"]*?)["'][\s,]*[^)]*\)/g, (full: string, lazy: string, name: string) => {
|
||||
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy)?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, name: string) => {
|
||||
const component = findComponent(components, name, options.mode)
|
||||
if (component) {
|
||||
// @ts-expect-error TODO: refactor to nuxi
|
||||
|
@ -18,7 +18,7 @@ function compareDirByPathLength ({ path: pathA }: { path: string }, { path: path
|
||||
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length
|
||||
}
|
||||
|
||||
const DEFAULT_COMPONENTS_DIRS_RE = /\/components(\/global|\/islands)?$/
|
||||
const DEFAULT_COMPONENTS_DIRS_RE = /\/components(?:\/(?:global|islands))?$/
|
||||
|
||||
export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]
|
||||
|
||||
|
@ -85,8 +85,8 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
|
||||
*/
|
||||
let fileName = basename(filePath, fileExt)
|
||||
|
||||
const island = /\.(island)(\.global)?$/.test(fileName) || dir.island
|
||||
const global = /\.(global)(\.island)?$/.test(fileName) || dir.global
|
||||
const island = /\.island(?:\.global)?$/.test(fileName) || dir.island
|
||||
const global = /\.global(?:\.island)?$/.test(fileName) || dir.global
|
||||
const mode = island ? 'server' : (fileName.match(/(?<=\.)(client|server)(\.global|\.island)*$/)?.[1] || 'all') as 'client' | 'server' | 'all'
|
||||
fileName = fileName.replace(/(\.(client|server))?(\.global|\.island)*$/, '')
|
||||
|
||||
|
@ -104,8 +104,8 @@ export const componentsTypeTemplate = {
|
||||
const buildDir = nuxt.options.buildDir
|
||||
const componentTypes = app.components.filter(c => !c.island).map((c) => {
|
||||
const type = `typeof ${genDynamicImport(isAbsolute(c.filePath)
|
||||
? relative(buildDir, c.filePath).replace(/(?<=\w)\.(?!vue)\w+$/g, '')
|
||||
: c.filePath.replace(/(?<=\w)\.(?!vue)\w+$/g, ''), { wrapper: false })}['${c.export}']`
|
||||
? relative(buildDir, c.filePath).replace(/\b\.(?!vue)\w+$/g, '')
|
||||
: c.filePath.replace(/\b\.(?!vue)\w+$/g, ''), { wrapper: false })}['${c.export}']`
|
||||
return [
|
||||
c.pascalName,
|
||||
c.island || c.mode === 'server' ? `IslandComponent<${type}>` : type,
|
||||
|
@ -16,7 +16,7 @@ interface TreeShakeTemplatePluginOptions {
|
||||
type AcornNode<N extends Node> = N & { start: number, end: number }
|
||||
|
||||
const SSR_RENDER_RE = /ssrRenderComponent/
|
||||
const PLACEHOLDER_EXACT_RE = /^(fallback|placeholder)$/
|
||||
const PLACEHOLDER_EXACT_RE = /^(?:fallback|placeholder)$/
|
||||
const CLIENT_ONLY_NAME_RE = /^(?:_unref\()?(?:_component_)?(?:Lazy|lazy_)?(?:client_only|ClientOnly\)?)$/
|
||||
const PARSER_OPTIONS = { sourceType: 'module', ecmaVersion: 'latest' }
|
||||
|
||||
|
@ -85,8 +85,8 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
|
||||
changedTemplates.push(template)
|
||||
}
|
||||
|
||||
const perf = performance.measure(fullPath, mark?.name) // TODO: remove when Node 14 reaches EOL
|
||||
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
|
||||
const perf = performance.measure(fullPath, mark.name)
|
||||
const setupTime = Math.round((perf.duration * 100)) / 100
|
||||
|
||||
if (nuxt.options.debug || setupTime > 500) {
|
||||
logger.info(`Compiled \`${template.filename}\` in ${setupTime}ms`)
|
||||
@ -179,7 +179,12 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
||||
app.middleware = []
|
||||
for (const config of reversedConfigs) {
|
||||
const middlewareDir = (config.rootDir === nuxt.options.rootDir ? nuxt.options : config).dir?.middleware || 'middleware'
|
||||
const middlewareFiles = await resolveFiles(config.srcDir, `${middlewareDir}/*{${nuxt.options.extensions.join(',')}}`)
|
||||
const middlewareFiles = await resolveFiles(config.srcDir, [
|
||||
`${middlewareDir}/*{${nuxt.options.extensions.join(',')}}`,
|
||||
...nuxt.options.future.compatibilityVersion === 4
|
||||
? [`${middlewareDir}/*/index{${nuxt.options.extensions.join(',')}}`]
|
||||
: [],
|
||||
])
|
||||
for (const file of middlewareFiles) {
|
||||
const name = getNameFromPath(file)
|
||||
if (!name) {
|
||||
@ -200,7 +205,7 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
||||
...config.srcDir
|
||||
? await resolveFiles(config.srcDir, [
|
||||
`${pluginDir}/*{${nuxt.options.extensions.join(',')}}`,
|
||||
`${pluginDir}/*/index{${nuxt.options.extensions.join(',')}}`, // TODO: remove, only scan top-level plugins #18418
|
||||
`${pluginDir}/*/index{${nuxt.options.extensions.join(',')}}`,
|
||||
])
|
||||
: [],
|
||||
].map(plugin => normalizePlugin(plugin as NuxtPlugin)))
|
||||
|
@ -24,7 +24,7 @@ export async function build (nuxt: Nuxt) {
|
||||
if (event === 'change') { return }
|
||||
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||
const relativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
|
||||
const restartPath = relativePaths.find(relativePath => /^(app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(relativePath))
|
||||
const restartPath = relativePaths.find(relativePath => /^(?:app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(relativePath))
|
||||
if (restartPath) {
|
||||
if (restartPath.startsWith('app')) {
|
||||
app.mainComponent = undefined
|
||||
|
@ -161,6 +161,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
'nuxt3/dist',
|
||||
'nuxt-nightly/dist',
|
||||
distDir,
|
||||
// Ensure app config files have auto-imports injected even if they are pure .js files
|
||||
...nuxt.options._layers.map(layer => resolve(layer.config.srcDir, 'app.config')),
|
||||
],
|
||||
traceInclude: [
|
||||
// force include files used in generated code from the runtime-compiler
|
||||
@ -218,9 +220,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
|
||||
// Add app manifest handler and prerender configuration
|
||||
if (nuxt.options.experimental.appManifest) {
|
||||
// @ts-expect-error untyped nuxt property
|
||||
const buildId = nuxt.options.appConfig.nuxt!.buildId ||=
|
||||
(nuxt.options.dev ? 'dev' : nuxt.options.test ? 'test' : nuxt.options.buildId)
|
||||
const buildId = nuxt.options.runtimeConfig.app.buildId ||= nuxt.options.buildId
|
||||
const buildTimestamp = Date.now()
|
||||
|
||||
const manifestPrefix = joinURL(nuxt.options.app.buildAssetsDir, 'builds')
|
||||
@ -383,7 +383,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
tsConfig.compilerOptions.paths[alias] = [absolutePath]
|
||||
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`]
|
||||
} else {
|
||||
tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(/(?<=\w)\.\w+$/g, '')] /* remove extension */
|
||||
tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(/\b\.\w+$/g, '')] /* remove extension */
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,10 @@ import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
|
||||
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
import fse from 'fs-extra'
|
||||
import { withoutLeadingSlash } from 'ufo'
|
||||
import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
|
||||
|
||||
import defu from 'defu'
|
||||
import { gt, satisfies } from 'semver'
|
||||
import pagesModule from '../pages/module'
|
||||
import metaModule from '../head/module'
|
||||
import componentsModule from '../components/module'
|
||||
@ -63,6 +64,11 @@ const nightlies = {
|
||||
'@nuxt/kit': '@nuxt/kit-nightly',
|
||||
}
|
||||
|
||||
const keyDependencies = [
|
||||
'@nuxt/kit',
|
||||
'@nuxt/schema',
|
||||
]
|
||||
|
||||
async function initNuxt (nuxt: Nuxt) {
|
||||
// Register user hooks
|
||||
for (const config of nuxt.options._layers.map(layer => layer.config).reverse()) {
|
||||
@ -71,6 +77,17 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
}
|
||||
}
|
||||
|
||||
// Restart Nuxt when layer directories are added or removed
|
||||
const layersDir = withTrailingSlash(resolve(nuxt.options.rootDir, 'layers'))
|
||||
nuxt.hook('builder:watch', (event, relativePath) => {
|
||||
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||
if (event === 'addDir' || event === 'unlinkDir') {
|
||||
if (path.startsWith(layersDir)) {
|
||||
return nuxt.callHook('restart', { hard: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Set nuxt instance for useNuxt
|
||||
nuxtCtx.set(nuxt)
|
||||
nuxt.hook('close', () => nuxtCtx.unset())
|
||||
@ -112,6 +129,8 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
if (nuxt.options.typescript.shim) {
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/vue-shim.d.ts') })
|
||||
}
|
||||
// Add shims for `#build/*` imports that do not already have matching types
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/build.d.ts') })
|
||||
// Add module augmentations directly to NuxtConfig
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/schema.d.ts') })
|
||||
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/app.config.d.ts') })
|
||||
@ -253,6 +272,9 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
...nuxt.options._layers.filter(i => i.cwd.includes('node_modules')).map(i => i.cwd as string),
|
||||
)
|
||||
|
||||
// Ensure we can resolve dependencies within layers
|
||||
nuxt.options.modulesDir.push(...nuxt.options._layers.map(l => resolve(l.cwd, 'node_modules')))
|
||||
|
||||
// Init user modules
|
||||
await nuxt.callHook('modules:before')
|
||||
const modulesToInstall = []
|
||||
@ -540,6 +562,12 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/payload.client'))
|
||||
}
|
||||
|
||||
// Show compatibility version banner when Nuxt is running with a compatibility version
|
||||
// that is different from the current major version
|
||||
if (!(satisfies(nuxt._version, nuxt.options.future.compatibilityVersion + '.x'))) {
|
||||
console.info(`Running with compatibility version \`${nuxt.options.future.compatibilityVersion}\``)
|
||||
}
|
||||
|
||||
await nuxt.callHook('ready', nuxt)
|
||||
}
|
||||
|
||||
@ -605,6 +633,8 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
||||
|
||||
const nuxt = createNuxt(options)
|
||||
|
||||
await Promise.all(keyDependencies.map(dependency => checkDependencyVersion(dependency, nuxt._version)))
|
||||
|
||||
// We register hooks layer-by-layer so any overrides need to be registered separately
|
||||
if (opts.overrides?.hooks) {
|
||||
nuxt.hooks.addHooks(opts.overrides.hooks)
|
||||
@ -621,4 +651,15 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
||||
return nuxt
|
||||
}
|
||||
|
||||
const RESTART_RE = /^(app|error|app\.config)\.(js|ts|mjs|jsx|tsx|vue)$/i
|
||||
async function checkDependencyVersion (name: string, nuxtVersion: string): Promise<void> {
|
||||
const path = await resolvePath(name).catch(() => null)
|
||||
|
||||
if (!path) { return }
|
||||
const { version } = await readPackageJSON(path)
|
||||
|
||||
if (version && gt(nuxtVersion, version)) {
|
||||
console.warn(`[nuxt] Expected \`${name}\` to be at least \`${nuxtVersion}\` but got \`${version}\`. This might lead to unexpected behavior. Check your package.json or refresh your lockfile.`)
|
||||
}
|
||||
}
|
||||
|
||||
const RESTART_RE = /^(?:app|error|app\.config)\.(?:js|ts|mjs|jsx|tsx|vue)$/i
|
||||
|
@ -22,7 +22,7 @@ export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: {
|
||||
])
|
||||
|
||||
patterns.push([
|
||||
/^((|~|~~|@|@@)\/)?nuxt\.config(\.|$)/,
|
||||
/^((~|~~|@|@@)?\/)?nuxt\.config(\.|$)/,
|
||||
'Importing directly from a `nuxt.config` file is not allowed. Instead, use runtime config or a module.',
|
||||
])
|
||||
|
||||
|
@ -6,11 +6,17 @@ import type { H3Event } from 'h3'
|
||||
import { withTrailingSlash } from 'ufo'
|
||||
import { getContext } from 'unctx'
|
||||
|
||||
import { isVNode } from 'vue'
|
||||
import type { NitroApp } from '#internal/nitro/app'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { rootDir } from '#internal/dev-server-logs-options'
|
||||
|
||||
const devReducers: Record<string, (data: any) => any> = {
|
||||
VNode: data => isVNode(data) ? { type: data.type, props: data.props } : undefined,
|
||||
URL: data => data instanceof URL ? data.toString() : undefined,
|
||||
}
|
||||
|
||||
interface NuxtDevAsyncContext {
|
||||
logs: LogObject[]
|
||||
event: H3Event
|
||||
@ -54,9 +60,10 @@ export default (nitroApp: NitroApp) => {
|
||||
const ctx = asyncContext.tryUse()
|
||||
if (!ctx) { return }
|
||||
try {
|
||||
htmlContext.bodyAppend.unshift(`<script type="application/json" id="__NUXT_LOGS__">${stringify(ctx.logs, ctx.event.context._payloadReducers)}</script>`)
|
||||
htmlContext.bodyAppend.unshift(`<script type="application/json" id="__NUXT_LOGS__">${stringify(ctx.logs, { ...devReducers, ...ctx.event.context._payloadReducers })}</script>`)
|
||||
} catch (e) {
|
||||
console.warn('[nuxt] Failed to stringify dev server logs. You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.', e)
|
||||
const shortError = e instanceof Error && 'toString' in e ? ` Received \`${e.toString()}\`.` : ''
|
||||
console.warn(`[nuxt] Failed to stringify dev server logs.${shortError} You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -70,8 +77,8 @@ function getStack () {
|
||||
return stack.stack?.replace(EXCLUDE_TRACE_RE, '').replace(/^Error.*\n/, '') || ''
|
||||
}
|
||||
|
||||
const FILENAME_RE = /at.*\(([^:)]+)[):]/
|
||||
const FILENAME_RE_GLOBAL = /at.*\(([^)]+)\)/g
|
||||
const FILENAME_RE = /at[^(]*\(([^:)]+)[):]/
|
||||
const FILENAME_RE_GLOBAL = /at[^(]*\(([^)]+)\)/g
|
||||
function extractFilenameFromStack (stacktrace: string) {
|
||||
return stacktrace.match(FILENAME_RE)?.[1].replace(withTrailingSlash(rootDir), '')
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import type { HeadEntryOptions } from '@unhead/schema'
|
||||
import type { Link, Script, Style } from '@unhead/vue'
|
||||
import { createServerHead } from '@unhead/vue'
|
||||
|
||||
import { defineRenderHandler, getRouteRules, useAppConfig, useRuntimeConfig, useStorage } from '#internal/nitro'
|
||||
import { defineRenderHandler, getRouteRules, useRuntimeConfig, useStorage } from '#internal/nitro'
|
||||
import { useNitroApp } from '#internal/nitro/app'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
@ -327,7 +327,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
|
||||
// Whether we are prerendering route
|
||||
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR && !isRenderingIsland
|
||||
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') + '?' + (useAppConfig().nuxt as any)?.buildId : undefined
|
||||
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.cdnURL || ssrContext.runtimeConfig.app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') + '?' + ssrContext.runtimeConfig.app.buildId : undefined
|
||||
if (import.meta.prerender) {
|
||||
ssrContext.payload.prerenderedAt = Date.now()
|
||||
}
|
||||
@ -646,7 +646,7 @@ function getServerComponentHTML (body: string[]): string {
|
||||
|
||||
const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/
|
||||
const SSR_CLIENT_TELEPORT_MARKER = /^uid=([^;]*);client=(.*)$/
|
||||
const SSR_CLIENT_SLOT_MARKER = /^island-slot=(?:[^;]*);(.*)$/
|
||||
const SSR_CLIENT_SLOT_MARKER = /^island-slot=[^;]*;(.*)$/
|
||||
|
||||
function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] {
|
||||
if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.slots).length) { return undefined }
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { dirname, resolve } from 'pathe'
|
||||
import chokidar from 'chokidar'
|
||||
import { interopDefault } from 'mlly'
|
||||
@ -26,7 +26,7 @@ export default defineNuxtModule({
|
||||
const resolver = createResolver(import.meta.url)
|
||||
|
||||
// Initialize untyped/jiti loader
|
||||
const _resolveSchema = jiti(dirname(import.meta.url), {
|
||||
const _resolveSchema = jiti(dirname(fileURLToPath(import.meta.url)), {
|
||||
esmResolve: true,
|
||||
interopDefault: true,
|
||||
cache: false,
|
||||
|
@ -130,6 +130,12 @@ declare module '#app' {
|
||||
}
|
||||
}
|
||||
|
||||
declare module '#app/defaults' {
|
||||
type DefaultAsyncDataErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
|
||||
type DefaultAsyncDataValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
|
||||
type DefaultErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties extends NuxtAppInjections { }
|
||||
}
|
||||
@ -266,10 +272,13 @@ export const useRuntimeConfig = () => window?.__NUXT__?.config || {}
|
||||
export const appConfigDeclarationTemplate: NuxtTemplate = {
|
||||
filename: 'types/app.config.d.ts',
|
||||
getContents ({ app, nuxt }) {
|
||||
const typesDir = join(nuxt.options.buildDir, 'types')
|
||||
const configPaths = app.configs.map(path => relative(typesDir, path).replace(/\b\.\w+$/g, ''))
|
||||
|
||||
return `
|
||||
import type { CustomAppConfig } from 'nuxt/schema'
|
||||
import type { Defu } from 'defu'
|
||||
${app.configs.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id.replace(/(?<=\w)\.\w+$/g, ''))}`).join('\n')}
|
||||
${configPaths.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id)}`).join('\n')}
|
||||
|
||||
declare const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)}
|
||||
type ResolvedAppConfig = Defu<typeof inlineConfig, [${app.configs.map((_id: string, index: number) => `typeof cfg${index}`).join(', ')}]>
|
||||
@ -393,11 +402,43 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
||||
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
|
||||
`export const devLogs = ${JSON.stringify(ctx.nuxt.options.features.devLogs)}`,
|
||||
`export const nuxtLinkDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.nuxtLink)}`,
|
||||
`export const asyncDataDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.useAsyncData)}`,
|
||||
`export const asyncDataDefaults = ${JSON.stringify({
|
||||
...ctx.nuxt.options.experimental.defaults.useAsyncData,
|
||||
value: ctx.nuxt.options.experimental.defaults.useAsyncData.value === 'null' ? null : undefined,
|
||||
errorValue: ctx.nuxt.options.experimental.defaults.useAsyncData.errorValue === 'null' ? null : undefined,
|
||||
})}`,
|
||||
`export const resetAsyncDataToUndefined = ${ctx.nuxt.options.experimental.resetAsyncDataToUndefined}`,
|
||||
`export const nuxtDefaultErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}`,
|
||||
`export const fetchDefaults = ${JSON.stringify(fetchDefaults)}`,
|
||||
`export const vueAppRootContainer = ${ctx.nuxt.options.app.rootId ? `'#${ctx.nuxt.options.app.rootId}'` : `'body > ${ctx.nuxt.options.app.rootTag}'`}`,
|
||||
`export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`,
|
||||
`export const buildId = ${JSON.stringify(ctx.nuxt.options.buildId)}`,
|
||||
`export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`,
|
||||
].join('\n\n')
|
||||
},
|
||||
}
|
||||
|
||||
const TYPE_FILENAME_RE = /\.([cm])?[jt]s$/
|
||||
const DECLARATION_RE = /\.d\.[cm]?ts$/
|
||||
export const buildTypeTemplate: NuxtTemplate = {
|
||||
filename: 'types/build.d.ts',
|
||||
getContents ({ app }) {
|
||||
let declarations = ''
|
||||
|
||||
for (const file of app.templates) {
|
||||
if (file.write || !file.filename || DECLARATION_RE.test(file.filename)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (TYPE_FILENAME_RE.test(file.filename)) {
|
||||
const typeFilenames = new Set([file.filename.replace(TYPE_FILENAME_RE, '.d.$1ts'), file.filename.replace(TYPE_FILENAME_RE, '.d.ts')])
|
||||
if (app.templates.some(f => f.filename && typeFilenames.has(f.filename))) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
declarations += 'declare module ' + JSON.stringify(join('#build', file.filename)) + ';\n'
|
||||
}
|
||||
|
||||
return declarations
|
||||
},
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export function isVue (id: string, opts: { type?: Array<'template' | 'script' |
|
||||
return true
|
||||
}
|
||||
|
||||
const JS_RE = /\.((c|m)?j|t)sx?$/
|
||||
const JS_RE = /\.(?:[cm]?j|t)sx?$/
|
||||
|
||||
export function isJS (id: string) {
|
||||
// JavaScript files
|
||||
|
@ -135,6 +135,7 @@ export const scriptsStubsPreset = {
|
||||
'useScriptGoogleMaps',
|
||||
'useScriptNpm',
|
||||
],
|
||||
priority: -1,
|
||||
from: '#app/composables/script-stubs',
|
||||
} satisfies InlinePreset
|
||||
|
||||
|
@ -422,11 +422,6 @@ export default defineNuxtModule({
|
||||
getContents: () => 'export { START_LOCATION, useRoute } from \'vue-router\'',
|
||||
})
|
||||
|
||||
// Optimize vue-router to ensure we share the same injection symbol
|
||||
nuxt.options.vite.optimizeDeps = nuxt.options.vite.optimizeDeps || {}
|
||||
nuxt.options.vite.optimizeDeps.include = nuxt.options.vite.optimizeDeps.include || []
|
||||
nuxt.options.vite.optimizeDeps.include.push('vue-router')
|
||||
|
||||
nuxt.options.vite.resolve = nuxt.options.vite.resolve || {}
|
||||
nuxt.options.vite.resolve.dedupe = nuxt.options.vite.resolve.dedupe || []
|
||||
nuxt.options.vite.resolve.dedupe.push('vue-router')
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import MagicString from 'magic-string'
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
import { stripLiteral } from 'strip-literal'
|
||||
import { isVue } from '../../core/utils'
|
||||
|
||||
const INJECTION_RE = /\b_ctx\.\$route\b/g
|
||||
const INJECTION_SINGLE_RE = /\b_ctx\.\$route\b/
|
||||
const INJECTION_RE_TEMPLATE = /\b_ctx\.\$route\b/g
|
||||
const INJECTION_RE_SCRIPT = /\bthis\.\$route\b/g
|
||||
|
||||
const INJECTION_SINGLE_RE = /\bthis\.\$route\b|\b_ctx\.\$route\b/
|
||||
|
||||
export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
|
||||
return {
|
||||
@ -14,14 +17,30 @@ export const RouteInjectionPlugin = (nuxt: Nuxt) => createUnplugin(() => {
|
||||
return isVue(id, { type: ['template', 'script'] })
|
||||
},
|
||||
transform (code) {
|
||||
if (!INJECTION_SINGLE_RE.test(code) || code.includes('_ctx._.provides[__nuxt_route_symbol')) { return }
|
||||
if (!INJECTION_SINGLE_RE.test(code) || code.includes('_ctx._.provides[__nuxt_route_symbol') || code.includes('this._.provides[__nuxt_route_symbol')) { return }
|
||||
|
||||
let replaced = false
|
||||
const s = new MagicString(code)
|
||||
s.replace(INJECTION_RE, () => {
|
||||
const strippedCode = stripLiteral(code)
|
||||
|
||||
// Local helper function for regex-based replacements using `strippedCode`
|
||||
const replaceMatches = (regExp: RegExp, replacement: string) => {
|
||||
for (const match of strippedCode.matchAll(regExp)) {
|
||||
const start = match.index!
|
||||
const end = start + match[0].length
|
||||
s.overwrite(start, end, replacement)
|
||||
if (!replaced) {
|
||||
replaced = true
|
||||
return '(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handles `$route` in template
|
||||
replaceMatches(INJECTION_RE_TEMPLATE, '(_ctx._.provides[__nuxt_route_symbol] || _ctx.$route)')
|
||||
|
||||
// handles `this.$route` in script
|
||||
replaceMatches(INJECTION_RE_SCRIPT, '(this._.provides[__nuxt_route_symbol] || this.$route)')
|
||||
|
||||
if (replaced) {
|
||||
s.prepend('import { PageRouteSymbol as __nuxt_route_symbol } from \'#app/components/injections\';\n')
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ export default defineNuxtPlugin(async () => {
|
||||
|
||||
// Implementation
|
||||
|
||||
const OPTIONAL_PARAM_RE = /^\/?:.*(\?|\(\.\*\)\*)$/
|
||||
const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/
|
||||
|
||||
function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPrerender = new Set<string>()) {
|
||||
for (const route of routes) {
|
||||
|
@ -135,7 +135,7 @@ export async function generateRoutesFromFiles (files: ScannedFile[], options: Ge
|
||||
return prepareRoutes(routes)
|
||||
}
|
||||
|
||||
const SFC_SCRIPT_RE = /<script\s*[^>]*>([\s\S]*?)<\/script\s*[^>]*>/i
|
||||
const SFC_SCRIPT_RE = /<script[^>]*>([\s\S]*?)<\/script[^>]*>/i
|
||||
export function extractScriptContent (html: string) {
|
||||
const match = html.match(SFC_SCRIPT_RE)
|
||||
|
||||
@ -146,7 +146,7 @@ export function extractScriptContent (html: string) {
|
||||
return null
|
||||
}
|
||||
|
||||
const PAGE_META_RE = /(definePageMeta\([\s\S]*?\))/
|
||||
const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/
|
||||
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
|
||||
|
||||
const pageContentsCache: Record<string, string> = {}
|
||||
@ -261,7 +261,7 @@ function getRoutePath (tokens: SegmentToken[]): string {
|
||||
}, '/')
|
||||
}
|
||||
|
||||
const PARAM_CHAR_RE = /[\w\d_.]/
|
||||
const PARAM_CHAR_RE = /[\w.]/
|
||||
|
||||
function parseSegment (segment: string) {
|
||||
let state: SegmentParserState = SegmentParserState.initial
|
||||
@ -537,7 +537,7 @@ export function pathToNitroGlob (path: string) {
|
||||
return null
|
||||
}
|
||||
|
||||
return path.replace(/\/(?:[^:/]+)?:\w+.*$/, '/**')
|
||||
return path.replace(/\/[^:/]*:\w.*$/, '/**')
|
||||
}
|
||||
|
||||
export function resolveRoutePaths (page: NuxtPage, parent = '/'): string[] {
|
||||
|
@ -1,11 +1,32 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { normalize } from 'pathe'
|
||||
import { withoutTrailingSlash } from 'ufo'
|
||||
import { readPackageJSON } from 'pkg-types'
|
||||
import { inc } from 'semver'
|
||||
import { loadNuxt } from '../src'
|
||||
import { version } from '../package.json'
|
||||
|
||||
const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url))))
|
||||
|
||||
vi.stubGlobal('console', {
|
||||
...console,
|
||||
error: vi.fn(console.error),
|
||||
warn: vi.fn(console.warn),
|
||||
})
|
||||
|
||||
vi.mock('pkg-types', async (og) => {
|
||||
const originalPkgTypes = (await og<typeof import('pkg-types')>())
|
||||
return {
|
||||
...originalPkgTypes,
|
||||
readPackageJSON: vi.fn(originalPkgTypes.readPackageJSON),
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('loadNuxt', () => {
|
||||
it('respects hook overrides', async () => {
|
||||
let hookRan = false
|
||||
@ -24,3 +45,44 @@ describe('loadNuxt', () => {
|
||||
expect(hookRan).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dependency mismatch', () => {
|
||||
it('expect mismatched dependency to log a warning', async () => {
|
||||
vi.mocked(readPackageJSON).mockReturnValue(Promise.resolve({
|
||||
version: '3.0.0',
|
||||
}))
|
||||
|
||||
const nuxt = await loadNuxt({
|
||||
cwd: repoRoot,
|
||||
})
|
||||
|
||||
expect(console.warn).toHaveBeenCalledWith(`[nuxt] Expected \`@nuxt/kit\` to be at least \`${version}\` but got \`3.0.0\`. This might lead to unexpected behavior. Check your package.json or refresh your lockfile.`)
|
||||
expect(console.warn).toHaveBeenCalledWith(`[nuxt] Expected \`@nuxt/schema\` to be at least \`${version}\` but got \`3.0.0\`. This might lead to unexpected behavior. Check your package.json or refresh your lockfile.`)
|
||||
|
||||
vi.mocked(readPackageJSON).mockRestore()
|
||||
await nuxt.close()
|
||||
})
|
||||
it.each([
|
||||
{
|
||||
name: 'nuxt version is lower',
|
||||
depVersion: inc(version, 'minor'),
|
||||
},
|
||||
{
|
||||
name: 'version matches',
|
||||
depVersion: version,
|
||||
},
|
||||
])('expect no warning when $name.', async ({ depVersion }) => {
|
||||
vi.mocked(readPackageJSON).mockReturnValue(Promise.resolve({
|
||||
depVersion,
|
||||
}))
|
||||
|
||||
const nuxt = await loadNuxt({
|
||||
cwd: repoRoot,
|
||||
})
|
||||
|
||||
expect(console.warn).not.toHaveBeenCalled()
|
||||
|
||||
await nuxt.close()
|
||||
vi.mocked(readPackageJSON).mockRestore()
|
||||
})
|
||||
})
|
||||
|
73
packages/nuxt/test/route-injection.test.ts
Normal file
73
packages/nuxt/test/route-injection.test.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { compileScript, compileTemplate, parse } from '@vue/compiler-sfc'
|
||||
import type { Plugin } from 'vite'
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
|
||||
import { RouteInjectionPlugin } from '../src/pages/plugins/route-injection'
|
||||
|
||||
describe('route-injection:transform', () => {
|
||||
const injectionPlugin = RouteInjectionPlugin({ options: { sourcemap: { client: false, server: false } } } as Nuxt).raw({}, { framework: 'rollup' }) as Plugin
|
||||
|
||||
const transform = async (source: string) => {
|
||||
const result = await (injectionPlugin.transform! as Function).call({ error: null, warn: null } as any, source, 'test.vue')
|
||||
const code: string = typeof result === 'string' ? result : result?.code
|
||||
let depth = 0
|
||||
return code.split('\n').map((l) => {
|
||||
l = l.trim()
|
||||
if (l.match(/^[}\]]/)) { depth-- }
|
||||
const res = ''.padStart(depth * 2, ' ') + l
|
||||
if (l.match(/[{[]$/)) { depth++ }
|
||||
return res
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
it('should correctly inject route in template', async () => {
|
||||
const sfc = `<template>{{ $route.path }}</template>`
|
||||
const res = compileTemplate({
|
||||
filename: 'test.vue',
|
||||
id: 'test.vue',
|
||||
source: sfc,
|
||||
})
|
||||
const transformResult = await transform(res.code)
|
||||
expect(transformResult).toMatchInlineSnapshot(`
|
||||
"import { PageRouteSymbol as __nuxt_route_symbol } from '#app/components/injections';
|
||||
import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
|
||||
|
||||
export function render(_ctx, _cache) {
|
||||
return (_openBlock(), _createElementBlock("template", null, [
|
||||
_createTextVNode(_toDisplayString((_ctx._.provides[__nuxt_route_symbol] || _ctx.$route).path), 1 /* TEXT */)
|
||||
]))
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should correctly inject route in options api', async () => {
|
||||
const sfc = `
|
||||
<template>{{ thing }}</template>
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
thing () {
|
||||
return this.$route.path
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
`
|
||||
|
||||
const res = compileScript(parse(sfc).descriptor, { id: 'test.vue' })
|
||||
const transformResult = await transform(res.content)
|
||||
expect(transformResult).toMatchInlineSnapshot(`
|
||||
"import { PageRouteSymbol as __nuxt_route_symbol } from '#app/components/injections';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
thing () {
|
||||
return (this._.provides[__nuxt_route_symbol] || this.$route).path
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
@ -182,7 +182,7 @@ describe('treeshake client only in ssr', () => {
|
||||
expect(treeshaken).not.toContain('ssrRenderComponent(_unref(HelloWorld')
|
||||
expect(treeshaken).toContain('ssrRenderComponent(_unref(Glob')
|
||||
}
|
||||
expect(treeshaken.replace(/data-v-[\d\w]{8}/g, 'data-v-one-hash').replace(/scoped=[\d\w]{8}/g, 'scoped=one-hash')).toMatchSnapshot()
|
||||
expect(treeshaken.replace(/data-v-\w{8}/g, 'data-v-one-hash').replace(/scoped=\w{8}/g, 'scoped=one-hash')).toMatchSnapshot()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -35,16 +35,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/telemetry": "2.5.4",
|
||||
"@nuxt/ui-templates": "1.3.3",
|
||||
"@nuxt/ui-templates": "1.3.4",
|
||||
"@types/file-loader": "5.0.4",
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/sass-loader": "8.0.8",
|
||||
"@unhead/schema": "1.9.10",
|
||||
"@unhead/schema": "1.9.11",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "3.1.0",
|
||||
"@vue/compiler-core": "3.4.27",
|
||||
"@vue/compiler-sfc": "3.4.27",
|
||||
"@vue/language-core": "2.0.16",
|
||||
"@vue/language-core": "2.0.19",
|
||||
"c12": "1.10.0",
|
||||
"esbuild-loader": "4.1.0",
|
||||
"h3": "1.11.1",
|
||||
@ -54,7 +54,7 @@
|
||||
"unbuild": "latest",
|
||||
"unctx": "2.3.1",
|
||||
"unenv": "1.9.0",
|
||||
"vite": "5.2.11",
|
||||
"vite": "5.2.12",
|
||||
"vue": "3.4.27",
|
||||
"vue-bundle-renderer": "2.1.0",
|
||||
"vue-loader": "17.4.2",
|
||||
@ -71,7 +71,7 @@
|
||||
"scule": "^1.3.0",
|
||||
"std-env": "^3.7.0",
|
||||
"ufo": "^1.5.3",
|
||||
"unimport": "^3.7.1",
|
||||
"unimport": "^3.7.2",
|
||||
"uncrypto": "^0.1.3",
|
||||
"untyped": "^1.4.2"
|
||||
},
|
||||
|
@ -45,7 +45,17 @@ export default defineUntypedSchema({
|
||||
/**
|
||||
* The base path of your Nuxt application.
|
||||
*
|
||||
* This can be set at runtime by setting the NUXT_APP_BASE_URL environment variable.
|
||||
* For example:
|
||||
* @example
|
||||
* ```ts
|
||||
* export default defineNuxtConfig({
|
||||
* app: {
|
||||
* baseURL: '/prefix/'
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* This can also be set at runtime by setting the NUXT_APP_BASE_URL environment variable.
|
||||
* @example
|
||||
* ```bash
|
||||
* NUXT_APP_BASE_URL=/prefix/ node .output/server/index.mjs
|
||||
@ -63,6 +73,16 @@ export default defineUntypedSchema({
|
||||
/**
|
||||
* An absolute URL to serve the public folder from (production-only).
|
||||
*
|
||||
* For example:
|
||||
* @example
|
||||
* ```ts
|
||||
* export default defineNuxtConfig({
|
||||
* app: {
|
||||
* cdnURL: 'https://mycdn.org/'
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* This can be set to a different value at runtime by setting the `NUXT_APP_CDN_URL` environment variable.
|
||||
* @example
|
||||
* ```bash
|
||||
@ -241,6 +261,7 @@ export default defineUntypedSchema({
|
||||
/**
|
||||
* Boolean or a path to an HTML file with the contents of which will be inserted into any HTML page
|
||||
* rendered with `ssr: false`.
|
||||
*
|
||||
* - If it is unset, it will use `~/app/spa-loading-template.html` file in one of your layers, if it exists.
|
||||
* - If it is false, no SPA loading indicator will be loaded.
|
||||
* - If true, Nuxt will look for `~/app/spa-loading-template.html` file in one of your layers, or a
|
||||
|
@ -154,11 +154,23 @@ export default defineUntypedSchema({
|
||||
$resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || '.nuxt'),
|
||||
},
|
||||
|
||||
/**
|
||||
* For multi-app projects, the unique name of the Nuxt application.
|
||||
*/
|
||||
appId: {
|
||||
$resolve: (val: string) => val ?? 'nuxt-app',
|
||||
},
|
||||
|
||||
/**
|
||||
* A unique identifier matching the build. This may contain the hash of the current state of the project.
|
||||
*/
|
||||
buildId: {
|
||||
$resolve: (val: string) => val ?? randomUUID(),
|
||||
$resolve: async (val: string | undefined, get): Promise<string> => {
|
||||
if (typeof val === 'string') { return val }
|
||||
|
||||
const [isDev, isTest] = await Promise.all([get('dev') as Promise<boolean>, get('test') as Promise<boolean>])
|
||||
return isDev ? 'dev' : isTest ? 'test' : randomUUID()
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
@ -236,7 +248,8 @@ export default defineUntypedSchema({
|
||||
*
|
||||
* Nuxt tries to resolve each item in the modules array using node require path
|
||||
* (in `node_modules`) and then will be resolved from project `srcDir` if `~` alias is used.
|
||||
* @note Modules are executed sequentially so the order is important.
|
||||
* @note Modules are executed sequentially so the order is important. First, the modules defined in `nuxt.config.ts` are loaded. Then, modules found in the `modules/`
|
||||
* directory are executed, and they load in alphabetical order.
|
||||
* @example
|
||||
* ```js
|
||||
* modules: [
|
||||
@ -527,11 +540,12 @@ export default defineUntypedSchema({
|
||||
*/
|
||||
runtimeConfig: {
|
||||
$resolve: async (val: RuntimeConfig, get): Promise<Record<string, unknown>> => {
|
||||
const app = await get('app') as Record<string, string>
|
||||
const [app, buildId] = await Promise.all([get('app') as Promise<Record<string, string>>, get('buildId') as Promise<string>])
|
||||
provideFallbackValues(val)
|
||||
return defu(val, {
|
||||
public: {},
|
||||
app: {
|
||||
buildId,
|
||||
baseURL: app.baseURL,
|
||||
buildAssetsDir: app.buildAssetsDir,
|
||||
cdnURL: app.cdnURL,
|
||||
|
@ -29,6 +29,7 @@ export default defineUntypedSchema({
|
||||
* compileTemplate: true,
|
||||
* templateUtils: true,
|
||||
* relativeWatchPaths: true,
|
||||
* resetAsyncDataToUndefined: true,
|
||||
* defaults: {
|
||||
* useAsyncData: {
|
||||
* deep: true
|
||||
@ -45,6 +46,11 @@ export default defineUntypedSchema({
|
||||
* @type {3 | 4}
|
||||
*/
|
||||
compatibilityVersion: 3,
|
||||
/**
|
||||
* This enables early access to the experimental multi-app support.
|
||||
* @see [Nuxt Issue #21635](https://github.com/nuxt/nuxt/issues/21635)
|
||||
*/
|
||||
multiApp: false,
|
||||
/**
|
||||
* This enables 'Bundler' module resolution mode for TypeScript, which is the recommended setting
|
||||
* for frameworks like Nuxt and Vite.
|
||||
@ -136,8 +142,18 @@ export default defineUntypedSchema({
|
||||
/**
|
||||
* Tree shakes contents of client-only components from server bundle.
|
||||
* @see [Nuxt PR #5750](https://github.com/nuxt/framework/pull/5750)
|
||||
* @deprecated This option will no longer be configurable in Nuxt v4
|
||||
*/
|
||||
treeshakeClientOnly: true,
|
||||
treeshakeClientOnly: {
|
||||
async $resolve (val, get) {
|
||||
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
|
||||
if (isV4 && val === false) {
|
||||
console.warn('Enabling `experimental.treeshakeClientOnly` in v4 compatibility mode as it will no longer be configurable in Nuxt v4.')
|
||||
return true
|
||||
}
|
||||
return val ?? true
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Emit `app:chunkError` hook when there is an error loading vite/webpack
|
||||
@ -246,19 +262,51 @@ export default defineUntypedSchema({
|
||||
/**
|
||||
* Config schema support
|
||||
* @see [Nuxt Issue #15592](https://github.com/nuxt/nuxt/issues/15592)
|
||||
* @deprecated This option will no longer be configurable in Nuxt v4
|
||||
*/
|
||||
configSchema: true,
|
||||
configSchema: {
|
||||
async $resolve (val, get) {
|
||||
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
|
||||
if (isV4 && val === false) {
|
||||
console.warn('Enabling `experimental.configSchema` in v4 compatibility mode as it will no longer be configurable in Nuxt v4.')
|
||||
return true
|
||||
}
|
||||
return val ?? true
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether or not to add a compatibility layer for modules, plugins or user code relying on the old
|
||||
* `@vueuse/head` API.
|
||||
*
|
||||
* This can be disabled for most Nuxt sites to reduce the client-side bundle by ~0.5kb.
|
||||
* This is disabled to reduce the client-side bundle by ~0.5kb.
|
||||
* @deprecated This feature will be removed in Nuxt v4.
|
||||
*/
|
||||
polyfillVueUseHead: false,
|
||||
polyfillVueUseHead: {
|
||||
async $resolve (val, get) {
|
||||
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
|
||||
if (isV4 && val === true) {
|
||||
console.warn('Disabling `experimental.polyfillVueUseHead` in v4 compatibility mode as it will no longer be configurable in Nuxt v4.')
|
||||
return false
|
||||
}
|
||||
return val ?? false
|
||||
},
|
||||
},
|
||||
|
||||
/** Allow disabling Nuxt SSR responses by setting the `x-nuxt-no-ssr` header. */
|
||||
respectNoSSRHeader: false,
|
||||
/**
|
||||
* Allow disabling Nuxt SSR responses by setting the `x-nuxt-no-ssr` header.
|
||||
* @deprecated This feature will be removed in Nuxt v4.
|
||||
*/
|
||||
respectNoSSRHeader: {
|
||||
async $resolve (val, get) {
|
||||
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
|
||||
if (isV4 && val === true) {
|
||||
console.warn('Disabling `experimental.respectNoSSRHeader` in v4 compatibility mode as it will no longer be configurable in Nuxt v4.')
|
||||
return false
|
||||
}
|
||||
return val ?? false
|
||||
},
|
||||
},
|
||||
|
||||
/** Resolve `~`, `~~`, `@` and `@@` aliases located within layers with respect to their layer source and root directories. */
|
||||
localLayerAliases: true,
|
||||
@ -295,8 +343,10 @@ export default defineUntypedSchema({
|
||||
|
||||
/**
|
||||
* Use new experimental head optimisations:
|
||||
*
|
||||
* - Add the capo.js head plugin in order to render tags in of the head in a more performant way.
|
||||
* - Uses the hash hydration plugin to reduce initial hydration
|
||||
*
|
||||
* @see [Nuxt Discussion #22632](https://github.com/nuxt/nuxt/discussions/22632]
|
||||
*/
|
||||
headNext: true,
|
||||
@ -345,7 +395,11 @@ export default defineUntypedSchema({
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
sharedPrerenderData: false,
|
||||
sharedPrerenderData: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
|
||||
@ -368,6 +422,18 @@ export default defineUntypedSchema({
|
||||
* Options that apply to `useAsyncData` (and also therefore `useFetch`)
|
||||
*/
|
||||
useAsyncData: {
|
||||
/** @type {'undefined' | 'null'} */
|
||||
value: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'undefined' : 'null')
|
||||
},
|
||||
},
|
||||
/** @type {'undefined' | 'null'} */
|
||||
errorValue: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'undefined' : 'null')
|
||||
},
|
||||
},
|
||||
deep: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? !((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
|
||||
@ -429,5 +495,15 @@ export default defineUntypedSchema({
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion !== 4)
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether `clear` and `clearNuxtData` should reset async data to its _default_ value or update
|
||||
* it to `null`/`undefined`.
|
||||
*/
|
||||
resetAsyncDataToUndefined: {
|
||||
async $resolve (val, get) {
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion !== 4)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -46,11 +46,13 @@ export default defineUntypedSchema({
|
||||
* Nitro server handlers.
|
||||
*
|
||||
* Each handler accepts the following options:
|
||||
*
|
||||
* - handler: The path to the file defining the handler.
|
||||
* - route: The route under which the handler is available. This follows the conventions of https://github.com/unjs/radix3.
|
||||
* - method: The HTTP method of requests that should be handled.
|
||||
* - middleware: Specifies whether it is a middleware handler.
|
||||
* - lazy: Specifies whether to use lazy loading to import the handler.
|
||||
*
|
||||
* @see https://nuxt.com/docs/guide/directory-structure/server
|
||||
* @note Files from `server/api`, `server/middleware` and `server/routes` will be automatically registered by Nuxt.
|
||||
* @example
|
||||
|
@ -41,6 +41,7 @@ export default defineUntypedSchema({
|
||||
'ofetch',
|
||||
// Key nuxt dependencies
|
||||
'@unhead/vue',
|
||||
'@nuxt/devtools',
|
||||
'vue',
|
||||
'@vue/runtime-core',
|
||||
'@vue/compiler-sfc',
|
||||
|
@ -157,7 +157,11 @@ export default defineUntypedSchema({
|
||||
* See https://github.com/esbuild-kit/esbuild-loader
|
||||
* @type {Omit<typeof import('esbuild-loader')['LoaderOptions'], 'loader'>}
|
||||
*/
|
||||
esbuild: {},
|
||||
esbuild: {
|
||||
jsxFactory: 'h',
|
||||
jsxFragment: 'Fragment',
|
||||
tsconfigRaw: '{}',
|
||||
},
|
||||
|
||||
/**
|
||||
* See: https://github.com/webpack-contrib/file-loader#options
|
||||
|
@ -23,7 +23,7 @@ export const DevRenderingPlugin = () => {
|
||||
const messages = JSON.parse(await fsp.readFile(r(page, 'messages.json'), 'utf-8'))
|
||||
|
||||
return template(contents, {
|
||||
interpolate: /{{{?([\s\S]+?)}?}}/g,
|
||||
interpolate: /\{\{\{?([\s\S]+?)\}?\}\}/g,
|
||||
})({
|
||||
messages: { ...genericMessages, ...messages },
|
||||
})
|
||||
|
@ -58,7 +58,7 @@ export const RenderPlugin = () => {
|
||||
}
|
||||
|
||||
// Inline our scripts
|
||||
const scriptSources = Array.from(html.matchAll(/<script[^>]*src="(.*)"[^>]*>[\s\S]*?<\/script>/g))
|
||||
const scriptSources = Array.from(html.matchAll(/<script[^>]*src="([^"]*)"[^>]*>[\s\S]*?<\/script>/g))
|
||||
.filter(([_block, src]) => src?.match(/^\/.*\.js$/))
|
||||
|
||||
for (const [scriptBlock, src] of scriptSources) {
|
||||
@ -79,10 +79,10 @@ export const RenderPlugin = () => {
|
||||
const messages = JSON.parse(readFileSync(r(`templates/${templateName}/messages.json`), 'utf-8'))
|
||||
|
||||
// Serialize into a js function
|
||||
const chunks = html.split(/\{{2,3}\s*[^{}]+\s*\}{2,3}/g).map(chunk => JSON.stringify(chunk))
|
||||
const chunks = html.split(/\{{2,3}[^{}]+\}{2,3}/g).map(chunk => JSON.stringify(chunk))
|
||||
const hasMessages = chunks.length > 1
|
||||
let templateString = chunks.shift()
|
||||
for (const expression of html.matchAll(/\{{2,3}(\s*[^{}]+\s*)\}{2,3}/g)) {
|
||||
for (const expression of html.matchAll(/\{{2,3}([^{}]+)\}{2,3}/g)) {
|
||||
templateString += ` + (${expression[1].trim()}) + ${chunks.shift()}`
|
||||
}
|
||||
if (chunks.length > 0) {
|
||||
@ -98,28 +98,28 @@ export const RenderPlugin = () => {
|
||||
].join('\n')
|
||||
|
||||
const templateContent = html
|
||||
.match(/<body.*?>([\s\S]*)<\/body>/)?.[0]
|
||||
.replace(/(?<=<|<\/)body/g, 'div')
|
||||
.match(/<body[^>]*>([\s\S]*)<\/body>/)?.[0]
|
||||
.replace(/(?<=<\/|<)body/g, 'div')
|
||||
.replace(/messages\./g, '')
|
||||
.replace(/<script[^>]*>([\s\S]*?)<\/script>/g, '')
|
||||
.replace(/<a href="(\/[^"]*)"([^>]*)>([\s\S]*)<\/a>/g, '<NuxtLink to="$1"$2>\n$3\n</NuxtLink>')
|
||||
|
||||
.replace(/<([^>]+) ([a-z]+)="([^"]*)({{\s*(\w+?)\s*}})([^"]*)"([^>]*)>/g, '<$1 :$2="`$3${$5}$6`"$7>')
|
||||
.replace(/>{{\s*(\w+?)\s*}}<\/[\w-]*>/g, ' v-text="$1" />')
|
||||
.replace(/>{{{\s*(\w+?)\s*}}}<\/[\w-]*>/g, ' v-html="$1" />')
|
||||
.replace(/<([^>]+) ([a-z]+)="([^"]*)(\{\{\s*(\w+)\s*\}\})([^"]*)"([^>]*)>/g, '<$1 :$2="`$3${$5}$6`"$7>')
|
||||
.replace(/>\{\{\s*(\w+)\s*\}\}<\/[\w-]*>/g, ' v-text="$1" />')
|
||||
.replace(/>\{\{\{\s*(\w+)\s*\}\}\}<\/[\w-]*>/g, ' v-html="$1" />')
|
||||
// We are not matching <link> <script> and <meta> tags as these aren't used yet in nuxt/ui
|
||||
// and should be taken care of wherever this SFC is used
|
||||
const title = html.match(/<title.*?>([\s\S]*)<\/title>/)?.[1].replace(/{{([\s\S]+?)}}/g, (r) => {
|
||||
const title = html.match(/<title[^>]*>([\s\S]*)<\/title>/)?.[1].replace(/\{\{([\s\S]+?)\}\}/g, (r) => {
|
||||
return `\${${r.slice(2, -2)}}`.replace(/messages\./g, 'props.')
|
||||
})
|
||||
const styleContent = Array.from(html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)).map(block => block[1]).join('\n')
|
||||
const globalStyles = styleContent.replace(/(\.[^{\d][^{]*?\{[^}]*?\})+.?/g, (r) => {
|
||||
const globalStyles = styleContent.replace(/(\.[^{\d][^{]*\{[^}]*\})+.?/g, (r) => {
|
||||
const lastChar = r[r.length - 1]
|
||||
if (lastChar && !['}', '.', '@', '*', ':'].includes(lastChar)) {
|
||||
return ';' + lastChar
|
||||
}
|
||||
return lastChar
|
||||
}).replace(/@media[^{]*?\{\}/g, '')
|
||||
}).replace(/@media[^{]*\{\}/g, '')
|
||||
const inlineScripts = Array.from(html.matchAll(/<script>([\s\S]*?)<\/script>/g))
|
||||
.map(block => block[1])
|
||||
.filter(i => !i.includes('const t=document.createElement("link")'))
|
||||
|
@ -20,9 +20,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-minifier": "4.0.5",
|
||||
"@unocss/reset": "0.60.0",
|
||||
"@unocss/reset": "0.60.3",
|
||||
"critters": "0.0.22",
|
||||
"execa": "9.0.1",
|
||||
"execa": "9.1.0",
|
||||
"globby": "14.0.1",
|
||||
"html-minifier": "4.0.0",
|
||||
"jiti": "1.21.0",
|
||||
@ -30,7 +30,7 @@
|
||||
"pathe": "1.1.2",
|
||||
"prettier": "3.2.5",
|
||||
"scule": "1.3.0",
|
||||
"unocss": "0.60.0",
|
||||
"vite": "5.2.11"
|
||||
"unocss": "0.60.3",
|
||||
"vite": "5.2.12"
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user