Merge branch 'main' into fix/20667-omit-usefetch-body-for-get-method

This commit is contained in:
Connor van Spronssen 2025-01-16 08:30:44 +01:00 committed by GitHub
commit fe4e742cfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 2718 additions and 1741 deletions

View File

@ -1,4 +1,4 @@
FROM node:lts@sha256:1f097426a7ddd1c5d0eacfe0402fdf91e38e4ecc37d23780428f6b87145ad2aa FROM node:lts@sha256:99981c3d1aac0d98cd9f03f74b92dddf30f30ffb0b34e6df8bd96283f62f12c6
RUN apt-get update && \ 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 && \ 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 && \

View File

@ -21,7 +21,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -33,4 +33,4 @@ jobs:
- name: Lint (docs) - name: Lint (docs)
run: pnpm lint:docs:fix run: pnpm lint:docs:fix
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

@ -17,14 +17,14 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Check engine ranges, peer dependency ranges and installed versions - 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) - name: Build (stub)
run: pnpm dev:prepare run: pnpm dev:prepare
@ -55,4 +55,4 @@ jobs:
- name: Lint (code) - name: Lint (code)
run: pnpm lint:fix run: pnpm lint:fix
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

@ -33,7 +33,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies

View File

@ -28,7 +28,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies

View File

@ -41,7 +41,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -56,11 +56,8 @@ jobs:
- name: Build - name: Build
run: pnpm build run: pnpm build
- name: Check types
run: pnpm test:attw
- name: Cache dist - name: Cache dist
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with: with:
retention-days: 3 retention-days: 3
name: dist name: dist
@ -81,7 +78,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with: with:
config: | config: |
paths: paths:
@ -98,7 +95,7 @@ jobs:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with: with:
category: "/language:${{ matrix.language }}" category: "/language:${{ matrix.language }}"
@ -118,7 +115,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -149,7 +146,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -161,6 +158,9 @@ jobs:
- name: Lint - name: Lint
run: pnpm lint run: pnpm lint
- name: Check built types
run: pnpm test:attw
test-unit: test-unit:
# autofix workflow will be triggered instead for PRs # autofix workflow will be triggered instead for PRs
if: github.event_name == 'push' if: github.event_name == 'push'
@ -173,7 +173,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -260,20 +260,18 @@ jobs:
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
build-release: release-nightly:
concurrency: concurrency:
group: release group: release
permissions: permissions:
id-token: write id-token: write
if: | if: |
github.event_name == 'push' && github.event_name == 'push' &&
github.repository == 'nuxt/nuxt' && github.repository_owner == 'nuxt' &&
!contains(github.event.head_commit.message, '[skip-release]') && !contains(github.event.head_commit.message, '[skip-release]') &&
!startsWith(github.event.head_commit.message, 'docs') !startsWith(github.event.head_commit.message, 'docs')
needs: needs:
- lint
- build - build
- test-fixtures
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
@ -284,7 +282,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -303,18 +301,9 @@ jobs:
NPM_CONFIG_PROVENANCE: true NPM_CONFIG_PROVENANCE: true
release-pr: release-pr:
concurrency: if: github.repository_owner == 'nuxt' && github.event_name != 'push'
group: release
permissions:
id-token: write
pull-requests: write
if: |
github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, '🧷 edge release')
needs: needs:
- lint
- build - build
- test-fixtures
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
@ -325,7 +314,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -337,8 +326,4 @@ jobs:
name: dist name: dist
path: packages path: packages
- name: Release Edge - run: pnpm pkg-pr-new publish --compact './packages/kit' './packages/nuxt' './packages/rspack' './packages/schema' './packages/vite' './packages/webpack'
run: ./scripts/release-edge.sh pr-${{ github.event.issue.number }}
env:
NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
NPM_CONFIG_PROVENANCE: true

View File

@ -25,7 +25,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies

View File

