diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2d09d397f7..0d41b7ba33 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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 && \ diff --git a/.github/workflows/autofix-docs.yml b/.github/workflows/autofix-docs.yml index 0d4a241a3b..b4ed01816e 100644 --- a/.github/workflows/autofix-docs.yml +++ b/.github/workflows/autofix-docs.yml @@ -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" diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 3eae960c13..4f65e10c0d 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -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" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 9d172b0137..a0ebe3284a 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -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 }} diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 6e1fd3b606..e66a5f6fe6 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -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" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c962f800ba..3f5453bd5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 81beca512c..b04478031a 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -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 diff --git a/.github/workflows/docs-check-links.yml b/.github/workflows/docs-check-links.yml index baa805fb9c..7d27acd60b 100644 --- a/.github/workflows/docs-check-links.yml +++ b/.github/workflows/docs-check-links.yml @@ -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: >- diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1aac781344..4353c01c59 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -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" diff --git a/.github/workflows/lint-sherif.yml b/.github/workflows/lint-sherif.yml index db815c2b16..1477d94c3b 100644 --- a/.github/workflows/lint-sherif.yml +++ b/.github/workflows/lint-sherif.yml @@ -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" diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-workflows.yml index 8645ce310b..0faef02c2e 100644 --- a/.github/workflows/lint-workflows.yml +++ b/.github/workflows/lint-workflows.yml @@ -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 diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index dff2aacd52..b7b46ad748 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc8203daa5..c0b74d25c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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/" diff --git a/.github/workflows/reproduire.yml b/.github/workflows/reproduire.yml index 435cea2e70..74288c20f1 100644 --- a/.github/workflows/reproduire.yml +++ b/.github/workflows/reproduire.yml @@ -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 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 08e0233e83..74f94cb61b 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -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 diff --git a/.github/workflows/stackblitz-link.yml b/.github/workflows/stackblitz-link.yml index 9649afed00..ad97a4e678 100644 --- a/.github/workflows/stackblitz-link.yml +++ b/.github/workflows/stackblitz-link.yml @@ -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' diff --git a/docs/1.getting-started/11.testing.md b/docs/1.getting-started/11.testing.md index dce433a768..60d0b8db18 100644 --- a/docs/1.getting-started/11.testing.md +++ b/docs/1.getting-started/11.testing.md @@ -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'} }) } diff --git a/docs/1.getting-started/12.upgrade.md b/docs/1.getting-started/12.upgrade.md index 7db21c0856..642592192b 100644 --- a/docs/1.getting-started/12.upgrade.md +++ b/docs/1.getting-started/12.upgrade.md @@ -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 diff --git a/docs/1.getting-started/4.styling.md b/docs/1.getting-started/4.styling.md index 383bed144b..d780ad3449 100644 --- a/docs/1.getting-started/4.styling.md +++ b/docs/1.getting-started/4.styling.md @@ -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--) { diff --git a/docs/1.getting-started/5.routing.md b/docs/1.getting-started/5.routing.md index e732502f6c..e88d9e8c24 100644 --- a/docs/1.getting-started/5.routing.md +++ b/docs/1.getting-started/5.routing.md @@ -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/ diff --git a/docs/1.getting-started/6.data-fetching.md b/docs/1.getting-started/6.data-fetching.md index 44bedcda96..9e4e0a47ae 100644 --- a/docs/1.getting-started/6.data-fetching.md +++ b/docs/1.getting-started/6.data-fetching.md @@ -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] + + + +``` + +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 Vue’s [``](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 Vue’s [``](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 [``](/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] + +``` + +::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] -``` - -::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. diff --git a/docs/2.guide/1.concepts/3.rendering.md b/docs/2.guide/1.concepts/3.rendering.md index db972cdd02..535b8bf57a 100644 --- a/docs/2.guide/1.concepts/3.rendering.md +++ b/docs/2.guide/1.concepts/3.rendering.md @@ -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] + + + +``` + +On the initial request, the `counter` ref is initialized in the server since it is rendered inside the `

` 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:** diff --git a/docs/2.guide/2.directory-structure/1.components.md b/docs/2.guide/2.directory-structure/1.components.md index 8fb3a0c59f..0656b96e75 100644 --- a/docs/2.guide/2.directory-structure/1.components.md +++ b/docs/2.guide/2.directory-structure/1.components.md @@ -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 `

` 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: diff --git a/docs/2.guide/2.directory-structure/1.composables.md b/docs/2.guide/2.directory-structure/1.composables.md index dbd3510b56..ed96746656 100644 --- a/docs/2.guide/2.directory-structure/1.composables.md +++ b/docs/2.guide/2.directory-structure/1.composables.md @@ -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. diff --git a/docs/2.guide/2.directory-structure/1.middleware.md b/docs/2.guide/2.directory-structure/1.middleware.md index 3fbcc732ce..d17c6ceb7c 100644 --- a/docs/2.guide/2.directory-structure/1.middleware.md +++ b/docs/2.guide/2.directory-structure/1.middleware.md @@ -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 diff --git a/docs/2.guide/2.directory-structure/1.pages.md b/docs/2.guide/2.directory-structure/1.pages.md index ce14e9075f..0efb13daac 100644 --- a/docs/2.guide/2.directory-structure/1.pages.md +++ b/docs/2.guide/2.directory-structure/1.pages.md @@ -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 ``` diff --git a/docs/2.guide/2.directory-structure/1.plugins.md b/docs/2.guide/2.directory-structure/1.plugins.md index d5edce56b3..572675300c 100644 --- a/docs/2.guide/2.directory-structure/1.plugins.md +++ b/docs/2.guide/2.directory-structure/1.plugins.md @@ -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({ diff --git a/docs/2.guide/2.directory-structure/3.app-config.md b/docs/2.guide/2.directory-structure/3.app-config.md index 656d5c16a1..d170a6ce5a 100644 --- a/docs/2.guide/2.directory-structure/3.app-config.md +++ b/docs/2.guide/2.directory-structure/3.app-config.md @@ -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] ``` -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] + +``` + +::read-more{to="/docs/api/utils/update-app-config"} +Read more about the `updateAppConfig` utility. +:: ## Typing App Config diff --git a/docs/2.guide/3.going-further/1.experimental-features.md b/docs/2.guide/3.going-further/1.experimental-features.md index 31bed1a8ae..57cd77803e 100644 --- a/docs/2.guide/3.going-further/1.experimental-features.md +++ b/docs/2.guide/3.going-further/1.experimental-features.md @@ -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] diff --git a/docs/2.guide/3.going-further/1.features.md b/docs/2.guide/3.going-further/1.features.md index 247df516e2..46150d86d3 100644 --- a/docs/2.guide/3.going-further/1.features.md +++ b/docs/2.guide/3.going-further/1.features.md @@ -61,6 +61,7 @@ export default defineNuxtConfig({ app: 'app' }, experimental: { + scanPageMeta: 'after-resolve', sharedPrerenderData: false, compileTemplate: true, resetAsyncDataToUndefined: true, diff --git a/docs/2.guide/4.recipes/3.custom-usefetch.md b/docs/2.guide/4.recipes/3.custom-usefetch.md index e8f25f6a2b..a0ac6d7e29 100644 --- a/docs/2.guide/4.recipes/3.custom-usefetch.md +++ b/docs/2.guide/4.recipes/3.custom-usefetch.md @@ -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') ``` +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( + url: string | (() => string), + options?: UseFetchOptions, +) { + return useFetch>(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`. :: diff --git a/docs/3.api/2.composables/use-fetch.md b/docs/3.api/2.composables/use-fetch.md index 6b07d44737..56effdc62c 100644 --- a/docs/3.api/2.composables/use-fetch.md +++ b/docs/3.api/2.composables/use-fetch.md @@ -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 diff --git a/docs/3.api/2.composables/use-response-header.md b/docs/3.api/2.composables/use-response-header.md new file mode 100644 index 0000000000..d78fd89a4a --- /dev/null +++ b/docs/3.api/2.composables/use-response-header.md @@ -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] + + + +``` + +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!`; +}); + +``` diff --git a/docs/3.api/3.utils/define-page-meta.md b/docs/3.api/3.utils/define-page-meta.md index e8427faf84..49aa8bc849 100644 --- a/docs/3.api/3.utils/define-page-meta.md +++ b/docs/3.api/3.utils/define-page-meta.md @@ -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[]` diff --git a/docs/3.api/3.utils/navigate-to.md b/docs/3.api/3.utils/navigate-to.md index 664de1773b..270bc80a00 100644 --- a/docs/3.api/3.utils/navigate-to.md +++ b/docs/3.api/3.utils/navigate-to.md @@ -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` diff --git a/nuxt.config.ts b/nuxt.config.ts index 54f547026e..b6777a9d28 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -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' }, }) diff --git a/package.json b/package.json index 77e239e970..43b63a3cc0 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/packages/kit/build.config.ts b/packages/kit/build.config.ts index 87a8ccb072..d2a1f7fa7d 100644 --- a/packages/kit/build.config.ts +++ b/packages/kit/build.config.ts @@ -6,6 +6,7 @@ export default defineBuildConfig({ 'src/index', ], externals: [ + '@rspack/core', '@nuxt/schema', 'nitropack', 'nitro', diff --git a/packages/kit/package.json b/packages/kit/package.json index e5c091f984..edfc5b5cd5 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -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": { diff --git a/packages/kit/src/build.ts b/packages/kit/src/build.ts index fab8c45228..ba4a1d17ba 100644 --- a/packages/kit/src/build.ts +++ b/packages/kit/src/build.ts @@ -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) + } } diff --git a/packages/kit/src/compatibility.ts b/packages/kit/src/compatibility.ts index 00ad74b67c..e650dc0710 100644 --- a/packages/kit/src/compatibility.ts +++ b/packages/kit/src/compatibility.ts @@ -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, '') } diff --git a/packages/kit/src/components.ts b/packages/kit/src/components.ts index 669d6ce410..82394e3811 100644 --- a/packages/kit/src/components.ts +++ b/packages/kit/src/components.ts @@ -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' } diff --git a/packages/kit/src/index.ts b/packages/kit/src/index.ts index bde038e6fb..c9b94204b0 100644 --- a/packages/kit/src/index.ts +++ b/packages/kit/src/index.ts @@ -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' diff --git a/packages/kit/src/layout.ts b/packages/kit/src/layout.ts index 65fd183163..a416fa8b21 100644 --- a/packages/kit/src/layout.ts +++ b/packages/kit/src/layout.ts @@ -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) => { diff --git a/packages/kit/src/module/install.ts b/packages/kit/src/module/install.ts index 1149fc9d94..39a460cbed 100644 --- a/packages/kit/src/module/install.ts +++ b/packages/kit/src/module/install.ts @@ -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') diff --git a/packages/kit/src/nitro.ts b/packages/kit/src/nitro.ts index 29b0b44981..4c046f6a9d 100644 --- a/packages/kit/src/nitro.ts +++ b/packages/kit/src/nitro.ts @@ -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, diff --git a/packages/kit/src/plugin.ts b/packages/kit/src/plugin.ts index aadcdbc718..116721214f 100644 --- a/packages/kit/src/plugin.ts +++ b/packages/kit/src/plugin.ts @@ -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' } diff --git a/packages/kit/src/template.ts b/packages/kit/src/template.ts index 903308497f..8b4ae046fd 100644 --- a/packages/kit/src/template.ts +++ b/packages/kit/src/template.ts @@ -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 (_template: NuxtTemplate | 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 = { - ...nuxt.options.alias, - '#build': nuxt.options.buildDir, - } - - // Exclude bridge alias types to support Volar - const excludedAlias = [/^@vue\/.*$/] + const aliases: Record = 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') || '.' } diff --git a/packages/kit/src/utils.ts b/packages/kit/src/utils.ts index 72b096120b..89fa591c50 100644 --- a/packages/kit/src/utils.ts +++ b/packages/kit/src/utils.ts @@ -2,3 +2,5 @@ export function toArray (value: T | T[]): T[] { return Array.isArray(value) ? value : [value] } + +export const MODE_RE = /\.(server|client)(\.\w+)*$/ diff --git a/packages/kit/test/generate-types.spec.ts b/packages/kit/test/generate-types.spec.ts index b5bcc9a6bb..d65d6809bd 100644 --- a/packages/kit/test/generate-types.spec.ts +++ b/packages/kit/test/generate-types.spec.ts @@ -34,9 +34,6 @@ describe('tsConfig generation', () => { const { tsConfig } = await _generateTypes(mockNuxt) expect(tsConfig.compilerOptions?.paths).toMatchInlineSnapshot(` { - "#build": [ - ".", - ], "some-custom-alias": [ "../some-alias", ], diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index b07297467d..21c02fa5f6 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -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", diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index c6bb6d8657..20d1f11cbf 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -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 + // 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 = {} 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')] diff --git a/packages/nuxt/src/app/components/nuxt-link.ts b/packages/nuxt/src/app/components/nuxt-link.ts index f38ddbd777..aa50e11e09 100644 --- a/packages/nuxt/src/app/components/nuxt-link.ts +++ b/packages/nuxt/src/app/components/nuxt-link.ts @@ -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 } diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts index 0bde127ec5..5fe9739e31 100644 --- a/packages/nuxt/src/app/components/utils.ts +++ b/packages/nuxt/src/app/components/utils.ts @@ -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 } diff --git a/packages/nuxt/src/app/composables/component.ts b/packages/nuxt/src/app/composables/component.ts index e8bd93e20c..914533e757 100644 --- a/packages/nuxt/src/app/composables/component.ts +++ b/packages/nuxt/src/app/composables/component.ts @@ -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) } diff --git a/packages/nuxt/src/app/composables/index.ts b/packages/nuxt/src/app/composables/index.ts index c0abdec2e4..05ba560d09 100644 --- a/packages/nuxt/src/app/composables/index.ts +++ b/packages/nuxt/src/app/composables/index.ts @@ -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' diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index fa8be0805c..0814873ff0 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -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 | 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 = { diff --git a/packages/nuxt/src/app/composables/ssr.ts b/packages/nuxt/src/app/composables/ssr.ts index 56f3383109..59db9bd327 100644 --- a/packages/nuxt/src/app/composables/ssr.ts +++ b/packages/nuxt/src/app/composables/ssr.ts @@ -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 } diff --git a/packages/nuxt/src/app/index.ts b/packages/nuxt/src/app/index.ts index 363c72d1bf..43fcc404e1 100644 --- a/packages/nuxt/src/app/index.ts +++ b/packages/nuxt/src/app/index.ts @@ -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' diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index 5c2d3102db..a2a1d8ca7d 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -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({ defaults: { dirs: [], }, - setup (componentOptions, nuxt) { + async setup (componentOptions, nuxt) { let componentDirs: ComponentsDir[] = [] const context = { components: [] as Component[], @@ -89,7 +91,7 @@ export default defineNuxtModule({ 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({ 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({ } }) - 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({ 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({ } }) - 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 = {}') + } + }) }) - }) + } } }, }) diff --git a/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts b/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts index 9f3e1b119c..85c3d8b82d 100644 --- a/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts +++ b/packages/nuxt/src/components/plugins/client-fallback-auto-id.ts @@ -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 ?? ''}>` }) diff --git a/packages/nuxt/src/components/plugins/component-names.ts b/packages/nuxt/src/components/plugins/component-names.ts index 4a24ebe96b..af01adb5dc 100644 --- a/packages/nuxt/src/components/plugins/component-names.ts +++ b/packages/nuxt/src/components/plugins/component-names.ts @@ -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 } diff --git a/packages/nuxt/src/components/plugins/islands-transform.ts b/packages/nuxt/src/components/plugins/islands-transform.ts index 70ad9fb895..a3e2aba41a 100644 --- a/packages/nuxt/src/components/plugins/islands-transform.ts +++ b/packages/nuxt/src/components/plugins/islands-transform.ts @@ -30,6 +30,7 @@ const TEMPLATE_RE = /