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

View File

@ -17,14 +17,14 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Check engine ranges, peer dependency ranges and installed versions
run: pnpm installed-check -d --fix
run: pnpm installed-check --no-include-workspace-root --ignore-dev --workspace-ignore='test/**,playground' --fix
- name: Build (stub)
run: pnpm dev:prepare
@ -55,4 +55,4 @@ jobs:
- name: Lint (code)
run: pnpm lint:fix
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
name: CI
name: ci
on:
push:
@ -29,7 +29,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@ -39,4 +39,4 @@ jobs:
run: pnpm sherif -r multiple-dependency-versions
- name: Check engine ranges, peer dependency ranges and installed versions
run: pnpm installed-check -d
run: pnpm installed-check --no-include-workspace-root --ignore-dev --workspace-ignore='test/**,playground'

View File

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

View File

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

View File

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

View File

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

View File

@ -202,6 +202,19 @@ const { data: discounts, status } = await useAsyncData('cart-discount', async ()
</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 about `useAsyncData`.
::

View File

@ -71,7 +71,7 @@ export const useFoo = () => {
### Access plugin injections
You can access [plugin injections](/docs/guide/directory-structure/plugins#automatically-providing-helpers) from composables:
You can access [plugin injections](/docs/guide/directory-structure/plugins#providing-helpers) from composables:
```js [composables/test.ts]
export const useHello = () => {

View File

@ -62,14 +62,23 @@ const { data: posts } = await useAsyncData(
## Params
- `key`: a unique key to ensure that data fetching can be properly de-duplicated across requests. If you do not provide a key, then a key that is unique to the file name and line number of the instance of `useAsyncData` will be generated for you.
- `handler`: an asynchronous function that must return a truthy value (for example, it should not be `undefined` or `null`) or the request may be duplicated on the client side
- `handler`: an asynchronous function that must return a truthy value (for example, it should not be `undefined` or `null`) or the request may be duplicated on the client side.
::warning
The `handler` function should be **side-effect free** to ensure predictable behavior during SSR and CSR hydration. If you need to trigger side effects, use the [`callOnce`](/docs/api/utils/call-once) utility to do so.
::
- `options`:
- `server`: whether to fetch the data on the server (defaults to `true`)
- `lazy`: whether to resolve the async function after loading the route, instead of blocking client-side navigation (defaults to `false`)
- `immediate`: when set to `false`, will prevent the request from firing immediately. (defaults to `true`)
- `default`: a factory function to set the default value of the `data`, before the async function resolves - useful with the `lazy: true` or `immediate: false` option
- `transform`: a function that can be used to alter `handler` function result after resolving
- `getCachedData`: Provide a function which returns cached data. A _null_ or _undefined_ return value will trigger a fetch. By default, this is: `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]`, which only caches data when `payloadExtraction` is enabled.
- `getCachedData`: Provide a function which returns cached data. A `null` or `undefined` return value will trigger a fetch. By default, this is:
```ts
const getDefaultCachedData = (key) => nuxtApp.isHydrating
? nuxtApp.payload.data[key]
: nuxtApp.static.data[key]
```
Which only caches data when `experimental.payloadExtraction` of `nuxt.config` is enabled.
- `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: watch reactive sources to auto-refresh
- `deep`: return data in a deep ref object. It is `false` by default to return data in a shallow ref object for performance.
@ -94,7 +103,13 @@ Learn how to use `transform` and `getCachedData` to avoid superfluous calls to a
- `data`: the result of the asynchronous function that is passed in.
- `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function.
- `error`: an error object if the data fetching failed.
- `status`: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`).
- `status`: a string indicating the status of the data request:
- `idle`: when the request has not started, such as:
- when `execute` has not yet been called and `{ immediate: false }` is set
- when rendering HTML on the server and `{ server: false }` is set
- `pending`: the request is in progress
- `success`: the request has completed successfully
- `error`: the request has failed
- `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `status` to `'idle'`, and mark any currently pending requests as cancelled.
By default, Nuxt waits until a `refresh` is finished before it can be executed again.

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`)
- `default`: a factory function to set the default value of the `data`, before the async function resolves - useful with the `lazy: true` or `immediate: false` option
- `transform`: a function that can be used to alter `handler` function result after resolving
- `getCachedData`: Provide a function which returns cached data. A _null_ or _undefined_ return value will trigger a fetch. By default, this is: `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]`, which only caches data when `payloadExtraction` is enabled.
- `getCachedData`: Provide a function which returns cached data. A `null` or `undefined` return value will trigger a fetch. By default, this is:
```ts
const getDefaultCachedData = (key) => nuxtApp.isHydrating
? nuxtApp.payload.data[key]
: nuxtApp.static.data[key]
```
Which only caches data when `experimental.payloadExtraction` of `nuxt.config` is enabled.
- `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: watch an array of reactive sources and auto-refresh the fetch result when they change. Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`. Together with `immediate: false`, this allows for a fully-manual `useFetch`. (You can [see an example here](/docs/getting-started/data-fetching#watch) of using `watch`.)
- `deep`: return data in a deep ref object. It is `false` by default to return data in a shallow ref object for performance.
@ -134,7 +140,13 @@ Learn how to use `transform` and `getCachedData` to avoid superfluous calls to a
- `data`: the result of the asynchronous function that is passed in.
- `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function.
- `error`: an error object if the data fetching failed.
- `status`: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`).
- `status`: a string indicating the status of the data request:
- `idle`: when the request has not started, such as:
- when `execute` has not yet been called and `{ immediate: false }` is set
- when rendering HTML on the server and `{ server: false }` is set
- `pending`: the request is in progress
- `success`: the request has completed successfully
- `error`: the request has failed
- `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `status` to `'idle'`, and mark any currently pending requests as cancelled.
By default, Nuxt waits until a `refresh` is finished before it can be executed again.
@ -147,7 +159,7 @@ If you have not fetched data on the server (for example, with `server: false`),
```ts [Signature]
function useFetch<DataT, ErrorT>(
url: string | Request | Ref<string | Request> | (() => string) | Request,
url: string | Request | Ref<string | Request> | (() => string | Request),
options?: UseFetchOptions<DataT>
): Promise<AsyncData<DataT, ErrorT>>

View File

@ -4,7 +4,7 @@ description: The recommended way to provide head data with user input.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/unjs/unhead/blob/main/packages/unhead/src/composables/useHeadSafe.ts
to: https://github.com/unjs/unhead/blob/main/packages/vue/src/composables/useHeadSafe.ts
size: xs
---

View File

@ -4,7 +4,7 @@ description: useHead customizes the head properties of individual pages of your
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/unjs/unhead/blob/main/packages/unhead/src/composables/useHead.ts
to: https://github.com/unjs/unhead/blob/main/packages/vue/src/composables/useHead.ts
size: xs
---

View File

@ -9,11 +9,27 @@ links:
---
::note
`useNuxtData` gives you access to the current cached value of [`useAsyncData`](/docs/api/composables/use-async-data) , `useLazyAsyncData`, [`useFetch`](/docs/api/composables/use-fetch) and [`useLazyFetch`](/docs/api/composables/use-lazy-fetch) with explicitly provided key.
`useNuxtData` gives you access to the current cached value of [`useAsyncData`](/docs/api/composables/use-async-data) , [`useLazyAsyncData`](/docs/api/composables/use-lazy-async-data), [`useFetch`](/docs/api/composables/use-fetch) and [`useLazyFetch`](/docs/api/composables/use-lazy-fetch) with explicitly provided key.
::
## Usage
The `useNuxtData` composable is used to access the current cached value of data-fetching composables such as `useAsyncData`, `useLazyAsyncData`, `useFetch`, and `useLazyFetch`. By providing the key used during the data fetch, you can retrieve the cached data and use it as needed.
This is particularly useful for optimizing performance by reusing already-fetched data or implementing features like Optimistic Updates or cascading data updates.
To use `useNuxtData`, ensure that the data-fetching composable (`useFetch`, `useAsyncData`, etc.) has been called with an explicitly provided key.
## Params
- `key`: The unique key that identifies the cached data. This key should match the one used during the original data fetch.
## Return Values
- `data`: A reactive reference to the cached data associated with the provided key. If no cached data exists, the value will be `null`. This `Ref` automatically updates if the cached data changes, allowing seamless reactivity in your components.
## Example
The example below shows how you can use cached data as a placeholder while the most recent data is being fetched from the server.
```vue [pages/posts.vue]
@ -26,13 +42,15 @@ const { data } = await useFetch('/api/posts', { key: 'posts' })
```vue [pages/posts/[id\\].vue]
<script setup lang="ts">
// Access to the cached value of useFetch in posts.vue (parent route)
const { id } = useRoute().params
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() {
// 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>
@ -40,7 +58,9 @@ const { data } = useLazyFetch(`/api/posts/${id}`, {
## Optimistic Updates
We can leverage the cache to update the UI after a mutation, while the data is being invalidated in the background.
The example below shows how implementing Optimistic Updates can be achieved using useNuxtData.
Optimistic Updates is a technique where the user interface is updated immediately, assuming a server operation will succeed. If the operation eventually fails, the UI is rolled back to its previous state.
```vue [pages/todos.vue]
<script setup lang="ts">
@ -52,28 +72,34 @@ const { data } = await useAsyncData('todos', () => $fetch('/api/todos'))
```vue [components/NewTodo.vue]
<script setup lang="ts">
const newTodo = ref('')
const previousTodos = ref([])
let previousTodos = []
// Access to the cached value of useAsyncData in todos.vue
const { data: todos } = useNuxtData('todos')
const { data } = await useFetch('/api/addTodo', {
async function addTodo () {
return $fetch('/api/addTodo', {
method: 'post',
body: {
todo: newTodo.value
},
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 () {
todos.value = previousTodos.value // Rollback the data if the request failed.
onResponseError () {
// Rollback the data if the request failed.
todos.value = previousTodos
},
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>
```

View File

@ -4,7 +4,7 @@ description: The useSeoMeta composable lets you define your site's SEO meta tags
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/unjs/unhead/blob/main/packages/unhead/src/composables/useSeoMeta.ts
to: https://github.com/unjs/unhead/blob/main/packages/vue/src/composables/useSeoMeta.ts
size: xs
---

View File

@ -4,7 +4,7 @@ description: The useServerSeoMeta composable lets you define your site's SEO met
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/unjs/unhead/blob/main/packages/unhead/src/composables/useServerSeoMeta.ts
to: https://github.com/unjs/unhead/blob/main/packages/vue/src/composables/useServerSeoMeta.ts
size: xs
---

View File

@ -4,7 +4,7 @@ description: 'Nuxt command to build your Nuxt module before publishing.'
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/cli/blob/main/src/commands/build-module.ts
to: https://github.com/nuxt/module-builder/blob/main/src/cli.ts
size: xs
---

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
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
**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)
**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;

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import { MODE_RE } from './utils'
export async function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) {
const nuxt = useNuxt()
await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt)
nuxt.options.components = nuxt.options.components || []
nuxt.options.components ||= []
dir.priority ||= 0
nuxt.hook('components:dirs', (dirs) => { dirs[opts.prepend ? 'unshift' : 'push'](dir) })
}
@ -26,7 +26,7 @@ export type AddComponentOptions = { name: string, filePath: string } & Partial<E
export async function addComponent (opts: AddComponentOptions) {
const nuxt = useNuxt()
await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt)
nuxt.options.components = nuxt.options.components || []
nuxt.options.components ||= []
if (!opts.mode) {
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>()
for (const layer of layers) {
// Resolve `rootDir` & `srcDir` of layers
layer.config = layer.config || {}
layer.config ||= {}
layer.config.rootDir = layer.config.rootDir ?? layer.cwd!
// Only process/resolve layers once

View File

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

View File

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

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

View File

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

View File

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

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 type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema'
import { useNuxt } from './context'
import { resolvePathSync } from 'mlly'
import { isWindows } from 'std-env'
import { MODE_RE, filterInPlace } from './utils'
import { tryUseNuxt, useNuxt } from './context'
import { addTemplate } from './template'
import { resolveAlias } from './resolve'
import { MODE_RE } from './utils'
/**
* Normalize a nuxt plugin object
*/
const pluginSymbol = Symbol.for('nuxt plugin')
export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
// Normalize src
if (typeof plugin === 'string') {
@ -16,6 +22,10 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin = { ...plugin }
}
if (pluginSymbol in plugin) {
return plugin
}
if (!plugin.src) {
throw new Error('Invalid plugin. src option is required: ' + JSON.stringify(plugin))
}
@ -23,6 +33,14 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
// Normalize full path to plugin
plugin.src = normalize(resolveAlias(plugin.src))
if (!existsSync(plugin.src) && isAbsolute(plugin.src)) {
try {
plugin.src = resolvePathSync(isWindows ? pathToFileURL(plugin.src).href : plugin.src, { extensions: tryUseNuxt()?.options.extensions })
} catch {
// ignore errors as the file may be in the nuxt vfs
}
}
// Normalize mode
if (plugin.ssr) {
plugin.mode = 'server'
@ -32,6 +50,9 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin.mode = mode as 'all' | 'client' | 'server'
}
// @ts-expect-error not adding symbol to types to avoid conflicts
plugin[pluginSymbol] = true
return plugin
}
@ -61,7 +82,7 @@ export function addPlugin (_plugin: NuxtPlugin | string, opts: AddPluginOptions
const plugin = normalizePlugin(_plugin)
// Remove any existing plugin with the same src
nuxt.options.plugins = nuxt.options.plugins.filter(p => normalizePlugin(p).src !== plugin.src)
filterInPlace(nuxt.options.plugins, p => normalizePlugin(p).src !== plugin.src)
// Prepend to array by default to be before user provided plugins since is usually used by modules
nuxt.options.plugins[opts.append ? 'push' : 'unshift'](plugin)

View File

@ -8,6 +8,7 @@ import type { TSConfig } from 'pkg-types'
import { gte } from 'semver'
import { readPackageJSON } from 'pkg-types'
import { filterInPlace } from './utils'
import { tryResolveModule } from './internal/esm'
import { getDirectory } from './module/install'
import { tryUseNuxt, useNuxt } from './context'
@ -23,7 +24,7 @@ export function addTemplate<T> (_template: NuxtTemplate<T> | string) {
const template = normalizeTemplate(_template)
// Remove any existing template with the same destination path
nuxt.options.build.templates = nuxt.options.build.templates.filter(p => normalizeTemplate(p).dst !== template.dst)
filterInPlace(nuxt.options.build.templates, p => normalizeTemplate(p).dst !== template.dst)
// Add to templates array
nuxt.options.build.templates.push(template)
@ -229,9 +230,9 @@ export async function _generateTypes (nuxt: Nuxt) {
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
: nuxt.options.buildDir
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
tsConfig.compilerOptions.paths = tsConfig.compilerOptions.paths || {}
tsConfig.include = tsConfig.include || []
tsConfig.compilerOptions ||= {}
tsConfig.compilerOptions.paths ||= {}
tsConfig.include ||= []
for (const alias in aliases) {
if (excludedAlias.some(re => re.test(alias))) {
@ -291,6 +292,10 @@ export async function _generateTypes (nuxt: Nuxt) {
}))
}
// Ensure `#build` is placed at the end of the paths object.
// https://github.com/nuxt/nuxt/issues/30325
sortTsPaths(tsConfig.compilerOptions.paths)
tsConfig.include = [...new Set(tsConfig.include.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
tsConfig.exclude = [...new Set(tsConfig.exclude!.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
@ -330,6 +335,17 @@ export async function writeTypes (nuxt: Nuxt) {
await writeFile()
}
function sortTsPaths (paths: Record<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>) {
const attrs: string[] = []
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]
}
/**
* 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+)*$/

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
import 'nuxi/cli'
import '@nuxt/cli/cli'

View File

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

View File

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

View File

@ -95,7 +95,7 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
if (isPromise(setupState)) {
return Promise.resolve(setupState).then((setupState) => {
if (typeof setupState !== 'function') {
setupState = setupState || {}
setupState ||= {}
setupState.mounted$ = mounted$
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 }
function shouldPrefetch (mode: 'visibility' | 'interaction') {
if (import.meta.server) { return }
return !prefetched.value && (typeof props.prefetchOn === 'string' ? props.prefetchOn === mode : (props.prefetchOn?.[mode] ?? options.prefetchOn?.[mode])) && (props.prefetch ?? options.prefetch) !== false && props.noPrefetch !== true && props.target !== '_blank' && !isSlowConnection()
}
async function prefetch (nuxtApp = useNuxtApp()) {
if (import.meta.server) { return }
if (prefetched.value) { return }
prefetched.value = true
@ -395,6 +398,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
// `custom` API cannot support fallthrough attributes as the slot
// may render fragment or text root nodes (#14897, #19375)
if (!props.custom) {
if (import.meta.client) {
if (shouldPrefetch('interaction')) {
routerLinkProps.onPointerenter = prefetch.bind(null, undefined)
routerLinkProps.onFocus = prefetch.bind(null, undefined)
@ -402,6 +406,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
if (prefetched.value) {
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
}
}
routerLinkProps.rel = props.rel || undefined
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -81,7 +81,7 @@ const nightlies = {
'@nuxt/kit': '@nuxt/kit-nightly',
}
const keyDependencies = [
export const keyDependencies = [
'@nuxt/kit',
'@nuxt/schema',
]
@ -802,8 +802,13 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
const nuxt = createNuxt(options)
if (nuxt.options.dev && !nuxt.options.test) {
nuxt.hooks.hookOnce('build:done', () => {
for (const dep of keyDependencies) {
checkDependencyVersion(dep, nuxt._version)
.catch(e => logger.warn(`Problem checking \`${dep}\` version.`, e))
}
})
}
// We register hooks layer-by-layer so any overrides need to be registered separately
@ -822,7 +827,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<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)
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}.`])
}

View File

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

View File

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

View File

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

View File

@ -53,12 +53,30 @@ export function withLocations<T> (node: T): 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 type: string
readonly scope: string
node: WithLocations<T>
constructor (node: WithLocations<T>) {
constructor (node: WithLocations<T>, scope: string) {
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.
*/
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> {
@ -90,8 +116,8 @@ class FunctionParamNode extends BaseNode {
type = 'FunctionParam' as const
fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>
constructor (node: WithLocations<Node>, fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) {
super(node)
constructor (node: WithLocations<Node>, scope: string, fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) {
super(node, scope)
this.fnNode = fnNode
}
@ -120,8 +146,8 @@ class VariableNode extends BaseNode<Identifier> {
type = 'Variable' as const
variableNode: WithLocations<VariableDeclaration>
constructor (node: WithLocations<Identifier>, variableNode: WithLocations<VariableDeclaration>) {
super(node)
constructor (node: WithLocations<Identifier>, scope: string, variableNode: WithLocations<VariableDeclaration>) {
super(node, scope)
this.variableNode = variableNode
}
@ -138,8 +164,8 @@ class ImportNode extends BaseNode<ImportSpecifier | ImportDefaultSpecifier | Imp
type = 'Import' as const
importNode: WithLocations<Node>
constructor (node: WithLocations<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>, importNode: WithLocations<Node>) {
super(node)
constructor (node: WithLocations<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>, scope: string, importNode: WithLocations<Node>) {
super(node, scope)
this.importNode = importNode
}
@ -156,8 +182,8 @@ class CatchParamNode extends BaseNode {
type = 'CatchParam' as const
catchNode: WithLocations<CatchClause>
constructor (node: WithLocations<Node>, catchNode: WithLocations<CatchClause>) {
super(node)
constructor (node: WithLocations<Node>, scope: string, catchNode: WithLocations<CatchClause>) {
super(node, scope)
this.catchNode = catchNode
}
@ -264,7 +290,7 @@ export class ScopeTracker {
const identifiers = getPatternIdentifiers(param)
for (const identifier of identifiers) {
this.declareIdentifier(identifier.name, new FunctionParamNode(identifier, fn))
this.declareIdentifier(identifier.name, new FunctionParamNode(identifier, this.scopeIndexKey, fn))
}
}
@ -276,10 +302,10 @@ export class ScopeTracker {
this.declareIdentifier(
identifier.name,
parent.type === 'VariableDeclaration'
? new VariableNode(identifier, parent)
? new VariableNode(identifier, this.scopeIndexKey, parent)
: parent.type === 'CatchClause'
? new CatchParamNode(identifier, parent)
: new FunctionParamNode(identifier, parent),
? new CatchParamNode(identifier, this.scopeIndexKey, parent)
: new FunctionParamNode(identifier, this.scopeIndexKey, parent),
)
}
}
@ -295,7 +321,7 @@ export class ScopeTracker {
case 'FunctionDeclaration':
// declare function name for named functions, skip for `export default`
if (node.id?.name) {
this.declareIdentifier(node.id.name, new FunctionNode(node))
this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
}
this.pushScope()
for (const param of node.params) {
@ -309,7 +335,7 @@ export class ScopeTracker {
this.pushScope()
// can be undefined, for example in class method definitions
if (node.id?.name) {
this.declareIdentifier(node.id.name, new FunctionNode(node))
this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
}
this.pushScope()
@ -333,7 +359,7 @@ export class ScopeTracker {
case 'ClassDeclaration':
// declare class name for named classes, skip for `export default`
if (node.id?.name) {
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id)))
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id), this.scopeIndexKey))
}
break
@ -342,13 +368,13 @@ export class ScopeTracker {
// e.g. const MyClass = class InternalClassName { // InternalClassName is only available within the class body
this.pushScope()
if (node.id?.name) {
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id)))
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id), this.scopeIndexKey))
}
break
case 'ImportDeclaration':
for (const specifier of node.specifiers) {
this.declareIdentifier(specifier.local.name, new ImportNode(withLocations(specifier), node))
this.declareIdentifier(specifier.local.name, new ImportNode(withLocations(specifier), this.scopeIndexKey, node))
}
break
@ -429,6 +455,26 @@ export class ScopeTracker {
return null
}
getCurrentScope () {
return this.scopeIndexKey
}
/**
* Check if the current scope is a child of a specific scope.
* @example
* ```ts
* // current scope is 0-1
* isCurrentScopeUnder('0') // true
* isCurrentScopeUnder('0-1') // false
* ```
*
* @param scope the parent scope
* @returns `true` if the current scope is a child of the specified scope, `false` otherwise (also when they are the same)
*/
isCurrentScopeUnder (scope: string) {
return isChildScope(this.scopeIndexKey, scope)
}
/**
* Freezes the scope tracker, preventing further declarations.
* It also resets the scope index stack to its initial state, so that the scope tracker can be reused.

View File

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

View File

@ -228,6 +228,8 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
if (!meta) { return }
const definePageMetaScope = scopeTracker.getCurrentScope()
walk(meta, {
scopeTracker,
enter (node, parent) {
@ -236,10 +238,24 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
|| node.type !== 'Identifier' // checking for `node.type` to narrow down the type
) { return }
const declaration = scopeTracker.getDeclaration(node.name)
if (declaration) {
// check if the declaration was made inside `definePageMeta` and if so, do not process it
// (ensures that we don't hoist local variables in inline middleware, for example)
if (
declaration.isUnderScope(definePageMetaScope)
// ensures that we compare the correct declaration to the reference
// (when in the same scope, the declaration must come before the reference, otherwise it must be in a parent scope)
&& (scopeTracker.isCurrentScopeUnder(declaration.scope) || declaration.start < node.start)
) {
return
}
}
if (isStaticIdentifier(node.name)) {
addImport(node.name)
} else {
processDeclaration(scopeTracker.getDeclaration(node.name))
} else if (declaration) {
processDeclaration(declaration)
}
},
})
@ -271,9 +287,9 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
handleHotUpdate: {
order: 'post',
handler: ({ file, modules, server }) => {
if (options.isPage?.(file)) {
if (options.routesPath && options.isPage?.(file)) {
const macroModule = server.moduleGraph.getModuleById(file + '?macro=true')
const routesModule = server.moduleGraph.getModuleById('virtual:nuxt:' + options.routesPath)
const routesModule = server.moduleGraph.getModuleById('virtual:nuxt:' + encodeURIComponent(options.routesPath))
return [
...modules,
...macroModule ? [macroModule] : [],

View File

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

View File

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

View File

@ -292,6 +292,8 @@ async function getResolvedApp (files: Array<string | { name: string, contents: s
}
for (const plugin of app.plugins) {
plugin.src = normaliseToRepo(plugin.src)!
// @ts-expect-error untyped symbol
delete plugin[Symbol.for('nuxt plugin')]
}
for (const mw of app.middleware) {
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 { normalize } from 'pathe'
import { withoutTrailingSlash } from 'ufo'
import { readPackageJSON } from 'pkg-types'
import { inc } from 'semver'
import { loadNuxt } from '../src'
import { version } from '../package.json'
const repoRoot = withoutTrailingSlash(normalize(fileURLToPath(new URL('../../../', import.meta.url))))
@ -45,45 +42,3 @@ describe('loadNuxt', () => {
expect(hookRan).toBe(true)
})
})
describe('dependency mismatch', () => {
it('expect mismatched dependency to log a warning', async () => {
vi.mocked(readPackageJSON).mockReturnValue(Promise.resolve({
version: '3.0.0',
}))
const nuxt = await loadNuxt({
cwd: repoRoot,
})
// @nuxt/kit is explicitly installed in repo root but @nuxt/schema isn't, so we only
// get warnings about @nuxt/schema
expect(console.warn).toHaveBeenCalledWith(`[nuxt] Expected \`@nuxt/kit\` to be at least \`${version}\` but got \`3.0.0\`. This might lead to unexpected behavior. Check your package.json or refresh your lockfile.`)
vi.mocked(readPackageJSON).mockRestore()
await nuxt.close()
})
it.each([
{
name: 'nuxt version is lower',
depVersion: inc(version, 'minor'),
},
{
name: 'version matches',
depVersion: version,
},
])('expect no warning when $name.', async ({ depVersion }) => {
vi.mocked(readPackageJSON).mockReturnValue(Promise.resolve({
depVersion,
}))
const nuxt = await loadNuxt({
cwd: repoRoot,
})
expect(console.warn).not.toHaveBeenCalled()
await nuxt.close()
vi.mocked(readPackageJSON).mockRestore()
})
})

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 () => {
const sfc = `
<script setup lang="ts">
@ -516,7 +698,12 @@ definePageMeta({
test () {}
}
console.log(hoisted.value)
const someFunction = () => {
const someValue = 'someValue'
console.log(someValue)
}
console.log(hoisted.value, val)
},
],
validate: (route) => {
@ -564,7 +751,12 @@ const hoisted = ref('hoisted')
test () {}
}
console.log(hoisted.value)
const someFunction = () => {
const someValue = 'someValue'
console.log(someValue)
}
console.log(hoisted.value, val)
},
],
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 { TestScopeTracker } from './fixture/scope-tracker'
@ -667,4 +667,87 @@ describe('parsing', () => {
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",
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
"memfs": "^4.15.3",
"memfs": "^4.17.0",
"ohash": "^1.1.4",
"pathe": "^2.0.0",
"pathe": "^2.0.1",
"pify": "^6.1.0",
"postcss": "^8.4.49",
"postcss": "^8.5.1",
"postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1",
@ -75,14 +75,14 @@
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.30.0",
"unbuild": "3.2.0",
"rollup": "4.30.1",
"unbuild": "3.3.1",
"vue": "3.5.13"
},
"peerDependencies": {
"vue": "^3.3.4"
},
"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/builder-env',
],
hooks: {
'rollup:options' (ctx, options) {
ctx.options.rollup.dts.respectExternal = false
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)
}
},
rollup: {
dts: { respectExternal: false },
inlineDependencies: ['untyped', 'knitwork'],
},
externals: [
// Type imports

View File

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

View File

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

View File

@ -26,8 +26,8 @@
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"rollup": "4.30.0",
"unbuild": "3.2.0",
"rollup": "4.30.1",
"unbuild": "3.3.1",
"vue": "3.5.13"
},
"dependencies": {
@ -36,7 +36,7 @@
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"autoprefixer": "^10.4.20",
"consola": "^3.3.3",
"consola": "^3.4.0",
"cssnano": "^7.0.6",
"defu": "^6.1.4",
"esbuild": "^0.24.2",
@ -47,10 +47,10 @@
"jiti": "^2.4.2",
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
"mlly": "^1.7.3",
"pathe": "^2.0.0",
"pkg-types": "^1.3.0",
"postcss": "^8.4.49",
"mlly": "^1.7.4",
"pathe": "^2.0.1",
"pkg-types": "^1.3.1",
"postcss": "^8.5.1",
"rollup-plugin-visualizer": "^5.13.1",
"std-env": "^3.8.0",
"ufo": "^1.5.4",
@ -65,6 +65,6 @@
"vue": "^3.3.4"
},
"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: {
alias: {
// work around vite optimizer bug
'#app-manifest': 'unenv/runtime/mock/empty',
// user aliases
...nodeCompat.alias,
...ctx.config.resolve?.alias,
'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
ctx.nuxt.hook('app:templatesGenerated', (_app, 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 || []) {
markInvalidate(mod)

View File

@ -212,7 +212,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
// Invalidate virtual modules when templates are re-generated
ctx.nuxt.hook('app:templatesGenerated', (_app, 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.reloadModule(mod)
}

View File

@ -46,12 +46,12 @@
"jiti": "^2.4.2",
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
"memfs": "^4.15.3",
"memfs": "^4.17.0",
"mini-css-extract-plugin": "^2.9.2",
"ohash": "^1.1.4",
"pathe": "^2.0.0",
"pathe": "^2.0.1",
"pify": "^6.1.0",
"postcss": "^8.4.49",
"postcss": "^8.5.1",
"postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1",
@ -77,14 +77,14 @@
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.30.0",
"unbuild": "3.2.0",
"rollup": "4.30.1",
"unbuild": "3.3.1",
"vue": "3.5.13"
},
"peerDependencies": {
"vue": "^3.3.4"
},
"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.resolve = ctx.config.resolve || {}
ctx.config.resolve ||= {}
ctx.config.resolve.fallback = {
...env(nodeless).alias,
...ctx.config.resolve.fallback,
@ -92,7 +92,7 @@ function clientHMR (ctx: WebpackConfigContext) {
`webpack-hot-middleware/client?${hotMiddlewareClientOptionsStr}`,
)
ctx.config.plugins = ctx.config.plugins || []
ctx.config.plugins ||= []
ctx.config.plugins.push(new webpack.HotModuleReplacementPlugin())
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -625,6 +625,44 @@ describe('pages', () => {
const html = await $fetch('/prerender/test')
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', () => {
@ -2738,7 +2776,7 @@ describe('teleports', () => {
const html = await $fetch<string>('/nuxt-teleport')
// 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"`)
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
.filter(m => m.endsWith('package.json'))
@ -86,6 +86,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"entities",
"estree-walker",
"hookable",
"packrup",
"source-map-js",
"ufo",
"unhead",
@ -102,7 +103,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"560k"`)
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
.filter(m => m.endsWith('package.json'))
@ -116,6 +117,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"db0",
"devalue",
"hookable",
"packrup",
"unhead",
]
`)
@ -128,7 +130,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"303k"`)
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
.filter(m => m.endsWith('package.json'))
@ -153,6 +155,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"entities",
"estree-walker",
"hookable",
"packrup",
"source-map-js",
"ufo",
"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">
to server page
</NuxtLink>
<NuxtLink to="/page-load-hook">
to page load hook
</NuxtLink>
</div>
</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',
},
{
'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',
},
])