Merge branch 'main' into patch-21

This commit is contained in:
Michael Brevard 2024-05-03 12:13:44 +03:00 committed by GitHub
commit ad6bef5f70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
187 changed files with 14699 additions and 7542 deletions

View File

@ -2,7 +2,7 @@
// https://containers.dev/implementors/json_reference/ // https://containers.dev/implementors/json_reference/
{ {
"name": "nuxt-devcontainer", "name": "nuxt-devcontainer",
"dockerFile": "Dockerfile", "build": { "dockerfile": "Dockerfile" },
"features": {}, "features": {},
"customizations": { "customizations": {
"vscode": { "vscode": {

View File

@ -11,7 +11,7 @@ Before creating the pull request, please make sure you do the following:
- Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us reviewing it. - Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us reviewing it.
- Read the contribution docs at https://nuxt.com/docs/community/contribution - Read the contribution docs at https://nuxt.com/docs/community/contribution
- Ensure that PR title follows conventional commits (https://conventionalcommits.org) - Ensure that PR title follows conventional commits (https://www.conventionalcommits.org)
- Update the corresponding documentation if needed. - Update the corresponding documentation if needed.
- Include relevant tests that fail without this PR but pass with it. - Include relevant tests that fail without this PR but pass with it.

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:

View File

@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
@ -46,7 +46,7 @@ jobs:
run: pnpm build run: pnpm build
- name: Run benchmarks - name: Run benchmarks
uses: CodSpeedHQ/action@1dbf41f0ae41cebfe61e084e535aebe533409b4d # v2.3.0 uses: CodSpeedHQ/action@0b631f8998f2389eb5144632b6f9f8fabd33a86e # v2.4.1
with: with:
run: pnpm vitest bench run: pnpm vitest bench
token: ${{ secrets.CODSPEED_TOKEN }} token: ${{ secrets.CODSPEED_TOKEN }}

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable

View File

@ -25,10 +25,10 @@ jobs:
restore-keys: cache-lychee- restore-keys: cache-lychee-
# check links with Lychee # check links with Lychee
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Lychee link checker - name: Lychee link checker
uses: lycheeverse/lychee-action@1e92115388e88fdc331019d99c8ab8dfe97ddd13 # for v1.8.0 uses: lycheeverse/lychee-action@054a8e8c7a88ada133165c6633a49825a32174e2 # for v1.8.0
with: with:
# arguments with file types to check # arguments with file types to check
args: >- args: >-

View File

@ -35,7 +35,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
@ -55,7 +55,7 @@ jobs:
run: pnpm build run: pnpm build
- name: Cache dist - name: Cache dist
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with: with:
retention-days: 3 retention-days: 3
name: dist name: dist
@ -72,7 +72,7 @@ jobs:
- build - build
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
@ -83,19 +83,19 @@ jobs:
run: pnpm install run: pnpm install
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
with: with:
languages: javascript languages: javascript
queries: +security-and-quality queries: +security-and-quality
- name: Restore dist cache - name: Restore dist cache
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with: with:
name: dist name: dist
path: packages path: packages
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
with: with:
category: "/language:javascript" category: "/language:javascript"
@ -111,7 +111,7 @@ jobs:
module: ["bundler", "node"] module: ["bundler", "node"]
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
@ -122,7 +122,7 @@ jobs:
run: pnpm install run: pnpm install
- name: Restore dist cache - name: Restore dist cache
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with: with:
name: dist name: dist
path: packages path: packages
@ -142,7 +142,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
@ -166,7 +166,7 @@ jobs:
needs: needs:
- build - build
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
@ -198,6 +198,7 @@ jobs:
builder: ["vite", "webpack"] builder: ["vite", "webpack"]
context: ["async", "default"] context: ["async", "default"]
manifest: ["manifest-on", "manifest-off"] manifest: ["manifest-on", "manifest-off"]
version: ["v4", "v3"]
node: [18] node: [18]
exclude: exclude:
- env: "dev" - env: "dev"
@ -208,7 +209,7 @@ jobs:
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:
@ -222,7 +223,7 @@ jobs:
run: pnpm playwright-core install chromium run: pnpm playwright-core install chromium
- name: Restore dist cache - name: Restore dist cache
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with: with:
name: dist name: dist
path: packages path: packages
@ -234,9 +235,10 @@ jobs:
TEST_BUILDER: ${{ matrix.builder }} TEST_BUILDER: ${{ matrix.builder }}
TEST_MANIFEST: ${{ matrix.manifest }} TEST_MANIFEST: ${{ matrix.manifest }}
TEST_CONTEXT: ${{ matrix.context }} TEST_CONTEXT: ${{ matrix.context }}
TEST_V4: ${{ matrix.version == 'v4' }}
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || runner.os == 'Windows' }} SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || runner.os == 'Windows' }}
- uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # v4.3.0 - uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # v4.3.1
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on' if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@ -258,7 +260,7 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable
@ -271,7 +273,7 @@ jobs:
run: pnpm install run: pnpm install
- name: Restore dist cache - name: Restore dist cache
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with: with:
name: dist name: dist
path: packages path: packages
@ -297,7 +299,7 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable
@ -310,7 +312,7 @@ jobs:
run: pnpm install run: pnpm install
- name: Restore dist cache - name: Restore dist cache
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with: with:
name: dist name: dist
path: packages path: packages

View File

@ -17,6 +17,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # v4.2.5 uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with: with:

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions # From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files - name: Check workflow files
run: | run: |

View File

@ -21,7 +21,7 @@ jobs:
permissions: permissions:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with: with:
ref: '2.x' ref: '2.x'
fetch-depth: 0 # All history fetch-depth: 0 # All history

View File

@ -29,7 +29,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with: with:
ref: refs/pull/${{ github.event.issue.number }}/merge ref: refs/pull/${{ github.event.issue.number }}/merge
fetch-depth: 0 fetch-depth: 0

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable

View File

@ -10,6 +10,7 @@ permissions:
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.repository == 'nuxt/nuxt'
steps: steps:
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with: with:

View File

@ -10,7 +10,7 @@ jobs:
reproduire: reproduire:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp - uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp
with: with:
label: needs reproduction label: needs reproduction

View File

@ -28,10 +28,11 @@ jobs:
id-token: write id-token: write
contents: read contents: read
actions: read actions: read
if: github.event_name == 'push' || github.repository == 'nuxt/nuxt'
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with: with:
persist-credentials: false persist-credentials: false
@ -58,7 +59,8 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
if: github.repository == 'nuxt/nuxt' && success()
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif
@ -66,6 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
if: github.repository == 'nuxt/nuxt' && success()
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -20,7 +20,7 @@ jobs:
name: Semantic pull request name: Semantic pull request
steps: steps:
- name: Validate PR title - name: Validate PR title
uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f # v5.4.0 uses: amannn/action-semantic-pull-request@cfb60706e18bc85e8aec535e3c577abe8f70378e # v5.5.2
with: with:
scopes: | scopes: |
kit kit
@ -28,6 +28,7 @@ jobs:
nuxt nuxt
schema schema
test-utils test-utils
ui-templates
vite vite
webpack webpack
deps deps

View File

@ -93,7 +93,7 @@ We invite you to contribute and help improve Nuxt 💚
Here are a few ways you can get involved: Here are a few ways you can get involved:
- **Reporting Bugs:** If you come across any bugs or issues, please check out the [reporting bugs guide](https://nuxt.com/docs/community/reporting-bugs) to learn how to submit a bug report. - **Reporting Bugs:** If you come across any bugs or issues, please check out the [reporting bugs guide](https://nuxt.com/docs/community/reporting-bugs) to learn how to submit a bug report.
- **Suggestions:** Have ideas to enhance Nuxt? We'd love to hear them! Check out the [contribution guide](https://nuxt.com/docs/community/contribution#creating-an-issue) to share your suggestions. - **Suggestions:** Have ideas to enhance Nuxt? We'd love to hear them! Check out the [contribution guide](https://nuxt.com/docs/community/contribution) to share your suggestions.
- **Questions:** If you have questions or need assistance, the [getting help guide](https://nuxt.com/docs/community/getting-help) provides resources to help you out. - **Questions:** If you have questions or need assistance, the [getting help guide](https://nuxt.com/docs/community/getting-help) provides resources to help you out.
## <a name="local-development">🏠 Local Development</a> ## <a name="local-development">🏠 Local Development</a>

View File

@ -10,6 +10,10 @@ We made everything so you can start writing `.vue` files from the beginning whil
Nuxt has no vendor lock-in, allowing you to deploy your application [**everywhere, even on the edge**](/blog/nuxt-on-the-edge). Nuxt has no vendor lock-in, allowing you to deploy your application [**everywhere, even on the edge**](/blog/nuxt-on-the-edge).
::tip
If you want to play around with Nuxt in your browser, you can [try it out in one of our online sandboxes](/docs/getting-started/installation#play-online).
::
## Automation and Conventions ## Automation and Conventions
Nuxt uses conventions and an opinionated directory structure to automate repetitive tasks and allow developers to focus on pushing features. The configuration file can still customize and override its default behaviors. Nuxt uses conventions and an opinionated directory structure to automate repetitive tasks and allow developers to focus on pushing features. The configuration file can still customize and override its default behaviors.

View File

@ -59,6 +59,11 @@ We currently ship an environment for unit testing code that needs a [Nuxt](https
}) })
``` ```
::tip
When importing `@nuxt/test-utils` in your vitest config, It is necessary to have `"type": "module"` specified in your `package.json` or rename your vitest config file appropriately.
> ie. `vitest.config.m{ts,js}`.
::
### Using a Nuxt Runtime Environment ### Using a Nuxt Runtime Environment
By default, `@nuxt/test-utils` will not change your default Vitest environment, so you can do fine-grained opt-in and run Nuxt tests together with other unit tests. By default, `@nuxt/test-utils` will not change your default Vitest environment, so you can do fine-grained opt-in and run Nuxt tests together with other unit tests.
@ -337,8 +342,8 @@ For example, to mock `/test/` endpoint, you can do:
```ts twoslash ```ts twoslash
import { registerEndpoint } from '@nuxt/test-utils/runtime' import { registerEndpoint } from '@nuxt/test-utils/runtime'
registerEndpoint("/test/", () => ({ registerEndpoint('/test/', () => ({
test: "test-field" test: 'test-field'
})) }))
``` ```
@ -347,9 +352,9 @@ By default, your request will be made using the `GET` method. You may use anothe
```ts twoslash ```ts twoslash
import { registerEndpoint } from '@nuxt/test-utils/runtime' import { registerEndpoint } from '@nuxt/test-utils/runtime'
registerEndpoint("/test/", { registerEndpoint('/test/', {
method: "POST", method: 'POST',
handler: () => ({ test: "test-field" }) handler: () => ({ test: 'test-field' })
}) })
``` ```
@ -364,7 +369,7 @@ If you would like to use both the end-to-end and unit testing functionality of `
`app.nuxt.spec.ts` `app.nuxt.spec.ts`
```ts twoslash ```ts twoslash
import { mockNuxtImport } from "@nuxt/test-utils/runtime" import { mockNuxtImport } from '@nuxt/test-utils/runtime'
mockNuxtImport('useStorage', () => { mockNuxtImport('useStorage', () => {
return () => { return () => {
@ -386,6 +391,95 @@ await setup({
// ... // ...
``` ```
### Using `@vue/test-utils`
If you prefer to use `@vue/test-utils` on its own for unit testing in Nuxt, and you are only testing components which do not rely on Nuxt composables, auto-imports or context, you can follow these steps to set it up.
1. Install the needed dependencies
::code-group
```bash [yarn]
yarn add --dev vitest @vue/test-utils happy-dom @vitejs/plugin-vue
```
```bash [npm]
npm i --save-dev vitest @vue/test-utils happy-dom @vitejs/plugin-vue
```
```bash [pnpm]
pnpm add -D vitest @vue/test-utils happy-dom @vitejs/plugin-vue
```
```bash [bun]
bun add --dev vitest @vue/test-utils happy-dom @vitejs/plugin-vue
```
::
2. Create a `vitest.config.ts` with the following content:
```ts twoslash
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom',
},
});
```
3. Add a new command for test in your `package.json`
```json
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
...
"test": "vitest"
},
```
4. Create a simple `<HelloWorld>` component `components/HelloWorld.vue` with the following content:
```vue
<template>
<p>Hello world</p>
</template>
```
5. Create a simple unit test for this newly created component `~/components/HelloWorld.spec.ts`
```ts twoslash
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from './HelloWorld.vue'
describe('HelloWorld', () => {
it('component renders Hello world properly', () => {
const wrapper = mount(HelloWorld)
expect(wrapper.text()).toContain('Hello world')
})
})
```
6. Run vitest command
::code-group
```bash [yarn]
yarn test
```
```bash [npm]
npm run test
```
```bash [pnpm]
pnpm run test
```
```bash [bun]
bun run test
```
::
Congratulations, you're all set to start unit testing with `@vue/test-utils` in Nuxt! Happy testing!
## End-To-End Testing ## End-To-End Testing
For end-to-end testing, we support [Vitest](https://github.com/vitest-dev/vitest), [Jest](https://jestjs.io), [Cucumber](https://cucumber.io/) and [Playwright](https://playwright.dev/) as test runners. For end-to-end testing, we support [Vitest](https://github.com/vitest-dev/vitest), [Jest](https://jestjs.io), [Cucumber](https://cucumber.io/) and [Playwright](https://playwright.dev/) as test runners.

View File

@ -6,14 +6,14 @@ navigation.icon: i-ph-play-duotone
## Play Online ## Play Online
You can start playing with Nuxt 3 in your browser using our online sandboxes: If you just want to play around with Nuxt in your browser without setting up a project, you can use one of our online sandboxes:
::card-group ::card-group
:card{title="Open on StackBlitz" icon="i-simple-icons-stackblitz" to="https://nuxt.new/s/v3" target="_blank"} :card{title="Open on StackBlitz" icon="i-simple-icons-stackblitz" to="https://nuxt.new/s/v3" target="_blank"}
:card{title="Open on CodeSandbox" icon="i-simple-icons-codesandbox" to="https://nuxt.new/c/v3" target="_blank"} :card{title="Open on CodeSandbox" icon="i-simple-icons-codesandbox" to="https://nuxt.new/c/v3" target="_blank"}
:: ::
Start with one of our starters and themes directly by opening [nuxt.new](https://nuxt.new). Or follow the steps below to set up a new Nuxt project on your computer.
## New Project ## New Project
@ -51,6 +51,10 @@ bunx nuxi@latest init <project-name>
:: ::
::tip
Alternatively, you can find other starters or themes by opening [nuxt.new](https://nuxt.new) and following the instructions there.
::
Open your project folder in Visual Studio Code: Open your project folder in Visual Studio Code:
```bash [Terminal] ```bash [Terminal]

View File

@ -135,7 +135,7 @@ Non primitive JS types | ❌ No | ✅ Yes
## External Configuration Files ## External Configuration Files
Nuxt uses [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt-config) file as the single source of trust for configurations and skips reading external configuration files. During the course of building your project, you may have a need to configure those. The following table highlights common configurations and, where applicable, how they can be configured with Nuxt. Nuxt uses [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt-config) file as the single source of truth for configurations and skips reading external configuration files. During the course of building your project, you may have a need to configure those. The following table highlights common configurations and, where applicable, how they can be configured with Nuxt.
Name | Config File | How To Configure Name | Config File | How To Configure
---------------------------------------------|---------------------------|------------------------- ---------------------------------------------|---------------------------|-------------------------
@ -149,7 +149,7 @@ Here is a list of other common config files:
Name | Config File | How To Configure Name | Config File | How To Configure
---------------------------------------------|-------------------------|-------------------------- ---------------------------------------------|-------------------------|--------------------------
[TypeScript](https://www.typescriptlang.org) | `tsconfig.json` | [More Info](/docs/guide/concepts/typescript#nuxttsconfigjson) [TypeScript](https://www.typescriptlang.org) | `tsconfig.json` | [More Info](/docs/guide/concepts/typescript#nuxttsconfigjson)
[ESLint](https://eslint.org) | `.eslintrc.js` | [More Info](https://eslint.org/docs/latest/use/configure/configuration-files) [ESLint](https://eslint.org) | `eslint.config.js` | [More Info](https://eslint.org/docs/latest/use/configure/configuration-files)
[Prettier](https://prettier.io) | `.prettierrc.json` | [More Info](https://prettier.io/docs/en/configuration.html) [Prettier](https://prettier.io) | `.prettierrc.json` | [More Info](https://prettier.io/docs/en/configuration.html)
[Stylelint](https://stylelint.io) | `.stylelintrc.json` | [More Info](https://stylelint.io/user-guide/configure) [Stylelint](https://stylelint.io) | `.stylelintrc.json` | [More Info](https://stylelint.io/user-guide/configure)
[TailwindCSS](https://tailwindcss.com) | `tailwind.config.js` | [More Info](https://tailwindcss.nuxtjs.org/tailwind/config) [TailwindCSS](https://tailwindcss.com) | `tailwind.config.js` | [More Info](https://tailwindcss.nuxtjs.org/tailwind/config)

View File

@ -96,6 +96,16 @@ If you only have a single layout in your application, we recommend using [`app.v
::code-group ::code-group
```vue [app.vue]
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
```
```vue [layouts/default.vue] ```vue [layouts/default.vue]
<template> <template>
<div> <div>

