Merge branch '3.x' into test/3.x-vfs

This commit is contained in:
Daniel Roe 2024-10-09 09:18:55 +02:00
commit 59d8427048
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
66 changed files with 1378 additions and 1534 deletions

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:

View File

@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:

View File

@ -22,7 +22,7 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable

View File

@ -37,7 +37,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
@ -57,7 +57,7 @@ jobs:
run: pnpm build run: pnpm build
- name: Cache dist - name: Cache dist
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 uses: actions/upload-artifact@84480863f228bb9747b473957fcc9e309aa96097 # v4.4.2
with: with:
retention-days: 3 retention-days: 3
name: dist name: dist
@ -72,10 +72,10 @@ jobs:
security-events: write security-events: write
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
with: with:
config: | config: |
paths: paths:
@ -91,7 +91,7 @@ jobs:
queries: +security-and-quality queries: +security-and-quality
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
with: with:
category: "/language:javascript-typescript" category: "/language:javascript-typescript"
@ -107,7 +107,7 @@ jobs:
module: ["bundler", "node"] module: ["bundler", "node"]
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
@ -138,7 +138,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
@ -162,7 +162,7 @@ jobs:
needs: needs:
- build - build
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
@ -214,7 +214,7 @@ jobs:
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:
@ -258,7 +258,6 @@ jobs:
github.event_name == 'push' && github.event_name == 'push' &&
github.repository == 'nuxt/nuxt' && github.repository == 'nuxt/nuxt' &&
!contains(github.event.head_commit.message, '[skip-release]') && !contains(github.event.head_commit.message, '[skip-release]') &&
!startsWith(github.event.head_commit.message, 'chore') &&
!startsWith(github.event.head_commit.message, 'docs') !startsWith(github.event.head_commit.message, 'docs')
needs: needs:
- lint - lint
@ -268,7 +267,7 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable
@ -309,7 +308,7 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable

View File

@ -17,6 +17,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4

View File

@ -19,17 +19,17 @@ jobs:
steps: steps:
# Cache lychee results (e.g. to avoid hitting rate limits) # Cache lychee results (e.g. to avoid hitting rate limits)
- name: Restore lychee cache - name: Restore lychee cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1
with: with:
path: .lycheecache path: .lycheecache
key: cache-lychee-${{ github.sha }} key: cache-lychee-${{ github.sha }}
restore-keys: cache-lychee- restore-keys: cache-lychee-
# check links with Lychee # check links with Lychee
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Lychee link checker - name: Lychee link checker
uses: lycheeverse/lychee-action@897f08a07f689df1a43076f4374af272f66a6dd1 # for v1.8.0 uses: lycheeverse/lychee-action@e360f3c89142a5391e094404ea45e5494f1317dd # for v1.8.0
with: with:
# arguments with file types to check # arguments with file types to check
args: >- args: >-

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:

View File

@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- run: corepack enable - run: corepack enable
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with: with:

View File

@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions # From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files - name: Check workflow files
uses: docker://rhysd/actionlint:1.7.3@sha256:7617f05bd698cd2f1c3aedc05bc733ccec92cca0738f3e8722c32c5b42c70ae6 uses: docker://rhysd/actionlint:1.7.3@sha256:7617f05bd698cd2f1c3aedc05bc733ccec92cca0738f3e8722c32c5b42c70ae6

View File

@ -24,7 +24,7 @@ jobs:
steps: steps:
- name: Ensure action is by maintainer - name: Ensure action is by maintainer
uses: octokit/request-action@872c5c97b3c85c23516a572f02b31401ef82415d # v2.3.1 uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0
id: check_role id: check_role
with: with:
route: GET /repos/nuxt/nuxt/collaborators/${{ github.event.comment.user.login }} route: GET /repos/nuxt/nuxt/collaborators/${{ github.event.comment.user.login }}
@ -48,7 +48,7 @@ jobs:
fi fi
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with: with:
ref: ${{ steps.pr.outputs.head_sha }} ref: ${{ steps.pr.outputs.head_sha }}
fetch-depth: 1 fetch-depth: 1

View File

@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
steps: steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with: with:
fetch-depth: 0 fetch-depth: 0
- run: corepack enable - run: corepack enable

View File

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

View File

@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with: with:
persist-credentials: false persist-credentials: false
@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 uses: actions/upload-artifact@84480863f228bb9747b473957fcc9e309aa96097 # v4.4.2
if: github.repository == 'nuxt/nuxt' && success() if: github.repository == 'nuxt/nuxt' && success()
with: with:
name: SARIF file name: SARIF file
@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12
if: github.repository == 'nuxt/nuxt' && success() if: github.repository == 'nuxt/nuxt' && success()
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

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

View File

@ -64,6 +64,10 @@ By default, the workload gets distributed to the workers with the round robin st
:read-more{to="https://nitro.unjs.io/deploy/node" title="the Nitro documentation for node-server preset"} :read-more{to="https://nitro.unjs.io/deploy/node" title="the Nitro documentation for node-server preset"}
::tip{icon="i-ph-video" to="https://www.youtube.com/watch?v=0x1H6K5yOfs" target="\_blank"}
Watch Daniel Roe's short video on the topic.
::
## Static Hosting ## Static Hosting
There are two ways to deploy a Nuxt application to any static hosting services: There are two ways to deploy a Nuxt application to any static hosting services:

View File

