Merge remote-tracking branch 'origin/main' into fix/21721-spa-loading

This commit is contained in:
Daniel Roe 2024-12-09 12:24:17 +00:00
commit 29f0663f05
No known key found for this signature in database
GPG Key ID: 3714AB03996F442B
80 changed files with 1620 additions and 1460 deletions

View File

@ -1,4 +1,4 @@
FROM node:lts@sha256:5c76d05034644fa8ecc9c2aa84e0a83cd981d0ef13af5455b87b9adf5b216561 FROM node:lts@sha256:35a5dd72bcac4bce43266408b58a02be6ff0b6098ffa6f5435aeea980a8951d7
RUN apt-get update && \ RUN apt-get update && \
apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \ apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \

View File

@ -23,6 +23,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Check engine ranges, peer dependency ranges and installed versions
run: pnpm installed-check --fix
- name: Build (stub) - name: Build (stub)
run: pnpm dev:prepare run: pnpm dev:prepare

View File

@ -4,12 +4,14 @@ on:
push: push:
paths: paths:
- "**/package.json" - "**/package.json"
- "pnpm-lock.yaml"
branches: branches:
- main - main
- 3.x - 3.x
pull_request: pull_request:
paths: paths:
- "**/package.json" - "**/package.json"
- "pnpm-lock.yaml"
branches: branches:
- main - main
- 3.x - 3.x
@ -32,5 +34,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Lint monorepo - name: Lint monorepo
run: pnpm sherif -r multiple-dependency-versions run: pnpm sherif -r multiple-dependency-versions
- name: Check engine ranges, peer dependency ranges and installed versions
run: pnpm installed-check

View File

