mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 16:12:12 +00:00
Merge branch 'main' into feat/kit-nuxt-module-options
This commit is contained in:
commit
317ab1a342
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -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:
|
||||
|
2
.github/ISSUE_TEMPLATE/z-bug-report-2.yml
vendored
2
.github/ISSUE_TEMPLATE/z-bug-report-2.yml
vendored
@ -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:
|
||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
8
.github/workflows/ecosystem-ci-trigger.yml
vendored
8
.github/workflows/ecosystem-ci-trigger.yml
vendored
@ -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 }}
|
||||
|
2
.github/workflows/introspect.yml
vendored
2
.github/workflows/introspect.yml
vendored
@ -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=""
|
||||
|
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
@ -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
|
||||
|
8
.github/workflows/semantic-pull-requests.yml
vendored
8
.github/workflows/semantic-pull-requests.yml
vendored
@ -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
12
.website/.gitignore
vendored
Executable 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
35
.website/README.md
Executable 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
14
.website/app.config.ts
Normal 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
20
.website/nuxt.config.ts
Executable 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
14
.website/package.json
Executable 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
3
.website/tsconfig.json
Executable file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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) => {
|
||||
|
@ -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: {
|
||||
|
@ -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}`)
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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}`)
|
||||
|
50
docs/3.api/2.components/8.nuxt-island.md
Normal file
50
docs/3.api/2.components/8.nuxt-island.md
Normal 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.
|
@ -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}`)
|
||||
|
@ -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`
|
||||
|
@ -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)
|
||||
```
|
||||
|
@ -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).
|
||||
::
|
||||
|
@ -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.
|
||||
|
@ -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')
|
||||
|
27
package.json
27
package.json
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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)
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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') || '.'
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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,
|
||||
|
@ -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 }))
|
||||
|
@ -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: {
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}"` : ''
|
||||
}
|
3
packages/nuxt/config.d.ts
vendored
3
packages/nuxt/config.d.ts
vendored
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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}`
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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)) {
|
||||
|
153
packages/nuxt/src/app/components/nuxt-layout.ts
Normal file
153
packages/nuxt/src/app/components/nuxt-layout.ts
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
@ -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' })
|
||||
|
@ -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 "//".')
|
||||
}
|
||||
|
@ -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.')
|
||||
|
@ -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)))
|
||||
|
@ -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()
|
||||
|
@ -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') {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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: [
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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, {
|
||||
|
@ -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) {
|
||||
|
@ -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')
|
||||
|
@ -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') })
|
||||
}
|
||||
|
9
packages/nuxt/src/head/runtime/plugins/capo.ts
Normal file
9
packages/nuxt/src/head/runtime/plugins/capo.ts
Normal 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 }))
|
||||
}
|
||||
})
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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')
|
||||
|
@ -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' {
|
||||
|
@ -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 }
|
||||
}
|
||||
|
||||
|
3
packages/nuxt/types.d.ts
vendored
3
packages/nuxt/types.d.ts
vendored
@ -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
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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>}
|
||||
*/
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
3562
pnpm-lock.yaml
3562
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -2,3 +2,4 @@ packages:
|
||||
- "packages/**"
|
||||
- "playground"
|
||||
- "test/fixtures/*"
|
||||
- ".website"
|
||||
|
@ -25,7 +25,6 @@
|
||||
"main"
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"typescript",
|
||||
"markdownlint-cli",
|
||||
"nuxt",
|
||||
"nuxt3",
|
||||
|
@ -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)) {
|
||||
|
@ -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=\\"[{"count":3}]\\"></div><p>hello world !!!</p><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"hello\\" nuxt-ssr-slot-data=\\"[{"t":0},{"t":1},{"t":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=\\"[{"t":"fall"},{"t":"back"}]\\"><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=\\"[{"count":3}]\\"></div><p>hello world !!!</p><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"hello\\" nuxt-ssr-slot-data=\\"[{"t":0},{"t":1},{"t":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=\\"[{"t":"fall"},{"t":"back"}]\\"><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": {},
|
||||
}
|
||||
`)
|
||||
|
@ -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'))
|
||||
|
8
test/fixtures/basic-types/nuxt.config.ts
vendored
8
test/fixtures/basic-types/nuxt.config.ts
vendored
@ -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',
|
||||
|
17
test/fixtures/basic-types/types.ts
vendored
17
test/fixtures/basic-types/types.ts
vendored
@ -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', () => {
|
||||
|
1
test/fixtures/basic/components/Tsx.tsx
vendored
1
test/fixtures/basic/components/Tsx.tsx
vendored
@ -3,6 +3,7 @@ export default defineComponent({
|
||||
return <div>
|
||||
TSX component
|
||||
<custom-component>custom</custom-component>
|
||||
<SugarCounter multiplier={2} />
|
||||
</div>
|
||||
}
|
||||
})
|
||||
|
@ -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>
|
||||
|
3
test/fixtures/basic/nuxt.config.ts
vendored
3
test/fixtures/basic/nuxt.config.ts
vendored
@ -187,7 +187,8 @@ export default defineNuxtConfig({
|
||||
componentIslands: true,
|
||||
reactivityTransform: true,
|
||||
treeshakeClientOnly: true,
|
||||
payloadExtraction: true
|
||||
payloadExtraction: true,
|
||||
headCapoPlugin: true
|
||||
},
|
||||
appConfig: {
|
||||
fromNuxtConfig: true,
|
||||
|
26
test/fixtures/basic/pages/server-components/lazy/end.vue
vendored
Normal file
26
test/fixtures/basic/pages/server-components/lazy/end.vue
vendored
Normal 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>
|
10
test/fixtures/basic/pages/server-components/lazy/start.vue
vendored
Normal file
10
test/fixtures/basic/pages/server-components/lazy/start.vue
vendored
Normal 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>
|
12
test/fixtures/basic/plugins/server-only.server.ts
vendored
Normal file
12
test/fixtures/basic/plugins/server-only.server.ts
vendored
Normal 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
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user