@ -4,7 +4,6 @@ description: 'Learn how to upgrade to the latest Nuxt version.'
navigation.icon: i-ph-arrow-circle-up navigation.icon: i-ph-arrow-circle-up
--- ---
## Upgrading Nuxt ## Upgrading Nuxt
### Latest release ### Latest release
@ -179,6 +178,7 @@ nuxt.config.ts
1. Create a new directory called `app/`. 1. Create a new directory called `app/`.
1. Move your `assets/`, `components/`, `composables/`, `layouts/`, `middleware/`, `pages/`, `plugins/` and `utils/` folders under it, as well as `app.vue`, `error.vue`, `app.config.ts`. If you have an `app/router-options.ts` or `app/spa-loading-template.html`, these paths remain the same. 1. Move your `assets/`, `components/`, `composables/`, `layouts/`, `middleware/`, `pages/`, `plugins/` and `utils/` folders under it, as well as `app.vue`, `error.vue`, `app.config.ts`. If you have an `app/router-options.ts` or `app/spa-loading-template.html`, these paths remain the same.
1. Make sure your `nuxt.config.ts`, `content/`, `layers/`, `modules/`, `public/` and `server/` folders remain outside the `app/` folder, in the root of your project. 1. Make sure your `nuxt.config.ts`, `content/`, `layers/`, `modules/`, `public/` and `server/` folders remain outside the `app/` folder, in the root of your project.
1. Remember to update any third-party configuration files to work with the new directory structure, such as your `tailwindcss` or `eslint` configuration (if required - `@nuxtjs/tailwindcss` should automatically configure `tailwindcss` correctly).
::tip ::tip
You can automate this migration by running `npx codemod@latest nuxt/4/file-structure` You can automate this migration by running `npx codemod@latest nuxt/4/file-structure`
@ -467,7 +467,7 @@ We have already proactively migrated the public Nuxt modules which we are aware
However, if you are a module author using the `builder:watch` hook and wishing to remain backwards/forwards compatible, you can use the following code to ensure that your code works the same in both Nuxt v3 and Nuxt v4: However, if you are a module author using the `builder:watch` hook and wishing to remain backwards/forwards compatible, you can use the following code to ensure that your code works the same in both Nuxt v3 and Nuxt v4:
```diff ```diff
+ import { relative, resolve } from 'node:fs' + import { relative, resolve } from 'node:fs'
// ... // ...
nuxt.hook('builder:watch', async (event, path) => { nuxt.hook('builder:watch', async (event, path) => {

View File

@ -4,7 +4,6 @@ description: Nuxt is configured with sensible defaults to make you productive.
navigation.icon: i-ph-gear navigation.icon: i-ph-gear
--- ---
By default, Nuxt is configured to cover most use cases. The [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt-config) file can override or extend this default configuration. By default, Nuxt is configured to cover most use cases. The [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt-config) file can override or extend this default configuration.
## Nuxt Configuration ## Nuxt Configuration

View File

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

View File

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

View File

@ -33,7 +33,7 @@ Nuxt auto-imports functions and composables to perform [data fetching](/docs/get
```vue twoslash ```vue twoslash
<script setup lang="ts"> <script setup lang="ts">
/* useAsyncData() and $fetch() are auto-imported */ /* useFetch() is auto-imported */
const { data, refresh, status } = await useFetch('/api/hello') const { data, refresh, status } = await useFetch('/api/hello')
</script> </script>
``` ```

View File

@ -8,9 +8,9 @@ navigation.icon: i-ph-folder
Nuxt automatically imports any components in this directory (along with components that are registered by any modules you may be using). Nuxt automatically imports any components in this directory (along with components that are registered by any modules you may be using).
```bash [Directory Structure] ```bash [Directory Structure]
| components/ -| components/
--| AppHeader.vue ---| AppHeader.vue
--| AppFooter.vue ---| AppFooter.vue
``` ```
```html [app.vue] ```html [app.vue]
@ -28,10 +28,10 @@ Nuxt automatically imports any components in this directory (along with componen
If you have a component in nested directories such as: If you have a component in nested directories such as:
```bash [Directory Structure] ```bash [Directory Structure]
| components/ -| components/
--| base/ ---| base/
----| foo/ -----| foo/
------| Button.vue -------| Button.vue
``` ```
... then the component's name will be based on its own path directory and filename, with duplicate segments being removed. Therefore, the component's name will be: ... then the component's name will be based on its own path directory and filename, with duplicate segments being removed. Therefore, the component's name will be:
@ -82,6 +82,10 @@ const MyButton = resolveComponent('MyButton')
If you are using `resolveComponent` to handle dynamic components, make sure not to insert anything but the name of the component, which must be a string and not a variable. If you are using `resolveComponent` to handle dynamic components, make sure not to insert anything but the name of the component, which must be a string and not a variable.
:: ::
::tip{icon="i-ph-video" to="https://www.youtube.com/watch?v=4kq8E5IUM2U" target="\_blank"}
Watch Daniel Roe's short video about `resolveComponent`.
::
Alternatively, though not recommended, you can register all your components globally, which will create async chunks for all your components and make them available throughout your application. Alternatively, though not recommended, you can register all your components globally, which will create async chunks for all your components and make them available throughout your application.
```diff ```diff
@ -281,8 +285,8 @@ export default defineNuxtConfig({
Now you can register server-only components with the `.server` suffix and use them anywhere in your application automatically. Now you can register server-only components with the `.server` suffix and use them anywhere in your application automatically.
```bash [Directory Structure] ```bash [Directory Structure]
| components/ -| components/
--| HighlightedMarkdown.server.vue ---| HighlightedMarkdown.server.vue
``` ```
```vue [pages/example.vue] ```vue [pages/example.vue]
@ -355,9 +359,9 @@ Slots can be interactive and are wrapped within a `<div>` with `display: content
In this case, the `.server` + `.client` components are two 'halves' of a component and can be used in advanced use cases for separate implementations of a component on server and client side. In this case, the `.server` + `.client` components are two 'halves' of a component and can be used in advanced use cases for separate implementations of a component on server and client side.
```bash [Directory Structure] ```bash [Directory Structure]
| components/ -| components/
--| Comments.client.vue ---| Comments.client.vue
--| Comments.server.vue ---| Comments.server.vue
``` ```
```vue [pages/example.vue] ```vue [pages/example.vue]
@ -385,15 +389,15 @@ You can use the `components:dirs` hook to extend the directory list without requ
Imagine a directory structure like this: Imagine a directory structure like this:
```bash [Directory Structure] ```bash [Directory Structure]
| node_modules/ -| node_modules/
---| awesome-ui/ ---| awesome-ui/
------| components/ -----| components/
---------| Alert.vue -------| Alert.vue
---------| Button.vue -------| Button.vue
------| nuxt.js -----| nuxt.js
| pages/ -| pages/
---| index.vue ---| index.vue
| nuxt.config.js -| nuxt.config.js
``` ```
Then in `awesome-ui/nuxt.js` you can use the `components:dirs` hook: Then in `awesome-ui/nuxt.js` you can use the `components:dirs` hook:

View File

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

View File

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

View File

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

View File

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

View File

@ -347,6 +347,22 @@ export default defineEventHandler((event) => {
}) })
``` ```
### Forwarding Context & Headers
By default, neither the headers from the incoming request nor the request context are forwarded when
making fetch requests in server routes. You can use `event.$fetch` to forward the request context and headers when making fetch requests in server routes.
```ts [server/api/forward.ts]
export default defineEventHandler((event) => {
return event.$fetch('/api/forwarded')
})
```
::note
Headers that are **not meant to be forwarded** will **not be included** in the request. These headers include, for example:
`transfer-encoding`, `connection`, `keep-alive`, `upgrade`, `expect`, `host`, `accept`
::
## Advanced Usage ## Advanced Usage
### Nitro Config ### Nitro Config

View File

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

View File

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

View File

@ -0,0 +1,52 @@
---
title: 'useRequestFetch'
description: 'Forward the request context and headers for server-side fetch requests with the useRequestFetch composable.'
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
---
You can use `useRequestFetch` to forward the request context and headers when making server-side fetch requests.
When making a client-side fetch request, the browser automatically sends the necessary headers.
However, when making a request during server-side rendering, because the request is made on the server, we need to forward the headers manually.
::note
Headers that are **not meant to be forwarded** will **not be included** in the request. These headers include, for example:
`transfer-encoding`, `connection`, `keep-alive`, `upgrade`, `expect`, `host`, `accept`
::
::tip
The [`useFetch`](/docs/api/composables/use-fetch) composable uses `useRequestFetch` under the hood to automatically forward the request context and headers.
::
::code-group
```vue [pages/index.vue]
<script setup lang="ts">
// This will forward the user's headers to the `/api/foo` event handler
// Result: { cookies: { foo: 'bar' } }
const requestFetch = useRequestFetch()
const { data: forwarded } = await useAsyncData(() => requestFetch('/api/cookies'))
// This will NOT forward anything
// Result: { cookies: {} }
const { data: notForwarded } = await useAsyncData(() => $fetch('/api/cookies'))
</script>
```
```ts [server/api/cookies.ts]
export default defineEventHandler((event) => {
const cookies = parseCookies(event)
return { cookies }
})
```
::
::tip
In the browser during client-side navigation, `useRequestFetch` will behave just like regular [`$fetch`](/docs/api/utils/dollarfetch).
::

View File

@ -39,30 +39,30 @@
"@nuxt/ui-templates": "workspace:*", "@nuxt/ui-templates": "workspace:*",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@types/node": "20.16.10", "@types/node": "20.16.11",
"@vue/compiler-core": "3.5.10", "@vue/compiler-core": "3.5.11",
"@vue/compiler-dom": "3.5.10", "@vue/compiler-dom": "3.5.11",
"@vue/shared": "3.5.10", "@vue/shared": "3.5.11",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"ohash": "1.1.4", "ohash": "1.1.4",
"postcss": "8.4.47", "postcss": "8.4.47",
"rollup": "4.24.0", "rollup": "4.24.0",
"send": ">=0.19.0", "send": ">=0.19.0",
"typescript": "5.6.2", "typescript": "5.6.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"vite": "5.4.8", "vite": "5.4.8",
"vue": "3.5.10" "vue": "3.5.11"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.11.1", "@eslint/js": "9.12.0",
"@nuxt/eslint-config": "0.5.7", "@nuxt/eslint-config": "0.5.7",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/test-utils": "3.14.2", "@nuxt/test-utils": "3.14.3",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/eslint__js": "8.42.3", "@types/eslint__js": "8.42.3",
"@types/node": "20.16.10", "@types/node": "20.16.11",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@unhead/schema": "1.11.7", "@unhead/schema": "1.11.7",
"@unhead/vue": "1.11.7", "@unhead/vue": "1.11.7",
@ -76,36 +76,36 @@
"cssnano": "7.0.6", "cssnano": "7.0.6",
"destr": "2.0.3", "destr": "2.0.3",
"devalue": "5.1.1", "devalue": "5.1.1",
"eslint": "9.11.1", "eslint": "9.12.0",
"eslint-plugin-no-only-tests": "3.3.0", "eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "3.8.0", "eslint-plugin-perfectionist": "3.8.0",
"eslint-typegen": "0.3.2", "eslint-typegen": "0.3.2",
"h3": "1.12.0", "h3": "1.12.0",
"happy-dom": "15.7.4", "happy-dom": "15.7.4",
"jiti": "2.3.1", "jiti": "2.3.3",
"markdownlint-cli": "0.42.0", "markdownlint-cli": "0.42.0",
"nitropack": "2.9.7", "nitropack": "2.9.7",
"nuxi": "3.14.0", "nuxi": "3.14.0",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.1", "nuxt-content-twoslash": "0.1.1",
"ofetch": "1.4.0", "ofetch": "1.4.1",
"pathe": "1.1.2", "pathe": "1.1.2",
"playwright-core": "1.47.2", "playwright-core": "1.48.0",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"semver": "7.6.3", "semver": "7.6.3",
"sherif": "1.0.0", "sherif": "1.0.0",
"std-env": "3.7.0", "std-env": "3.7.0",
"tinyexec": "0.3.0", "tinyexec": "0.3.0",
"tinyglobby": "0.2.9", "tinyglobby": "0.2.9",
"typescript": "5.6.2", "typescript": "5.6.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"vitest": "2.1.2", "vitest": "2.1.2",
"vitest-environment-nuxt": "1.0.1", "vitest-environment-nuxt": "1.0.1",
"vue": "3.5.10", "vue": "3.5.11",
"vue-router": "4.4.5", "vue-router": "4.4.5",
"vue-tsc": "2.1.6" "vue-tsc": "2.1.6"
}, },
"packageManager": "pnpm@9.12.0", "packageManager": "pnpm@9.12.1",
"engines": { "engines": {
"node": "^16.10.0 || >=18.0.0" "node": "^16.10.0 || >=18.0.0"
}, },

View File

@ -34,12 +34,12 @@
"globby": "^14.0.2", "globby": "^14.0.2",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"ignore": "^6.0.2", "ignore": "^6.0.2",
"jiti": "^2.3.1", "jiti": "^2.3.3",
"klona": "^2.0.6", "klona": "^2.0.6",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"mlly": "^1.7.1", "mlly": "^1.7.2",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"pkg-types": "^1.2.0", "pkg-types": "^1.2.1",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",
"ufo": "^1.5.4", "ufo": "^1.5.4",

View File

@ -1,7 +1,7 @@
import { existsSync, promises as fsp } from 'node:fs' import { existsSync, promises as fsp } from 'node:fs'
import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe' import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
import hash from 'hash-sum' import hash from 'hash-sum'
import type { Nuxt, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema' import type { Nuxt, NuxtServerTemplate, NuxtTemplate, NuxtTypeTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema'
import { withTrailingSlash } from 'ufo' import { withTrailingSlash } from 'ufo'
import { defu } from 'defu' import { defu } from 'defu'
import type { TSConfig } from 'pkg-types' import type { TSConfig } from 'pkg-types'
@ -32,6 +32,18 @@ export function addTemplate<T> (_template: NuxtTemplate<T> | string) {
return template return template
} }
/**
* Adds a virtual file that can be used within the Nuxt Nitro server build.
*/
export function addServerTemplate (template: NuxtServerTemplate) {
const nuxt = useNuxt()
nuxt.options.nitro.virtual ||= {}
nuxt.options.nitro.virtual[template.filename] = template.getContents
return template
}
/** /**
* Renders given types using lodash template during build into the project buildDir * Renders given types using lodash template during build into the project buildDir
* and register them as types. * and register them as types.

View File

@ -69,10 +69,10 @@
"@unhead/shared": "^1.11.7", "@unhead/shared": "^1.11.7",
"@unhead/ssr": "^1.11.7", "@unhead/ssr": "^1.11.7",
"@unhead/vue": "^1.11.7", "@unhead/vue": "^1.11.7",
"@vue/shared": "^3.5.10", "@vue/shared": "^3.5.11",
"acorn": "8.12.1", "acorn": "8.12.1",
"c12": "^2.0.1", "c12": "^2.0.1",
"chokidar": "^3.6.0", "chokidar": "^4.0.1",
"compatx": "^0.1.8", "compatx": "^0.1.8",
"consola": "^3.2.3", "consola": "^3.2.3",
"cookie-es": "^1.2.2", "cookie-es": "^1.2.2",
@ -88,20 +88,20 @@
"hookable": "^5.5.3", "hookable": "^5.5.3",
"ignore": "^6.0.2", "ignore": "^6.0.2",
"impound": "^0.1.0", "impound": "^0.1.0",
"jiti": "^2.3.1", "jiti": "^2.3.3",
"klona": "^2.0.6", "klona": "^2.0.6",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
"mlly": "^1.7.1", "mlly": "^1.7.2",
"nanotar": "^0.1.1", "nanotar": "^0.1.1",
"nitropack": "^2.9.7", "nitropack": "^2.9.7",
"nuxi": "^3.14.0", "nuxi": "^3.14.0",
"nypm": "^0.3.12", "nypm": "^0.3.12",
"ofetch": "^1.4.0", "ofetch": "^1.4.1",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"pkg-types": "^1.2.0", "pkg-types": "^1.2.1",
"radix3": "^1.1.2", "radix3": "^1.1.2",
"scule": "^1.3.0", "scule": "^1.3.0",
"semver": "^7.6.3", "semver": "^7.6.3",
@ -119,7 +119,7 @@
"unplugin-vue-router": "^0.10.8", "unplugin-vue-router": "^0.10.8",
"unstorage": "^1.12.0", "unstorage": "^1.12.0",
"untyped": "^1.5.1", "untyped": "^1.5.1",
"vue": "^3.5.10", "vue": "^3.5.11",
"vue-bundle-renderer": "^2.1.1", "vue-bundle-renderer": "^2.1.1",
"vue-devtools-stub": "^0.1.0", "vue-devtools-stub": "^0.1.0",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
@ -130,7 +130,7 @@
"@parcel/watcher": "2.4.1", "@parcel/watcher": "2.4.1",
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.10", "@vue/compiler-sfc": "3.5.11",
"unbuild": "latest", "unbuild": "latest",
"vite": "5.4.8", "vite": "5.4.8",
"vitest": "2.1.2" "vitest": "2.1.2"

View File

@ -328,8 +328,9 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
const path = typeof to.value === 'string' const path = typeof to.value === 'string'
? to.value ? to.value
: isExternal.value ? resolveRouteObject(to.value) : router.resolve(to.value).fullPath : isExternal.value ? resolveRouteObject(to.value) : router.resolve(to.value).fullPath
const normalizedPath = isExternal.value ? new URL(path, window.location.href).href : path
await Promise.all([ await Promise.all([
nuxtApp.hooks.callHook('link:prefetch', path).catch(() => {}), nuxtApp.hooks.callHook('link:prefetch', normalizedPath).catch(() => {}),
!isExternal.value && !hasTarget.value && preloadRouteComponents(to.value as string, router).catch(() => {}), !isExternal.value && !hasTarget.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
]) ])
} }

View File

@ -1,6 +1,6 @@
import { existsSync, statSync, writeFileSync } from 'node:fs' import { existsSync, statSync, writeFileSync } from 'node:fs'
import { isAbsolute, join, normalize, relative, resolve } from 'pathe' import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit' import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit'
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema' import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
import { distDir } from '../dirs' import { distDir } from '../dirs'
@ -9,8 +9,8 @@ import { scanComponents } from './scan'
import { ClientFallbackAutoIdPlugin } from './plugins/client-fallback-auto-id' import { ClientFallbackAutoIdPlugin } from './plugins/client-fallback-auto-id'
import { LoaderPlugin } from './plugins/loader' import { LoaderPlugin } from './plugins/loader'
import { componentsChunkPlugin, islandsTransform } from './plugins/islands-transform' import { ComponentsChunkPlugin, IslandsTransformPlugin } from './plugins/islands-transform'
import { createTransformPlugin } from './plugins/transform' import { TransformPlugin } from './plugins/transform'
import { TreeShakeTemplatePlugin } from './plugins/tree-shake' import { TreeShakeTemplatePlugin } from './plugins/tree-shake'
import { ComponentNamePlugin } from './plugins/component-names' import { ComponentNamePlugin } from './plugins/component-names'
@ -134,14 +134,8 @@ export default defineNuxtModule<ComponentsOptions>({
addTemplate(componentsMetadataTemplate) addTemplate(componentsMetadataTemplate)
} }
const TransformPluginServer = createTransformPlugin(nuxt, getComponents, 'server') addBuildPlugin(TransformPlugin(nuxt, getComponents, 'server'), { server: true, client: false })
const TransformPluginClient = createTransformPlugin(nuxt, getComponents, 'client') addBuildPlugin(TransformPlugin(nuxt, getComponents, 'client'), { server: false, client: true })
addVitePlugin(() => TransformPluginServer.vite(), { server: true, client: false })
addVitePlugin(() => TransformPluginClient.vite(), { server: false, client: true })
addWebpackPlugin(() => TransformPluginServer.webpack(), { server: true, client: false })
addWebpackPlugin(() => TransformPluginClient.webpack(), { server: false, client: true })
// Do not prefetch global components chunks // Do not prefetch global components chunks
nuxt.hook('build:manifest', (manifest) => { nuxt.hook('build:manifest', (manifest) => {
@ -219,37 +213,52 @@ export default defineNuxtModule<ComponentsOptions>({
} }
}) })
nuxt.hook('vite:extendConfig', (config, { isClient, isServer }) => { if (nuxt.options.experimental.treeshakeClientOnly) {
const mode = isClient ? 'client' : 'server' addBuildPlugin(TreeShakeTemplatePlugin({ sourcemap: !!nuxt.options.sourcemap.server, getComponents }), { client: false })
}
config.plugins = config.plugins || [] if (nuxt.options.experimental.clientFallback) {
if (nuxt.options.experimental.treeshakeClientOnly && isServer) { addBuildPlugin(ClientFallbackAutoIdPlugin({ sourcemap: !!nuxt.options.sourcemap.client, rootDir: nuxt.options.rootDir }), { server: false })
config.plugins.push(TreeShakeTemplatePlugin.vite({ addBuildPlugin(ClientFallbackAutoIdPlugin({ sourcemap: !!nuxt.options.sourcemap.server, rootDir: nuxt.options.rootDir }), { client: false })
sourcemap: !!nuxt.options.sourcemap[mode], }
getComponents,
}))
}
if (nuxt.options.experimental.clientFallback) {
config.plugins.push(ClientFallbackAutoIdPlugin.vite({
sourcemap: !!nuxt.options.sourcemap[mode],
rootDir: nuxt.options.rootDir,
}))
}
config.plugins.push(LoaderPlugin.vite({
sourcemap: !!nuxt.options.sourcemap[mode],
getComponents,
mode,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands,
}))
if (nuxt.options.experimental.componentIslands) { const sharedLoaderOptions = {
const selectiveClient = typeof nuxt.options.experimental.componentIslands === 'object' && nuxt.options.experimental.componentIslands.selectiveClient getComponents,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands,
}
addBuildPlugin(LoaderPlugin({ ...sharedLoaderOptions, sourcemap: !!nuxt.options.sourcemap.client, mode: 'client' }), { server: false })
addBuildPlugin(LoaderPlugin({ ...sharedLoaderOptions, sourcemap: !!nuxt.options.sourcemap.server, mode: 'server' }), { client: false })
if (nuxt.options.experimental.componentIslands) {
const selectiveClient = typeof nuxt.options.experimental.componentIslands === 'object' && nuxt.options.experimental.componentIslands.selectiveClient
addVitePlugin({
name: 'nuxt-server-component-hmr',
handleHotUpdate (ctx) {
const components = getComponents()
const filePath = normalize(ctx.file)
const comp = components.find(c => c.filePath === filePath)
if (comp?.mode === 'server') {
ctx.server.ws.send({
event: `nuxt-server-component:${comp.pascalName}`,
type: 'custom',
})
}
},
}, { server: false })
addBuildPlugin(IslandsTransformPlugin({ getComponents, selectiveClient }), { client: false })
// TODO: refactor this
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
config.plugins = config.plugins || []
if (isClient && selectiveClient) { if (isClient && selectiveClient) {
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}') writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
if (!nuxt.options.dev) { if (!nuxt.options.dev) {
config.plugins.push(componentsChunkPlugin.vite({ config.plugins.push(ComponentsChunkPlugin.vite({
getComponents, getComponents,
buildDir: nuxt.options.buildDir, buildDir: nuxt.options.buildDir,
})) }))
@ -263,65 +272,18 @@ export default defineNuxtModule<ComponentsOptions>({
)}`) )}`)
} }
} }
})
if (isServer) { nuxt.hook('webpack:config', (configs) => {
config.plugins.push(islandsTransform.vite({ configs.forEach((config) => {
getComponents, const mode = config.name === 'client' ? 'client' : 'server'
selectiveClient, config.plugins = config.plugins || []
}))
}
}
if (!isServer && nuxt.options.experimental.componentIslands) {
config.plugins.push({
name: 'nuxt-server-component-hmr',
handleHotUpdate (ctx) {
const components = getComponents()
const filePath = normalize(ctx.file)
const comp = components.find(c => c.filePath === filePath)
if (comp?.mode === 'server') {
ctx.server.ws.send({
event: `nuxt-server-component:${comp.pascalName}`,
type: 'custom',
})
}
},
})
}
})
nuxt.hook('webpack:config', (configs) => {
configs.forEach((config) => {
const mode = config.name === 'client' ? 'client' : 'server'
config.plugins = config.plugins || []
if (nuxt.options.experimental.treeshakeClientOnly && mode === 'server') {
config.plugins.push(TreeShakeTemplatePlugin.webpack({
sourcemap: !!nuxt.options.sourcemap[mode],
getComponents,
}))
}
if (nuxt.options.experimental.clientFallback) {
config.plugins.push(ClientFallbackAutoIdPlugin.webpack({
sourcemap: !!nuxt.options.sourcemap[mode],
rootDir: nuxt.options.rootDir,
}))
}
config.plugins.push(LoaderPlugin.webpack({
sourcemap: !!nuxt.options.sourcemap[mode],
getComponents,
mode,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands,
}))
if (nuxt.options.experimental.componentIslands) { if (mode !== 'server') {
if (mode === 'server') {
config.plugins.push(islandsTransform.webpack({
getComponents,
}))
} else {
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}') writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
} }
} })
}) })
}) }
}, },
}) })

View File

@ -12,7 +12,7 @@ interface LoaderOptions {
} }
const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/ const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
export const ClientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions) => { export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnplugin(() => {
const exclude = options.transform?.exclude || [] const exclude = options.transform?.exclude || []
const include = options.transform?.include || [] const include = options.transform?.include || []

View File

@ -35,14 +35,14 @@ function wrapWithVForDiv (code: string, vfor: string): string {
return `<div v-for="${vfor}" style="display: contents;">${code}</div>` return `<div v-for="${vfor}" style="display: contents;">${code}</div>`
} }
export const islandsTransform = createUnplugin((options: ServerOnlyComponentTransformPluginOptions, meta) => { export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPluginOptions) => createUnplugin((_options, meta) => {
const isVite = meta.framework === 'vite' const isVite = meta.framework === 'vite'
return { return {
name: 'server-only-component-transform', name: 'nuxt:server-only-component-transform',
enforce: 'pre', enforce: 'pre',
transformInclude (id) { transformInclude (id) {
if (!isVue(id)) { return false } if (!isVue(id)) { return false }
if (options.selectiveClient === 'deep') { return true } if (isVite && options.selectiveClient === 'deep') { return true }
const components = options.getComponents() const components = options.getComponents()
const islands = components.filter(component => const islands = components.filter(component =>
@ -70,54 +70,68 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
const ast = parse(template[0]) const ast = parse(template[0])
await walk(ast, (node) => { await walk(ast, (node) => {
if (node.type === ELEMENT_NODE) { if (node.type !== ELEMENT_NODE) {
if (node.name === 'slot') { return
const { attributes, children, loc } = node
const slotName = attributes.name ?? 'default'
if (attributes.name) { delete attributes.name }
if (attributes['v-bind']) {
attributes._bind = extractAttributes(attributes, ['v-bind'])['v-bind']!
}
const teleportAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
const bindings = getPropsToString(attributes)
// add the wrapper
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportSsrSlot${attributeToString(teleportAttributes)} name="${slotName}" :props="${bindings}">`)
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, '')
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, ''))
}
s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportSsrSlot>')
} else if (options.selectiveClient && ('nuxt-client' in node.attributes || ':nuxt-client' in node.attributes)) {
hasNuxtClient = true
const { loc, attributes } = node
const attributeValue = attributes[':nuxt-client'] || attributes['nuxt-client'] || 'true'
if (isVite) {
const uid = hash(id + node.loc[0].start + node.loc[0].end)
const wrapperAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
let startTag = code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replace(NUXTCLIENT_ATTR_RE, '')
if (wrapperAttributes) {
startTag = startTag.replaceAll(EXTRACTED_ATTRS_RE, '')
}
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportIslandComponent${attributeToString(wrapperAttributes)} to="${node.name}-${uid}" :nuxt-client="${attributeValue}">`)
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, startTag)
s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportIslandComponent>')
}
}
} }
if (node.name === 'slot') {
const { attributes, children, loc } = node
const slotName = attributes.name ?? 'default'
if (attributes.name) { delete attributes.name }
if (attributes['v-bind']) {
attributes._bind = extractAttributes(attributes, ['v-bind'])['v-bind']!
}
const teleportAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
const bindings = getPropsToString(attributes)
// add the wrapper
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportSsrSlot${attributeToString(teleportAttributes)} name="${slotName}" :props="${bindings}">`)
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, '')
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, ''))
}
s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportSsrSlot>')
return
}
if (!('nuxt-client' in node.attributes) && !(':nuxt-client' in node.attributes)) {
return
}
hasNuxtClient = true
if (!isVite || !options.selectiveClient) {
return
}
const { loc, attributes } = node
const attributeValue = attributes[':nuxt-client'] || attributes['nuxt-client'] || 'true'
const uid = hash(id + node.loc[0].start + node.loc[0].end)
const wrapperAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
let startTag = code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replace(NUXTCLIENT_ATTR_RE, '')
if (wrapperAttributes) {
startTag = startTag.replaceAll(EXTRACTED_ATTRS_RE, '')
}
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportIslandComponent${attributeToString(wrapperAttributes)} to="${node.name}-${uid}" :nuxt-client="${attributeValue}">`)
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, startTag)
s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportIslandComponent>')
}) })
if (!isVite && hasNuxtClient) { if (hasNuxtClient) {
console.warn(`nuxt-client attribute and client components within islands is only supported with Vite. file: ${id}`) if (!options.selectiveClient) {
console.warn(`The \`nuxt-client\` attribute and client components within islands are only supported when \`experimental.componentIslands.selectiveClient\` is enabled. file: ${id}`)
} else if (!isVite) {
console.warn(`The \`nuxt-client\` attribute and client components within islands are only supported with Vite. file: ${id}`)
}
} }
if (s.hasChanged()) { if (s.hasChanged()) {
@ -164,10 +178,10 @@ function getPropsToString (bindings: Record<string, string>): string {
} }
} }
export const componentsChunkPlugin = createUnplugin((options: ComponentChunkOptions) => { export const ComponentsChunkPlugin = createUnplugin((options: ComponentChunkOptions) => {
const { buildDir } = options const { buildDir } = options
return { return {
name: 'componentsChunkPlugin', name: 'nuxt:components-chunk',
vite: { vite: {
async config (config) { async config (config) {
const components = options.getComponents() const components = options.getComponents()

View File

@ -2,7 +2,7 @@ import { createUnplugin } from 'unplugin'
import { genDynamicImport, genImport } from 'knitwork' import { genDynamicImport, genImport } from 'knitwork'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { pascalCase } from 'scule' import { pascalCase } from 'scule'
import { resolve } from 'pathe' import { relative, resolve } from 'pathe'
import type { Component, ComponentsOptions } from 'nuxt/schema' import type { Component, ComponentsOptions } from 'nuxt/schema'
import { logger, tryUseNuxt } from '@nuxt/kit' import { logger, tryUseNuxt } from '@nuxt/kit'
@ -17,10 +17,11 @@ interface LoaderOptions {
experimentalComponentIslands?: boolean experimentalComponentIslands?: boolean
} }
export const LoaderPlugin = createUnplugin((options: LoaderOptions) => { export const LoaderPlugin = (options: LoaderOptions) => createUnplugin(() => {
const exclude = options.transform?.exclude || [] const exclude = options.transform?.exclude || []
const include = options.transform?.include || [] const include = options.transform?.include || []
const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component') const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component')
const nuxt = tryUseNuxt()
return { return {
name: 'nuxt:components-loader', name: 'nuxt:components-loader',
@ -34,7 +35,7 @@ export const LoaderPlugin = createUnplugin((options: LoaderOptions) => {
} }
return isVue(id, { type: ['template', 'script'] }) || !!id.match(/\.[tj]sx$/) return isVue(id, { type: ['template', 'script'] }) || !!id.match(/\.[tj]sx$/)
}, },
transform (code) { transform (code, id) {
const components = options.getComponents() const components = options.getComponents()
let num = 0 let num = 0
@ -46,10 +47,14 @@ export const LoaderPlugin = createUnplugin((options: LoaderOptions) => {
s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, name: string) => { s.replace(/(?<=[ (])_?resolveComponent\(\s*["'](lazy-|Lazy(?=[A-Z]))?([^'"]*)["'][^)]*\)/g, (full: string, lazy: string, name: string) => {
const component = findComponent(components, name, options.mode) const component = findComponent(components, name, options.mode)
if (component) { if (component) {
// @ts-expect-error TODO: refactor to nuxi // TODO: refactor to nuxi
if (component._internal_install && tryUseNuxt()?.options.test === false) { const internalInstall = ((component as any)._internal_install) as string
// @ts-expect-error TODO: refactor to nuxi if (internalInstall && nuxt?.options.test === false) {
import('../../core/features').then(({ installNuxtModule }) => installNuxtModule(component._internal_install)) if (!nuxt.options.dev) {
const relativePath = relative(nuxt.options.rootDir, id)
throw new Error(`[nuxt] \`~/${relativePath}\` is using \`${component.pascalName}\` which requires \`${internalInstall}\``)
}
import('../../core/features').then(({ installNuxtModule }) => installNuxtModule(internalInstall))
} }
let identifier = map.get(component) || `__nuxt_component_${num++}` let identifier = map.get(component) || `__nuxt_component_${num++}`
map.set(component, identifier) map.set(component, identifier)

View File

@ -12,7 +12,7 @@ import type { getComponentsT } from '../module'
const COMPONENT_QUERY_RE = /[?&]nuxt_component=/ const COMPONENT_QUERY_RE = /[?&]nuxt_component=/
export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT, mode: 'client' | 'server' | 'all') { export function TransformPlugin (nuxt: Nuxt, getComponents: getComponentsT, mode: 'client' | 'server' | 'all') {
const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component') const serverComponentRuntime = resolve(distDir, 'components/runtime/server-component')
const componentUnimport = createUnimport({ const componentUnimport = createUnimport({
imports: [ imports: [

View File

@ -20,7 +20,7 @@ const PLACEHOLDER_EXACT_RE = /^(?:fallback|placeholder)$/
const CLIENT_ONLY_NAME_RE = /^(?:_unref\()?(?:_component_)?(?:Lazy|lazy_)?(?:client_only|ClientOnly\)?)$/ const CLIENT_ONLY_NAME_RE = /^(?:_unref\()?(?:_component_)?(?:Lazy|lazy_)?(?:client_only|ClientOnly\)?)$/
const PARSER_OPTIONS = { sourceType: 'module', ecmaVersion: 'latest' } const PARSER_OPTIONS = { sourceType: 'module', ecmaVersion: 'latest' }
export const TreeShakeTemplatePlugin = createUnplugin((options: TreeShakeTemplatePluginOptions) => { export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions) => createUnplugin(() => {
const regexpMap = new WeakMap<Component[], [RegExp, RegExp, string[]]>() const regexpMap = new WeakMap<Component[], [RegExp, RegExp, string[]]>()
return { return {
name: 'nuxt:tree-shake-template', name: 'nuxt:tree-shake-template',

View File

@ -33,9 +33,7 @@ import { version } from '../../package.json'
import { scriptsStubsPreset } from '../imports/presets' import { scriptsStubsPreset } from '../imports/presets'
import { resolveTypePath } from './utils/types' import { resolveTypePath } from './utils/types'
import { nuxtImportProtections } from './plugins/import-protection' import { nuxtImportProtections } from './plugins/import-protection'
import type { UnctxTransformPluginOptions } from './plugins/unctx'
import { UnctxTransformPlugin } from './plugins/unctx' import { UnctxTransformPlugin } from './plugins/unctx'
import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake'
import { TreeShakeComposablesPlugin } from './plugins/tree-shake' import { TreeShakeComposablesPlugin } from './plugins/tree-shake'
import { DevOnlyPlugin } from './plugins/dev-only' import { DevOnlyPlugin } from './plugins/dev-only'
import { LayerAliasingPlugin } from './plugins/layer-aliasing' import { LayerAliasingPlugin } from './plugins/layer-aliasing'
@ -270,58 +268,45 @@ async function initNuxt (nuxt: Nuxt) {
if (nuxt.options.experimental.localLayerAliases) { if (nuxt.options.experimental.localLayerAliases) {
// Add layer aliasing support for ~, ~~, @ and @@ aliases // Add layer aliasing support for ~, ~~, @ and @@ aliases
addVitePlugin(() => LayerAliasingPlugin.vite({ addBuildPlugin(LayerAliasingPlugin({
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
dev: nuxt.options.dev, dev: nuxt.options.dev,
root: nuxt.options.srcDir, root: nuxt.options.srcDir,
// skip top-level layer (user's project) as the aliases will already be correctly resolved // skip top-level layer (user's project) as the aliases will already be correctly resolved
layers: nuxt.options._layers.slice(1), layers: nuxt.options._layers.slice(1),
})) }))
addWebpackPlugin(() => LayerAliasingPlugin.webpack({
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
dev: nuxt.options.dev,
root: nuxt.options.srcDir,
// skip top-level layer (user's project) as the aliases will already be correctly resolved
layers: nuxt.options._layers.slice(1),
transform: true,
}))
} }
nuxt.hook('modules:done', async () => { nuxt.hook('modules:done', async () => {
// Add unctx transform // Add unctx transform
const options = { addBuildPlugin(UnctxTransformPlugin({
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
transformerOptions: { transformerOptions: {
...nuxt.options.optimization.asyncTransforms, ...nuxt.options.optimization.asyncTransforms,
helperModule: await tryResolveModule('unctx', nuxt.options.modulesDir) ?? 'unctx', helperModule: await tryResolveModule('unctx', nuxt.options.modulesDir) ?? 'unctx',
}, },
} satisfies UnctxTransformPluginOptions }))
addVitePlugin(() => UnctxTransformPlugin.vite(options))
addWebpackPlugin(() => UnctxTransformPlugin.webpack(options))
// Add composable tree-shaking optimisations // Add composable tree-shaking optimisations
const serverTreeShakeOptions: TreeShakeComposablesPluginOptions = { if (Object.keys(nuxt.options.optimization.treeShake.composables.server).length) {
sourcemap: !!nuxt.options.sourcemap.server, addBuildPlugin(TreeShakeComposablesPlugin({
composables: nuxt.options.optimization.treeShake.composables.server, sourcemap: !!nuxt.options.sourcemap.server,
composables: nuxt.options.optimization.treeShake.composables.server,
}), { client: false })
} }
if (Object.keys(serverTreeShakeOptions.composables).length) { if (Object.keys(nuxt.options.optimization.treeShake.composables.client).length) {
addVitePlugin(() => TreeShakeComposablesPlugin.vite(serverTreeShakeOptions), { client: false }) addBuildPlugin(TreeShakeComposablesPlugin({
addWebpackPlugin(() => TreeShakeComposablesPlugin.webpack(serverTreeShakeOptions), { client: false }) sourcemap: !!nuxt.options.sourcemap.client,
} composables: nuxt.options.optimization.treeShake.composables.client,
const clientTreeShakeOptions: TreeShakeComposablesPluginOptions = { }), { server: false })
sourcemap: !!nuxt.options.sourcemap.client,
composables: nuxt.options.optimization.treeShake.composables.client,
}
if (Object.keys(clientTreeShakeOptions.composables).length) {
addVitePlugin(() => TreeShakeComposablesPlugin.vite(clientTreeShakeOptions), { server: false })
addWebpackPlugin(() => TreeShakeComposablesPlugin.webpack(clientTreeShakeOptions), { server: false })
} }
}) })
if (!nuxt.options.dev) { if (!nuxt.options.dev) {
// DevOnly component tree-shaking - build time only // DevOnly component tree-shaking - build time only
addVitePlugin(() => DevOnlyPlugin.vite({ sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client })) addBuildPlugin(DevOnlyPlugin({
addWebpackPlugin(() => DevOnlyPlugin.webpack({ sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client })) sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
}))
} }
if (nuxt.options.dev) { if (nuxt.options.dev) {

View File

@ -10,7 +10,7 @@ interface DevOnlyPluginOptions {
const DEVONLY_COMP_SINGLE_RE = /<(?:dev-only|DevOnly|lazy-dev-only|LazyDevOnly)>[\s\S]*?<\/(?:dev-only|DevOnly|lazy-dev-only|LazyDevOnly)>/ const DEVONLY_COMP_SINGLE_RE = /<(?:dev-only|DevOnly|lazy-dev-only|LazyDevOnly)>[\s\S]*?<\/(?:dev-only|DevOnly|lazy-dev-only|LazyDevOnly)>/
const DEVONLY_COMP_RE = /<(?:dev-only|DevOnly|lazy-dev-only|LazyDevOnly)>[\s\S]*?<\/(?:dev-only|DevOnly|lazy-dev-only|LazyDevOnly)>/g const DEVONLY_COMP_RE = /<(?:dev-only|DevOnly|lazy-dev-only|LazyDevOnly)>[\s\S]*?<\/(?:dev-only|DevOnly|lazy-dev-only|LazyDevOnly)>/g
export const DevOnlyPlugin = createUnplugin((options: DevOnlyPluginOptions) => { export const DevOnlyPlugin = (options: DevOnlyPluginOptions) => createUnplugin(() => {
return { return {
name: 'nuxt:server-devonly:transform', name: 'nuxt:server-devonly:transform',
enforce: 'pre', enforce: 'pre',

View File

@ -6,7 +6,6 @@ import MagicString from 'magic-string'
interface LayerAliasingOptions { interface LayerAliasingOptions {
sourcemap?: boolean sourcemap?: boolean
transform?: boolean
root: string root: string
dev: boolean dev: boolean
layers: NuxtConfigLayer[] layers: NuxtConfigLayer[]
@ -15,7 +14,7 @@ interface LayerAliasingOptions {
const ALIAS_RE = /(?<=['"])[~@]{1,2}(?=\/)/g const ALIAS_RE = /(?<=['"])[~@]{1,2}(?=\/)/g
const ALIAS_RE_SINGLE = /(?<=['"])[~@]{1,2}(?=\/)/ const ALIAS_RE_SINGLE = /(?<=['"])[~@]{1,2}(?=\/)/
export const LayerAliasingPlugin = createUnplugin((options: LayerAliasingOptions) => { export const LayerAliasingPlugin = (options: LayerAliasingOptions) => createUnplugin((_options, meta) => {
const aliases: Record<string, Record<string, string>> = {} const aliases: Record<string, Record<string, string>> = {}
for (const layer of options.layers) { for (const layer of options.layers) {
const srcDir = layer.config.srcDir || layer.cwd const srcDir = layer.config.srcDir || layer.cwd
@ -52,12 +51,13 @@ export const LayerAliasingPlugin = createUnplugin((options: LayerAliasingOptions
// webpack-only transform // webpack-only transform
transformInclude: (id) => { transformInclude: (id) => {
if (!options.transform) { return false } if (meta.framework === 'vite') { return false }
const _id = normalize(id) const _id = normalize(id)
return layers.some(dir => _id.startsWith(dir)) return layers.some(dir => _id.startsWith(dir))
}, },
transform (code, id) { transform (code, id) {
if (!options.transform) { return } if (meta.framework === 'vite') { return }
const _id = normalize(id) const _id = normalize(id)
const layer = layers.find(l => _id.startsWith(l)) const layer = layers.find(l => _id.startsWith(l))

View File

@ -5,12 +5,12 @@ import { isJS, isVue } from '../utils'
type ImportPath = string type ImportPath = string
export interface TreeShakeComposablesPluginOptions { interface TreeShakeComposablesPluginOptions {
sourcemap?: boolean sourcemap?: boolean
composables: Record<ImportPath, string[]> composables: Record<ImportPath, string[]>
} }
export const TreeShakeComposablesPlugin = createUnplugin((options: TreeShakeComposablesPluginOptions) => { export const TreeShakeComposablesPlugin = (options: TreeShakeComposablesPluginOptions) => createUnplugin(() => {
/** /**
* @todo Use the options import-path to tree-shake composables in a safer way. * @todo Use the options import-path to tree-shake composables in a safer way.
*/ */

View File

@ -6,12 +6,12 @@ import { isJS, isVue } from '../utils'
const TRANSFORM_MARKER = '/* _processed_nuxt_unctx_transform */\n' const TRANSFORM_MARKER = '/* _processed_nuxt_unctx_transform */\n'
export interface UnctxTransformPluginOptions { interface UnctxTransformPluginOptions {
sourcemap?: boolean sourcemap?: boolean
transformerOptions: TransformerOptions transformerOptions: TransformerOptions
} }
export const UnctxTransformPlugin = createUnplugin((options: UnctxTransformPluginOptions) => { export const UnctxTransformPlugin = (options: UnctxTransformPluginOptions) => createUnplugin(() => {
const transformer = createTransformer(options.transformerOptions) const transformer = createTransformer(options.transformerOptions)
return { return {
name: 'unctx:transform', name: 'unctx:transform',

View File

@ -8,6 +8,7 @@ import { hash } from 'ohash'
import { camelCase } from 'scule' import { camelCase } from 'scule'
import { filename } from 'pathe/utils' import { filename } from 'pathe/utils'
import type { NuxtTemplate, NuxtTypeTemplate } from 'nuxt/schema' import type { NuxtTemplate, NuxtTypeTemplate } from 'nuxt/schema'
import type { Nitro } from 'nitropack'
import { annotatePlugins, checkForCircularDependencies } from './app' import { annotatePlugins, checkForCircularDependencies } from './app'
@ -512,6 +513,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`, `export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`,
`export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`, `export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`,
`export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`, `export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`,
`export const crawlLinks = ${!!((ctx.nuxt as any)._nitro as Nitro).options.prerender.crawlLinks}`,
].join('\n\n') ].join('\n\n')
}, },
} }

View File

@ -1,6 +1,6 @@
import { existsSync, readdirSync } from 'node:fs' import { existsSync, readdirSync } from 'node:fs'
import { mkdir, readFile } from 'node:fs/promises' import { mkdir, readFile } from 'node:fs/promises'
import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, logger, resolvePath, updateTemplates, useNitro } from '@nuxt/kit' import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, defineNuxtModule, findPath, logger, resolvePath, updateTemplates, useNitro } from '@nuxt/kit'
import { dirname, join, relative, resolve } from 'pathe' import { dirname, join, relative, resolve } from 'pathe'
import { genImport, genObjectFromRawEntries, genString } from 'knitwork' import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
import { joinURL } from 'ufo' import { joinURL } from 'ufo'
@ -8,6 +8,7 @@ import type { Nuxt, NuxtApp, NuxtPage } from 'nuxt/schema'
import { createRoutesContext } from 'unplugin-vue-router' import { createRoutesContext } from 'unplugin-vue-router'
import { resolveOptions } from 'unplugin-vue-router/options' import { resolveOptions } from 'unplugin-vue-router/options'
import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router' import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router'
import { createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
import type { NitroRouteConfig } from 'nitropack' import type { NitroRouteConfig } from 'nitropack'
import { defu } from 'defu' import { defu } from 'defu'
@ -15,7 +16,6 @@ import { distDir } from '../dirs'
import { resolveTypePath } from '../core/utils/types' import { resolveTypePath } from '../core/utils/types'
import { normalizeRoutes, resolvePagesRoutes, resolveRoutePaths } from './utils' import { normalizeRoutes, resolvePagesRoutes, resolveRoutePaths } from './utils'
import { extractRouteRules, getMappedPages } from './route-rules' import { extractRouteRules, getMappedPages } from './route-rules'
import type { PageMetaPluginOptions } from './plugins/page-meta'
import { PageMetaPlugin } from './plugins/page-meta' import { PageMetaPlugin } from './plugins/page-meta'
import { RouteInjectionPlugin } from './plugins/route-injection' import { RouteInjectionPlugin } from './plugins/route-injection'
@ -273,7 +273,7 @@ export default defineNuxtModule({
nuxt.hook('app:resolve', (app) => { nuxt.hook('app:resolve', (app) => {
const nitro = useNitro() const nitro = useNitro()
if (nitro.options.prerender.crawlLinks) { if (nitro.options.prerender.crawlLinks || Object.values(nitro.options.routeRules).some(rule => rule.prerender)) {
app.plugins.push({ app.plugins.push({
src: resolve(runtimeDir, 'plugins/prerender.server'), src: resolve(runtimeDir, 'plugins/prerender.server'),
mode: 'server', mode: 'server',
@ -311,7 +311,20 @@ export default defineNuxtModule({
}) })
nuxt.hook('nitro:build:before', (nitro) => { nuxt.hook('nitro:build:before', (nitro) => {
if (nuxt.options.dev || !nitro.options.static || nuxt.options.router.options.hashMode || !nitro.options.prerender.crawlLinks) { return } if (nuxt.options.dev || nuxt.options.router.options.hashMode) { return }
// Inject page patterns that explicitly match `prerender: true` route rule
if (!nitro.options.static && !nitro.options.prerender.crawlLinks) {
const routeRulesMatcher = toRouteMatcher(createRadixRouter({ routes: nitro.options.routeRules }))
for (const route of prerenderRoutes) {
const rules = defu({} as Record<string, any>, ...routeRulesMatcher.matchAll(route).reverse())
if (rules.prerender) {
nitro.options.prerender.routes.push(route)
}
}
}
if (!nitro.options.static || !nitro.options.prerender.crawlLinks) { return }
// Only hint the first route when `ssr: true` and no routes are provided // Only hint the first route when `ssr: true` and no routes are provided
// as the rest will be injected at runtime when this is prerendered // as the rest will be injected at runtime when this is prerendered
@ -418,13 +431,11 @@ export default defineNuxtModule({
} }
// Extract macros from pages // Extract macros from pages
const pageMetaOptions: PageMetaPluginOptions = {
dev: nuxt.options.dev,
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
}
nuxt.hook('modules:done', () => { nuxt.hook('modules:done', () => {
addVitePlugin(() => PageMetaPlugin.vite(pageMetaOptions)) addBuildPlugin(PageMetaPlugin({
addWebpackPlugin(() => PageMetaPlugin.webpack(pageMetaOptions)) dev: nuxt.options.dev,
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
}))
}) })
// Add prefetching support for middleware & layouts // Add prefetching support for middleware & layouts

View File

@ -10,7 +10,7 @@ import MagicString from 'magic-string'
import { isAbsolute } from 'pathe' import { isAbsolute } from 'pathe'
import { logger } from '@nuxt/kit' import { logger } from '@nuxt/kit'
export interface PageMetaPluginOptions { interface PageMetaPluginOptions {
dev?: boolean dev?: boolean
sourcemap?: boolean sourcemap?: boolean
} }
@ -36,7 +36,7 @@ if (import.meta.webpackHot) {
}) })
}` }`
export const PageMetaPlugin = createUnplugin((options: PageMetaPluginOptions) => { export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin(() => {
return { return {
name: 'nuxt:pages-macros-transform', name: 'nuxt:pages-macros-transform',
enforce: 'post', enforce: 'post',

View File

@ -1,20 +1,31 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import { joinURL } from 'ufo' import { joinURL } from 'ufo'
import { createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
import defu from 'defu'
import { defineNuxtPlugin } from '#app/nuxt' import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
import { prerenderRoutes } from '#app/composables/ssr' import { prerenderRoutes } from '#app/composables/ssr'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import _routes from '#build/routes' import _routes from '#build/routes'
import routerOptions from '#build/router.options' import routerOptions from '#build/router.options'
// @ts-expect-error virtual file
import { crawlLinks } from '#build/nuxt.config.mjs'
let routes: string[] let routes: string[]
let _routeRulesMatcher: undefined | ReturnType<typeof toRouteMatcher> = undefined
export default defineNuxtPlugin(async () => { export default defineNuxtPlugin(async () => {
if (!import.meta.server || !import.meta.prerender || routerOptions.hashMode) { if (!import.meta.server || !import.meta.prerender || routerOptions.hashMode) {
return return
} }
if (routes && !routes.length) { return } if (routes && !routes.length) { return }
const routeRules = useRuntimeConfig().nitro!.routeRules
if (!crawlLinks && routeRules && Object.values(routeRules).some(r => r.prerender)) {
_routeRulesMatcher = toRouteMatcher(createRadixRouter({ routes: routeRules }))
}
routes ||= Array.from(processRoutes(await routerOptions.routes?.(_routes) ?? _routes)) routes ||= Array.from(processRoutes(await routerOptions.routes?.(_routes) ?? _routes))
const batch = routes.splice(0, 10) const batch = routes.splice(0, 10)
prerenderRoutes(batch) prerenderRoutes(batch)
@ -24,10 +35,14 @@ export default defineNuxtPlugin(async () => {
const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/ const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/
function shouldPrerender (path: string) {
return !_routeRulesMatcher || defu({} as Record<string, any>, ..._routeRulesMatcher.matchAll(path).reverse()).prerender
}
function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPrerender = new Set<string>()) { function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPrerender = new Set<string>()) {
for (const route of routes) { for (const route of routes) {
// Add root of optional dynamic paths and catchalls // Add root of optional dynamic paths and catchalls
if (OPTIONAL_PARAM_RE.test(route.path) && !route.children?.length) { if (OPTIONAL_PARAM_RE.test(route.path) && !route.children?.length && shouldPrerender(currentPath)) {
routesToPrerender.add(currentPath) routesToPrerender.add(currentPath)
} }
// Skip dynamic paths // Skip dynamic paths
@ -35,7 +50,9 @@ function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPre
continue continue
} }
const fullPath = joinURL(currentPath, route.path) const fullPath = joinURL(currentPath, route.path)
routesToPrerender.add(fullPath) if (shouldPrerender(fullPath)) {
routesToPrerender.add(fullPath)
}
if (route.children) { if (route.children) {
processRoutes(route.children, fullPath, routesToPrerender) processRoutes(route.children, fullPath, routesToPrerender)
} }

View File

@ -4,7 +4,7 @@ import type { Component, Nuxt } from '@nuxt/schema'
import { kebabCase } from 'scule' import { kebabCase } from 'scule'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import { createTransformPlugin } from '../src/components/plugins/transform' import { TransformPlugin } from '../src/components/plugins/transform'
describe('components:transform', () => { describe('components:transform', () => {
it('should transform #components imports', async () => { it('should transform #components imports', async () => {
@ -92,7 +92,7 @@ function createTransformer (components: Component[], mode: 'client' | 'server' |
}, },
}, },
} as Nuxt } as Nuxt
const plugin = createTransformPlugin(stubNuxt, () => components, mode).vite() const plugin = TransformPlugin(stubNuxt, () => components, mode).vite()
return async (code: string, id: string) => { return async (code: string, id: string) => {
const result = await (plugin as any).transform!(code, id) const result = await (plugin as any).transform!(code, id)

View File

@ -3,7 +3,7 @@ import type { Plugin } from 'vite'
import { DevOnlyPlugin } from '../src/core/plugins/dev-only' import { DevOnlyPlugin } from '../src/core/plugins/dev-only'
import { normalizeLineEndings } from './utils' import { normalizeLineEndings } from './utils'
const pluginVite = DevOnlyPlugin.raw({}, { framework: 'vite' }) as Plugin const pluginVite = DevOnlyPlugin({}).raw({}, { framework: 'vite' }) as Plugin
const viteTransform = async (source: string, id: string) => { const viteTransform = async (source: string, id: string) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type

View File

@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
import type { Component } from '@nuxt/schema' import type { Component } from '@nuxt/schema'
import type { UnpluginOptions } from 'unplugin' import type { UnpluginOptions } from 'unplugin'
import { islandsTransform } from '../src/components/plugins/islands-transform' import { IslandsTransformPlugin } from '../src/components/plugins/islands-transform'
import { normalizeLineEndings } from './utils' import { normalizeLineEndings } from './utils'
const getComponents = () => [{ const getComponents = () => [{
@ -18,16 +18,16 @@ const getComponents = () => [{
preload: false, preload: false,
}] as Component[] }] as Component[]
const pluginWebpack = islandsTransform.raw({ const pluginWebpack = IslandsTransformPlugin({
getComponents, getComponents,
selectiveClient: true, selectiveClient: true,
}, { framework: 'webpack', webpack: { compiler: {} as any } }) }).raw({}, { framework: 'webpack', webpack: { compiler: {} as any } })
const viteTransform = async (source: string, id: string, selectiveClient = false) => { const viteTransform = async (source: string, id: string, selectiveClient = false) => {
const vitePlugin = islandsTransform.raw({ const vitePlugin = IslandsTransformPlugin({
getComponents, getComponents,
selectiveClient, selectiveClient,
}, { framework: 'vite' }) as Plugin }).raw({}, { framework: 'vite' }) as Plugin
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const result = await (vitePlugin.transform! as Function)(source, id) const result = await (vitePlugin.transform! as Function)(source, id)
@ -454,7 +454,7 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
" "
`) `)
expect(spyOnWarn).toHaveBeenCalledWith('nuxt-client attribute and client components within islands is only supported with Vite. file: hello.server.vue') expect(spyOnWarn).toHaveBeenCalledWith('The `nuxt-client` attribute and client components within islands are only supported with Vite. file: hello.server.vue')
}) })
}) })
}) })

View File

@ -27,7 +27,7 @@ function vuePlugin (options: Options) {
const WithClientOnly = normalizeLineEndings(readFileSync(path.resolve(fixtureDir, './components/client/WithClientOnlySetup.vue')).toString()) const WithClientOnly = normalizeLineEndings(readFileSync(path.resolve(fixtureDir, './components/client/WithClientOnlySetup.vue')).toString())
const treeshakeTemplatePlugin = TreeShakeTemplatePlugin.raw({ const treeshakeTemplatePlugin = TreeShakeTemplatePlugin({
sourcemap: false, sourcemap: false,
getComponents () { getComponents () {
return [{ return [{
@ -52,7 +52,7 @@ const treeshakeTemplatePlugin = TreeShakeTemplatePlugin.raw({
mode: 'client', mode: 'client',
}] }]
}, },
}, { framework: 'rollup' }) as Plugin }).raw({}, { framework: 'rollup' }) as Plugin
const treeshake = async (source: string): Promise<string> => { const treeshake = async (source: string): Promise<string> => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type

View File

@ -62,6 +62,6 @@ function transform (code: string, id = 'app.vue') {
definePageMeta: ['middleware', 'validate'], definePageMeta: ['middleware', 'validate'],
}, },
} }
const plugin = UnctxTransformPlugin.raw({ sourcemap: false, transformerOptions }, {} as any) as any const plugin = UnctxTransformPlugin({ sourcemap: false, transformerOptions }).raw({}, {} as any) as any
return plugin.transformInclude(id) ? Promise.resolve(plugin.transform(code)).then((r: any) => r?.code.replace(/^ {6}/gm, '').trim()) : null return plugin.transformInclude(id) ? Promise.resolve(plugin.transform(code)).then((r: any) => r?.code.replace(/^ {6}/gm, '').trim()) : null
} }

View File

@ -42,20 +42,20 @@
"@unhead/schema": "1.11.7", "@unhead/schema": "1.11.7",
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",
"@vitejs/plugin-vue-jsx": "4.0.1", "@vitejs/plugin-vue-jsx": "4.0.1",
"@vue/compiler-core": "3.5.10", "@vue/compiler-core": "3.5.11",
"@vue/compiler-sfc": "3.5.10", "@vue/compiler-sfc": "3.5.11",
"@vue/language-core": "2.1.6", "@vue/language-core": "2.1.6",
"c12": "2.0.1", "c12": "2.0.1",
"esbuild-loader": "4.2.2", "esbuild-loader": "4.2.2",
"h3": "1.12.0", "h3": "1.12.0",
"ignore": "6.0.2", "ignore": "6.0.2",
"nitropack": "2.9.7", "nitropack": "2.9.7",
"ofetch": "1.4.0", "ofetch": "1.4.1",
"unbuild": "latest", "unbuild": "latest",
"unctx": "2.3.1", "unctx": "2.3.1",
"unenv": "1.10.0", "unenv": "1.10.0",
"vite": "5.4.8", "vite": "5.4.8",
"vue": "3.5.10", "vue": "3.5.11",
"vue-bundle-renderer": "2.1.1", "vue-bundle-renderer": "2.1.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"vue-router": "4.4.5", "vue-router": "4.4.5",
@ -68,7 +68,7 @@
"defu": "^6.1.4", "defu": "^6.1.4",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"pkg-types": "^1.2.0", "pkg-types": "^1.2.1",
"scule": "^1.3.0", "scule": "^1.3.0",
"std-env": "^3.7.0", "std-env": "^3.7.0",
"ufo": "^1.5.4", "ufo": "^1.5.4",

View File

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

View File

@ -42,6 +42,12 @@ export interface NuxtTemplate<Options = TemplateDefaultOptions> {
write?: boolean write?: boolean
} }
export interface NuxtServerTemplate {
/** The target filename once the template is copied into the Nuxt buildDir */
filename: string
getContents: () => string | Promise<string>
}
export interface ResolvedNuxtTemplate<Options = TemplateDefaultOptions> extends NuxtTemplate<Options> { export interface ResolvedNuxtTemplate<Options = TemplateDefaultOptions> extends NuxtTemplate<Options> {
filename: string filename: string
dst: string dst: string

View File

@ -19,11 +19,11 @@
}, },
"devDependencies": { "devDependencies": {
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@unocss/reset": "0.63.2", "@unocss/reset": "0.63.4",
"critters": "0.0.24", "critters": "0.0.24",
"html-validate": "8.24.0", "html-validate": "8.24.1",
"htmlnano": "2.1.1", "htmlnano": "2.1.1",
"jiti": "2.3.1", "jiti": "2.3.3",
"knitwork": "1.1.0", "knitwork": "1.1.0",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"pathe": "1.1.2", "pathe": "1.1.2",
@ -31,7 +31,7 @@
"scule": "1.3.0", "scule": "1.3.0",
"tinyexec": "0.3.0", "tinyexec": "0.3.0",
"tinyglobby": "0.2.9", "tinyglobby": "0.2.9",
"unocss": "0.63.2", "unocss": "0.63.4",
"vite": "5.4.8" "vite": "5.4.8"
} }
} }

View File

@ -29,7 +29,7 @@
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"rollup": "4.24.0", "rollup": "4.24.0",
"unbuild": "latest", "unbuild": "latest",
"vue": "3.5.10" "vue": "3.5.11"
}, },
"dependencies": { "dependencies": {
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
@ -47,14 +47,14 @@
"externality": "^1.0.2", "externality": "^1.0.2",
"get-port-please": "^3.1.2", "get-port-please": "^3.1.2",
"h3": "^1.12.0", "h3": "^1.12.0",
"jiti": "^2.3.1", "jiti": "^2.3.3",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
"mlly": "^1.7.1", "mlly": "^1.7.2",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"pkg-types": "^1.2.0", "pkg-types": "^1.2.1",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"std-env": "^3.7.0", "std-env": "^3.7.0",

View File

@ -39,12 +39,12 @@
"fork-ts-checker-webpack-plugin": "^9.0.2", "fork-ts-checker-webpack-plugin": "^9.0.2",
"h3": "^1.12.0", "h3": "^1.12.0",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"jiti": "^2.3.1", "jiti": "^2.3.3",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
"memfs": "^4.12.0", "memfs": "^4.13.0",
"mini-css-extract-plugin": "^2.9.1", "mini-css-extract-plugin": "^2.9.1",
"mlly": "^1.7.1", "mlly": "^1.7.2",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"pify": "^6.1.0", "pify": "^6.1.0",
@ -76,7 +76,7 @@
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
"unbuild": "latest", "unbuild": "latest",
"vue": "3.5.10" "vue": "3.5.11"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.3.4" "vue": "^3.3.4"

File diff suppressed because it is too large Load Diff

View File

@ -618,6 +618,11 @@ describe('pages', () => {
expect(status).toBe(200) expect(status).toBe(200)
} }
}) })
it.skipIf(isDev() || isWebpack /* TODO: fix bug with import.meta.prerender being undefined in webpack build */)('prerenders pages hinted with a route rule', async () => {
const html = await $fetch('/prerender/test')
expect(html).toContain('should be prerendered: true')
})
}) })
describe('nuxt composables', () => { describe('nuxt composables', () => {
@ -1834,7 +1839,13 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
it('does not load stylesheet for page styles', async () => { it('does not load stylesheet for page styles', async () => {
const html: string = await $fetch<string>('/styles') const html: string = await $fetch<string>('/styles')
expect(html.match(/<link [^>]*href="[^"]*\.css">(?: crossorigin)?/g)?.filter(m => m.includes('entry'))?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(`undefined`) const cssFiles = html.match(/<link [^>]*href="[^"]*\.css"/g)
expect(cssFiles?.length).toBeGreaterThan(0)
expect(cssFiles?.filter(m => m.includes('entry'))?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(`
[
"<link rel="stylesheet" href="/_nuxt/entry.css"",
]
`)
}) })
it('still downloads client-only styles', async () => { it('still downloads client-only styles', async () => {

View File

@ -40,7 +40,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"213k"`) expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"213k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir) const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1389k"`) expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1390k"`)
const packages = modules.files const packages = modules.files
.filter(m => m.endsWith('package.json')) .filter(m => m.endsWith('package.json'))

View File

@ -67,6 +67,7 @@ export default defineNuxtConfig({
'/route-rules/middleware': { appMiddleware: 'route-rules-middleware' }, '/route-rules/middleware': { appMiddleware: 'route-rules-middleware' },
'/hydration/spa-redirection/**': { ssr: false }, '/hydration/spa-redirection/**': { ssr: false },
'/no-scripts': { experimentalNoScripts: true }, '/no-scripts': { experimentalNoScripts: true },
'/prerender/**': { prerender: true },
}, },
output: { dir: process.env.NITRO_OUTPUT_DIR }, output: { dir: process.env.NITRO_OUTPUT_DIR },
prerender: { prerender: {

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
const wasPrerendered = useState(() => import.meta.prerender)
</script>
<template>
<div>
should be prerendered: {{ wasPrerendered }}
</div>
</template>