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 && \
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
run: pnpm install
- name: Check engine ranges, peer dependency ranges and installed versions
run: pnpm installed-check --fix
- name: Build (stub)
run: pnpm dev:prepare

View File

@ -4,12 +4,14 @@ on:
push:
paths:
- "**/package.json"
- "pnpm-lock.yaml"
branches:
- main
- 3.x
pull_request:
paths:
- "**/package.json"
- "pnpm-lock.yaml"
branches:
- main
- 3.x
@ -32,5 +34,9 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Lint monorepo
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.
- **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`
- **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**.

View File

@ -91,6 +91,9 @@ pnpm dev -o
```bash [bun]
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`
[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`
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)
[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)
[Stylelint](https://stylelint.io) | `.stylelintrc.json` | [More Info](https://stylelint.io/user-guide/configure)
[TailwindCSS](https://tailwindcss.com) | `tailwind.config.js` | [More Info](https://tailwindcss.nuxtjs.org/tailwind/config)
[Prettier](https://prettier.io) | `prettier.config.js` | [More Info](https://prettier.io/docs/en/configuration.html)
[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)
[Vitest](https://vitest.dev) | `vitest.config.ts` | [More Info](https://vitest.dev/config)
## Vue Configuration

View File

@ -27,7 +27,7 @@ For example, referencing an image file in the `public/img/` directory, available
## 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.

View File

@ -204,7 +204,7 @@ export default defineNuxtConfig({
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:
@ -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).
## Using PostCSS

View File

@ -21,7 +21,7 @@ We chose to build Nuxt on top of Vue for these reasons:
### 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

View File

@ -85,7 +85,7 @@ export default defineNuxtConfig({
### typescriptBundlerResolution
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`.

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`.
::
::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
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).
::
::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"}
## Example

View File

@ -17,3 +17,4 @@ The `upgrade` command upgrades Nuxt to the latest version.
Option | Default | Description
-------------------------|-----------------|------------------
`--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.
::
@ -329,7 +329,7 @@ interface ExtendViteConfigOptions {
```
::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

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/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/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).
## 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:
- [Vite](https://vitejs.dev) or [webpack](https://webpack.js.org)
- [Vite](https://vite.dev) or [webpack](https://webpack.js.org)
- [Rollup](https://rollupjs.org)
- [PostCSS](https://postcss.org)
- [esbuild](https://esbuild.github.io)

View File

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

View File

@ -34,7 +34,7 @@
"errx": "^0.1.0",
"globby": "^14.0.2",
"ignore": "^6.0.2",
"jiti": "^2.4.0",
"jiti": "^2.4.1",
"klona": "^2.0.6",
"mlly": "^1.7.3",
"ohash": "^1.1.4",
@ -48,15 +48,15 @@
"untyped": "^1.5.1"
},
"devDependencies": {
"@rspack/core": "1.1.4",
"@rspack/core": "1.1.5",
"@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"unbuild": "3.0.0-rc.11",
"vite": "6.0.1",
"vitest": "2.1.6",
"vite": "6.0.2",
"vitest": "2.1.8",
"webpack": "5.96.1"
},
"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 { pathToFileURL } from 'node:url'
import { fileURLToPath, pathToFileURL } from 'node:url'
import type { ModuleMeta, Nuxt, NuxtConfig, NuxtModule } from '@nuxt/schema'
import { dirname, isAbsolute, join, resolve } from 'pathe'
import { defu } from 'defu'
import { createJiti } from 'jiti'
import { resolve as resolveModule } from 'mlly'
import { parseNodeModulePath, resolve as resolveModule } from 'mlly'
import { isRelative } from 'ufo'
import { useNuxt } from '../context'
import { resolveAlias, resolvePath } from '../resolve'
@ -17,7 +17,7 @@ export async function installModule<
T extends string | NuxtModule,
Config extends Extract<NonNullable<NuxtConfig['modules']>[number], [T, any]>,
> (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>()
for (const l of nuxt.options._layers) {
@ -33,9 +33,12 @@ export async function installModule<
return
}
if (typeof moduleToInstall === 'string') {
nuxt.options.build.transpile.push(normalizeModuleTranspilePath(moduleToInstall))
const directory = getDirectory(moduleToInstall)
const modulePath = resolvedModulePath || moduleToInstall
if (typeof modulePath === 'string') {
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)) {
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()) {
let buildTimeModuleMeta: ModuleMeta = {}
let resolvedModulePath: string | undefined
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 })
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
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
if (typeof nuxtModule !== 'function') {
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)
// Remove any existing template with the same destination path
nuxt.options.build.templates = nuxt.options.build.templates
.filter(p => normalizeTemplate(p).dst !== template.dst)
nuxt.options.build.templates = nuxt.options.build.templates.filter(p => normalizeTemplate(p).dst !== template.dst)
// Add to templates array
nuxt.options.build.templates.push(template)
@ -68,7 +67,7 @@ export function addTypeTemplate<T> (_template: NuxtTypeTemplate<T>) {
/**
* 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) {
throw new Error('Invalid template: ' + JSON.stringify(template))
}
@ -87,17 +86,16 @@ export function normalizeTemplate<T> (template: NuxtTemplate<T> | string): Resol
}
if (!template.filename) {
const srcPath = parse(template.src)
template.filename = (template as any).fileName ||
`${basename(srcPath.dir)}.${srcPath.name}.${hash(template.src)}${srcPath.ext}`
template.filename = (template as any).fileName || `${basename(srcPath.dir)}.${srcPath.name}.${hash(template.src)}${srcPath.ext}`
}
}
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) {
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
@ -107,8 +105,7 @@ export function normalizeTemplate<T> (template: NuxtTemplate<T> | string): Resol
// Resolve dst
if (!template.dst) {
const nuxt = useNuxt()
template.dst = resolve(nuxt.options.buildDir, template.filename)
template.dst = resolve(buildDir ?? useNuxt().options.buildDir, template.filename)
}
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": {
"types": "./dist/app/index.d.ts",
"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": [
@ -60,7 +68,7 @@
},
"dependencies": {
"@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.6.1",
"@nuxt/devtools": "^1.6.2",
"@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.0",
@ -88,7 +96,7 @@
"hookable": "^5.5.3",
"ignore": "^6.0.2",
"impound": "^0.2.0",
"jiti": "^2.4.0",
"jiti": "^2.4.1",
"klona": "^2.0.6",
"knitwork": "^1.1.0",
"magic-string": "^0.30.14",
@ -96,7 +104,7 @@
"nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "^3.16.0",
"nypm": "^0.4.0",
"nypm": "^0.4.1",
"ofetch": "^1.4.1",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
@ -115,7 +123,7 @@
"unenv": "^1.10.0",
"unhead": "^1.11.13",
"unimport": "^3.13.4",
"unplugin": "^1.16.0",
"unplugin": "^2.0.0",
"unplugin-vue-router": "^0.10.8",
"unstorage": "^1.13.1",
"untyped": "^1.5.1",
@ -131,12 +139,12 @@
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
"unbuild": "3.0.0-rc.11",
"vite": "6.0.1",
"vitest": "2.1.6"
"vite": "6.0.2",
"vitest": "2.1.8"
},
"peerDependencies": {
"@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": {
"@parcel/watcher": {
@ -147,6 +155,6 @@
}
},
"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 { Teleport, defineComponent, h, inject, provide } from 'vue'
import { Teleport, defineComponent, h, inject, provide, useId } from 'vue'
import { useNuxtApp } from '../nuxt'
// @ts-expect-error virtual file
import { paths } from '#build/components-chunk'
@ -20,10 +20,6 @@ export default defineComponent({
name: 'NuxtTeleportIslandComponent',
inheritAttrs: false,
props: {
to: {
type: String,
required: true,
},
nuxtClient: {
type: Boolean,
default: false,
@ -31,11 +27,12 @@ export default defineComponent({
},
setup (props, { slots }) {
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 (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient || inject(NuxtTeleportIslandSymbol, false)) { return () => slots.default?.() }
provide(NuxtTeleportIslandSymbol, props.to)
provide(NuxtTeleportIslandSymbol, to)
const islandContext = nuxtApp.ssrContext!.islandContext!
return () => {
@ -43,7 +40,7 @@ export default defineComponent({
const slotType = slot.type as ExtendedComponent
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],
props: slot.props || {},
}
@ -51,8 +48,8 @@ export default defineComponent({
return [h('div', {
'style': 'display: contents;',
'data-island-uid': '',
'data-island-component': props.to,
}, []), h(Teleport, { to: props.to }, slot)]
'data-island-component': to,
}, []), h(Teleport, { to }, slot)]
}
},
})

View File

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

View File

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

View File

@ -85,15 +85,18 @@ async function _importPayload (payloadURL: string) {
}
/** @since 3.0.0 */
export async function isPrerendered (url = useRoute().path) {
const nuxtApp = useNuxtApp()
// 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)
const manifest = await getAppManifest()
if (manifest.prerendered.includes(url)) {
return true
}
const rules = await getRouteRules(url)
return !!rules.prerender && !rules.redirect
return nuxtApp.runWithContext(async () => {
const rules = await getRouteRules(url)
return !!rules.prerender && !rules.redirect
})
}
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 { computed, getCurrentInstance, ref } from 'vue'
import { useServerHead } from '@unhead/vue'
import type { H3Event$Fetch } from 'nitro/types'
import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt'
@ -39,11 +40,11 @@ export function useRequestHeader (header: string) {
}
/** @since 3.2.0 */
export function useRequestFetch (): typeof global.$fetch {
export function useRequestFetch (): H3Event$Fetch | typeof global.$fetch {
if (import.meta.client) {
return globalThis.$fetch
}
return useRequestEvent()?.$fetch as typeof globalThis.$fetch || globalThis.$fetch
return useRequestEvent()?.$fetch || globalThis.$fetch
}
/** @since 3.0.0 */

View File

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

View File

@ -6,7 +6,6 @@ import { parseURL } from 'ufo'
import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string'
import { ELEMENT_NODE, parse, walk } from 'ultrahtml'
import { hash } from 'ohash'
import { resolvePath } from '@nuxt/kit'
import defu from 'defu'
import { isVue } from '../../core/utils'
@ -113,8 +112,6 @@ export const IslandsTransformPlugin = (options: ServerOnlyComponentTransformPlug
const { loc, attributes } = node
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'])
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, '')
}
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.appendRight(startingIndex + loc[1].end, '</NuxtTeleportIslandComponent>')
})

View File

@ -1,6 +1,7 @@
import { defineAsyncComponent, defineComponent, h } from 'vue'
import type { AsyncComponentLoader } from 'vue'
import ClientOnly from '#app/components/client-only'
import { useNuxtApp } from '#app/nuxt'
/* @__NO_SIDE_EFFECTS__ */
export const createClientPage = (loader: AsyncComponentLoader) => {
@ -15,11 +16,15 @@ export const createClientPage = (loader: AsyncComponentLoader) => {
return defineComponent({
inheritAttrs: false,
setup (_, { attrs }) {
return () => h('div', [
h(ClientOnly, undefined, {
default: () => h(page, attrs),
}),
])
const nuxtApp = useNuxtApp()
if (import.meta.server || nuxtApp.isHydrating) {
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)
// 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
// 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`)
// 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) => {
config.alias ||= {}
config.alias['#app-manifest'] = join(tempDir, `meta/${buildId}.json`)
const rules = config.routeRules
for (const rule in rules) {
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`
const FORWARD_SLASH_RE = /\//g
if (!nuxt.options.ssr) {

View File

@ -548,6 +548,12 @@ async function initNuxt (nuxt: Nuxt) {
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) {
await installModule(key, options)
}

View File

@ -30,7 +30,7 @@ import { renderSSRHeadOptions } from '#internal/unhead.config.mjs'
import type { NuxtPayload, NuxtSSRContext } from '#app'
// @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
import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths'
@ -389,7 +389,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Setup head
const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext)
// 1.Extracted payload preloading
// 1. Preload payloads and app manifest
if (_PAYLOAD_EXTRACTION && !NO_SCRIPTS && !isRenderingIsland) {
head.push({
link: [
@ -399,7 +399,13 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
],
}, 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
if (inlinedStyles.length) {
head.push({ style: inlinedStyles })

View File

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

View File

@ -41,13 +41,19 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
// Filter disabled sources
// 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
const ctx = createUnimport({
injectAtEnd: true,
...options,
...rest,
addons: {
addons,
vueTemplate: options.autoImport,
...options.addons,
vueDirectives: options.autoImport === false ? undefined : true,
...addonsOptions,
},
presets,
})

View File

@ -455,6 +455,8 @@ export default defineNuxtModule({
addBuildPlugin(PageMetaPlugin({
dev: nuxt.options.dev,
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({
filename: 'routes.mjs',
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(), {
serverComponentRuntime,
clientComponentRuntime,
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 {
dev?: boolean
sourcemap?: boolean
isPage?: (file: string) => boolean
routesPath?: string
}
const HAS_MACRO_RE = /\bdefinePageMeta\s*\(\s*/
@ -22,6 +24,11 @@ const __nuxt_page_meta = null
export default __nuxt_page_meta
`
const CODE_DEV_EMPTY = `
const __nuxt_page_meta = {}
export default __nuxt_page_meta
`
const CODE_HMR = `
// Vite
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 (!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))
logger.error(`The file \`${pathname}\` is not a valid page as it has no content.`)
} 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()
@ -147,19 +154,23 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
})
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()
},
vite: {
handleHotUpdate: {
order: 'pre',
handler: ({ modules }) => {
// Remove macro file from modules list to prevent HMR overrides
const index = modules.findIndex(i => i.id?.includes('?macro=true'))
if (index !== -1) {
modules.splice(index, 1)
order: 'post',
handler: ({ file, modules, server }) => {
if (options.isPage?.(file)) {
const macroModule = server.moduleGraph.getModuleById(file + '?macro=true')
const routesModule = server.moduleGraph.getModuleById('virtual:nuxt:' + options.routesPath)
return [
...modules,
...macroModule ? [macroModule] : [],
...routesModule ? [routesModule] : [],
]
}
},
},

View File

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

View File

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

View File

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

View File

@ -428,5 +428,14 @@ export default defineUntypedSchema({
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.
*
* @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.
* @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 {
/** 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
/** 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
/**

View File

@ -1,7 +1,7 @@
import type { RouterHistory, RouterOptions as _RouterOptions } from 'vue-router'
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']>
hashMode?: boolean
scrollBehaviorType?: 'smooth' | 'auto'

View File

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

View File

@ -26,7 +26,7 @@
"devDependencies": {
"@nuxt/schema": "workspace:*",
"@types/clear": "0.1.4",
"rollup": "4.27.4",
"rollup": "4.28.0",
"unbuild": "3.0.0-rc.11",
"vue": "3.5.13"
},
@ -45,7 +45,7 @@
"externality": "^1.0.2",
"get-port-please": "^3.1.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "^2.4.0",
"jiti": "^2.4.1",
"knitwork": "^1.1.0",
"magic-string": "^0.30.14",
"mlly": "^1.7.3",
@ -56,9 +56,9 @@
"std-env": "^3.8.0",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^1.16.0",
"vite": "^6.0.1",
"vite-node": "^2.1.6",
"unplugin": "^2.0.0",
"vite": "^6.0.2",
"vite-node": "^2.1.8",
"vite-plugin-checker": "^0.8.0",
"vue-bundle-renderer": "^2.1.1"
},
@ -66,6 +66,6 @@
"vue": "^3.3.4"
},
"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',
'#internal/nuxt/paths',
'#internal/nuxt/app-config',
'#app-manifest',
'#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',
enforce: 'post',
configureServer (server) {
function invalidateVirtualModules () {
for (const [id, mod] of server.moduleGraph.idToModuleMap) {
if (id.startsWith('virtual:') || id.startsWith('\0virtual:')) {
server.middlewares.use('/__nuxt_vite_node__', toNodeListener(createViteNodeApp(ctx, invalidates)))
// 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)
}
}
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) => {
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) => {
// Invalidate virtual modules when templates are re-generated
ctx.nuxt.hook('app:templatesGenerated', () => {
for (const [id, mod] of server.moduleGraph.idToModuleMap) {
if (id.startsWith('virtual:') || id.startsWith('\0virtual:')) {
ctx.nuxt.hook('app:templatesGenerated', (_app, changedTemplates) => {
for (const template of changedTemplates) {
for (const mod of server.moduleGraph.getModulesByFile(`virtual:nuxt:${template.dst}`) || []) {
server.moduleGraph.invalidateModule(mod)
server.reloadModule(mod)
}
}
})

View File

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

View File

@ -8,5 +8,8 @@
},
"dependencies": {
"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": [
"nitro",
"h3",
"typescript",
"nuxt",
"nuxt3",
"@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.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) {
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 {
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')
@ -2316,7 +2316,7 @@ describe('component islands', () => {
const { components } = result
result.components = {}
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 || {})
@ -2327,12 +2327,11 @@ describe('component islands', () => {
"link": [],
"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": {},
}
`)
expect(teleportsEntries).toHaveLength(1)
expect(teleportsEntries[0]![0].startsWith('Counter-')).toBeTruthy()
expect(teleportsEntries[0]![1].props).toMatchInlineSnapshot(`
{
"multiplier": 1,

View File

@ -14,5 +14,8 @@
"vitest": "1.6.0",
"vue": "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>>()
})
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', () => {
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>>()

View File

@ -15,5 +15,8 @@
"ufo": "latest",
"unplugin": "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>
<template>
<pre id="hmr-id">
HMR ID: {{ hmrId }}
</pre>
<div>
HMR ID:
<span data-testid="hmr-id">{{ hmrId }}</span>
</div>
</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": {
"nuxt": "workspace:*"
},
"engines": {
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
}
}

View File

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

View File

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

View File

@ -9,5 +9,8 @@
},
"devDependencies": {
"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 { $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
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
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({
rootDir: fixturePath,
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', () => {
it('should work', async () => {
const { page, pageErrors, consoleLogs } = await renderPage('/')
expect(await page.title()).toBe('Basic fixture')
expect((await page.$('.sugar-counter').then(r => r!.textContent()))!.trim())
.toEqual('Sugar Counter 12 x 2 = 24 Inc')
expect(await page.title()).toBe('HMR fixture')
expect(await page.getByTestId('count').textContent()).toBe('1')
// reactive
await page.$('.sugar-counter button').then(r => r!.click())
expect((await page.$('.sugar-counter').then(r => r!.textContent()))!.trim())
.toEqual('Sugar Counter 13 x 2 = 26 Inc')
await page.getByRole('button').click()
expect(await page.getByTestId('count').textContent()).toBe('2')
// modify file
let indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8')
indexVue = indexVue
.replace('<Title>Basic fixture</Title>', '<Title>Basic fixture HMR</Title>')
.replace('<h1>Hello Nuxt 3!</h1>', '<h1>Hello Nuxt 3! HMR</h1>')
indexVue += '<style scoped>\nh1 { color: red }\n</style>'
await fsp.writeFile(join(fixturePath, 'pages/index.vue'), indexVue)
let newContents = indexVue
.replace('<Title>HMR fixture</Title>', '<Title>HMR fixture HMR</Title>')
.replace('<h1>Home page</h1>', '<h1>Home page - but not as you knew it</h1>')
newContents += '<style scoped>\nh1 { color: red }\n</style>'
await fsp.writeFile(join(fixturePath, 'pages/index.vue'), newContents)
await expectWithPolling(
() => page.title(),
'Basic fixture HMR',
)
await expectWithPolling(() => page.title(), 'HMR fixture HMR')
// content HMR
const h1 = await page.$('h1')
expect(await h1!.textContent()).toBe('Hello Nuxt 3! HMR')
const h1 = page.getByRole('heading')
expect(await h1!.textContent()).toBe('Home page - but not as you knew it')
// 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)"')
// ensure no errors
const consoleLogErrors = consoleLogs.filter(i => i.type === 'error')
const consoleLogWarnings = consoleLogs.filter(i => i.type === 'warn')
expectNoErrorsOrWarnings(consoleLogs)
expect(pageErrors).toEqual([])
expect(consoleLogErrors).toEqual([])
expect(consoleLogWarnings).toEqual([])
await page.close()
}, 60_000)
})
it('should detect new routes', async () => {
await expectWithPolling(
() => $fetch<string>('/catchall/some-404').then(r => r.includes('catchall at some-404')).catch(() => null),
true,
)
const res = await fetch('/some-404')
expect(res.status).toBe(404)
// write new page route
const indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8')
await fsp.writeFile(join(fixturePath, 'pages/catchall/some-404.vue'), indexVue)
await expectWithPolling(
() => $fetch<string>('/catchall/some-404').then(r => r.includes('Hello Nuxt 3')).catch(() => null),
true,
)
await fsp.writeFile(join(fixturePath, 'pages/some-404.vue'), indexVue)
await expectWithPolling(() => $fetch<string>('/some-404').then(r => r.includes('Home page')).catch(() => null), true)
})
it('should hot reload route rules', async () => {
await expectWithPolling(
() => fetch('/route-rules/inline').then(r => r.headers.get('x-extend') === 'added in routeRules').catch(() => null),
true,
)
await expectWithPolling(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null), 'added in routeRules')
// write new page route
const file = await fsp.readFile(join(fixturePath, 'pages/route-rules/inline.vue'), 'utf8')
await fsp.writeFile(join(fixturePath, 'pages/route-rules/inline.vue'), file.replace('added in routeRules', 'edited in dev'))
const file = await fsp.readFile(join(fixturePath, 'pages/route-rules.vue'), 'utf8')
await fsp.writeFile(join(fixturePath, 'pages/route-rules.vue'), file.replace('added in routeRules', 'edited in dev'))
await expectWithPolling(
() => fetch('/route-rules/inline').then(r => r.headers.get('x-extend') === 'edited in dev').catch(() => null),
true,
)
await expectWithPolling(() => fetch('/route-rules').then(r => r.headers.get('x-extend')).catch(() => null), 'edited in dev')
})
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 triggerHmr = async () => fsp.writeFile(
componentPath,
(await fsp.readFile(componentPath, 'utf8'))
.replace(`ref(${hmrId++})`, `ref(${hmrId})`),
)
const componentContents = await fsp.readFile(componentPath, 'utf8')
const triggerHmr = (number: string) => fsp.writeFile(componentPath, componentContents.replace('ref(0)', `ref(${number})`))
// initial state
await expectWithPolling(
resolveHmrId,
0,
)
await expectWithPolling(async () => await page.getByTestId('hmr-id').innerText(), '0')
// first edit
await triggerHmr()
await expectWithPolling(
resolveHmrId,
1,
)
await triggerHmr('1')
await expectWithPolling(async () => await page.getByTestId('hmr-id').innerText(), '1')
// just in-case
await triggerHmr()
await expectWithPolling(
resolveHmrId,
2,
)
await triggerHmr('2')
await expectWithPolling(async () => await page.getByTestId('hmr-id').innerText(), '2')
// ensure no errors
const consoleLogErrors = consoleLogs.filter(i => i.type === 'error')
const consoleLogWarnings = consoleLogs.filter(i => i.type === 'warn')
expectNoErrorsOrWarnings(consoleLogs)
expect(pageErrors).toEqual([])
expect(consoleLogErrors).toEqual([])
expect(consoleLogWarnings).toEqual([])
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 {
describe.skip('hmr', () => {})

View File

@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest'
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 { flushPromises, mount } from '@vue/test-utils'
import { createClientOnly } from '../../packages/nuxt/src/app/components/client-only'
import { createClientPage } from '../../packages/nuxt/dist/components/runtime/client-component'
const Client = defineComponent({
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))!
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 consoleLogWarnings = consoleLogs.filter(i => i.type === 'warning')
expect(pageErrors).toEqual([])
expect(consoleLogErrors).toEqual([])
expect(consoleLogWarnings).toEqual([])
await page.close()
}
export async function gotoPath (page: Page, path: string) {