@ -25,7 +25,7 @@ Nuxt uses conventions and an opinionated directory structure to automate repetit
- **Auto-imports:** write Vue composables and components in their respective directories and use them without having to import them with the benefits of tree-shaking and optimized JS bundles. - **Auto-imports:** write Vue composables and components in their respective directories and use them without having to import them with the benefits of tree-shaking and optimized JS bundles.
- **Data-fetching utilities:** Nuxt provides composables to handle SSR-compatible data fetching as well as different strategies. - **Data-fetching utilities:** Nuxt provides composables to handle SSR-compatible data fetching as well as different strategies.
- **Zero-config TypeScript support:** write type-safe code without having to learn TypeScript with our auto-generated types and `tsconfig.json` - **Zero-config TypeScript support:** write type-safe code without having to learn TypeScript with our auto-generated types and `tsconfig.json`
- **Configured build tools:** we use [Vite](https://vitejs.dev) by default to support hot module replacement (HMR) in development and bundling your code for production with best-practices baked-in. - **Configured build tools:** we use [Vite](https://vite.dev) by default to support hot module replacement (HMR) in development and bundling your code for production with best-practices baked-in.
Nuxt takes care of these and provides both frontend and backend functionality so you can focus on what matters: **creating your web application**. Nuxt takes care of these and provides both frontend and backend functionality so you can focus on what matters: **creating your web application**.

View File

@ -91,6 +91,9 @@ pnpm dev -o
```bash [bun] ```bash [bun]
bun run dev -o bun run dev -o
# To use the Bun runtime during development
# bun --bun run dev -o
``` ```
:: ::

View File

@ -153,7 +153,7 @@ Name | Config File | How
---------------------------------------------|---------------------------|------------------------- ---------------------------------------------|---------------------------|-------------------------
[Nitro](https://nitro.unjs.io) | ~~`nitro.config.ts`~~ | Use [`nitro`](/docs/api/nuxt-config#nitro) key in `nuxt.config` [Nitro](https://nitro.unjs.io) | ~~`nitro.config.ts`~~ | Use [`nitro`](/docs/api/nuxt-config#nitro) key in `nuxt.config`
[PostCSS](https://postcss.org) | ~~`postcss.config.js`~~ | Use [`postcss`](/docs/api/nuxt-config#postcss) key in `nuxt.config` [PostCSS](https://postcss.org) | ~~`postcss.config.js`~~ | Use [`postcss`](/docs/api/nuxt-config#postcss) key in `nuxt.config`
[Vite](https://vitejs.dev) | ~~`vite.config.ts`~~ | Use [`vite`](/docs/api/nuxt-config#vite) key in `nuxt.config` [Vite](https://vite.dev) | ~~`vite.config.ts`~~ | Use [`vite`](/docs/api/nuxt-config#vite) key in `nuxt.config`
[webpack](https://webpack.js.org) | ~~`webpack.config.ts`~~ | Use [`webpack`](/docs/api/nuxt-config#webpack-1) key in `nuxt.config` [webpack](https://webpack.js.org) | ~~`webpack.config.ts`~~ | Use [`webpack`](/docs/api/nuxt-config#webpack-1) key in `nuxt.config`
Here is a list of other common config files: Here is a list of other common config files:
@ -162,9 +162,9 @@ Name | Config File | How To
---------------------------------------------|-------------------------|-------------------------- ---------------------------------------------|-------------------------|--------------------------
[TypeScript](https://www.typescriptlang.org) | `tsconfig.json` | [More Info](/docs/guide/concepts/typescript#nuxttsconfigjson) [TypeScript](https://www.typescriptlang.org) | `tsconfig.json` | [More Info](/docs/guide/concepts/typescript#nuxttsconfigjson)
[ESLint](https://eslint.org) | `eslint.config.js` | [More Info](https://eslint.org/docs/latest/use/configure/configuration-files) [ESLint](https://eslint.org) | `eslint.config.js` | [More Info](https://eslint.org/docs/latest/use/configure/configuration-files)
[Prettier](https://prettier.io) | `.prettierrc.json` | [More Info](https://prettier.io/docs/en/configuration.html) [Prettier](https://prettier.io) | `prettier.config.js` | [More Info](https://prettier.io/docs/en/configuration.html)
[Stylelint](https://stylelint.io) | `.stylelintrc.json` | [More Info](https://stylelint.io/user-guide/configure) [Stylelint](https://stylelint.io) | `stylelint.config.js` | [More Info](https://stylelint.io/user-guide/configure)
[TailwindCSS](https://tailwindcss.com) | `tailwind.config.js` | [More Info](https://tailwindcss.nuxtjs.org/tailwind/config) [TailwindCSS](https://tailwindcss.com) | `tailwind.config.js` | [More Info](https://tailwindcss.nuxtjs.org/tailwind/config)
[Vitest](https://vitest.dev) | `vitest.config.ts` | [More Info](https://vitest.dev/config) [Vitest](https://vitest.dev) | `vitest.config.ts` | [More Info](https://vitest.dev/config)
## Vue Configuration ## Vue Configuration

View File

@ -27,7 +27,7 @@ For example, referencing an image file in the `public/img/` directory, available
## Assets Directory ## Assets Directory
Nuxt uses [Vite](https://vitejs.dev/guide/assets.html) (default) or [webpack](https://webpack.js.org/guides/asset-management) to build and bundle your application. The main function of these build tools is to process JavaScript files, but they can be extended through [plugins](https://vitejs.dev/plugins) (for Vite) or [loaders](https://webpack.js.org/loaders) (for webpack) to process other kind of assets, like stylesheets, fonts or SVG. This step transforms the original file mainly for performance or caching purposes (such as stylesheets minification or browser cache invalidation). Nuxt uses [Vite](https://vite.dev/guide/assets.html) (default) or [webpack](https://webpack.js.org/guides/asset-management) to build and bundle your application. The main function of these build tools is to process JavaScript files, but they can be extended through [plugins](https://vite.dev/plugins) (for Vite) or [loaders](https://webpack.js.org/loaders) (for webpack) to process other kind of assets, like stylesheets, fonts or SVG. This step transforms the original file mainly for performance or caching purposes (such as stylesheets minification or browser cache invalidation).
By convention, Nuxt uses the [`assets/`](/docs/guide/directory-structure/assets) directory to store these files but there is no auto-scan functionality for this directory, and you can use any other name for it. By convention, Nuxt uses the [`assets/`](/docs/guide/directory-structure/assets) directory to store these files but there is no auto-scan functionality for this directory, and you can use any other name for it.

View File

@ -204,7 +204,7 @@ export default defineNuxtConfig({
In both cases, the compiled stylesheets will be inlined in the HTML rendered by Nuxt. In both cases, the compiled stylesheets will be inlined in the HTML rendered by Nuxt.
:: ::
If you need to inject code in pre-processed files, like a [sass partial](https://sass-lang.com/documentation/at-rules/use#partials) with color variables, you can do so with the vite [preprocessors options](https://vitejs.dev/config/shared-options.html#css-preprocessoroptions). If you need to inject code in pre-processed files, like a [sass partial](https://sass-lang.com/documentation/at-rules/use#partials) with color variables, you can do so with the vite [preprocessors options](https://vite.dev/config/shared-options.html#css-preprocessoroptions).
Create some partials in your `assets` directory: Create some partials in your `assets` directory:
@ -416,7 +416,7 @@ SFC style blocks support preprocessors syntax. Vite come with built-in support f
:: ::
You can refer to the [Vite CSS docs](https://vitejs.dev/guide/features.html#css) and the [@vitejs/plugin-vue docs](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue). You can refer to the [Vite CSS docs](https://vite.dev/guide/features.html#css) and the [@vitejs/plugin-vue docs](https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue).
For webpack users, refer to the [vue loader docs](https://vue-loader.vuejs.org). For webpack users, refer to the [vue loader docs](https://vue-loader.vuejs.org).
## Using PostCSS ## Using PostCSS

View File

@ -21,7 +21,7 @@ We chose to build Nuxt on top of Vue for these reasons:
### Single File Components ### Single File Components
[Vues single-file components](https://v3.vuejs.org/guide/single-file-component.html) (SFC or `*.vue` files) encapsulate the markup (`<template>`), logic (`<script>`) and styling (`<style>`) of a Vue component. Nuxt provides a zero-config experience for SFCs with [Hot Module Replacement](https://vitejs.dev/guide/features.html#hot-module-replacement) that offers a seamless developer experience. [Vues single-file components](https://vuejs.org/guide/scaling-up/sfc.html) (SFC or `*.vue` files) encapsulate the markup (`<template>`), logic (`<script>`) and styling (`<style>`) of a Vue component. Nuxt provides a zero-config experience for SFCs with [Hot Module Replacement](https://vite.dev/guide/features.html#hot-module-replacement) that offers a seamless developer experience.
### Auto-imports ### Auto-imports

View File

@ -85,7 +85,7 @@ export default defineNuxtConfig({
### typescriptBundlerResolution ### typescriptBundlerResolution
This enables 'Bundler' module resolution mode for TypeScript, which is the recommended setting This enables 'Bundler' module resolution mode for TypeScript, which is the recommended setting
for frameworks like Nuxt and [Vite](https://vitejs.dev/guide/performance.html#reduce-resolve-operations). for frameworks like Nuxt and [Vite](https://vite.dev/guide/performance.html#reduce-resolve-operations).
It improves type support when using modern libraries with `exports`. It improves type support when using modern libraries with `exports`.

View File

@ -104,6 +104,10 @@ If you want to extend a private remote source, you need to add the environment v
If you want to extend a remote source from a self-hosted GitHub or GitLab instance, you need to supply its URL with the `GIGET_GITHUB_URL=<url>` or `GIGET_GITLAB_URL=<url>` environment variable - or directly configure it with [the `auth` option](https://github.com/unjs/c12#extending-config-layer-from-remote-sources) in your `nuxt.config`. If you want to extend a remote source from a self-hosted GitHub or GitLab instance, you need to supply its URL with the `GIGET_GITHUB_URL=<url>` or `GIGET_GITLAB_URL=<url>` environment variable - or directly configure it with [the `auth` option](https://github.com/unjs/c12#extending-config-layer-from-remote-sources) in your `nuxt.config`.
:: ::
::warning
Bear in mind that if you are extending a remote source as a layer, you will not be able to access its dependencies outside of Nuxt. For example, if the remote layer depends on an eslint plugin, this will not be usable in your eslint config. That is because these dependencies will be located in a special location (`node_modules/.c12/layer_name/node_modules/`) that is not accessible to your package manager.
::
::note ::note
When using git remote sources, if a layer has npm dependencies and you wish to install them, you can do so by specifying `install: true` in your layer options. When using git remote sources, if a layer has npm dependencies and you wish to install them, you can do so by specifying `install: true` in your layer options.

View File

@ -1,42 +0,0 @@
---
title: "useId"
description: Generate an SSR-friendly unique identifier that can be passed to accessibility attributes.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/id.ts
size: xs
---
::important
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.
Call `useId` at the top level of your component to generate a unique string identifier:
```vue [components/EmailField.vue]
<script setup lang="ts">
const id = useId()
</script>
<template>
<div>
<label :for="id">Email</label>
<input :id="id" name="email" type="email" />
</div>
</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.
## Returns
`useId` returns a unique string associated with this particular `useId` call in this particular component.

View File

@ -16,6 +16,10 @@ By default, [`useFetch`](/docs/api/composables/use-fetch) blocks navigation unti
`useLazyFetch` has the same signature as [`useFetch`](/docs/api/composables/use-fetch). `useLazyFetch` has the same signature as [`useFetch`](/docs/api/composables/use-fetch).
:: ::
::note
Awaiting `useLazyFetch` in this mode only ensures the call is initialized. On client-side navigation, data may not be immediately available, and you should make sure to handle the pending state in your app.
::
:read-more{to="/docs/api/composables/use-fetch"} :read-more{to="/docs/api/composables/use-fetch"}
## Example ## Example

View File

@ -17,3 +17,4 @@ The `upgrade` command upgrades Nuxt to the latest version.
Option | Default | Description Option | Default | Description
-------------------------|-----------------|------------------ -------------------------|-----------------|------------------
`--force, -f` | `false` | Removes `node_modules` and lock files before upgrade. `--force, -f` | `false` | Removes `node_modules` and lock files before upgrade.
`--channel, -ch` | `"stable"` | Specify a channel to install from ("nightly" or "stable")

View File

@ -123,7 +123,7 @@ export interface ExtendViteConfigOptions {
} }
``` ```
::read-more{to="https://vitejs.dev/config" target="_blank" color="gray" icon="i-simple-icons-vite"} ::read-more{to="https://vite.dev/config" target="_blank" color="gray" icon="i-simple-icons-vite"}
Checkout Vite website for more information about its configuration. Checkout Vite website for more information about its configuration.
:: ::
@ -329,7 +329,7 @@ interface ExtendViteConfigOptions {
``` ```
::tip ::tip
See [Vite website](https://vitejs.dev/guide/api-plugin.html) for more information about Vite plugins. You can also use [this repository](https://github.com/vitejs/awesome-vite#plugins) to find a plugin that suits your needs. See [Vite website](https://vite.dev/guide/api-plugin.html) for more information about Vite plugins. You can also use [this repository](https://github.com/vitejs/awesome-vite#plugins) to find a plugin that suits your needs.
:: ::
### Parameters ### Parameters

View File

@ -12,7 +12,7 @@ Once you've read the [general contribution guide](/docs/community/contribution),
- `packages/nuxt`: The core of Nuxt, published as [`nuxt`](https://npmjs.com/package/nuxt). - `packages/nuxt`: The core of Nuxt, published as [`nuxt`](https://npmjs.com/package/nuxt).
- `packages/schema`: Cross-version Nuxt typedefs and defaults, published as [`@nuxt/schema`](https://npmjs.com/package/@nuxt/schema). - `packages/schema`: Cross-version Nuxt typedefs and defaults, published as [`@nuxt/schema`](https://npmjs.com/package/@nuxt/schema).
- `packages/test-utils`: Test utilities for Nuxt, published as [`@nuxt/test-utils`](https://npmjs.com/package/@nuxt/test-utils). - `packages/test-utils`: Test utilities for Nuxt, published as [`@nuxt/test-utils`](https://npmjs.com/package/@nuxt/test-utils).
- `packages/vite`: The [Vite](https://vitejs.dev) bundler for Nuxt, published as [`@nuxt/vite-builder`](https://npmjs.com/package/@nuxt/vite-builder). - `packages/vite`: The [Vite](https://vite.dev) bundler for Nuxt, published as [`@nuxt/vite-builder`](https://npmjs.com/package/@nuxt/vite-builder).
- `packages/webpack`: The [webpack](https://webpack.js.org) bundler for Nuxt 3, published as [`@nuxt/webpack-builder`](https://npmjs.com/package/@nuxt/webpack-builder). - `packages/webpack`: The [webpack](https://webpack.js.org) bundler for Nuxt 3, published as [`@nuxt/webpack-builder`](https://npmjs.com/package/@nuxt/webpack-builder).
## Setup ## Setup

View File

@ -5,7 +5,7 @@ description: 'Learn how to migrate from Nuxt 2 to Nuxt 3 build tooling.'
We use the following build tools by default: We use the following build tools by default:
- [Vite](https://vitejs.dev) or [webpack](https://webpack.js.org) - [Vite](https://vite.dev) or [webpack](https://webpack.js.org)
- [Rollup](https://rollupjs.org) - [Rollup](https://rollupjs.org)
- [PostCSS](https://postcss.org) - [PostCSS](https://postcss.org)
- [esbuild](https://esbuild.github.io) - [esbuild](https://esbuild.github.io)

View File

@ -50,33 +50,34 @@
"@vue/shared": "3.5.13", "@vue/shared": "3.5.13",
"c12": "2.0.1", "c12": "2.0.1",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "2.4.0", "jiti": "2.4.1",
"magic-string": "^0.30.14", "magic-string": "^0.30.14",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"ohash": "1.1.4", "ohash": "1.1.4",
"postcss": "8.4.49", "postcss": "8.4.49",
"rollup": "4.27.4", "rollup": "4.28.0",
"send": ">=1.1.0", "send": ">=1.1.0",
"typescript": "5.6.3", "typescript": "5.6.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"unhead": "1.11.13", "unhead": "1.11.13",
"vite": "6.0.1", "unimport": "3.13.4",
"vite": "6.0.2",
"vue": "3.5.13" "vue": "3.5.13"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint-config": "0.7.2", "@nuxt/eslint-config": "0.7.2",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*", "@nuxt/rspack-builder": "workspace:*",
"@nuxt/test-utils": "3.14.4", "@nuxt/test-utils": "3.15.1",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/node": "22.10.1", "@types/node": "22.10.1",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@unhead/schema": "1.11.13", "@unhead/schema": "1.11.13",
"@unhead/vue": "1.11.13", "@unhead/vue": "1.11.13",
"@vitest/coverage-v8": "2.1.6", "@vitest/coverage-v8": "2.1.8",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"case-police": "0.7.2", "case-police": "0.7.2",
@ -85,14 +86,15 @@
"cssnano": "7.0.6", "cssnano": "7.0.6",
"destr": "2.0.3", "destr": "2.0.3",
"devalue": "5.1.1", "devalue": "5.1.1",
"eslint": "9.15.0", "eslint": "9.16.0",
"eslint-plugin-no-only-tests": "3.3.0", "eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "4.1.2", "eslint-plugin-perfectionist": "4.1.2",
"eslint-typegen": "0.3.2", "eslint-typegen": "0.3.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"happy-dom": "15.11.7", "happy-dom": "15.11.7",
"jiti": "2.4.0", "installed-check": "9.3.0",
"knip": "5.38.2", "jiti": "2.4.1",
"knip": "5.39.1",
"markdownlint-cli": "0.43.0", "markdownlint-cli": "0.43.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "3.16.0", "nuxi": "3.16.0",
@ -109,14 +111,14 @@
"tinyglobby": "0.2.10", "tinyglobby": "0.2.10",
"typescript": "5.6.3", "typescript": "5.6.3",
"ufo": "1.5.4", "ufo": "1.5.4",
"vitest": "2.1.6", "vitest": "2.1.8",
"vitest-environment-nuxt": "1.0.1", "vitest-environment-nuxt": "1.0.1",
"vue": "3.5.13", "vue": "3.5.13",
"vue-tsc": "2.1.10" "vue-tsc": "2.1.10"
}, },
"packageManager": "pnpm@9.14.2", "packageManager": "pnpm@9.14.4",
"engines": { "engines": {
"node": "^16.10.0 || >=18.0.0" "node": "^20.9.0 || ^22.0.0 || >=23.0.0"
}, },
"version": "" "version": ""
} }

View File

@ -34,7 +34,7 @@
"errx": "^0.1.0", "errx": "^0.1.0",
"globby": "^14.0.2", "globby": "^14.0.2",
"ignore": "^6.0.2", "ignore": "^6.0.2",
"jiti": "^2.4.0", "jiti": "^2.4.1",
"klona": "^2.0.6", "klona": "^2.0.6",
"mlly": "^1.7.3", "mlly": "^1.7.3",
"ohash": "^1.1.4", "ohash": "^1.1.4",
@ -48,15 +48,15 @@
"untyped": "^1.5.1" "untyped": "^1.5.1"
}, },
"devDependencies": { "devDependencies": {
"@rspack/core": "1.1.4", "@rspack/core": "1.1.5",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"vite": "6.0.1", "vite": "6.0.2",
"vitest": "2.1.6", "vitest": "2.1.8",
"webpack": "5.96.1" "webpack": "5.96.1"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^18.12.0 || ^20.0.0 || >=22.0.0"
} }
} }

View File

@ -1,10 +1,10 @@
import { existsSync, promises as fsp, lstatSync } from 'node:fs' import { existsSync, promises as fsp, lstatSync } from 'node:fs'
import { pathToFileURL } from 'node:url' import { fileURLToPath, pathToFileURL } from 'node:url'
import type { ModuleMeta, Nuxt, NuxtConfig, NuxtModule } from '@nuxt/schema' import type { ModuleMeta, Nuxt, NuxtConfig, NuxtModule } from '@nuxt/schema'
import { dirname, isAbsolute, join, resolve } from 'pathe' import { dirname, isAbsolute, join, resolve } from 'pathe'
import { defu } from 'defu' import { defu } from 'defu'
import { createJiti } from 'jiti' import { createJiti } from 'jiti'
import { resolve as resolveModule } from 'mlly' import { parseNodeModulePath, resolve as resolveModule } from 'mlly'
import { isRelative } from 'ufo' import { isRelative } from 'ufo'
import { useNuxt } from '../context' import { useNuxt } from '../context'
import { resolveAlias, resolvePath } from '../resolve' import { resolveAlias, resolvePath } from '../resolve'
@ -17,7 +17,7 @@ export async function installModule<
T extends string | NuxtModule, T extends string | NuxtModule,
Config extends Extract<NonNullable<NuxtConfig['modules']>[number], [T, any]>, Config extends Extract<NonNullable<NuxtConfig['modules']>[number], [T, any]>,
> (moduleToInstall: T, inlineOptions?: [Config] extends [never] ? any : Config[1], nuxt: Nuxt = useNuxt()) { > (moduleToInstall: T, inlineOptions?: [Config] extends [never] ? any : Config[1], nuxt: Nuxt = useNuxt()) {
const { nuxtModule, buildTimeModuleMeta } = await loadNuxtModuleInstance(moduleToInstall, nuxt) const { nuxtModule, buildTimeModuleMeta, resolvedModulePath } = await loadNuxtModuleInstance(moduleToInstall, nuxt)
const localLayerModuleDirs = new Set<string>() const localLayerModuleDirs = new Set<string>()
for (const l of nuxt.options._layers) { for (const l of nuxt.options._layers) {
@ -33,9 +33,12 @@ export async function installModule<
return return
} }
if (typeof moduleToInstall === 'string') { const modulePath = resolvedModulePath || moduleToInstall
nuxt.options.build.transpile.push(normalizeModuleTranspilePath(moduleToInstall)) if (typeof modulePath === 'string') {
const directory = getDirectory(moduleToInstall) const parsed = parseNodeModulePath(modulePath)
const moduleRoot = parsed.dir ? parsed.dir + parsed.name : modulePath
nuxt.options.build.transpile.push(normalizeModuleTranspilePath(moduleRoot))
const directory = parsed.dir ? moduleRoot : getDirectory(modulePath)
if (directory !== moduleToInstall && !localLayerModuleDirs.has(directory)) { if (directory !== moduleToInstall && !localLayerModuleDirs.has(directory)) {
nuxt.options.modulesDir.push(resolve(directory, 'node_modules')) nuxt.options.modulesDir.push(resolve(directory, 'node_modules'))
} }
@ -74,6 +77,7 @@ export const normalizeModuleTranspilePath = (p: string) => {
export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, nuxt: Nuxt = useNuxt()) { export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, nuxt: Nuxt = useNuxt()) {
let buildTimeModuleMeta: ModuleMeta = {} let buildTimeModuleMeta: ModuleMeta = {}
let resolvedModulePath: string | undefined
const jiti = createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias }) const jiti = createJiti(nuxt.options.rootDir, { alias: nuxt.options.alias })
@ -98,6 +102,7 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
: await resolveModule(path, { url: pathToFileURL(parentURL.replace(/\/node_modules\/?$/, '')), extensions: nuxt.options.extensions }) : await resolveModule(path, { url: pathToFileURL(parentURL.replace(/\/node_modules\/?$/, '')), extensions: nuxt.options.extensions })
nuxtModule = await jiti.import(src, { default: true }) as NuxtModule nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
resolvedModulePath = fileURLToPath(new URL(src))
// nuxt-module-builder generates a module.json with metadata including the version // nuxt-module-builder generates a module.json with metadata including the version
const moduleMetadataPath = new URL('module.json', src) const moduleMetadataPath = new URL('module.json', src)
@ -118,10 +123,15 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
} }
} }
// Throw error if module could not be found
if (typeof nuxtModule === 'string') {
throw new TypeError(`Could not load \`${nuxtModule}\`. Is it installed?`)
}
// Throw error if input is not a function // Throw error if input is not a function
if (typeof nuxtModule !== 'function') { if (typeof nuxtModule !== 'function') {
throw new TypeError('Nuxt module should be a function: ' + nuxtModule) throw new TypeError('Nuxt module should be a function: ' + nuxtModule)
} }
return { nuxtModule, buildTimeModuleMeta } as { nuxtModule: NuxtModule<any>, buildTimeModuleMeta: ModuleMeta } return { nuxtModule, buildTimeModuleMeta, resolvedModulePath } as { nuxtModule: NuxtModule<any>, buildTimeModuleMeta: ModuleMeta, resolvedModulePath?: string }
} }

View File

@ -23,8 +23,7 @@ export function addTemplate<T> (_template: NuxtTemplate<T> | string) {
const template = normalizeTemplate(_template) const template = normalizeTemplate(_template)
// Remove any existing template with the same destination path // Remove any existing template with the same destination path
nuxt.options.build.templates = nuxt.options.build.templates nuxt.options.build.templates = nuxt.options.build.templates.filter(p => normalizeTemplate(p).dst !== template.dst)
.filter(p => normalizeTemplate(p).dst !== template.dst)
// Add to templates array // Add to templates array
nuxt.options.build.templates.push(template) nuxt.options.build.templates.push(template)
@ -68,7 +67,7 @@ export function addTypeTemplate<T> (_template: NuxtTypeTemplate<T>) {
/** /**
* Normalize a nuxt template object * Normalize a nuxt template object
*/ */
export function normalizeTemplate<T> (template: NuxtTemplate<T> | string): ResolvedNuxtTemplate<T> { export function normalizeTemplate<T> (template: NuxtTemplate<T> | string, buildDir?: string): ResolvedNuxtTemplate<T> {
if (!template) { if (!template) {
throw new Error('Invalid template: ' + JSON.stringify(template)) throw new Error('Invalid template: ' + JSON.stringify(template))
} }
@ -87,17 +86,16 @@ export function normalizeTemplate<T> (template: NuxtTemplate<T> | string): Resol
} }
if (!template.filename) { if (!template.filename) {
const srcPath = parse(template.src) const srcPath = parse(template.src)
template.filename = (template as any).fileName || template.filename = (template as any).fileName || `${basename(srcPath.dir)}.${srcPath.name}.${hash(template.src)}${srcPath.ext}`
`${basename(srcPath.dir)}.${srcPath.name}.${hash(template.src)}${srcPath.ext}`
} }
} }
if (!template.src && !template.getContents) { if (!template.src && !template.getContents) {
throw new Error('Invalid template. Either getContents or src options should be provided: ' + JSON.stringify(template)) throw new Error('Invalid template. Either `getContents` or `src` should be provided: ' + JSON.stringify(template))
} }
if (!template.filename) { if (!template.filename) {
throw new Error('Invalid template. Either filename should be provided: ' + JSON.stringify(template)) throw new Error('Invalid template. `filename` must be provided: ' + JSON.stringify(template))
} }
// Always write declaration files // Always write declaration files
@ -107,8 +105,7 @@ export function normalizeTemplate<T> (template: NuxtTemplate<T> | string): Resol
// Resolve dst // Resolve dst
if (!template.dst) { if (!template.dst) {
const nuxt = useNuxt() template.dst = resolve(buildDir ?? useNuxt().options.buildDir, template.filename)
template.dst = resolve(nuxt.options.buildDir, template.filename)
} }
return template as ResolvedNuxtTemplate<T> return template as ResolvedNuxtTemplate<T>

View File

@ -1 +1 @@
export * from './dist/app/index.js' export * from './dist/app/index'

View File

@ -43,6 +43,14 @@
"#app": { "#app": {
"types": "./dist/app/index.d.ts", "types": "./dist/app/index.d.ts",
"import": "./dist/app/index.js" "import": "./dist/app/index.js"
},
"#app/defaults": {
"types": "./dist/app/defaults.d.ts",
"import": "./dist/app/defaults.js"
},
"#app/nuxt": {
"types": "./dist/app/nuxt.d.ts",
"import": "./dist/app/nuxt.js"
} }
}, },
"files": [ "files": [
@ -60,7 +68,7 @@
}, },
"dependencies": { "dependencies": {
"@nuxt/devalue": "^2.0.2", "@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.6.1", "@nuxt/devtools": "^1.6.2",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.0", "@nuxt/telemetry": "^2.6.0",
@ -88,7 +96,7 @@
"hookable": "^5.5.3", "hookable": "^5.5.3",
"ignore": "^6.0.2", "ignore": "^6.0.2",
"impound": "^0.2.0", "impound": "^0.2.0",
"jiti": "^2.4.0", "jiti": "^2.4.1",
"klona": "^2.0.6", "klona": "^2.0.6",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"magic-string": "^0.30.14", "magic-string": "^0.30.14",
@ -96,7 +104,7 @@
"nanotar": "^0.1.1", "nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "^3.16.0", "nuxi": "^3.16.0",
"nypm": "^0.4.0", "nypm": "^0.4.1",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
@ -115,7 +123,7 @@
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unhead": "^1.11.13", "unhead": "^1.11.13",
"unimport": "^3.13.4", "unimport": "^3.13.4",
"unplugin": "^1.16.0", "unplugin": "^2.0.0",
"unplugin-vue-router": "^0.10.8", "unplugin-vue-router": "^0.10.8",
"unstorage": "^1.13.1", "unstorage": "^1.13.1",
"untyped": "^1.5.1", "untyped": "^1.5.1",
@ -131,12 +139,12 @@
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13", "@vue/compiler-sfc": "3.5.13",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"vite": "6.0.1", "vite": "6.0.2",
"vitest": "2.1.6" "vitest": "2.1.8"
}, },
"peerDependencies": { "peerDependencies": {
"@parcel/watcher": "^2.1.0", "@parcel/watcher": "^2.1.0",
"@types/node": "^14.18.0 || >=16.10.0" "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@parcel/watcher": { "@parcel/watcher": {
@ -147,6 +155,6 @@
} }
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -1,5 +1,5 @@
import type { Component, InjectionKey } from 'vue' import type { Component, InjectionKey } from 'vue'
import { Teleport, defineComponent, h, inject, provide } from 'vue' import { Teleport, defineComponent, h, inject, provide, useId } from 'vue'
import { useNuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { paths } from '#build/components-chunk' import { paths } from '#build/components-chunk'
@ -20,10 +20,6 @@ export default defineComponent({
name: 'NuxtTeleportIslandComponent', name: 'NuxtTeleportIslandComponent',
inheritAttrs: false, inheritAttrs: false,
props: { props: {
to: {
type: String,
required: true,
},
nuxtClient: { nuxtClient: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -31,11 +27,12 @@ export default defineComponent({
}, },
setup (props, { slots }) { setup (props, { slots }) {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
const to = useId()
// if there's already a teleport parent, we don't need to teleport or to render the wrapped component client side // 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?.() } if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient || inject(NuxtTeleportIslandSymbol, false)) { return () => slots.default?.() }
provide(NuxtTeleportIslandSymbol, props.to) provide(NuxtTeleportIslandSymbol, to)
const islandContext = nuxtApp.ssrContext!.islandContext! const islandContext = nuxtApp.ssrContext!.islandContext!
return () => { return () => {
@ -43,7 +40,7 @@ export default defineComponent({
const slotType = slot.type as ExtendedComponent const slotType = slot.type as ExtendedComponent
const name = (slotType.__name || slotType.name) as string const name = (slotType.__name || slotType.name) as string
islandContext.components[props.to] = { islandContext.components[to] = {
chunk: import.meta.dev ? nuxtApp.$config.app.buildAssetsDir + paths[name] : paths[name], chunk: import.meta.dev ? nuxtApp.$config.app.buildAssetsDir + paths[name] : paths[name],
props: slot.props || {}, props: slot.props || {},
} }
@ -51,8 +48,8 @@ export default defineComponent({
return [h('div', { return [h('div', {
'style': 'display: contents;', 'style': 'display: contents;',
'data-island-uid': '', 'data-island-uid': '',
'data-island-component': props.to, 'data-island-component': to,
}, []), h(Teleport, { to: props.to }, slot)] }, []), h(Teleport, { to }, slot)]
} }
}, },
}) })

View File

@ -1,3 +1,4 @@
import { useId as _useId } from 'vue' import { useId as _useId } from 'vue'
/** @deprecated Use `useId` from `vue` */
export const useId = _useId export const useId = _useId

View File

@ -1,7 +1,7 @@
import type { MatcherExport, RouteMatcher } from 'radix3' import type { MatcherExport, RouteMatcher } from 'radix3'
import { createMatcherFromExport, createRouter as createRadixRouter, toRouteMatcher } from 'radix3' import { createMatcherFromExport, createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
import { defu } from 'defu' import { defu } from 'defu'
import { useRuntimeConfig } from '../nuxt' import { useNuxtApp, useRuntimeConfig } from '../nuxt'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs' import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
// @ts-expect-error virtual file // @ts-expect-error virtual file
@ -24,9 +24,14 @@ function fetchManifest () {
if (!isAppManifestEnabled) { if (!isAppManifestEnabled) {
throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`') throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`')
} }
manifest = $fetch<NuxtAppManifest>(buildAssetsURL(`builds/meta/${useRuntimeConfig().app.buildId}.json`), { if (import.meta.server) {
responseType: 'json', // @ts-expect-error virtual file
}) manifest = import('#app-manifest')
} else {
manifest = $fetch<NuxtAppManifest>(buildAssetsURL(`builds/meta/${useRuntimeConfig().app.buildId}.json`), {
responseType: 'json',
})
}
manifest.then((m) => { manifest.then((m) => {
matcher = createMatcherFromExport(m.matcher) matcher = createMatcherFromExport(m.matcher)
}).catch((e) => { }).catch((e) => {
@ -40,12 +45,16 @@ export function getAppManifest (): Promise<NuxtAppManifest> {
if (!isAppManifestEnabled) { if (!isAppManifestEnabled) {
throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`') throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`')
} }
if (import.meta.server) {
useNuxtApp().ssrContext!._preloadManifest = true
}
return manifest || fetchManifest() return manifest || fetchManifest()
} }
/** @since 3.7.4 */ /** @since 3.7.4 */
export async function getRouteRules (url: string) { export async function getRouteRules (url: string) {
if (import.meta.server) { if (import.meta.server) {
useNuxtApp().ssrContext!._preloadManifest = true
const _routeRulesMatcher = toRouteMatcher( const _routeRulesMatcher = toRouteMatcher(
createRadixRouter({ routes: useRuntimeConfig().nitro!.routeRules }), createRadixRouter({ routes: useRuntimeConfig().nitro!.routeRules }),
) )

View File

@ -85,15 +85,18 @@ async function _importPayload (payloadURL: string) {
} }
/** @since 3.0.0 */ /** @since 3.0.0 */
export async function isPrerendered (url = useRoute().path) { export async function isPrerendered (url = useRoute().path) {
const nuxtApp = useNuxtApp()
// Note: Alternative for server is checking x-nitro-prerender header // Note: Alternative for server is checking x-nitro-prerender header
if (!appManifest) { return !!useNuxtApp().payload.prerenderedAt } if (!appManifest) { return !!nuxtApp.payload.prerenderedAt }
url = withoutTrailingSlash(url) url = withoutTrailingSlash(url)
const manifest = await getAppManifest() const manifest = await getAppManifest()
if (manifest.prerendered.includes(url)) { if (manifest.prerendered.includes(url)) {
return true return true
} }
const rules = await getRouteRules(url) return nuxtApp.runWithContext(async () => {
return !!rules.prerender && !rules.redirect const rules = await getRouteRules(url)
return !!rules.prerender && !rules.redirect
})
} }
let payloadCache: NuxtPayload | null = null let payloadCache: NuxtPayload | null = null

View File

@ -2,6 +2,7 @@ import type { H3Event } from 'h3'
import { setResponseStatus as _setResponseStatus, appendHeader, getRequestHeader, getRequestHeaders, getResponseHeader, removeResponseHeader, setResponseHeader } from 'h3' import { setResponseStatus as _setResponseStatus, appendHeader, getRequestHeader, getRequestHeaders, getResponseHeader, removeResponseHeader, setResponseHeader } from 'h3'
import { computed, getCurrentInstance, ref } from 'vue' import { computed, getCurrentInstance, ref } from 'vue'
import { useServerHead } from '@unhead/vue' import { useServerHead } from '@unhead/vue'
import type { H3Event$Fetch } from 'nitro/types'
import type { NuxtApp } from '../nuxt' import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt' import { useNuxtApp } from '../nuxt'
@ -39,11 +40,11 @@ export function useRequestHeader (header: string) {
} }
/** @since 3.2.0 */ /** @since 3.2.0 */
export function useRequestFetch (): typeof global.$fetch { export function useRequestFetch (): H3Event$Fetch | typeof global.$fetch {
if (import.meta.client) { if (import.meta.client) {
return globalThis.$fetch return globalThis.$fetch
} }
return useRequestEvent()?.$fetch as typeof globalThis.$fetch || globalThis.$fetch return useRequestEvent()?.$fetch || globalThis.$fetch
} }
/** @since 3.0.0 */ /** @since 3.0.0 */

View File

@ -81,6 +81,8 @@ export interface NuxtSSRContext extends SSRContext {
get<T = unknown> (key: string): Promise<T> | undefined get<T = unknown> (key: string): Promise<T> | undefined
set<T> (key: string, value: Promise<T>): Promise<void> set<T> (key: string, value: Promise<T>): Promise<void>
} }
/** @internal */
_preloadManifest?: boolean
} }
export interface NuxtPayload { export interface NuxtPayload {
@ -114,11 +116,6 @@ interface _NuxtApp {
* The id of the Nuxt application. * The id of the Nuxt application.
* @internal */ * @internal */
_id: string _id: string
/**
* The next id that can be used for generating unique ids via `useId`.
* @internal
*/
_genId?: number
/** @internal */ /** @internal */
_scope: EffectScope _scope: EffectScope
/** @internal */ /** @internal */

View File

@ -0,0 +1,42 @@
import { defineNuxtPlugin } from '../nuxt'
export default defineNuxtPlugin({
name: 'nuxt:browser-devtools-timing',
enforce: 'pre',
setup (nuxtApp) {
nuxtApp.hooks.beforeEach((event) => {
// @ts-expect-error __startTime is not a public API
event.__startTime = performance.now()
})
// After each
nuxtApp.hooks.afterEach((event) => {
performance.measure(event.name, {
// @ts-expect-error __startTime is not a public API
start: event.__startTime,
detail: {
devtools: {
dataType: 'track-entry',
track: 'nuxt',
color: 'tertiary-dark',
} satisfies ExtensionTrackEntryPayload,
},
})
})
},
})
type DevToolsColor =
'primary' | 'primary-light' | 'primary-dark' |
'secondary' | 'secondary-light' | 'secondary-dark' |
'tertiary' | 'tertiary-light' | 'tertiary-dark' |
'error'
interface ExtensionTrackEntryPayload {
dataType?: 'track-entry' // Defaults to "track-entry"
color?: DevToolsColor // Defaults to "primary"
track: string // Required: Name of the custom track
trackGroup?: string // Optional: Group for organizing tracks
properties?: [string, string][] // Key-value pairs for detailed view
tooltipText?: string // Short description for tooltip
}

View File

@ -1,5 +1,5 @@
import type { UseHeadInput } from '@unhead/vue' import type { UseHeadInput } from '@unhead/vue'
import type { NuxtApp, useNuxtApp } from '../nuxt' import type { NuxtApp, useNuxtApp } from '../nuxt.js'
declare global { declare global {
namespace NodeJS { namespace NodeJS {

View File

@ -6,7 +6,6 @@ import { parseURL } from 'ufo'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { ELEMENT_NODE, parse, walk } from 'ultrahtml' import { ELEMENT_NODE, parse, walk } from 'ultrahtml'
import { hash } from 'ohash'
import { resolvePath } from '@nuxt/kit' import { resolvePath } from '@nuxt/kit'
import defu from 'defu' import defu from 'defu'
import { isVue } from '../../core/utils' import { isVue } from '../../core/utils'
@ -113,8 +112,6 @@ export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPlug
const { loc, attributes } = node const { loc, attributes } = node
const attributeValue = attributes[':nuxt-client'] || attributes['nuxt-client'] || 'true' const attributeValue = attributes[':nuxt-client'] || attributes['nuxt-client'] || 'true'
const uid = hash(id + node.loc[0].start + node.loc[0].end)
const wrapperAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else']) const wrapperAttributes = extractAttributes(attributes, ['v-if', 'v-else-if', 'v-else'])
let startTag = code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replace(NUXTCLIENT_ATTR_RE, '') let startTag = code.slice(startingIndex + loc[0].start, startingIndex + loc[0].end).replace(NUXTCLIENT_ATTR_RE, '')
@ -122,7 +119,7 @@ export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPlug
startTag = startTag.replaceAll(EXTRACTED_ATTRS_RE, '') startTag = startTag.replaceAll(EXTRACTED_ATTRS_RE, '')
} }
s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportIslandComponent${attributeToString(wrapperAttributes)} to="${node.name}-${uid}" :nuxt-client="${attributeValue}">`) s.appendLeft(startingIndex + loc[0].start, `<NuxtTeleportIslandComponent${attributeToString(wrapperAttributes)} :nuxt-client="${attributeValue}">`)
s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, startTag) s.overwrite(startingIndex + loc[0].start, startingIndex + loc[0].end, startTag)
s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportIslandComponent>') s.appendRight(startingIndex + loc[1].end, '</NuxtTeleportIslandComponent>')
}) })

View File

@ -1,6 +1,7 @@
import { defineAsyncComponent, defineComponent, h } from 'vue' import { defineAsyncComponent, defineComponent, h } from 'vue'
import type { AsyncComponentLoader } from 'vue' import type { AsyncComponentLoader } from 'vue'
import ClientOnly from '#app/components/client-only' import ClientOnly from '#app/components/client-only'
import { useNuxtApp } from '#app/nuxt'
/* @__NO_SIDE_EFFECTS__ */ /* @__NO_SIDE_EFFECTS__ */
export const createClientPage = (loader: AsyncComponentLoader) => { export const createClientPage = (loader: AsyncComponentLoader) => {
@ -15,11 +16,15 @@ export const createClientPage = (loader: AsyncComponentLoader) => {
return defineComponent({ return defineComponent({
inheritAttrs: false, inheritAttrs: false,
setup (_, { attrs }) { setup (_, { attrs }) {
return () => h('div', [ const nuxtApp = useNuxtApp()
h(ClientOnly, undefined, { if (import.meta.server || nuxtApp.isHydrating) {
default: () => h(page, attrs), return () => h('div', [
}), h(ClientOnly, undefined, {
]) default: () => h(page, attrs),
}),
])
}
return () => h(page, attrs)
}, },
}) })
} }

View File

@ -37,7 +37,7 @@ export async function generateApp (nuxt: Nuxt, app: NuxtApp, options: { filter?:
await nuxt.callHook('app:templates', app) await nuxt.callHook('app:templates', app)
// Normalize templates // Normalize templates
app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl)) app.templates = app.templates.map(tmpl => normalizeTemplate(tmpl, nuxt.options.buildDir))
// compile plugins first as they are needed within the nuxt.vfs // compile plugins first as they are needed within the nuxt.vfs
// in order to annotate templated plugins // in order to annotate templated plugins

View File

@ -273,7 +273,18 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nuxt.options.alias['#app-manifest'] = join(tempDir, `meta/${buildId}.json`) nuxt.options.alias['#app-manifest'] = join(tempDir, `meta/${buildId}.json`)
// write stub manifest before build so external import of #app-manifest can be resolved
if (!nuxt.options.dev) {
nuxt.hook('build:before', async () => {
await fsp.mkdir(join(tempDir, 'meta'), { recursive: true })
await fsp.writeFile(join(tempDir, `meta/${buildId}.json`), JSON.stringify({}))
})
}
nuxt.hook('nitro:config', (config) => { nuxt.hook('nitro:config', (config) => {
config.alias ||= {}
config.alias['#app-manifest'] = join(tempDir, `meta/${buildId}.json`)
const rules = config.routeRules const rules = config.routeRules
for (const rule in rules) { for (const rule in rules) {
if (!(rules[rule] as any).appMiddleware) { continue } if (!(rules[rule] as any).appMiddleware) { continue }
@ -349,6 +360,11 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}) })
} }
// add stub alias to allow vite to resolve import
if (!nuxt.options.experimental.appManifest) {
nuxt.options.alias['#app-manifest'] = 'unenv/runtime/mock/proxy'
}
// Add fallback server for `ssr: false` // Add fallback server for `ssr: false`
const FORWARD_SLASH_RE = /\//g const FORWARD_SLASH_RE = /\//g
if (!nuxt.options.ssr) { if (!nuxt.options.ssr) {

View File

@ -548,6 +548,12 @@ async function initNuxt (nuxt: Nuxt) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/debug')) addPlugin(resolve(nuxt.options.appDir, 'plugins/debug'))
} }
// Add experimental Chrome devtools timings support
// https://developer.chrome.com/docs/devtools/performance/extension
if (nuxt.options.experimental.browserDevtoolsTiming) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/browser-devtools-timing.client'))
}
for (const [key, options] of modulesToInstall) { for (const [key, options] of modulesToInstall) {
await installModule(key, options) await installModule(key, options)
} }

View File

@ -30,7 +30,7 @@ import { renderSSRHeadOptions } from '#internal/unhead.config.mjs'
import type { NuxtPayload, NuxtSSRContext } from '#app' import type { NuxtPayload, NuxtSSRContext } from '#app'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appHead, appId, appRootAttrs, appRootTag, appSpaLoaderAttrs, appSpaLoaderTag, appTeleportAttrs, appTeleportTag, componentIslands, multiApp, spaLoadingTemplateOutside } from '#internal/nuxt.config.mjs' import { appHead, appId, appRootAttrs, appRootTag, appSpaLoaderAttrs, appSpaLoaderTag, appTeleportAttrs, appTeleportTag, componentIslands, appManifest as isAppManifestEnabled, multiApp, spaLoadingTemplateOutside } from '#internal/nuxt.config.mjs'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths' import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths'
@ -389,7 +389,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Setup head // Setup head
const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext) const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext)
// 1.Extracted payload preloading // 1. Preload payloads and app manifest
if (_PAYLOAD_EXTRACTION && !NO_SCRIPTS && !isRenderingIsland) { if (_PAYLOAD_EXTRACTION && !NO_SCRIPTS && !isRenderingIsland) {
head.push({ head.push({
link: [ link: [
@ -399,7 +399,13 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
], ],
}, headEntryOptions) }, headEntryOptions)
} }
if (isAppManifestEnabled && ssrContext._preloadManifest) {
head.push({
link: [
{ rel: 'preload', as: 'fetch', fetchpriority: 'low', crossorigin: 'anonymous', href: buildAssetsURL(`builds/meta/${ssrContext.runtimeConfig.app.buildId}.json`) },
],
}, { ...headEntryOptions, tagPriority: 'low' })
}
// 2. Styles // 2. Styles
if (inlinedStyles.length) { if (inlinedStyles.length) {
head.push({ style: inlinedStyles }) head.push({ style: inlinedStyles })

View File

@ -285,7 +285,7 @@ export const layoutTemplate: NuxtTemplate = {
filename: 'layouts.mjs', filename: 'layouts.mjs',
getContents ({ app }) { getContents ({ app }) {
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => { const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
return [name, `defineAsyncComponent(${genDynamicImport(file)})`] return [name, `defineAsyncComponent(${genDynamicImport(file, { interopDefault: true })})`]
})) }))
return [ return [
`import { defineAsyncComponent } from 'vue'`, `import { defineAsyncComponent } from 'vue'`,

View File

@ -41,13 +41,19 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
// Filter disabled sources // Filter disabled sources
// options.sources = options.sources.filter(source => source.disabled !== true) // options.sources = options.sources.filter(source => source.disabled !== true)
const { addons: inlineAddons, ...rest } = options
const [addons, addonsOptions] = Array.isArray(inlineAddons) ? [inlineAddons] : [[], inlineAddons]
// Create a context to share state between module internals // Create a context to share state between module internals
const ctx = createUnimport({ const ctx = createUnimport({
injectAtEnd: true, injectAtEnd: true,
...options, ...rest,
addons: { addons: {
addons,
vueTemplate: options.autoImport, vueTemplate: options.autoImport,
...options.addons, vueDirectives: options.autoImport === false ? undefined : true,
...addonsOptions,
}, },
presets, presets,
}) })

View File

@ -455,6 +455,8 @@ export default defineNuxtModule({
addBuildPlugin(PageMetaPlugin({ addBuildPlugin(PageMetaPlugin({
dev: nuxt.options.dev, dev: nuxt.options.dev,
sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client,
isPage,
routesPath: resolve(nuxt.options.buildDir, 'routes.mjs'),
})) }))
}) })
@ -499,13 +501,13 @@ export default defineNuxtModule({
addTemplate({ addTemplate({
filename: 'routes.mjs', filename: 'routes.mjs',
getContents ({ app }) { getContents ({ app }) {
if (!app.pages) { return 'export default []' } if (!app.pages) { return ROUTES_HMR_CODE + 'export default []' }
const { routes, imports } = normalizeRoutes(app.pages, new Set(), { const { routes, imports } = normalizeRoutes(app.pages, new Set(), {
serverComponentRuntime, serverComponentRuntime,
clientComponentRuntime, clientComponentRuntime,
overrideMeta: !!nuxt.options.experimental.scanPageMeta, overrideMeta: !!nuxt.options.experimental.scanPageMeta,
}) })
return [...imports, `export default ${routes}`].join('\n') return ROUTES_HMR_CODE + [...imports, `export default ${routes}`].join('\n')
}, },
}) })
@ -610,3 +612,26 @@ export default defineNuxtModule({
}) })
}, },
}) })
const ROUTES_HMR_CODE = /* js */`
if (import.meta.hot) {
import.meta.hot.accept((mod) => {
const router = import.meta.hot.data.router
if (!router) {
import.meta.hot.invalidate('[nuxt] Cannot replace routes because there is no active router. Reloading.')
return
}
router.clearRoutes()
for (const route of mod.default || mod) {
router.addRoute(route)
}
router.replace('')
})
}
export function handleHotUpdate(_router) {
if (import.meta.hot) {
import.meta.hot.data.router = _router
}
}
`

View File

@ -13,6 +13,8 @@ import { parseAndWalk, withLocations } from '../../core/utils/parse'
interface PageMetaPluginOptions { interface PageMetaPluginOptions {
dev?: boolean dev?: boolean
sourcemap?: boolean sourcemap?: boolean
isPage?: (file: string) => boolean
routesPath?: string
} }
const HAS_MACRO_RE = /\bdefinePageMeta\s*\(\s*/ const HAS_MACRO_RE = /\bdefinePageMeta\s*\(\s*/
@ -22,6 +24,11 @@ const __nuxt_page_meta = null
export default __nuxt_page_meta export default __nuxt_page_meta
` `
const CODE_DEV_EMPTY = `
const __nuxt_page_meta = {}
export default __nuxt_page_meta
`
const CODE_HMR = ` const CODE_HMR = `
// Vite // Vite
if (import.meta.hot) { if (import.meta.hot) {
@ -89,11 +96,11 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
if (!hasMacro && !code.includes('export { default }') && !code.includes('__nuxt_page_meta')) { if (!hasMacro && !code.includes('export { default }') && !code.includes('__nuxt_page_meta')) {
if (!code) { if (!code) {
s.append(CODE_EMPTY + (options.dev ? CODE_HMR : '')) s.append(options.dev ? (CODE_DEV_EMPTY + CODE_HMR) : CODE_EMPTY)
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)) const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
logger.error(`The file \`${pathname}\` is not a valid page as it has no content.`) logger.error(`The file \`${pathname}\` is not a valid page as it has no content.`)
} else { } else {
s.overwrite(0, code.length, CODE_EMPTY + (options.dev ? CODE_HMR : '')) s.overwrite(0, code.length, options.dev ? (CODE_DEV_EMPTY + CODE_HMR) : CODE_EMPTY)
} }
return result() return result()
@ -147,19 +154,23 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
}) })
if (!s.hasChanged() && !code.includes('__nuxt_page_meta')) { if (!s.hasChanged() && !code.includes('__nuxt_page_meta')) {
s.overwrite(0, code.length, CODE_EMPTY + (options.dev ? CODE_HMR : '')) s.overwrite(0, code.length, options.dev ? (CODE_DEV_EMPTY + CODE_HMR) : CODE_EMPTY)
} }
return result() return result()
}, },
vite: { vite: {
handleHotUpdate: { handleHotUpdate: {
order: 'pre', order: 'post',
handler: ({ modules }) => { handler: ({ file, modules, server }) => {
// Remove macro file from modules list to prevent HMR overrides if (options.isPage?.(file)) {
const index = modules.findIndex(i => i.id?.includes('?macro=true')) const macroModule = server.moduleGraph.getModuleById(file + '?macro=true')
if (index !== -1) { const routesModule = server.moduleGraph.getModuleById('virtual:nuxt:' + options.routesPath)
modules.splice(index, 1) return [
...modules,
...macroModule ? [macroModule] : [],
...routesModule ? [routesModule] : [],
]
} }
}, },
}, },

View File

@ -17,7 +17,7 @@ import { navigateTo } from '#app/composables/router'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs' import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import _routes from '#build/routes' import _routes, { handleHotUpdate } from '#build/routes'
import routerOptions from '#build/router.options' import routerOptions from '#build/router.options'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { globalMiddleware, namedMiddleware } from '#build/middleware' import { globalMiddleware, namedMiddleware } from '#build/middleware'
@ -87,6 +87,8 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
routes, routes,
}) })
handleHotUpdate(router)
if (import.meta.client && 'scrollRestoration' in window.history) { if (import.meta.client && 'scrollRestoration' in window.history) {
window.history.scrollRestoration = 'auto' window.history.scrollRestoration = 'auto'
} }

View File

@ -271,7 +271,7 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
"<template> "<template>
<div> <div>
<HelloWorld /> <HelloWorld />
<NuxtTeleportIslandComponent to="HelloWorld-CyH3UXLuYA" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent> <NuxtTeleportIslandComponent :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
</div> </div>
</template> </template>
@ -305,7 +305,7 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
"<template> "<template>
<div> <div>
<HelloWorld /> <HelloWorld />
<NuxtTeleportIslandComponent to="HelloWorld-eo0XycWCUV" :nuxt-client="nuxtClient"><HelloWorld /></NuxtTeleportIslandComponent> <NuxtTeleportIslandComponent :nuxt-client="nuxtClient"><HelloWorld /></NuxtTeleportIslandComponent>
</div> </div>
</template> </template>
@ -376,7 +376,7 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'</script><template> import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'</script><template>
<div> <div>
<HelloWorld /> <HelloWorld />
<NuxtTeleportIslandComponent to="HelloWorld-CyH3UXLuYA" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent> <NuxtTeleportIslandComponent :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
</div> </div>
</template> </template>
@ -402,9 +402,9 @@ withDefaults(defineProps<{ things?: any[]; somethingElse?: string }>(), {
import NuxtTeleportIslandComponent from '#app/components/nuxt-teleport-island-component' import NuxtTeleportIslandComponent from '#app/components/nuxt-teleport-island-component'
import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'</script><template> import NuxtTeleportSsrSlot from '#app/components/nuxt-teleport-island-slot'</script><template>
<div> <div>
<NuxtTeleportIslandComponent v-if="false" to="HelloWorld-D9uaHyzL7X" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent> <NuxtTeleportIslandComponent v-if="false" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
<NuxtTeleportIslandComponent v-else-if="true" to="HelloWorld-o4RZMtArnE" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent> <NuxtTeleportIslandComponent v-else-if="true" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
<NuxtTeleportIslandComponent v-else to="HelloWorld-m1IbXHdd8O" :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent> <NuxtTeleportIslandComponent v-else :nuxt-client="true"><HelloWorld /></NuxtTeleportIslandComponent>
</div> </div>
</template> </template>
" "

View File

@ -31,7 +31,7 @@
"dependencies": { "dependencies": {
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0", "@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
"@nuxt/kit": "workspace:*", "@nuxt/kit": "workspace:*",
"@rspack/core": "^1.1.4", "@rspack/core": "^1.1.5",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0", "css-minimizer-webpack-plugin": "^7.0.0",
@ -43,11 +43,11 @@
"fork-ts-checker-webpack-plugin": "^9.0.2", "fork-ts-checker-webpack-plugin": "^9.0.2",
"globby": "^14.0.2", "globby": "^14.0.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "^2.4.0", "jiti": "^2.4.1",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"magic-string": "^0.30.14", "magic-string": "^0.30.14",
"memfs": "^4.14.0", "memfs": "^4.14.1",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
"pify": "^6.1.0", "pify": "^6.1.0",
@ -61,7 +61,7 @@
"time-fix-plugin": "^2.0.7", "time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unplugin": "^1.16.0", "unplugin": "^2.0.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1", "vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2", "vue-loader": "^17.4.2",
@ -76,7 +76,7 @@
"@types/pify": "5.0.4", "@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.27.4", "rollup": "4.28.0",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"vue": "3.5.13" "vue": "3.5.13"
}, },
@ -84,6 +84,6 @@
"vue": "^3.3.4" "vue": "^3.3.4"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -50,7 +50,7 @@
"ofetch": "1.4.1", "ofetch": "1.4.1",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"unctx": "2.3.1", "unctx": "2.3.1",
"vite": "6.0.1", "vite": "6.0.2",
"vue": "3.5.13", "vue": "3.5.13",
"vue-bundle-renderer": "2.1.1", "vue-bundle-renderer": "2.1.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
@ -74,6 +74,6 @@
"untyped": "^1.5.1" "untyped": "^1.5.1"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^18.12.0 || ^20.0.0 || >=22.0.0"
} }
} }

View File

@ -24,7 +24,16 @@ export default defineUntypedSchema({
}, },
/** /**
* Whether to generate sourcemaps. * Configures whether and how sourcemaps are generated for server and/or client bundles.
*
* If set to a single boolean, that value applies to both server and client.
* Additionally, the `'hidden'` option is also available for both server and client.
*
* Available options for both client and server:
* - `true`: Generates sourcemaps and includes source references in the final bundle.
* - `false`: Does not generate any sourcemaps.
* - `'hidden'`: Generates sourcemaps but does not include references in the final bundle.
*
* @type {boolean | { server?: boolean | 'hidden', client?: boolean | 'hidden' }} * @type {boolean | { server?: boolean | 'hidden', client?: boolean | 'hidden' }}
*/ */
sourcemap: { sourcemap: {

View File

@ -428,5 +428,14 @@ export default defineUntypedSchema({
return val ?? (((await get('future') as Record<string, unknown>).compatibilityVersion === 4) ? 'body' : 'within') return val ?? (((await get('future') as Record<string, unknown>).compatibilityVersion === 4) ? 'body' : 'within')
}, },
}, },
/**
* Enable timings for Nuxt application hooks in the performance panel of Chromium-based browsers.
*
* @see [the Chrome DevTools extensibility API](https://developer.chrome.com/docs/devtools/performance/extension#tracks)
*/
browserDevtoolsTiming: {
$resolve: async (val, get) => val ?? await get('dev'),
},
}, },
}) })

View File

@ -8,7 +8,7 @@ export default defineUntypedSchema({
/** /**
* Configuration that will be passed directly to Vite. * Configuration that will be passed directly to Vite.
* *
* @see [Vite configuration docs](https://vitejs.dev/config) for more information. * @see [Vite configuration docs](https://vite.dev/config) for more information.
* Please note that not all vite options are supported in Nuxt. * Please note that not all vite options are supported in Nuxt.
* @type {typeof import('../src/types/config').ViteConfig & { $client?: typeof import('../src/types/config').ViteConfig, $server?: typeof import('../src/types/config').ViteConfig }} * @type {typeof import('../src/types/config').ViteConfig & { $client?: typeof import('../src/types/config').ViteConfig, $server?: typeof import('../src/types/config').ViteConfig }}
*/ */

View File

@ -119,10 +119,10 @@ export interface ImportGlobEagerFunction {
} }
export interface ViteImportMeta { export interface ViteImportMeta {
/** Vite client HMR API - see https://vitejs.dev/guide/api-hmr.html */ /** Vite client HMR API - see https://vite.dev/guide/api-hmr.html */
readonly hot?: ViteHot readonly hot?: ViteHot
/** vite glob import utility - https://vitejs.dev/guide/features.html#glob-import */ /** vite glob import utility - https://vite.dev/guide/features.html#glob-import */
glob: ImportGlobFunction glob: ImportGlobFunction
/** /**

View File

@ -1,7 +1,7 @@
import type { RouterHistory, RouterOptions as _RouterOptions } from 'vue-router' import type { RouterHistory, RouterOptions as _RouterOptions } from 'vue-router'
export type RouterOptions = Partial<Omit<_RouterOptions, 'history' | 'routes'>> & { export type RouterOptions = Partial<Omit<_RouterOptions, 'history' | 'routes'>> & {
history?: (baseURL?: string) => RouterHistory history?: (baseURL?: string) => RouterHistory | null | undefined
routes?: (_routes: _RouterOptions['routes']) => _RouterOptions['routes'] | Promise<_RouterOptions['routes']> routes?: (_routes: _RouterOptions['routes']) => _RouterOptions['routes'] | Promise<_RouterOptions['routes']>
hashMode?: boolean hashMode?: boolean
scrollBehaviorType?: 'smooth' | 'auto' scrollBehaviorType?: 'smooth' | 'auto'

View File

@ -17,11 +17,11 @@
"prerender": "pnpm build && jiti ./lib/prerender" "prerender": "pnpm build && jiti ./lib/prerender"
}, },
"devDependencies": { "devDependencies": {
"@unocss/reset": "0.64.1", "@unocss/reset": "0.65.0",
"beasties": "0.1.0", "beasties": "0.1.0",
"html-validate": "8.26.0", "html-validate": "8.27.0",
"htmlnano": "2.1.1", "htmlnano": "2.1.1",
"jiti": "2.4.0", "jiti": "2.4.1",
"knitwork": "1.1.0", "knitwork": "1.1.0",
"pathe": "1.1.2", "pathe": "1.1.2",
"prettier": "3.4.1", "prettier": "3.4.1",
@ -29,7 +29,10 @@
"svgo": "3.3.2", "svgo": "3.3.2",
"tinyexec": "0.3.1", "tinyexec": "0.3.1",
"tinyglobby": "0.2.10", "tinyglobby": "0.2.10",
"unocss": "0.64.1", "unocss": "0.65.0",
"vite": "6.0.1" "vite": "6.0.2"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
} }
} }

View File

@ -26,7 +26,7 @@
"devDependencies": { "devDependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@types/clear": "0.1.4", "@types/clear": "0.1.4",
"rollup": "4.27.4", "rollup": "4.28.0",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"vue": "3.5.13" "vue": "3.5.13"
}, },
@ -45,7 +45,7 @@
"externality": "^1.0.2", "externality": "^1.0.2",
"get-port-please": "^3.1.2", "get-port-please": "^3.1.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "^2.4.0", "jiti": "^2.4.1",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"magic-string": "^0.30.14", "magic-string": "^0.30.14",
"mlly": "^1.7.3", "mlly": "^1.7.3",
@ -56,9 +56,9 @@
"std-env": "^3.8.0", "std-env": "^3.8.0",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unplugin": "^1.16.0", "unplugin": "^2.0.0",
"vite": "^6.0.1", "vite": "^6.0.2",
"vite-node": "^2.1.6", "vite-node": "^2.1.8",
"vite-plugin-checker": "^0.8.0", "vite-plugin-checker": "^0.8.0",
"vue-bundle-renderer": "^2.1.1" "vue-bundle-renderer": "^2.1.1"
}, },
@ -66,6 +66,6 @@
"vue": "^3.3.4" "vue": "^3.3.4"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -85,6 +85,7 @@ export async function buildServer (ctx: ViteBuildContext) {
'nitro/runtime', 'nitro/runtime',
'#internal/nuxt/paths', '#internal/nuxt/paths',
'#internal/nuxt/app-config', '#internal/nuxt/app-config',
'#app-manifest',
'#shared', '#shared',
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared)))), new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared)))),
], ],

View File

@ -39,33 +39,21 @@ export function viteNodePlugin (ctx: ViteBuildContext): VitePlugin {
name: 'nuxt:vite-node-server', name: 'nuxt:vite-node-server',
enforce: 'post', enforce: 'post',
configureServer (server) { configureServer (server) {
function invalidateVirtualModules () { server.middlewares.use('/__nuxt_vite_node__', toNodeListener(createViteNodeApp(ctx, invalidates)))
for (const [id, mod] of server.moduleGraph.idToModuleMap) {
if (id.startsWith('virtual:') || id.startsWith('\0virtual:')) { // invalidate changed virtual modules when templates are regenerated
ctx.nuxt.hook('app:templatesGenerated', (_app, changedTemplates) => {
for (const template of changedTemplates) {
const mods = server.moduleGraph.getModulesByFile(`virtual:nuxt:${template.dst}`)
for (const mod of mods || []) {
markInvalidate(mod) markInvalidate(mod)
} }
} }
if (ctx.nuxt.apps.default) {
for (const template of ctx.nuxt.apps.default.templates) {
markInvalidates(server.moduleGraph.getModulesByFile(template.dst!))
}
}
}
server.middlewares.use('/__nuxt_vite_node__', toNodeListener(createViteNodeApp(ctx, invalidates)))
// Invalidate all virtual modules when templates are regenerated
ctx.nuxt.hook('app:templatesGenerated', () => {
invalidateVirtualModules()
}) })
server.watcher.on('all', (event, file) => { server.watcher.on('all', (event, file) => {
markInvalidates(server.moduleGraph.getModulesByFile(normalize(file))) markInvalidates(server.moduleGraph.getModulesByFile(normalize(file)))
// Invalidate all virtual modules when a file is added or removed
if (event === 'add' || event === 'unlink') {
invalidateVirtualModules()
}
}) })
}, },
} }

View File

@ -210,10 +210,11 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer, env) => { nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer, env) => {
// Invalidate virtual modules when templates are re-generated // Invalidate virtual modules when templates are re-generated
ctx.nuxt.hook('app:templatesGenerated', () => { ctx.nuxt.hook('app:templatesGenerated', (_app, changedTemplates) => {
for (const [id, mod] of server.moduleGraph.idToModuleMap) { for (const template of changedTemplates) {
if (id.startsWith('virtual:') || id.startsWith('\0virtual:')) { for (const mod of server.moduleGraph.getModulesByFile(`virtual:nuxt:${template.dst}`) || []) {
server.moduleGraph.invalidateModule(mod) server.moduleGraph.invalidateModule(mod)
server.reloadModule(mod)
} }
} }
}) })

View File

@ -42,11 +42,11 @@
"fork-ts-checker-webpack-plugin": "^9.0.2", "fork-ts-checker-webpack-plugin": "^9.0.2",
"globby": "^14.0.2", "globby": "^14.0.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "^2.4.0", "jiti": "^2.4.1",
"knitwork": "^1.1.0", "knitwork": "^1.1.0",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"magic-string": "^0.30.14", "magic-string": "^0.30.14",
"memfs": "^4.14.0", "memfs": "^4.14.1",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
"ohash": "^1.1.4", "ohash": "^1.1.4",
"pathe": "^1.1.2", "pathe": "^1.1.2",
@ -61,7 +61,7 @@
"time-fix-plugin": "^2.0.7", "time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4", "ufo": "^1.5.4",
"unenv": "^1.10.0", "unenv": "^1.10.0",
"unplugin": "^1.16.0", "unplugin": "^2.0.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1", "vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2", "vue-loader": "^17.4.2",
@ -73,12 +73,12 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@rspack/core": "1.1.4", "@rspack/core": "1.1.5",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/pify": "5.0.4", "@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0", "@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9", "@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.27.4", "rollup": "4.28.0",
"unbuild": "3.0.0-rc.11", "unbuild": "3.0.0-rc.11",
"vue": "3.5.13" "vue": "3.5.13"
}, },
@ -86,6 +86,6 @@
"vue": "^3.3.4" "vue": "^3.3.4"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.10.0" "node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -59,7 +59,7 @@ function serverStandalone (ctx: WebpackConfigContext) {
resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared), resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared),
] ]
if (!ctx.nuxt.options.dev) { if (!ctx.nuxt.options.dev) {
external.push('#internal/nuxt/paths', '#internal/nuxt/app-config') external.push('#internal/nuxt/paths', '#internal/nuxt/app-config', '#app-manifest')
} }
if (!Array.isArray(ctx.config.externals)) { return } if (!Array.isArray(ctx.config.externals)) { return }

View File

@ -8,5 +8,8 @@
}, },
"dependencies": { "dependencies": {
"nuxt": "workspace:*" "nuxt": "workspace:*"
},
"engines": {
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -28,11 +28,22 @@
"ignoreDeps": [ "ignoreDeps": [
"nitro", "nitro",
"h3", "h3",
"typescript",
"nuxt", "nuxt",
"nuxt3", "nuxt3",
"@nuxt/kit" "@nuxt/kit"
] ]
},
{
"groupName": "typescript",
"matchPackageNames": [
"typescript"
]
},
{
"groupName": "unimport",
"matchPackageNames": [
"unimport"
]
} }
] ]
} }

View File

@ -1960,12 +1960,12 @@ describe('server components/islands', () => {
await page.waitForLoadState('networkidle') await page.waitForLoadState('networkidle')
await page.getByText('Go to page without lazy server component').click() await page.getByText('Go to page without lazy server component').click()
const text = (await page.innerText('pre')).replaceAll(/ data-island-uid="([^"]*)"/g, '').replace(/data-island-component="([^"]*)"/g, (_, content) => `data-island-component="${content.split('-')[0]}"`) const text = (await page.innerText('pre')).replaceAll(/ data-island-uid="([^"]*)"/g, '').replace(/data-island-component="([^"]*)"/g, 'data-island-component')
if (isWebpack) { if (isWebpack) {
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <div class="sugar-counter" nuxt-client=""> Sugar Counter 12 x 1 = 12 <button> Inc </button></div></div></div>"') expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <div class="sugar-counter" nuxt-client=""> Sugar Counter 12 x 1 = 12 <button> Inc </button></div></div></div>"')
} else { } else {
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>"') expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-component></div><!--teleport start--><!--teleport end--><!--]--></div></div>"')
} }
expect(text).toContain('async component that was very long') expect(text).toContain('async component that was very long')
@ -2316,7 +2316,7 @@ describe('component islands', () => {
const { components } = result const { components } = result
result.components = {} result.components = {}
result.slots = {} result.slots = {}
result.html = result.html.replace(/ data-island-component="([^"]*)"/g, (_, content) => ` data-island-component="${content.split('-')[0]}"`) result.html = result.html.replace(/data-island-component="([^"]*)"/g, 'data-island-component')
const teleportsEntries = Object.entries(components || {}) const teleportsEntries = Object.entries(components || {})
@ -2327,12 +2327,11 @@ describe('component islands', () => {
"link": [], "link": [],
"style": [], "style": [],
}, },
"html": "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>", "html": "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component></div><!--teleport start--><!--teleport end--><!--]--></div></div>",
"slots": {}, "slots": {},
} }
`) `)
expect(teleportsEntries).toHaveLength(1) expect(teleportsEntries).toHaveLength(1)
expect(teleportsEntries[0]![0].startsWith('Counter-')).toBeTruthy()
expect(teleportsEntries[0]![1].props).toMatchInlineSnapshot(` expect(teleportsEntries[0]![1].props).toMatchInlineSnapshot(`
{ {
"multiplier": 1, "multiplier": 1,

View File

@ -14,5 +14,8 @@
"vitest": "1.6.0", "vitest": "1.6.0",
"vue": "latest", "vue": "latest",
"vue-router": "latest" "vue-router": "latest"
},
"engines": {
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -34,6 +34,23 @@ describe('API routes', () => {
expectTypeOf($fetch<TestResponse>('/test')).toEqualTypeOf<Promise<TestResponse>>() expectTypeOf($fetch<TestResponse>('/test')).toEqualTypeOf<Promise<TestResponse>>()
}) })
it('works with useRequestFetch', () => {
const $fetch = useRequestFetch()
expectTypeOf($fetch('/api/hello')).toEqualTypeOf<Promise<string>>()
// registered in extends
expectTypeOf($fetch('/api/foo')).toEqualTypeOf<Promise<string>>()
// registered in module
expectTypeOf($fetch('/auto-registered-module')).toEqualTypeOf<Promise<string>>()
expectTypeOf($fetch('/api/hey')).toEqualTypeOf<Promise<{ foo: string, baz: string }>>()
expectTypeOf($fetch('/api/hey', { method: 'get' })).toEqualTypeOf<Promise<{ foo: string, baz: string }>>()
expectTypeOf($fetch('/api/hey', { method: 'post' })).toEqualTypeOf<Promise<{ method: 'post' }>>()
// @ts-expect-error not a valid method
expectTypeOf($fetch('/api/hey', { method: 'patch ' })).toEqualTypeOf<Promise<{ foo: string, baz: string }>>()
expectTypeOf($fetch('/api/union')).toEqualTypeOf<Promise<{ type: 'a', foo: string } | { type: 'b', baz: string }>>()
expectTypeOf($fetch('/api/other')).toEqualTypeOf<Promise<unknown>>()
expectTypeOf($fetch<TestResponse>('/test')).toEqualTypeOf<Promise<TestResponse>>()
})
it('works with useAsyncData', () => { it('works with useAsyncData', () => {
expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>() expectTypeOf(useAsyncData('api-hello', () => $fetch('/api/hello')).data).toEqualTypeOf<Ref<string | DefaultAsyncDataValue>>()
expectTypeOf(useAsyncData('api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>() expectTypeOf(useAsyncData('api-hey', () => $fetch('/api/hey')).data).toEqualTypeOf<Ref<{ foo: string, baz: string } | DefaultAsyncDataValue>>()

View File

@ -15,5 +15,8 @@
"ufo": "latest", "ufo": "latest",
"unplugin": "latest", "unplugin": "latest",
"vue": "latest" "vue": "latest"
},
"engines": {
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -3,7 +3,8 @@ const hmrId = ref(0)
</script> </script>
<template> <template>
<pre id="hmr-id"> <div>
HMR ID: {{ hmrId }} HMR ID:
</pre> <span data-testid="hmr-id">{{ hmrId }}</span>
</div>
</template> </template>

10
test/fixtures/hmr/nuxt.config.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export default defineNuxtConfig({
builder: process.env.TEST_BUILDER as 'webpack' | 'rspack' | 'vite' ?? 'vite',
experimental: {
asyncContext: process.env.TEST_CONTEXT === 'async',
appManifest: process.env.TEST_MANIFEST !== 'manifest-off',
renderJsonPayloads: process.env.TEST_PAYLOAD !== 'js',
inlineRouteRules: true,
},
compatibilityDate: '2024-06-28',
})

13
test/fixtures/hmr/package.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"private": true,
"name": "fixture-hmr",
"scripts": {
"build": "nuxi build"
},
"dependencies": {
"nuxt": "workspace:*"
},
"engines": {
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
}
}

21
test/fixtures/hmr/pages/index.vue vendored Normal file
View File

@ -0,0 +1,21 @@
<script setup lang="ts">
definePageMeta({
some: 'stuff',
})
const count = ref(1)
</script>
<template>
<div>
<Title>HMR fixture</Title>
<h1>Home page</h1>
<div>
Count:
<span data-testid="count">{{ count }}</span>
</div>
<button @click="count++">
Increment
</button>
<pre>{{ $route.meta }}</pre>
</div>
</template>

11
test/fixtures/hmr/pages/page-meta.vue vendored Normal file
View File

@ -0,0 +1,11 @@
<script setup lang="ts">
definePageMeta({
some: 'stuff',
})
</script>
<template>
<div>
<pre data-testid="meta">{{ $route.meta }}</pre>
</div>
</template>

13
test/fixtures/hmr/pages/route-rules.vue vendored Normal file
View File

@ -0,0 +1,13 @@
<script setup lang="ts">
defineRouteRules({
headers: {
'x-extend': 'added in routeRules',
},
})
</script>
<template>
<div>
Route rules defined inline
</div>
</template>

View File

@ -0,0 +1,7 @@
<template>
<div>
<NuxtLink to="/routes/non-existent">
To non-existent link
</NuxtLink>
</div>
</template>

3
test/fixtures/hmr/tsconfig.json vendored Normal file
View File

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

View File

@ -7,5 +7,8 @@
}, },
"dependencies": { "dependencies": {
"nuxt": "workspace:*" "nuxt": "workspace:*"
},
"engines": {
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -6,5 +6,8 @@
}, },
"dependencies": { "dependencies": {
"nuxt": "workspace:*" "nuxt": "workspace:*"
},
"engines": {
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -6,5 +6,8 @@
}, },
"dependencies": { "dependencies": {
"nuxt": "workspace:*" "nuxt": "workspace:*"
},
"engines": {
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -9,5 +9,8 @@
}, },
"devDependencies": { "devDependencies": {
"typescript": "latest" "typescript": "latest"
},
"engines": {
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
} }
} }

View File

@ -5,7 +5,7 @@ import { isWindows } from 'std-env'
import { join } from 'pathe' import { join } from 'pathe'
import { $fetch as _$fetch, fetch, setup } from '@nuxt/test-utils/e2e' import { $fetch as _$fetch, fetch, setup } from '@nuxt/test-utils/e2e'
import { expectWithPolling, renderPage } from './utils' import { expectNoErrorsOrWarnings, expectWithPolling, renderPage } from './utils'
// TODO: update @nuxt/test-utils // TODO: update @nuxt/test-utils
const $fetch = _$fetch as import('nitro/types').$Fetch<unknown, import('nitro/types').NitroFetchRequest> const $fetch = _$fetch as import('nitro/types').$Fetch<unknown, import('nitro/types').NitroFetchRequest>
@ -14,7 +14,7 @@ const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUI
// TODO: fix HMR on Windows // TODO: fix HMR on Windows
if (process.env.TEST_ENV !== 'built' && !isWindows) { if (process.env.TEST_ENV !== 'built' && !isWindows) {
const fixturePath = fileURLToPath(new URL('./fixtures-temp/basic', import.meta.url)) const fixturePath = fileURLToPath(new URL('./fixtures-temp/hmr', import.meta.url))
await setup({ await setup({
rootDir: fixturePath, rootDir: fixturePath,
dev: true, dev: true,
@ -26,127 +26,143 @@ if (process.env.TEST_ENV !== 'built' && !isWindows) {
}, },
}) })
const indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8')
describe('hmr', () => { describe('hmr', () => {
it('should work', async () => { it('should work', async () => {
const { page, pageErrors, consoleLogs } = await renderPage('/') const { page, pageErrors, consoleLogs } = await renderPage('/')
expect(await page.title()).toBe('Basic fixture') expect(await page.title()).toBe('HMR fixture')
expect((await page.$('.sugar-counter').then(r => r!.textContent()))!.trim()) expect(await page.getByTestId('count').textContent()).toBe('1')
.toEqual('Sugar Counter 12 x 2 = 24 Inc')
// reactive // reactive
await page.$('.sugar-counter button').then(r => r!.click()) await page.getByRole('button').click()
expect((await page.$('.sugar-counter').then(r => r!.textContent()))!.trim()) expect(await page.getByTestId('count').textContent()).toBe('2')
.toEqual('Sugar Counter 13 x 2 = 26 Inc')
// modify file // modify file
let indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8') let newContents = indexVue
indexVue = indexVue .replace('<Title>HMR fixture</Title>', '<Title>HMR fixture HMR</Title>')
.replace('<Title>Basic fixture</Title>', '<Title>Basic fixture HMR</Title>') .replace('<h1>Home page</h1>', '<h1>Home page - but not as you knew it</h1>')
.replace('<h1>Hello Nuxt 3!</h1>', '<h1>Hello Nuxt 3! HMR</h1>') newContents += '<style scoped>\nh1 { color: red }\n</style>'
indexVue += '<style scoped>\nh1 { color: red }\n</style>' await fsp.writeFile(join(fixturePath, 'pages/index.vue'), newContents)
await fsp.writeFile(join(fixturePath, 'pages/index.vue'), indexVue)
await expectWithPolling( await expectWithPolling(() => page.title(), 'HMR fixture HMR')
() => page.title(),
'Basic fixture HMR',
)
// content HMR // content HMR
const h1 = await page.$('h1') const h1 = page.getByRole('heading')
expect(await h1!.textContent()).toBe('Hello Nuxt 3! HMR') expect(await h1!.textContent()).toBe('Home page - but not as you knew it')
// style HMR // style HMR
const h1Color = await h1!.evaluate(el => window.getComputedStyle(el).getPropertyValue('color')) const h1Color = await h1.evaluate(el => window.getComputedStyle(el).getPropertyValue('color'))
expect(h1Color).toMatchInlineSnapshot('"rgb(255, 0, 0)"') expect(h1Color).toMatchInlineSnapshot('"rgb(255, 0, 0)"')
// ensure no errors // ensure no errors
const consoleLogErrors = consoleLogs.filter(i => i.type === 'error') expectNoErrorsOrWarnings(consoleLogs)
const consoleLogWarnings = consoleLogs.filter(i => i.type === 'warn')
expect(pageErrors).toEqual([]) expect(pageErrors).toEqual([])
expect(consoleLogErrors).toEqual([])
expect(consoleLogWarnings).toEqual([])
await page.close() await page.close()
}, 60_000) })
it('should detect new routes', async () => { it('should detect new routes', async () => {
await expectWithPolling( const res = await fetch('/some-404')
() => $fetch<string>('/catchall/some-404').then(r => r.includes('catchall at some-404')).catch(() => null), expect(res.status).toBe(404)
true,
)
// write new page route // write new page route
const indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8') await fsp.writeFile(join(fixturePath, 'pages/some-404.vue'), indexVue)
await fsp.writeFile(join(fixturePath, 'pages/catchall/some-404.vue'), indexVue) await expectWithPolling(() => $fetch<string>('/some-404').then(r => r.includes('Home page')).catch(() => null), true)
await expectWithPolling(
() => $fetch<string>('/catchall/some-404').then(r => r.includes('Hello Nuxt 3')).catch(() => null),
true,
)
}) })
it('should hot reload route rules', async () => { it('should hot reload route rules', async () => {
await expectWithPolling( await expectWithPolling(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null), 'added in routeRules')
() => fetch('/route-rules/inline').then(r => r.headers.get('x-extend') === 'added in routeRules').catch(() => null),
true,
)
// write new page route // write new page route
const file = await fsp.readFile(join(fixturePath, 'pages/route-rules/inline.vue'), 'utf8') const file = await fsp.readFile(join(fixturePath, 'pages/route-rules.vue'), 'utf8')
await fsp.writeFile(join(fixturePath, 'pages/route-rules/inline.vue'), file.replace('added in routeRules', 'edited in dev')) await fsp.writeFile(join(fixturePath, 'pages/route-rules.vue'), file.replace('added in routeRules', 'edited in dev'))
await expectWithPolling( await expectWithPolling(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null), 'edited in dev')
() => fetch('/route-rules/inline').then(r => r.headers.get('x-extend') === 'edited in dev').catch(() => null),
true,
)
}) })
it('should HMR islands', async () => { it('should HMR islands', async () => {
const { page, pageErrors, consoleLogs } = await renderPage('/server-component-hmr') const { page, pageErrors, consoleLogs } = await renderPage('/server-component')
let hmrId = 0
const resolveHmrId = async () => {
const node = await page.$('#hmr-id')
const text = await node?.innerText() || ''
return Number(text.trim().split(':')[1]?.trim() || '')
}
const componentPath = join(fixturePath, 'components/islands/HmrComponent.vue') const componentPath = join(fixturePath, 'components/islands/HmrComponent.vue')
const triggerHmr = async () => fsp.writeFile( const componentContents = await fsp.readFile(componentPath, 'utf8')
componentPath, const triggerHmr = (number: string) => fsp.writeFile(componentPath, componentContents.replace('ref(0)', `ref(${number})`))
(await fsp.readFile(componentPath, 'utf8'))
.replace(`ref(${hmrId++})`, `ref(${hmrId})`),
)
// initial state // initial state
await expectWithPolling( await expectWithPolling(async () => await page.getByTestId('hmr-id').innerText(), '0')
resolveHmrId,
0,
)
// first edit // first edit
await triggerHmr() await triggerHmr('1')
await expectWithPolling( await expectWithPolling(async () => await page.getByTestId('hmr-id').innerText(), '1')
resolveHmrId,
1,
)
// just in-case // just in-case
await triggerHmr() await triggerHmr('2')
await expectWithPolling( await expectWithPolling(async () => await page.getByTestId('hmr-id').innerText(), '2')
resolveHmrId,
2,
)
// ensure no errors // ensure no errors
const consoleLogErrors = consoleLogs.filter(i => i.type === 'error') expectNoErrorsOrWarnings(consoleLogs)
const consoleLogWarnings = consoleLogs.filter(i => i.type === 'warn')
expect(pageErrors).toEqual([]) expect(pageErrors).toEqual([])
expect(consoleLogErrors).toEqual([])
expect(consoleLogWarnings).toEqual([])
await page.close() await page.close()
}, 60_000) })
it.skipIf(isWebpack)('should HMR page meta', async () => {
const { page, pageErrors, consoleLogs } = await renderPage('/page-meta')
const pagePath = join(fixturePath, 'pages/page-meta.vue')
const pageContents = await fsp.readFile(pagePath, 'utf8')
expect(JSON.parse(await page.getByTestId('meta').textContent() || '{}')).toStrictEqual({ some: 'stuff' })
const initialConsoleLogs = structuredClone(consoleLogs)
await fsp.writeFile(pagePath, pageContents.replace(`some: 'stuff'`, `some: 'other stuff'`))
await expectWithPolling(async () => await page.getByTestId('meta').textContent() || '{}', JSON.stringify({ some: 'other stuff' }, null, 2))
expect(consoleLogs).toStrictEqual([
...initialConsoleLogs,
{
'text': '[vite] hot updated: /pages/page-meta.vue',
'type': 'debug',
},
{
'text': '[vite] hot updated: /pages/page-meta.vue?macro=true',
'type': 'debug',
},
{
'text': `[vite] hot updated: /@id/virtual:nuxt:${fixturePath}/.nuxt/routes.mjs`,
'type': 'debug',
},
])
// ensure no errors
expectNoErrorsOrWarnings(consoleLogs)
expect(pageErrors).toEqual([])
await page.close()
})
it.skipIf(isWebpack)('should HMR routes', async () => {
const { page, pageErrors, consoleLogs } = await renderPage('/routes')
await fsp.writeFile(join(fixturePath, 'pages/routes/non-existent.vue'), `<template><div data-testid="contents">A new route!</div></template>`)
await page.getByRole('link').click()
await expectWithPolling(() => page.getByTestId('contents').textContent(), 'A new route!')
for (const log of consoleLogs) {
if (log.text.includes('No match found for location with path "/routes/non-existent"')) {
// we expect this warning before the routes are updated
log.type = 'debug'
}
}
// ensure no errors
expectNoErrorsOrWarnings(consoleLogs)
expect(pageErrors).toEqual([])
await page.close()
})
}) })
} else { } else {
describe.skip('hmr', () => {}) describe.skip('hmr', () => {})

View File

@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import type { ComponentOptions } from 'vue' import type { ComponentOptions } from 'vue'
import { defineComponent, h, toDisplayString, useAttrs } from 'vue' import { Suspense, defineComponent, h, toDisplayString, useAttrs } from 'vue'
import { mountSuspended } from '@nuxt/test-utils/runtime' import { mountSuspended } from '@nuxt/test-utils/runtime'
import { flushPromises, mount } from '@vue/test-utils'
import { createClientOnly } from '../../packages/nuxt/src/app/components/client-only' import { createClientOnly } from '../../packages/nuxt/src/app/components/client-only'
import { createClientPage } from '../../packages/nuxt/dist/components/runtime/client-component'
const Client = defineComponent({ const Client = defineComponent({
name: 'TestClient', name: 'TestClient',
@ -27,3 +29,39 @@ describe('createClient attribute inheritance', () => {
`) `)
}) })
}) })
describe('client page', () => {
it('Should be suspensed when out of hydration', async () => {
let resolve
const promise = new Promise((_resolve) => {
resolve = _resolve
})
const comp = defineComponent({
async setup () {
await promise
return () => h('div', { id: 'async' }, 'async resolved')
},
})
const wrapper = mount({
setup () {
return () => h('div', {}, [
h(Suspense, {}, {
default: () => h(createClientPage(() => Promise.resolve(comp)), {}),
fallback: () => h('div', { id: 'fallback' }, 'loading'),
}),
])
},
})
await flushPromises()
expect(wrapper.find('#fallback').exists()).toBe(true)
expect(wrapper.find('#async').exists()).toBe(false)
resolve!()
await flushPromises()
expect(wrapper.find('#async').exists()).toBe(true)
expect(wrapper.find('#fallback').exists()).toBe(false)
})
})

View File

@ -57,14 +57,18 @@ export async function expectNoClientErrors (path: string) {
const { page, pageErrors, consoleLogs } = (await renderPage(path))! const { page, pageErrors, consoleLogs } = (await renderPage(path))!
expect(pageErrors).toEqual([])
expectNoErrorsOrWarnings(consoleLogs)
await page.close()
}
export function expectNoErrorsOrWarnings (consoleLogs: Array<{ type: string, text: string }>) {
const consoleLogErrors = consoleLogs.filter(i => i.type === 'error') const consoleLogErrors = consoleLogs.filter(i => i.type === 'error')
const consoleLogWarnings = consoleLogs.filter(i => i.type === 'warning') const consoleLogWarnings = consoleLogs.filter(i => i.type === 'warning')
expect(pageErrors).toEqual([])
expect(consoleLogErrors).toEqual([]) expect(consoleLogErrors).toEqual([])
expect(consoleLogWarnings).toEqual([]) expect(consoleLogWarnings).toEqual([])
await page.close()
} }
export async function gotoPath (page: Page, path: string) { export async function gotoPath (page: Page, path: string) {