@ -1,4 +1,4 @@
name: CI name: ci
on: on:
push: push:
@ -29,7 +29,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 22 node-version: lts/*
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@ -39,4 +39,4 @@ jobs:
run: pnpm sherif -r multiple-dependency-versions run: pnpm sherif -r multiple-dependency-versions
- name: Check engine ranges, peer dependency ranges and installed 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'

View File

@ -25,7 +25,7 @@ jobs:
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: lts/*
registry-url: "https://registry.npmjs.org/" registry-url: "https://registry.npmjs.org/"
cache: "pnpm" cache: "pnpm"

View File

@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - 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() if: github.repository == 'nuxt/nuxt' && success()
with: with:
name: SARIF file name: SARIF file
@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
if: github.repository == 'nuxt/nuxt' && success() if: github.repository == 'nuxt/nuxt' && success()
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -17,7 +17,7 @@ jobs:
statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
if: github.repository == 'nuxt/nuxt' && !startsWith(github.head_ref, 'v') if: github.repository == 'nuxt/nuxt' && !startsWith(github.head_ref, 'v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Semantic pull request name: semantic-pr
steps: steps:
- name: Validate PR title - name: Validate PR title
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3

View File

@ -174,6 +174,7 @@ Under the hood, `mountSuspended` wraps `mount` from `@vue/test-utils`, so you ca
For example: For example:
```ts twoslash ```ts twoslash
// @noErrors
import { it, expect } from 'vitest' import { it, expect } from 'vitest'
import type { Component } from 'vue' import type { Component } from 'vue'
declare module '#components' { declare module '#components' {
@ -194,6 +195,7 @@ it('can mount some component', async () => {
``` ```
```ts twoslash ```ts twoslash
// @noErrors
import { it, expect } from 'vitest' import { it, expect } from 'vitest'
// ---cut--- // ---cut---
// tests/components/SomeComponents.nuxt.spec.ts // tests/components/SomeComponents.nuxt.spec.ts
@ -225,6 +227,7 @@ The passed in component will be rendered inside a `<div id="test-wrapper"></div>
Examples: Examples:
```ts twoslash ```ts twoslash
// @noErrors
import { it, expect } from 'vitest' import { it, expect } from 'vitest'
import type { Component } from 'vue' import type { Component } from 'vue'
declare module '#components' { declare module '#components' {
@ -243,6 +246,7 @@ it('can render some component', async () => {
``` ```
```ts twoslash ```ts twoslash
// @noErrors
import { it, expect } from 'vitest' import { it, expect } from 'vitest'
// ---cut--- // ---cut---
// tests/App.nuxt.spec.ts // tests/App.nuxt.spec.ts

View File

@ -202,6 +202,19 @@ const { data: discounts, status } = await useAsyncData('cart-discount', async ()
</script> </script>
``` ```
::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
<script setup lang="ts">
const offersStore = useOffersStore()
// you can't do this
await useAsyncData(() => offersStore.getOffer(route.params.slug))
</script>
```
::
::read-more{to="/docs/api/composables/use-async-data"} ::read-more{to="/docs/api/composables/use-async-data"}
Read more about `useAsyncData`. Read more about `useAsyncData`.
:: ::

View File

@ -71,7 +71,7 @@ export const useFoo = () => {
### Access plugin injections ### 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] ```js [composables/test.ts]
export const useHello = () => { export const useHello = () => {

View File

@ -62,14 +62,23 @@ const { data: posts } = await useAsyncData(
## Params ## 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. - `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`: - `options`:
- `server`: whether to fetch the data on the server (defaults to `true`) - `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`) - `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`) - `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 - `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 - `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 - `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: watch reactive sources to auto-refresh - `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. - `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. - `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. - `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. - `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. - `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. By default, Nuxt waits until a `refresh` is finished before it can be executed again.

View File

@ -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`) - `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 - `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 - `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 - `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`.) - `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. - `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. - `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. - `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. - `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. - `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. 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] ```ts [Signature]
function useFetch<DataT, ErrorT>( function useFetch<DataT, ErrorT>(
url: string | Request | Ref<string | Request> | (() => string) | Request, url: string | Request | Ref<string | Request> | (() => string | Request),
options?: UseFetchOptions<DataT> options?: UseFetchOptions<DataT>
): Promise<AsyncData<DataT, ErrorT>> ): Promise<AsyncData<DataT, ErrorT>>

View File

@ -4,7 +4,7 @@ description: The recommended way to provide head data with user input.
links: links:
- label: Source - label: Source
icon: i-simple-icons-github 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 size: xs
--- ---

View File

@ -4,7 +4,7 @@ description: useHead customizes the head properties of individual pages of your
links: links:
- label: Source - label: Source
icon: i-simple-icons-github 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 size: xs
--- ---

View File

@ -9,11 +9,27 @@ links:
--- ---
::note ::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 ## 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. 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] ```vue [pages/posts.vue]
@ -26,13 +42,15 @@ const { data } = await useFetch('/api/posts', { key: 'posts' })
```vue [pages/posts/[id\\].vue] ```vue [pages/posts/[id\\].vue]
<script setup lang="ts"> <script setup lang="ts">
// Access to the cached value of useFetch in posts.vue (parent route) // Access to the cached value of useFetch in posts.vue (parent route)
const { id } = useRoute().params
const { data: posts } = useNuxtData('posts') const { data: posts } = useNuxtData('posts')
const { data } = useLazyFetch(`/api/posts/${id}`, {
key: `post-${id}`, const route = useRoute()
const { data } = useLazyFetch(`/api/posts/${route.params.id}`, {
key: `post-${route.params.id}`,
default() { default() {
// Find the individual post from the cache and set it as the default value. // Find the individual post from the cache and set it as the default value.
return posts.value.find(post => post.id === id) return posts.value.find(post => post.id === route.params.id)
} }
}) })
</script> </script>
@ -40,7 +58,9 @@ const { data } = useLazyFetch(`/api/posts/${id}`, {
## Optimistic Updates ## 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] ```vue [pages/todos.vue]
<script setup lang="ts"> <script setup lang="ts">
@ -52,28 +72,34 @@ const { data } = await useAsyncData('todos', () => $fetch('/api/todos'))
```vue [components/NewTodo.vue] ```vue [components/NewTodo.vue]
<script setup lang="ts"> <script setup lang="ts">
const newTodo = ref('') const newTodo = ref('')
const previousTodos = ref([]) let previousTodos = []
// Access to the cached value of useAsyncData in todos.vue // Access to the cached value of useAsyncData in todos.vue
const { data: todos } = useNuxtData('todos') const { data: todos } = useNuxtData('todos')
const { data } = await useFetch('/api/addTodo', { async function addTodo () {
return $fetch('/api/addTodo', {
method: 'post', method: 'post',
body: { body: {
todo: newTodo.value todo: newTodo.value
}, },
onRequest () { onRequest () {
previousTodos.value = todos.value // Store the previously cached value to restore if fetch fails. // Store the previously cached value to restore if fetch fails.
previousTodos = todos.value
todos.value.push(newTodo.value) // Optimistically update the todos. // Optimistically update the todos.
todos.value = [...todos.value, newTodo.value]
}, },
onRequestError () { onResponseError () {
todos.value = previousTodos.value // Rollback the data if the request failed. // Rollback the data if the request failed.
todos.value = previousTodos
}, },
async onResponse () { async onResponse () {
await refreshNuxtData('todos') // Invalidate todos in the background if the request succeeded. // Invalidate todos in the background if the request succeeded.
await refreshNuxtData('todos')
} }
}) })
}
</script> </script>
``` ```

View File

@ -4,7 +4,7 @@ description: The useSeoMeta composable lets you define your site's SEO meta tags
links: links:
- label: Source - label: Source
icon: i-simple-icons-github 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 size: xs
--- ---

View File

@ -4,7 +4,7 @@ description: The useServerSeoMeta composable lets you define your site's SEO met
links: links:
- label: Source - label: Source
icon: i-simple-icons-github 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 size: xs
--- ---

View File

@ -4,7 +4,7 @@ description: 'Nuxt command to build your Nuxt module before publishing.'
links: links:
- label: Source - label: Source
icon: i-simple-icons-github 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 size: xs
--- ---

View File

@ -63,7 +63,7 @@ Each active version has its own nightly releases which are generated automatical
Release | | Initial release | End Of Life | Docs Release | | Initial release | End Of Life | Docs
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|------- ----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
**4.x** (scheduled) | | 2024 Q3 | | &nbsp; **4.x** (scheduled) | | approximately 1 month after release of nitro v3 | | &nbsp;
**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) **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** (unsupported) | <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) **2.x** (unsupported) | <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 | &nbsp; **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 | &nbsp;

View File

@ -39,12 +39,12 @@
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@types/node": "22.10.5", "@types/node": "22.10.6",
"@unhead/dom": "1.11.14", "@unhead/dom": "1.11.18",
"@unhead/schema": "1.11.14", "@unhead/schema": "1.11.18",
"@unhead/shared": "1.11.14", "@unhead/shared": "1.11.18",
"@unhead/ssr": "1.11.14", "@unhead/ssr": "1.11.18",
"@unhead/vue": "1.11.14", "@unhead/vue": "1.11.18",
"@vue/compiler-core": "3.5.13", "@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13", "@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13", "@vue/shared": "3.5.13",
@ -55,62 +55,64 @@
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"ohash": "1.1.4", "ohash": "1.1.4",
"postcss": "8.4.49", "postcss": "8.5.1",
"rollup": "4.30.0", "rollup": "4.30.1",
"send": ">=1.1.0", "send": ">=1.1.0",
"typescript": "5.7.2", "typescript": "5.7.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"unbuild": "3.2.0", "unbuild": "3.3.1",
"unhead": "1.11.14", "unhead": "1.11.18",
"unimport": "3.14.5", "unimport": "3.14.6",
"vite": "6.0.7", "vite": "6.0.7",
"vue": "3.5.13" "vue": "3.5.13"
}, },
"devDependencies": { "devDependencies": {
"@arethetypeswrong/cli": "0.17.2", "@arethetypeswrong/cli": "0.17.3",
"@nuxt/eslint-config": "0.7.4", "@nuxt/cli": "3.20.0",
"@nuxt/eslint-config": "0.7.5",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*", "@nuxt/rspack-builder": "workspace:*",
"@nuxt/test-utils": "3.15.1", "@nuxt/test-utils": "3.15.4",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/node": "22.10.5", "@types/node": "22.10.6",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@unhead/schema": "1.11.14", "@unhead/schema": "1.11.18",
"@unhead/vue": "1.11.14", "@unhead/vue": "1.11.18",
"@vitest/coverage-v8": "2.1.8", "@vitest/coverage-v8": "2.1.8",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"case-police": "0.7.2", "case-police": "0.7.2",
"changelogen": "0.5.7", "changelogen": "0.5.7",
"consola": "3.3.3", "consola": "3.4.0",
"cssnano": "7.0.6", "cssnano": "7.0.6",
"destr": "2.0.3", "destr": "2.0.3",
"devalue": "5.1.1", "devalue": "5.1.1",
"eslint": "9.17.0", "eslint": "9.18.0",
"eslint-plugin-no-only-tests": "3.3.0", "eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "4.6.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", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"happy-dom": "16.3.0", "happy-dom": "16.6.0",
"installed-check": "9.3.0", "installed-check": "9.3.0",
"jiti": "2.4.2", "jiti": "2.4.2",
"knip": "5.41.1", "knip": "5.42.1",
"markdownlint-cli": "0.43.0", "markdownlint-cli": "0.43.0",
"memfs": "4.15.3", "memfs": "4.17.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "3.18.2",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.2", "nuxt-content-twoslash": "0.1.2",
"ofetch": "1.4.1", "ofetch": "1.4.1",
"pathe": "2.0.0", "pathe": "2.0.1",
"pkg-pr-new": "0.0.39",
"playwright-core": "1.49.1", "playwright-core": "1.49.1",
"rollup": "4.30.1",
"semver": "7.6.3", "semver": "7.6.3",
"sherif": "1.1.1", "sherif": "1.1.1",
"std-env": "3.8.0", "std-env": "3.8.0",
"tinyexec": "0.3.2", "tinyexec": "0.3.2",
"tinyglobby": "0.2.10", "tinyglobby": "0.2.10",
"typescript": "5.7.2", "typescript": "5.7.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"vitest": "2.1.8", "vitest": "2.1.8",
"vitest-environment-nuxt": "1.0.1", "vitest-environment-nuxt": "1.0.1",
@ -118,9 +120,6 @@
"vue-tsc": "2.2.0", "vue-tsc": "2.2.0",
"webpack": "5.97.1" "webpack": "5.97.1"
}, },
"packageManager": "pnpm@9.15.3", "packageManager": "pnpm@9.15.4",
"engines": {
"node": "^18.20.4 || ^20.9.0 || ^22.0.0 || >=23.0.0"
},
"version": "" "version": ""
} }

View File

@ -29,35 +29,36 @@
"dependencies": { "dependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"c12": "^2.0.1", "c12": "^2.0.1",
"consola": "^3.3.3", "consola": "^3.4.0",
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
"errx": "^0.1.0", "errx": "^0.1.0",
"globby": "^14.0.2", "globby": "^14.0.2",
"ignore": "^7.0.0", "ignore": "^7.0.3",
"jiti": "^2.4.2", "jiti": "^2.4.2",
"klona": "^2.0.6", "klona": "^2.0.6",
"mlly": "^1.7.3", "mlly": "^1.7.4",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"pkg-types": "^1.3.0", "pkg-types": "^1.3.1",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"std-env": "^3.8.0",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unctx": "^2.4.1", "unctx": "^2.4.1",
"unimport": "^3.14.5", "unimport": "^3.14.6",
"untyped": "^1.5.2" "untyped": "^1.5.2"
}, },
"devDependencies": { "devDependencies": {
"@rspack/core": "1.1.8", "@rspack/core": "1.1.8",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"unbuild": "3.2.0", "unbuild": "3.3.1",
"vite": "6.0.7", "vite": "6.0.7",
"vitest": "2.1.8", "vitest": "2.1.8",
"webpack": "5.97.1" "webpack": "5.97.1"
}, },
"engines": { "engines": {
"node": ">=18.20.5" "node": ">=18.0.0"
} }
} }

View File

@ -114,7 +114,7 @@ export function addWebpackPlugin (pluginOrGetter: WebpackPluginInstance | Webpac
const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push' const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter
config.plugins = config.plugins || [] config.plugins ||= []
config.plugins[method](...toArray(plugin)) config.plugins[method](...toArray(plugin))
}, options) }, options)
} }
@ -126,7 +126,7 @@ export function addRspackPlugin (pluginOrGetter: RspackPluginInstance | RspackPl
const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push' const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter
config.plugins = config.plugins || [] config.plugins ||= []
config.plugins[method](...toArray(plugin)) config.plugins[method](...toArray(plugin))
}, options) }, options)
} }
@ -139,7 +139,7 @@ export function addVitePlugin (pluginOrGetter: VitePlugin | VitePlugin[] | (() =
const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push' const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter
config.plugins = config.plugins || [] config.plugins ||= []
config.plugins[method](...toArray(plugin)) config.plugins[method](...toArray(plugin))
}, options) }, options)
} }

View File

@ -11,7 +11,7 @@ import { MODE_RE } from './utils'
export async function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) { export async function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) {
const nuxt = useNuxt() const nuxt = useNuxt()
await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt) await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt)
nuxt.options.components = nuxt.options.components || [] nuxt.options.components ||= []
dir.priority ||= 0 dir.priority ||= 0
nuxt.hook('components:dirs', (dirs) => { dirs[opts.prepend ? 'unshift' : 'push'](dir) }) nuxt.hook('components:dirs', (dirs) => { dirs[opts.prepend ? 'unshift' : 'push'](dir) })
} }
@ -26,7 +26,7 @@ export type AddComponentOptions = { name: string, filePath: string } & Partial<E
export async function addComponent (opts: AddComponentOptions) { export async function addComponent (opts: AddComponentOptions) {
const nuxt = useNuxt() const nuxt = useNuxt()
await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt) await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt)
nuxt.options.components = nuxt.options.components || [] nuxt.options.components ||= []
if (!opts.mode) { if (!opts.mode) {
const [, mode = 'all'] = opts.filePath.match(MODE_RE) || [] const [, mode = 'all'] = opts.filePath.match(MODE_RE) || []

View File

@ -58,7 +58,7 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
const processedLayers = new Set<string>() const processedLayers = new Set<string>()
for (const layer of layers) { for (const layer of layers) {
// Resolve `rootDir` & `srcDir` of layers // Resolve `rootDir` & `srcDir` of layers
layer.config = layer.config || {} layer.config ||= {}
layer.config.rootDir = layer.config.rootDir ?? layer.cwd! layer.config.rootDir = layer.config.rootDir ?? layer.cwd!
// Only process/resolve layers once // Only process/resolve layers once

View File

@ -16,7 +16,7 @@ export interface LoadNuxtOptions extends LoadNuxtConfigOptions {
export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> { export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
// Backward compatibility // Backward compatibility
opts.cwd = resolve(opts.cwd || (opts as any).rootDir /* backwards compat */ || '.') 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 // Apply dev as config override
opts.overrides.dev = !!opts.dev opts.overrides.dev = !!opts.dev

