Merge remote-tracking branch 'origin/main' into perf/unusedplugin

This commit is contained in:
julien huang 2024-04-04 20:09:04 +02:00
commit 3e4d39ecbe
227 changed files with 6160 additions and 2966 deletions

168
.eslintrc
View File

@ -1,168 +0,0 @@
{
"$schema": "https://json.schemastore.org/eslintrc",
"ignorePatterns": [
"dist",
"public",
"node_modules",
"packages/schema/schema"
],
"globals": {
"NodeJS": true,
"$fetch": true
},
"plugins": ["jsdoc", "import", "unicorn", "no-only-tests"],
"extends": [
"plugin:jsdoc/recommended",
"@nuxt/eslint-config",
"plugin:import/typescript"
],
"rules": {
"sort-imports": [
"error",
{
"ignoreDeclarationSort": true
}
],
"no-only-tests/no-only-tests": "error",
"unicorn/prefer-node-protocol": "error",
"no-console": "warn",
"vue/one-component-per-file": "off",
"vue/require-default-prop": "off",
// Vue stylistic rules from `@antfu/eslint-config`
"vue/array-bracket-spacing": ["error", "never"],
"vue/arrow-spacing": ["error", { "after": true, "before": true }],
"vue/block-spacing": ["error", "always"],
"vue/block-tag-newline": [
"error",
{
"multiline": "always",
"singleline": "always"
}
],
"vue/brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
"vue/comma-dangle": ["error", "always-multiline"],
"vue/comma-spacing": ["error", { "after": true, "before": false }],
"vue/comma-style": ["error", "last"],
"vue/html-comment-content-spacing": [
"error",
"always",
{
"exceptions": ["-"]
}
],
"vue/key-spacing": ["error", { "afterColon": true, "beforeColon": false }],
"vue/keyword-spacing": ["error", { "after": true, "before": true }],
"vue/object-curly-newline": "off",
"vue/object-curly-spacing": ["error", "always"],
"vue/object-property-newline": [
"error",
{ "allowMultiplePropertiesPerLine": true }
],
"vue/operator-linebreak": ["error", "before"],
"vue/padding-line-between-blocks": ["error", "always"],
"vue/quote-props": ["error", "consistent-as-needed"],
"vue/space-in-parens": ["error", "never"],
"vue/template-curly-spacing": "error",
"jsdoc/require-jsdoc": "off",
"jsdoc/require-param": "off",
"jsdoc/require-returns": "off",
"jsdoc/require-param-type": "off",
"import/order": [
"error",
{
"pathGroups": [
{
"pattern": "#vue-router",
"group": "external"
}
]
}
],
"import/no-restricted-paths": [
"error",
{
"zones": [
{
"from": "packages/nuxt/src/!(core)/**/*",
"target": "packages/nuxt/src/core",
"message": "core should not directly import from modules."
},
{
"from": "packages/nuxt/src/!(app)/**/*",
"target": "packages/nuxt/src/app",
"message": "app should not directly import from modules."
},
{
"from": "packages/nuxt/src/app/**/index.ts",
"target": "packages/nuxt/src",
"message": "should not import from barrel/index files"
},
{
"from": "packages/nitro",
"target": "packages/!(nitro)/**/*",
"message": "nitro should not directly import other packages."
}
]
}
],
"@typescript-eslint/consistent-type-imports": [
"error",
{
"disallowTypeAnnotations": false
}
],
"@typescript-eslint/ban-ts-comment": [
"error",
{
"ts-expect-error": "allow-with-description",
"ts-ignore": true
}
],
"@typescript-eslint/prefer-ts-expect-error": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"jsdoc/check-tag-names": [
"error",
{
"definedTags": ["__NO_SIDE_EFFECTS__"]
}
]
},
"overrides": [
{
"files": ["packages/schema/**"],
"rules": {
"jsdoc/valid-types": "off",
"jsdoc/check-tag-names": [
"error",
{
"definedTags": ["experimental"]
}
]
}
},
{
"files": ["packages/nuxt/src/app/**", "test/**", "**/runtime/**"],
"rules": {
"no-console": "off"
}
}
],
"settings": {
"jsdoc": {
"ignoreInternal": true,
"tagNamePreference": {
"warning": "warning",
"note": "note"
}
}
}
}

View File

@ -1,10 +1,10 @@
blank_issues_enabled: true
contact_links:
- name: 📚 Nuxt 3 Documentation
url: https://nuxt.com/docs/
url: https://nuxt.com/docs
about: Check the documentation for usage of Nuxt 3
- name: 📚 Nuxt 2 Documentation
url: https://v2.nuxt.com/
url: https://v2.nuxt.com
about: Check the documentation for usage of Nuxt 2
- name: 💬 Discussions
url: https://github.com/nuxt/nuxt/discussions

View File

@ -1,37 +1,19 @@
<!---
☝️ PR title should follow conventional commits (https://conventionalcommits.org)
Please carefully read the contribution docs before creating a pull request
👉 https://nuxt.com/docs/community/contribution
-->
### 🔗 Linked issue
<!-- Please ensure there is an open issue and mention its number as #123 -->
### ❓ Type of change
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->
- [ ] 📖 Documentation (updates to the documentation, readme or JSdoc annotations)
- [ ] 🐞 Bug fix (a non-breaking change that fixes an issue)
- [ ] 👌 Enhancement (improving an existing functionality like performance)
- [ ] ✨ New feature (a non-breaking change that adds functionality)
- [ ] 🧹 Chore (updates to the build process or auxiliary tools and libraries)
- [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change)
<!-- Please ensure there is an open issue and mention its number. For example, "resolves #123" -->
### 📚 Description
<!-- Describe your changes in detail -->
<!-- Why is this change required? What problem does it solve? -->
<!-- If it resolves an open issue, please link to the issue here. For example "Resolves #1337" -->
<!-- Describe your changes in detail. Why is this change required? What problem does it solve? -->
### 📝 Checklist
<!----------------------------------------------------------------------
Before creating the pull request, please make sure you do the following:
<!-- Put an `x` in all the boxes that apply. -->
<!-- If your change requires a documentation PR, please link it appropriately -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us reviewing it.
- Read the contribution docs at https://nuxt.com/docs/community/contribution
- Ensure that PR title follows conventional commits (https://conventionalcommits.org)
- Update the corresponding documentation if needed.
- Include relevant tests that fail without this PR but pass with it.
- [ ] I have linked an issue or discussion.
- [ ] I have added tests (if possible).
- [ ] I have updated the documentation accordingly.
Thank you for contributing to Nuxt!
----------------------------------------------------------------------->

View File

@ -26,9 +26,6 @@ jobs:
- name: Build (stub)
run: pnpm dev:prepare
- name: Lint (code)
run: pnpm lint:fix
- name: Test (unit)
run: pnpm test:unit -u
@ -52,4 +49,7 @@ jobs:
if: ${{ !contains(github.head_ref, 'renovate') }}
run: pnpm vitest run bundle -u
- name: Lint (code)
run: pnpm lint:fix
- uses: autofix-ci/action@ea32e3a12414e6d3183163c3424a7d7a8631ad84

View File

@ -15,8 +15,6 @@ env:
# 7 GiB by default on GitHub, setting to 6 GiB
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
NODE_OPTIONS: --max-old-space-size=6144
# install playwright binary manually (because pnpm only runs install script once)
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
# Remove default permissions of GITHUB_TOKEN for security
# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
@ -48,7 +46,7 @@ jobs:
run: pnpm build
- name: Run benchmarks
uses: CodSpeedHQ/action@2e04019f4572c19684929a755da499f19a00b25b # v2.2.1
uses: CodSpeedHQ/action@1dbf41f0ae41cebfe61e084e535aebe533409b4d # v2.3.0
with:
run: pnpm vitest bench
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@ -1,4 +1,4 @@
name: Release
name: changelog
on:
push:
@ -15,7 +15,7 @@ concurrency:
cancel-in-progress: ${{ github.event_name != 'push' }}
jobs:
update-changelog:
update:
if: github.repository_owner == 'nuxt' && !contains(github.event.head_commit.message, 'v3.')
runs-on: ubuntu-latest

View File

@ -18,7 +18,7 @@ jobs:
steps:
# Cache lychee results (e.g. to avoid hitting rate limits)
- name: Restore lychee cache
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: .lycheecache
key: cache-lychee-${{ github.sha }}

View File

@ -20,8 +20,6 @@ env:
# 7 GiB by default on GitHub, setting to 6 GiB
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
NODE_OPTIONS: --max-old-space-size=6144
# install playwright binary manually (because pnpm only runs install script once)
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
# Remove default permissions of GITHUB_TOKEN for security
# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
@ -85,19 +83,19 @@ jobs:
run: pnpm install
- name: Initialize CodeQL
uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5
uses: github/codeql-action/init@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
with:
languages: javascript
queries: +security-and-quality
- name: Restore dist cache
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: dist
path: packages
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5
uses: github/codeql-action/analyze@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
with:
category: "/language:javascript"
@ -124,7 +122,7 @@ jobs:
run: pnpm install
- name: Restore dist cache
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: dist
path: packages
@ -220,34 +218,11 @@ jobs:
- name: Install dependencies
run: pnpm install
# Install playwright's binary under custom directory to cache
- name: (non-windows) Set Playwright path and Get playwright version
if: runner.os != 'Windows'
run: |
echo "PLAYWRIGHT_BROWSERS_PATH=$HOME/.cache/playwright-bin" >> $GITHUB_ENV
PLAYWRIGHT_VERSION="$(pnpm ls --depth 0 --json -w playwright-core | jq --raw-output '.[0].devDependencies["playwright-core"].version')"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: (windows) Set Playwright path and Get playwright version
if: runner.os == 'Windows'
run: |
echo "PLAYWRIGHT_BROWSERS_PATH=$HOME\.cache\playwright-bin" >> $env:GITHUB_ENV
$env:PLAYWRIGHT_VERSION="$(pnpm ls --depth 0 --json -w playwright-core | jq --raw-output '.[0].devDependencies["playwright-core"].version')"
echo "PLAYWRIGHT_VERSION=$env:PLAYWRIGHT_VERSION" >> $env:GITHUB_ENV
- name: Cache Playwright's binary
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
with:
key: ${{ runner.os }}-playwright-bin-v1-${{ env.PLAYWRIGHT_VERSION }}
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
restore-keys: |
${{ runner.os }}-playwright-bin-v1-
- name: Install Playwright
run: pnpm playwright-core install chromium
- name: Restore dist cache
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: dist
path: packages
@ -261,7 +236,7 @@ jobs:
TEST_CONTEXT: ${{ matrix.context }}
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || runner.os == 'Windows' }}
- uses: codecov/codecov-action@0cfda1dd0a4ad9efc75517f399d859cd1ea4ced1 # v4.0.2
- uses: codecov/codecov-action@7afa10ed9b269c561c2336fd862446844e0cbf71 # v4.2.0
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -296,7 +271,7 @@ jobs:
run: pnpm install
- name: Restore dist cache
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: dist
path: packages
@ -335,7 +310,7 @@ jobs:
run: pnpm install
- name: Restore dist cache
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: dist
path: packages

View File

@ -19,4 +19,4 @@ jobs:
- name: 'Checkout Repository'
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: 'Dependency Review'
uses: actions/dependency-review-action@9129d7d40b8c12c1ed0f60400d00c92d437adcce # v4.1.3
uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # v4.2.5

View File

@ -1,4 +1,4 @@
name: release
name: release-pr
on:
issue_comment:
@ -22,7 +22,7 @@ jobs:
steps:
- name: Ensure action is by maintainer
uses: octokit/request-action@89697eb6635e52c6e1e5559f15b5c91ba5100cb0 # v2.1.9
uses: octokit/request-action@d69a4d4369a61d4045c56451421ff93ee5e7bea0 # v2.2.0
id: check_role
with:
route: GET /repos/nuxt/nuxt/collaborators/${{ github.event.comment.user.login }}

40
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: release
on:
push:
tags:
- "v*"
# Remove default permissions of GITHUB_TOKEN for security
# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
permissions: {}
jobs:
release:
if: github.repository == 'nuxt/nuxt' && startsWith(github.event.head_commit.message, 'v3.')
permissions:
id-token: write
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 20
registry-url: "https://registry.npmjs.org/"
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build (stub)
run: pnpm dev:prepare
- name: Release
run: ./scripts/release.sh
env:
NODE_AUTH_TOKEN: ${{secrets.RELEASE_NODE_AUTH_TOKEN}}
NPM_CONFIG_PROVENANCE: true

View File

@ -66,6 +66,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
with:
sarif_file: results.sarif

1
.npmrc
View File

@ -1,3 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false
shell-emulator=true

View File

