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
👉 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
id: bug-env
attributes:

View File

@ -10,7 +10,7 @@ body:
Please use a template below to create a minimal reproduction
👉 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
id: bug-env
attributes:

View File

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

View File

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

View File

@ -66,6 +66,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- 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:
sarif_file: results.sarif

View File

@ -7,14 +7,20 @@ on:
- edited
- synchronize
permissions:
contents: read
jobs:
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'
runs-on: ubuntu-latest
name: Semantic pull request
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@v5
uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 # v5.2.0
with:
scopes: |
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
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
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]
defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: true
}
}
})
```
Working of the Nitro crawler:
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.
2. Save the HTML and `payload.json` to the `~/.output/public/` directory to be served statically.
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
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]
defineNuxtConfig({
nitro: {
prerender: {
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]
defineNuxtConfig({
/* The /dynamic route won't be crawled */
nitro: {
prerender: { crawlLinks: true, ignore: ['/dynamic'] }
},
experimental: {
payloadExtraction: true
prerender: {
crawlLinks: true,
routes: ['/sitemap.xml', '/robots.txt']
}
}
})
```
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
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
:summary[Additional notes for an optimal setup:]
- **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)
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.
```vue [pages/posts/[id].vue]
```vue [pages/posts/[id\\].vue]
<script setup lang="ts">
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.
```vue [pages/posts/[id].vue]
```vue [pages/posts/[id\\].vue]
<script setup lang="ts">
definePageMeta({
validate: async (route) => {

View File

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

View File

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

View File

@ -90,7 +90,7 @@ export default defineNuxtConfig({
// Homepage pre-rendered at build time
'/': { prerender: true },
// Product page generated on-demand, revalidates in background
'/products/**': { swr: true },
'/products/**': { swr: 3600 },
// Blog post generated on-demand once until next deploy
'/blog/**': { isr: true },
// 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`
- `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
- `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.
- `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)
- `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: 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
- `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:
```vue [pages/[...slug].vue]
```vue [pages/[...slug\\].vue]
<template>
<main>
<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:
```vue [pages/users-[group]/[id].vue]
```vue [pages/users-[group\\]/[id\\].vue]
<template>
<p>{{ $route.params.group }} - {{ $route.params.id }}</p>
</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.
```vue [pages/[...slug].vue]
```vue [pages/[...slug\\].vue]
<template>
<p>{{ $route.params.slug }}</p>
</template>

View File

@ -151,7 +151,7 @@ Server routes can use dynamic parameters within brackets in the file name like `
**Example:**
```ts [server/api/hello/[name].ts]
```ts [server/api/hello/[name\\].ts]
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:**
```ts [server/api/foo/[...].ts]
```ts [server/api/foo/[...\\].ts]
export default defineEventHandler(() => `Default foo handler`)
```
```ts [server/api/[...].ts]
```ts [server/api/[...\\].ts]
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`
```ts [server/api/validation/[id].ts]
```ts [server/api/validation/[id\\].ts]
export default defineEventHandler((event) => {
const id = parseInt(event.context.params.id) as number
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`
```ts [server/api/validation/[id].ts]
```ts [server/api/validation/[id\\].ts]
export default defineEventHandler((event) => {
setResponseStatus(event, 202)
})
@ -284,7 +284,7 @@ export default defineNuxtConfig({
### Using a Nested Router
```ts [server/api/hello/[...slug].ts]
```ts [server/api/hello/[...slug\\].ts]
import { createRouter, defineEventHandler, useBase } from 'h3'
const router = createRouter()

View File

@ -7,6 +7,8 @@ head.title: ".env"
# .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`.
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.
::
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.

View File

@ -150,9 +150,9 @@ It is also possible to type your runtime config manually:
declare module 'nuxt/schema' {
interface RuntimeConfig {
apiSecret: string
public: {
apiBase: string
}
}
interface PublicRuntimeConfig {
apiBase: string
}
}
// 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",
"request": "launch",
"name": "server: nuxt",
"outputCapture": "std",
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
"args": [
"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
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.
```html [~/pages/[slug].vue]
```html [~/pages/[slug\\].vue]
<script setup lang="ts">
const route = useRoute()
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
```vue [pages/movies/[slug].vue]
```vue [pages/movies/[slug\\].vue]
<script setup lang="ts">
const route = useRoute()
const { data } = await useFetch(`/api/movies/${route.params.slug}`)

View File

@ -32,6 +32,7 @@ interface PageMeta {
keepalive?: boolean | KeepAliveProps
layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>
middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
[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).
**`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]`**
- **Type**: `any`

View File

@ -112,3 +112,42 @@ description: Nuxt Kit provides composable utilities to help interacting with Nux
- `extendViteConfig(callback, options?)`
- `addWebpackPlugin(webpackPlugin, 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
`--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**:
: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**:
: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**:
: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}
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).
```diff [pages/users/[id].vue]
```diff [pages/users/[id\\].vue]
- <script>
- export default {
- 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.
```vue [pages/users/[id].vue]
```vue [pages/users/[id\\].vue]
<script setup lang="ts">
const route = useRoute()
const { data, refresh } = await useFetch('/api/user')

View File

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

View File

@ -34,9 +34,10 @@
"pkg-types": "^1.0.3",
"scule": "^1.0.0",
"semver": "^7.5.4",
"ufo": "^1.2.0",
"unctx": "^2.3.1",
"unimport": "^3.1.0",
"untyped": "^1.3.2"
"unimport": "^3.1.3",
"untyped": "^1.4.0"
},
"devDependencies": {
"@types/hash-sum": "1.0.0",
@ -45,7 +46,7 @@
"lodash-es": "4.17.21",
"nitropack": "2.5.2",
"unbuild": "latest",
"vite": "4.4.7",
"vite": "4.4.8",
"vitest": "0.33.0",
"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 */
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
return normalize(_require.resolve(id, {
paths: ([] as Array<string | undefined>).concat(
global.__NUXT_PREPATHS__,
opts.paths || [],
process.cwd(),
global.__NUXT_PATHS__
).filter(Boolean) as string[]
paths: getModulePaths(opts.paths)
}))
}

View File

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

View File

@ -1,4 +1,5 @@
import { resolve } from 'pathe'
import type { JSValue } from 'untyped'
import { applyDefaults } from 'untyped'
import type { LoadConfigOptions } from 'c12'
import { loadConfig } from 'c12'
@ -50,5 +51,5 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
}
// 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: {
name: 'nuxt-module-foo'
}
})
}),
[
defineNuxtModule({
meta: {
name: 'module-instance-with-options'
}
}),
{
foo: 'bar'
}
]
]
}
})
expect(hasNuxtModule('nuxt-module-foo', nuxt)).toStrictEqual(true)
expect(hasNuxtModule('module-instance-with-options', nuxt)).toStrictEqual(true)
await nuxt.close()
})
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 type { Nuxt, NuxtModule } from '@nuxt/schema'
import type { Nuxt, NuxtModule, NuxtOptions } from '@nuxt/schema'
import { useNuxt } from '../context'
import { normalizeSemanticVersion } from '../compatibility'
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.
*
@ -11,8 +21,10 @@ import { loadNuxtModuleInstance } from './install'
* 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 {
// check installed modules
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
}
// 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)
return buildTimeModuleMeta.version || false
}