View File

@ -87,7 +87,7 @@ function _defineNuxtModule<
// Avoid duplicate installs // Avoid duplicate installs
const uniqueKey = module.meta.name || module.meta.configKey const uniqueKey = module.meta.name || module.meta.configKey
if (uniqueKey) { if (uniqueKey) {
nuxt.options._requiredModules = nuxt.options._requiredModules || {} nuxt.options._requiredModules ||= {}
if (nuxt.options._requiredModules[uniqueKey]) { if (nuxt.options._requiredModules[uniqueKey]) {
return false return false
} }

View File

@ -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 const entryPath = typeof moduleToInstall === 'string' ? resolveAlias(moduleToInstall) : undefined
if (typeof moduleToInstall === 'string' && entryPath !== moduleToInstall) { if (typeof moduleToInstall === 'string' && entryPath !== moduleToInstall) {
@ -95,11 +95,10 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
paths.add(nuxtModule) paths.add(nuxtModule)
for (const path of paths) { for (const path of paths) {
for (const parentURL of nuxt.options.modulesDir) {
try { try {
const src = isAbsolute(path) const src = isAbsolute(path)
? pathToFileURL(await resolvePath(path, { cwd: parentURL, fallbackToOriginal: false, extensions: nuxt.options.extensions })).href ? pathToFileURL(await resolvePath(path, { fallbackToOriginal: false, extensions: nuxt.options.extensions })).href
: await resolveModule(path, { url: pathToFileURL(parentURL.replace(/\/node_modules\/?$/, '')), extensions: nuxt.options.extensions }) : 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 nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
resolvedModulePath = fileURLToPath(new URL(src)) resolvedModulePath = fileURLToPath(new URL(src))
@ -119,8 +118,6 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
throw error throw error
} }
} }
if (typeof nuxtModule !== 'string') { break }
}
} }
// Throw error if module could not be found // Throw error if module could not be found

View File

@ -40,7 +40,7 @@ export function addDevServerHandler (handler: NitroDevEventHandler) {
*/ */
export function addServerPlugin (plugin: string) { export function addServerPlugin (plugin: string) {
const nuxt = useNuxt() const nuxt = useNuxt()
nuxt.options.nitro.plugins = nuxt.options.nitro.plugins || [] nuxt.options.nitro.plugins ||= []
nuxt.options.nitro.plugins.push(normalize(plugin)) nuxt.options.nitro.plugins.push(normalize(plugin))
} }
@ -89,8 +89,8 @@ export function useNitro (): Nitro {
export function addServerImports (imports: Import[]) { export function addServerImports (imports: Import[]) {
const nuxt = useNuxt() const nuxt = useNuxt()
nuxt.hook('nitro:config', (config) => { nuxt.hook('nitro:config', (config) => {
config.imports = config.imports || {} config.imports ||= {}
config.imports.imports = config.imports.imports || [] config.imports.imports ||= []
config.imports.imports.push(...imports) config.imports.imports.push(...imports)
}) })
} }
@ -102,8 +102,8 @@ export function addServerImportsDir (dirs: string | string[], opts: { prepend?:
const nuxt = useNuxt() const nuxt = useNuxt()
const _dirs = toArray(dirs) const _dirs = toArray(dirs)
nuxt.hook('nitro:config', (config) => { nuxt.hook('nitro:config', (config) => {
config.imports = config.imports || {} config.imports ||= {}
config.imports.dirs = config.imports.dirs || [] config.imports.dirs ||= []
config.imports.dirs[opts.prepend ? 'unshift' : 'push'](..._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 } = {}) { export function addServerScanDir (dirs: string | string[], opts: { prepend?: boolean } = {}) {
const nuxt = useNuxt() const nuxt = useNuxt()
nuxt.hook('nitro:config', (config) => { nuxt.hook('nitro:config', (config) => {
config.scanDirs = config.scanDirs || [] config.scanDirs ||= []
for (const dir of toArray(dirs)) { for (const dir of toArray(dirs)) {
config.scanDirs[opts.prepend ? 'unshift' : 'push'](dir) config.scanDirs[opts.prepend ? 'unshift' : 'push'](dir)

View File

@ -20,9 +20,7 @@ export interface ExtendRouteRulesOptions {
export function extendRouteRules (route: string, rule: NitroRouteConfig, options: ExtendRouteRulesOptions = {}) { export function extendRouteRules (route: string, rule: NitroRouteConfig, options: ExtendRouteRulesOptions = {}) {
const nuxt = useNuxt() const nuxt = useNuxt()
for (const opts of [nuxt.options, nuxt.options.nitro]) { for (const opts of [nuxt.options, nuxt.options.nitro]) {
if (!opts.routeRules) { opts.routeRules ||= {}
opts.routeRules = {}
}
opts.routeRules[route] = options.override opts.routeRules[route] = options.override
? defu(rule, opts.routeRules[route]) ? defu(rule, opts.routeRules[route])
: defu(opts.routeRules[route], rule) : defu(opts.routeRules[route], rule)

View File

@ -1,13 +1,19 @@
import { existsSync } from 'node:fs'
import { isAbsolute } from 'node:path'
import { pathToFileURL } from 'node:url'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema' 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 { addTemplate } from './template'
import { resolveAlias } from './resolve' import { resolveAlias } from './resolve'
import { MODE_RE } from './utils'
/** /**
* Normalize a nuxt plugin object * Normalize a nuxt plugin object
*/ */
const pluginSymbol = Symbol.for('nuxt plugin')
export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin { export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
// Normalize src // Normalize src
if (typeof plugin === 'string') { if (typeof plugin === 'string') {
@ -16,6 +22,10 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin = { ...plugin } plugin = { ...plugin }
} }
if (pluginSymbol in plugin) {
return plugin
}
if (!plugin.src) { if (!plugin.src) {
throw new Error('Invalid plugin. src option is required: ' + JSON.stringify(plugin)) 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 // Normalize full path to plugin
plugin.src = normalize(resolveAlias(plugin.src)) 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 // Normalize mode
if (plugin.ssr) { if (plugin.ssr) {
plugin.mode = 'server' plugin.mode = 'server'
@ -32,6 +50,9 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin.mode = mode as 'all' | 'client' | 'server' plugin.mode = mode as 'all' | 'client' | 'server'
} }
// @ts-expect-error not adding symbol to types to avoid conflicts
plugin[pluginSymbol] = true
return plugin return plugin
} }
@ -61,7 +82,7 @@ export function addPlugin (_plugin: NuxtPlugin | string, opts: AddPluginOptions
const plugin = normalizePlugin(_plugin) const plugin = normalizePlugin(_plugin)
// Remove any existing plugin with the same src // 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 // 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) nuxt.options.plugins[opts.append ? 'push' : 'unshift'](plugin)

View File

@ -8,6 +8,7 @@ import type { TSConfig } from 'pkg-types'
import { gte } from 'semver' import { gte } from 'semver'
import { readPackageJSON } from 'pkg-types' import { readPackageJSON } from 'pkg-types'
import { filterInPlace } from './utils'
import { tryResolveModule } from './internal/esm' import { tryResolveModule } from './internal/esm'
import { getDirectory } from './module/install' import { getDirectory } from './module/install'
import { tryUseNuxt, useNuxt } from './context' import { tryUseNuxt, useNuxt } from './context'
@ -23,7 +24,7 @@ export function addTemplate<T> (_template: NuxtTemplate<T> | string) {
const template = normalizeTemplate(_template) const template = normalizeTemplate(_template)
// Remove any existing template with the same destination path // 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 // Add to templates array
nuxt.options.build.templates.push(template) nuxt.options.build.templates.push(template)
@ -229,9 +230,9 @@ export async function _generateTypes (nuxt: Nuxt) {
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
: nuxt.options.buildDir : nuxt.options.buildDir
tsConfig.compilerOptions = tsConfig.compilerOptions || {} tsConfig.compilerOptions ||= {}
tsConfig.compilerOptions.paths = tsConfig.compilerOptions.paths || {} tsConfig.compilerOptions.paths ||= {}
tsConfig.include = tsConfig.include || [] tsConfig.include ||= []
for (const alias in aliases) { for (const alias in aliases) {
if (excludedAlias.some(re => re.test(alias))) { 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.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))] 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() await writeFile()
} }
function sortTsPaths (paths: Record<string, string[]>) {
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<string, string>) { function renderAttrs (obj: Record<string, string>) {
const attrs: string[] = [] const attrs: string[] = []
for (const key in obj) { for (const key in obj) {

View File

@ -3,4 +3,19 @@ export function toArray<T> (value: T | T[]): T[] {
return Array.isArray(value) ? value : [value] 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<T> (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+)*$/ export const MODE_RE = /\.(server|client)(\.\w+)*$/

View File

@ -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/*',
],
})
})
}) })

View File

@ -1,2 +1,2 @@
#!/usr/bin/env node #!/usr/bin/env node
import 'nuxi/cli' import '@nuxt/cli/cli'

View File

@ -22,7 +22,7 @@ export default defineBuildConfig({
}, },
}, },
dependencies: [ dependencies: [
'nuxi', '@nuxt/cli',
'vue-router', 'vue-router',
'ofetch', 'ofetch',
], ],

View File

@ -64,22 +64,23 @@
"test:attw": "attw --pack" "test:attw": "attw --pack"
}, },
"dependencies": { "dependencies": {
"@nuxt/cli": "^3.20.0",
"@nuxt/devalue": "^2.0.2", "@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.7.0", "@nuxt/devtools": "^1.7.0",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.4", "@nuxt/telemetry": "^2.6.4",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.11.14", "@unhead/dom": "^1.11.18",
"@unhead/shared": "^1.11.14", "@unhead/shared": "^1.11.18",
"@unhead/ssr": "^1.11.14", "@unhead/ssr": "^1.11.18",
"@unhead/vue": "^1.11.14", "@unhead/vue": "^1.11.18",
"@vue/shared": "^3.5.13", "@vue/shared": "^3.5.13",
"acorn": "8.14.0", "acorn": "8.14.0",
"c12": "^2.0.1", "c12": "^2.0.1",
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
"compatx": "^0.1.8", "compatx": "^0.1.8",
"consola": "^3.3.3", "consola": "^3.4.0",
"cookie-es": "^1.2.2", "cookie-es": "^1.2.2",
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
@ -91,35 +92,34 @@
"globby": "^14.0.2", "globby": "^14.0.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"ignore": "^7.0.0", "ignore": "^7.0.3",
"impound": "^0.2.0", "impound": "^0.2.0",
"jiti": "^2.4.2", "jiti": "^2.4.2",
"klona": "^2.0.6", "klona": "^2.0.6",
"knitwork": "^1.2.0", "knitwork": "^1.2.0",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"mlly": "^1.7.3", "mlly": "^1.7.4",
"nanotar": "^0.1.1", "nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "^3.18.2",
"nypm": "^0.4.1", "nypm": "^0.4.1",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"pkg-types": "^1.3.0", "pkg-types": "^1.3.1",
"radix3": "^1.1.2", "radix3": "^1.1.2",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"std-env": "^3.8.0", "std-env": "^3.8.0",
"strip-literal": "^2.1.1", "strip-literal": "^3.0.0",
"tinyglobby": "0.2.10", "tinyglobby": "0.2.10",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"ultrahtml": "^1.5.3", "ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
"unctx": "^2.4.1", "unctx": "^2.4.1",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unhead": "^1.11.14", "unhead": "^1.11.18",
"unimport": "^3.14.5", "unimport": "^3.14.6",
"unplugin": "^2.1.2", "unplugin": "^2.1.2",
"unplugin-vue-router": "^0.10.9", "unplugin-vue-router": "^0.10.9",
"unstorage": "^1.14.4", "unstorage": "^1.14.4",
@ -135,7 +135,7 @@
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13", "@vue/compiler-sfc": "3.5.13",
"unbuild": "3.2.0", "unbuild": "3.3.1",
"vite": "6.0.7", "vite": "6.0.7",
"vitest": "2.1.8" "vitest": "2.1.8"
}, },

View File

@ -95,7 +95,7 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
if (isPromise(setupState)) { if (isPromise(setupState)) {
return Promise.resolve(setupState).then((setupState) => { return Promise.resolve(setupState).then((setupState) => {
if (typeof setupState !== 'function') { if (typeof setupState !== 'function') {
setupState = setupState || {} setupState ||= {}
setupState.mounted$ = mounted$ setupState.mounted$ = mounted$
return setupState return setupState
} }

View File

@ -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 } const elRef = import.meta.server ? undefined : (ref: any) => { el!.value = props.custom ? ref?.$el?.nextElementSibling : ref?.$el }
function shouldPrefetch (mode: 'visibility' | 'interaction') { 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() 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()) { async function prefetch (nuxtApp = useNuxtApp()) {
if (import.meta.server) { return }
if (prefetched.value) { return } if (prefetched.value) { return }
prefetched.value = true prefetched.value = true
@ -395,6 +398,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
// `custom` API cannot support fallthrough attributes as the slot // `custom` API cannot support fallthrough attributes as the slot
// may render fragment or text root nodes (#14897, #19375) // may render fragment or text root nodes (#14897, #19375)
if (!props.custom) { if (!props.custom) {
if (import.meta.client) {
if (shouldPrefetch('interaction')) { if (shouldPrefetch('interaction')) {
routerLinkProps.onPointerenter = prefetch.bind(null, undefined) routerLinkProps.onPointerenter = prefetch.bind(null, undefined)
routerLinkProps.onFocus = prefetch.bind(null, undefined) routerLinkProps.onFocus = prefetch.bind(null, undefined)
@ -402,6 +406,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
if (prefetched.value) { if (prefetched.value) {
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
} }
}
routerLinkProps.rel = props.rel || undefined routerLinkProps.rel = props.rel || undefined
} }

View File

@ -37,7 +37,7 @@ export async function callOnce (...args: any): Promise<void> {
return return
} }
nuxtApp._once = nuxtApp._once || {} nuxtApp._once ||= {}
nuxtApp._once[_key] = nuxtApp._once[_key] || fn() || true nuxtApp._once[_key] = nuxtApp._once[_key] || fn() || true
await nuxtApp._once[_key] await nuxtApp._once[_key]
nuxtApp.payload.once.add(_key) nuxtApp.payload.once.add(_key)

View File

@ -250,7 +250,7 @@ export default defineNuxtModule<ComponentsOptions>({
// TODO: refactor this // TODO: refactor this
nuxt.hook('vite:extendConfig', (config, { isClient }) => { nuxt.hook('vite:extendConfig', (config, { isClient }) => {
config.plugins = config.plugins || [] config.plugins ||= []
if (isClient && selectiveClient) { if (isClient && selectiveClient) {
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}') writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
@ -275,7 +275,7 @@ export default defineNuxtModule<ComponentsOptions>({
nuxt.hook(key, (configs) => { nuxt.hook(key, (configs) => {
configs.forEach((config) => { configs.forEach((config) => {
const mode = config.name === 'client' ? 'client' : 'server' const mode = config.name === 'client' ? 'client' : 'server'
config.plugins = config.plugins || [] config.plugins ||= []
if (mode !== 'server') { if (mode !== 'server') {
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}') writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')

View File

@ -63,8 +63,11 @@ export async function build (nuxt: Nuxt) {
return return
} }
if (nuxt.options.dev) { if (nuxt.options.dev && !nuxt.options.test) {
nuxt.hooks.hookOnce('build:done', () => {
checkForExternalConfigurationFiles() checkForExternalConfigurationFiles()
.catch(e => logger.warn('Problem checking for external configuration files.', e))
})
} }
await bundle(nuxt) await bundle(nuxt)

View File

@ -53,7 +53,7 @@ export function installNuxtModule (name: string, options?: EnsurePackageInstalle
installPrompts.add(name) installPrompts.add(name)
const nuxt = useNuxt() const nuxt = useNuxt()
return promptToInstall(name, async () => { return promptToInstall(name, async () => {
const { runCommand } = await import('nuxi') const { runCommand } = await import('@nuxt/cli')
await runCommand('module', ['add', name, '--cwd', nuxt.options.rootDir]) await runCommand('module', ['add', name, '--cwd', nuxt.options.rootDir])
}, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options }) }, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options })
} }

View File

@ -239,7 +239,11 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Resolve user-provided paths // Resolve user-provided paths
nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!) 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 // Resolve aliases in user-provided input - so `~/server/test` will work
nitroConfig.plugins = nitroConfig.plugins?.map(plugin => plugin ? resolveAlias(plugin, nuxt.options.alias) : plugin) 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 basePath = nitroConfig.typescript!.tsConfig!.compilerOptions?.baseUrl ? resolve(nuxt.options.buildDir, nitroConfig.typescript!.tsConfig!.compilerOptions?.baseUrl) : nuxt.options.buildDir
const aliases = nitroConfig.alias! const aliases = nitroConfig.alias!
const tsConfig = nitroConfig.typescript!.tsConfig! const tsConfig = nitroConfig.typescript!.tsConfig!
tsConfig.compilerOptions = tsConfig.compilerOptions || {} tsConfig.compilerOptions ||= {}
tsConfig.compilerOptions.paths = tsConfig.compilerOptions.paths || {} tsConfig.compilerOptions.paths ||= {}
for (const _alias in aliases) { for (const _alias in aliases) {
const alias = _alias as keyof typeof aliases const alias = _alias as keyof typeof aliases
if (excludedAlias.some(pattern => typeof pattern === 'string' ? alias === pattern : pattern.test(alias))) { if (excludedAlias.some(pattern => typeof pattern === 'string' ? alias === pattern : pattern.test(alias))) {
continue continue
} }
if (alias in tsConfig.compilerOptions.paths) { continue } if (alias in tsConfig.compilerOptions.paths) {
continue
}
const absolutePath = resolve(basePath, aliases[alias]!) const absolutePath = resolve(basePath, aliases[alias]!)
const stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */) 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) await writeTypes(nitro)
} }
// Exclude nitro output dir from typescript // 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.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') }) opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/nitro.d.ts') })
}) })

View File

@ -81,7 +81,7 @@ const nightlies = {
'@nuxt/kit': '@nuxt/kit-nightly', '@nuxt/kit': '@nuxt/kit-nightly',
} }
const keyDependencies = [ export const keyDependencies = [
'@nuxt/kit', '@nuxt/kit',
'@nuxt/schema', '@nuxt/schema',
] ]
@ -802,8 +802,13 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
const nuxt = createNuxt(options) const nuxt = createNuxt(options)
if (nuxt.options.dev && !nuxt.options.test) {
nuxt.hooks.hookOnce('build:done', () => {
for (const dep of keyDependencies) { for (const dep of keyDependencies) {
checkDependencyVersion(dep, nuxt._version) 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 // 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<Nuxt> {
return nuxt return nuxt
} }
async function checkDependencyVersion (name: string, nuxtVersion: string): Promise<void> { export async function checkDependencyVersion (name: string, nuxtVersion: string): Promise<void> {
const path = await resolvePath(name, { fallbackToOriginal: true }).catch(() => null) const path = await resolvePath(name, { fallbackToOriginal: true }).catch(() => null)
if (!path || path === name) { return } if (!path || path === name) { return }

View File

@ -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}.`]) patterns.push([i, `This module cannot be imported in ${context}.`])
} }

View File

@ -38,25 +38,25 @@ export const VirtualFSPlugin = (nuxt: Nuxt, options: VirtualFSPluginOptions) =>
const resolvedId = resolveWithExt(id) const resolvedId = resolveWithExt(id)
if (resolvedId) { if (resolvedId) {
return PREFIX + resolvedId return PREFIX + encodeURIComponent(resolvedId)
} }
if (importer && RELATIVE_ID_RE.test(id)) { 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) const resolved = resolveWithExt(path)
if (resolved) { if (resolved) {
return PREFIX + resolved return PREFIX + encodeURIComponent(resolved)
} }
} }
}, },
loadInclude (id) { loadInclude (id) {
return id.startsWith(PREFIX) && withoutPrefix(id) in nuxt.vfs return id.startsWith(PREFIX) && withoutPrefix(decodeURIComponent(id)) in nuxt.vfs
}, },
load (id) { load (id) {
return { return {
code: nuxt.vfs[withoutPrefix(id)] || '', code: nuxt.vfs[withoutPrefix(decodeURIComponent(id))] || '',
map: null, map: null,
} }
}, },

View File

@ -2,7 +2,7 @@ import { defineEventHandler, getRequestHeader } from 'h3'
export default defineEventHandler((event) => { export default defineEventHandler((event) => {
if (getRequestHeader(event, 'x-nuxt-no-ssr')) { if (getRequestHeader(event, 'x-nuxt-no-ssr')) {
event.context.nuxt = event.context.nuxt || {} event.context.nuxt ||= {}
event.context.nuxt.noSSR = true event.context.nuxt.noSSR = true
} }
}) })

View File

@ -488,8 +488,8 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
} }
// TODO: remove for v4 // TODO: remove for v4
islandHead.link = islandHead.link || [] islandHead.link ||= []
islandHead.style = islandHead.style || [] islandHead.style ||= []
const islandResponse: NuxtIslandResponse = { const islandResponse: NuxtIslandResponse = {
id: islandContext.id, id: islandContext.id,

View File

@ -53,12 +53,30 @@ export function withLocations<T> (node: T): WithLocations<T> {
return node as WithLocations<T> return node as WithLocations<T>
} }
/**
* 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<T extends Node = Node> { abstract class BaseNode<T extends Node = Node> {
abstract type: string abstract type: string
readonly scope: string
node: WithLocations<T> node: WithLocations<T>
constructor (node: WithLocations<T>) { constructor (node: WithLocations<T>, scope: string) {
this.node = node this.node = node
this.scope = scope
} }
/** /**
@ -72,6 +90,14 @@ abstract class BaseNode<T extends Node = Node> {
* For instance, for a function parameter, this would be the end of the function declaration. * For instance, for a function parameter, this would be the end of the function declaration.
*/ */
abstract get end (): number 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<Identifier> { class IdentifierNode extends BaseNode<Identifier> {
@ -90,8 +116,8 @@ class FunctionParamNode extends BaseNode {
type = 'FunctionParam' as const type = 'FunctionParam' as const
fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression> fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>
constructor (node: WithLocations<Node>, fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) { constructor (node: WithLocations<Node>, scope: string, fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) {
super(node) super(node, scope)
this.fnNode = fnNode this.fnNode = fnNode
} }
@ -120,8 +146,8 @@ class VariableNode extends BaseNode<Identifier> {
type = 'Variable' as const type = 'Variable' as const
variableNode: WithLocations<VariableDeclaration> variableNode: WithLocations<VariableDeclaration>
constructor (node: WithLocations<Identifier>, variableNode: WithLocations<VariableDeclaration>) { constructor (node: WithLocations<Identifier>, scope: string, variableNode: WithLocations<VariableDeclaration>) {
super(node) super(node, scope)
this.variableNode = variableNode this.variableNode = variableNode
} }
@ -138,8 +164,8 @@ class ImportNode extends BaseNode<ImportSpecifier | ImportDefaultSpecifier | Imp
type = 'Import' as const type = 'Import' as const
importNode: WithLocations<Node> importNode: WithLocations<Node>
constructor (node: WithLocations<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>, importNode: WithLocations<Node>) { constructor (node: WithLocations<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>, scope: string, importNode: WithLocations<Node>) {
super(node) super(node, scope)
this.importNode = importNode this.importNode = importNode
} }
@ -156,8 +182,8 @@ class CatchParamNode extends BaseNode {
type = 'CatchParam' as const type = 'CatchParam' as const
catchNode: WithLocations<CatchClause> catchNode: WithLocations<CatchClause>
constructor (node: WithLocations<Node>, catchNode: WithLocations<CatchClause>) { constructor (node: WithLocations<Node>, scope: string, catchNode: WithLocations<CatchClause>) {
super(node) super(node, scope)
this.catchNode = catchNode this.catchNode = catchNode
} }
@ -264,7 +290,7 @@ export class ScopeTracker {
const identifiers = getPatternIdentifiers(param) const identifiers = getPatternIdentifiers(param)
for (const identifier of identifiers) { 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( this.declareIdentifier(
identifier.name, identifier.name,
parent.type === 'VariableDeclaration' parent.type === 'VariableDeclaration'
? new VariableNode(identifier, parent) ? new VariableNode(identifier, this.scopeIndexKey, parent)
: parent.type === 'CatchClause' : parent.type === 'CatchClause'
? new CatchParamNode(identifier, parent) ? new CatchParamNode(identifier, this.scopeIndexKey, parent)
: new FunctionParamNode(identifier, parent), : new FunctionParamNode(identifier, this.scopeIndexKey, parent),
) )
} }
} }
@ -295,7 +321,7 @@ export class ScopeTracker {
case 'FunctionDeclaration': case 'FunctionDeclaration':
// declare function name for named functions, skip for `export default` // declare function name for named functions, skip for `export default`
if (node.id?.name) { if (node.id?.name) {
this.declareIdentifier(node.id.name, new FunctionNode(node)) this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
} }
this.pushScope() this.pushScope()
for (const param of node.params) { for (const param of node.params) {
@ -309,7 +335,7 @@ export class ScopeTracker {
this.pushScope() this.pushScope()
// can be undefined, for example in class method definitions // can be undefined, for example in class method definitions
if (node.id?.name) { if (node.id?.name) {
this.declareIdentifier(node.id.name, new FunctionNode(node)) this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
} }
this.pushScope() this.pushScope()
@ -333,7 +359,7 @@ export class ScopeTracker {
case 'ClassDeclaration': case 'ClassDeclaration':
// declare class name for named classes, skip for `export default` // declare class name for named classes, skip for `export default`
if (node.id?.name) { 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 break
@ -342,13 +368,13 @@ export class ScopeTracker {
// e.g. const MyClass = class InternalClassName { // InternalClassName is only available within the class body // e.g. const MyClass = class InternalClassName { // InternalClassName is only available within the class body
this.pushScope() this.pushScope()
if (node.id?.name) { 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 break
case 'ImportDeclaration': case 'ImportDeclaration':
for (const specifier of node.specifiers) { 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 break
@ -429,6 +455,26 @@ export class ScopeTracker {
return null 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. * 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. * It also resets the scope index stack to its initial state, so that the scope tracker can be reused.

View File

@ -530,8 +530,8 @@ export default defineNuxtModule({
getContents: () => 'export { START_LOCATION, useRoute } from \'vue-router\'', getContents: () => 'export { START_LOCATION, useRoute } from \'vue-router\'',
}) })
nuxt.options.vite.resolve = nuxt.options.vite.resolve || {} nuxt.options.vite.resolve ||= {}
nuxt.options.vite.resolve.dedupe = nuxt.options.vite.resolve.dedupe || [] nuxt.options.vite.resolve.dedupe ||= []
nuxt.options.vite.resolve.dedupe.push('vue-router') nuxt.options.vite.resolve.dedupe.push('vue-router')
// Add router options template // Add router options template
@ -642,7 +642,7 @@ if (import.meta.hot) {
for (const route of routes) { for (const route of routes) {
router.addRoute(route) router.addRoute(route)
} }
router.replace('') router.replace(router.currentRoute.value.fullPath)
} }
if (routes && 'then' in routes) { if (routes && 'then' in routes) {
routes.then(addRoutes) routes.then(addRoutes)

View File

@ -228,6 +228,8 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
if (!meta) { return } if (!meta) { return }
const definePageMetaScope = scopeTracker.getCurrentScope()
walk(meta, { walk(meta, {
scopeTracker, scopeTracker,
enter (node, parent) { 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 || node.type !== 'Identifier' // checking for `node.type` to narrow down the type
) { return } ) { 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)) { if (isStaticIdentifier(node.name)) {
addImport(node.name) addImport(node.name)
} else { } else if (declaration) {
processDeclaration(scopeTracker.getDeclaration(node.name)) processDeclaration(declaration)
} }
}, },
}) })
@ -271,9 +287,9 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
handleHotUpdate: { handleHotUpdate: {
order: 'post', order: 'post',
handler: ({ file, modules, server }) => { handler: ({ file, modules, server }) => {
if (options.isPage?.(file)) { if (options.routesPath && options.isPage?.(file)) {
const macroModule = server.moduleGraph.getModuleById(file + '?macro=true') 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 [ return [
...modules, ...modules,
...macroModule ? [macroModule] : [], ...macroModule ? [macroModule] : [],

View File

@ -65,7 +65,7 @@ export default defineComponent({
if (import.meta.dev) { if (import.meta.dev) {
nuxtApp._isNuxtPageUsed = true nuxtApp._isNuxtPageUsed = true
} }
let pageLoadingEndHookAlreadyCalled = false
return () => { return () => {
return h(RouterView, { name: props.name, route: props.route, ...attrs }, { return h(RouterView, { name: props.name, route: props.route, ...attrs }, {
default: (routeProps: RouterViewSlotProps) => { default: (routeProps: RouterViewSlotProps) => {
@ -99,6 +99,7 @@ export default defineComponent({
const key = generateRouteKey(routeProps, props.pageKey) const key = generateRouteKey(routeProps, props.pageKey)
if (!nuxtApp.isHydrating && !hasChildrenRoutes(forkRoute, routeProps.route, routeProps.Component) && previousPageKey === key) { if (!nuxtApp.isHydrating && !hasChildrenRoutes(forkRoute, routeProps.route, routeProps.Component) && previousPageKey === key) {
nuxtApp.callHook('page:loading:end') nuxtApp.callHook('page:loading:end')
pageLoadingEndHookAlreadyCalled = true
} }
previousPageKey = key previousPageKey = key
@ -115,7 +116,14 @@ export default defineComponent({
wrapInKeepAlive(keepaliveConfig, h(Suspense, { wrapInKeepAlive(keepaliveConfig, h(Suspense, {
suspensible: true, suspensible: true,
onPending: () => nuxtApp.callHook('page:start', routeProps.Component), 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: () => { default: () => {
const providerVNode = h(RouteProvider, { const providerVNode = h(RouteProvider, {

View File

@ -68,7 +68,10 @@ export async function resolvePagesRoutes (nuxt = useNuxt()): Promise<NuxtPage[]>
return pages 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') { if (shouldAugment === 'after-resolve') {
await nuxt.callHook('pages:extend', pages) await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs, augmentCtx) 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++) { for (let i = 0; i < segments.length; i++) {
const segment = segments[i] const segment = segments[i]
const tokens = parseSegment(segment!) const tokens = parseSegment(segment!, file.absolutePath)
// Skip group segments // Skip group segments
if (tokens.every(token => token.type === SegmentTokenType.group)) { if (tokens.every(token => token.type === SegmentTokenType.group)) {
@ -154,6 +157,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
} }
interface AugmentPagesContext { interface AugmentPagesContext {
fullyResolvedPaths?: Set<string>
pagesToSkip?: Set<string> pagesToSkip?: Set<string>
augmentedPages?: Set<string> augmentedPages?: Set<string>
extraExtractionKeys?: string[] extraExtractionKeys?: string[]
@ -163,7 +167,9 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
ctx.augmentedPages ??= new Set() ctx.augmentedPages ??= new Set()
for (const route of routes) { for (const route of routes) {
if (route.file && !ctx.pagesToSkip?.has(route.file)) { if (route.file && !ctx.pagesToSkip?.has(route.file)) {
const fileContent = route.file in vfs ? vfs[route.file]! : fs.readFileSync(await resolvePath(route.file), 'utf-8') const fileContent = route.file in vfs
? vfs[route.file]!
: fs.readFileSync(ctx.fullyResolvedPaths?.has(route.file) ? route.file : await resolvePath(route.file), 'utf-8')
const routeMeta = await getRouteMeta(fileContent, route.file, ctx.extraExtractionKeys) const routeMeta = await getRouteMeta(fileContent, route.file, ctx.extraExtractionKeys)
if (route.meta) { if (route.meta) {
routeMeta.meta = { ...routeMeta.meta, ...route.meta } routeMeta.meta = { ...routeMeta.meta, ...route.meta }
@ -331,7 +337,7 @@ function getRoutePath (tokens: SegmentToken[]): string {
const PARAM_CHAR_RE = /[\w.]/ const PARAM_CHAR_RE = /[\w.]/
function parseSegment (segment: string) { function parseSegment (segment: string, absolutePath: string) {
let state: SegmentParserState = SegmentParserState.initial let state: SegmentParserState = SegmentParserState.initial
let i = 0 let i = 0
@ -418,8 +424,8 @@ function parseSegment (segment: string) {
state = SegmentParserState.initial state = SegmentParserState.initial
} else if (c && PARAM_CHAR_RE.test(c)) { } else if (c && PARAM_CHAR_RE.test(c)) {
buffer += c buffer += c
} else { } else if (state === SegmentParserState.dynamic || state === SegmentParserState.optional) {
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`) logger.warn(`'\`${c}\`' is not allowed in a dynamic route parameter and has been ignored. Consider renaming \`${absolutePath}\`.`)
} }
break break
} }

View File

@ -292,6 +292,8 @@ async function getResolvedApp (files: Array<string | { name: string, contents: s
} }
for (const plugin of app.plugins) { for (const plugin of app.plugins) {
plugin.src = normaliseToRepo(plugin.src)! plugin.src = normaliseToRepo(plugin.src)!
// @ts-expect-error untyped symbol
delete plugin[Symbol.for('nuxt plugin')]
} }
for (const mw of app.middleware) { for (const mw of app.middleware) {
mw.path = normaliseToRepo(mw.path)! mw.path = normaliseToRepo(mw.path)!

View File

@ -0,0 +1,62 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { readPackageJSON } from 'pkg-types'
import { inc } from 'semver'
import { version } from '../package.json'
import { checkDependencyVersion, keyDependencies } from '../src/core/nuxt'
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('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()
})
})

View File

@ -2,10 +2,7 @@ import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it, vi } from 'vitest' import { afterEach, describe, expect, it, vi } from 'vitest'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import { withoutTrailingSlash } from 'ufo' import { withoutTrailingSlash } from 'ufo'
import { readPackageJSON } from 'pkg-types'
import { inc } from 'semver'
import { loadNuxt } from '../src' import { loadNuxt } from '../src'
import { version } from '../package.json'
const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url)))) const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url))))
@ -45,45 +42,3 @@ describe('loadNuxt', () => {
expect(hookRan).toBe(true) 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()
})
})

View File

@ -393,6 +393,188 @@ definePageMeta({
`) `)
}) })
it('should not import static identifiers when shadowed in the same scope', () => {
const sfc = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
middleware: () => {
const useState = (key) => ({ value: { isLoggedIn: false } })
const auth = useState('auth')
if (!auth.value.isLoggedIn) {
return navigateTo('/login')
}
},
})
</script>
`
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 = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
middleware: () => {
function isLoggedIn() {
const auth = useState('auth')
return auth.value.isLoggedIn
}
const useState = (key) => ({ value: { isLoggedIn: false } })
if (!isLoggedIn()) {
return navigateTo('/login')
}
},
})
</script>
`
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 = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
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')
}
}
]
})
</script>
`
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 = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
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')
}
},
})
</script>
`
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 () => { it('should work with esbuild.keepNames = true', async () => {
const sfc = ` const sfc = `
<script setup lang="ts"> <script setup lang="ts">
@ -516,7 +698,12 @@ definePageMeta({
test () {} test () {}
} }
console.log(hoisted.value) const someFunction = () => {
const someValue = 'someValue'
console.log(someValue)
}
console.log(hoisted.value, val)
}, },
], ],
validate: (route) => { validate: (route) => {
@ -564,7 +751,12 @@ const hoisted = ref('hoisted')
test () {} test () {}
} }
console.log(hoisted.value) const someFunction = () => {
const someValue = 'someValue'
console.log(someValue)
}
console.log(hoisted.value, val)
}, },
], ],
validate: (route) => { validate: (route) => {

View File

@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest' import { assert, describe, expect, it } from 'vitest'
import { getUndeclaredIdentifiersInFunction, parseAndWalk } from '../src/core/utils/parse' import { getUndeclaredIdentifiersInFunction, parseAndWalk } from '../src/core/utils/parse'
import { TestScopeTracker } from './fixture/scope-tracker' import { TestScopeTracker } from './fixture/scope-tracker'
@ -667,4 +667,87 @@ describe('parsing', () => {
expect(processedFunctions).toBe(5) expect(processedFunctions).toBe(5)
}) })
it ('should correctly compare identifiers defined in different scopes', () => {
const code = `
// ""
const a = 1
// ""
const func = () => {
// "0-0"
const b = 2
// "0-0"
function foo() {
// "0-0-0-0"
const c = 3
}
}
// ""
const func2 = () => {
// "1-0"
const d = 2
// "1-0"
function bar() {
// "1-0-0-0"
const e = 3
}
}
// ""
const f = 4
`
const scopeTracker = new TestScopeTracker({
keepExitedScopes: true,
})
parseAndWalk(code, filename, {
scopeTracker,
})
const a = scopeTracker.getDeclarationFromScope('a', '')
const func = scopeTracker.getDeclarationFromScope('func', '')
const foo = scopeTracker.getDeclarationFromScope('foo', '0-0')
const b = scopeTracker.getDeclarationFromScope('b', '0-0')
const c = scopeTracker.getDeclarationFromScope('c', '0-0-0-0')
const func2 = scopeTracker.getDeclarationFromScope('func2', '')
const bar = scopeTracker.getDeclarationFromScope('bar', '1-0')
const d = scopeTracker.getDeclarationFromScope('d', '1-0')
const e = scopeTracker.getDeclarationFromScope('e', '1-0-0-0')
const f = scopeTracker.getDeclarationFromScope('f', '')
assert(a && func && foo && b && c && func2 && bar && d && e && f, 'All declarations should be found')
// identifiers in the same scope should be equal
expect(f.isUnderScope(a.scope)).toBe(false)
expect(func.isUnderScope(a.scope)).toBe(false)
expect(d.isUnderScope(bar.scope)).toBe(false)
// identifiers in deeper scopes should be under the scope of the parent scope
expect(b.isUnderScope(a.scope)).toBe(true)
expect(b.isUnderScope(func.scope)).toBe(true)
expect(c.isUnderScope(a.scope)).toBe(true)
expect(c.isUnderScope(b.scope)).toBe(true)
expect(d.isUnderScope(a.scope)).toBe(true)
expect(d.isUnderScope(func2.scope)).toBe(true)
expect(e.isUnderScope(a.scope)).toBe(true)
expect(e.isUnderScope(d.scope)).toBe(true)
// identifiers in parent scope should not be under the scope of the children
expect(a.isUnderScope(b.scope)).toBe(false)
expect(a.isUnderScope(c.scope)).toBe(false)
expect(a.isUnderScope(d.scope)).toBe(false)
expect(a.isUnderScope(e.scope)).toBe(false)
expect(b.isUnderScope(c.scope)).toBe(false)
// identifiers in parallel scopes should not influence each other
expect(d.isUnderScope(b.scope)).toBe(false)
expect(e.isUnderScope(b.scope)).toBe(false)
expect(b.isUnderScope(d.scope)).toBe(false)
expect(c.isUnderScope(e.scope)).toBe(false)
})
}) })

