Merge remote-tracking branch 'origin/main' into feat-add-auto-import-directives

This commit is contained in:
Daniel Roe 2024-10-25 09:50:01 +01:00
commit 93722c95e5
No known key found for this signature in database
GPG Key ID: 3714AB03996F442B
156 changed files with 3937 additions and 2707 deletions

View File

@ -1,4 +1,4 @@
FROM node:lts@sha256:db5dd2f30cb82a8bdbd16acd4a8f3f2789f5b24f6ce43f98aa041be848c82e45 FROM node:lts@sha256:a5e0ed56f2c20b9689e0f7dd498cac7e08d2a3a283e92d9304e7b9b83e3c6ff3
RUN apt-get update && \ RUN apt-get update && \
apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \ apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \

View File

@ -17,9 +17,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"

View File

@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"

View File

@ -29,9 +29,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"
@ -46,7 +46,7 @@ jobs:
run: pnpm build run: pnpm build
- name: Run benchmarks - name: Run benchmarks
uses: CodSpeedHQ/action@ab07afd34cbbb7a1306e8d14b7cc44e029eee37a # v3.0.0 uses: CodSpeedHQ/action@b587655f756aab640e742fec141261bc6f0a569d # v3.0.1
with: with:
run: pnpm vitest bench run: pnpm vitest bench
token: ${{ secrets.CODSPEED_TOKEN }} token: ${{ secrets.CODSPEED_TOKEN }}

View File

@ -22,11 +22,11 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"

View File

@ -37,9 +37,9 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"
@ -57,7 +57,7 @@ jobs:
run: pnpm build run: pnpm build
- name: Cache dist - name: Cache dist
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
retention-days: 3 retention-days: 3
name: dist name: dist
@ -72,10 +72,10 @@ jobs:
security-events: write security-events: write
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with: with:
config: | config: |
paths: paths:
@ -91,7 +91,7 @@ jobs:
queries: +security-and-quality queries: +security-and-quality
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
with: with:
category: "/language:javascript-typescript" category: "/language:javascript-typescript"
@ -107,9 +107,9 @@ jobs:
module: ["bundler", "node"] module: ["bundler", "node"]
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"
@ -138,9 +138,9 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"
@ -162,9 +162,9 @@ jobs:
needs: needs:
- build - build
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"
@ -191,7 +191,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
env: ["dev", "built"] env: ["dev", "built"]
builder: ["vite", "webpack"] builder: ["vite", "rspack", "webpack"]
context: ["async", "default"] context: ["async", "default"]
manifest: ["manifest-on", "manifest-off"] manifest: ["manifest-on", "manifest-off"]
payload: ["json", "js"] payload: ["json", "js"]
@ -199,12 +199,18 @@ jobs:
exclude: exclude:
- builder: "webpack" - builder: "webpack"
payload: "js" payload: "js"
- builder: "rspack"
payload: "js"
- manifest: "manifest-off" - manifest: "manifest-off"
payload: "js" payload: "js"
- context: "default" - context: "default"
payload: "js" payload: "js"
- os: windows-latest - os: windows-latest
payload: "js" payload: "js"
- env: "dev"
builder: "rspack"
- manifest: "manifest-off"
builder: "rspack"
- env: "dev" - env: "dev"
builder: "webpack" builder: "webpack"
- manifest: "manifest-off" - manifest: "manifest-off"
@ -213,9 +219,9 @@ jobs:
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
cache: "pnpm" cache: "pnpm"
@ -242,7 +248,7 @@ jobs:
TEST_PAYLOAD: ${{ matrix.payload }} TEST_PAYLOAD: ${{ matrix.payload }}
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || matrix.payload == 'js' || runner.os == 'Windows' }} SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || matrix.payload == 'js' || runner.os == 'Windows' }}
- uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
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 }}
@ -256,7 +262,6 @@ jobs:
github.event_name == 'push' && github.event_name == 'push' &&
github.repository == 'nuxt/nuxt' && github.repository == 'nuxt/nuxt' &&
!contains(github.event.head_commit.message, '[skip-release]') && !contains(github.event.head_commit.message, '[skip-release]') &&
!startsWith(github.event.head_commit.message, 'chore') &&
!startsWith(github.event.head_commit.message, 'docs') !startsWith(github.event.head_commit.message, 'docs')
needs: needs:
- lint - lint
@ -266,11 +271,11 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"
@ -307,11 +312,11 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"

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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 uses: actions/dependency-review-action@a6993e2c61fd5dc440b409aa1d6904921c5e1894 # v4.3.5

View File

@ -19,17 +19,17 @@ jobs:
steps: steps:
# Cache lychee results (e.g. to avoid hitting rate limits) # Cache lychee results (e.g. to avoid hitting rate limits)
- name: Restore lychee cache - name: Restore lychee cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
with: with:
path: .lycheecache path: .lycheecache
key: cache-lychee-${{ github.sha }} key: cache-lychee-${{ github.sha }}
restore-keys: cache-lychee- restore-keys: cache-lychee-
# check links with Lychee # check links with Lychee
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Lychee link checker - name: Lychee link checker
uses: lycheeverse/lychee-action@5047c2a4052946424ce139fe111135f6d7c0fe0b # for v1.8.0 uses: lycheeverse/lychee-action@7cd0af4c74a61395d455af97419279d86aafaede # for v1.8.0
with: with:
# arguments with file types to check # arguments with file types to check
args: >- args: >-

View File

@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"

View File

@ -23,9 +23,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"

View File

@ -23,9 +23,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# 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
uses: docker://rhysd/actionlint:1.7.2@sha256:89d3f90f82781dee3c8724651129634b08cf2241bbd67fcd02a1c5198119fc5e uses: docker://rhysd/actionlint:1.7.3@sha256:7617f05bd698cd2f1c3aedc05bc733ccec92cca0738f3e8722c32c5b42c70ae6
with: with:
args: -color args: -color

View File

@ -24,7 +24,7 @@ jobs:
steps: steps:
- name: Ensure action is by maintainer - name: Ensure action is by maintainer
uses: octokit/request-action@872c5c97b3c85c23516a572f02b31401ef82415d # v2.3.1 uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0
id: check_role id: check_role
with: with:
route: GET /repos/nuxt/nuxt/collaborators/${{ github.event.comment.user.login }} route: GET /repos/nuxt/nuxt/collaborators/${{ github.event.comment.user.login }}
@ -48,13 +48,13 @@ jobs:
fi fi
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: ${{ steps.pr.outputs.head_sha }} ref: ${{ steps.pr.outputs.head_sha }}
fetch-depth: 1 fetch-depth: 1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
cache: "pnpm" cache: "pnpm"

View File

@ -19,11 +19,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
node-version: 20 node-version: 20
registry-url: "https://registry.npmjs.org/" registry-url: "https://registry.npmjs.org/"

View File

@ -10,7 +10,7 @@ jobs:
reproduire: reproduire:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- 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

@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
persist-credentials: false persist-credentials: false
@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
if: github.repository == 'nuxt/nuxt' && success() if: github.repository == 'nuxt/nuxt' && success()
with: with:
name: SARIF file name: SARIF file
@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
if: github.repository == 'nuxt/nuxt' && success() if: github.repository == 'nuxt/nuxt' && success()
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -11,7 +11,7 @@ jobs:
stackblitz: stackblitz:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: huang-julien/reproduire-sur-stackblitz@9ceccbfbb0f2f9a9a8db2d1f0dd909cf5cfe67aa # v1.0.2 - uses: huang-julien/reproduire-sur-stackblitz@9ceccbfbb0f2f9a9a8db2d1f0dd909cf5cfe67aa # v1.0.2
with: with:
reproduction-heading: '### Reproduction' reproduction-heading: '### Reproduction'

View File

@ -68,6 +68,10 @@ When importing `@nuxt/test-utils` in your vitest config, It is necessary to have
> ie. `vitest.config.m{ts,js}`. > ie. `vitest.config.m{ts,js}`.
:: ::
::tip
It is possible to set environment variables for testing by using the `.env.test` file.
::
### 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.
@ -285,7 +289,7 @@ import { mockNuxtImport } from '@nuxt/test-utils/runtime'
const { useStorageMock } = vi.hoisted(() => { const { useStorageMock } = vi.hoisted(() => {
return { return {
useStorageMock: vi.fn().mockImplementation(() => { useStorageMock: vi.fn(() => {
return { value: 'mocked storage'} return { value: 'mocked storage'}
}) })
} }

View File

@ -67,6 +67,7 @@ export default defineNuxtConfig({
// app: 'app' // app: 'app'
// }, // },
// experimental: { // experimental: {
// scanPageMeta: 'after-resolve',
// sharedPrerenderData: false, // sharedPrerenderData: false,
// compileTemplate: true, // compileTemplate: true,
// resetAsyncDataToUndefined: true, // resetAsyncDataToUndefined: true,
@ -178,6 +179,7 @@ nuxt.config.ts
1. Create a new directory called `app/`. 1. Create a new directory called `app/`.
1. Move your `assets/`, `components/`, `composables/`, `layouts/`, `middleware/`, `pages/`, `plugins/` and `utils/` folders under it, as well as `app.vue`, `error.vue`, `app.config.ts`. If you have an `app/router-options.ts` or `app/spa-loading-template.html`, these paths remain the same. 1. Move your `assets/`, `components/`, `composables/`, `layouts/`, `middleware/`, `pages/`, `plugins/` and `utils/` folders under it, as well as `app.vue`, `error.vue`, `app.config.ts`. If you have an `app/router-options.ts` or `app/spa-loading-template.html`, these paths remain the same.
1. Make sure your `nuxt.config.ts`, `content/`, `layers/`, `modules/`, `public/` and `server/` folders remain outside the `app/` folder, in the root of your project. 1. Make sure your `nuxt.config.ts`, `content/`, `layers/`, `modules/`, `public/` and `server/` folders remain outside the `app/` folder, in the root of your project.
1. Remember to update any third-party configuration files to work with the new directory structure, such as your `tailwindcss` or `eslint` configuration (if required - `@nuxtjs/tailwindcss` should automatically configure `tailwindcss` correctly).
::tip ::tip
You can automate this migration by running `npx codemod@latest nuxt/4/file-structure` You can automate this migration by running `npx codemod@latest nuxt/4/file-structure`
@ -235,6 +237,45 @@ export default defineNuxtConfig({
}) })
``` ```
#### Scan Page Meta After Resolution
🚦 **Impact Level**: Minimal
##### What Changed
We now scan page metadata (defined in `definePageMeta`) _after_ calling the `pages:extend` hook rather than before.
##### Reasons for Change
This was to allow scanning metadata for pages that users wanted to add in `pages:extend`. We still offer an opportunity to change or override page metadata in a new `pages:resolved` hook.
##### Migration Steps
If you want to override page metadata, do that in `pages:resolved` rather than in `pages:extend`.
```diff
export default defineNuxtConfig({
hooks: {
- 'pages:extend'(pages) {
+ 'pages:resolved'(pages) {
const myPage = pages.find(page => page.path === '/')
myPage.meta ||= {}
myPage.meta.layout = 'overridden-layout'
}
}
})
```
Alternatively, you can revert to the previous behaviour with:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
scanPageMeta: true
}
})
```
#### Shared Prerender Data #### Shared Prerender Data
🚦 **Impact Level**: Medium 🚦 **Impact Level**: Medium

View File

@ -530,7 +530,7 @@ export default defineNuxtConfig({
hooks: { hooks: {
'build:manifest': (manifest) => { 'build:manifest': (manifest) => {
// find the app entry, css list // find the app entry, css list
const css = manifest['node_modules/nuxt/dist/app/entry.js']?.css const css = Object.values(manifest).find(options => options.isEntry)?.css
if (css) { if (css) {
// start from the end of the array and go to the beginning // start from the end of the array and go to the beginning
for (let i = css.length - 1; i >= 0; i--) { for (let i = css.length - 1; i >= 0; i--) {

View File

@ -15,7 +15,7 @@ This file system routing uses naming conventions to create dynamic and nested ro
::code-group ::code-group
```bash [Directory Structure] ```bash [Directory Structure]
| pages/ -| pages/
---| about.vue ---| about.vue
---| index.vue ---| index.vue
---| posts/ ---| posts/

View File

@ -8,21 +8,17 @@ Nuxt comes with two composables and a built-in library to perform data-fetching
In a nutshell: In a nutshell:
- [`useFetch`](/docs/api/composables/use-fetch) is the most straightforward way to handle data fetching in a component setup function. - [`$fetch`](/docs/api/utils/dollarfetch) is the simplest way to make a network request.
- [`$fetch`](/docs/api/utils/dollarfetch) is great to make network requests based on user interaction. - [`useFetch`](/docs/api/composables/use-fetch) is wrapper around `$fetch` that fetches data only once in [universal rendering](/docs/guide/concepts/rendering#universal-rendering).
- [`useAsyncData`](/docs/api/composables/use-async-data), combined with `$fetch`, offers more fine-grained control. - [`useAsyncData`](/docs/api/composables/use-async-data) is similar to `useFetch` but offers more fine-grained control.
Both `useFetch` and `useAsyncData` share a common set of options and patterns that we will detail in the last sections. Both `useFetch` and `useAsyncData` share a common set of options and patterns that we will detail in the last sections.
Before that, it's imperative to know why these composables exist in the first place. ## The need for `useFetch` and `useAsyncData`
## Why use specific composables for data fetching? Nuxt is a framework which can run isomorphic (or universal) code in both server and client environments. If the [`$fetch` function](/docs/api/utils/dollarfetch) is used to perform data fetching in the setup function of a Vue component, this may cause data to be fetched twice, once on the server (to render the HTML) and once again on the client (when the HTML is hydrated). This can cause hydration issues, increase the time to interactivity and cause unpredictable behavior.
Nuxt is a framework which can run isomorphic (or universal) code in both server and client environments. If the [`$fetch` function](/docs/api/utils/dollarfetch) is used to perform data fetching in the setup function of a Vue component, this may cause data to be fetched twice, once on the server (to render the HTML) and once again on the client (when the HTML is hydrated). This is why Nuxt offers specific data fetching composables so data is fetched only once. The [`useFetch`](/docs/api/composables/use-fetch) and [`useAsyncData`](/docs/api/composables/use-async-data) composables solve this problem by ensuring that if an API call is made on the server, the data is forwarded to the client in the payload.
### Network calls duplication
The [`useFetch`](/docs/api/composables/use-fetch) and [`useAsyncData`](/docs/api/composables/use-async-data) composables ensure that once an API call is made on the server, the data is properly forwarded to the client in the payload.
The payload is a JavaScript object accessible through [`useNuxtApp().payload`](/docs/api/composables/use-nuxt-app#payload). It is used on the client to avoid refetching the same data when the code is executed in the browser [during hydration](/docs/guide/concepts/rendering#universal-rendering). The payload is a JavaScript object accessible through [`useNuxtApp().payload`](/docs/api/composables/use-nuxt-app#payload). It is used on the client to avoid refetching the same data when the code is executed in the browser [during hydration](/docs/guide/concepts/rendering#universal-rendering).
@ -30,17 +26,71 @@ The payload is a JavaScript object accessible through [`useNuxtApp().payload`](/
Use the [Nuxt DevTools](https://devtools.nuxt.com) to inspect this data in the **Payload tab**. Use the [Nuxt DevTools](https://devtools.nuxt.com) to inspect this data in the **Payload tab**.
:: ::
```vue [app.vue]
<script setup lang="ts">
const { data } = await useFetch('/api/data')
async function handleFormSubmit() {
const res = await $fetch('/api/submit', {
method: 'POST',
body: {
// My form data
}
})
}
</script>
<template>
<div v-if="data == null">
No data
</div>
<div v-else>
<form @submit="handleFormSubmit">
<!-- form input tags -->
</form>
</div>
</template>
```
In the example above, `useFetch` would make sure that the request would occur in the server and is properly forwarded to the browser. `$fetch` has no such mechanism and is a better option to use when the request is solely made from the browser.
### Suspense ### Suspense
Nuxt uses Vues [`<Suspense>`](https://vuejs.org/guide/built-ins/suspense) component under the hood to prevent navigation before every async data is available to the view. The data fetching composables can help you leverage this feature and use what suits best on a per-calls basis. Nuxt uses Vues [`<Suspense>`](https://vuejs.org/guide/built-ins/suspense) component under the hood to prevent navigation before every async data is available to the view. The data fetching composables can help you leverage this feature and use what suits best on a per-call basis.
::note ::note
You can add the [`<NuxtLoadingIndicator>`](/docs/api/components/nuxt-loading-indicator) to add a progress bar between page navigations. You can add the [`<NuxtLoadingIndicator>`](/docs/api/components/nuxt-loading-indicator) to add a progress bar between page navigations.
:: ::
## `$fetch`
Nuxt includes the [ofetch](https://github.com/unjs/ofetch) library, and is auto-imported as the `$fetch` alias globally across your application.
```vue twoslash [pages/todos.vue]
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// My todo data
}
})
}
</script>
```
::warning
Beware that using only `$fetch` will not provide [network calls de-duplication and navigation prevention](#the-need-for-usefetch-and-useasyncdata). :br
It is recommended to use `$fetch` for client-side interactions (event based) or combined with [`useAsyncData`](#useasyncdata) when fetching the initial component data.
::
::read-more{to="/docs/api/utils/dollarfetch"}
Read more about `$fetch`.
::
## `useFetch` ## `useFetch`
The [`useFetch`](/docs/api/composables/use-fetch) composable is the most straightforward way to perform data fetching. The [`useFetch`](/docs/api/composables/use-fetch) composable uses `$fetch` under-the-hood to make SSR-safe network calls in the setup function.
```vue twoslash [app.vue] ```vue twoslash [app.vue]
<script setup lang="ts"> <script setup lang="ts">
@ -62,32 +112,6 @@ Watch the video from Alexander Lichter to avoid using `useFetch` the wrong way!
:link-example{to="/docs/examples/features/data-fetching"} :link-example{to="/docs/examples/features/data-fetching"}
## `$fetch`
Nuxt includes the [ofetch](https://github.com/unjs/ofetch) library, and is auto-imported as the `$fetch` alias globally across your application. It's what `useFetch` uses behind the scenes.
```vue twoslash [pages/todos.vue]
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// My todo data
}
})
}
</script>
```
::warning
Beware that using only `$fetch` will not provide [network calls de-duplication and navigation prevention](#why-use-specific-composables-for-data-fetching). :br
It is recommended to use `$fetch` for client-side interactions (event based) or combined with [`useAsyncData`](#useasyncdata) when fetching the initial component data.
::
::read-more{to="/docs/api/utils/dollarfetch"}
Read more about `$fetch`.
::
## `useAsyncData` ## `useAsyncData`
The `useAsyncData` composable is responsible for wrapping async logic and returning the result once it is resolved. The `useAsyncData` composable is responsible for wrapping async logic and returning the result once it is resolved.

View File

@ -11,18 +11,41 @@ By default, Nuxt uses **universal rendering** to provide better user experience,
## Universal Rendering ## Universal Rendering
When the browser requests a URL with universal (server-side + client-side) rendering enabled, the server returns a fully rendered HTML page to the browser. Whether the page has been generated in advance and cached or is rendered on the fly, at some point, Nuxt has run the JavaScript (Vue.js) code in a server environment, producing an HTML document. Users immediately get the content of our application, contrary to client-side rendering. This step is similar to traditional **server-side rendering** performed by PHP or Ruby applications. This step is similar to traditional **server-side rendering** performed by PHP or Ruby applications. When the browser requests a URL with universal rendering enabled, Nuxt runs the JavaScript (Vue.js) code in a server environment and returns a fully rendered HTML page to the browser. Nuxt may also return a fully rendered HTML page from a cache if the page was generated in advance. Users immediately get the entirety of the initial content of the application, contrary to client-side rendering.
To not lose the benefits of the client-side rendering method, such as dynamic interfaces and pages transitions, the Client (browser) loads the JavaScript code that runs on the Server in the background once the HTML document has been downloaded. The browser interprets it again (hence **Universal rendering**) and Vue.js takes control of the document and enables interactivity. Once the HTML document has been downloaded, the browser interprets this and Vue.js takes control of the document. The same JavaScript code that once ran on the server runs on the client (browser) **again** in the background now enabling interactivity (hence **Universal rendering**) by binding its listeners to the HTML. This is called **Hydration**. When hydration is complete, the page can enjoy benefits such as dynamic interfaces and page transitions.
Making a static page interactive in the browser is called "Hydration".
Universal rendering allows a Nuxt application to provide quick page load times while preserving the benefits of client-side rendering. Furthermore, as the content is already present in the HTML document, crawlers can index it without overhead. Universal rendering allows a Nuxt application to provide quick page load times while preserving the benefits of client-side rendering. Furthermore, as the content is already present in the HTML document, crawlers can index it without overhead.
![Users can access the static content when the HTML document is loaded. Hydration then allows page's interactivity](/assets/docs/concepts/rendering/ssr.svg) ![Users can access the static content when the HTML document is loaded. Hydration then allows page's interactivity](/assets/docs/concepts/rendering/ssr.svg)
**What's server-rendered and what's client-rendered?**
It is normal to ask which parts of a Vue file runs on the server and/or the client in universal rendering mode.
```vue [app.vue]
<script setup lang="ts">
const counter = ref(0); // executes in server and client environments
const handleClick = () => {
counter.value++; // executes only in a client environment
};
</script>
<template>
<div>
<p>Count: {{ counter }}</p>
<button @click="handleClick">Increment</button>
</div>
</template>
```
On the initial request, the `counter` ref is initialized in the server since it is rendered inside the `<p>` tag. The contents of `handleClick` is never executed here. During hydration in the browser, the `counter` ref is re-initialized. The `handleClick` finally binds itself to the button; Therefore it is reasonable to deduce that the body of `handleClick` will always run in a browser environment.
[Middlewares](/docs/guide/directory-structure/middleware) and [pages](/docs/guide/directory-structure/pages) run in the server and on the client during hydration. [Plugins](/docs/guide/directory-structure/plugins) can be rendered on the server or client or both. [Components](/docs/guide/directory-structure/components) can be forced to run on the client only as well. [Composables](/docs/guide/directory-structure/composables) and [utilities](/docs/guide/directory-structure/utils) are rendered based on the context of their usage.
**Benefits of server-side rendering:** **Benefits of server-side rendering:**
- **Performance**: Users can get immediate access to the page's content because browsers can display static content much faster than JavaScript-generated content. At the same time, Nuxt preserves the interactivity of a web application when the hydration process happens. - **Performance**: Users can get immediate access to the page's content because browsers can display static content much faster than JavaScript-generated content. At the same time, Nuxt preserves the interactivity of a web application during the hydration process.
- **Search Engine Optimization**: Universal rendering delivers the entire HTML content of the page to the browser as a classic server application. Web crawlers can directly index the page's content, which makes Universal rendering a great choice for any content that you want to index quickly. - **Search Engine Optimization**: Universal rendering delivers the entire HTML content of the page to the browser as a classic server application. Web crawlers can directly index the page's content, which makes Universal rendering a great choice for any content that you want to index quickly.
**Downsides of server-side rendering:** **Downsides of server-side rendering:**

View File

@ -8,9 +8,9 @@ navigation.icon: i-ph-folder
Nuxt automatically imports any components in this directory (along with components that are registered by any modules you may be using). Nuxt automatically imports any components in this directory (along with components that are registered by any modules you may be using).
```bash [Directory Structure] ```bash [Directory Structure]
| components/ -| components/
--| AppHeader.vue ---| AppHeader.vue
--| AppFooter.vue ---| AppFooter.vue
``` ```
```html [app.vue] ```html [app.vue]
@ -28,10 +28,10 @@ Nuxt automatically imports any components in this directory (along with componen
If you have a component in nested directories such as: If you have a component in nested directories such as:
```bash [Directory Structure] ```bash [Directory Structure]
| components/ -| components/
--| base/ ---| base/
----| foo/ -----| foo/
------| Button.vue -------| Button.vue
``` ```
... then the component's name will be based on its own path directory and filename, with duplicate segments being removed. Therefore, the component's name will be: ... then the component's name will be based on its own path directory and filename, with duplicate segments being removed. Therefore, the component's name will be:
@ -285,8 +285,8 @@ export default defineNuxtConfig({
Now you can register server-only components with the `.server` suffix and use them anywhere in your application automatically. Now you can register server-only components with the `.server` suffix and use them anywhere in your application automatically.
```bash [Directory Structure] ```bash [Directory Structure]
| components/ -| components/
--| HighlightedMarkdown.server.vue ---| HighlightedMarkdown.server.vue
``` ```
```vue [pages/example.vue] ```vue [pages/example.vue]
@ -359,9 +359,9 @@ Slots can be interactive and are wrapped within a `<div>` with `display: content
In this case, the `.server` + `.client` components are two 'halves' of a component and can be used in advanced use cases for separate implementations of a component on server and client side. In this case, the `.server` + `.client` components are two 'halves' of a component and can be used in advanced use cases for separate implementations of a component on server and client side.
```bash [Directory Structure] ```bash [Directory Structure]
| components/ -| components/
--| Comments.client.vue ---| Comments.client.vue
--| Comments.server.vue ---| Comments.server.vue
``` ```
```vue [pages/example.vue] ```vue [pages/example.vue]
@ -389,15 +389,15 @@ You can use the `components:dirs` hook to extend the directory list without requ
Imagine a directory structure like this: Imagine a directory structure like this:
```bash [Directory Structure] ```bash [Directory Structure]
| node_modules/ -| node_modules/
---| awesome-ui/ ---| awesome-ui/
------| components/ -----| components/
---------| Alert.vue -------| Alert.vue
---------| Button.vue -------| Button.vue
------| nuxt.js -----| nuxt.js
| pages/ -| pages/
---| index.vue ---| index.vue
| nuxt.config.js -| nuxt.config.js
``` ```
Then in `awesome-ui/nuxt.js` you can use the `components:dirs` hook: Then in `awesome-ui/nuxt.js` you can use the `components:dirs` hook:

