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 && \
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
steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,9 +23,9 @@ jobs:
runs-on: ubuntu-latest
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
- name: Check workflow files
uses: docker://rhysd/actionlint:1.7.2@sha256:89d3f90f82781dee3c8724651129634b08cf2241bbd67fcd02a1c5198119fc5e
uses: docker://rhysd/actionlint:1.7.3@sha256:7617f05bd698cd2f1c3aedc05bc733ccec92cca0738f3e8722c32c5b42c70ae6
with:
args: -color

View File

@ -24,7 +24,7 @@ jobs:
steps:
- 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
with:
route: GET /repos/nuxt/nuxt/collaborators/${{ github.event.comment.user.login }}
@ -48,13 +48,13 @@ jobs:
fi
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ steps.pr.outputs.head_sha }}
fetch-depth: 1
- run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
cache: "pnpm"

View File

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

View File

@ -10,7 +10,7 @@ jobs:
reproduire:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp
with:
label: needs reproduction

View File

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

View File

@ -11,7 +11,7 @@ jobs:
stackblitz:
runs-on: ubuntu-latest
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
with:
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}`.
::
::tip
It is possible to set environment variables for testing by using the `.env.test` file.
::
### 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.
@ -285,7 +289,7 @@ import { mockNuxtImport } from '@nuxt/test-utils/runtime'
const { useStorageMock } = vi.hoisted(() => {
return {
useStorageMock: vi.fn().mockImplementation(() => {
useStorageMock: vi.fn(() => {
return { value: 'mocked storage'}
})
}

View File

@ -67,6 +67,7 @@ export default defineNuxtConfig({
// app: 'app'
// },
// experimental: {
// scanPageMeta: 'after-resolve',
// sharedPrerenderData: false,
// compileTemplate: true,
// resetAsyncDataToUndefined: true,
@ -178,6 +179,7 @@ nuxt.config.ts
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. 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
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
🚦 **Impact Level**: Medium

View File

@ -530,7 +530,7 @@ export default defineNuxtConfig({
hooks: {
'build:manifest': (manifest) => {
// 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) {
// start from the end of the array and go to the beginning
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
```bash [Directory Structure]
| pages/
-| pages/
---| about.vue
---| index.vue
---| posts/

View File

@ -8,21 +8,17 @@ Nuxt comes with two composables and a built-in library to perform data-fetching
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 great to make network requests based on user interaction.
- [`useAsyncData`](/docs/api/composables/use-async-data), combined with `$fetch`, offers more fine-grained control.
- [`$fetch`](/docs/api/utils/dollarfetch) is the simplest way to make a network request.
- [`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) 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.
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.
### 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 [`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.
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**.
::
```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
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
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`
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]
<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"}
## `$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`
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
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.
Making a static page interactive in the browser is called "Hydration".
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.
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)
**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:**
- **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.
**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).
```bash [Directory Structure]
| components/
--| AppHeader.vue
--| AppFooter.vue
-| components/
---| AppHeader.vue
---| AppFooter.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:
```bash [Directory Structure]
| components/
--| base/
----| foo/
------| Button.vue
-| components/
---| base/
-----| foo/
-------| 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:
@ -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.
```bash [Directory Structure]
| components/
--| HighlightedMarkdown.server.vue
-| components/
---| HighlightedMarkdown.server.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.
```bash [Directory Structure]
| components/
--| Comments.client.vue
--| Comments.server.vue
-| components/
---| Comments.client.vue
---| Comments.server.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:
```bash [Directory Structure]
| node_modules/
-| node_modules/
---| awesome-ui/
------| components/
---------| Alert.vue
---------| Button.vue
------| nuxt.js
| pages/
-----| components/
-------| Alert.vue
-------| Button.vue
-----| nuxt.js
-| pages/
---| index.vue
| nuxt.config.js
-| nuxt.config.js
```
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.:
```bash [Directory Structure]
| composables/
-| composables/
---| index.ts // scanned
---| useFoo.ts // scanned
-----| nested/
-------| utils.ts // not scanned
---| nested/
-----| utils.ts // not scanned
```
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:
```text [middleware/ directory]
middleware/
--| analytics.global.ts
--| setup.global.ts
--| auth.ts
```bash [middleware/ directory]
-| middleware/
---| analytics.global.ts
---| setup.global.ts
---| auth.ts
```
```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.
```text [Directory structure]
middleware/
--| 01.setup.global.ts
--| 02.analytics.global.ts
--| auth.ts
```bash [Directory structure]
-| middleware/
---| 01.setup.global.ts
---| 02.analytics.global.ts
---| auth.ts
```
::note

View File

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

View File

@ -108,7 +108,7 @@ In case you're new to 'alphabetical' numbering, remember that filenames are sort
### 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]
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.
::
::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
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]
<script setup lang="ts">
@ -41,7 +45,23 @@ console.log(appConfig.theme)
</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

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.
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.
```ts twoslash [nuxt.config.ts]

View File

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

View File