View File

@ -0,0 +1,84 @@
import { describe, expect, it } from 'vitest'
import type { Nuxt } from '@nuxt/schema'
import { rollup } from 'rollup'
import { VirtualFSPlugin } from '../src/core/plugins/virtual'
describe('virtual fs plugin', () => {
it('should support loading files virtually', async () => {
const code = await generateCode('export { foo } from "#build/foo"', {
vfs: {
'/.nuxt/foo': 'export const foo = "hello world"',
},
})
expect(code).toMatchInlineSnapshot(`
"const foo = "hello world";
export { foo };"
`)
})
it('should support loading virtual files by suffix', async () => {
const code = await generateCode('export { foo } from "#build/foo"', {
mode: 'client',
vfs: {
'/.nuxt/foo.server.ts': 'export const foo = "foo server file"',
'/.nuxt/foo.client.ts': 'export const foo = "foo client file"',
},
})
expect(code).toMatchInlineSnapshot(`
"const foo = "foo client file";
export { foo };"
`)
})
it('should support loading files referenced relatively', async () => {
const code = await generateCode('export { foo } from "#build/foo"', {
vfs: {
'/.nuxt/foo': 'export { foo } from "./bar"',
'/.nuxt/bar': 'export const foo = "relative import"',
},
})
expect(code).toMatchInlineSnapshot(`
"const foo = "relative import";
export { foo };"
`)
})
})
async function generateCode (input: string, options: { mode?: 'client' | 'server', vfs: Record<string, string> }) {
const stubNuxt = {
options: {
extensions: ['.ts', '.js'],
alias: {
'~': '/',
'#build': '/.nuxt',
},
},
vfs: options.vfs,
} as unknown as Nuxt
const bundle = await rollup({
input: 'entry.ts',
plugins: [
{
name: 'entry',
resolveId (id) {
if (id === 'entry.ts') {
return id
}
},
load (id) {
if (id === 'entry.ts') {
return input
}
},
},
VirtualFSPlugin(stubNuxt, { mode: options.mode || 'client', alias: stubNuxt.options.alias }).rollup(),
],
})
const { output: [chunk] } = await bundle.generate({})
return chunk.code.trim()
}

