mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-22 11:22:43 +00:00
Merge remote-tracking branch 'origin/main' into fix/21721-spa-loading
This commit is contained in:
commit
29f0663f05
@ -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 && \
|
||||
|
3
.github/workflows/autofix.yml
vendored
3
.github/workflows/autofix.yml
vendored
@ -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
|
||||
|
||||
|
@ -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
|
@ -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**.
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
::
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -21,7 +21,7 @@ We chose to build Nuxt on top of Vue for these reasons:
|
||||
|
||||
### Single File Components
|
||||
|
||||
[Vue’s 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.
|
||||
[Vue’s 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
|
||||
|
||||
|
@ -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`.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
24
package.json
24
package.json
@ -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": ""
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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>
|
||||
|
2
packages/nuxt/app.d.ts
vendored
2
packages/nuxt/app.d.ts
vendored
@ -1 +1 @@
|
||||
export * from './dist/app/index.js'
|
||||
export * from './dist/app/index'
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useId as _useId } from 'vue'
|
||||
|
||||
/** @deprecated Use `useId` from `vue` */
|
||||
export const useId = _useId
|
||||
|
@ -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 }),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
|
@ -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
|
||||
}
|
2
packages/nuxt/src/app/types/augments.d.ts
vendored
2
packages/nuxt/src/app/types/augments.d.ts
vendored
@ -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 {
|
||||
|
@ -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>')
|
||||
})
|
||||
|
@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 })
|
||||
|
@ -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'`,
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
|
@ -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] : [],
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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>
|
||||
"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -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'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -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 }}
|
||||
*/
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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'
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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)))),
|
||||
],
|
||||
|
@ -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()
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -8,5 +8,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
|
||||
}
|
||||
}
|
||||
|
2162
pnpm-lock.yaml
2162
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -28,11 +28,22 @@
|
||||
"ignoreDeps": [
|
||||
"nitro",
|
||||
"h3",
|
||||
"typescript",
|
||||
"nuxt",
|
||||
"nuxt3",
|
||||
"@nuxt/kit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "typescript",
|
||||
"matchPackageNames": [
|
||||
"typescript"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "unimport",
|
||||
"matchPackageNames": [
|
||||
"unimport"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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,
|
||||
|
3
test/fixtures/basic-types/package.json
vendored
3
test/fixtures/basic-types/package.json
vendored
@ -14,5 +14,8 @@
|
||||
"vitest": "1.6.0",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
|
||||
}
|
||||
}
|
||||
|
17
test/fixtures/basic-types/types.ts
vendored
17
test/fixtures/basic-types/types.ts
vendored
@ -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>>()
|
||||
|
3
test/fixtures/basic/package.json
vendored
3
test/fixtures/basic/package.json
vendored
@ -15,5 +15,8 @@
|
||||
"ufo": "latest",
|
||||
"unplugin": "latest",
|
||||
"vue": "latest"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -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
10
test/fixtures/hmr/nuxt.config.ts
vendored
Normal 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
13
test/fixtures/hmr/package.json
vendored
Normal 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
21
test/fixtures/hmr/pages/index.vue
vendored
Normal 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
11
test/fixtures/hmr/pages/page-meta.vue
vendored
Normal 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
13
test/fixtures/hmr/pages/route-rules.vue
vendored
Normal 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>
|
7
test/fixtures/hmr/pages/routes/index.vue
vendored
Normal file
7
test/fixtures/hmr/pages/routes/index.vue
vendored
Normal 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
3
test/fixtures/hmr/tsconfig.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
3
test/fixtures/minimal-types/package.json
vendored
3
test/fixtures/minimal-types/package.json
vendored
@ -7,5 +7,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
|
||||
}
|
||||
}
|
||||
|
3
test/fixtures/minimal/package.json
vendored
3
test/fixtures/minimal/package.json
vendored
@ -6,5 +6,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
|
||||
}
|
||||
}
|
||||
|
3
test/fixtures/runtime-compiler/package.json
vendored
3
test/fixtures/runtime-compiler/package.json
vendored
@ -6,5 +6,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "workspace:*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
|
||||
}
|
||||
}
|
||||
|
3
test/fixtures/suspense/package.json
vendored
3
test/fixtures/suspense/package.json
vendored
@ -9,5 +9,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "latest"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
|
||||
}
|
||||
}
|
||||
|
172
test/hmr.test.ts
172
test/hmr.test.ts
@ -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', () => {})
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user