Merge branch 'main' into feat/kit-nuxt-module-options

This commit is contained in:
Harlan Wilton 2023-08-04 22:16:54 +03:00 committed by GitHub
commit 317ab1a342
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 4022 additions and 1498 deletions

View File

@ -10,7 +10,7 @@ body:
Please use a template below to create a minimal reproduction Please use a template below to create a minimal reproduction
👉 https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz 👉 https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz
👉 https://codesandbox.io/p/github/nuxt/starter/v3-codesandbox 👉 https://codesandbox.io/s/github/nuxt/starter/v3-codesandbox
- type: textarea - type: textarea
id: bug-env id: bug-env
attributes: attributes:

View File

@ -10,7 +10,7 @@ body:
Please use a template below to create a minimal reproduction Please use a template below to create a minimal reproduction
👉 https://stackblitz.com/github/nuxt/starter/tree/v2 👉 https://stackblitz.com/github/nuxt/starter/tree/v2
👉 https://codesandbox.io/p/github/nuxt/starter/v2 👉 https://codesandbox.io/s/github/nuxt/starter/v2
- type: textarea - type: textarea
id: bug-env id: bug-env
attributes: attributes:

View File

@ -86,7 +86,7 @@ jobs:
run: pnpm install run: pnpm install
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
with: with:
languages: javascript languages: javascript
queries: +security-and-quality queries: +security-and-quality
@ -98,7 +98,7 @@ jobs:
path: packages path: packages
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
with: with:
category: "/language:javascript" category: "/language:javascript"
@ -112,7 +112,6 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
module: ['bundler', 'node'] module: ['bundler', 'node']
base: ['with-base-url', 'without-base-url']
steps: steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
@ -135,7 +134,6 @@ jobs:
run: pnpm test:types run: pnpm test:types
env: env:
MODULE_RESOLUTION: ${{ matrix.module }} MODULE_RESOLUTION: ${{ matrix.module }}
TS_BASE_URL: ${{ matrix.base }}
lint: lint:
# autofix workflow will be triggered instead for PRs # autofix workflow will be triggered instead for PRs

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'nuxt/nuxt' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') if: github.repository == 'nuxt/nuxt' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
with: with:
script: | script: |
const user = context.payload.sender.login const user = context.payload.sender.login
@ -48,7 +48,7 @@ jobs:
}) })
throw new Error('not allowed') throw new Error('not allowed')
} }
- uses: actions/github-script@v6 - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
id: get-pr-data id: get-pr-data
with: with:
script: | script: |
@ -64,12 +64,12 @@ jobs:
repo: pr.head.repo.full_name repo: pr.head.repo.full_name
} }
- id: generate-token - id: generate-token
uses: tibdex/github-app-token@v1 uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
with: with:
app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
repository: "${{ github.repository_owner }}/ecosystem-ci" repository: "${{ github.repository_owner }}/ecosystem-ci"
- uses: actions/github-script@v6 - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
id: trigger id: trigger
env: env:
COMMENT: ${{ github.event.comment.body }} COMMENT: ${{ github.event.comment.body }}

View File

@ -25,5 +25,5 @@ jobs:
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions # From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files - name: Check workflow files
run: | run: |
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/590d3bd9dde0c91f7a66071d40eb84716526e5a6/scripts/download-actionlint.bash)
./actionlint -color -shellcheck="" ./actionlint -color -shellcheck=""

View File

@ -66,6 +66,6 @@ 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@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0 uses: github/codeql-action/upload-sarif@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -7,14 +7,20 @@ on:
- edited - edited
- synchronize - synchronize
permissions:
contents: read
jobs: jobs:
main: main:
permissions:
pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs
statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
if: github.repository == 'nuxt/nuxt' if: github.repository == 'nuxt/nuxt'
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Semantic pull request name: Semantic pull request
steps: steps:
- name: Validate PR title - name: Validate PR title
uses: amannn/action-semantic-pull-request@v5 uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 # v5.2.0
with: with:
scopes: | scopes: |
kit kit

12
.website/.gitignore vendored Executable file
View File

@ -0,0 +1,12 @@
node_modules
*.iml
.idea
*.log*
.nuxt
.vscode
.DS_Store
coverage
dist
sw.*
.env
.output

35
.website/README.md Executable file
View File

@ -0,0 +1,35 @@
# Nuxt Docs Website
This is a temporary directory until we open source the repository for nuxt.com.
The goal is to simplify the contribution in the meantine to the documentation by having the possibility to preview the changes locally.
## Setup
Install dependencies in the root of the `nuxt` folder:
```bash
pnpm i
```
Then stub the dependencies:
```bash
pnpm build:stub
```
## Development
In the root of the `nuxt` folder, run:
```bash
pnpm docs:dev
```
Then open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Update the documentation within the `docs` folder.
---
For a detailed explanation of how things work, check out [Docus](https://docus.dev).

14
.website/app.config.ts Normal file
View File

@ -0,0 +1,14 @@
export default defineAppConfig({
docus: {
title: 'Nuxt Docs [dev]',
description: 'The best place to start your documentation.',
socials: {
twitter: 'nuxt_js',
github: 'nuxt/nuxt'
},
aside: {
level: 1,
collapsed: false,
},
}
})

20
.website/nuxt.config.ts Executable file
View File

@ -0,0 +1,20 @@
import { createResolver } from 'nuxt/kit'
const { resolve } = createResolver(import.meta.url)
export default defineNuxtConfig({
// https://github.com/nuxt-themes/docus
extends: '@nuxt-themes/docus',
content: {
sources: {
docs: {
driver: 'fs',
prefix: '/',
base: resolve('../docs')
}
}
},
experimental: {
renderJsonPayloads: false
}
})

14
.website/package.json Executable file
View File

@ -0,0 +1,14 @@
{
"name": "docus-starter",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"preview": "nuxt preview"
},
"devDependencies": {
"@nuxt-themes/docus": "1.14.6"
}
}

