Merge remote-tracking branch 'origin/main' into feat/oxc

This commit is contained in:
Daniel Roe 2024-12-09 15:40:54 +00:00
commit 84680265f2
No known key found for this signature in database
GPG Key ID: 3714AB03996F442B
111 changed files with 2156 additions and 1495 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

@ -56,6 +56,9 @@ jobs:
- name: Build
run: pnpm build
- name: Check types
run: pnpm test:attw
- name: Cache dist
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:

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

@ -73,7 +73,8 @@ export default defineNuxtConfig({
// resetAsyncDataToUndefined: true,
// templateUtils: true,
// relativeWatchPaths: true,
// normalizeComponentNames: false
// normalizeComponentNames: false,
// spaLoadingTemplateLocation: 'within',
// defaults: {
// useAsyncData: {
// deep: true
@ -237,6 +238,45 @@ export default defineNuxtConfig({
})
```
#### New DOM Location for SPA Loading Screen
🚦 **Impact Level**: Minimal
##### What Changed
When rendering a client-only page (with `ssr: false`), we optionally render a loading screen (from `app/spa-loading-template.html`), within the Nuxt app root:
```html
<div id="__nuxt">
<!-- spa loading template -->
</div>
```
Now, we default to rendering the template alongside the Nuxt app root:
```html
<div id="__nuxt"></div>
<!-- spa loading template -->
```
##### Reasons for Change
This allows the spa loading template to remain in the DOM until the Vue app suspense resolves, preventing a flash of white.
##### Migration Steps
If you were targeting the spa loading template with CSS or `document.queryElement` you will need to update your selectors. For this purpose you can use the new `app.spaLoaderTag` and `app.spaLoaderAttrs` configuration options.
Alternatively, you can revert to the previous behaviour with:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
experimental: {
spaLoadingTemplateLocation: 'within',
}
})
```
#### Scan Page Meta After Resolution
🚦 **Impact Level**: Minimal

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,8 +162,8 @@ 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)
[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)

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

@ -451,3 +451,24 @@ In this case, the component name would be `MyComponent`, as far as Vue is concer
But in order to auto-import it, you would need to use `SomeFolderMyComponent`.
By setting `experimental.normalizeComponentNames`, these two values match, and Vue will generate a component name that matches the Nuxt pattern for component naming.
## spaLoadingTemplateLocation
When rendering a client-only page (with `ssr: false`), we optionally render a loading screen (from `app/spa-loading-template.html`).
It can be set to `within`, which will render it like this:
```html
<div id="__nuxt">
<!-- spa loading template -->
</div>
```
Alternatively, you can render the template alongside the Nuxt app root by setting it to `body`:
```html
<div id="__nuxt"></div>
<!-- spa loading template -->
```
This avoids a white flash when hydrating a client-only page.

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

@ -38,7 +38,7 @@ Read more on `unhead` documentation.
useHeadSafe(input: MaybeComputedRef<HeadSafe>): void
```
The whitelist of safe values is:
The list of allowed values is:
```ts
export default {

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

@ -30,6 +30,7 @@
"test:runtime": "vitest -c vitest.nuxt.config.ts",
"test:types": "pnpm --filter './test/fixtures/**' test:types",
"test:unit": "vitest run packages/",
"test:attw": "pnpm --filter './packages/**' test:attw",
"typecheck": "tsc --noEmit",
"typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs --languages html"
},
@ -50,33 +51,35 @@
"@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": {
"@arethetypeswrong/cli": "0.17.1",
"@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 +88,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 +113,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": ""
}

3
packages/kit/.attw.json Normal file
View File

@ -0,0 +1,3 @@
{
"ignoreRules": ["cjs-resolves-to-esm"]
}

View File

@ -23,7 +23,8 @@
"dist"
],
"scripts": {
"prepack": "unbuild"
"prepack": "unbuild",
"test:attw": "attw --pack"
},
"dependencies": {
"@nuxt/schema": "workspace:*",
@ -34,7 +35,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 +49,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>

3
packages/nuxt/.attw.json Normal file
View File

@ -0,0 +1,3 @@
{
"ignoreRules": ["cjs-resolves-to-esm", "false-esm"]
}

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": [
@ -56,11 +64,12 @@
"schema.*"
],
"scripts": {
"prepack": "unbuild"
"prepack": "unbuild",
"test:attw": "attw --pack"
},
"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",
@ -86,7 +95,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",
@ -94,7 +103,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",
"oxc-parser": "^0.38.0",
@ -115,7 +124,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",
@ -130,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": {
@ -146,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 { defineComponent, h } from 'vue'
import type { Politeness } from '#app/composables/route-announcer'
import type { Politeness } from 'nuxt/app'
import { useRouteAnnouncer } from '#app/composables/route-announcer'
export default defineComponent({

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

@ -38,4 +38,5 @@ export { useRequestURL } from './url'
export { usePreviewMode } from './preview'
export { useId } from './id'
export { useRouteAnnouncer } from './route-announcer'
export type { Politeness } from './route-announcer'
export { useRuntimeHook } from './runtime-hook'

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`')
}
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
}
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

@ -17,7 +17,7 @@ import plugins from '#build/plugins'
// @ts-expect-error virtual file
import RootComponent from '#build/root-component.mjs'
// @ts-expect-error virtual file
import { appId, multiApp, vueAppRootContainer } from '#build/nuxt.config.mjs'
import { appId, appSpaLoaderAttrs, multiApp, spaLoadingTemplateOutside, vueAppRootContainer } from '#build/nuxt.config.mjs'
let entry: (ssrContext?: CreateOptions['ssrContext']) => Promise<App<Element>>
@ -72,6 +72,13 @@ if (import.meta.client) {
if (vueApp.config.errorHandler === handleVueError) { vueApp.config.errorHandler = undefined }
})
if (spaLoadingTemplateOutside && !isSSR && appSpaLoaderAttrs.id) {
// Remove spa loader if present
nuxt.hook('app:suspense:resolve', () => {
document.getElementById(appSpaLoaderAttrs.id)?.remove()
})
}
try {
await applyPlugins(nuxt, plugins)
} catch (err) {

View File

@ -2,7 +2,7 @@ export { applyPlugin, applyPlugins, callWithNuxt, createNuxtApp, defineAppConfig
export type { CreateOptions, NuxtApp, NuxtPayload, NuxtPluginIndicator, NuxtSSRContext, ObjectPlugin, Plugin, PluginEnvContext, PluginMeta, ResolvedPluginMeta, RuntimeNuxtHooks } from './nuxt'
export { defineNuxtComponent, useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData, useHydration, callOnce, useState, clearNuxtState, clearError, createError, isNuxtError, showError, useError, useFetch, useLazyFetch, useCookie, refreshCookie, onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader, onNuxtReady, abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter, preloadComponents, prefetchComponents, preloadRouteComponents, isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver, getAppManifest, getRouteRules, reloadNuxtApp, useRequestURL, usePreviewMode, useId, useRouteAnnouncer, useHead, useSeoMeta, useServerSeoMeta, useRuntimeHook } from './composables/index'
export type { AddRouteMiddlewareOptions, AsyncData, AsyncDataOptions, AsyncDataRequestStatus, CookieOptions, CookieRef, FetchResult, NuxtAppManifest, NuxtAppManifestMeta, NuxtError, ReloadNuxtAppOptions, RouteMiddleware, UseFetchOptions } from './composables/index'
export type { AddRouteMiddlewareOptions, AsyncData, AsyncDataOptions, AsyncDataRequestStatus, CookieOptions, CookieRef, FetchResult, NuxtAppManifest, NuxtAppManifestMeta, NuxtError, Politeness, ReloadNuxtAppOptions, RouteMiddleware, UseFetchOptions } from './composables/index'
export { defineNuxtLink } from './components/index'
export type { NuxtLinkOptions, NuxtLinkProps } from './components/index'

View File

@ -11,6 +11,8 @@ import type { RenderResponse } from 'nitro/types'
import type { LogObject } from 'consola'
import type { MergeHead, VueHeadClient } from '@unhead/vue'
import type { NuxtAppLiterals } from 'nuxt/app'
import type { NuxtIslandContext } from '../app/types'
import type { RouteMiddleware } from '../app/composables/router'
import type { NuxtError } from '../app/composables/error'
@ -22,8 +24,6 @@ import type { RouteAnnouncer } from '../app/composables/route-announcer'
// @ts-expect-error virtual file
import { appId, chunkErrorEvent, multiApp } from '#build/nuxt.config.mjs'
import type { NuxtAppLiterals } from '#app'
function getNuxtAppCtx (id = appId || 'nuxt-app') {
return getContext<NuxtApp>(id, {
asyncContext: !!__NUXT_ASYNC_CONTEXT__ && import.meta.server,
@ -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

@ -28,7 +28,7 @@ export type getComponentsT = (mode?: 'client' | 'server' | 'all') => Component[]
export default defineNuxtModule<ComponentsOptions>({
meta: {
name: 'components',
name: 'nuxt:components',
configKey: 'components',
},
defaults: {

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 }) {
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

@ -4,12 +4,12 @@ import { defu } from 'defu'
import { findPath, logger, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath } from '@nuxt/kit'
import type { Nuxt, NuxtApp, NuxtPlugin, NuxtTemplate, ResolvedNuxtTemplate } from 'nuxt/schema'
import type { PluginMeta } from 'nuxt/app'
import * as defaultTemplates from './templates'
import { getNameFromPath, hasSuffix, uniqueBy } from './utils'
import { extractMetadata, orderMap } from './plugins/plugin-metadata'
import type { PluginMeta } from '#app'
export function createApp (nuxt: Nuxt, options: Partial<NuxtApp> = {}): NuxtApp {
return defu(options, {
dir: nuxt.options.srcDir,
@ -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

@ -6,9 +6,10 @@ import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string'
import { normalize } from 'pathe'
import { logger } from '@nuxt/kit'
import { parseAndWalk } from 'oxc-walker'
import type { ObjectPlugin, PluginMeta } from '#app'
import type { ObjectPlugin, PluginMeta } from 'nuxt/app'
import { parseAndWalk } from 'oxc-walker'
const internalOrderMap = {
// -50: pre-all (nuxt)

View File

@ -3,7 +3,7 @@ import type { NitroErrorHandler } from 'nitro/types'
import type { H3Error, H3Event } from 'h3'
import { getRequestHeader, getRequestHeaders, send, setResponseHeader, setResponseStatus } from 'h3'
import { useNitroApp, useRuntimeConfig } from 'nitro/runtime'
import type { NuxtPayload } from '#app'
import type { NuxtPayload } from 'nuxt/app'
export default <NitroErrorHandler> async function errorhandler (error: H3Error, event) {
// Parse and normalize error

View File

@ -22,15 +22,15 @@ import type { Link, Script, Style } from '@unhead/vue'
import { createServerHead, resolveUnrefHeadInput } from '@unhead/vue'
import { defineRenderHandler, getRouteRules, useNitroApp, useRuntimeConfig, useStorage } from 'nitro/runtime'
import type { NuxtPayload, NuxtSSRContext } from 'nuxt/app'
// @ts-expect-error virtual file
import unheadPlugins from '#internal/unhead-plugins.mjs'
// @ts-expect-error virtual file
import { renderSSRHeadOptions } from '#internal/unhead.config.mjs'
import type { NuxtPayload, NuxtSSRContext } from '#app'
// @ts-expect-error virtual file
import { appHead, appId, appRootAttrs, appRootTag, appTeleportAttrs, appTeleportTag, componentIslands, multiApp } 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'
@ -144,7 +144,17 @@ const getSPARenderer = lazyCachedFunction(async () => {
// @ts-expect-error virtual file
const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '')
.then(r => APP_ROOT_OPEN_TAG + r + APP_ROOT_CLOSE_TAG)
.then((r) => {
if (spaLoadingTemplateOutside) {
const APP_SPA_LOADER_OPEN_TAG = `<${appSpaLoaderTag}${propsToString(appSpaLoaderAttrs)}>`
const APP_SPA_LOADER_CLOSE_TAG = `</${appSpaLoaderTag}>`
const appTemplate = APP_ROOT_OPEN_TAG + APP_ROOT_CLOSE_TAG
const loaderTemplate = r ? APP_SPA_LOADER_OPEN_TAG + r + APP_SPA_LOADER_CLOSE_TAG : ''
return appTemplate + loaderTemplate
} else {
return APP_ROOT_OPEN_TAG + r + APP_ROOT_CLOSE_TAG
}
})
const options = {
manifest,
@ -379,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: [
@ -389,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

@ -16,7 +16,7 @@ import { createJiti } from 'jiti'
export default defineNuxtModule({
meta: {
name: 'nuxt-config-schema',
name: 'nuxt:nuxt-config-schema',
},
async setup (_, nuxt) {
const resolver = createResolver(import.meta.url)

View File

@ -177,7 +177,6 @@ export { }
},
}
const adHocModules = ['router', 'pages', 'imports', 'meta', 'components', 'nuxt-config-schema']
const IMPORT_NAME_RE = /\.\w+$/
const GIT_RE = /^git\+/
export const schemaTemplate: NuxtTemplate = {
@ -187,7 +186,7 @@ export const schemaTemplate: NuxtTemplate = {
const getImportName = (name: string) => (name[0] === '.' ? './' + join(relativeRoot, name) : name).replace(IMPORT_NAME_RE, '')
const modules = nuxt.options._installedModules
.filter(m => m.meta && m.meta.configKey && m.meta.name && !adHocModules.includes(m.meta.name))
.filter(m => m.meta && m.meta.configKey && m.meta.name && !m.meta.name.startsWith('nuxt:') && m.meta.name !== 'nuxt-config-schema')
.map(m => [genString(m.meta.configKey), getImportName(m.entryPath || m.meta.name), m] as const)
const privateRuntimeConfig = Object.create(null)
@ -285,7 +284,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'`,
@ -525,6 +524,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`,
`export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`,
`export const crawlLinks = ${!!((ctx.nuxt as any)._nitro as Nitro).options.prerender.crawlLinks}`,
`export const spaLoadingTemplateOutside = ${ctx.nuxt.options.experimental.spaLoadingTemplateLocation === 'body'}`,
].join('\n\n')
},
}

View File

@ -7,7 +7,7 @@ const components = ['NoScript', 'Link', 'Base', 'Title', 'Meta', 'Style', 'Head'
export default defineNuxtModule<NuxtOptions['unhead']>({
meta: {
name: 'meta',
name: 'nuxt:meta',
configKey: 'unhead',
},
async setup (options, nuxt) {

View File

@ -13,7 +13,7 @@ import { defaultPresets } from './presets'
export default defineNuxtModule<Partial<ImportsOptions>>({
meta: {
name: 'imports',
name: 'nuxt:imports',
configKey: 'imports',
},
defaults: nuxt => ({
@ -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

@ -23,7 +23,8 @@ const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/
export default defineNuxtModule({
meta: {
name: 'pages',
name: 'nuxt:pages',
configKey: 'pages',
},
async setup (_options, nuxt) {
const useExperimentalTypedPages = nuxt.options.experimental.typedPages
@ -455,6 +456,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 +502,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 +613,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

@ -11,6 +11,8 @@ import { parseAndWalk, walk } from 'oxc-walker'
interface PageMetaPluginOptions {
dev?: boolean
sourcemap?: boolean
isPage?: (file: string) => boolean
routesPath?: string
}
const HAS_MACRO_RE = /\bdefinePageMeta\s*\(\s*/
@ -20,6 +22,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) {
@ -87,11 +94,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()
@ -145,19 +152,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

@ -3,8 +3,8 @@ import { getCurrentInstance } from 'vue'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router'
import { useRoute } from 'vue-router'
import type { NitroRouteConfig } from 'nitro/types'
import type { NuxtError } from 'nuxt/app'
import { useNuxtApp } from '#app/nuxt'
import type { NuxtError } from '#app'
export interface PageMeta {
[key: string]: unknown

View File

@ -5,10 +5,11 @@ import { START_LOCATION, createMemoryHistory, createRouter, createWebHashHistory
import { createError } from 'h3'
import { isEqual, withoutBase } from 'ufo'
import type { Plugin, RouteMiddleware } from 'nuxt/app'
import type { PageMeta } from '../composables'
import { toArray } from '../utils'
import type { Plugin, RouteMiddleware } from '#app'
import { getRouteRules } from '#app/composables/manifest'
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
import { clearError, showError, useError } from '#app/composables/error'
@ -17,7 +18,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 +88,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

@ -0,0 +1,3 @@
{
"ignoreRules": ["cjs-resolves-to-esm"]
}

View File

@ -26,12 +26,13 @@
"builder.mjs"
],
"scripts": {
"prepack": "unbuild"
"prepack": "unbuild",
"test:attw": "attw --pack"
},
"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 +44,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 +62,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 +77,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 +85,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

@ -0,0 +1,5 @@
{
"ignoreRules": [
"cjs-resolves-to-esm"
]
}

View File

@ -22,7 +22,7 @@ export default defineBuildConfig({
],
externals: [
// Type imports
'#app/components/nuxt-link',
'nuxt/app',
'cssnano',
'autoprefixer',
'ofetch',

View File

@ -1 +1 @@
export * from './dist/env'
export * from './dist/builder-env'

View File

@ -28,10 +28,12 @@
"files": [
"dist",
"schema",
"builder-env.d.ts",
"env.d.ts"
],
"scripts": {
"prepack": "unbuild"
"prepack": "unbuild",
"test:attw": "attw --pack"
},
"devDependencies": {
"@types/file-loader": "5.0.4",
@ -50,7 +52,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 +76,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

@ -235,7 +235,7 @@ export default defineUntypedSchema({
},
/**
* Customize Nuxt root element tag.
* Customize Nuxt Teleport element tag.
*/
teleportTag: {
$resolve: val => val || 'div',
@ -262,6 +262,21 @@ export default defineUntypedSchema({
})
},
},
/**
* Customize Nuxt SpaLoader element tag.
*/
spaLoaderTag: {
$resolve: val => val || 'div',
},
/**
* Customize Nuxt Nuxt SpaLoader element attributes.
* @type {typeof import('@unhead/schema').HtmlAttributes}
*/
spaLoaderAttrs: {
id: '__nuxt-loader',
},
},
/**

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

@ -360,7 +360,7 @@ export default defineUntypedSchema({
* `app/` directory.
*/
defaults: {
/** @type {typeof import('#app/components/nuxt-link')['NuxtLinkOptions']} */
/** @type {typeof import('nuxt/app')['NuxtLinkOptions']} */
nuxtLink: {
componentName: 'NuxtLink',
prefetch: true,
@ -417,5 +417,25 @@ export default defineUntypedSchema({
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
},
},
/**
* Keep showing the spa-loading-template until suspense:resolve
* @see [Nuxt Issues #24770](https://github.com/nuxt/nuxt/issues/21721)
* @type {'body' | 'within'}
*/
spaLoadingTemplateLocation: {
$resolve: async (val, get) => {
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"
}
}

3
packages/vite/.attw.json Normal file
View File

@ -0,0 +1,3 @@
{
"ignoreRules": ["cjs-resolves-to-esm"]
}

View File

@ -21,12 +21,13 @@
"dist"
],
"scripts": {
"prepack": "unbuild"
"prepack": "unbuild",
"test:attw": "attw --pack"
},
"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 +46,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 +57,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 +67,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

@ -0,0 +1,3 @@
{
"ignoreRules": ["cjs-resolves-to-esm"]
}

View File

@ -26,7 +26,8 @@
"builder.mjs"
],
"scripts": {
"prepack": "unbuild"
"prepack": "unbuild",
"test:attw": "attw --pack"
},
"dependencies": {
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
@ -42,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",
"mini-css-extract-plugin": "^2.9.2",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
@ -61,7 +62,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 +74,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 +87,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

@ -37,7 +37,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"208k"`)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"209k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1396k"`)
@ -78,7 +78,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output-inline/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"559k"`)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"560k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"94.4k"`)

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"
}
}

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