@ -13,16 +13,32 @@
Nuxt is a free and open-source framework with an intuitive and extendable way to create type-safe, performant and production-grade full-stack web applications and websites with Vue.js.
It provides a number of features that make it easy to build fast, SEO-friendly, and scalable web applications, including:
- Server-side rendering, Static Site Generation or Hybrid Rendering
- Automatic routing with code-splitting
- State management
- SEO Optimization
- Auto imports
- Extensible with [180+ modules](https://nuxt.com/modules)
- Server-side rendering, Static Site Generation, Hybrid Rendering and Edge-Side Rendering
- Automatic routing with code-splitting and pre-fetching
- Data fetching and state management
- SEO Optimization and Meta tags definition
- Auto imports of components, composables and utils
- TypeScript with zero configuration
- Go fullstack with our server/ directory
- Extensible with [200+ modules](https://nuxt.com/modules)
- Deployment to a variety of [hosting platforms](https://nuxt.com/deploy)
- ...[and much more](https://nuxt.com) 🚀
## Getting Started
### Table of Contents
- 🚀 [Getting Started](#getting-started)
- 💻 [ Vue Development](#vue-development)
- 📖 [Documentation](#documentation)
- 🧩 [Modules](#modules)
- ❤️ [Contribute](#contribute)
- 🏠 [Local Development](#local-development)
- ⛰️ [Nuxt 2](#nuxt-2)
- 🔗 [Follow us](#follow-us)
- ⚖️ [License](#license)
---
## <a name="getting-started">🚀 Getting Started</a>
Use the following command to create a new starter project. This will create a starter project with all the necessary files and dependencies:
@ -30,9 +46,10 @@ Use the following command to create a new starter project. This will create a st
npx nuxi@latest init <my-project>
```
Discover also [nuxt.new](https://nuxt.new): Open a Nuxt starter on CodeSandbox, StackBlitz or locally to get up and running in a few seconds.
> [!TIP]
> Discover also [nuxt.new](https://nuxt.new): Open a Nuxt starter on CodeSandbox, StackBlitz or locally to get up and running in a few seconds.
## Vue Development
## <a name="vue-development">💻 Vue Development</a>
Simple, intuitive and powerful, Nuxt lets you write Vue components in a way that makes sense. Every repetitive task is automated, so you can focus on writing your full-stack Vue application with confidence.
@ -54,7 +71,7 @@ useSeoMeta({
</div>
</template>
<style>
<style scoped>
#app {
background-color: #020420;
color: #00DC82;
@ -62,15 +79,15 @@ useSeoMeta({
</style>
```
## Documentation
## <a name="documentation">📖 Documentation</a>
We highly recommend you take a look at the [Nuxt documentation](https://nuxt.com/docs) to level up. Its a great resource for learning more about the framework. It covers everything from getting started to advanced topics.
## Modules
## <a name="modules">🧩 Modules</a>
Discover our [list of modules](https://nuxt.com/modules) to supercharge your Nuxt project, created by the Nuxt team and community.
## Contribute
## <a name="contribute">❤️ Contribute</a>
We invite you to contribute and help improve Nuxt 💚
@ -79,21 +96,20 @@ Here are a few ways you can get involved:
- **Suggestions:** Have ideas to enhance Nuxt? We'd love to hear them! Check out the [contribution guide](https://nuxt.com/docs/community/contribution#creating-an-issue) to share your suggestions.
- **Questions:** If you have questions or need assistance, the [getting help guide](https://nuxt.com/docs/community/getting-help) provides resources to help you out.
## Local Development
## <a name="local-development">🏠 Local Development</a>
Follow the docs to [Set Up Your Local Development Environment](https://nuxt.com/docs/community/framework-contribution#setup) to contribute to the framework and documentation.
## Nuxt 2
## <a name="nuxt-2">⛰️ Nuxt 2</a>
You can find the code for Nuxt 2 on the [`2.x` branch](https://github.com/nuxt/nuxt/tree/2.x) and the documentation at [v2.nuxt.com](https://v2.nuxt.com).
## Follow us
## <a name="follow-us">🔗 Follow us</a>
<p valign="center">
<a href="https://chat.nuxt.dev"><img width="20px" src="./.github/assets/discord.svg" alt="Discord"></a>&nbsp;&nbsp;<a href="https://twitter.nuxt.dev"><img width="20px" src="./.github/assets/twitter.svg" alt="Twitter"></a>&nbsp;&nbsp;<a href="https://github.nuxt.dev"><img width="20px" src="./.github/assets/github.svg" alt="GitHub"></a>
</p>
## License
## <a name="license">⚖️ License</a>
[MIT](./LICENSE)

View File

@ -15,8 +15,8 @@ Nuxt offers first-class support for end-to-end and unit testing of your Nuxt app
In order to allow you to manage your other testing dependencies, `@nuxt/test-utils` ships with various optional peer dependencies. For example:
- you can choose between `happy-dom` and `jsdom` for a runtime Nuxt environment
- you can choose between `vitest` and `jest` for end-to-end test runners
- `playwright-core` is only required if you wish to use the built-in browser testing utilities
- you can choose between `vitest`, `cucumber`, `jest` and `playwright` for end-to-end test runners
- `playwright-core` is only required if you wish to use the built-in browser testing utilities (and are not using `@playwright/test` as your test runner)
::code-group
```bash [yarn]
@ -88,6 +88,7 @@ export default defineVitestConfig({
// environmentOptions: {
// nuxt: {
// rootDir: fileURLToPath(new URL('./playground', import.meta.url)),
// domEnvironment: 'happy-dom', // 'happy-dom' (default) or 'jsdom'
// overrides: {
// // other Nuxt config you want to pass
// }
@ -387,7 +388,7 @@ await setup({
## End-To-End Testing
For end-to-end testing, we support [Vitest](https://github.com/vitest-dev/vitest) and [Jest](https://jestjs.io) as test runners.
For end-to-end testing, we support [Vitest](https://github.com/vitest-dev/vitest), [Jest](https://jestjs.io), [Cucumber](https://cucumber.io/) and [Playwright](https://playwright.dev/) as test runners.
### Setup
@ -453,7 +454,7 @@ Please use the options below for the `setup` method.
- `type`: The type of browser to launch - either `chromium`, `firefox` or `webkit`
- `launch`: `object` of options that will be passed to playwright when launching the browser. See [full API reference](https://playwright.dev/docs/api/class-browsertype#browser-type-launch).
- `runner`: Specify the runner for the test suite. Currently, [Vitest](https://vitest.dev) is recommended.
- Type: `'vitest' | 'jest'`
- Type: `'vitest' | 'jest' | 'cucumber'`
- Default: `'vitest'`
### APIs
@ -492,11 +493,11 @@ const pageUrl = url('/page')
### Testing in a Browser
We provide built-in support using Playwright within `@nuxt/test-utils`, but you can also use other test runners for end-to-end browser testing.
We provide built-in support using Playwright within `@nuxt/test-utils`, either programmatically or via the Playwright test runner.
#### `createPage(url)`
You can create a configured Playwright browser instance, and (optionally) point it at a path from the running server. You can find out more about the API methods available from [in the Playwright documentation](https://playwright.dev/docs/api/class-page).
Within `vitest`, `jest` or `cucumber`, you can create a configured Playwright browser instance with `createPage`, and (optionally) point it at a path from the running server. You can find out more about the API methods available from [in the Playwright documentation](https://playwright.dev/docs/api/class-page).
```ts twoslash
import { createPage } from '@nuxt/test-utils/e2e'
@ -504,3 +505,70 @@ import { createPage } from '@nuxt/test-utils/e2e'
const page = await createPage('/page')
// you can access all the Playwright APIs from the `page` variable
```
#### Testing with Playwright Test Runner
We also provide first-class support for testing Nuxt within [the Playwright test runner](https://playwright.dev/docs/intro).
::code-group
```bash [yarn]
yarn add --dev @playwright/test @nuxt/test-utils
```
```bash [npm]
npm i --save-dev @playwright/test @nuxt/test-utils
```
```bash [pnpm]
pnpm add -D @playwright/test @nuxt/test-utils
```
```bash [bun]
bun add --dev @playwright/test @nuxt/test-utils
```
::
You can provide global Nuxt configuration, with the same configuration details as the `setup()` function mentioned earlier in this section.
```ts [playwright.config.ts]
import { fileURLToPath } from 'node:url'
import { defineConfig, devices } from '@playwright/test'
import type { ConfigOptions } from '@nuxt/test-utils/playwright'
export default defineConfig<ConfigOptions>({
use: {
nuxt: {
rootDir: fileURLToPath(new URL('.', import.meta.url))
}
},
// ...
})
```
::read-more{title="See full example config" to="https://github.com/nuxt/test-utils/blob/main/examples/app-playwright/playwright.config.ts" target="_blank"}
::
Your test file should then use `expect` and `test` directly from `@nuxt/test-utils/playwright`:
```ts [tests/example.test.ts]
import { expect, test } from '@nuxt/test-utils/playwright'
test('test', async ({ page, goto }) => {
await goto('/', { waitUntil: 'hydration' })
await expect(page.getByRole('heading')).toHaveText('Welcome to Playwright!')
})
```
You can alternatively configure your Nuxt server directly within your test file:
```ts [tests/example.test.ts]
import { expect, test } from '@nuxt/test-utils/playwright'
test.use({
nuxt: {
rootDir: fileURLToPath(new URL('..', import.meta.url))
}
})
test('test', async ({ page, goto }) => {
await goto('/', { waitUntil: 'hydration' })
await expect(page.getByRole('heading')).toHaveText('Welcome to Playwright!')
})
```

View File

@ -22,7 +22,7 @@ Start with one of our starters and themes directly by opening [nuxt.new](https:/
#### Prerequisites
- **Node.js** - [`v18.0.0`](https://nodejs.org/en) or newer
- **Text editor** - We recommend [Visual Studio Code](https://code.visualstudio.com/) with the [Volar Extension](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
- **Text editor** - We recommend [Visual Studio Code](https://code.visualstudio.com/) with the [official Vue extension](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (previously known as Volar)
- **Terminal** - In order to run Nuxt commands
::note
@ -30,17 +30,6 @@ Start with one of our starters and themes directly by opening [nuxt.new](https:/
:summary[Additional notes for an optimal setup:]
- **Node.js**: Make sure to use an even numbered version (18, 20, etc)
- **Nuxtr**: Install the community-developed [Nuxtr extension](https://marketplace.visualstudio.com/items?itemName=Nuxtr.nuxtr-vscode)
- **Volar**: Either enable [**Take Over Mode**](https://vuejs.org/guide/typescript/overview.html#volar-takeover-mode) (recommended) or add the [TypeScript Vue Plugin](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin)
If you have enabled **Take Over Mode** or installed the **TypeScript Vue Plugin (Volar)**, you can disable generating the shim for `*.vue` files in your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt-config) file:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
typescript: {
shim: false
}
})
```
::
::

View File

@ -243,7 +243,7 @@ Nuxt uses Vite by default. If you wish to use webpack instead, refer to each pre
## Single File Components (SFC) Styling
One of the best things about Vue and SFC is how great it is at naturally dealing with styling. You can directly write CSS or preprocessor code in the style block of your components file, therefore you will have fantastic developer experience without having to use something like CSS-in-JS. However if you wish to use CSS-in-JS, you can find 3rd party libraries and modules that support it, such as [pinceau](https://pinceau.dev).
One of the best things about Vue and SFC is how great it is at naturally dealing with styling. You can directly write CSS or preprocessor code in the style block of your components file, therefore you will have fantastic developer experience without having to use something like CSS-in-JS. However if you wish to use CSS-in-JS, you can find 3rd party libraries and modules that support it, such as [pinceau](https://github.com/Tahul/pinceau).
You can refer to the [Vue docs](https://vuejs.org/api/sfc-css-features.html) for a comprehensive reference about styling components in SFC.
@ -421,7 +421,7 @@ For proper syntax highlighting in SFC, you can use the postcss lang attribute.
```vue
<style lang="postcss">
/* Write stylus here */
/* Write postcss here */
</style>
```
@ -430,7 +430,7 @@ By default, Nuxt comes with the following plugins already pre-configured:
- [postcss-import](https://github.com/postcss/postcss-import): Improves the `@import` rule
- [postcss-url](https://github.com/postcss/postcss-url): Transforms `url()` statements
- [autoprefixer](https://github.com/postcss/autoprefixer): Automatically adds vendor prefixes
- [cssnano](https://cssnano.co): Minification and purge
- [cssnano](https://cssnano.github.io/cssnano): Minification and purge
## Leveraging Layouts For Multiple Styles
@ -458,14 +458,14 @@ Use different styles for different layouts.
Nuxt isn't opinionated when it comes to styling and provides you with a wide variety of options. You can use any styling tool that you want, such as popular libraries like [UnoCSS](https://unocss.dev) or [Tailwind CSS](https://tailwindcss.com).
The community and the Nuxt team have developed plenty of Nuxt modules to makes the integration easier.
The community and the Nuxt team have developed plenty of Nuxt modules to make the integration easier.
You can discover them on the [modules section](/modules) of the website.
Here are a few modules to help you get started:
- [UnoCSS](/modules/unocss): Instant on-demand atomic CSS engine
- [Tailwind CSS](/modules/tailwindcss): Utility-first CSS framework
- [Fontaine](https://github.com/nuxt-modules/fontaine): Font metric fallback
- [Pinceau](https://pinceau.dev): Adaptable styling framework
- [Pinceau](https://github.com/Tahul/pinceau): Adaptable styling framework
- [Nuxt UI](https://ui.nuxt.com): A UI Library for Modern Web Apps
- [Panda CSS](https://panda-css.com/docs/installation/nuxt): CSS-in-JS engine that generates atomic CSS at build time

View File

@ -6,7 +6,7 @@ navigation.icon: i-ph-file-search-duotone
## Defaults
Out-of-the-box, Nuxt provides sane defaults, which you can override if needed.
Out-of-the-box, Nuxt provides sensible defaults, which you can override if needed.
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
@ -174,7 +174,7 @@ You can use the `titleTemplate` option to provide a dynamic template for customi
The `titleTemplate` can either be a string, where `%s` is replaced with the title, or a function.
If you want to use a function (for full control), then this cannot be set in your `nuxt.config`, and it is recommended instead to set it within your `app.vue` file, where it will apply to all pages on your site:
If you want to use a function (for full control), then this cannot be set in your `nuxt.config`. It is recommended instead to set it within your `app.vue` file where it will apply to all pages on your site:
::code-group

View File

@ -60,7 +60,7 @@ This composable is a wrapper around the [`useAsyncData`](/docs/api/composables/u
## `$fetch`
Nuxt includes the `ofetch` library, and is auto-imported as the `$fetch` alias globally across your application. It's what `useFetch` uses behind the scenes.
Nuxt includes the [ofetch](https://github.com/unjs/ofetch) library, and is auto-imported as the `$fetch` alias globally across your application. It's what `useFetch` uses behind the scenes.
```vue twoslash [pages/todos.vue]
<script setup lang="ts">
@ -150,6 +150,7 @@ Read more about `useAsyncData`.
- `data`: the result of the asynchronous function that is passed in.
- `pending`: a boolean indicating whether the data is still being fetched.
- `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function.
- `clear`: a function that can be used to set `data` to undefined, set `error` to `null`, set `pending` to `false`, set `status` to `idle`, and mark any currently pending requests as cancelled.
- `error`: an error object if the data fetching failed.
- `status`: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`).
@ -294,6 +295,21 @@ The `execute` function is an alias for `refresh` that works in exactly the same
To globally refetch or invalidate cached data, see [`clearNuxtData`](/docs/api/utils/clear-nuxt-data) and [`refreshNuxtData`](/docs/api/utils/refresh-nuxt-data).
::
#### Clear
If you want to clear the data provided, for whatever reason, without needing to know the specific key to pass to `clearNuxtData`, you can use the `clear` function provided by the composables.
```vue twoslash
<script setup lang="ts">
const { data, clear } = await useFetch('/api/users')
const route = useRoute()
watch(() => route.path, (path) => {
if (path === '/') clear()
})
</script>
```
#### Watch
To re-run your fetching function each time other reactive values in your application change, use the `watch` option. You can use it for one or multiple _watchable_ elements.

View File

@ -130,12 +130,12 @@ export const useLocale = () => {
export const useDefaultLocale = (fallback = 'en-US') => {
const locale = ref(fallback)
if (process.server) {
if (import.meta.server) {
const reqLocale = useRequestHeaders()['accept-language']?.split(',')[0]
if (reqLocale) {
locale.value = reqLocale
}
} else if (process.client) {
} else if (import.meta.client) {
const navLang = navigator.language
if (navLang) {
locale.value = navLang
@ -192,7 +192,6 @@ const date = useLocaleDate(new Date('2016-10-26'))
By using [auto-imported composables](/docs/guide/directory-structure/composables) we can define global type-safe states and import them across the app.
```ts twoslash [composables/states.ts]
export const useCounter = () => useState<number>('counter', () => 0)
export const useColor = () => useState<string>('color', () => 'pink')
```

View File

@ -7,15 +7,15 @@ navigation.icon: i-ph-bug-beetle-duotone
Nuxt 3 is a full-stack framework, which means there are several sources of unpreventable user runtime errors that can happen in different contexts:
- Errors during the Vue rendering lifecycle (SSR & CSR)
- Errors during Nitro server lifecycle ([`server/`](/docs/guide/directory-structure/server) directory)
- Server and client startup errors (SSR + CSR)
- Errors during Nitro server lifecycle ([`server/`](/docs/guide/directory-structure/server) directory)
- Errors downloading JS chunks
::tip
**SSR** stands for **Server-Side Rendering** and **CSR** for **Client-Side Rendering**.
::
## Vue Rendering Lifecycle
## Vue Errors
You can hook into Vue errors using [`onErrorCaptured`](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured).
@ -51,7 +51,7 @@ This includes:
- mounting the app (on client-side), though you should handle this case with `onErrorCaptured` or with `vue:error`
- processing the `app:mounted` hook
## Nitro Server Lifecycle
## Nitro Server Errors
You cannot currently define a server-side handler for these errors, but can render an error page, see the [Render an Error Page](#error-page) section.

View File

@ -83,9 +83,9 @@ export default defineNuxtConfig({
Learn about all available route rules are available to customize the rendering mode of your routes.
::
In addition, there are some route rules (for example, `ssr` and `experimentalNoScripts`) that are Nuxt specific to change the behavior when rendering your pages to HTML.
In addition, there are some route rules (for example, `ssr`, `appMiddleware`, and `experimentalNoScripts`) that are Nuxt specific to change the behavior when rendering your pages to HTML.
Some route rules (`redirect` and `prerender`) also affect client-side behavior.
Some route rules (`appMiddleware`, `redirect` and `prerender`) also affect client-side behavior.
Nitro is used to build the app for server side rendering, as well as pre-rendering.

View File

@ -14,12 +14,13 @@ One of the core features of Nuxt 3 is the layers and extending support. You can
- Create Nuxt module presets
- Share standard setup across projects
- Create Nuxt themes
- Enhance code organization by implementing a modular architecture and support Domain-Driven Design (DDD) pattern in large scale projects.
## Usage
You can extend a layer by adding the [extends](/docs/api/nuxt-config#extends) property to the [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt-config) file.
```ts twoslash [nuxt.config.ts]
```ts [nuxt.config.ts]
export default defineNuxtConfig({
extends: [
'../base', // Extend from a local layer
@ -29,6 +30,19 @@ export default defineNuxtConfig({
})
```
You can also pass an authentication token if you are extending from a private GitHub repository:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
extends: [
// per layer configuration
['github:my-themes/private-awesome', { auth: process.env.GITHUB_TOKEN }]
]
})
```
Nuxt uses [unjs/c12](https://c12.unjs.io) and [unjs/giget](https://giget.unjs.io) for extending remote layers. Check the documentation for more information and all available options.
::read-more{to="/docs/guide/going-further/layers"}
Read more about layers in the **Layer Author Guide**.
::

View File

@ -84,7 +84,7 @@ export const useMyComposable = () => {
```ts twoslash [composables/example.ts]
export const useMyComposable = () => {
// Because your composable is called in the right place in the lifecycle,
// useRuntimeConfig will also work
// useRuntimeConfig will work here
const config = useRuntimeConfig()
// ...

View File

@ -113,6 +113,7 @@ The different properties you can use are the following:
- `isr: number | boolean`{lang=ts} - The behavior is the same as `swr` except that we are able to add the response to the CDN cache on platforms that support this (currently Netlify or Vercel). If `true` is used, the content persists until the next deploy inside the CDN.
- `prerender: boolean`{lang=ts} - Prerenders routes at build time and includes them in your build as static assets
- `experimentalNoScripts: boolean`{lang=ts} - Disables rendering of Nuxt scripts and JS resource hints for sections of your site.
- `appMiddleware: string | string[] | Record<string, boolean>`{lang=ts} - Allows you to define middleware that should or should not run for page paths within the Vue app part of your application (that is, not your Nitro routes)
Whenever possible, route rules will be automatically applied to the deployment platform's native rules for optimal performances (Netlify and Vercel are currently supported).

View File

@ -9,22 +9,26 @@ By default, Nuxt doesn't check types when you run [`nuxi dev`](/docs/api/command
To enable type-checking at build or development time, install `vue-tsc` and `typescript` as development dependency:
::alert{type="warning"}
You may experience issues with the latest `vue-tsc` and `vite-plugin-checker`, used internally when type checking. For now, you may need to stay on v1 of `vue-tsc`, and follow these upstream issues for updates: [fi3ework/vite-plugin-checker#306](https://github.com/fi3ework/vite-plugin-checker/issues/306) and [vuejs/language-tools#3969](https://github.com/vuejs/language-tools/issues/3969).
::
::code-group
```bash [yarn]
yarn add --dev vue-tsc typescript
yarn add --dev vue-tsc@^1 typescript
```
```bash [npm]
npm install --save-dev vue-tsc typescript
npm install --save-dev vue-tsc@^1 typescript
```
```bash [pnpm]
pnpm add -D vue-tsc typescript
pnpm add -D vue-tsc@^1 typescript
```
```bash [bun]
bun add -D vue-tsc typescript
bun add -D vue-tsc@^1 typescript
```
::

View File

@ -244,6 +244,10 @@ This feature only works with Nuxt auto-imports and `#components` imports. Explic
`.client` components are rendered only after being mounted. To access the rendered template using `onMounted()`, add `await nextTick()` in the callback of the `onMounted()` hook.
::
::read-more{to="/docs/api/components/client-only"}
You can also achieve a similar result with the `<ClientOnly>` component.
::
## Server Components
Server components allow server-rendering individual components within your client-side apps. It's possible to use server components within Nuxt, even if you are generating a static site. That makes it possible to build complex sites that mix dynamic components, server-rendered HTML and even static chunks of markup.
@ -295,6 +299,14 @@ Now you can register server-only components with the `.server` suffix and use th
Server-only components use [`<NuxtIsland>`](/docs/api/components/nuxt-island) under the hood, meaning that `lazy` prop and `#fallback` slot are both passed down to it.
::alert{type=warning}
Server components (and islands) must have a single root element. (HTML comments are considered elements as well.)
::
::alert{type=warning}
Most features for server-only components and island components, such as slots and client components, are only available for single file components.
::
#### Client components within server components
::alert{type=info}
@ -314,7 +326,7 @@ You can partially hydrate a component by setting a `nuxt-client` attribute on th
```
::alert{type=info}
This only works within a server component. Slots for client components are not available yet.
This only works within a server component. Slots for client components are working only with `experimental.componentIsland.selectiveClient` set to `'deep'` and since they are rendered server-side, they are not interactive once client-side.
::
#### Server Component Context
@ -353,89 +365,12 @@ In this case, the `.server` + `.client` components are two 'halves' of a compone
</template>
```
## `<ClientOnly>` Component
## Built-In Nuxt Components
Nuxt provides the [`<ClientOnly>`](/docs/api/components/client-only) component for purposely rendering a component only on client side.
There are a number of components that Nuxt provides, including `<ClientOnly>` and `<DevOnly>`. You can read more about them in the API documentation.
```vue [pages/example.vue]
<template>
<div>
<Sidebar />
<ClientOnly>
<!-- this component will only be rendered on client-side -->
<Comments />
</ClientOnly>
</div>
</template>
```
Use a slot as fallback until `<ClientOnly>` is mounted on client side.
```vue [pages/example.vue]
<template>
<div>
<Sidebar />
<!-- This renders the "span" element on the server side -->
<ClientOnly fallbackTag="span">
<!-- this component will only be rendered on client side -->
<Comments />
<template #fallback>
<!-- this will be rendered on server side -->
<p>Loading comments...</p>
</template>
</ClientOnly>
</div>
</template>
```
<!-- TODO: Add back after passing treeshakeClientOnly experiment -->
<!--
::note
Make sure not to _nest_ `<ClientOnly>` components or other client-only components. Nuxt performs an optimization to remove the contents of these components from the server-side render, which can break in this case.
::read-more{to="/docs/api"}
::
-->
## `<DevOnly>` Component
Nuxt provides the `<DevOnly>` component to render a component only during development.
The content will not be included in production builds and tree-shaken.
```vue [pages/example.vue]
<template>
<div>
<Sidebar />
<DevOnly>
<!-- this component will only be rendered during development -->
<LazyDebugBar />
<!-- if you ever require to have a replacement during production -->
<!-- be sure to test these using `nuxt preview` -->
<template #fallback>
<div><!-- empty div for flex.justify-between --></div>
</template>
</DevOnly>
</div>
</template>
```
## `<NuxtClientFallback>` Component
Nuxt provides the `<NuxtClientFallback>` component to render its content on the client if any of its children trigger an error in SSR.
You can specify a `fallbackTag` to make it render a specific tag if it fails to render on the server.
```vue [pages/example.vue]
<template>
<div>
<Sidebar />
<!-- this component will be rendered on client-side -->
<NuxtClientFallback fallback-tag="span">
<Comments />
<BrokeInSSR />
</NuxtClientFallback>
</div>
</template>
```
## Library Authors

View File

@ -125,12 +125,12 @@ However, if you want to avoid this behaviour you can do so:
```ts twoslash [middleware/example.ts]
export default defineNuxtRouteMiddleware(to => {
// skip middleware on server
if (process.server) return
if (import.meta.server) return
// skip middleware on client side entirely
if (process.client) return
if (import.meta.client) return
// or only skip middleware on initial client load
const nuxtApp = useNuxtApp()
if (process.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return
if (import.meta.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return
})
```

View File

@ -56,3 +56,7 @@ modules/
```
:read-more{to="/docs/guide/going-further/modules"}
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/creating-your-first-module-from-scratch" target="_blank"}
Watch Vue School video about Nuxt private modules.
::

View File

@ -57,7 +57,7 @@ If you are using [`app.vue`](/docs/guide/directory-structure/app), make sure to
</template>
```
Pages **must have a single root element** to allow [route transitions](/docs/getting-started/transitions) between pages, HTML comments are considered elements as well.
Pages **must have a single root element** to allow [route transitions](/docs/getting-started/transitions) between pages. HTML comments are considered elements as well.
This means that when the route is server-rendered, or statically generated, you will be able to see its contents correctly, but when you navigate towards that route during client-side navigation the transition between routes will fail and you'll see that the route will not be rendered.
@ -357,12 +357,16 @@ function navigate(){
</script>
```
## Client-Only Pages
You can define a page as [client only](/docs/guide/directory-structure/components#client-components) by giving it a `.client.vue` suffix. None of the content of this page will be rendered on the server.
## Server-Only Pages
You can define a page as [server only](/docs/guide/directory-structure/components#server-components) by giving it a `.server.vue` suffix. While you will be able to navigate to the page using client-side navigation, controlled by `vue-router`, it will be rendered with a server component automatically, meaning the code required to render the page will not be in your client-side bundle.
::note
You will also need to enable `experimental.componentIslands` in order to make this possible.
::alert{type=warning}
Server-only pages must have a single root element. (HTML comments are considered elements as well.)
::
## Custom Routing

View File

@ -78,7 +78,7 @@ export default defineNuxtPlugin({
::note
If you are using the object-syntax, the properties may be statically analyzed in future to produce a more optimized build. So you should not define them at runtime. :br
For example, setting `enforce: process.server ? 'pre' : 'post'` would defeat any future optimization Nuxt is able to do for your plugins.
For example, setting `enforce: import.meta.server ? 'pre' : 'post'` would defeat any future optimization Nuxt is able to do for your plugins.
::
## Registration Order

View File

@ -99,7 +99,7 @@ Nuxt uses a custom merging strategy for the `AppConfig` within [the layers](/doc
This strategy is implemented using a [Function Merger](https://github.com/unjs/defu#function-merger), which allows defining a custom merging strategy for every key in `app.config` that has an array as value.
::note
The Function Merger should only be used in the base `app.config` of your application.
The function merger can only be used in the extended layers and not the main `app.config` in project.
::
Here's an example of how you can use:

View File

@ -98,7 +98,7 @@ The behavior is different between the client-side and server-side:
const config = useRuntimeConfig()
console.log('Runtime config:', config)
if (process.server) {
if (import.meta.server) {
console.log('API secret:', config.apiSecret)
}
</script>
@ -164,3 +164,7 @@ declare module 'nuxt/schema' {
// It is always important to ensure you import/export something when augmenting a type
export {}
```
::note
`nuxt/schema` is provided as a convenience for end-users to access the version of the schema used by Nuxt in their project. Module authors should instead augment `@nuxt/schema`.
::

View File

@ -30,6 +30,10 @@ This will create a `my-module` project with all the boilerplate necessary to dev
Learn how to perform basic tasks with the module starter.
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/navigating-the-official-starter-template" target="_blank"}
Watch Vue School video about Nuxt module starter template.
::
#### How to Develop
While your module source code lives inside the `src` directory, in most cases, to develop a module, you need a Nuxt application. That's what the `playground` directory is about. It's a Nuxt application you can tinker with that is already configured to run with your module.
@ -255,6 +259,10 @@ export default defineNuxtModule({
When you need to handle more complex configuration alterations, you should consider using [defu](https://github.com/unjs/defu).
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/extending-and-altering-nuxt-configuration-and-options" target="_blank"}
Watch Vue School video about altering Nuxt configuration.
::
#### Exposing Options to Runtime
Because modules aren't part of the application runtime, their options aren't either. However, in many cases, you might need access to some of these module options within your runtime code. We recommend exposing the needed config using Nuxt's [`runtimeConfig`](/docs/api/nuxt-config#runtimeconfig).
@ -288,6 +296,10 @@ Be careful not to expose any sensitive module configuration on the public runtim
:read-more{to="/docs/guide/going-further/runtime-config"}
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/passing-and-exposing-module-options" target="_blank"}
Watch Vue School video about passing and exposing Nuxt module options.
::
#### Injecting Plugins With `addPlugin`
Plugins are a common way for a module to add runtime logic. You can use the `addPlugin` utility to register them from your module.
@ -511,6 +523,10 @@ export default defineNuxtModule({
:read-more{to="/docs/api/advanced/hooks"}
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/nuxt-lifecycle-hooks" target="_blank"}
Watch Vue School video about using Nuxt lifecycle hooks in modules.
::
::note
**Module cleanup**
:br
@ -733,6 +749,10 @@ The module starter comes with a default set of tools and configurations (e.g. ES
[Nuxt Module ecosystem](/modules) represents more than 15 million monthly NPM downloads and provides extended functionalities and integrations with all sort of tools. You can be part of this ecosystem!
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/exploring-nuxt-modules-ecosystem-and-module-types" target="_blank"}
Watch Vue School video about Nuxt module types.
::
### Module Types
**Official modules** are modules prefixed (scoped) with `@nuxt/` (e.g. [`@nuxt/content`](https://content.nuxtjs.org)). They are made and maintained actively by the Nuxt team. Like with the framework, contributions from the community are more than welcome to help make them better!

View File

@ -105,7 +105,7 @@ export default defineNuxtConfig({
const resolver = createResolver(import.meta.url)
// add a route
files.push({
path: resolver.resolve('./runtime/app/router-options')
path: resolver.resolve('./runtime/app/router-options'),
optional: true
})
}
@ -172,6 +172,6 @@ import { createMemoryHistory } from 'vue-router'
export default <RouterConfig> {
// https://router.vuejs.org/api/interfaces/routeroptions.html
history: base => process.client ? createMemoryHistory(base) : null /* default */
history: base => import.meta.client ? createMemoryHistory(base) : null /* default */
}
```

View File

@ -38,6 +38,10 @@ It is possible to debug your Nuxt app in your IDE while you are developing it.
You may need to update the config below with a path to your web browser. For more information, visit the [VS Code documentation about debug configuration](https://go.microsoft.com/fwlink/?linkid=830387).
::important
If you use `pnpm`, you will need to have `nuxi` installed as a devDependency for the configuration below to work.
::
```json5
{
// Use IntelliSense to learn about possible attributes.

View File

@ -8,7 +8,11 @@ links:
size: xs
---
The `<ClientOnly>` component renders its slot only in client-side. To import a component only on the client, register the component in a client-side only plugin.
The `<ClientOnly>` component is used for purposely rendering a component only on client side.
::note
The content of the default slot will be tree-shaken out of the server build. (This does mean that any CSS used by components within it may not be inlined when rendering the initial HTML.)
::
## Props
@ -19,6 +23,7 @@ The `<ClientOnly>` component renders its slot only in client-side. To import a c
<template>
<div>
<Sidebar />
<!-- The <Comment> component will only be rendered on client-side -->
<ClientOnly fallback-tag="span" fallback="Loading comments...">
<Comment />
</ClientOnly>
@ -28,14 +33,16 @@ The `<ClientOnly>` component renders its slot only in client-side. To import a c
## Slots
- `#fallback`: specify a content to be displayed server-side.
- `#fallback`: specify a content to be rendered on the server and displayed until `<ClientOnly>` is mounted in the browser.
```vue
```vue [pages/example.vue]
<template>
<div>
<Sidebar />
<ClientOnly>
<!-- ... -->
<!-- This renders the "span" element on the server side -->
<ClientOnly fallbackTag="span">
<!-- this component will only be rendered on client side -->
<Comments />
<template #fallback>
<!-- this will be rendered on server side -->
<p>Loading comments...</p>

View File

@ -0,0 +1,51 @@
---
title: '<DevOnly>'
description: Render components only during development with the <DevOnly> component.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/dev-only.ts
size: xs
---
Nuxt provides the `<DevOnly>` component to render a component only during development.
The content will not be included in production builds.
```vue [pages/example.vue]
<template>
<div>
<Sidebar />
<DevOnly>
<!-- this component will only be rendered during development -->
<LazyDebugBar />
<!-- if you ever require to have a replacement during production -->
<!-- be sure to test these using `nuxt preview` -->
<template #fallback>
<div><!-- empty div for flex.justify-between --></div>
</template>
</DevOnly>
</div>
</template>
```
## Slots
- `#fallback`: if you ever require to have a replacement during production.
```vue
<template>
<div>
<Sidebar />
<DevOnly>
<!-- this component will only be rendered during development -->
<LazyDebugBar />
<!-- be sure to test these using `nuxt preview` -->
<template #fallback>
<div><!-- empty div for flex.justify-between --></div>
</template>
</DevOnly>
</div>
</template>
```

View File

@ -12,10 +12,25 @@ links:
size: xs
---
Nuxt provides the `<NuxtClientFallback>` component to render its content on the client if any of its children trigger an error in SSR.
::note{to="/docs/guide/going-further/experimental-features#clientfallback"}
This component is experimental and in order to use it you must enable the `experimental.clientFallback` option in your `nuxt.config`.
::
```vue [pages/example.vue]
<template>
<div>
<Sidebar />
<!-- this component will be rendered on client-side -->
<NuxtClientFallback fallback-tag="span">
<Comments />
<BrokeInSSR />
</NuxtClientFallback>
</div>
</template>
```
## Events
- `@ssr-error`: Event emitted when a child triggers an error in SSR. Note that this will only be triggered on the server.
@ -30,7 +45,7 @@ This component is experimental and in order to use it you must enable the `exper
## Props
- `placeholderTag` | `fallbackTag`: Specify a fallback tag to be rendered if the slot fails to render.
- `placeholderTag` | `fallbackTag`: Specify a fallback tag to be rendered if the slot fails to render on the server.
- **type**: `string`
- **default**: `div`
- `placeholder` | `fallback`: Specify fallback content to be rendered if the slot fails to render.

View File

@ -55,6 +55,10 @@ Please note the layout name is normalized to kebab-case, so if your layout file
Read more about dynamic layouts.
::
- `fallback`: If an invalid layout is passed to the `name` prop, no layout will be rendered. Specify a `fallback` layout to be rendered in this scenario. It **must** match the name of the corresponding layout file in the [`layouts/`](/docs/guide/directory-structure/layouts) directory.
- **type**: `string`
- **default**: `null`
## Additional Props
`NuxtLayout` also accepts any additional props that you may need to pass to the layout. These custom props are then made accessible as attributes.
@ -83,6 +87,8 @@ console.log(layoutCustomProps.title) // I am a custom layout
`<NuxtLayout />` renders incoming content via `<slot />`, which is then wrapped around Vues `<Transition />` component to activate layout transition. For this to work as expected, it is recommended that `<NuxtLayout />` is **not** the root element of the page component.
::code-group
```vue [pages/index.vue]
<template>
<div>
@ -93,13 +99,27 @@ console.log(layoutCustomProps.title) // I am a custom layout
</template>
```
```vue [layouts/custom.vue]
<template>
<div>
<!-- named slot -->
<slot name="header" />
<slot />
</div>
</template>
```
::
:read-more{to="/docs/getting-started/transitions"}
## Layout's Ref
To get the ref of a layout component, access it through `ref.value.layoutRef`.
````vue [app.vue]
::code-group
```vue [app.vue]
<script setup lang="ts">
const layout = ref()
@ -109,8 +129,28 @@ function logFoo () {
</script>
<template>
<NuxtLayout ref="layout" />
<NuxtLayout ref="layout">
default layout
</NuxtLayout>
</template>
````
```
```vue [layouts/default.vue]
<script setup lang="ts">
const foo = () => console.log('foo')
defineExpose({
foo
})
</script>
<template>
<div>
default layout
<slot />
</div>
</template>
```
::
:read-more{to="/docs/guide/directory-structure/layouts"}

View File

@ -12,10 +12,6 @@ When rendering an island component, the content of the island component is stati
Changing the island component props triggers a refetch of the island component to re-render it again.
::read-more{to="/docs/guide/going-further/experimental-features#componentislands" icon="i-ph-star-duotone"}
This component is experimental and in order to use it you must enable the `experimental.componentIslands` option in your `nuxt.config`.
::
::note
Global styles of your application are sent with the response.
::
@ -60,3 +56,11 @@ Some slots are reserved to `NuxtIsland` for special cases.
- `refresh()`
- **type**: `() => Promise<void>`
- **description**: force refetch the server component by refetching it.
## Events
- `error`
- **parameters**:
- **error**:
- **type**: `unknown`
- **description**: emitted when when `NuxtIsland` fails to fetch the new island.

View File

@ -119,10 +119,10 @@ type AsyncDataOptions<DataT> = {
deep?: boolean
dedupe?: 'cancel' | 'defer'
default?: () => DataT | Ref<DataT> | null
transform?: (input: DataT) => DataT
transform?: (input: DataT) => DataT | Promise<DataT>
pick?: string[]
watch?: WatchSource[]
getCachedData?: (key: string) => DataT
getCachedData?: (key: string, nuxtApp: NuxtApp) => DataT
}
type AsyncData<DataT, ErrorT> = {
@ -130,6 +130,7 @@ type AsyncData<DataT, ErrorT> = {
pending: Ref<boolean>
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
clear: () => void
error: Ref<ErrorT | null>
status: Ref<AsyncDataRequestStatus>
};

View File

@ -144,11 +144,11 @@ type UseFetchOptions<DataT> = {
server?: boolean
lazy?: boolean
immediate?: boolean
getCachedData?: (key: string) => DataT
getCachedData?: (key: string, nuxtApp: NuxtApp) => DataT
deep?: boolean
dedupe?: 'cancel' | 'defer'
default?: () => DataT
transform?: (input: DataT) => DataT
transform?: (input: DataT) => DataT | Promise<DataT>
pick?: string[]
watch?: WatchSource[] | false
}
@ -158,6 +158,7 @@ type AsyncData<DataT, ErrorT> = {
pending: Ref<boolean>
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
clear: () => void
error: Ref<ErrorT | null>
status: Ref<AsyncDataRequestStatus>
}

View File

@ -4,7 +4,7 @@ description: Generate an SSR-friendly unique identifier that can be passed to ac
---
::important
This composable is available since [Nuxt v3.10](/v3-10#ssr-safe-accessible-unique-id-creation).
This composable is available since [Nuxt v3.10](/blog/v3-10#ssr-safe-accessible-unique-id-creation).
::
`useId` generates an SSR-friendly unique identifier that can be passed to accessibility attributes.
@ -24,6 +24,10 @@ const id = useId()
</template>
```
::note
`useId` must be used in a component with a single root element, as it uses this root element's attributes to pass the id from server to client.
::
## Parameters
`useId` does not take any parameters.

View File

@ -39,7 +39,7 @@ Set `isLoading` to true and start to increase the `progress` value.
### `finish()`
Set the `progress` value to `100`, stop all timers and intervals then reset the loading state `500` ms later.
Set the `progress` value to `100`, stop all timers and intervals then reset the loading state `500` ms later. `finish` accepts a `{ force: true }` option to skip the interval before the state is reset.
### `clear()`

View File

@ -51,7 +51,7 @@ export default defineNuxtPlugin((nuxtApp) => {
})
nuxtApp.hook('vue:error', (..._args) => {
console.log('vue:error')
// if (process.client) {
// if (import.meta.client) {
// console.log(..._args)
// }
})
@ -120,7 +120,7 @@ Nuxt exposes the following properties through `ssrContext`:
export const useColor = () => useState<string>('color', () => 'pink')
export default defineNuxtPlugin((nuxtApp) => {
if (process.server) {
if (import.meta.server) {
const color = useColor()
}
})
@ -128,9 +128,9 @@ Nuxt exposes the following properties through `ssrContext`:
It is also possible to use more advanced types, such as `ref`, `reactive`, `shallowRef`, `shallowReactive` and `NuxtError`.
Since [Nuxt v3.4](https://nuxt.com/blog/v3-4#payload-enhancements), it is possible to define your own serializer/deserializer for types that are not supported by Nuxt.
Since [Nuxt v3.4](https://nuxt.com/blog/v3-4#payload-enhancements), it is possible to define your own reducer/reviver for types that are not supported by Nuxt.
In the example below, we define a serializer for the [Luxon](https://moment.github.io/luxon/#/) DateTime class.
In the example below, we define a reducer (or a serializer) and a reviver (or deserializer) for the [Luxon](https://moment.github.io/luxon/#/) DateTime class, using a payload plugin.
```ts [plugins/date-time-payload.ts]
/**
@ -159,7 +159,7 @@ export default defineComponent({
setup (_props, { slots, emit }) {
const nuxtApp = useNuxtApp()
onErrorCaptured((err) => {
if (process.client && !nuxtApp.isHydrating) {
if (import.meta.client && !nuxtApp.isHydrating) {
// ...
}
})

View File

@ -0,0 +1,82 @@
---
title: "usePreviewMode"
description: "Use usePreviewMode to check and control preview mode in Nuxt"
---
# `usePreviewMode`
You can use the built-in `usePreviewMode` composable to access and control preview state in Nuxt. If the composable detects preview mode it will automatically force any updates necessary for [`useAsyncData`](/docs/api/composables/use-async-data) and [`useFetch`](/docs/api/composables/use-fetch) to rerender preview content.
```js
const { enabled, state } = usePreviewMode()
```
## Options
### Custom `enable` check
You can specify a custom way to enable preview mode. By default the `usePreviewMode` composable will enable preview mode if there is a `preview` param in url that is equal to `true` (for example, `http://localhost:3000?preview=true`). You can wrap the `usePreviewMode` into custom composable, to keep options consistent across usages and prevent any errors.
```js
export function useMyPreviewMode () {
return usePreviewMode({
shouldEnable: () => {
return !!route.query.customPreview
}
});
}
```
### Modify default state
`usePreviewMode` will try to store the value of a `token` param from url in state. You can modify this state and it will be available for all [`usePreviewMode`](/docs/api/composables/use-preview-mode) calls.
```js
const data1 = ref('data1')
const { enabled, state } = usePreviewMode({
getState: (currentState) => {
return { data1, data2: 'data2' }
}
})
```
::note
The `getState` function will append returned values to current state, so be careful not to accidentally overwrite important state.
::
## Example
```vue [pages/some-page.vue]
<script setup>
const route = useRoute()
const { enabled, state } = usePreviewMode({
shouldEnable: () => {
return route.query.customPreview === 'true'
},
})
const { data } = await useFetch('/api/preview', {
query: {
apiKey: state.token
}
})
</script>
<template>
<div>
Some base content
<p v-if="enabled">
Only preview content: {{ state.token }}
<br>
<button @click="enabled = false">
disable preview mode
</button>
</p>
</div>
</template>
```

View File

@ -10,6 +10,12 @@ links:
`useRequestURL` is a helper function that returns an [URL object](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) working on both server-side and client-side.
::important
When utilizing [Hybrid Rendering](/docs/guide/concepts/rendering#hybrid-rendering) with cache strategies, all incoming request headers are dropped when handling the cached responses via the [Nitro caching layer](https://nitro.unjs.io/guide/cache) (meaning `useRequestURL` will return `localhost` for the `host`).
You can define the [`cache.varies` option](https://nitro.unjs.io/guide/cache#options) to specify headers that will be considered when caching and serving the responses, such as `host` and `x-forwarded-host` for multi-tenant environments.
::
::code-group
```vue [pages/about.vue]

View File

@ -46,6 +46,6 @@ useSeoMeta({
## Parameters
There are over 100 parameters. See the [full list of parameters in the source code](https://github.com/harlan-zw/zhead/blob/main/src/metaFlat.ts).
There are over 100 parameters. See the [full list of parameters in the source code](https://github.com/harlan-zw/zhead/blob/main/packages/zhead/src/metaFlat.ts#L1035).
:read-more{to="/docs/getting-started/seo-meta"}

View File

@ -200,7 +200,7 @@ The two routes "/test-category" and "/1234-post" match both `[postId]-[postSlug]
To make sure that we are only matching digits (`\d+`) for `postId` in the `[postId]-[postSlug]` route, we can add the following to the `[postId]-[postSlug].vue` page template:
```vue [pages/[postId]-[postSlug].vue]
```vue [pages/[postId\\]-[postSlug\\].vue]
<script setup lang="ts">
definePageMeta({
path: '/:postId(\\d+)-:postSlug'

View File

@ -14,6 +14,10 @@ When prerendering, you can hint to Nitro to prerender additional paths, even if
`prerenderRoutes` can only be called within the [Nuxt context](/docs/guide/going-further/nuxt-app#the-nuxt-context).
::
::note
`prerenderRoutes` has to be executed during prerendering. If the `prerenderRoutes` is used in dynamic pages/routes which are not prerendered, then it will not be executed.
::
```js
const route = useRoute()

View File

@ -117,7 +117,7 @@ import { defineNuxtPlugin } from '#imports'
import metaConfig from '#build/meta.config.mjs'
export default defineNuxtPlugin((nuxtApp) => {
const createHead = process.server ? createServerHead : createClientHead
const createHead = import.meta.server ? createServerHead : createClientHead
const head = createHead()
head.push(metaConfig.globalMeta)

View File

@ -211,6 +211,10 @@ Type of path to resolve. If set to `'file'`, the function will try to resolve a
Creates resolver relative to base path.
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/resolving-paths-and-injecting-assets-to-the-app" target="_blank"}
Watch Vue School video about createResolver.
::
### Type
```ts

View File

@ -18,6 +18,10 @@ These functions are designed for registering your own utils, composables and Vue
Nuxt auto-imports helper functions, composables and Vue APIs to use across your application without explicitly importing them. Based on the directory structure, every Nuxt application can also use auto-imports for its own composables and plugins. Composables or plugins can use these functions.
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/expanding-nuxt-s-auto-imports" target="_blank"}
Watch Vue School video about Auto-imports Nuxt Kit utilities.
::
## `addImports`
Add imports to the Nuxt application. It makes your imports available in the Nuxt application without the need to import them manually.

View File

@ -10,6 +10,10 @@ links:
Components are the building blocks of your Nuxt application. They are reusable Vue instances that can be used to create a user interface. In Nuxt, components from the components directory are automatically imported by default. However, if you need to import components from an alternative directory or wish to selectively import them as needed, `@nuxt/kit` provides the `addComponentsDir` and `addComponent` methods. These utils allow you to customize the component configuration to better suit your needs.
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/injecting-components-and-component-directories" target="_blank"}
Watch Vue School video about injecting components.
::
## `addComponentsDir`
Register a directory to be scanned for components and imported only when used. Keep in mind, that this does not register components globally, until you specify `global: true` option.
@ -37,6 +41,11 @@ interface ComponentsDir {
transpile?: 'auto' | boolean
}
// You can augment this interface (exported from `@nuxt/schema`) if needed
interface ComponentMeta {
[key: string]: unknown
}
interface Component {
pascalName: string
kebabName: string
@ -50,6 +59,7 @@ interface Component {
island?: boolean
mode?: 'client' | 'server' | 'all'
priority?: number
meta?: ComponentMeta
}
```

View File

@ -12,6 +12,10 @@ links:
In Nuxt 3, routes are automatically generated based on the structure of the files in the `pages` directory. However, there may be scenarios where you'd want to customize these routes. For instance, you might need to add a route for a dynamic page not generated by Nuxt, remove an existing route, or modify the configuration of a route. For such customizations, Nuxt 3 offers the `extendPages` feature, which allows you to extend and alter the pages configuration.
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/extend-and-alter-nuxt-pages" target="_blank"}
Watch Vue School video about extendPages.
::
### Type
```ts
@ -67,6 +71,10 @@ Nuxt is powered by the [Nitro](https://nitro.unjs.io) server engine. With Nitro,
You can read more about Nitro route rules in the [Nitro documentation](https://nitro.unjs.io/guide/routing#route-rules).
::
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/adding-route-rules-and-route-middlewares" target="_blank"}
Watch Vue School video about adding route rules and route middelwares.
::
### Type
```ts
@ -184,6 +192,10 @@ Route middlewares can be also defined in plugins via [`addRouteMiddleware`](/doc
Read more about route middlewares in the [Route middleware documentation](/docs/getting-started/routing#route-middleware).
::
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/adding-route-rules-and-route-middlewares" target="_blank"}
Watch Vue School video about adding route rules and route middelwares.
::
### Type
```ts

View File

@ -14,6 +14,10 @@ Plugins are self-contained code that usually add app-level functionality to Vue.
Registers a Nuxt plugin and to the plugins array.
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/injecting-plugins" target="_blank"}
Watch Vue School video about addPlugin.
::
### Type
```ts
@ -110,6 +114,10 @@ export default defineNuxtPlugin((nuxtApp) => {
Adds a template and registers as a nuxt plugin. This is useful for plugins that need to generate code at build time.
::tip{icon="i-ph-video-duotone" to="https://vueschool.io/lessons/injecting-plugin-templates" target="_blank"}
Watch Vue School video about addPluginTemplate.
::
### Type
```ts
@ -243,7 +251,7 @@ export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(VueFire, { firebaseApp })
<% if(options.ssr) { %>
if (process.server) {
if (import.meta.server) {
nuxtApp.payload.vuefire = useSSRInitialState(undefined, firebaseApp)
} else if (nuxtApp.payload?.vuefire) {
useSSRInitialState(nuxtApp.payload.vuefire, firebaseApp)

View File

@ -22,12 +22,15 @@ Hook | Arguments | Environment | Description
`app:beforeMount` | `vueApp` | Client | Called before mounting the app, called only on client side.
`app:mounted` | `vueApp` | Client | Called when Vue app is initialized and mounted in browser.
`app:suspense:resolve` | `appComponent` | Client | On [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event.
`app:manifest:update` | `{ id, timestamp }` | Client | Called when there is a newer version of your app detected.
`link:prefetch` | `to` | Client | Called when a `<NuxtLink>` is observed to be prefetched.
`page:start` | `pageComponent?` | Client | Called on [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) pending event.
`page:finish` | `pageComponent?` | Client | Called on [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense) resolved event.
`page:loading:start` | - | Client | Called when the `setup()` of the new page is running.
`page:loading:end` | - | Client | Called after `page:finish`
`page:transition:finish`| `pageComponent?` | Client | After page transition [onAfterLeave](https://vuejs.org/guide/built-ins/transition.html#javascript-hooks) event.
`dev:ssr-logs` | `logs` | Client | Called with an array of server-side logs that have been passed to the client (if `features.devLogs` is enabled).
`page:view-transition:start` | `transition` | Client | Called after `document.startViewTransition` is called when [experimental viewTransition support is enabled](https://nuxt.com/docs/getting-started/transitions#view-transitions-api-experimental).
## Nuxt Hooks (build time)
@ -90,6 +93,7 @@ See [Nitro](https://nitro.unjs.io/guide/plugins#available-hooks) for all availab
Hook | Arguments | Description | Types
-----------------------|-----------------------|--------------------------------------|------------------
`dev:ssr-logs` | `{ path, logs }` | Server | Called at the end of a request cycle with an array of server-side logs.
`render:response` | `response, { event }` | Called before sending the response. | [response](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/renderer.ts#L24), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38)
`render:html` | `html, { event }` | Called before constructing the HTML. | [html](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/renderer.ts#L15), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38)
`render:island` | `islandResponse, { event, islandContext }` | Called before constructing the island HTML. | [islandResponse](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L28), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38), [islandContext](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L38)

View File

@ -146,8 +146,8 @@ We recommend using [VS Code](https://code.visualstudio.com) along with the [ESLi
```json [settings.json]
{
"editor.codeActionsOnSave": {
"source.fixAll": false,
"source.fixAll.eslint": true
"source.fixAll": "never",
"source.fixAll.eslint": "explicit"
}
}
```

View File

@ -30,23 +30,20 @@ Check [Discussions](https://github.com/nuxt/nuxt/discussions) and [RFCs](https:/
Milestone | Expected date | Notes | Description
-------------|---------------|------------------------------------------------------------------------|-----------------------
SEO & PWA | 2023 | [nuxt/nuxt#18395](https://github.com/nuxt/nuxt/discussions/18395) | Migrating from [nuxt-community/pwa-module](https://github.com/nuxt-community/pwa-module) for built-in SEO utils and service worker support
DevTools | 2023 | - | Integrated and modular devtools experience for Nuxt
Scripts | 2023 | [nuxt/nuxt#22016](https://github.com/nuxt/nuxt/discussions/22016) | Easy 3rd party script management.
Fonts | 2023 | [nuxt/nuxt#22014](https://github.com/nuxt/nuxt/discussions/22014) | Allow developers to easily configure fonts in their Nuxt apps.
Assets | 2023 | [nuxt/nuxt#22012](https://github.com/nuxt/nuxt/discussions/22012) | Allow developers and modules to handle loading third-party assets.
A11y | 2023 | [nuxt/nuxt#23255](https://github.com/nuxt/nuxt/issues/23255) | Accessibility hinting and utilities
SEO & PWA | 2024 | [nuxt/nuxt#18395](https://github.com/nuxt/nuxt/discussions/18395) | Migrating from [nuxt-community/pwa-module](https://github.com/nuxt-community/pwa-module) for built-in SEO utils and service worker support
Assets | 2024 | [nuxt/nuxt#22012](https://github.com/nuxt/nuxt/discussions/22012) | Allow developers and modules to handle loading third-party assets.
Translations | - | [nuxt/translations#4](https://github.com/nuxt/translations/discussions/4) ([request access](https://github.com/nuxt/nuxt/discussions/16054)) | A collaborative project for a stable translation process for Nuxt 3 docs. Currently pending for ideas and documentation tooling support (content v2 with remote sources).
## Core Modules
## Core Modules Roadmap
In addition to the Nuxt framework, there are modules that are vital for the ecosystem. Their status will be updated below.
Module | Status | Nuxt Support | Repository | Description
---------------|---------------------|--------------|------------|-------------------
Scripts | April 2024 | 3.x | `nuxt/scripts` to be announced | Easy 3rd party script management. [nuxt/nuxt#22016](https://github.com/nuxt/nuxt/discussions/22016)
A11y | Planned | 3.x | `nuxt/a11y` to be announced | Accessibility hinting and utilities [nuxt/nuxt#23255](https://github.com/nuxt/nuxt/issues/23255)
Auth | Planned | 3.x | `nuxt/auth` to be announced | Nuxt 3 support is planned after session support
Image | Active | 2.x and 3.x | [nuxt/image](https://github.com/nuxt/image) | Nuxt 3 support is in progress: [nuxt/image#548](https://github.com/nuxt/image/discussions/548)
I18n | Active | 2.x and 3.x | [nuxt-modules/i18n](https://github.com/nuxt-modules/i18n) | See [nuxt-modules/i18n#1287](https://github.com/nuxt-modules/i18n/discussions/1287) for Nuxt 3 support
Hints | Planned | 3.x | `nuxt/hints` to be announced | Guidance and suggestions for enhancing development practices
## Release Cycle

View File

@ -48,6 +48,16 @@ navigation.icon: i-ph-notification-duotone
::card
---
icon: i-simple-icons-github
title: nuxt/fonts
to: https://github.com/nuxt/fonts/releases
target: _blank
ui.icon.base: text-black dark:text-white
---
Nuxt Fonts releases.
::
::card
---
icon: i-simple-icons-github
title: nuxt/image
to: https://github.com/nuxt/image/releases
target: _blank

View File

@ -22,6 +22,16 @@ export default defineNuxtConfig({
// Enable Nuxt 3 compatible useHead
// meta: true,
// Enable definePageMeta macro
// macros: {
// pageMeta: true
// },
// Enable transpiling TypeScript with esbuild
// typescript: {
// esbuild: true
// },
// -- Default features --
// Use legacy server instead of Nitro

View File

@ -42,7 +42,24 @@ export default defineNuxtRouteMiddleware((to) => {
Use of `defineNuxtRouteMiddleware` is not supported outside of the middleware directory.
::
::note
## definePageMeta
You can also use [`definePageMeta`](https://nuxt.com/docs/api/utils/define-page-meta) in Nuxt Bridge.
You can be enabled with the `macros.pageMeta` option in your configuration file
```ts [nuxt.config.ts]
import { defineNuxtConfig } from '@nuxt/bridge'
export default defineNuxtConfig({
bridge: {
macros: {
pageMeta: true
}
}
})
```
::note
But only for `middleware` and `layout`.
::

View File

@ -57,6 +57,45 @@ Nuxt configuration will be loaded using [`unjs/jiti`](https://github.com/unjs/ji
::
1. If you were using `router.routeNameSplitter` you can achieve same result by updating route name generation logic in the new `pages:extend` hook:
::code-group
```ts [Nuxt 2]
export default {
router: {
routeNameSplitter: '/'
}
}
```
```ts [Nuxt 3]
import { createResolver } from '@nuxt/kit'
export default defineNuxtConfig({
hooks: {
'pages:extend' (routes) {
const routeNameSplitter = '/'
const root = createResolver(import.meta.url).resolve('./pages')
function updateName(routes) {
if (!routes) return
for (const route of routes) {
const relativePath = route.file.substring(root.length + 1)
route.name = relativePath.slice(0, -4).replace(/\/index$/, '').replace(/\//g, routeNameSplitter)
updateName(route.children)
}
}
updateName(routes)
},
},
})
```
::
#### ESM Syntax
Nuxt 3 is an [ESM native framework](/docs/guide/concepts/esm). Although [`unjs/jiti`](https://github.com/unjs/jiti) provides semi compatibility when loading `nuxt.config` file, avoid any usage of `require` and `module.exports` in this file.

280
eslint.config.mjs Normal file
View File

@ -0,0 +1,280 @@
// Configs
// import standard from "eslint-config-standard"
// import nuxt from '@nuxt/eslint-config'
// Plugins
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import jsdoc from 'eslint-plugin-jsdoc'
import esImport from 'eslint-plugin-import'
import unicorn from 'eslint-plugin-unicorn'
import noOnlyTests from 'eslint-plugin-no-only-tests'
/**
* eslintrc compatibility
* @see https://eslint.org/docs/latest/use/configure/migration-guide#using-eslintrc-configs-in-flat-config
* @see https://github.com/eslint/eslintrc#usage-esm
*/
import { FlatCompat } from '@eslint/eslintrc'
import js from '@eslint/js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended
})
// TODO: Type definition?
export default [
{
ignores: [
'**/dist/**',
'**/.nuxt/**',
'**/.nuxt-*/**',
'**/.output/**',
'**/.output-*/**',
'**/public/**',
'**/node_modules/**',
'packages/schema/schema',
// TODO: remove when fully migrated to flat config
'**/*.d.mts'
]
},
// standard,
...compat.extends('eslint-config-standard'),
jsdoc.configs['flat/recommended'],
// nuxt,
...compat.extends('@nuxt/eslint-config'),
esImport.configs.typescript,
{
rules: {
'import/export': 'off'
}
},
{
files: ['**/*.vue', '**/*.ts', '**/*.mts', '**/*.js', '**/*.cjs', '**/*.mjs'],
languageOptions: {
globals: {
NodeJS: 'readonly',
$fetch: 'readonly'
}
},
plugins: {
jsdoc,
import: esImport,
unicorn,
'no-only-tests': noOnlyTests
},
rules: {
// Imports should come first
'import/first': 'error',
// Other import rules
'import/no-mutable-exports': 'error',
// Allow unresolved imports
'import/no-unresolved': 'off',
// Allow paren-less arrow functions only when there's no braces
'arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
// Allow async-await
'generator-star-spacing': 'off',
// Prefer const over let
'prefer-const': ['error', { destructuring: 'any', ignoreReadBeforeAssign: false }],
// No single if in an "else" block
'no-lonely-if': 'error',
// Force curly braces for control flow,
// including if blocks with a single statement
curly: ['error', 'all'],
// No async function without await
'require-await': 'error',
// Force dot notation when possible
'dot-notation': 'error',
'no-var': 'error',
// Force object shorthand where possible
'object-shorthand': 'error',
// No useless destructuring/importing/exporting renames
'no-useless-rename': 'error',
/**********************/
/* Unicorn Rules */
/**********************/
// Pass error message when throwing errors
'unicorn/error-message': 'error',
// Uppercase regex escapes
'unicorn/escape-case': 'error',
// Array.isArray instead of instanceof
'unicorn/no-array-instanceof': 'error',
// Prevent deprecated `new Buffer()`
'unicorn/no-new-buffer': 'error',
// Keep regex literals safe!
'unicorn/no-unsafe-regex': 'off',
// Lowercase number formatting for octal, hex, binary (0x12 instead of 0X12)
'unicorn/number-literal-case': 'error',
// ** instead of Math.pow()
'unicorn/prefer-exponentiation-operator': 'error',
// includes over indexOf when checking for existence
'unicorn/prefer-includes': 'error',
// String methods startsWith/endsWith instead of more complicated stuff
'unicorn/prefer-starts-ends-with': 'error',
// textContent instead of innerText
'unicorn/prefer-text-content': 'error',
// Enforce throwing type error when throwing error while checking typeof
'unicorn/prefer-type-error': 'error',
// Use new when throwing error
'unicorn/throw-new-error': 'error',
'sort-imports': [
'error',
{
ignoreDeclarationSort: true
}
],
'no-only-tests/no-only-tests': 'error',
'unicorn/prefer-node-protocol': 'error',
'no-console': ['warn', { allow: ['warn', 'error', 'debug'] }],
'vue/one-component-per-file': 'off',
'vue/require-default-prop': 'off',
// Vue stylistic rules from `@antfu/eslint-config`
'vue/array-bracket-spacing': ['error', 'never'],
'vue/arrow-spacing': ['error', { after: true, before: true }],
'vue/block-spacing': ['error', 'always'],
'vue/block-tag-newline': [
'error',
{
multiline: 'always',
singleline: 'always'
}
],
'vue/brace-style': ['error', 'stroustrup', { allowSingleLine: true }],
'vue/comma-dangle': ['error', 'always-multiline'],
'vue/comma-spacing': ['error', { after: true, before: false }],
'vue/comma-style': ['error', 'last'],
'vue/html-comment-content-spacing': [
'error',
'always',
{
exceptions: ['-']
}
],
'vue/key-spacing': ['error', { afterColon: true, beforeColon: false }],
'vue/keyword-spacing': ['error', { after: true, before: true }],
'vue/object-curly-newline': 'off',
'vue/object-curly-spacing': ['error', 'always'],
'vue/object-property-newline': [
'error',
{ allowMultiplePropertiesPerLine: true }
],
'vue/operator-linebreak': ['error', 'before'],
'vue/padding-line-between-blocks': ['error', 'always'],
'vue/quote-props': ['error', 'consistent-as-needed'],
'vue/space-in-parens': ['error', 'never'],
'vue/template-curly-spacing': 'error',
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-param': 'off',
'jsdoc/require-returns': 'off',
'jsdoc/require-param-type': 'off',
'import/order': [
'error',
{
pathGroups: [
{
pattern: '#vue-router',
group: 'external'
}
]
}
],
'import/no-restricted-paths': [
'error',
{
zones: [
{
from: 'packages/nuxt/src/!(core)/**/*',
target: 'packages/nuxt/src/core',
message: 'core should not directly import from modules.'
},
{
from: 'packages/nuxt/src/!(app)/**/*',
target: 'packages/nuxt/src/app',
message: 'app should not directly import from modules.'
},
{
from: 'packages/nuxt/src/app/**/index.ts',
target: 'packages/nuxt/src',
message: 'should not import from barrel/index files'
},
{
from: 'packages/nitro',
target: 'packages/!(nitro)/**/*',
message: 'nitro should not directly import other packages.'
}
]
}
],
'@typescript-eslint/consistent-type-imports': [
'error',
{
disallowTypeAnnotations: false
}
],
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-expect-error': 'allow-with-description',
'ts-ignore': true
}
],
'@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'jsdoc/check-tag-names': [
'error',
{
definedTags: ['__NO_SIDE_EFFECTS__']
}
]
},
settings: {
jsdoc: {
ignoreInternal: true,
tagNamePreference: {
warning: 'warning',
note: 'note'
}
}
}
},
{
files: ['packages/schema/**'],
rules: {
'jsdoc/valid-types': 'off',
'jsdoc/check-tag-names': [
'error',
{
definedTags: ['experimental']
}
]
}
},
{
files: ['packages/nuxt/src/app/**', 'test/**', '**/runtime/**', '**/*.test.ts'],
rules: {
'no-console': 'off'
}
},
{
files: ['test/fixtures/**'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'vue/valid-v-for': 'off'
}
}
]

View File

@ -1,9 +1,11 @@
{
"$schema": "https://unpkg.com/knip@2/schema.json",
"$schema": "https://unpkg.com/knip@5/schema.json",
"workspaces": {
".": {
"entry": [
"scripts/*"
"scripts/*",
"test/*",
"test/fixtures/*"
]
},
"packages/*": {

View File

@ -11,5 +11,6 @@ exclude = [
"https://twitter.nuxt.dev/",
"https://github.com/nuxt/translations/discussions/4",
"https://stackoverflow.com/help/minimal-reproducible-example",
"(https?:\/\/github\.com\/)(.*\/)(generate)",
# single-quotes are required for regexp
'(https?:\/\/github\.com\/)(.*\/)(generate)',
]

View File

@ -3,13 +3,14 @@
import { addPluginTemplate } from 'nuxt/kit'
export default defineNuxtConfig({
typescript: { shim: process.env.DOCS_TYPECHECK === 'true' },
pages: process.env.DOCS_TYPECHECK === 'true',
modules: [
function () {
addPluginTemplate({
filename: 'plugins/my-plugin.mjs',
getContents: () => `export default defineNuxtPlugin({ name: 'my-plugin' })`
getContents: () => 'export default defineNuxtPlugin({ name: \'my-plugin\' })'
})
}
],
]
})

View File

@ -13,8 +13,8 @@
"cleanup": "rimraf 'packages/**/node_modules' 'playground/node_modules' 'node_modules'",
"dev": "pnpm play",
"dev:prepare": "pnpm --filter './packages/**' prepack --stub",
"lint": "eslint --ext .vue,.ts,.js,.mjs .",
"lint:fix": "eslint --ext .vue,.ts,.js,.mjs . --fix",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md",
"lint:docs:fix": "markdownlint ./docs --fix && case-police 'docs/**/*.md' *.md --fix",
"lint:knip": "pnpx knip",
@ -37,60 +37,61 @@
"@nuxt/schema": "workspace:*",
"@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
"rollup": "^4.12.0",
"rollup": "^4.14.0",
"nuxt": "workspace:*",
"vite": "5.1.4",
"vue": "3.4.20",
"magic-string": "^0.30.7"
"vite": "5.2.8",
"vue": "3.4.21",
"magic-string": "^0.30.9"
},
"devDependencies": {
"@codspeed/vitest-plugin": "3.1.0",
"@eslint/eslintrc": "3.0.2",
"@eslint/js": "8.57.0",
"@nuxt/eslint-config": "0.2.0",
"@nuxt/kit": "workspace:*",
"@nuxt/test-utils": "3.11.0",
"@nuxt/test-utils": "3.12.0",
"@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.0.2",
"@testing-library/vue": "8.0.3",
"@types/fs-extra": "11.0.4",
"@types/node": "20.11.20",
"@types/node": "20.12.4",
"@types/semver": "7.5.8",
"@vitest/coverage-v8": "1.3.1",
"@vue/test-utils": "2.4.4",
"@vitest/coverage-v8": "1.4.0",
"@vue/test-utils": "2.4.5",
"case-police": "0.6.1",
"changelogen": "0.5.5",
"consola": "3.2.3",
"devalue": "4.3.2",
"eslint": "8.57.0",
"eslint-config-standard": "17.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsdoc": "48.2.0",
"eslint-plugin-jsdoc": "48.2.2",
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-unicorn": "51.0.1",
"eslint-plugin-unicorn": "52.0.0",
"execa": "8.0.1",
"fs-extra": "11.2.0",
"globby": "14.0.1",
"h3": "1.11.1",
"happy-dom": "13.6.2",
"happy-dom": "14.4.0",
"jiti": "1.21.0",
"markdownlint-cli": "0.39.0",
"nitropack": "2.8.1",
"nuxi": "3.10.1",
"nitropack": "2.9.6",
"nuxi": "3.11.1",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.0.10",
"ofetch": "1.3.3",
"ofetch": "1.3.4",
"pathe": "1.1.2",
"playwright-core": "1.41.2",
"playwright-core": "1.42.1",
"rimraf": "5.0.5",
"semver": "7.6.0",
"std-env": "3.7.0",
"typescript": "5.3.3",
"ufo": "1.4.0",
"vitest": "1.3.1",
"typescript": "5.4.3",
"ufo": "1.5.3",
"vitest": "1.4.0",
"vitest-environment-nuxt": "1.0.0",
"vue": "3.4.20",
"vue-eslint-parser": "9.4.2",
"vue": "3.4.21",
"vue-router": "4.3.0",
"vue-tsc": "1.8.27"
"vue-tsc": "2.0.7"
},
"packageManager": "pnpm@8.15.4",
"packageManager": "pnpm@8.15.6",
"engines": {
"node": "^14.18.0 || >=16.10.0"
},

View File

@ -1,6 +1,6 @@
{
"name": "@nuxt/kit",
"version": "3.10.3",
"version": "3.11.2",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -27,20 +27,20 @@
},
"dependencies": {
"@nuxt/schema": "workspace:*",
"c12": "^1.9.0",
"c12": "^1.10.0",
"consola": "^3.2.3",
"defu": "^6.1.4",
"globby": "^14.0.1",
"hash-sum": "^2.0.0",
"ignore": "^5.3.1",
"jiti": "^1.21.0",
"knitwork": "^1.0.0",
"knitwork": "^1.1.0",
"mlly": "^1.6.1",
"pathe": "^1.1.2",
"pkg-types": "^1.0.3",
"scule": "^1.3.0",
"semver": "^7.6.0",
"ufo": "^1.4.0",
"ufo": "^1.5.3",
"unctx": "^2.3.1",
"unimport": "^3.7.1",
"untyped": "^1.4.2"
@ -50,11 +50,11 @@
"@types/lodash-es": "4.17.12",
"@types/semver": "7.5.8",
"lodash-es": "4.17.21",
"nitropack": "2.8.1",
"nitropack": "2.9.6",
"unbuild": "latest",
"vite": "5.1.4",
"vitest": "1.3.1",
"webpack": "5.90.3"
"vite": "5.2.8",
"vitest": "1.4.0",
"webpack": "5.91.0"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"

View File

@ -48,6 +48,7 @@ export async function addComponent (opts: AddComponentOptions) {
mode: 'all',
shortPath: opts.filePath,
priority: 0,
meta: {},
...opts
}

View File

@ -20,7 +20,7 @@ export * from './pages'
export * from './plugin'
export * from './resolve'
export * from './nitro'
export * from './template'
export { addTemplate, addTypeTemplate, normalizeTemplate, updateTemplates, writeTypes } from './template'
export * from './logger'
// Internal Utils

View File

@ -3,13 +3,13 @@ import { describe, expect, it, vi } from 'vitest'
import { consola } from 'consola'
import { logger, useLogger } from './logger'
vi.mock("consola", () => {
const logger = {} as any;
vi.mock('consola', () => {
const logger = {} as any
logger.create = vi.fn(() => ({...logger}));
logger.withTag = vi.fn(() => ({...logger}));
logger.create = vi.fn(() => ({ ...logger }))
logger.withTag = vi.fn(() => ({ ...logger }))
return { consola: logger };
return { consola: logger }
})
describe('logger', () => {
@ -20,29 +20,28 @@ describe('logger', () => {
describe('useLogger', () => {
it('should expose consola when not passing a tag', () => {
expect(useLogger()).toBe(consola);
});
expect(useLogger()).toBe(consola)
})
it('should create a new instance when passing a tag', () => {
const logger = vi.mocked(consola);
const logger = vi.mocked(consola)
const instance = useLogger("tag");
const instance = useLogger('tag')
expect(instance).toEqual(logger);
expect(instance).not.toBe(logger);
expect(logger.create).toBeCalledWith({});
expect(logger.withTag).toBeCalledWith("tag");
});
expect(instance).toEqual(logger)
expect(instance).not.toBe(logger)
expect(logger.create).toBeCalledWith({})
expect(logger.withTag).toBeCalledWith('tag')
})
it('should create a new instance when passing a tag and options', () => {
const logger = vi.mocked(consola);
const logger = vi.mocked(consola)
const instance = useLogger("tag", { level: 0 });
expect(instance).toEqual(logger);
expect(instance).not.toBe(logger);
expect(logger.create).toBeCalledWith({ level: 0 });
expect(logger.withTag).toBeCalledWith("tag");
});
const instance = useLogger('tag', { level: 0 })
expect(instance).toEqual(logger)
expect(instance).not.toBe(logger)
expect(logger.create).toBeCalledWith({ level: 0 })
expect(logger.withTag).toBeCalledWith('tag')
})
})

View File

@ -1,5 +1,5 @@
import { consola } from 'consola'
import type { ConsolaOptions } from 'consola';
import type { ConsolaOptions } from 'consola'
export const logger = consola

View File

@ -38,7 +38,7 @@ export async function installModule (moduleToInstall: string | NuxtModule, inlin
nuxt.options.build.transpile.push(normalizeModuleTranspilePath(moduleToInstall))
const directory = getDirectory(moduleToInstall)
if (directory !== moduleToInstall && !localLayerModuleDirs.has(directory)) {
nuxt.options.modulesDir.push(directory)
nuxt.options.modulesDir.push(resolve(directory, 'node_modules'))
}
}
@ -74,9 +74,9 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
const paths = [join(nuxtModule, 'nuxt'), join(nuxtModule, 'module'), nuxtModule]
let error: unknown
for (const path of paths) {
try {
const src = await resolvePath(path)
// Prefer ESM resolution if possible
try {
nuxtModule = await importModule(src, nuxt.options.modulesDir).catch(() => null) ?? requireModule(src, { paths: nuxt.options.modulesDir })
// nuxt-module-builder generates a module.json with metadata including the version

View File

@ -53,12 +53,12 @@ export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], op
if (find >= 0) {
if (app.middleware[find].path === middleware.path) { continue }
if (options.override === true) {
app.middleware[find] = middleware
app.middleware[find] = { ...middleware }
} else {
logger.warn(`'${middleware.name}' middleware already exists at '${app.middleware[find].path}'. You can set \`override: true\` to replace it.`)
}
} else {
app.middleware.push(middleware)
app.middleware.push({ ...middleware })
}
}
})

View File

@ -110,7 +110,8 @@ export function normalizeTemplate <T>(template: NuxtTemplate<T> | string): Resol
export async function updateTemplates (options?: { filter?: (template: ResolvedNuxtTemplate<any>) => boolean }) {
return await tryUseNuxt()?.hooks.callHook('builder:generateApp', options)
}
export async function writeTypes (nuxt: Nuxt) {
export async function _generateTypes (nuxt: Nuxt) {
const nodeModulePaths = getModulePaths(nuxt.options.modulesDir)
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
@ -248,6 +249,15 @@ export async function writeTypes (nuxt: Nuxt) {
''
].join('\n')
return {
declaration,
tsConfig
}
}
export async function writeTypes (nuxt: Nuxt) {
const { tsConfig, declaration } = await _generateTypes(nuxt)
async function writeFile () {
const GeneratedBy = '// Generated by nuxi'

View File

@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest'
import type { Nuxt, NuxtConfig } from '@nuxt/schema'
import { defu } from 'defu'
import { _generateTypes } from '../src/template'
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Record<string, any> ? DeepPartial<T[P]> : T[P]
}
const mockNuxt = {
options: {
rootDir: '/my-app',
srcDir: '/my-app',
alias: {
'~': '/my-app',
'some-custom-alias': '/my-app/some-alias'
},
typescript: { includeWorkspace: false },
buildDir: '/my-app/.nuxt',
modulesDir: ['/my-app/node_modules', '/node_modules'],
modules: [],
_layers: [{ config: { srcDir: '/my-app' } }],
_installedModules: [],
_modules: []
},
callHook: () => {}
} satisfies DeepPartial<Nuxt> as unknown as Nuxt
const mockNuxtWithOptions = (options: NuxtConfig) => defu({ options }, mockNuxt) as Nuxt
describe('tsConfig generation', () => {
it('should add correct relative paths for aliases', async () => {
const { tsConfig } = await _generateTypes(mockNuxt)
expect(tsConfig.compilerOptions?.paths).toMatchInlineSnapshot(`
{
"#build": [
".",
],
"some-custom-alias": [
"../some-alias",
],
"~": [
"..",
],
}
`)
})
it('should add exclude for module paths', async () => {
const { tsConfig } = await _generateTypes(mockNuxtWithOptions({
modulesDir: ['/my-app/modules/test/node_modules', '/my-app/modules/node_modules', '/my-app/node_modules/@some/module/node_modules']
}))
expect(tsConfig.exclude).toMatchInlineSnapshot(`
[
"../modules/test/node_modules",
"../modules/node_modules",
"../node_modules/@some/module/node_modules",
"../node_modules",
"../../node_modules",
"../dist",
]
`)
})
})

View File

@ -9,7 +9,7 @@ const fixtures = {
'basic test fixture': 'test/fixtures/basic',
'basic test fixture (types)': 'test/fixtures/basic-types',
'minimal test fixture': 'test/fixtures/minimal',
'minimal test fixture (types)': 'test/fixtures/minimal-types',
'minimal test fixture (types)': 'test/fixtures/minimal-types'
}
describe('loadNuxtConfig', () => {

View File

@ -1,3 +1,4 @@
/* eslint-disable jsdoc/require-jsdoc */
function defineNuxtConfig (config) {
return config
}

View File

@ -1,6 +1,6 @@
{
"name": "nuxt",
"version": "3.10.3",
"version": "3.11.2",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -60,24 +60,24 @@
},
"dependencies": {
"@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.0.8",
"@nuxt/devtools": "^1.1.5",
"@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.5.3",
"@nuxt/ui-templates": "^1.3.1",
"@nuxt/ui-templates": "^1.3.2",
"@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.8.10",
"@unhead/ssr": "^1.8.10",
"@unhead/vue": "^1.8.10",
"@vue/shared": "^3.4.20",
"@unhead/dom": "^1.9.4",
"@unhead/ssr": "^1.9.4",
"@unhead/vue": "^1.9.4",
"@vue/shared": "^3.4.21",
"acorn": "8.11.3",
"c12": "^1.9.0",
"c12": "^1.10.0",
"chokidar": "^3.6.0",
"cookie-es": "^1.0.0",
"cookie-es": "^1.1.0",
"defu": "^6.1.4",
"destr": "^2.0.3",
"devalue": "^4.3.2",
"esbuild": "^0.20.1",
"esbuild": "^0.20.2",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"fs-extra": "^11.2.0",
@ -86,31 +86,32 @@
"hookable": "^5.5.3",
"jiti": "^1.21.0",
"klona": "^2.0.6",
"knitwork": "^1.0.0",
"magic-string": "^0.30.7",
"knitwork": "^1.1.0",
"magic-string": "^0.30.9",
"mlly": "^1.6.1",
"nitropack": "^2.8.1",
"nuxi": "^3.10.1",
"nypm": "^0.3.6",
"ofetch": "^1.3.3",
"nitropack": "^2.9.6",
"nuxi": "^3.11.1",
"nypm": "^0.3.8",
"ofetch": "^1.3.4",
"ohash": "^1.1.3",
"pathe": "^1.1.2",
"perfect-debounce": "^1.0.0",
"pkg-types": "^1.0.3",
"radix3": "^1.1.0",
"radix3": "^1.1.2",
"scule": "^1.3.0",
"std-env": "^3.7.0",
"strip-literal": "^2.0.0",
"ufo": "^1.4.0",
"strip-literal": "^2.1.0",
"ufo": "^1.5.3",
"ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3",
"unctx": "^2.3.1",
"unenv": "^1.9.0",
"unimport": "^3.7.1",
"unplugin": "^1.7.1",
"unplugin": "^1.10.1",
"unplugin-vue-router": "^0.7.0",
"unstorage": "^1.10.2",
"untyped": "^1.4.2",
"vue": "^3.4.20",
"vue": "^3.4.21",
"vue-bundle-renderer": "^2.0.0",
"vue-devtools-stub": "^0.1.0",
"vue-router": "^4.3.0"
@ -121,8 +122,8 @@
"@types/fs-extra": "11.0.4",
"@vitejs/plugin-vue": "5.0.4",
"unbuild": "latest",
"vite": "5.1.4",
"vitest": "1.3.1"
"vite": "5.2.8",
"vitest": "1.4.0"
},
"peerDependencies": {
"@parcel/watcher": "^2.1.0",

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line import/export
export * from 'vue'
export const install = () => {}

View File

@ -2,7 +2,9 @@ import { createError } from '../composables/error'
const intervalError = '[nuxt] `setInterval` should not be used on the server. Consider wrapping it with an `onNuxtReady`, `onBeforeMount` or `onMounted` lifecycle hook, or ensure you only call it in the browser by checking `import.meta.client`.'
export const setInterval = import.meta.client ? window.setInterval : () => {
export const setInterval = import.meta.client
? window.setInterval
: () => {
if (import.meta.dev) {
throw createError({
statusCode: 500,

View File

@ -9,14 +9,13 @@ import { joinURL, withQuery } from 'ufo'
import type { FetchResponse } from 'ofetch'
import { join } from 'pathe'
// eslint-disable-next-line import/no-restricted-paths
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
import type { NuxtIslandResponse } from '../types'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import { prerenderRoutes, useRequestEvent } from '../composables/ssr'
import { getFragmentHTML } from './utils'
// @ts-expect-error virtual file
import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'
import { appBaseURL, remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'
const pKey = '_islandPromises'
const SSR_UID_RE = /data-island-uid="([^"]*)"/
@ -29,7 +28,7 @@ const getId = import.meta.client ? () => (id++).toString() : randomUUID
const components = import.meta.client ? new Map<string, Component>() : undefined
async function loadComponents (source = '/', paths: NuxtIslandResponse['components']) {
async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['components']) {
const promises = []
for (const component in paths) {
@ -69,7 +68,8 @@ export default defineComponent({
default: false
}
},
async setup (props, { slots, expose }) {
emits: ['error'],
async setup (props, { slots, expose, emit }) {
let canTeleport = import.meta.server
const teleportKey = ref(0)
const key = ref(0)
@ -88,37 +88,41 @@ export default defineComponent({
onMounted(() => { mounted.value = true; teleportKey.value++ })
function setPayload (key: string, result: NuxtIslandResponse) {
const toRevive: Partial<NuxtIslandResponse> = {}
if (result.props) { toRevive.props = result.props }
if (result.slots) { toRevive.slots = result.slots }
if (result.components) { toRevive.components = result.components }
nuxtApp.payload.data[key] = {
__nuxt_island: {
key,
...(import.meta.server && import.meta.prerender)
? {}
: { params: { ...props.context, props: props.props ? JSON.stringify(props.props) : undefined } },
result: {
props: result.props,
slots: result.slots,
components: result.components
}
result: toRevive
},
...result
}
}
const payloads: Required<Pick<NuxtIslandResponse, 'slots' | 'components'>> = {
slots: {},
components: {}
const payloads: Partial<Pick<NuxtIslandResponse, 'slots' | 'components'>> = {}
if (instance.vnode.el) {
const slots = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.slots
if (slots) { payloads.slots = slots }
if (selectiveClient) {
const components = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.components
if (components) { payloads.components = components }
}
if (nuxtApp.isHydrating) {
payloads.slots = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.slots ?? {}
payloads.components = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.components ?? {}
}
const ssrHTML = ref<string>('')
if (import.meta.client && nuxtApp.isHydrating) {
ssrHTML.value = getFragmentHTML(instance.vnode?.el ?? null, true)?.join('') || ''
if (import.meta.client && instance.vnode?.el) {
ssrHTML.value = getFragmentHTML(instance.vnode.el, true)?.join('') || ''
const key = `${props.name}_${hashId.value}`
nuxtApp.payload.data[key] ||= {}
nuxtApp.payload.data[key].html = ssrHTML.value
}
const uid = ref<string>(ssrHTML.value.match(SSR_UID_RE)?.[1] ?? getId())
@ -135,12 +139,15 @@ export default defineComponent({
}
}
if (payloads.slots) {
return html.replaceAll(SLOT_FALLBACK_RE, (full, slotName) => {
if (!currentSlots.includes(slotName)) {
return full + (payloads.slots[slotName]?.fallback || '')
return full + (payloads.slots?.[slotName]?.fallback || '')
}
return full
})
}
return html
})
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
@ -149,7 +156,7 @@ export default defineComponent({
async function _fetchComponent (force = false) {
const key = `${props.name}_${hashId.value}`
if (nuxtApp.payload.data[key]?.html && !force) { return nuxtApp.payload.data[key] }
if (!force && nuxtApp.payload.data[key]?.html) { return nuxtApp.payload.data[key] }
const url = remoteComponentIslands && props.source ? new URL(`/__nuxt_island/${key}.json`, props.source).href : `/__nuxt_island/${key}.json`
@ -207,6 +214,7 @@ export default defineComponent({
}
} catch (e) {
error.value = e
emit('error', e)
}
}
@ -247,38 +255,46 @@ export default defineComponent({
// this is used to force trigger Teleport when vue makes the diff between old and new node
const isKeyOdd = teleportKey.value === 0 || !!(teleportKey.value && !(teleportKey.value % 2))
if (uid.value && html.value && (import.meta.server || props.lazy ? canTeleport : mounted.value || nuxtApp.isHydrating)) {
if (uid.value && html.value && (import.meta.server || props.lazy ? canTeleport : (mounted.value || instance.vnode?.el))) {
for (const slot in slots) {
if (availableSlots.value.includes(slot)) {
teleports.push(createVNode(Teleport,
// use different selectors for even and odd teleportKey to force trigger the teleport
{ to: import.meta.client ? `${isKeyOdd ? 'div' : ''}[data-island-uid="${uid.value}"][data-island-slot="${slot}"]` : `uid=${uid.value};slot=${slot}` },
{ default: () => (payloads.slots[slot].props?.length ? payloads.slots[slot].props : [{}]).map((data: any) => slots[slot]?.(data)) })
{ default: () => (payloads.slots?.[slot].props?.length ? payloads.slots[slot].props : [{}]).map((data: any) => slots[slot]?.(data)) })
)
}
}
if (selectiveClient) {
if (import.meta.server) {
for (const [id, info] of Object.entries(payloads.components ?? {})) {
const { html } = info
if (payloads.components) {
for (const [id, info] of Object.entries(payloads.components)) {
const { html, slots } = info
let replaced = html.replaceAll('data-island-uid', `data-island-uid="${uid.value}"`)
for (const slot in slots) {
replaced = replaced.replaceAll(`data-island-slot="${slot}">`, full => full + slots[slot])
}
teleports.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
default: () => [createStaticVNode(html, 1)]
default: () => [createStaticVNode(replaced, 1)]
}))
}
}
if (selectiveClient && import.meta.client && canLoadClientComponent.value) {
for (const [id, info] of Object.entries(payloads.components ?? {})) {
const { props } = info
} else if (canLoadClientComponent.value && payloads.components) {
for (const [id, info] of Object.entries(payloads.components)) {
const { props, slots } = info
const component = components!.get(id)!
// use different selectors for even and odd teleportKey to force trigger the teleport
const vnode = createVNode(Teleport, { to: `${isKeyOdd ? 'div' : ''}[data-island-uid='${uid.value}'][data-island-component="${id}"]` }, {
default: () => {
return [h(component, props)]
return [h(component, props, Object.fromEntries(Object.entries(slots || {}).map(([k, v]) => ([k, () => createStaticVNode(`<div style="display: contents" data-island-uid data-island-slot="${k}">${v}</div>`, 1)
]))))]
}
})
teleports.push(vnode)
}
}
}
}
return h(Fragment, teleports)
}, _cache, 1)

View File

@ -23,30 +23,7 @@ const firstNonUndefined = <T> (...args: (T | undefined)[]) => args.find(arg => a
const NuxtLinkDevKeySymbol: InjectionKey<boolean> = Symbol('nuxt-link-dev-key')
/**
* Create a NuxtLink component with given options as defaults.
* @see https://nuxt.com/docs/api/components/nuxt-link
*/
export interface NuxtLinkOptions extends
Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>,
Pick<NuxtLinkProps, 'prefetchedClass'> {
/**
* The name of the component.
* @default "NuxtLink"
*/
componentName?: string
/**
* A default `rel` attribute value applied on external links. Defaults to `"noopener noreferrer"`. Set it to `""` to disable.
*/
externalRelAttribute?: string | null
/**
* An option to either add or remove trailing slashes in the `href`.
* If unset or not matching the valid values `append` or `remove`, it will be ignored.
*/
trailingSlash?: 'append' | 'remove'
}
/**
* <NuxtLink> is a drop-in replacement for both Vue Router's <RouterLink> component and HTML's <a> tag.
* `<NuxtLink>` is a drop-in replacement for both Vue Router's `<RouterLink>` component and HTML's `<a>` tag.
* @see https://nuxt.com/docs/api/components/nuxt-link
*/
export interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
@ -88,6 +65,29 @@ export interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
noPrefetch?: boolean
}
/**
* Create a NuxtLink component with given options as defaults.
* @see https://nuxt.com/docs/api/components/nuxt-link
*/
export interface NuxtLinkOptions extends
Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>,
Pick<NuxtLinkProps, 'prefetchedClass'> {
/**
* The name of the component.
* @default "NuxtLink"
*/
componentName?: string
/**
* A default `rel` attribute value applied on external links. Defaults to `"noopener noreferrer"`. Set it to `""` to disable.
*/
externalRelAttribute?: string | null
/**
* An option to either add or remove trailing slashes in the `href`.
* If unset or not matching the valid values `append` or `remove`, it will be ignored.
*/
trailingSlash?: 'append' | 'remove'
}
/* @__NO_SIDE_EFFECTS__ */
export function defineNuxtLink (options: NuxtLinkOptions) {
const componentName = options.componentName || 'NuxtLink'
@ -113,14 +113,10 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
const resolvedPath = {
...to,
name: undefined, // named routes would otherwise always override trailing slash behavior
path: applyTrailingSlashBehavior(path, options.trailingSlash)
}
// named routes would otherwise always override trailing slash behavior
if ('name' in resolvedPath) {
delete resolvedPath.name
}
return resolvedPath
}

View File

@ -23,13 +23,13 @@ export default defineComponent({
estimatedProgress: {
type: Function as unknown as () => (duration: number, elapsed: number) => number,
required: false
},
}
},
setup (props, { slots, expose }) {
const { progress, isLoading, start, finish, clear } = useLoadingIndicator({
duration: props.duration,
throttle: props.throttle,
estimatedProgress: props.estimatedProgress,
estimatedProgress: props.estimatedProgress
})
expose({

View File

@ -24,8 +24,10 @@ import { useRoute, useRouter } from '../composables/router'
import { PageRouteSymbol } from '../components/injections'
import AppComponent from '#build/app-component.mjs'
import ErrorComponent from '#build/error-component.mjs'
// @ts-expect-error virtual file
import { componentIslands } from '#build/nuxt.config.mjs'
const IslandRenderer = import.meta.server
const IslandRenderer = import.meta.server && componentIslands
? defineAsyncComponent(() => import('./island-renderer').then(r => r.default || r))
: () => null

View File

@ -1,5 +1,5 @@
import type { Component } from 'vue'
import { Teleport, defineComponent, h } from 'vue'
import type { Component, InjectionKey } from 'vue'
import { Teleport, defineComponent, h, inject, provide } from 'vue'
import { useNuxtApp } from '../nuxt'
// @ts-expect-error virtual file
import { paths } from '#build/components-chunk'
@ -9,6 +9,8 @@ type ExtendedComponent = Component & {
__name: string
}
export const NuxtTeleportIslandSymbol = Symbol('NuxtTeleportIslandComponent') as InjectionKey<false | string>
/**
* component only used with componentsIsland
* this teleport the component in SSR only if it needs to be hydrated on client
@ -37,8 +39,10 @@ export default defineComponent({
setup (props, { slots }) {
const nuxtApp = useNuxtApp()
if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient) { return () => slots.default!() }
// if there's already a teleport parent, we don't need to teleport or to render the wrapped component client side
if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient || inject(NuxtTeleportIslandSymbol, false)) { return () => slots.default?.() }
provide(NuxtTeleportIslandSymbol, props.to)
const islandContext = nuxtApp.ssrContext!.islandContext!
return () => {

View File

@ -1,5 +1,7 @@
import { Teleport, defineComponent, h } from 'vue'
import type { VNode } from 'vue'
import { Teleport, createVNode, defineComponent, h, inject } from 'vue'
import { useNuxtApp } from '../nuxt'
import { NuxtTeleportIslandSymbol } from './nuxt-teleport-island-component'
/**
* component only used within islands for slot teleport
@ -22,21 +24,34 @@ export default defineComponent({
setup (props, { slots }) {
const nuxtApp = useNuxtApp()
const islandContext = nuxtApp.ssrContext?.islandContext
if (!islandContext) {
return () => slots.default?.()
return () => slots.default?.()[0]
}
const componentName = inject(NuxtTeleportIslandSymbol, false)
islandContext.slots[props.name] = {
props: (props.props || []) as unknown[]
}
return () => {
const vnodes = [h('div', {
const vnodes: VNode[] = []
if (nuxtApp.ssrContext?.islandContext && slots.default) {
vnodes.push(h('div', {
style: 'display: contents;',
'data-island-uid': '',
'data-island-slot': props.name,
})]
'data-island-slot': props.name
}, {
// Teleport in slot to not be hydrated client-side with the staticVNode
default: () => [createVNode(Teleport, { to: `island-slot=${componentName};${props.name}` }, slots.default?.())]
}))
} else {
vnodes.push(h('div', {
style: 'display: contents;',
'data-island-uid': '',
'data-island-slot': props.name
}))
}
if (slots.fallback) {
vnodes.push(h(Teleport, { to: `island-fallback=${props.name}` }, slots.fallback()))

View File

@ -75,24 +75,6 @@ export function createBuffer () {
}
}
const TRANSLATE_RE = /&(nbsp|amp|quot|lt|gt);/g
const NUMSTR_RE = /&#(\d+);/gi
export function decodeHtmlEntities (html: string) {
const translateDict = {
nbsp: ' ',
amp: '&',
quot: '"',
lt: '<',
gt: '>'
} as const
return html.replace(TRANSLATE_RE, function (_, entity: keyof typeof translateDict) {
return translateDict[entity]
}).replace(NUMSTR_RE, function (_, numStr: string) {
const num = parseInt(numStr, 10)
return String.fromCharCode(num)
})
}
/**
* helper for NuxtIsland to generate a correct array for scoped data
*/

View File

@ -1,4 +1,4 @@
import { getCurrentInstance, onBeforeMount, onServerPrefetch, onUnmounted, ref, shallowRef, toRef, unref, watch } from 'vue'
import { computed, getCurrentInstance, getCurrentScope, onBeforeMount, onScopeDispose, onServerPrefetch, onUnmounted, ref, shallowRef, toRef, unref, watch } from 'vue'
import type { Ref, WatchSource } from 'vue'
import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt'
@ -12,7 +12,7 @@ import { asyncDataDefaults } from '#build/nuxt.config.mjs'
export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
export type _Transform<Input = any, Output = any> = (input: Input) => Output
export type _Transform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>
export type PickFrom<T, K extends Array<string>> = T extends Array<any>
? T
@ -61,13 +61,15 @@ export interface AsyncDataOptions<
* A `null` or `undefined` return value will trigger a fetch.
* Default is `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]` which only caches data when payloadExtraction is enabled.
*/
getCachedData?: (key: string) => DataT
getCachedData?: (key: string, nuxtApp: NuxtApp) => DataT
/**
* A function that can be used to alter handler function result after resolving
* A function that can be used to alter handler function result after resolving.
* Do not use it along with the `pick` option.
*/
transform?: _Transform<ResT, DataT>
/**
* Only pick specified keys in this array from the handler function result
* Only pick specified keys in this array from the handler function result.
* Do not use it along with the `transform` option.
*/
pick?: PickKeys
/**
@ -109,6 +111,7 @@ export interface _AsyncData<DataT, ErrorT> {
pending: Ref<boolean>
refresh: (opts?: AsyncDataExecuteOptions) => Promise<void>
execute: (opts?: AsyncDataExecuteOptions) => Promise<void>
clear: () => void
error: Ref<ErrorT | null>
status: Ref<AsyncDataRequestStatus>
}
@ -212,11 +215,14 @@ export function useAsyncData<
const nuxtApp = useNuxtApp()
// When prerendering, share payload data automatically between requests
const handler = import.meta.client || !import.meta.prerender || !nuxtApp.ssrContext?._sharedPrerenderCache ? _handler : async () => {
const value = await nuxtApp.ssrContext!._sharedPrerenderCache!.get(key)
if (value) { return value as ResT }
const handler = import.meta.client || !import.meta.prerender || !nuxtApp.ssrContext?._sharedPrerenderCache
? _handler
: () => {
const value = nuxtApp.ssrContext!._sharedPrerenderCache!.get(key)
if (value) { return value as Promise<ResT> }
const promise = nuxtApp.runWithContext(_handler)
nuxtApp.ssrContext!._sharedPrerenderCache!.set(key, promise)
return promise
}
@ -239,7 +245,7 @@ export function useAsyncData<
console.warn('[nuxt] `boolean` values are deprecated for the `dedupe` option of `useAsyncData` and will be removed in the future. Use \'cancel\' or \'defer\' instead.')
}
const hasCachedData = () => ![null, undefined].includes(options.getCachedData!(key) as any)
const hasCachedData = () => options.getCachedData!(key, nuxtApp) != null
// Create or use a shared asyncData entity
if (!nuxtApp._asyncData[key] || !options.immediate) {
@ -248,7 +254,7 @@ export function useAsyncData<
const _ref = options.deep ? ref : shallowRef
nuxtApp._asyncData[key] = {
data: _ref(options.getCachedData!(key) ?? options.default!()),
data: _ref(options.getCachedData!(key, nuxtApp) ?? options.default!()),
pending: ref(!hasCachedData()),
error: toRef(nuxtApp.payload._errors, key),
status: ref('idle')
@ -268,7 +274,7 @@ export function useAsyncData<
}
// Avoid fetching same key that is already fetched
if ((opts._initial || (nuxtApp.isHydrating && opts._initial !== false)) && hasCachedData()) {
return Promise.resolve(options.getCachedData!(key))
return Promise.resolve(options.getCachedData!(key, nuxtApp))
}
asyncData.pending.value = true
asyncData.status.value = 'pending'
@ -281,13 +287,13 @@ export function useAsyncData<
reject(err)
}
})
.then((_result) => {
.then(async (_result) => {
// If this request is cancelled, resolve to the latest request.
if ((promise as any).cancelled) { return nuxtApp._asyncDataPromises[key] }
let result = _result as unknown as DataT
if (options.transform) {
result = options.transform(_result)
result = await options.transform(_result)
}
if (options.pick) {
result = pick(result as any, options.pick) as DataT
@ -318,6 +324,8 @@ export function useAsyncData<
return nuxtApp._asyncDataPromises[key]!
}
asyncData.clear = () => clearNuxtDataByKey(nuxtApp, key)
const initialFetch = () => asyncData.refresh({ _initial: true })
const fetchOnServer = options.server !== false && nuxtApp.payload.serverRendered
@ -343,14 +351,12 @@ export function useAsyncData<
if (instance && !instance._nuxtOnBeforeMountCbs) {
instance._nuxtOnBeforeMountCbs = []
const cbs = instance._nuxtOnBeforeMountCbs
if (instance) {
onBeforeMount(() => {
cbs.forEach((cb) => { cb() })
cbs.splice(0, cbs.length)
})
onUnmounted(() => cbs.splice(0, cbs.length))
}
}
if (fetchOnServer && nuxtApp.isHydrating && (asyncData.error.value || hasCachedData())) {
// 1. Hydration (server: true): no fetch
@ -364,16 +370,20 @@ export function useAsyncData<
// 4. Navigation (lazy: false) - or plugin usage: await fetch
initialFetch()
}
const hasScope = getCurrentScope()
if (options.watch) {
watch(options.watch, () => asyncData.refresh())
const unsub = watch(options.watch, () => asyncData.refresh())
if (hasScope) {
onScopeDispose(unsub)
}
}
const off = nuxtApp.hook('app:data:refresh', async (keys) => {
if (!keys || keys.includes(key)) {
await asyncData.refresh()
}
})
if (instance) {
onUnmounted(off)
if (hasScope) {
onScopeDispose(off)
}
}
@ -457,7 +467,18 @@ export function useNuxtData<DataT = any> (key: string): { data: Ref<DataT | null
}
return {
data: toRef(nuxtApp.payload.data, key)
data: computed({
get () {
return nuxtApp._asyncData[key]?.data.value ?? nuxtApp.payload.data[key]
},
set (value) {
if (nuxtApp._asyncData[key]) {
nuxtApp._asyncData[key]!.data.value = value
} else {
nuxtApp.payload.data[key] = value
}
}
})
}
}
@ -484,23 +505,31 @@ export function clearNuxtData (keys?: string | string[] | ((key: string) => bool
: toArray(keys)
for (const key of _keys) {
clearNuxtDataByKey(nuxtApp, key)
}
}
function clearNuxtDataByKey (nuxtApp: NuxtApp, key: string): void {
if (key in nuxtApp.payload.data) {
nuxtApp.payload.data[key] = undefined
}
if (key in nuxtApp.payload._errors) {
nuxtApp.payload._errors[key] = null
}
if (nuxtApp._asyncData[key]) {
nuxtApp._asyncData[key]!.data.value = undefined
nuxtApp._asyncData[key]!.error.value = null
nuxtApp._asyncData[key]!.pending.value = false
nuxtApp._asyncData[key]!.status.value = 'idle'
}
if (key in nuxtApp._asyncDataPromises) {
(nuxtApp._asyncDataPromises[key] as any).cancelled = true
nuxtApp._asyncDataPromises[key] = undefined
}
}
}
function pick (obj: Record<string, any>, keys: string[]) {
const newObj = {}

View File

@ -55,7 +55,7 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
// use a custom ref to expire the cookie on client side otherwise use basic ref
const cookie = import.meta.client && delay && !hasExpired
? cookieRef<T | undefined>(cookieValue, delay)
? cookieRef<T | undefined>(cookieValue, delay, opts.watch && opts.watch !== 'shallow')
: ref<T | undefined>(cookieValue)
if (import.meta.dev && hasExpired) {
@ -63,7 +63,15 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
}
if (import.meta.client) {
const channel = store || typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`nuxt:cookies:${name}`)
let channel: null | BroadcastChannel = null
try {
if (!store && typeof BroadcastChannel !== 'undefined') {
channel = new BroadcastChannel(`nuxt:cookies:${name}`)
}
} catch {
// BroadcastChannel will fail in certain situations when cookies are disabled
// or running in an iframe: see https://github.com/nuxt/nuxt/issues/26338
}
const callback = () => {
if (opts.readonly || isEqual(cookie.value, cookies[name])) { return }
writeClientCookie(name, cookie.value, opts as CookieSerializeOptions)
@ -92,7 +100,7 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
if (store) {
store.onchange = (event) => {
const cookie = event.changed.find((c: any) => c.name === name)
if (cookie) handleChange({ value: cookie.value })
if (cookie) { handleChange({ value: cookie.value }) }
}
} else if (channel) {
channel.onmessage = ({ data }) => handleChange(data)
@ -124,7 +132,7 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
}
/** @since 3.10.0 */
export function refreshCookie (name: string) {
if (store || typeof BroadcastChannel === 'undefined') return
if (import.meta.server || store || typeof BroadcastChannel === 'undefined') { return }
new BroadcastChannel(`nuxt:cookies:${name}`)?.postMessage({ refresh: true })
}
@ -174,14 +182,21 @@ function writeServerCookie (event: H3Event, name: string, value: any, opts: Cook
const MAX_TIMEOUT_DELAY = 2_147_483_647
// custom ref that will update the value to undefined if the cookie expires
function cookieRef<T> (value: T | undefined, delay: number) {
function cookieRef<T> (value: T | undefined, delay: number, shouldWatch: boolean) {
let timeout: NodeJS.Timeout
let unsubscribe: (() => void) | undefined
let elapsed = 0
const internalRef = shouldWatch ? ref(value) : { value }
if (getCurrentScope()) {
onScopeDispose(() => { clearTimeout(timeout) })
onScopeDispose(() => {
unsubscribe?.()
clearTimeout(timeout)
})
}
return customRef((track, trigger) => {
if (shouldWatch) { unsubscribe = watch(internalRef, trigger) }
function createExpirationTimeout () {
clearTimeout(timeout)
const timeRemaining = delay - elapsed
@ -190,7 +205,7 @@ function cookieRef<T> (value: T | undefined, delay: number) {
elapsed += timeoutLength
if (elapsed < delay) { return createExpirationTimeout() }
value = undefined
internalRef.value = undefined
trigger()
}, timeoutLength)
}
@ -198,12 +213,12 @@ function cookieRef<T> (value: T | undefined, delay: number) {
return {
get () {
track()
return value
return internalRef.value
},
set (newValue) {
createExpirationTimeout()
value = newValue
internalRef.value = newValue
trigger()
}
}

View File

@ -53,9 +53,7 @@ export const clearError = async (options: { redirect?: string } = {}) => {
/** @since 3.0.0 */
export const isNuxtError = <DataT = unknown>(
error?: string | object
): error is NuxtError<DataT> => (
!!error && typeof error === 'object' && NUXT_ERROR_SIGNATURE in error
)
): error is NuxtError<DataT> => !!error && typeof error === 'object' && NUXT_ERROR_SIGNATURE in error
/** @since 3.0.0 */
export const createError = <DataT = unknown>(

View File

@ -94,13 +94,7 @@ export function useFetch<
) {
const [opts = {}, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]
const _request = computed(() => {
let r = request
if (typeof r === 'function') {
r = r()
}
return toValue(r)
})
const _request = computed(() => toValue(request))
const _key = opts.key || hash([autoKey, typeof _request.value === 'string' ? _request.value : '', ...generateOptionSegments(opts)])
if (!_key || typeof _key !== 'string') {
@ -246,7 +240,7 @@ export function useLazyFetch<
function generateOptionSegments <_ResT, DataT, DefaultT> (opts: UseFetchOptions<_ResT, DataT, any, DefaultT, any, any>) {
const segments: Array<string | undefined | Record<string, string>> = [
toValue(opts.method as MaybeRef<string | undefined> | undefined)?.toUpperCase() || 'GET',
toValue(opts.baseURL),
toValue(opts.baseURL)
]
for (const _obj of [opts.params || opts.query]) {
const obj = toValue(_obj)

View File

@ -3,6 +3,7 @@ import { useNuxtApp } from '../nuxt'
import { clientOnlySymbol } from '#app/components/client-only'
const ATTR_KEY = 'data-n-ids'
const SEPARATOR = '-'
/**
* Generate an SSR-friendly unique identifier that can be passed to accessibility attributes.
@ -13,7 +14,8 @@ export function useId (key?: string): string {
throw new TypeError('[nuxt] [useId] key must be a string.')
}
// TODO: implement in composable-keys
key = key.slice(1)
// Make sure key starts with a letter to be a valid selector
key = `n${key.slice(1)}`
const nuxtApp = useNuxtApp()
const instance = getCurrentInstance()
@ -26,11 +28,11 @@ export function useId (key?: string): string {
instance._nuxtIdIndex ||= {}
instance._nuxtIdIndex[key] ||= 0
const instanceIndex = key + ':' + instance._nuxtIdIndex[key]++
const instanceIndex = key + SEPARATOR + instance._nuxtIdIndex[key]++
if (import.meta.server) {
const ids = JSON.parse(instance.attrs[ATTR_KEY] as string | undefined || '{}')
ids[instanceIndex] = key + ':' + nuxtApp._id++
ids[instanceIndex] = key + SEPARATOR + nuxtApp._id++
instance.attrs[ATTR_KEY] = JSON.stringify(ids)
return ids[instanceIndex]
}

View File

@ -14,7 +14,7 @@ export {
export { defineNuxtComponent } from './component'
export { useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData } from './asyncData'
export type { AsyncDataOptions, AsyncData } from './asyncData'
export type { AsyncDataOptions, AsyncData, AsyncDataRequestStatus } from './asyncData'
export { useHydration } from './hydrate'
export { callOnce } from './once'
export { useState, clearNuxtState } from './state'
@ -35,4 +35,5 @@ export type { NuxtAppManifest, NuxtAppManifestMeta } from './manifest'
export type { ReloadNuxtAppOptions } from './chunk'
export { reloadNuxtApp } from './chunk'
export { useRequestURL } from './url'
export { usePreviewMode } from './preview'
export { useId } from './id'

View File

@ -7,6 +7,10 @@ export type LoadingIndicatorOpts = {
duration: number
/** @default 200 */
throttle: number
/** @default 500 */
hideDelay: number
/** @default 400 */
resetDelay: number
/**
* You can provide a custom function to customize the progress estimation,
* which is a function that receives the duration of the loading bar (above)
@ -15,22 +19,13 @@ export type LoadingIndicatorOpts = {
estimatedProgress?: (duration: number, elapsed: number) => number
}
function _hide (isLoading: Ref<boolean>, progress: Ref<number>) {
if (import.meta.client) {
setTimeout(() => {
isLoading.value = false
setTimeout(() => { progress.value = 0 }, 400)
}, 500)
}
}
export type LoadingIndicator = {
_cleanup: () => void
progress: Ref<number>
isLoading: Ref<boolean>
start: () => void
set: (value: number) => void
finish: () => void
finish: (opts?: { force?: boolean }) => void
clear: () => void
}
@ -40,7 +35,7 @@ function defaultEstimatedProgress (duration: number, elapsed: number): number {
}
function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
const { duration = 2000, throttle = 200 } = opts
const { duration = 2000, throttle = 200, hideDelay = 500, resetDelay = 400 } = opts
const getProgress = opts.estimatedProgress || defaultEstimatedProgress
const nuxtApp = useNuxtApp()
const progress = ref(0)
@ -48,7 +43,9 @@ function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
let done = false
let rafId: number
let _throttle: any = null
let throttleTimeout: number | NodeJS.Timeout
let hideTimeout: number | NodeJS.Timeout
let resetTimeout: number | NodeJS.Timeout
const start = () => set(0)
@ -60,7 +57,7 @@ function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
clear()
progress.value = at < 0 ? 0 : at
if (throttle && import.meta.client) {
_throttle = setTimeout(() => {
throttleTimeout = setTimeout(() => {
isLoading.value = true
_startProgress()
}, throttle)
@ -70,19 +67,40 @@ function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
}
}
function finish () {
function _hide () {
if (import.meta.client) {
hideTimeout = setTimeout(() => {
isLoading.value = false
resetTimeout = setTimeout(() => { progress.value = 0 }, resetDelay)
}, hideDelay)
}
}
function finish (opts: { force?: boolean } = {}) {
progress.value = 100
done = true
clear()
_hide(isLoading, progress)
_clearTimeouts()
if (opts.force) {
progress.value = 0
isLoading.value = false
} else {
_hide()
}
}
function _clearTimeouts () {
if (import.meta.client) {
clearTimeout(hideTimeout)
clearTimeout(resetTimeout)
}
}
function clear () {
clearTimeout(_throttle)
if (import.meta.client) {
clearTimeout(throttleTimeout)
cancelAnimationFrame(rafId)
}
_throttle = null
}
function _startProgress () {
@ -113,7 +131,7 @@ function createLoadingIndicator (opts: Partial<LoadingIndicatorOpts> = {}) {
const unsubLoadingFinishHook = nuxtApp.hook('page:loading:end', () => {
finish()
})
const unsubError = nuxtApp.hook('vue:error', finish)
const unsubError = nuxtApp.hook('vue:error', () => finish())
_cleanup = () => {
unsubError()

View File

@ -1,11 +1,12 @@
import type { MatcherExport, RouteMatcher } from 'radix3'
import { createMatcherFromExport } from 'radix3'
import { createMatcherFromExport, createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
import { defu } from 'defu'
import { useAppConfig } from '../config'
import { useRuntimeConfig } from '../nuxt'
// @ts-expect-error virtual file
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
// @ts-expect-error virtual file
import { buildAssetsURL } from '#build/paths.mjs'
import { buildAssetsURL } from '#internal/nuxt/paths'
export interface NuxtAppManifestMeta {
id: string
@ -43,6 +44,12 @@ export function getAppManifest (): Promise<NuxtAppManifest> {
/** @since 3.7.4 */
export async function getRouteRules (url: string) {
if (import.meta.server) {
const _routeRulesMatcher = toRouteMatcher(
createRadixRouter({ routes: useRuntimeConfig().nitro!.routeRules })
)
return defu({} as Record<string, any>, ..._routeRulesMatcher.matchAll(url).reverse())
}
await getAppManifest()
return defu({} as Record<string, any>, ...matcher.matchAll(url).reverse())
}

View File

@ -3,6 +3,7 @@ import { parse } from 'devalue'
import { useHead } from '@unhead/vue'
import { getCurrentInstance } from 'vue'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import { useAppConfig } from '../config'
import { useRoute } from './router'
import { getAppManifest, getRouteRules } from './manifest'
@ -50,14 +51,14 @@ export function preloadPayload (url: string, opts: LoadPayloadOptions = {}) {
// --- Internal ---
const extension = renderJsonPayloads ? 'json' : 'js'
const filename = renderJsonPayloads ? '_payload.json' : '_payload.js'
function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
const u = new URL(url, 'http://localhost')
if (u.host !== 'localhost' || hasProtocol(u.pathname, { acceptRelative: true })) {
throw new Error('Payload URL must not include hostname: ' + url)
}
const hash = opts.hash || (opts.fresh ? Date.now() : '')
return joinURL(useRuntimeConfig().app.baseURL, u.pathname, hash ? `_payload.${hash}.${extension}` : `_payload.${extension}`)
const hash = opts.hash || (opts.fresh ? Date.now() : (useAppConfig().nuxt as any)?.buildId)
return joinURL(useRuntimeConfig().app.baseURL, u.pathname, filename + (hash ? `?${hash}` : ''))
}
async function _importPayload (payloadURL: string) {

View File

@ -0,0 +1,91 @@
import { toRef, watch } from 'vue'
import { useState } from './state'
import { refreshNuxtData } from './asyncData'
import { useRoute, useRouter } from './router'
interface Preview {
enabled: boolean
state: Record<any, unknown>
_initialized?: boolean
}
interface PreviewModeOptions<S> {
shouldEnable?: (state: Preview['state']) => boolean,
getState?: (state: Preview['state']) => S,
}
type EnteredState = Record<any, unknown> | null | undefined | void
let unregisterRefreshHook: (() => any) | undefined
/** @since 3.11.0 */
export function usePreviewMode<S extends EnteredState> (options: PreviewModeOptions<S> = {}) {
const preview = useState<Preview>('_preview-state', () => ({
enabled: false,
state: {}
}))
if (preview.value._initialized) {
return {
enabled: toRef(preview.value, 'enabled'),
state: preview.value.state as S extends void ? Preview['state'] : (NonNullable<S> & Preview['state'])
}
}
if (import.meta.client) {
preview.value._initialized = true
}
if (!preview.value.enabled) {
const shouldEnable = options.shouldEnable ?? defaultShouldEnable
const result = shouldEnable(preview.value.state)
if (typeof result === 'boolean') { preview.value.enabled = result }
}
watch(() => preview.value.enabled, (value) => {
if (value) {
const getState = options.getState ?? getDefaultState
const newState = getState(preview.value.state)
if (newState !== preview.value.state) {
Object.assign(preview.value.state, newState)
}
if (import.meta.client && !unregisterRefreshHook) {
refreshNuxtData()
unregisterRefreshHook = useRouter().afterEach(() => refreshNuxtData())
}
} else if (unregisterRefreshHook) {
unregisterRefreshHook()
unregisterRefreshHook = undefined
}
}, { immediate: true, flush: 'sync' })
return {
enabled: toRef(preview.value, 'enabled'),
state: preview.value.state as S extends void ? Preview['state'] : (NonNullable<S> & Preview['state'])
}
}
function defaultShouldEnable () {
const route = useRoute()
const previewQueryName = 'preview'
return route.query[previewQueryName] === 'true'
}
function getDefaultState (state: Preview['state']) {
if (state.token !== undefined) {
return state
}
const route = useRoute()
state.token = Array.isArray(route.query.token) ? route.query.token[0] : route.query.token
return state
}

Some files were not shown because too many files have changed in this diff Show More