3
.website/tsconfig.json Executable file
View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -81,52 +81,61 @@ There are two ways to deploy a Nuxt application to any static hosting services:
### Crawl-based Pre-rendering ### Crawl-based Pre-rendering
Use the [`nuxi generate` command](/docs/api/commands/generate) to build your application. For every page, Nuxt uses a crawler to generate a corresponding HTML and payload files. The built files will be generated in the `.output/public` directory. Use the [`nuxi generate` command](/docs/api/commands/generate) to build and pre-render your application using the [Nitro](/docs/guide/concepts/server-engine) crawler. This command is similar to `nuxt build` with the `nitro.static` option set to `true`, or running `nuxt build --prerender`.
```bash ```bash
npx nuxi generate npx nuxi generate
``` ```
You can enable crawl-based pre-rendering when using `nuxt build` in the `nuxt.config` file: That's it! You can now deploy the `.output/public` directory to any static hosting service or preview it locally with `npx serve .output/public`.
```ts [nuxt.config.ts|js] Working of the Nitro crawler:
defineNuxtConfig({
nitro: { 1. Load the HTML of your application's root route (`/`), any non-dynamic pages in your `~/pages` directory, and any other routes in the `nitro.prerender.routes` array.
prerender: { 2. Save the HTML and `payload.json` to the `~/.output/public/` directory to be served statically.
crawlLinks: true 3. Find all anchor tags (`<a href="...">`) in the HTML to navigate to other routes.
} 4. Repeat steps 1-3 for each anchor tag found until there are no more anchor tags to crawl.
}
}) This is important to understand since pages that are not linked to a discoverable page can't be pre-rendered automatically.
```
::alert{type=info}
Read more about the [`nuxi generate` command](/docs/api/commands/generate#nuxi-generate).
::
### Selective Pre-rendering ### Selective Pre-rendering
You can manually specify routes that [Nitro](/docs/guide/concepts/server-engine) will fetch and pre-render during the build. You can manually specify routes that [Nitro](/docs/guide/concepts/server-engine) will fetch and pre-render during the build or ignore routes that you don't want to pre-render like `/dynamic` in the `nuxt.config` file:
```ts [nuxt.config.ts|js] ```ts [nuxt.config.ts|js]
defineNuxtConfig({ defineNuxtConfig({
nitro: { nitro: {
prerender: { prerender: {
routes: ['/user/1', '/user/2'] routes: ['/user/1', '/user/2']
ignore: ['/dynamic']
} }
} }
}) })
``` ```
When using this option with `nuxi build`, static payloads won't be generated by default at build time. For now, selective payload generation is under an experimental flag. You can combine this with the `crawLinks` option to pre-render a set of routes that the crawler can't discover like your `/sitemap.xml` or `/robots.txt`:
```ts [nuxt.config.ts|js] ```ts [nuxt.config.ts|js]
defineNuxtConfig({ defineNuxtConfig({
/* The /dynamic route won't be crawled */
nitro: { nitro: {
prerender: { crawlLinks: true, ignore: ['/dynamic'] } prerender: {
}, crawlLinks: true,
experimental: { routes: ['/sitemap.xml', '/robots.txt']
payloadExtraction: true }
} }
}) })
``` ```
Setting `nitro.prerender` to `true` is similar to `nitro.prerender.crawlLinks` to `true`.
::alert{type=info}
Read more about [pre-rendering](https://nitro.unjs.io/config#prerender) in the Nitro documentation.
::
### Client-side Only Rendering ### Client-side Only Rendering
If you don't want to pre-render your routes, another way of using static hosting is to set the `ssr` property to `false` in the `nuxt.config` file. The `nuxi generate` command will then output an `.output/public/index.html` entrypoint and JavaScript bundles like a classic client-side Vue.js application. If you don't want to pre-render your routes, another way of using static hosting is to set the `ssr` property to `false` in the `nuxt.config` file. The `nuxi generate` command will then output an `.output/public/index.html` entrypoint and JavaScript bundles like a classic client-side Vue.js application.

View File

@ -31,7 +31,7 @@ Start with one of our starters and themes directly by opening [nuxt.new](https:/
::details ::details
:summary[Additional notes for an optimal setup:] :summary[Additional notes for an optimal setup:]
- **Node.js**: Make sure to use an even numbered version (18, 20, etc) - **Node.js**: Make sure to use an even numbered version (18, 20, etc)
- **Nuxtr**: Install the community-developed [Nuxtr extension](https://marketplace.visualstudio.com/items?itemName=Nuxtr.nuxtr-vscode)
- **Volar**: Either enable [**Take Over Mode**](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) (recommended) or add the [TypeScript Vue Plugin](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) - **Volar**: Either enable [**Take Over Mode**](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) (recommended) or add the [TypeScript Vue Plugin](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin)
If you have enabled **Take Over Mode** or installed the **TypeScript Vue Plugin (Volar)**, you can disable generating the shim for `*.vue` files in your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) file: If you have enabled **Take Over Mode** or installed the **TypeScript Vue Plugin (Volar)**, you can disable generating the shim for `*.vue` files in your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) file:

View File

@ -71,7 +71,7 @@ When a `<NuxtLink>` enters the viewport on the client side, Nuxt will automatica
The `useRoute()` composable can be used in a `<script setup>` block or a `setup()` method of a Vue component to access the current route details. The `useRoute()` composable can be used in a `<script setup>` block or a `setup()` method of a Vue component to access the current route details.
```vue [pages/posts/[id].vue] ```vue [pages/posts/[id\\].vue]
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute() const route = useRoute()
@ -133,7 +133,7 @@ The `validate` property accepts the `route` as an argument. You can return a boo
If you have a more complex use case, then you can use anonymous route middleware instead. If you have a more complex use case, then you can use anonymous route middleware instead.
```vue [pages/posts/[id].vue] ```vue [pages/posts/[id\\].vue]
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
validate: async (route) => { validate: async (route) => {

View File

@ -318,7 +318,7 @@ To apply dynamic transitions using conditional logic, you can leverage inline [m
::code-group ::code-group
```html [pages/[id].vue] ```html [pages/[id\\].vue]
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
pageTransition: { pageTransition: {

View File

@ -111,7 +111,7 @@ If you throw an error created with `createError`:
### Example ### Example
```vue [pages/movies/[slug].vue] ```vue [pages/movies/[slug\\].vue]
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute() const route = useRoute()
const { data } = await useFetch(`/api/movies/${route.params.slug}`) const { data } = await useFetch(`/api/movies/${route.params.slug}`)

View File

@ -90,7 +90,7 @@ export default defineNuxtConfig({
// Homepage pre-rendered at build time // Homepage pre-rendered at build time
'/': { prerender: true }, '/': { prerender: true },
// Product page generated on-demand, revalidates in background // Product page generated on-demand, revalidates in background
'/products/**': { swr: true }, '/products/**': { swr: 3600 },
// Blog post generated on-demand once until next deploy // Blog post generated on-demand once until next deploy
'/blog/**': { isr: true }, '/blog/**': { isr: true },
// Admin dashboard renders only on client-side // Admin dashboard renders only on client-side
@ -108,8 +108,8 @@ The different properties you can use are the following:
- `ssr: boolean`{lang=ts} - Disables server-side rendering for sections of your app and make them SPA-only with `ssr: false` - `ssr: boolean`{lang=ts} - Disables server-side rendering for sections of your app and make them SPA-only with `ssr: false`
- `cors: boolean`{lang=ts} - Automatically adds cors headers with `cors: true` - you can customize the output by overriding with `headers` - `cors: boolean`{lang=ts} - Automatically adds cors headers with `cors: true` - you can customize the output by overriding with `headers`
- `headers: object`{lang=ts} - Add specific headers to sections of your site - for example, your assets - `headers: object`{lang=ts} - Add specific headers to sections of your site - for example, your assets
- `swr: number`{lang=ts} - Add cache headers to the server response and cache it on the server or reverse proxy for a configurable TTL (time to live). The `node-server` preset of Nitro is able to cache the full response. When the TTL expired, the cached response will be sent while the page will be regenerated in the background. - `swr: number|boolean`{lang=ts} - Add cache headers to the server response and cache it on the server or reverse proxy for a configurable TTL (time to live). The `node-server` preset of Nitro is able to cache the full response. When the TTL expired, the cached response will be sent while the page will be regenerated in the background. If true is used, a `stale-while-revalidate` header is added without a MaxAge.
- `isr: boolean`{lang=ts} - The behavior is the same as `swr` except that we are able to add the response to the CDN cache on platforms that support this (currently Netlify or Vercel) - `isr: number|boolean`{lang=ts} - The behavior is the same as `swr` except that we are able to add the response to the CDN cache on platforms that support this (currently Netlify or Vercel). If `true` is used, the content persists until the next deploy inside the CDN.
- `prerender:boolean`{lang=ts} - Prerenders routes at build time and includes them in your build as static assets - `prerender:boolean`{lang=ts} - Prerenders routes at build time and includes them in your build as static assets
- `experimentalNoScripts: boolean`{lang=ts} - Disables rendering of Nuxt scripts and JS resource hints for sections of your site. - `experimentalNoScripts: boolean`{lang=ts} - Disables rendering of Nuxt scripts and JS resource hints for sections of your site.

View File

@ -67,7 +67,7 @@ The module automatically loads and parses them.
To render content pages, add a [catch-all route](/docs/guide/directory-structure/pages/#catch-all-route) using the `ContentDoc` component: To render content pages, add a [catch-all route](/docs/guide/directory-structure/pages/#catch-all-route) using the `ContentDoc` component:
```vue [pages/[...slug].vue] ```vue [pages/[...slug\\].vue]
<template> <template>
<main> <main>
<ContentDoc /> <ContentDoc />

View File

@ -110,7 +110,7 @@ If you want a parameter to be _optional_, you must enclose it in double square b
Given the example above, you can access group/id within your component via the `$route` object: Given the example above, you can access group/id within your component via the `$route` object:
```vue [pages/users-[group]/[id].vue] ```vue [pages/users-[group\\]/[id\\].vue]
<template> <template>
<p>{{ $route.params.group }} - {{ $route.params.id }}</p> <p>{{ $route.params.group }} - {{ $route.params.id }}</p>
</template> </template>
@ -138,7 +138,7 @@ if (route.params.group === 'admins' && !route.params.id) {
If you need a catch-all route, you create it by using a file named like `[...slug].vue`. This will match _all_ routes under that path. If you need a catch-all route, you create it by using a file named like `[...slug].vue`. This will match _all_ routes under that path.
```vue [pages/[...slug].vue] ```vue [pages/[...slug\\].vue]
<template> <template>
<p>{{ $route.params.slug }}</p> <p>{{ $route.params.slug }}</p>
</template> </template>

View File

@ -151,7 +151,7 @@ Server routes can use dynamic parameters within brackets in the file name like `
**Example:** **Example:**
```ts [server/api/hello/[name].ts] ```ts [server/api/hello/[name\\].ts]
export default defineEventHandler((event) => `Hello, ${event.context.params.name}!`) export default defineEventHandler((event) => `Hello, ${event.context.params.name}!`)
``` ```
@ -181,11 +181,11 @@ Catch-all routes are helpful for fallback route handling. For example, creating
**Examples:** **Examples:**
```ts [server/api/foo/[...].ts] ```ts [server/api/foo/[...\\].ts]
export default defineEventHandler(() => `Default foo handler`) export default defineEventHandler(() => `Default foo handler`)
``` ```
```ts [server/api/[...].ts] ```ts [server/api/[...\\].ts]
export default defineEventHandler(() => `Default api handler`) export default defineEventHandler(() => `Default api handler`)
``` ```
@ -221,7 +221,7 @@ If no errors are thrown, a status code of `200 OK` will be returned. Any uncaugh
To return other error codes, throw an exception with `createError` To return other error codes, throw an exception with `createError`
```ts [server/api/validation/[id].ts] ```ts [server/api/validation/[id\\].ts]
export default defineEventHandler((event) => { export default defineEventHandler((event) => {
const id = parseInt(event.context.params.id) as number const id = parseInt(event.context.params.id) as number
if (!Number.isInteger(id)) { if (!Number.isInteger(id)) {
@ -240,7 +240,7 @@ To return other status codes, you can use the `setResponseStatus` utility.
For example, to return `202 Accepted` For example, to return `202 Accepted`
```ts [server/api/validation/[id].ts] ```ts [server/api/validation/[id\\].ts]
export default defineEventHandler((event) => { export default defineEventHandler((event) => {
setResponseStatus(event, 202) setResponseStatus(event, 202)
}) })
@ -284,7 +284,7 @@ export default defineNuxtConfig({
### Using a Nested Router ### Using a Nested Router
```ts [server/api/hello/[...slug].ts] ```ts [server/api/hello/[...slug\\].ts]
import { createRouter, defineEventHandler, useBase } from 'h3' import { createRouter, defineEventHandler, useBase } from 'h3'
const router = createRouter() const router = createRouter()

View File

@ -7,6 +7,8 @@ head.title: ".env"
# .env File # .env File
## At Build, Dev, and Generate Time
Nuxt CLI has built-in [dotenv](https://github.com/motdotla/dotenv) support in development mode and when running `nuxi build` and `nuxi generate`. Nuxt CLI has built-in [dotenv](https://github.com/motdotla/dotenv) support in development mode and when running `nuxi build` and `nuxi generate`.
In addition to any process environment variables, if you have a `.env` file in your project root directory, it will be automatically loaded **at build, dev, and generate time**, and any environment variables set there will be accessible within your `nuxt.config` file and modules. In addition to any process environment variables, if you have a `.env` file in your project root directory, it will be automatically loaded **at build, dev, and generate time**, and any environment variables set there will be accessible within your `nuxt.config` file and modules.
@ -27,7 +29,17 @@ When updating `.env` in development mode, the Nuxt instance is automatically res
Note that removing a variable from `.env` or removing the `.env` file entirely will not unset values that have already been set. Note that removing a variable from `.env` or removing the `.env` file entirely will not unset values that have already been set.
:: ::
However, **after your server is built**, you are responsible for setting environment variables when you run the server. Your `.env` file will not be read at this point. How you do this is different for every environment. On a Linux server, you could pass the environment variables as arguments using the terminal `DATABASE_HOST=mydatabaseconnectionstring node .output/server/index.mjs`. Or you could source your env file using `source .env && node .output/server/index.mjs`. ## Production Preview
**After your server is built**, you are responsible for setting environment variables when you run the server. Your `.env` file will not be read at this point. How you do this is different for every environment.
For local production preview purpose, we recommend using [`nuxi preview`](https://nuxt.com/docs/api/commands/preview) since using this command, the `.env` file will be loaded into `process.env` for convenience. Note that this command requires dependencies to be installed in the package directory.
Or you could pass the environment variables as arguments using the terminal. For example, on Linux or macOS:
```bash
DATABASE_HOST=mydatabaseconnectionstring node .output/server/index.mjs
```
Note that for a purely static site, it is not possible to set runtime configuration config after your project is prerendered. Note that for a purely static site, it is not possible to set runtime configuration config after your project is prerendered.

View File

@ -150,9 +150,9 @@ It is also possible to type your runtime config manually:
declare module 'nuxt/schema' { declare module 'nuxt/schema' {
interface RuntimeConfig { interface RuntimeConfig {
apiSecret: string apiSecret: string
public: { }
apiBase: string interface PublicRuntimeConfig {
} apiBase: string
} }
} }
// It is always important to ensure you import/export something when augmenting a type // It is always important to ensure you import/export something when augmenting a type

View File

@ -42,6 +42,7 @@ You may need to update the config below with a path to your web browser. For mor
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "server: nuxt", "name": "server: nuxt",
"outputCapture": "std",
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs", "program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
"args": [ "args": [
"dev" "dev"
@ -60,6 +61,12 @@ You may need to update the config below with a path to your web browser. For mor
} }
``` ```
If you prefer your usual browser extensions, add this to the _chrome_ configuration above:
```json5
"userDataDir": false,
```
### Example JetBrains IDEs Debug Configuration ### Example JetBrains IDEs Debug Configuration
You can also debug your Nuxt app in JetBrains IDEs such as IntelliJ IDEA, WebStorm, or PhpStorm. You can also debug your Nuxt app in JetBrains IDEs such as IntelliJ IDEA, WebStorm, or PhpStorm.

View File

@ -13,7 +13,7 @@ Within the template of a Vue component, you can access the route using `$route`.
In the following example, we call an API via [`useFetch`](/docs/api/composables/use-fetch) using a dynamic page parameter - `slug` - as part of the URL. In the following example, we call an API via [`useFetch`](/docs/api/composables/use-fetch) using a dynamic page parameter - `slug` - as part of the URL.
```html [~/pages/[slug].vue] ```html [~/pages/[slug\\].vue]
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute() const route = useRoute()
const { data: mountain } = await useFetch(`https://api.nuxtjs.dev/mountains/${route.params.slug}`) const { data: mountain } = await useFetch(`https://api.nuxtjs.dev/mountains/${route.params.slug}`)

View File

@ -0,0 +1,50 @@
---
title: "<NuxtIsland>"
description: "Nuxt provides `<NuxtIsland>` component to render a non-interactive component without any client JS"
---
# `<NuxtIsland>`
Nuxt provide `<NuxtIsland>` to render components only server side.
When rendering an island component, the content of the island component is static, thus no JS is downloaded client-side.
Changing the island component props triggers a refetch of the island component to re-render it again.
::alert{type=warning}
This component is experimental and in order to use it you must enable the `experimental.componentsIsland` option in your `nuxt.config`.
::
::alert{type=info}
Global styles of your application are sent with the response
::
::alert{type=info}
Server only components use `<NuxtIsland>` under the hood
::
## Props
- **name** : Name of the component to render.
- **type**: `string`
- **required**
- **lazy**: Make the component non-blocking.
- **type**: `boolean`
- **default**: `false`
- **props**: Props to send to the component to render.
- **type**: `Record<string, any>`
- **source**: Remote source to call the island to render.
- **type**: `string`
::alert{type=warning}
Remote islands need `experimental.componentsIsland` to be `'local+remote'` in your `nuxt.config`.
::
## `Slots`
Slots can be passed to an island component if declared.
Every slot is interactive since the parent component is the one providing it.
Some slots are reserved to `NuxtIsland` for special cases.
- **fallback**: Specify the content to be rendered before the island loads (if the component is lazy) or if `NuxtIsland` fails to fetch the component.

View File

@ -19,7 +19,7 @@ If you throw an error created with `createError`:
### Example ### Example
```vue [pages/movies/[slug].vue] ```vue [pages/movies/[slug\\].vue]
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute() const route = useRoute()
const { data } = await useFetch(`/api/movies/${route.params.slug}`) const { data } = await useFetch(`/api/movies/${route.params.slug}`)

View File

@ -32,6 +32,7 @@ interface PageMeta {
keepalive?: boolean | KeepAliveProps keepalive?: boolean | KeepAliveProps
layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey> layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>
middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard> middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
[key: string]: unknown [key: string]: unknown
} }
``` ```
@ -98,6 +99,12 @@ interface PageMeta {
Validate whether a given route can validly be rendered with this page. Return true if it is valid, or false if not. If another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked). Validate whether a given route can validly be rendered with this page. Return true if it is valid, or false if not. If another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked).
**`scrollTopTop`**
- **Type**: `boolean | (to: RouteLocationNormalized, from: RouteLocationNormalized) => boolean`
Tell Nuxt to scroll to the top before rendering the page or not. If you want to overwrite the default scroll behavior of Nuxt, you can do so in `~/app/router.options.ts` (see [docs](/docs/guide/directory-structure/pages/#router-options)) for more info.
**`[key: string]`** **`[key: string]`**
- **Type**: `any` - **Type**: `any`

View File

@ -112,3 +112,42 @@ description: Nuxt Kit provides composable utilities to help interacting with Nux
- `extendViteConfig(callback, options?)` - `extendViteConfig(callback, options?)`
- `addWebpackPlugin(webpackPlugin, options?)` - `addWebpackPlugin(webpackPlugin, options?)`
- `addVitePlugin(vitePlugin, options?)` - `addVitePlugin(vitePlugin, options?)`
## Examples
### Accessing Nuxt Vite Config
If you are building an integration that needs access to the runtime Vite or webpack config that Nuxt uses, it is possible to extract this using Kit utilities.
Some examples of projects doing this already:
- [histoire](https://github.com/histoire-dev/histoire/blob/main/packages/histoire-plugin-nuxt/src/index.ts)
- [nuxt-vitest](https://github.com/danielroe/nuxt-vitest/blob/main/packages/nuxt-vitest/src/config.ts)
- [@storybook-vue/nuxt](https://github.com/storybook-vue/nuxt/blob/main/src/preset.ts)
Here is a brief example of how you might access the Vite config from a project; you could implement a similar approach to get the webpack configuration.
```js
import { loadNuxt, buildNuxt } from '@nuxt/kit'
// https://github.com/nuxt/nuxt/issues/14534
async function getViteConfig() {
const nuxt = await loadNuxt({ cwd: process.cwd(), dev: false, overrides: { ssr: false } })
return new Promise((resolve, reject) => {
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
if (isClient) {
resolve(config)
throw new Error('_stop_')
}
})
buildNuxt(nuxt).catch((err) => {
if (!err.toString().includes('_stop_')) {
reject(err)
}
})
}).finally(() => nuxt.close())
}
const viteConfig = await getViteConfig()
console.log(viteConfig)
```

View File

@ -15,3 +15,7 @@ Option | Default | Description
-------------------------|-----------------|------------------ -------------------------|-----------------|------------------
`rootDir` | `.` | The root directory of the application to generate `rootDir` | `.` | The root directory of the application to generate
`--dotenv` | `.` | Point to another `.env` file to load, **relative** to the root directory. `--dotenv` | `.` | Point to another `.env` file to load, **relative** to the root directory.
::alert{type=info}
Read more about [pre-rendering and static hosting](/docs/1.getting-started/10.deployment.md#static-hosting).
::

View File

@ -33,16 +33,16 @@ If your issue concerns Vue 3 or Vite, please try to reproduce it first with the
**Nuxt 3**: **Nuxt 3**:
:button-link[Nuxt 3 on StackBlitz]{href="https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz" blank .mr-2} :button-link[Nuxt 3 on StackBlitz]{href="https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz" blank .mr-2}
:button-link[Nuxt 3 on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt/starter/v3-codesandbox" blank} :button-link[Nuxt 3 on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt/starter/v3-codesandbox" blank}
**Nuxt Bridge**: **Nuxt Bridge**:
:button-link[Nuxt Bridge on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt/starter/v2-bridge-codesandbox" blank} :button-link[Nuxt Bridge on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt/starter/v2-bridge-codesandbox" blank}
**Vue 3**: **Vue 3**:
:button-link[Vue 3 SSR on StackBlitz]{href="https://stackblitz.com/github/nuxt-contrib/vue3-ssr-starter/tree/main?terminal=dev" blank .mr-2} :button-link[Vue 3 SSR on StackBlitz]{href="https://stackblitz.com/github/nuxt-contrib/vue3-ssr-starter/tree/main?terminal=dev" blank .mr-2}
:button-link[Vue 3 SSR on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt-contrib/vue3-ssr-starter/main" blank .mr-2} :button-link[Vue 3 SSR on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt-contrib/vue3-ssr-starter/main" blank .mr-2}
:button-link[Vue 3 SSR Template]{href="https://github.com/nuxt-contrib/vue3-ssr-starter/generate" blank} :button-link[Vue 3 SSR Template]{href="https://github.com/nuxt-contrib/vue3-ssr-starter/generate" blank}
Once you've reproduced the issue, remove as much code from your reproduction as you can (while still recreating the bug). The time spent making the reproduction as minimal as possible will make a huge difference to whoever sets out to fix the issue. Once you've reproduced the issue, remove as much code from your reproduction as you can (while still recreating the bug). The time spent making the reproduction as minimal as possible will make a huge difference to whoever sets out to fix the issue.

View File

@ -114,7 +114,7 @@ See [layout migration](/docs/migration/pages-and-layouts).
The validate hook in Nuxt 3 only accepts a single argument, the `route`. Just as in Nuxt 2, you can return a boolean value. If you return false and another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked). The validate hook in Nuxt 3 only accepts a single argument, the `route`. Just as in Nuxt 2, you can return a boolean value. If you return false and another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked).
```diff [pages/users/[id].vue] ```diff [pages/users/[id\\].vue]
- <script> - <script>
- export default { - export default {
- async validate({ params }) { - async validate({ params }) {
@ -135,7 +135,7 @@ The validate hook in Nuxt 3 only accepts a single argument, the `route`. Just as
This is not supported in Nuxt 3. Instead, you can directly use a watcher to trigger refetching data. This is not supported in Nuxt 3. Instead, you can directly use a watcher to trigger refetching data.
```vue [pages/users/[id].vue] ```vue [pages/users/[id\\].vue]
<script setup lang="ts"> <script setup lang="ts">
const route = useRoute() const route = useRoute()
const { data, refresh } = await useFetch('/api/user') const { data, refresh } = await useFetch('/api/user')

View File

@ -14,6 +14,7 @@
"lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md", "lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md",
"lint:docs:fix": "markdownlint ./docs --fix && case-police 'docs/**/*.md' *.md --fix", "lint:docs:fix": "markdownlint ./docs --fix && case-police 'docs/**/*.md' *.md --fix",
"lint:knip": "pnpx knip", "lint:knip": "pnpx knip",
"docs:dev": "nuxi dev .website",
"play": "nuxi dev playground", "play": "nuxi dev playground",
"play:build": "nuxi build playground", "play:build": "nuxi build playground",
"play:preview": "nuxi preview playground", "play:preview": "nuxi preview playground",
@ -35,9 +36,9 @@
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"nuxi": "workspace:*", "nuxi": "workspace:*",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"vite": "4.4.7", "vite": "4.4.8",
"vue": "3.3.4", "vue": "3.3.4",
"magic-string": "^0.30.1" "magic-string": "^0.30.2"
}, },
"devDependencies": { "devDependencies": {
"@actions/core": "1.10.0", "@actions/core": "1.10.0",
@ -45,7 +46,7 @@
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@nuxtjs/eslint-config-typescript": "12.0.0", "@nuxtjs/eslint-config-typescript": "12.0.0",
"@types/fs-extra": "11.0.1", "@types/fs-extra": "11.0.1",
"@types/node": "18.17.0", "@types/node": "18.17.1",
"@types/semver": "7.5.0", "@types/semver": "7.5.0",
"case-police": "0.6.1", "case-police": "0.6.1",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -53,15 +54,15 @@
"cheerio": "1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
"consola": "3.2.3", "consola": "3.2.3",
"devalue": "4.3.2", "devalue": "4.3.2",
"eslint": "8.45.0", "eslint": "8.46.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.28.0",
"eslint-plugin-jsdoc": "41.1.2", "eslint-plugin-jsdoc": "41.1.2",
"eslint-plugin-no-only-tests": "3.1.0", "eslint-plugin-no-only-tests": "3.1.0",
"execa": "7.1.1", "execa": "7.2.0",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",
"globby": "13.2.2", "globby": "13.2.2",
"h3": "1.7.1", "h3": "1.7.1",
"happy-dom": "10.5.2", "happy-dom": "10.7.0",
"jiti": "1.19.1", "jiti": "1.19.1",
"markdownlint-cli": "^0.33.0", "markdownlint-cli": "^0.33.0",
"nitropack": "2.5.2", "nitropack": "2.5.2",
@ -70,21 +71,21 @@
"nuxt-vitest": "0.10.2", "nuxt-vitest": "0.10.2",
"ofetch": "1.1.1", "ofetch": "1.1.1",
"pathe": "1.1.1", "pathe": "1.1.1",
"playwright-core": "1.36.1", "playwright-core": "1.36.2",
"rimraf": "5.0.1", "rimraf": "5.0.1",
"semver": "7.5.4", "semver": "7.5.4",
"std-env": "3.3.3", "std-env": "3.3.3",
"typescript": "5.0.4", "typescript": "5.1.6",
"ufo": "1.1.2", "ufo": "1.2.0",
"vite": "4.4.7", "vite": "4.4.8",
"vitest": "0.33.0", "vitest": "0.33.0",
"vitest-environment-nuxt": "0.10.2", "vitest-environment-nuxt": "0.10.2",
"vue": "3.3.4", "vue": "3.3.4",
"vue-eslint-parser": "9.3.1", "vue-eslint-parser": "9.3.1",
"vue-router": "4.2.4", "vue-router": "4.2.4",
"vue-tsc": "1.8.6" "vue-tsc": "1.8.8"
}, },
"packageManager": "pnpm@8.6.10", "packageManager": "pnpm@8.6.11",
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }

View File

@ -34,9 +34,10 @@
"pkg-types": "^1.0.3", "pkg-types": "^1.0.3",
"scule": "^1.0.0", "scule": "^1.0.0",
"semver": "^7.5.4", "semver": "^7.5.4",
"ufo": "^1.2.0",
"unctx": "^2.3.1", "unctx": "^2.3.1",
"unimport": "^3.1.0", "unimport": "^3.1.3",
"untyped": "^1.3.2" "untyped": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/hash-sum": "1.0.0", "@types/hash-sum": "1.0.0",
@ -45,7 +46,7 @@
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"nitropack": "2.5.2", "nitropack": "2.5.2",
"unbuild": "latest", "unbuild": "latest",
"vite": "4.4.7", "vite": "4.4.8",
"vitest": "0.33.0", "vitest": "0.33.0",
"webpack": "5.88.2" "webpack": "5.88.2"
}, },

View File

@ -60,15 +60,19 @@ function getRequireCacheItem (id: string) {
} }
} }
export function getModulePaths (paths?: string[] | string) {
return ([] as Array<string | undefined>).concat(
global.__NUXT_PREPATHS__,
paths || [],
process.cwd(),
global.__NUXT_PATHS__
).filter(Boolean) as string[]
}
/** @deprecated Do not use CJS utils */ /** @deprecated Do not use CJS utils */
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) { export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
return normalize(_require.resolve(id, { return normalize(_require.resolve(id, {
paths: ([] as Array<string | undefined>).concat( paths: getModulePaths(opts.paths)
global.__NUXT_PREPATHS__,
opts.paths || [],
process.cwd(),
global.__NUXT_PATHS__
).filter(Boolean) as string[]
})) }))
} }

View File

@ -6,6 +6,7 @@ import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork'
import type { NuxtTemplate } from '@nuxt/schema' import type { NuxtTemplate } from '@nuxt/schema'
/** @deprecated */ /** @deprecated */
// TODO: Remove support for compiling ejs templates in v4
export async function compileTemplate (template: NuxtTemplate, ctx: any) { export async function compileTemplate (template: NuxtTemplate, ctx: any) {
const data = { ...ctx, options: template.options } const data = { ...ctx, options: template.options }
if (template.src) { if (template.src) {

View File

@ -1,4 +1,5 @@
import { resolve } from 'pathe' import { resolve } from 'pathe'
import type { JSValue } from 'untyped'
import { applyDefaults } from 'untyped' import { applyDefaults } from 'untyped'
import type { LoadConfigOptions } from 'c12' import type { LoadConfigOptions } from 'c12'
import { loadConfig } from 'c12' import { loadConfig } from 'c12'
@ -50,5 +51,5 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
} }
// Resolve and apply defaults // Resolve and apply defaults
return await applyDefaults(NuxtConfigSchema, nuxtConfig) as NuxtOptions return await applyDefaults(NuxtConfigSchema, nuxtConfig as NuxtConfig & Record<string, JSValue>) as unknown as NuxtOptions
} }

View File

@ -12,11 +12,22 @@ describe('nuxt module compatibility', () => {
meta: { meta: {
name: 'nuxt-module-foo' name: 'nuxt-module-foo'
} }
}) }),
[
defineNuxtModule({
meta: {
name: 'module-instance-with-options'
}
}),
{
foo: 'bar'
}
]
] ]
} }
}) })
expect(hasNuxtModule('nuxt-module-foo', nuxt)).toStrictEqual(true) expect(hasNuxtModule('nuxt-module-foo', nuxt)).toStrictEqual(true)
expect(hasNuxtModule('module-instance-with-options', nuxt)).toStrictEqual(true)
await nuxt.close() await nuxt.close()
}) })
it('can retrieve module version from module instance', async () => { it('can retrieve module version from module instance', async () => {

View File

@ -1,9 +1,19 @@
import satisfies from 'semver/functions/satisfies.js' // npm/node-semver#381 import satisfies from 'semver/functions/satisfies.js' // npm/node-semver#381
import type { Nuxt, NuxtModule } from '@nuxt/schema' import type { Nuxt, NuxtModule, NuxtOptions } from '@nuxt/schema'
import { useNuxt } from '../context' import { useNuxt } from '../context'
import { normalizeSemanticVersion } from '../compatibility' import { normalizeSemanticVersion } from '../compatibility'
import { loadNuxtModuleInstance } from './install' import { loadNuxtModuleInstance } from './install'
function resolveNuxtModuleEntryName (m: NuxtOptions['modules'][number]): string | false {
if (typeof m === 'object' && !Array.isArray(m)) {
return (m as any as NuxtModule).name
}
if (Array.isArray(m)) {
return resolveNuxtModuleEntryName(m[0])
}
return m as string || false
}
/** /**
* Check if a Nuxt module is installed by name. * Check if a Nuxt module is installed by name.
* *
@ -11,8 +21,10 @@ import { loadNuxtModuleInstance } from './install'
* that it cannot detect if a module is _going to be_ installed programmatically by another module. * that it cannot detect if a module is _going to be_ installed programmatically by another module.
*/ */
export function hasNuxtModule (moduleName: string, nuxt: Nuxt = useNuxt()) : boolean { export function hasNuxtModule (moduleName: string, nuxt: Nuxt = useNuxt()) : boolean {
// check installed modules
return nuxt.options._installedModules.some(({ meta }) => meta.name === moduleName) || return nuxt.options._installedModules.some(({ meta }) => meta.name === moduleName) ||
nuxt.options.modules.includes(moduleName) // check modules to be installed
nuxt.options.modules.some(m => moduleName === resolveNuxtModuleEntryName(m))
} }
/** /**
@ -46,7 +58,7 @@ export async function getNuxtModuleVersion (module: string | NuxtModule, nuxt: N
return version return version
} }
// it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance // it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance
if (nuxt.options.modules.includes(moduleMeta.name)) { if (hasNuxtModule(moduleMeta.name)) {
const { buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleMeta.name, nuxt) const { buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleMeta.name, nuxt)
return buildTimeModuleMeta.version || false return buildTimeModuleMeta.version || false
} }

View File

@ -1,8 +1,15 @@
import { existsSync } from 'node:fs' import { existsSync, promises as fsp } from 'node:fs'
import { basename, parse, resolve } from 'pathe' import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
import hash from 'hash-sum' import hash from 'hash-sum'
import type { NuxtTemplate, ResolvedNuxtTemplate } from '@nuxt/schema' import type { Nuxt, NuxtTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema'
import { withTrailingSlash } from 'ufo'
import { defu } from 'defu'
import type { TSConfig } from 'pkg-types'
import { readPackageJSON } from 'pkg-types'
import { tryResolveModule } from './internal/esm'
import { tryUseNuxt, useNuxt } from './context' import { tryUseNuxt, useNuxt } from './context'
import { getModulePaths } from './internal/cjs'
/** /**
* Renders given template using lodash template during build into the project buildDir * Renders given template using lodash template during build into the project buildDir
@ -101,3 +108,162 @@ export function normalizeTemplate (template: NuxtTemplate<any> | string): Resolv
export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate<any>) => boolean }) { export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate<any>) => boolean }) {
return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options) return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options)
} }
export async function writeTypes (nuxt: Nuxt) {
const modulePaths = getModulePaths(nuxt.options.modulesDir)
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, {
compilerOptions: {
forceConsistentCasingInFileNames: true,
jsx: 'preserve',
target: 'ESNext',
module: 'ESNext',
moduleResolution: nuxt.options.experimental?.typescriptBundlerResolution ? 'Bundler' : 'Node',
skipLibCheck: true,
isolatedModules: true,
useDefineForClassFields: true,
strict: nuxt.options.typescript?.strict ?? true,
allowJs: true,
noEmit: true,
resolveJsonModule: true,
allowSyntheticDefaultImports: true,
types: ['node'],
paths: {}
},
include: [
'./nuxt.d.ts',
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'),
...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [],
...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd)
.filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules'))
.map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')),
...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : []
],
exclude: [
...nuxt.options.modulesDir.map(m => relativeWithDot(nuxt.options.buildDir, m)),
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist'))
]
} satisfies TSConfig)
const aliases: Record<string, string> = {
...nuxt.options.alias,
'#build': nuxt.options.buildDir
}
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/]
const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
tsConfig.include = tsConfig.include || []
for (const alias in aliases) {
if (excludedAlias.some(re => re.test(alias))) {
continue
}
let absolutePath = resolve(basePath, aliases[alias])
let stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */)
if (!stats) {
const resolvedModule = await tryResolveModule(aliases[alias], nuxt.options.modulesDir)
if (resolvedModule) {
absolutePath = resolvedModule
stats = await fsp.stat(resolvedModule).catch(() => null)
}
}
const relativePath = relativeWithDot(nuxt.options.buildDir, absolutePath)
if (stats?.isDirectory()) {
tsConfig.compilerOptions.paths[alias] = [relativePath]
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${relativePath}/*`]
if (!absolutePath.startsWith(rootDirWithSlash)) {
tsConfig.include.push(relativePath)
}
} else {
const path = stats?.isFile()
// remove extension
? relativePath.replace(/(?<=\w)\.\w+$/g, '')
// non-existent file probably shouldn't be resolved
: aliases[alias]
tsConfig.compilerOptions.paths[alias] = [path]
if (!absolutePath.startsWith(rootDirWithSlash)) {
tsConfig.include.push(path)
}
}
}
const references: TSReference[] = await Promise.all([
...nuxt.options.modules,
...nuxt.options._modules
]
.filter(f => typeof f === 'string')
.map(async id => ({ types: (await readPackageJSON(id, { url: modulePaths }).catch(() => null))?.name || id })))
if (nuxt.options.experimental?.reactivityTransform) {
references.push({ types: 'vue/macros-global' })
}
const declarations: string[] = []
await nuxt.callHook('prepare:types', { references, declarations, tsConfig })
for (const alias in tsConfig.compilerOptions!.paths) {
const paths = tsConfig.compilerOptions!.paths[alias]
tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => {
if (!isAbsolute(path)) { return path }
const stats = await fsp.stat(path).catch(() => null /* file does not exist */)
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */ : path)
}))
}
tsConfig.include = [...new Set(tsConfig.include.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
tsConfig.exclude = [...new Set(tsConfig.exclude!.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
const declaration = [
...references.map((ref) => {
if ('path' in ref && isAbsolute(ref.path)) {
ref.path = relative(nuxt.options.buildDir, ref.path)
}
return `/// <reference ${renderAttrs(ref)} />`
}),
...declarations,
'',
'export {}',
''
].join('\n')
async function writeFile () {
const GeneratedBy = '// Generated by nuxi'
const tsConfigPath = resolve(nuxt.options.buildDir, 'tsconfig.json')
await fsp.mkdir(nuxt.options.buildDir, { recursive: true })
await fsp.writeFile(tsConfigPath, GeneratedBy + '\n' + JSON.stringify(tsConfig, null, 2))
const declarationPath = resolve(nuxt.options.buildDir, 'nuxt.d.ts')
await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration)
}
// This is needed for Nuxt 2 which clears the build directory again before building
// https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144
// @ts-expect-error TODO: Nuxt 2 hook
nuxt.hook('builder:prepared', writeFile)
await writeFile()
}
function renderAttrs (obj: Record<string, string>) {
return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ')
}
function renderAttr (key: string, value: string) {
return value ? `${key}="${value}"` : ''
}
function relativeWithDot (from: string, to: string) {
return relative(from, to).replace(/^([^.])/, './$1') || '.'
}

View File

@ -32,13 +32,13 @@
"consola": "3.2.3", "consola": "3.2.3",
"deep-object-diff": "1.1.9", "deep-object-diff": "1.1.9",
"defu": "6.1.2", "defu": "6.1.2",
"destr": "2.0.0", "destr": "2.0.1",
"execa": "7.1.1", "execa": "7.2.0",
"flat": "5.0.2", "flat": "5.0.2",
"giget": "1.1.2", "giget": "1.1.2",
"h3": "1.7.1", "h3": "1.7.1",
"jiti": "1.19.1", "jiti": "1.19.1",
"listhen": "1.1.2", "listhen": "1.2.2",
"mlly": "1.4.0", "mlly": "1.4.0",
"mri": "1.2.0", "mri": "1.2.0",
"ohash": "1.1.2", "ohash": "1.1.2",
@ -47,7 +47,7 @@
"pkg-types": "1.0.3", "pkg-types": "1.0.3",
"scule": "1.0.0", "scule": "1.0.0",
"semver": "7.5.4", "semver": "7.5.4",
"ufo": "1.1.2", "ufo": "1.2.0",
"unbuild": "latest" "unbuild": "latest"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -1,6 +1,8 @@
import { relative, resolve } from 'pathe' import { relative, resolve } from 'pathe'
import { consola } from 'consola' import { consola } from 'consola'
import { writeTypes } from '../utils/prepare'
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
import { loadKit } from '../utils/kit' import { loadKit } from '../utils/kit'
import { clearBuildDir } from '../utils/fs' import { clearBuildDir } from '../utils/fs'
import { overrideEnv } from '../utils/env' import { overrideEnv } from '../utils/env'
@ -19,7 +21,7 @@ export default defineNuxtCommand({
const rootDir = resolve(args._[0] || '.') const rootDir = resolve(args._[0] || '.')
showVersions(rootDir) showVersions(rootDir)
const { loadNuxt, buildNuxt, useNitro } = await loadKit(rootDir) const { loadNuxt, buildNuxt, useNitro, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const nuxt = await loadNuxt({ const nuxt = await loadNuxt({
rootDir, rootDir,

View File

@ -7,8 +7,10 @@ import type { Nuxt } from '@nuxt/schema'
import { consola } from 'consola' import { consola } from 'consola'
import { withTrailingSlash } from 'ufo' import { withTrailingSlash } from 'ufo'
import { setupDotenv } from 'c12' import { setupDotenv } from 'c12'
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
import { showBanner, showVersions } from '../utils/banner' import { showBanner, showVersions } from '../utils/banner'
import { writeTypes } from '../utils/prepare'
import { loadKit } from '../utils/kit' import { loadKit } from '../utils/kit'
import { importModule } from '../utils/esm' import { importModule } from '../utils/esm'
import { overrideEnv } from '../utils/env' import { overrideEnv } from '../utils/env'
@ -30,7 +32,7 @@ export default defineNuxtCommand({
await setupDotenv({ cwd: rootDir, fileName: args.dotenv }) await setupDotenv({ cwd: rootDir, fileName: args.dotenv })
const { loadNuxt, loadNuxtConfig, buildNuxt } = await loadKit(rootDir) const { loadNuxt, loadNuxtConfig, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const config = await loadNuxtConfig({ const config = await loadNuxtConfig({
cwd: rootDir, cwd: rootDir,
@ -46,7 +48,7 @@ export default defineNuxtCommand({
let currentHandler: RequestListener | undefined let currentHandler: RequestListener | undefined
let loadingMessage = 'Nuxt is starting...' let loadingMessage = 'Nuxt is starting...'
const loadingHandler: RequestListener = async (_req, res) => { const loadingHandler: RequestListener = async (_req, res) => {
const { loading: loadingTemplate } = await importModule('@nuxt/ui-templates', config.modulesDir) const loadingTemplate = config.devServer.loadingTemplate ?? await importModule('@nuxt/ui-templates', config.modulesDir).then(r => r.loading)
res.setHeader('Content-Type', 'text/html; charset=UTF-8') res.setHeader('Content-Type', 'text/html; charset=UTF-8')
res.statusCode = 503 // Service Unavailable res.statusCode = 503 // Service Unavailable
res.end(loadingTemplate({ loading: loadingMessage })) res.end(loadingTemplate({ loading: loadingMessage }))

View File

@ -1,8 +1,10 @@
import { relative, resolve } from 'pathe' import { relative, resolve } from 'pathe'
import { consola } from 'consola' import { consola } from 'consola'
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
import { clearBuildDir } from '../utils/fs' import { clearBuildDir } from '../utils/fs'
import { loadKit } from '../utils/kit' import { loadKit } from '../utils/kit'
import { writeTypes } from '../utils/prepare'
import { defineNuxtCommand } from './index' import { defineNuxtCommand } from './index'
export default defineNuxtCommand({ export default defineNuxtCommand({
@ -15,7 +17,7 @@ export default defineNuxtCommand({
process.env.NODE_ENV = process.env.NODE_ENV || 'production' process.env.NODE_ENV = process.env.NODE_ENV || 'production'
const rootDir = resolve(args._[0] || '.') const rootDir = resolve(args._[0] || '.')
const { loadNuxt, buildNuxt } = await loadKit(rootDir) const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const nuxt = await loadNuxt({ const nuxt = await loadNuxt({
rootDir, rootDir,
overrides: { overrides: {

View File

@ -1,9 +1,10 @@
import { execa } from 'execa' import { execa } from 'execa'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { tryResolveModule } from '../utils/esm'
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
import { tryResolveModule } from '../utils/esm'
import { loadKit } from '../utils/kit' import { loadKit } from '../utils/kit'
import { writeTypes } from '../utils/prepare'
import { defineNuxtCommand } from './index' import { defineNuxtCommand } from './index'
export default defineNuxtCommand({ export default defineNuxtCommand({
@ -16,7 +17,7 @@ export default defineNuxtCommand({
process.env.NODE_ENV = process.env.NODE_ENV || 'production' process.env.NODE_ENV = process.env.NODE_ENV || 'production'
const rootDir = resolve(args._[0] || '.') const rootDir = resolve(args._[0] || '.')
const { loadNuxt, buildNuxt } = await loadKit(rootDir) const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const nuxt = await loadNuxt({ const nuxt = await loadNuxt({
rootDir, rootDir,
overrides: { overrides: {

View File

@ -1,7 +1,7 @@
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { dirname, normalize } from 'pathe' import { normalize } from 'pathe'
export function getModulePaths (paths?: string | string[]): string[] { function getModulePaths (paths?: string | string[]): string[] {
return ([] as Array<string | undefined>) return ([] as Array<string | undefined>)
.concat( .concat(
global.__NUXT_PREPATHS__, global.__NUXT_PREPATHS__,
@ -25,11 +25,3 @@ function requireModule (id: string, paths?: string | string[]) {
export function tryRequireModule (id: string, paths?: string | string[]) { export function tryRequireModule (id: string, paths?: string | string[]) {
try { return requireModule(id, paths) } catch { return null } try { return requireModule(id, paths) } catch { return null }
} }
export function getNearestPackage (id: string, paths?: string | string[]) {
while (dirname(id) !== id) {
try { return requireModule(id + '/package.json', paths) } catch {}
id = dirname(id)
}
return null
}

View File

@ -1,137 +0,0 @@
import { promises as fsp } from 'node:fs'
import { isAbsolute, join, relative, resolve } from 'pathe'
import type { Nuxt, TSReference } from '@nuxt/schema'
import { defu } from 'defu'
import type { TSConfig } from 'pkg-types'
import { withTrailingSlash } from 'ufo'
import { getModulePaths, getNearestPackage } from './cjs'
export const writeTypes = async (nuxt: Nuxt) => {
const modulePaths = getModulePaths(nuxt.options.modulesDir)
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, {
compilerOptions: {
forceConsistentCasingInFileNames: true,
jsx: 'preserve',
target: 'ESNext',
module: 'ESNext',
moduleResolution: nuxt.options.experimental?.typescriptBundlerResolution ? 'Bundler' : 'Node',
skipLibCheck: true,
strict: nuxt.options.typescript?.strict ?? true,
allowJs: true,
// TODO: remove by default in 3.7
baseUrl: nuxt.options.srcDir,
noEmit: true,
resolveJsonModule: true,
allowSyntheticDefaultImports: true,
types: ['node'],
paths: {}
},
include: [
'./nuxt.d.ts',
join(relative(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'),
...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [],
...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd)
.filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules'))
.map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')),
...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : []
],
exclude: [
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
relative(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist'))
]
} satisfies TSConfig)
const aliases: Record<string, string> = {
...nuxt.options.alias,
'#build': nuxt.options.buildDir
}
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/]
const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
tsConfig.include = tsConfig.include || []
for (const alias in aliases) {
if (excludedAlias.some(re => re.test(alias))) {
continue
}
const absolutePath = resolve(basePath, aliases[alias])
const stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */)
if (stats?.isDirectory()) {
tsConfig.compilerOptions.paths[alias] = [absolutePath]
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${absolutePath}/*`]
if (!absolutePath.startsWith(rootDirWithSlash)) {
tsConfig.include.push(absolutePath)
tsConfig.include.push(`${absolutePath}/*`)
}
} else {
tsConfig.compilerOptions.paths[alias] = [absolutePath.replace(/(?<=\w)\.\w+$/g, '')] /* remove extension */
if (!absolutePath.startsWith(rootDirWithSlash)) {
tsConfig.include.push(absolutePath.replace(/(?<=\w)\.\w+$/g, ''))
}
}
}
const references: TSReference[] = [
...nuxt.options.modules,
...nuxt.options._modules
]
.filter(f => typeof f === 'string')
.map(id => ({ types: getNearestPackage(id, modulePaths)?.name || id }))
if (nuxt.options.experimental?.reactivityTransform) {
references.push({ types: 'vue/macros-global' })
}
const declarations: string[] = []
await nuxt.callHook('prepare:types', { references, declarations, tsConfig })
const declaration = [
...references.map((ref) => {
if ('path' in ref && isAbsolute(ref.path)) {
ref.path = relative(nuxt.options.buildDir, ref.path)
}
return `/// <reference ${renderAttrs(ref)} />`
}),
...declarations,
'',
'export {}',
''
].join('\n')
async function writeFile () {
const GeneratedBy = '// Generated by nuxi'
const tsConfigPath = resolve(nuxt.options.buildDir, 'tsconfig.json')
await fsp.mkdir(nuxt.options.buildDir, { recursive: true })
await fsp.writeFile(tsConfigPath, GeneratedBy + '\n' + JSON.stringify(tsConfig, null, 2))
const declarationPath = resolve(nuxt.options.buildDir, 'nuxt.d.ts')
await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration)
}
// This is needed for Nuxt 2 which clears the build directory again before building
// https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144
// @ts-expect-error TODO: Nuxt 2 hook
nuxt.hook('builder:prepared', writeFile)
await writeFile()
}
function renderAttrs (obj: Record<string, string>) {
return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ')
}
function renderAttr (key: string, value: string) {
return value ? `${key}="${value}"` : ''
}

View File

@ -2,4 +2,5 @@ import type { NuxtConfig } from 'nuxt/schema'
import type { DefineConfig, InputConfig, UserInputConfig, ConfigLayerMeta } from 'c12' import type { DefineConfig, InputConfig, UserInputConfig, ConfigLayerMeta } from 'c12'
export { NuxtConfig } from 'nuxt/schema' export { NuxtConfig } from 'nuxt/schema'
export declare const defineNuxtConfig: DefineConfig<NuxtConfig, ConfigLayerMeta> export interface DefineNuxtConfig extends DefineConfig<NuxtConfig, ConfigLayerMeta> {}
export declare const defineNuxtConfig: DefineNuxtConfig

View File

@ -56,19 +56,19 @@
"@nuxt/kit": "workspace:../kit", "@nuxt/kit": "workspace:../kit",
"@nuxt/schema": "workspace:../schema", "@nuxt/schema": "workspace:../schema",
"@nuxt/telemetry": "^2.3.2", "@nuxt/telemetry": "^2.3.2",
"@nuxt/ui-templates": "^1.2.0", "@nuxt/ui-templates": "^1.3.1",
"@nuxt/vite-builder": "workspace:../vite", "@nuxt/vite-builder": "workspace:../vite",
"@unhead/ssr": "^1.1.32", "@unhead/ssr": "^1.2.2",
"@unhead/vue": "^1.1.32", "@unhead/vue": "^1.2.2",
"@vue/shared": "^3.3.4", "@vue/shared": "^3.3.4",
"acorn": "8.10.0", "acorn": "8.10.0",
"c12": "^1.4.2", "c12": "^1.4.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cookie-es": "^1.0.0", "cookie-es": "^1.0.0",
"defu": "^6.1.2", "defu": "^6.1.2",
"destr": "^2.0.0", "destr": "^2.0.1",
"devalue": "^4.3.2", "devalue": "^4.3.2",
"esbuild": "^0.18.16", "esbuild": "^0.18.17",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
@ -78,8 +78,7 @@
"jiti": "^1.19.1", "jiti": "^1.19.1",
"klona": "^2.0.6", "klona": "^2.0.6",
"knitwork": "^1.0.0", "knitwork": "^1.0.0",
"local-pkg": "^0.4.3", "magic-string": "^0.30.2",
"magic-string": "^0.30.1",
"mlly": "^1.4.0", "mlly": "^1.4.0",
"nitropack": "^2.5.2", "nitropack": "^2.5.2",
"nuxi": "workspace:../nuxi", "nuxi": "workspace:../nuxi",
@ -88,20 +87,21 @@
"ohash": "^1.1.2", "ohash": "^1.1.2",
"pathe": "^1.1.1", "pathe": "^1.1.1",
"perfect-debounce": "^1.0.0", "perfect-debounce": "^1.0.0",
"pkg-types": "^1.0.3",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"scule": "^1.0.0", "scule": "^1.0.0",
"strip-literal": "^1.0.1", "strip-literal": "^1.3.0",
"ufo": "^1.1.2", "ufo": "^1.2.0",
"ultrahtml": "^1.3.0", "ultrahtml": "^1.3.0",
"uncrypto": "^0.1.3", "uncrypto": "^0.1.3",
"unctx": "^2.3.1", "unctx": "^2.3.1",
"unenv": "^1.5.2", "unenv": "^1.6.1",
"unimport": "^3.1.0", "unimport": "^3.1.3",
"unplugin": "^1.4.0", "unplugin": "^1.4.0",
"unplugin-vue-router": "^0.6.4", "unplugin-vue-router": "^0.6.4",
"untyped": "^1.3.2", "untyped": "^1.4.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-bundle-renderer": "^1.0.3", "vue-bundle-renderer": "^2.0.0",
"vue-devtools-stub": "^0.1.0", "vue-devtools-stub": "^0.1.0",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
}, },
@ -112,7 +112,7 @@
"@types/prompts": "2.4.4", "@types/prompts": "2.4.4",
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.2.3",
"unbuild": "latest", "unbuild": "latest",
"vite": "4.4.7", "vite": "4.4.8",
"vitest": "0.33.0" "vitest": "0.33.0"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -18,7 +18,7 @@ export default defineComponent({
if (!component) { if (!component) {
throw createError({ throw createError({
statusCode: 404, statusCode: 404,
statusMessage: `Island component not found: ${JSON.stringify(component)}` statusMessage: `Island component not found: ${props.context.name}`
}) })
} }

View File

@ -1,150 +1,2 @@
import type { Ref, VNode } from 'vue' // TODO: remove in 4.x
import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue' export { default } from './nuxt-layout'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { _wrapIf } from './utils'
import { LayoutMetaSymbol, PageRouteSymbol } from './injections'
import { useRoute } from '#app/composables/router'
// @ts-expect-error virtual file
import { useRoute as useVueRouterRoute } from '#build/pages'
// @ts-expect-error virtual file
import layouts from '#build/layouts'
// @ts-expect-error virtual file
import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs'
import { useNuxtApp } from '#app'
// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved
const LayoutLoader = defineComponent({
name: 'LayoutLoader',
inheritAttrs: false,
props: {
name: String,
layoutProps: Object
},
async setup (props, context) {
const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r)
return () => h(LayoutComponent, props.layoutProps, context.slots)
}
})
export default defineComponent({
name: 'NuxtLayout',
inheritAttrs: false,
props: {
name: {
type: [String, Boolean, Object] as unknown as () => string | false | Ref<string | false>,
default: null
}
},
setup (props, context) {
const nuxtApp = useNuxtApp()
// Need to ensure (if we are not a child of `<NuxtPage>`) that we use synchronous route (not deferred)
const injectedRoute = inject(PageRouteSymbol)
const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute
const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default')
const layoutRef = ref()
context.expose({ layoutRef })
const done = nuxtApp.deferHydration()
return () => {
const hasLayout = layout.value && layout.value in layouts
if (process.dev && layout.value && !hasLayout && layout.value !== 'default') {
console.warn(`Invalid layout \`${layout.value}\` selected.`)
}
const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition
// We avoid rendering layout transition if there is no layout to render
return _wrapIf(Transition, hasLayout && transitionProps, {
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
default: () => h(
// @ts-expect-error seems to be an issue in vue types
LayoutProvider,
{
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
key: layout.value,
name: layout.value,
shouldProvide: !props.name,
hasTransition: !!transitionProps
}, context.slots)
})
}).default()
}
}
})
const LayoutProvider = defineComponent({
name: 'NuxtLayoutProvider',
inheritAttrs: false,
props: {
name: {
type: [String, Boolean]
},
layoutProps: {
type: Object
},
hasTransition: {
type: Boolean
},
shouldProvide: {
type: Boolean
}
},
setup (props, context) {
// Prevent reactivity when the page will be rerendered in a different suspense fork
// eslint-disable-next-line vue/no-setup-props-destructure
const name = props.name
if (props.shouldProvide) {
provide(LayoutMetaSymbol, {
isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default')
})
}
let vnode: VNode | undefined
if (process.dev && process.client) {
onMounted(() => {
nextTick(() => {
if (['#comment', '#text'].includes(vnode?.el?.nodeName)) {
if (name) {
console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`)
} else {
console.warn('[nuxt] `<NuxtLayout>` needs to be passed a single root node in its default slot.')
}
}
})
})
}
return () => {
if (!name || (typeof name === 'string' && !(name in layouts))) {
if (process.dev && process.client && props.hasTransition) {
vnode = context.slots.default?.() as VNode | undefined
return vnode
}
return context.slots.default?.()
}
if (process.dev && process.client && props.hasTransition) {
vnode = h(
// @ts-expect-error seems to be an issue in vue types
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
)
return vnode
}
return h(
// @ts-expect-error seems to be an issue in vue types
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
)
}
}
})

View File

@ -13,6 +13,9 @@ import { getFragmentHTML, getSlotProps } from './utils'
import { useNuxtApp, useRuntimeConfig } from '#app/nuxt' import { useNuxtApp, useRuntimeConfig } from '#app/nuxt'
import { useRequestEvent } from '#app/composables/ssr' import { useRequestEvent } from '#app/composables/ssr'
// @ts-expect-error virtual file
import { remoteComponentIslands } from '#build/nuxt.config.mjs'
const pKey = '_islandPromises' const pKey = '_islandPromises'
const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/ const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/ const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
@ -29,6 +32,7 @@ export default defineComponent({
type: String, type: String,
required: true required: true
}, },
lazy: Boolean,
props: { props: {
type: Object, type: Object,
default: () => undefined default: () => undefined
@ -36,12 +40,17 @@ export default defineComponent({
context: { context: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
},
source: {
type: String,
default: () => undefined
} }
}, },
async setup (props, { slots }) { async setup (props, { slots }) {
const error = ref<unknown>(null)
const config = useRuntimeConfig() const config = useRuntimeConfig()
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const hashId = computed(() => hash([props.name, props.props, props.context])) const hashId = computed(() => hash([props.name, props.props, props.context, props.source]))
const instance = getCurrentInstance()! const instance = getCurrentInstance()!
const event = useRequestEvent() const event = useRequestEvent()
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved // TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
@ -61,7 +70,7 @@ export default defineComponent({
} }
} }
const ssrHTML = ref('<div></div>') const ssrHTML = ref<string>('')
if (process.client) { if (process.client) {
const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('') const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('')
if (renderedHTML && nuxtApp.isHydrating) { if (renderedHTML && nuxtApp.isHydrating) {
@ -74,7 +83,7 @@ export default defineComponent({
} }
}) })
} }
ssrHTML.value = renderedHTML ?? '<div></div>' ssrHTML.value = renderedHTML
} }
const slotProps = computed(() => getSlotProps(ssrHTML.value)) const slotProps = computed(() => getSlotProps(ssrHTML.value))
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID()) const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID())
@ -100,7 +109,8 @@ export default defineComponent({
const key = `${props.name}_${hashId.value}` const key = `${props.name}_${hashId.value}`
if (nuxtApp.payload.data[key] && !force) { return nuxtApp.payload.data[key] } if (nuxtApp.payload.data[key] && !force) { return nuxtApp.payload.data[key] }
const url = `/__nuxt_island/${key}` const url = remoteComponentIslands && props.source ? new URL(`/__nuxt_island/${key}`, props.source).href : `/__nuxt_island/${key}`
if (process.server && process.env.prerender) { if (process.server && process.env.prerender) {
// Hint to Nitro to prerender the island component // Hint to Nitro to prerender the island component
appendResponseHeader(event, 'x-nitro-prerender', url) appendResponseHeader(event, 'x-nitro-prerender', url)
@ -130,18 +140,23 @@ export default defineComponent({
delete nuxtApp[pKey]![uid.value] delete nuxtApp[pKey]![uid.value]
}) })
} }
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value] try {
cHead.value.link = res.head.link const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
cHead.value.style = res.head.style cHead.value.link = res.head.link
ssrHTML.value = res.html.replace(UID_ATTR, () => { cHead.value.style = res.head.style
return `nuxt-ssr-component-uid="${getId()}"` ssrHTML.value = res.html.replace(UID_ATTR, () => {
}) return `nuxt-ssr-component-uid="${getId()}"`
key.value++ })
if (process.client) { key.value++
// must await next tick for Teleport to work correctly with static node re-rendering error.value = null
await nextTick() if (process.client) {
// must await next tick for Teleport to work correctly with static node re-rendering
await nextTick()
}
setUid()
} catch (e) {
error.value = e
} }
setUid()
} }
if (import.meta.hot) { if (import.meta.hot) {
@ -154,15 +169,19 @@ export default defineComponent({
watch(props, debounce(() => fetchComponent(), 100)) watch(props, debounce(() => fetchComponent(), 100))
} }
// TODO: allow lazy loading server islands if (process.client && !nuxtApp.isHydrating && props.lazy) {
if (process.server || !nuxtApp.isHydrating) { fetchComponent()
} else if (process.server || !nuxtApp.isHydrating) {
await fetchComponent() await fetchComponent()
} }
return () => { return () => {
if ((!html.value || error.value) && slots.fallback) {
return [slots.fallback({ error: error.value })]
}
const nodes = [createVNode(Fragment, { const nodes = [createVNode(Fragment, {
key: key.value key: key.value
}, [h(createStaticVNode(html.value, 1))])] }, [h(createStaticVNode(html.value || '<div></div>', 1))])]
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) { if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
for (const slot in slots) { for (const slot in slots) {
if (availableSlots.value.includes(slot)) { if (availableSlots.value.includes(slot)) {

View File

@ -0,0 +1,153 @@
import type { DefineComponent, MaybeRef, VNode } from 'vue'
import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { _wrapIf } from './utils'
import { LayoutMetaSymbol, PageRouteSymbol } from './injections'
import type { PageMeta } from '#app'
import { useRoute } from '#app/composables/router'
import { useNuxtApp } from '#app/nuxt'
// @ts-expect-error virtual file
import { useRoute as useVueRouterRoute } from '#build/pages'
// @ts-expect-error virtual file
import layouts from '#build/layouts'
// @ts-expect-error virtual file
import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs'
// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved
const LayoutLoader = defineComponent({
name: 'LayoutLoader',
inheritAttrs: false,
props: {
name: String,
layoutProps: Object
},
async setup (props, context) {
const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r)
return () => h(LayoutComponent, props.layoutProps, context.slots)
}
})
export default defineComponent({
name: 'NuxtLayout',
inheritAttrs: false,
props: {
name: {
type: [String, Boolean, Object] as unknown as () => unknown extends PageMeta['layout'] ? MaybeRef<string | false> : PageMeta['layout'],
default: null
}
},
setup (props, context) {
const nuxtApp = useNuxtApp()
// Need to ensure (if we are not a child of `<NuxtPage>`) that we use synchronous route (not deferred)
const injectedRoute = inject(PageRouteSymbol)
const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute
const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default')
const layoutRef = ref()
context.expose({ layoutRef })
const done = nuxtApp.deferHydration()
return () => {
const hasLayout = layout.value && layout.value in layouts
if (process.dev && layout.value && !hasLayout && layout.value !== 'default') {
console.warn(`Invalid layout \`${layout.value}\` selected.`)
}
const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition
// We avoid rendering layout transition if there is no layout to render
return _wrapIf(Transition, hasLayout && transitionProps, {
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
default: () => h(
// @ts-expect-error seems to be an issue in vue types
LayoutProvider,
{
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
key: layout.value,
name: layout.value,
shouldProvide: !props.name,
hasTransition: !!transitionProps
}, context.slots)
})
}).default()
}
}
}) as unknown as DefineComponent<{
name?: unknown extends PageMeta['layout'] ? MaybeRef<string | false> : PageMeta['layout']
}>
const LayoutProvider = defineComponent({
name: 'NuxtLayoutProvider',
inheritAttrs: false,
props: {
name: {
type: [String, Boolean]
},
layoutProps: {
type: Object
},
hasTransition: {
type: Boolean
},
shouldProvide: {
type: Boolean
}
},
setup (props, context) {
// Prevent reactivity when the page will be rerendered in a different suspense fork
// eslint-disable-next-line vue/no-setup-props-destructure
const name = props.name
if (props.shouldProvide) {
provide(LayoutMetaSymbol, {
isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default')
})
}
let vnode: VNode | undefined
if (process.dev && process.client) {
onMounted(() => {
nextTick(() => {
if (['#comment', '#text'].includes(vnode?.el?.nodeName)) {
if (name) {
console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`)
} else {
console.warn('[nuxt] `<NuxtLayout>` needs to be passed a single root node in its default slot.')
}
}
})
})
}
return () => {
if (!name || (typeof name === 'string' && !(name in layouts))) {
if (process.dev && process.client && props.hasTransition) {
vnode = context.slots.default?.() as VNode | undefined
return vnode
}
return context.slots.default?.()
}
if (process.dev && process.client && props.hasTransition) {
vnode = h(
// @ts-expect-error seems to be an issue in vue types
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
)
return vnode
}
return h(
// @ts-expect-error seems to be an issue in vue types
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
)
}
}
})

View File

@ -53,8 +53,8 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
} }
if (opts.watch) { if (opts.watch) {
watch(cookie, (newVal, oldVal) => { watch(cookie, () => {
if (watchPaused || isEqual(newVal, oldVal)) { return } if (watchPaused) { return }
callback() callback()
}, },
{ deep: opts.watch !== 'shallow' }) { deep: opts.watch !== 'shallow' })

View File

@ -1,18 +1,25 @@
import type { FetchError } from 'ofetch' import type { FetchError, FetchOptions } from 'ofetch'
import type { AvailableRouterMethod, NitroFetchOptions, NitroFetchRequest, TypedInternalResponse } from 'nitropack' import type { NitroFetchRequest, TypedInternalResponse, AvailableRouterMethod as _AvailableRouterMethod } from 'nitropack'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { computed, reactive, unref } from 'vue' import { computed, reactive, unref } from 'vue'
import { hash } from 'ohash' import { hash } from 'ohash'
import { useRequestFetch } from './ssr' import { useRequestFetch } from './ssr'
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom, _Transform } from './asyncData' import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData'
import { useAsyncData } from './asyncData' import { useAsyncData } from './asyncData'
export type FetchResult<ReqT extends NitroFetchRequest, M extends AvailableRouterMethod<ReqT>> = TypedInternalResponse<ReqT, unknown, M> // support uppercase methods, detail: https://github.com/nuxt/nuxt/issues/22313
type AvailableRouterMethod<R extends NitroFetchRequest> = _AvailableRouterMethod<R> | Uppercase<_AvailableRouterMethod<R>>
export type FetchResult<ReqT extends NitroFetchRequest, M extends AvailableRouterMethod<ReqT>> = TypedInternalResponse<ReqT, unknown, Lowercase<M>>
type ComputedOptions<T extends Record<string, any>> = { type ComputedOptions<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends Function ? T[K] : T[K] extends Record<string, any> ? ComputedOptions<T[K]> | Ref<T[K]> | T[K] : Ref<T[K]> | T[K] [K in keyof T]: T[K] extends Function ? T[K] : T[K] extends Record<string, any> ? ComputedOptions<T[K]> | Ref<T[K]> | T[K] : Ref<T[K]> | T[K]
} }
interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> extends FetchOptions {
method?: M;
}
type ComputedFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R>> = ComputedOptions<NitroFetchOptions<R, M>> type ComputedFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R>> = ComputedOptions<NitroFetchOptions<R, M>>
export interface UseFetchOptions< export interface UseFetchOptions<
@ -69,15 +76,6 @@ export function useFetch<
arg2?: string arg2?: string
) { ) {
const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]
const _key = opts.key || hash([autoKey, unref(opts.baseURL), typeof request === 'string' ? request : '', unref(opts.params || opts.query)])
if (!_key || typeof _key !== 'string') {
throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key)
}
if (!request) {
throw new Error('[nuxt] [useFetch] request is missing.')
}
const key = _key === autoKey ? '$f' + _key : _key
const _request = computed(() => { const _request = computed(() => {
let r = request let r = request
@ -87,6 +85,16 @@ export function useFetch<
return unref(r) return unref(r)
}) })
const _key = opts.key || hash([autoKey, unref(opts.baseURL), typeof _request.value === 'string' ? _request.value : '', unref(opts.params || opts.query)])
if (!_key || typeof _key !== 'string') {
throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key)
}
if (!request) {
throw new Error('[nuxt] [useFetch] request is missing.')
}
const key = _key === autoKey ? '$f' + _key : _key
if (!opts.baseURL && typeof _request.value === 'string' && _request.value.startsWith('//')) { if (!opts.baseURL && typeof _request.value === 'string' && _request.value.startsWith('//')) {
throw new Error('[nuxt] [useFetch] the request URL must not start with "//".') throw new Error('[nuxt] [useFetch] the request URL must not start with "//".')
} }

View File

@ -2,7 +2,7 @@ import { getCurrentInstance, hasInjectionContext, inject, onUnmounted } from 'vu
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router' import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router'
import { sanitizeStatusCode } from 'h3' import { sanitizeStatusCode } from 'h3'
import { hasProtocol, joinURL, parseURL, withQuery } from 'ufo' import { hasProtocol, isScriptProtocol, joinURL, parseURL, withQuery } from 'ufo'
import { useNuxtApp, useRuntimeConfig } from '../nuxt' import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import type { NuxtError } from './error' import type { NuxtError } from './error'
@ -133,11 +133,14 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
} }
const isExternal = options?.external || hasProtocol(toPath, { acceptRelative: true }) const isExternal = options?.external || hasProtocol(toPath, { acceptRelative: true })
if (isExternal && !options?.external) { if (isExternal) {
throw new Error('Navigating to external URL is not allowed by default. Use `navigateTo (url, { external: true })`.') if (!options?.external) {
} throw new Error('Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.')
if (isExternal && parseURL(toPath).protocol === 'script:') { }
throw new Error('Cannot navigate to an URL with script protocol.') const protocol = parseURL(toPath).protocol
if (protocol && isScriptProtocol(protocol)) {
throw new Error(`Cannot navigate to a URL with '${protocol}' protocol.`)
}
} }
const inMiddleware = isProcessingMiddleware() const inMiddleware = isProcessingMiddleware()
@ -218,7 +221,7 @@ export const abortNavigation = (err?: string | Partial<NuxtError>) => {
throw err throw err
} }
export const setPageLayout = (layout: string) => { export const setPageLayout = (layout: unknown extends PageMeta['layout'] ? string : PageMeta['layout']) => {
if (process.server) { if (process.server) {
if (process.dev && getCurrentInstance() && useState('_layout').value !== layout) { if (process.dev && getCurrentInstance() && useState('_layout').value !== layout) {
console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout on the server within a component as this will cause hydration errors.') console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout on the server within a component as this will cause hydration errors.')

View File

@ -10,6 +10,7 @@ import type { H3Event } from 'h3'
import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema' import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema'
import type { RenderResponse } from 'nitropack' import type { RenderResponse } from 'nitropack'
import type { MergeHead, VueHeadClient } from '@unhead/vue'
// eslint-disable-next-line import/no-restricted-paths // eslint-disable-next-line import/no-restricted-paths
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer' import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
import type { RouteMiddleware } from '../../app' import type { RouteMiddleware } from '../../app'
@ -18,15 +19,6 @@ import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app') const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
type NuxtMeta = {
htmlAttrs?: string
headAttrs?: string
bodyAttrs?: string
headTags?: string
bodyScriptsPrepend?: string
bodyScripts?: string
}
type HookResult = Promise<void> | void type HookResult = Promise<void> | void
type AppRenderedContext = { ssrContext: NuxtApp['ssrContext'], renderResult: null | Awaited<ReturnType<ReturnType<typeof createRenderer>['renderToString']>> } type AppRenderedContext = { ssrContext: NuxtApp['ssrContext'], renderResult: null | Awaited<ReturnType<ReturnType<typeof createRenderer>['renderToString']>> }
@ -59,10 +51,10 @@ export interface NuxtSSRContext extends SSRContext {
error?: boolean error?: boolean
nuxt: _NuxtApp nuxt: _NuxtApp
payload: NuxtPayload payload: NuxtPayload
head: VueHeadClient<MergeHead>
/** This is used solely to render runtime config with SPA renderer. */ /** This is used solely to render runtime config with SPA renderer. */
config?: Pick<RuntimeConfig, 'public' | 'app'> config?: Pick<RuntimeConfig, 'public' | 'app'>
teleports?: Record<string, string> teleports?: Record<string, string>
renderMeta?: () => Promise<NuxtMeta> | NuxtMeta
islandContext?: NuxtIslandContext islandContext?: NuxtIslandContext
/** @internal */ /** @internal */
_renderResponse?: Partial<RenderResponse> _renderResponse?: Partial<RenderResponse>
@ -163,6 +155,16 @@ export interface PluginMeta {
order?: number order?: number
} }
export interface PluginEnvContext {
/**
* This enable the plugin for islands components.
* Require `experimental.componentsIslands`.
*
* @default true
*/
islands?: boolean
}
export interface ResolvedPluginMeta { export interface ResolvedPluginMeta {
name?: string name?: string
parallel?: boolean parallel?: boolean
@ -177,6 +179,7 @@ export interface Plugin<Injections extends Record<string, unknown> = Record<stri
export interface ObjectPlugin<Injections extends Record<string, unknown> = Record<string, unknown>> extends PluginMeta { export interface ObjectPlugin<Injections extends Record<string, unknown> = Record<string, unknown>> extends PluginMeta {
hooks?: Partial<RuntimeNuxtHooks> hooks?: Partial<RuntimeNuxtHooks>
setup?: Plugin<Injections> setup?: Plugin<Injections>
env?: PluginEnvContext
/** /**
* Execute plugin in parallel with other parallel plugins. * Execute plugin in parallel with other parallel plugins.
* *
@ -326,6 +329,7 @@ export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & Ob
const parallels: Promise<any>[] = [] const parallels: Promise<any>[] = []
const errors: Error[] = [] const errors: Error[] = []
for (const plugin of plugins) { for (const plugin of plugins) {
if (process.server && nuxtApp.ssrContext?.islandContext && plugin.env?.islands === false) { continue }
const promise = applyPlugin(nuxtApp, plugin) const promise = applyPlugin(nuxtApp, plugin)
if (plugin.parallel) { if (plugin.parallel) {
parallels.push(promise.catch(e => errors.push(e))) parallels.push(promise.catch(e => errors.push(e)))

View File

@ -31,7 +31,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
if (include.some(pattern => pattern.test(id))) { if (include.some(pattern => pattern.test(id))) {
return true return true
} }
return isVue(id, { type: ['template', 'script'] }) return isVue(id, { type: ['template', 'script'] }) || !!id.match(/\.[tj]sx$/)
}, },
transform (code) { transform (code) {
const components = options.getComponents() const components = options.getComponents()

View File

@ -129,11 +129,11 @@ export default defineNuxtModule<ComponentsOptions>({
const unpluginServer = createTransformPlugin(nuxt, getComponents, 'server') const unpluginServer = createTransformPlugin(nuxt, getComponents, 'server')
const unpluginClient = createTransformPlugin(nuxt, getComponents, 'client') const unpluginClient = createTransformPlugin(nuxt, getComponents, 'client')
addVitePlugin(unpluginServer.vite(), { server: true, client: false }) addVitePlugin(() => unpluginServer.vite(), { server: true, client: false })
addVitePlugin(unpluginClient.vite(), { server: false, client: true }) addVitePlugin(() => unpluginClient.vite(), { server: false, client: true })
addWebpackPlugin(unpluginServer.webpack(), { server: true, client: false }) addWebpackPlugin(() => unpluginServer.webpack(), { server: true, client: false })
addWebpackPlugin(unpluginClient.webpack(), { server: false, client: true }) addWebpackPlugin(() => unpluginClient.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) => {
@ -148,12 +148,14 @@ export default defineNuxtModule<ComponentsOptions>({
}) })
// Restart dev server when component directories are added/removed // Restart dev server when component directories are added/removed
nuxt.hook('builder:watch', (event, path) => { nuxt.hook('builder:watch', (event, relativePath) => {
const isDirChange = ['addDir', 'unlinkDir'].includes(event) if (!['addDir', 'unlinkDir'].includes(event)) {
const fullPath = resolve(nuxt.options.srcDir, path) return
}
if (isDirChange && componentDirs.some(dir => dir.path === fullPath)) { const path = resolve(nuxt.options.srcDir, relativePath)
console.info(`Directory \`${path}/\` ${event === 'addDir' ? 'created' : 'removed'}`) if (componentDirs.some(dir => dir.path === path)) {
console.info(`Directory \`${relativePath}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
return nuxt.callHook('restart') return nuxt.callHook('restart')
} }
}) })
@ -183,12 +185,12 @@ export default defineNuxtModule<ComponentsOptions>({
}) })
// Watch for changes // Watch for changes
nuxt.hook('builder:watch', async (event, path) => { nuxt.hook('builder:watch', async (event, relativePath) => {
if (!['add', 'unlink'].includes(event)) { if (!['add', 'unlink'].includes(event)) {
return return
} }
const fPath = resolve(nuxt.options.srcDir, path) const path = resolve(nuxt.options.srcDir, relativePath)
if (componentDirs.find(dir => fPath.startsWith(dir.path))) { if (componentDirs.some(dir => path.startsWith(dir.path + '/'))) {
await updateTemplates({ await updateTemplates({
filter: template => [ filter: template => [
'components.plugin.mjs', 'components.plugin.mjs',
@ -219,7 +221,7 @@ export default defineNuxtModule<ComponentsOptions>({
getComponents, getComponents,
mode, mode,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined, transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: nuxt.options.experimental.componentIslands experimentalComponentIslands: !!nuxt.options.experimental.componentIslands
})) }))
if (isServer && nuxt.options.experimental.componentIslands) { if (isServer && nuxt.options.experimental.componentIslands) {
@ -263,7 +265,7 @@ export default defineNuxtModule<ComponentsOptions>({
getComponents, getComponents,
mode, mode,
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined, transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
experimentalComponentIslands: nuxt.options.experimental.componentIslands experimentalComponentIslands: !!nuxt.options.experimental.componentIslands
})) }))
if (nuxt.options.experimental.componentIslands && mode === 'server') { if (nuxt.options.experimental.componentIslands && mode === 'server') {

View File

@ -5,9 +5,11 @@ export const createServerComponent = (name: string) => {
return defineComponent({ return defineComponent({
name, name,
inheritAttrs: false, inheritAttrs: false,
setup (_props, { attrs, slots }) { props: { lazy: Boolean },
setup (props, { attrs, slots }) {
return () => h(NuxtIsland, { return () => h(NuxtIsland, {
name, name,
lazy: props.lazy,
props: attrs props: attrs
}, slots) }, slots)
} }

View File

@ -1,4 +1,4 @@
import { promises as fsp } from 'node:fs' import { promises as fsp, mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'pathe' import { dirname, join, resolve } from 'pathe'
import { defu } from 'defu' import { defu } from 'defu'
import { compileTemplate, findPath, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath, templateUtils, tryResolveModule } from '@nuxt/kit' import { compileTemplate, findPath, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath, templateUtils, tryResolveModule } from '@nuxt/kit'
@ -32,13 +32,21 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl)) app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl))
// Compile templates into vfs // Compile templates into vfs
// TODO: remove utils in v4
const templateContext = { utils: templateUtils, nuxt, app } const templateContext = { utils: templateUtils, nuxt, app }
await Promise.all((app.templates as Array<ReturnType<typeof normalizeTemplate>>) const filteredTemplates = (app.templates as Array<ReturnType<typeof normalizeTemplate>>)
.filter(template => !options.filter || options.filter(template)) .filter(template => !options.filter || options.filter(template))
.map(async (template) => {
const contents = await compileTemplate(template, templateContext)
const writes: Array<() => void> = []
await Promise.allSettled(filteredTemplates
.map(async (template) => {
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!) const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
const mark = performance.mark(fullPath)
const contents = await compileTemplate(template, templateContext).catch((e) => {
console.error(`[nuxt] Could not compile template \`${template.filename}\`.`)
throw e
})
nuxt.vfs[fullPath] = contents nuxt.vfs[fullPath] = contents
const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '') const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '')
@ -49,13 +57,26 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents
} }
const perf = performance.measure(fullPath, mark?.name) // TODO: remove when Node 14 reaches EOL
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
if (nuxt.options.debug || setupTime > 500) {
console.info(`[nuxt] compiled \`${template.filename}\` in ${setupTime}ms`)
}
if (template.write) { if (template.write) {
await fsp.mkdir(dirname(fullPath), { recursive: true }) writes.push(() => {
await fsp.writeFile(fullPath, contents, 'utf8') mkdirSync(dirname(fullPath), { recursive: true })
writeFileSync(fullPath, contents, 'utf8')
})
} }
})) }))
await nuxt.callHook('app:templatesGenerated', app) // Write template files in single synchronous step to avoid (possible) additional
// runtime overhead of cascading HMRs from vite/webpack
for (const write of writes) { write() }
await nuxt.callHook('app:templatesGenerated', app, filteredTemplates, options)
} }
async function resolveApp (nuxt: Nuxt, app: NuxtApp) { async function resolveApp (nuxt: Nuxt, app: NuxtApp) {

View File

@ -5,7 +5,7 @@ import chokidar from 'chokidar'
import { isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit' import { isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit'
import { interopDefault } from 'mlly' import { interopDefault } from 'mlly'
import { debounce } from 'perfect-debounce' import { debounce } from 'perfect-debounce'
import { normalize, resolve } from 'pathe' import { normalize, relative, resolve } from 'pathe'
import type { Nuxt } from 'nuxt/schema' import type { Nuxt } from 'nuxt/schema'
import { generateApp as _generateApp, createApp } from './app' import { generateApp as _generateApp, createApp } from './app'
@ -19,12 +19,16 @@ export async function build (nuxt: Nuxt) {
if (nuxt.options.dev) { if (nuxt.options.dev) {
watch(nuxt) watch(nuxt)
nuxt.hook('builder:watch', async (event, path) => { nuxt.hook('builder:watch', async (event, relativePath) => {
if (event !== 'change' && /^(app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(path)) { if (event === 'change') { return }
if (path.startsWith('app')) { const path = resolve(nuxt.options.srcDir, relativePath)
const relativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
const restartPath = relativePaths.find(relativePath => /^(app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(relativePath))
if (restartPath) {
if (restartPath.startsWith('app')) {
app.mainComponent = undefined app.mainComponent = undefined
} }
if (path.startsWith('error')) { if (restartPath.startsWith('error')) {
app.errorComponent = undefined app.errorComponent = undefined
} }
await generateApp() await generateApp()
@ -72,7 +76,6 @@ function createWatcher () {
const watcher = chokidar.watch(nuxt.options._layers.map(i => i.config.srcDir as string).filter(Boolean), { const watcher = chokidar.watch(nuxt.options._layers.map(i => i.config.srcDir as string).filter(Boolean), {
...nuxt.options.watchers.chokidar, ...nuxt.options.watchers.chokidar,
cwd: nuxt.options.srcDir,
ignoreInitial: true, ignoreInitial: true,
ignored: [ ignored: [
isIgnored, isIgnored,
@ -80,7 +83,8 @@ function createWatcher () {
] ]
}) })
watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path))) // TODO: consider moving to emit absolute path in 3.8 or 4.0
watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(relative(nuxt.options.srcDir, path))))
nuxt.hook('close', () => watcher?.close()) nuxt.hook('close', () => watcher?.close())
} }
@ -94,7 +98,7 @@ function createGranularWatcher () {
let pending = 0 let pending = 0
const ignoredDirs = new Set([...nuxt.options.modulesDir, nuxt.options.buildDir]) const ignoredDirs = new Set([...nuxt.options.modulesDir, nuxt.options.buildDir])
const pathsToWatch = nuxt.options._layers.map(layer => layer.config.srcDir).filter(d => d && !isIgnored(d)) const pathsToWatch = nuxt.options._layers.map(layer => layer.config.srcDir || layer.cwd).filter(d => d && !isIgnored(d))
for (const pattern of nuxt.options.watch) { for (const pattern of nuxt.options.watch) {
if (typeof pattern !== 'string') { continue } if (typeof pattern !== 'string') { continue }
const path = resolve(nuxt.options.srcDir, pattern) const path = resolve(nuxt.options.srcDir, pattern)
@ -109,7 +113,8 @@ function createGranularWatcher () {
watcher.on('all', (event, path) => { watcher.on('all', (event, path) => {
path = normalize(path) path = normalize(path)
if (!pending) { if (!pending) {
nuxt.callHook('builder:watch', event, path) // TODO: consider moving to emit absolute path in 3.8 or 4.0
nuxt.callHook('builder:watch', event, relative(nuxt.options.srcDir, path))
} }
if (event === 'unlinkDir' && path in watchers) { if (event === 'unlinkDir' && path in watchers) {
watchers[path]?.close() watchers[path]?.close()
@ -117,7 +122,8 @@ function createGranularWatcher () {
} }
if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) { if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) {
watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] }) watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] })
watchers[path].on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path))) // TODO: consider moving to emit absolute path in 3.8 or 4.0
watchers[path].on('all', (event, p) => nuxt.callHook('builder:watch', event, normalize(relative(nuxt.options.srcDir, p))))
nuxt.hook('close', () => watchers[path]?.close()) nuxt.hook('close', () => watchers[path]?.close())
} }
}) })
@ -144,7 +150,8 @@ async function createParcelWatcher () {
if (err) { return } if (err) { return }
for (const event of events) { for (const event of events) {
if (isIgnored(event.path)) { continue } if (isIgnored(event.path)) { continue }
nuxt.callHook('builder:watch', watchEvents[event.type], normalize(event.path)) // TODO: consider moving to emit absolute path in 3.8 or 4.0
nuxt.callHook('builder:watch', watchEvents[event.type], normalize(relative(nuxt.options.srcDir, event.path)))
} }
}, { }, {
ignore: [ ignore: [

View File

@ -1,10 +1,10 @@
import { addDependency } from 'nypm' import { addDependency } from 'nypm'
import { isPackageExists } from 'local-pkg' import { resolvePackageJSON } from 'pkg-types'
import { logger } from '@nuxt/kit' import { logger } from '@nuxt/kit'
import prompts from 'prompts' import prompts from 'prompts'
export async function ensurePackageInstalled (rootDir: string, name: string, searchPaths?: string[]) { export async function ensurePackageInstalled (rootDir: string, name: string, searchPaths?: string[]) {
if (isPackageExists(name, { paths: searchPaths })) { if (await resolvePackageJSON(name, { url: searchPaths }).catch(() => null)) {
return true return true
} }

View File

@ -8,8 +8,6 @@ import escapeRE from 'escape-string-regexp'
import { defu } from 'defu' import { defu } from 'defu'
import fsExtra from 'fs-extra' import fsExtra from 'fs-extra'
import { dynamicEventHandler } from 'h3' import { dynamicEventHandler } from 'h3'
import { createHeadCore } from '@unhead/vue'
import { renderSSRHead } from '@unhead/ssr'
import type { Nuxt } from 'nuxt/schema' import type { Nuxt } from 'nuxt/schema'
// @ts-expect-error TODO: add legacy type support for subpath imports // @ts-expect-error TODO: add legacy type support for subpath imports
import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs' import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs'
@ -205,12 +203,6 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Resolve user-provided paths // Resolve user-provided paths
nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!) nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!)
// Add head chunk for SPA renders
const head = createHeadCore()
head.push(nuxt.options.app.head)
const headChunk = await renderSSRHead(head)
nitroConfig.virtual!['#head-static'] = `export default ${JSON.stringify(headChunk)}`
// Add fallback server for `ssr: false` // Add fallback server for `ssr: false`
if (!nuxt.options.ssr) { if (!nuxt.options.ssr) {
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}' nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
@ -281,6 +273,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Init nitro // Init nitro
const nitro = await createNitro(nitroConfig) const nitro = await createNitro(nitroConfig)
// Set prerender-only options
nitro.options._config.storage ||= {}
nitro.options._config.storage['internal:nuxt:prerender'] = { driver: 'memory' }
nitro.options._config.storage['internal:nuxt:prerender:island'] = { driver: 'lruCache', max: 1000 }
nitro.options._config.storage['internal:nuxt:prerender:payload'] = { driver: 'lruCache', max: 1000 }
// Expose nitro to modules and kit // Expose nitro to modules and kit
nuxt._nitro = nitro nuxt._nitro = nitro
await nuxt.callHook('nitro:init', nitro) await nuxt.callHook('nitro:init', nitro)

View File

@ -180,7 +180,7 @@ async function initNuxt (nuxt: Nuxt) {
`${config.dir?.modules || 'modules'}/*/index{${nuxt.options.extensions.join(',')}}` `${config.dir?.modules || 'modules'}/*/index{${nuxt.options.extensions.join(',')}}`
]) ])
for (const mod of layerModules) { for (const mod of layerModules) {
watchedPaths.add(relative(config.srcDir, mod)) watchedPaths.add(mod)
if (specifiedModules.has(mod)) { continue } if (specifiedModules.has(mod)) { continue }
specifiedModules.add(mod) specifiedModules.add(mod)
modulesToInstall.push(mod) modulesToInstall.push(mod)
@ -200,7 +200,7 @@ async function initNuxt (nuxt: Nuxt) {
addComponent({ addComponent({
name: 'NuxtLayout', name: 'NuxtLayout',
priority: 10, // built-in that we do not expect the user to override priority: 10, // built-in that we do not expect the user to override
filePath: resolve(nuxt.options.appDir, 'components/layout') filePath: resolve(nuxt.options.appDir, 'components/nuxt-layout')
}) })
// Add <NuxtErrorBoundary> // Add <NuxtErrorBoundary>
@ -341,19 +341,25 @@ async function initNuxt (nuxt: Nuxt) {
await nuxt.callHook('modules:done') await nuxt.callHook('modules:done')
nuxt.hooks.hook('builder:watch', (event, path) => { nuxt.hooks.hook('builder:watch', (event, relativePath) => {
const path = resolve(nuxt.options.srcDir, relativePath)
// Local module patterns // Local module patterns
if (watchedPaths.has(path)) { if (watchedPaths.has(path)) {
return nuxt.callHook('restart', { hard: true }) return nuxt.callHook('restart', { hard: true })
} }
// User provided patterns // User provided patterns
const layerRelativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
for (const pattern of nuxt.options.watch) { for (const pattern of nuxt.options.watch) {
if (typeof pattern === 'string') { if (typeof pattern === 'string') {
if (pattern === path) { return nuxt.callHook('restart') } // Test (normalised) strings against absolute path and relative path to any layer `srcDir`
if (pattern === path || layerRelativePaths.includes(pattern)) { return nuxt.callHook('restart') }
continue continue
} }
if (pattern.test(path)) { return nuxt.callHook('restart') } // Test regular expressions against path to _any_ layer `srcDir`
if (layerRelativePaths.some(p => pattern.test(p))) {
return nuxt.callHook('restart')
}
} }
// Core Nuxt files: app.vue, error.vue and app.config.ts // Core Nuxt files: app.vue, error.vue and app.config.ts
@ -405,6 +411,13 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
} }
} }
// Nuxt Webpack Builder is currently opt-in
if (options.builder === '@nuxt/webpack-builder') {
if (!await import('./features').then(r => r.ensurePackageInstalled(options.rootDir, '@nuxt/webpack-builder', options.modulesDir))) {
logger.warn('Failed to install `@nuxt/webpack-builder`, please install it manually, or change the `builder` option to vite in `nuxt.config`')
}
}
// Add core modules // Add core modules
options._modules.push(pagesModule, metaModule, componentsModule) options._modules.push(pagesModule, metaModule, componentsModule)
options._modules.push([importsModule, { options._modules.push([importsModule, {

View File

@ -1,4 +1,10 @@
import { createRenderer, renderResourceHeaders } from 'vue-bundle-renderer/runtime' import {
createRenderer,
getPrefetchLinks,
getPreloadLinks,
getRequestDependencies,
renderResourceHeaders
} from 'vue-bundle-renderer/runtime'
import type { RenderResponse } from 'nitropack' import type { RenderResponse } from 'nitropack'
import type { Manifest } from 'vite' import type { Manifest } from 'vite'
import type { H3Event } from 'h3' import type { H3Event } from 'h3'
@ -9,14 +15,17 @@ import destr from 'destr'
import { joinURL, withoutTrailingSlash } from 'ufo' import { joinURL, withoutTrailingSlash } from 'ufo'
import { renderToString as _renderToString } from 'vue/server-renderer' import { renderToString as _renderToString } from 'vue/server-renderer'
import { hash } from 'ohash' import { hash } from 'ohash'
import { renderSSRHead } from '@unhead/ssr'
import { defineRenderHandler, getRouteRules, useRuntimeConfig } from '#internal/nitro' import { defineRenderHandler, getRouteRules, useRuntimeConfig, useStorage } from '#internal/nitro'
import { useNitroApp } from '#internal/nitro/app' import { useNitroApp } from '#internal/nitro/app'
import type { Link, Script } from '@unhead/vue'
import { createServerHead } from '@unhead/vue'
// eslint-disable-next-line import/no-restricted-paths // eslint-disable-next-line import/no-restricted-paths
import type { NuxtPayload, NuxtSSRContext } from '#app/nuxt' import type { NuxtPayload, NuxtSSRContext } from '#app/nuxt'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appRootId, appRootTag } from '#internal/nuxt.config.mjs' import { appHead, appRootId, appRootTag } from '#internal/nuxt.config.mjs'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { buildAssetsURL, publicAssetsURL } from '#paths' import { buildAssetsURL, publicAssetsURL } from '#paths'
@ -71,9 +80,6 @@ const getEntryIds: () => Promise<string[]> = () => getClientManifest().then(r =>
r._globalCSS r._globalCSS
).map(r => r.src!)) ).map(r => r.src!))
// @ts-expect-error virtual file
const getStaticRenderedHead = (): Promise<NuxtMeta> => import('#head-static').then(r => r.default || r)
// @ts-expect-error file will be produced after app build // @ts-expect-error file will be produced after app build
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r) const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
@ -140,7 +146,6 @@ const getSPARenderer = lazyCachedFunction(async () => {
public: config.public, public: config.public,
app: config.app app: config.app
} }
ssrContext!.renderMeta = ssrContext!.renderMeta ?? getStaticRenderedHead
return Promise.resolve(result) return Promise.resolve(result)
} }
@ -150,9 +155,18 @@ const getSPARenderer = lazyCachedFunction(async () => {
} }
}) })
const payloadCache = process.env.prerender ? useStorage('internal:nuxt:prerender:payload') : null
const islandCache = process.env.prerender ? useStorage('internal:nuxt:prerender:island') : null
const islandPropCache = process.env.prerender ? useStorage('internal:nuxt:prerender:island-props') : null
async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> { async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
// TODO: Strict validation for url // TODO: Strict validation for url
const url = event.node.req.url?.substring('/__nuxt_island'.length + 1) || '' let url = event.node.req.url || ''
if (process.env.prerender && event.node.req.url && await islandPropCache!.hasItem(event.node.req.url)) {
// rehydrate props from cache so we can rerender island if cache does not have it any more
url = await islandPropCache!.getItem(event.node.req.url) as string
}
url = url.substring('/__nuxt_island'.length + 1) || ''
const [componentName, hashId] = url.split('?')[0].split('_') const [componentName, hashId] = url.split('?')[0].split('_')
// TODO: Validate context // TODO: Validate context
@ -170,8 +184,6 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
return ctx return ctx
} }
const PAYLOAD_CACHE = (process.env.NUXT_PAYLOAD_EXTRACTION && process.env.prerender) ? new Map() : null // TODO: Use LRU cache
const ISLAND_CACHE = (process.env.NUXT_COMPONENT_ISLANDS && process.env.prerender) ? new Map() : null // TODO: Use LRU cache
const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload(\.[a-zA-Z0-9]+)?.json(\?.*)?$/ : /\/_payload(\.[a-zA-Z0-9]+)?.js(\?.*)?$/ const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload(\.[a-zA-Z0-9]+)?.json(\?.*)?$/ : /\/_payload(\.[a-zA-Z0-9]+)?.js(\?.*)?$/
const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag} id="${appRootId}">([\\s\\S]*)</${appRootTag}>$`) const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag} id="${appRootId}">([\\s\\S]*)</${appRootTag}>$`)
@ -201,8 +213,8 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
? await getIslandContext(event) ? await getIslandContext(event)
: undefined : undefined
if (process.env.prerender && islandContext && ISLAND_CACHE!.has(event.node.req.url)) { if (process.env.prerender && islandContext && event.node.req.url && await islandCache!.hasItem(event.node.req.url)) {
return ISLAND_CACHE!.get(event.node.req.url) return islandCache!.getItem(event.node.req.url) as Promise<Partial<RenderResponse>>
} }
// Request url // Request url
@ -213,14 +225,17 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
if (isRenderingPayload) { if (isRenderingPayload) {
url = url.substring(0, url.lastIndexOf('/')) || '/' url = url.substring(0, url.lastIndexOf('/')) || '/'
event.node.req.url = url event.node.req.url = url
if (process.env.prerender && PAYLOAD_CACHE!.has(url)) { if (process.env.prerender && await payloadCache!.hasItem(url)) {
return PAYLOAD_CACHE!.get(url) return payloadCache!.getItem(url) as Promise<Partial<RenderResponse>>
} }
} }
// Get route options (currently to apply `ssr: false`) // Get route options (currently to apply `ssr: false`)
const routeOptions = getRouteRules(event) const routeOptions = getRouteRules(event)
const head = createServerHead()
head.push(appHead)
// Initialize ssr context // Initialize ssr context
const ssrContext: NuxtSSRContext = { const ssrContext: NuxtSSRContext = {
url, url,
@ -231,6 +246,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
event.context.nuxt?.noSSR || event.context.nuxt?.noSSR ||
routeOptions.ssr === false || routeOptions.ssr === false ||
(process.env.prerender ? PRERENDER_NO_SSR_ROUTES.has(url) : false), (process.env.prerender ? PRERENDER_NO_SSR_ROUTES.has(url) : false),
head,
error: !!ssrError, error: !!ssrError,
nuxt: undefined!, /* NuxtApp */ nuxt: undefined!, /* NuxtApp */
payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload, payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload,
@ -276,7 +292,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
if (isRenderingPayload) { if (isRenderingPayload) {
const response = renderPayloadResponse(ssrContext) const response = renderPayloadResponse(ssrContext)
if (process.env.prerender) { if (process.env.prerender) {
PAYLOAD_CACHE!.set(url, response) await payloadCache!.setItem(url, response)
} }
return response return response
} }
@ -285,12 +301,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Hint nitro to prerender payload for this route // Hint nitro to prerender payload for this route
appendResponseHeader(event, 'x-nitro-prerender', joinURL(url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js')) appendResponseHeader(event, 'x-nitro-prerender', joinURL(url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js'))
// Use same ssr context to generate payload for this route // Use same ssr context to generate payload for this route
PAYLOAD_CACHE!.set(withoutTrailingSlash(url), renderPayloadResponse(ssrContext)) await payloadCache!.setItem(withoutTrailingSlash(url), renderPayloadResponse(ssrContext))
} }
// Render meta
const renderedMeta = await ssrContext.renderMeta?.() ?? {}
if (process.env.NUXT_INLINE_STYLES && !islandContext) { if (process.env.NUXT_INLINE_STYLES && !islandContext) {
const source = ssrContext.modules ?? ssrContext._registeredComponents const source = ssrContext.modules ?? ssrContext._registeredComponents
if (source) { if (source) {
@ -303,45 +316,81 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Render inline styles // Render inline styles
const inlinedStyles = (process.env.NUXT_INLINE_STYLES || Boolean(islandContext)) const inlinedStyles = (process.env.NUXT_INLINE_STYLES || Boolean(islandContext))
? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? []) ? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? [])
: '' : []
const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.experimentalNoScripts const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.experimentalNoScripts
// Setup head
const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext)
// 1.Extracted payload preloading
if (_PAYLOAD_EXTRACTION) {
head.push({
link: [
process.env.NUXT_JSON_PAYLOADS
? { rel: 'preload', as: 'fetch', crossorigin: 'anonymous', href: payloadURL }
: { rel: 'modulepreload', href: payloadURL }
]
})
}
// 2. Styles
head.push({
link: Object.values(styles)
.map(resource =>
({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) })
),
style: inlinedStyles
})
if (!NO_SCRIPTS) {
// 3. Resource Hints
// TODO: add priorities based on Capo
head.push({
link: getPreloadLinks(ssrContext, renderer.rendererContext) as Link[]
})
head.push({
link: getPrefetchLinks(ssrContext, renderer.rendererContext) as Link[]
})
// 4. Payloads
head.push({
script: _PAYLOAD_EXTRACTION
? process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
}, {
// this should come before another end of body scripts
tagPosition: 'bodyClose',
tagPriority: 'high'
})
}
// 5. Scripts
if (!routeOptions.experimentalNoScripts) {
head.push({
script: Object.values(scripts).map(resource => (<Script> {
type: resource.module ? 'module' : null,
src: renderer.rendererContext.buildAssetsURL(resource.file),
defer: resource.module ? null : true,
crossorigin: ''
}))
})
}
// remove certain tags for nuxt islands
const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)
// Create render context // Create render context
const htmlContext: NuxtRenderHTMLContext = { const htmlContext: NuxtRenderHTMLContext = {
island: Boolean(islandContext), island: Boolean(islandContext),
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]), htmlAttrs: [htmlAttrs],
head: normalizeChunks([ head: normalizeChunks([headTags, ssrContext.styles]),
renderedMeta.headTags, bodyAttrs: [bodyAttrs],
process.env.NUXT_JSON_PAYLOADS bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
? _PAYLOAD_EXTRACTION ? `<link rel="preload" as="fetch" crossorigin="anonymous" href="${payloadURL}">` : null
: _PAYLOAD_EXTRACTION ? `<link rel="modulepreload" href="${payloadURL}">` : null,
NO_SCRIPTS ? null : _rendered.renderResourceHints(),
_rendered.renderStyles(),
inlinedStyles,
ssrContext.styles
]),
bodyAttrs: normalizeChunks([renderedMeta.bodyAttrs!]),
bodyPrepend: normalizeChunks([
renderedMeta.bodyScriptsPrepend,
ssrContext.teleports?.body
]),
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceServerOnlyComponentsSlots(ssrContext, _rendered.html) : _rendered.html], body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceServerOnlyComponentsSlots(ssrContext, _rendered.html) : _rendered.html],
bodyAppend: normalizeChunks([ bodyAppend: [bodyTags]
NO_SCRIPTS
? undefined
: (_PAYLOAD_EXTRACTION
? process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
),
routeOptions.experimentalNoScripts ? undefined : _rendered.renderScripts(),
// Note: bodyScripts may contain tags other than <script>
renderedMeta.bodyScripts
])
} }
// Allow hooking into the rendered result // Allow hooking into the rendered result
@ -349,21 +398,21 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Response for component islands // Response for component islands
if (process.env.NUXT_COMPONENT_ISLANDS && islandContext) { if (process.env.NUXT_COMPONENT_ISLANDS && islandContext) {
const _tags = htmlContext.head.flatMap(head => extractHTMLTags(head)) const islandHead: NuxtIslandResponse['head'] = {
const head: NuxtIslandResponse['head'] = { link: [],
link: _tags.filter(tag => tag.tagName === 'link' && tag.attrs.rel === 'stylesheet' && tag.attrs.href.includes('scoped') && !tag.attrs.href.includes('pages/')).map(tag => ({ style: []
key: 'island-link-' + hash(tag.attrs.href), }
...tag.attrs for (const tag of await head.resolveTags()) {
})), if (tag.tag === 'link' && tag.props.rel === 'stylesheet' && tag.props.href.includes('scoped') && !tag.props.href.includes('pages/')) {
style: _tags.filter(tag => tag.tagName === 'style' && tag.innerHTML).map(tag => ({ islandHead.link.push({ ...tag.props, key: 'island-link-' + hash(tag.props.href) })
key: 'island-style-' + hash(tag.innerHTML), }
innerHTML: tag.innerHTML if (tag.tag === 'style' && tag.innerHTML) {
})) islandHead.style.push({ key: 'island-style-' + hash(tag.innerHTML), innerHTML: tag.innerHTML })
}
} }
const islandResponse: NuxtIslandResponse = { const islandResponse: NuxtIslandResponse = {
id: islandContext.id, id: islandContext.id,
head, head: islandHead,
html: getServerComponentHTML(htmlContext.body), html: getServerComponentHTML(htmlContext.body),
state: ssrContext.payload.state state: ssrContext.payload.state
} }
@ -380,7 +429,8 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
} }
} satisfies RenderResponse } satisfies RenderResponse
if (process.env.prerender) { if (process.env.prerender) {
ISLAND_CACHE!.set(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, response) await islandCache!.setItem(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, response)
await islandPropCache!.setItem(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, event.node.req.url!)
} }
return response return response
} }
@ -429,33 +479,17 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) {
</html>` </html>`
} }
// TODO: Move to external library
const HTML_TAG_RE = /<(?<tag>[a-z]+)(?<rawAttrs> [^>]*)?>(?:(?<innerHTML>[\s\S]*?)<\/\k<tag>)?/g
const HTML_TAG_ATTR_RE = /(?<name>[a-z]+)="(?<value>[^"]*)"/g
function extractHTMLTags (html: string) {
const tags: { tagName: string, attrs: Record<string, string>, innerHTML: string }[] = []
for (const tagMatch of html.matchAll(HTML_TAG_RE)) {
const attrs: Record<string, string> = {}
for (const attrMatch of tagMatch.groups!.rawAttrs?.matchAll(HTML_TAG_ATTR_RE) || []) {
attrs[attrMatch.groups!.name] = attrMatch.groups!.value
}
const innerHTML = tagMatch.groups!.innerHTML || ''
tags.push({ tagName: tagMatch.groups!.tag, attrs, innerHTML })
}
return tags
}
async function renderInlineStyles (usedModules: Set<string> | string[]) { async function renderInlineStyles (usedModules: Set<string> | string[]) {
const styleMap = await getSSRStyles() const styleMap = await getSSRStyles()
const inlinedStyles = new Set<string>() const inlinedStyles = new Set<string>()
for (const mod of usedModules) { for (const mod of usedModules) {
if (mod in styleMap) { if (mod in styleMap) {
for (const style of await styleMap[mod]()) { for (const style of await styleMap[mod]()) {
inlinedStyles.add(`<style>${style}</style>`) inlinedStyles.add(style)
} }
} }
} }
return Array.from(inlinedStyles).join('') return Array.from(inlinedStyles).map(style => ({ innerHTML: style }))
} }
function renderPayloadResponse (ssrContext: NuxtSSRContext) { function renderPayloadResponse (ssrContext: NuxtSSRContext) {
@ -472,25 +506,41 @@ function renderPayloadResponse (ssrContext: NuxtSSRContext) {
} satisfies RenderResponse } satisfies RenderResponse
} }
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }) { function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
const attrs = [
'type="application/json"',
`id="${opts.id}"`,
`data-ssr="${!(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)}"`,
opts.src ? `data-src="${opts.src}"` : ''
].filter(Boolean)
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : '' const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
return `<script ${attrs.join(' ')}>${contents}</script>` + const payload: Script = {
`<script>window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}</script>` type: 'application/json',
id: opts.id,
innerHTML: contents,
'data-ssr': !(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)
}
if (opts.src) {
payload['data-src'] = opts.src
}
return [
payload,
{
innerHTML: `window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}`
}
]
} }
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }) { function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
opts.data.config = opts.ssrContext.config opts.data.config = opts.ssrContext.config
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
if (_PAYLOAD_EXTRACTION) { if (_PAYLOAD_EXTRACTION) {
return `<script type="module">import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})}</script>` return [
{
type: 'module',
innerHTML: `import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})`
}
]
} }
return `<script>window.__NUXT__=${devalue(opts.data)}</script>` return [
{
innerHTML: `window.__NUXT__=${devalue(opts.data)}`
}
]
} }
function splitPayload (ssrContext: NuxtSSRContext) { function splitPayload (ssrContext: NuxtSSRContext) {

View File

@ -1,6 +1,7 @@
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genSafeVariableName, genString } from 'knitwork' import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genSafeVariableName, genString } from 'knitwork'
import { isAbsolute, join, relative, resolve } from 'pathe' import { isAbsolute, join, relative, resolve } from 'pathe'
import type { JSValue } from 'untyped'
import { generateTypes, resolveSchema } from 'untyped' import { generateTypes, resolveSchema } from 'untyped'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import { hash } from 'ohash' import { hash } from 'ohash'
@ -144,7 +145,7 @@ export const schemaTemplate: NuxtTemplate<TemplateContext> = {
), ),
modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName]) => `[${genString(importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '', modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName]) => `[${genString(importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
' }', ' }',
generateTypes(await resolveSchema(Object.fromEntries(Object.entries(nuxt.options.runtimeConfig).filter(([key]) => key !== 'public'))), generateTypes(await resolveSchema(Object.fromEntries(Object.entries(nuxt.options.runtimeConfig).filter(([key]) => key !== 'public')) as Record<string, JSValue>),
{ {
interfaceName: 'RuntimeConfig', interfaceName: 'RuntimeConfig',
addExport: false, addExport: false,
@ -152,7 +153,7 @@ export const schemaTemplate: NuxtTemplate<TemplateContext> = {
allowExtraKeys: false, allowExtraKeys: false,
indentation: 2 indentation: 2
}), }),
generateTypes(await resolveSchema(nuxt.options.runtimeConfig.public), generateTypes(await resolveSchema(nuxt.options.runtimeConfig.public as Record<string, JSValue>),
{ {
interfaceName: 'PublicRuntimeConfig', interfaceName: 'PublicRuntimeConfig',
addExport: false, addExport: false,
@ -328,6 +329,7 @@ export const nuxtConfigTemplate = {
...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`), ...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`),
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`, `export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
`export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`, `export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`,
`export const remoteComponentIslands = ${ctx.nuxt.options.experimental.componentIslands === 'local+remote'}`,
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`, `export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}` `export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`
].join('\n\n') ].join('\n\n')

View File

@ -54,6 +54,10 @@ export default defineNuxtModule({
addPlugin({ src: resolve(runtimeDir, 'plugins/vueuse-head-polyfill') }) addPlugin({ src: resolve(runtimeDir, 'plugins/vueuse-head-polyfill') })
} }
if (nuxt.options.experimental.headCapoPlugin) {
addPlugin({ src: resolve(runtimeDir, 'plugins/capo') })
}
// Add library-specific plugin // Add library-specific plugin
addPlugin({ src: resolve(runtimeDir, 'plugins/unhead') }) addPlugin({ src: resolve(runtimeDir, 'plugins/unhead') })
} }

View File

@ -0,0 +1,9 @@
import { CapoPlugin } from '@unhead/vue'
import { defineNuxtPlugin } from '#app/nuxt'
export default defineNuxtPlugin({
name: 'nuxt:head:capo',
setup (nuxtApp) {
nuxtApp.vueApp._context.provides.usehead.use(CapoPlugin({ track: true }))
}
})

View File

@ -1,16 +1,11 @@
import { createHead as createClientHead, createServerHead } from '@unhead/vue' import { createHead as createClientHead } from '@unhead/vue'
import { renderSSRHead } from '@unhead/ssr'
import { defineNuxtPlugin } from '#app/nuxt' import { defineNuxtPlugin } from '#app/nuxt'
// @ts-expect-error untyped
import { appHead } from '#build/nuxt.config.mjs'
export default defineNuxtPlugin({ export default defineNuxtPlugin({
name: 'nuxt:head', name: 'nuxt:head',
setup (nuxtApp) { setup (nuxtApp) {
const createHead = process.server ? createServerHead : createClientHead const head = process.server ? nuxtApp.ssrContext!.head : createClientHead()
const head = createHead() // nuxt.config appHead is set server-side within the renderer
head.push(appHead)
nuxtApp.vueApp.use(head) nuxtApp.vueApp.use(head)
if (process.client) { if (process.client) {
@ -28,17 +23,5 @@ export default defineNuxtPlugin({
// unpause the DOM once the mount suspense is resolved // unpause the DOM once the mount suspense is resolved
nuxtApp.hooks.hook('app:suspense:resolve', unpauseDom) nuxtApp.hooks.hook('app:suspense:resolve', unpauseDom)
} }
if (process.server) {
nuxtApp.ssrContext!.renderMeta = async () => {
const meta = await renderSSRHead(head)
return {
...meta,
bodyScriptsPrepend: meta.bodyTagsOpen,
// resolves naming difference with NuxtMeta and Unhead
bodyScripts: meta.bodyTags
}
}
}
} }
}) })

View File

@ -63,12 +63,12 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
composablesDirs = composablesDirs.map(dir => normalize(dir)) composablesDirs = composablesDirs.map(dir => normalize(dir))
// Restart nuxt when composable directories are added/removed // Restart nuxt when composable directories are added/removed
nuxt.hook('builder:watch', (event, path) => { nuxt.hook('builder:watch', (event, relativePath) => {
const isDirChange = ['addDir', 'unlinkDir'].includes(event) if (!['addDir', 'unlinkDir'].includes(event)) { return }
const fullPath = resolve(nuxt.options.srcDir, path)
if (isDirChange && composablesDirs.includes(fullPath)) { const path = resolve(nuxt.options.srcDir, relativePath)
console.info(`Directory \`${path}/\` ${event === 'addDir' ? 'created' : 'removed'}`) if (composablesDirs.includes(path)) {
console.info(`Directory \`${relativePath}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
return nuxt.callHook('restart') return nuxt.callHook('restart')
} }
}) })
@ -119,9 +119,9 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
'imports.d.ts', 'imports.d.ts',
'imports.mjs' 'imports.mjs'
] ]
nuxt.hook('builder:watch', async (_, path) => { nuxt.hook('builder:watch', async (_, relativePath) => {
const _resolved = resolve(nuxt.options.srcDir, path) const path = resolve(nuxt.options.srcDir, relativePath)
if (composablesDirs.find(dir => _resolved.startsWith(dir))) { if (composablesDirs.some(dir => dir === path || path.startsWith(dir + '/'))) {
await updateTemplates({ await updateTemplates({
filter: template => templates.includes(template.filename) filter: template => templates.includes(template.filename)
}) })

View File

@ -3,7 +3,6 @@ import { mkdir, readFile } from 'node:fs/promises'
import { addComponent, addPlugin, addTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, updateTemplates } from '@nuxt/kit' import { addComponent, addPlugin, addTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, updateTemplates } 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 escapeRE from 'escape-string-regexp'
import { joinURL } from 'ufo' import { joinURL } from 'ufo'
import type { NuxtApp, NuxtPage } from 'nuxt/schema' import type { NuxtApp, NuxtPage } from 'nuxt/schema'
import { createRoutesContext } from 'unplugin-vue-router' import { createRoutesContext } from 'unplugin-vue-router'
@ -52,12 +51,13 @@ export default defineNuxtModule({
// Restart Nuxt when pages dir is added or removed // Restart Nuxt when pages dir is added or removed
const restartPaths = nuxt.options._layers.flatMap(layer => [ const restartPaths = nuxt.options._layers.flatMap(layer => [
join(layer.config.srcDir, 'app/router.options.ts'), join(layer.config.srcDir || layer.cwd, 'app/router.options.ts'),
join(layer.config.srcDir, layer.config.dir?.pages || 'pages') join(layer.config.srcDir || layer.cwd, layer.config.dir?.pages || 'pages')
]) ])
nuxt.hooks.hook('builder:watch', async (event, path) => {
const fullPath = join(nuxt.options.srcDir, path) nuxt.hooks.hook('builder:watch', async (event, relativePath) => {
if (restartPaths.some(path => path === fullPath || fullPath.startsWith(path + '/'))) { const path = resolve(nuxt.options.srcDir, relativePath)
if (restartPaths.some(p => p === path || path.startsWith(p + '/'))) {
const newSetting = await isPagesEnabled() const newSetting = await isPagesEnabled()
if (nuxt.options.pages !== newSetting) { if (nuxt.options.pages !== newSetting) {
console.info('Pages', newSetting ? 'enabled' : 'disabled') console.info('Pages', newSetting ? 'enabled' : 'disabled')
@ -174,15 +174,17 @@ export default defineNuxtModule({
}) })
// Regenerate templates when adding or removing pages // Regenerate templates when adding or removing pages
nuxt.hook('builder:watch', async (event, path) => { const updateTemplatePaths = nuxt.options._layers.flatMap(l => [
const dirs = [ join(l.config.srcDir || l.cwd, l.config.dir?.pages || 'pages') + '/',
nuxt.options.dir.pages, join(l.config.srcDir || l.cwd, l.config.dir?.layouts || 'layouts') + '/',
nuxt.options.dir.layouts, join(l.config.srcDir || l.cwd, l.config.dir?.middleware || 'middleware') + '/'
nuxt.options.dir.middleware ])
].filter(Boolean)
const pathPattern = new RegExp(`(^|\\/)(${dirs.map(escapeRE).join('|')})/`) nuxt.hook('builder:watch', async (event, relativePath) => {
if (event !== 'change' && pathPattern.test(path)) { if (event === 'change') { return }
const path = resolve(nuxt.options.srcDir, relativePath)
if (updateTemplatePaths.some(dir => path.startsWith(dir))) {
await updateTemplates({ await updateTemplates({
filter: template => template.filename === 'routes.mjs' filter: template => template.filename === 'routes.mjs'
}) })
@ -351,11 +353,11 @@ export default defineNuxtModule({
getContents: ({ app }: { app: NuxtApp }) => { getContents: ({ app }: { app: NuxtApp }) => {
const composablesFile = resolve(runtimeDir, 'composables') const composablesFile = resolve(runtimeDir, 'composables')
return [ return [
'import { ComputedRef, Ref } from \'vue\'', 'import { ComputedRef, MaybeRef } from \'vue\'',
`export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`, `export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`,
`declare module ${genString(composablesFile)} {`, `declare module ${genString(composablesFile)} {`,
' interface PageMeta {', ' interface PageMeta {',
' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>', ' layout?: MaybeRef<LayoutKey | false> | ComputedRef<LayoutKey | false>',
' }', ' }',
'}' '}'
].join('\n') ].join('\n')

View File

@ -14,7 +14,7 @@ export interface PageMeta {
* statusCode/statusMessage to respond immediately with an error (other matches * statusCode/statusMessage to respond immediately with an error (other matches
* will not be checked). * will not be checked).
*/ */
validate?: (route: RouteLocationNormalized) => boolean | Promise<boolean> | Partial<NuxtError> | Promise<Partial<NuxtError>> validate?: (route: RouteLocationNormalized) => boolean | Partial<NuxtError> | Promise<boolean | Partial<NuxtError>>
/** /**
* Where to redirect if the route is directly matched. The redirection happens * Where to redirect if the route is directly matched. The redirection happens
* before any navigation guard and triggers a new navigation with the new * before any navigation guard and triggers a new navigation with the new
@ -36,7 +36,7 @@ export interface PageMeta {
/** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */ /** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */
path?: string path?: string
/** Set to `false` to avoid scrolling to top on page navigations */ /** Set to `false` to avoid scrolling to top on page navigations */
scrollToTop?: boolean scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
} }
declare module 'vue-router' { declare module 'vue-router' {

View File

@ -20,8 +20,10 @@ export default <RouterConfig> {
// savedPosition is only available for popstate navigations (back button) // savedPosition is only available for popstate navigations (back button)
let position: ScrollPosition = savedPosition || undefined let position: ScrollPosition = savedPosition || undefined
const routeAllowsScrollToTop = typeof to.meta.scrollToTop === 'function' ? to.meta.scrollToTop(to, from) : to.meta.scrollToTop
// Scroll to top if route is changed by default // Scroll to top if route is changed by default
if (!position && from && to && to.meta.scrollToTop !== false && _isDifferentRoute(from, to)) { if (!position && from && to && routeAllowsScrollToTop !== false && _isDifferentRoute(from, to)) {
position = { left: 0, top: 0 } position = { left: 0, top: 0 }
} }

View File

@ -1,12 +1,13 @@
/// <reference types="nitropack" /> /// <reference types="nitropack" />
export * from './dist/index' export * from './dist/index'
import type { DefineNuxtConfig } from 'nuxt/config'
import type { SchemaDefinition, RuntimeConfig } from 'nuxt/schema' import type { SchemaDefinition, RuntimeConfig } from 'nuxt/schema'
import type { H3Event } from 'h3' import type { H3Event } from 'h3'
import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from './dist/core/runtime/nitro/renderer' import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from './dist/core/runtime/nitro/renderer'
declare global { declare global {
const defineNuxtConfig: typeof import('nuxt/config')['defineNuxtConfig'] const defineNuxtConfig: DefineNuxtConfig
const defineNuxtSchema: (schema: SchemaDefinition) => SchemaDefinition const defineNuxtSchema: (schema: SchemaDefinition) => SchemaDefinition
} }

View File

@ -30,34 +30,35 @@
"@types/file-loader": "5.0.1", "@types/file-loader": "5.0.1",
"@types/pug": "2.0.6", "@types/pug": "2.0.6",
"@types/sass-loader": "8.0.5", "@types/sass-loader": "8.0.5",
"@unhead/schema": "1.1.32", "@unhead/schema": "1.2.2",
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.2.3",
"@vitejs/plugin-vue-jsx": "3.0.1", "@vitejs/plugin-vue-jsx": "3.0.1",
"@vue/compiler-core": "3.3.4", "@vue/compiler-core": "3.3.4",
"esbuild-loader": "3.0.1", "esbuild-loader": "3.1.0",
"h3": "1.7.1", "h3": "1.7.1",
"ignore": "5.2.4", "ignore": "5.2.4",
"nitropack": "2.5.2", "nitropack": "2.5.2",
"unbuild": "latest", "unbuild": "latest",
"unctx": "2.3.1", "unctx": "2.3.1",
"vite": "4.4.7", "vite": "4.4.8",
"vue": "3.3.4", "vue": "3.3.4",
"vue-bundle-renderer": "1.0.3", "vue-bundle-renderer": "2.0.0",
"vue-loader": "17.2.2", "vue-loader": "17.2.2",
"vue-router": "4.2.4", "vue-router": "4.2.4",
"webpack": "5.88.2", "webpack": "5.88.2",
"webpack-dev-middleware": "6.1.1" "webpack-dev-middleware": "6.1.1"
}, },
"dependencies": { "dependencies": {
"@nuxt/ui-templates": "^1.3.1",
"defu": "^6.1.2", "defu": "^6.1.2",
"hookable": "^5.5.3", "hookable": "^5.5.3",
"pathe": "^1.1.1", "pathe": "^1.1.1",
"pkg-types": "^1.0.3", "pkg-types": "^1.0.3",
"postcss-import-resolver": "^2.0.0", "postcss-import-resolver": "^2.0.0",
"std-env": "^3.3.3", "std-env": "^3.3.3",
"ufo": "^1.1.2", "ufo": "^1.2.0",
"unimport": "^3.1.0", "unimport": "^3.1.3",
"untyped": "^1.3.2" "untyped": "^1.4.0"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"

View File

@ -365,8 +365,9 @@ export default defineUntypedSchema({
/** /**
* The watch property lets you define patterns that will restart the Nuxt dev server when changed. * The watch property lets you define patterns that will restart the Nuxt dev server when changed.
* *
* It is an array of strings or regular expressions, which will be matched against the file path * It is an array of strings or regular expressions. Strings should be either absolute paths or
* relative to the project `srcDir`. * relative to the `srcDir` (and the `srcDir` of any layers). Regular expressions will be matched
* against the path relative to the project `srcDir` (and the `srcDir` of any layers).
* *
* @type {Array<string | RegExp>} * @type {Array<string | RegExp>}
*/ */

View File

@ -1,4 +1,5 @@
import { defineUntypedSchema } from 'untyped' import { defineUntypedSchema } from 'untyped'
import { loading as loadingTemplate } from '@nuxt/ui-templates'
export default defineUntypedSchema({ export default defineUntypedSchema({
devServer: { devServer: {
@ -36,5 +37,12 @@ export default defineUntypedSchema({
* dev server with the full URL (for module and internal use). * dev server with the full URL (for module and internal use).
*/ */
url: 'http://localhost:3000', url: 'http://localhost:3000',
/**
* Template to show a loading screen
*
* @type {(data: { loading?: string }) => string}
*/
loadingTemplate: loadingTemplate
} }
}) })

View File

@ -19,7 +19,7 @@ export default defineUntypedSchema({
*/ */
reactivityTransform: false, reactivityTransform: false,
// TODO: Remove in v3.6 when nitro has support for mocking traced dependencies // TODO: Remove in v3.8 when nitro has support for mocking traced dependencies
// https://github.com/unjs/nitro/issues/1118 // https://github.com/unjs/nitro/issues/1118
/** /**
* Externalize `vue`, `@vue/*` and `vue-router` when building. * Externalize `vue`, `@vue/*` and `vue-router` when building.
@ -137,8 +137,15 @@ export default defineUntypedSchema({
/** /**
* Experimental component islands support with <NuxtIsland> and .island.vue files. * Experimental component islands support with <NuxtIsland> and .island.vue files.
* @type {true | 'local' | 'local+remote' | false}
*/ */
componentIslands: false, componentIslands: {
$resolve: (val) => {
if (typeof val === 'string') { return val }
if (val === true) { return 'local' }
return false
}
},
/** /**
* Config schema support * Config schema support
@ -200,6 +207,13 @@ export default defineUntypedSchema({
* @see https://github.com/parcel-bundler/watcher * @see https://github.com/parcel-bundler/watcher
* @type {'chokidar' | 'parcel' | 'chokidar-granular'} * @type {'chokidar' | 'parcel' | 'chokidar-granular'}
*/ */
watcher: 'chokidar-granular' watcher: 'chokidar-granular',
/**
* Add the capo.js head plugin in order to render tags in of the head in a more performant way.
*
* @see https://rviscomi.github.io/capo.js/user/rules/
*/
headCapoPlugin: false
} }
}) })

View File

@ -123,7 +123,7 @@ export interface NuxtHooks {
* @param app The configured `NuxtApp` object * @param app The configured `NuxtApp` object
* @returns Promise * @returns Promise
*/ */
'app:templatesGenerated': (app: NuxtApp) => HookResult 'app:templatesGenerated': (app: NuxtApp, templates: ResolvedNuxtTemplate[], options?: GenerateAppOptions) => HookResult
/** /**
* Called before Nuxt bundle builder. * Called before Nuxt bundle builder.

View File

@ -26,15 +26,15 @@
"@nuxt/schema": "workspace:../schema", "@nuxt/schema": "workspace:../schema",
"consola": "^3.2.3", "consola": "^3.2.3",
"defu": "^6.1.2", "defu": "^6.1.2",
"execa": "^7.1.1", "execa": "^7.2.0",
"get-port-please": "^3.0.1", "get-port-please": "^3.0.1",
"ofetch": "^1.1.1", "ofetch": "^1.1.1",
"pathe": "^1.1.1", "pathe": "^1.1.1",
"ufo": "^1.1.2" "ufo": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.6.1", "@jest/globals": "29.6.2",
"playwright-core": "1.36.1", "playwright-core": "1.36.2",
"unbuild": "latest", "unbuild": "latest",
"vitest": "0.33.0" "vitest": "0.33.0"
}, },

View File

@ -35,7 +35,7 @@
"consola": "^3.2.3", "consola": "^3.2.3",
"cssnano": "^6.0.1", "cssnano": "^6.0.1",
"defu": "^6.1.2", "defu": "^6.1.2",
"esbuild": "^0.18.16", "esbuild": "^0.18.17",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"externality": "^1.0.2", "externality": "^1.0.2",
@ -43,7 +43,7 @@
"get-port-please": "^3.0.1", "get-port-please": "^3.0.1",
"h3": "^1.7.1", "h3": "^1.7.1",
"knitwork": "^1.0.0", "knitwork": "^1.0.0",
"magic-string": "^0.30.1", "magic-string": "^0.30.2",
"mlly": "^1.4.0", "mlly": "^1.4.0",
"ohash": "^1.1.2", "ohash": "^1.1.2",
"pathe": "^1.1.1", "pathe": "^1.1.1",
@ -54,13 +54,13 @@
"postcss-url": "^10.1.3", "postcss-url": "^10.1.3",
"rollup-plugin-visualizer": "^5.9.2", "rollup-plugin-visualizer": "^5.9.2",
"std-env": "^3.3.3", "std-env": "^3.3.3",
"strip-literal": "^1.0.1", "strip-literal": "^1.3.0",
"ufo": "^1.1.2", "ufo": "^1.2.0",
"unplugin": "^1.4.0", "unplugin": "^1.4.0",
"vite": "^4.4.7", "vite": "^4.4.8",
"vite-node": "^0.33.0", "vite-node": "^0.33.0",
"vite-plugin-checker": "^0.6.1", "vite-plugin-checker": "^0.6.1",
"vue-bundle-renderer": "^1.0.3" "vue-bundle-renderer": "^2.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.3.4" "vue": "^3.3.4"

View File

@ -26,7 +26,7 @@
"css-minimizer-webpack-plugin": "^5.0.1", "css-minimizer-webpack-plugin": "^5.0.1",
"cssnano": "^6.0.1", "cssnano": "^6.0.1",
"defu": "^6.1.2", "defu": "^6.1.2",
"esbuild-loader": "^3.0.1", "esbuild-loader": "^3.1.0",
"escape-string-regexp": "^5.0.0", "escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
@ -35,7 +35,7 @@
"h3": "^1.7.1", "h3": "^1.7.1",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"magic-string": "^0.30.1", "magic-string": "^0.30.2",
"memfs": "^4.2.0", "memfs": "^4.2.0",
"mini-css-extract-plugin": "^2.7.6", "mini-css-extract-plugin": "^2.7.6",
"mlly": "^1.4.0", "mlly": "^1.4.0",
@ -49,10 +49,10 @@
"pug-plain-loader": "^1.1.0", "pug-plain-loader": "^1.1.0",
"std-env": "^3.3.3", "std-env": "^3.3.3",
"time-fix-plugin": "^2.0.7", "time-fix-plugin": "^2.0.7",
"ufo": "^1.1.2", "ufo": "^1.2.0",
"unplugin": "^1.4.0", "unplugin": "^1.4.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-bundle-renderer": "^1.0.3", "vue-bundle-renderer": "^2.0.0",
"vue-loader": "^17.2.2", "vue-loader": "^17.2.2",
"webpack": "^5.88.2", "webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.9.0", "webpack-bundle-analyzer": "^4.9.0",

File diff suppressed because it is too large Load Diff

View File

@ -2,3 +2,4 @@ packages:
- "packages/**" - "packages/**"
- "playground" - "playground"
- "test/fixtures/*" - "test/fixtures/*"
- ".website"

View File

@ -25,7 +25,6 @@
"main" "main"
], ],
"ignoreDeps": [ "ignoreDeps": [
"typescript",
"markdownlint-cli", "markdownlint-cli",
"nuxt", "nuxt",
"nuxt3", "nuxt3",

View File

@ -10,10 +10,12 @@ async function main () {
const date = Math.round(Date.now() / (1000 * 60)) const date = Math.round(Date.now() / (1000 * 60))
const nuxtPkg = workspace.find('nuxt') const nuxtPkg = workspace.find('nuxt')
const nitroInfo = await $fetch('https://registry.npmjs.org/nitropack-edge') const { version: latestNitro } = await $fetch<{ version: string }>('https://registry.npmjs.org/nitropack-edge/latest')
const latestNitro = nitroInfo['dist-tags'].latest
nuxtPkg.data.dependencies.nitropack = `npm:nitropack-edge@^${latestNitro}` nuxtPkg.data.dependencies.nitropack = `npm:nitropack-edge@^${latestNitro}`
const { version: latestNuxi } = await $fetch<{ version: string }>('https://registry.npmjs.org/nuxi-ng/latest')
nuxtPkg.data.dependencies.nuxi = `npm:nuxi-ng@^${latestNuxi}`
const bumpType = await determineBumpType() const bumpType = await determineBumpType()
for (const pkg of workspace.packages.filter(p => !p.data.private)) { for (const pkg of workspace.packages.filter(p => !p.data.private)) {

View File

@ -105,6 +105,7 @@ describe('pages', () => {
// should import JSX/TSX components with custom elements // should import JSX/TSX components with custom elements
expect(html).toContain('TSX component') expect(html).toContain('TSX component')
expect(html).toContain('<custom-component>custom</custom-component>') expect(html).toContain('<custom-component>custom</custom-component>')
expect(html).toContain('Sugar Counter 12 x 2 = 24')
}) })
it('respects aliases in page metadata', async () => { it('respects aliases in page metadata', async () => {
@ -1364,7 +1365,6 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
const html: string = await $fetch('/styles') const html: string = await $fetch('/styles')
expect(html.match(/<link [^>]*href="[^"]*\.css">/g)?.filter(m => m.includes('entry'))?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(` expect(html.match(/<link [^>]*href="[^"]*\.css">/g)?.filter(m => m.includes('entry'))?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(`
[ [
"<link rel=\\"preload\\" as=\\"style\\" href=\\"/_nuxt/entry.css\\">",
"<link rel=\\"stylesheet\\" href=\\"/_nuxt/entry.css\\">", "<link rel=\\"stylesheet\\" href=\\"/_nuxt/entry.css\\">",
] ]
`) `)
@ -1418,6 +1418,43 @@ describe('server components/islands', () => {
await page.close() await page.close()
}) })
it('lazy server components', async () => {
const page = await createPage('/server-components/lazy/start')
await page.waitForLoadState('networkidle')
await page.getByText('Go to page with lazy server component').click()
const text = await page.innerText('pre')
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id=\\"fallback\\"> Loading server component </section><section id=\\"no-fallback\\"><div></div></section>"')
expect(text).not.toContain('async component that was very long')
expect(text).toContain('Loading server component')
// Wait for all pending micro ticks to be cleared
// await page.waitForLoadState('networkidle')
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.close()
})
it('non-lazy server components', async () => {
const page = await createPage('/server-components/lazy/start')
await page.waitForLoadState('networkidle')
await page.getByText('Go to page without lazy server component').click()
const text = await page.innerText('pre')
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id=\\"fallback\\"><div nuxt-ssr-component-uid=\\"0\\"> This is a .server (20ms) async component that was very long ... <div id=\\"async-server-component-count\\">42</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div></div></section><section id=\\"no-fallback\\"><div nuxt-ssr-component-uid=\\"1\\"> This is a .server (20ms) async component that was very long ... <div id=\\"async-server-component-count\\">42</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div></div></section>"')
expect(text).toContain('async component that was very long')
// Wait for all pending micro ticks to be cleared
// await page.waitForLoadState('networkidle')
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
await page.close()
})
it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => { it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => {
// @ts-expect-error ssssh! untyped secret property // @ts-expect-error ssssh! untyped secret property
const publicDir = useTestContext().nuxt._nitro.options.output.publicDir const publicDir = useTestContext().nuxt._nitro.options.output.publicDir
@ -1648,7 +1685,7 @@ describe('component islands', () => {
"link": [], "link": [],
"style": [], "style": [],
}, },
"html": "<div nuxt-ssr-component-uid><div> count is above 2 </div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div> that was very long ... <div id=\\"long-async-component-count\\">3</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"test\\" nuxt-ssr-slot-data=\\"[{&quot;count&quot;:3}]\\"></div><p>hello world !!!</p><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"hello\\" nuxt-ssr-slot-data=\\"[{&quot;t&quot;:0},{&quot;t&quot;:1},{&quot;t&quot;:2}]\\"><div nuxt-slot-fallback-start=\\"hello\\"></div><!--[--><div style=\\"display:contents;\\"><div> fallback slot -- index: 0</div></div><div style=\\"display:contents;\\"><div> fallback slot -- index: 1</div></div><div style=\\"display:contents;\\"><div> fallback slot -- index: 2</div></div><!--]--><div nuxt-slot-fallback-end></div></div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"fallback\\" nuxt-ssr-slot-data=\\"[{&quot;t&quot;:&quot;fall&quot;},{&quot;t&quot;:&quot;back&quot;}]\\"><div nuxt-slot-fallback-start=\\"fallback\\"></div><!--[--><div style=\\"display:contents;\\"><div>fall slot -- index: 0</div><div class=\\"fallback-slot-content\\"> wonderful fallback </div></div><div style=\\"display:contents;\\"><div>back slot -- index: 1</div><div class=\\"fallback-slot-content\\"> wonderful fallback </div></div><!--]--><div nuxt-slot-fallback-end></div></div></div>", "html": "<div nuxt-ssr-component-uid><div> count is above 2 </div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div> that was very long ... <div id=\\"long-async-component-count\\">3</div> <div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"test\\" nuxt-ssr-slot-data=\\"[{&quot;count&quot;:3}]\\"></div><p>hello world !!!</p><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"hello\\" nuxt-ssr-slot-data=\\"[{&quot;t&quot;:0},{&quot;t&quot;:1},{&quot;t&quot;:2}]\\"><div nuxt-slot-fallback-start=\\"hello\\"></div><!--[--><div style=\\"display:contents;\\"><div> fallback slot -- index: 0</div></div><div style=\\"display:contents;\\"><div> fallback slot -- index: 1</div></div><div style=\\"display:contents;\\"><div> fallback slot -- index: 2</div></div><!--]--><div nuxt-slot-fallback-end></div></div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"fallback\\" nuxt-ssr-slot-data=\\"[{&quot;t&quot;:&quot;fall&quot;},{&quot;t&quot;:&quot;back&quot;}]\\"><div nuxt-slot-fallback-start=\\"fallback\\"></div><!--[--><div style=\\"display:contents;\\"><div>fall slot -- index: 0</div><div class=\\"fallback-slot-content\\"> wonderful fallback </div></div><div style=\\"display:contents;\\"><div>back slot -- index: 1</div><div class=\\"fallback-slot-content\\"> wonderful fallback </div></div><!--]--><div nuxt-slot-fallback-end></div></div></div>",
"state": {}, "state": {},
} }
`) `)

View File

@ -19,7 +19,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
for (const outputDir of ['.output', '.output-inline']) { for (const outputDir of ['.output', '.output-inline']) {
it('default client bundle size', async () => { it('default client bundle size', async () => {
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public')) const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.3k"') expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.4k"')
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(` expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
[ [
"_nuxt/entry.js", "_nuxt/entry.js",
@ -35,7 +35,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.4k"') expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.4k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir) const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2330k"') expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2346k"')
const packages = modules.files const packages = modules.files
.filter(m => m.endsWith('package.json')) .filter(m => m.endsWith('package.json'))
@ -95,7 +95,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"370k"') expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"370k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir) const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"591k"') expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"608k"')
const packages = modules.files const packages = modules.files
.filter(m => m.endsWith('package.json')) .filter(m => m.endsWith('package.json'))

View File

@ -28,14 +28,6 @@ export default defineNuxtConfig({
} }
}, },
modules: [ modules: [
function (_, nuxt) {
// TODO: remove in v3.7
if (process.env.TS_BASE_URL === 'without-base-url') {
nuxt.hook('prepare:types', ({ tsConfig }) => {
delete tsConfig.compilerOptions!.baseUrl
})
}
},
function () { function () {
addTypeTemplate({ addTypeTemplate({
filename: 'test.d.ts', filename: 'test.d.ts',

View File

@ -56,9 +56,9 @@ describe('API routes', () => {
it('works with useFetch', () => { it('works with useFetch', () => {
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>() expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>() expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
// @ts-expect-error TODO: remove when fixed upstream: https://github.com/unjs/nitro/pull/1247
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>() expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>() expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>() expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
// @ts-expect-error not a valid method // @ts-expect-error not a valid method
useFetch('/api/hey', { method: 'PATCH' }) useFetch('/api/hey', { method: 'PATCH' })
@ -116,6 +116,21 @@ describe('middleware', () => {
abortNavigation(true) abortNavigation(true)
}, { global: true }) }, { global: true })
}) })
it('handles return types of validate', () => {
definePageMeta({
validate: async () => {
await new Promise(resolve => setTimeout(resolve, 1000))
// eslint-disable-next-line
if (0) {
return createError({
statusCode: 404,
statusMessage: 'resource-type-not-found'
})
}
return true
}
})
})
}) })
describe('typed router integration', () => { describe('typed router integration', () => {

View File

@ -3,6 +3,7 @@ export default defineComponent({
return <div> return <div>
TSX component TSX component
<custom-component>custom</custom-component> <custom-component>custom</custom-component>
<SugarCounter multiplier={2} />
</div> </div>
} }
}) })

View File

@ -8,6 +8,7 @@
<div id="long-async-component-count"> <div id="long-async-component-count">
{{ count }} {{ count }}
</div> </div>
{{ headers['custom-head'] }}
<slot name="test" :count="count" /> <slot name="test" :count="count" />
<p>hello world !!!</p> <p>hello world !!!</p>
<slot v-for="(t, index) in 3" name="hello" :t="t"> <slot v-for="(t, index) in 3" name="hello" :t="t">
@ -28,8 +29,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getResponseHeaders } from 'h3'
defineProps<{ defineProps<{
count: number count: number
}>() }>()
const evt = useRequestEvent()
const headers = getResponseHeaders(evt)
const { data } = await useFetch('/api/very-long-request') const { data } = await useFetch('/api/very-long-request')
</script> </script>

View File

@ -187,7 +187,8 @@ export default defineNuxtConfig({
componentIslands: true, componentIslands: true,
reactivityTransform: true, reactivityTransform: true,
treeshakeClientOnly: true, treeshakeClientOnly: true,
payloadExtraction: true payloadExtraction: true,
headCapoPlugin: true
}, },
appConfig: { appConfig: {
fromNuxtConfig: true, fromNuxtConfig: true,

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
const page = ref<HTMLDivElement | undefined>()
const mountedHTML = ref()
onMounted(() => {
mountedHTML.value = page.value?.innerHTML
})
const lazy = useRoute().query.lazy === 'true'
</script>
<template>
<div ref="page" class="end-page">
End page
<pre>{{ mountedHTML }}</pre>
<section id="fallback">
<AsyncServerComponent :lazy="lazy" :count="42">
<template #fallback>
Loading server component
</template>
</AsyncServerComponent>
</section>
<section id="no-fallback">
<AsyncServerComponent :lazy="lazy" :count="42" />
</section>
</div>
</template>

View File

@ -0,0 +1,10 @@
<template>
<div>
<NuxtLink to="/server-components/lazy/end?lazy=true">
Go to page with lazy server component
</NuxtLink>
<NuxtLink to="/server-components/lazy/end?lazy=false">
Go to page without lazy server component
</NuxtLink>
</div>
</template>

View File

@ -0,0 +1,12 @@
import { setHeader } from 'h3'
export default defineNuxtPlugin({
name: 'server-only-plugin',
setup () {
const evt = useRequestEvent()
setHeader(evt, 'custom-head', 'hello')
},
env: {
islands: false
}
})