mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-30 17:37:14 +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
|
Please use a template below to create a minimal reproduction
|
||||||
👉 https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz
|
👉 https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz
|
||||||
👉 https://codesandbox.io/p/github/nuxt/starter/v3-codesandbox
|
👉 https://codesandbox.io/s/github/nuxt/starter/v3-codesandbox
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: bug-env
|
id: bug-env
|
||||||
attributes:
|
attributes:
|
||||||
|
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
|
Please use a template below to create a minimal reproduction
|
||||||
👉 https://stackblitz.com/github/nuxt/starter/tree/v2
|
👉 https://stackblitz.com/github/nuxt/starter/tree/v2
|
||||||
👉 https://codesandbox.io/p/github/nuxt/starter/v2
|
👉 https://codesandbox.io/s/github/nuxt/starter/v2
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: bug-env
|
id: bug-env
|
||||||
attributes:
|
attributes:
|
||||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -86,7 +86,7 @@ jobs:
|
|||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
|
uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
queries: +security-and-quality
|
queries: +security-and-quality
|
||||||
@ -98,7 +98,7 @@ jobs:
|
|||||||
path: packages
|
path: packages
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
|
uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
|
||||||
with:
|
with:
|
||||||
category: "/language:javascript"
|
category: "/language:javascript"
|
||||||
|
|
||||||
@ -112,7 +112,6 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
module: ['bundler', 'node']
|
module: ['bundler', 'node']
|
||||||
base: ['with-base-url', 'without-base-url']
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||||
@ -135,7 +134,6 @@ jobs:
|
|||||||
run: pnpm test:types
|
run: pnpm test:types
|
||||||
env:
|
env:
|
||||||
MODULE_RESOLUTION: ${{ matrix.module }}
|
MODULE_RESOLUTION: ${{ matrix.module }}
|
||||||
TS_BASE_URL: ${{ matrix.base }}
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
# autofix workflow will be triggered instead for PRs
|
# autofix workflow will be triggered instead for PRs
|
||||||
|
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
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'nuxt/nuxt' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
|
if: github.repository == 'nuxt/nuxt' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const user = context.payload.sender.login
|
const user = context.payload.sender.login
|
||||||
@ -48,7 +48,7 @@ jobs:
|
|||||||
})
|
})
|
||||||
throw new Error('not allowed')
|
throw new Error('not allowed')
|
||||||
}
|
}
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
|
||||||
id: get-pr-data
|
id: get-pr-data
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
@ -64,12 +64,12 @@ jobs:
|
|||||||
repo: pr.head.repo.full_name
|
repo: pr.head.repo.full_name
|
||||||
}
|
}
|
||||||
- id: generate-token
|
- id: generate-token
|
||||||
uses: tibdex/github-app-token@v1
|
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
|
app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }}
|
||||||
private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }}
|
||||||
repository: "${{ github.repository_owner }}/ecosystem-ci"
|
repository: "${{ github.repository_owner }}/ecosystem-ci"
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
|
||||||
id: trigger
|
id: trigger
|
||||||
env:
|
env:
|
||||||
COMMENT: ${{ github.event.comment.body }}
|
COMMENT: ${{ github.event.comment.body }}
|
||||||
|
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
|
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
|
||||||
- name: Check workflow files
|
- name: Check workflow files
|
||||||
run: |
|
run: |
|
||||||
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
|
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/590d3bd9dde0c91f7a66071d40eb84716526e5a6/scripts/download-actionlint.bash)
|
||||||
./actionlint -color -shellcheck=""
|
./actionlint -color -shellcheck=""
|
||||||
|
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.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@1813ca74c3faaa3a2da2070b9b8a0b3e7373a0d8 # v2.21.0
|
uses: github/codeql-action/upload-sarif@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
8
.github/workflows/semantic-pull-requests.yml
vendored
8
.github/workflows/semantic-pull-requests.yml
vendored
@ -7,14 +7,20 @@ on:
|
|||||||
- edited
|
- edited
|
||||||
- synchronize
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
|
permissions:
|
||||||
|
pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs
|
||||||
|
statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
|
||||||
if: github.repository == 'nuxt/nuxt'
|
if: github.repository == 'nuxt/nuxt'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Semantic pull request
|
name: Semantic pull request
|
||||||
steps:
|
steps:
|
||||||
- name: Validate PR title
|
- name: Validate PR title
|
||||||
uses: amannn/action-semantic-pull-request@v5
|
uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 # v5.2.0
|
||||||
with:
|
with:
|
||||||
scopes: |
|
scopes: |
|
||||||
kit
|
kit
|
||||||
|
12
.website/.gitignore
vendored
Executable file
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
|
### Crawl-based Pre-rendering
|
||||||
|
|
||||||
Use the [`nuxi generate` command](/docs/api/commands/generate) to build your application. For every page, Nuxt uses a crawler to generate a corresponding HTML and payload files. The built files will be generated in the `.output/public` directory.
|
Use the [`nuxi generate` command](/docs/api/commands/generate) to build and pre-render your application using the [Nitro](/docs/guide/concepts/server-engine) crawler. This command is similar to `nuxt build` with the `nitro.static` option set to `true`, or running `nuxt build --prerender`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx nuxi generate
|
npx nuxi generate
|
||||||
```
|
```
|
||||||
|
|
||||||
You can enable crawl-based pre-rendering when using `nuxt build` in the `nuxt.config` file:
|
That's it! You can now deploy the `.output/public` directory to any static hosting service or preview it locally with `npx serve .output/public`.
|
||||||
|
|
||||||
```ts [nuxt.config.ts|js]
|
Working of the Nitro crawler:
|
||||||
defineNuxtConfig({
|
|
||||||
nitro: {
|
1. Load the HTML of your application's root route (`/`), any non-dynamic pages in your `~/pages` directory, and any other routes in the `nitro.prerender.routes` array.
|
||||||
prerender: {
|
2. Save the HTML and `payload.json` to the `~/.output/public/` directory to be served statically.
|
||||||
crawlLinks: true
|
3. Find all anchor tags (`<a href="...">`) in the HTML to navigate to other routes.
|
||||||
}
|
4. Repeat steps 1-3 for each anchor tag found until there are no more anchor tags to crawl.
|
||||||
}
|
|
||||||
})
|
This is important to understand since pages that are not linked to a discoverable page can't be pre-rendered automatically.
|
||||||
```
|
|
||||||
|
::alert{type=info}
|
||||||
|
Read more about the [`nuxi generate` command](/docs/api/commands/generate#nuxi-generate).
|
||||||
|
::
|
||||||
|
|
||||||
### Selective Pre-rendering
|
### Selective Pre-rendering
|
||||||
|
|
||||||
You can manually specify routes that [Nitro](/docs/guide/concepts/server-engine) will fetch and pre-render during the build.
|
You can manually specify routes that [Nitro](/docs/guide/concepts/server-engine) will fetch and pre-render during the build or ignore routes that you don't want to pre-render like `/dynamic` in the `nuxt.config` file:
|
||||||
|
|
||||||
```ts [nuxt.config.ts|js]
|
```ts [nuxt.config.ts|js]
|
||||||
defineNuxtConfig({
|
defineNuxtConfig({
|
||||||
nitro: {
|
nitro: {
|
||||||
prerender: {
|
prerender: {
|
||||||
routes: ['/user/1', '/user/2']
|
routes: ['/user/1', '/user/2']
|
||||||
|
ignore: ['/dynamic']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
When using this option with `nuxi build`, static payloads won't be generated by default at build time. For now, selective payload generation is under an experimental flag.
|
You can combine this with the `crawLinks` option to pre-render a set of routes that the crawler can't discover like your `/sitemap.xml` or `/robots.txt`:
|
||||||
|
|
||||||
```ts [nuxt.config.ts|js]
|
```ts [nuxt.config.ts|js]
|
||||||
defineNuxtConfig({
|
defineNuxtConfig({
|
||||||
/* The /dynamic route won't be crawled */
|
|
||||||
nitro: {
|
nitro: {
|
||||||
prerender: { crawlLinks: true, ignore: ['/dynamic'] }
|
prerender: {
|
||||||
},
|
crawlLinks: true,
|
||||||
experimental: {
|
routes: ['/sitemap.xml', '/robots.txt']
|
||||||
payloadExtraction: true
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Setting `nitro.prerender` to `true` is similar to `nitro.prerender.crawlLinks` to `true`.
|
||||||
|
|
||||||
|
::alert{type=info}
|
||||||
|
Read more about [pre-rendering](https://nitro.unjs.io/config#prerender) in the Nitro documentation.
|
||||||
|
::
|
||||||
|
|
||||||
### Client-side Only Rendering
|
### Client-side Only Rendering
|
||||||
|
|
||||||
If you don't want to pre-render your routes, another way of using static hosting is to set the `ssr` property to `false` in the `nuxt.config` file. The `nuxi generate` command will then output an `.output/public/index.html` entrypoint and JavaScript bundles like a classic client-side Vue.js application.
|
If you don't want to pre-render your routes, another way of using static hosting is to set the `ssr` property to `false` in the `nuxt.config` file. The `nuxi generate` command will then output an `.output/public/index.html` entrypoint and JavaScript bundles like a classic client-side Vue.js application.
|
||||||
|
@ -31,7 +31,7 @@ Start with one of our starters and themes directly by opening [nuxt.new](https:/
|
|||||||
::details
|
::details
|
||||||
:summary[Additional notes for an optimal setup:]
|
:summary[Additional notes for an optimal setup:]
|
||||||
- **Node.js**: Make sure to use an even numbered version (18, 20, etc)
|
- **Node.js**: Make sure to use an even numbered version (18, 20, etc)
|
||||||
|
- **Nuxtr**: Install the community-developed [Nuxtr extension](https://marketplace.visualstudio.com/items?itemName=Nuxtr.nuxtr-vscode)
|
||||||
- **Volar**: Either enable [**Take Over Mode**](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) (recommended) or add the [TypeScript Vue Plugin](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin)
|
- **Volar**: Either enable [**Take Over Mode**](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) (recommended) or add the [TypeScript Vue Plugin](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin)
|
||||||
|
|
||||||
If you have enabled **Take Over Mode** or installed the **TypeScript Vue Plugin (Volar)**, you can disable generating the shim for `*.vue` files in your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) file:
|
If you have enabled **Take Over Mode** or installed the **TypeScript Vue Plugin (Volar)**, you can disable generating the shim for `*.vue` files in your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt.config) file:
|
||||||
|
@ -71,7 +71,7 @@ When a `<NuxtLink>` enters the viewport on the client side, Nuxt will automatica
|
|||||||
|
|
||||||
The `useRoute()` composable can be used in a `<script setup>` block or a `setup()` method of a Vue component to access the current route details.
|
The `useRoute()` composable can be used in a `<script setup>` block or a `setup()` method of a Vue component to access the current route details.
|
||||||
|
|
||||||
```vue [pages/posts/[id].vue]
|
```vue [pages/posts/[id\\].vue]
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ The `validate` property accepts the `route` as an argument. You can return a boo
|
|||||||
|
|
||||||
If you have a more complex use case, then you can use anonymous route middleware instead.
|
If you have a more complex use case, then you can use anonymous route middleware instead.
|
||||||
|
|
||||||
```vue [pages/posts/[id].vue]
|
```vue [pages/posts/[id\\].vue]
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
validate: async (route) => {
|
validate: async (route) => {
|
||||||
|
@ -318,7 +318,7 @@ To apply dynamic transitions using conditional logic, you can leverage inline [m
|
|||||||
|
|
||||||
::code-group
|
::code-group
|
||||||
|
|
||||||
```html [pages/[id].vue]
|
```html [pages/[id\\].vue]
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
pageTransition: {
|
pageTransition: {
|
||||||
|
@ -111,7 +111,7 @@ If you throw an error created with `createError`:
|
|||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```vue [pages/movies/[slug].vue]
|
```vue [pages/movies/[slug\\].vue]
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { data } = await useFetch(`/api/movies/${route.params.slug}`)
|
const { data } = await useFetch(`/api/movies/${route.params.slug}`)
|
||||||
|
@ -90,7 +90,7 @@ export default defineNuxtConfig({
|
|||||||
// Homepage pre-rendered at build time
|
// Homepage pre-rendered at build time
|
||||||
'/': { prerender: true },
|
'/': { prerender: true },
|
||||||
// Product page generated on-demand, revalidates in background
|
// Product page generated on-demand, revalidates in background
|
||||||
'/products/**': { swr: true },
|
'/products/**': { swr: 3600 },
|
||||||
// Blog post generated on-demand once until next deploy
|
// Blog post generated on-demand once until next deploy
|
||||||
'/blog/**': { isr: true },
|
'/blog/**': { isr: true },
|
||||||
// Admin dashboard renders only on client-side
|
// Admin dashboard renders only on client-side
|
||||||
@ -108,8 +108,8 @@ The different properties you can use are the following:
|
|||||||
- `ssr: boolean`{lang=ts} - Disables server-side rendering for sections of your app and make them SPA-only with `ssr: false`
|
- `ssr: boolean`{lang=ts} - Disables server-side rendering for sections of your app and make them SPA-only with `ssr: false`
|
||||||
- `cors: boolean`{lang=ts} - Automatically adds cors headers with `cors: true` - you can customize the output by overriding with `headers`
|
- `cors: boolean`{lang=ts} - Automatically adds cors headers with `cors: true` - you can customize the output by overriding with `headers`
|
||||||
- `headers: object`{lang=ts} - Add specific headers to sections of your site - for example, your assets
|
- `headers: object`{lang=ts} - Add specific headers to sections of your site - for example, your assets
|
||||||
- `swr: number`{lang=ts} - Add cache headers to the server response and cache it on the server or reverse proxy for a configurable TTL (time to live). The `node-server` preset of Nitro is able to cache the full response. When the TTL expired, the cached response will be sent while the page will be regenerated in the background.
|
- `swr: number|boolean`{lang=ts} - Add cache headers to the server response and cache it on the server or reverse proxy for a configurable TTL (time to live). The `node-server` preset of Nitro is able to cache the full response. When the TTL expired, the cached response will be sent while the page will be regenerated in the background. If true is used, a `stale-while-revalidate` header is added without a MaxAge.
|
||||||
- `isr: boolean`{lang=ts} - The behavior is the same as `swr` except that we are able to add the response to the CDN cache on platforms that support this (currently Netlify or Vercel)
|
- `isr: number|boolean`{lang=ts} - The behavior is the same as `swr` except that we are able to add the response to the CDN cache on platforms that support this (currently Netlify or Vercel). If `true` is used, the content persists until the next deploy inside the CDN.
|
||||||
- `prerender:boolean`{lang=ts} - Prerenders routes at build time and includes them in your build as static assets
|
- `prerender:boolean`{lang=ts} - Prerenders routes at build time and includes them in your build as static assets
|
||||||
- `experimentalNoScripts: boolean`{lang=ts} - Disables rendering of Nuxt scripts and JS resource hints for sections of your site.
|
- `experimentalNoScripts: boolean`{lang=ts} - Disables rendering of Nuxt scripts and JS resource hints for sections of your site.
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ The module automatically loads and parses them.
|
|||||||
|
|
||||||
To render content pages, add a [catch-all route](/docs/guide/directory-structure/pages/#catch-all-route) using the `ContentDoc` component:
|
To render content pages, add a [catch-all route](/docs/guide/directory-structure/pages/#catch-all-route) using the `ContentDoc` component:
|
||||||
|
|
||||||
```vue [pages/[...slug].vue]
|
```vue [pages/[...slug\\].vue]
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<ContentDoc />
|
<ContentDoc />
|
||||||
|
@ -110,7 +110,7 @@ If you want a parameter to be _optional_, you must enclose it in double square b
|
|||||||
|
|
||||||
Given the example above, you can access group/id within your component via the `$route` object:
|
Given the example above, you can access group/id within your component via the `$route` object:
|
||||||
|
|
||||||
```vue [pages/users-[group]/[id].vue]
|
```vue [pages/users-[group\\]/[id\\].vue]
|
||||||
<template>
|
<template>
|
||||||
<p>{{ $route.params.group }} - {{ $route.params.id }}</p>
|
<p>{{ $route.params.group }} - {{ $route.params.id }}</p>
|
||||||
</template>
|
</template>
|
||||||
@ -138,7 +138,7 @@ if (route.params.group === 'admins' && !route.params.id) {
|
|||||||
|
|
||||||
If you need a catch-all route, you create it by using a file named like `[...slug].vue`. This will match _all_ routes under that path.
|
If you need a catch-all route, you create it by using a file named like `[...slug].vue`. This will match _all_ routes under that path.
|
||||||
|
|
||||||
```vue [pages/[...slug].vue]
|
```vue [pages/[...slug\\].vue]
|
||||||
<template>
|
<template>
|
||||||
<p>{{ $route.params.slug }}</p>
|
<p>{{ $route.params.slug }}</p>
|
||||||
</template>
|
</template>
|
||||||
|
@ -151,7 +151,7 @@ Server routes can use dynamic parameters within brackets in the file name like `
|
|||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```ts [server/api/hello/[name].ts]
|
```ts [server/api/hello/[name\\].ts]
|
||||||
export default defineEventHandler((event) => `Hello, ${event.context.params.name}!`)
|
export default defineEventHandler((event) => `Hello, ${event.context.params.name}!`)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -181,11 +181,11 @@ Catch-all routes are helpful for fallback route handling. For example, creating
|
|||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
```ts [server/api/foo/[...].ts]
|
```ts [server/api/foo/[...\\].ts]
|
||||||
export default defineEventHandler(() => `Default foo handler`)
|
export default defineEventHandler(() => `Default foo handler`)
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts [server/api/[...].ts]
|
```ts [server/api/[...\\].ts]
|
||||||
export default defineEventHandler(() => `Default api handler`)
|
export default defineEventHandler(() => `Default api handler`)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -221,7 +221,7 @@ If no errors are thrown, a status code of `200 OK` will be returned. Any uncaugh
|
|||||||
|
|
||||||
To return other error codes, throw an exception with `createError`
|
To return other error codes, throw an exception with `createError`
|
||||||
|
|
||||||
```ts [server/api/validation/[id].ts]
|
```ts [server/api/validation/[id\\].ts]
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler((event) => {
|
||||||
const id = parseInt(event.context.params.id) as number
|
const id = parseInt(event.context.params.id) as number
|
||||||
if (!Number.isInteger(id)) {
|
if (!Number.isInteger(id)) {
|
||||||
@ -240,7 +240,7 @@ To return other status codes, you can use the `setResponseStatus` utility.
|
|||||||
|
|
||||||
For example, to return `202 Accepted`
|
For example, to return `202 Accepted`
|
||||||
|
|
||||||
```ts [server/api/validation/[id].ts]
|
```ts [server/api/validation/[id\\].ts]
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler((event) => {
|
||||||
setResponseStatus(event, 202)
|
setResponseStatus(event, 202)
|
||||||
})
|
})
|
||||||
@ -284,7 +284,7 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
### Using a Nested Router
|
### Using a Nested Router
|
||||||
|
|
||||||
```ts [server/api/hello/[...slug].ts]
|
```ts [server/api/hello/[...slug\\].ts]
|
||||||
import { createRouter, defineEventHandler, useBase } from 'h3'
|
import { createRouter, defineEventHandler, useBase } from 'h3'
|
||||||
|
|
||||||
const router = createRouter()
|
const router = createRouter()
|
||||||
|
@ -7,6 +7,8 @@ head.title: ".env"
|
|||||||
|
|
||||||
# .env File
|
# .env File
|
||||||
|
|
||||||
|
## At Build, Dev, and Generate Time
|
||||||
|
|
||||||
Nuxt CLI has built-in [dotenv](https://github.com/motdotla/dotenv) support in development mode and when running `nuxi build` and `nuxi generate`.
|
Nuxt CLI has built-in [dotenv](https://github.com/motdotla/dotenv) support in development mode and when running `nuxi build` and `nuxi generate`.
|
||||||
|
|
||||||
In addition to any process environment variables, if you have a `.env` file in your project root directory, it will be automatically loaded **at build, dev, and generate time**, and any environment variables set there will be accessible within your `nuxt.config` file and modules.
|
In addition to any process environment variables, if you have a `.env` file in your project root directory, it will be automatically loaded **at build, dev, and generate time**, and any environment variables set there will be accessible within your `nuxt.config` file and modules.
|
||||||
@ -27,7 +29,17 @@ When updating `.env` in development mode, the Nuxt instance is automatically res
|
|||||||
Note that removing a variable from `.env` or removing the `.env` file entirely will not unset values that have already been set.
|
Note that removing a variable from `.env` or removing the `.env` file entirely will not unset values that have already been set.
|
||||||
::
|
::
|
||||||
|
|
||||||
However, **after your server is built**, you are responsible for setting environment variables when you run the server. Your `.env` file will not be read at this point. How you do this is different for every environment. On a Linux server, you could pass the environment variables as arguments using the terminal `DATABASE_HOST=mydatabaseconnectionstring node .output/server/index.mjs`. Or you could source your env file using `source .env && node .output/server/index.mjs`.
|
## Production Preview
|
||||||
|
|
||||||
|
**After your server is built**, you are responsible for setting environment variables when you run the server. Your `.env` file will not be read at this point. How you do this is different for every environment.
|
||||||
|
|
||||||
|
For local production preview purpose, we recommend using [`nuxi preview`](https://nuxt.com/docs/api/commands/preview) since using this command, the `.env` file will be loaded into `process.env` for convenience. Note that this command requires dependencies to be installed in the package directory.
|
||||||
|
|
||||||
|
Or you could pass the environment variables as arguments using the terminal. For example, on Linux or macOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_HOST=mydatabaseconnectionstring node .output/server/index.mjs
|
||||||
|
```
|
||||||
|
|
||||||
Note that for a purely static site, it is not possible to set runtime configuration config after your project is prerendered.
|
Note that for a purely static site, it is not possible to set runtime configuration config after your project is prerendered.
|
||||||
|
|
||||||
|
@ -150,9 +150,9 @@ It is also possible to type your runtime config manually:
|
|||||||
declare module 'nuxt/schema' {
|
declare module 'nuxt/schema' {
|
||||||
interface RuntimeConfig {
|
interface RuntimeConfig {
|
||||||
apiSecret: string
|
apiSecret: string
|
||||||
public: {
|
}
|
||||||
apiBase: string
|
interface PublicRuntimeConfig {
|
||||||
}
|
apiBase: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// It is always important to ensure you import/export something when augmenting a type
|
// It is always important to ensure you import/export something when augmenting a type
|
||||||
|
@ -42,6 +42,7 @@ You may need to update the config below with a path to your web browser. For mor
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "server: nuxt",
|
"name": "server: nuxt",
|
||||||
|
"outputCapture": "std",
|
||||||
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
|
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
|
||||||
"args": [
|
"args": [
|
||||||
"dev"
|
"dev"
|
||||||
@ -60,6 +61,12 @@ You may need to update the config below with a path to your web browser. For mor
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you prefer your usual browser extensions, add this to the _chrome_ configuration above:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
"userDataDir": false,
|
||||||
|
```
|
||||||
|
|
||||||
### Example JetBrains IDEs Debug Configuration
|
### Example JetBrains IDEs Debug Configuration
|
||||||
|
|
||||||
You can also debug your Nuxt app in JetBrains IDEs such as IntelliJ IDEA, WebStorm, or PhpStorm.
|
You can also debug your Nuxt app in JetBrains IDEs such as IntelliJ IDEA, WebStorm, or PhpStorm.
|
||||||
|
@ -13,7 +13,7 @@ Within the template of a Vue component, you can access the route using `$route`.
|
|||||||
|
|
||||||
In the following example, we call an API via [`useFetch`](/docs/api/composables/use-fetch) using a dynamic page parameter - `slug` - as part of the URL.
|
In the following example, we call an API via [`useFetch`](/docs/api/composables/use-fetch) using a dynamic page parameter - `slug` - as part of the URL.
|
||||||
|
|
||||||
```html [~/pages/[slug].vue]
|
```html [~/pages/[slug\\].vue]
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { data: mountain } = await useFetch(`https://api.nuxtjs.dev/mountains/${route.params.slug}`)
|
const { data: mountain } = await useFetch(`https://api.nuxtjs.dev/mountains/${route.params.slug}`)
|
||||||
|
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
|
### Example
|
||||||
|
|
||||||
```vue [pages/movies/[slug].vue]
|
```vue [pages/movies/[slug\\].vue]
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { data } = await useFetch(`/api/movies/${route.params.slug}`)
|
const { data } = await useFetch(`/api/movies/${route.params.slug}`)
|
||||||
|
@ -32,6 +32,7 @@ interface PageMeta {
|
|||||||
keepalive?: boolean | KeepAliveProps
|
keepalive?: boolean | KeepAliveProps
|
||||||
layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>
|
layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>
|
||||||
middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>
|
middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>
|
||||||
|
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -98,6 +99,12 @@ interface PageMeta {
|
|||||||
|
|
||||||
Validate whether a given route can validly be rendered with this page. Return true if it is valid, or false if not. If another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked).
|
Validate whether a given route can validly be rendered with this page. Return true if it is valid, or false if not. If another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked).
|
||||||
|
|
||||||
|
**`scrollTopTop`**
|
||||||
|
|
||||||
|
- **Type**: `boolean | (to: RouteLocationNormalized, from: RouteLocationNormalized) => boolean`
|
||||||
|
|
||||||
|
Tell Nuxt to scroll to the top before rendering the page or not. If you want to overwrite the default scroll behavior of Nuxt, you can do so in `~/app/router.options.ts` (see [docs](/docs/guide/directory-structure/pages/#router-options)) for more info.
|
||||||
|
|
||||||
**`[key: string]`**
|
**`[key: string]`**
|
||||||
|
|
||||||
- **Type**: `any`
|
- **Type**: `any`
|
||||||
|
@ -112,3 +112,42 @@ description: Nuxt Kit provides composable utilities to help interacting with Nux
|
|||||||
- `extendViteConfig(callback, options?)`
|
- `extendViteConfig(callback, options?)`
|
||||||
- `addWebpackPlugin(webpackPlugin, options?)`
|
- `addWebpackPlugin(webpackPlugin, options?)`
|
||||||
- `addVitePlugin(vitePlugin, options?)`
|
- `addVitePlugin(vitePlugin, options?)`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Accessing Nuxt Vite Config
|
||||||
|
|
||||||
|
If you are building an integration that needs access to the runtime Vite or webpack config that Nuxt uses, it is possible to extract this using Kit utilities.
|
||||||
|
|
||||||
|
Some examples of projects doing this already:
|
||||||
|
|
||||||
|
- [histoire](https://github.com/histoire-dev/histoire/blob/main/packages/histoire-plugin-nuxt/src/index.ts)
|
||||||
|
- [nuxt-vitest](https://github.com/danielroe/nuxt-vitest/blob/main/packages/nuxt-vitest/src/config.ts)
|
||||||
|
- [@storybook-vue/nuxt](https://github.com/storybook-vue/nuxt/blob/main/src/preset.ts)
|
||||||
|
|
||||||
|
Here is a brief example of how you might access the Vite config from a project; you could implement a similar approach to get the webpack configuration.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { loadNuxt, buildNuxt } from '@nuxt/kit'
|
||||||
|
|
||||||
|
// https://github.com/nuxt/nuxt/issues/14534
|
||||||
|
async function getViteConfig() {
|
||||||
|
const nuxt = await loadNuxt({ cwd: process.cwd(), dev: false, overrides: { ssr: false } })
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
|
||||||
|
if (isClient) {
|
||||||
|
resolve(config)
|
||||||
|
throw new Error('_stop_')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
buildNuxt(nuxt).catch((err) => {
|
||||||
|
if (!err.toString().includes('_stop_')) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).finally(() => nuxt.close())
|
||||||
|
}
|
||||||
|
|
||||||
|
const viteConfig = await getViteConfig()
|
||||||
|
console.log(viteConfig)
|
||||||
|
```
|
||||||
|
@ -15,3 +15,7 @@ Option | Default | Description
|
|||||||
-------------------------|-----------------|------------------
|
-------------------------|-----------------|------------------
|
||||||
`rootDir` | `.` | The root directory of the application to generate
|
`rootDir` | `.` | The root directory of the application to generate
|
||||||
`--dotenv` | `.` | Point to another `.env` file to load, **relative** to the root directory.
|
`--dotenv` | `.` | Point to another `.env` file to load, **relative** to the root directory.
|
||||||
|
|
||||||
|
::alert{type=info}
|
||||||
|
Read more about [pre-rendering and static hosting](/docs/1.getting-started/10.deployment.md#static-hosting).
|
||||||
|
::
|
||||||
|
@ -33,16 +33,16 @@ If your issue concerns Vue 3 or Vite, please try to reproduce it first with the
|
|||||||
**Nuxt 3**:
|
**Nuxt 3**:
|
||||||
|
|
||||||
:button-link[Nuxt 3 on StackBlitz]{href="https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz" blank .mr-2}
|
:button-link[Nuxt 3 on StackBlitz]{href="https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz" blank .mr-2}
|
||||||
:button-link[Nuxt 3 on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt/starter/v3-codesandbox" blank}
|
:button-link[Nuxt 3 on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt/starter/v3-codesandbox" blank}
|
||||||
|
|
||||||
**Nuxt Bridge**:
|
**Nuxt Bridge**:
|
||||||
|
|
||||||
:button-link[Nuxt Bridge on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt/starter/v2-bridge-codesandbox" blank}
|
:button-link[Nuxt Bridge on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt/starter/v2-bridge-codesandbox" blank}
|
||||||
|
|
||||||
**Vue 3**:
|
**Vue 3**:
|
||||||
|
|
||||||
:button-link[Vue 3 SSR on StackBlitz]{href="https://stackblitz.com/github/nuxt-contrib/vue3-ssr-starter/tree/main?terminal=dev" blank .mr-2}
|
:button-link[Vue 3 SSR on StackBlitz]{href="https://stackblitz.com/github/nuxt-contrib/vue3-ssr-starter/tree/main?terminal=dev" blank .mr-2}
|
||||||
:button-link[Vue 3 SSR on CodeSandbox]{href="https://codesandbox.io/p/github/nuxt-contrib/vue3-ssr-starter/main" blank .mr-2}
|
:button-link[Vue 3 SSR on CodeSandbox]{href="https://codesandbox.io/s/github/nuxt-contrib/vue3-ssr-starter/main" blank .mr-2}
|
||||||
:button-link[Vue 3 SSR Template]{href="https://github.com/nuxt-contrib/vue3-ssr-starter/generate" blank}
|
:button-link[Vue 3 SSR Template]{href="https://github.com/nuxt-contrib/vue3-ssr-starter/generate" blank}
|
||||||
|
|
||||||
Once you've reproduced the issue, remove as much code from your reproduction as you can (while still recreating the bug). The time spent making the reproduction as minimal as possible will make a huge difference to whoever sets out to fix the issue.
|
Once you've reproduced the issue, remove as much code from your reproduction as you can (while still recreating the bug). The time spent making the reproduction as minimal as possible will make a huge difference to whoever sets out to fix the issue.
|
||||||
|
@ -114,7 +114,7 @@ See [layout migration](/docs/migration/pages-and-layouts).
|
|||||||
|
|
||||||
The validate hook in Nuxt 3 only accepts a single argument, the `route`. Just as in Nuxt 2, you can return a boolean value. If you return false and another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked).
|
The validate hook in Nuxt 3 only accepts a single argument, the `route`. Just as in Nuxt 2, you can return a boolean value. If you return false and another match can't be found, this will mean a 404. You can also directly return an object with `statusCode`/`statusMessage` to respond immediately with an error (other matches will not be checked).
|
||||||
|
|
||||||
```diff [pages/users/[id].vue]
|
```diff [pages/users/[id\\].vue]
|
||||||
- <script>
|
- <script>
|
||||||
- export default {
|
- export default {
|
||||||
- async validate({ params }) {
|
- async validate({ params }) {
|
||||||
@ -135,7 +135,7 @@ The validate hook in Nuxt 3 only accepts a single argument, the `route`. Just as
|
|||||||
|
|
||||||
This is not supported in Nuxt 3. Instead, you can directly use a watcher to trigger refetching data.
|
This is not supported in Nuxt 3. Instead, you can directly use a watcher to trigger refetching data.
|
||||||
|
|
||||||
```vue [pages/users/[id].vue]
|
```vue [pages/users/[id\\].vue]
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { data, refresh } = await useFetch('/api/user')
|
const { data, refresh } = await useFetch('/api/user')
|
||||||
|
27
package.json
27
package.json
@ -14,6 +14,7 @@
|
|||||||
"lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md",
|
"lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md",
|
||||||
"lint:docs:fix": "markdownlint ./docs --fix && case-police 'docs/**/*.md' *.md --fix",
|
"lint:docs:fix": "markdownlint ./docs --fix && case-police 'docs/**/*.md' *.md --fix",
|
||||||
"lint:knip": "pnpx knip",
|
"lint:knip": "pnpx knip",
|
||||||
|
"docs:dev": "nuxi dev .website",
|
||||||
"play": "nuxi dev playground",
|
"play": "nuxi dev playground",
|
||||||
"play:build": "nuxi build playground",
|
"play:build": "nuxi build playground",
|
||||||
"play:preview": "nuxi preview playground",
|
"play:preview": "nuxi preview playground",
|
||||||
@ -35,9 +36,9 @@
|
|||||||
"@nuxt/webpack-builder": "workspace:*",
|
"@nuxt/webpack-builder": "workspace:*",
|
||||||
"nuxi": "workspace:*",
|
"nuxi": "workspace:*",
|
||||||
"nuxt": "workspace:*",
|
"nuxt": "workspace:*",
|
||||||
"vite": "4.4.7",
|
"vite": "4.4.8",
|
||||||
"vue": "3.3.4",
|
"vue": "3.3.4",
|
||||||
"magic-string": "^0.30.1"
|
"magic-string": "^0.30.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/core": "1.10.0",
|
"@actions/core": "1.10.0",
|
||||||
@ -45,7 +46,7 @@
|
|||||||
"@nuxt/webpack-builder": "workspace:*",
|
"@nuxt/webpack-builder": "workspace:*",
|
||||||
"@nuxtjs/eslint-config-typescript": "12.0.0",
|
"@nuxtjs/eslint-config-typescript": "12.0.0",
|
||||||
"@types/fs-extra": "11.0.1",
|
"@types/fs-extra": "11.0.1",
|
||||||
"@types/node": "18.17.0",
|
"@types/node": "18.17.1",
|
||||||
"@types/semver": "7.5.0",
|
"@types/semver": "7.5.0",
|
||||||
"case-police": "0.6.1",
|
"case-police": "0.6.1",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
@ -53,15 +54,15 @@
|
|||||||
"cheerio": "1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"consola": "3.2.3",
|
"consola": "3.2.3",
|
||||||
"devalue": "4.3.2",
|
"devalue": "4.3.2",
|
||||||
"eslint": "8.45.0",
|
"eslint": "8.46.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.28.0",
|
||||||
"eslint-plugin-jsdoc": "41.1.2",
|
"eslint-plugin-jsdoc": "41.1.2",
|
||||||
"eslint-plugin-no-only-tests": "3.1.0",
|
"eslint-plugin-no-only-tests": "3.1.0",
|
||||||
"execa": "7.1.1",
|
"execa": "7.2.0",
|
||||||
"fs-extra": "11.1.1",
|
"fs-extra": "11.1.1",
|
||||||
"globby": "13.2.2",
|
"globby": "13.2.2",
|
||||||
"h3": "1.7.1",
|
"h3": "1.7.1",
|
||||||
"happy-dom": "10.5.2",
|
"happy-dom": "10.7.0",
|
||||||
"jiti": "1.19.1",
|
"jiti": "1.19.1",
|
||||||
"markdownlint-cli": "^0.33.0",
|
"markdownlint-cli": "^0.33.0",
|
||||||
"nitropack": "2.5.2",
|
"nitropack": "2.5.2",
|
||||||
@ -70,21 +71,21 @@
|
|||||||
"nuxt-vitest": "0.10.2",
|
"nuxt-vitest": "0.10.2",
|
||||||
"ofetch": "1.1.1",
|
"ofetch": "1.1.1",
|
||||||
"pathe": "1.1.1",
|
"pathe": "1.1.1",
|
||||||
"playwright-core": "1.36.1",
|
"playwright-core": "1.36.2",
|
||||||
"rimraf": "5.0.1",
|
"rimraf": "5.0.1",
|
||||||
"semver": "7.5.4",
|
"semver": "7.5.4",
|
||||||
"std-env": "3.3.3",
|
"std-env": "3.3.3",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.1.6",
|
||||||
"ufo": "1.1.2",
|
"ufo": "1.2.0",
|
||||||
"vite": "4.4.7",
|
"vite": "4.4.8",
|
||||||
"vitest": "0.33.0",
|
"vitest": "0.33.0",
|
||||||
"vitest-environment-nuxt": "0.10.2",
|
"vitest-environment-nuxt": "0.10.2",
|
||||||
"vue": "3.3.4",
|
"vue": "3.3.4",
|
||||||
"vue-eslint-parser": "9.3.1",
|
"vue-eslint-parser": "9.3.1",
|
||||||
"vue-router": "4.2.4",
|
"vue-router": "4.2.4",
|
||||||
"vue-tsc": "1.8.6"
|
"vue-tsc": "1.8.8"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.6.10",
|
"packageManager": "pnpm@8.6.11",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.10.0"
|
"node": "^14.18.0 || >=16.10.0"
|
||||||
}
|
}
|
||||||
|
@ -34,9 +34,10 @@
|
|||||||
"pkg-types": "^1.0.3",
|
"pkg-types": "^1.0.3",
|
||||||
"scule": "^1.0.0",
|
"scule": "^1.0.0",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
|
"ufo": "^1.2.0",
|
||||||
"unctx": "^2.3.1",
|
"unctx": "^2.3.1",
|
||||||
"unimport": "^3.1.0",
|
"unimport": "^3.1.3",
|
||||||
"untyped": "^1.3.2"
|
"untyped": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/hash-sum": "1.0.0",
|
"@types/hash-sum": "1.0.0",
|
||||||
@ -45,7 +46,7 @@
|
|||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"nitropack": "2.5.2",
|
"nitropack": "2.5.2",
|
||||||
"unbuild": "latest",
|
"unbuild": "latest",
|
||||||
"vite": "4.4.7",
|
"vite": "4.4.8",
|
||||||
"vitest": "0.33.0",
|
"vitest": "0.33.0",
|
||||||
"webpack": "5.88.2"
|
"webpack": "5.88.2"
|
||||||
},
|
},
|
||||||
|
@ -60,15 +60,19 @@ function getRequireCacheItem (id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getModulePaths (paths?: string[] | string) {
|
||||||
|
return ([] as Array<string | undefined>).concat(
|
||||||
|
global.__NUXT_PREPATHS__,
|
||||||
|
paths || [],
|
||||||
|
process.cwd(),
|
||||||
|
global.__NUXT_PATHS__
|
||||||
|
).filter(Boolean) as string[]
|
||||||
|
}
|
||||||
|
|
||||||
/** @deprecated Do not use CJS utils */
|
/** @deprecated Do not use CJS utils */
|
||||||
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
|
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
|
||||||
return normalize(_require.resolve(id, {
|
return normalize(_require.resolve(id, {
|
||||||
paths: ([] as Array<string | undefined>).concat(
|
paths: getModulePaths(opts.paths)
|
||||||
global.__NUXT_PREPATHS__,
|
|
||||||
opts.paths || [],
|
|
||||||
process.cwd(),
|
|
||||||
global.__NUXT_PATHS__
|
|
||||||
).filter(Boolean) as string[]
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork'
|
|||||||
import type { NuxtTemplate } from '@nuxt/schema'
|
import type { NuxtTemplate } from '@nuxt/schema'
|
||||||
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
|
// TODO: Remove support for compiling ejs templates in v4
|
||||||
export async function compileTemplate (template: NuxtTemplate, ctx: any) {
|
export async function compileTemplate (template: NuxtTemplate, ctx: any) {
|
||||||
const data = { ...ctx, options: template.options }
|
const data = { ...ctx, options: template.options }
|
||||||
if (template.src) {
|
if (template.src) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { resolve } from 'pathe'
|
import { resolve } from 'pathe'
|
||||||
|
import type { JSValue } from 'untyped'
|
||||||
import { applyDefaults } from 'untyped'
|
import { applyDefaults } from 'untyped'
|
||||||
import type { LoadConfigOptions } from 'c12'
|
import type { LoadConfigOptions } from 'c12'
|
||||||
import { loadConfig } from 'c12'
|
import { loadConfig } from 'c12'
|
||||||
@ -50,5 +51,5 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve and apply defaults
|
// Resolve and apply defaults
|
||||||
return await applyDefaults(NuxtConfigSchema, nuxtConfig) as NuxtOptions
|
return await applyDefaults(NuxtConfigSchema, nuxtConfig as NuxtConfig & Record<string, JSValue>) as unknown as NuxtOptions
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,22 @@ describe('nuxt module compatibility', () => {
|
|||||||
meta: {
|
meta: {
|
||||||
name: 'nuxt-module-foo'
|
name: 'nuxt-module-foo'
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
[
|
||||||
|
defineNuxtModule({
|
||||||
|
meta: {
|
||||||
|
name: 'module-instance-with-options'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
foo: 'bar'
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(hasNuxtModule('nuxt-module-foo', nuxt)).toStrictEqual(true)
|
expect(hasNuxtModule('nuxt-module-foo', nuxt)).toStrictEqual(true)
|
||||||
|
expect(hasNuxtModule('module-instance-with-options', nuxt)).toStrictEqual(true)
|
||||||
await nuxt.close()
|
await nuxt.close()
|
||||||
})
|
})
|
||||||
it('can retrieve module version from module instance', async () => {
|
it('can retrieve module version from module instance', async () => {
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
import satisfies from 'semver/functions/satisfies.js' // npm/node-semver#381
|
import satisfies from 'semver/functions/satisfies.js' // npm/node-semver#381
|
||||||
import type { Nuxt, NuxtModule } from '@nuxt/schema'
|
import type { Nuxt, NuxtModule, NuxtOptions } from '@nuxt/schema'
|
||||||
import { useNuxt } from '../context'
|
import { useNuxt } from '../context'
|
||||||
import { normalizeSemanticVersion } from '../compatibility'
|
import { normalizeSemanticVersion } from '../compatibility'
|
||||||
import { loadNuxtModuleInstance } from './install'
|
import { loadNuxtModuleInstance } from './install'
|
||||||
|
|
||||||
|
function resolveNuxtModuleEntryName (m: NuxtOptions['modules'][number]): string | false {
|
||||||
|
if (typeof m === 'object' && !Array.isArray(m)) {
|
||||||
|
return (m as any as NuxtModule).name
|
||||||
|
}
|
||||||
|
if (Array.isArray(m)) {
|
||||||
|
return resolveNuxtModuleEntryName(m[0])
|
||||||
|
}
|
||||||
|
return m as string || false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a Nuxt module is installed by name.
|
* Check if a Nuxt module is installed by name.
|
||||||
*
|
*
|
||||||
@ -11,8 +21,10 @@ import { loadNuxtModuleInstance } from './install'
|
|||||||
* that it cannot detect if a module is _going to be_ installed programmatically by another module.
|
* that it cannot detect if a module is _going to be_ installed programmatically by another module.
|
||||||
*/
|
*/
|
||||||
export function hasNuxtModule (moduleName: string, nuxt: Nuxt = useNuxt()) : boolean {
|
export function hasNuxtModule (moduleName: string, nuxt: Nuxt = useNuxt()) : boolean {
|
||||||
|
// check installed modules
|
||||||
return nuxt.options._installedModules.some(({ meta }) => meta.name === moduleName) ||
|
return nuxt.options._installedModules.some(({ meta }) => meta.name === moduleName) ||
|
||||||
nuxt.options.modules.includes(moduleName)
|
// check modules to be installed
|
||||||
|
nuxt.options.modules.some(m => moduleName === resolveNuxtModuleEntryName(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,7 +58,7 @@ export async function getNuxtModuleVersion (module: string | NuxtModule, nuxt: N
|
|||||||
return version
|
return version
|
||||||
}
|
}
|
||||||
// it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance
|
// it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance
|
||||||
if (nuxt.options.modules.includes(moduleMeta.name)) {
|
if (hasNuxtModule(moduleMeta.name)) {
|
||||||
const { buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleMeta.name, nuxt)
|
const { buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleMeta.name, nuxt)
|
||||||
return buildTimeModuleMeta.version || false
|
return buildTimeModuleMeta.version || false
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
import { existsSync } from 'node:fs'
|
import { existsSync, promises as fsp } from 'node:fs'
|
||||||
import { basename, parse, resolve } from 'pathe'
|
import { basename, isAbsolute, join, parse, relative, resolve } from 'pathe'
|
||||||
import hash from 'hash-sum'
|
import hash from 'hash-sum'
|
||||||
import type { NuxtTemplate, ResolvedNuxtTemplate } from '@nuxt/schema'
|
import type { Nuxt, NuxtTemplate, ResolvedNuxtTemplate, TSReference } from '@nuxt/schema'
|
||||||
|
import { withTrailingSlash } from 'ufo'
|
||||||
|
import { defu } from 'defu'
|
||||||
|
import type { TSConfig } from 'pkg-types'
|
||||||
|
import { readPackageJSON } from 'pkg-types'
|
||||||
|
|
||||||
|
import { tryResolveModule } from './internal/esm'
|
||||||
import { tryUseNuxt, useNuxt } from './context'
|
import { tryUseNuxt, useNuxt } from './context'
|
||||||
|
import { getModulePaths } from './internal/cjs'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders given template using lodash template during build into the project buildDir
|
* Renders given template using lodash template during build into the project buildDir
|
||||||
@ -101,3 +108,162 @@ export function normalizeTemplate (template: NuxtTemplate<any> | string): Resolv
|
|||||||
export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate<any>) => boolean }) {
|
export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate<any>) => boolean }) {
|
||||||
return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options)
|
return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options)
|
||||||
}
|
}
|
||||||
|
export async function writeTypes (nuxt: Nuxt) {
|
||||||
|
const modulePaths = getModulePaths(nuxt.options.modulesDir)
|
||||||
|
|
||||||
|
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
|
||||||
|
|
||||||
|
const tsConfig: TSConfig = defu(nuxt.options.typescript?.tsConfig, {
|
||||||
|
compilerOptions: {
|
||||||
|
forceConsistentCasingInFileNames: true,
|
||||||
|
jsx: 'preserve',
|
||||||
|
target: 'ESNext',
|
||||||
|
module: 'ESNext',
|
||||||
|
moduleResolution: nuxt.options.experimental?.typescriptBundlerResolution ? 'Bundler' : 'Node',
|
||||||
|
skipLibCheck: true,
|
||||||
|
isolatedModules: true,
|
||||||
|
useDefineForClassFields: true,
|
||||||
|
strict: nuxt.options.typescript?.strict ?? true,
|
||||||
|
allowJs: true,
|
||||||
|
noEmit: true,
|
||||||
|
resolveJsonModule: true,
|
||||||
|
allowSyntheticDefaultImports: true,
|
||||||
|
types: ['node'],
|
||||||
|
paths: {}
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
'./nuxt.d.ts',
|
||||||
|
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'),
|
||||||
|
...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [],
|
||||||
|
...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd)
|
||||||
|
.filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules'))
|
||||||
|
.map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')),
|
||||||
|
...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : []
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
...nuxt.options.modulesDir.map(m => relativeWithDot(nuxt.options.buildDir, m)),
|
||||||
|
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
|
||||||
|
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist'))
|
||||||
|
]
|
||||||
|
} satisfies TSConfig)
|
||||||
|
|
||||||
|
const aliases: Record<string, string> = {
|
||||||
|
...nuxt.options.alias,
|
||||||
|
'#build': nuxt.options.buildDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude bridge alias types to support Volar
|
||||||
|
const excludedAlias = [/^@vue\/.*$/]
|
||||||
|
|
||||||
|
const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir
|
||||||
|
|
||||||
|
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
|
||||||
|
tsConfig.include = tsConfig.include || []
|
||||||
|
|
||||||
|
for (const alias in aliases) {
|
||||||
|
if (excludedAlias.some(re => re.test(alias))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let absolutePath = resolve(basePath, aliases[alias])
|
||||||
|
let stats = await fsp.stat(absolutePath).catch(() => null /* file does not exist */)
|
||||||
|
if (!stats) {
|
||||||
|
const resolvedModule = await tryResolveModule(aliases[alias], nuxt.options.modulesDir)
|
||||||
|
if (resolvedModule) {
|
||||||
|
absolutePath = resolvedModule
|
||||||
|
stats = await fsp.stat(resolvedModule).catch(() => null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = relativeWithDot(nuxt.options.buildDir, absolutePath)
|
||||||
|
if (stats?.isDirectory()) {
|
||||||
|
tsConfig.compilerOptions.paths[alias] = [relativePath]
|
||||||
|
tsConfig.compilerOptions.paths[`${alias}/*`] = [`${relativePath}/*`]
|
||||||
|
|
||||||
|
if (!absolutePath.startsWith(rootDirWithSlash)) {
|
||||||
|
tsConfig.include.push(relativePath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const path = stats?.isFile()
|
||||||
|
// remove extension
|
||||||
|
? relativePath.replace(/(?<=\w)\.\w+$/g, '')
|
||||||
|
// non-existent file probably shouldn't be resolved
|
||||||
|
: aliases[alias]
|
||||||
|
|
||||||
|
tsConfig.compilerOptions.paths[alias] = [path]
|
||||||
|
|
||||||
|
if (!absolutePath.startsWith(rootDirWithSlash)) {
|
||||||
|
tsConfig.include.push(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const references: TSReference[] = await Promise.all([
|
||||||
|
...nuxt.options.modules,
|
||||||
|
...nuxt.options._modules
|
||||||
|
]
|
||||||
|
.filter(f => typeof f === 'string')
|
||||||
|
.map(async id => ({ types: (await readPackageJSON(id, { url: modulePaths }).catch(() => null))?.name || id })))
|
||||||
|
|
||||||
|
if (nuxt.options.experimental?.reactivityTransform) {
|
||||||
|
references.push({ types: 'vue/macros-global' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const declarations: string[] = []
|
||||||
|
|
||||||
|
await nuxt.callHook('prepare:types', { references, declarations, tsConfig })
|
||||||
|
|
||||||
|
for (const alias in tsConfig.compilerOptions!.paths) {
|
||||||
|
const paths = tsConfig.compilerOptions!.paths[alias]
|
||||||
|
tsConfig.compilerOptions!.paths[alias] = await Promise.all(paths.map(async (path: string) => {
|
||||||
|
if (!isAbsolute(path)) { return path }
|
||||||
|
const stats = await fsp.stat(path).catch(() => null /* file does not exist */)
|
||||||
|
return relativeWithDot(nuxt.options.buildDir, stats?.isFile() ? path.replace(/(?<=\w)\.\w+$/g, '') /* remove extension */ : path)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
tsConfig.include = [...new Set(tsConfig.include.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
|
||||||
|
tsConfig.exclude = [...new Set(tsConfig.exclude!.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
|
||||||
|
|
||||||
|
const declaration = [
|
||||||
|
...references.map((ref) => {
|
||||||
|
if ('path' in ref && isAbsolute(ref.path)) {
|
||||||
|
ref.path = relative(nuxt.options.buildDir, ref.path)
|
||||||
|
}
|
||||||
|
return `/// <reference ${renderAttrs(ref)} />`
|
||||||
|
}),
|
||||||
|
...declarations,
|
||||||
|
'',
|
||||||
|
'export {}',
|
||||||
|
''
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
async function writeFile () {
|
||||||
|
const GeneratedBy = '// Generated by nuxi'
|
||||||
|
|
||||||
|
const tsConfigPath = resolve(nuxt.options.buildDir, 'tsconfig.json')
|
||||||
|
await fsp.mkdir(nuxt.options.buildDir, { recursive: true })
|
||||||
|
await fsp.writeFile(tsConfigPath, GeneratedBy + '\n' + JSON.stringify(tsConfig, null, 2))
|
||||||
|
|
||||||
|
const declarationPath = resolve(nuxt.options.buildDir, 'nuxt.d.ts')
|
||||||
|
await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is needed for Nuxt 2 which clears the build directory again before building
|
||||||
|
// https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144
|
||||||
|
// @ts-expect-error TODO: Nuxt 2 hook
|
||||||
|
nuxt.hook('builder:prepared', writeFile)
|
||||||
|
|
||||||
|
await writeFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAttrs (obj: Record<string, string>) {
|
||||||
|
return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAttr (key: string, value: string) {
|
||||||
|
return value ? `${key}="${value}"` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function relativeWithDot (from: string, to: string) {
|
||||||
|
return relative(from, to).replace(/^([^.])/, './$1') || '.'
|
||||||
|
}
|
||||||
|
@ -32,13 +32,13 @@
|
|||||||
"consola": "3.2.3",
|
"consola": "3.2.3",
|
||||||
"deep-object-diff": "1.1.9",
|
"deep-object-diff": "1.1.9",
|
||||||
"defu": "6.1.2",
|
"defu": "6.1.2",
|
||||||
"destr": "2.0.0",
|
"destr": "2.0.1",
|
||||||
"execa": "7.1.1",
|
"execa": "7.2.0",
|
||||||
"flat": "5.0.2",
|
"flat": "5.0.2",
|
||||||
"giget": "1.1.2",
|
"giget": "1.1.2",
|
||||||
"h3": "1.7.1",
|
"h3": "1.7.1",
|
||||||
"jiti": "1.19.1",
|
"jiti": "1.19.1",
|
||||||
"listhen": "1.1.2",
|
"listhen": "1.2.2",
|
||||||
"mlly": "1.4.0",
|
"mlly": "1.4.0",
|
||||||
"mri": "1.2.0",
|
"mri": "1.2.0",
|
||||||
"ohash": "1.1.2",
|
"ohash": "1.1.2",
|
||||||
@ -47,7 +47,7 @@
|
|||||||
"pkg-types": "1.0.3",
|
"pkg-types": "1.0.3",
|
||||||
"scule": "1.0.0",
|
"scule": "1.0.0",
|
||||||
"semver": "7.5.4",
|
"semver": "7.5.4",
|
||||||
"ufo": "1.1.2",
|
"ufo": "1.2.0",
|
||||||
"unbuild": "latest"
|
"unbuild": "latest"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { relative, resolve } from 'pathe'
|
import { relative, resolve } from 'pathe'
|
||||||
import { consola } from 'consola'
|
import { consola } from 'consola'
|
||||||
import { writeTypes } from '../utils/prepare'
|
|
||||||
|
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
|
||||||
|
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
|
||||||
import { loadKit } from '../utils/kit'
|
import { loadKit } from '../utils/kit'
|
||||||
import { clearBuildDir } from '../utils/fs'
|
import { clearBuildDir } from '../utils/fs'
|
||||||
import { overrideEnv } from '../utils/env'
|
import { overrideEnv } from '../utils/env'
|
||||||
@ -19,7 +21,7 @@ export default defineNuxtCommand({
|
|||||||
const rootDir = resolve(args._[0] || '.')
|
const rootDir = resolve(args._[0] || '.')
|
||||||
showVersions(rootDir)
|
showVersions(rootDir)
|
||||||
|
|
||||||
const { loadNuxt, buildNuxt, useNitro } = await loadKit(rootDir)
|
const { loadNuxt, buildNuxt, useNitro, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
|
||||||
|
|
||||||
const nuxt = await loadNuxt({
|
const nuxt = await loadNuxt({
|
||||||
rootDir,
|
rootDir,
|
||||||
|
@ -7,8 +7,10 @@ import type { Nuxt } from '@nuxt/schema'
|
|||||||
import { consola } from 'consola'
|
import { consola } from 'consola'
|
||||||
import { withTrailingSlash } from 'ufo'
|
import { withTrailingSlash } from 'ufo'
|
||||||
import { setupDotenv } from 'c12'
|
import { setupDotenv } from 'c12'
|
||||||
|
|
||||||
|
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
|
||||||
|
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
|
||||||
import { showBanner, showVersions } from '../utils/banner'
|
import { showBanner, showVersions } from '../utils/banner'
|
||||||
import { writeTypes } from '../utils/prepare'
|
|
||||||
import { loadKit } from '../utils/kit'
|
import { loadKit } from '../utils/kit'
|
||||||
import { importModule } from '../utils/esm'
|
import { importModule } from '../utils/esm'
|
||||||
import { overrideEnv } from '../utils/env'
|
import { overrideEnv } from '../utils/env'
|
||||||
@ -30,7 +32,7 @@ export default defineNuxtCommand({
|
|||||||
|
|
||||||
await setupDotenv({ cwd: rootDir, fileName: args.dotenv })
|
await setupDotenv({ cwd: rootDir, fileName: args.dotenv })
|
||||||
|
|
||||||
const { loadNuxt, loadNuxtConfig, buildNuxt } = await loadKit(rootDir)
|
const { loadNuxt, loadNuxtConfig, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
|
||||||
|
|
||||||
const config = await loadNuxtConfig({
|
const config = await loadNuxtConfig({
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
@ -46,7 +48,7 @@ export default defineNuxtCommand({
|
|||||||
let currentHandler: RequestListener | undefined
|
let currentHandler: RequestListener | undefined
|
||||||
let loadingMessage = 'Nuxt is starting...'
|
let loadingMessage = 'Nuxt is starting...'
|
||||||
const loadingHandler: RequestListener = async (_req, res) => {
|
const loadingHandler: RequestListener = async (_req, res) => {
|
||||||
const { loading: loadingTemplate } = await importModule('@nuxt/ui-templates', config.modulesDir)
|
const loadingTemplate = config.devServer.loadingTemplate ?? await importModule('@nuxt/ui-templates', config.modulesDir).then(r => r.loading)
|
||||||
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
|
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
|
||||||
res.statusCode = 503 // Service Unavailable
|
res.statusCode = 503 // Service Unavailable
|
||||||
res.end(loadingTemplate({ loading: loadingMessage }))
|
res.end(loadingTemplate({ loading: loadingMessage }))
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { relative, resolve } from 'pathe'
|
import { relative, resolve } from 'pathe'
|
||||||
import { consola } from 'consola'
|
import { consola } from 'consola'
|
||||||
|
|
||||||
|
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
|
||||||
|
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
|
||||||
import { clearBuildDir } from '../utils/fs'
|
import { clearBuildDir } from '../utils/fs'
|
||||||
import { loadKit } from '../utils/kit'
|
import { loadKit } from '../utils/kit'
|
||||||
import { writeTypes } from '../utils/prepare'
|
|
||||||
import { defineNuxtCommand } from './index'
|
import { defineNuxtCommand } from './index'
|
||||||
|
|
||||||
export default defineNuxtCommand({
|
export default defineNuxtCommand({
|
||||||
@ -15,7 +17,7 @@ export default defineNuxtCommand({
|
|||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
|
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
|
||||||
const rootDir = resolve(args._[0] || '.')
|
const rootDir = resolve(args._[0] || '.')
|
||||||
|
|
||||||
const { loadNuxt, buildNuxt } = await loadKit(rootDir)
|
const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
|
||||||
const nuxt = await loadNuxt({
|
const nuxt = await loadNuxt({
|
||||||
rootDir,
|
rootDir,
|
||||||
overrides: {
|
overrides: {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { execa } from 'execa'
|
import { execa } from 'execa'
|
||||||
import { resolve } from 'pathe'
|
import { resolve } from 'pathe'
|
||||||
import { tryResolveModule } from '../utils/esm'
|
|
||||||
|
|
||||||
|
// we are deliberately inlining this code as a backup in case user has `@nuxt/schema<3.7`
|
||||||
|
import { writeTypes as writeTypesLegacy } from '../../../kit/src/template'
|
||||||
|
import { tryResolveModule } from '../utils/esm'
|
||||||
import { loadKit } from '../utils/kit'
|
import { loadKit } from '../utils/kit'
|
||||||
import { writeTypes } from '../utils/prepare'
|
|
||||||
import { defineNuxtCommand } from './index'
|
import { defineNuxtCommand } from './index'
|
||||||
|
|
||||||
export default defineNuxtCommand({
|
export default defineNuxtCommand({
|
||||||
@ -16,7 +17,7 @@ export default defineNuxtCommand({
|
|||||||
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
|
process.env.NODE_ENV = process.env.NODE_ENV || 'production'
|
||||||
const rootDir = resolve(args._[0] || '.')
|
const rootDir = resolve(args._[0] || '.')
|
||||||
|
|
||||||
const { loadNuxt, buildNuxt } = await loadKit(rootDir)
|
const { loadNuxt, buildNuxt, writeTypes = writeTypesLegacy } = await loadKit(rootDir)
|
||||||
const nuxt = await loadNuxt({
|
const nuxt = await loadNuxt({
|
||||||
rootDir,
|
rootDir,
|
||||||
overrides: {
|
overrides: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
import { dirname, normalize } from 'pathe'
|
import { normalize } from 'pathe'
|
||||||
|
|
||||||
export function getModulePaths (paths?: string | string[]): string[] {
|
function getModulePaths (paths?: string | string[]): string[] {
|
||||||
return ([] as Array<string | undefined>)
|
return ([] as Array<string | undefined>)
|
||||||
.concat(
|
.concat(
|
||||||
global.__NUXT_PREPATHS__,
|
global.__NUXT_PREPATHS__,
|
||||||
@ -25,11 +25,3 @@ function requireModule (id: string, paths?: string | string[]) {
|
|||||||
export function tryRequireModule (id: string, paths?: string | string[]) {
|
export function tryRequireModule (id: string, paths?: string | string[]) {
|
||||||
try { return requireModule(id, paths) } catch { return null }
|
try { return requireModule(id, paths) } catch { return null }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNearestPackage (id: string, paths?: string | string[]) {
|
|
||||||
while (dirname(id) !== id) {
|
|
||||||
try { return requireModule(id + '/package.json', paths) } catch {}
|
|
||||||
id = dirname(id)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
@ -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'
|
import type { DefineConfig, InputConfig, UserInputConfig, ConfigLayerMeta } from 'c12'
|
||||||
export { NuxtConfig } from 'nuxt/schema'
|
export { NuxtConfig } from 'nuxt/schema'
|
||||||
|
|
||||||
export declare const defineNuxtConfig: DefineConfig<NuxtConfig, ConfigLayerMeta>
|
export interface DefineNuxtConfig extends DefineConfig<NuxtConfig, ConfigLayerMeta> {}
|
||||||
|
export declare const defineNuxtConfig: DefineNuxtConfig
|
||||||
|
@ -56,19 +56,19 @@
|
|||||||
"@nuxt/kit": "workspace:../kit",
|
"@nuxt/kit": "workspace:../kit",
|
||||||
"@nuxt/schema": "workspace:../schema",
|
"@nuxt/schema": "workspace:../schema",
|
||||||
"@nuxt/telemetry": "^2.3.2",
|
"@nuxt/telemetry": "^2.3.2",
|
||||||
"@nuxt/ui-templates": "^1.2.0",
|
"@nuxt/ui-templates": "^1.3.1",
|
||||||
"@nuxt/vite-builder": "workspace:../vite",
|
"@nuxt/vite-builder": "workspace:../vite",
|
||||||
"@unhead/ssr": "^1.1.32",
|
"@unhead/ssr": "^1.2.2",
|
||||||
"@unhead/vue": "^1.1.32",
|
"@unhead/vue": "^1.2.2",
|
||||||
"@vue/shared": "^3.3.4",
|
"@vue/shared": "^3.3.4",
|
||||||
"acorn": "8.10.0",
|
"acorn": "8.10.0",
|
||||||
"c12": "^1.4.2",
|
"c12": "^1.4.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"cookie-es": "^1.0.0",
|
"cookie-es": "^1.0.0",
|
||||||
"defu": "^6.1.2",
|
"defu": "^6.1.2",
|
||||||
"destr": "^2.0.0",
|
"destr": "^2.0.1",
|
||||||
"devalue": "^4.3.2",
|
"devalue": "^4.3.2",
|
||||||
"esbuild": "^0.18.16",
|
"esbuild": "^0.18.17",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
@ -78,8 +78,7 @@
|
|||||||
"jiti": "^1.19.1",
|
"jiti": "^1.19.1",
|
||||||
"klona": "^2.0.6",
|
"klona": "^2.0.6",
|
||||||
"knitwork": "^1.0.0",
|
"knitwork": "^1.0.0",
|
||||||
"local-pkg": "^0.4.3",
|
"magic-string": "^0.30.2",
|
||||||
"magic-string": "^0.30.1",
|
|
||||||
"mlly": "^1.4.0",
|
"mlly": "^1.4.0",
|
||||||
"nitropack": "^2.5.2",
|
"nitropack": "^2.5.2",
|
||||||
"nuxi": "workspace:../nuxi",
|
"nuxi": "workspace:../nuxi",
|
||||||
@ -88,20 +87,21 @@
|
|||||||
"ohash": "^1.1.2",
|
"ohash": "^1.1.2",
|
||||||
"pathe": "^1.1.1",
|
"pathe": "^1.1.1",
|
||||||
"perfect-debounce": "^1.0.0",
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"pkg-types": "^1.0.3",
|
||||||
"prompts": "^2.4.2",
|
"prompts": "^2.4.2",
|
||||||
"scule": "^1.0.0",
|
"scule": "^1.0.0",
|
||||||
"strip-literal": "^1.0.1",
|
"strip-literal": "^1.3.0",
|
||||||
"ufo": "^1.1.2",
|
"ufo": "^1.2.0",
|
||||||
"ultrahtml": "^1.3.0",
|
"ultrahtml": "^1.3.0",
|
||||||
"uncrypto": "^0.1.3",
|
"uncrypto": "^0.1.3",
|
||||||
"unctx": "^2.3.1",
|
"unctx": "^2.3.1",
|
||||||
"unenv": "^1.5.2",
|
"unenv": "^1.6.1",
|
||||||
"unimport": "^3.1.0",
|
"unimport": "^3.1.3",
|
||||||
"unplugin": "^1.4.0",
|
"unplugin": "^1.4.0",
|
||||||
"unplugin-vue-router": "^0.6.4",
|
"unplugin-vue-router": "^0.6.4",
|
||||||
"untyped": "^1.3.2",
|
"untyped": "^1.4.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-bundle-renderer": "^1.0.3",
|
"vue-bundle-renderer": "^2.0.0",
|
||||||
"vue-devtools-stub": "^0.1.0",
|
"vue-devtools-stub": "^0.1.0",
|
||||||
"vue-router": "^4.2.4"
|
"vue-router": "^4.2.4"
|
||||||
},
|
},
|
||||||
@ -112,7 +112,7 @@
|
|||||||
"@types/prompts": "2.4.4",
|
"@types/prompts": "2.4.4",
|
||||||
"@vitejs/plugin-vue": "4.2.3",
|
"@vitejs/plugin-vue": "4.2.3",
|
||||||
"unbuild": "latest",
|
"unbuild": "latest",
|
||||||
"vite": "4.4.7",
|
"vite": "4.4.8",
|
||||||
"vitest": "0.33.0"
|
"vitest": "0.33.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -18,7 +18,7 @@ export default defineComponent({
|
|||||||
if (!component) {
|
if (!component) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
statusMessage: `Island component not found: ${JSON.stringify(component)}`
|
statusMessage: `Island component not found: ${props.context.name}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,150 +1,2 @@
|
|||||||
import type { Ref, VNode } from 'vue'
|
// TODO: remove in 4.x
|
||||||
import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue'
|
export { default } from './nuxt-layout'
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
||||||
import { _wrapIf } from './utils'
|
|
||||||
import { LayoutMetaSymbol, PageRouteSymbol } from './injections'
|
|
||||||
|
|
||||||
import { useRoute } from '#app/composables/router'
|
|
||||||
// @ts-expect-error virtual file
|
|
||||||
import { useRoute as useVueRouterRoute } from '#build/pages'
|
|
||||||
// @ts-expect-error virtual file
|
|
||||||
import layouts from '#build/layouts'
|
|
||||||
// @ts-expect-error virtual file
|
|
||||||
import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs'
|
|
||||||
import { useNuxtApp } from '#app'
|
|
||||||
|
|
||||||
// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved
|
|
||||||
const LayoutLoader = defineComponent({
|
|
||||||
name: 'LayoutLoader',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
name: String,
|
|
||||||
layoutProps: Object
|
|
||||||
},
|
|
||||||
async setup (props, context) {
|
|
||||||
const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r)
|
|
||||||
|
|
||||||
return () => h(LayoutComponent, props.layoutProps, context.slots)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'NuxtLayout',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
name: {
|
|
||||||
type: [String, Boolean, Object] as unknown as () => string | false | Ref<string | false>,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props, context) {
|
|
||||||
const nuxtApp = useNuxtApp()
|
|
||||||
// Need to ensure (if we are not a child of `<NuxtPage>`) that we use synchronous route (not deferred)
|
|
||||||
const injectedRoute = inject(PageRouteSymbol)
|
|
||||||
const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute
|
|
||||||
|
|
||||||
const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default')
|
|
||||||
|
|
||||||
const layoutRef = ref()
|
|
||||||
context.expose({ layoutRef })
|
|
||||||
|
|
||||||
const done = nuxtApp.deferHydration()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const hasLayout = layout.value && layout.value in layouts
|
|
||||||
if (process.dev && layout.value && !hasLayout && layout.value !== 'default') {
|
|
||||||
console.warn(`Invalid layout \`${layout.value}\` selected.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition
|
|
||||||
|
|
||||||
// We avoid rendering layout transition if there is no layout to render
|
|
||||||
return _wrapIf(Transition, hasLayout && transitionProps, {
|
|
||||||
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
|
|
||||||
default: () => h(
|
|
||||||
// @ts-expect-error seems to be an issue in vue types
|
|
||||||
LayoutProvider,
|
|
||||||
{
|
|
||||||
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
|
|
||||||
key: layout.value,
|
|
||||||
name: layout.value,
|
|
||||||
shouldProvide: !props.name,
|
|
||||||
hasTransition: !!transitionProps
|
|
||||||
}, context.slots)
|
|
||||||
})
|
|
||||||
}).default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const LayoutProvider = defineComponent({
|
|
||||||
name: 'NuxtLayoutProvider',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
name: {
|
|
||||||
type: [String, Boolean]
|
|
||||||
},
|
|
||||||
layoutProps: {
|
|
||||||
type: Object
|
|
||||||
},
|
|
||||||
hasTransition: {
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
shouldProvide: {
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props, context) {
|
|
||||||
// Prevent reactivity when the page will be rerendered in a different suspense fork
|
|
||||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
|
||||||
const name = props.name
|
|
||||||
if (props.shouldProvide) {
|
|
||||||
provide(LayoutMetaSymbol, {
|
|
||||||
isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let vnode: VNode | undefined
|
|
||||||
if (process.dev && process.client) {
|
|
||||||
onMounted(() => {
|
|
||||||
nextTick(() => {
|
|
||||||
if (['#comment', '#text'].includes(vnode?.el?.nodeName)) {
|
|
||||||
if (name) {
|
|
||||||
console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`)
|
|
||||||
} else {
|
|
||||||
console.warn('[nuxt] `<NuxtLayout>` needs to be passed a single root node in its default slot.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (!name || (typeof name === 'string' && !(name in layouts))) {
|
|
||||||
if (process.dev && process.client && props.hasTransition) {
|
|
||||||
vnode = context.slots.default?.() as VNode | undefined
|
|
||||||
return vnode
|
|
||||||
}
|
|
||||||
return context.slots.default?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.dev && process.client && props.hasTransition) {
|
|
||||||
vnode = h(
|
|
||||||
// @ts-expect-error seems to be an issue in vue types
|
|
||||||
LayoutLoader,
|
|
||||||
{ key: name, layoutProps: props.layoutProps, name },
|
|
||||||
context.slots
|
|
||||||
)
|
|
||||||
|
|
||||||
return vnode
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(
|
|
||||||
// @ts-expect-error seems to be an issue in vue types
|
|
||||||
LayoutLoader,
|
|
||||||
{ key: name, layoutProps: props.layoutProps, name },
|
|
||||||
context.slots
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
@ -13,6 +13,9 @@ import { getFragmentHTML, getSlotProps } from './utils'
|
|||||||
import { useNuxtApp, useRuntimeConfig } from '#app/nuxt'
|
import { useNuxtApp, useRuntimeConfig } from '#app/nuxt'
|
||||||
import { useRequestEvent } from '#app/composables/ssr'
|
import { useRequestEvent } from '#app/composables/ssr'
|
||||||
|
|
||||||
|
// @ts-expect-error virtual file
|
||||||
|
import { remoteComponentIslands } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
const pKey = '_islandPromises'
|
const pKey = '_islandPromises'
|
||||||
const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
|
const SSR_UID_RE = /nuxt-ssr-component-uid="([^"]*)"/
|
||||||
const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
|
const UID_ATTR = /nuxt-ssr-component-uid(="([^"]*)")?/
|
||||||
@ -29,6 +32,7 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
lazy: Boolean,
|
||||||
props: {
|
props: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => undefined
|
default: () => undefined
|
||||||
@ -36,12 +40,17 @@ export default defineComponent({
|
|||||||
context: {
|
context: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
type: String,
|
||||||
|
default: () => undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setup (props, { slots }) {
|
async setup (props, { slots }) {
|
||||||
|
const error = ref<unknown>(null)
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
const hashId = computed(() => hash([props.name, props.props, props.context]))
|
const hashId = computed(() => hash([props.name, props.props, props.context, props.source]))
|
||||||
const instance = getCurrentInstance()!
|
const instance = getCurrentInstance()!
|
||||||
const event = useRequestEvent()
|
const event = useRequestEvent()
|
||||||
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
|
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
|
||||||
@ -61,7 +70,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ssrHTML = ref('<div></div>')
|
const ssrHTML = ref<string>('')
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('')
|
const renderedHTML = getFragmentHTML(instance.vnode?.el ?? null).join('')
|
||||||
if (renderedHTML && nuxtApp.isHydrating) {
|
if (renderedHTML && nuxtApp.isHydrating) {
|
||||||
@ -74,7 +83,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ssrHTML.value = renderedHTML ?? '<div></div>'
|
ssrHTML.value = renderedHTML
|
||||||
}
|
}
|
||||||
const slotProps = computed(() => getSlotProps(ssrHTML.value))
|
const slotProps = computed(() => getSlotProps(ssrHTML.value))
|
||||||
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID())
|
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? randomUUID())
|
||||||
@ -100,7 +109,8 @@ export default defineComponent({
|
|||||||
const key = `${props.name}_${hashId.value}`
|
const key = `${props.name}_${hashId.value}`
|
||||||
if (nuxtApp.payload.data[key] && !force) { return nuxtApp.payload.data[key] }
|
if (nuxtApp.payload.data[key] && !force) { return nuxtApp.payload.data[key] }
|
||||||
|
|
||||||
const url = `/__nuxt_island/${key}`
|
const url = remoteComponentIslands && props.source ? new URL(`/__nuxt_island/${key}`, props.source).href : `/__nuxt_island/${key}`
|
||||||
|
|
||||||
if (process.server && process.env.prerender) {
|
if (process.server && process.env.prerender) {
|
||||||
// Hint to Nitro to prerender the island component
|
// Hint to Nitro to prerender the island component
|
||||||
appendResponseHeader(event, 'x-nitro-prerender', url)
|
appendResponseHeader(event, 'x-nitro-prerender', url)
|
||||||
@ -130,18 +140,23 @@ export default defineComponent({
|
|||||||
delete nuxtApp[pKey]![uid.value]
|
delete nuxtApp[pKey]![uid.value]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
|
try {
|
||||||
cHead.value.link = res.head.link
|
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
|
||||||
cHead.value.style = res.head.style
|
cHead.value.link = res.head.link
|
||||||
ssrHTML.value = res.html.replace(UID_ATTR, () => {
|
cHead.value.style = res.head.style
|
||||||
return `nuxt-ssr-component-uid="${getId()}"`
|
ssrHTML.value = res.html.replace(UID_ATTR, () => {
|
||||||
})
|
return `nuxt-ssr-component-uid="${getId()}"`
|
||||||
key.value++
|
})
|
||||||
if (process.client) {
|
key.value++
|
||||||
// must await next tick for Teleport to work correctly with static node re-rendering
|
error.value = null
|
||||||
await nextTick()
|
if (process.client) {
|
||||||
|
// must await next tick for Teleport to work correctly with static node re-rendering
|
||||||
|
await nextTick()
|
||||||
|
}
|
||||||
|
setUid()
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e
|
||||||
}
|
}
|
||||||
setUid()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
@ -154,15 +169,19 @@ export default defineComponent({
|
|||||||
watch(props, debounce(() => fetchComponent(), 100))
|
watch(props, debounce(() => fetchComponent(), 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: allow lazy loading server islands
|
if (process.client && !nuxtApp.isHydrating && props.lazy) {
|
||||||
if (process.server || !nuxtApp.isHydrating) {
|
fetchComponent()
|
||||||
|
} else if (process.server || !nuxtApp.isHydrating) {
|
||||||
await fetchComponent()
|
await fetchComponent()
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if ((!html.value || error.value) && slots.fallback) {
|
||||||
|
return [slots.fallback({ error: error.value })]
|
||||||
|
}
|
||||||
const nodes = [createVNode(Fragment, {
|
const nodes = [createVNode(Fragment, {
|
||||||
key: key.value
|
key: key.value
|
||||||
}, [h(createStaticVNode(html.value, 1))])]
|
}, [h(createStaticVNode(html.value || '<div></div>', 1))])]
|
||||||
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
|
if (uid.value && (mounted.value || nuxtApp.isHydrating || process.server)) {
|
||||||
for (const slot in slots) {
|
for (const slot in slots) {
|
||||||
if (availableSlots.value.includes(slot)) {
|
if (availableSlots.value.includes(slot)) {
|
||||||
|
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) {
|
if (opts.watch) {
|
||||||
watch(cookie, (newVal, oldVal) => {
|
watch(cookie, () => {
|
||||||
if (watchPaused || isEqual(newVal, oldVal)) { return }
|
if (watchPaused) { return }
|
||||||
callback()
|
callback()
|
||||||
},
|
},
|
||||||
{ deep: opts.watch !== 'shallow' })
|
{ deep: opts.watch !== 'shallow' })
|
||||||
|
@ -1,18 +1,25 @@
|
|||||||
import type { FetchError } from 'ofetch'
|
import type { FetchError, FetchOptions } from 'ofetch'
|
||||||
import type { AvailableRouterMethod, NitroFetchOptions, NitroFetchRequest, TypedInternalResponse } from 'nitropack'
|
import type { NitroFetchRequest, TypedInternalResponse, AvailableRouterMethod as _AvailableRouterMethod } from 'nitropack'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { computed, reactive, unref } from 'vue'
|
import { computed, reactive, unref } from 'vue'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
import { useRequestFetch } from './ssr'
|
import { useRequestFetch } from './ssr'
|
||||||
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom, _Transform } from './asyncData'
|
import type { AsyncData, AsyncDataOptions, KeysOf, MultiWatchSources, PickFrom } from './asyncData'
|
||||||
import { useAsyncData } from './asyncData'
|
import { useAsyncData } from './asyncData'
|
||||||
|
|
||||||
export type FetchResult<ReqT extends NitroFetchRequest, M extends AvailableRouterMethod<ReqT>> = TypedInternalResponse<ReqT, unknown, M>
|
// support uppercase methods, detail: https://github.com/nuxt/nuxt/issues/22313
|
||||||
|
type AvailableRouterMethod<R extends NitroFetchRequest> = _AvailableRouterMethod<R> | Uppercase<_AvailableRouterMethod<R>>
|
||||||
|
|
||||||
|
export type FetchResult<ReqT extends NitroFetchRequest, M extends AvailableRouterMethod<ReqT>> = TypedInternalResponse<ReqT, unknown, Lowercase<M>>
|
||||||
|
|
||||||
type ComputedOptions<T extends Record<string, any>> = {
|
type ComputedOptions<T extends Record<string, any>> = {
|
||||||
[K in keyof T]: T[K] extends Function ? T[K] : T[K] extends Record<string, any> ? ComputedOptions<T[K]> | Ref<T[K]> | T[K] : Ref<T[K]> | T[K]
|
[K in keyof T]: T[K] extends Function ? T[K] : T[K] extends Record<string, any> ? ComputedOptions<T[K]> | Ref<T[K]> | T[K] : Ref<T[K]> | T[K]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> extends FetchOptions {
|
||||||
|
method?: M;
|
||||||
|
}
|
||||||
|
|
||||||
type ComputedFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R>> = ComputedOptions<NitroFetchOptions<R, M>>
|
type ComputedFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R>> = ComputedOptions<NitroFetchOptions<R, M>>
|
||||||
|
|
||||||
export interface UseFetchOptions<
|
export interface UseFetchOptions<
|
||||||
@ -69,15 +76,6 @@ export function useFetch<
|
|||||||
arg2?: string
|
arg2?: string
|
||||||
) {
|
) {
|
||||||
const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]
|
const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]
|
||||||
const _key = opts.key || hash([autoKey, unref(opts.baseURL), typeof request === 'string' ? request : '', unref(opts.params || opts.query)])
|
|
||||||
if (!_key || typeof _key !== 'string') {
|
|
||||||
throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key)
|
|
||||||
}
|
|
||||||
if (!request) {
|
|
||||||
throw new Error('[nuxt] [useFetch] request is missing.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = _key === autoKey ? '$f' + _key : _key
|
|
||||||
|
|
||||||
const _request = computed(() => {
|
const _request = computed(() => {
|
||||||
let r = request
|
let r = request
|
||||||
@ -87,6 +85,16 @@ export function useFetch<
|
|||||||
return unref(r)
|
return unref(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const _key = opts.key || hash([autoKey, unref(opts.baseURL), typeof _request.value === 'string' ? _request.value : '', unref(opts.params || opts.query)])
|
||||||
|
if (!_key || typeof _key !== 'string') {
|
||||||
|
throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key)
|
||||||
|
}
|
||||||
|
if (!request) {
|
||||||
|
throw new Error('[nuxt] [useFetch] request is missing.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = _key === autoKey ? '$f' + _key : _key
|
||||||
|
|
||||||
if (!opts.baseURL && typeof _request.value === 'string' && _request.value.startsWith('//')) {
|
if (!opts.baseURL && typeof _request.value === 'string' && _request.value.startsWith('//')) {
|
||||||
throw new Error('[nuxt] [useFetch] the request URL must not start with "//".')
|
throw new Error('[nuxt] [useFetch] the request URL must not start with "//".')
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { getCurrentInstance, hasInjectionContext, inject, onUnmounted } from 'vu
|
|||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router'
|
import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router'
|
||||||
import { sanitizeStatusCode } from 'h3'
|
import { sanitizeStatusCode } from 'h3'
|
||||||
import { hasProtocol, joinURL, parseURL, withQuery } from 'ufo'
|
import { hasProtocol, isScriptProtocol, joinURL, parseURL, withQuery } from 'ufo'
|
||||||
|
|
||||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||||
import type { NuxtError } from './error'
|
import type { NuxtError } from './error'
|
||||||
@ -133,11 +133,14 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isExternal = options?.external || hasProtocol(toPath, { acceptRelative: true })
|
const isExternal = options?.external || hasProtocol(toPath, { acceptRelative: true })
|
||||||
if (isExternal && !options?.external) {
|
if (isExternal) {
|
||||||
throw new Error('Navigating to external URL is not allowed by default. Use `navigateTo (url, { external: true })`.')
|
if (!options?.external) {
|
||||||
}
|
throw new Error('Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.')
|
||||||
if (isExternal && parseURL(toPath).protocol === 'script:') {
|
}
|
||||||
throw new Error('Cannot navigate to an URL with script protocol.')
|
const protocol = parseURL(toPath).protocol
|
||||||
|
if (protocol && isScriptProtocol(protocol)) {
|
||||||
|
throw new Error(`Cannot navigate to a URL with '${protocol}' protocol.`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inMiddleware = isProcessingMiddleware()
|
const inMiddleware = isProcessingMiddleware()
|
||||||
@ -218,7 +221,7 @@ export const abortNavigation = (err?: string | Partial<NuxtError>) => {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setPageLayout = (layout: string) => {
|
export const setPageLayout = (layout: unknown extends PageMeta['layout'] ? string : PageMeta['layout']) => {
|
||||||
if (process.server) {
|
if (process.server) {
|
||||||
if (process.dev && getCurrentInstance() && useState('_layout').value !== layout) {
|
if (process.dev && getCurrentInstance() && useState('_layout').value !== layout) {
|
||||||
console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout on the server within a component as this will cause hydration errors.')
|
console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout on the server within a component as this will cause hydration errors.')
|
||||||
|
@ -10,6 +10,7 @@ import type { H3Event } from 'h3'
|
|||||||
import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema'
|
import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema'
|
||||||
import type { RenderResponse } from 'nitropack'
|
import type { RenderResponse } from 'nitropack'
|
||||||
|
|
||||||
|
import type { MergeHead, VueHeadClient } from '@unhead/vue'
|
||||||
// eslint-disable-next-line import/no-restricted-paths
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
|
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
|
||||||
import type { RouteMiddleware } from '../../app'
|
import type { RouteMiddleware } from '../../app'
|
||||||
@ -18,15 +19,6 @@ import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
|||||||
|
|
||||||
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
|
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
|
||||||
|
|
||||||
type NuxtMeta = {
|
|
||||||
htmlAttrs?: string
|
|
||||||
headAttrs?: string
|
|
||||||
bodyAttrs?: string
|
|
||||||
headTags?: string
|
|
||||||
bodyScriptsPrepend?: string
|
|
||||||
bodyScripts?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type HookResult = Promise<void> | void
|
type HookResult = Promise<void> | void
|
||||||
|
|
||||||
type AppRenderedContext = { ssrContext: NuxtApp['ssrContext'], renderResult: null | Awaited<ReturnType<ReturnType<typeof createRenderer>['renderToString']>> }
|
type AppRenderedContext = { ssrContext: NuxtApp['ssrContext'], renderResult: null | Awaited<ReturnType<ReturnType<typeof createRenderer>['renderToString']>> }
|
||||||
@ -59,10 +51,10 @@ export interface NuxtSSRContext extends SSRContext {
|
|||||||
error?: boolean
|
error?: boolean
|
||||||
nuxt: _NuxtApp
|
nuxt: _NuxtApp
|
||||||
payload: NuxtPayload
|
payload: NuxtPayload
|
||||||
|
head: VueHeadClient<MergeHead>
|
||||||
/** This is used solely to render runtime config with SPA renderer. */
|
/** This is used solely to render runtime config with SPA renderer. */
|
||||||
config?: Pick<RuntimeConfig, 'public' | 'app'>
|
config?: Pick<RuntimeConfig, 'public' | 'app'>
|
||||||
teleports?: Record<string, string>
|
teleports?: Record<string, string>
|
||||||
renderMeta?: () => Promise<NuxtMeta> | NuxtMeta
|
|
||||||
islandContext?: NuxtIslandContext
|
islandContext?: NuxtIslandContext
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_renderResponse?: Partial<RenderResponse>
|
_renderResponse?: Partial<RenderResponse>
|
||||||
@ -163,6 +155,16 @@ export interface PluginMeta {
|
|||||||
order?: number
|
order?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PluginEnvContext {
|
||||||
|
/**
|
||||||
|
* This enable the plugin for islands components.
|
||||||
|
* Require `experimental.componentsIslands`.
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
islands?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResolvedPluginMeta {
|
export interface ResolvedPluginMeta {
|
||||||
name?: string
|
name?: string
|
||||||
parallel?: boolean
|
parallel?: boolean
|
||||||
@ -177,6 +179,7 @@ export interface Plugin<Injections extends Record<string, unknown> = Record<stri
|
|||||||
export interface ObjectPlugin<Injections extends Record<string, unknown> = Record<string, unknown>> extends PluginMeta {
|
export interface ObjectPlugin<Injections extends Record<string, unknown> = Record<string, unknown>> extends PluginMeta {
|
||||||
hooks?: Partial<RuntimeNuxtHooks>
|
hooks?: Partial<RuntimeNuxtHooks>
|
||||||
setup?: Plugin<Injections>
|
setup?: Plugin<Injections>
|
||||||
|
env?: PluginEnvContext
|
||||||
/**
|
/**
|
||||||
* Execute plugin in parallel with other parallel plugins.
|
* Execute plugin in parallel with other parallel plugins.
|
||||||
*
|
*
|
||||||
@ -326,6 +329,7 @@ export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & Ob
|
|||||||
const parallels: Promise<any>[] = []
|
const parallels: Promise<any>[] = []
|
||||||
const errors: Error[] = []
|
const errors: Error[] = []
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
|
if (process.server && nuxtApp.ssrContext?.islandContext && plugin.env?.islands === false) { continue }
|
||||||
const promise = applyPlugin(nuxtApp, plugin)
|
const promise = applyPlugin(nuxtApp, plugin)
|
||||||
if (plugin.parallel) {
|
if (plugin.parallel) {
|
||||||
parallels.push(promise.catch(e => errors.push(e)))
|
parallels.push(promise.catch(e => errors.push(e)))
|
||||||
|
@ -31,7 +31,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
|||||||
if (include.some(pattern => pattern.test(id))) {
|
if (include.some(pattern => pattern.test(id))) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return isVue(id, { type: ['template', 'script'] })
|
return isVue(id, { type: ['template', 'script'] }) || !!id.match(/\.[tj]sx$/)
|
||||||
},
|
},
|
||||||
transform (code) {
|
transform (code) {
|
||||||
const components = options.getComponents()
|
const components = options.getComponents()
|
||||||
|
@ -129,11 +129,11 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
const unpluginServer = createTransformPlugin(nuxt, getComponents, 'server')
|
const unpluginServer = createTransformPlugin(nuxt, getComponents, 'server')
|
||||||
const unpluginClient = createTransformPlugin(nuxt, getComponents, 'client')
|
const unpluginClient = createTransformPlugin(nuxt, getComponents, 'client')
|
||||||
|
|
||||||
addVitePlugin(unpluginServer.vite(), { server: true, client: false })
|
addVitePlugin(() => unpluginServer.vite(), { server: true, client: false })
|
||||||
addVitePlugin(unpluginClient.vite(), { server: false, client: true })
|
addVitePlugin(() => unpluginClient.vite(), { server: false, client: true })
|
||||||
|
|
||||||
addWebpackPlugin(unpluginServer.webpack(), { server: true, client: false })
|
addWebpackPlugin(() => unpluginServer.webpack(), { server: true, client: false })
|
||||||
addWebpackPlugin(unpluginClient.webpack(), { server: false, client: true })
|
addWebpackPlugin(() => unpluginClient.webpack(), { server: false, client: true })
|
||||||
|
|
||||||
// Do not prefetch global components chunks
|
// Do not prefetch global components chunks
|
||||||
nuxt.hook('build:manifest', (manifest) => {
|
nuxt.hook('build:manifest', (manifest) => {
|
||||||
@ -148,12 +148,14 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Restart dev server when component directories are added/removed
|
// Restart dev server when component directories are added/removed
|
||||||
nuxt.hook('builder:watch', (event, path) => {
|
nuxt.hook('builder:watch', (event, relativePath) => {
|
||||||
const isDirChange = ['addDir', 'unlinkDir'].includes(event)
|
if (!['addDir', 'unlinkDir'].includes(event)) {
|
||||||
const fullPath = resolve(nuxt.options.srcDir, path)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isDirChange && componentDirs.some(dir => dir.path === fullPath)) {
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
console.info(`Directory \`${path}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
|
if (componentDirs.some(dir => dir.path === path)) {
|
||||||
|
console.info(`Directory \`${relativePath}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
|
||||||
return nuxt.callHook('restart')
|
return nuxt.callHook('restart')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -183,12 +185,12 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Watch for changes
|
// Watch for changes
|
||||||
nuxt.hook('builder:watch', async (event, path) => {
|
nuxt.hook('builder:watch', async (event, relativePath) => {
|
||||||
if (!['add', 'unlink'].includes(event)) {
|
if (!['add', 'unlink'].includes(event)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const fPath = resolve(nuxt.options.srcDir, path)
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
if (componentDirs.find(dir => fPath.startsWith(dir.path))) {
|
if (componentDirs.some(dir => path.startsWith(dir.path + '/'))) {
|
||||||
await updateTemplates({
|
await updateTemplates({
|
||||||
filter: template => [
|
filter: template => [
|
||||||
'components.plugin.mjs',
|
'components.plugin.mjs',
|
||||||
@ -219,7 +221,7 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
getComponents,
|
getComponents,
|
||||||
mode,
|
mode,
|
||||||
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
|
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
|
||||||
experimentalComponentIslands: nuxt.options.experimental.componentIslands
|
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (isServer && nuxt.options.experimental.componentIslands) {
|
if (isServer && nuxt.options.experimental.componentIslands) {
|
||||||
@ -263,7 +265,7 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
getComponents,
|
getComponents,
|
||||||
mode,
|
mode,
|
||||||
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
|
transform: typeof nuxt.options.components === 'object' && !Array.isArray(nuxt.options.components) ? nuxt.options.components.transform : undefined,
|
||||||
experimentalComponentIslands: nuxt.options.experimental.componentIslands
|
experimentalComponentIslands: !!nuxt.options.experimental.componentIslands
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (nuxt.options.experimental.componentIslands && mode === 'server') {
|
if (nuxt.options.experimental.componentIslands && mode === 'server') {
|
||||||
|
@ -5,9 +5,11 @@ export const createServerComponent = (name: string) => {
|
|||||||
return defineComponent({
|
return defineComponent({
|
||||||
name,
|
name,
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
setup (_props, { attrs, slots }) {
|
props: { lazy: Boolean },
|
||||||
|
setup (props, { attrs, slots }) {
|
||||||
return () => h(NuxtIsland, {
|
return () => h(NuxtIsland, {
|
||||||
name,
|
name,
|
||||||
|
lazy: props.lazy,
|
||||||
props: attrs
|
props: attrs
|
||||||
}, slots)
|
}, slots)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { promises as fsp } from 'node:fs'
|
import { promises as fsp, mkdirSync, writeFileSync } from 'node:fs'
|
||||||
import { dirname, join, resolve } from 'pathe'
|
import { dirname, join, resolve } from 'pathe'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { compileTemplate, findPath, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath, templateUtils, tryResolveModule } from '@nuxt/kit'
|
import { compileTemplate, findPath, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath, templateUtils, tryResolveModule } from '@nuxt/kit'
|
||||||
@ -32,13 +32,21 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
|
|||||||
app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl))
|
app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl))
|
||||||
|
|
||||||
// Compile templates into vfs
|
// Compile templates into vfs
|
||||||
|
// TODO: remove utils in v4
|
||||||
const templateContext = { utils: templateUtils, nuxt, app }
|
const templateContext = { utils: templateUtils, nuxt, app }
|
||||||
await Promise.all((app.templates as Array<ReturnType<typeof normalizeTemplate>>)
|
const filteredTemplates = (app.templates as Array<ReturnType<typeof normalizeTemplate>>)
|
||||||
.filter(template => !options.filter || options.filter(template))
|
.filter(template => !options.filter || options.filter(template))
|
||||||
.map(async (template) => {
|
|
||||||
const contents = await compileTemplate(template, templateContext)
|
|
||||||
|
|
||||||
|
const writes: Array<() => void> = []
|
||||||
|
await Promise.allSettled(filteredTemplates
|
||||||
|
.map(async (template) => {
|
||||||
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
|
const fullPath = template.dst || resolve(nuxt.options.buildDir, template.filename!)
|
||||||
|
const mark = performance.mark(fullPath)
|
||||||
|
const contents = await compileTemplate(template, templateContext).catch((e) => {
|
||||||
|
console.error(`[nuxt] Could not compile template \`${template.filename}\`.`)
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
|
||||||
nuxt.vfs[fullPath] = contents
|
nuxt.vfs[fullPath] = contents
|
||||||
|
|
||||||
const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '')
|
const aliasPath = '#build/' + template.filename!.replace(/\.\w+$/, '')
|
||||||
@ -49,13 +57,26 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
|
|||||||
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents
|
nuxt.vfs[fullPath.replace(/\//g, '\\')] = contents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const perf = performance.measure(fullPath, mark?.name) // TODO: remove when Node 14 reaches EOL
|
||||||
|
const setupTime = perf ? Math.round((perf.duration * 100)) / 100 : 0 // TODO: remove when Node 14 reaches EOL
|
||||||
|
|
||||||
|
if (nuxt.options.debug || setupTime > 500) {
|
||||||
|
console.info(`[nuxt] compiled \`${template.filename}\` in ${setupTime}ms`)
|
||||||
|
}
|
||||||
|
|
||||||
if (template.write) {
|
if (template.write) {
|
||||||
await fsp.mkdir(dirname(fullPath), { recursive: true })
|
writes.push(() => {
|
||||||
await fsp.writeFile(fullPath, contents, 'utf8')
|
mkdirSync(dirname(fullPath), { recursive: true })
|
||||||
|
writeFileSync(fullPath, contents, 'utf8')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await nuxt.callHook('app:templatesGenerated', app)
|
// Write template files in single synchronous step to avoid (possible) additional
|
||||||
|
// runtime overhead of cascading HMRs from vite/webpack
|
||||||
|
for (const write of writes) { write() }
|
||||||
|
|
||||||
|
await nuxt.callHook('app:templatesGenerated', app, filteredTemplates, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
||||||
|
@ -5,7 +5,7 @@ import chokidar from 'chokidar'
|
|||||||
import { isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit'
|
import { isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit'
|
||||||
import { interopDefault } from 'mlly'
|
import { interopDefault } from 'mlly'
|
||||||
import { debounce } from 'perfect-debounce'
|
import { debounce } from 'perfect-debounce'
|
||||||
import { normalize, resolve } from 'pathe'
|
import { normalize, relative, resolve } from 'pathe'
|
||||||
import type { Nuxt } from 'nuxt/schema'
|
import type { Nuxt } from 'nuxt/schema'
|
||||||
|
|
||||||
import { generateApp as _generateApp, createApp } from './app'
|
import { generateApp as _generateApp, createApp } from './app'
|
||||||
@ -19,12 +19,16 @@ export async function build (nuxt: Nuxt) {
|
|||||||
|
|
||||||
if (nuxt.options.dev) {
|
if (nuxt.options.dev) {
|
||||||
watch(nuxt)
|
watch(nuxt)
|
||||||
nuxt.hook('builder:watch', async (event, path) => {
|
nuxt.hook('builder:watch', async (event, relativePath) => {
|
||||||
if (event !== 'change' && /^(app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(path)) {
|
if (event === 'change') { return }
|
||||||
if (path.startsWith('app')) {
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
|
const relativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
|
||||||
|
const restartPath = relativePaths.find(relativePath => /^(app\.|error\.|plugins\/|middleware\/|layouts\/)/i.test(relativePath))
|
||||||
|
if (restartPath) {
|
||||||
|
if (restartPath.startsWith('app')) {
|
||||||
app.mainComponent = undefined
|
app.mainComponent = undefined
|
||||||
}
|
}
|
||||||
if (path.startsWith('error')) {
|
if (restartPath.startsWith('error')) {
|
||||||
app.errorComponent = undefined
|
app.errorComponent = undefined
|
||||||
}
|
}
|
||||||
await generateApp()
|
await generateApp()
|
||||||
@ -72,7 +76,6 @@ function createWatcher () {
|
|||||||
|
|
||||||
const watcher = chokidar.watch(nuxt.options._layers.map(i => i.config.srcDir as string).filter(Boolean), {
|
const watcher = chokidar.watch(nuxt.options._layers.map(i => i.config.srcDir as string).filter(Boolean), {
|
||||||
...nuxt.options.watchers.chokidar,
|
...nuxt.options.watchers.chokidar,
|
||||||
cwd: nuxt.options.srcDir,
|
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
ignored: [
|
ignored: [
|
||||||
isIgnored,
|
isIgnored,
|
||||||
@ -80,7 +83,8 @@ function createWatcher () {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path)))
|
// TODO: consider moving to emit absolute path in 3.8 or 4.0
|
||||||
|
watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(relative(nuxt.options.srcDir, path))))
|
||||||
nuxt.hook('close', () => watcher?.close())
|
nuxt.hook('close', () => watcher?.close())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +98,7 @@ function createGranularWatcher () {
|
|||||||
let pending = 0
|
let pending = 0
|
||||||
|
|
||||||
const ignoredDirs = new Set([...nuxt.options.modulesDir, nuxt.options.buildDir])
|
const ignoredDirs = new Set([...nuxt.options.modulesDir, nuxt.options.buildDir])
|
||||||
const pathsToWatch = nuxt.options._layers.map(layer => layer.config.srcDir).filter(d => d && !isIgnored(d))
|
const pathsToWatch = nuxt.options._layers.map(layer => layer.config.srcDir || layer.cwd).filter(d => d && !isIgnored(d))
|
||||||
for (const pattern of nuxt.options.watch) {
|
for (const pattern of nuxt.options.watch) {
|
||||||
if (typeof pattern !== 'string') { continue }
|
if (typeof pattern !== 'string') { continue }
|
||||||
const path = resolve(nuxt.options.srcDir, pattern)
|
const path = resolve(nuxt.options.srcDir, pattern)
|
||||||
@ -109,7 +113,8 @@ function createGranularWatcher () {
|
|||||||
watcher.on('all', (event, path) => {
|
watcher.on('all', (event, path) => {
|
||||||
path = normalize(path)
|
path = normalize(path)
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
nuxt.callHook('builder:watch', event, path)
|
// TODO: consider moving to emit absolute path in 3.8 or 4.0
|
||||||
|
nuxt.callHook('builder:watch', event, relative(nuxt.options.srcDir, path))
|
||||||
}
|
}
|
||||||
if (event === 'unlinkDir' && path in watchers) {
|
if (event === 'unlinkDir' && path in watchers) {
|
||||||
watchers[path]?.close()
|
watchers[path]?.close()
|
||||||
@ -117,7 +122,8 @@ function createGranularWatcher () {
|
|||||||
}
|
}
|
||||||
if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) {
|
if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) {
|
||||||
watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] })
|
watchers[path] = chokidar.watch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] })
|
||||||
watchers[path].on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path)))
|
// TODO: consider moving to emit absolute path in 3.8 or 4.0
|
||||||
|
watchers[path].on('all', (event, p) => nuxt.callHook('builder:watch', event, normalize(relative(nuxt.options.srcDir, p))))
|
||||||
nuxt.hook('close', () => watchers[path]?.close())
|
nuxt.hook('close', () => watchers[path]?.close())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -144,7 +150,8 @@ async function createParcelWatcher () {
|
|||||||
if (err) { return }
|
if (err) { return }
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (isIgnored(event.path)) { continue }
|
if (isIgnored(event.path)) { continue }
|
||||||
nuxt.callHook('builder:watch', watchEvents[event.type], normalize(event.path))
|
// TODO: consider moving to emit absolute path in 3.8 or 4.0
|
||||||
|
nuxt.callHook('builder:watch', watchEvents[event.type], normalize(relative(nuxt.options.srcDir, event.path)))
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
ignore: [
|
ignore: [
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { addDependency } from 'nypm'
|
import { addDependency } from 'nypm'
|
||||||
import { isPackageExists } from 'local-pkg'
|
import { resolvePackageJSON } from 'pkg-types'
|
||||||
import { logger } from '@nuxt/kit'
|
import { logger } from '@nuxt/kit'
|
||||||
import prompts from 'prompts'
|
import prompts from 'prompts'
|
||||||
|
|
||||||
export async function ensurePackageInstalled (rootDir: string, name: string, searchPaths?: string[]) {
|
export async function ensurePackageInstalled (rootDir: string, name: string, searchPaths?: string[]) {
|
||||||
if (isPackageExists(name, { paths: searchPaths })) {
|
if (await resolvePackageJSON(name, { url: searchPaths }).catch(() => null)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,6 @@ import escapeRE from 'escape-string-regexp'
|
|||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import fsExtra from 'fs-extra'
|
import fsExtra from 'fs-extra'
|
||||||
import { dynamicEventHandler } from 'h3'
|
import { dynamicEventHandler } from 'h3'
|
||||||
import { createHeadCore } from '@unhead/vue'
|
|
||||||
import { renderSSRHead } from '@unhead/ssr'
|
|
||||||
import type { Nuxt } from 'nuxt/schema'
|
import type { Nuxt } from 'nuxt/schema'
|
||||||
// @ts-expect-error TODO: add legacy type support for subpath imports
|
// @ts-expect-error TODO: add legacy type support for subpath imports
|
||||||
import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs'
|
import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs'
|
||||||
@ -205,12 +203,6 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
|||||||
// Resolve user-provided paths
|
// Resolve user-provided paths
|
||||||
nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!)
|
nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!)
|
||||||
|
|
||||||
// Add head chunk for SPA renders
|
|
||||||
const head = createHeadCore()
|
|
||||||
head.push(nuxt.options.app.head)
|
|
||||||
const headChunk = await renderSSRHead(head)
|
|
||||||
nitroConfig.virtual!['#head-static'] = `export default ${JSON.stringify(headChunk)}`
|
|
||||||
|
|
||||||
// Add fallback server for `ssr: false`
|
// Add fallback server for `ssr: false`
|
||||||
if (!nuxt.options.ssr) {
|
if (!nuxt.options.ssr) {
|
||||||
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
|
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
|
||||||
@ -281,6 +273,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
|||||||
// Init nitro
|
// Init nitro
|
||||||
const nitro = await createNitro(nitroConfig)
|
const nitro = await createNitro(nitroConfig)
|
||||||
|
|
||||||
|
// Set prerender-only options
|
||||||
|
nitro.options._config.storage ||= {}
|
||||||
|
nitro.options._config.storage['internal:nuxt:prerender'] = { driver: 'memory' }
|
||||||
|
nitro.options._config.storage['internal:nuxt:prerender:island'] = { driver: 'lruCache', max: 1000 }
|
||||||
|
nitro.options._config.storage['internal:nuxt:prerender:payload'] = { driver: 'lruCache', max: 1000 }
|
||||||
|
|
||||||
// Expose nitro to modules and kit
|
// Expose nitro to modules and kit
|
||||||
nuxt._nitro = nitro
|
nuxt._nitro = nitro
|
||||||
await nuxt.callHook('nitro:init', nitro)
|
await nuxt.callHook('nitro:init', nitro)
|
||||||
|
@ -180,7 +180,7 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
`${config.dir?.modules || 'modules'}/*/index{${nuxt.options.extensions.join(',')}}`
|
`${config.dir?.modules || 'modules'}/*/index{${nuxt.options.extensions.join(',')}}`
|
||||||
])
|
])
|
||||||
for (const mod of layerModules) {
|
for (const mod of layerModules) {
|
||||||
watchedPaths.add(relative(config.srcDir, mod))
|
watchedPaths.add(mod)
|
||||||
if (specifiedModules.has(mod)) { continue }
|
if (specifiedModules.has(mod)) { continue }
|
||||||
specifiedModules.add(mod)
|
specifiedModules.add(mod)
|
||||||
modulesToInstall.push(mod)
|
modulesToInstall.push(mod)
|
||||||
@ -200,7 +200,7 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
addComponent({
|
addComponent({
|
||||||
name: 'NuxtLayout',
|
name: 'NuxtLayout',
|
||||||
priority: 10, // built-in that we do not expect the user to override
|
priority: 10, // built-in that we do not expect the user to override
|
||||||
filePath: resolve(nuxt.options.appDir, 'components/layout')
|
filePath: resolve(nuxt.options.appDir, 'components/nuxt-layout')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add <NuxtErrorBoundary>
|
// Add <NuxtErrorBoundary>
|
||||||
@ -341,19 +341,25 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
|
|
||||||
await nuxt.callHook('modules:done')
|
await nuxt.callHook('modules:done')
|
||||||
|
|
||||||
nuxt.hooks.hook('builder:watch', (event, path) => {
|
nuxt.hooks.hook('builder:watch', (event, relativePath) => {
|
||||||
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
// Local module patterns
|
// Local module patterns
|
||||||
if (watchedPaths.has(path)) {
|
if (watchedPaths.has(path)) {
|
||||||
return nuxt.callHook('restart', { hard: true })
|
return nuxt.callHook('restart', { hard: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
// User provided patterns
|
// User provided patterns
|
||||||
|
const layerRelativePaths = nuxt.options._layers.map(l => relative(l.config.srcDir || l.cwd, path))
|
||||||
for (const pattern of nuxt.options.watch) {
|
for (const pattern of nuxt.options.watch) {
|
||||||
if (typeof pattern === 'string') {
|
if (typeof pattern === 'string') {
|
||||||
if (pattern === path) { return nuxt.callHook('restart') }
|
// Test (normalised) strings against absolute path and relative path to any layer `srcDir`
|
||||||
|
if (pattern === path || layerRelativePaths.includes(pattern)) { return nuxt.callHook('restart') }
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (pattern.test(path)) { return nuxt.callHook('restart') }
|
// Test regular expressions against path to _any_ layer `srcDir`
|
||||||
|
if (layerRelativePaths.some(p => pattern.test(p))) {
|
||||||
|
return nuxt.callHook('restart')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core Nuxt files: app.vue, error.vue and app.config.ts
|
// Core Nuxt files: app.vue, error.vue and app.config.ts
|
||||||
@ -405,6 +411,13 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nuxt Webpack Builder is currently opt-in
|
||||||
|
if (options.builder === '@nuxt/webpack-builder') {
|
||||||
|
if (!await import('./features').then(r => r.ensurePackageInstalled(options.rootDir, '@nuxt/webpack-builder', options.modulesDir))) {
|
||||||
|
logger.warn('Failed to install `@nuxt/webpack-builder`, please install it manually, or change the `builder` option to vite in `nuxt.config`')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add core modules
|
// Add core modules
|
||||||
options._modules.push(pagesModule, metaModule, componentsModule)
|
options._modules.push(pagesModule, metaModule, componentsModule)
|
||||||
options._modules.push([importsModule, {
|
options._modules.push([importsModule, {
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { createRenderer, renderResourceHeaders } from 'vue-bundle-renderer/runtime'
|
import {
|
||||||
|
createRenderer,
|
||||||
|
getPrefetchLinks,
|
||||||
|
getPreloadLinks,
|
||||||
|
getRequestDependencies,
|
||||||
|
renderResourceHeaders
|
||||||
|
} from 'vue-bundle-renderer/runtime'
|
||||||
import type { RenderResponse } from 'nitropack'
|
import type { RenderResponse } from 'nitropack'
|
||||||
import type { Manifest } from 'vite'
|
import type { Manifest } from 'vite'
|
||||||
import type { H3Event } from 'h3'
|
import type { H3Event } from 'h3'
|
||||||
@ -9,14 +15,17 @@ import destr from 'destr'
|
|||||||
import { joinURL, withoutTrailingSlash } from 'ufo'
|
import { joinURL, withoutTrailingSlash } from 'ufo'
|
||||||
import { renderToString as _renderToString } from 'vue/server-renderer'
|
import { renderToString as _renderToString } from 'vue/server-renderer'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
|
import { renderSSRHead } from '@unhead/ssr'
|
||||||
|
|
||||||
import { defineRenderHandler, getRouteRules, useRuntimeConfig } from '#internal/nitro'
|
import { defineRenderHandler, getRouteRules, useRuntimeConfig, useStorage } from '#internal/nitro'
|
||||||
import { useNitroApp } from '#internal/nitro/app'
|
import { useNitroApp } from '#internal/nitro/app'
|
||||||
|
|
||||||
|
import type { Link, Script } from '@unhead/vue'
|
||||||
|
import { createServerHead } from '@unhead/vue'
|
||||||
// eslint-disable-next-line import/no-restricted-paths
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import type { NuxtPayload, NuxtSSRContext } from '#app/nuxt'
|
import type { NuxtPayload, NuxtSSRContext } from '#app/nuxt'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { appRootId, appRootTag } from '#internal/nuxt.config.mjs'
|
import { appHead, appRootId, appRootTag } from '#internal/nuxt.config.mjs'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { buildAssetsURL, publicAssetsURL } from '#paths'
|
import { buildAssetsURL, publicAssetsURL } from '#paths'
|
||||||
|
|
||||||
@ -71,9 +80,6 @@ const getEntryIds: () => Promise<string[]> = () => getClientManifest().then(r =>
|
|||||||
r._globalCSS
|
r._globalCSS
|
||||||
).map(r => r.src!))
|
).map(r => r.src!))
|
||||||
|
|
||||||
// @ts-expect-error virtual file
|
|
||||||
const getStaticRenderedHead = (): Promise<NuxtMeta> => import('#head-static').then(r => r.default || r)
|
|
||||||
|
|
||||||
// @ts-expect-error file will be produced after app build
|
// @ts-expect-error file will be produced after app build
|
||||||
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
|
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
|
||||||
|
|
||||||
@ -140,7 +146,6 @@ const getSPARenderer = lazyCachedFunction(async () => {
|
|||||||
public: config.public,
|
public: config.public,
|
||||||
app: config.app
|
app: config.app
|
||||||
}
|
}
|
||||||
ssrContext!.renderMeta = ssrContext!.renderMeta ?? getStaticRenderedHead
|
|
||||||
return Promise.resolve(result)
|
return Promise.resolve(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,9 +155,18 @@ const getSPARenderer = lazyCachedFunction(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const payloadCache = process.env.prerender ? useStorage('internal:nuxt:prerender:payload') : null
|
||||||
|
const islandCache = process.env.prerender ? useStorage('internal:nuxt:prerender:island') : null
|
||||||
|
const islandPropCache = process.env.prerender ? useStorage('internal:nuxt:prerender:island-props') : null
|
||||||
|
|
||||||
async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
|
async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
|
||||||
// TODO: Strict validation for url
|
// TODO: Strict validation for url
|
||||||
const url = event.node.req.url?.substring('/__nuxt_island'.length + 1) || ''
|
let url = event.node.req.url || ''
|
||||||
|
if (process.env.prerender && event.node.req.url && await islandPropCache!.hasItem(event.node.req.url)) {
|
||||||
|
// rehydrate props from cache so we can rerender island if cache does not have it any more
|
||||||
|
url = await islandPropCache!.getItem(event.node.req.url) as string
|
||||||
|
}
|
||||||
|
url = url.substring('/__nuxt_island'.length + 1) || ''
|
||||||
const [componentName, hashId] = url.split('?')[0].split('_')
|
const [componentName, hashId] = url.split('?')[0].split('_')
|
||||||
|
|
||||||
// TODO: Validate context
|
// TODO: Validate context
|
||||||
@ -170,8 +184,6 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
|
|||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAYLOAD_CACHE = (process.env.NUXT_PAYLOAD_EXTRACTION && process.env.prerender) ? new Map() : null // TODO: Use LRU cache
|
|
||||||
const ISLAND_CACHE = (process.env.NUXT_COMPONENT_ISLANDS && process.env.prerender) ? new Map() : null // TODO: Use LRU cache
|
|
||||||
const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload(\.[a-zA-Z0-9]+)?.json(\?.*)?$/ : /\/_payload(\.[a-zA-Z0-9]+)?.js(\?.*)?$/
|
const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload(\.[a-zA-Z0-9]+)?.json(\?.*)?$/ : /\/_payload(\.[a-zA-Z0-9]+)?.js(\?.*)?$/
|
||||||
const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag} id="${appRootId}">([\\s\\S]*)</${appRootTag}>$`)
|
const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag} id="${appRootId}">([\\s\\S]*)</${appRootTag}>$`)
|
||||||
|
|
||||||
@ -201,8 +213,8 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
? await getIslandContext(event)
|
? await getIslandContext(event)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
if (process.env.prerender && islandContext && ISLAND_CACHE!.has(event.node.req.url)) {
|
if (process.env.prerender && islandContext && event.node.req.url && await islandCache!.hasItem(event.node.req.url)) {
|
||||||
return ISLAND_CACHE!.get(event.node.req.url)
|
return islandCache!.getItem(event.node.req.url) as Promise<Partial<RenderResponse>>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request url
|
// Request url
|
||||||
@ -213,14 +225,17 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
if (isRenderingPayload) {
|
if (isRenderingPayload) {
|
||||||
url = url.substring(0, url.lastIndexOf('/')) || '/'
|
url = url.substring(0, url.lastIndexOf('/')) || '/'
|
||||||
event.node.req.url = url
|
event.node.req.url = url
|
||||||
if (process.env.prerender && PAYLOAD_CACHE!.has(url)) {
|
if (process.env.prerender && await payloadCache!.hasItem(url)) {
|
||||||
return PAYLOAD_CACHE!.get(url)
|
return payloadCache!.getItem(url) as Promise<Partial<RenderResponse>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get route options (currently to apply `ssr: false`)
|
// Get route options (currently to apply `ssr: false`)
|
||||||
const routeOptions = getRouteRules(event)
|
const routeOptions = getRouteRules(event)
|
||||||
|
|
||||||
|
const head = createServerHead()
|
||||||
|
head.push(appHead)
|
||||||
|
|
||||||
// Initialize ssr context
|
// Initialize ssr context
|
||||||
const ssrContext: NuxtSSRContext = {
|
const ssrContext: NuxtSSRContext = {
|
||||||
url,
|
url,
|
||||||
@ -231,6 +246,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
event.context.nuxt?.noSSR ||
|
event.context.nuxt?.noSSR ||
|
||||||
routeOptions.ssr === false ||
|
routeOptions.ssr === false ||
|
||||||
(process.env.prerender ? PRERENDER_NO_SSR_ROUTES.has(url) : false),
|
(process.env.prerender ? PRERENDER_NO_SSR_ROUTES.has(url) : false),
|
||||||
|
head,
|
||||||
error: !!ssrError,
|
error: !!ssrError,
|
||||||
nuxt: undefined!, /* NuxtApp */
|
nuxt: undefined!, /* NuxtApp */
|
||||||
payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload,
|
payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload,
|
||||||
@ -276,7 +292,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
if (isRenderingPayload) {
|
if (isRenderingPayload) {
|
||||||
const response = renderPayloadResponse(ssrContext)
|
const response = renderPayloadResponse(ssrContext)
|
||||||
if (process.env.prerender) {
|
if (process.env.prerender) {
|
||||||
PAYLOAD_CACHE!.set(url, response)
|
await payloadCache!.setItem(url, response)
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@ -285,12 +301,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
// Hint nitro to prerender payload for this route
|
// Hint nitro to prerender payload for this route
|
||||||
appendResponseHeader(event, 'x-nitro-prerender', joinURL(url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js'))
|
appendResponseHeader(event, 'x-nitro-prerender', joinURL(url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js'))
|
||||||
// Use same ssr context to generate payload for this route
|
// Use same ssr context to generate payload for this route
|
||||||
PAYLOAD_CACHE!.set(withoutTrailingSlash(url), renderPayloadResponse(ssrContext))
|
await payloadCache!.setItem(withoutTrailingSlash(url), renderPayloadResponse(ssrContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render meta
|
|
||||||
const renderedMeta = await ssrContext.renderMeta?.() ?? {}
|
|
||||||
|
|
||||||
if (process.env.NUXT_INLINE_STYLES && !islandContext) {
|
if (process.env.NUXT_INLINE_STYLES && !islandContext) {
|
||||||
const source = ssrContext.modules ?? ssrContext._registeredComponents
|
const source = ssrContext.modules ?? ssrContext._registeredComponents
|
||||||
if (source) {
|
if (source) {
|
||||||
@ -303,45 +316,81 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
// Render inline styles
|
// Render inline styles
|
||||||
const inlinedStyles = (process.env.NUXT_INLINE_STYLES || Boolean(islandContext))
|
const inlinedStyles = (process.env.NUXT_INLINE_STYLES || Boolean(islandContext))
|
||||||
? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? [])
|
? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? [])
|
||||||
: ''
|
: []
|
||||||
|
|
||||||
const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.experimentalNoScripts
|
const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.experimentalNoScripts
|
||||||
|
|
||||||
|
// Setup head
|
||||||
|
const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext)
|
||||||
|
// 1.Extracted payload preloading
|
||||||
|
if (_PAYLOAD_EXTRACTION) {
|
||||||
|
head.push({
|
||||||
|
link: [
|
||||||
|
process.env.NUXT_JSON_PAYLOADS
|
||||||
|
? { rel: 'preload', as: 'fetch', crossorigin: 'anonymous', href: payloadURL }
|
||||||
|
: { rel: 'modulepreload', href: payloadURL }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Styles
|
||||||
|
head.push({
|
||||||
|
link: Object.values(styles)
|
||||||
|
.map(resource =>
|
||||||
|
({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) })
|
||||||
|
),
|
||||||
|
style: inlinedStyles
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!NO_SCRIPTS) {
|
||||||
|
// 3. Resource Hints
|
||||||
|
// TODO: add priorities based on Capo
|
||||||
|
head.push({
|
||||||
|
link: getPreloadLinks(ssrContext, renderer.rendererContext) as Link[]
|
||||||
|
})
|
||||||
|
head.push({
|
||||||
|
link: getPrefetchLinks(ssrContext, renderer.rendererContext) as Link[]
|
||||||
|
})
|
||||||
|
// 4. Payloads
|
||||||
|
head.push({
|
||||||
|
script: _PAYLOAD_EXTRACTION
|
||||||
|
? process.env.NUXT_JSON_PAYLOADS
|
||||||
|
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
||||||
|
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
||||||
|
: process.env.NUXT_JSON_PAYLOADS
|
||||||
|
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
|
||||||
|
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
|
||||||
|
}, {
|
||||||
|
// this should come before another end of body scripts
|
||||||
|
tagPosition: 'bodyClose',
|
||||||
|
tagPriority: 'high'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Scripts
|
||||||
|
if (!routeOptions.experimentalNoScripts) {
|
||||||
|
head.push({
|
||||||
|
script: Object.values(scripts).map(resource => (<Script> {
|
||||||
|
type: resource.module ? 'module' : null,
|
||||||
|
src: renderer.rendererContext.buildAssetsURL(resource.file),
|
||||||
|
defer: resource.module ? null : true,
|
||||||
|
crossorigin: ''
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove certain tags for nuxt islands
|
||||||
|
const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)
|
||||||
|
|
||||||
// Create render context
|
// Create render context
|
||||||
const htmlContext: NuxtRenderHTMLContext = {
|
const htmlContext: NuxtRenderHTMLContext = {
|
||||||
island: Boolean(islandContext),
|
island: Boolean(islandContext),
|
||||||
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
|
htmlAttrs: [htmlAttrs],
|
||||||
head: normalizeChunks([
|
head: normalizeChunks([headTags, ssrContext.styles]),
|
||||||
renderedMeta.headTags,
|
bodyAttrs: [bodyAttrs],
|
||||||
process.env.NUXT_JSON_PAYLOADS
|
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
|
||||||
? _PAYLOAD_EXTRACTION ? `<link rel="preload" as="fetch" crossorigin="anonymous" href="${payloadURL}">` : null
|
|
||||||
: _PAYLOAD_EXTRACTION ? `<link rel="modulepreload" href="${payloadURL}">` : null,
|
|
||||||
NO_SCRIPTS ? null : _rendered.renderResourceHints(),
|
|
||||||
_rendered.renderStyles(),
|
|
||||||
inlinedStyles,
|
|
||||||
ssrContext.styles
|
|
||||||
]),
|
|
||||||
bodyAttrs: normalizeChunks([renderedMeta.bodyAttrs!]),
|
|
||||||
bodyPrepend: normalizeChunks([
|
|
||||||
renderedMeta.bodyScriptsPrepend,
|
|
||||||
ssrContext.teleports?.body
|
|
||||||
]),
|
|
||||||
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceServerOnlyComponentsSlots(ssrContext, _rendered.html) : _rendered.html],
|
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceServerOnlyComponentsSlots(ssrContext, _rendered.html) : _rendered.html],
|
||||||
bodyAppend: normalizeChunks([
|
bodyAppend: [bodyTags]
|
||||||
NO_SCRIPTS
|
|
||||||
? undefined
|
|
||||||
: (_PAYLOAD_EXTRACTION
|
|
||||||
? process.env.NUXT_JSON_PAYLOADS
|
|
||||||
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
|
||||||
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
|
||||||
: process.env.NUXT_JSON_PAYLOADS
|
|
||||||
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
|
|
||||||
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
|
|
||||||
),
|
|
||||||
routeOptions.experimentalNoScripts ? undefined : _rendered.renderScripts(),
|
|
||||||
// Note: bodyScripts may contain tags other than <script>
|
|
||||||
renderedMeta.bodyScripts
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow hooking into the rendered result
|
// Allow hooking into the rendered result
|
||||||
@ -349,21 +398,21 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
|
|
||||||
// Response for component islands
|
// Response for component islands
|
||||||
if (process.env.NUXT_COMPONENT_ISLANDS && islandContext) {
|
if (process.env.NUXT_COMPONENT_ISLANDS && islandContext) {
|
||||||
const _tags = htmlContext.head.flatMap(head => extractHTMLTags(head))
|
const islandHead: NuxtIslandResponse['head'] = {
|
||||||
const head: NuxtIslandResponse['head'] = {
|
link: [],
|
||||||
link: _tags.filter(tag => tag.tagName === 'link' && tag.attrs.rel === 'stylesheet' && tag.attrs.href.includes('scoped') && !tag.attrs.href.includes('pages/')).map(tag => ({
|
style: []
|
||||||
key: 'island-link-' + hash(tag.attrs.href),
|
}
|
||||||
...tag.attrs
|
for (const tag of await head.resolveTags()) {
|
||||||
})),
|
if (tag.tag === 'link' && tag.props.rel === 'stylesheet' && tag.props.href.includes('scoped') && !tag.props.href.includes('pages/')) {
|
||||||
style: _tags.filter(tag => tag.tagName === 'style' && tag.innerHTML).map(tag => ({
|
islandHead.link.push({ ...tag.props, key: 'island-link-' + hash(tag.props.href) })
|
||||||
key: 'island-style-' + hash(tag.innerHTML),
|
}
|
||||||
innerHTML: tag.innerHTML
|
if (tag.tag === 'style' && tag.innerHTML) {
|
||||||
}))
|
islandHead.style.push({ key: 'island-style-' + hash(tag.innerHTML), innerHTML: tag.innerHTML })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const islandResponse: NuxtIslandResponse = {
|
const islandResponse: NuxtIslandResponse = {
|
||||||
id: islandContext.id,
|
id: islandContext.id,
|
||||||
head,
|
head: islandHead,
|
||||||
html: getServerComponentHTML(htmlContext.body),
|
html: getServerComponentHTML(htmlContext.body),
|
||||||
state: ssrContext.payload.state
|
state: ssrContext.payload.state
|
||||||
}
|
}
|
||||||
@ -380,7 +429,8 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
}
|
}
|
||||||
} satisfies RenderResponse
|
} satisfies RenderResponse
|
||||||
if (process.env.prerender) {
|
if (process.env.prerender) {
|
||||||
ISLAND_CACHE!.set(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, response)
|
await islandCache!.setItem(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, response)
|
||||||
|
await islandPropCache!.setItem(`/__nuxt_island/${islandContext!.name}_${islandContext!.id}`, event.node.req.url!)
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@ -429,33 +479,17 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) {
|
|||||||
</html>`
|
</html>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move to external library
|
|
||||||
const HTML_TAG_RE = /<(?<tag>[a-z]+)(?<rawAttrs> [^>]*)?>(?:(?<innerHTML>[\s\S]*?)<\/\k<tag>)?/g
|
|
||||||
const HTML_TAG_ATTR_RE = /(?<name>[a-z]+)="(?<value>[^"]*)"/g
|
|
||||||
function extractHTMLTags (html: string) {
|
|
||||||
const tags: { tagName: string, attrs: Record<string, string>, innerHTML: string }[] = []
|
|
||||||
for (const tagMatch of html.matchAll(HTML_TAG_RE)) {
|
|
||||||
const attrs: Record<string, string> = {}
|
|
||||||
for (const attrMatch of tagMatch.groups!.rawAttrs?.matchAll(HTML_TAG_ATTR_RE) || []) {
|
|
||||||
attrs[attrMatch.groups!.name] = attrMatch.groups!.value
|
|
||||||
}
|
|
||||||
const innerHTML = tagMatch.groups!.innerHTML || ''
|
|
||||||
tags.push({ tagName: tagMatch.groups!.tag, attrs, innerHTML })
|
|
||||||
}
|
|
||||||
return tags
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderInlineStyles (usedModules: Set<string> | string[]) {
|
async function renderInlineStyles (usedModules: Set<string> | string[]) {
|
||||||
const styleMap = await getSSRStyles()
|
const styleMap = await getSSRStyles()
|
||||||
const inlinedStyles = new Set<string>()
|
const inlinedStyles = new Set<string>()
|
||||||
for (const mod of usedModules) {
|
for (const mod of usedModules) {
|
||||||
if (mod in styleMap) {
|
if (mod in styleMap) {
|
||||||
for (const style of await styleMap[mod]()) {
|
for (const style of await styleMap[mod]()) {
|
||||||
inlinedStyles.add(`<style>${style}</style>`)
|
inlinedStyles.add(style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Array.from(inlinedStyles).join('')
|
return Array.from(inlinedStyles).map(style => ({ innerHTML: style }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
||||||
@ -472,25 +506,41 @@ function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
|||||||
} satisfies RenderResponse
|
} satisfies RenderResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }) {
|
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
|
||||||
const attrs = [
|
|
||||||
'type="application/json"',
|
|
||||||
`id="${opts.id}"`,
|
|
||||||
`data-ssr="${!(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)}"`,
|
|
||||||
opts.src ? `data-src="${opts.src}"` : ''
|
|
||||||
].filter(Boolean)
|
|
||||||
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
|
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
|
||||||
return `<script ${attrs.join(' ')}>${contents}</script>` +
|
const payload: Script = {
|
||||||
`<script>window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}</script>`
|
type: 'application/json',
|
||||||
|
id: opts.id,
|
||||||
|
innerHTML: contents,
|
||||||
|
'data-ssr': !(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)
|
||||||
|
}
|
||||||
|
if (opts.src) {
|
||||||
|
payload['data-src'] = opts.src
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
innerHTML: `window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}`
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }) {
|
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
|
||||||
opts.data.config = opts.ssrContext.config
|
opts.data.config = opts.ssrContext.config
|
||||||
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
|
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
|
||||||
if (_PAYLOAD_EXTRACTION) {
|
if (_PAYLOAD_EXTRACTION) {
|
||||||
return `<script type="module">import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})}</script>`
|
return [
|
||||||
|
{
|
||||||
|
type: 'module',
|
||||||
|
innerHTML: `import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})`
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
return `<script>window.__NUXT__=${devalue(opts.data)}</script>`
|
return [
|
||||||
|
{
|
||||||
|
innerHTML: `window.__NUXT__=${devalue(opts.data)}`
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitPayload (ssrContext: NuxtSSRContext) {
|
function splitPayload (ssrContext: NuxtSSRContext) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genSafeVariableName, genString } from 'knitwork'
|
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genSafeVariableName, genString } from 'knitwork'
|
||||||
import { isAbsolute, join, relative, resolve } from 'pathe'
|
import { isAbsolute, join, relative, resolve } from 'pathe'
|
||||||
|
import type { JSValue } from 'untyped'
|
||||||
import { generateTypes, resolveSchema } from 'untyped'
|
import { generateTypes, resolveSchema } from 'untyped'
|
||||||
import escapeRE from 'escape-string-regexp'
|
import escapeRE from 'escape-string-regexp'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
@ -144,7 +145,7 @@ export const schemaTemplate: NuxtTemplate<TemplateContext> = {
|
|||||||
),
|
),
|
||||||
modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName]) => `[${genString(importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
|
modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName]) => `[${genString(importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
|
||||||
' }',
|
' }',
|
||||||
generateTypes(await resolveSchema(Object.fromEntries(Object.entries(nuxt.options.runtimeConfig).filter(([key]) => key !== 'public'))),
|
generateTypes(await resolveSchema(Object.fromEntries(Object.entries(nuxt.options.runtimeConfig).filter(([key]) => key !== 'public')) as Record<string, JSValue>),
|
||||||
{
|
{
|
||||||
interfaceName: 'RuntimeConfig',
|
interfaceName: 'RuntimeConfig',
|
||||||
addExport: false,
|
addExport: false,
|
||||||
@ -152,7 +153,7 @@ export const schemaTemplate: NuxtTemplate<TemplateContext> = {
|
|||||||
allowExtraKeys: false,
|
allowExtraKeys: false,
|
||||||
indentation: 2
|
indentation: 2
|
||||||
}),
|
}),
|
||||||
generateTypes(await resolveSchema(nuxt.options.runtimeConfig.public),
|
generateTypes(await resolveSchema(nuxt.options.runtimeConfig.public as Record<string, JSValue>),
|
||||||
{
|
{
|
||||||
interfaceName: 'PublicRuntimeConfig',
|
interfaceName: 'PublicRuntimeConfig',
|
||||||
addExport: false,
|
addExport: false,
|
||||||
@ -328,6 +329,7 @@ export const nuxtConfigTemplate = {
|
|||||||
...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`),
|
...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`),
|
||||||
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
|
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
|
||||||
`export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`,
|
`export const componentIslands = ${!!ctx.nuxt.options.experimental.componentIslands}`,
|
||||||
|
`export const remoteComponentIslands = ${ctx.nuxt.options.experimental.componentIslands === 'local+remote'}`,
|
||||||
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
|
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
|
||||||
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`
|
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`
|
||||||
].join('\n\n')
|
].join('\n\n')
|
||||||
|
@ -54,6 +54,10 @@ export default defineNuxtModule({
|
|||||||
addPlugin({ src: resolve(runtimeDir, 'plugins/vueuse-head-polyfill') })
|
addPlugin({ src: resolve(runtimeDir, 'plugins/vueuse-head-polyfill') })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nuxt.options.experimental.headCapoPlugin) {
|
||||||
|
addPlugin({ src: resolve(runtimeDir, 'plugins/capo') })
|
||||||
|
}
|
||||||
|
|
||||||
// Add library-specific plugin
|
// Add library-specific plugin
|
||||||
addPlugin({ src: resolve(runtimeDir, 'plugins/unhead') })
|
addPlugin({ src: resolve(runtimeDir, 'plugins/unhead') })
|
||||||
}
|
}
|
||||||
|
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 { createHead as createClientHead } from '@unhead/vue'
|
||||||
import { renderSSRHead } from '@unhead/ssr'
|
|
||||||
import { defineNuxtPlugin } from '#app/nuxt'
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
// @ts-expect-error untyped
|
|
||||||
import { appHead } from '#build/nuxt.config.mjs'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin({
|
export default defineNuxtPlugin({
|
||||||
name: 'nuxt:head',
|
name: 'nuxt:head',
|
||||||
setup (nuxtApp) {
|
setup (nuxtApp) {
|
||||||
const createHead = process.server ? createServerHead : createClientHead
|
const head = process.server ? nuxtApp.ssrContext!.head : createClientHead()
|
||||||
const head = createHead()
|
// nuxt.config appHead is set server-side within the renderer
|
||||||
head.push(appHead)
|
|
||||||
|
|
||||||
nuxtApp.vueApp.use(head)
|
nuxtApp.vueApp.use(head)
|
||||||
|
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
@ -28,17 +23,5 @@ export default defineNuxtPlugin({
|
|||||||
// unpause the DOM once the mount suspense is resolved
|
// unpause the DOM once the mount suspense is resolved
|
||||||
nuxtApp.hooks.hook('app:suspense:resolve', unpauseDom)
|
nuxtApp.hooks.hook('app:suspense:resolve', unpauseDom)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.server) {
|
|
||||||
nuxtApp.ssrContext!.renderMeta = async () => {
|
|
||||||
const meta = await renderSSRHead(head)
|
|
||||||
return {
|
|
||||||
...meta,
|
|
||||||
bodyScriptsPrepend: meta.bodyTagsOpen,
|
|
||||||
// resolves naming difference with NuxtMeta and Unhead
|
|
||||||
bodyScripts: meta.bodyTags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -63,12 +63,12 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
|
|||||||
composablesDirs = composablesDirs.map(dir => normalize(dir))
|
composablesDirs = composablesDirs.map(dir => normalize(dir))
|
||||||
|
|
||||||
// Restart nuxt when composable directories are added/removed
|
// Restart nuxt when composable directories are added/removed
|
||||||
nuxt.hook('builder:watch', (event, path) => {
|
nuxt.hook('builder:watch', (event, relativePath) => {
|
||||||
const isDirChange = ['addDir', 'unlinkDir'].includes(event)
|
if (!['addDir', 'unlinkDir'].includes(event)) { return }
|
||||||
const fullPath = resolve(nuxt.options.srcDir, path)
|
|
||||||
|
|
||||||
if (isDirChange && composablesDirs.includes(fullPath)) {
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
console.info(`Directory \`${path}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
|
if (composablesDirs.includes(path)) {
|
||||||
|
console.info(`Directory \`${relativePath}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
|
||||||
return nuxt.callHook('restart')
|
return nuxt.callHook('restart')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -119,9 +119,9 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
|
|||||||
'imports.d.ts',
|
'imports.d.ts',
|
||||||
'imports.mjs'
|
'imports.mjs'
|
||||||
]
|
]
|
||||||
nuxt.hook('builder:watch', async (_, path) => {
|
nuxt.hook('builder:watch', async (_, relativePath) => {
|
||||||
const _resolved = resolve(nuxt.options.srcDir, path)
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
if (composablesDirs.find(dir => _resolved.startsWith(dir))) {
|
if (composablesDirs.some(dir => dir === path || path.startsWith(dir + '/'))) {
|
||||||
await updateTemplates({
|
await updateTemplates({
|
||||||
filter: template => templates.includes(template.filename)
|
filter: template => templates.includes(template.filename)
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,6 @@ import { mkdir, readFile } from 'node:fs/promises'
|
|||||||
import { addComponent, addPlugin, addTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, updateTemplates } from '@nuxt/kit'
|
import { addComponent, addPlugin, addTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, updateTemplates } from '@nuxt/kit'
|
||||||
import { dirname, join, relative, resolve } from 'pathe'
|
import { dirname, join, relative, resolve } from 'pathe'
|
||||||
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
|
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
|
||||||
import escapeRE from 'escape-string-regexp'
|
|
||||||
import { joinURL } from 'ufo'
|
import { joinURL } from 'ufo'
|
||||||
import type { NuxtApp, NuxtPage } from 'nuxt/schema'
|
import type { NuxtApp, NuxtPage } from 'nuxt/schema'
|
||||||
import { createRoutesContext } from 'unplugin-vue-router'
|
import { createRoutesContext } from 'unplugin-vue-router'
|
||||||
@ -52,12 +51,13 @@ export default defineNuxtModule({
|
|||||||
|
|
||||||
// Restart Nuxt when pages dir is added or removed
|
// Restart Nuxt when pages dir is added or removed
|
||||||
const restartPaths = nuxt.options._layers.flatMap(layer => [
|
const restartPaths = nuxt.options._layers.flatMap(layer => [
|
||||||
join(layer.config.srcDir, 'app/router.options.ts'),
|
join(layer.config.srcDir || layer.cwd, 'app/router.options.ts'),
|
||||||
join(layer.config.srcDir, layer.config.dir?.pages || 'pages')
|
join(layer.config.srcDir || layer.cwd, layer.config.dir?.pages || 'pages')
|
||||||
])
|
])
|
||||||
nuxt.hooks.hook('builder:watch', async (event, path) => {
|
|
||||||
const fullPath = join(nuxt.options.srcDir, path)
|
nuxt.hooks.hook('builder:watch', async (event, relativePath) => {
|
||||||
if (restartPaths.some(path => path === fullPath || fullPath.startsWith(path + '/'))) {
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
|
if (restartPaths.some(p => p === path || path.startsWith(p + '/'))) {
|
||||||
const newSetting = await isPagesEnabled()
|
const newSetting = await isPagesEnabled()
|
||||||
if (nuxt.options.pages !== newSetting) {
|
if (nuxt.options.pages !== newSetting) {
|
||||||
console.info('Pages', newSetting ? 'enabled' : 'disabled')
|
console.info('Pages', newSetting ? 'enabled' : 'disabled')
|
||||||
@ -174,15 +174,17 @@ export default defineNuxtModule({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Regenerate templates when adding or removing pages
|
// Regenerate templates when adding or removing pages
|
||||||
nuxt.hook('builder:watch', async (event, path) => {
|
const updateTemplatePaths = nuxt.options._layers.flatMap(l => [
|
||||||
const dirs = [
|
join(l.config.srcDir || l.cwd, l.config.dir?.pages || 'pages') + '/',
|
||||||
nuxt.options.dir.pages,
|
join(l.config.srcDir || l.cwd, l.config.dir?.layouts || 'layouts') + '/',
|
||||||
nuxt.options.dir.layouts,
|
join(l.config.srcDir || l.cwd, l.config.dir?.middleware || 'middleware') + '/'
|
||||||
nuxt.options.dir.middleware
|
])
|
||||||
].filter(Boolean)
|
|
||||||
|
|
||||||
const pathPattern = new RegExp(`(^|\\/)(${dirs.map(escapeRE).join('|')})/`)
|
nuxt.hook('builder:watch', async (event, relativePath) => {
|
||||||
if (event !== 'change' && pathPattern.test(path)) {
|
if (event === 'change') { return }
|
||||||
|
|
||||||
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
|
if (updateTemplatePaths.some(dir => path.startsWith(dir))) {
|
||||||
await updateTemplates({
|
await updateTemplates({
|
||||||
filter: template => template.filename === 'routes.mjs'
|
filter: template => template.filename === 'routes.mjs'
|
||||||
})
|
})
|
||||||
@ -351,11 +353,11 @@ export default defineNuxtModule({
|
|||||||
getContents: ({ app }: { app: NuxtApp }) => {
|
getContents: ({ app }: { app: NuxtApp }) => {
|
||||||
const composablesFile = resolve(runtimeDir, 'composables')
|
const composablesFile = resolve(runtimeDir, 'composables')
|
||||||
return [
|
return [
|
||||||
'import { ComputedRef, Ref } from \'vue\'',
|
'import { ComputedRef, MaybeRef } from \'vue\'',
|
||||||
`export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`,
|
`export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`,
|
||||||
`declare module ${genString(composablesFile)} {`,
|
`declare module ${genString(composablesFile)} {`,
|
||||||
' interface PageMeta {',
|
' interface PageMeta {',
|
||||||
' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>',
|
' layout?: MaybeRef<LayoutKey | false> | ComputedRef<LayoutKey | false>',
|
||||||
' }',
|
' }',
|
||||||
'}'
|
'}'
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
@ -14,7 +14,7 @@ export interface PageMeta {
|
|||||||
* statusCode/statusMessage to respond immediately with an error (other matches
|
* statusCode/statusMessage to respond immediately with an error (other matches
|
||||||
* will not be checked).
|
* will not be checked).
|
||||||
*/
|
*/
|
||||||
validate?: (route: RouteLocationNormalized) => boolean | Promise<boolean> | Partial<NuxtError> | Promise<Partial<NuxtError>>
|
validate?: (route: RouteLocationNormalized) => boolean | Partial<NuxtError> | Promise<boolean | Partial<NuxtError>>
|
||||||
/**
|
/**
|
||||||
* Where to redirect if the route is directly matched. The redirection happens
|
* Where to redirect if the route is directly matched. The redirection happens
|
||||||
* before any navigation guard and triggers a new navigation with the new
|
* before any navigation guard and triggers a new navigation with the new
|
||||||
@ -36,7 +36,7 @@ export interface PageMeta {
|
|||||||
/** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */
|
/** You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. */
|
||||||
path?: string
|
path?: string
|
||||||
/** Set to `false` to avoid scrolling to top on page navigations */
|
/** Set to `false` to avoid scrolling to top on page navigations */
|
||||||
scrollToTop?: boolean
|
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
|
@ -20,8 +20,10 @@ export default <RouterConfig> {
|
|||||||
// savedPosition is only available for popstate navigations (back button)
|
// savedPosition is only available for popstate navigations (back button)
|
||||||
let position: ScrollPosition = savedPosition || undefined
|
let position: ScrollPosition = savedPosition || undefined
|
||||||
|
|
||||||
|
const routeAllowsScrollToTop = typeof to.meta.scrollToTop === 'function' ? to.meta.scrollToTop(to, from) : to.meta.scrollToTop
|
||||||
|
|
||||||
// Scroll to top if route is changed by default
|
// Scroll to top if route is changed by default
|
||||||
if (!position && from && to && to.meta.scrollToTop !== false && _isDifferentRoute(from, to)) {
|
if (!position && from && to && routeAllowsScrollToTop !== false && _isDifferentRoute(from, to)) {
|
||||||
position = { left: 0, top: 0 }
|
position = { left: 0, top: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
packages/nuxt/types.d.ts
vendored
3
packages/nuxt/types.d.ts
vendored
@ -1,12 +1,13 @@
|
|||||||
/// <reference types="nitropack" />
|
/// <reference types="nitropack" />
|
||||||
export * from './dist/index'
|
export * from './dist/index'
|
||||||
|
|
||||||
|
import type { DefineNuxtConfig } from 'nuxt/config'
|
||||||
import type { SchemaDefinition, RuntimeConfig } from 'nuxt/schema'
|
import type { SchemaDefinition, RuntimeConfig } from 'nuxt/schema'
|
||||||
import type { H3Event } from 'h3'
|
import type { H3Event } from 'h3'
|
||||||
import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from './dist/core/runtime/nitro/renderer'
|
import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from './dist/core/runtime/nitro/renderer'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const defineNuxtConfig: typeof import('nuxt/config')['defineNuxtConfig']
|
const defineNuxtConfig: DefineNuxtConfig
|
||||||
const defineNuxtSchema: (schema: SchemaDefinition) => SchemaDefinition
|
const defineNuxtSchema: (schema: SchemaDefinition) => SchemaDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,34 +30,35 @@
|
|||||||
"@types/file-loader": "5.0.1",
|
"@types/file-loader": "5.0.1",
|
||||||
"@types/pug": "2.0.6",
|
"@types/pug": "2.0.6",
|
||||||
"@types/sass-loader": "8.0.5",
|
"@types/sass-loader": "8.0.5",
|
||||||
"@unhead/schema": "1.1.32",
|
"@unhead/schema": "1.2.2",
|
||||||
"@vitejs/plugin-vue": "4.2.3",
|
"@vitejs/plugin-vue": "4.2.3",
|
||||||
"@vitejs/plugin-vue-jsx": "3.0.1",
|
"@vitejs/plugin-vue-jsx": "3.0.1",
|
||||||
"@vue/compiler-core": "3.3.4",
|
"@vue/compiler-core": "3.3.4",
|
||||||
"esbuild-loader": "3.0.1",
|
"esbuild-loader": "3.1.0",
|
||||||
"h3": "1.7.1",
|
"h3": "1.7.1",
|
||||||
"ignore": "5.2.4",
|
"ignore": "5.2.4",
|
||||||
"nitropack": "2.5.2",
|
"nitropack": "2.5.2",
|
||||||
"unbuild": "latest",
|
"unbuild": "latest",
|
||||||
"unctx": "2.3.1",
|
"unctx": "2.3.1",
|
||||||
"vite": "4.4.7",
|
"vite": "4.4.8",
|
||||||
"vue": "3.3.4",
|
"vue": "3.3.4",
|
||||||
"vue-bundle-renderer": "1.0.3",
|
"vue-bundle-renderer": "2.0.0",
|
||||||
"vue-loader": "17.2.2",
|
"vue-loader": "17.2.2",
|
||||||
"vue-router": "4.2.4",
|
"vue-router": "4.2.4",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.88.2",
|
||||||
"webpack-dev-middleware": "6.1.1"
|
"webpack-dev-middleware": "6.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nuxt/ui-templates": "^1.3.1",
|
||||||
"defu": "^6.1.2",
|
"defu": "^6.1.2",
|
||||||
"hookable": "^5.5.3",
|
"hookable": "^5.5.3",
|
||||||
"pathe": "^1.1.1",
|
"pathe": "^1.1.1",
|
||||||
"pkg-types": "^1.0.3",
|
"pkg-types": "^1.0.3",
|
||||||
"postcss-import-resolver": "^2.0.0",
|
"postcss-import-resolver": "^2.0.0",
|
||||||
"std-env": "^3.3.3",
|
"std-env": "^3.3.3",
|
||||||
"ufo": "^1.1.2",
|
"ufo": "^1.2.0",
|
||||||
"unimport": "^3.1.0",
|
"unimport": "^3.1.3",
|
||||||
"untyped": "^1.3.2"
|
"untyped": "^1.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.10.0"
|
"node": "^14.18.0 || >=16.10.0"
|
||||||
|
@ -365,8 +365,9 @@ export default defineUntypedSchema({
|
|||||||
/**
|
/**
|
||||||
* The watch property lets you define patterns that will restart the Nuxt dev server when changed.
|
* The watch property lets you define patterns that will restart the Nuxt dev server when changed.
|
||||||
*
|
*
|
||||||
* It is an array of strings or regular expressions, which will be matched against the file path
|
* It is an array of strings or regular expressions. Strings should be either absolute paths or
|
||||||
* relative to the project `srcDir`.
|
* relative to the `srcDir` (and the `srcDir` of any layers). Regular expressions will be matched
|
||||||
|
* against the path relative to the project `srcDir` (and the `srcDir` of any layers).
|
||||||
*
|
*
|
||||||
* @type {Array<string | RegExp>}
|
* @type {Array<string | RegExp>}
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { defineUntypedSchema } from 'untyped'
|
import { defineUntypedSchema } from 'untyped'
|
||||||
|
import { loading as loadingTemplate } from '@nuxt/ui-templates'
|
||||||
|
|
||||||
export default defineUntypedSchema({
|
export default defineUntypedSchema({
|
||||||
devServer: {
|
devServer: {
|
||||||
@ -36,5 +37,12 @@ export default defineUntypedSchema({
|
|||||||
* dev server with the full URL (for module and internal use).
|
* dev server with the full URL (for module and internal use).
|
||||||
*/
|
*/
|
||||||
url: 'http://localhost:3000',
|
url: 'http://localhost:3000',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template to show a loading screen
|
||||||
|
*
|
||||||
|
* @type {(data: { loading?: string }) => string}
|
||||||
|
*/
|
||||||
|
loadingTemplate: loadingTemplate
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -19,7 +19,7 @@ export default defineUntypedSchema({
|
|||||||
*/
|
*/
|
||||||
reactivityTransform: false,
|
reactivityTransform: false,
|
||||||
|
|
||||||
// TODO: Remove in v3.6 when nitro has support for mocking traced dependencies
|
// TODO: Remove in v3.8 when nitro has support for mocking traced dependencies
|
||||||
// https://github.com/unjs/nitro/issues/1118
|
// https://github.com/unjs/nitro/issues/1118
|
||||||
/**
|
/**
|
||||||
* Externalize `vue`, `@vue/*` and `vue-router` when building.
|
* Externalize `vue`, `@vue/*` and `vue-router` when building.
|
||||||
@ -137,8 +137,15 @@ export default defineUntypedSchema({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Experimental component islands support with <NuxtIsland> and .island.vue files.
|
* Experimental component islands support with <NuxtIsland> and .island.vue files.
|
||||||
|
* @type {true | 'local' | 'local+remote' | false}
|
||||||
*/
|
*/
|
||||||
componentIslands: false,
|
componentIslands: {
|
||||||
|
$resolve: (val) => {
|
||||||
|
if (typeof val === 'string') { return val }
|
||||||
|
if (val === true) { return 'local' }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config schema support
|
* Config schema support
|
||||||
@ -200,6 +207,13 @@ export default defineUntypedSchema({
|
|||||||
* @see https://github.com/parcel-bundler/watcher
|
* @see https://github.com/parcel-bundler/watcher
|
||||||
* @type {'chokidar' | 'parcel' | 'chokidar-granular'}
|
* @type {'chokidar' | 'parcel' | 'chokidar-granular'}
|
||||||
*/
|
*/
|
||||||
watcher: 'chokidar-granular'
|
watcher: 'chokidar-granular',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the capo.js head plugin in order to render tags in of the head in a more performant way.
|
||||||
|
*
|
||||||
|
* @see https://rviscomi.github.io/capo.js/user/rules/
|
||||||
|
*/
|
||||||
|
headCapoPlugin: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -123,7 +123,7 @@ export interface NuxtHooks {
|
|||||||
* @param app The configured `NuxtApp` object
|
* @param app The configured `NuxtApp` object
|
||||||
* @returns Promise
|
* @returns Promise
|
||||||
*/
|
*/
|
||||||
'app:templatesGenerated': (app: NuxtApp) => HookResult
|
'app:templatesGenerated': (app: NuxtApp, templates: ResolvedNuxtTemplate[], options?: GenerateAppOptions) => HookResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called before Nuxt bundle builder.
|
* Called before Nuxt bundle builder.
|
||||||
|
@ -26,15 +26,15 @@
|
|||||||
"@nuxt/schema": "workspace:../schema",
|
"@nuxt/schema": "workspace:../schema",
|
||||||
"consola": "^3.2.3",
|
"consola": "^3.2.3",
|
||||||
"defu": "^6.1.2",
|
"defu": "^6.1.2",
|
||||||
"execa": "^7.1.1",
|
"execa": "^7.2.0",
|
||||||
"get-port-please": "^3.0.1",
|
"get-port-please": "^3.0.1",
|
||||||
"ofetch": "^1.1.1",
|
"ofetch": "^1.1.1",
|
||||||
"pathe": "^1.1.1",
|
"pathe": "^1.1.1",
|
||||||
"ufo": "^1.1.2"
|
"ufo": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.6.1",
|
"@jest/globals": "29.6.2",
|
||||||
"playwright-core": "1.36.1",
|
"playwright-core": "1.36.2",
|
||||||
"unbuild": "latest",
|
"unbuild": "latest",
|
||||||
"vitest": "0.33.0"
|
"vitest": "0.33.0"
|
||||||
},
|
},
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
"consola": "^3.2.3",
|
"consola": "^3.2.3",
|
||||||
"cssnano": "^6.0.1",
|
"cssnano": "^6.0.1",
|
||||||
"defu": "^6.1.2",
|
"defu": "^6.1.2",
|
||||||
"esbuild": "^0.18.16",
|
"esbuild": "^0.18.17",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"externality": "^1.0.2",
|
"externality": "^1.0.2",
|
||||||
@ -43,7 +43,7 @@
|
|||||||
"get-port-please": "^3.0.1",
|
"get-port-please": "^3.0.1",
|
||||||
"h3": "^1.7.1",
|
"h3": "^1.7.1",
|
||||||
"knitwork": "^1.0.0",
|
"knitwork": "^1.0.0",
|
||||||
"magic-string": "^0.30.1",
|
"magic-string": "^0.30.2",
|
||||||
"mlly": "^1.4.0",
|
"mlly": "^1.4.0",
|
||||||
"ohash": "^1.1.2",
|
"ohash": "^1.1.2",
|
||||||
"pathe": "^1.1.1",
|
"pathe": "^1.1.1",
|
||||||
@ -54,13 +54,13 @@
|
|||||||
"postcss-url": "^10.1.3",
|
"postcss-url": "^10.1.3",
|
||||||
"rollup-plugin-visualizer": "^5.9.2",
|
"rollup-plugin-visualizer": "^5.9.2",
|
||||||
"std-env": "^3.3.3",
|
"std-env": "^3.3.3",
|
||||||
"strip-literal": "^1.0.1",
|
"strip-literal": "^1.3.0",
|
||||||
"ufo": "^1.1.2",
|
"ufo": "^1.2.0",
|
||||||
"unplugin": "^1.4.0",
|
"unplugin": "^1.4.0",
|
||||||
"vite": "^4.4.7",
|
"vite": "^4.4.8",
|
||||||
"vite-node": "^0.33.0",
|
"vite-node": "^0.33.0",
|
||||||
"vite-plugin-checker": "^0.6.1",
|
"vite-plugin-checker": "^0.6.1",
|
||||||
"vue-bundle-renderer": "^1.0.3"
|
"vue-bundle-renderer": "^2.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.3.4"
|
"vue": "^3.3.4"
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
"css-minimizer-webpack-plugin": "^5.0.1",
|
"css-minimizer-webpack-plugin": "^5.0.1",
|
||||||
"cssnano": "^6.0.1",
|
"cssnano": "^6.0.1",
|
||||||
"defu": "^6.1.2",
|
"defu": "^6.1.2",
|
||||||
"esbuild-loader": "^3.0.1",
|
"esbuild-loader": "^3.1.0",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
@ -35,7 +35,7 @@
|
|||||||
"h3": "^1.7.1",
|
"h3": "^1.7.1",
|
||||||
"hash-sum": "^2.0.0",
|
"hash-sum": "^2.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"magic-string": "^0.30.1",
|
"magic-string": "^0.30.2",
|
||||||
"memfs": "^4.2.0",
|
"memfs": "^4.2.0",
|
||||||
"mini-css-extract-plugin": "^2.7.6",
|
"mini-css-extract-plugin": "^2.7.6",
|
||||||
"mlly": "^1.4.0",
|
"mlly": "^1.4.0",
|
||||||
@ -49,10 +49,10 @@
|
|||||||
"pug-plain-loader": "^1.1.0",
|
"pug-plain-loader": "^1.1.0",
|
||||||
"std-env": "^3.3.3",
|
"std-env": "^3.3.3",
|
||||||
"time-fix-plugin": "^2.0.7",
|
"time-fix-plugin": "^2.0.7",
|
||||||
"ufo": "^1.1.2",
|
"ufo": "^1.2.0",
|
||||||
"unplugin": "^1.4.0",
|
"unplugin": "^1.4.0",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"vue-bundle-renderer": "^1.0.3",
|
"vue-bundle-renderer": "^2.0.0",
|
||||||
"vue-loader": "^17.2.2",
|
"vue-loader": "^17.2.2",
|
||||||
"webpack": "^5.88.2",
|
"webpack": "^5.88.2",
|
||||||
"webpack-bundle-analyzer": "^4.9.0",
|
"webpack-bundle-analyzer": "^4.9.0",
|
||||||
|
3562
pnpm-lock.yaml
3562
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -2,3 +2,4 @@ packages:
|
|||||||
- "packages/**"
|
- "packages/**"
|
||||||
- "playground"
|
- "playground"
|
||||||
- "test/fixtures/*"
|
- "test/fixtures/*"
|
||||||
|
- ".website"
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
"main"
|
"main"
|
||||||
],
|
],
|
||||||
"ignoreDeps": [
|
"ignoreDeps": [
|
||||||
"typescript",
|
|
||||||
"markdownlint-cli",
|
"markdownlint-cli",
|
||||||
"nuxt",
|
"nuxt",
|
||||||
"nuxt3",
|
"nuxt3",
|
||||||
|
@ -10,10 +10,12 @@ async function main () {
|
|||||||
const date = Math.round(Date.now() / (1000 * 60))
|
const date = Math.round(Date.now() / (1000 * 60))
|
||||||
|
|
||||||
const nuxtPkg = workspace.find('nuxt')
|
const nuxtPkg = workspace.find('nuxt')
|
||||||
const nitroInfo = await $fetch('https://registry.npmjs.org/nitropack-edge')
|
const { version: latestNitro } = await $fetch<{ version: string }>('https://registry.npmjs.org/nitropack-edge/latest')
|
||||||
const latestNitro = nitroInfo['dist-tags'].latest
|
|
||||||
nuxtPkg.data.dependencies.nitropack = `npm:nitropack-edge@^${latestNitro}`
|
nuxtPkg.data.dependencies.nitropack = `npm:nitropack-edge@^${latestNitro}`
|
||||||
|
|
||||||
|
const { version: latestNuxi } = await $fetch<{ version: string }>('https://registry.npmjs.org/nuxi-ng/latest')
|
||||||
|
nuxtPkg.data.dependencies.nuxi = `npm:nuxi-ng@^${latestNuxi}`
|
||||||
|
|
||||||
const bumpType = await determineBumpType()
|
const bumpType = await determineBumpType()
|
||||||
|
|
||||||
for (const pkg of workspace.packages.filter(p => !p.data.private)) {
|
for (const pkg of workspace.packages.filter(p => !p.data.private)) {
|
||||||
|
@ -105,6 +105,7 @@ describe('pages', () => {
|
|||||||
// should import JSX/TSX components with custom elements
|
// should import JSX/TSX components with custom elements
|
||||||
expect(html).toContain('TSX component')
|
expect(html).toContain('TSX component')
|
||||||
expect(html).toContain('<custom-component>custom</custom-component>')
|
expect(html).toContain('<custom-component>custom</custom-component>')
|
||||||
|
expect(html).toContain('Sugar Counter 12 x 2 = 24')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('respects aliases in page metadata', async () => {
|
it('respects aliases in page metadata', async () => {
|
||||||
@ -1364,7 +1365,6 @@ describe.skipIf(isDev() || isWebpack)('inlining component styles', () => {
|
|||||||
const html: string = await $fetch('/styles')
|
const html: string = await $fetch('/styles')
|
||||||
expect(html.match(/<link [^>]*href="[^"]*\.css">/g)?.filter(m => m.includes('entry'))?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(`
|
expect(html.match(/<link [^>]*href="[^"]*\.css">/g)?.filter(m => m.includes('entry'))?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
"<link rel=\\"preload\\" as=\\"style\\" href=\\"/_nuxt/entry.css\\">",
|
|
||||||
"<link rel=\\"stylesheet\\" href=\\"/_nuxt/entry.css\\">",
|
"<link rel=\\"stylesheet\\" href=\\"/_nuxt/entry.css\\">",
|
||||||
]
|
]
|
||||||
`)
|
`)
|
||||||
@ -1418,6 +1418,43 @@ describe('server components/islands', () => {
|
|||||||
await page.close()
|
await page.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('lazy server components', async () => {
|
||||||
|
const page = await createPage('/server-components/lazy/start')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
await page.getByText('Go to page with lazy server component').click()
|
||||||
|
|
||||||
|
const text = await page.innerText('pre')
|
||||||
|
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id=\\"fallback\\"> Loading server component </section><section id=\\"no-fallback\\"><div></div></section>"')
|
||||||
|
expect(text).not.toContain('async component that was very long')
|
||||||
|
expect(text).toContain('Loading server component')
|
||||||
|
|
||||||
|
// Wait for all pending micro ticks to be cleared
|
||||||
|
// await page.waitForLoadState('networkidle')
|
||||||
|
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
|
||||||
|
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
|
||||||
|
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('non-lazy server components', async () => {
|
||||||
|
const page = await createPage('/server-components/lazy/start')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
await page.getByText('Go to page without lazy server component').click()
|
||||||
|
|
||||||
|
const text = await page.innerText('pre')
|
||||||
|
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id=\\"fallback\\"><div nuxt-ssr-component-uid=\\"0\\"> This is a .server (20ms) async component that was very long ... <div id=\\"async-server-component-count\\">42</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div></div></section><section id=\\"no-fallback\\"><div nuxt-ssr-component-uid=\\"1\\"> This is a .server (20ms) async component that was very long ... <div id=\\"async-server-component-count\\">42</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div></div></section>"')
|
||||||
|
expect(text).toContain('async component that was very long')
|
||||||
|
|
||||||
|
// Wait for all pending micro ticks to be cleared
|
||||||
|
// await page.waitForLoadState('networkidle')
|
||||||
|
// await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
|
||||||
|
await page.waitForFunction(() => (document.querySelector('#no-fallback') as HTMLElement)?.innerText?.includes('async component'))
|
||||||
|
await page.waitForFunction(() => (document.querySelector('#fallback') as HTMLElement)?.innerText?.includes('async component'))
|
||||||
|
|
||||||
|
await page.close()
|
||||||
|
})
|
||||||
|
|
||||||
it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => {
|
it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => {
|
||||||
// @ts-expect-error ssssh! untyped secret property
|
// @ts-expect-error ssssh! untyped secret property
|
||||||
const publicDir = useTestContext().nuxt._nitro.options.output.publicDir
|
const publicDir = useTestContext().nuxt._nitro.options.output.publicDir
|
||||||
@ -1648,7 +1685,7 @@ describe('component islands', () => {
|
|||||||
"link": [],
|
"link": [],
|
||||||
"style": [],
|
"style": [],
|
||||||
},
|
},
|
||||||
"html": "<div nuxt-ssr-component-uid><div> count is above 2 </div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"default\\"></div> that was very long ... <div id=\\"long-async-component-count\\">3</div><div style=\\"display:contents;\\" nuxt-ssr-slot-name=\\"test\\" nuxt-ssr-slot-data=\\"[{"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": {},
|
"state": {},
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
@ -19,7 +19,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
for (const outputDir of ['.output', '.output-inline']) {
|
for (const outputDir of ['.output', '.output-inline']) {
|
||||||
it('default client bundle size', async () => {
|
it('default client bundle size', async () => {
|
||||||
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
|
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
|
||||||
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.3k"')
|
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.4k"')
|
||||||
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
"_nuxt/entry.js",
|
"_nuxt/entry.js",
|
||||||
@ -35,7 +35,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.4k"')
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.4k"')
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2330k"')
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2346k"')
|
||||||
|
|
||||||
const packages = modules.files
|
const packages = modules.files
|
||||||
.filter(m => m.endsWith('package.json'))
|
.filter(m => m.endsWith('package.json'))
|
||||||
@ -95,7 +95,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"370k"')
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"370k"')
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"591k"')
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"608k"')
|
||||||
|
|
||||||
const packages = modules.files
|
const packages = modules.files
|
||||||
.filter(m => m.endsWith('package.json'))
|
.filter(m => m.endsWith('package.json'))
|
||||||
|
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: [
|
modules: [
|
||||||
function (_, nuxt) {
|
|
||||||
// TODO: remove in v3.7
|
|
||||||
if (process.env.TS_BASE_URL === 'without-base-url') {
|
|
||||||
nuxt.hook('prepare:types', ({ tsConfig }) => {
|
|
||||||
delete tsConfig.compilerOptions!.baseUrl
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function () {
|
function () {
|
||||||
addTypeTemplate({
|
addTypeTemplate({
|
||||||
filename: 'test.d.ts',
|
filename: 'test.d.ts',
|
||||||
|
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', () => {
|
it('works with useFetch', () => {
|
||||||
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
|
expectTypeOf(useFetch('/api/hello').data).toEqualTypeOf<Ref<string | null>>()
|
||||||
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
expectTypeOf(useFetch('/api/hey').data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
||||||
// @ts-expect-error TODO: remove when fixed upstream: https://github.com/unjs/nitro/pull/1247
|
|
||||||
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
expectTypeOf(useFetch('/api/hey', { method: 'GET' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
||||||
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
expectTypeOf(useFetch('/api/hey', { method: 'get' }).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | null>>()
|
||||||
|
expectTypeOf(useFetch('/api/hey', { method: 'POST' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
|
||||||
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
|
expectTypeOf(useFetch('/api/hey', { method: 'post' }).data).toEqualTypeOf<Ref<{ method: 'post' } | null>>()
|
||||||
// @ts-expect-error not a valid method
|
// @ts-expect-error not a valid method
|
||||||
useFetch('/api/hey', { method: 'PATCH' })
|
useFetch('/api/hey', { method: 'PATCH' })
|
||||||
@ -116,6 +116,21 @@ describe('middleware', () => {
|
|||||||
abortNavigation(true)
|
abortNavigation(true)
|
||||||
}, { global: true })
|
}, { global: true })
|
||||||
})
|
})
|
||||||
|
it('handles return types of validate', () => {
|
||||||
|
definePageMeta({
|
||||||
|
validate: async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (0) {
|
||||||
|
return createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'resource-type-not-found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('typed router integration', () => {
|
describe('typed router integration', () => {
|
||||||
|
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>
|
return <div>
|
||||||
TSX component
|
TSX component
|
||||||
<custom-component>custom</custom-component>
|
<custom-component>custom</custom-component>
|
||||||
|
<SugarCounter multiplier={2} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
<div id="long-async-component-count">
|
<div id="long-async-component-count">
|
||||||
{{ count }}
|
{{ count }}
|
||||||
</div>
|
</div>
|
||||||
|
{{ headers['custom-head'] }}
|
||||||
<slot name="test" :count="count" />
|
<slot name="test" :count="count" />
|
||||||
<p>hello world !!!</p>
|
<p>hello world !!!</p>
|
||||||
<slot v-for="(t, index) in 3" name="hello" :t="t">
|
<slot v-for="(t, index) in 3" name="hello" :t="t">
|
||||||
@ -28,8 +29,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { getResponseHeaders } from 'h3'
|
||||||
defineProps<{
|
defineProps<{
|
||||||
count: number
|
count: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const evt = useRequestEvent()
|
||||||
|
const headers = getResponseHeaders(evt)
|
||||||
const { data } = await useFetch('/api/very-long-request')
|
const { data } = await useFetch('/api/very-long-request')
|
||||||
</script>
|
</script>
|
||||||
|
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,
|
componentIslands: true,
|
||||||
reactivityTransform: true,
|
reactivityTransform: true,
|
||||||
treeshakeClientOnly: true,
|
treeshakeClientOnly: true,
|
||||||
payloadExtraction: true
|
payloadExtraction: true,
|
||||||
|
headCapoPlugin: true
|
||||||
},
|
},
|
||||||
appConfig: {
|
appConfig: {
|
||||||
fromNuxtConfig: true,
|
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