View File

@ -1,8 +1,15 @@
import { existsSync } from 'node:fs'
import { basename, parse, resolve } from 'pathe'
import { existsSync, promises as fsp } from 'node:fs'
import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
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 { getModulePaths } from './internal/cjs'
/**
* 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 }) {
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",
"deep-object-diff": "1.1.9",
"defu": "6.1.2",
"destr": "2.0.0",
"execa": "7.1.1",
"destr": "2.0.1",
"execa": "7.2.0",
"flat": "5.0.2",
"giget": "1.1.2",
"h3": "1.7.1",
"jiti": "1.19.1",
"listhen": "1.1.2",
"listhen": "1.2.2",
"mlly": "1.4.0",
"mri": "1.2.0",
"ohash": "1.1.2",
@ -47,7 +47,7 @@
"pkg-types": "1.0.3",
"scule": "1.0.0",
"semver": "7.5.4",
"ufo": "1.1.2",
"ufo": "1.2.0",
"unbuild": "latest"
},
"optionalDependencies": {

View File

@ -1,6 +1,8 @@
import { relative, resolve } from 'pathe'
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 { clearBuildDir } from '../utils/fs'
import { overrideEnv } from '../utils/env'
@ -19,7 +21,7 @@ export default defineNuxtCommand({
const rootDir = resolve(args._[0] || '.')
showVersions(rootDir)
const { loadNuxt, buildNuxt, useNitro } = await loadKit(rootDir)
const { loadNuxt, buildNuxt, useNitro, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const nuxt = await loadNuxt({
rootDir,

View File

@ -7,8 +7,10 @@ import type { Nuxt } from '@nuxt/schema'
import { consola } from 'consola'
import { withTrailingSlash } from 'ufo'
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 { writeTypes } from '../utils/prepare'
import { loadKit } from '../utils/kit'
import { importModule } from '../utils/esm'
import { overrideEnv } from '../utils/env'
@ -30,7 +32,7 @@ export default defineNuxtCommand({
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({
cwd: rootDir,
@ -46,7 +48,7 @@ export default defineNuxtCommand({
let currentHandler: RequestListener | undefined
let loadingMessage = 'Nuxt is starting...'
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.statusCode = 503 // Service Unavailable
res.end(loadingTemplate({ loading: loadingMessage }))

View File

@ -1,8 +1,10 @@
import { relative, resolve } from 'pathe'
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 { loadKit } from '../utils/kit'
import { writeTypes } from '../utils/prepare'
import { defineNuxtCommand } from './index'
export default defineNuxtCommand({
@ -15,7 +17,7 @@ export default defineNuxtCommand({
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
const rootDir = resolve(args._[0] || '.')
const { loadNuxt, buildNuxt } = await loadKit(rootDir)
const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const nuxt = await loadNuxt({
rootDir,
overrides: {

View File

@ -1,9 +1,10 @@
import { execa } from 'execa'
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 { writeTypes } from '../utils/prepare'
import { defineNuxtCommand } from './index'
export default defineNuxtCommand({
@ -16,7 +17,7 @@ export default defineNuxtCommand({
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
const rootDir = resolve(args._[0] || '.')
const { loadNuxt, buildNuxt } = await loadKit(rootDir)
const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
const nuxt = await loadNuxt({
rootDir,
overrides: {

View File

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

View File

@ -18,7 +18,7 @@ export default defineComponent({
if (!component) {
throw createError({
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'
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 { 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
)
}
}
})
// TODO: remove in 4.x
export { default } from './nuxt-layout'

View File

@ -13,6 +13,9 @@ import { getFragmentHTML, getSlotProps } from './utils'
import { useNuxtApp, useRuntimeConfig } from '#app/nuxt'
import { useRequestEvent } from '#app/composables/ssr'
// @ts-expect-error virtual file
import { remoteComponentIslands } from '#build/nuxt.config.mjs'
const pKey = '_islandPromises'
const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
@ -29,6 +32,7 @@ export default defineComponent({
type: String,
required: true
},
lazy: Boolean,
props: {
type: Object,
default: () => undefined
@ -36,12 +40,17 @@ export default defineComponent({
context: {
type: Object,
default: () => ({})
},
source: {
type: String,
default: () => undefined
}
},
async setup (props, { slots }) {
const error = ref<unknown>(null)
const config = useRuntimeConfig()
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 event = useRequestEvent()
// 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) {
const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('')
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 uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID())
@ -100,7 +109,8 @@ export default defineComponent({
const key = `${props.name}_${hashId.value}`
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) {
// Hint to Nitro to prerender the island component
appendResponseHeader(event, 'x-nitro-prerender', url)
@ -130,18 +140,23 @@ export default defineComponent({
delete nuxtApp[pKey]![uid.value]
})
}
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
cHead.value.link = res.head.link
cHead.value.style = res.head.style
ssrHTML.value = res.html.replace(UID_ATTR, () => {
return `nuxt-ssr-component-uid="${getId()}"`
})
key.value++
if (process.client) {
// must await next tick for Teleport to work correctly with static node re-rendering
await nextTick()
try {
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
cHead.value.link = res.head.link
cHead.value.style = res.head.style
ssrHTML.value = res.html.replace(UID_ATTR, () => {
return `nuxt-ssr-component-uid="${getId()}"`
})
key.value++
error.value = null
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) {
@ -154,15 +169,19 @@ export default defineComponent({
watch(props, debounce(() => fetchComponent(), 100))
}
// TODO: allow lazy loading server islands
if (process.server || !nuxtApp.isHydrating) {
if (process.client && !nuxtApp.isHydrating && props.lazy) {
fetchComponent()
} else if (process.server || !nuxtApp.isHydrating) {
await fetchComponent()
}
return () => {
if ((!html.value || error.value) && slots.fallback) {
return [slots.fallback({ error: error.value })]
}
const nodes = [createVNode(Fragment, {
key: key.value
}, [h(createStaticVNode(html.value, 1))])]
}, [h(createStaticVNode(html.value || '<div></div>', 1))])]
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
for (const slot in slots) {
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) {
watch(cookie, (newVal, oldVal) => {
if (watchPaused || isEqual(newVal, oldVal)) { return }
watch(cookie, () => {
if (watchPaused) { return }
callback()
},
{ deep: opts.watch !== 'shallow' })

View File

@ -1,18 +1,25 @@
import type { FetchError } from 'ofetch'
import type { AvailableRouterMethod, NitroFetchOptions, NitroFetchRequest, TypedInternalResponse } from 'nitropack'
import type { FetchError, FetchOptions } from 'ofetch'
import type { NitroFetchRequest, TypedInternalResponse, AvailableRouterMethod as _AvailableRouterMethod } from 'nitropack'
import type { Ref } from 'vue'
import { computed, reactive, unref } from 'vue'
import { hash } from 'ohash'
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'
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>> = {
[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>>
export interface UseFetchOptions<
@ -69,15 +76,6 @@ export function useFetch<
arg2?: string
) {
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(() => {
let r = request
@ -87,6 +85,16 @@ export function useFetch<
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('//')) {
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 { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router'
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 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 })
if (isExternal && !options?.external) {
throw new Error('Navigating to 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.')
if (isExternal) {
if (!options?.external) {
throw new Error('Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.')
}
const protocol = parseURL(toPath).protocol
if (protocol && isScriptProtocol(protocol)) {
throw new Error(`Cannot navigate to a URL with '${protocol}' protocol.`)
}
}
const inMiddleware = isProcessingMiddleware()
@ -218,7 +221,7 @@ export const abortNavigation = (err?: string | Partial<NuxtError>) => {
throw err
}
export const setPageLayout = (layout: string) => {
export const setPageLayout = (layout: unknown extends PageMeta['layout'] ? string : PageMeta['layout']) => {
if (process.server) {
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.')

View File

@ -10,6 +10,7 @@ import type { H3Event } from 'h3'
import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema'
import type { RenderResponse } from 'nitropack'
import type { MergeHead, VueHeadClient } from '@unhead/vue'
// eslint-disable-next-line import/no-restricted-paths
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
import type { RouteMiddleware } from '../../app'
@ -18,15 +19,6 @@ import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
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 AppRenderedContext = { ssrContext: NuxtApp['ssrContext'], renderResult: null | Awaited<ReturnType<ReturnType<typeof createRenderer>['renderToString']>> }
@ -59,10 +51,10 @@ export interface NuxtSSRContext extends SSRContext {
error?: boolean
nuxt: _NuxtApp
payload: NuxtPayload
head: VueHeadClient<MergeHead>
/** This is used solely to render runtime config with SPA renderer. */
config?: Pick<RuntimeConfig, 'public' | 'app'>
teleports?: Record<string, string>
renderMeta?: () => Promise<NuxtMeta> | NuxtMeta
islandContext?: NuxtIslandContext
/** @internal */
_renderResponse?: Partial<RenderResponse>
@ -163,6 +155,16 @@ export interface PluginMeta {
order?: number
}
export interface PluginEnvContext {
/**
* This enable the plugin for islands components.
* Require `experimental.componentsIslands`.
*
* @default true
*/
islands?: boolean
}
export interface ResolvedPluginMeta {
name?: string
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 {
hooks?: Partial<RuntimeNuxtHooks>
setup?: Plugin<Injections>
env?: PluginEnvContext
/**
* 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 errors: Error[] = []
for (const plugin of plugins) {
if (process.server && nuxtApp.ssrContext?.islandContext && plugin.env?.islands === false) { continue }
const promise = applyPlugin(nuxtApp, plugin)
if (plugin.parallel) {
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))) {
return true
}
return isVue(id, { type: ['template', 'script'] })
return isVue(id, { type: ['template', 'script'] }) || !!id.match(/\.[tj]sx$/)
},
transform (code) {
const components = options.getComponents()

View File

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

View File

@ -5,9 +5,11 @@ export const createServerComponent = (name: string) => {
return defineComponent({
name,
inheritAttrs: false,
setup (_props, { attrs, slots }) {
props: { lazy: Boolean },
setup (props, { attrs, slots }) {
return () => h(NuxtIsland, {
name,
lazy: props.lazy,
props: attrs
}, 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 { defu } from 'defu'
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))
// Compile templates into vfs
// TODO: remove utils in v4
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))
.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 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
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
}
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) {
await fsp.mkdir(dirname(fullPath), { recursive: true })
await fsp.writeFile(fullPath, contents, 'utf8')
writes.push(() => {
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) {

View File

@ -5,7 +5,7 @@ import chokidar from 'chokidar'
import { isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit'
import { interopDefault } from 'mlly'
import { debounce } from 'perfect-debounce'
import { normalize, resolve } from 'pathe'
import { normalize, relative, resolve } from 'pathe'
import type { Nuxt } from 'nuxt/schema'
import { generateApp as _generateApp, createApp } from './app'
@ -19,12 +19,16 @@ export async function build (nuxt: Nuxt) {
if (nuxt.options.dev) {
watch(nuxt)
nuxt.hook('builder:watch', async (event, path) => {
if (event !== 'change' && /^(app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(path)) {
if (path.startsWith('app')) {
nuxt.hook('builder:watch', async (event, relativePath) => {
if (event === 'change') { return }
const path = resolve(nuxt.options.srcDir, relativePath)
const relativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
const restartPath = relativePaths.find(relativePath => /^(app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(relativePath))
if (restartPath) {
if (restartPath.startsWith('app')) {
app.mainComponent = undefined
}
if (path.startsWith('error')) {
if (restartPath.startsWith('error')) {
app.errorComponent = undefined
}
await generateApp()
@ -72,7 +76,6 @@ function createWatcher () {
const watcher = chokidar.watch(nuxt.options._layers.map(i => i.config.srcDir as string).filter(Boolean), {
...nuxt.options.watchers.chokidar,
cwd: nuxt.options.srcDir,
ignoreInitial: true,
ignored: [
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())
}
@ -94,7 +98,7 @@ function createGranularWatcher () {
let pending = 0
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) {
if (typeof pattern !== 'string') { continue }
const path = resolve(nuxt.options.srcDir, pattern)
@ -109,7 +113,8 @@ function createGranularWatcher () {
watcher.on('all', (event, path) => {
path = normalize(path)
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) {
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)) {
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())
}
})
@ -144,7 +150,8 @@ async function createParcelWatcher () {
if (err) { return }
for (const event of events) {
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: [

View File

@ -1,10 +1,10 @@
import { addDependency } from 'nypm'
import { isPackageExists } from 'local-pkg'
import { resolvePackageJSON } from 'pkg-types'
import { logger } from '@nuxt/kit'
import prompts from 'prompts'
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
}

View File

@ -8,8 +8,6 @@ import escapeRE from 'escape-string-regexp'
import { defu } from 'defu'
import fsExtra from 'fs-extra'
import { dynamicEventHandler } from 'h3'
import { createHeadCore } from '@unhead/vue'
import { renderSSRHead } from '@unhead/ssr'
import type { Nuxt } from 'nuxt/schema'
// @ts-expect-error TODO: add legacy type support for subpath imports
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
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`
if (!nuxt.options.ssr) {
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
@ -281,6 +273,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
// Init nitro
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
nuxt._nitro = 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(',')}}`
])
for (const mod of layerModules) {
watchedPaths.add(relative(config.srcDir, mod))
watchedPaths.add(mod)
if (specifiedModules.has(mod)) { continue }
specifiedModules.add(mod)
modulesToInstall.push(mod)
@ -200,7 +200,7 @@ async function initNuxt (nuxt: Nuxt) {
addComponent({
name: 'NuxtLayout',
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>
@ -341,19 +341,25 @@ async function initNuxt (nuxt: Nuxt) {
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
if (watchedPaths.has(path)) {
return nuxt.callHook('restart', { hard: true })
}
// User provided patterns
const layerRelativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
for (const pattern of nuxt.options.watch) {
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
}
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
@ -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
options._modules.push(pagesModule, metaModule, componentsModule)
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 { Manifest } from 'vite'
import type { H3Event } from 'h3'
@ -9,14 +15,17 @@ import destr from 'destr'
import { joinURL, withoutTrailingSlash } from 'ufo'
import { renderToString as _renderToString } from 'vue/server-renderer'
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 type { Link, Script } from '@unhead/vue'
import { createServerHead } from '@unhead/vue'
// eslint-disable-next-line import/no-restricted-paths
import type { NuxtPayload, NuxtSSRContext } from '#app/nuxt'
// @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
import { buildAssetsURL, publicAssetsURL } from '#paths'
@ -71,9 +80,6 @@ const getEntryIds: () => Promise<string[]> = () => getClientManifest().then(r =>
r._globalCSS
).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
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
@ -140,7 +146,6 @@ const getSPARenderer = lazyCachedFunction(async () => {
public: config.public,
app: config.app
}
ssrContext!.renderMeta = ssrContext!.renderMeta ?? getStaticRenderedHead
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> {
// 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('_')
// TODO: Validate context
@ -170,8 +184,6 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
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 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)
: undefined
if (process.env.prerender && islandContext && ISLAND_CACHE!.has(event.node.req.url)) {
return ISLAND_CACHE!.get(event.node.req.url)
if (process.env.prerender && islandContext && event.node.req.url && await islandCache!.hasItem(event.node.req.url)) {
return islandCache!.getItem(event.node.req.url) as Promise<Partial<RenderResponse>>
}
// Request url
@ -213,14 +225,17 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
if (isRenderingPayload) {
url = url.substring(0, url.lastIndexOf('/')) || '/'
event.node.req.url = url
if (process.env.prerender && PAYLOAD_CACHE!.has(url)) {
return PAYLOAD_CACHE!.get(url)
if (process.env.prerender && await payloadCache!.hasItem(url)) {
return payloadCache!.getItem(url) as Promise<Partial<RenderResponse>>
}
}
// Get route options (currently to apply `ssr: false`)
const routeOptions = getRouteRules(event)
const head = createServerHead()
head.push(appHead)
// Initialize ssr context
const ssrContext: NuxtSSRContext = {
url,
@ -231,6 +246,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
event.context.nuxt?.noSSR ||
routeOptions.ssr === false ||
(process.env.prerender ? PRERENDER_NO_SSR_ROUTES.has(url) : false),
head,
error: !!ssrError,
nuxt: undefined!, /* NuxtApp */
payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload,
@ -276,7 +292,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
if (isRenderingPayload) {
const response = renderPayloadResponse(ssrContext)
if (process.env.prerender) {
PAYLOAD_CACHE!.set(url, response)
await payloadCache!.setItem(url, response)
}
return response
}
@ -285,12 +301,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Hint nitro to prerender payload for this route
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
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) {
const source = ssrContext.modules ?? ssrContext._registeredComponents
if (source) {
@ -303,45 +316,81 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Render inline styles
const inlinedStyles = (process.env.NUXT_INLINE_STYLES || Boolean(islandContext))
? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? [])
: ''
: []
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
const htmlContext: NuxtRenderHTMLContext = {
island: Boolean(islandContext),
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
head: normalizeChunks([
renderedMeta.headTags,
process.env.NUXT_JSON_PAYLOADS
? _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
]),
htmlAttrs: [htmlAttrs],
head: normalizeChunks([headTags, ssrContext.styles]),
bodyAttrs: [bodyAttrs],
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceServerOnlyComponentsSlots(ssrContext, _rendered.html) : _rendered.html],
bodyAppend: normalizeChunks([
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
])
bodyAppend: [bodyTags]
}
// Allow hooking into the rendered result
@ -349,21 +398,21 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Response for component islands
if (process.env.NUXT_COMPONENT_ISLANDS && islandContext) {
const _tags = htmlContext.head.flatMap(head => extractHTMLTags(head))
const head: NuxtIslandResponse['head'] = {
link: _tags.filter(tag => tag.tagName === 'link' && tag.attrs.rel === 'stylesheet' && tag.attrs.href.includes('scoped') && !tag.attrs.href.includes('pages/')).map(tag => ({
key: 'island-link-' + hash(tag.attrs.href),
...tag.attrs
})),
style: _tags.filter(tag => tag.tagName === 'style' && tag.innerHTML).map(tag => ({
key: 'island-style-' + hash(tag.innerHTML),
innerHTML: tag.innerHTML
}))
const islandHead: NuxtIslandResponse['head'] = {
link: [],
style: []
}
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/')) {
islandHead.link.push({ ...tag.props, key: 'island-link-' + hash(tag.props.href) })
}
if (tag.tag === 'style' && tag.innerHTML) {
islandHead.style.push({ key: 'island-style-' + hash(tag.innerHTML), innerHTML: tag.innerHTML })
}
}
const islandResponse: NuxtIslandResponse = {
id: islandContext.id,
head,
head: islandHead,
html: getServerComponentHTML(htmlContext.body),
state: ssrContext.payload.state
}
@ -380,7 +429,8 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
}
} satisfies RenderResponse
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
}
@ -429,33 +479,17 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) {
</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[]) {
const styleMap = await getSSRStyles()
const inlinedStyles = new Set<string>()
for (const mod of usedModules) {
if (mod in styleMap) {
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) {
@ -472,25 +506,41 @@ function renderPayloadResponse (ssrContext: NuxtSSRContext) {
} satisfies RenderResponse
}
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }) {
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)
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
return `<script ${attrs.join(' ')}>${contents}</script>` +
`<script>window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}</script>`
const payload: 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
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
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) {

View File

@ -1,6 +1,7 @@
import { existsSync } from 'node:fs'
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genSafeVariableName, genString } from 'knitwork'
import { isAbsolute, join, relative, resolve } from 'pathe'
import type { JSValue } from 'untyped'
import { generateTypes, resolveSchema } from 'untyped'
import escapeRE from 'escape-string-regexp'
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(' | ')})[],` : '',
' }',
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',
addExport: false,
@ -152,7 +153,7 @@ export const schemaTemplate: NuxtTemplate<TemplateContext> = {
allowExtraKeys: false,
indentation: 2
}),
generateTypes(await resolveSchema(nuxt.options.runtimeConfig.public),
generateTypes(await resolveSchema(nuxt.options.runtimeConfig.public as Record<string, JSValue>),
{
interfaceName: 'PublicRuntimeConfig',
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)}`),
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
`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 devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`
].join('\n\n')