View File

@ -47,11 +47,11 @@
"jiti": "^2.4.2", "jiti": "^2.4.2",
"knitwork": "^1.2.0", "knitwork": "^1.2.0",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"memfs": "^4.15.3", "memfs": "^4.17.0",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"pify": "^6.1.0", "pify": "^6.1.0",
"postcss": "^8.4.49", "postcss": "^8.5.1",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0", "postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1", "postcss-loader": "^8.1.1",
@ -75,14 +75,14 @@
"@types/pify": "5.0.4", "@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.30.0", "rollup": "4.30.1",
"unbuild": "3.2.0", "unbuild": "3.3.1",
"vue": "3.5.13" "vue": "3.5.13"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.3.4" "vue": "^3.3.4"
}, },
"engines": { "engines": {
"node": "^18.20.5 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -20,17 +20,9 @@ export default defineBuildConfig({
'src/index', 'src/index',
'src/builder-env', 'src/builder-env',
], ],
hooks: { rollup: {
'rollup:options' (ctx, options) { dts: { respectExternal: false },
ctx.options.rollup.dts.respectExternal = false inlineDependencies: ['untyped', 'knitwork'],
const isExternal = options.external! as (id: string, importer?: string, isResolved?: boolean) => boolean
options.external = (source, importer, isResolved) => {
if (source === 'untyped' || source === 'knitwork') {
return false
}
return isExternal(source, importer, isResolved)
}
},
}, },
externals: [ externals: [
// Type imports // Type imports

View File

@ -37,7 +37,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@unhead/schema": "1.11.14", "@unhead/schema": "1.11.18",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.1",
"@vitejs/plugin-vue-jsx": "4.1.1", "@vitejs/plugin-vue-jsx": "4.1.1",
"@vue/compiler-core": "3.5.13", "@vue/compiler-core": "3.5.13",
@ -49,15 +49,15 @@
"file-loader": "6.2.0", "file-loader": "6.2.0",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hookable": "5.5.3", "hookable": "5.5.3",
"ignore": "7.0.0", "ignore": "7.0.3",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"ofetch": "1.4.1", "ofetch": "1.4.1",
"pkg-types": "1.3.0", "pkg-types": "1.3.1",
"sass-loader": "16.0.4", "sass-loader": "16.0.4",
"scule": "1.3.0", "scule": "1.3.0",
"unbuild": "3.2.0", "unbuild": "3.3.1",
"unctx": "2.4.1", "unctx": "2.4.1",
"unimport": "3.14.5", "unimport": "3.14.6",
"untyped": "1.5.2", "untyped": "1.5.2",
"vite": "6.0.7", "vite": "6.0.7",
"vue": "3.5.13", "vue": "3.5.13",
@ -68,9 +68,9 @@
"webpack-dev-middleware": "7.4.2" "webpack-dev-middleware": "7.4.2"
}, },
"dependencies": { "dependencies": {
"consola": "^3.3.3", "consola": "^3.4.0",
"defu": "^6.1.4", "defu": "^6.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"std-env": "^3.8.0" "std-env": "^3.8.0"
}, },
"engines": { "engines": {

View File

@ -17,22 +17,19 @@
"prerender": "pnpm build && jiti ./lib/prerender" "prerender": "pnpm build && jiti ./lib/prerender"
}, },
"devDependencies": { "devDependencies": {
"@unocss/reset": "0.65.3", "@unocss/reset": "65.4.0",
"beasties": "0.2.0", "beasties": "0.2.0",
"html-validate": "9.1.1", "html-validate": "9.1.3",
"htmlnano": "2.1.1", "htmlnano": "2.1.1",
"jiti": "2.4.2", "jiti": "2.4.2",
"knitwork": "1.2.0", "knitwork": "1.2.0",
"pathe": "2.0.0", "pathe": "2.0.1",
"prettier": "3.4.2", "prettier": "3.4.2",
"scule": "1.3.0", "scule": "1.3.0",
"svgo": "3.3.2", "svgo": "3.3.2",
"tinyexec": "0.3.2", "tinyexec": "0.3.2",
"tinyglobby": "0.2.10", "tinyglobby": "0.2.10",
"unocss": "0.65.3", "unocss": "65.4.0",
"vite": "6.0.7" "vite": "6.0.7"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
} }
} }