View File

@ -85,11 +85,11 @@ export const useHello = () => {
Nuxt only scans files at the top level of the [`composables/` directory](/docs/guide/directory-structure/composables), e.g.: Nuxt only scans files at the top level of the [`composables/` directory](/docs/guide/directory-structure/composables), e.g.:
```bash [Directory Structure] ```bash [Directory Structure]
| composables/ -| composables/
---| index.ts // scanned ---| index.ts // scanned
---| useFoo.ts // scanned ---| useFoo.ts // scanned
-----| nested/ ---| nested/
-------| utils.ts // not scanned -----| utils.ts // not scanned
``` ```
Only `composables/index.ts` and `composables/useFoo.ts` would be searched for imports. Only `composables/index.ts` and `composables/useFoo.ts` would be searched for imports.

View File

@ -72,11 +72,11 @@ Middleware runs in the following order:
For example, assuming you have the following middleware and component: For example, assuming you have the following middleware and component:
```text [middleware/ directory] ```bash [middleware/ directory]
middleware/ -| middleware/
--| analytics.global.ts ---| analytics.global.ts
--| setup.global.ts ---| setup.global.ts
--| auth.ts ---| auth.ts
``` ```
```vue twoslash [pages/profile.vue] ```vue twoslash [pages/profile.vue]
@ -105,11 +105,11 @@ By default, global middleware is executed alphabetically based on the filename.
However, there may be times you want to define a specific order. For example, in the last scenario, `setup.global.ts` may need to run before `analytics.global.ts`. In that case, we recommend prefixing global middleware with 'alphabetical' numbering. However, there may be times you want to define a specific order. For example, in the last scenario, `setup.global.ts` may need to run before `analytics.global.ts`. In that case, we recommend prefixing global middleware with 'alphabetical' numbering.
```text [Directory structure] ```bash [Directory structure]
middleware/ -| middleware/
--| 01.setup.global.ts ---| 01.setup.global.ts
--| 02.analytics.global.ts ---| 02.analytics.global.ts
--| auth.ts ---| auth.ts
``` ```
::note ::note

View File

@ -159,7 +159,7 @@ Example:
```bash [Directory Structure] ```bash [Directory Structure]
-| pages/ -| pages/
---| parent/ ---| parent/
------| child.vue -----| child.vue
---| parent.vue ---| parent.vue
``` ```
@ -408,7 +408,7 @@ However, you can use [Nuxt Layers](/docs/getting-started/layers) to create group
```bash [Directory Structure] ```bash [Directory Structure]
-| some-app/ -| some-app/
---| nuxt.config.ts ---| nuxt.config.ts
---| pages ---| pages/
-----| app-page.vue -----| app-page.vue
-| nuxt.config.ts -| nuxt.config.ts
``` ```

View File

@ -108,7 +108,7 @@ In case you're new to 'alphabetical' numbering, remember that filenames are sort
### Parallel Plugins ### Parallel Plugins
By default, Nuxt loads plugins sequentially. You can define a plugin as `parallel` so Nuxt won't wait the end of the plugin's execution before loading the next plugin. By default, Nuxt loads plugins sequentially. You can define a plugin as `parallel` so Nuxt won't wait until the end of the plugin's execution before loading the next plugin.
```ts twoslash [plugins/my-plugin.ts] ```ts twoslash [plugins/my-plugin.ts]
export default defineNuxtPlugin({ export default defineNuxtPlugin({

View File

@ -19,6 +19,10 @@ export default defineAppConfig({
Do not put any secret values inside `app.config` file. It is exposed to the user client bundle. Do not put any secret values inside `app.config` file. It is exposed to the user client bundle.
:: ::
::note
When configuring a custom [`srcDir`](/docs/api/nuxt-config#srcdir), make sure to place the `app.config` file at the root of the new `srcDir` path.
::
## Usage ## Usage
To expose config and environment variables to the rest of your app, you will need to define configuration in `app.config` file. To expose config and environment variables to the rest of your app, you will need to define configuration in `app.config` file.
@ -31,7 +35,7 @@ export default defineAppConfig({
}) })
``` ```
When adding `theme` to the `app.config`, Nuxt uses Vite or webpack to bundle the code. We can universally access `theme` both when server-rendering the page and in the browser using [`useAppConfig`](/docs/api/composables/use-app-config) composable. We can now universally access `theme` both when server-rendering the page and in the browser using [`useAppConfig`](/docs/api/composables/use-app-config) composable.
```vue [pages/index.vue] ```vue [pages/index.vue]
<script setup lang="ts"> <script setup lang="ts">
@ -41,7 +45,23 @@ console.log(appConfig.theme)
</script> </script>
``` ```
When configuring a custom [`srcDir`](/docs/api/nuxt-config#srcdir), make sure to place the `app.config` file at the root of the new `srcDir` path. The [`updateAppConfig`](/docs/api/utils/update-app-config) utility can be used to update the `app.config` at runtime.
```vue [pages/index.vue]
<script setup>
const appConfig = useAppConfig() // { foo: 'bar' }
const newAppConfig = { foo: 'baz' }
updateAppConfig(newAppConfig)
console.log(appConfig) // { foo: 'baz' }
</script>
```
::read-more{to="/docs/api/utils/update-app-config"}
Read more about the `updateAppConfig` utility.
::
## Typing App Config ## Typing App Config

View File

@ -334,6 +334,8 @@ This option allows exposing some route metadata defined in `definePageMeta` at b
This only works with static or strings/arrays rather than variables or conditional assignment. See [original issue](https://github.com/nuxt/nuxt/issues/24770) for more information and context. This only works with static or strings/arrays rather than variables or conditional assignment. See [original issue](https://github.com/nuxt/nuxt/issues/24770) for more information and context.
It is also possible to scan page metadata only after all routes have been registered in `pages:extend`. Then another hook, `pages:resolved` will be called. To enable this behavior, set `scanPageMeta: 'after-resolve'`.
You can disable this feature if it causes issues in your project. You can disable this feature if it causes issues in your project.
```ts twoslash [nuxt.config.ts] ```ts twoslash [nuxt.config.ts]

View File

@ -61,6 +61,7 @@ export default defineNuxtConfig({
app: 'app' app: 'app'
}, },
experimental: { experimental: {
scanPageMeta: 'after-resolve',
sharedPrerenderData: false, sharedPrerenderData: false,
compileTemplate: true, compileTemplate: true,
resetAsyncDataToUndefined: true, resetAsyncDataToUndefined: true,

View File

@ -31,14 +31,8 @@ export default defineNuxtPlugin((nuxtApp) => {
baseURL: 'https://api.nuxt.com', baseURL: 'https://api.nuxt.com',
onRequest({ request, options, error }) { onRequest({ request, options, error }) {
if (session.value?.token) { if (session.value?.token) {
const headers = options.headers ||= {} // note that this relies on ofetch >= 1.4.0 - you may need to refresh your lockfile
if (Array.isArray(headers)) { options.headers.set('Authorization', `Bearer ${session.value?.token}`)
headers.push(['Authorization', `Bearer ${session.value?.token}`])
} else if (headers instanceof Headers) {
headers.set('Authorization', `Bearer ${session.value?.token}`)
} else {
headers.Authorization = `Bearer ${session.value?.token}`
}
} }
}, },
async onResponseError({ response }) { async onResponseError({ response }) {
@ -96,6 +90,28 @@ const { data: modules } = await useAPI('/modules')
</script> </script>
``` ```
If you want to customize the type of any error returned, you can also do so:
```ts
import type { FetchError } from 'ofetch'
import type { UseFetchOptions } from 'nuxt/app'
interface CustomError {
message: string
statusCode: number
}
export function useAPI<T>(
url: string | (() => string),
options?: UseFetchOptions<T>,
) {
return useFetch<T, FetchError<CustomError>>(url, {
...options,
$fetch: useNuxtApp().$api
})
}
```
::note ::note
This example demonstrates how to use a custom `useFetch`, but the same structure is identical for a custom `useAsyncData`. This example demonstrates how to use a custom `useFetch`, but the same structure is identical for a custom `useAsyncData`.
:: ::

View File

@ -50,8 +50,8 @@ You can also use [interceptors](https://github.com/unjs/ofetch#%EF%B8%8F-interce
const { data, status, error, refresh, clear } = await useFetch('/api/auth/login', { const { data, status, error, refresh, clear } = await useFetch('/api/auth/login', {
onRequest({ request, options }) { onRequest({ request, options }) {
// Set the request headers // Set the request headers
options.headers = options.headers || {} // note that this relies on ofetch >= 1.4.0 - you may need to refresh your lockfile
options.headers.authorization = '...' options.headers.set('Authorization', '...')
}, },
onRequestError({ request, options, error }) { onRequestError({ request, options, error }) {
// Handle the request errors // Handle the request errors

View File

@ -0,0 +1,48 @@
---
title: "useResponseHeader"
description: "Use useResponseHeader to set a server response header."
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/ssr.ts
size: xs
---
::important
This composable is available in Nuxt v3.14+.
::
You can use the built-in [`useResponseHeader`](/docs/api/composables/use-response-header) composable to set any server response header within your pages, components, and plugins.
```ts
// Set the a custom response header
const header = useResponseHeader('X-My-Header');
header.value = 'my-value';
```
## Example
We can use `useResponseHeader` to easily set a response header on a per-page basis.
```vue [pages/test.vue]
<script setup>
// pages/test.vue
const header = useResponseHeader('X-My-Header');
header.value = 'my-value';
</script>
<template>
<h1>Test page with custom header</h1>
<p>The response from the server for this "/test" page will have a custom "X-My-Header" header.</p>
</template>
```
We can use `useResponseHeader` for example in Nuxt [middleware](/docs/guide/directory-structure/middleware) to set a response header for all pages.
```ts [middleware/my-header-middleware.ts]
export default defineNuxtRouteMiddleware((to, from) => {
const header = useResponseHeader('X-My-Always-Header');
header.value = `I'm Always here!`;
});
```

View File

@ -30,6 +30,7 @@ interface PageMeta {
redirect?: RouteRecordRedirectOption redirect?: RouteRecordRedirectOption
name?: string name?: string
path?: string path?: string
props?: RouteRecordRaw['props']
alias?: string | string[] alias?: string | string[]
pageTransition?: boolean | TransitionProps pageTransition?: boolean | TransitionProps
layoutTransition?: boolean | TransitionProps layoutTransition?: boolean | TransitionProps
@ -63,6 +64,12 @@ interface PageMeta {
You may define a [custom regular expression](#using-a-custom-regular-expression) if you have a more complex pattern than can be expressed with the file name. You may define a [custom regular expression](#using-a-custom-regular-expression) if you have a more complex pattern than can be expressed with the file name.
**`props`**
- **Type**: [`RouteRecordRaw['props']`](https://router.vuejs.org/guide/essentials/passing-props)
Allows accessing the route `params` as props passed to the page component.
**`alias`** **`alias`**
- **Type**: `string | string[]` - **Type**: `string | string[]`

View File

@ -125,6 +125,19 @@ Make sure to always use `await` or `return` on result of `navigateTo` when calli
`to` can be a plain string or a route object to redirect to. When passed as `undefined` or `null`, it will default to `'/'`. `to` can be a plain string or a route object to redirect to. When passed as `undefined` or `null`, it will default to `'/'`.
#### Example
```ts
// Passing the URL directly will redirect to the '/blog' page
await navigateTo('/blog')
// Using the route object, will redirect to the route with the name 'blog'
await navigateTo({ name: 'blog' })
// Redirects to the 'product' route while passing a parameter (id = 1) using the route object.
await navigateTo({ name: 'product', params: { id: 1 } })
```
### `options` (optional) ### `options` (optional)
**Type**: `NavigateToOptions` **Type**: `NavigateToOptions`

View File

@ -3,8 +3,6 @@
import { addPluginTemplate, addRouteMiddleware } from 'nuxt/kit' import { addPluginTemplate, addRouteMiddleware } from 'nuxt/kit'
export default defineNuxtConfig({ export default defineNuxtConfig({
typescript: { shim: process.env.DOCS_TYPECHECK === 'true' },
pages: process.env.DOCS_TYPECHECK === 'true',
modules: [ modules: [
function () { function () {
if (!process.env.DOCS_TYPECHECK) { return } if (!process.env.DOCS_TYPECHECK) { return }
@ -18,4 +16,6 @@ export default defineNuxtConfig({
}) })
}, },
], ],
pages: process.env.DOCS_TYPECHECK === 'true',
typescript: { shim: process.env.DOCS_TYPECHECK === 'true' },
}) })

View File

@ -35,44 +35,46 @@
}, },
"resolutions": { "resolutions": {
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/ui-templates": "workspace:*", "@nuxt/ui-templates": "workspace:*",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@types/node": "20.16.10", "@types/node": "20.17.0",
"@vue/compiler-core": "3.5.10", "@vue/compiler-core": "3.5.12",
"@vue/compiler-dom": "3.5.10", "@vue/compiler-dom": "3.5.12",
"@vue/shared": "3.5.10", "@vue/shared": "3.5.12",
"c12": "2.0.0-beta.3", "c12": "2.0.1",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "2.0.0", "jiti": "2.3.3",
"magic-string": "^0.30.11", "magic-string": "^0.30.12",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"ohash": "1.1.4", "ohash": "1.1.4",
"postcss": "8.4.47", "postcss": "8.4.47",
"rollup": "4.22.5", "rollup": "4.24.0",
"send": ">=0.19.0", "send": ">=1.1.0",
"typescript": "5.6.2", "typescript": "5.6.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"unbuild": "3.0.0-rc.8", "unbuild": "3.0.0-rc.11",
"vite": "5.4.8", "vite": "5.4.10",
"vue": "3.5.10" "vue": "3.5.12"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.11.1", "@eslint/js": "9.13.0",
"@nuxt/eslint-config": "0.5.7", "@nuxt/eslint-config": "0.6.0",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/test-utils": "3.14.2", "@nuxt/rspack-builder": "workspace:*",
"@nuxt/test-utils": "3.14.4",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/eslint__js": "8.42.3", "@types/eslint__js": "8.42.3",
"@types/node": "20.16.10", "@types/node": "20.17.0",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@unhead/schema": "1.11.6", "@unhead/schema": "1.11.10",
"@unhead/vue": "1.11.6", "@unhead/vue": "1.11.10",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",
"@vitest/coverage-v8": "2.1.1", "@vitest/coverage-v8": "2.1.3",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"case-police": "0.7.0", "case-police": "0.7.0",
@ -81,36 +83,36 @@
"cssnano": "7.0.6", "cssnano": "7.0.6",
"destr": "2.0.3", "destr": "2.0.3",
"devalue": "5.1.1", "devalue": "5.1.1",
"eslint": "9.11.1", "eslint": "9.13.0",
"eslint-plugin-no-only-tests": "3.3.0", "eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "3.7.0", "eslint-plugin-perfectionist": "3.9.1",
"eslint-typegen": "0.3.2", "eslint-typegen": "0.3.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"happy-dom": "15.7.4", "happy-dom": "15.7.4",
"jiti": "2.0.0", "jiti": "2.3.3",
"markdownlint-cli": "0.42.0", "markdownlint-cli": "0.42.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxi": "3.14.0", "nuxi": "3.15.0",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.1", "nuxt-content-twoslash": "0.1.1",
"ofetch": "1.4.0", "ofetch": "1.4.1",
"pathe": "1.1.2", "pathe": "1.1.2",
"playwright-core": "1.47.2", "playwright-core": "1.48.1",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"semver": "7.6.3", "semver": "7.6.3",
"sherif": "1.0.0", "sherif": "1.0.1",
"std-env": "3.7.0", "std-env": "3.7.0",
"tinyexec": "0.3.0", "tinyexec": "0.3.1",
"tinyglobby": "0.2.6", "tinyglobby": "0.2.9",
"typescript": "5.6.2", "typescript": "5.6.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"vitest": "2.1.1", "vitest": "2.1.3",
"vitest-environment-nuxt": "1.0.1", "vitest-environment-nuxt": "1.0.1",
"vue": "3.5.10", "vue": "3.5.12",
"vue-router": "4.4.5", "vue-router": "4.4.5",
"vue-tsc": "2.1.6" "vue-tsc": "2.1.6"
}, },
"packageManager": "pnpm@9.11.0", "packageManager": "pnpm@9.12.2",
"engines": { "engines": {
"node": "^16.10.0 || >=18.0.0" "node": "^16.10.0 || >=18.0.0"
}, },

View File

@ -6,6 +6,7 @@ export default defineBuildConfig({
'src/index', 'src/index',
], ],
externals: [ externals: [
'@rspack/core',
'@nuxt/schema', '@nuxt/schema',
'nitropack', 'nitropack',
'nitro', 'nitro',

View File

@ -27,7 +27,7 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"c12": "^2.0.0-beta.3", "c12": "^2.0.1",
"consola": "^3.2.3", "consola": "^3.2.3",
"defu": "^6.1.4", "defu": "^6.1.4",
"destr": "^2.0.3", "destr": "^2.0.3",
@ -35,25 +35,26 @@
"globby": "^14.0.2", "globby": "^14.0.2",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"ignore": "^6.0.2", "ignore": "^6.0.2",
"jiti": "^2.0.0", "jiti": "^2.3.3",
"klona": "^2.0.6", "klona": "^2.0.6",
"mlly": "^1.7.1", "mlly": "^1.7.2",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"pkg-types": "^1.2.0", "pkg-types": "^1.2.1",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unctx": "^2.3.1", "unctx": "^2.3.1",
"unimport": "^3.13.1", "unimport": "^3.13.1",
"untyped": "^1.5.0" "untyped": "^1.5.1"
}, },
"devDependencies": { "devDependencies": {
"@rspack/core": "1.0.14",
"@types/hash-sum": "1.0.2", "@types/hash-sum": "1.0.2",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"unbuild": "3.0.0-rc.8", "unbuild": "3.0.0-rc.11",
"vite": "5.4.8", "vite": "5.4.10",
"vitest": "2.1.1", "vitest": "2.1.3",
"webpack": "5.95.0" "webpack": "5.95.0"
}, },
"engines": { "engines": {

View File

@ -1,4 +1,5 @@
import type { Configuration as WebpackConfig, WebpackPluginInstance } from 'webpack' import type { Configuration as WebpackConfig, WebpackPluginInstance } from 'webpack'
import type { RspackPluginInstance } from '@rspack/core'
import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite' import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite'
import { useNuxt } from './context' import { useNuxt } from './context'
import { toArray } from './utils' import { toArray } from './utils'
@ -36,16 +37,7 @@ export interface ExtendWebpackConfigOptions extends ExtendConfigOptions {}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type // eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ExtendViteConfigOptions extends ExtendConfigOptions {} export interface ExtendViteConfigOptions extends ExtendConfigOptions {}
/** const extendWebpackCompatibleConfig = (builder: 'rspack' | 'webpack') => (fn: ((config: WebpackConfig) => void), options: ExtendWebpackConfigOptions = {}) => {
* Extend webpack config
*
* The fallback function might be called multiple times
* when applying to both client and server builds.
*/
export function extendWebpackConfig (
fn: ((config: WebpackConfig) => void),
options: ExtendWebpackConfigOptions = {},
) {
const nuxt = useNuxt() const nuxt = useNuxt()
if (options.dev === false && nuxt.options.dev) { if (options.dev === false && nuxt.options.dev) {
@ -55,7 +47,7 @@ export function extendWebpackConfig (
return return
} }
nuxt.hook('webpack:config', (configs: WebpackConfig[]) => { nuxt.hook(`${builder}:config`, (configs) => {
if (options.server !== false) { if (options.server !== false) {
const config = configs.find(i => i.name === 'server') const config = configs.find(i => i.name === 'server')
if (config) { if (config) {
@ -71,13 +63,25 @@ export function extendWebpackConfig (
}) })
} }
/**
* Extend webpack config
*
* The fallback function might be called multiple times
* when applying to both client and server builds.
*/
export const extendWebpackConfig = extendWebpackCompatibleConfig('webpack')
/**
* Extend rspack config
*
* The fallback function might be called multiple times
* when applying to both client and server builds.
*/
export const extendRspackConfig = extendWebpackCompatibleConfig('rspack')
/** /**
* Extend Vite config * Extend Vite config
*/ */
export function extendViteConfig ( export function extendViteConfig (fn: ((config: ViteConfig) => void), options: ExtendViteConfigOptions = {}) {
fn: ((config: ViteConfig) => void),
options: ExtendViteConfigOptions = {},
) {
const nuxt = useNuxt() const nuxt = useNuxt()
if (options.dev === false && nuxt.options.dev) { if (options.dev === false && nuxt.options.dev) {
@ -114,6 +118,18 @@ export function addWebpackPlugin (pluginOrGetter: WebpackPluginInstance | Webpac
config.plugins[method](...toArray(plugin)) config.plugins[method](...toArray(plugin))
}, options) }, options)
} }
/**
* Append rspack plugin to the config.
*/
export function addRspackPlugin (pluginOrGetter: RspackPluginInstance | RspackPluginInstance[] | (() => RspackPluginInstance | RspackPluginInstance[]), options?: ExtendWebpackConfigOptions) {
extendRspackConfig((config) => {
const method: 'push' | 'unshift' = options?.prepend ? 'unshift' : 'push'
const plugin = typeof pluginOrGetter === 'function' ? pluginOrGetter() : pluginOrGetter
config.plugins = config.plugins || []
config.plugins[method](...toArray(plugin))
}, options)
}
/** /**
* Append Vite plugin to the config. * Append Vite plugin to the config.
@ -131,6 +147,7 @@ export function addVitePlugin (pluginOrGetter: VitePlugin | VitePlugin[] | (() =
interface AddBuildPluginFactory { interface AddBuildPluginFactory {
vite?: () => VitePlugin | VitePlugin[] vite?: () => VitePlugin | VitePlugin[]
webpack?: () => WebpackPluginInstance | WebpackPluginInstance[] webpack?: () => WebpackPluginInstance | WebpackPluginInstance[]
rspack?: () => RspackPluginInstance | RspackPluginInstance[]
} }
export function addBuildPlugin (pluginFactory: AddBuildPluginFactory, options?: ExtendConfigOptions) { export function addBuildPlugin (pluginFactory: AddBuildPluginFactory, options?: ExtendConfigOptions) {
@ -141,4 +158,8 @@ export function addBuildPlugin (pluginFactory: AddBuildPluginFactory, options?:
if (pluginFactory.webpack) { if (pluginFactory.webpack) {
addWebpackPlugin(pluginFactory.webpack, options) addWebpackPlugin(pluginFactory.webpack, options)
} }
if (pluginFactory.rspack) {
addRspackPlugin(pluginFactory.rspack, options)
}
} }

View File

@ -3,11 +3,13 @@ import { readPackageJSON } from 'pkg-types'
import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema' import type { Nuxt, NuxtCompatibility, NuxtCompatibilityIssues } from '@nuxt/schema'
import { useNuxt } from './context' import { useNuxt } from './context'
const SEMANTIC_VERSION_RE = /-\d+\.[0-9a-f]+/
export function normalizeSemanticVersion (version: string) { export function normalizeSemanticVersion (version: string) {
return version.replace(/-\d+\.[0-9a-f]+/, '') // Remove edge prefix return version.replace(SEMANTIC_VERSION_RE, '') // Remove edge prefix
} }
const builderMap = { const builderMap = {
'@nuxt/rspack-builder': 'rspack',
'@nuxt/vite-builder': 'vite', '@nuxt/vite-builder': 'vite',
'@nuxt/webpack-builder': 'webpack', '@nuxt/webpack-builder': 'webpack',
} }
@ -103,6 +105,7 @@ export function isNuxt3 (nuxt: Nuxt = useNuxt()) {
return isNuxtMajorVersion(3, nuxt) return isNuxtMajorVersion(3, nuxt)
} }
const NUXT_VERSION_RE = /^v/g
/** /**
* Get nuxt version * Get nuxt version
*/ */
@ -111,5 +114,5 @@ export function getNuxtVersion (nuxt: Nuxt | any = useNuxt() /* TODO: LegacyNuxt
if (typeof rawVersion !== 'string') { if (typeof rawVersion !== 'string') {
throw new TypeError('Cannot determine nuxt version! Is current instance passed?') throw new TypeError('Cannot determine nuxt version! Is current instance passed?')
} }
return rawVersion.replace(/^v/g, '') return rawVersion.replace(NUXT_VERSION_RE, '')
} }

View File

@ -3,6 +3,7 @@ import type { Component, ComponentsDir } from '@nuxt/schema'
import { useNuxt } from './context' import { useNuxt } from './context'
import { assertNuxtCompatibility } from './compatibility' import { assertNuxtCompatibility } from './compatibility'
import { logger } from './logger' import { logger } from './logger'
import { MODE_RE } from './utils'
/** /**
* Register a directory to be scanned for components and imported only when used. * Register a directory to be scanned for components and imported only when used.
@ -28,7 +29,7 @@ export async function addComponent (opts: AddComponentOptions) {
nuxt.options.components = nuxt.options.components || [] nuxt.options.components = nuxt.options.components || []
if (!opts.mode) { if (!opts.mode) {
const [, mode = 'all'] = opts.filePath.match(/\.(server|client)(\.\w+)*$/) || [] const [, mode = 'all'] = opts.filePath.match(MODE_RE) || []
opts.mode = mode as 'all' | 'client' | 'server' opts.mode = mode as 'all' | 'client' | 'server'
} }

View File

@ -13,7 +13,7 @@ export type { LoadNuxtOptions } from './loader/nuxt'
// Utils // Utils
export { addImports, addImportsDir, addImportsSources } from './imports' export { addImports, addImportsDir, addImportsSources } from './imports'
export { updateRuntimeConfig, useRuntimeConfig } from './runtime-config' export { updateRuntimeConfig, useRuntimeConfig } from './runtime-config'
export { addBuildPlugin, addVitePlugin, addWebpackPlugin, extendViteConfig, extendWebpackConfig } from './build' export { addBuildPlugin, addVitePlugin, addRspackPlugin, addWebpackPlugin, extendViteConfig, extendRspackConfig, extendWebpackConfig } from './build'
export type { ExtendConfigOptions, ExtendViteConfigOptions, ExtendWebpackConfigOptions } from './build' export type { ExtendConfigOptions, ExtendViteConfigOptions, ExtendWebpackConfigOptions } from './build'
export { assertNuxtCompatibility, checkNuxtCompatibility, getNuxtVersion, hasNuxtCompatibility, isNuxtMajorVersion, normalizeSemanticVersion, isNuxt2, isNuxt3 } from './compatibility' export { assertNuxtCompatibility, checkNuxtCompatibility, getNuxtVersion, hasNuxtCompatibility, isNuxtMajorVersion, normalizeSemanticVersion, isNuxt2, isNuxt3 } from './compatibility'
export { addComponent, addComponentsDir } from './components' export { addComponent, addComponentsDir } from './components'

View File

@ -5,10 +5,11 @@ import { useNuxt } from './context'
import { logger } from './logger' import { logger } from './logger'
import { addTemplate } from './template' import { addTemplate } from './template'
const LAYOUT_RE = /["']/g
export function addLayout (template: NuxtTemplate | string, name?: string) { export function addLayout (template: NuxtTemplate | string, name?: string) {
const nuxt = useNuxt() const nuxt = useNuxt()
const { filename, src } = addTemplate(template) const { filename, src } = addTemplate(template)
const layoutName = kebabCase(name || parse(filename).name).replace(/["']/g, '') const layoutName = kebabCase(name || parse(filename).name).replace(LAYOUT_RE, '')
// Nuxt 3 adds layouts on app // Nuxt 3 adds layouts on app
nuxt.hook('app:templates', (app) => { nuxt.hook('app:templates', (app) => {

View File

@ -72,10 +72,7 @@ export const normalizeModuleTranspilePath = (p: string) => {
export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, nuxt: Nuxt = useNuxt()) { export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, nuxt: Nuxt = useNuxt()) {
let buildTimeModuleMeta: ModuleMeta = {} let buildTimeModuleMeta: ModuleMeta = {}
const jiti = createJiti(nuxt.options.rootDir, { const jiti = createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias })
interopDefault: true,
alias: nuxt.options.alias,
})
// Import if input is string // Import if input is string
if (typeof nuxtModule === 'string') { if (typeof nuxtModule === 'string') {
@ -85,7 +82,7 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
for (const path of paths) { for (const path of paths) {
try { try {
const src = jiti.esmResolve(path, { parentURL: parentURL.replace(/\/node_modules\/?$/, '') }) const src = jiti.esmResolve(path, { parentURL: parentURL.replace(/\/node_modules\/?$/, '') })
nuxtModule = await jiti.import(src) as NuxtModule nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
// nuxt-module-builder generates a module.json with metadata including the version // nuxt-module-builder generates a module.json with metadata including the version
const moduleMetadataPath = join(dirname(src), 'module.json') const moduleMetadataPath = join(dirname(src), 'module.json')

View File

@ -4,13 +4,14 @@ import { normalize } from 'pathe'
import { useNuxt } from './context' import { useNuxt } from './context'
import { toArray } from './utils' import { toArray } from './utils'
const HANDLER_METHOD_RE = /\.(get|head|patch|post|put|delete|connect|options|trace)(\.\w+)*$/
/** /**
* normalize handler object * normalize handler object
* *
*/ */
function normalizeHandlerMethod (handler: NitroEventHandler) { function normalizeHandlerMethod (handler: NitroEventHandler) {
// retrieve method from handler file name // retrieve method from handler file name
const [, method = undefined] = handler.handler.match(/\.(get|head|patch|post|put|delete|connect|options|trace)(\.\w+)*$/) || [] const [, method = undefined] = handler.handler.match(HANDLER_METHOD_RE) || []
return { return {
method: method as 'get' | 'head' | 'patch' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | undefined, method: method as 'get' | 'head' | 'patch' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | undefined,
...handler, ...handler,

View File

@ -3,6 +3,7 @@ import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema'
import { useNuxt } from './context' import { useNuxt } from './context'
import { addTemplate } from './template' import { addTemplate } from './template'
import { resolveAlias } from './resolve' import { resolveAlias } from './resolve'
import { MODE_RE } from './utils'
/** /**
* Normalize a nuxt plugin object * Normalize a nuxt plugin object
@ -27,7 +28,7 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin.mode = 'server' plugin.mode = 'server'
} }
if (!plugin.mode) { if (!plugin.mode) {
const [, mode = 'all'] = plugin.src.match(/\.(server|client)(\.\w+)*$/) || [] const [, mode = 'all'] = plugin.src.match(MODE_RE) || []
plugin.mode = mode as 'all' | 'client' | 'server' plugin.mode = mode as 'all' | 'client' | 'server'
} }

View File

@ -1,7 +1,7 @@
import { existsSync, promises as fsp } from 'node:fs' import { existsSync, promises as fsp } from 'node:fs'
import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe' import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
import hash from 'hash-sum' import hash from 'hash-sum'
import type { Nuxt, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema' import type { Nuxt, NuxtServerTemplate, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema'
import { withTrailingSlash } from 'ufo' import { withTrailingSlash } from 'ufo'
import { defu } from 'defu' import { defu } from 'defu'
import type { TSConfig } from 'pkg-types' import type { TSConfig } from 'pkg-types'
@ -32,6 +32,18 @@ export function addTemplate<T> (_template: NuxtTemplate<T> | string) {
return template return template
} }
/**
* Adds a virtual file that can be used within the Nuxt Nitro server build.
*/
export function addServerTemplate (template: NuxtServerTemplate) {
const nuxt = useNuxt()
nuxt.options.nitro.virtual ||= {}
nuxt.options.nitro.virtual[template.filename] = template.getContents
return template
}
/** /**
* Renders given types using lodash template during build into the project buildDir * Renders given types using lodash template during build into the project buildDir
* and register them as types. * and register them as types.
@ -111,6 +123,9 @@ export async function updateTemplates (options?: { filter?: (template: ResolvedN
return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options) return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options)
} }
const EXTENSION_RE = /\b\.\w+$/g
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/, /^#internal\/nuxt/]
export async function _generateTypes (nuxt: Nuxt) { export async function _generateTypes (nuxt: Nuxt) {
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir) const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir) const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir)
@ -211,13 +226,7 @@ export async function _generateTypes (nuxt: Nuxt) {
exclude: [...exclude], exclude: [...exclude],
} satisfies TSConfig) } satisfies TSConfig)
const aliases: Record<string, string> = { const aliases: Record<string, string> = nuxt.options.alias
...nuxt.options.alias,
'#build': nuxt.options.buildDir,
}
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/]
const basePath = tsConfig.compilerOptions!.baseUrl const basePath = tsConfig.compilerOptions!.baseUrl
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
@ -251,7 +260,7 @@ export async function _generateTypes (nuxt: Nuxt) {
} else { } else {
const path = stats?.isFile() const path = stats?.isFile()
// remove extension // remove extension
? relativePath.replace(/\b\.\w+$/g, '') ? relativePath.replace(EXTENSION_RE, '')
// non-existent file probably shouldn't be resolved // non-existent file probably shouldn't be resolved
: aliases[alias]! : aliases[alias]!
@ -280,7 +289,7 @@ export async function _generateTypes (nuxt: Nuxt) {
tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => { tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => {
if (!isAbsolute(path)) { return path } if (!isAbsolute(path)) { return path }
const stats = await fsp.stat(path).catch(() => null /* file does not exist */) const stats = await fsp.stat(path).catch(() => null /* file does not exist */)
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/\b\.\w+$/g, '') /* remove extension */ : path) return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(EXTENSION_RE, '') /* remove extension */ : path)
})) }))
} }
@ -335,6 +344,7 @@ function renderAttr (key: string, value?: string) {
return value ? `${key}="${value}"` : '' return value ? `${key}="${value}"` : ''
} }
const RELATIVE_WITH_DOT_RE = /^([^.])/
function relativeWithDot (from: string, to: string) { function relativeWithDot (from: string, to: string) {
return relative(from, to).replace(/^([^.])/, './$1') || '.' return relative(from, to).replace(RELATIVE_WITH_DOT_RE, './$1') || '.'
} }

View File

@ -2,3 +2,5 @@
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]
} }
export const MODE_RE = /\.(server|client)(\.\w+)*$/

View File

@ -34,9 +34,6 @@ describe('tsConfig generation', () => {
const { tsConfig } = await _generateTypes(mockNuxt) const { tsConfig } = await _generateTypes(mockNuxt)
expect(tsConfig.compilerOptions?.paths).toMatchInlineSnapshot(` expect(tsConfig.compilerOptions?.paths).toMatchInlineSnapshot(`
{ {
"#build": [
".",
],
"some-custom-alias": [ "some-custom-alias": [
"../some-alias", "../some-alias",
], ],

View File

@ -60,19 +60,19 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/devalue": "^2.0.2", "@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.5.1", "@nuxt/devtools": "^1.6.0",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.0", "@nuxt/telemetry": "^2.6.0",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.11.6", "@unhead/dom": "^1.11.10",
"@unhead/shared": "^1.11.6", "@unhead/shared": "^1.11.10",
"@unhead/ssr": "^1.11.6", "@unhead/ssr": "^1.11.10",
"@unhead/vue": "^1.11.6", "@unhead/vue": "^1.11.10",
"@vue/shared": "^3.5.10", "@vue/shared": "^3.5.12",
"acorn": "8.12.1", "acorn": "8.13.0",
"c12": "^2.0.0-beta.3", "c12": "^2.0.1",
"chokidar": "^3.6.0", "chokidar": "^4.0.1",
"compatx": "^0.1.8", "compatx": "^0.1.8",
"consola": "^3.2.3", "consola": "^3.2.3",
"cookie-es": "^1.2.2", "cookie-es": "^1.2.2",
@ -87,53 +87,53 @@
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"ignore": "^6.0.2", "ignore": "^6.0.2",
"impound": "^0.1.0", "impound": "^0.2.0",
"jiti": "^2.0.0", "jiti": "^2.3.3",
"klona": "^2.0.6", "klona": "^2.0.6",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.12",
"mlly": "^1.7.1", "mlly": "^1.7.2",
"nanotar": "^0.1.1", "nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"nuxi": "^3.14.0", "nuxi": "^3.15.0",
"nypm": "^0.3.12", "nypm": "^0.3.12",
"ofetch": "^1.4.0", "ofetch": "^1.4.1",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"pkg-types": "^1.2.0", "pkg-types": "^1.2.1",
"radix3": "^1.1.2", "radix3": "^1.1.2",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"std-env": "^3.7.0", "std-env": "^3.7.0",
"strip-literal": "^2.1.0", "strip-literal": "^2.1.0",
"tinyglobby": "0.2.6", "tinyglobby": "0.2.9",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"ultrahtml": "^1.5.3", "ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
"unctx": "^2.3.1", "unctx": "^2.3.1",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unhead": "^1.11.6", "unhead": "^1.11.10",
"unimport": "^3.13.1", "unimport": "^3.13.1",
"unplugin": "^1.14.1", "unplugin": "^1.14.1",
"unplugin-vue-router": "^0.10.8", "unplugin-vue-router": "^0.10.8",
"unstorage": "^1.12.0", "unstorage": "^1.12.0",
"untyped": "^1.5.0", "untyped": "^1.5.1",
"vue": "^3.5.10", "vue": "^3.5.12",
"vue-bundle-renderer": "^2.1.1", "vue-bundle-renderer": "^2.1.1",
"vue-devtools-stub": "^0.1.0", "vue-devtools-stub": "^0.1.0",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/scripts": "0.9.4", "@nuxt/scripts": "0.9.5",
"@nuxt/ui-templates": "1.3.4", "@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1", "@parcel/watcher": "2.4.1",
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.10", "@vue/compiler-sfc": "3.5.12",
"unbuild": "3.0.0-rc.8", "unbuild": "3.0.0-rc.11",
"vite": "5.4.8", "vite": "5.4.10",
"vitest": "2.1.1" "vitest": "2.1.3"
}, },
"peerDependencies": { "peerDependencies": {
"@parcel/watcher": "^2.1.0", "@parcel/watcher": "^2.1.0",

View File

@ -1,9 +1,9 @@
import type { Component, PropType, VNode } from 'vue' import type { Component, PropType, VNode } from 'vue'
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue' import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch, withMemo } from 'vue'
import { debounce } from 'perfect-debounce' import { debounce } from 'perfect-debounce'
import { hash } from 'ohash' import { hash } from 'ohash'
import { appendResponseHeader } from 'h3' import { appendResponseHeader } from 'h3'
import { injectHead } from '@unhead/vue' import { type ActiveHeadEntry, type Head, injectHead } from '@unhead/vue'
import { randomUUID } from 'uncrypto' import { randomUUID } from 'uncrypto'
import { joinURL, withQuery } from 'ufo' import { joinURL, withQuery } from 'ufo'
import type { FetchResponse } from 'ofetch' import type { FetchResponse } from 'ofetch'
@ -22,6 +22,7 @@ const SSR_UID_RE = /data-island-uid="([^"]*)"/
const DATA_ISLAND_UID_RE = /data-island-uid(="")?(?!="[^"])/g const DATA_ISLAND_UID_RE = /data-island-uid(="")?(?!="[^"])/g
const SLOTNAME_RE = /data-island-slot="([^"]*)"/g const SLOTNAME_RE = /data-island-slot="([^"]*)"/g
const SLOT_FALLBACK_RE = / data-island-slot="([^"]*)"[^>]*>/g const SLOT_FALLBACK_RE = / data-island-slot="([^"]*)"[^>]*>/g
const ISLAND_SCOPE_ID_RE = /^<[^> ]*/
let id = 1 let id = 1
const getId = import.meta.client ? () => (id++).toString() : randomUUID const getId = import.meta.client ? () => (id++).toString() : randomUUID
@ -90,11 +91,13 @@ export default defineComponent({
const instance = getCurrentInstance()! const instance = getCurrentInstance()!
const event = useRequestEvent() const event = useRequestEvent()
let activeHead: ActiveHeadEntry<Head>
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved // TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
const eventFetch = import.meta.server ? event!.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch const eventFetch = import.meta.server ? event!.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch
const mounted = ref(false) const mounted = ref(false)
onMounted(() => { mounted.value = true; teleportKey.value++ }) onMounted(() => { mounted.value = true; teleportKey.value++ })
onBeforeUnmount(() => { if (activeHead) { activeHead.dispose() } })
function setPayload (key: string, result: NuxtIslandResponse) { function setPayload (key: string, result: NuxtIslandResponse) {
const toRevive: Partial<NuxtIslandResponse> = {} const toRevive: Partial<NuxtIslandResponse> = {}
if (result.props) { toRevive.props = result.props } if (result.props) { toRevive.props = result.props }
@ -140,7 +143,7 @@ export default defineComponent({
let html = ssrHTML.value let html = ssrHTML.value
if (props.scopeId) { if (props.scopeId) {
html = html.replace(/^<[^> ]*/, full => full + ' ' + props.scopeId) html = html.replace(ISLAND_SCOPE_ID_RE, full => full + ' ' + props.scopeId)
} }
if (import.meta.client && !canLoadClientComponent.value) { if (import.meta.client && !canLoadClientComponent.value) {
@ -215,6 +218,14 @@ export default defineComponent({
} }
} }
if (res?.head) {
if (activeHead) {
activeHead.patch(res.head)
} else {
activeHead = head.push(res.head)
}
}
if (import.meta.client) { if (import.meta.client) {
// must await next tick for Teleport to work correctly with static node re-rendering // must await next tick for Teleport to work correctly with static node re-rendering
nextTick(() => { nextTick(() => {
@ -250,14 +261,6 @@ export default defineComponent({
await loadComponents(props.source, payloads.components) await loadComponents(props.source, payloads.components)
} }
if (import.meta.server || nuxtApp.isHydrating) {
// re-push head into active head instance
const responseHead = (nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head
if (responseHead) {
head.push(responseHead)
}
}
return (_ctx: any, _cache: any) => { return (_ctx: any, _cache: any) => {
if (!html.value || error.value) { if (!html.value || error.value) {
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')] return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]

View File

@ -328,8 +328,9 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
const path = typeof to.value === 'string' const path = typeof to.value === 'string'
? to.value ? to.value
: isExternal.value ? resolveRouteObject(to.value) : router.resolve(to.value).fullPath : isExternal.value ? resolveRouteObject(to.value) : router.resolve(to.value).fullPath
const normalizedPath = isExternal.value ? new URL(path, window.location.href).href : path
await Promise.all([ await Promise.all([
nuxtApp.hooks.callHook('link:prefetch', path).catch(() => {}), nuxtApp.hooks.callHook('link:prefetch', normalizedPath).catch(() => {}),
!isExternal.value && !hasTarget.value && preloadRouteComponents(to.value as string, router).catch(() => {}), !isExternal.value && !hasTarget.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
]) ])
} }
@ -520,11 +521,12 @@ function useObserver (): { observe: ObserveFn } | undefined {
return _observer return _observer
} }
const IS_2G_RE = /2g/
function isSlowConnection () { function isSlowConnection () {
if (import.meta.server) { return } if (import.meta.server) { return }
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection
const cn = (navigator as any).connection as { saveData: boolean, effectiveType: string } | null const cn = (navigator as any).connection as { saveData: boolean, effectiveType: string } | null
if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true } if (cn && (cn.saveData || IS_2G_RE.test(cn.effectiveType))) { return true }
return false return false
} }

View File

@ -15,13 +15,16 @@ export const _wrapIf = (component: Component, props: any, slots: any) => {
return { default: () => props ? h(component, props, slots) : slots.default?.() } return { default: () => props ? h(component, props, slots) : slots.default?.() }
} }
const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g
const ROUTE_KEY_SYMBOLS_RE = /(:\w+)[?+*]/g
const ROUTE_KEY_NORMAL_RE = /:\w+/g
// TODO: consider refactoring into single utility // TODO: consider refactoring into single utility
// See https://github.com/nuxt/nuxt/tree/main/packages/nuxt/src/pages/runtime/utils.ts#L8-L19 // See https://github.com/nuxt/nuxt/tree/main/packages/nuxt/src/pages/runtime/utils.ts#L8-L19
function generateRouteKey (route: RouteLocationNormalized) { function generateRouteKey (route: RouteLocationNormalized) {
const source = route?.meta.key ?? route.path const source = route?.meta.key ?? route.path
.replace(/(:\w+)\([^)]+\)/g, '$1') .replace(ROUTE_KEY_PARENTHESES_RE, '$1')
.replace(/(:\w+)[?+*]/g, '$1') .replace(ROUTE_KEY_SYMBOLS_RE, '$1')
.replace(/:\w+/g, r => route.params[r.slice(1)]?.toString() || '') .replace(ROUTE_KEY_NORMAL_RE, r => route.params[r.slice(1)]?.toString() || '')
return typeof source === 'function' ? source(route) : source return typeof source === 'function' ? source(route) : source
} }

View File

@ -56,7 +56,6 @@ export const defineNuxtComponent: typeof defineComponent =
} }
if (options.head) { if (options.head) {
const nuxtApp = useNuxtApp()
useHead(typeof options.head === 'function' ? () => options.head(nuxtApp) : options.head) useHead(typeof options.head === 'function' ? () => options.head(nuxtApp) : options.head)
} }

View File

@ -24,7 +24,7 @@ export { useFetch, useLazyFetch } from './fetch'
export type { FetchResult, UseFetchOptions } from './fetch' export type { FetchResult, UseFetchOptions } from './fetch'
export { useCookie, refreshCookie } from './cookie' export { useCookie, refreshCookie } from './cookie'
export type { CookieOptions, CookieRef } from './cookie' export type { CookieOptions, CookieRef } from './cookie'
export { onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus } from './ssr' export { onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader } from './ssr'
export { onNuxtReady } from './ready' export { onNuxtReady } from './ready'
export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter } from './router' export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter } from './router'
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router' export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'

View File

@ -114,6 +114,7 @@ export interface NavigateToOptions {
open?: OpenOptions open?: OpenOptions
} }
const URL_QUOTE_RE = /"/g
/** @since 3.0.0 */ /** @since 3.0.0 */
export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: NavigateToOptions): Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw => { export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: NavigateToOptions): Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw => {
if (!to) { if (!to) {
@ -166,7 +167,7 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
const redirect = async function (response: any) { const redirect = async function (response: any) {
// TODO: consider deprecating in favour of `app:rendered` and removing // TODO: consider deprecating in favour of `app:rendered` and removing
await nuxtApp.callHook('app:redirected') await nuxtApp.callHook('app:redirected')
const encodedLoc = location.replace(/"/g, '%22') const encodedLoc = location.replace(URL_QUOTE_RE, '%22')
const encodedHeader = encodeURL(location, isExternalHost) const encodedHeader = encodeURL(location, isExternalHost)
nuxtApp.ssrContext!._renderResponse = { nuxtApp.ssrContext!._renderResponse = {

View File

@ -1,6 +1,6 @@
import type { H3Event } from 'h3' import type { H3Event } from 'h3'
import { setResponseStatus as _setResponseStatus, appendHeader, getRequestHeader, getRequestHeaders } from 'h3' import { setResponseStatus as _setResponseStatus, appendHeader, getRequestHeader, getRequestHeaders, getResponseHeader, removeResponseHeader, setResponseHeader } from 'h3'
import { getCurrentInstance } from 'vue' import { computed, getCurrentInstance, ref } from 'vue'
import { useServerHead } from '@unhead/vue' import { useServerHead } from '@unhead/vue'
import type { NuxtApp } from '../nuxt' import type { NuxtApp } from '../nuxt'
@ -61,6 +61,34 @@ export function setResponseStatus (arg1: H3Event | number | undefined, arg2?: nu
} }
} }
/** @since 3.14.0 */
export function useResponseHeader (header: string) {
if (import.meta.client) {
if (import.meta.dev) {
return computed({
get: () => undefined,
set: () => console.warn('[nuxt] Setting response headers is not supported in the browser.'),
})
}
return ref()
}
const event = useRequestEvent()!
return computed({
get () {
return getResponseHeader(event, header)
},
set (newValue) {
if (!newValue) {
return removeResponseHeader(event, header)
}
return setResponseHeader(event, header, newValue)
},
})
}
/** @since 3.8.0 */ /** @since 3.8.0 */
export function prerenderRoutes (path: string | string[]) { export function prerenderRoutes (path: string | string[]) {
if (!import.meta.server || !import.meta.prerender) { return } if (!import.meta.server || !import.meta.prerender) { return }

View File

@ -1,7 +1,7 @@
export { applyPlugin, applyPlugins, callWithNuxt, createNuxtApp, defineAppConfig, defineNuxtPlugin, definePayloadPlugin, isNuxtPlugin, registerPluginHooks, tryUseNuxtApp, useNuxtApp, useRuntimeConfig } from './nuxt' export { applyPlugin, applyPlugins, callWithNuxt, createNuxtApp, defineAppConfig, defineNuxtPlugin, definePayloadPlugin, isNuxtPlugin, registerPluginHooks, tryUseNuxtApp, useNuxtApp, useRuntimeConfig } from './nuxt'
export type { CreateOptions, NuxtApp, NuxtPayload, NuxtPluginIndicator, NuxtSSRContext, ObjectPlugin, Plugin, PluginEnvContext, PluginMeta, ResolvedPluginMeta, RuntimeNuxtHooks } from './nuxt' export type { CreateOptions, NuxtApp, NuxtPayload, NuxtPluginIndicator, NuxtSSRContext, ObjectPlugin, Plugin, PluginEnvContext, PluginMeta, ResolvedPluginMeta, RuntimeNuxtHooks } from './nuxt'
export { defineNuxtComponent, useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData, useHydration, callOnce, useState, clearNuxtState, clearError, createError, isNuxtError, showError, useError, useFetch, useLazyFetch, useCookie, refreshCookie, onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, onNuxtReady, abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter, preloadComponents, prefetchComponents, preloadRouteComponents, isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver, getAppManifest, getRouteRules, reloadNuxtApp, useRequestURL, usePreviewMode, useId, useRouteAnnouncer, useHead, useSeoMeta, useServerSeoMeta } from './composables/index' export { defineNuxtComponent, useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData, useHydration, callOnce, useState, clearNuxtState, clearError, createError, isNuxtError, showError, useError, useFetch, useLazyFetch, useCookie, refreshCookie, onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader, onNuxtReady, abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter, preloadComponents, prefetchComponents, preloadRouteComponents, isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver, getAppManifest, getRouteRules, reloadNuxtApp, useRequestURL, usePreviewMode, useId, useRouteAnnouncer, useHead, useSeoMeta, useServerSeoMeta } from './composables/index'
export type { AddRouteMiddlewareOptions, AsyncData, AsyncDataOptions, AsyncDataRequestStatus, CookieOptions, CookieRef, FetchResult, NuxtAppManifest, NuxtAppManifestMeta, NuxtError, ReloadNuxtAppOptions, RouteMiddleware, UseFetchOptions } from './composables/index' export type { AddRouteMiddlewareOptions, AsyncData, AsyncDataOptions, AsyncDataRequestStatus, CookieOptions, CookieRef, FetchResult, NuxtAppManifest, NuxtAppManifestMeta, NuxtError, ReloadNuxtAppOptions, RouteMiddleware, UseFetchOptions } from './composables/index'
export { defineNuxtLink } from './components/index' export { defineNuxtLink } from './components/index'

View File

@ -1,6 +1,6 @@
import { existsSync, statSync, writeFileSync } from 'node:fs' import { existsSync, statSync, writeFileSync } from 'node:fs'
import { isAbsolute, join, normalize, relative, resolve } from 'pathe' import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit' import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, findPath, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit'
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema' import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
import { distDir } from '../dirs' import { distDir } from '../dirs'
@ -16,11 +16,13 @@ import { ComponentNamePlugin } from './plugins/component-names'
const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string' const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch { return false } } const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch { return false } }
const SLASH_SEPARATOR_RE = /[\\/]/
function compareDirByPathLength ({ path: pathA }: { path: string }, { path: pathB }: { path: string }) { function compareDirByPathLength ({ path: pathA }: { path: string }, { path: pathB }: { path: string }) {
return pathB.split(/[\\/]/).filter(Boolean).length - pathA.split(/[\\/]/).filter(Boolean).length return pathB.split(SLASH_SEPARATOR_RE).filter(Boolean).length - pathA.split(SLASH_SEPARATOR_RE).filter(Boolean).length
} }
const DEFAULT_COMPONENTS_DIRS_RE = /\/components(?:\/(?:global|islands))?$/ const DEFAULT_COMPONENTS_DIRS_RE = /\/components(?:\/(?:global|islands))?$/
const STARTER_DOT_RE = /^\./g
export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[] export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]
@ -32,7 +34,7 @@ export default defineNuxtModule<ComponentsOptions>({
defaults: { defaults: {
dirs: [], dirs: [],
}, },
setup (componentOptions, nuxt) { async setup (componentOptions, nuxt) {
let componentDirs: ComponentsDir[] = [] let componentDirs: ComponentsDir[] = []
const context = { const context = {
components: [] as Component[], components: [] as Component[],
@ -89,7 +91,7 @@ export default defineNuxtModule<ComponentsOptions>({
const dirOptions: ComponentsDir = typeof dir === 'object' ? dir : { path: dir } const dirOptions: ComponentsDir = typeof dir === 'object' ? dir : { path: dir }
const dirPath = resolveAlias(dirOptions.path) const dirPath = resolveAlias(dirOptions.path)
const transpile = typeof dirOptions.transpile === 'boolean' ? dirOptions.transpile : 'auto' const transpile = typeof dirOptions.transpile === 'boolean' ? dirOptions.transpile : 'auto'
const extensions = (dirOptions.extensions || nuxt.options.extensions).map(e => e.replace(/^\./g, '')) const extensions = (dirOptions.extensions || nuxt.options.extensions).map(e => e.replace(STARTER_DOT_RE, ''))
const present = isDirectory(dirPath) const present = isDirectory(dirPath)
if (!present && !DEFAULT_COMPONENTS_DIRS_RE.test(dirOptions.path)) { if (!present && !DEFAULT_COMPONENTS_DIRS_RE.test(dirOptions.path)) {
@ -134,8 +136,9 @@ export default defineNuxtModule<ComponentsOptions>({
addTemplate(componentsMetadataTemplate) addTemplate(componentsMetadataTemplate)
} }
addBuildPlugin(TransformPlugin(nuxt, getComponents, 'server'), { server: true, client: false }) const serverComponentRuntime = await findPath(join(distDir, 'components/runtime/server-component')) ?? join(distDir, 'components/runtime/server-component')
addBuildPlugin(TransformPlugin(nuxt, getComponents, 'client'), { server: false, client: true }) addBuildPlugin(TransformPlugin(nuxt, { getComponents, serverComponentRuntime, mode: 'server' }), { server: true, client: false })
addBuildPlugin(TransformPlugin(nuxt, { getComponents, serverComponentRuntime, mode: 'client' }), { server: false, client: true })
// Do not prefetch global components chunks // Do not prefetch global components chunks
nuxt.hook('build:manifest', (manifest) => { nuxt.hook('build:manifest', (manifest) => {
@ -162,7 +165,7 @@ export default defineNuxtModule<ComponentsOptions>({
} }
}) })
const serverPlaceholderPath = resolve(distDir, 'app/components/server-placeholder') const serverPlaceholderPath = await findPath(join(distDir, 'app/components/server-placeholder')) ?? join(distDir, 'app/components/server-placeholder')
// Scan components and add to plugin // Scan components and add to plugin
nuxt.hook('app:templates', async (app) => { nuxt.hook('app:templates', async (app) => {
@ -222,6 +225,7 @@ export default defineNuxtModule<ComponentsOptions>({
const sharedLoaderOptions = { const sharedLoaderOptions = {
getComponents, getComponents,
serverComponentRuntime,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined, transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands, experimentalComponentIslands: !!nuxt.options.experimental.componentIslands,
} }
@ -272,16 +276,18 @@ export default defineNuxtModule<ComponentsOptions>({
} }
}) })
nuxt.hook('webpack:config', (configs) => { for (const key of ['rspack:config', 'webpack:config'] as const) {
configs.forEach((config) => { nuxt.hook(key, (configs) => {
const mode = config.name === 'client' ? 'client' : 'server' configs.forEach((config) => {
config.plugins = config.plugins || [] const mode = config.name === 'client' ? 'client' : 'server'
config.plugins = config.plugins || []
if (mode !== 'server') { if (mode !== 'server') {
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}') writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
} }
})
}) })
}) }
} }
}, },
}) })

View File

@ -12,6 +12,7 @@ interface LoaderOptions {
} }
const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/ const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
const UID_RE = / :?uid=/
export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnplugin(() => { export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnplugin(() => {
const exclude = options.transform?.exclude || [] const exclude = options.transform?.exclude || []
const include = options.transform?.include || [] const include = options.transform?.include || []
@ -37,7 +38,7 @@ export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnpl
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => { s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
count++ count++
if (/ :?uid=/.test(attrs)) { return full } if (UID_RE.test(attrs)) { return full }
return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>` return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>`
}) })

View File

@ -1,12 +1,13 @@
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import type { Component } from 'nuxt/schema' import type { Component } from 'nuxt/schema'
import { isVue } from '../../core/utils' import { SX_RE, isVue } from '../../core/utils'
interface NameDevPluginOptions { interface NameDevPluginOptions {
sourcemap: boolean sourcemap: boolean
getComponents: () => Component[] getComponents: () => Component[]
} }
const FILENAME_RE = /([^/\\]+)\.\w+$/
/** /**
* Set the default name of components to their PascalCase name * Set the default name of components to their PascalCase name
*/ */
@ -15,10 +16,10 @@ export const ComponentNamePlugin = (options: NameDevPluginOptions) => createUnpl
name: 'nuxt:component-name-plugin', name: 'nuxt:component-name-plugin',
enforce: 'post', enforce: 'post',
transformInclude (id) { transformInclude (id) {
return isVue(id) || !!id.match(/\.[tj]sx$/) return isVue(id) || !!id.match(SX_RE)
}, },
transform (code, id) { transform (code, id) {
const filename = id.match(/([^/\\]+)\.\w+$/)?.[1] const filename = id.match(FILENAME_RE)?.[1]
if (!filename) { if (!filename) {
return return
} }

View File

@ -30,6 +30,7 @@ const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g
const IMPORT_CODE = '\nimport { mergeProps as __mergeProps } from \'vue\'' + '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport NuxtTeleportIslandComponent from \'#app/components/nuxt-teleport-island-component\'' + '\nimport NuxtTeleportSsrSlot from \'#app/components/nuxt-teleport-island-slot\'' const IMPORT_CODE = '\nimport { mergeProps as __mergeProps } from \'vue\'' + '\nimport { vforToArray as __vforToArray } from \'#app/components/utils\'' + '\nimport NuxtTeleportIslandComponent from \'#app/components/nuxt-teleport-island-component\'' + '\nimport NuxtTeleportSsrSlot from \'#app/components/nuxt-teleport-island-slot\''
const EXTRACTED_ATTRS_RE = /v-(?:if|else-if|else)(="[^"]*")?/g const EXTRACTED_ATTRS_RE = /v-(?:if|else-if|else)(="[^"]*")?/g
const KEY_RE = /:?key="[^"]"/g
function wrapWithVForDiv (code: string, vfor: string): string { function wrapWithVForDiv (code: string, vfor: string): string {
return `<div v-for="${vfor}" style="display: contents;">${code}</div>` return `<div v-for="${vfor}" style="display: contents;">${code}</div>`
@ -90,7 +91,7 @@ export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPlug
if (children.length) { if (children.length) {
// 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_RE, '')
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>`) 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, ''))

View File

@ -2,25 +2,26 @@ import { createUnplugin } from 'unplugin'
import { genDynamicImport, genImport } from 'knitwork' import { genDynamicImport, genImport } from 'knitwork'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { pascalCase } from 'scule' import { pascalCase } from 'scule'
import { resolve } from 'pathe' import { relative } from 'pathe'
import type { Component, ComponentsOptions } from 'nuxt/schema' import type { Component, ComponentsOptions } from 'nuxt/schema'
import { logger, tryUseNuxt } from '@nuxt/kit' import { logger, tryUseNuxt } from '@nuxt/kit'
import { distDir } from '../../dirs' import { QUOTE_RE, SX_RE, isVue } from '../../core/utils'
import { isVue } from '../../core/utils'
interface LoaderOptions { interface LoaderOptions {
getComponents (): Component[] getComponents (): Component[]
mode: 'server' | 'client' mode: 'server' | 'client'
serverComponentRuntime: string
sourcemap?: boolean sourcemap?: boolean
transform?: ComponentsOptions['transform'] transform?: ComponentsOptions['transform']
experimentalComponentIslands?: boolean experimentalComponentIslands?: boolean
} }
const REPLACE_COMPONENT_TO_DIRECT_IMPORT_RE = /(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?([^'"]*)["'][^)]*\)/g
export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => { export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
const exclude = options.transform?.exclude || [] const exclude = options.transform?.exclude || []
const include = options.transform?.include || [] const include = options.transform?.include || []
const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component') const nuxt = tryUseNuxt()
return { return {
name: 'nuxt:components-loader', name: 'nuxt:components-loader',
@ -32,9 +33,9 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
if (include.some(pattern => pattern.test(id))) { if (include.some(pattern => pattern.test(id))) {
return true return true
} }
return isVue(id, { type: ['template', 'script'] }) || !!id.match(/\.[tj]sx$/) return isVue(id, { type: ['template', 'script'] }) || !!id.match(SX_RE)
}, },
transform (code) { transform (code, id) {
const components = options.getComponents() const components = options.getComponents()
let num = 0 let num = 0
@ -43,13 +44,17 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
const s = new MagicString(code) const s = new MagicString(code)
// replace `_resolveComponent("...")` to direct import // replace `_resolveComponent("...")` to direct import
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, name: string) => { s.replace(REPLACE_COMPONENT_TO_DIRECT_IMPORT_RE, (full: string, lazy: string, name: string) => {
const component = findComponent(components, name, options.mode) const component = findComponent(components, name, options.mode)
if (component) { if (component) {
// @ts-expect-error TODO: refactor to nuxi // TODO: refactor to nuxi
if (component._internal_install && tryUseNuxt()?.options.test === false) { const internalInstall = ((component as any)._internal_install) as string
// @ts-expect-error TODO: refactor to nuxi if (internalInstall && nuxt?.options.test === false) {
import('../../core/features').then(({ installNuxtModule }) => installNuxtModule(component._internal_install)) if (!nuxt.options.dev) {
const relativePath = relative(nuxt.options.rootDir, id)
throw new Error(`[nuxt] \`~/${relativePath}\` is using \`${component.pascalName}\` which requires \`${internalInstall}\``)
}
import('../../core/features').then(({ installNuxtModule }) => installNuxtModule(internalInstall))
} }
let identifier = map.get(component) || `__nuxt_component_${num++}` let identifier = map.get(component) || `__nuxt_component_${num++}`
map.set(component, identifier) map.set(component, identifier)
@ -57,7 +62,7 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
const isServerOnly = !component._raw && component.mode === 'server' && const isServerOnly = !component._raw && component.mode === 'server' &&
!components.some(c => c.pascalName === component.pascalName && c.mode === 'client') !components.some(c => c.pascalName === component.pascalName && c.mode === 'client')
if (isServerOnly) { if (isServerOnly) {
imports.add(genImport(serverComponentRuntime, [{ name: 'createServerComponent' }])) imports.add(genImport(options.serverComponentRuntime, [{ name: 'createServerComponent' }]))
imports.add(`const ${identifier} = createServerComponent(${JSON.stringify(component.pascalName)})`) imports.add(`const ${identifier} = createServerComponent(${JSON.stringify(component.pascalName)})`)
if (!options.experimentalComponentIslands) { if (!options.experimentalComponentIslands) {
logger.warn(`Standalone server components (\`${name}\`) are not yet supported without enabling \`experimental.componentIslands\`.`) logger.warn(`Standalone server components (\`${name}\`) are not yet supported without enabling \`experimental.componentIslands\`.`)
@ -107,7 +112,7 @@ export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
}) })
function findComponent (components: Component[], name: string, mode: LoaderOptions['mode']) { function findComponent (components: Component[], name: string, mode: LoaderOptions['mode']) {
const id = pascalCase(name).replace(/["']/g, '') const id = pascalCase(name).replace(QUOTE_RE, '')
// Prefer exact match // Prefer exact match
const component = components.find(component => id === component.pascalName && ['all', mode, undefined].includes(component.mode)) const component = components.find(component => id === component.pascalName && ['all', mode, undefined].includes(component.mode))
if (component) { return component } if (component) { return component }

View File

@ -5,15 +5,19 @@ import { createUnimport } from 'unimport'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import { parseURL } from 'ufo' import { parseURL } from 'ufo'
import { parseQuery } from 'vue-router' import { parseQuery } from 'vue-router'
import { normalize, resolve } from 'pathe' import { normalize } from 'pathe'
import { genImport } from 'knitwork' import { genImport } from 'knitwork'
import { distDir } from '../../dirs'
import type { getComponentsT } from '../module' import type { getComponentsT } from '../module'
const COMPONENT_QUERY_RE = /[?&]nuxt_component=/ const COMPONENT_QUERY_RE = /[?&]nuxt_component=/
export function TransformPlugin (nuxt: Nuxt, getComponents: getComponentsT, mode: 'client' | 'server' | 'all') { interface TransformPluginOptions {
const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component') getComponents: getComponentsT
mode: 'client' | 'server' | 'all'
serverComponentRuntime: string
}
export function TransformPlugin (nuxt: Nuxt, options: TransformPluginOptions) {
const componentUnimport = createUnimport({ const componentUnimport = createUnimport({
imports: [ imports: [
{ {
@ -26,7 +30,7 @@ export function TransformPlugin (nuxt: Nuxt, getComponents: getComponentsT, mode
}) })
function getComponentsImports (): Import[] { function getComponentsImports (): Import[] {
const components = getComponents(mode) const components = options.getComponents(options.mode)
return components.flatMap((c): Import[] => { return components.flatMap((c): Import[] => {
const withMode = (mode: string | undefined) => mode const withMode = (mode: string | undefined) => mode
? `${c.filePath}${c.filePath.includes('?') ? '&' : '?'}nuxt_component=${mode}&nuxt_component_name=${c.pascalName}&nuxt_component_export=${c.export || 'default'}` ? `${c.filePath}${c.filePath.includes('?') ? '&' : '?'}nuxt_component=${mode}&nuxt_component_name=${c.pascalName}&nuxt_component_export=${c.export || 'default'}`
@ -95,7 +99,7 @@ export function TransformPlugin (nuxt: Nuxt, getComponents: getComponentsT, mode
const name = query.nuxt_component_name const name = query.nuxt_component_name
return { return {
code: [ code: [
`import { createServerComponent } from ${JSON.stringify(serverComponentRuntime)}`, `import { createServerComponent } from ${JSON.stringify(options.serverComponentRuntime)}`,
`${exportWording} createServerComponent(${JSON.stringify(name)})`, `${exportWording} createServerComponent(${JSON.stringify(name)})`,
].join('\n'), ].join('\n'),
map: null, map: null,

View File

@ -33,8 +33,9 @@ export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions)
const components = options.getComponents() const components = options.getComponents()
if (!regexpMap.has(components)) { if (!regexpMap.has(components)) {
const serverPlaceholderPath = resolve(distDir, 'app/components/server-placeholder')
const clientOnlyComponents = components const clientOnlyComponents = components
.filter(c => c.mode === 'client' && !components.some(other => other.mode !== 'client' && other.pascalName === c.pascalName && other.filePath !== resolve(distDir, 'app/components/server-placeholder'))) .filter(c => c.mode === 'client' && !components.some(other => other.mode !== 'client' && other.pascalName === c.pascalName && !other.filePath.startsWith(serverPlaceholderPath)))
.flatMap(c => [c.pascalName, c.kebabName.replaceAll('-', '_')]) .flatMap(c => [c.pascalName, c.kebabName.replaceAll('-', '_')])
.concat(['ClientOnly', 'client_only']) .concat(['ClientOnly', 'client_only'])

View File

@ -6,8 +6,12 @@ import { isIgnored, logger, useNuxt } from '@nuxt/kit'
import { withTrailingSlash } from 'ufo' import { withTrailingSlash } from 'ufo'
import type { Component, ComponentsDir } from 'nuxt/schema' import type { Component, ComponentsDir } from 'nuxt/schema'
import { resolveComponentNameSegments } from '../core/utils' import { QUOTE_RE, resolveComponentNameSegments } from '../core/utils'
const ISLAND_RE = /\.island(?:\.global)?$/
const GLOBAL_RE = /\.global(?:\.island)?$/
const COMPONENT_MODE_RE = /(?<=\.)(client|server)(\.global|\.island)*$/
const MODE_REPLACEMENT_RE = /(\.(client|server))?(\.global|\.island)*$/
/** /**
* Scan the components inside different components folders * Scan the components inside different components folders
* and return a unique list of components * and return a unique list of components
@ -83,17 +87,17 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
*/ */
let fileName = basename(filePath, extname(filePath)) let fileName = basename(filePath, extname(filePath))
const island = /\.island(?:\.global)?$/.test(fileName) || dir.island const island = ISLAND_RE.test(fileName) || dir.island
const global = /\.global(?:\.island)?$/.test(fileName) || dir.global const global = GLOBAL_RE.test(fileName) || dir.global
const mode = island ? 'server' : (fileName.match(/(?<=\.)(client|server)(\.global|\.island)*$/)?.[1] || 'all') as 'client' | 'server' | 'all' const mode = island ? 'server' : (fileName.match(COMPONENT_MODE_RE)?.[1] || 'all') as 'client' | 'server' | 'all'
fileName = fileName.replace(/(\.(client|server))?(\.global|\.island)*$/, '') fileName = fileName.replace(MODE_REPLACEMENT_RE, '')
if (fileName.toLowerCase() === 'index') { if (fileName.toLowerCase() === 'index') {
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */ fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
} }
const suffix = (mode !== 'all' ? `-${mode}` : '') const suffix = (mode !== 'all' ? `-${mode}` : '')
const componentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts) const componentNameSegments = resolveComponentNameSegments(fileName.replace(QUOTE_RE, ''), prefixParts)
const pascalName = pascalCase(componentNameSegments) const pascalName = pascalCase(componentNameSegments)
if (LAZY_COMPONENT_NAME_REGEX.test(pascalName)) { if (LAZY_COMPONENT_NAME_REGEX.test(pascalName)) {

View File

@ -102,14 +102,15 @@ export const componentsIslandsTemplate: NuxtTemplate = {
}, },
} }
const NON_VUE_RE = /\b\.(?!vue)\w+$/g
export const componentsTypeTemplate = { export const componentsTypeTemplate = {
filename: 'components.d.ts' as const, filename: 'components.d.ts' as const,
getContents: ({ app, nuxt }) => { getContents: ({ app, nuxt }) => {
const buildDir = nuxt.options.buildDir const buildDir = nuxt.options.buildDir
const componentTypes = app.components.filter(c => !c.island).map((c) => { const componentTypes = app.components.filter(c => !c.island).map((c) => {
const type = `typeof ${genDynamicImport(isAbsolute(c.filePath) const type = `typeof ${genDynamicImport(isAbsolute(c.filePath)
? relative(buildDir, c.filePath).replace(/\b\.(?!vue)\w+$/g, '') ? relative(buildDir, c.filePath).replace(NON_VUE_RE, '')
: c.filePath.replace(/\b\.(?!vue)\w+$/g, ''), { wrapper: false })}['${c.export}']` : c.filePath.replace(NON_VUE_RE, ''), { wrapper: false })}['${c.export}']`
return [ return [
c.pascalName, c.pascalName,
c.island || c.mode === 'server' ? `IslandComponent<${type}>` : type, c.island || c.mode === 'server' ? `IslandComponent<${type}>` : type,

View File

@ -57,7 +57,7 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
const writes: Array<() => void> = [] const writes: Array<() => void> = []
const changedTemplates: Array<ResolvedNuxtTemplate<any>> = [] const changedTemplates: Array<ResolvedNuxtTemplate<any>> = []
const FORWARD_SLASH_RE = /\//g
async function processTemplate (template: ResolvedNuxtTemplate) { async function processTemplate (template: ResolvedNuxtTemplate) {
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!) const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
const start = performance.now() const start = performance.now()
@ -72,12 +72,12 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
if (template.modified) { if (template.modified) {
nuxt.vfs[fullPath] = contents nuxt.vfs[fullPath] = contents
const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '') const aliasPath = '#build/' + template.filename
nuxt.vfs[aliasPath] = contents nuxt.vfs[aliasPath] = contents
// In case a non-normalized absolute path is called for on Windows // In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') { if (process.platform === 'win32') {
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents nuxt.vfs[fullPath.replace(FORWARD_SLASH_RE, '\\')] = contents
} }
changedTemplates.push(template) changedTemplates.push(template)

View File

@ -10,6 +10,7 @@ import { generateApp as _generateApp, createApp } from './app'
import { checkForExternalConfigurationFiles } from './external-config-files' import { checkForExternalConfigurationFiles } from './external-config-files'
import { cleanupCaches, getVueHash } from './cache' import { cleanupCaches, getVueHash } from './cache'
const IS_RESTART_PATH_RE = /^(?:app\.|error\.|plugins\/|middleware\/|layouts\/)/i
export async function build (nuxt: Nuxt) { export async function build (nuxt: Nuxt) {
const app = createApp(nuxt) const app = createApp(nuxt)
nuxt.apps.default = app nuxt.apps.default = app
@ -23,7 +24,7 @@ export async function build (nuxt: Nuxt) {
if (event === 'change') { return } if (event === 'change') { return }
const path = resolve(nuxt.options.srcDir, relativePath) const path = resolve(nuxt.options.srcDir, relativePath)
const relativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path)) const relativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
const restartPath = relativePaths.find(relativePath => /^(?:app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(relativePath)) const restartPath = relativePaths.find(relativePath => IS_RESTART_PATH_RE.test(relativePath))
if (restartPath) { if (restartPath) {
if (restartPath.startsWith('app')) { if (restartPath.startsWith('app')) {
app.mainComponent = undefined app.mainComponent = undefined

View File

@ -18,6 +18,7 @@ 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 { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
import { nuxtImportProtections } from './plugins/import-protection' import { nuxtImportProtections } from './plugins/import-protection'
import { EXTENSION_RE } from './utils'
const logLevelMapReverse = { const logLevelMapReverse = {
silent: 0, silent: 0,
@ -25,12 +26,14 @@ const logLevelMapReverse = {
verbose: 3, verbose: 3,
} satisfies Record<NuxtOptions['logLevel'], NitroConfig['logLevel']> } satisfies Record<NuxtOptions['logLevel'], NitroConfig['logLevel']>
const NODE_MODULES_RE = /(?<=\/)node_modules\/(.+)$/
const PNPM_NODE_MODULES_RE = /\.pnpm\/.+\/node_modules\/(.+)$/
export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Resolve config // Resolve config
const excludePaths = nuxt.options._layers const excludePaths = nuxt.options._layers
.flatMap(l => [ .flatMap(l => [
l.cwd.match(/(?<=\/)node_modules\/(.+)$/)?.[1], l.cwd.match(NODE_MODULES_RE)?.[1],
l.cwd.match(/\.pnpm\/.+\/node_modules\/(.+)$/)?.[1], l.cwd.match(PNPM_NODE_MODULES_RE)?.[1],
]) ])
.filter((dir): dir is string => Boolean(dir)) .filter((dir): dir is string => Boolean(dir))
.map(dir => escapeRE(dir)) .map(dir => escapeRE(dir))
@ -101,8 +104,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
devHandlers: [], devHandlers: [],
baseURL: nuxt.options.app.baseURL, baseURL: nuxt.options.app.baseURL,
virtual: { virtual: {
'#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config'], '#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config.mjs'],
'#internal/nuxt/app-config': () => nuxt.vfs['#build/app.config']?.replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, ''), '#internal/nuxt/app-config': () => nuxt.vfs['#build/app.config.mjs']?.replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, ''),
'#spa-template': async () => `export const template = ${JSON.stringify(await spaLoadingTemplate(nuxt))}`, '#spa-template': async () => `export const template = ${JSON.stringify(await spaLoadingTemplate(nuxt))}`,
}, },
routeRules: { routeRules: {
@ -189,11 +192,11 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}, },
'@vue/devtools-api': 'vue-devtools-stub', '@vue/devtools-api': 'vue-devtools-stub',
// Paths
'#internal/nuxt/paths': resolve(distDir, 'core/runtime/nitro/paths'),
// Nuxt aliases // Nuxt aliases
...nuxt.options.alias, ...nuxt.options.alias,
// Paths
'#internal/nuxt/paths': resolve(distDir, 'core/runtime/nitro/paths'),
}, },
replace: { replace: {
'process.env.NUXT_NO_SSR': nuxt.options.ssr === false, 'process.env.NUXT_NO_SSR': nuxt.options.ssr === false,
@ -339,19 +342,20 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
} }
// Add fallback server for `ssr: false` // Add fallback server for `ssr: false`
const FORWARD_SLASH_RE = /\//g
if (!nuxt.options.ssr) { if (!nuxt.options.ssr) {
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}' nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
// In case a non-normalized absolute path is called for on Windows // In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') { if (process.platform === 'win32') {
nitroConfig.virtual!['#build/dist/server/server.mjs'.replace(/\//g, '\\')] = 'export default () => {}' nitroConfig.virtual!['#build/dist/server/server.mjs'.replace(FORWARD_SLASH_RE, '\\')] = 'export default () => {}'
} }
} }
if (nuxt.options.builder === '@nuxt/webpack-builder' || nuxt.options.dev) { if (nuxt.options.dev || nuxt.options.builder === '@nuxt/webpack-builder' || nuxt.options.builder === '@nuxt/rspack-builder') {
nitroConfig.virtual!['#build/dist/server/styles.mjs'] = 'export default {}' nitroConfig.virtual!['#build/dist/server/styles.mjs'] = 'export default {}'
// In case a non-normalized absolute path is called for on Windows // In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') { if (process.platform === 'win32') {
nitroConfig.virtual!['#build/dist/server/styles.mjs'.replace(/\//g, '\\')] = 'export default {}' nitroConfig.virtual!['#build/dist/server/styles.mjs'.replace(FORWARD_SLASH_RE, '\\')] = 'export default {}'
} }
} }
@ -389,7 +393,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
tsConfig.compilerOptions.paths[alias] = [absolutePath] tsConfig.compilerOptions.paths[alias] = [absolutePath]
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`] tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`]
} else { } else {
tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(/\b\.\w+$/g, '')] /* remove extension */ tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(EXTENSION_RE, '')] /* remove extension */
} }
} }
@ -448,18 +452,20 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
} }
} }
}) })
nuxt.hook('webpack:config', (configuration) => { for (const hook of ['webpack:config', 'rspack:config'] as const) {
const clientConfig = configuration.find(config => config.name === 'client') nuxt.hook(hook, (configuration) => {
if (!clientConfig!.resolve) { clientConfig!.resolve!.alias = {} } const clientConfig = configuration.find(config => config.name === 'client')
if (Array.isArray(clientConfig!.resolve!.alias)) { if (!clientConfig!.resolve) { clientConfig!.resolve!.alias = {} }
clientConfig!.resolve!.alias.push({ if (Array.isArray(clientConfig!.resolve!.alias)) {
name: 'vue', clientConfig!.resolve!.alias.push({
alias: 'vue/dist/vue.esm-bundler', name: 'vue',
}) alias: 'vue/dist/vue.esm-bundler',
} else { })
clientConfig!.resolve!.alias!.vue = 'vue/dist/vue.esm-bundler' } else {
} clientConfig!.resolve!.alias!.vue = 'vue/dist/vue.esm-bundler'
}) }
})
}
} }
// Setup handlers // Setup handlers
@ -545,13 +551,15 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// nuxt dev // nuxt dev
if (nuxt.options.dev) { if (nuxt.options.dev) {
nuxt.hook('webpack:compile', ({ name, compiler }) => { for (const builder of ['webpack', 'rspack'] as const) {
if (name === 'server') { nuxt.hook(`${builder}:compile`, ({ name, compiler }) => {
const memfs = compiler.outputFileSystem as typeof import('node:fs') if (name === 'server') {
nitro.options.virtual['#build/dist/server/server.mjs'] = () => memfs.readFileSync(join(nuxt.options.buildDir, 'dist/server/server.mjs'), 'utf-8') const memfs = compiler.outputFileSystem as typeof import('node:fs')
} nitro.options.virtual['#build/dist/server/server.mjs'] = () => memfs.readFileSync(join(nuxt.options.buildDir, 'dist/server/server.mjs'), 'utf-8')
}) }
nuxt.hook('webpack:compiled', () => { nuxt.server.reload() }) })
nuxt.hook(`${builder}:compiled`, () => { nuxt.server.reload() })
}
nuxt.hook('vite:compiled', () => { nuxt.server.reload() }) nuxt.hook('vite:compiled', () => { nuxt.server.reload() })
nuxt.hook('server:devHandler', (h) => { devMiddlewareHandler.set(h) }) nuxt.hook('server:devHandler', (h) => { devMiddlewareHandler.set(h) })
@ -562,8 +570,9 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
} }
} }
const RELATIVE_RE = /^([^.])/
function relativeWithDot (from: string, to: string) { function relativeWithDot (from: string, to: string) {
return relative(from, to).replace(/^([^.])/, './$1') || '.' return relative(from, to).replace(RELATIVE_RE, './$1') || '.'
} }
async function spaLoadingTemplatePath (nuxt: Nuxt) { async function spaLoadingTemplatePath (nuxt: Nuxt) {

View File

@ -44,6 +44,7 @@ import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata'
import { AsyncContextInjectionPlugin } from './plugins/async-context' import { AsyncContextInjectionPlugin } from './plugins/async-context'
import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports' import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports'
import { prehydrateTransformPlugin } from './plugins/prehydrate' import { prehydrateTransformPlugin } from './plugins/prehydrate'
import { VirtualFSPlugin } from './plugins/virtual'
export function createNuxt (options: NuxtOptions): Nuxt { export function createNuxt (options: NuxtOptions): Nuxt {
const hooks = createHooks<NuxtHooks>() const hooks = createHooks<NuxtHooks>()
@ -177,9 +178,10 @@ async function initNuxt (nuxt: Nuxt) {
const coreTypePackages = nuxt.options.typescript.hoist || [] const coreTypePackages = nuxt.options.typescript.hoist || []
const packageJSON = await readPackageJSON(nuxt.options.rootDir).catch(() => ({}) as PackageJson) const packageJSON = await readPackageJSON(nuxt.options.rootDir).catch(() => ({}) as PackageJson)
const NESTED_PKG_RE = /^[^@]+\//
nuxt._dependencies = new Set([...Object.keys(packageJSON.dependencies || {}), ...Object.keys(packageJSON.devDependencies || {})]) nuxt._dependencies = new Set([...Object.keys(packageJSON.dependencies || {}), ...Object.keys(packageJSON.devDependencies || {})])
const paths = Object.fromEntries(await Promise.all(coreTypePackages.map(async (pkg) => { const paths = Object.fromEntries(await Promise.all(coreTypePackages.map(async (pkg) => {
const [_pkg = pkg, _subpath] = /^[^@]+\//.test(pkg) ? pkg.split('/') : [pkg] const [_pkg = pkg, _subpath] = NESTED_PKG_RE.test(pkg) ? pkg.split('/') : [pkg]
const subpath = _subpath ? '/' + _subpath : '' const subpath = _subpath ? '/' + _subpath : ''
// ignore packages that exist in `package.json` as these can be resolved by TypeScript // ignore packages that exist in `package.json` as these can be resolved by TypeScript
@ -240,6 +242,10 @@ async function initNuxt (nuxt: Nuxt) {
} }
} }
// Support Nuxt VFS
addBuildPlugin(VirtualFSPlugin(nuxt, { mode: 'server' }), { client: false })
addBuildPlugin(VirtualFSPlugin(nuxt, { mode: 'client', alias: { 'nitro/runtime': join(nuxt.options.buildDir, 'nitro.client.mjs') } }), { server: false })
// Add plugin normalization plugin // Add plugin normalization plugin
addBuildPlugin(RemovePluginMetadataPlugin(nuxt)) addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
@ -492,9 +498,11 @@ async function initNuxt (nuxt: Nuxt) {
const envMap = { const envMap = {
// defaults from `builder` based on package name // defaults from `builder` based on package name
'@nuxt/rspack-builder': '@rspack/core/module',
'@nuxt/vite-builder': 'vite/client', '@nuxt/vite-builder': 'vite/client',
'@nuxt/webpack-builder': 'webpack/module', '@nuxt/webpack-builder': 'webpack/module',
// simpler overrides from `typescript.builder` for better DX // simpler overrides from `typescript.builder` for better DX
'rspack': '@rspack/core/module',
'vite': 'vite/client', 'vite': 'vite/client',
'webpack': 'webpack/module', 'webpack': 'webpack/module',
// default 'merged' builder environment for module authors // default 'merged' builder environment for module authors

View File

@ -16,7 +16,7 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
conditions = config.mode === 'test' ? [...config.resolve.conditions, 'import', 'require'] : config.resolve.conditions conditions = config.mode === 'test' ? [...config.resolve.conditions, 'import', 'require'] : config.resolve.conditions
}, },
async resolveId (id, importer) { async resolveId (id, importer) {
if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:')) || exclude.some(e => id.startsWith(e))) { if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:') && !importer.startsWith('\0virtual:')) || exclude.some(e => id.startsWith(e))) {
return return
} }

View File

@ -0,0 +1,68 @@
import { resolveAlias } from '@nuxt/kit'
import type { Nuxt } from '@nuxt/schema'
import { dirname, isAbsolute, resolve } from 'pathe'
import { createUnplugin } from 'unplugin'
const PREFIX = '\0virtual:nuxt:'
interface VirtualFSPluginOptions {
mode: 'client' | 'server'
alias?: Record<string, string>
}
const RELATIVE_ID_RE = /^\.{1,2}[\\/]/
export const VirtualFSPlugin = (nuxt: Nuxt, options: VirtualFSPluginOptions) => createUnplugin(() => {
const extensions = ['', ...nuxt.options.extensions]
const alias = { ...nuxt.options.alias, ...options.alias }
const resolveWithExt = (id: string) => {
for (const suffix of ['', '.' + options.mode]) {
for (const ext of extensions) {
const rId = id + suffix + ext
if (rId in nuxt.vfs) {
return rId
}
}
}
}
return {
name: 'nuxt:virtual',
resolveId (id, importer) {
id = resolveAlias(id, alias)
if (process.platform === 'win32' && isAbsolute(id)) {
// Add back C: prefix on Windows
id = resolve(id)
}
const resolvedId = resolveWithExt(id)
if (resolvedId) {
return PREFIX + resolvedId
}
if (importer && RELATIVE_ID_RE.test(id)) {
const path = resolve(dirname(withoutPrefix(importer)), id)
const resolved = resolveWithExt(path)
if (resolved) {
return PREFIX + resolved
}
}
},
loadInclude (id) {
return id.startsWith(PREFIX) && withoutPrefix(id) in nuxt.vfs
},
load (id) {
return {
code: nuxt.vfs[withoutPrefix(id)] || '',
map: null,
}
},
}
})
function withoutPrefix (id: string) {
return id.startsWith(PREFIX) ? id.slice(PREFIX.length) : id
}

View File

@ -23,7 +23,6 @@ export default defineNuxtModule({
// Initialize untyped/jiti loader // Initialize untyped/jiti loader
const _resolveSchema = createJiti(fileURLToPath(import.meta.url), { const _resolveSchema = createJiti(fileURLToPath(import.meta.url), {
interopDefault: true,
cache: false, cache: false,
transformOptions: { transformOptions: {
babel: { babel: {
@ -97,7 +96,7 @@ export default defineNuxtModule({
let loadedConfig: SchemaDefinition let loadedConfig: SchemaDefinition
try { try {
// TODO: fix type for second argument of `import` // TODO: fix type for second argument of `import`
loadedConfig = await _resolveSchema.import(filePath, {}) as SchemaDefinition loadedConfig = await _resolveSchema.import(filePath, { default: true }) as SchemaDefinition
} catch (err) { } catch (err) {
logger.warn( logger.warn(
'Unable to load schema from', 'Unable to load schema from',

View File

@ -11,6 +11,7 @@ import type { NuxtTemplate } from 'nuxt/schema'
import type { Nitro } from 'nitro/types' import type { Nitro } from 'nitro/types'
import { annotatePlugins, checkForCircularDependencies } from './app' import { annotatePlugins, checkForCircularDependencies } from './app'
import { EXTENSION_RE } from './utils'
export const vueShim: NuxtTemplate = { export const vueShim: NuxtTemplate = {
filename: 'types/vue-shim.d.ts', filename: 'types/vue-shim.d.ts',
@ -57,8 +58,9 @@ export const cssTemplate: NuxtTemplate = {
getContents: ctx => ctx.nuxt.options.css.map(i => genImport(i)).join('\n'), getContents: ctx => ctx.nuxt.options.css.map(i => genImport(i)).join('\n'),
} }
const PLUGIN_TEMPLATE_RE = /_(45|46|47)/g
export const clientPluginTemplate: NuxtTemplate = { export const clientPluginTemplate: NuxtTemplate = {
filename: 'plugins/client.mjs', filename: 'plugins.client.mjs',
async getContents (ctx) { async getContents (ctx) {
const clientPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'server')) const clientPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'server'))
checkForCircularDependencies(clientPlugins) checkForCircularDependencies(clientPlugins)
@ -66,7 +68,7 @@ export const clientPluginTemplate: NuxtTemplate = {
const imports: string[] = [] const imports: string[] = []
for (const plugin of clientPlugins) { for (const plugin of clientPlugins) {
const path = relative(ctx.nuxt.options.rootDir, plugin.src) const path = relative(ctx.nuxt.options.rootDir, plugin.src)
const variable = genSafeVariableName(filename(plugin.src)).replace(/_(45|46|47)/g, '_') + '_' + hash(path) const variable = genSafeVariableName(filename(plugin.src)).replace(PLUGIN_TEMPLATE_RE, '_') + '_' + hash(path)
exports.push(variable) exports.push(variable)
imports.push(genImport(plugin.src, variable)) imports.push(genImport(plugin.src, variable))
} }
@ -78,7 +80,7 @@ export const clientPluginTemplate: NuxtTemplate = {
} }
export const serverPluginTemplate: NuxtTemplate = { export const serverPluginTemplate: NuxtTemplate = {
filename: 'plugins/server.mjs', filename: 'plugins.server.mjs',
async getContents (ctx) { async getContents (ctx) {
const serverPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'client')) const serverPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'client'))
checkForCircularDependencies(serverPlugins) checkForCircularDependencies(serverPlugins)
@ -86,7 +88,7 @@ export const serverPluginTemplate: NuxtTemplate = {
const imports: string[] = [] const imports: string[] = []
for (const plugin of serverPlugins) { for (const plugin of serverPlugins) {
const path = relative(ctx.nuxt.options.rootDir, plugin.src) const path = relative(ctx.nuxt.options.rootDir, plugin.src)
const variable = genSafeVariableName(filename(path)).replace(/_(45|46|47)/g, '_') + '_' + hash(path) const variable = genSafeVariableName(filename(path)).replace(PLUGIN_TEMPLATE_RE, '_') + '_' + hash(path)
exports.push(variable) exports.push(variable)
imports.push(genImport(plugin.src, variable)) imports.push(genImport(plugin.src, variable))
} }
@ -98,7 +100,9 @@ export const serverPluginTemplate: NuxtTemplate = {
} }
const TS_RE = /\.[cm]?tsx?$/ const TS_RE = /\.[cm]?tsx?$/
const JS_LETTER_RE = /\.(?<letter>[cm])?jsx?$/
const JS_RE = /\.[cm]jsx?$/
const JS_CAPTURE_RE = /\.[cm](jsx?)$/
export const pluginsDeclaration: NuxtTemplate = { export const pluginsDeclaration: NuxtTemplate = {
filename: 'types/plugins.d.ts', filename: 'types/plugins.d.ts',
getContents: async ({ nuxt, app }) => { getContents: async ({ nuxt, app }) => {
@ -120,18 +124,18 @@ export const pluginsDeclaration: NuxtTemplate = {
const pluginPath = resolve(typesDir, plugin.src) const pluginPath = resolve(typesDir, plugin.src)
const relativePath = relative(typesDir, pluginPath) const relativePath = relative(typesDir, pluginPath)
const correspondingDeclaration = pluginPath.replace(/\.(?<letter>[cm])?jsx?$/, '.d.$<letter>ts') const correspondingDeclaration = pluginPath.replace(JS_LETTER_RE, '.d.$<letter>ts')
// if `.d.ts` file exists alongside a `.js` plugin, or if `.d.mts` file exists alongside a `.mjs` plugin, we can use the entire path // if `.d.ts` file exists alongside a `.js` plugin, or if `.d.mts` file exists alongside a `.mjs` plugin, we can use the entire path
if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) { if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) {
tsImports.push(relativePath) tsImports.push(relativePath)
continue continue
} }
const incorrectDeclaration = pluginPath.replace(/\.[cm]jsx?$/, '.d.ts') const incorrectDeclaration = pluginPath.replace(JS_RE, '.d.ts')
// if `.d.ts` file exists, but plugin is `.mjs`, add `.js` extension to the import // if `.d.ts` file exists, but plugin is `.mjs`, add `.js` extension to the import
// to hotfix issue until ecosystem updates to `@nuxt/module-builder@>=0.8.0` // to hotfix issue until ecosystem updates to `@nuxt/module-builder@>=0.8.0`
if (incorrectDeclaration !== pluginPath && exists(incorrectDeclaration)) { if (incorrectDeclaration !== pluginPath && exists(incorrectDeclaration)) {
tsImports.push(relativePath.replace(/\.[cm](jsx?)$/, '.$1')) tsImports.push(relativePath.replace(JS_CAPTURE_RE, '.$1'))
continue continue
} }
@ -174,11 +178,13 @@ export { }
} }
const adHocModules = ['router', 'pages', 'imports', 'meta', 'components', 'nuxt-config-schema'] const adHocModules = ['router', 'pages', 'imports', 'meta', 'components', 'nuxt-config-schema']
const IMPORT_NAME_RE = /\.\w+$/
const GIT_RE = /^git\+/
export const schemaTemplate: NuxtTemplate = { export const schemaTemplate: NuxtTemplate = {
filename: 'types/schema.d.ts', filename: 'types/schema.d.ts',
getContents: async ({ nuxt }) => { getContents: async ({ nuxt }) => {
const relativeRoot = relative(resolve(nuxt.options.buildDir, 'types'), nuxt.options.rootDir) const relativeRoot = relative(resolve(nuxt.options.buildDir, 'types'), nuxt.options.rootDir)
const getImportName = (name: string) => (name[0] === '.' ? './' + join(relativeRoot, name) : name).replace(/\.\w+$/, '') const getImportName = (name: string) => (name[0] === '.' ? './' + join(relativeRoot, name) : name).replace(IMPORT_NAME_RE, '')
const modules = nuxt.options._installedModules const modules = nuxt.options._installedModules
.filter(m => m.meta && m.meta.configKey && m.meta.name && !adHocModules.includes(m.meta.name)) .filter(m => m.meta && m.meta.configKey && m.meta.name && !adHocModules.includes(m.meta.name))
@ -210,7 +216,7 @@ export const schemaTemplate: NuxtTemplate = {
} }
if (link) { if (link) {
if (link.startsWith('git+')) { if (link.startsWith('git+')) {
link = link.replace(/^git\+/, '') link = link.replace(GIT_RE, '')
} }
if (!link.startsWith('http')) { if (!link.startsWith('http')) {
link = 'https://github.com/' + link link = 'https://github.com/' + link
@ -377,7 +383,7 @@ export const appConfigDeclarationTemplate: NuxtTemplate = {
filename: 'types/app.config.d.ts', filename: 'types/app.config.d.ts',
getContents ({ app, nuxt }) { getContents ({ app, nuxt }) {
const typesDir = join(nuxt.options.buildDir, 'types') const typesDir = join(nuxt.options.buildDir, 'types')
const configPaths = app.configs.map(path => relative(typesDir, path).replace(/\b\.\w+$/g, '')) const configPaths = app.configs.map(path => relative(typesDir, path).replace(EXTENSION_RE, ''))
return ` return `
import type { CustomAppConfig } from 'nuxt/schema' import type { CustomAppConfig } from 'nuxt/schema'

View File

@ -14,3 +14,7 @@ export function uniqueBy<T, K extends keyof T> (arr: T[], key: K) {
} }
return res return res
} }
export const QUOTE_RE = /["']/g
export const EXTENSION_RE = /\b\.\w+$/g
export const SX_RE = /\.[tj]sx$/

View File

@ -1,6 +1,7 @@
import { basename, dirname, extname, normalize } from 'pathe' import { basename, dirname, extname, normalize } from 'pathe'
import { kebabCase, splitByCase } from 'scule' import { kebabCase, splitByCase } from 'scule'
import { withTrailingSlash } from 'ufo' import { withTrailingSlash } from 'ufo'
import { QUOTE_RE } from '.'
export function getNameFromPath (path: string, relativeTo?: string) { export function getNameFromPath (path: string, relativeTo?: string) {
const relativePath = relativeTo const relativePath = relativeTo
@ -9,7 +10,7 @@ export function getNameFromPath (path: string, relativeTo?: string) {
const prefixParts = splitByCase(dirname(relativePath)) const prefixParts = splitByCase(dirname(relativePath))
const fileName = basename(relativePath, extname(relativePath)) const fileName = basename(relativePath, extname(relativePath))
const segments = resolveComponentNameSegments(fileName.toLowerCase() === 'index' ? '' : fileName, prefixParts).filter(Boolean) const segments = resolveComponentNameSegments(fileName.toLowerCase() === 'index' ? '' : fileName, prefixParts).filter(Boolean)
return kebabCase(segments).replace(/["']/g, '') return kebabCase(segments).replace(QUOTE_RE, '')
} }
export function hasSuffix (path: string, suffix: string) { export function hasSuffix (path: string, suffix: string) {

View File

@ -76,8 +76,8 @@ export default import.meta.server ? [CapoPlugin({ track: true })] : [];`
// template is only exposed in nuxt context, expose in nitro context as well // template is only exposed in nuxt context, expose in nitro context as well
nuxt.hooks.hook('nitro:config', (config) => { nuxt.hooks.hook('nitro:config', (config) => {
config.virtual!['#internal/unhead-plugins.mjs'] = () => nuxt.vfs['#build/unhead-plugins'] config.virtual!['#internal/unhead-plugins.mjs'] = () => nuxt.vfs['#build/unhead-plugins.mjs']
config.virtual!['#internal/unhead.config.mjs'] = () => nuxt.vfs['#build/unhead.config'] config.virtual!['#internal/unhead.config.mjs'] = () => nuxt.vfs['#build/unhead.config.mjs']
}) })
// Add library-specific plugin // Add library-specific plugin

View File

@ -66,7 +66,7 @@ const granularAppPresets: InlinePreset[] = [
from: '#app/composables/cookie', from: '#app/composables/cookie',
}, },
{ {
imports: ['onPrehydrate', 'prerenderRoutes', 'useRequestHeader', 'useRequestHeaders', 'useRequestEvent', 'useRequestFetch', 'setResponseStatus'], imports: ['onPrehydrate', 'prerenderRoutes', 'useRequestHeader', 'useRequestHeaders', 'useResponseHeader', 'useRequestEvent', 'useRequestFetch', 'setResponseStatus'],
from: '#app/composables/ssr', from: '#app/composables/ssr',
}, },
{ {

View File

@ -52,7 +52,7 @@ export default defineNuxtModule({
} }
// Add default options at beginning // Add default options at beginning
context.files.unshift({ path: resolve(runtimeDir, 'router.options'), optional: true }) context.files.unshift({ path: await findPath(resolve(runtimeDir, 'router.options')) || resolve(runtimeDir, 'router.options'), optional: true })
await nuxt.callHook('pages:routerOptions', context) await nuxt.callHook('pages:routerOptions', context)
return context.files return context.files
@ -170,10 +170,15 @@ export default defineNuxtModule({
if (nuxt.apps.default) { if (nuxt.apps.default) {
nuxt.apps.default.pages = pages nuxt.apps.default.pages = pages
} }
const addedPagePaths = new Set<string>()
function addPage (parent: EditableTreeNode, page: NuxtPage) { function addPage (parent: EditableTreeNode, page: NuxtPage) {
// Avoid duplicate keys in the generated RouteNamedMap type
const absolutePagePath = joinURL(parent.path, page.path)
// @ts-expect-error TODO: either fix types upstream or figure out another // @ts-expect-error TODO: either fix types upstream or figure out another
// way to add a route without a file, which must be possible // way to add a route without a file, which must be possible
const route = parent.insert(page.path, page.file) const route = addedPagePaths.has(absolutePagePath) ? parent : parent.insert(page.path, page.file)
addedPagePaths.add(absolutePagePath)
if (page.meta) { if (page.meta) {
route.addToMeta(page.meta) route.addToMeta(page.meta)
} }
@ -414,8 +419,18 @@ export default defineNuxtModule({
}) })
} }
const componentStubPath = await resolvePath(resolve(runtimeDir, 'component-stub'))
if (nuxt.options.test && nuxt.options.dev) {
// add component testing route so 404 won't be triggered
nuxt.hook('pages:extend', (routes) => {
routes.push({
_sync: true,
path: '/__nuxt_component_test__/:pathMatch(.*)',
file: componentStubPath,
})
})
}
if (nuxt.options.experimental.appManifest) { if (nuxt.options.experimental.appManifest) {
const componentStubPath = await resolvePath(resolve(runtimeDir, 'component-stub'))
// Add all redirect paths as valid routes to router; we will handle these in a client-side middleware // Add all redirect paths as valid routes to router; we will handle these in a client-side middleware
// when the app manifest is enabled. // when the app manifest is enabled.
nuxt.hook('pages:extend', (routes) => { nuxt.hook('pages:extend', (routes) => {
@ -477,12 +492,19 @@ export default defineNuxtModule({
} }
}) })
const serverComponentRuntime = await findPath(join(distDir, 'components/runtime/server-component')) ?? join(distDir, 'components/runtime/server-component')
const clientComponentRuntime = await findPath(join(distDir, 'components/runtime/client-component')) ?? join(distDir, 'components/runtime/client-component')
// Add routes template // Add routes template
addTemplate({ addTemplate({
filename: 'routes.mjs', filename: 'routes.mjs',
getContents ({ app }) { getContents ({ app }) {
if (!app.pages) { return 'export default []' } if (!app.pages) { return 'export default []' }
const { routes, imports } = normalizeRoutes(app.pages, new Set(), nuxt.options.experimental.scanPageMeta) const { routes, imports } = normalizeRoutes(app.pages, new Set(), {
serverComponentRuntime,
clientComponentRuntime,
overrideMeta: !!nuxt.options.experimental.scanPageMeta,
})
return [...imports, `export default ${routes}`].join('\n') return [...imports, `export default ${routes}`].join('\n')
}, },
}) })

View File

@ -176,8 +176,10 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin
// https://github.com/vuejs/vue-loader/pull/1911 // https://github.com/vuejs/vue-loader/pull/1911
// https://github.com/vitejs/vite/issues/8473 // https://github.com/vitejs/vite/issues/8473
const QUERY_START_RE = /^\?/
const MACRO_RE = /&macro=true/
function rewriteQuery (id: string) { function rewriteQuery (id: string) {
return id.replace(/\?.+$/, r => '?macro=true&' + r.replace(/^\?/, '').replace(/&macro=true/, '')) return id.replace(/\?.+$/, r => '?macro=true&' + r.replace(QUERY_START_RE, '').replace(MACRO_RE, ''))
} }
function parseMacroQuery (id: string) { function parseMacroQuery (id: string) {
@ -189,6 +191,7 @@ function parseMacroQuery (id: string) {
return query return query
} }
const QUOTED_SPECIFIER_RE = /(["']).*\1/
function getQuotedSpecifier (id: string) { function getQuotedSpecifier (id: string) {
return id.match(/(["']).*\1/)?.[0] return id.match(QUOTED_SPECIFIER_RE)?.[0]
} }

View File

@ -1,6 +1,6 @@
import type { KeepAliveProps, TransitionProps, UnwrapRef } from 'vue' import type { KeepAliveProps, TransitionProps, UnwrapRef } from 'vue'
import { getCurrentInstance } from 'vue' import { getCurrentInstance } from 'vue'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRedirectOption } from 'vue-router' import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import type { NitroRouteConfig } from 'nitro/types' import type { NitroRouteConfig } from 'nitro/types'
import { useNuxtApp } from '#app/nuxt' import { useNuxtApp } from '#app/nuxt'
@ -37,6 +37,11 @@ export interface PageMeta {
name?: string name?: string
/** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */ /** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */
path?: string path?: string
/**
* Allows accessing the route `params` as props passed to the page component.
* @see https://router.vuejs.org/guide/essentials/passing-props
*/
props?: RouteRecordRaw['props']
/** Set to `false` to avoid scrolling to top on page navigations */ /** Set to `false` to avoid scrolling to top on page navigations */
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean) scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
} }

View File

@ -148,16 +148,8 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
if (import.meta.server && failure?.type === 4 /* ErrorTypes.NAVIGATION_ABORTED */) { if (import.meta.server && failure?.type === 4 /* ErrorTypes.NAVIGATION_ABORTED */) {
return return
} }
if (to.matched.length === 0) {
await nuxtApp.runWithContext(() => showError(createError({ if (import.meta.server && to.redirectedFrom && to.fullPath !== initialURL) {
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 || '/')) await nuxtApp.runWithContext(() => navigateTo(to.fullPath || '/'))
} }
}) })
@ -252,6 +244,19 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
await nuxtApp.callHook('page:loading:end') await nuxtApp.callHook('page:loading:end')
}) })
router.afterEach(async (to, _from) => {
if (to.matched.length === 0) {
await nuxtApp.runWithContext(() => showError(createError({
statusCode: 404,
fatal: false,
statusMessage: `Page not found: ${to.fullPath}`,
data: {
path: to.fullPath,
},
})))
}
})
nuxtApp.hooks.hookOnce('app:created', async () => { nuxtApp.hooks.hookOnce('app:created', async () => {
try { try {
// #4920, #4982 // #4920, #4982

View File

@ -5,11 +5,14 @@ type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
type RouterViewSlot = Exclude<InstanceOf<typeof RouterView>['$slots']['default'], undefined> type RouterViewSlot = Exclude<InstanceOf<typeof RouterView>['$slots']['default'], undefined>
export type RouterViewSlotProps = Parameters<RouterViewSlot>[0] export type RouterViewSlotProps = Parameters<RouterViewSlot>[0]
const ROUTE_KEY_PARENTHESES_RE = /(:\w+)\([^)]+\)/g
const ROUTE_KEY_SYMBOLS_RE = /(:\w+)[?+*]/g
const ROUTE_KEY_NORMAL_RE = /:\w+/g
const interpolatePath = (route: RouteLocationNormalizedLoaded, match: RouteLocationMatched) => { const interpolatePath = (route: RouteLocationNormalizedLoaded, match: RouteLocationMatched) => {
return match.path return match.path
.replace(/(:\w+)\([^)]+\)/g, '$1') .replace(ROUTE_KEY_PARENTHESES_RE, '$1')
.replace(/(:\w+)[?+*]/g, '$1') .replace(ROUTE_KEY_SYMBOLS_RE, '$1')
.replace(/:\w+/g, r => route.params[r.slice(1)]?.toString() || '') .replace(ROUTE_KEY_NORMAL_RE, r => route.params[r.slice(1)]?.toString() || '')
} }
export const generateRouteKey = (routeProps: RouterViewSlotProps, override?: string | ((route: RouteLocationNormalizedLoaded) => string)) => { export const generateRouteKey = (routeProps: RouterViewSlotProps, override?: string | ((route: RouteLocationNormalizedLoaded) => string)) => {

View File

@ -15,7 +15,6 @@ import type { NuxtPage } from 'nuxt/schema'
import { getLoader, uniqueBy } from '../core/utils' import { getLoader, uniqueBy } from '../core/utils'
import { toArray } from '../utils' import { toArray } from '../utils'
import { distDir } from '../dirs'
enum SegmentParserState { enum SegmentParserState {
initial, initial,
@ -65,18 +64,25 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
}) })
const pages = uniqueBy(allRoutes, 'path') const pages = uniqueBy(allRoutes, 'path')
const shouldAugment = nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages const shouldAugment = nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages
if (shouldAugment) { if (shouldAugment === false) {
await nuxt.callHook('pages:extend', pages)
return pages
}
if (shouldAugment === 'after-resolve') {
await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs)
} else {
const augmentedPages = await augmentPages(pages, nuxt.vfs) const augmentedPages = await augmentPages(pages, nuxt.vfs)
await nuxt.callHook('pages:extend', pages) await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs, augmentedPages) await augmentPages(pages, nuxt.vfs, augmentedPages)
augmentedPages.clear() augmentedPages.clear()
} else {
await nuxt.callHook('pages:extend', pages)
} }
await nuxt.callHook('pages:resolved', pages)
return pages return pages
} }
@ -84,6 +90,7 @@ type GenerateRoutesFromFilesOptions = {
shouldUseServerComponents?: boolean shouldUseServerComponents?: boolean
} }
const INDEX_PAGE_RE = /\/index$/
export function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): NuxtPage[] { export function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): NuxtPage[] {
const routes: NuxtPage[] = [] const routes: NuxtPage[] = []
@ -129,7 +136,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
route.name += (route.name && '/') + segmentName route.name += (route.name && '/') + segmentName
// ex: parent.vue + parent/child.vue // ex: parent.vue + parent/child.vue
const path = withLeadingSlash(joinURL(route.path, getRoutePath(tokens).replace(/\/index$/, '/'))) const path = withLeadingSlash(joinURL(route.path, getRoutePath(tokens).replace(INDEX_PAGE_RE, '/')))
const child = parent.find(parentRoute => parentRoute.name === route.name && parentRoute.path === path) const child = parent.find(parentRoute => parentRoute.name === route.name && parentRoute.path === path)
if (child && child.children) { if (child && child.children) {
@ -184,7 +191,7 @@ export function extractScriptContent (html: string) {
} }
const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/ const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const const extractionKeys = ['name', 'path', 'props', 'alias', 'redirect'] as const
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
const pageContentsCache: Record<string, string> = {} const pageContentsCache: Record<string, string> = {}
@ -266,7 +273,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
continue continue
} }
if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') { if (property.value.type !== 'Literal' || (typeof property.value.value !== 'string' && typeof property.value.value !== 'boolean')) {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`) console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
dynamicProperties.add(key) dynamicProperties.add(key)
continue continue
@ -301,6 +308,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
return extractedMeta return extractedMeta
} }
const COLON_RE = /:/g
function getRoutePath (tokens: SegmentToken[]): string { function getRoutePath (tokens: SegmentToken[]): string {
return tokens.reduce((path, token) => { return tokens.reduce((path, token) => {
return ( return (
@ -313,7 +321,7 @@ function getRoutePath (tokens: SegmentToken[]): string {
? `:${token.value}(.*)*` ? `:${token.value}(.*)*`
: token.type === SegmentTokenType.group : token.type === SegmentTokenType.group
? '' ? ''
: encodePath(token.value).replace(/:/g, '\\:')) : encodePath(token.value).replace(COLON_RE, '\\:'))
) )
}, '/') }, '/')
} }
@ -433,13 +441,14 @@ function findRouteByName (name: string, routes: NuxtPage[]): NuxtPage | undefine
return findRouteByName(name, routes) return findRouteByName(name, routes)
} }
const NESTED_PAGE_RE = /\//g
function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage, names = new Set<string>()) { function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage, names = new Set<string>()) {
for (const route of routes) { for (const route of routes) {
// Remove -index // Remove -index
if (route.name) { if (route.name) {
route.name = route.name route.name = route.name
.replace(/\/index$/, '') .replace(INDEX_PAGE_RE, '')
.replace(/\//g, '-') .replace(NESTED_PAGE_RE, '-')
if (names.has(route.name)) { if (names.has(route.name)) {
const existingRoute = findRouteByName(route.name, routes) const existingRoute = findRouteByName(route.name, routes)
@ -476,7 +485,12 @@ function serializeRouteValue (value: any, skipSerialisation = false) {
type NormalizedRoute = Partial<Record<Exclude<keyof NuxtPage, 'file'>, string>> & { component?: string } type NormalizedRoute = Partial<Record<Exclude<keyof NuxtPage, 'file'>, string>> & { component?: string }
type NormalizedRouteKeys = (keyof NormalizedRoute)[] type NormalizedRouteKeys = (keyof NormalizedRoute)[]
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set(), overrideMeta = false): { imports: Set<string>, routes: string } { interface NormalizeRoutesOptions {
overrideMeta?: boolean
serverComponentRuntime: string
clientComponentRuntime: string
}
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set(), options: NormalizeRoutesOptions): { imports: Set<string>, routes: string } {
return { return {
imports: metaImports, imports: metaImports,
routes: genArrayFromRaw(routes.map((page) => { routes: genArrayFromRaw(routes.map((page) => {
@ -506,7 +520,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
} }
if (page.children?.length) { if (page.children?.length) {
route.children = normalizeRoutes(page.children, metaImports, overrideMeta).routes route.children = normalizeRoutes(page.children, metaImports, options).routes
} }
// Without a file, we can't use `definePageMeta` to extract route-level meta from the file // Without a file, we can't use `definePageMeta` to extract route-level meta from the file
@ -528,6 +542,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
const metaRoute: NormalizedRoute = { const metaRoute: NormalizedRoute = {
name: `${metaImportName}?.name ?? ${route.name}`, name: `${metaImportName}?.name ?? ${route.name}`,
path: `${metaImportName}?.path ?? ${route.path}`, path: `${metaImportName}?.path ?? ${route.path}`,
props: `${metaImportName}?.props ?? false`,
meta: `${metaImportName} || {}`, meta: `${metaImportName} || {}`,
alias: `${metaImportName}?.alias || []`, alias: `${metaImportName}?.alias || []`,
redirect: `${metaImportName}?.redirect`, redirect: `${metaImportName}?.redirect`,
@ -542,14 +557,14 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
metaImports.add(` metaImports.add(`
let _createIslandPage let _createIslandPage
async function createIslandPage (name) { async function createIslandPage (name) {
_createIslandPage ||= await import(${JSON.stringify(resolve(distDir, 'components/runtime/server-component'))}).then(r => r.createIslandPage) _createIslandPage ||= await import(${JSON.stringify(options?.serverComponentRuntime)}).then(r => r.createIslandPage)
return _createIslandPage(name) return _createIslandPage(name)
};`) };`)
} else if (page.mode === 'client') { } else if (page.mode === 'client') {
metaImports.add(` metaImports.add(`
let _createClientPage let _createClientPage
async function createClientPage(loader) { async function createClientPage(loader) {
_createClientPage ||= await import(${JSON.stringify(resolve(distDir, 'components/runtime/client-component'))}).then(r => r.createClientPage) _createClientPage ||= await import(${JSON.stringify(options?.clientComponentRuntime)}).then(r => r.createClientPage)
return _createClientPage(loader); return _createClientPage(loader);
}`) }`)
} }
@ -562,7 +577,7 @@ async function createClientPage(loader) {
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }` metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`
} }
if (overrideMeta) { if (options?.overrideMeta) {
// skip and retain fallback if marked dynamic // skip and retain fallback if marked dynamic
// set to extracted value or fallback if none extracted // set to extracted value or fallback if none extracted
for (const key of ['name', 'path'] satisfies NormalizedRouteKeys) { for (const key of ['name', 'path'] satisfies NormalizedRouteKeys) {
@ -571,7 +586,7 @@ async function createClientPage(loader) {
} }
// set to extracted value or delete if none extracted // set to extracted value or delete if none extracted
for (const key of ['meta', 'alias', 'redirect'] satisfies NormalizedRouteKeys) { for (const key of ['meta', 'alias', 'redirect', 'props'] satisfies NormalizedRouteKeys) {
if (markedDynamic.has(key)) { continue } if (markedDynamic.has(key)) { continue }
if (route[key] == null) { if (route[key] == null) {
@ -596,6 +611,7 @@ async function createClientPage(loader) {
} }
} }
const PATH_TO_NITRO_GLOB_RE = /\/[^:/]*:\w.*$/
export function pathToNitroGlob (path: string) { export function pathToNitroGlob (path: string) {
if (!path) { if (!path) {
return null return null
@ -605,7 +621,7 @@ export function pathToNitroGlob (path: string) {
return null return null
} }
return path.replace(/\/[^:/]*:\w.*$/, '/**') return path.replace(PATH_TO_NITRO_GLOB_RE, '/**')
} }
export function resolveRoutePaths (page: NuxtPage, parent = '/'): string[] { export function resolveRoutePaths (page: NuxtPage, parent = '/'): string[] {

View File

@ -6,6 +6,7 @@
"meta": "{ ...(mockMeta || {}), ...{"someMetaData":true} }", "meta": "{ ...(mockMeta || {}), ...{"someMetaData":true} }",
"name": "mockMeta?.name ?? "pushed-route"", "name": "mockMeta?.name ?? "pushed-route"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -24,6 +25,18 @@
"meta": "{ ...(mockMeta || {}), ...{"test":1} }", "meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": "mockMeta?.name ?? "page-with-meta"", "name": "mockMeta?.name ?? "page-with-meta"",
"path": "mockMeta?.path ?? "/page-with-meta"", "path": "mockMeta?.path ?? "/page-with-meta"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect",
},
],
"route.meta props generate by file": [
{
"alias": "mockMeta?.alias || []",
"component": "() => import("pages/page-with-props.vue")",
"meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "page-with-props"",
"path": "mockMeta?.path ?? "/page-with-props"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -34,6 +47,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "test:name"", "name": "mockMeta?.name ?? "test:name"",
"path": "mockMeta?.path ?? "/test\\:name"", "path": "mockMeta?.path ?? "/test\\:name"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -50,6 +64,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "param-index"", "name": "mockMeta?.name ?? "param-index"",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -58,6 +73,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "param-index-sibling"", "name": "mockMeta?.name ?? "param-index-sibling"",
"path": "mockMeta?.path ?? "sibling"", "path": "mockMeta?.path ?? "sibling"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -65,6 +81,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -73,6 +90,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "param-sibling"", "name": "mockMeta?.name ?? "param-sibling"",
"path": "mockMeta?.path ?? "sibling"", "path": "mockMeta?.path ?? "sibling"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -80,6 +98,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/param"", "path": "mockMeta?.path ?? "/param"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -91,6 +110,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "wrapper-expose-other"", "name": "mockMeta?.name ?? "wrapper-expose-other"",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -99,6 +119,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "wrapper-expose-other-sibling"", "name": "mockMeta?.name ?? "wrapper-expose-other-sibling"",
"path": "mockMeta?.path ?? "sibling"", "path": "mockMeta?.path ?? "sibling"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -106,6 +127,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/wrapper-expose/other"", "path": "mockMeta?.path ?? "/wrapper-expose/other"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -116,6 +138,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "home"", "name": "mockMeta?.name ?? "home"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": ""/"", "redirect": ""/"",
}, },
], ],
@ -126,6 +149,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "slug"", "name": "mockMeta?.name ?? "slug"",
"path": "mockMeta?.path ?? "/:slug(.*)*"", "path": "mockMeta?.path ?? "/:slug(.*)*"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -134,6 +158,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -144,6 +169,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -152,6 +178,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "slug"", "name": "mockMeta?.name ?? "slug"",
"path": "mockMeta?.path ?? "/:slug()"", "path": "mockMeta?.path ?? "/:slug()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -163,6 +190,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "foo"", "name": "mockMeta?.name ?? "foo"",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -170,6 +198,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/:foo?"", "path": "mockMeta?.path ?? "/:foo?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -178,6 +207,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-opt"", "name": "mockMeta?.name ?? "optional-opt"",
"path": "mockMeta?.path ?? "/optional/:opt?"", "path": "mockMeta?.path ?? "/optional/:opt?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -186,6 +216,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-prefix-opt"", "name": "mockMeta?.name ?? "optional-prefix-opt"",
"path": "mockMeta?.path ?? "/optional/prefix-:opt?"", "path": "mockMeta?.path ?? "/optional/prefix-:opt?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -194,6 +225,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-opt-postfix"", "name": "mockMeta?.name ?? "optional-opt-postfix"",
"path": "mockMeta?.path ?? "/optional/:opt?-postfix"", "path": "mockMeta?.path ?? "/optional/:opt?-postfix"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -202,6 +234,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "optional-prefix-opt-postfix"", "name": "mockMeta?.name ?? "optional-prefix-opt-postfix"",
"path": "mockMeta?.path ?? "/optional/prefix-:opt?-postfix"", "path": "mockMeta?.path ?? "/optional/prefix-:opt?-postfix"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -210,6 +243,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "bar"", "name": "mockMeta?.name ?? "bar"",
"path": "mockMeta?.path ?? "/:bar()"", "path": "mockMeta?.path ?? "/:bar()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -218,6 +252,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "nonopt-slug"", "name": "mockMeta?.name ?? "nonopt-slug"",
"path": "mockMeta?.path ?? "/nonopt/:slug()"", "path": "mockMeta?.path ?? "/nonopt/:slug()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -226,6 +261,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "opt-slug"", "name": "mockMeta?.name ?? "opt-slug"",
"path": "mockMeta?.path ?? "/opt/:slug?"", "path": "mockMeta?.path ?? "/opt/:slug?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -234,6 +270,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "sub-route-slug"", "name": "mockMeta?.name ?? "sub-route-slug"",
"path": "mockMeta?.path ?? "/:sub?/route-:slug()"", "path": "mockMeta?.path ?? "/:sub?/route-:slug()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -244,6 +281,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories"", "name": "mockMeta?.name ?? "stories"",
"path": "mockMeta?.path ?? "/:stories(.*)*"", "path": "mockMeta?.path ?? "/:stories(.*)*"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -252,6 +290,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories-id"", "name": "mockMeta?.name ?? "stories-id"",
"path": "mockMeta?.path ?? "/stories/:id()"", "path": "mockMeta?.path ?? "/stories/:id()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -262,6 +301,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories-id"", "name": "mockMeta?.name ?? "stories-id"",
"path": "mockMeta?.path ?? "/stories/:id()"", "path": "mockMeta?.path ?? "/stories/:id()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -270,6 +310,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "stories"", "name": "mockMeta?.name ?? "stories"",
"path": "mockMeta?.path ?? "/:stories(.*)*"", "path": "mockMeta?.path ?? "/:stories(.*)*"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -280,6 +321,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "kebab-case"", "name": "mockMeta?.name ?? "kebab-case"",
"path": "mockMeta?.path ?? "/kebab-case"", "path": "mockMeta?.path ?? "/kebab-case"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -290,6 +332,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "snake_case"", "name": "mockMeta?.name ?? "snake_case"",
"path": "mockMeta?.path ?? "/snake_case"", "path": "mockMeta?.path ?? "/snake_case"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -300,6 +343,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -308,6 +352,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent"", "name": "mockMeta?.name ?? "parent"",
"path": "mockMeta?.path ?? "/parent"", "path": "mockMeta?.path ?? "/parent"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -316,6 +361,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"", "name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "/parent/child"", "path": "mockMeta?.path ?? "/parent/child"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -329,6 +375,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"", "name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "child"", "path": "mockMeta?.path ?? "child"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -336,6 +383,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent"", "name": "mockMeta?.name ?? "parent"",
"path": "mockMeta?.path ?? "/parent"", "path": "mockMeta?.path ?? "/parent"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -346,6 +394,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -357,6 +406,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "about"", "name": "mockMeta?.name ?? "about"",
"path": "mockMeta?.path ?? """, "path": "mockMeta?.path ?? """,
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -364,6 +414,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? undefined", "name": "mockMeta?.name ?? undefined",
"path": "mockMeta?.path ?? "/about"", "path": "mockMeta?.path ?? "/about"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -377,6 +428,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index-index-all"", "name": "mockMeta?.name ?? "index-index-all"",
"path": "mockMeta?.path ?? "all"", "path": "mockMeta?.path ?? "all"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -384,6 +436,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -394,6 +447,7 @@
"meta": "{ ...(mockMeta || {}), ...{"test":1} }", "meta": "{ ...(mockMeta || {}), ...{"test":1} }",
"name": "mockMeta?.name ?? "page-with-meta"", "name": "mockMeta?.name ?? "page-with-meta"",
"path": "mockMeta?.path ?? "/page-with-meta"", "path": "mockMeta?.path ?? "/page-with-meta"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -404,6 +458,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"", "name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "/parent/:child()"", "path": "mockMeta?.path ?? "/parent/:child()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -412,6 +467,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "parent-child"", "name": "mockMeta?.name ?? "parent-child"",
"path": "mockMeta?.path ?? "/parent-:child()"", "path": "mockMeta?.path ?? "/parent-:child()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -422,6 +478,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "foo"", "name": "mockMeta?.name ?? "foo"",
"path": "mockMeta?.path ?? "/:foo?"", "path": "mockMeta?.path ?? "/:foo?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -430,6 +487,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "foo"", "name": "mockMeta?.name ?? "foo"",
"path": "mockMeta?.path ?? "/:foo()"", "path": "mockMeta?.path ?? "/:foo()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -440,6 +498,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "a1_1a"", "name": "mockMeta?.name ?? "a1_1a"",
"path": "mockMeta?.path ?? "/:a1_1a()"", "path": "mockMeta?.path ?? "/:a1_1a()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -448,6 +507,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "b2.2b"", "name": "mockMeta?.name ?? "b2.2b"",
"path": "mockMeta?.path ?? "/:b2.2b()"", "path": "mockMeta?.path ?? "/:b2.2b()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -456,6 +516,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "b2_2b"", "name": "mockMeta?.name ?? "b2_2b"",
"path": "mockMeta?.path ?? "/:b2()_:2b()"", "path": "mockMeta?.path ?? "/:b2()_:2b()"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -464,6 +525,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "c33c"", "name": "mockMeta?.name ?? "c33c"",
"path": "mockMeta?.path ?? "/:c33c?"", "path": "mockMeta?.path ?? "/:c33c?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
{ {
@ -472,6 +534,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "d44d"", "name": "mockMeta?.name ?? "d44d"",
"path": "mockMeta?.path ?? "/:d44d?"", "path": "mockMeta?.path ?? "/:d44d?"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -482,6 +545,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "home"", "name": "mockMeta?.name ?? "home"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],
@ -492,6 +556,7 @@
"meta": "mockMeta || {}", "meta": "mockMeta || {}",
"name": "mockMeta?.name ?? "index"", "name": "mockMeta?.name ?? "index"",
"path": "mockMeta?.path ?? "/"", "path": "mockMeta?.path ?? "/"",
"props": "mockMeta?.props ?? false",
"redirect": "mockMeta?.redirect", "redirect": "mockMeta?.redirect",
}, },
], ],

View File

@ -24,6 +24,13 @@
"path": ""/page-with-meta"", "path": ""/page-with-meta"",
}, },
], ],
"route.meta props generate by file": [
{
"component": "() => import("pages/page-with-props.vue")",
"name": ""page-with-props"",
"path": ""/page-with-props"",
},
],
"should allow pages with `:` in their path": [ "should allow pages with `:` in their path": [
{ {
"component": "() => import("pages/test:name.vue")", "component": "() => import("pages/test:name.vue")",

View File

@ -92,7 +92,11 @@ function createTransformer (components: Component[], mode: 'client' | 'server' |
}, },
}, },
} as Nuxt } as Nuxt
const plugin = TransformPlugin(stubNuxt, () => components, mode).vite() const plugin = TransformPlugin(stubNuxt, {
mode,
getComponents: () => components,
serverComponentRuntime: '<repo>/nuxt/src/components/runtime/server-component',
}).vite()
return async (code: string, id: string) => { return async (code: string, id: string) => {
const result = await (plugin as any).transform!(code, id) const result = await (plugin as any).transform!(code, id)

View File

@ -168,7 +168,11 @@ describe('normalizeRoutes', () => {
page.meta.layout = 'test' page.meta.layout = 'test'
page.meta.foo = 'bar' page.meta.foo = 'bar'
const { routes, imports } = normalizeRoutes([page], new Set(), true) const { routes, imports } = normalizeRoutes([page], new Set(), {
clientComponentRuntime: '<client-component-runtime>',
serverComponentRuntime: '<server-component-runtime>',
overrideMeta: true,
})
expect({ routes, imports }).toMatchInlineSnapshot(` expect({ routes, imports }).toMatchInlineSnapshot(`
{ {
"imports": Set { "imports": Set {
@ -193,7 +197,11 @@ describe('normalizeRoutes', () => {
page.meta.layout = 'test' page.meta.layout = 'test'
page.meta.foo = 'bar' page.meta.foo = 'bar'
const { routes, imports } = normalizeRoutes([page], new Set()) const { routes, imports } = normalizeRoutes([page], new Set(), {
clientComponentRuntime: '<client-component-runtime>',
serverComponentRuntime: '<server-component-runtime>',
overrideMeta: false,
})
expect({ routes, imports }).toMatchInlineSnapshot(` expect({ routes, imports }).toMatchInlineSnapshot(`
{ {
"imports": Set { "imports": Set {
@ -203,6 +211,7 @@ describe('normalizeRoutes', () => {
{ {
name: indexN6pT4Un8hYMeta?.name ?? undefined, name: indexN6pT4Un8hYMeta?.name ?? undefined,
path: indexN6pT4Un8hYMeta?.path ?? "/", path: indexN6pT4Un8hYMeta?.path ?? "/",
props: indexN6pT4Un8hYMeta?.props ?? false,
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} }, meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
alias: indexN6pT4Un8hYMeta?.alias || [], alias: indexN6pT4Un8hYMeta?.alias || [],
redirect: indexN6pT4Un8hYMeta?.redirect, redirect: indexN6pT4Un8hYMeta?.redirect,

View File

@ -601,6 +601,30 @@ describe('pages:generateRoutesFromFiles', () => {
}, },
], ],
}, },
{
description: 'route.meta props generate by file',
files: [
{
path: `${pagesDir}/page-with-props.vue`,
template: `
<script setup lang="ts">
definePageMeta({
props: true
})
</script>
`,
},
],
output: [
{
name: 'page-with-props',
path: '/page-with-props',
file: `${pagesDir}/page-with-props.vue`,
children: [],
props: true,
},
],
},
{ {
description: 'should handle route groups', description: 'should handle route groups',
files: [ files: [
@ -667,8 +691,18 @@ describe('pages:generateRoutesFromFiles', () => {
if (result) { if (result) {
expect(result).toEqual(test.output) expect(result).toEqual(test.output)
normalizedResults[test.description] = normalizeRoutes(result, new Set()).routes
normalizedOverrideMetaResults[test.description] = normalizeRoutes(result, new Set(), true).routes normalizedResults[test.description] = normalizeRoutes(result, new Set(), {
clientComponentRuntime: '<client-component-runtime>',
serverComponentRuntime: '<server-component-runtime>',
overrideMeta: false,
}).routes
normalizedOverrideMetaResults[test.description] = normalizeRoutes(result, new Set(), {
clientComponentRuntime: '<client-component-runtime>',
serverComponentRuntime: '<server-component-runtime>',
overrideMeta: true,
}).routes
} }
}) })
} }

View File

@ -0,0 +1,18 @@
import { defineBuildConfig } from 'unbuild'
import config from '../webpack/build.config'
export default defineBuildConfig({
...config[0],
externals: [
'@rspack/core',
'#builder',
'@nuxt/schema',
],
entries: [
{
input: '../webpack/src/index',
name: 'index',
declaration: true,
},
],
})

View File

@ -0,0 +1,5 @@
import webpack from '@rspack/core'
export const builder = 'rspack'
export { webpack }
export const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin

View File

@ -0,0 +1,94 @@
{
"name": "@nuxt/rspack-builder",
"version": "3.12.2",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
"directory": "packages/rspack"
},
"description": "rspack bundler for Nuxt",
"homepage": "https://nuxt.com",
"license": "MIT",
"type": "module",
"types": "./dist/index.d.ts",
"imports": {
"#builder": "./builder.mjs"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
},
"./dist/*": "./dist/*"
},
"files": [
"dist",
"builder.mjs"
],
"scripts": {
"prepack": "unbuild"
},
"dependencies": {
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
"@nuxt/kit": "workspace:*",
"@rspack/core": "^1.0.14",
"autoprefixer": "^10.4.20",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"cssnano": "^7.0.6",
"defu": "^6.1.4",
"esbuild-loader": "^4.2.2",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.2",
"globby": "^14.0.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hash-sum": "^2.0.0",
"jiti": "^2.3.3",
"knitwork": "^1.1.0",
"lodash-es": "4.17.21",
"magic-string": "^0.30.12",
"memfs": "^4.14.0",
"mlly": "^1.7.2",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
"pify": "^6.1.0",
"postcss": "^8.4.47",
"postcss-import": "^16.1.0",
"postcss-import-resolver": "^2.0.0",
"postcss-loader": "^8.1.1",
"postcss-url": "^10.1.3",
"pug-plain-loader": "^1.1.0",
"std-env": "^3.7.0",
"time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^1.14.1",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-middleware": "^7.4.2",
"webpack-hot-middleware": "^2.26.1",
"webpack-virtual-modules": "^0.6.2",
"webpackbar": "^6.0.1"
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"@types/hash-sum": "1.0.2",
"@types/lodash-es": "4.17.12",
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.24.0",
"unbuild": "3.0.0-rc.11",
"vue": "3.5.12"
},
"peerDependencies": {
"vue": "^3.3.4"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
}

View File

@ -39,23 +39,22 @@
"@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.9", "@types/sass-loader": "8.0.9",
"@unhead/schema": "1.11.6", "@unhead/schema": "1.11.10",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",
"@vitejs/plugin-vue-jsx": "4.0.1", "@vitejs/plugin-vue-jsx": "4.0.1",
"@vue/compiler-core": "3.5.10", "@vue/compiler-core": "3.5.12",
"@vue/compiler-sfc": "3.5.10", "@vue/compiler-sfc": "3.5.12",
"@vue/language-core": "2.1.6", "@vue/language-core": "2.1.6",
"c12": "2.0.0-beta.3",
"esbuild-loader": "4.2.2", "esbuild-loader": "4.2.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"ignore": "6.0.2", "ignore": "6.0.2",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda", "nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"ofetch": "1.4.0", "ofetch": "1.4.1",
"unbuild": "3.0.0-rc.8", "unbuild": "3.0.0-rc.11",
"unctx": "2.3.1", "unctx": "2.3.1",
"unenv": "1.10.0", "unenv": "1.10.0",
"vite": "5.4.8", "vite": "5.4.10",
"vue": "3.5.10", "vue": "3.5.12",
"vue-bundle-renderer": "2.1.1", "vue-bundle-renderer": "2.1.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue-router": "4.4.5", "vue-router": "4.4.5",
@ -63,18 +62,19 @@
"webpack-dev-middleware": "7.4.2" "webpack-dev-middleware": "7.4.2"
}, },
"dependencies": { "dependencies": {
"c12": "^2.0.1",
"compatx": "^0.1.8", "compatx": "^0.1.8",
"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.2.0", "pkg-types": "^1.2.1",
"scule": "^1.3.0", "scule": "^1.3.0",
"std-env": "^3.7.0", "std-env": "^3.7.0",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
"unimport": "^3.13.1", "unimport": "^3.13.1",
"untyped": "^1.5.0" "untyped": "^1.5.1"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"

View File

@ -7,14 +7,15 @@ import { consola } from 'consola'
export default defineUntypedSchema({ export default defineUntypedSchema({
/** /**
* The builder to use for bundling the Vue part of your application. * The builder to use for bundling the Vue part of your application.
* @type {'vite' | 'webpack' | { bundle: (nuxt: typeof import('../src/types/nuxt').Nuxt) => Promise<void> }} * @type {'vite' | 'webpack' | 'rspack' | { bundle: (nuxt: typeof import('../src/types/nuxt').Nuxt) => Promise<void> }}
*/ */
builder: { builder: {
$resolve: async (val: 'vite' | 'webpack' | { bundle: (nuxt: unknown) => Promise<void> } | undefined = 'vite', get) => { $resolve: async (val: 'vite' | 'webpack' | 'rspack' | { bundle: (nuxt: unknown) => Promise<void> } | undefined = 'vite', get) => {
if (typeof val === 'object') { if (typeof val === 'object') {
return val return val
} }
const map: Record<string, string> = { const map: Record<string, string> = {
rspack: '@nuxt/rspack-builder',
vite: '@nuxt/vite-builder', vite: '@nuxt/vite-builder',
webpack: '@nuxt/webpack-builder', webpack: '@nuxt/webpack-builder',
} }

View File

@ -424,7 +424,7 @@ export default defineUntypedSchema({
*/ */
alias: { alias: {
$resolve: async (val: Record<string, string>, get): Promise<Record<string, string>> => { $resolve: async (val: Record<string, string>, get): Promise<Record<string, string>> => {
const [srcDir, rootDir, assetsDir, publicDir] = await Promise.all([get('srcDir'), get('rootDir'), get('dir.assets'), get('dir.public')]) as [string, string, string, string] const [srcDir, rootDir, assetsDir, publicDir, buildDir] = await Promise.all([get('srcDir'), get('rootDir'), get('dir.assets'), get('dir.public'), get('buildDir')]) as [string, string, string, string, string]
return { return {
'~': srcDir, '~': srcDir,
'@': srcDir, '@': srcDir,
@ -432,6 +432,8 @@ export default defineUntypedSchema({
'@@': rootDir, '@@': rootDir,
[basename(assetsDir)]: resolve(srcDir, assetsDir), [basename(assetsDir)]: resolve(srcDir, assetsDir),
[basename(publicDir)]: resolve(srcDir, publicDir), [basename(publicDir)]: resolve(srcDir, publicDir),
'#build': buildDir,
'#internal/nuxt/paths': resolve(buildDir, 'paths.mjs'),
...val, ...val,
} }
}, },

View File

@ -297,8 +297,13 @@ export default defineUntypedSchema({
* This only works with static or strings/arrays rather than variables or conditional assignment. * This only works with static or strings/arrays rather than variables or conditional assignment.
* *
* @see [Nuxt Issues #24770](https://github.com/nuxt/nuxt/issues/24770) * @see [Nuxt Issues #24770](https://github.com/nuxt/nuxt/issues/24770)
* @type {boolean | 'after-resolve'}
*/ */
scanPageMeta: true, scanPageMeta: {
async $resolve (val, get) {
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? 'after-resolve' : true)
},
},
/** /**
* Automatically share payload _data_ between pages that are prerendered. This can result in a significant * Automatically share payload _data_ between pages that are prerendered. This can result in a significant

View File

@ -20,7 +20,7 @@ export default defineUntypedSchema({
* builder environment types (with `false`) to handle this fully yourself, or opt for a 'shared' option. * builder environment types (with `false`) to handle this fully yourself, or opt for a 'shared' option.
* *
* The 'shared' option is advised for module authors, who will want to support multiple possible builders. * The 'shared' option is advised for module authors, who will want to support multiple possible builders.
* @type {'vite' | 'webpack' | 'shared' | false | undefined} * @type {'vite' | 'webpack' | 'rspack' | 'shared' | false | undefined}
*/ */
builder: { builder: {
$resolve: val => val ?? null, $resolve: val => val ?? null,

View File

@ -6,7 +6,7 @@ export type { GenerateAppOptions, HookResult, ImportPresetWithDeprecation, NuxtA
export type { ImportsOptions } from './types/imports' export type { ImportsOptions } from './types/imports'
export type { AppHeadMetaObject, MetaObject, MetaObjectRaw, HeadAugmentations } from './types/head' export type { AppHeadMetaObject, MetaObject, MetaObjectRaw, HeadAugmentations } from './types/head'
export type { ModuleDefinition, ModuleMeta, ModuleOptions, ModuleSetupInstallResult, ModuleSetupReturn, NuxtModule, ResolvedModuleOptions } from './types/module' export type { ModuleDefinition, ModuleMeta, ModuleOptions, ModuleSetupInstallResult, ModuleSetupReturn, NuxtModule, ResolvedModuleOptions } from './types/module'
export type { Nuxt, NuxtApp, NuxtPlugin, NuxtPluginTemplate, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate } from './types/nuxt' export type { Nuxt, NuxtApp, NuxtPlugin, NuxtPluginTemplate, NuxtTemplate, NuxtTypeTemplate, NuxtServerTemplate, ResolvedNuxtTemplate } from './types/nuxt'
export type { RouterConfig, RouterConfigSerializable, RouterOptions } from './types/router' export type { RouterConfig, RouterConfigSerializable, RouterOptions } from './types/router'
// Schema // Schema

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