mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 01:15:58 +00:00
Merge branch 'main' into fix/20667-omit-usefetch-body-for-get-method
This commit is contained in:
commit
fe4e742cfa
@ -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 && \
|
||||
|
4
.github/workflows/autofix-docs.yml
vendored
4
.github/workflows/autofix-docs.yml
vendored
@ -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
|
||||
|
6
.github/workflows/autofix.yml
vendored
6
.github/workflows/autofix.yml
vendored
@ -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
|
||||
|
2
.github/workflows/benchmark.yml
vendored
2
.github/workflows/benchmark.yml
vendored
@ -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
|
||||
|
2
.github/workflows/changelog.yml
vendored
2
.github/workflows/changelog.yml
vendored
@ -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
|
||||
|
49
.github/workflows/ci.yml
vendored
49
.github/workflows/ci.yml
vendored
@ -41,7 +41,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
@ -56,11 +56,8 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Check types
|
||||
run: pnpm test:attw
|
||||
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
retention-days: 3
|
||||
name: dist
|
||||
@ -81,7 +78,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
|
||||
with:
|
||||
config: |
|
||||
paths:
|
||||
@ -98,7 +95,7 @@ jobs:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
@ -118,7 +115,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
@ -149,7 +146,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
@ -161,6 +158,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Check built types
|
||||
run: pnpm test:attw
|
||||
|
||||
test-unit:
|
||||
# autofix workflow will be triggered instead for PRs
|
||||
if: github.event_name == 'push'
|
||||
@ -173,7 +173,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
@ -260,20 +260,18 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build-release:
|
||||
release-nightly:
|
||||
concurrency:
|
||||
group: release
|
||||
permissions:
|
||||
id-token: write
|
||||
if: |
|
||||
github.event_name == 'push' &&
|
||||
github.repository == 'nuxt/nuxt' &&
|
||||
github.repository_owner == 'nuxt' &&
|
||||
!contains(github.event.head_commit.message, '[skip-release]') &&
|
||||
!startsWith(github.event.head_commit.message, 'docs')
|
||||
needs:
|
||||
- lint
|
||||
- build
|
||||
- test-fixtures
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
@ -284,7 +282,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
@ -299,22 +297,13 @@ jobs:
|
||||
- name: Release Edge
|
||||
run: ./scripts/release-edge.sh ${{ github.ref == 'refs/heads/main' && 'latest' || '3x' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
release-pr:
|
||||
concurrency:
|
||||
group: release
|
||||
permissions:
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
contains(github.event.pull_request.labels.*.name, '🧷 edge release')
|
||||
if: github.repository_owner == 'nuxt' && github.event_name != 'push'
|
||||
needs:
|
||||
- lint
|
||||
- build
|
||||
- test-fixtures
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
@ -325,7 +314,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
@ -337,8 +326,4 @@ jobs:
|
||||
name: dist
|
||||
path: packages
|
||||
|
||||
- name: Release Edge
|
||||
run: ./scripts/release-edge.sh pr-${{ github.event.issue.number }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
- run: pnpm pkg-pr-new publish --compact './packages/kit' './packages/nuxt' './packages/rspack' './packages/schema' './packages/vite' './packages/webpack'
|
||||
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@ -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
|
||||
|
6
.github/workflows/lint-monorepo.yml
vendored
6
.github/workflows/lint-monorepo.yml
vendored
@ -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'
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -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"
|
||||
|
||||
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
@ -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
|
||||
|
2
.github/workflows/semantic-pull-requests.yml
vendored
2
.github/workflows/semantic-pull-requests.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -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`.
|
||||
::
|
||||
|
@ -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 = () => {
|
||||
|
@ -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.
|
||||
|
@ -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>>
|
||||
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
|
@ -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', {
|
||||
method: 'post',
|
||||
body: {
|
||||
todo: newTodo.value
|
||||
},
|
||||
onRequest () {
|
||||
previousTodos.value = todos.value // Store the previously cached value to restore if fetch fails.
|
||||
async function addTodo () {
|
||||
return $fetch('/api/addTodo', {
|
||||
method: 'post',
|
||||
body: {
|
||||
todo: newTodo.value
|
||||
},
|
||||
onRequest () {
|
||||
// Store the previously cached value to restore if fetch fails.
|
||||
previousTodos = todos.value
|
||||
|
||||
todos.value.push(newTodo.value) // Optimistically update the todos.
|
||||
},
|
||||
onRequestError () {
|
||||
todos.value = previousTodos.value // Rollback the data if the request failed.
|
||||
},
|
||||
async onResponse () {
|
||||
await refreshNuxtData('todos') // Invalidate todos in the background if the request succeeded.
|
||||
}
|
||||
})
|
||||
// Optimistically update the todos.
|
||||
todos.value = [...todos.value, newTodo.value]
|
||||
},
|
||||
onResponseError () {
|
||||
// Rollback the data if the request failed.
|
||||
todos.value = previousTodos
|
||||
},
|
||||
async onResponse () {
|
||||
// Invalidate todos in the background if the request succeeded.
|
||||
await refreshNuxtData('todos')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
|
@ -63,7 +63,7 @@ Each active version has its own nightly releases which are generated automatical
|
||||
|
||||
Release | | Initial release | End Of Life | Docs
|
||||
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
|
||||
**4.x** (scheduled) | | 2024 Q3 | |
|
||||
**4.x** (scheduled) | | approximately 1 month after release of nitro v3 | |
|
||||
**3.x** (stable) | <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 |
|
||||
|
61
package.json
61
package.json
@ -39,12 +39,12 @@
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@types/node": "22.10.5",
|
||||
"@unhead/dom": "1.11.14",
|
||||
"@unhead/schema": "1.11.14",
|
||||
"@unhead/shared": "1.11.14",
|
||||
"@unhead/ssr": "1.11.14",
|
||||
"@unhead/vue": "1.11.14",
|
||||
"@types/node": "22.10.6",
|
||||
"@unhead/dom": "1.11.18",
|
||||
"@unhead/schema": "1.11.18",
|
||||
"@unhead/shared": "1.11.18",
|
||||
"@unhead/ssr": "1.11.18",
|
||||
"@unhead/vue": "1.11.18",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
@ -55,62 +55,64 @@
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
|
||||
"nuxt": "workspace:*",
|
||||
"ohash": "1.1.4",
|
||||
"postcss": "8.4.49",
|
||||
"rollup": "4.30.0",
|
||||
"postcss": "8.5.1",
|
||||
"rollup": "4.30.1",
|
||||
"send": ">=1.1.0",
|
||||
"typescript": "5.7.2",
|
||||
"typescript": "5.7.3",
|
||||
"ufo": "1.5.4",
|
||||
"unbuild": "3.2.0",
|
||||
"unhead": "1.11.14",
|
||||
"unimport": "3.14.5",
|
||||
"unbuild": "3.3.1",
|
||||
"unhead": "1.11.18",
|
||||
"unimport": "3.14.6",
|
||||
"vite": "6.0.7",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "0.17.2",
|
||||
"@nuxt/eslint-config": "0.7.4",
|
||||
"@arethetypeswrong/cli": "0.17.3",
|
||||
"@nuxt/cli": "3.20.0",
|
||||
"@nuxt/eslint-config": "0.7.5",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@nuxt/rspack-builder": "workspace:*",
|
||||
"@nuxt/test-utils": "3.15.1",
|
||||
"@nuxt/test-utils": "3.15.4",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/node": "22.10.5",
|
||||
"@types/node": "22.10.6",
|
||||
"@types/semver": "7.5.8",
|
||||
"@unhead/schema": "1.11.14",
|
||||
"@unhead/vue": "1.11.14",
|
||||
"@unhead/schema": "1.11.18",
|
||||
"@unhead/vue": "1.11.18",
|
||||
"@vitest/coverage-v8": "2.1.8",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "10.4.20",
|
||||
"case-police": "0.7.2",
|
||||
"changelogen": "0.5.7",
|
||||
"consola": "3.3.3",
|
||||
"consola": "3.4.0",
|
||||
"cssnano": "7.0.6",
|
||||
"destr": "2.0.3",
|
||||
"devalue": "5.1.1",
|
||||
"eslint": "9.17.0",
|
||||
"eslint": "9.18.0",
|
||||
"eslint-plugin-no-only-tests": "3.3.0",
|
||||
"eslint-plugin-perfectionist": "4.6.0",
|
||||
"eslint-typegen": "0.3.2",
|
||||
"eslint-typegen": "1.0.0",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"happy-dom": "16.3.0",
|
||||
"happy-dom": "16.6.0",
|
||||
"installed-check": "9.3.0",
|
||||
"jiti": "2.4.2",
|
||||
"knip": "5.41.1",
|
||||
"knip": "5.42.1",
|
||||
"markdownlint-cli": "0.43.0",
|
||||
"memfs": "4.15.3",
|
||||
"memfs": "4.17.0",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
|
||||
"nuxi": "3.18.2",
|
||||
"nuxt": "workspace:*",
|
||||
"nuxt-content-twoslash": "0.1.2",
|
||||
"ofetch": "1.4.1",
|
||||
"pathe": "2.0.0",
|
||||
"pathe": "2.0.1",
|
||||
"pkg-pr-new": "0.0.39",
|
||||
"playwright-core": "1.49.1",
|
||||
"rollup": "4.30.1",
|
||||
"semver": "7.6.3",
|
||||
"sherif": "1.1.1",
|
||||
"std-env": "3.8.0",
|
||||
"tinyexec": "0.3.2",
|
||||
"tinyglobby": "0.2.10",
|
||||
"typescript": "5.7.2",
|
||||
"typescript": "5.7.3",
|
||||
"ufo": "1.5.4",
|
||||
"vitest": "2.1.8",
|
||||
"vitest-environment-nuxt": "1.0.1",
|
||||
@ -118,9 +120,6 @@
|
||||
"vue-tsc": "2.2.0",
|
||||
"webpack": "5.97.1"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.3",
|
||||
"engines": {
|
||||
"node": "^18.20.4 || ^20.9.0 || ^22.0.0 || >=23.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"version": ""
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) || []
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export async function installModule<
|
||||
}
|
||||
}
|
||||
|
||||
nuxt.options._installedModules = nuxt.options._installedModules || []
|
||||
nuxt.options._installedModules ||= []
|
||||
const entryPath = typeof moduleToInstall === 'string' ? resolveAlias(moduleToInstall) : undefined
|
||||
|
||||
if (typeof moduleToInstall === 'string' && entryPath !== moduleToInstall) {
|
||||
@ -95,31 +95,28 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
|
||||
paths.add(nuxtModule)
|
||||
|
||||
for (const path of paths) {
|
||||
for (const parentURL of nuxt.options.modulesDir) {
|
||||
try {
|
||||
const src = isAbsolute(path)
|
||||
? pathToFileURL(await resolvePath(path, { cwd: parentURL, fallbackToOriginal: false, extensions: nuxt.options.extensions })).href
|
||||
: await resolveModule(path, { url: pathToFileURL(parentURL.replace(/\/node_modules\/?$/, '')), extensions: nuxt.options.extensions })
|
||||
try {
|
||||
const src = isAbsolute(path)
|
||||
? pathToFileURL(await resolvePath(path, { fallbackToOriginal: false, extensions: nuxt.options.extensions })).href
|
||||
: await resolveModule(path, { url: nuxt.options.modulesDir.map(m => pathToFileURL(m.replace(/\/node_modules\/?$/, ''))), extensions: nuxt.options.extensions })
|
||||
|
||||
nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
|
||||
resolvedModulePath = fileURLToPath(new URL(src))
|
||||
nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
|
||||
resolvedModulePath = fileURLToPath(new URL(src))
|
||||
|
||||
// nuxt-module-builder generates a module.json with metadata including the version
|
||||
const moduleMetadataPath = new URL('module.json', src)
|
||||
if (existsSync(moduleMetadataPath)) {
|
||||
buildTimeModuleMeta = JSON.parse(await fsp.readFile(moduleMetadataPath, 'utf-8'))
|
||||
}
|
||||
break
|
||||
} catch (error: unknown) {
|
||||
const code = (error as Error & { code?: string }).code
|
||||
if (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || code === 'ENOTDIR') {
|
||||
continue
|
||||
}
|
||||
logger.error(`Error while importing module \`${nuxtModule}\`: ${error}`)
|
||||
throw error
|
||||
// nuxt-module-builder generates a module.json with metadata including the version
|
||||
const moduleMetadataPath = new URL('module.json', src)
|
||||
if (existsSync(moduleMetadataPath)) {
|
||||
buildTimeModuleMeta = JSON.parse(await fsp.readFile(moduleMetadataPath, 'utf-8'))
|
||||
}
|
||||
break
|
||||
} catch (error: unknown) {
|
||||
const code = (error as Error & { code?: string }).code
|
||||
if (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || code === 'ENOTDIR') {
|
||||
continue
|
||||
}
|
||||
logger.error(`Error while importing module \`${nuxtModule}\`: ${error}`)
|
||||
throw error
|
||||
}
|
||||
if (typeof nuxtModule !== 'string') { break }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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+)*$/
|
||||
|
@ -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/*',
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,2 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
import 'nuxi/cli'
|
||||
import '@nuxt/cli/cli'
|
||||
|
@ -22,7 +22,7 @@ export default defineBuildConfig({
|
||||
},
|
||||
},
|
||||
dependencies: [
|
||||
'nuxi',
|
||||
'@nuxt/cli',
|
||||
'vue-router',
|
||||
'ofetch',
|
||||
],
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -325,10 +325,13 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
const elRef = import.meta.server ? undefined : (ref: any) => { el!.value = props.custom ? ref?.$el?.nextElementSibling : ref?.$el }
|
||||
|
||||
function shouldPrefetch (mode: 'visibility' | 'interaction') {
|
||||
if (import.meta.server) { return }
|
||||
return !prefetched.value && (typeof props.prefetchOn === 'string' ? props.prefetchOn === mode : (props.prefetchOn?.[mode] ?? options.prefetchOn?.[mode])) && (props.prefetch ?? options.prefetch) !== false && props.noPrefetch !== true && props.target !== '_blank' && !isSlowConnection()
|
||||
}
|
||||
|
||||
async function prefetch (nuxtApp = useNuxtApp()) {
|
||||
if (import.meta.server) { return }
|
||||
|
||||
if (prefetched.value) { return }
|
||||
|
||||
prefetched.value = true
|
||||
@ -395,12 +398,14 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
// `custom` API cannot support fallthrough attributes as the slot
|
||||
// may render fragment or text root nodes (#14897, #19375)
|
||||
if (!props.custom) {
|
||||
if (shouldPrefetch('interaction')) {
|
||||
routerLinkProps.onPointerenter = prefetch.bind(null, undefined)
|
||||
routerLinkProps.onFocus = prefetch.bind(null, undefined)
|
||||
}
|
||||
if (prefetched.value) {
|
||||
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
|
||||
if (import.meta.client) {
|
||||
if (shouldPrefetch('interaction')) {
|
||||
routerLinkProps.onPointerenter = prefetch.bind(null, undefined)
|
||||
routerLinkProps.onFocus = prefetch.bind(null, undefined)
|
||||
}
|
||||
if (prefetched.value) {
|
||||
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
|
||||
}
|
||||
}
|
||||
routerLinkProps.rel = props.rel || undefined
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 = {}')
|
||||
|
@ -63,8 +63,11 @@ export async function build (nuxt: Nuxt) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nuxt.options.dev) {
|
||||
checkForExternalConfigurationFiles()
|
||||
if (nuxt.options.dev && !nuxt.options.test) {
|
||||
nuxt.hooks.hookOnce('build:done', () => {
|
||||
checkForExternalConfigurationFiles()
|
||||
.catch(e => logger.warn('Problem checking for external configuration files.', e))
|
||||
})
|
||||
}
|
||||
|
||||
await bundle(nuxt)
|
||||
|
@ -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 })
|
||||
}
|
||||
|
@ -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') })
|
||||
})
|
||||
|
@ -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)
|
||||
|
||||
for (const dep of keyDependencies) {
|
||||
checkDependencyVersion(dep, nuxt._version)
|
||||
if (nuxt.options.dev && !nuxt.options.test) {
|
||||
nuxt.hooks.hookOnce('build:done', () => {
|
||||
for (const dep of keyDependencies) {
|
||||
checkDependencyVersion(dep, nuxt._version)
|
||||
.catch(e => logger.warn(`Problem checking \`${dep}\` version.`, e))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// We register hooks layer-by-layer so any overrides need to be registered separately
|
||||
@ -822,7 +827,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<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 }
|
||||
|
@ -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}.`])
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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] : [],
|
||||
|
@ -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, {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)!
|
||||
|
62
packages/nuxt/test/check-dependencies.test.ts
Normal file
62
packages/nuxt/test/check-dependencies.test.ts
Normal 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()
|
||||
})
|
||||
})
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -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) => {
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
84
packages/nuxt/test/virtual.test.ts
Normal file
84
packages/nuxt/test/virtual.test.ts
Normal 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()
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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'),
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
3070
pnpm-lock.yaml
3070
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,10 @@
|
||||
"main",
|
||||
"3.x"
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"node",
|
||||
"npm"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "vitest",
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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
6
test/fixtures/basic/app.vue
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<NuxtLoadingIndicator :throttle="0" />
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
3
test/fixtures/basic/pages/index.vue
vendored
3
test/fixtures/basic/pages/index.vue
vendored
@ -94,6 +94,9 @@
|
||||
<NuxtLink to="/server-page">
|
||||
to server page
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/page-load-hook">
|
||||
to page load hook
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
9
test/fixtures/basic/pages/page-load-hook.vue
vendored
Normal file
9
test/fixtures/basic/pages/page-load-hook.vue
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
Page for hook tests.
|
||||
<NuxtLink to="/page-load-hook/subpage">
|
||||
To sub page
|
||||
</NuxtLink>
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
7
test/fixtures/basic/pages/page-load-hook/[slug].vue
vendored
Normal file
7
test/fixtures/basic/pages/page-load-hook/[slug].vue
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtLink to="/page-load-hook">
|
||||
Back to parent
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
8
test/fixtures/basic/plugins/page-hook-plugin.ts
vendored
Normal file
8
test/fixtures/basic/plugins/page-hook-plugin.ts
vendored
Normal 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')
|
||||
}
|
||||
})
|
||||
})
|
@ -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',
|
||||
},
|
||||
])
|
||||
|
Loading…
Reference in New Issue
Block a user