View File

@ -113,7 +113,8 @@ export default defineNuxtConfig({
head: { head: {
link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css' }] link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css' }]
} }
}}) }
})
``` ```
### Dynamically Adding Stylesheets ### Dynamically Adding Stylesheets
@ -153,15 +154,15 @@ To use a preprocessor like SCSS, Sass, Less or Stylus, install it first.
::code-group ::code-group
```bash [Sass & SCSS] ```bash [Sass & SCSS]
npm install sass npm install -D sass
``` ```
```bash [Less] ```bash [Less]
npm install less npm install -D less
``` ```
```bash [Stylus] ```bash [Stylus]
npm install stylus npm install -D stylus
``` ```
:: ::

View File

@ -48,7 +48,11 @@ Read more about layers in the **Layer Author Guide**.
:: ::
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=lnFCM7c9f7I" target="_blank"} ::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=lnFCM7c9f7I" target="_blank"}
Watch Learn Vue video about Nuxt Layers. Watch a video from Learn Vue about Nuxt Layers.
::
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=fr5yo3aVkfA&t=271s" target="_blank"}
Watch a video from Alexander Lichter about Nuxt Layers.
:: ::
## Examples ## Examples

View File

@ -1,6 +1,6 @@
--- ---
title: 'Vue.js Development' title: 'Vue.js Development'
description: "Nuxt uses Vue.js and adds features such as component auto-imports, file-based routing and composables for a SSR-friendly usage." description: "Nuxt uses Vue.js and adds features such as component auto-imports, file-based routing and composables for an SSR-friendly usage."
--- ---
Nuxt integrates Vue 3, the new major release of Vue that enables new patterns for Nuxt users. Nuxt integrates Vue 3, the new major release of Vue that enables new patterns for Nuxt users.

View File