View File

@ -54,6 +54,10 @@ export default defineNuxtModule({
addPlugin({ src: resolve(runtimeDir, 'plugins/vueuse-head-polyfill') })
}
if (nuxt.options.experimental.headCapoPlugin) {
addPlugin({ src: resolve(runtimeDir, 'plugins/capo') })
}
// Add library-specific plugin
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 { renderSSRHead } from '@unhead/ssr'
import { createHead as createClientHead } from '@unhead/vue'
import { defineNuxtPlugin } from '#app/nuxt'
// @ts-expect-error untyped
import { appHead } from '#build/nuxt.config.mjs'
export default defineNuxtPlugin({
name: 'nuxt:head',
setup (nuxtApp) {
const createHead = process.server ? createServerHead : createClientHead
const head = createHead()
head.push(appHead)
const head = process.server ? nuxtApp.ssrContext!.head : createClientHead()
// nuxt.config appHead is set server-side within the renderer
nuxtApp.vueApp.use(head)
if (process.client) {
@ -28,17 +23,5 @@ export default defineNuxtPlugin({
// unpause the DOM once the mount suspense is resolved
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))
// Restart nuxt when composable directories are added/removed
nuxt.hook('builder:watch', (event, path) => {
const isDirChange = ['addDir', 'unlinkDir'].includes(event)
const fullPath = resolve(nuxt.options.srcDir, path)
nuxt.hook('builder:watch', (event, relativePath) => {
if (!['addDir', 'unlinkDir'].includes(event)) { return }
if (isDirChange && composablesDirs.includes(fullPath)) {
console.info(`Directory \`${path}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
const path = resolve(nuxt.options.srcDir, relativePath)
if (composablesDirs.includes(path)) {
console.info(`Directory \`${relativePath}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
return nuxt.callHook('restart')
}
})
@ -119,9 +119,9 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
'imports.d.ts',
'imports.mjs'
]
nuxt.hook('builder:watch', async (_, path) => {
const _resolved = resolve(nuxt.options.srcDir, path)
if (composablesDirs.find(dir => _resolved.startsWith(dir))) {
nuxt.hook('builder:watch', async (_, relativePath) => {
const path = resolve(nuxt.options.srcDir, relativePath)
if (composablesDirs.some(dir => dir === path || path.startsWith(dir + '/'))) {
await updateTemplates({
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 { dirname, join, relative, resolve } from 'pathe'
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
import escapeRE from 'escape-string-regexp'
import { joinURL } from 'ufo'
import type { NuxtApp, NuxtPage } from 'nuxt/schema'
import { createRoutesContext } from 'unplugin-vue-router'
@ -52,12 +51,13 @@ export default defineNuxtModule({
// Restart Nuxt when pages dir is added or removed
const restartPaths = nuxt.options._layers.flatMap(layer => [
join(layer.config.srcDir, 'app/router.options.ts'),
join(layer.config.srcDir, layer.config.dir?.pages || 'pages')
join(layer.config.srcDir || layer.cwd, 'app/router.options.ts'),
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)
if (restartPaths.some(path => path === fullPath || fullPath.startsWith(path + '/'))) {
nuxt.hooks.hook('builder:watch', async (event, relativePath) => {
const path = resolve(nuxt.options.srcDir, relativePath)
if (restartPaths.some(p => p === path || path.startsWith(p + '/'))) {
const newSetting = await isPagesEnabled()
if (nuxt.options.pages !== newSetting) {
console.info('Pages', newSetting ? 'enabled' : 'disabled')
@ -174,15 +174,17 @@ export default defineNuxtModule({
})
// Regenerate templates when adding or removing pages
nuxt.hook('builder:watch', async (event, path) => {
const dirs = [
nuxt.options.dir.pages,
nuxt.options.dir.layouts,
nuxt.options.dir.middleware
].filter(Boolean)
const updateTemplatePaths = nuxt.options._layers.flatMap(l => [
join(l.config.srcDir || l.cwd, l.config.dir?.pages || 'pages') + '/',
join(l.config.srcDir || l.cwd, l.config.dir?.layouts || 'layouts') + '/',
join(l.config.srcDir || l.cwd, l.config.dir?.middleware || 'middleware') + '/'
])
const pathPattern = new RegExp(`(^|\\/)(${dirs.map(escapeRE).join('|')})/`)
if (event !== 'change' && pathPattern.test(path)) {
nuxt.hook('builder:watch', async (event, relativePath) => {
if (event === 'change') { return }
const path = resolve(nuxt.options.srcDir, relativePath)
if (updateTemplatePaths.some(dir => path.startsWith(dir))) {
await updateTemplates({
filter: template => template.filename === 'routes.mjs'
})
@ -351,11 +353,11 @@ export default defineNuxtModule({
getContents: ({ app }: { app: NuxtApp }) => {
const composablesFile = resolve(runtimeDir, 'composables')
return [
'import { ComputedRef, Ref } from \'vue\'',
'import { ComputedRef, MaybeRef } from \'vue\'',
`export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`,
`declare module ${genString(composablesFile)} {`,
' interface PageMeta {',
' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>',
' layout?: MaybeRef<LayoutKey | false> | ComputedRef<LayoutKey | false>',
' }',
'}'
].join('\n')

View File

@ -14,7 +14,7 @@ export interface PageMeta {
* statusCode/statusMessage to respond immediately with an error (other matches
* 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
* 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. */
path?: string
/** Set to `false` to avoid scrolling to top on page navigations */
scrollToTop?: boolean
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
}
declare module 'vue-router' {

View File

@ -20,8 +20,10 @@ export default <RouterConfig> {
// savedPosition is only available for popstate navigations (back button)
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
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 }
}

View File

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

View File

@ -30,34 +30,35 @@
"@types/file-loader": "5.0.1",
"@types/pug": "2.0.6",
"@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-jsx": "3.0.1",
"@vue/compiler-core": "3.3.4",
"esbuild-loader": "3.0.1",
"esbuild-loader": "3.1.0",
"h3": "1.7.1",
"ignore": "5.2.4",
"nitropack": "2.5.2",
"unbuild": "latest",
"unctx": "2.3.1",
"vite": "4.4.7",
"vite": "4.4.8",
"vue": "3.3.4",
"vue-bundle-renderer": "1.0.3",
"vue-bundle-renderer": "2.0.0",
"vue-loader": "17.2.2",
"vue-router": "4.2.4",
"webpack": "5.88.2",
"webpack-dev-middleware": "6.1.1"
},
"dependencies": {
"@nuxt/ui-templates": "^1.3.1",
"defu": "^6.1.2",
"hookable": "^5.5.3",
"pathe": "^1.1.1",
"pkg-types": "^1.0.3",
"postcss-import-resolver": "^2.0.0",
"std-env": "^3.3.3",
"ufo": "^1.1.2",
"unimport": "^3.1.0",
"untyped": "^1.3.2"
"ufo": "^1.2.0",
"unimport": "^3.1.3",
"untyped": "^1.4.0"
},
"engines": {
"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.
*
* It is an array of strings or regular expressions, which will be matched against the file path
* relative to the project `srcDir`.
* It is an array of strings or regular expressions. Strings should be either absolute paths or
* 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>}
*/

View File

@ -1,4 +1,5 @@
import { defineUntypedSchema } from 'untyped'
import { loading as loadingTemplate } from '@nuxt/ui-templates'
export default defineUntypedSchema({
devServer: {
@ -36,5 +37,12 @@ export default defineUntypedSchema({
* dev server with the full URL (for module and internal use).
*/
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,
// 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
/**
* 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.
* @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
@ -200,6 +207,13 @@ export default defineUntypedSchema({
* @see https://github.com/parcel-bundler/watcher
* @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
* @returns Promise
*/
'app:templatesGenerated': (app: NuxtApp) => HookResult
'app:templatesGenerated': (app: NuxtApp, templates: ResolvedNuxtTemplate[], options?: GenerateAppOptions) => HookResult
/**
* Called before Nuxt bundle builder.

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -10,10 +10,12 @@ async function main () {
const date = Math.round(Date.now() / (1000 * 60))
const nuxtPkg = workspace.find('nuxt')
const nitroInfo = await $fetch('https://registry.npmjs.org/nitropack-edge')
const latestNitro = nitroInfo['dist-tags'].latest
const { version: latestNitro } = await $fetch<{ version: string }>('https://registry.npmjs.org/nitropack-edge/latest')
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()
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
expect(html).toContain('TSX 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 () => {
@ -1364,7 +1365,6 @@ describe.skipIf(isDev() || isWebpack)('inlining component 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(`
[
"<link rel=\\"preload\\" as=\\"style\\" href=\\"/_nuxt/entry.css\\">",
"<link rel=\\"stylesheet\\" href=\\"/_nuxt/entry.css\\">",
]
`)
@ -1418,6 +1418,43 @@ describe('server components/islands', () => {
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 () => {
// @ts-expect-error ssssh! untyped secret property
const publicDir = useTestContext().nuxt._nitro.options.output.publicDir
@ -1648,7 +1685,7 @@ describe('component islands', () => {
"link": [],
"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": {},
}
`)

View File

@ -19,7 +19,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
for (const outputDir of ['.output', '.output-inline']) {
it('default client bundle size', async () => {
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(`
[
"_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"')
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
.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"')
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
.filter(m => m.endsWith('package.json'))

View File

@ -28,14 +28,6 @@ export default defineNuxtConfig({
}
},
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 () {
addTypeTemplate({
filename: 'test.d.ts',

View File

@ -56,9 +56,9 @@ describe('API routes', () => {
it('works with useFetch', () => {
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<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: '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
useFetch('/api/hey', { method: 'PATCH' })
@ -116,6 +116,21 @@ describe('middleware', () => {
abortNavigation(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', () => {

View File

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

View File

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

View File

@ -187,7 +187,8 @@ export default defineNuxtConfig({
componentIslands: true,
reactivityTransform: true,
treeshakeClientOnly: true,
payloadExtraction: true
payloadExtraction: true,
headCapoPlugin: true
},
appConfig: {
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
}
})