diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 1b57ec4a8a..3c387296d5 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:lts@sha256:1f097426a7ddd1c5d0eacfe0402fdf91e38e4ecc37d23780428f6b87145ad2aa
+FROM node:lts@sha256:99981c3d1aac0d98cd9f03f74b92dddf30f30ffb0b34e6df8bd96283f62f12c6
RUN apt-get update && \
apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \
diff --git a/.github/workflows/autofix-docs.yml b/.github/workflows/autofix-docs.yml
index 221468868a..90d2c5900c 100644
--- a/.github/workflows/autofix-docs.yml
+++ b/.github/workflows/autofix-docs.yml
@@ -21,7 +21,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@@ -33,4 +33,4 @@ jobs:
- name: Lint (docs)
run: pnpm lint:docs:fix
- - uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
+ - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml
index ef7cffdf16..2deae847ee 100644
--- a/.github/workflows/autofix.yml
+++ b/.github/workflows/autofix.yml
@@ -17,14 +17,14 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Check engine ranges, peer dependency ranges and installed versions
- run: pnpm installed-check -d --fix
+ run: pnpm installed-check --no-include-workspace-root --ignore-dev --workspace-ignore='test/**,playground' --fix
- name: Build (stub)
run: pnpm dev:prepare
@@ -55,4 +55,4 @@ jobs:
- name: Lint (code)
run: pnpm lint:fix
- - uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
+ - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 3fe89901fd..9ca68fbbb3 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -33,7 +33,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
index 23f47acac0..9440a6b164 100644
--- a/.github/workflows/changelog.yml
+++ b/.github/workflows/changelog.yml
@@ -28,7 +28,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6fc0803c60..a848c52ea0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -41,7 +41,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 20
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@@ -56,11 +56,8 @@ jobs:
- name: Build
run: pnpm build
- - name: Check types
- run: pnpm test:attw
-
- name: Cache dist
- uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
retention-days: 3
name: dist
@@ -81,7 +78,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL
- uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
+ uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with:
config: |
paths:
@@ -98,7 +95,7 @@ jobs:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
+ uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with:
category: "/language:${{ matrix.language }}"
@@ -118,7 +115,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@@ -149,7 +146,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@@ -161,6 +158,9 @@ jobs:
- name: Lint
run: pnpm lint
+ - name: Check built types
+ run: pnpm test:attw
+
test-unit:
# autofix workflow will be triggered instead for PRs
if: github.event_name == 'push'
@@ -173,7 +173,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@@ -260,20 +260,18 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
- build-release:
+ release-nightly:
concurrency:
group: release
permissions:
id-token: write
if: |
github.event_name == 'push' &&
- github.repository == 'nuxt/nuxt' &&
+ github.repository_owner == 'nuxt' &&
!contains(github.event.head_commit.message, '[skip-release]') &&
!startsWith(github.event.head_commit.message, 'docs')
needs:
- - lint
- build
- - test-fixtures
runs-on: ubuntu-latest
timeout-minutes: 20
@@ -284,7 +282,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@@ -299,22 +297,13 @@ jobs:
- name: Release Edge
run: ./scripts/release-edge.sh ${{ github.ref == 'refs/heads/main' && 'latest' || '3x' }}
env:
- NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
+ NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
NPM_CONFIG_PROVENANCE: true
release-pr:
- concurrency:
- group: release
- permissions:
- id-token: write
- pull-requests: write
- if: |
- github.event_name == 'pull_request' &&
- contains(github.event.pull_request.labels.*.name, '🧷 edge release')
+ if: github.repository_owner == 'nuxt' && github.event_name != 'push'
needs:
- - lint
- build
- - test-fixtures
runs-on: ubuntu-latest
timeout-minutes: 20
@@ -325,7 +314,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@@ -337,8 +326,4 @@ jobs:
name: dist
path: packages
- - name: Release Edge
- run: ./scripts/release-edge.sh pr-${{ github.event.issue.number }}
- env:
- NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
- NPM_CONFIG_PROVENANCE: true
+ - run: pnpm pkg-pr-new publish --compact './packages/kit' './packages/nuxt' './packages/rspack' './packages/schema' './packages/vite' './packages/webpack'
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 342573e564..08bbd42e7b 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -25,7 +25,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
diff --git a/.github/workflows/lint-monorepo.yml b/.github/workflows/lint-monorepo.yml
index 3e7930da4b..046f3fe709 100644
--- a/.github/workflows/lint-monorepo.yml
+++ b/.github/workflows/lint-monorepo.yml
@@ -1,4 +1,4 @@
-name: CI
+name: ci
on:
push:
@@ -29,7 +29,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 22
+ node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@@ -39,4 +39,4 @@ jobs:
run: pnpm sherif -r multiple-dependency-versions
- name: Check engine ranges, peer dependency ranges and installed versions
- run: pnpm installed-check -d
+ run: pnpm installed-check --no-include-workspace-root --ignore-dev --workspace-ignore='test/**,playground'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c0b74d25c2..34494d2c6c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,7 +25,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
- node-version: 20
+ node-version: lts/*
registry-url: "https://registry.npmjs.org/"
cache: "pnpm"
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index cd4b3fc9af..21ee3efc2e 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
- uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
+ uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
if: github.repository == 'nuxt/nuxt' && success()
with:
name: SARIF file
@@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
+ uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
if: github.repository == 'nuxt/nuxt' && success()
with:
sarif_file: results.sarif
diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml
index d563f5045f..41e53837bc 100644
--- a/.github/workflows/semantic-pull-requests.yml
+++ b/.github/workflows/semantic-pull-requests.yml
@@ -17,7 +17,7 @@ jobs:
statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
if: github.repository == 'nuxt/nuxt' && !startsWith(github.head_ref, 'v')
runs-on: ubuntu-latest
- name: Semantic pull request
+ name: semantic-pr
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
diff --git a/docs/1.getting-started/11.testing.md b/docs/1.getting-started/11.testing.md
index 60d0b8db18..97bf358b2d 100644
--- a/docs/1.getting-started/11.testing.md
+++ b/docs/1.getting-started/11.testing.md
@@ -174,6 +174,7 @@ Under the hood, `mountSuspended` wraps `mount` from `@vue/test-utils`, so you ca
For example:
```ts twoslash
+// @noErrors
import { it, expect } from 'vitest'
import type { Component } from 'vue'
declare module '#components' {
@@ -194,6 +195,7 @@ it('can mount some component', async () => {
```
```ts twoslash
+// @noErrors
import { it, expect } from 'vitest'
// ---cut---
// tests/components/SomeComponents.nuxt.spec.ts
@@ -225,6 +227,7 @@ The passed in component will be rendered inside a `
Examples:
```ts twoslash
+// @noErrors
import { it, expect } from 'vitest'
import type { Component } from 'vue'
declare module '#components' {
@@ -243,6 +246,7 @@ it('can render some component', async () => {
```
```ts twoslash
+// @noErrors
import { it, expect } from 'vitest'
// ---cut---
// tests/App.nuxt.spec.ts
diff --git a/docs/1.getting-started/6.data-fetching.md b/docs/1.getting-started/6.data-fetching.md
index f8c48b3759..c82f5ca2f8 100644
--- a/docs/1.getting-started/6.data-fetching.md
+++ b/docs/1.getting-started/6.data-fetching.md
@@ -202,6 +202,19 @@ const { data: discounts, status } = await useAsyncData('cart-discount', async ()
```
+::note
+`useAsyncData` is for fetching and caching data, not triggering side effects like calling Pinia actions, as this can cause unintended behavior such as repeated executions with nullish values. If you need to trigger side effects, use the [`callOnce`](/docs/api/utils/call-once) utility to do so.
+
+```vue
+
+```
+::
+
::read-more{to="/docs/api/composables/use-async-data"}
Read more about `useAsyncData`.
::
diff --git a/docs/2.guide/2.directory-structure/1.composables.md b/docs/2.guide/2.directory-structure/1.composables.md
index ed96746656..c728be0fb7 100644
--- a/docs/2.guide/2.directory-structure/1.composables.md
+++ b/docs/2.guide/2.directory-structure/1.composables.md
@@ -71,7 +71,7 @@ export const useFoo = () => {
### Access plugin injections
-You can access [plugin injections](/docs/guide/directory-structure/plugins#automatically-providing-helpers) from composables:
+You can access [plugin injections](/docs/guide/directory-structure/plugins#providing-helpers) from composables:
```js [composables/test.ts]
export const useHello = () => {
diff --git a/docs/3.api/2.composables/use-async-data.md b/docs/3.api/2.composables/use-async-data.md
index 7619dd1eb7..6f422d19e5 100644
--- a/docs/3.api/2.composables/use-async-data.md
+++ b/docs/3.api/2.composables/use-async-data.md
@@ -62,14 +62,23 @@ const { data: posts } = await useAsyncData(
## Params
- `key`: a unique key to ensure that data fetching can be properly de-duplicated across requests. If you do not provide a key, then a key that is unique to the file name and line number of the instance of `useAsyncData` will be generated for you.
-- `handler`: an asynchronous function that must return a truthy value (for example, it should not be `undefined` or `null`) or the request may be duplicated on the client side
+- `handler`: an asynchronous function that must return a truthy value (for example, it should not be `undefined` or `null`) or the request may be duplicated on the client side.
+::warning
+The `handler` function should be **side-effect free** to ensure predictable behavior during SSR and CSR hydration. If you need to trigger side effects, use the [`callOnce`](/docs/api/utils/call-once) utility to do so.
+::
- `options`:
- `server`: whether to fetch the data on the server (defaults to `true`)
- `lazy`: whether to resolve the async function after loading the route, instead of blocking client-side navigation (defaults to `false`)
- `immediate`: when set to `false`, will prevent the request from firing immediately. (defaults to `true`)
- `default`: a factory function to set the default value of the `data`, before the async function resolves - useful with the `lazy: true` or `immediate: false` option
- `transform`: a function that can be used to alter `handler` function result after resolving
- - `getCachedData`: Provide a function which returns cached data. A _null_ or _undefined_ return value will trigger a fetch. By default, this is: `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]`, which only caches data when `payloadExtraction` is enabled.
+ - `getCachedData`: Provide a function which returns cached data. A `null` or `undefined` return value will trigger a fetch. By default, this is:
+ ```ts
+ const getDefaultCachedData = (key) => nuxtApp.isHydrating
+ ? nuxtApp.payload.data[key]
+ : nuxtApp.static.data[key]
+ ```
+ Which only caches data when `experimental.payloadExtraction` of `nuxt.config` is enabled.
- `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: watch reactive sources to auto-refresh
- `deep`: return data in a deep ref object. It is `false` by default to return data in a shallow ref object for performance.
@@ -94,7 +103,13 @@ Learn how to use `transform` and `getCachedData` to avoid superfluous calls to a
- `data`: the result of the asynchronous function that is passed in.
- `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function.
- `error`: an error object if the data fetching failed.
-- `status`: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`).
+- `status`: a string indicating the status of the data request:
+ - `idle`: when the request has not started, such as:
+ - when `execute` has not yet been called and `{ immediate: false }` is set
+ - when rendering HTML on the server and `{ server: false }` is set
+ - `pending`: the request is in progress
+ - `success`: the request has completed successfully
+ - `error`: the request has failed
- `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `status` to `'idle'`, and mark any currently pending requests as cancelled.
By default, Nuxt waits until a `refresh` is finished before it can be executed again.
diff --git a/docs/3.api/2.composables/use-fetch.md b/docs/3.api/2.composables/use-fetch.md
index 038082ab65..6f6e522620 100644
--- a/docs/3.api/2.composables/use-fetch.md
+++ b/docs/3.api/2.composables/use-fetch.md
@@ -109,7 +109,13 @@ All fetch options can be given a `computed` or `ref` value. These will be watche
- `immediate`: when set to `false`, will prevent the request from firing immediately. (defaults to `true`)
- `default`: a factory function to set the default value of the `data`, before the async function resolves - useful with the `lazy: true` or `immediate: false` option
- `transform`: a function that can be used to alter `handler` function result after resolving
- - `getCachedData`: Provide a function which returns cached data. A _null_ or _undefined_ return value will trigger a fetch. By default, this is: `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]`, which only caches data when `payloadExtraction` is enabled.
+ - `getCachedData`: Provide a function which returns cached data. A `null` or `undefined` return value will trigger a fetch. By default, this is:
+ ```ts
+ const getDefaultCachedData = (key) => nuxtApp.isHydrating
+ ? nuxtApp.payload.data[key]
+ : nuxtApp.static.data[key]
+ ```
+ Which only caches data when `experimental.payloadExtraction` of `nuxt.config` is enabled.
- `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: watch an array of reactive sources and auto-refresh the fetch result when they change. Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`. Together with `immediate: false`, this allows for a fully-manual `useFetch`. (You can [see an example here](/docs/getting-started/data-fetching#watch) of using `watch`.)
- `deep`: return data in a deep ref object. It is `false` by default to return data in a shallow ref object for performance.
@@ -134,7 +140,13 @@ Learn how to use `transform` and `getCachedData` to avoid superfluous calls to a
- `data`: the result of the asynchronous function that is passed in.
- `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function.
- `error`: an error object if the data fetching failed.
-- `status`: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`).
+- `status`: a string indicating the status of the data request:
+ - `idle`: when the request has not started, such as:
+ - when `execute` has not yet been called and `{ immediate: false }` is set
+ - when rendering HTML on the server and `{ server: false }` is set
+ - `pending`: the request is in progress
+ - `success`: the request has completed successfully
+ - `error`: the request has failed
- `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `status` to `'idle'`, and mark any currently pending requests as cancelled.
By default, Nuxt waits until a `refresh` is finished before it can be executed again.
@@ -147,7 +159,7 @@ If you have not fetched data on the server (for example, with `server: false`),
```ts [Signature]
function useFetch(
- url: string | Request | Ref | (() => string) | Request,
+ url: string | Request | Ref | (() => string | Request),
options?: UseFetchOptions
): Promise>
diff --git a/docs/3.api/2.composables/use-head-safe.md b/docs/3.api/2.composables/use-head-safe.md
index 2d4b74e34a..4df11b5fb9 100644
--- a/docs/3.api/2.composables/use-head-safe.md
+++ b/docs/3.api/2.composables/use-head-safe.md
@@ -4,7 +4,7 @@ description: The recommended way to provide head data with user input.
links:
- label: Source
icon: i-simple-icons-github
- to: https://github.com/unjs/unhead/blob/main/packages/unhead/src/composables/useHeadSafe.ts
+ to: https://github.com/unjs/unhead/blob/main/packages/vue/src/composables/useHeadSafe.ts
size: xs
---
diff --git a/docs/3.api/2.composables/use-head.md b/docs/3.api/2.composables/use-head.md
index 43036a5e84..19eabfc7bd 100644
--- a/docs/3.api/2.composables/use-head.md
+++ b/docs/3.api/2.composables/use-head.md
@@ -4,7 +4,7 @@ description: useHead customizes the head properties of individual pages of your
links:
- label: Source
icon: i-simple-icons-github
- to: https://github.com/unjs/unhead/blob/main/packages/unhead/src/composables/useHead.ts
+ to: https://github.com/unjs/unhead/blob/main/packages/vue/src/composables/useHead.ts
size: xs
---
diff --git a/docs/3.api/2.composables/use-nuxt-data.md b/docs/3.api/2.composables/use-nuxt-data.md
index a5d3c99016..fc77d5b875 100644
--- a/docs/3.api/2.composables/use-nuxt-data.md
+++ b/docs/3.api/2.composables/use-nuxt-data.md
@@ -9,11 +9,27 @@ links:
---
::note
-`useNuxtData` gives you access to the current cached value of [`useAsyncData`](/docs/api/composables/use-async-data) , `useLazyAsyncData`, [`useFetch`](/docs/api/composables/use-fetch) and [`useLazyFetch`](/docs/api/composables/use-lazy-fetch) with explicitly provided key.
+`useNuxtData` gives you access to the current cached value of [`useAsyncData`](/docs/api/composables/use-async-data) , [`useLazyAsyncData`](/docs/api/composables/use-lazy-async-data), [`useFetch`](/docs/api/composables/use-fetch) and [`useLazyFetch`](/docs/api/composables/use-lazy-fetch) with explicitly provided key.
::
## Usage
+The `useNuxtData` composable is used to access the current cached value of data-fetching composables such as `useAsyncData`, `useLazyAsyncData`, `useFetch`, and `useLazyFetch`. By providing the key used during the data fetch, you can retrieve the cached data and use it as needed.
+
+This is particularly useful for optimizing performance by reusing already-fetched data or implementing features like Optimistic Updates or cascading data updates.
+
+To use `useNuxtData`, ensure that the data-fetching composable (`useFetch`, `useAsyncData`, etc.) has been called with an explicitly provided key.
+
+## Params
+
+- `key`: The unique key that identifies the cached data. This key should match the one used during the original data fetch.
+
+## Return Values
+
+- `data`: A reactive reference to the cached data associated with the provided key. If no cached data exists, the value will be `null`. This `Ref` automatically updates if the cached data changes, allowing seamless reactivity in your components.
+
+## Example
+
The example below shows how you can use cached data as a placeholder while the most recent data is being fetched from the server.
```vue [pages/posts.vue]
@@ -26,13 +42,15 @@ const { data } = await useFetch('/api/posts', { key: 'posts' })
```vue [pages/posts/[id\\].vue]
@@ -40,7 +58,9 @@ const { data } = useLazyFetch(`/api/posts/${id}`, {
## Optimistic Updates
-We can leverage the cache to update the UI after a mutation, while the data is being invalidated in the background.
+The example below shows how implementing Optimistic Updates can be achieved using useNuxtData.
+
+Optimistic Updates is a technique where the user interface is updated immediately, assuming a server operation will succeed. If the operation eventually fails, the UI is rolled back to its previous state.
```vue [pages/todos.vue]
```
diff --git a/docs/3.api/2.composables/use-seo-meta.md b/docs/3.api/2.composables/use-seo-meta.md
index f3b9e95e63..750aaa3453 100644
--- a/docs/3.api/2.composables/use-seo-meta.md
+++ b/docs/3.api/2.composables/use-seo-meta.md
@@ -4,7 +4,7 @@ description: The useSeoMeta composable lets you define your site's SEO meta tags
links:
- label: Source
icon: i-simple-icons-github
- to: https://github.com/unjs/unhead/blob/main/packages/unhead/src/composables/useSeoMeta.ts
+ to: https://github.com/unjs/unhead/blob/main/packages/vue/src/composables/useSeoMeta.ts
size: xs
---
diff --git a/docs/3.api/2.composables/use-server-seo-meta.md b/docs/3.api/2.composables/use-server-seo-meta.md
index 6b1b2db9fb..25fb71026b 100644
--- a/docs/3.api/2.composables/use-server-seo-meta.md
+++ b/docs/3.api/2.composables/use-server-seo-meta.md
@@ -4,7 +4,7 @@ description: The useServerSeoMeta composable lets you define your site's SEO met
links:
- label: Source
icon: i-simple-icons-github
- to: https://github.com/unjs/unhead/blob/main/packages/unhead/src/composables/useServerSeoMeta.ts
+ to: https://github.com/unjs/unhead/blob/main/packages/vue/src/composables/useServerSeoMeta.ts
size: xs
---
diff --git a/docs/3.api/4.commands/build-module.md b/docs/3.api/4.commands/build-module.md
index ce53933c03..6725485823 100644
--- a/docs/3.api/4.commands/build-module.md
+++ b/docs/3.api/4.commands/build-module.md
@@ -4,7 +4,7 @@ description: 'Nuxt command to build your Nuxt module before publishing.'
links:
- label: Source
icon: i-simple-icons-github
- to: https://github.com/nuxt/cli/blob/main/src/commands/build-module.ts
+ to: https://github.com/nuxt/module-builder/blob/main/src/cli.ts
size: xs
---
diff --git a/docs/5.community/6.roadmap.md b/docs/5.community/6.roadmap.md
index c659fd0e55..95362bd084 100644
--- a/docs/5.community/6.roadmap.md
+++ b/docs/5.community/6.roadmap.md
@@ -63,7 +63,7 @@ Each active version has its own nightly releases which are generated automatical
Release | | Initial release | End Of Life | Docs
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
-**4.x** (scheduled) | | 2024 Q3 | |
+**4.x** (scheduled) | | approximately 1 month after release of nitro v3 | |
**3.x** (stable) | | 2022-11-16 | TBA | [nuxt.com](/docs)
**2.x** (unsupported) | | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs)
**1.x** (unsupported) | | 2018-01-08 | 2019-09-21 |
diff --git a/package.json b/package.json
index 68b10501ce..0ac6c21cf0 100644
--- a/package.json
+++ b/package.json
@@ -39,12 +39,12 @@
"@nuxt/schema": "workspace:*",
"@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
- "@types/node": "22.10.5",
- "@unhead/dom": "1.11.14",
- "@unhead/schema": "1.11.14",
- "@unhead/shared": "1.11.14",
- "@unhead/ssr": "1.11.14",
- "@unhead/vue": "1.11.14",
+ "@types/node": "22.10.6",
+ "@unhead/dom": "1.11.18",
+ "@unhead/schema": "1.11.18",
+ "@unhead/shared": "1.11.18",
+ "@unhead/ssr": "1.11.18",
+ "@unhead/vue": "1.11.18",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13",
@@ -55,62 +55,64 @@
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxt": "workspace:*",
"ohash": "1.1.4",
- "postcss": "8.4.49",
- "rollup": "4.30.0",
+ "postcss": "8.5.1",
+ "rollup": "4.30.1",
"send": ">=1.1.0",
- "typescript": "5.7.2",
+ "typescript": "5.7.3",
"ufo": "1.5.4",
- "unbuild": "3.2.0",
- "unhead": "1.11.14",
- "unimport": "3.14.5",
+ "unbuild": "3.3.1",
+ "unhead": "1.11.18",
+ "unimport": "3.14.6",
"vite": "6.0.7",
"vue": "3.5.13"
},
"devDependencies": {
- "@arethetypeswrong/cli": "0.17.2",
- "@nuxt/eslint-config": "0.7.4",
+ "@arethetypeswrong/cli": "0.17.3",
+ "@nuxt/cli": "3.20.0",
+ "@nuxt/eslint-config": "0.7.5",
"@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*",
- "@nuxt/test-utils": "3.15.1",
+ "@nuxt/test-utils": "3.15.4",
"@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0",
- "@types/node": "22.10.5",
+ "@types/node": "22.10.6",
"@types/semver": "7.5.8",
- "@unhead/schema": "1.11.14",
- "@unhead/vue": "1.11.14",
+ "@unhead/schema": "1.11.18",
+ "@unhead/vue": "1.11.18",
"@vitest/coverage-v8": "2.1.8",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20",
"case-police": "0.7.2",
"changelogen": "0.5.7",
- "consola": "3.3.3",
+ "consola": "3.4.0",
"cssnano": "7.0.6",
"destr": "2.0.3",
"devalue": "5.1.1",
- "eslint": "9.17.0",
+ "eslint": "9.18.0",
"eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "4.6.0",
- "eslint-typegen": "0.3.2",
+ "eslint-typegen": "1.0.0",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
- "happy-dom": "16.3.0",
+ "happy-dom": "16.6.0",
"installed-check": "9.3.0",
"jiti": "2.4.2",
- "knip": "5.41.1",
+ "knip": "5.42.1",
"markdownlint-cli": "0.43.0",
- "memfs": "4.15.3",
+ "memfs": "4.17.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
- "nuxi": "3.18.2",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.2",
"ofetch": "1.4.1",
- "pathe": "2.0.0",
+ "pathe": "2.0.1",
+ "pkg-pr-new": "0.0.39",
"playwright-core": "1.49.1",
+ "rollup": "4.30.1",
"semver": "7.6.3",
"sherif": "1.1.1",
"std-env": "3.8.0",
"tinyexec": "0.3.2",
"tinyglobby": "0.2.10",
- "typescript": "5.7.2",
+ "typescript": "5.7.3",
"ufo": "1.5.4",
"vitest": "2.1.8",
"vitest-environment-nuxt": "1.0.1",
@@ -118,9 +120,6 @@
"vue-tsc": "2.2.0",
"webpack": "5.97.1"
},
- "packageManager": "pnpm@9.15.3",
- "engines": {
- "node": "^18.20.4 || ^20.9.0 || ^22.0.0 || >=23.0.0"
- },
+ "packageManager": "pnpm@9.15.4",
"version": ""
}
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 78bb5d437e..ecd8334e74 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -29,35 +29,36 @@
"dependencies": {
"@nuxt/schema": "workspace:*",
"c12": "^2.0.1",
- "consola": "^3.3.3",
+ "consola": "^3.4.0",
"defu": "^6.1.4",
"destr": "^2.0.3",
"errx": "^0.1.0",
"globby": "^14.0.2",
- "ignore": "^7.0.0",
+ "ignore": "^7.0.3",
"jiti": "^2.4.2",
"klona": "^2.0.6",
- "mlly": "^1.7.3",
+ "mlly": "^1.7.4",
"ohash": "^1.1.4",
- "pathe": "^2.0.0",
- "pkg-types": "^1.3.0",
+ "pathe": "^2.0.1",
+ "pkg-types": "^1.3.1",
"scule": "^1.3.0",
"semver": "^7.6.3",
+ "std-env": "^3.8.0",
"ufo": "^1.5.4",
"unctx": "^2.4.1",
- "unimport": "^3.14.5",
+ "unimport": "^3.14.6",
"untyped": "^1.5.2"
},
"devDependencies": {
"@rspack/core": "1.1.8",
"@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
- "unbuild": "3.2.0",
+ "unbuild": "3.3.1",
"vite": "6.0.7",
"vitest": "2.1.8",
"webpack": "5.97.1"
},
"engines": {
- "node": ">=18.20.5"
+ "node": ">=18.0.0"
}
}
diff --git a/packages/kit/src/build.ts b/packages/kit/src/build.ts
index ba4a1d17ba..0f3bb8f48c 100644
--- a/packages/kit/src/build.ts
+++ b/packages/kit/src/build.ts
@@ -114,7 +114,7 @@ export function addWebpackPlugin (pluginOrGetter: WebpackPluginInstance | Webpac
const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter
- config.plugins = config.plugins || []
+ config.plugins ||= []
config.plugins[method](...toArray(plugin))
}, options)
}
@@ -126,7 +126,7 @@ export function addRspackPlugin (pluginOrGetter: RspackPluginInstance | RspackPl
const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter
- config.plugins = config.plugins || []
+ config.plugins ||= []
config.plugins[method](...toArray(plugin))
}, options)
}
@@ -139,7 +139,7 @@ export function addVitePlugin (pluginOrGetter: VitePlugin | VitePlugin[] | (() =
const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter
- config.plugins = config.plugins || []
+ config.plugins ||= []
config.plugins[method](...toArray(plugin))
}, options)
}
diff --git a/packages/kit/src/components.ts b/packages/kit/src/components.ts
index 82394e3811..b10fd98f16 100644
--- a/packages/kit/src/components.ts
+++ b/packages/kit/src/components.ts
@@ -11,7 +11,7 @@ import { MODE_RE } from './utils'
export async function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) {
const nuxt = useNuxt()
await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt)
- nuxt.options.components = nuxt.options.components || []
+ nuxt.options.components ||= []
dir.priority ||= 0
nuxt.hook('components:dirs', (dirs) => { dirs[opts.prepend ? 'unshift' : 'push'](dir) })
}
@@ -26,7 +26,7 @@ export type AddComponentOptions = { name: string, filePath: string } & Partial=2.13' }, nuxt)
- nuxt.options.components = nuxt.options.components || []
+ nuxt.options.components ||= []
if (!opts.mode) {
const [, mode = 'all'] = opts.filePath.match(MODE_RE) || []
diff --git a/packages/kit/src/loader/config.ts b/packages/kit/src/loader/config.ts
index 80139fb015..74652c9b43 100644
--- a/packages/kit/src/loader/config.ts
+++ b/packages/kit/src/loader/config.ts
@@ -58,7 +58,7 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise()
for (const layer of layers) {
// Resolve `rootDir` & `srcDir` of layers
- layer.config = layer.config || {}
+ layer.config ||= {}
layer.config.rootDir = layer.config.rootDir ?? layer.cwd!
// Only process/resolve layers once
diff --git a/packages/kit/src/loader/nuxt.ts b/packages/kit/src/loader/nuxt.ts
index 892286f5b6..71e60a908e 100644
--- a/packages/kit/src/loader/nuxt.ts
+++ b/packages/kit/src/loader/nuxt.ts
@@ -16,7 +16,7 @@ export interface LoadNuxtOptions extends LoadNuxtConfigOptions {
export async function loadNuxt (opts: LoadNuxtOptions): Promise {
// Backward compatibility
opts.cwd = resolve(opts.cwd || (opts as any).rootDir /* backwards compat */ || '.')
- opts.overrides = opts.overrides || (opts as any).config as NuxtConfig /* backwards compat */ || {}
+ opts.overrides ||= (opts as any).config as NuxtConfig /* backwards compat */ || {}
// Apply dev as config override
opts.overrides.dev = !!opts.dev
diff --git a/packages/kit/src/module/define.ts b/packages/kit/src/module/define.ts
index b0f30d80fb..4624692527 100644
--- a/packages/kit/src/module/define.ts
+++ b/packages/kit/src/module/define.ts
@@ -87,7 +87,7 @@ function _defineNuxtModule<
// Avoid duplicate installs
const uniqueKey = module.meta.name || module.meta.configKey
if (uniqueKey) {
- nuxt.options._requiredModules = nuxt.options._requiredModules || {}
+ nuxt.options._requiredModules ||= {}
if (nuxt.options._requiredModules[uniqueKey]) {
return false
}
diff --git a/packages/kit/src/module/install.ts b/packages/kit/src/module/install.ts
index 3eda24a871..8b1348dbd2 100644
--- a/packages/kit/src/module/install.ts
+++ b/packages/kit/src/module/install.ts
@@ -44,7 +44,7 @@ export async function installModule<
}
}
- nuxt.options._installedModules = nuxt.options._installedModules || []
+ nuxt.options._installedModules ||= []
const entryPath = typeof moduleToInstall === 'string' ? resolveAlias(moduleToInstall) : undefined
if (typeof moduleToInstall === 'string' && entryPath !== moduleToInstall) {
@@ -95,31 +95,28 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
paths.add(nuxtModule)
for (const path of paths) {
- for (const parentURL of nuxt.options.modulesDir) {
- try {
- const src = isAbsolute(path)
- ? pathToFileURL(await resolvePath(path, { cwd: parentURL, fallbackToOriginal: false, extensions: nuxt.options.extensions })).href
- : await resolveModule(path, { url: pathToFileURL(parentURL.replace(/\/node_modules\/?$/, '')), extensions: nuxt.options.extensions })
+ try {
+ const src = isAbsolute(path)
+ ? pathToFileURL(await resolvePath(path, { fallbackToOriginal: false, extensions: nuxt.options.extensions })).href
+ : await resolveModule(path, { url: nuxt.options.modulesDir.map(m => pathToFileURL(m.replace(/\/node_modules\/?$/, ''))), extensions: nuxt.options.extensions })
- nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
- resolvedModulePath = fileURLToPath(new URL(src))
+ nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
+ resolvedModulePath = fileURLToPath(new URL(src))
- // nuxt-module-builder generates a module.json with metadata including the version
- const moduleMetadataPath = new URL('module.json', src)
- if (existsSync(moduleMetadataPath)) {
- buildTimeModuleMeta = JSON.parse(await fsp.readFile(moduleMetadataPath, 'utf-8'))
- }
- break
- } catch (error: unknown) {
- const code = (error as Error & { code?: string }).code
- if (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || code === 'ENOTDIR') {
- continue
- }
- logger.error(`Error while importing module \`${nuxtModule}\`: ${error}`)
- throw error
+ // nuxt-module-builder generates a module.json with metadata including the version
+ const moduleMetadataPath = new URL('module.json', src)
+ if (existsSync(moduleMetadataPath)) {
+ buildTimeModuleMeta = JSON.parse(await fsp.readFile(moduleMetadataPath, 'utf-8'))
}
+ break
+ } catch (error: unknown) {
+ const code = (error as Error & { code?: string }).code
+ if (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || code === 'ENOTDIR') {
+ continue
+ }
+ logger.error(`Error while importing module \`${nuxtModule}\`: ${error}`)
+ throw error
}
- if (typeof nuxtModule !== 'string') { break }
}
}
diff --git a/packages/kit/src/nitro.ts b/packages/kit/src/nitro.ts
index 4c046f6a9d..361c3c7872 100644
--- a/packages/kit/src/nitro.ts
+++ b/packages/kit/src/nitro.ts
@@ -40,7 +40,7 @@ export function addDevServerHandler (handler: NitroDevEventHandler) {
*/
export function addServerPlugin (plugin: string) {
const nuxt = useNuxt()
- nuxt.options.nitro.plugins = nuxt.options.nitro.plugins || []
+ nuxt.options.nitro.plugins ||= []
nuxt.options.nitro.plugins.push(normalize(plugin))
}
@@ -89,8 +89,8 @@ export function useNitro (): Nitro {
export function addServerImports (imports: Import[]) {
const nuxt = useNuxt()
nuxt.hook('nitro:config', (config) => {
- config.imports = config.imports || {}
- config.imports.imports = config.imports.imports || []
+ config.imports ||= {}
+ config.imports.imports ||= []
config.imports.imports.push(...imports)
})
}
@@ -102,8 +102,8 @@ export function addServerImportsDir (dirs: string | string[], opts: { prepend?:
const nuxt = useNuxt()
const _dirs = toArray(dirs)
nuxt.hook('nitro:config', (config) => {
- config.imports = config.imports || {}
- config.imports.dirs = config.imports.dirs || []
+ config.imports ||= {}
+ config.imports.dirs ||= []
config.imports.dirs[opts.prepend ? 'unshift' : 'push'](..._dirs)
})
}
@@ -115,7 +115,7 @@ export function addServerImportsDir (dirs: string | string[], opts: { prepend?:
export function addServerScanDir (dirs: string | string[], opts: { prepend?: boolean } = {}) {
const nuxt = useNuxt()
nuxt.hook('nitro:config', (config) => {
- config.scanDirs = config.scanDirs || []
+ config.scanDirs ||= []
for (const dir of toArray(dirs)) {
config.scanDirs[opts.prepend ? 'unshift' : 'push'](dir)
diff --git a/packages/kit/src/pages.ts b/packages/kit/src/pages.ts
index 35c73fae83..dbb153487e 100644
--- a/packages/kit/src/pages.ts
+++ b/packages/kit/src/pages.ts
@@ -20,9 +20,7 @@ export interface ExtendRouteRulesOptions {
export function extendRouteRules (route: string, rule: NitroRouteConfig, options: ExtendRouteRulesOptions = {}) {
const nuxt = useNuxt()
for (const opts of [nuxt.options, nuxt.options.nitro]) {
- if (!opts.routeRules) {
- opts.routeRules = {}
- }
+ opts.routeRules ||= {}
opts.routeRules[route] = options.override
? defu(rule, opts.routeRules[route])
: defu(opts.routeRules[route], rule)
diff --git a/packages/kit/src/plugin.ts b/packages/kit/src/plugin.ts
index 116721214f..26ae9b70dc 100644
--- a/packages/kit/src/plugin.ts
+++ b/packages/kit/src/plugin.ts
@@ -1,13 +1,19 @@
+import { existsSync } from 'node:fs'
+import { isAbsolute } from 'node:path'
+import { pathToFileURL } from 'node:url'
import { normalize } from 'pathe'
import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema'
-import { useNuxt } from './context'
+import { resolvePathSync } from 'mlly'
+import { isWindows } from 'std-env'
+import { MODE_RE, filterInPlace } from './utils'
+import { tryUseNuxt, useNuxt } from './context'
import { addTemplate } from './template'
import { resolveAlias } from './resolve'
-import { MODE_RE } from './utils'
/**
* Normalize a nuxt plugin object
*/
+const pluginSymbol = Symbol.for('nuxt plugin')
export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
// Normalize src
if (typeof plugin === 'string') {
@@ -16,6 +22,10 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin = { ...plugin }
}
+ if (pluginSymbol in plugin) {
+ return plugin
+ }
+
if (!plugin.src) {
throw new Error('Invalid plugin. src option is required: ' + JSON.stringify(plugin))
}
@@ -23,6 +33,14 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
// Normalize full path to plugin
plugin.src = normalize(resolveAlias(plugin.src))
+ if (!existsSync(plugin.src) && isAbsolute(plugin.src)) {
+ try {
+ plugin.src = resolvePathSync(isWindows ? pathToFileURL(plugin.src).href : plugin.src, { extensions: tryUseNuxt()?.options.extensions })
+ } catch {
+ // ignore errors as the file may be in the nuxt vfs
+ }
+ }
+
// Normalize mode
if (plugin.ssr) {
plugin.mode = 'server'
@@ -32,6 +50,9 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin.mode = mode as 'all' | 'client' | 'server'
}
+ // @ts-expect-error not adding symbol to types to avoid conflicts
+ plugin[pluginSymbol] = true
+
return plugin
}
@@ -61,7 +82,7 @@ export function addPlugin (_plugin: NuxtPlugin | string, opts: AddPluginOptions
const plugin = normalizePlugin(_plugin)
// Remove any existing plugin with the same src
- nuxt.options.plugins = nuxt.options.plugins.filter(p => normalizePlugin(p).src !== plugin.src)
+ filterInPlace(nuxt.options.plugins, p => normalizePlugin(p).src !== plugin.src)
// Prepend to array by default to be before user provided plugins since is usually used by modules
nuxt.options.plugins[opts.append ? 'push' : 'unshift'](plugin)
diff --git a/packages/kit/src/template.ts b/packages/kit/src/template.ts
index d471a7c1eb..9bb78f0f28 100644
--- a/packages/kit/src/template.ts
+++ b/packages/kit/src/template.ts
@@ -8,6 +8,7 @@ import type { TSConfig } from 'pkg-types'
import { gte } from 'semver'
import { readPackageJSON } from 'pkg-types'
+import { filterInPlace } from './utils'
import { tryResolveModule } from './internal/esm'
import { getDirectory } from './module/install'
import { tryUseNuxt, useNuxt } from './context'
@@ -23,7 +24,7 @@ export function addTemplate (_template: NuxtTemplate | string) {
const template = normalizeTemplate(_template)
// Remove any existing template with the same destination path
- nuxt.options.build.templates = nuxt.options.build.templates.filter(p => normalizeTemplate(p).dst !== template.dst)
+ filterInPlace(nuxt.options.build.templates, p => normalizeTemplate(p).dst !== template.dst)
// Add to templates array
nuxt.options.build.templates.push(template)
@@ -229,9 +230,9 @@ export async function _generateTypes (nuxt: Nuxt) {
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
: nuxt.options.buildDir
- tsConfig.compilerOptions = tsConfig.compilerOptions || {}
- tsConfig.compilerOptions.paths = tsConfig.compilerOptions.paths || {}
- tsConfig.include = tsConfig.include || []
+ tsConfig.compilerOptions ||= {}
+ tsConfig.compilerOptions.paths ||= {}
+ tsConfig.include ||= []
for (const alias in aliases) {
if (excludedAlias.some(re => re.test(alias))) {
@@ -291,6 +292,10 @@ export async function _generateTypes (nuxt: Nuxt) {
}))
}
+ // Ensure `#build` is placed at the end of the paths object.
+ // https://github.com/nuxt/nuxt/issues/30325
+ sortTsPaths(tsConfig.compilerOptions.paths)
+
tsConfig.include = [...new Set(tsConfig.include.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
tsConfig.exclude = [...new Set(tsConfig.exclude!.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
@@ -330,6 +335,17 @@ export async function writeTypes (nuxt: Nuxt) {
await writeFile()
}
+function sortTsPaths (paths: Record) {
+ for (const pathKey in paths) {
+ if (pathKey.startsWith('#build')) {
+ const pathValue = paths[pathKey]!
+ // Delete & Reassign to ensure key is inserted at the end of object.
+ delete paths[pathKey]
+ paths[pathKey] = pathValue
+ }
+ }
+}
+
function renderAttrs (obj: Record) {
const attrs: string[] = []
for (const key in obj) {
diff --git a/packages/kit/src/utils.ts b/packages/kit/src/utils.ts
index 89fa591c50..0816bcf5cd 100644
--- a/packages/kit/src/utils.ts
+++ b/packages/kit/src/utils.ts
@@ -3,4 +3,19 @@ export function toArray (value: T | T[]): T[] {
return Array.isArray(value) ? value : [value]
}
+/**
+ * Filter out items from an array in place. This function mutates the array.
+ * `predicate` get through the array from the end to the start for performance.
+ *
+ * This function should be faster than `Array.prototype.filter` on large arrays.
+ */
+export function filterInPlace (array: T[], predicate: (item: T, index: number, arr: T[]) => unknown) {
+ for (let i = array.length; i--; i >= 0) {
+ if (!predicate(array[i]!, i, array)) {
+ array.splice(i, 1)
+ }
+ }
+ return array
+}
+
export const MODE_RE = /\.(server|client)(\.\w+)*$/
diff --git a/packages/kit/test/generate-types.spec.ts b/packages/kit/test/generate-types.spec.ts
index d65d6809bd..b01811e131 100644
--- a/packages/kit/test/generate-types.spec.ts
+++ b/packages/kit/test/generate-types.spec.ts
@@ -59,4 +59,42 @@ describe('tsConfig generation', () => {
]
`)
})
+
+ it('should add #build after #components to paths', async () => {
+ const { tsConfig } = await _generateTypes(mockNuxtWithOptions({
+ alias: {
+ '~': '/my-app',
+ '@': '/my-app',
+ 'some-custom-alias': '/my-app/some-alias',
+ '#build': './build-dir',
+ '#build/*': './build-dir/*',
+ '#imports': './imports',
+ '#components': './components',
+ },
+ }))
+
+ expect(tsConfig.compilerOptions?.paths).toMatchObject({
+ '~': [
+ '..',
+ ],
+ 'some-custom-alias': [
+ '../some-alias',
+ ],
+ '@': [
+ '..',
+ ],
+ '#imports': [
+ './imports',
+ ],
+ '#components': [
+ './components',
+ ],
+ '#build': [
+ './build-dir',
+ ],
+ '#build/*': [
+ './build-dir/*',
+ ],
+ })
+ })
})
diff --git a/packages/nuxt/bin/nuxt.mjs b/packages/nuxt/bin/nuxt.mjs
index 7faab34846..8c0237f4ee 100755
--- a/packages/nuxt/bin/nuxt.mjs
+++ b/packages/nuxt/bin/nuxt.mjs
@@ -1,2 +1,2 @@
#!/usr/bin/env node
-import 'nuxi/cli'
+import '@nuxt/cli/cli'
diff --git a/packages/nuxt/build.config.ts b/packages/nuxt/build.config.ts
index 1f8a82cf20..4301b165fb 100644
--- a/packages/nuxt/build.config.ts
+++ b/packages/nuxt/build.config.ts
@@ -22,7 +22,7 @@ export default defineBuildConfig({
},
},
dependencies: [
- 'nuxi',
+ '@nuxt/cli',
'vue-router',
'ofetch',
],
diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json
index 7ed7b6cf80..23a8ba012b 100644
--- a/packages/nuxt/package.json
+++ b/packages/nuxt/package.json
@@ -64,22 +64,23 @@
"test:attw": "attw --pack"
},
"dependencies": {
+ "@nuxt/cli": "^3.20.0",
"@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.7.0",
"@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.4",
"@nuxt/vite-builder": "workspace:*",
- "@unhead/dom": "^1.11.14",
- "@unhead/shared": "^1.11.14",
- "@unhead/ssr": "^1.11.14",
- "@unhead/vue": "^1.11.14",
+ "@unhead/dom": "^1.11.18",
+ "@unhead/shared": "^1.11.18",
+ "@unhead/ssr": "^1.11.18",
+ "@unhead/vue": "^1.11.18",
"@vue/shared": "^3.5.13",
"acorn": "8.14.0",
"c12": "^2.0.1",
"chokidar": "^4.0.3",
"compatx": "^0.1.8",
- "consola": "^3.3.3",
+ "consola": "^3.4.0",
"cookie-es": "^1.2.2",
"defu": "^6.1.4",
"destr": "^2.0.3",
@@ -91,35 +92,34 @@
"globby": "^14.0.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hookable": "^5.5.3",
- "ignore": "^7.0.0",
+ "ignore": "^7.0.3",
"impound": "^0.2.0",
"jiti": "^2.4.2",
"klona": "^2.0.6",
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
- "mlly": "^1.7.3",
+ "mlly": "^1.7.4",
"nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
- "nuxi": "^3.18.2",
"nypm": "^0.4.1",
"ofetch": "^1.4.1",
"ohash": "^1.1.4",
- "pathe": "^2.0.0",
+ "pathe": "^2.0.1",
"perfect-debounce": "^1.0.0",
- "pkg-types": "^1.3.0",
+ "pkg-types": "^1.3.1",
"radix3": "^1.1.2",
"scule": "^1.3.0",
"semver": "^7.6.3",
"std-env": "^3.8.0",
- "strip-literal": "^2.1.1",
+ "strip-literal": "^3.0.0",
"tinyglobby": "0.2.10",
"ufo": "^1.5.4",
"ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3",
"unctx": "^2.4.1",
"unenv": "^1.10.0",
- "unhead": "^1.11.14",
- "unimport": "^3.14.5",
+ "unhead": "^1.11.18",
+ "unimport": "^3.14.6",
"unplugin": "^2.1.2",
"unplugin-vue-router": "^0.10.9",
"unstorage": "^1.14.4",
@@ -135,7 +135,7 @@
"@types/estree": "1.0.6",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
- "unbuild": "3.2.0",
+ "unbuild": "3.3.1",
"vite": "6.0.7",
"vitest": "2.1.8"
},
diff --git a/packages/nuxt/src/app/components/client-only.ts b/packages/nuxt/src/app/components/client-only.ts
index c65eec1b55..46149f6a79 100644
--- a/packages/nuxt/src/app/components/client-only.ts
+++ b/packages/nuxt/src/app/components/client-only.ts
@@ -95,7 +95,7 @@ export function createClientOnly (component: T) {
if (isPromise(setupState)) {
return Promise.resolve(setupState).then((setupState) => {
if (typeof setupState !== 'function') {
- setupState = setupState || {}
+ setupState ||= {}
setupState.mounted$ = mounted$
return setupState
}
diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts
index 353f8ccda6..c1bbd619cf 100644
--- a/packages/nuxt/src/app/components/nuxt-link.ts
+++ b/packages/nuxt/src/app/components/nuxt-link.ts
@@ -325,10 +325,13 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
const elRef = import.meta.server ? undefined : (ref: any) => { el!.value = props.custom ? ref?.$el?.nextElementSibling : ref?.$el }
function shouldPrefetch (mode: 'visibility' | 'interaction') {
+ if (import.meta.server) { return }
return !prefetched.value && (typeof props.prefetchOn === 'string' ? props.prefetchOn === mode : (props.prefetchOn?.[mode] ?? options.prefetchOn?.[mode])) && (props.prefetch ?? options.prefetch) !== false && props.noPrefetch !== true && props.target !== '_blank' && !isSlowConnection()
}
async function prefetch (nuxtApp = useNuxtApp()) {
+ if (import.meta.server) { return }
+
if (prefetched.value) { return }
prefetched.value = true
@@ -395,12 +398,14 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
// `custom` API cannot support fallthrough attributes as the slot
// may render fragment or text root nodes (#14897, #19375)
if (!props.custom) {
- if (shouldPrefetch('interaction')) {
- routerLinkProps.onPointerenter = prefetch.bind(null, undefined)
- routerLinkProps.onFocus = prefetch.bind(null, undefined)
- }
- if (prefetched.value) {
- routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
+ if (import.meta.client) {
+ if (shouldPrefetch('interaction')) {
+ routerLinkProps.onPointerenter = prefetch.bind(null, undefined)
+ routerLinkProps.onFocus = prefetch.bind(null, undefined)
+ }
+ if (prefetched.value) {
+ routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
+ }
}
routerLinkProps.rel = props.rel || undefined
}
diff --git a/packages/nuxt/src/app/composables/once.ts b/packages/nuxt/src/app/composables/once.ts
index ae8746fff0..7bd3a98c23 100644
--- a/packages/nuxt/src/app/composables/once.ts
+++ b/packages/nuxt/src/app/composables/once.ts
@@ -37,7 +37,7 @@ export async function callOnce (...args: any): Promise {
return
}
- nuxtApp._once = nuxtApp._once || {}
+ nuxtApp._once ||= {}
nuxtApp._once[_key] = nuxtApp._once[_key] || fn() || true
await nuxtApp._once[_key]
nuxtApp.payload.once.add(_key)
diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts
index f61061e5d9..97d318c12c 100644
--- a/packages/nuxt/src/components/module.ts
+++ b/packages/nuxt/src/components/module.ts
@@ -250,7 +250,7 @@ export default defineNuxtModule({
// TODO: refactor this
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
- config.plugins = config.plugins || []
+ config.plugins ||= []
if (isClient && selectiveClient) {
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
@@ -275,7 +275,7 @@ export default defineNuxtModule({
nuxt.hook(key, (configs) => {
configs.forEach((config) => {
const mode = config.name === 'client' ? 'client' : 'server'
- config.plugins = config.plugins || []
+ config.plugins ||= []
if (mode !== 'server') {
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
diff --git a/packages/nuxt/src/core/builder.ts b/packages/nuxt/src/core/builder.ts
index 6b24372b88..cc420dc2bc 100644
--- a/packages/nuxt/src/core/builder.ts
+++ b/packages/nuxt/src/core/builder.ts
@@ -63,8 +63,11 @@ export async function build (nuxt: Nuxt) {
return
}
- if (nuxt.options.dev) {
- checkForExternalConfigurationFiles()
+ if (nuxt.options.dev && !nuxt.options.test) {
+ nuxt.hooks.hookOnce('build:done', () => {
+ checkForExternalConfigurationFiles()
+ .catch(e => logger.warn('Problem checking for external configuration files.', e))
+ })
}
await bundle(nuxt)
diff --git a/packages/nuxt/src/core/features.ts b/packages/nuxt/src/core/features.ts
index 6e22d04bff..d19b4021c0 100644
--- a/packages/nuxt/src/core/features.ts
+++ b/packages/nuxt/src/core/features.ts
@@ -53,7 +53,7 @@ export function installNuxtModule (name: string, options?: EnsurePackageInstalle
installPrompts.add(name)
const nuxt = useNuxt()
return promptToInstall(name, async () => {
- const { runCommand } = await import('nuxi')
+ const { runCommand } = await import('@nuxt/cli')
await runCommand('module', ['add', name, '--cwd', nuxt.options.rootDir])
}, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options })
}
diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts
index 698cb04b3a..085a2d8f80 100644
--- a/packages/nuxt/src/core/nitro.ts
+++ b/packages/nuxt/src/core/nitro.ts
@@ -239,7 +239,11 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Resolve user-provided paths
nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!)
- nitroConfig.ignore = [...(nitroConfig.ignore || []), ...resolveIgnorePatterns(nitroConfig.srcDir), `!${join(nuxt.options.buildDir, 'dist/client', nuxt.options.app.buildAssetsDir, '**/*')}`]
+ nitroConfig.ignore ||= []
+ nitroConfig.ignore.push(
+ ...resolveIgnorePatterns(nitroConfig.srcDir),
+ `!${join(nuxt.options.buildDir, 'dist/client', nuxt.options.app.buildAssetsDir, '**/*')}`,
+ )
// Resolve aliases in user-provided input - so `~/server/test` will work
nitroConfig.plugins = nitroConfig.plugins?.map(plugin => plugin ? resolveAlias(plugin, nuxt.options.alias) : plugin)
@@ -411,14 +415,16 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
const basePath = nitroConfig.typescript!.tsConfig!.compilerOptions?.baseUrl ? resolve(nuxt.options.buildDir, nitroConfig.typescript!.tsConfig!.compilerOptions?.baseUrl) : nuxt.options.buildDir
const aliases = nitroConfig.alias!
const tsConfig = nitroConfig.typescript!.tsConfig!
- tsConfig.compilerOptions = tsConfig.compilerOptions || {}
- tsConfig.compilerOptions.paths = tsConfig.compilerOptions.paths || {}
+ tsConfig.compilerOptions ||= {}
+ tsConfig.compilerOptions.paths ||= {}
for (const _alias in aliases) {
const alias = _alias as keyof typeof aliases
if (excludedAlias.some(pattern => typeof pattern === 'string' ? alias === pattern : pattern.test(alias))) {
continue
}
- if (alias in tsConfig.compilerOptions.paths) { continue }
+ if (alias in tsConfig.compilerOptions.paths) {
+ continue
+ }
const absolutePath = resolve(basePath, aliases[alias]!)
const stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */)
@@ -532,7 +538,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
await writeTypes(nitro)
}
// Exclude nitro output dir from typescript
- opts.tsConfig.exclude = opts.tsConfig.exclude || []
+ opts.tsConfig.exclude ||= []
opts.tsConfig.exclude.push(relative(nuxt.options.buildDir, resolve(nuxt.options.rootDir, nitro.options.output.dir)))
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/nitro.d.ts') })
})
diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts
index 957da98314..46d38059d4 100644
--- a/packages/nuxt/src/core/nuxt.ts
+++ b/packages/nuxt/src/core/nuxt.ts
@@ -81,7 +81,7 @@ const nightlies = {
'@nuxt/kit': '@nuxt/kit-nightly',
}
-const keyDependencies = [
+export const keyDependencies = [
'@nuxt/kit',
'@nuxt/schema',
]
@@ -802,8 +802,13 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise {
const nuxt = createNuxt(options)
- for (const dep of keyDependencies) {
- checkDependencyVersion(dep, nuxt._version)
+ if (nuxt.options.dev && !nuxt.options.test) {
+ nuxt.hooks.hookOnce('build:done', () => {
+ for (const dep of keyDependencies) {
+ checkDependencyVersion(dep, nuxt._version)
+ .catch(e => logger.warn(`Problem checking \`${dep}\` version.`, e))
+ }
+ })
}
// We register hooks layer-by-layer so any overrides need to be registered separately
@@ -822,7 +827,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise {
return nuxt
}
-async function checkDependencyVersion (name: string, nuxtVersion: string): Promise {
+export async function checkDependencyVersion (name: string, nuxtVersion: string): Promise {
const path = await resolvePath(name, { fallbackToOriginal: true }).catch(() => null)
if (!path || path === name) { return }
diff --git a/packages/nuxt/src/core/plugins/import-protection.ts b/packages/nuxt/src/core/plugins/import-protection.ts
index a48dc75d78..635edbfeca 100644
--- a/packages/nuxt/src/core/plugins/import-protection.ts
+++ b/packages/nuxt/src/core/plugins/import-protection.ts
@@ -36,7 +36,7 @@ export const createImportProtectionPatterns = (nuxt: { options: NuxtOptions }, o
])
}
- for (const i of [/(^|node_modules\/)@nuxt\/(kit|test-utils)/, /(^|node_modules\/)nuxi/, /(^|node_modules\/)nitro(?:pack)?(?:-nightly)?(?:$|\/)(?!(?:dist\/)?(?:node_modules|presets|runtime|types))/, /(^|node_modules\/)nuxt\/(config|kit|schema)/]) {
+ for (const i of [/(^|node_modules\/)@nuxt\/(cli|kit|test-utils)/, /(^|node_modules\/)nuxi/, /(^|node_modules\/)nitro(?:pack)?(?:-nightly)?(?:$|\/)(?!(?:dist\/)?(?:node_modules|presets|runtime|types))/, /(^|node_modules\/)nuxt\/(config|kit|schema)/]) {
patterns.push([i, `This module cannot be imported in ${context}.`])
}
diff --git a/packages/nuxt/src/core/plugins/virtual.ts b/packages/nuxt/src/core/plugins/virtual.ts
index e50970c225..2a3f00a3bd 100644
--- a/packages/nuxt/src/core/plugins/virtual.ts
+++ b/packages/nuxt/src/core/plugins/virtual.ts
@@ -38,25 +38,25 @@ export const VirtualFSPlugin = (nuxt: Nuxt, options: VirtualFSPluginOptions) =>
const resolvedId = resolveWithExt(id)
if (resolvedId) {
- return PREFIX + resolvedId
+ return PREFIX + encodeURIComponent(resolvedId)
}
if (importer && RELATIVE_ID_RE.test(id)) {
- const path = resolve(dirname(withoutPrefix(importer)), id)
+ const path = resolve(dirname(withoutPrefix(decodeURIComponent(importer))), id)
const resolved = resolveWithExt(path)
if (resolved) {
- return PREFIX + resolved
+ return PREFIX + encodeURIComponent(resolved)
}
}
},
loadInclude (id) {
- return id.startsWith(PREFIX) && withoutPrefix(id) in nuxt.vfs
+ return id.startsWith(PREFIX) && withoutPrefix(decodeURIComponent(id)) in nuxt.vfs
},
load (id) {
return {
- code: nuxt.vfs[withoutPrefix(id)] || '',
+ code: nuxt.vfs[withoutPrefix(decodeURIComponent(id))] || '',
map: null,
}
},
diff --git a/packages/nuxt/src/core/runtime/nitro/no-ssr.ts b/packages/nuxt/src/core/runtime/nitro/no-ssr.ts
index 556c3f98e5..d6ede3076d 100644
--- a/packages/nuxt/src/core/runtime/nitro/no-ssr.ts
+++ b/packages/nuxt/src/core/runtime/nitro/no-ssr.ts
@@ -2,7 +2,7 @@ import { defineEventHandler, getRequestHeader } from 'h3'
export default defineEventHandler((event) => {
if (getRequestHeader(event, 'x-nuxt-no-ssr')) {
- event.context.nuxt = event.context.nuxt || {}
+ event.context.nuxt ||= {}
event.context.nuxt.noSSR = true
}
})
diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index bed864aeb5..7be579a7b3 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -488,8 +488,8 @@ export default defineRenderHandler(async (event): Promise (node: T): WithLocations {
return node as WithLocations
}
+/**
+ * A function to check whether scope A is a child of scope B.
+ * @example
+ * ```ts
+ * isChildScope('0-1-2', '0-1') // true
+ * isChildScope('0-1', '0-1') // false
+ * ```
+ *
+ * @param a the child scope
+ * @param b the parent scope
+ * @returns true if scope A is a child of scope B, false otherwise (also when they are the same)
+ */
+function isChildScope (a: string, b: string) {
+ return a.startsWith(b) && a.length > b.length
+}
+
abstract class BaseNode {
abstract type: string
+ readonly scope: string
node: WithLocations
- constructor (node: WithLocations) {
+ constructor (node: WithLocations, scope: string) {
this.node = node
+ this.scope = scope
}
/**
@@ -72,6 +90,14 @@ abstract class BaseNode {
* For instance, for a function parameter, this would be the end of the function declaration.
*/
abstract get end (): number
+
+ /**
+ * Check if the node is defined under a specific scope.
+ * @param scope
+ */
+ isUnderScope (scope: string) {
+ return isChildScope(this.scope, scope)
+ }
}
class IdentifierNode extends BaseNode {
@@ -90,8 +116,8 @@ class FunctionParamNode extends BaseNode {
type = 'FunctionParam' as const
fnNode: WithLocations
- constructor (node: WithLocations, fnNode: WithLocations) {
- super(node)
+ constructor (node: WithLocations, scope: string, fnNode: WithLocations) {
+ super(node, scope)
this.fnNode = fnNode
}
@@ -120,8 +146,8 @@ class VariableNode extends BaseNode {
type = 'Variable' as const
variableNode: WithLocations
- constructor (node: WithLocations, variableNode: WithLocations) {
- super(node)
+ constructor (node: WithLocations, scope: string, variableNode: WithLocations) {
+ super(node, scope)
this.variableNode = variableNode
}
@@ -138,8 +164,8 @@ class ImportNode extends BaseNode
- constructor (node: WithLocations, importNode: WithLocations) {
- super(node)
+ constructor (node: WithLocations, scope: string, importNode: WithLocations) {
+ super(node, scope)
this.importNode = importNode
}
@@ -156,8 +182,8 @@ class CatchParamNode extends BaseNode {
type = 'CatchParam' as const
catchNode: WithLocations
- constructor (node: WithLocations, catchNode: WithLocations) {
- super(node)
+ constructor (node: WithLocations, scope: string, catchNode: WithLocations) {
+ super(node, scope)
this.catchNode = catchNode
}
@@ -264,7 +290,7 @@ export class ScopeTracker {
const identifiers = getPatternIdentifiers(param)
for (const identifier of identifiers) {
- this.declareIdentifier(identifier.name, new FunctionParamNode(identifier, fn))
+ this.declareIdentifier(identifier.name, new FunctionParamNode(identifier, this.scopeIndexKey, fn))
}
}
@@ -276,10 +302,10 @@ export class ScopeTracker {
this.declareIdentifier(
identifier.name,
parent.type === 'VariableDeclaration'
- ? new VariableNode(identifier, parent)
+ ? new VariableNode(identifier, this.scopeIndexKey, parent)
: parent.type === 'CatchClause'
- ? new CatchParamNode(identifier, parent)
- : new FunctionParamNode(identifier, parent),
+ ? new CatchParamNode(identifier, this.scopeIndexKey, parent)
+ : new FunctionParamNode(identifier, this.scopeIndexKey, parent),
)
}
}
@@ -295,7 +321,7 @@ export class ScopeTracker {
case 'FunctionDeclaration':
// declare function name for named functions, skip for `export default`
if (node.id?.name) {
- this.declareIdentifier(node.id.name, new FunctionNode(node))
+ this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
}
this.pushScope()
for (const param of node.params) {
@@ -309,7 +335,7 @@ export class ScopeTracker {
this.pushScope()
// can be undefined, for example in class method definitions
if (node.id?.name) {
- this.declareIdentifier(node.id.name, new FunctionNode(node))
+ this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
}
this.pushScope()
@@ -333,7 +359,7 @@ export class ScopeTracker {
case 'ClassDeclaration':
// declare class name for named classes, skip for `export default`
if (node.id?.name) {
- this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id)))
+ this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id), this.scopeIndexKey))
}
break
@@ -342,13 +368,13 @@ export class ScopeTracker {
// e.g. const MyClass = class InternalClassName { // InternalClassName is only available within the class body
this.pushScope()
if (node.id?.name) {
- this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id)))
+ this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id), this.scopeIndexKey))
}
break
case 'ImportDeclaration':
for (const specifier of node.specifiers) {
- this.declareIdentifier(specifier.local.name, new ImportNode(withLocations(specifier), node))
+ this.declareIdentifier(specifier.local.name, new ImportNode(withLocations(specifier), this.scopeIndexKey, node))
}
break
@@ -429,6 +455,26 @@ export class ScopeTracker {
return null
}
+ getCurrentScope () {
+ return this.scopeIndexKey
+ }
+
+ /**
+ * Check if the current scope is a child of a specific scope.
+ * @example
+ * ```ts
+ * // current scope is 0-1
+ * isCurrentScopeUnder('0') // true
+ * isCurrentScopeUnder('0-1') // false
+ * ```
+ *
+ * @param scope the parent scope
+ * @returns `true` if the current scope is a child of the specified scope, `false` otherwise (also when they are the same)
+ */
+ isCurrentScopeUnder (scope: string) {
+ return isChildScope(this.scopeIndexKey, scope)
+ }
+
/**
* Freezes the scope tracker, preventing further declarations.
* It also resets the scope index stack to its initial state, so that the scope tracker can be reused.
diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts
index b5b7d1b0ac..8238f27873 100644
--- a/packages/nuxt/src/pages/module.ts
+++ b/packages/nuxt/src/pages/module.ts
@@ -530,8 +530,8 @@ export default defineNuxtModule({
getContents: () => 'export { START_LOCATION, useRoute } from \'vue-router\'',
})
- nuxt.options.vite.resolve = nuxt.options.vite.resolve || {}
- nuxt.options.vite.resolve.dedupe = nuxt.options.vite.resolve.dedupe || []
+ nuxt.options.vite.resolve ||= {}
+ nuxt.options.vite.resolve.dedupe ||= []
nuxt.options.vite.resolve.dedupe.push('vue-router')
// Add router options template
@@ -642,7 +642,7 @@ if (import.meta.hot) {
for (const route of routes) {
router.addRoute(route)
}
- router.replace('')
+ router.replace(router.currentRoute.value.fullPath)
}
if (routes && 'then' in routes) {
routes.then(addRoutes)
diff --git a/packages/nuxt/src/pages/plugins/page-meta.ts b/packages/nuxt/src/pages/plugins/page-meta.ts
index a5972f4e44..0a6cd606bc 100644
--- a/packages/nuxt/src/pages/plugins/page-meta.ts
+++ b/packages/nuxt/src/pages/plugins/page-meta.ts
@@ -228,6 +228,8 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
if (!meta) { return }
+ const definePageMetaScope = scopeTracker.getCurrentScope()
+
walk(meta, {
scopeTracker,
enter (node, parent) {
@@ -236,10 +238,24 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
|| node.type !== 'Identifier' // checking for `node.type` to narrow down the type
) { return }
+ const declaration = scopeTracker.getDeclaration(node.name)
+ if (declaration) {
+ // check if the declaration was made inside `definePageMeta` and if so, do not process it
+ // (ensures that we don't hoist local variables in inline middleware, for example)
+ if (
+ declaration.isUnderScope(definePageMetaScope)
+ // ensures that we compare the correct declaration to the reference
+ // (when in the same scope, the declaration must come before the reference, otherwise it must be in a parent scope)
+ && (scopeTracker.isCurrentScopeUnder(declaration.scope) || declaration.start < node.start)
+ ) {
+ return
+ }
+ }
+
if (isStaticIdentifier(node.name)) {
addImport(node.name)
- } else {
- processDeclaration(scopeTracker.getDeclaration(node.name))
+ } else if (declaration) {
+ processDeclaration(declaration)
}
},
})
@@ -271,9 +287,9 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
handleHotUpdate: {
order: 'post',
handler: ({ file, modules, server }) => {
- if (options.isPage?.(file)) {
+ if (options.routesPath && options.isPage?.(file)) {
const macroModule = server.moduleGraph.getModuleById(file + '?macro=true')
- const routesModule = server.moduleGraph.getModuleById('virtual:nuxt:' + options.routesPath)
+ const routesModule = server.moduleGraph.getModuleById('virtual:nuxt:' + encodeURIComponent(options.routesPath))
return [
...modules,
...macroModule ? [macroModule] : [],
diff --git a/packages/nuxt/src/pages/runtime/page.ts b/packages/nuxt/src/pages/runtime/page.ts
index 5ba304cd23..3f82ed8393 100644
--- a/packages/nuxt/src/pages/runtime/page.ts
+++ b/packages/nuxt/src/pages/runtime/page.ts
@@ -65,7 +65,7 @@ export default defineComponent({
if (import.meta.dev) {
nuxtApp._isNuxtPageUsed = true
}
-
+ let pageLoadingEndHookAlreadyCalled = false
return () => {
return h(RouterView, { name: props.name, route: props.route, ...attrs }, {
default: (routeProps: RouterViewSlotProps) => {
@@ -99,6 +99,7 @@ export default defineComponent({
const key = generateRouteKey(routeProps, props.pageKey)
if (!nuxtApp.isHydrating && !hasChildrenRoutes(forkRoute, routeProps.route, routeProps.Component) && previousPageKey === key) {
nuxtApp.callHook('page:loading:end')
+ pageLoadingEndHookAlreadyCalled = true
}
previousPageKey = key
@@ -115,7 +116,14 @@ export default defineComponent({
wrapInKeepAlive(keepaliveConfig, h(Suspense, {
suspensible: true,
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
- onResolve: () => { nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).then(() => nuxtApp.callHook('page:loading:end')).finally(done)) },
+ onResolve: () => {
+ nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).then(() => {
+ if (!pageLoadingEndHookAlreadyCalled) {
+ return nuxtApp.callHook('page:loading:end')
+ }
+ pageLoadingEndHookAlreadyCalled = false
+ }).finally(done))
+ },
}, {
default: () => {
const providerVNode = h(RouteProvider, {
diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts
index 7f078eed28..7f52fa09ca 100644
--- a/packages/nuxt/src/pages/utils.ts
+++ b/packages/nuxt/src/pages/utils.ts
@@ -68,7 +68,10 @@ export async function resolvePagesRoutes (nuxt = useNuxt()): Promise
return pages
}
- const augmentCtx = { extraExtractionKeys: nuxt.options.experimental.extraPageMetaExtractionKeys }
+ const augmentCtx = {
+ extraExtractionKeys: nuxt.options.experimental.extraPageMetaExtractionKeys,
+ fullyResolvedPaths: new Set(scannedFiles.map(file => file.absolutePath)),
+ }
if (shouldAugment === 'after-resolve') {
await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs, augmentCtx)
@@ -121,7 +124,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
- const tokens = parseSegment(segment!)
+ const tokens = parseSegment(segment!, file.absolutePath)
// Skip group segments
if (tokens.every(token => token.type === SegmentTokenType.group)) {
@@ -154,6 +157,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
}
interface AugmentPagesContext {
+ fullyResolvedPaths?: Set
pagesToSkip?: Set
augmentedPages?: Set
extraExtractionKeys?: string[]
@@ -163,7 +167,9 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record {
+ const originalPkgTypes = (await og())
+ return {
+ ...originalPkgTypes,
+ readPackageJSON: vi.fn(originalPkgTypes.readPackageJSON),
+ }
+})
+
+afterEach(() => {
+ vi.clearAllMocks()
+})
+
+describe('dependency mismatch', () => {
+ it.sequential('expect mismatched dependency to log a warning', async () => {
+ vi.mocked(readPackageJSON).mockReturnValue(Promise.resolve({
+ version: '3.0.0',
+ }))
+
+ for (const dep of keyDependencies) {
+ await checkDependencyVersion(dep, version)
+ }
+
+ // @nuxt/kit is explicitly installed in repo root but @nuxt/schema isn't, so we only
+ // get warnings about @nuxt/schema
+ 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.`)
+
+ vi.mocked(readPackageJSON).mockRestore()
+ })
+ it.sequential.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,
+ }))
+
+ for (const dep of keyDependencies) {
+ await checkDependencyVersion(dep, version)
+ }
+
+ expect(console.warn).not.toHaveBeenCalled()
+ vi.mocked(readPackageJSON).mockRestore()
+ })
+})
diff --git a/packages/nuxt/test/load-nuxt.test.ts b/packages/nuxt/test/load-nuxt.test.ts
index dffcdf7130..96fd8723cf 100644
--- a/packages/nuxt/test/load-nuxt.test.ts
+++ b/packages/nuxt/test/load-nuxt.test.ts
@@ -2,10 +2,7 @@ import { fileURLToPath } from 'node:url'
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))))
@@ -45,45 +42,3 @@ 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,
- })
-
- // @nuxt/kit is explicitly installed in repo root but @nuxt/schema isn't, so we only
- // get warnings about @nuxt/schema
- 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.`)
-
- 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()
- })
-})
diff --git a/packages/nuxt/test/page-metadata.test.ts b/packages/nuxt/test/page-metadata.test.ts
index 60247fd4e1..ab1c44b46a 100644
--- a/packages/nuxt/test/page-metadata.test.ts
+++ b/packages/nuxt/test/page-metadata.test.ts
@@ -393,6 +393,188 @@ definePageMeta({
`)
})
+ it('should not import static identifiers when shadowed in the same scope', () => {
+ const sfc = `
+
+ `
+ const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
+ expect(transformPlugin.transform.call({
+ parse: (code: string, opts: any = {}) => Parser.parse(code, {
+ sourceType: 'module',
+ ecmaVersion: 'latest',
+ locations: true,
+ ...opts,
+ }),
+ }, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
+ "const __nuxt_page_meta = {
+ middleware: () => {
+ const useState = (key) => ({ value: { isLoggedIn: false } })
+ const auth = useState('auth')
+ if (!auth.value.isLoggedIn) {
+ return navigateTo('/login')
+ }
+ },
+ }
+ export default __nuxt_page_meta"
+ `)
+ })
+
+ it('should not import static identifiers when shadowed in parent scope', () => {
+ const sfc = `
+
+ `
+ const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
+ expect(transformPlugin.transform.call({
+ parse: (code: string, opts: any = {}) => Parser.parse(code, {
+ sourceType: 'module',
+ ecmaVersion: 'latest',
+ locations: true,
+ ...opts,
+ }),
+ }, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
+ "const __nuxt_page_meta = {
+ middleware: () => {
+ function isLoggedIn() {
+ const auth = useState('auth')
+ return auth.value.isLoggedIn
+ }
+
+ const useState = (key) => ({ value: { isLoggedIn: false } })
+ if (!isLoggedIn()) {
+ return navigateTo('/login')
+ }
+ },
+ }
+ export default __nuxt_page_meta"
+ `)
+ })
+
+ it('should import static identifiers when a shadowed and a non-shadowed one is used', () => {
+ const sfc = `
+
+ `
+ const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
+ expect(transformPlugin.transform.call({
+ parse: (code: string, opts: any = {}) => Parser.parse(code, {
+ sourceType: 'module',
+ ecmaVersion: 'latest',
+ locations: true,
+ ...opts,
+ }),
+ }, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
+ "import { useState } from '#app/composables/state'
+
+ const __nuxt_page_meta = {
+ middleware: [
+ () => {
+ const useState = (key) => ({ value: { isLoggedIn: false } })
+ const auth = useState('auth')
+ if (!auth.value.isLoggedIn) {
+ return navigateTo('/login')
+ }
+ },
+ () => {
+ const auth = useState('auth')
+ if (!auth.value.isLoggedIn) {
+ return navigateTo('/login')
+ }
+ }
+ ]
+ }
+ export default __nuxt_page_meta"
+ `)
+ })
+
+ it('should import static identifiers when a shadowed and a non-shadowed one is used in the same scope', () => {
+ const sfc = `
+
+ `
+ const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
+ expect(transformPlugin.transform.call({
+ parse: (code: string, opts: any = {}) => Parser.parse(code, {
+ sourceType: 'module',
+ ecmaVersion: 'latest',
+ locations: true,
+ ...opts,
+ }),
+ }, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
+ "import { useState } from '#app/composables/state'
+
+ const __nuxt_page_meta = {
+ middleware: () => {
+ const auth1 = useState('auth')
+ const useState = (key) => ({ value: { isLoggedIn: false } })
+ const auth2 = useState('auth')
+ if (!auth1.value.isLoggedIn || !auth2.value.isLoggedIn) {
+ return navigateTo('/login')
+ }
+ },
+ }
+ export default __nuxt_page_meta"
+ `)
+ })
+
it('should work with esbuild.keepNames = true', async () => {
const sfc = `