View File

@ -26,8 +26,8 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"rollup": "4.30.0", "rollup": "4.30.1",
"unbuild": "3.2.0", "unbuild": "3.3.1",
"vue": "3.5.13" "vue": "3.5.13"
}, },
"dependencies": { "dependencies": {
@ -36,7 +36,7 @@
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.1.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"consola": "^3.3.3", "consola": "^3.4.0",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"defu": "^6.1.4", "defu": "^6.1.4",
"esbuild": "^0.24.2", "esbuild": "^0.24.2",
@ -47,10 +47,10 @@
"jiti": "^2.4.2", "jiti": "^2.4.2",
"knitwork": "^1.2.0", "knitwork": "^1.2.0",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"mlly": "^1.7.3", "mlly": "^1.7.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"pkg-types": "^1.3.0", "pkg-types": "^1.3.1",
"postcss": "^8.4.49", "postcss": "^8.5.1",
"rollup-plugin-visualizer": "^5.13.1", "rollup-plugin-visualizer": "^5.13.1",
"std-env": "^3.8.0", "std-env": "^3.8.0",
"ufo": "^1.5.4", "ufo": "^1.5.4",
@ -65,6 +65,6 @@
"vue": "^3.3.4" "vue": "^3.3.4"
}, },
"engines": { "engines": {
"node": "^18.20.5 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -110,6 +110,9 @@ export async function buildClient (ctx: ViteBuildContext) {
}, },
resolve: { resolve: {
alias: { alias: {
// work around vite optimizer bug
'#app-manifest': 'unenv/runtime/mock/empty',
// user aliases
...nodeCompat.alias, ...nodeCompat.alias,
...ctx.config.resolve?.alias, ...ctx.config.resolve?.alias,
'nitro/runtime': join(ctx.nuxt.options.buildDir, 'nitro.client.mjs'), 'nitro/runtime': join(ctx.nuxt.options.buildDir, 'nitro.client.mjs'),

View File

@ -44,7 +44,7 @@ export function viteNodePlugin (ctx: ViteBuildContext): VitePlugin {
// invalidate changed virtual modules when templates are regenerated // invalidate changed virtual modules when templates are regenerated
ctx.nuxt.hook('app:templatesGenerated', (_app, changedTemplates) => { ctx.nuxt.hook('app:templatesGenerated', (_app, changedTemplates) => {
for (const template of changedTemplates) { for (const template of changedTemplates) {
const mods = server.moduleGraph.getModulesByFile(`virtual:nuxt:${template.dst}`) const mods = server.moduleGraph.getModulesByFile(`virtual:nuxt:${encodeURIComponent(template.dst)}`)
for (const mod of mods || []) { for (const mod of mods || []) {
markInvalidate(mod) markInvalidate(mod)

View File

@ -212,7 +212,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
// Invalidate virtual modules when templates are re-generated // Invalidate virtual modules when templates are re-generated
ctx.nuxt.hook('app:templatesGenerated', (_app, changedTemplates) => { ctx.nuxt.hook('app:templatesGenerated', (_app, changedTemplates) => {
for (const template of changedTemplates) { for (const template of changedTemplates) {
for (const mod of server.moduleGraph.getModulesByFile(`virtual:nuxt:${template.dst}`) || []) { for (const mod of server.moduleGraph.getModulesByFile(`virtual:nuxt:${encodeURIComponent(template.dst)}`) || []) {
server.moduleGraph.invalidateModule(mod) server.moduleGraph.invalidateModule(mod)
server.reloadModule(mod) server.reloadModule(mod)
} }

View File

@ -46,12 +46,12 @@
"jiti": "^2.4.2", "jiti": "^2.4.2",
"knitwork": "^1.2.0", "knitwork": "^1.2.0",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"memfs": "^4.15.3", "memfs": "^4.17.0",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^2.0.0", "pathe": "^2.0.1",
"pify": "^6.1.0", "pify": "^6.1.0",
"postcss": "^8.4.49", "postcss": "^8.5.1",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0", "postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1", "postcss-loader": "^8.1.1",
@ -77,14 +77,14 @@
"@types/pify": "5.0.4", "@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.30.0", "rollup": "4.30.1",
"unbuild": "3.2.0", "unbuild": "3.3.1",
"vue": "3.5.13" "vue": "3.5.13"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.3.4" "vue": "^3.3.4"
}, },
"engines": { "engines": {
"node": "^18.20.5 || ^20.9.0 || >=22.0.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -56,7 +56,7 @@ function clientNodeCompat (ctx: WebpackConfigContext) {
} }
ctx.config.plugins!.push(new webpack.DefinePlugin({ global: 'globalThis' })) ctx.config.plugins!.push(new webpack.DefinePlugin({ global: 'globalThis' }))
ctx.config.resolve = ctx.config.resolve || {} ctx.config.resolve ||= {}
ctx.config.resolve.fallback = { ctx.config.resolve.fallback = {
...env(nodeless).alias, ...env(nodeless).alias,
...ctx.config.resolve.fallback, ...ctx.config.resolve.fallback,
@ -92,7 +92,7 @@ function clientHMR (ctx: WebpackConfigContext) {
`webpack-hot-middleware/client?${hotMiddlewareClientOptionsStr}`, `webpack-hot-middleware/client?${hotMiddlewareClientOptionsStr}`,
) )
ctx.config.plugins = ctx.config.plugins || [] ctx.config.plugins ||= []
ctx.config.plugins.push(new webpack.HotModuleReplacementPlugin()) ctx.config.plugins.push(new webpack.HotModuleReplacementPlugin())
} }

View File

@ -85,7 +85,7 @@ function serverStandalone (ctx: WebpackConfigContext) {
} }
function serverPlugins (ctx: WebpackConfigContext) { function serverPlugins (ctx: WebpackConfigContext) {
ctx.config.plugins = ctx.config.plugins || [] ctx.config.plugins ||= []
// Server polyfills // Server polyfills
if (ctx.userConfig.serverURLPolyfill) { if (ctx.userConfig.serverURLPolyfill) {

View File

@ -50,7 +50,7 @@ function baseConfig (ctx: WebpackConfigContext) {
} }
function basePlugins (ctx: WebpackConfigContext) { function basePlugins (ctx: WebpackConfigContext) {
ctx.config.plugins = ctx.config.plugins || [] ctx.config.plugins ||= []
// Add timefix-plugin before other plugins // Add timefix-plugin before other plugins
if (ctx.options.dev) { if (ctx.options.dev) {

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,10 @@
"main", "main",
"3.x" "3.x"
], ],
"ignoreDeps": [
"node",
"npm"
],
"packageRules": [ "packageRules": [
{ {
"groupName": "vitest", "groupName": "vitest",

View File

@ -625,6 +625,44 @@ describe('pages', () => {
const html = await $fetch('/prerender/test') const html = await $fetch('/prerender/test')
expect(html).toContain('should be prerendered: true') expect(html).toContain('should be prerendered: true')
}) })
it('should trigger page:loading:end only once', async () => {
const { page, consoleLogs } = await renderPage('/')
await page.getByText('to page load hook').click()
await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/page-load-hook')
const loadingEndLogs = consoleLogs.filter(c => c.text.includes('page:loading:end'))
expect(loadingEndLogs.length).toBe(1)
await page.close()
})
it('should hide nuxt page load indicator after navigate back from nested page', async () => {
const LOAD_INDICATOR_SELECTOR = '.nuxt-loading-indicator'
const { page } = await renderPage('/page-load-hook')
await page.getByText('To sub page').click()
await page.waitForFunction(path => window.useNuxtApp?.()._route.fullPath === path, '/page-load-hook/subpage')
await page.waitForSelector(LOAD_INDICATOR_SELECTOR)
let isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR)
expect(isVisible).toBe(true)
await page.waitForSelector(LOAD_INDICATOR_SELECTOR, { state: 'hidden' })
isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR)
expect(isVisible).toBe(false)
await page.goBack()
await page.waitForSelector(LOAD_INDICATOR_SELECTOR)
isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR)
expect(isVisible).toBe(true)
await page.waitForSelector(LOAD_INDICATOR_SELECTOR, { state: 'hidden' })
isVisible = await page.isVisible(LOAD_INDICATOR_SELECTOR)
expect(isVisible).toBe(false)
await page.close()
})
}) })
describe('nuxt composables', () => { describe('nuxt composables', () => {
@ -2738,7 +2776,7 @@ describe('teleports', () => {
const html = await $fetch<string>('/nuxt-teleport') const html = await $fetch<string>('/nuxt-teleport')
// Teleport is appended to body, after the __nuxt div // Teleport is appended to body, after the __nuxt div
expect(html).toContain('<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div></div></div><span id="nuxt-teleport"><!--teleport start anchor--><div>Nuxt Teleport</div><!--teleport anchor--></span><script') expect(html).toContain('<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div></div><!--]--></div><span id="nuxt-teleport"><!--teleport start anchor--><div>Nuxt Teleport</div><!--teleport anchor--></span><script')
}) })
}) })

View File

@ -61,7 +61,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"210k"`) expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"210k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir) const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1396k"`) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1398k"`)
const packages = modules.files const packages = modules.files
.filter(m => m.endsWith('package.json')) .filter(m => m.endsWith('package.json'))
@ -86,6 +86,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"entities", "entities",
"estree-walker", "estree-walker",
"hookable", "hookable",
"packrup",
"source-map-js", "source-map-js",
"ufo", "ufo",
"unhead", "unhead",
@ -102,7 +103,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"560k"`) expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"560k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir) const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"94.4k"`) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"96.4k"`)
const packages = modules.files const packages = modules.files
.filter(m => m.endsWith('package.json')) .filter(m => m.endsWith('package.json'))
@ -116,6 +117,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"db0", "db0",
"devalue", "devalue",
"hookable", "hookable",
"packrup",
"unhead", "unhead",
] ]
`) `)
@ -128,7 +130,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"303k"`) expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"303k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir) const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1396k"`) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1398k"`)
const packages = modules.files const packages = modules.files
.filter(m => m.endsWith('package.json')) .filter(m => m.endsWith('package.json'))
@ -153,6 +155,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"entities", "entities",
"estree-walker", "estree-walker",
"hookable", "hookable",
"packrup",
"source-map-js", "source-map-js",
"ufo", "ufo",
"unhead", "unhead",

6
test/fixtures/basic/app.vue vendored Normal file
View File

@ -0,0 +1,6 @@
<template>
<NuxtLoadingIndicator :throttle="0" />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

View File

@ -94,6 +94,9 @@
<NuxtLink to="/server-page"> <NuxtLink to="/server-page">
to server page to server page
</NuxtLink> </NuxtLink>
<NuxtLink to="/page-load-hook">
to page load hook
</NuxtLink>
</div> </div>
</template> </template>

View File

@ -0,0 +1,9 @@
<template>
<div>
Page for hook tests.
<NuxtLink to="/page-load-hook/subpage">
To sub page
</NuxtLink>
<NuxtPage />
</div>
</template>

View File

@ -0,0 +1,7 @@
<template>
<div>
<NuxtLink to="/page-load-hook">
Back to parent
</NuxtLink>
</div>
</template>

View File

@ -0,0 +1,8 @@
export default defineNuxtPlugin((nuxtApp) => {
const route = useRoute()
nuxtApp.hook('page:loading:end', () => {
if (route.path === '/page-load-hook') {
console.log('page:loading:end')
}
})
})

View File

@ -134,7 +134,7 @@ if (process.env.TEST_ENV !== 'built' && !isWindows) {
'type': 'debug', 'type': 'debug',
}, },
{ {
'text': `[vite] hot updated: /@id/virtual:nuxt:${fixturePath}/.nuxt/routes.mjs`, 'text': `[vite] hot updated: /@id/virtual:nuxt:${encodeURIComponent(join(fixturePath, '.nuxt/routes.mjs'))}`,
'type': 'debug', 'type': 'debug',
}, },
]) ])