@ -31,14 +31,8 @@ export default defineNuxtPlugin((nuxtApp) => {
baseURL: 'https://api.nuxt.com',
onRequest({ request, options, error }) {
if (session.value?.token) {
const headers = options.headers ||= {}
if (Array.isArray(headers)) {
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}`
}
// note that this relies on ofetch >= 1.4.0 - you may need to refresh your lockfile
options.headers.set('Authorization', `Bearer ${session.value?.token}`)
}
},
async onResponseError({ response }) {
@ -96,6 +90,28 @@ const { data: modules } = await useAPI('/modules')
</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
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', {
onRequest({ request, options }) {
// Set the request headers
options.headers = options.headers || {}
options.headers.authorization = '...'
// note that this relies on ofetch >= 1.4.0 - you may need to refresh your lockfile
options.headers.set('Authorization', '...')
},
onRequestError({ request, options, error }) {
// 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
name?: string
path?: string
props?: RouteRecordRaw['props']
alias?: string | string[]
pageTransition?: 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.
**`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`**
- **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 `'/'`.
#### 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)
**Type**: `NavigateToOptions`

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
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 { useNuxt } from './context'
import { toArray } from './utils'
@ -36,16 +37,7 @@ export interface ExtendWebpackConfigOptions extends ExtendConfigOptions {}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ExtendViteConfigOptions extends ExtendConfigOptions {}
/**
* 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 extendWebpackCompatibleConfig = (builder: 'rspack' | 'webpack') => (fn: ((config: WebpackConfig) => void), options: ExtendWebpackConfigOptions = {}) => {
const nuxt = useNuxt()
if (options.dev === false && nuxt.options.dev) {
@ -55,7 +47,7 @@ export function extendWebpackConfig (
return
}
nuxt.hook('webpack:config', (configs: WebpackConfig[]) => {
nuxt.hook(`${builder}:config`, (configs) => {
if (options.server !== false) {
const config = configs.find(i => i.name === 'server')
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
*/
export function extendViteConfig (
fn: ((config: ViteConfig) => void),
options: ExtendViteConfigOptions = {},
) {
export function extendViteConfig (fn: ((config: ViteConfig) => void), options: ExtendViteConfigOptions = {}) {
const nuxt = useNuxt()
if (options.dev === false && nuxt.options.dev) {
@ -114,6 +118,18 @@ export function addWebpackPlugin (pluginOrGetter: WebpackPluginInstance | Webpac
config.plugins[method](...toArray(plugin))
}, 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.
@ -131,6 +147,7 @@ export function addVitePlugin (pluginOrGetter: VitePlugin | VitePlugin[] | (() =
interface AddBuildPluginFactory {
vite?: () => VitePlugin | VitePlugin[]
webpack?: () => WebpackPluginInstance | WebpackPluginInstance[]
rspack?: () => RspackPluginInstance | RspackPluginInstance[]
}
export function addBuildPlugin (pluginFactory: AddBuildPluginFactory, options?: ExtendConfigOptions) {
@ -141,4 +158,8 @@ export function addBuildPlugin (pluginFactory: AddBuildPluginFactory, options?:
if (pluginFactory.webpack) {
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 { useNuxt } from './context'
const SEMANTIC_VERSION_RE = /-\d+\.[0-9a-f]+/
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 = {
'@nuxt/rspack-builder': 'rspack',
'@nuxt/vite-builder': 'vite',
'@nuxt/webpack-builder': 'webpack',
}
@ -103,6 +105,7 @@ export function isNuxt3 (nuxt: Nuxt = useNuxt()) {
return isNuxtMajorVersion(3, nuxt)
}
const NUXT_VERSION_RE = /^v/g
/**
* Get nuxt version
*/
@ -111,5 +114,5 @@ export function getNuxtVersion (nuxt: Nuxt | any = useNuxt() /* TODO: LegacyNuxt
if (typeof rawVersion !== 'string') {
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 { assertNuxtCompatibility } from './compatibility'
import { logger } from './logger'
import { MODE_RE } from './utils'
/**
* 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 || []
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'
}

View File

@ -13,7 +13,7 @@ export type { LoadNuxtOptions } from './loader/nuxt'
// Utils
export { addImports, addImportsDir, addImportsSources } from './imports'
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 { assertNuxtCompatibility, checkNuxtCompatibility, getNuxtVersion, hasNuxtCompatibility, isNuxtMajorVersion, normalizeSemanticVersion, isNuxt2, isNuxt3 } from './compatibility'
export { addComponent, addComponentsDir } from './components'

View File

@ -5,10 +5,11 @@ import { useNuxt } from './context'
import { logger } from './logger'
import { addTemplate } from './template'
const LAYOUT_RE = /["']/g
export function addLayout (template: NuxtTemplate | string, name?: string) {
const nuxt = useNuxt()
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.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()) {
let buildTimeModuleMeta: ModuleMeta = {}
const jiti = createJiti(nuxt.options.rootDir, {
interopDefault: true,
alias: nuxt.options.alias,
})
const jiti = createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias })
// Import if input is string
if (typeof nuxtModule === 'string') {
@ -85,7 +82,7 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
for (const path of paths) {
try {
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
const moduleMetadataPath = join(dirname(src), 'module.json')

View File

@ -4,13 +4,14 @@ import { normalize } from 'pathe'
import { useNuxt } from './context'
import { toArray } from './utils'
const HANDLER_METHOD_RE = /\.(get|head|patch|post|put|delete|connect|options|trace)(\.\w+)*$/
/**
* normalize handler object
*
*/
function normalizeHandlerMethod (handler: NitroEventHandler) {
// 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 {
method: method as 'get' | 'head' | 'patch' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | undefined,
...handler,

View File

@ -3,6 +3,7 @@ import type { NuxtPlugin, NuxtPluginTemplate } from '@nuxt/schema'
import { useNuxt } from './context'
import { addTemplate } from './template'
import { resolveAlias } from './resolve'
import { MODE_RE } from './utils'
/**
* Normalize a nuxt plugin object
@ -27,7 +28,7 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
plugin.mode = 'server'
}
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'
}

View File

@ -1,7 +1,7 @@
import { existsSync, promises as fsp } from 'node:fs'
import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
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 { defu } from 'defu'
import type { TSConfig } from 'pkg-types'
@ -32,6 +32,18 @@ export function addTemplate<T> (_template: NuxtTemplate<T> | string) {
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
* 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)
}
const EXTENSION_RE = /\b\.\w+$/g
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/, /^#internal\/nuxt/]
export async function _generateTypes (nuxt: Nuxt) {
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir)
@ -211,13 +226,7 @@ export async function _generateTypes (nuxt: Nuxt) {
exclude: [...exclude],
} satisfies TSConfig)
const aliases: Record<string, string> = {
...nuxt.options.alias,
'#build': nuxt.options.buildDir,
}
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/]
const aliases: Record<string, string> = nuxt.options.alias
const basePath = tsConfig.compilerOptions!.baseUrl
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
@ -251,7 +260,7 @@ export async function _generateTypes (nuxt: Nuxt) {
} else {
const path = stats?.isFile()
// remove extension
? relativePath.replace(/\b\.\w+$/g, '')
? relativePath.replace(EXTENSION_RE, '')
// non-existent file probably shouldn't be resolved
: aliases[alias]!
@ -280,7 +289,7 @@ export async function _generateTypes (nuxt: Nuxt) {
tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => {
if (!isAbsolute(path)) { return path }
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}"` : ''
}
const RELATIVE_WITH_DOT_RE = /^([^.])/
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[] {
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)
expect(tsConfig.compilerOptions?.paths).toMatchInlineSnapshot(`
{
"#build": [
".",
],
"some-custom-alias": [
"../some-alias",
],

View File

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

View File

@ -1,9 +1,9 @@
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 { hash } from 'ohash'
import { appendResponseHeader } from 'h3'
import { injectHead } from '@unhead/vue'
import { type ActiveHeadEntry, type Head, injectHead } from '@unhead/vue'
import { randomUUID } from 'uncrypto'
import { joinURL, withQuery } from 'ufo'
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 SLOTNAME_RE = /data-island-slot="([^"]*)"/g
const SLOT_FALLBACK_RE = / data-island-slot="([^"]*)"[^>]*>/g
const ISLAND_SCOPE_ID_RE = /^<[^> ]*/
let id = 1
const getId = import.meta.client ? () => (id++).toString() : randomUUID
@ -90,11 +91,13 @@ export default defineComponent({
const instance = getCurrentInstance()!
const event = useRequestEvent()
let activeHead: ActiveHeadEntry<Head>
// 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 mounted = ref(false)
onMounted(() => { mounted.value = true; teleportKey.value++ })
onBeforeUnmount(() => { if (activeHead) { activeHead.dispose() } })
function setPayload (key: string, result: NuxtIslandResponse) {
const toRevive: Partial<NuxtIslandResponse> = {}
if (result.props) { toRevive.props = result.props }
@ -140,7 +143,7 @@ export default defineComponent({
let html = ssrHTML.value
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) {
@ -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) {
// must await next tick for Teleport to work correctly with static node re-rendering
nextTick(() => {
@ -250,14 +261,6 @@ export default defineComponent({
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) => {
if (!html.value || error.value) {
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'
? to.value
: 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([
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(() => {}),
])
}
@ -520,11 +521,12 @@ function useObserver (): { observe: ObserveFn } | undefined {
return _observer
}
const IS_2G_RE = /2g/
function isSlowConnection () {
if (import.meta.server) { return }
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection
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
}

View File

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

View File

@ -56,7 +56,6 @@ export const defineNuxtComponent: typeof defineComponent =
}
if (options.head) {
const nuxtApp = useNuxtApp()
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 { useCookie, refreshCookie } 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 { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter } from './router'
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'

View File

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

View File

@ -1,6 +1,6 @@
import type { H3Event } from 'h3'
import { setResponseStatus as _setResponseStatus, appendHeader, getRequestHeader, getRequestHeaders } from 'h3'
import { getCurrentInstance } from 'vue'
import { setResponseStatus as _setResponseStatus, appendHeader, getRequestHeader, getRequestHeaders, getResponseHeader, removeResponseHeader, setResponseHeader } from 'h3'
import { computed, getCurrentInstance, ref } from 'vue'
import { useServerHead } from '@unhead/vue'
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 */
export function prerenderRoutes (path: string | string[]) {
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 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 { defineNuxtLink } from './components/index'

View File

@ -1,6 +1,6 @@
import { existsSync, statSync, writeFileSync } from 'node:fs'
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 { 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 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 }) {
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 STARTER_DOT_RE = /^\./g
export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]
@ -32,7 +34,7 @@ export default defineNuxtModule<ComponentsOptions>({
defaults: {
dirs: [],
},
setup (componentOptions, nuxt) {
async setup (componentOptions, nuxt) {
let componentDirs: ComponentsDir[] = []
const context = {
components: [] as Component[],
@ -89,7 +91,7 @@ export default defineNuxtModule<ComponentsOptions>({
const dirOptions: ComponentsDir = typeof dir === 'object' ? dir : { path: dir }
const dirPath = resolveAlias(dirOptions.path)
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)
if (!present && !DEFAULT_COMPONENTS_DIRS_RE.test(dirOptions.path)) {
@ -134,8 +136,9 @@ export default defineNuxtModule<ComponentsOptions>({
addTemplate(componentsMetadataTemplate)
}
addBuildPlugin(TransformPlugin(nuxt, getComponents, 'server'), { server: true, client: false })
addBuildPlugin(TransformPlugin(nuxt, getComponents, 'client'), { server: false, client: true })
const serverComponentRuntime = await findPath(join(distDir, 'components/runtime/server-component')) ?? join(distDir, 'components/runtime/server-component')
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
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
nuxt.hook('app:templates', async (app) => {
@ -222,6 +225,7 @@ export default defineNuxtModule<ComponentsOptions>({
const sharedLoaderOptions = {
getComponents,
serverComponentRuntime,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands,
}
@ -272,16 +276,18 @@ export default defineNuxtModule<ComponentsOptions>({
}
})
nuxt.hook('webpack:config', (configs) => {
configs.forEach((config) => {
const mode = config.name === 'client' ? 'client' : 'server'
config.plugins = config.plugins || []
for (const key of ['rspack:config', 'webpack:config'] as const) {
nuxt.hook(key, (configs) => {
configs.forEach((config) => {
const mode = config.name === 'client' ? 'client' : 'server'
config.plugins = config.plugins || []
if (mode !== 'server') {
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
}
if (mode !== 'server') {
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_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
const UID_RE = / :?uid=/
export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnplugin(() => {
const exclude = options.transform?.exclude || []
const include = options.transform?.include || []
@ -37,7 +38,7 @@ export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnpl
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
count++
if (/ :?uid=/.test(attrs)) { return full }
if (UID_RE.test(attrs)) { return full }
return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>`
})

View File

@ -1,12 +1,13 @@
import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string'
import type { Component } from 'nuxt/schema'
import { isVue } from '../../core/utils'
import { SX_RE, isVue } from '../../core/utils'
interface NameDevPluginOptions {
sourcemap: boolean
getComponents: () => Component[]
}
const FILENAME_RE = /([^/\\]+)\.\w+$/
/**
* 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',
enforce: 'post',
transformInclude (id) {
return isVue(id) || !!id.match(/\.[tj]sx$/)
return isVue(id) || !!id.match(SX_RE)
},
transform (code, id) {
const filename = id.match(/([^/\\]+)\.\w+$/)?.[1]
const filename = id.match(FILENAME_RE)?.[1]
if (!filename) {
return
}

View File

@ -30,6 +30,7 @@ const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
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 EXTRACTED_ATTRS_RE = /v-(?:if|else-if|else)(="[^"]*")?/g
const KEY_RE = /:?key="[^"]"/g
function wrapWithVForDiv (code: string, vfor: string): string {
return `<div v-for="${vfor}" style="display: contents;">${code}</div>`
@ -90,7 +91,7 @@ export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPlug
if (children.length) {
// pass slot fallback to NuxtTeleportSsrSlot fallback
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>`)
} 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, ''))

View File

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

View File

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

View File

@ -33,8 +33,9 @@ export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions)
const components = options.getComponents()
if (!regexpMap.has(components)) {
const serverPlaceholderPath = resolve(distDir, 'app/components/server-placeholder')
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('-', '_')])
.concat(['ClientOnly', 'client_only'])

View File

@ -6,8 +6,12 @@ import { isIgnored, logger, useNuxt } from '@nuxt/kit'
import { withTrailingSlash } from 'ufo'
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
* 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))
const island = /\.island(?:\.global)?$/.test(fileName) || dir.island
const global = /\.global(?:\.island)?$/.test(fileName) || dir.global
const mode = island ? 'server' : (fileName.match(/(?<=\.)(client|server)(\.global|\.island)*$/)?.[1] || 'all') as 'client' | 'server' | 'all'
fileName = fileName.replace(/(\.(client|server))?(\.global|\.island)*$/, '')
const island = ISLAND_RE.test(fileName) || dir.island
const global = GLOBAL_RE.test(fileName) || dir.global
const mode = island ? 'server' : (fileName.match(COMPONENT_MODE_RE)?.[1] || 'all') as 'client' | 'server' | 'all'
fileName = fileName.replace(MODE_REPLACEMENT_RE, '')
if (fileName.toLowerCase() === 'index') {
fileName = dir.pathPrefix === false ? basename(dirname(filePath)) : '' /* inherits from path */
}
const suffix = (mode !== 'all' ? `-${mode}` : '')
const componentNameSegments = resolveComponentNameSegments(fileName.replace(/["']/g, ''), prefixParts)
const componentNameSegments = resolveComponentNameSegments(fileName.replace(QUOTE_RE, ''), prefixParts)
const pascalName = pascalCase(componentNameSegments)
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 = {
filename: 'components.d.ts' as const,
getContents: ({ app, nuxt }) => {
const buildDir = nuxt.options.buildDir
const componentTypes = app.components.filter(c => !c.island).map((c) => {
const type = `typeof ${genDynamicImport(isAbsolute(c.filePath)
? relative(buildDir, c.filePath).replace(/\b\.(?!vue)\w+$/g, '')
: c.filePath.replace(/\b\.(?!vue)\w+$/g, ''), { wrapper: false })}['${c.export}']`
? relative(buildDir, c.filePath).replace(NON_VUE_RE, '')
: c.filePath.replace(NON_VUE_RE, ''), { wrapper: false })}['${c.export}']`
return [
c.pascalName,
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 changedTemplates: Array<ResolvedNuxtTemplate<any>> = []
const FORWARD_SLASH_RE = /\//g
async function processTemplate (template: ResolvedNuxtTemplate) {
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
const start = performance.now()
@ -72,12 +72,12 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
if (template.modified) {
nuxt.vfs[fullPath] = contents
const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '')
const aliasPath = '#build/' + template.filename
nuxt.vfs[aliasPath] = contents
// In case a non-normalized absolute path is called for on Windows
if (process.platform === 'win32') {
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents
nuxt.vfs[fullPath.replace(FORWARD_SLASH_RE, '\\')] = contents
}
changedTemplates.push(template)

View File

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

View File

@ -18,6 +18,7 @@ import { distDir } from '../dirs'
import { toArray } from '../utils'
import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
import { nuxtImportProtections } from './plugins/import-protection'
import { EXTENSION_RE } from './utils'
const logLevelMapReverse = {
silent: 0,
@ -25,12 +26,14 @@ const logLevelMapReverse = {
verbose: 3,
} 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 }) {
// Resolve config
const excludePaths = nuxt.options._layers
.flatMap(l => [
l.cwd.match(/(?<=\/)node_modules\/(.+)$/)?.[1],
l.cwd.match(/\.pnpm\/.+\/node_modules\/(.+)$/)?.[1],
l.cwd.match(NODE_MODULES_RE)?.[1],
l.cwd.match(PNPM_NODE_MODULES_RE)?.[1],
])
.filter((dir): dir is string => Boolean(dir))
.map(dir => escapeRE(dir))
@ -101,8 +104,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
devHandlers: [],
baseURL: nuxt.options.app.baseURL,
virtual: {
'#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config'],
'#internal/nuxt/app-config': () => nuxt.vfs['#build/app.config']?.replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, ''),
'#internal/nuxt.config.mjs': () => nuxt.vfs['#build/nuxt.config.mjs'],
'#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))}`,
},
routeRules: {
@ -189,11 +192,11 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
},
'@vue/devtools-api': 'vue-devtools-stub',
// Paths
'#internal/nuxt/paths': resolve(distDir, 'core/runtime/nitro/paths'),
// Nuxt aliases
...nuxt.options.alias,
// Paths
'#internal/nuxt/paths': resolve(distDir, 'core/runtime/nitro/paths'),
},
replace: {
'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`
const FORWARD_SLASH_RE = /\//g
if (!nuxt.options.ssr) {
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
// In case a non-normalized absolute path is called for on Windows
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 {}'
// In case a non-normalized absolute path is called for on Windows
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}/*`]
} 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) => {
const clientConfig = configuration.find(config => config.name === 'client')
if (!clientConfig!.resolve) { clientConfig!.resolve!.alias = {} }
if (Array.isArray(clientConfig!.resolve!.alias)) {
clientConfig!.resolve!.alias.push({
name: 'vue',
alias: 'vue/dist/vue.esm-bundler',
})
} else {
clientConfig!.resolve!.alias!.vue = 'vue/dist/vue.esm-bundler'
}
})
for (const hook of ['webpack:config', 'rspack:config'] as const) {
nuxt.hook(hook, (configuration) => {
const clientConfig = configuration.find(config => config.name === 'client')
if (!clientConfig!.resolve) { clientConfig!.resolve!.alias = {} }
if (Array.isArray(clientConfig!.resolve!.alias)) {
clientConfig!.resolve!.alias.push({
name: 'vue',
alias: 'vue/dist/vue.esm-bundler',
})
} else {
clientConfig!.resolve!.alias!.vue = 'vue/dist/vue.esm-bundler'
}
})
}
}
// Setup handlers
@ -545,13 +551,15 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// nuxt dev
if (nuxt.options.dev) {
nuxt.hook('webpack:compile', ({ name, compiler }) => {
if (name === 'server') {
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() })
for (const builder of ['webpack', 'rspack'] as const) {
nuxt.hook(`${builder}:compile`, ({ name, compiler }) => {
if (name === 'server') {
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(`${builder}:compiled`, () => { nuxt.server.reload() })
}
nuxt.hook('vite:compiled', () => { nuxt.server.reload() })
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) {
return relative(from, to).replace(/^([^.])/, './$1') || '.'
return relative(from, to).replace(RELATIVE_RE, './$1') || '.'
}
async function spaLoadingTemplatePath (nuxt: Nuxt) {

View File

@ -44,6 +44,7 @@ import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata'
import { AsyncContextInjectionPlugin } from './plugins/async-context'
import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports'
import { prehydrateTransformPlugin } from './plugins/prehydrate'
import { VirtualFSPlugin } from './plugins/virtual'
export function createNuxt (options: NuxtOptions): Nuxt {
const hooks = createHooks<NuxtHooks>()
@ -177,9 +178,10 @@ async function initNuxt (nuxt: Nuxt) {
const coreTypePackages = nuxt.options.typescript.hoist || []
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 || {})])
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 : ''
// 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
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
@ -492,9 +498,11 @@ async function initNuxt (nuxt: Nuxt) {
const envMap = {
// defaults from `builder` based on package name
'@nuxt/rspack-builder': '@rspack/core/module',
'@nuxt/vite-builder': 'vite/client',
'@nuxt/webpack-builder': 'webpack/module',
// simpler overrides from `typescript.builder` for better DX
'rspack': '@rspack/core/module',
'vite': 'vite/client',
'webpack': 'webpack/module',
// 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
},
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
}

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
const _resolveSchema = createJiti(fileURLToPath(import.meta.url), {
interopDefault: true,
cache: false,
transformOptions: {
babel: {
@ -97,7 +96,7 @@ export default defineNuxtModule({
let loadedConfig: SchemaDefinition
try {
// 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) {
logger.warn(
'Unable to load schema from',

View File

@ -11,6 +11,7 @@ import type { NuxtTemplate } from 'nuxt/schema'
import type { Nitro } from 'nitro/types'
import { annotatePlugins, checkForCircularDependencies } from './app'
import { EXTENSION_RE } from './utils'
export const vueShim: NuxtTemplate = {
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'),
}
const PLUGIN_TEMPLATE_RE = /_(45|46|47)/g
export const clientPluginTemplate: NuxtTemplate = {
filename: 'plugins/client.mjs',
filename: 'plugins.client.mjs',
async getContents (ctx) {
const clientPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'server'))
checkForCircularDependencies(clientPlugins)
@ -66,7 +68,7 @@ export const clientPluginTemplate: NuxtTemplate = {
const imports: string[] = []
for (const plugin of clientPlugins) {
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)
imports.push(genImport(plugin.src, variable))
}
@ -78,7 +80,7 @@ export const clientPluginTemplate: NuxtTemplate = {
}
export const serverPluginTemplate: NuxtTemplate = {
filename: 'plugins/server.mjs',
filename: 'plugins.server.mjs',
async getContents (ctx) {
const serverPlugins = await annotatePlugins(ctx.nuxt, ctx.app.plugins.filter(p => !p.mode || p.mode !== 'client'))
checkForCircularDependencies(serverPlugins)
@ -86,7 +88,7 @@ export const serverPluginTemplate: NuxtTemplate = {
const imports: string[] = []
for (const plugin of serverPlugins) {
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)
imports.push(genImport(plugin.src, variable))
}
@ -98,7 +100,9 @@ export const serverPluginTemplate: NuxtTemplate = {
}
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 = {
filename: 'types/plugins.d.ts',
getContents: async ({ nuxt, app }) => {
@ -120,18 +124,18 @@ export const pluginsDeclaration: NuxtTemplate = {
const pluginPath = resolve(typesDir, plugin.src)
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 (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) {
tsImports.push(relativePath)
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
// to hotfix issue until ecosystem updates to `@nuxt/module-builder@>=0.8.0`
if (incorrectDeclaration !== pluginPath && exists(incorrectDeclaration)) {
tsImports.push(relativePath.replace(/\.[cm](jsx?)$/, '.$1'))
tsImports.push(relativePath.replace(JS_CAPTURE_RE, '.$1'))
continue
}
@ -174,11 +178,13 @@ export { }
}
const adHocModules = ['router', 'pages', 'imports', 'meta', 'components', 'nuxt-config-schema']
const IMPORT_NAME_RE = /\.\w+$/
const GIT_RE = /^git\+/
export const schemaTemplate: NuxtTemplate = {
filename: 'types/schema.d.ts',
getContents: async ({ nuxt }) => {
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
.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.startsWith('git+')) {
link = link.replace(/^git\+/, '')
link = link.replace(GIT_RE, '')
}
if (!link.startsWith('http')) {
link = 'https://github.com/' + link
@ -377,7 +383,7 @@ export const appConfigDeclarationTemplate: NuxtTemplate = {
filename: 'types/app.config.d.ts',
getContents ({ app, nuxt }) {
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 `
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
}
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 { kebabCase, splitByCase } from 'scule'
import { withTrailingSlash } from 'ufo'
import { QUOTE_RE } from '.'
export function getNameFromPath (path: string, relativeTo?: string) {
const relativePath = relativeTo
@ -9,7 +10,7 @@ export function getNameFromPath (path: string, relativeTo?: string) {
const prefixParts = splitByCase(dirname(relativePath))
const fileName = basename(relativePath, extname(relativePath))
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) {

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
nuxt.hooks.hook('nitro:config', (config) => {
config.virtual!['#internal/unhead-plugins.mjs'] = () => nuxt.vfs['#build/unhead-plugins']
config.virtual!['#internal/unhead.config.mjs'] = () => nuxt.vfs['#build/unhead.config']
config.virtual!['#internal/unhead-plugins.mjs'] = () => nuxt.vfs['#build/unhead-plugins.mjs']
config.virtual!['#internal/unhead.config.mjs'] = () => nuxt.vfs['#build/unhead.config.mjs']
})
// Add library-specific plugin

View File

@ -66,7 +66,7 @@ const granularAppPresets: InlinePreset[] = [
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',
},
{

View File

@ -52,7 +52,7 @@ export default defineNuxtModule({
}
// 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)
return context.files
@ -170,10 +170,15 @@ export default defineNuxtModule({
if (nuxt.apps.default) {
nuxt.apps.default.pages = pages
}
const addedPagePaths = new Set<string>()
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
// 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) {
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) {
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
// when the app manifest is enabled.
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
addTemplate({
filename: 'routes.mjs',
getContents ({ app }) {
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')
},
})

View File

@ -176,8 +176,10 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin
// https://github.com/vuejs/vue-loader/pull/1911
// https://github.com/vitejs/vite/issues/8473
const QUERY_START_RE = /^\?/
const MACRO_RE = /&macro=true/
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) {
@ -189,6 +191,7 @@ function parseMacroQuery (id: string) {
return query
}
const QUOTED_SPECIFIER_RE = /(["']).*\1/
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 { 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 type { NitroRouteConfig } from 'nitro/types'
import { useNuxtApp } from '#app/nuxt'
@ -37,6 +37,11 @@ export interface PageMeta {
name?: string
/** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */
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 */
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 */) {
return
}
if (to.matched.length === 0) {
await nuxtApp.runWithContext(() => showError(createError({
statusCode: 404,
fatal: false,
statusMessage: `Page not found: ${to.fullPath}`,
data: {
path: to.fullPath,
},
})))
} else if (import.meta.server && to.redirectedFrom && to.fullPath !== initialURL) {
if (import.meta.server && to.redirectedFrom && to.fullPath !== initialURL) {
await nuxtApp.runWithContext(() => navigateTo(to.fullPath || '/'))
}
})
@ -252,6 +244,19 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
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 () => {
try {
// #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>
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) => {
return match.path
.replace(/(:\w+)\([^)]+\)/g, '$1')
.replace(/(:\w+)[?+*]/g, '$1')
.replace(/:\w+/g, r => route.params[r.slice(1)]?.toString() || '')
.replace(ROUTE_KEY_PARENTHESES_RE, '$1')
.replace(ROUTE_KEY_SYMBOLS_RE, '$1')
.replace(ROUTE_KEY_NORMAL_RE, r => route.params[r.slice(1)]?.toString() || '')
}
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 { toArray } from '../utils'
import { distDir } from '../dirs'
enum SegmentParserState {
initial,
@ -65,18 +64,25 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
})
const pages = uniqueBy(allRoutes, 'path')
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)
await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs, augmentedPages)
augmentedPages.clear()
} else {
await nuxt.callHook('pages:extend', pages)
}
await nuxt.callHook('pages:resolved', pages)
return pages
}
@ -84,6 +90,7 @@ type GenerateRoutesFromFilesOptions = {
shouldUseServerComponents?: boolean
}
const INDEX_PAGE_RE = /\/index$/
export function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): NuxtPage[] {
const routes: NuxtPage[] = []
@ -129,7 +136,7 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
route.name += (route.name && '/') + segmentName
// 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)
if (child && child.children) {
@ -184,7 +191,7 @@ export function extractScriptContent (html: string) {
}
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 pageContentsCache: Record<string, string> = {}
@ -266,7 +273,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
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}\`).`)
dynamicProperties.add(key)
continue
@ -301,6 +308,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
return extractedMeta
}
const COLON_RE = /:/g
function getRoutePath (tokens: SegmentToken[]): string {
return tokens.reduce((path, token) => {
return (
@ -313,7 +321,7 @@ function getRoutePath (tokens: SegmentToken[]): string {
? `:${token.value}(.*)*`
: 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)
}
const NESTED_PAGE_RE = /\//g
function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage, names = new Set<string>()) {
for (const route of routes) {
// Remove -index
if (route.name) {
route.name = route.name
.replace(/\/index$/, '')
.replace(/\//g, '-')
.replace(INDEX_PAGE_RE, '')
.replace(NESTED_PAGE_RE, '-')
if (names.has(route.name)) {
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 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 {
imports: metaImports,
routes: genArrayFromRaw(routes.map((page) => {
@ -506,7 +520,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
}
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
@ -528,6 +542,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
const metaRoute: NormalizedRoute = {
name: `${metaImportName}?.name ?? ${route.name}`,
path: `${metaImportName}?.path ?? ${route.path}`,
props: `${metaImportName}?.props ?? false`,
meta: `${metaImportName} || {}`,
alias: `${metaImportName}?.alias || []`,
redirect: `${metaImportName}?.redirect`,
@ -542,14 +557,14 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
metaImports.add(`
let _createIslandPage
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)
};`)
} else if (page.mode === 'client') {
metaImports.add(`
let _createClientPage
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);
}`)
}
@ -562,7 +577,7 @@ async function createClientPage(loader) {
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`
}
if (overrideMeta) {
if (options?.overrideMeta) {
// skip and retain fallback if marked dynamic
// set to extracted value or fallback if none extracted
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
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 (route[key] == null) {
@ -596,6 +611,7 @@ async function createClientPage(loader) {
}
}
const PATH_TO_NITRO_GLOB_RE = /\/[^:/]*:\w.*$/
export function pathToNitroGlob (path: string) {
if (!path) {
return null
@ -605,7 +621,7 @@ export function pathToNitroGlob (path: string) {
return null
}
return path.replace(/\/[^:/]*:\w.*$/, '/**')
return path.replace(PATH_TO_NITRO_GLOB_RE, '/**')
}
export function resolveRoutePaths (page: NuxtPage, parent = '/'): string[] {

View File

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

View File

@ -24,6 +24,13 @@
"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": [
{
"component": "() => import("pages/test:name.vue")",

View File

@ -92,7 +92,11 @@ function createTransformer (components: Component[], mode: 'client' | 'server' |
},
},
} 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) => {
const result = await (plugin as any).transform!(code, id)

View File

@ -168,7 +168,11 @@ describe('normalizeRoutes', () => {
page.meta.layout = 'test'
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(`
{
"imports": Set {
@ -193,7 +197,11 @@ describe('normalizeRoutes', () => {
page.meta.layout = 'test'
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(`
{
"imports": Set {
@ -203,6 +211,7 @@ describe('normalizeRoutes', () => {
{
name: indexN6pT4Un8hYMeta?.name ?? undefined,
path: indexN6pT4Un8hYMeta?.path ?? "/",
props: indexN6pT4Un8hYMeta?.props ?? false,
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
alias: indexN6pT4Un8hYMeta?.alias || [],
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',
files: [
@ -667,8 +691,18 @@ describe('pages:generateRoutesFromFiles', () => {
if (result) {
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/pug": "2.0.10",
"@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-jsx": "4.0.1",
"@vue/compiler-core": "3.5.10",
"@vue/compiler-sfc": "3.5.10",
"@vue/compiler-core": "3.5.12",
"@vue/compiler-sfc": "3.5.12",
"@vue/language-core": "2.1.6",
"c12": "2.0.0-beta.3",
"esbuild-loader": "4.2.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"ignore": "6.0.2",
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
"ofetch": "1.4.0",
"unbuild": "3.0.0-rc.8",
"ofetch": "1.4.1",
"unbuild": "3.0.0-rc.11",
"unctx": "2.3.1",
"unenv": "1.10.0",
"vite": "5.4.8",
"vue": "3.5.10",
"vite": "5.4.10",
"vue": "3.5.12",
"vue-bundle-renderer": "2.1.1",
"vue-loader": "17.4.2",
"vue-router": "4.4.5",
@ -63,18 +62,19 @@
"webpack-dev-middleware": "7.4.2"
},
"dependencies": {
"c12": "^2.0.1",
"compatx": "^0.1.8",
"consola": "^3.2.3",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"pathe": "^1.1.2",
"pkg-types": "^1.2.0",
"pkg-types": "^1.2.1",
"scule": "^1.3.0",
"std-env": "^3.7.0",
"ufo": "^1.5.4",
"uncrypto": "^0.1.3",
"unimport": "^3.13.1",
"untyped": "^1.5.0"
"untyped": "^1.5.1"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"

View File

@ -7,14 +7,15 @@ import { consola } from 'consola'
export default defineUntypedSchema({
/**
* 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: {
$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') {
return val
}
const map: Record<string, string> = {
rspack: '@nuxt/rspack-builder',
vite: '@nuxt/vite-builder',
webpack: '@nuxt/webpack-builder',
}

View File

@ -424,7 +424,7 @@ export default defineUntypedSchema({
*/
alias: {
$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 {
'~': srcDir,
'@': srcDir,
@ -432,6 +432,8 @@ export default defineUntypedSchema({
'@@': rootDir,
[basename(assetsDir)]: resolve(srcDir, assetsDir),
[basename(publicDir)]: resolve(srcDir, publicDir),
'#build': buildDir,
'#internal/nuxt/paths': resolve(buildDir, 'paths.mjs'),
...val,
}
},

View File

@ -297,8 +297,13 @@ export default defineUntypedSchema({
* 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)
* @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

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.
*
* 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: {
$resolve: val => val ?? null,

View File

@ -6,7 +6,7 @@ export type { GenerateAppOptions, HookResult, ImportPresetWithDeprecation, NuxtA
export type { ImportsOptions } from './types/imports'
export type { AppHeadMetaObject, MetaObject, MetaObjectRaw, HeadAugmentations } from './types/head'
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'
// Schema

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