@ -76,18 +76,18 @@ Overwriting options such as `"compilerOptions.paths"` with your own configuratio
In case you need to extend options provided by `./.nuxt/tsconfig.json` further, you can use the [`alias` property](/docs/api/nuxt-config#alias) within your `nuxt.config`. `nuxi` will pick them up and extend `./.nuxt/tsconfig.json` accordingly. In case you need to extend options provided by `./.nuxt/tsconfig.json` further, you can use the [`alias` property](/docs/api/nuxt-config#alias) within your `nuxt.config`. `nuxi` will pick them up and extend `./.nuxt/tsconfig.json` accordingly.
:: ::
## Stricter Checks ## Strict Checks
TypeScript comes with certain checks to give you more safety and analysis of your program. TypeScript comes with certain checks to give you more safety and analysis of your program.
Once youve converted your codebase to TypeScript and felt familiar with it, you can start enabling these checks for greater safety ([read more](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html#getting-stricter-checks)). [Strict checks](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html#getting-stricter-checks) are enabled by default in Nuxt 3 to give you greater type safety.
In order to enable strict type checking, you have to update `nuxt.config`: If you are currently converting your codebase to TypeScript, you may want to temporarily disable strict checks by setting `strict` to `false` in your `nuxt.config`:
```ts twoslash [nuxt.config.ts] ```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({ export default defineNuxtConfig({
typescript: { typescript: {
strict: true strict: false
} }
}) })
``` ```

View File

@ -5,76 +5,20 @@ description: "Nuxt supports ESLint out of the box"
## ESLint ## ESLint
The recommended approach for Nuxt is to enable ESLint support using [`@nuxt/eslint-config`](https://github.com/nuxt/eslint-config). The recommended approach for Nuxt is to enable ESLint support using the [`@nuxt/eslint`](https://eslint.nuxt.com/packages/module) module, that will setup project-aware ESLint configuration for you.
At the moment, this configuration will not format your files; you can set up Prettier or another tool to do so. :::callout{icon="i-ph-lightbulb-duotone"}
The module is designed for the [new ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new) with is the [default format since ESLint v9](https://eslint.org/blog/2024/04/eslint-v9.0.0-released/).
::alert{type=info} If you are using the legacy `.eslintrc` config, you will need to [configure manually with `@nuxt/eslint-config`](https://eslint.nuxt.com/packages/config#legacy-config-format). We highly recommend you to migrate over the flat config to be future-proof.
We're currently working to refactor the Nuxt ESLint configuration. Subscribe to the [Nuxt ESLint roadmap](https://github.com/nuxt/eslint-config/issues/303) to follow updates. :::
::
### Install Dependencies ## Quick Setup
Install both ESLint and the Nuxt configuration as development dependencies. ```bash
npx nuxi module add eslint
::code-group
```bash [yarn]
yarn add --dev eslint @nuxt/eslint-config
``` ```
```bash [npm] Start your Nuxt app, a `eslint.config.mjs` file will be generated under your project root. You can customize it as needed.
npm install --save-dev eslint @nuxt/eslint-config
```
```bash [pnpm] You can learn more about the module and customizations in [Nuxt ESLint's documentation](https://eslint.nuxt.com/packages/module).
pnpm add -D eslint @nuxt/eslint-config
```
```bash [bun]
bun add -D eslint @nuxt/eslint-config
```
::
### Configuration
Add `.eslintrc.cjs` to the root folder of your Nuxt app.
```js
module.exports = {
root: true,
extends: ['@nuxt/eslint-config'],
}
```
### Modify package.json
Add the below to lint commands to your `package.json` script section:
```json
"scripts": {
...
"lint": "eslint .",
"lint:fix": "eslint . --fix",
...
},
```
Run the `lint` command to check if the code style is correct or run `lint:fix` to automatically fix issues.
### Configuring VS Code
Install the [VS Code ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint).
In VS Code press `ctrl+shift+p` (`cmd+shift+p` on Mac) to open the command prompt, find `Open Workspace Settings (JSON)`, add the below lines to the JSON and save:
```json
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
```
You're good to go! On save, your files will be linted and auto-fixed.

View File

@ -6,7 +6,7 @@ navigation.icon: i-ph-folder-duotone
--- ---
::note ::note
To reduce your application's bundle size, this directory is **optional**, meaning that [`vue-router`](https://router.vuejs.org) won't be included if you only use [`app.vue`](/docs/guide/directory-structure/app). To force the pages system, set `pages: true` in `nuxt.config` or have a [`app/router.options.ts`](/docs/guide/directory-structure/pages#router-options). To reduce your application's bundle size, this directory is **optional**, meaning that [`vue-router`](https://router.vuejs.org) won't be included if you only use [`app.vue`](/docs/guide/directory-structure/app). To force the pages system, set `pages: true` in `nuxt.config` or have a [`app/router.options.ts`](/docs/guide/going-further/custom-routing#using-approuteroptions).
:: ::
## Usage ## Usage
@ -193,6 +193,14 @@ To display the `child.vue` component, you have to insert the `<NuxtPage>` compon
</template> </template>
``` ```
```vue {}[pages/parent/child.vue]
<script setup lang="ts">
const props = defineProps(['foobar'])
console.log(props.foobar)
</script>
```
### Child Route Keys ### Child Route Keys
If you want more control over when the `<NuxtPage>` component is re-rendered (for example, for transitions), you can either pass a string or function via the `pageKey` prop, or you can define a `key` value via `definePageMeta`: If you want more control over when the `<NuxtPage>` component is re-rendered (for example, for transitions), you can either pass a string or function via the `pageKey` prop, or you can define a `key` value via `definePageMeta`:
@ -208,7 +216,7 @@ If you want more control over when the `<NuxtPage>` component is re-rendered (fo
Or alternatively: Or alternatively:
```vue twoslash {}[pages/child.vue] ```vue twoslash {}[pages/parent/child.vue]
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
key: route => route.fullPath key: route => route.fullPath

View File

@ -37,6 +37,42 @@ export default defineNuxtConfig({
There is also a `future` namespace for early opting-in to new features that will become default in a future (possibly major) version of the framework. There is also a `future` namespace for early opting-in to new features that will become default in a future (possibly major) version of the framework.
### compatibilityVersion
::important
This configuration option is available in Nuxt v3.12+.
::
This enables early access to Nuxt features or flags.
Setting `compatibilityVersion` to `4` changes defaults throughout your
Nuxt configuration to opt-in to Nuxt v4 behaviour, but you can granularly re-enable Nuxt v3 behaviour
when testing (see example). Please file issues if so, so that we can
address in Nuxt or in the ecosystem.
```ts
export default defineNuxtConfig({
future: {
compatibilityVersion: 4,
},
// To re-enable _all_ Nuxt v3 behaviour, set the following options:
srcDir: '.',
dir: {
app: 'app'
},
experimental: {
compileTemplate: true,
templateUtils: true,
relativeWatchPaths: true,
defaults: {
useAsyncData: {
deep: true
}
}
}
})
```
### typescriptBundlerResolution ### typescriptBundlerResolution
This enables 'Bundler' module resolution mode for TypeScript, which is the recommended setting This enables 'Bundler' module resolution mode for TypeScript, which is the recommended setting

View File

@ -76,7 +76,7 @@ Before publishing your module to npm, makes sure you have an [npmjs.com](https:/
While you can publish your module by bumping its version and using the `npm publish` command, the module starter comes with a release script that helps you make sure you publish a working version of your module to npm and more. While you can publish your module by bumping its version and using the `npm publish` command, the module starter comes with a release script that helps you make sure you publish a working version of your module to npm and more.
To use the release script, first, commit all your changes (we recommend you follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to also take advantage of automatic version bump and changelog update), then run the release script with `npm run release`. To use the release script, first, commit all your changes (we recommend you follow [Conventional Commits](https://www.conventionalcommits.org) to also take advantage of automatic version bump and changelog update), then run the release script with `npm run release`.
When running the release script, the following will happen: When running the release script, the following will happen:

View File

@ -100,6 +100,10 @@ export default defineNuxtConfig({
If you want to extend a private remote source, you need to add the environment variable `GIGET_AUTH=<token>` to provide a token. If you want to extend a private remote source, you need to add the environment variable `GIGET_AUTH=<token>` to provide a token.
:: ::
::tip
If you want to extend a remote source from a self-hosted GitHub or GitLab instance, you need to supply its URL with the `GIGET_GITHUB_URL=<url>` or `GIGET_GITLAB_URL=<url>` environment variable - or directly configure it with [the `auth` option](https://github.com/unjs/c12#extending-config-layer-from-remote-sources) in your `nuxt.config`.
::
::note ::note
When using git remote sources, if a layer has npm dependencies and you wish to install them, you can do so by specifying `install: true` in your layer options. When using git remote sources, if a layer has npm dependencies and you wish to install them, you can do so by specifying `install: true` in your layer options.

View File

@ -0,0 +1,65 @@
---
navigation.title: 'Vite Plugins'
title: Using Vite Plugins in Nuxt
description: Learn how to integrate Vite plugins into your Nuxt project.
---
While Nuxt modules offer extensive functionality, sometimes a specific Vite plugin might meet your needs more directly.
First, we need to install the Vite plugin, for our example, we'll use `@rollup/plugin-yaml`:
::code-group
```bash [npm]
npm install @rollup/plugin-yaml
```
```bash [yarn]
yarn add @rollup/plugin-yaml
```
```bash [pnpm]
pnpm add @rollup/plugin-yaml
```
```bash [bun]
bun add @rollup/plugin-yaml
```
::
Next, we need to import and add it to our [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt-config) file:
```ts [nuxt.config.ts]
import yaml from '@rollup/plugin-yaml'
export default defineNuxtConfig({
vite: {
plugins: [
yaml()
]
}
})
```
Now we installed and configured our Vite plugin, we can start using YAML files directly in our project.
For example, we can have a `config.yaml` that stores configuration data and import this data in our Nuxt components:
::code-group
```yaml [data/hello.yaml]
greeting: "Hello, Nuxt with Vite!"
```
```vue [components/Hello.vue]
<script setup>
import config from '~/data/hello.yaml'
</script>
<template>
<h1>{{ config.greeting }}</h1>
</template>
```
::

View File

@ -0,0 +1,3 @@
title: Recipes
titleTemplate: '%s · Recipes'
icon: i-ph-cooking-pot-duotone

View File

@ -0,0 +1,56 @@
---
title: '<NuxtRouteAnnouncer>'
description: 'Add a hidden element with the page title for assistive technologies.'
navigation:
badge: New
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/nuxt-route-announcer.ts
size: xs
---
::important
This component will be available in Nuxt v3.12.
::
## Usage
Add `<NuxtRouteAnnouncer/>` in your [`app.vue`](/docs/guide/directory-structure/app) or [`layouts/`](/docs/guide/directory-structure/layouts) to enhance accessibility by informing assistive technologies about page's title changes. This ensures that navigational changes are announced to users relying on screen readers.
```vue [app.vue]
<template>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```
## Slots
You can pass custom HTML or components through the route announcer default slot.
```vue
<template>
<NuxtRouteAnnouncer>
<template #default="{ message }">
<p>{{ message }} was loaded.</p>
</template>
</NuxtRouteAnnouncer>
</template>
```
## Props
- `atomic`: Controls if screen readers announce only changes or the entire content. Set to true for full content readout on updates, false for changes only. (default `false`)
- `politeness`: Sets the urgency for screen reader announcements: `off` (disable the announcement), `polite` (waits for silence), or `assertive` (interrupts immediately). (default `polite`)
::callout
This component is optional. :br
To achieve full customization, you can implement your own one based on [its source code](https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/nuxt-route-announcer.ts).
::
::callout
You can hook into the underlying announcer instance using [the `useRouteAnnouncer` composable](/docs/api/composables/use-route-announcer), which allows you to set a custom announcement message.
::

View File

@ -86,6 +86,18 @@ function logFoo () {
</template> </template>
```` ````
````vue [my-page.vue]
<script setup lang="ts">
const foo = () => {
console.log('foo method called')
}
defineExpose({
foo,
})
</script>
````
## Custom Props ## Custom Props
In addition, `<NuxtPage>` also accepts custom props that you may need to pass further down the hierarchy. In addition, `<NuxtPage>` also accepts custom props that you may need to pass further down the hierarchy.

View File

@ -1,6 +1,6 @@
--- ---
title: 'useAsyncData' title: 'useAsyncData'
description: useAsyncData provides access to data that resolves asynchronously in a SSR-friendly composable. description: useAsyncData provides access to data that resolves asynchronously in an SSR-friendly composable.
links: links:
- label: Source - label: Source
icon: i-simple-icons-github icon: i-simple-icons-github

View File

@ -1,6 +1,6 @@
--- ---
title: 'useFetch' title: 'useFetch'
description: 'Fetch data from an API endpoint with a SSR-friendly composable.' description: 'Fetch data from an API endpoint with an SSR-friendly composable.'
links: links:
- label: Source - label: Source
icon: i-simple-icons-github icon: i-simple-icons-github
@ -97,7 +97,7 @@ All fetch options can be given a `computed` or `ref` value. These will be watche
- `transform`: a function that can be used to alter `handler` function result after resolving - `transform`: a function that can be used to alter `handler` function result after resolving
- `getCachedData`: Provide a function which returns cached data. A _null_ or _undefined_ return value will trigger a fetch. By default, this is: `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]`, which only caches data when `payloadExtraction` is enabled. - `getCachedData`: Provide a function which returns cached data. A _null_ or _undefined_ return value will trigger a fetch. By default, this is: `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]`, which only caches data when `payloadExtraction` is enabled.
- `pick`: only pick specified keys in this array from the `handler` function result - `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: watch an array of reactive sources and auto-refresh the fetch result when they change. Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`. Together with `immediate: false`, this allows for a fully-manual `useFetch`. - `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 `true` by default). It can be set to `false` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive. - `deep`: return data in a deep ref object (it is `true` by default). It can be set to `false` to return data in a shallow ref object, which can improve performance if your data does not need to be deeply reactive.
- `dedupe`: avoid fetching same key more than once at a time (defaults to `cancel`). Possible options: - `dedupe`: avoid fetching same key more than once at a time (defaults to `cancel`). Possible options:
- `cancel` - cancels existing requests when a new one is made - `cancel` - cancels existing requests when a new one is made
@ -107,6 +107,10 @@ All fetch options can be given a `computed` or `ref` value. These will be watche
If you provide a function or ref as the `url` parameter, or if you provide functions as arguments to the `options` parameter, then the `useFetch` call will not match other `useFetch` calls elsewhere in your codebase, even if the options seem to be identical. If you wish to force a match, you may provide your own key in `options`. If you provide a function or ref as the `url` parameter, or if you provide functions as arguments to the `options` parameter, then the `useFetch` call will not match other `useFetch` calls elsewhere in your codebase, even if the options seem to be identical. If you wish to force a match, you may provide your own key in `options`.
:: ::
::note
If you use `useFetch` to call an (external) HTTPS URL with a self-signed certificate in development, you will need to set `NODE_TLS_REJECT_UNAUTHORIZED=0` in your environment.
::
::tip{icon="i-simple-icons-youtube" color="gray" to="https://www.youtube.com/watch?v=aQPR0xn-MMk" target="_blank"} ::tip{icon="i-simple-icons-youtube" color="gray" to="https://www.youtube.com/watch?v=aQPR0xn-MMk" target="_blank"}
Learn how to use `transform` and `getCachedData` to avoid superfluous calls to an API and cache data for visitors on the client. Learn how to use `transform` and `getCachedData` to avoid superfluous calls to an API and cache data for visitors on the client.
:: ::

View File

@ -1,6 +1,11 @@
--- ---
title: "useId" title: "useId"
description: Generate an SSR-friendly unique identifier that can be passed to accessibility attributes. description: Generate an SSR-friendly unique identifier that can be passed to accessibility attributes.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/id.ts
size: xs
--- ---
::important ::important

View File

@ -44,4 +44,4 @@ watch(count, (newCount) => {
`useLazyAsyncData` is a reserved function name transformed by the compiler, so you should not name your own function `useLazyAsyncData`. `useLazyAsyncData` is a reserved function name transformed by the compiler, so you should not name your own function `useLazyAsyncData`.
:: ::
:read-more{to="/docs/getting-started/data-fetching#uselazyasyncdata"} :read-more{to="/docs/getting-started/data-fetching"}

View File

@ -1,10 +1,17 @@
--- ---
title: "usePreviewMode" title: "usePreviewMode"
description: "Use usePreviewMode to check and control preview mode in Nuxt" description: "Use usePreviewMode to check and control preview mode in Nuxt"
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/preview.ts
size: xs
--- ---
# `usePreviewMode` # `usePreviewMode`
Preview mode allows you to see how your changes would be displayed on a live site without revealing them to users.
You can use the built-in `usePreviewMode` composable to access and control preview state in Nuxt. If the composable detects preview mode it will automatically force any updates necessary for [`useAsyncData`](/docs/api/composables/use-async-data) and [`useFetch`](/docs/api/composables/use-fetch) to rerender preview content. You can use the built-in `usePreviewMode` composable to access and control preview state in Nuxt. If the composable detects preview mode it will automatically force any updates necessary for [`useAsyncData`](/docs/api/composables/use-async-data) and [`useFetch`](/docs/api/composables/use-fetch) to rerender preview content.
```js ```js
@ -47,15 +54,11 @@ The `getState` function will append returned values to current state, so be care
## Example ## Example
The example below creates a page where part of a content is rendered only in preview mode.
```vue [pages/some-page.vue] ```vue [pages/some-page.vue]
<script setup> <script setup>
const route = useRoute() const { enabled, state } = usePreviewMode()
const { enabled, state } = usePreviewMode({
shouldEnable: () => {
return route.query.customPreview === 'true'
},
})
const { data } = await useFetch('/api/preview', { const { data } = await useFetch('/api/preview', {
query: { query: {
@ -67,12 +70,9 @@ const { data } = await useFetch('/api/preview', {
<template> <template>
<div> <div>
Some base content Some base content
<p v-if="enabled"> <p v-if="enabled">
Only preview content: {{ state.token }} Only preview content: {{ state.token }}
<br> <br>
<button @click="enabled = false"> <button @click="enabled = false">
disable preview mode disable preview mode
</button> </button>
@ -80,3 +80,20 @@ const { data } = await useFetch('/api/preview', {
</div> </div>
</template> </template>
``` ```
Now you can generate your site and serve it:
```bash [Terminal]
npx nuxi generate
npx nuxi preview
```
Then you can see your preview page by adding the query param `preview` to the end of the page you want to see once:
```js
?preview=true
```
::note
`usePreviewMode` should be tested locally with `nuxi generate` and then `nuxi preview` rather than `nuxi dev`. (The [preview command](/docs/api/commands/preview) is not related to preview mode.)
::

View File

@ -0,0 +1,60 @@
---
title: 'useRouteAnnouncer'
description: This composable observes the page title changes and updates the announcer message accordingly.
navigation:
badge: New
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/route-announcer.ts
size: xs
---
::important
This composable will be available in Nuxt v3.12.
::
## Description
A composable which observes the page title changes and updates the announcer message accordingly. Used by [`<NuxtRouteAnnouncer>`](/docs/api/components/nuxt-route-announcer) and controllable.
It hooks into Unhead's [`dom:rendered`](https://unhead.unjs.io/api/core/hooks#dom-hooks) to read the page's title and set it as the announcer message.
## Parameters
- `politeness`: Sets the urgency for screen reader announcements: `off` (disable the announcement), `polite` (waits for silence), or `assertive` (interrupts immediately). (default `polite`).
## Properties
### `message`
- **type**: `Ref<string>`
- **description**: The message to announce
### `politeness`
- **type**: `Ref<string>`
- **description**: Screen reader announcement urgency level `off`, `polite`, or `assertive`
## Methods
### `set(message, politeness = "polite")`
Sets the message to announce with its urgency level.
### `polite(message)`
Sets the message with `politeness = "polite"`
### `assertive(message)`
Sets the message with `politeness = "assertive"`
## Example
```ts
<script setup lang="ts">
const { message, politeness, set, polite, assertive } = useRouteAnnouncer({
politeness: 'assertive'
})
</script>
```

View File

@ -55,3 +55,7 @@ function contactForm() {
::tip ::tip
`$fetch` is the preferred way to make HTTP calls in Nuxt instead of [@nuxt/http](https://github.com/nuxt/http) and [@nuxtjs/axios](https://github.com/nuxt-community/axios-module) that are made for Nuxt 2. `$fetch` is the preferred way to make HTTP calls in Nuxt instead of [@nuxt/http](https://github.com/nuxt/http) and [@nuxtjs/axios](https://github.com/nuxt-community/axios-module) that are made for Nuxt 2.
:: ::
::note
If you use `$fetch` to call an (external) HTTPS URL with a self-signed certificate in development, you will need to set `NODE_TLS_REJECT_UNAUTHORIZED=0` in your environment.
::

View File

@ -129,7 +129,7 @@ interface PageMeta {
- **Type**: `boolean | (to: RouteLocationNormalized, from: RouteLocationNormalized) => boolean` - **Type**: `boolean | (to: RouteLocationNormalized, from: RouteLocationNormalized) => boolean`
Tell Nuxt to scroll to the top before rendering the page or not. If you want to overwrite the default scroll behavior of Nuxt, you can do so in `~/app/router.options.ts` (see [docs](/docs/guide/directory-structure/pages/#router-options)) for more info. Tell Nuxt to scroll to the top before rendering the page or not. If you want to overwrite the default scroll behavior of Nuxt, you can do so in `~/app/router.options.ts` (see [custom routing](/docs/guide/going-further/custom-routing#using-approuteroptions)) for more info.
**`[key: string]`** **`[key: string]`**

View File

@ -135,7 +135,7 @@ You can also run `pnpm lint:docs:fix` to highlight and resolve any lint issues.
### Open a PR ### Open a PR
Please make sure your PR title adheres to the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0) guidelines. Please make sure your PR title adheres to the [conventional commits](https://www.conventionalcommits.org) guidelines.
```bash [Example of PR title] ```bash [Example of PR title]
docs: update the section about the nuxt.config.ts file docs: update the section about the nuxt.config.ts file

View File

@ -20,6 +20,9 @@ export default createConfigForNuxt({
// Don't add other attributes to this object // Don't add other attributes to this object
ignores: [ ignores: [
'packages/schema/schema/**', 'packages/schema/schema/**',
'packages/nuxt/src/app/components/welcome.vue',
'packages/nuxt/src/app/components/error-*.vue',
'packages/nuxt/src/core/runtime/nitro/error-*',
], ],
}, },
{ {
@ -207,6 +210,12 @@ export default createConfigForNuxt({
'perfectionist/sort-objects': 'error', 'perfectionist/sort-objects': 'error',
}, },
}, },
{
files: ['packages/nuxt/src/app/components/welcome.vue'],
rules: {
'vue/multi-word-component-names': 'off',
},
},
) )
// Generate type definitions for the eslint config // Generate type definitions for the eslint config

View File

@ -11,6 +11,8 @@ exclude = [
"https://twitter.nuxt.dev/", "https://twitter.nuxt.dev/",
"https://github.com/nuxt/translations/discussions/4", "https://github.com/nuxt/translations/discussions/4",
"https://stackoverflow.com/help/minimal-reproducible-example", "https://stackoverflow.com/help/minimal-reproducible-example",
# TODO: remove when their SSL certificate is valid again
"https://www.conventionalcommits.org",
# single-quotes are required for regexp # single-quotes are required for regexp
'(https?:\/\/github\.com\/)(.*\/)(generate)', '(https?:\/\/github\.com\/)(.*\/)(generate)',
] ]

View File

@ -8,11 +8,11 @@
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "pnpm --filter './packages/**' prepack", "build": "pnpm --filter @nuxt/ui-templates prepack && pnpm --filter './packages/[^u]**' prepack",
"build:stub": "pnpm dev:prepare", "build:stub": "pnpm dev:prepare",
"cleanup": "rimraf 'packages/**/node_modules' 'playground/node_modules' 'node_modules'", "cleanup": "rimraf 'packages/**/node_modules' 'playground/node_modules' 'node_modules'",
"dev": "pnpm play", "dev": "pnpm play",
"dev:prepare": "pnpm --filter './packages/**' prepack --stub", "dev:prepare": "pnpm --filter @nuxt/ui-templates prepack && pnpm --filter './packages/[^u]**' prepack --stub",
"lint": "eslint . --cache", "lint": "eslint . --cache",
"lint:fix": "eslint . --cache --fix", "lint:fix": "eslint . --cache --fix",
"lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md", "lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md",
@ -26,51 +26,52 @@
"test:fixtures": "pnpm test:prepare && vitest run --dir test", "test:fixtures": "pnpm test:prepare && vitest run --dir test",
"test:fixtures:dev": "TEST_ENV=dev pnpm test:fixtures", "test:fixtures:dev": "TEST_ENV=dev pnpm test:fixtures",
"test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures", "test:fixtures:webpack": "TEST_BUILDER=webpack pnpm test:fixtures",
"test:runtime": "vitest -c vitest.nuxt.config.ts --coverage", "test:runtime": "vitest -c vitest.nuxt.config.ts",
"test:types": "pnpm --filter './test/fixtures/**' test:types", "test:types": "pnpm --filter './test/fixtures/**' test:types",
"test:unit": "vitest run packages/ --coverage", "test:unit": "JITI_CACHE=0 vitest run packages/",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs" "typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs"
}, },
"resolutions": { "resolutions": {
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/ui-templates": "workspace:*",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"rollup": "^4.14.2", "magic-string": "^0.30.10",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"vite": "5.2.8", "rollup": "^4.17.2",
"vue": "3.4.21", "vite": "5.2.11",
"magic-string": "^0.30.9" "vue": "3.4.26"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.0.0", "@eslint/js": "9.1.1",
"@nuxt/eslint-config": "0.3.6", "@nuxt/eslint-config": "0.3.10",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/test-utils": "3.12.0", "@nuxt/test-utils": "3.12.1",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.0.3", "@testing-library/vue": "8.0.3",
"@types/eslint__js": "8.42.3", "@types/eslint__js": "8.42.3",
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"@types/node": "20.12.7", "@types/node": "20.12.8",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@vitest/coverage-v8": "1.4.0", "@vitest/coverage-v8": "1.5.3",
"@vue/test-utils": "2.4.5", "@vue/test-utils": "2.4.5",
"case-police": "0.6.1", "case-police": "0.6.1",
"changelogen": "0.5.5", "changelogen": "0.5.5",
"consola": "3.2.3", "consola": "3.2.3",
"devalue": "4.3.2", "devalue": "5.0.0",
"eslint": "9.0.0", "eslint": "9.1.1",
"eslint-plugin-no-only-tests": "3.1.0", "eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-perfectionist": "2.8.0", "eslint-plugin-perfectionist": "2.10.0",
"eslint-typegen": "0.2.2", "eslint-typegen": "0.2.4",
"execa": "8.0.1", "execa": "8.0.1",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"globby": "14.0.1", "globby": "14.0.1",
"h3": "1.11.1", "h3": "1.11.1",
"happy-dom": "14.7.1", "happy-dom": "14.7.1",
"jiti": "1.21.0", "jiti": "1.21.0",
"markdownlint-cli": "0.39.0", "markdownlint-cli": "0.40.0",
"nitropack": "2.9.6", "nitropack": "2.9.6",
"nuxi": "3.11.1", "nuxi": "3.11.1",
"nuxt": "workspace:*", "nuxt": "workspace:*",
@ -83,13 +84,13 @@
"std-env": "3.7.0", "std-env": "3.7.0",
"typescript": "5.4.5", "typescript": "5.4.5",
"ufo": "1.5.3", "ufo": "1.5.3",
"vitest": "1.4.0", "vitest": "1.5.3",
"vitest-environment-nuxt": "1.0.0", "vitest-environment-nuxt": "1.0.0",
"vue": "3.4.21", "vue": "3.4.26",
"vue-router": "4.3.0", "vue-router": "4.3.2",
"vue-tsc": "2.0.13" "vue-tsc": "2.0.16"
}, },
"packageManager": "pnpm@8.15.6", "packageManager": "pnpm@9.0.6",
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
}, },

View File

@ -35,9 +35,9 @@
"ignore": "^5.3.1", "ignore": "^5.3.1",
"jiti": "^1.21.0", "jiti": "^1.21.0",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"mlly": "^1.6.1", "mlly": "^1.7.0",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"pkg-types": "^1.0.3", "pkg-types": "^1.1.0",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.0", "semver": "^7.6.0",
"ufo": "^1.5.3", "ufo": "^1.5.3",
@ -52,8 +52,8 @@
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"nitropack": "2.9.6", "nitropack": "2.9.6",
"unbuild": "latest", "unbuild": "latest",
"vite": "5.2.8", "vite": "5.2.11",
"vitest": "1.4.0", "vitest": "1.5.3",
"webpack": "5.91.0" "webpack": "5.91.0"
}, },
"engines": { "engines": {

View File

@ -1,5 +1,17 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { resolveGroupSyntax } from './ignore.js' import type { Nuxt, NuxtOptions } from '@nuxt/schema'
import { isIgnored, resolveGroupSyntax, resolveIgnorePatterns } from './ignore.js'
import * as context from './context.js'
describe('isIgnored', () => {
it('should populate _ignore', () => {
const mockNuxt = { options: { ignore: ['my-dir'] } as NuxtOptions } as Nuxt
vi.spyOn(context, 'tryUseNuxt').mockReturnValue(mockNuxt)
expect(isIgnored('my-dir/my-file.ts')).toBe(true)
expect(resolveIgnorePatterns()?.includes('my-dir')).toBe(true)
})
})
describe('resolveGroupSyntax', () => { describe('resolveGroupSyntax', () => {
it('should resolve single group syntax', () => { it('should resolve single group syntax', () => {

View File

@ -28,6 +28,8 @@ export function isIgnored (pathname: string): boolean {
return !!(relativePath && nuxt._ignore.ignores(relativePath)) return !!(relativePath && nuxt._ignore.ignores(relativePath))
} }
const NEGATION_RE = /^(!?)(.*)$/
export function resolveIgnorePatterns (relativePath?: string): string[] { export function resolveIgnorePatterns (relativePath?: string): string[] {
const nuxt = tryUseNuxt() const nuxt = tryUseNuxt()
@ -36,22 +38,26 @@ export function resolveIgnorePatterns (relativePath?: string): string[] {
return [] return []
} }
if (!nuxt._ignorePatterns) { const ignorePatterns = nuxt.options.ignore.flatMap(s => resolveGroupSyntax(s))
nuxt._ignorePatterns = nuxt.options.ignore.flatMap(s => resolveGroupSyntax(s))
const nuxtignoreFile = join(nuxt.options.rootDir, '.nuxtignore') const nuxtignoreFile = join(nuxt.options.rootDir, '.nuxtignore')
if (existsSync(nuxtignoreFile)) { if (existsSync(nuxtignoreFile)) {
const contents = readFileSync(nuxtignoreFile, 'utf-8') const contents = readFileSync(nuxtignoreFile, 'utf-8')
nuxt._ignorePatterns.push(...contents.trim().split(/\r?\n/)) ignorePatterns.push(...contents.trim().split(/\r?\n/))
}
} }
if (relativePath) { if (relativePath) {
// Map ignore patterns based on if they start with * or !* // Map ignore patterns based on if they start with * or !*
return nuxt._ignorePatterns.map(p => p[0] === '*' || (p[0] === '!' && p[1] === '*') ? p : relative(relativePath, resolve(nuxt.options.rootDir, p))) return ignorePatterns.map((p) => {
const [_, negation = '', pattern] = p.match(NEGATION_RE) || []
if (pattern[0] === '*') {
return p
}
return negation + relative(relativePath, resolve(nuxt.options.rootDir, pattern || p))
})
} }
return nuxt._ignorePatterns return ignorePatterns
} }
/** /**

View File

@ -1,13 +1,15 @@
import { resolve } from 'pathe'
import type { JSValue } from 'untyped' import type { JSValue } from 'untyped'
import { applyDefaults } from 'untyped' import { applyDefaults } from 'untyped'
import type { LoadConfigOptions } from 'c12' import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
import { loadConfig } from 'c12' import { loadConfig } from 'c12'
import type { NuxtConfig, NuxtOptions } from '@nuxt/schema' import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
import { NuxtConfigSchema } from '@nuxt/schema' import { NuxtConfigSchema } from '@nuxt/schema'
export interface LoadNuxtConfigOptions extends LoadConfigOptions<NuxtConfig> {} export interface LoadNuxtConfigOptions extends LoadConfigOptions<NuxtConfig> {}
const layerSchemaKeys = ['future', 'srcDir', 'rootDir', 'dir']
const layerSchema = Object.fromEntries(Object.entries(NuxtConfigSchema).filter(([key]) => layerSchemaKeys.includes(key)))
export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> { export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> {
(globalThis as any).defineNuxtConfig = (c: any) => c (globalThis as any).defineNuxtConfig = (c: any) => c
const result = await loadConfig<NuxtConfig>({ const result = await loadConfig<NuxtConfig>({
@ -28,15 +30,20 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
nuxtConfig._nuxtConfigFile = configFile nuxtConfig._nuxtConfigFile = configFile
nuxtConfig._nuxtConfigFiles = [configFile] nuxtConfig._nuxtConfigFiles = [configFile]
// Resolve `rootDir` & `srcDir` of layers const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
for (const layer of layers) { for (const layer of layers) {
// Resolve `rootDir` & `srcDir` of layers
layer.config = layer.config || {} layer.config = layer.config || {}
layer.config.rootDir = layer.config.rootDir ?? layer.cwd layer.config.rootDir = layer.config.rootDir ?? layer.cwd
layer.config.srcDir = resolve(layer.config.rootDir!, layer.config.srcDir!)
// Normalise layer directories
layer.config = await applyDefaults(layerSchema, layer.config as NuxtConfig & Record<string, JSValue>) as unknown as NuxtConfig
// Filter layers
if (!layer.configFile || layer.configFile.endsWith('.nuxtrc')) { continue }
_layers.push(layer)
} }
// Filter layers
const _layers = layers.filter(layer => layer.configFile && !layer.configFile.endsWith('.nuxtrc'))
;(nuxtConfig as any)._layers = _layers ;(nuxtConfig as any)._layers = _layers
// Ensure at least one layer remains (without nuxt.config) // Ensure at least one layer remains (without nuxt.config)

View File

@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import { resolve } from 'pathe'
import { loadNuxt } from './loader/nuxt'
import { findPath, resolvePath } from './resolve'
import { defineNuxtModule } from './module/define'
import { addTemplate } from './template'
const nuxt = await loadNuxt({
overrides: {
modules: [
defineNuxtModule(() => {
addTemplate({
filename: 'my-template.mjs',
getContents: () => 'export const myUtil = () => \'hello\'',
})
}),
],
},
})
describe('resolvePath', () => {
it('should resolve paths correctly', async () => {
expect(await resolvePath('.nuxt/app.config')).toBe(resolve(nuxt.options.buildDir, 'app.config'))
})
})
describe('findPath', () => {
it('should find paths correctly', async () => {
expect(await findPath(resolve(nuxt.options.buildDir, 'my-template'), { virtual: true })).toBe(resolve(nuxt.options.buildDir, 'my-template.mjs'))
})
})

View File

@ -17,6 +17,12 @@ export interface ResolvePathOptions {
/** The file extensions to try. Default is Nuxt configured extensions. */ /** The file extensions to try. Default is Nuxt configured extensions. */
extensions?: string[] extensions?: string[]
/**
* Whether to resolve files that exist in the Nuxt VFS (for example, as a Nuxt template).
* @default false
*/
virtual?: boolean
} }
/** /**
@ -30,8 +36,13 @@ export async function resolvePath (path: string, opts: ResolvePathOptions = {}):
path = normalize(path) path = normalize(path)
// Fast return if the path exists // Fast return if the path exists
if (isAbsolute(path) && existsSync(path) && !(await isDirectory(path))) { if (isAbsolute(path)) {
return path if (opts?.virtual && existsInVFS(path)) {
return path
}
if (existsSync(path) && !(await isDirectory(path))) {
return path
}
} }
// Use current nuxt options // Use current nuxt options
@ -49,6 +60,10 @@ export async function resolvePath (path: string, opts: ResolvePathOptions = {}):
} }
// Check if resolvedPath is a file // Check if resolvedPath is a file
if (opts?.virtual && existsInVFS(path, nuxt)) {
return path
}
let _isDir = false let _isDir = false
if (existsSync(path)) { if (existsSync(path)) {
_isDir = await isDirectory(path) _isDir = await isDirectory(path)
@ -61,11 +76,17 @@ export async function resolvePath (path: string, opts: ResolvePathOptions = {}):
for (const ext of extensions) { for (const ext of extensions) {
// path.[ext] // path.[ext]
const pathWithExt = path + ext const pathWithExt = path + ext
if (opts?.virtual && existsInVFS(pathWithExt, nuxt)) {
return pathWithExt
}
if (existsSync(pathWithExt)) { if (existsSync(pathWithExt)) {
return pathWithExt return pathWithExt
} }
// path/index.[ext] // path/index.[ext]
const pathWithIndex = join(path, 'index' + ext) const pathWithIndex = join(path, 'index' + ext)
if (opts?.virtual && existsInVFS(pathWithIndex, nuxt)) {
return pathWithIndex
}
if (_isDir && existsSync(pathWithIndex)) { if (_isDir && existsSync(pathWithIndex)) {
return pathWithIndex return pathWithIndex
} }
@ -85,8 +106,17 @@ export async function resolvePath (path: string, opts: ResolvePathOptions = {}):
* Try to resolve first existing file in paths * Try to resolve first existing file in paths
*/ */
export async function findPath (paths: string | string[], opts?: ResolvePathOptions, pathType: 'file' | 'dir' = 'file'): Promise<string | null> { export async function findPath (paths: string | string[], opts?: ResolvePathOptions, pathType: 'file' | 'dir' = 'file'): Promise<string | null> {
const nuxt = opts?.virtual ? tryUseNuxt() : undefined
for (const path of toArray(paths)) { for (const path of toArray(paths)) {
const rPath = await resolvePath(path, opts) const rPath = await resolvePath(path, opts)
// Check VFS
if (opts?.virtual && existsInVFS(rPath, nuxt)) {
return rPath
}
// Check file system
if (await existsSensitive(rPath)) { if (await existsSensitive(rPath)) {
const _isDir = await isDirectory(rPath) const _isDir = await isDirectory(rPath)
if (!pathType || (pathType === 'file' && !_isDir) || (pathType === 'dir' && _isDir)) { if (!pathType || (pathType === 'file' && !_isDir) || (pathType === 'dir' && _isDir)) {
@ -160,6 +190,17 @@ async function isDirectory (path: string) {
return (await fsp.lstat(path)).isDirectory() return (await fsp.lstat(path)).isDirectory()
} }
function existsInVFS (path: string, nuxt = tryUseNuxt()) {
if (!nuxt) { return false }
if (path in nuxt.vfs) {
return true
}
const templates = nuxt.apps.default?.templates ?? nuxt.options.build.templates
return templates.some(template => template.dst === path)
}
export async function resolveFiles (path: string, pattern: string | string[], opts: { followSymbolicLinks?: boolean } = {}) { export async function resolveFiles (path: string, pattern: string | string[], opts: { followSymbolicLinks?: boolean } = {}) {
const files = await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true }) const files = await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true })
return files.map(p => resolve(path, p)).filter(p => !isIgnored(p)).sort() return files.map(p => resolve(path, p)).filter(p => !isIgnored(p)).sort()

View File

@ -129,6 +129,7 @@ export async function _generateTypes (nuxt: Nuxt) {
jsxImportSource: 'vue', jsxImportSource: 'vue',
target: 'ESNext', target: 'ESNext',
module: 'ESNext', module: 'ESNext',
moduleDetection: 'force',
moduleResolution: nuxt.options.future?.typescriptBundlerResolution || (nuxt.options.experimental as any)?.typescriptBundlerResolution ? 'Bundler' : 'Node', moduleResolution: nuxt.options.future?.typescriptBundlerResolution || (nuxt.options.experimental as any)?.typescriptBundlerResolution ? 'Bundler' : 'Node',
skipLibCheck: true, skipLibCheck: true,
isolatedModules: true, isolatedModules: true,

View File

@ -1,3 +1,4 @@
/** @since 3.9.0 */
export function toArray<T> (value: T | T[]): T[] { export function toArray<T> (value: T | T[]): T[] {
return Array.isArray(value) ? value : [value] return Array.isArray(value) ? value : [value]
} }

View File

@ -60,23 +60,22 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/devalue": "^2.0.2", "@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.1.5", "@nuxt/devtools": "^1.2.0",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.5.4", "@nuxt/telemetry": "^2.5.4",
"@nuxt/ui-templates": "^1.3.3",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.9.5", "@unhead/dom": "^1.9.8",
"@unhead/ssr": "^1.9.5", "@unhead/ssr": "^1.9.8",
"@unhead/vue": "^1.9.5", "@unhead/vue": "^1.9.8",
"@vue/shared": "^3.4.21", "@vue/shared": "^3.4.26",
"acorn": "8.11.3", "acorn": "8.11.3",
"c12": "^1.10.0", "c12": "^1.10.0",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"cookie-es": "^1.1.0", "cookie-es": "^1.1.0",
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
"devalue": "^4.3.2", "devalue": "^5.0.0",
"esbuild": "^0.20.2", "esbuild": "^0.20.2",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
@ -84,11 +83,12 @@
"globby": "^14.0.1", "globby": "^14.0.1",
"h3": "^1.11.1", "h3": "^1.11.1",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"ignore": "^5.3.1",
"jiti": "^1.21.0", "jiti": "^1.21.0",
"klona": "^2.0.6", "klona": "^2.0.6",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"magic-string": "^0.30.9", "magic-string": "^0.30.10",
"mlly": "^1.6.1", "mlly": "^1.7.0",
"nitropack": "^2.9.6", "nitropack": "^2.9.6",
"nuxi": "^3.11.1", "nuxi": "^3.11.1",
"nypm": "^0.3.8", "nypm": "^0.3.8",
@ -96,7 +96,7 @@
"ohash": "^1.1.3", "ohash": "^1.1.3",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"pkg-types": "^1.0.3", "pkg-types": "^1.1.0",
"radix3": "^1.1.2", "radix3": "^1.1.2",
"scule": "^1.3.0", "scule": "^1.3.0",
"std-env": "^3.7.0", "std-env": "^3.7.0",
@ -111,19 +111,20 @@
"unplugin-vue-router": "^0.7.0", "unplugin-vue-router": "^0.7.0",
"unstorage": "^1.10.2", "unstorage": "^1.10.2",
"untyped": "^1.4.2", "untyped": "^1.4.2",
"vue": "^3.4.21", "vue": "^3.4.26",
"vue-bundle-renderer": "^2.0.0", "vue-bundle-renderer": "^2.0.0",
"vue-devtools-stub": "^0.1.0", "vue-devtools-stub": "^0.1.0",
"vue-router": "^4.3.0" "vue-router": "^4.3.2"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/ui-templates": "1.3.3",
"@parcel/watcher": "2.4.1", "@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"unbuild": "latest", "unbuild": "latest",
"vite": "5.2.8", "vite": "5.2.11",
"vitest": "1.4.0" "vitest": "1.5.3"
}, },
"peerDependencies": { "peerDependencies": {
"@parcel/watcher": "^2.1.0", "@parcel/watcher": "^2.1.0",

View File

@ -41,9 +41,10 @@ const NuxtClientFallbackServer = defineComponent({
const vm = getCurrentInstance() const vm = getCurrentInstance()
const ssrFailed = ref(false) const ssrFailed = ref(false)
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const error = useState<boolean | undefined>(`${props.uid}`)
onErrorCaptured((err) => { onErrorCaptured((err) => {
useState(`${props.uid}`, () => true) error.value = true
ssrFailed.value = true ssrFailed.value = true
ctx.emit('ssr-error', err) ctx.emit('ssr-error', err)
return false return false

View File

@ -0,0 +1 @@
../../../../ui-templates/dist/templates/error-404.vue

View File

@ -0,0 +1 @@
../../../../ui-templates/dist/templates/error-500.vue

View File

@ -0,0 +1 @@
../../../../ui-templates/dist/templates/error-dev.vue

View File

@ -40,10 +40,10 @@ const description = _error.message || _error.toString()
const stack = import.meta.dev && !is404 ? _error.description || `<pre>${stacktrace}</pre>` : undefined const stack = import.meta.dev && !is404 ? _error.description || `<pre>${stacktrace}</pre>` : undefined
// TODO: Investigate side-effect issue with imports // TODO: Investigate side-effect issue with imports
const _Error404 = defineAsyncComponent(() => import('@nuxt/ui-templates/templates/error-404.vue').then(r => r.default || r)) const _Error404 = defineAsyncComponent(() => import('./error-404.vue').then(r => r.default || r))
const _Error = import.meta.dev const _Error = import.meta.dev
? defineAsyncComponent(() => import('@nuxt/ui-templates/templates/error-dev.vue').then(r => r.default || r)) ? defineAsyncComponent(() => import('./error-dev.vue').then(r => r.default || r))
: defineAsyncComponent(() => import('@nuxt/ui-templates/templates/error-500.vue').then(r => r.default || r)) : defineAsyncComponent(() => import('./error-500.vue').then(r => r.default || r))
const ErrorTemplate = is404 ? _Error404 : _Error const ErrorTemplate = is404 ? _Error404 : _Error
</script> </script>

View File

@ -7,7 +7,7 @@ import type {
VNodeProps, VNodeProps,
} from 'vue' } from 'vue'
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue' import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue'
import type { RouteLocation, RouteLocationRaw, Router, RouterLinkProps } from '#vue-router' import type { RouteLocation, RouteLocationRaw, Router, RouterLink, RouterLinkProps, useLink } from '#vue-router'
import { hasProtocol, joinURL, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash } from 'ufo' import { hasProtocol, joinURL, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash } from 'ufo'
import { preloadRouteComponents } from '../composables/preload' import { preloadRouteComponents } from '../composables/preload'
import { onNuxtReady } from '../composables/ready' import { onNuxtReady } from '../composables/ready'
@ -121,6 +121,79 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
return resolvedPath return resolvedPath
} }
function useNuxtLink (props: NuxtLinkProps) {
const router = useRouter()
const config = useRuntimeConfig()
// Resolving `to` value from `to` and `href` props
const to: ComputedRef<string | RouteLocationRaw> = computed(() => {
checkPropConflicts(props, 'to', 'href')
const path = props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
return resolveTrailingSlashBehavior(path, router.resolve)
})
// Lazily check whether to.value has a protocol
const isAbsoluteUrl = computed(() => typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true }))
// Resolves `to` value if it's a route location object
const href = computed(() => (typeof to.value === 'object'
? router.resolve(to.value)?.href ?? null
: (to.value && !props.external && !isAbsoluteUrl.value)
? resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve) as string
: to.value
))
const builtinRouterLink = resolveComponent('RouterLink') as string | typeof RouterLink
const useBuiltinLink = builtinRouterLink && typeof builtinRouterLink !== 'string' ? builtinRouterLink.useLink : undefined
const link = useBuiltinLink?.({
...props,
to: to.value,
})
const hasTarget = computed(() => props.target && props.target !== '_self')
// Resolving link type
const isExternal = computed<boolean>(() => {
// External prop is explicitly set
if (props.external) {
return true
}
// When `target` prop is set, link is external
if (hasTarget.value) {
return true
}
// When `to` is a route object then it's an internal link
if (typeof to.value === 'object') {
return false
}
return to.value === '' || isAbsoluteUrl.value
})
return {
to,
hasTarget,
isAbsoluteUrl,
isExternal,
//
href,
isActive: link?.isActive ?? computed(() => to.value === router.currentRoute.value.path),
isExactActive: link?.isExactActive ?? computed(() => to.value === router.currentRoute.value.path),
route: link?.route ?? computed(() => router.resolve(to.value)),
async navigate () {
await navigateTo(href.value, { replace: props.replace, external: props.external })
},
} satisfies ReturnType<typeof useLink> & {
to: ComputedRef<string | RouteLocationRaw>
hasTarget: ComputedRef<boolean | null | undefined>
isAbsoluteUrl: ComputedRef<boolean>
isExternal: ComputedRef<boolean>
}
}
return defineComponent({ return defineComponent({
name: componentName, name: componentName,
props: { props: {
@ -208,43 +281,11 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
required: false, required: false,
}, },
}, },
useLink: useNuxtLink,
setup (props, { slots }) { setup (props, { slots }) {
const router = useRouter() const router = useRouter()
const config = useRuntimeConfig()
// Resolving `to` value from `to` and `href` props const { to, href, navigate, isExternal, hasTarget, isAbsoluteUrl } = useNuxtLink(props)
const to: ComputedRef<string | RouteLocationRaw> = computed(() => {
checkPropConflicts(props, 'to', 'href')
const path = props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
return resolveTrailingSlashBehavior(path, router.resolve)
})
// Lazily check whether to.value has a protocol
const isAbsoluteUrl = computed(() => typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true }))
const hasTarget = computed(() => props.target && props.target !== '_self')
// Resolving link type
const isExternal = computed<boolean>(() => {
// External prop is explicitly set
if (props.external) {
return true
}
// When `target` prop is set, link is external
if (hasTarget.value) {
return true
}
// When `to` is a route object then it's an internal link
if (typeof to.value === 'object') {
return false
}
return to.value === '' || isAbsoluteUrl.value
})
// Prefetching // Prefetching
const prefetched = ref(false) const prefetched = ref(false)
@ -324,14 +365,6 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
) )
} }
// Resolves `to` value if it's a route location object
// converts `""` to `null` to prevent the attribute from being added as empty (`href=""`)
const href = typeof to.value === 'object'
? router.resolve(to.value)?.href ?? null
: (to.value && !props.external && !isAbsoluteUrl.value)
? resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve) as string
: to.value || null
// Resolves `target` value // Resolves `target` value
const target = props.target || null const target = props.target || null
@ -354,15 +387,13 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
return null return null
} }
const navigate = () => navigateTo(href, { replace: props.replace, external: props.external })
return slots.default({ return slots.default({
href, href: href.value,
navigate, navigate,
get route () { get route () {
if (!href) { return undefined } if (!href.value) { return undefined }
const url = parseURL(href) const url = parseURL(href.value)
return { return {
path: url.pathname, path: url.pathname,
fullPath: url.pathname, fullPath: url.pathname,
@ -373,7 +404,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
matched: [], matched: [],
redirectedFrom: undefined, redirectedFrom: undefined,
meta: {}, meta: {},
href, href: href.value,
} satisfies RouteLocation & { href: string } } satisfies RouteLocation & { href: string }
}, },
rel, rel,
@ -384,7 +415,8 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
}) })
} }
return h('a', { ref: el, href, rel, target }, slots.default?.()) // converts `""` to `null` to prevent the attribute from being added as empty (`href=""`)
return h('a', { ref: el, href: href.value || null, rel, target }, slots.default?.())
} }
}, },
}) as unknown as DefineComponent<NuxtLinkProps> }) as unknown as DefineComponent<NuxtLinkProps>

View File

@ -0,0 +1,48 @@
import { defineComponent, h } from 'vue'
import type { Politeness } from '#app/composables/route-announcer'
import { useRouteAnnouncer } from '#app/composables/route-announcer'
export default defineComponent({
name: 'NuxtRouteAnnouncer',
props: {
atomic: {
type: Boolean,
default: false,
},
politeness: {
type: String as () => Politeness,
default: 'polite',
},
},
setup (props, { slots, expose }) {
const { set, polite, assertive, message, politeness } = useRouteAnnouncer({ politeness: props.politeness })
expose({
set, polite, assertive, message, politeness,
})
return () => h('span', {
class: 'nuxt-route-announcer',
style: {
position: 'absolute',
},
}, h('span', {
'role': 'alert',
'aria-live': politeness.value,
'aria-atomic': props.atomic,
'style': {
'border': '0',
'clip': 'rect(0 0 0 0)',
'clip-path': 'inset(50%)',
'height': '1px',
'width': '1px',
'overflow': 'hidden',
'position': 'absolute',
'white-space': 'nowrap',
'word-wrap': 'normal',
'margin': '-1px',
'padding': '0',
},
}, slots.default ? slots.default({ message: message.value }) : message.value))
},
})

View File

@ -0,0 +1 @@
../../../../ui-templates/dist/templates/welcome.vue

View File

@ -110,6 +110,9 @@ export interface AsyncDataExecuteOptions {
export interface _AsyncData<DataT, ErrorT> { export interface _AsyncData<DataT, ErrorT> {
data: Ref<DataT> data: Ref<DataT>
/**
* @deprecated Use `status` instead. This may be removed in a future major version.
*/
pending: Ref<boolean> pending: Ref<boolean>
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void> refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void> execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
@ -375,7 +378,9 @@ export function useAsyncData<
const hasScope = getCurrentScope() const hasScope = getCurrentScope()
if (options.watch) { if (options.watch) {
const unsub = watch(options.watch, () => asyncData.refresh()) const unsub = watch(options.watch, () => asyncData.refresh())
if (hasScope) { if (instance) {
onUnmounted(unsub)
} else if (hasScope) {
onScopeDispose(unsub) onScopeDispose(unsub)
} }
} }
@ -384,7 +389,9 @@ export function useAsyncData<
await asyncData.refresh() await asyncData.refresh()
} }
}) })
if (hasScope) { if (instance) {
onUnmounted(off)
} else if (hasScope) {
onScopeDispose(off) onScopeDispose(off)
} }
} }

View File

@ -0,0 +1,89 @@
import type { Ref } from 'vue'
import { getCurrentScope, onScopeDispose, ref } from 'vue'
import { injectHead } from '@unhead/vue'
import { useNuxtApp } from '#app'
export type Politeness = 'assertive' | 'polite' | 'off'
export type NuxtRouteAnnouncerOpts = {
/** @default 'polite' */
politeness?: Politeness
}
export type RouteAnnouncer = {
message: Ref<string>
politeness: Ref<Politeness>
set: (message: string, politeness: Politeness) => void
polite: (message: string) => void
assertive: (message: string) => void
_cleanup: () => void
}
function createRouteAnnouncer (opts: NuxtRouteAnnouncerOpts = {}) {
const message = ref('')
const politeness = ref<Politeness>(opts.politeness || 'polite')
const activeHead = injectHead()
function set (messageValue: string = '', politenessSetting: Politeness = 'polite') {
message.value = messageValue
politeness.value = politenessSetting
}
function polite (message: string) {
return set(message, 'polite')
}
function assertive (message: string) {
return set(message, 'assertive')
}
function _updateMessageWithPageHeading () {
set(document?.title?.trim(), politeness.value)
}
function _cleanup () {
activeHead?.hooks?.removeHook('dom:rendered', _updateMessageWithPageHeading)
}
_updateMessageWithPageHeading()
activeHead?.hooks?.hook('dom:rendered', () => {
_updateMessageWithPageHeading()
})
return {
_cleanup,
message,
politeness,
set,
polite,
assertive,
}
}
/**
* composable to handle the route announcer
* @since 3.12.0
*/
export function useRouteAnnouncer (opts: Partial<NuxtRouteAnnouncerOpts> = {}): Omit<RouteAnnouncer, '_cleanup'> {
const nuxtApp = useNuxtApp()
// Initialise global route announcer if it doesn't exist already
const announcer = nuxtApp._routeAnnouncer = nuxtApp._routeAnnouncer || createRouteAnnouncer(opts)
if (opts.politeness !== announcer.politeness.value) {
announcer.politeness.value = opts.politeness || 'polite'
}
if (import.meta.client && getCurrentScope()) {
nuxtApp._routeAnnouncerDeps = nuxtApp._routeAnnouncerDeps || 0
nuxtApp._routeAnnouncerDeps++
onScopeDispose(() => {
nuxtApp._routeAnnouncerDeps!--
if (nuxtApp._routeAnnouncerDeps === 0) {
announcer._cleanup()
delete nuxtApp._routeAnnouncer
}
})
}
return announcer
}

View File

@ -169,7 +169,8 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
nuxtApp.ssrContext!._renderResponse = { nuxtApp.ssrContext!._renderResponse = {
statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302), statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302),
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`, body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodeURI(location) }, // do not encode as this would break some modules and some environments
headers: { location },
} }
return response return response
} }

View File

@ -27,7 +27,8 @@ function deepAssign (obj: any, newObj: any) {
for (const key in newObj) { for (const key in newObj) {
const val = newObj[key] const val = newObj[key]
if (val !== null && typeof val === 'object') { if (val !== null && typeof val === 'object') {
obj[key] = obj[key] || {} const defaultVal = Array.isArray(val) ? [] : {}
obj[key] = obj[key] || defaultVal
deepAssign(obj[key], val) deepAssign(obj[key], val)
} else { } else {
obj[key] = val obj[key] = val

View File

@ -17,6 +17,7 @@ import type { NuxtError } from '../app/composables/error'
import type { AsyncDataRequestStatus } from '../app/composables/asyncData' import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
import type { NuxtAppManifestMeta } from '../app/composables/manifest' import type { NuxtAppManifestMeta } from '../app/composables/manifest'
import type { LoadingIndicator } from '../app/composables/loading-indicator' import type { LoadingIndicator } from '../app/composables/loading-indicator'
import type { RouteAnnouncer } from '../app/composables/route-announcer'
import type { ViewTransition } from './plugins/view-transitions.client' import type { ViewTransition } from './plugins/view-transitions.client'
import type { NuxtAppLiterals } from '#app' import type { NuxtAppLiterals } from '#app'
@ -150,6 +151,11 @@ interface _NuxtApp {
/** @internal */ /** @internal */
_payloadRevivers: Record<string, (data: any) => any> _payloadRevivers: Record<string, (data: any) => any>
/** @internal */
_routeAnnouncer?: RouteAnnouncer
/** @internal */
_routeAnnouncerDeps?: number
// Nuxt injections // Nuxt injections
$config: RuntimeConfig $config: RuntimeConfig
@ -227,6 +233,7 @@ export interface CreateOptions {
globalName?: NuxtApp['globalName'] globalName?: NuxtApp['globalName']
} }
/** @since 3.0.0 */
export function createNuxtApp (options: CreateOptions) { export function createNuxtApp (options: CreateOptions) {
let hydratingCount = 0 let hydratingCount = 0
const nuxtApp: NuxtApp = { const nuxtApp: NuxtApp = {
@ -247,7 +254,12 @@ export function createNuxtApp (options: CreateOptions) {
static: { static: {
data: {}, data: {},
}, },
runWithContext: (fn: any) => nuxtApp._scope.run(() => callWithNuxt(nuxtApp, fn)), runWithContext (fn: any) {
if (nuxtApp._scope.active) {
return nuxtApp._scope.run(() => callWithNuxt(nuxtApp, fn))
}
return callWithNuxt(nuxtApp, fn)
},
isHydrating: import.meta.client, isHydrating: import.meta.client,
deferHydration () { deferHydration () {
if (!nuxtApp.isHydrating) { return () => {} } if (!nuxtApp.isHydrating) { return () => {} }
@ -343,6 +355,7 @@ export function createNuxtApp (options: CreateOptions) {
return nuxtApp return nuxtApp
} }
/** @since 3.0.0 */
export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin & ObjectPlugin<any>) { export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin & ObjectPlugin<any>) {
if (plugin.hooks) { if (plugin.hooks) {
nuxtApp.hooks.addHooks(plugin.hooks) nuxtApp.hooks.addHooks(plugin.hooks)
@ -357,6 +370,7 @@ export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin & ObjectPlug
} }
} }
/** @since 3.0.0 */
export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & ObjectPlugin<any>>) { export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & ObjectPlugin<any>>) {
const resolvedPlugins: string[] = [] const resolvedPlugins: string[] = []
const unresolvedPlugins: [Set<string>, Plugin & ObjectPlugin<any>][] = [] const unresolvedPlugins: [Set<string>, Plugin & ObjectPlugin<any>][] = []
@ -407,6 +421,7 @@ export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & Ob
if (errors.length) { throw errors[0] } if (errors.length) { throw errors[0] }
} }
/** @since 3.0.0 */
/* @__NO_SIDE_EFFECTS__ */ /* @__NO_SIDE_EFFECTS__ */
export function defineNuxtPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPlugin<T>): Plugin<T> & ObjectPlugin<T> { export function defineNuxtPlugin<T extends Record<string, unknown>> (plugin: Plugin<T> | ObjectPlugin<T>): Plugin<T> & ObjectPlugin<T> {
if (typeof plugin === 'function') { return plugin } if (typeof plugin === 'function') { return plugin }
@ -419,14 +434,16 @@ export function defineNuxtPlugin<T extends Record<string, unknown>> (plugin: Plu
/* @__NO_SIDE_EFFECTS__ */ /* @__NO_SIDE_EFFECTS__ */
export const definePayloadPlugin = defineNuxtPlugin export const definePayloadPlugin = defineNuxtPlugin
/** @since 3.0.0 */
export function isNuxtPlugin (plugin: unknown) { export function isNuxtPlugin (plugin: unknown) {
return typeof plugin === 'function' && NuxtPluginIndicator in plugin return typeof plugin === 'function' && NuxtPluginIndicator in plugin
} }
/** /**
* Ensures that the setup function passed in has access to the Nuxt instance via `useNuxt`. * Ensures that the setup function passed in has access to the Nuxt instance via `useNuxtApp`.
* @param nuxt A Nuxt instance * @param nuxt A Nuxt instance
* @param setup The function to call * @param setup The function to call
* @since 3.0.0
*/ */
export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters<T>) { export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp | _NuxtApp, setup: T, args?: Parameters<T>) {
const fn: () => ReturnType<T> = () => args ? setup(...args as Parameters<T>) : setup() const fn: () => ReturnType<T> = () => args ? setup(...args as Parameters<T>) : setup()
@ -444,6 +461,7 @@ export function callWithNuxt<T extends (...args: any[]) => any> (nuxt: NuxtApp |
* Returns the current Nuxt instance. * Returns the current Nuxt instance.
* *
* Returns `null` if Nuxt instance is unavailable. * Returns `null` if Nuxt instance is unavailable.
* @since 3.10.0
*/ */
export function tryUseNuxtApp (): NuxtApp | null { export function tryUseNuxtApp (): NuxtApp | null {
let nuxtAppInstance let nuxtAppInstance
@ -461,6 +479,7 @@ export function tryUseNuxtApp (): NuxtApp | null {
* Returns the current Nuxt instance. * Returns the current Nuxt instance.
* *
* Throws an error if Nuxt instance is unavailable. * Throws an error if Nuxt instance is unavailable.
* @since 3.0.0
*/ */
export function useNuxtApp (): NuxtApp { export function useNuxtApp (): NuxtApp {
const nuxtAppInstance = tryUseNuxtApp() const nuxtAppInstance = tryUseNuxtApp()
@ -476,6 +495,7 @@ export function useNuxtApp (): NuxtApp {
return nuxtAppInstance return nuxtAppInstance
} }
/** @since 3.0.0 */
/* @__NO_SIDE_EFFECTS__ */ /* @__NO_SIDE_EFFECTS__ */
export function useRuntimeConfig (_event?: H3Event<EventHandlerRequest>): RuntimeConfig { export function useRuntimeConfig (_event?: H3Event<EventHandlerRequest>): RuntimeConfig {
return useNuxtApp().$config return useNuxtApp().$config
@ -485,6 +505,7 @@ function defineGetter<K extends string | number | symbol, V> (obj: Record<K, V>,
Object.defineProperty(obj, key, { get: () => val }) Object.defineProperty(obj, key, { get: () => val })
} }
/** @since 3.0.0 */
export function defineAppConfig<C extends AppConfigInput> (config: C): C { export function defineAppConfig<C extends AppConfigInput> (config: C): C {
return config return config
} }

View File

@ -82,18 +82,13 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
const { attributes, children, loc } = node const { attributes, children, loc } = node
const slotName = attributes.name ?? 'default' const slotName = attributes.name ?? 'default'
let vfor: string | undefined
if (attributes['v-for']) {
vfor = attributes['v-for']
}
delete attributes['v-for']
if (attributes.name) { delete attributes.name } if (attributes.name) { delete attributes.name }
if (attributes['v-bind']) { if (attributes['v-bind']) {
attributes._bind = extractAttributes(attributes, ['v-bind'])['v-bind'] attributes._bind = extractAttributes(attributes, ['v-bind'])['v-bind']
} }
const teleportAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else']) const teleportAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
const bindings = getPropsToString(attributes, vfor?.split(' in ').map((v: string) => v.trim()) as [string, string]) const bindings = getPropsToString(attributes)
// add the wrapper // add the wrapper
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportSsrSlot${attributeToString(teleportAttributes)} name="${slotName}" :props="${bindings}">`) s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportSsrSlot${attributeToString(teleportAttributes)} name="${slotName}" :props="${bindings}">`)
@ -101,7 +96,7 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
// pass slot fallback to NuxtTeleportSsrSlot fallback // pass slot fallback to NuxtTeleportSsrSlot fallback
const attrString = attributeToString(attributes) const attrString = attributeToString(attributes)
const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(/:?key="[^"]"/g, '') const slice = code.slice(startingIndex + loc[0].end, startingIndex + loc[1].start).replaceAll(/:?key="[^"]"/g, '')
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[1].end, `<slot${attrString.replaceAll(EXTRACTED_ATTRS_RE, '')}/><template #fallback>${vfor ? wrapWithVForDiv(slice, vfor) : slice}</template>`) s.overwrite(startingIndex + loc[0].start, startingIndex + loc[1].end, `<slot${attrString.replaceAll(EXTRACTED_ATTRS_RE, '')}/><template #fallback>${attributes['v-for'] ? wrapWithVForDiv(slice, attributes['v-for']) : slice}</template>`)
} else { } else {
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replaceAll(EXTRACTED_ATTRS_RE, '')) s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replaceAll(EXTRACTED_ATTRS_RE, ''))
} }
@ -164,9 +159,10 @@ function isBinding (attr: string): boolean {
return attr.startsWith(':') return attr.startsWith(':')
} }
function getPropsToString (bindings: Record<string, string>, vfor?: [string, string]): string { function getPropsToString (bindings: Record<string, string>): string {
const vfor = bindings['v-for']?.split(' in ').map((v: string) => v.trim()) as [string, string] | undefined
if (Object.keys(bindings).length === 0) { return 'undefined' } if (Object.keys(bindings).length === 0) { return 'undefined' }
const content = Object.entries(bindings).filter(b => b[0] && b[0] !== '_bind').map(([name, value]) => isBinding(name) ? `[\`${name.slice(1)}\`]: ${value}` : `[\`${name}\`]: \`${value}\``).join(',') const content = Object.entries(bindings).filter(b => b[0] && (b[0] !== '_bind' && b[0] !== 'v-for')).map(([name, value]) => isBinding(name) ? `[\`${name.slice(1)}\`]: ${value}` : `[\`${name}\`]: \`${value}\``).join(',')
const data = bindings._bind ? `mergeProps(${bindings._bind}, { ${content} })` : `{ ${content} }` const data = bindings._bind ? `mergeProps(${bindings._bind}, { ${content} })` : `{ ${content} }`
if (!vfor) { if (!vfor) {
return `[${data}]` return `[${data}]`

View File

@ -26,6 +26,9 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
const scannedPaths: string[] = [] const scannedPaths: string[] = []
for (const dir of dirs) { for (const dir of dirs) {
if (dir.enabled === false) {
continue
}
// A map from resolved path to component name (used for making duplicate warning message) // A map from resolved path to component name (used for making duplicate warning message)
const resolvedNames = new Map<string, string>() const resolvedNames = new Map<string, string>()

View File

@ -1,7 +1,7 @@
import { promises as fsp, mkdirSync, writeFileSync } from 'node:fs' import { promises as fsp, mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'pathe' import { dirname, join, relative, resolve } from 'pathe'
import { defu } from 'defu' import { defu } from 'defu'
import { compileTemplate, findPath, logger, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath, templateUtils, tryResolveModule } from '@nuxt/kit' import { compileTemplate as _compileTemplate, findPath, logger, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath, templateUtils } from '@nuxt/kit'
import type { Nuxt, NuxtApp, NuxtPlugin, NuxtTemplate, ResolvedNuxtTemplate } from 'nuxt/schema' import type { Nuxt, NuxtApp, NuxtPlugin, NuxtTemplate, ResolvedNuxtTemplate } from 'nuxt/schema'
import * as defaultTemplates from './templates' import * as defaultTemplates from './templates'
@ -20,6 +20,12 @@ export function createApp (nuxt: Nuxt, options: Partial<NuxtApp> = {}): NuxtApp
} as unknown as NuxtApp) as NuxtApp } as unknown as NuxtApp) as NuxtApp
} }
const postTemplates = [
defaultTemplates.clientPluginTemplate.filename,
defaultTemplates.serverPluginTemplate.filename,
defaultTemplates.pluginsDeclaration.filename,
]
export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?: (template: ResolvedNuxtTemplate<any>) => boolean } = {}) { export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?: (template: ResolvedNuxtTemplate<any>) => boolean } = {}) {
// Resolve app // Resolve app
await resolveApp(nuxt, app) await resolveApp(nuxt, app)
@ -33,64 +39,98 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
// Normalize templates // Normalize templates
app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl)) app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl))
// compile plugins first as they are needed within the nuxt.vfs
// in order to annotate templated plugins
const filteredTemplates: Record<'pre' | 'post', Array<ResolvedNuxtTemplate<any>>> = {
pre: [],
post: [],
}
for (const template of app.templates as Array<ResolvedNuxtTemplate<any>>) {
if (options.filter && !options.filter(template)) { continue }
const key = template.filename && postTemplates.includes(template.filename) ? 'post' : 'pre'
filteredTemplates[key].push(template)
}
// Compile templates into vfs // Compile templates into vfs
// TODO: remove utils in v4 // TODO: remove utils in v4
const templateContext = { utils: templateUtils, nuxt, app } const templateContext = { utils: templateUtils, nuxt, app }
const filteredTemplates = (app.templates as Array<ResolvedNuxtTemplate<any>>) const compileTemplate = nuxt.options.experimental.compileTemplate ? _compileTemplate : futureCompileTemplate
.filter(template => !options.filter || options.filter(template))
const writes: Array<() => void> = [] const writes: Array<() => void> = []
await Promise.allSettled(filteredTemplates const changedTemplates: Array<ResolvedNuxtTemplate<any>> = []
.map(async (template) => {
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!) async function processTemplate (template: ResolvedNuxtTemplate) {
const mark = performance.mark(fullPath) const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
const oldContents = nuxt.vfs[fullPath] const mark = performance.mark(fullPath)
const contents = await compileTemplate(template, templateContext).catch((e) => { const oldContents = nuxt.vfs[fullPath]
logger.error(`Could not compile template \`${template.filename}\`.`) const contents = await compileTemplate(template, templateContext).catch((e) => {
logger.error(e) logger.error(`Could not compile template \`${template.filename}\`.`)
throw e logger.error(e)
throw e
})
template.modified = oldContents !== contents
if (template.modified) {
nuxt.vfs[fullPath] = contents
const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '')
nuxt.vfs[aliasPath] = contents
// In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') {
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents
}
changedTemplates.push(template)
}
const perf = performance.measure(fullPath, mark?.name) // TODO: remove when Node 14 reaches EOL
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
if (nuxt.options.debug || setupTime > 500) {
logger.info(`Compiled \`${template.filename}\` in ${setupTime}ms`)
}
if (template.modified && template.write) {
writes.push(() => {
mkdirSync(dirname(fullPath), { recursive: true })
writeFileSync(fullPath, contents, 'utf8')
}) })
}
}
template.modified = oldContents !== contents await Promise.allSettled(filteredTemplates.pre.map(processTemplate))
if (template.modified) { await Promise.allSettled(filteredTemplates.post.map(processTemplate))
nuxt.vfs[fullPath] = contents
const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '')
nuxt.vfs[aliasPath] = contents
// In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') {
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents
}
}
const perf = performance.measure(fullPath, mark?.name) // TODO: remove when Node 14 reaches EOL
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
if (nuxt.options.debug || setupTime > 500) {
logger.info(`Compiled \`${template.filename}\` in ${setupTime}ms`)
}
if (template.modified && template.write) {
writes.push(() => {
mkdirSync(dirname(fullPath), { recursive: true })
writeFileSync(fullPath, contents, 'utf8')
})
}
}))
// Write template files in single synchronous step to avoid (possible) additional // Write template files in single synchronous step to avoid (possible) additional
// runtime overhead of cascading HMRs from vite/webpack // runtime overhead of cascading HMRs from vite/webpack
for (const write of writes) { write() } for (const write of writes) { write() }
const changedTemplates = filteredTemplates.filter(t => t.modified)
if (changedTemplates.length) { if (changedTemplates.length) {
await nuxt.callHook('app:templatesGenerated', app, changedTemplates, options) await nuxt.callHook('app:templatesGenerated', app, changedTemplates, options)
} }
} }
/** @internal */ /** @internal */
async function futureCompileTemplate<T> (template: NuxtTemplate<T>, ctx: { nuxt: Nuxt, app: NuxtApp, utils?: unknown }) {
delete ctx.utils
if (template.src) {
try {
return await fsp.readFile(template.src, 'utf-8')
} catch (err) {
logger.error(`[nuxt] Error reading template from \`${template.src}\``)
throw err
}
}
if (template.getContents) {
return template.getContents({ ...ctx, options: template.options! })
}
throw new Error('[nuxt] Invalid template. Templates must have either `src` or `getContents`: ' + JSON.stringify(template))
}
export async function resolveApp (nuxt: Nuxt, app: NuxtApp) { export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
// Resolve main (app.vue) // Resolve main (app.vue)
if (!app.mainComponent) { if (!app.mainComponent) {
@ -102,7 +142,7 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
) )
} }
if (!app.mainComponent) { if (!app.mainComponent) {
app.mainComponent = (await tryResolveModule('@nuxt/ui-templates/templates/welcome.vue', nuxt.options.modulesDir)) ?? '@nuxt/ui-templates/templates/welcome.vue' app.mainComponent = resolve(nuxt.options.appDir, 'components/welcome.vue')
} }
// Resolve root component // Resolve root component

View File

@ -85,7 +85,7 @@ function createWatcher () {
}) })
// TODO: consider moving to emit absolute path in 3.8 or 4.0 // TODO: consider moving to emit absolute path in 3.8 or 4.0
watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(relative(nuxt.options.srcDir, path)))) watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, nuxt.options.experimental.relativeWatchPaths ? normalize(relative(nuxt.options.srcDir, path)) : normalize(path)))
nuxt.hook('close', () => watcher?.close()) nuxt.hook('close', () => watcher?.close())
} }
@ -116,7 +116,7 @@ function createGranularWatcher () {
path = normalize(path) path = normalize(path)
if (!pending) { if (!pending) {
// TODO: consider moving to emit absolute path in 3.8 or 4.0 // TODO: consider moving to emit absolute path in 3.8 or 4.0
nuxt.callHook('builder:watch', event, relative(nuxt.options.srcDir, path)) nuxt.callHook('builder:watch', event, nuxt.options.experimental.relativeWatchPaths ? relative(nuxt.options.srcDir, path) : path)
} }
if (event === 'unlinkDir' && path in watchers) { if (event === 'unlinkDir' && path in watchers) {
watchers[path]?.close() watchers[path]?.close()
@ -125,7 +125,7 @@ function createGranularWatcher () {
if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) { if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) {
watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] }) watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] })
// TODO: consider moving to emit absolute path in 3.8 or 4.0 // TODO: consider moving to emit absolute path in 3.8 or 4.0
watchers[path].on('all', (event, p) => nuxt.callHook('builder:watch', event, normalize(relative(nuxt.options.srcDir, p)))) watchers[path].on('all', (event, p) => nuxt.callHook('builder:watch', event, nuxt.options.experimental.relativeWatchPaths ? normalize(relative(nuxt.options.srcDir, p)) : normalize(p)))
nuxt.hook('close', () => watchers[path]?.close()) nuxt.hook('close', () => watchers[path]?.close())
} }
}) })
@ -159,7 +159,7 @@ async function createParcelWatcher () {
for (const event of events) { for (const event of events) {
if (isIgnored(event.path)) { continue } if (isIgnored(event.path)) { continue }
// TODO: consider moving to emit absolute path in 3.8 or 4.0 // TODO: consider moving to emit absolute path in 3.8 or 4.0
nuxt.callHook('builder:watch', watchEvents[event.type], normalize(relative(nuxt.options.srcDir, event.path))) nuxt.callHook('builder:watch', watchEvents[event.type], nuxt.options.experimental.relativeWatchPaths ? normalize(relative(nuxt.options.srcDir, event.path)) : normalize(event.path))
} }
}, { }, {
ignore: [ ignore: [

View File

@ -1,7 +1,7 @@
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import { existsSync, promises as fsp, readFileSync } from 'node:fs' import { existsSync, promises as fsp, readFileSync } from 'node:fs'
import { cpus } from 'node:os' import { cpus } from 'node:os'
import { join, normalize, relative, resolve } from 'pathe' import { join, relative, resolve } from 'pathe'
import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from 'radix3' import { createRouter as createRadixRouter, exportMatcher, toRouteMatcher } from 'radix3'
import { randomUUID } from 'uncrypto' import { randomUUID } from 'uncrypto'
import { joinURL, withTrailingSlash } from 'ufo' import { joinURL, withTrailingSlash } from 'ufo'
@ -14,10 +14,10 @@ import fsExtra from 'fs-extra'
import { dynamicEventHandler } from 'h3' import { dynamicEventHandler } from 'h3'
import { isWindows } from 'std-env' import { isWindows } from 'std-env'
import type { Nuxt, NuxtOptions, RuntimeConfig } from 'nuxt/schema' import type { Nuxt, NuxtOptions, RuntimeConfig } from 'nuxt/schema'
import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs'
import { version as nuxtVersion } from '../../package.json' import { version as nuxtVersion } from '../../package.json'
import { distDir } from '../dirs' import { distDir } from '../dirs'
import { toArray } from '../utils' import { toArray } from '../utils'
import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection' import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection'
const logLevelMapReverse = { const logLevelMapReverse = {
@ -154,7 +154,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
baseURL: nuxt.options.app.buildAssetsDir, baseURL: nuxt.options.app.buildAssetsDir,
}, },
...nuxt.options._layers ...nuxt.options._layers
.map(layer => join(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.public || 'public')) .map(layer => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.public || 'public'))
.filter(dir => existsSync(dir)) .filter(dir => existsSync(dir))
.map(dir => ({ dir })), .map(dir => ({ dir })),
], ],
@ -231,7 +231,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Resolve user-provided paths // Resolve user-provided paths
nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!) nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!)
nitroConfig.ignore = [...(nitroConfig.ignore || []), ...resolveIgnorePatterns(nitroConfig.srcDir), `!${join(nuxt.options.buildDir, 'dist/client', nuxt.options.app.buildAssetsDir)}/**/*`] nitroConfig.ignore = [...(nitroConfig.ignore || []), ...resolveIgnorePatterns(nitroConfig.srcDir), `!${join(nuxt.options.buildDir, 'dist/client', nuxt.options.app.buildAssetsDir, '**/*')}`]
// Add app manifest handler and prerender configuration // Add app manifest handler and prerender configuration
if (nuxt.options.experimental.appManifest) { if (nuxt.options.experimental.appManifest) {
@ -409,8 +409,9 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Trigger Nitro reload when SPA loading template changes // Trigger Nitro reload when SPA loading template changes
const spaLoadingTemplateFilePath = await spaLoadingTemplatePath(nuxt) const spaLoadingTemplateFilePath = await spaLoadingTemplatePath(nuxt)
nuxt.hook('builder:watch', async (_event, path) => { nuxt.hook('builder:watch', async (_event, relativePath) => {
if (normalize(path) === spaLoadingTemplateFilePath) { const path = resolve(nuxt.options.srcDir, relativePath)
if (path === spaLoadingTemplateFilePath) {
await nitro.hooks.callHook('rollup:reload') await nitro.hooks.callHook('rollup:reload')
} }
}) })
@ -569,9 +570,9 @@ async function spaLoadingTemplatePath (nuxt: Nuxt) {
return resolve(nuxt.options.srcDir, nuxt.options.spaLoadingTemplate) return resolve(nuxt.options.srcDir, nuxt.options.spaLoadingTemplate)
} }
const possiblePaths = nuxt.options._layers.map(layer => join(layer.config.srcDir, 'app/spa-loading-template.html')) const possiblePaths = nuxt.options._layers.map(layer => resolve(layer.config.srcDir, layer.config.dir?.app || 'app', 'spa-loading-template.html'))
return await findPath(possiblePaths) ?? resolve(nuxt.options.srcDir, 'app/spa-loading-template.html') return await findPath(possiblePaths) ?? resolve(nuxt.options.srcDir, nuxt.options.dir?.app || 'app', 'spa-loading-template.html')
} }
async function spaLoadingTemplate (nuxt: Nuxt) { async function spaLoadingTemplate (nuxt: Nuxt) {

View File

@ -1,7 +1,8 @@
import { dirname, join, normalize, relative, resolve } from 'pathe' import { dirname, join, normalize, relative, resolve } from 'pathe'
import { createDebugger, createHooks } from 'hookable' import { createDebugger, createHooks } from 'hookable'
import ignore from 'ignore'
import type { LoadNuxtOptions } from '@nuxt/kit' import type { LoadNuxtOptions } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addServerPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit' import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addServerPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
import { resolvePath as _resolvePath } from 'mlly' import { resolvePath as _resolvePath } from 'mlly'
import type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema' import type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema'
import type { PackageJson } from 'pkg-types' import type { PackageJson } from 'pkg-types'
@ -275,7 +276,7 @@ async function initNuxt (nuxt: Nuxt) {
addComponent({ addComponent({
name: 'NuxtWelcome', name: 'NuxtWelcome',
priority: 10, // built-in that we do not expect the user to override priority: 10, // built-in that we do not expect the user to override
filePath: (await tryResolveModule('@nuxt/ui-templates/templates/welcome.vue', nuxt.options.modulesDir))!, filePath: resolve(nuxt.options.appDir, 'components/welcome'),
}) })
addComponent({ addComponent({
@ -326,6 +327,14 @@ async function initNuxt (nuxt: Nuxt) {
filePath: resolve(nuxt.options.appDir, 'components/nuxt-loading-indicator'), filePath: resolve(nuxt.options.appDir, 'components/nuxt-loading-indicator'),
}) })
// Add <NuxtRouteAnnouncer>
addComponent({
name: 'NuxtRouteAnnouncer',
priority: 10, // built-in that we do not expect the user to override
filePath: resolve(nuxt.options.appDir, 'components/nuxt-route-announcer'),
mode: 'client',
})
// Add <NuxtClientFallback> // Add <NuxtClientFallback>
if (nuxt.options.experimental.clientFallback) { if (nuxt.options.experimental.clientFallback) {
addComponent({ addComponent({
@ -445,6 +454,10 @@ async function initNuxt (nuxt: Nuxt) {
} }
} }
// (Re)initialise ignore handler with resolved ignores from modules
nuxt._ignore = ignore(nuxt.options.ignoreOptions)
nuxt._ignore.add(resolveIgnorePatterns())
await nuxt.callHook('modules:done') await nuxt.callHook('modules:done')
if (nuxt.options.experimental.appManifest) { if (nuxt.options.experimental.appManifest) {
@ -478,6 +491,12 @@ async function initNuxt (nuxt: Nuxt) {
} }
} }
// Restart Nuxt when new `app/` dir is added
if (event === 'addDir' && path === resolve(nuxt.options.srcDir, 'app')) {
logger.info(`\`${path}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
return nuxt.callHook('restart', { hard: true })
}
// Core Nuxt files: app.vue, error.vue and app.config.ts // Core Nuxt files: app.vue, error.vue and app.config.ts
const isFileChange = ['add', 'unlink'].includes(event) const isFileChange = ['add', 'unlink'].includes(event)
if (isFileChange && RESTART_RE.test(path)) { if (isFileChange && RESTART_RE.test(path)) {
@ -554,7 +573,6 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
options.modulesDir.push(resolve(options.workspaceDir, 'node_modules')) options.modulesDir.push(resolve(options.workspaceDir, 'node_modules'))
options.modulesDir.push(resolve(pkgDir, 'node_modules')) options.modulesDir.push(resolve(pkgDir, 'node_modules'))
options.build.transpile.push( options.build.transpile.push(
'@nuxt/ui-templates', // this exposes vue SFCs
'std-env', // we need to statically replace process.env when used in runtime code 'std-env', // we need to statically replace process.env when used in runtime code
) )
options.alias['vue-demi'] = resolve(options.appDir, 'compat/vue-demi') options.alias['vue-demi'] = resolve(options.appDir, 'compat/vue-demi')

View File

@ -0,0 +1 @@
../../../../../ui-templates/dist/templates/error-500.d.ts

View File

@ -0,0 +1 @@
../../../../../ui-templates/dist/templates/error-500.js

View File

@ -0,0 +1 @@
../../../../../ui-templates/dist/templates/error-dev.d.ts

View File

@ -0,0 +1 @@
../../../../../ui-templates/dist/templates/error-dev.js

View File

@ -66,9 +66,7 @@ export default <NitroErrorHandler> async function errorhandler (error: H3Error,
// Fallback to static rendered error page // Fallback to static rendered error page
if (!res) { if (!res) {
const { template } = import.meta.dev const { template } = import.meta.dev ? await import('./error-dev') : await import('./error-500')
? await import('@nuxt/ui-templates/templates/error-dev.mjs')
: await import('@nuxt/ui-templates/templates/error-500.mjs')
if (import.meta.dev) { if (import.meta.dev) {
// TODO: Support `message` in template // TODO: Support `message` in template
(errorObject as any).description = errorObject.message (errorObject as any).description = errorObject.message

View File

@ -16,7 +16,7 @@ import destr from 'destr'
import { getQuery as getURLQuery, joinURL, withoutTrailingSlash } from 'ufo' import { getQuery as getURLQuery, joinURL, withoutTrailingSlash } from 'ufo'
import { renderToString as _renderToString } from 'vue/server-renderer' import { renderToString as _renderToString } from 'vue/server-renderer'
import { hash } from 'ohash' import { hash } from 'ohash'
import { renderSSRHead } from '@unhead/ssr' import { propsToString, renderSSRHead } from '@unhead/ssr'
import type { HeadEntryOptions } from '@unhead/schema' import type { HeadEntryOptions } from '@unhead/schema'
import type { Link, Script, Style } from '@unhead/vue' import type { Link, Script, Style } from '@unhead/vue'
import { createServerHead } from '@unhead/vue' import { createServerHead } from '@unhead/vue'
@ -29,7 +29,7 @@ import unheadPlugins from '#internal/unhead-plugins.mjs'
import type { NuxtPayload, NuxtSSRContext } from '#app' import type { NuxtPayload, NuxtSSRContext } from '#app'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appHead, appRootId, appRootTag, appTeleportId, appTeleportTag, componentIslands } from '#internal/nuxt.config.mjs' import { appHead, appRootAttrs, appRootTag, appTeleportAttrs, appTeleportTag, componentIslands } from '#internal/nuxt.config.mjs'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths' import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths'
@ -77,7 +77,6 @@ export interface NuxtIslandContext {
export interface NuxtIslandResponse { export interface NuxtIslandResponse {
id?: string id?: string
html: string html: string
state: Record<string, any>
head: { head: {
link: (Record<string, string>)[] link: (Record<string, string>)[]
style: ({ innerHTML: string, key: string })[] style: ({ innerHTML: string, key: string })[]
@ -233,15 +232,15 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
return ctx return ctx
} }
const HAS_APP_TELEPORTS = !!(appTeleportTag && appTeleportId) const HAS_APP_TELEPORTS = !!(appTeleportTag && appTeleportAttrs.id)
const APP_TELEPORT_OPEN_TAG = HAS_APP_TELEPORTS ? `<${appTeleportTag} id="${appTeleportId}">` : '' const APP_TELEPORT_OPEN_TAG = HAS_APP_TELEPORTS ? `<${appTeleportTag}${propsToString(appTeleportAttrs)}>` : ''
const APP_TELEPORT_CLOSE_TAG = HAS_APP_TELEPORTS ? `</${appTeleportTag}>` : '' const APP_TELEPORT_CLOSE_TAG = HAS_APP_TELEPORTS ? `</${appTeleportTag}>` : ''
const APP_ROOT_OPEN_TAG = `<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>` const APP_ROOT_OPEN_TAG = `<${appRootTag}${propsToString(appRootAttrs)}>`
const APP_ROOT_CLOSE_TAG = `</${appRootTag}>` const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`
const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload.json(\?.*)?$/ : /\/_payload.js(\?.*)?$/ const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload.json(\?.*)?$/ : /\/_payload.js(\?.*)?$/
const ROOT_NODE_REGEX = new RegExp(`^${APP_ROOT_OPEN_TAG}([\\s\\S]*)${APP_ROOT_CLOSE_TAG}$`) const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag}[^>]*>([\\s\\S]*)<\\/${appRootTag}>$`)
const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html']) const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html'])
@ -471,7 +470,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]), bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
body: [ body: [
componentIslands ? replaceIslandTeleports(ssrContext, _rendered.html) : _rendered.html, componentIslands ? replaceIslandTeleports(ssrContext, _rendered.html) : _rendered.html,
APP_TELEPORT_OPEN_TAG + (HAS_APP_TELEPORTS ? joinTags([ssrContext.teleports?.[`#${appTeleportId}`]]) : '') + APP_TELEPORT_CLOSE_TAG, APP_TELEPORT_OPEN_TAG + (HAS_APP_TELEPORTS ? joinTags([ssrContext.teleports?.[`#${appTeleportAttrs.id}`]]) : '') + APP_TELEPORT_CLOSE_TAG,
], ],
bodyAppend: [bodyTags], bodyAppend: [bodyTags],
} }
@ -496,7 +495,6 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
id: islandContext.id, id: islandContext.id,
head: islandHead, head: islandHead,
html: getServerComponentHTML(htmlContext.body), html: getServerComponentHTML(htmlContext.body),
state: ssrContext.payload.state,
components: getClientIslandResponse(ssrContext), components: getClientIslandResponse(ssrContext),
slots: getSlotIslandResponse(ssrContext), slots: getSlotIslandResponse(ssrContext),
} }

View File

@ -18,7 +18,7 @@ export function hasSuffix (path: string, suffix: string) {
export function resolveComponentNameSegments (fileName: string, prefixParts: string[]) { export function resolveComponentNameSegments (fileName: string, prefixParts: string[]) {
/** /**
* Array of fileName parts splitted by case, / or - * Array of fileName parts split by case, / or -
* @example third-component -> ['third', 'component'] * @example third-component -> ['third', 'component']
* @example AwesomeComponent -> ['Awesome', 'Component'] * @example AwesomeComponent -> ['Awesome', 'Component']
*/ */

View File

@ -34,7 +34,7 @@ export default defineNuxtModule({
} }
for (const layer of nuxt.options._layers) { for (const layer of nuxt.options._layers) {
const path = await findPath(resolve(layer.config.srcDir, 'app/router.options')) const path = await findPath(resolve(layer.config.srcDir, layer.config.dir?.app || 'app', 'router.options'))
if (path) { context.files.unshift({ path }) } if (path) { context.files.unshift({ path }) }
} }
@ -86,8 +86,8 @@ export default defineNuxtModule({
const restartPaths = nuxt.options._layers.flatMap((layer) => { const restartPaths = nuxt.options._layers.flatMap((layer) => {
const pagesDir = (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages' const pagesDir = (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'
return [ return [
join(layer.config.srcDir || layer.cwd, 'app/router.options.ts'), resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.app || 'app', 'router.options.ts'),
join(layer.config.srcDir || layer.cwd, pagesDir), resolve(layer.config.srcDir || layer.cwd, pagesDir),
] ]
}) })
@ -228,9 +228,9 @@ export default defineNuxtModule({
const updateTemplatePaths = nuxt.options._layers.flatMap((l) => { const updateTemplatePaths = nuxt.options._layers.flatMap((l) => {
const dir = (l.config.rootDir === nuxt.options.rootDir ? nuxt.options : l.config).dir const dir = (l.config.rootDir === nuxt.options.rootDir ? nuxt.options : l.config).dir
return [ return [
join(l.config.srcDir || l.cwd, dir?.pages || 'pages') + '/', resolve(l.config.srcDir || l.cwd, dir?.pages || 'pages') + '/',
join(l.config.srcDir || l.cwd, dir?.layouts || 'layouts') + '/', resolve(l.config.srcDir || l.cwd, dir?.layouts || 'layouts') + '/',
join(l.config.srcDir || l.cwd, dir?.middleware || 'middleware') + '/', resolve(l.config.srcDir || l.cwd, dir?.middleware || 'middleware') + '/',
] ]
}) })
@ -253,7 +253,7 @@ export default defineNuxtModule({
nuxt.hook('app:resolve', (app) => { nuxt.hook('app:resolve', (app) => {
// Add default layout for pages // Add default layout for pages
if (app.mainComponent!.includes('@nuxt/ui-templates')) { if (app.mainComponent === resolve(nuxt.options.appDir, 'components/welcome.vue')) {
app.mainComponent = resolve(runtimeDir, 'app.vue') app.mainComponent = resolve(runtimeDir, 'app.vue')
} }
app.middleware.unshift({ app.middleware.unshift({
@ -324,7 +324,7 @@ export default defineNuxtModule({
} }
nuxt.hook('builder:watch', async (event, relativePath) => { nuxt.hook('builder:watch', async (event, relativePath) => {
const path = join(nuxt.options.srcDir, relativePath) const path = resolve(nuxt.options.srcDir, relativePath)
if (!(path in pageToGlobMap)) { return } if (!(path in pageToGlobMap)) { return }
if (event === 'unlink') { if (event === 'unlink') {
delete inlineRules[path] delete inlineRules[path]

View File

@ -9,7 +9,7 @@ import {
createWebHistory, createWebHistory,
} from '#vue-router' } from '#vue-router'
import { createError } from 'h3' import { createError } from 'h3'
import { isEqual, isSamePath, withoutBase } from 'ufo' import { isEqual, withoutBase } from 'ufo'
import type { PageMeta } from '../composables' import type { PageMeta } from '../composables'
@ -139,6 +139,36 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
named: {}, named: {},
} }
const error = useError()
if (import.meta.client || !nuxtApp.ssrContext?.islandContext) {
router.afterEach(async (to, _from, failure) => {
delete nuxtApp._processingMiddleware
if (import.meta.client && !nuxtApp.isHydrating && error.value) {
// Clear any existing errors
await nuxtApp.runWithContext(clearError)
}
if (failure) {
await nuxtApp.callHook('page:loading:end')
}
if (import.meta.server && failure?.type === 4 /* ErrorTypes.NAVIGATION_ABORTED */) {
return
}
if (to.matched.length === 0) {
await nuxtApp.runWithContext(() => showError(createError({
statusCode: 404,
fatal: false,
statusMessage: `Page not found: ${to.fullPath}`,
data: {
path: to.fullPath,
},
})))
} else if (import.meta.server && to.redirectedFrom && to.fullPath !== initialURL) {
await nuxtApp.runWithContext(() => navigateTo(to.fullPath || '/'))
}
})
}
try { try {
if (import.meta.server) { if (import.meta.server) {
await router.push(initialURL) await router.push(initialURL)
@ -228,34 +258,6 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
await nuxtApp.callHook('page:loading:end') await nuxtApp.callHook('page:loading:end')
}) })
const error = useError()
router.afterEach(async (to, _from, failure) => {
delete nuxtApp._processingMiddleware
if (import.meta.client && !nuxtApp.isHydrating && error.value) {
// Clear any existing errors
await nuxtApp.runWithContext(clearError)
}
if (failure) {
await nuxtApp.callHook('page:loading:end')
}
if (import.meta.server && failure?.type === 4 /* ErrorTypes.NAVIGATION_ABORTED */) {
return
}
if (to.matched.length === 0) {
await nuxtApp.runWithContext(() => showError(createError({
statusCode: 404,
fatal: false,
statusMessage: `Page not found: ${to.fullPath}`,
data: {
path: to.fullPath,
},
})))
} else if (import.meta.server && to.fullPath !== initialURL && (to.redirectedFrom || !isSamePath(to.fullPath, initialURL))) {
await nuxtApp.runWithContext(() => navigateTo(to.fullPath || '/'))
}
})
nuxtApp.hooks.hookOnce('app:created', async () => { nuxtApp.hooks.hookOnce('app:created', async () => {
try { try {
// #4920, #4982 // #4920, #4982

View File

@ -22,6 +22,7 @@ export const wrapInKeepAlive = (props: any, children: any) => {
return { default: () => import.meta.client && props ? h(KeepAlive, props === true ? {} : props, children) : children } return { default: () => import.meta.client && props ? h(KeepAlive, props === true ? {} : props, children) : children }
} }
/** @since 3.9.0 */
export function toArray<T> (value: T | T[]): T[] { export function toArray<T> (value: T | T[]): T[] {
return Array.isArray(value) ? value : [value] return Array.isArray(value) ? value : [value]
} }

View File

@ -1,5 +1,6 @@
import { promises as fsp } from 'node:fs' import { promises as fsp } from 'node:fs'
/** @since 3.9.0 */
export function toArray<T> (value: T | T[]): T[] { export function toArray<T> (value: T | T[]): T[] {
return Array.isArray(value) ? value : [value] return Array.isArray(value) ? value : [value]
} }

View File

@ -30,7 +30,7 @@ describe('resolveApp', () => {
".vue", ".vue",
], ],
"layouts": {}, "layouts": {},
"mainComponent": "@nuxt/ui-templates/dist/templates/welcome.vue", "mainComponent": "<repoRoot>/packages/nuxt/src/app/components/welcome.vue",
"middleware": [ "middleware": [
{ {
"global": true, "global": true,

View File

@ -86,7 +86,7 @@ const dirs: ComponentsDir[] = [
transpile: false, transpile: false,
}, },
] ]
const dirUnable = dirs.map((d) => { return { ...d, enabled: false } })
const expectedComponents = [ const expectedComponents = [
{ {
chunkName: 'components/isle-server', chunkName: 'components/isle-server',
@ -243,3 +243,8 @@ it('components:scanComponents', async () => {
} }
expect(scannedComponents).deep.eq(expectedComponents) expect(scannedComponents).deep.eq(expectedComponents)
}) })
it('components:scanComponents:unable', async () => {
const scannedComponents = await scanComponents(dirUnable, srcDir)
expect(scannedComponents).deep.eq([])
})

View File

@ -146,7 +146,7 @@ describe('treeshake client only in ssr', () => {
expect(treeshaken).toContain('const { ButShouldNotBeTreeShaken } = defineAsyncComponent(async () => {') expect(treeshaken).toContain('const { ButShouldNotBeTreeShaken } = defineAsyncComponent(async () => {')
expect(treeshaken).toContain('const [ { Dont, }, That] = defineAsyncComponent(async () => {') expect(treeshaken).toContain('const [ { Dont, }, That] = defineAsyncComponent(async () => {')
// treeshake object that has an assignement pattern // treeshake object that has an assignment pattern
expect(treeshaken).toContain('const { woooooo, } = defineAsyncComponent(async () => {') expect(treeshaken).toContain('const { woooooo, } = defineAsyncComponent(async () => {')
expect(treeshaken).not.toContain('const { Deep, assignment: { Pattern = ofComponent } } = defineAsyncComponent(async () => {') expect(treeshaken).not.toContain('const { Deep, assignment: { Pattern = ofComponent } } = defineAsyncComponent(async () => {')

View File

@ -35,15 +35,16 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxt/telemetry": "2.5.4", "@nuxt/telemetry": "2.5.4",
"@nuxt/ui-templates": "1.3.3",
"@types/file-loader": "5.0.4", "@types/file-loader": "5.0.4",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/sass-loader": "8.0.8", "@types/sass-loader": "8.0.8",
"@unhead/schema": "1.9.5", "@unhead/schema": "1.9.8",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"@vitejs/plugin-vue-jsx": "3.1.0", "@vitejs/plugin-vue-jsx": "3.1.0",
"@vue/compiler-core": "3.4.21", "@vue/compiler-core": "3.4.26",
"@vue/compiler-sfc": "3.4.21", "@vue/compiler-sfc": "3.4.26",
"@vue/language-core": "2.0.13", "@vue/language-core": "2.0.16",
"c12": "1.10.0", "c12": "1.10.0",
"esbuild-loader": "4.1.0", "esbuild-loader": "4.1.0",
"h3": "1.11.1", "h3": "1.11.1",
@ -53,21 +54,20 @@
"unbuild": "latest", "unbuild": "latest",
"unctx": "2.3.1", "unctx": "2.3.1",
"unenv": "1.9.0", "unenv": "1.9.0",
"vite": "5.2.8", "vite": "5.2.11",
"vue": "3.4.21", "vue": "3.4.26",
"vue-bundle-renderer": "2.0.0", "vue-bundle-renderer": "2.0.0",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue-router": "4.3.0", "vue-router": "4.3.2",
"webpack": "5.91.0", "webpack": "5.91.0",
"webpack-dev-middleware": "7.2.1" "webpack-dev-middleware": "7.2.1"
}, },
"dependencies": { "dependencies": {
"@nuxt/ui-templates": "^1.3.3",
"consola": "^3.2.3", "consola": "^3.2.3",
"defu": "^6.1.4", "defu": "^6.1.4",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"pkg-types": "^1.0.3", "pkg-types": "^1.1.0",
"scule": "^1.3.0", "scule": "^1.3.0",
"std-env": "^3.7.0", "std-env": "^3.7.0",
"ufo": "^1.5.3", "ufo": "^1.5.3",

View File

@ -182,9 +182,10 @@ export default defineUntypedSchema({
/** /**
* Customize Nuxt root element id. * Customize Nuxt root element id.
* @type {string | false} * @type {string | false}
* @deprecated Prefer `rootAttrs.id` instead
*/ */
rootId: { rootId: {
$resolve: val => val === false ? false : val || '__nuxt', $resolve: val => val === false ? false : (val || '__nuxt'),
}, },
/** /**
@ -194,6 +195,19 @@ export default defineUntypedSchema({
$resolve: val => val || 'div', $resolve: val => val || 'div',
}, },
/**
* Customize Nuxt root element id.
* @type {typeof import('@unhead/schema').HtmlAttributes}
*/
rootAttrs: {
$resolve: async (val: undefined | null | Record<string, unknown>, get) => {
const rootId = await get('app.rootId')
return defu(val, {
id: rootId === false ? undefined : (rootId || '__nuxt'),
})
},
},
/** /**
* Customize Nuxt root element tag. * Customize Nuxt root element tag.
*/ */
@ -204,10 +218,24 @@ export default defineUntypedSchema({
/** /**
* Customize Nuxt Teleport element id. * Customize Nuxt Teleport element id.
* @type {string | false} * @type {string | false}
* @deprecated Prefer `teleportAttrs.id` instead
*/ */
teleportId: { teleportId: {
$resolve: val => val === false ? false : (val || 'teleports'), $resolve: val => val === false ? false : (val || 'teleports'),
}, },
/**
* Customize Nuxt Teleport element attributes.
* @type {typeof import('@unhead/schema').HtmlAttributes}
*/
teleportAttrs: {
$resolve: async (val: undefined | null | Record<string, unknown>, get) => {
const teleportId = await get('app.teleportId')
return defu(val, {
id: teleportId === false ? undefined : (teleportId || 'teleports'),
})
},
},
}, },
/** /**

View File

@ -1,3 +1,4 @@
import { existsSync } from 'node:fs'
import { defineUntypedSchema } from 'untyped' import { defineUntypedSchema } from 'untyped'
import { join, relative, resolve } from 'pathe' import { join, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env' import { isDebug, isDevelopment, isTest } from 'std-env'
@ -79,7 +80,7 @@ export default defineUntypedSchema({
* ------| middleware/ * ------| middleware/
* ------| pages/ * ------| pages/
* ------| plugins/ * ------| plugins/
* ------| static/ * ------| public/
* ------| store/ * ------| store/
* ------| server/ * ------| server/
* ------| app.config.ts * ------| app.config.ts
@ -88,7 +89,32 @@ export default defineUntypedSchema({
* ``` * ```
*/ */
srcDir: { srcDir: {
$resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || '.'), $resolve: async (val: string | undefined, get): Promise<string> => {
if (val) {
return resolve(await get('rootDir') as string, val)
}
const [rootDir, isV4] = await Promise.all([
get('rootDir') as Promise<string>,
(get('future') as Promise<Record<string, unknown>>).then(r => r.compatibilityVersion === 4),
])
if (!isV4) {
return rootDir
}
const srcDir = resolve(rootDir, 'app')
if (!existsSync(srcDir)) {
const keys = ['assets', 'layouts', 'middleware', 'pages', 'plugins'] as const
const dirs = await Promise.all(keys.map(key => get(`dir.${key}`) as Promise<string>))
for (const dir of dirs) {
if (existsSync(resolve(rootDir, dir))) {
return rootDir
}
}
}
return srcDir
},
}, },
/** /**
@ -99,7 +125,11 @@ export default defineUntypedSchema({
* *
*/ */
serverDir: { serverDir: {
$resolve: async (val: string | undefined, get): Promise<string> => resolve(await get('rootDir') as string, val || resolve(await get('srcDir') as string, 'server')), $resolve: async (val: string | undefined, get): Promise<string> => {
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
return resolve(await get('rootDir') as string, (val || isV4) ? 'server' : resolve(await get('srcDir') as string, 'server'))
},
}, },
/** /**
@ -219,6 +249,15 @@ export default defineUntypedSchema({
* It is better to stick with defaults unless needed. * It is better to stick with defaults unless needed.
*/ */
dir: { dir: {
app: {
$resolve: async (val: string | undefined, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
if (isV4) {
return resolve(await get('srcDir') as string, val || '.')
}
return val || 'app'
},
},
/** /**
* The assets directory (aliased as `~assets` in your build). * The assets directory (aliased as `~assets` in your build).
*/ */
@ -237,7 +276,15 @@ export default defineUntypedSchema({
/** /**
* The modules directory, each file in which will be auto-registered as a Nuxt module. * The modules directory, each file in which will be auto-registered as a Nuxt module.
*/ */
modules: 'modules', modules: {
$resolve: async (val: string | undefined, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
if (isV4) {
return resolve(await get('rootDir') as string, val || 'modules')
}
return val || 'modules'
},
},
/** /**
* The directory which will be processed to auto-generate your application page routes. * The directory which will be processed to auto-generate your application page routes.
@ -254,7 +301,13 @@ export default defineUntypedSchema({
* and copied across into your `dist` folder when your app is generated. * and copied across into your `dist` folder when your app is generated.
*/ */
public: { public: {
$resolve: async (val, get) => val || await get('dir.static') || 'public', $resolve: async (val: string | undefined, get) => {
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
if (isV4) {
return resolve(await get('rootDir') as string, val || await get('dir.static') as string || 'public')
}
return val || await get('dir.static') as string || 'public'
},
}, },
static: { static: {

View File

@ -1,5 +1,5 @@
import { defineUntypedSchema } from 'untyped' import { defineUntypedSchema } from 'untyped'
import { loading as loadingTemplate } from '@nuxt/ui-templates' import { template as loadingTemplate } from '../../../ui-templates/dist/templates/loading'
export default defineUntypedSchema({ export default defineUntypedSchema({
devServer: { devServer: {

View File

@ -6,6 +6,40 @@ export default defineUntypedSchema({
* (possibly major) version of the framework. * (possibly major) version of the framework.
*/ */
future: { future: {
/**
* Enable early access to Nuxt v4 features or flags.
*
* Setting `compatibilityVersion` to `4` changes defaults throughout your
* Nuxt configuration, but you can granularly re-enable Nuxt v3 behaviour
* when testing (see example). Please file issues if so, so that we can
* address in Nuxt or in the ecosystem.
*
* @example
* ```ts
* export default defineNuxtConfig({
* future: {
* compatibilityVersion: 4,
* },
* // To re-enable _all_ Nuxt v3 behaviour, set the following options:
* srcDir: '.',
* dir: {
* app: 'app'
* },
* experimental: {
* compileTemplate: true,
* templateUtils: true,
* relativeWatchPaths: true,
* defaults: {
* useAsyncData: {
* deep: true
* }
* }
* }
* })
* ```
* @type {3 | 4}
*/
compatibilityVersion: 3,
/** /**
* This enables 'Bundler' module resolution mode for TypeScript, which is the recommended setting * This enables 'Bundler' module resolution mode for TypeScript, which is the recommended setting
* for frameworks like Nuxt and Vite. * for frameworks like Nuxt and Vite.
@ -267,7 +301,7 @@ export default defineUntypedSchema({
* - Uses the hash hydration plugin to reduce initial hydration * - Uses the hash hydration plugin to reduce initial hydration
* @see [Nuxt Discussion #22632](https://github.com/nuxt/nuxt/discussions/22632] * @see [Nuxt Discussion #22632](https://github.com/nuxt/nuxt/discussions/22632]
*/ */
headNext: false, headNext: true,
/** /**
* Allow defining `routeRules` directly within your `~/pages` directory using `defineRouteRules`. * Allow defining `routeRules` directly within your `~/pages` directory using `defineRouteRules`.
@ -319,7 +353,7 @@ export default defineUntypedSchema({
* Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values. * Enables CookieStore support to listen for cookie updates (if supported by the browser) and refresh `useCookie` ref values.
* @see [CookieStore](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore) * @see [CookieStore](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore)
*/ */
cookieStore: false, cookieStore: true,
/** /**
* This allows specifying the default options for core Nuxt components and composables. * This allows specifying the default options for core Nuxt components and composables.
@ -336,7 +370,11 @@ export default defineUntypedSchema({
* Options that apply to `useAsyncData` (and also therefore `useFetch`) * Options that apply to `useAsyncData` (and also therefore `useFetch`)
*/ */
useAsyncData: { useAsyncData: {
deep: true, deep: {
async $resolve (val, get) {
return val ?? !((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
},
},
}, },
/** @type {Pick<typeof import('ofetch')['FetchOptions'], 'timeout' | 'retry' | 'retryDelay' | 'retryStatusCodes'>} */ /** @type {Pick<typeof import('ofetch')['FetchOptions'], 'timeout' | 'retry' | 'retryDelay' | 'retryStatusCodes'>} */
useFetch: {}, useFetch: {},
@ -356,5 +394,42 @@ export default defineUntypedSchema({
* @type {boolean} * @type {boolean}
*/ */
clientNodeCompat: false, clientNodeCompat: false,
/**
* Whether to use `lodash.template` to compile Nuxt templates.
*
* This flag will be removed with the release of v4 and exists only for
* advance testing within Nuxt v3.12+.
*/
compileTemplate: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion !== 4)
},
},
/**
* Whether to provide a legacy `templateUtils` object (with `serialize`,
* `importName` and `importSources`) when compiling Nuxt templates.
*
* This flag will be removed with the release of v4 and exists only for
* advance testing within Nuxt v3.12+.
*/
templateUtils: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion !== 4)
},
},
/**
* Whether to provide relative paths in the `builder:watch` hook.
*
* This flag will be removed with the release of v4 and exists only for
* advance testing within Nuxt v3.12+.
*/
relativeWatchPaths: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion !== 4)
},
},
}, },
}) })

View File

@ -53,7 +53,7 @@ export interface ScanDir {
*/ */
pathPrefix?: boolean pathPrefix?: boolean
/** /**
* Ignore scanning this directory if set to `true` * Ignore scanning this directory if set to `false`
*/ */
enabled?: boolean enabled?: boolean
/** /**

View File

@ -75,7 +75,6 @@ export interface Nuxt {
// Private fields. // Private fields.
_version: string _version: string
_ignore?: Ignore _ignore?: Ignore
_ignorePatterns?: string[]
/** The resolved Nuxt configuration. */ /** The resolved Nuxt configuration. */
options: NuxtOptions options: NuxtOptions

View File

@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest'
import { applyDefaults } from 'untyped'
import { NuxtConfigSchema } from '../src'
import type { NuxtOptions } from '../src'
describe('nuxt folder structure', () => {
it('should resolve directories for v3 setup correctly', async () => {
const result = await applyDefaults(NuxtConfigSchema, {})
expect(getDirs(result as unknown as NuxtOptions)).toMatchInlineSnapshot(`
{
"dir": {
"app": "app",
"modules": "modules",
"public": "public",
},
"rootDir": "<cwd>",
"serverDir": "<cwd>/server",
"srcDir": "<cwd>",
"workspaceDir": "<cwd>",
}
`)
})
it('should resolve directories with a custom `srcDir` and `rootDir`', async () => {
const result = await applyDefaults(NuxtConfigSchema, { srcDir: 'src/', rootDir: '/test' })
expect(getDirs(result as unknown as NuxtOptions)).toMatchInlineSnapshot(`
{
"dir": {
"app": "app",
"modules": "modules",
"public": "public",
},
"rootDir": "/test",
"serverDir": "/test/src/server",
"srcDir": "/test/src",
"workspaceDir": "/test",
}
`)
})
it('should resolve directories when opting-in to v4 schema', async () => {
const result = await applyDefaults(NuxtConfigSchema, { future: { compatibilityVersion: 4 } })
expect(getDirs(result as unknown as NuxtOptions)).toMatchInlineSnapshot(`
{
"dir": {
"app": "<cwd>/app",
"modules": "<cwd>/modules",
"public": "<cwd>/public",
},
"rootDir": "<cwd>",
"serverDir": "<cwd>/server",
"srcDir": "<cwd>/app",
"workspaceDir": "<cwd>",
}
`)
})
it('should resolve directories when opting-in to v4 schema with a custom `srcDir` and `rootDir`', async () => {
const result = await applyDefaults(NuxtConfigSchema, { future: { compatibilityVersion: 4 }, srcDir: 'customApp/', rootDir: '/test' })
expect(getDirs(result as unknown as NuxtOptions)).toMatchInlineSnapshot(`
{
"dir": {
"app": "/test/customApp",
"modules": "/test/modules",
"public": "/test/public",
},
"rootDir": "/test",
"serverDir": "/test/server",
"srcDir": "/test/customApp",
"workspaceDir": "/test",
}
`)
})
})
function getDirs (options: NuxtOptions) {
const stripRoot = (dir: string) => dir.replace(process.cwd(), '<cwd>')
return {
rootDir: stripRoot(options.rootDir),
serverDir: stripRoot(options.serverDir),
srcDir: stripRoot(options.srcDir),
dir: {
app: stripRoot(options.dir.app),
modules: stripRoot(options.dir.modules),
public: stripRoot(options.dir.public),
},
workspaceDir: stripRoot(options.workspaceDir!),
}
}

Some files were not shown because too many files have changed in this diff Show More