Merge branch 'main' into patch-21

This commit is contained in:
Michael Brevard 2024-06-11 19:20:31 +03:00 committed by GitHub
commit 6e6832fc3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 901 additions and 417 deletions

View File

@ -71,62 +71,7 @@ There are two ways to deploy a Nuxt application to any static hosting services:
- Static site generation (SSG) with `ssr: true` pre-renders routes of your application at build time. (This is the default behavior when running `nuxi generate`.) It will also generate `/200.html` and `/404.html` single-page app fallback pages, which can render dynamic routes or 404 errors on the client (though you may need to configure this on your static host).
- Alternatively, you can prerender your site with `ssr: false` (static single-page app). This will produce HTML pages with an empty `<div id="__nuxt"></div>` where your Vue app would normally be rendered. You will lose many SEO benefits of prerendering your site, so it is suggested instead to use [`<ClientOnly>`](/docs/api/components/client-only) to wrap the portions of your site that cannot be server rendered (if any).
### Crawl-based Pre-rendering
Use the [`nuxi generate` command](/docs/api/commands/generate) to build and pre-render your application using the [Nitro](/docs/guide/concepts/server-engine) crawler. This command is similar to `nuxt build` with the `nitro.static` option set to `true`, or running `nuxt build --prerender`.
```bash [Terminal]
npx nuxi generate
```
That's it! You can now deploy the `.output/public` directory to any static hosting service or preview it locally with `npx serve .output/public`.
Working of the Nitro crawler:
1. Load the HTML of your application's root route (`/`), any non-dynamic pages in your `~/pages` directory, and any other routes in the `nitro.prerender.routes` array.
2. Save the HTML and `payload.json` to the `~/.output/public/` directory to be served statically.
3. Find all anchor tags (`<a href="...">`) in the HTML to navigate to other routes.
4. Repeat steps 1-3 for each anchor tag found until there are no more anchor tags to crawl.
This is important to understand since pages that are not linked to a discoverable page can't be pre-rendered automatically.
::read-more{to="/docs/api/commands/generate#nuxi-generate"}
Read more about the `nuxi generate` command.
::
### Selective Pre-rendering
You can manually specify routes that [Nitro](/docs/guide/concepts/server-engine) will fetch and pre-render during the build or ignore routes that you don't want to pre-render like `/dynamic` in the `nuxt.config` file:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/user/1', '/user/2'],
ignore: ['/dynamic']
}
}
})
```
You can combine this with the `crawlLinks` option to pre-render a set of routes that the crawler can't discover like your `/sitemap.xml` or `/robots.txt`:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: true,
routes: ['/sitemap.xml', '/robots.txt']
}
}
})
```
Setting `nitro.prerender` to `true` is similar to `nitro.prerender.crawlLinks` to `true`.
::read-more{to="https://nitro.unjs.io/config#prerender"}
Read more about pre-rendering in the Nitro documentation.
::
:read-more{title="Nuxt prerendering" to="/docs/getting-started/prerendering"}
### Client-side Only Rendering

View File

@ -23,7 +23,7 @@ To use the latest Nuxt build and test features before their release, read about
Nuxt 4 is planned to be released **on or before June 14** (though obviously this is dependent on having enough time after Nitro's major release to be properly tested in the community, so be aware that this is not an exact date).
Until then, it is possible to test many of Nuxt 4's breaking changes on the nightly release channel.
Until then, it is possible to test many of Nuxt 4's breaking changes from Nuxt version 3.12 or via the nightly release channel.
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=r4wFKlcJK6c" target="_blank"}
Watch a video from Alexander Lichter showing how to opt in to Nuxt 4's breaking changes already.
@ -223,6 +223,46 @@ export default defineNuxtConfig({
Please report an issue if you are doing this, as we do not plan to keep this as configurable.
#### Removal of deprecated `boolean` values for `dedupe` option when calling `refresh` in `useAsyncData` and `useFetch`
🚦 **Impact Level**: Minimal
##### What Changed
Previously it was possible to pass `dedupe: boolean` to `refresh`. These were aliases of `cancel` (`true`) and `defer` (`false`).
```ts twoslash [app.vue]
const { refresh } = await useAsyncData(async () => ({ message: 'Hello, Nuxt 3!' }))
async function refreshData () {
await refresh({ dedupe: true })
}
```
##### Reasons for Change
These aliases were removed, for greater clarity.
The issue came up when adding `dedupe` as an option to `useAsyncData`, and we removed the boolean values as they ended up being _opposites_.
`refresh({ dedupe: false })` meant 'do not _cancel_ existing requests in favour of this new one'. But passing `dedupe: true` within the options of `useAsyncData` means 'do not make any new requests if there is an existing pending request.' (See [PR](https://github.com/nuxt/nuxt/pull/24564#pullrequestreview-1764584361).)
##### Migration Steps
The migration should be straightforward:
```diff
const { refresh } = await useAsyncData(async () => ({ message: 'Hello, Nuxt 3!' }))
async function refreshData () {
- await refresh({ dedupe: true })
+ await refresh({ dedupe: 'cancel' })
- await refresh({ dedupe: false })
+ await refresh({ dedupe: 'defer' })
}
```
#### Respect defaults when clearing `data` in `useAsyncData` and `useFetch`
🚦 **Impact Level**: Minimal

View File

@ -0,0 +1,177 @@
---
title: "Prerendering"
description: Nuxt allows pages to be statically rendered at build time to improve certain performance or SEO metrics
navigation.icon: i-ph-code-block-duotone
---
Nuxt allows for select pages from your application to be rendered at build time. Nuxt will serve the prebuilt pages when requested instead of generating them on the fly.
:read-more{title="Nuxt rendering modes" to="/docs/guide/concepts/rendering"}
## Crawl-based Pre-rendering
Use the [`nuxi generate` command](/docs/api/commands/generate) to build and pre-render your application using the [Nitro](/docs/guide/concepts/server-engine) crawler. This command is similar to `nuxt build` with the `nitro.static` option set to `true`, or running `nuxt build --prerender`.
This will build your site, stand up a nuxt instance, and, by default, prerender the root page `/` along with any of your site's pages it links to, any of your site's pages they link to, and so on.
```bash [Terminal]
npx nuxi generate
```
You can now deploy the `.output/public` directory to any static hosting service or preview it locally with `npx serve .output/public`.
Working of the Nitro crawler:
1. Load the HTML of your application's root route (`/`), any non-dynamic pages in your `~/pages` directory, and any other routes in the `nitro.prerender.routes` array.
2. Save the HTML and `payload.json` to the `~/.output/public/` directory to be served statically.
3. Find all anchor tags (`<a href="...">`) in the HTML to navigate to other routes.
4. Repeat steps 1-3 for each anchor tag found until there are no more anchor tags to crawl.
This is important to understand since pages that are not linked to a discoverable page can't be pre-rendered automatically.
::read-more{to="/docs/api/commands/generate#nuxi-generate"}
Read more about the `nuxi generate` command.
::
### Selective Pre-rendering
You can manually specify routes that [Nitro](/docs/guide/concepts/server-engine) will fetch and pre-render during the build or ignore routes that you don't want to pre-render like `/dynamic` in the `nuxt.config` file:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ["/user/1", "/user/2"],
ignore: ["/dynamic"],
},
},
});
```
You can combine this with the `crawlLinks` option to pre-render a set of routes that the crawler can't discover like your `/sitemap.xml` or `/robots.txt`:
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: true,
routes: ["/sitemap.xml", "/robots.txt"],
},
},
});
```
Setting `nitro.prerender` to `true` is similar to `nitro.prerender.crawlLinks` to `true`.
::read-more{to="https://nitro.unjs.io/config#prerender"}
Read more about pre-rendering in the Nitro documentation.
::
Lastly, you can manually configure this using routeRules.
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
routeRules: {
// Set prerender to true to configure it to be prerendered
"/rss.xml": { prerender: true },
// Set it to false to configure it to be skipped for prerendering
"/this-DOES-NOT-get-prerendered": { prerender: false },
// Everything under /blog gets prerendered as long as it
// is linked to from another page
"/blog/**": { prerender: true },
},
});
```
::read-more{to="https://nitro.unjs.io/config/#routerules"}
Read more about Nitro's `routeRules` configuration.
::
As a shorthand, you can also configure this in a page file using [`defineRouteRules`](/docs/api/utils/define-route-rules).
::read-more{to="/docs/guide/going-further/experimental-features#inlinerouterules" icon="i-ph-star-duotone"}
This feature is experimental and in order to use it you must enable the `experimental.inlineRouteRules` option in your `nuxt.config`.
::
```vue [pages/index.vue]
<script setup>
// Or set at the page level
defineRouteRules({
prerender: true,
});
</script>
<template>
<div>
<h1>Homepage</h1>
<p>Pre-rendered at build time</p>
</div>
</template>
```
This will be translated to:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
routeRules: {
"/": { prerender: true },
},
});
```
## Runtime prerender configuration
### `prerenderRoutes`
You can use this at runtime within a [Nuxt context](/docs/guide/going-further/nuxt-app#the-nuxt-context) to add more routes for Nitro to prerender.
```vue [pages/index.vue]
<script setup>
prerenderRoutes(["/some/other/url"]);
</script>
<template>
<div>
<h1>This will register other routes for prerendering when prerendered</h1>
</div>
</template>
```
:read-more{title="prerenderRoutes" to="/docs/api/utils/prerender-routes"}
### `prerender:routes` Nuxt hook
This is called before prerendering for additional routes to be registered.
```ts [nitro.config.ts]
export default defineNuxtConfig({
hooks: {
async "prerender:routes"(ctx) {
const { pages } = await fetch("https://api.some-cms.com/pages").then(
(res) => res.json(),
);
for (const page of pages) {
ctx.routes.add(`/${page.name}`);
}
},
},
});
```
### `prerender:generate` Nitro hook
This is called for each route during prerendering. You can use this for fine grained handling of each route that gets prerendered.
```ts [nitro.config.ts]
export default defineNuxtConfig({
nitro: {
hooks: {
"prerender:generate"(route) {
if (route.route?.includes("private")) {
route.skip = true;
}
},
},
},
});
```

View File

@ -81,8 +81,9 @@ Watch a video from Alexander Lichter about the Object Syntax for Nuxt plugins.
::
::note
If you are using the object-syntax, the properties may be statically analyzed in future to produce a more optimized build. So you should not define them at runtime. :br
If you are using the object-syntax, the properties are statically analyzed to produce a more optimized build. So you should not define them at runtime. :br
For example, setting `enforce: import.meta.server ? 'pre' : 'post'` would defeat any future optimization Nuxt is able to do for your plugins.
Nuxt does statically pre-load any hook listeners when using object-syntax, allowing you to define hooks without needing to worry about order of plugin registration.
::
## Registration Order

View File

@ -386,7 +386,7 @@ This option allows exposing some route metadata defined in `definePageMeta` at b
This only works with static or strings/arrays rather than variables or conditional assignment. See [original issue](https://github.com/nuxt/nuxt/issues/24770) for more information and context.
<!-- You can disable this feature if it causes issues in your project.
You can disable this feature if it causes issues in your project.
```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({
@ -394,7 +394,7 @@ export default defineNuxtConfig({
scanPageMeta: false
}
})
``` -->
```
## cookieStore

View File

@ -40,7 +40,7 @@ There is also a `future` namespace for early opting-in to new features that will
### compatibilityVersion
::important
This configuration option is available in Nuxt v3.12+ or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
This configuration option is available in Nuxt v3.12+.
::
This enables early access to Nuxt features or flags.

View File

@ -11,7 +11,7 @@ links:
---
::important
This component will be available in Nuxt v3.12 or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
This component is available in Nuxt v3.12+.
::
## Usage

View File

@ -10,7 +10,7 @@ links:
---
::important
This composable will be available in Nuxt v3.12+ or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
This composable is available in Nuxt v3.12+.
::
`onPrehydrate` is a composable lifecycle hook that allows you to run a callback on the client immediately before

View File

@ -11,7 +11,7 @@ links:
---
::important
This composable will be available in Nuxt v3.12 or in [the nightly release channel](/docs/guide/going-further/nightly-release-channel).
This composable is available in Nuxt v3.12+.
::
## Description

View File

@ -73,10 +73,10 @@
"globby": "14.0.1",
"h3": "1.11.1",
"happy-dom": "14.12.0",
"jiti": "1.21.0",
"jiti": "1.21.6",
"markdownlint-cli": "0.41.0",
"nitropack": "2.9.6",
"nuxi": "3.11.1",
"nuxi": "3.12.0",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.0.10",
"ofetch": "1.3.4",
@ -90,10 +90,10 @@
"vitest": "1.6.0",
"vitest-environment-nuxt": "1.0.0",
"vue": "3.4.27",
"vue-router": "4.3.2",
"vue-router": "4.3.3",
"vue-tsc": "2.0.21"
},
"packageManager": "pnpm@9.2.0",
"packageManager": "pnpm@9.3.0",
"engines": {
"node": "^16.10.0 || >=18.0.0"
},

View File

@ -1,6 +1,6 @@
{
"name": "@nuxt/kit",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -34,7 +34,7 @@
"globby": "^14.0.1",
"hash-sum": "^2.0.0",
"ignore": "^5.3.1",
"jiti": "^1.21.0",
"jiti": "^1.21.6",
"klona": "^2.0.6",
"knitwork": "^1.1.0",
"mlly": "^1.7.1",

View File

@ -3,7 +3,7 @@ import { performance } from 'node:perf_hooks'
import { defu } from 'defu'
import { applyDefaults } from 'untyped'
import { dirname } from 'pathe'
import type { ModuleDefinition, ModuleOptions, ModuleSetupInstallResult, ModuleSetupReturn, Nuxt, NuxtModule, NuxtOptions, ResolvedModuleOptions, ResolvedNuxtTemplate } from '@nuxt/schema'
import type { ModuleDefinition, ModuleOptions, ModuleSetupReturn, Nuxt, NuxtModule, NuxtOptions, ResolvedNuxtTemplate } from '@nuxt/schema'
import { logger } from '../logger'
import { nuxtCtx, tryUseNuxt, useNuxt } from '../context'
import { checkNuxtCompatibility, isNuxt2 } from '../compatibility'
@ -13,53 +13,28 @@ import { compileTemplate, templateUtils } from '../internal/template'
* Define a Nuxt module, automatically merging defaults with user provided options, installing
* any hooks that are provided, and calling an optional setup function for full control.
*/
export function defineNuxtModule<TOptions extends ModuleOptions> (definition: ModuleDefinition<TOptions> | NuxtModule<TOptions>): NuxtModule<TOptions>
export function defineNuxtModule<TOptions extends ModuleOptions> (): {
with: <TOptionsDefaults extends Partial<TOptions>> (
definition: ModuleDefinition<TOptions, TOptionsDefaults> | NuxtModule<TOptions, TOptionsDefaults>
) => NuxtModule<TOptions, TOptionsDefaults>
}
export function defineNuxtModule<TOptions extends ModuleOptions> (definition?: ModuleDefinition<TOptions> | NuxtModule<TOptions>) {
if (definition) {
return _defineNuxtModule(definition)
}
return {
with: <TOptionsDefaults extends Partial<TOptions>>(
definition: ModuleDefinition<TOptions, TOptionsDefaults> | NuxtModule<TOptions, TOptionsDefaults>,
) => _defineNuxtModule(definition),
}
}
function _defineNuxtModule<TOptions extends ModuleOptions, TOptionsDefaults extends Partial<TOptions>> (definition: ModuleDefinition<TOptions, TOptionsDefaults> | NuxtModule<TOptions, TOptionsDefaults>): NuxtModule<TOptions, TOptionsDefaults> {
if (typeof definition === 'function') { return _defineNuxtModule<TOptions, TOptionsDefaults>({ setup: definition }) }
export function defineNuxtModule<OptionsT extends ModuleOptions> (definition: ModuleDefinition<OptionsT> | NuxtModule<OptionsT>): NuxtModule<OptionsT> {
if (typeof definition === 'function') { return defineNuxtModule({ setup: definition }) }
// Normalize definition and meta
const module: ModuleDefinition<TOptions, TOptionsDefaults> & Required<Pick<ModuleDefinition<TOptions, TOptionsDefaults>, 'meta'>> = defu(definition, { meta: {} })
module.meta.configKey ||= module.meta.name
const module: ModuleDefinition<OptionsT> & Required<Pick<ModuleDefinition<OptionsT>, 'meta'>> = defu(definition, { meta: {} })
if (module.meta.configKey === undefined) {
module.meta.configKey = module.meta.name
}
// Resolves module options from inline options, [configKey] in nuxt.config, defaults and schema
async function getOptions (inlineOptions?: Partial<TOptions>, nuxt: Nuxt = useNuxt()): Promise<ResolvedModuleOptions<TOptions, TOptionsDefaults>> {
const nuxtConfigOptionsKey = module.meta.configKey || module.meta.name
const nuxtConfigOptions: Partial<TOptions> = nuxtConfigOptionsKey && nuxtConfigOptionsKey in nuxt.options ? nuxt.options[<keyof NuxtOptions> nuxtConfigOptionsKey] : {}
const optionsDefaults: TOptionsDefaults = module.defaults instanceof Function ? module.defaults(nuxt) : module.defaults ?? <TOptionsDefaults> {}
let options: ResolvedModuleOptions<TOptions, TOptionsDefaults> = defu(inlineOptions, nuxtConfigOptions, optionsDefaults)
async function getOptions (inlineOptions?: OptionsT, nuxt: Nuxt = useNuxt()) {
const configKey = module.meta.configKey || module.meta.name!
const _defaults = module.defaults instanceof Function ? module.defaults(nuxt) : module.defaults
let _options = defu(inlineOptions, nuxt.options[configKey as keyof NuxtOptions], _defaults) as OptionsT
if (module.schema) {
options = await applyDefaults(module.schema, options) as any
_options = await applyDefaults(module.schema, _options) as OptionsT
}
return Promise.resolve(options)
return Promise.resolve(_options)
}
// Module format is always a simple function
async function normalizedModule (this: any, inlineOptions: Partial<TOptions>, nuxt: Nuxt): Promise<ModuleSetupReturn> {
async function normalizedModule (this: any, inlineOptions: OptionsT, nuxt: Nuxt) {
if (!nuxt) {
nuxt = tryUseNuxt() || this.nuxt /* invoked by nuxt 2 */
}
@ -112,7 +87,7 @@ function _defineNuxtModule<TOptions extends ModuleOptions, TOptionsDefaults exte
if (res === false) { return false }
// Return module install result
return defu(res, <ModuleSetupInstallResult> {
return defu(res, <ModuleSetupReturn> {
timings: {
setup: setupTime,
},
@ -123,7 +98,7 @@ function _defineNuxtModule<TOptions extends ModuleOptions, TOptionsDefaults exte
normalizedModule.getMeta = () => Promise.resolve(module.meta)
normalizedModule.getOptions = getOptions
return <NuxtModule<TOptions, TOptionsDefaults>> normalizedModule
return normalizedModule as NuxtModule<OptionsT>
}
// -- Nuxt 2 compatibility shims --

View File

@ -31,7 +31,7 @@ export async function installModule<
isNuxt2()
// @ts-expect-error Nuxt 2 `moduleContainer` is not typed
? await nuxtModule.call(nuxt.moduleContainer, inlineOptions, nuxt)
: await nuxtModule(inlineOptions || {}, nuxt)
: await nuxtModule(inlineOptions, nuxt)
) ?? {}
if (res === false /* setup aborted */) {
return
@ -84,7 +84,7 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
let error: unknown
for (const path of paths) {
try {
const src = await resolvePath(path)
const src = await resolvePath(path, { fallbackToOriginal: true })
// Prefer ESM resolution if possible
nuxtModule = await importModule(src, nuxt.options.modulesDir).catch(() => null) ?? requireModule(src, { paths: nuxt.options.modulesDir })

View File

@ -23,6 +23,13 @@ export interface ResolvePathOptions {
* @default false
*/
virtual?: boolean
/**
* Whether to fallback to the original path if the resolved path does not exist instead of returning the normalized input path.
*
* @default false
*/
fallbackToOriginal?: boolean
}
/**
@ -99,7 +106,7 @@ export async function resolvePath (path: string, opts: ResolvePathOptions = {}):
}
// Return normalized input
return path
return opts.fallbackToOriginal ? _path : path
}
/**

View File

@ -135,7 +135,7 @@ export async function _generateTypes (nuxt: Nuxt) {
/* Base options: */
esModuleInterop: true,
skipLibCheck: true,
target: 'es2022',
target: 'ESNext',
allowJs: true,
resolveJsonModule: true,
moduleDetection: 'force',
@ -147,11 +147,11 @@ export async function _generateTypes (nuxt: Nuxt) {
forceConsistentCasingInFileNames: true,
noImplicitOverride: true,
/* If NOT transpiling with TypeScript: */
module: hasTypescriptVersionWithModulePreserve ? 'preserve' : 'es2022',
module: hasTypescriptVersionWithModulePreserve ? 'preserve' : 'ESNext',
noEmit: true,
/* If your code runs in the DOM: */
lib: [
'es2022',
'ESNext',
'dom',
'dom.iterable',
],

View File

@ -1,6 +1,6 @@
{
"name": "nuxt",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -84,13 +84,13 @@
"h3": "^1.11.1",
"hookable": "^5.5.3",
"ignore": "^5.3.1",
"jiti": "^1.21.0",
"jiti": "^1.21.6",
"klona": "^2.0.6",
"knitwork": "^1.1.0",
"magic-string": "^0.30.10",
"mlly": "^1.7.1",
"nitropack": "^2.9.6",
"nuxi": "^3.11.1",
"nuxi": "^3.12.0",
"nypm": "^0.3.8",
"ofetch": "^1.3.4",
"ohash": "^1.1.3",
@ -115,9 +115,10 @@
"vue": "^3.4.27",
"vue-bundle-renderer": "^2.1.0",
"vue-devtools-stub": "^0.1.0",
"vue-router": "^4.3.2"
"vue-router": "^4.3.3"
},
"devDependencies": {
"@nuxt/scripts": "0.4.7",
"@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5",

View File

@ -1,4 +1,4 @@
import type { Component } from 'vue'
import type { Component, PropType } from 'vue'
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
@ -59,6 +59,10 @@ export default defineComponent({
type: Object,
default: () => ({}),
},
scopeId: {
type: String as PropType<string | undefined | null>,
default: () => undefined,
},
source: {
type: String,
default: () => undefined,
@ -131,6 +135,10 @@ export default defineComponent({
const currentSlots = Object.keys(slots)
let html = ssrHTML.value
if (props.scopeId) {
html = html.replace(/^<[^> ]*/, full => full + ' ' + props.scopeId)
}
if (import.meta.client && !canLoadClientComponent.value) {
for (const [key, value] of Object.entries(payloads.components || {})) {
html = html.replace(new RegExp(` data-island-uid="${uid.value}" data-island-component="${key}"[^>]*>`), (full) => {

View File

@ -11,7 +11,7 @@ import { onNuxtReady } from './ready'
import { asyncDataDefaults, resetAsyncDataToUndefined } from '#build/nuxt.config.mjs'
// TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
import type { DedupeOption, DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
@ -99,7 +99,6 @@ export interface AsyncDataOptions<
export interface AsyncDataExecuteOptions {
_initial?: boolean
// TODO: remove boolean option in Nuxt 4
/**
* Force a refresh, even if there is already a pending request. Previous requests will
* not be cancelled, but their result will not affect the data/pending state - and any
@ -108,7 +107,7 @@ export interface AsyncDataExecuteOptions {
* Instead of using `boolean` values, use `cancel` for `true` and `defer` for `false`.
* Boolean values will be removed in a future release.
*/
dedupe?: boolean | 'cancel' | 'defer'
dedupe?: DedupeOption
}
export interface _AsyncData<DataT, ErrorT> {

View File

@ -7,8 +7,8 @@ export const onNuxtReady = (callback: () => any) => {
const nuxtApp = useNuxtApp()
if (nuxtApp.isHydrating) {
nuxtApp.hooks.hookOnce('app:suspense:resolve', () => { requestIdleCallback(callback) })
nuxtApp.hooks.hookOnce('app:suspense:resolve', () => { requestIdleCallback(() => callback()) })
} else {
requestIdleCallback(callback)
requestIdleCallback(() => callback())
}
}

View File

@ -3,5 +3,6 @@
export type DefaultAsyncDataErrorValue = null
export type DefaultAsyncDataValue = null
export type DefaultErrorValue = null
export type DedupeOption = boolean | 'cancel' | 'defer'
export {}

View File

@ -387,11 +387,15 @@ export function createNuxtApp (options: CreateOptions) {
return nuxtApp
}
/** @since 3.0.0 */
export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin & ObjectPlugin<any>) {
/** @since 3.12.0 */
export function registerPluginHooks (nuxtApp: NuxtApp, plugin: Plugin & ObjectPlugin<any>) {
if (plugin.hooks) {
nuxtApp.hooks.addHooks(plugin.hooks)
}
}
/** @since 3.0.0 */
export async function applyPlugin (nuxtApp: NuxtApp, plugin: Plugin & ObjectPlugin<any>) {
if (typeof plugin === 'function') {
const { provide } = await nuxtApp.runWithContext(() => plugin(nuxtApp)) || {}
if (provide && typeof provide === 'object') {
@ -438,6 +442,11 @@ export async function applyPlugins (nuxtApp: NuxtApp, plugins: Array<Plugin & Ob
}
}
for (const plugin of plugins) {
if (import.meta.server && nuxtApp.ssrContext?.islandContext && plugin.env?.islands === false) { continue }
registerPluginHooks(nuxtApp, plugin)
}
for (const plugin of plugins) {
if (import.meta.server && nuxtApp.ssrContext?.islandContext && plugin.env?.islands === false) { continue }
await executePlugin(plugin)
@ -554,8 +563,8 @@ function wrappedConfig (runtimeConfig: Record<string, unknown>) {
const keys = Object.keys(runtimeConfig).map(key => `\`${key}\``)
const lastKey = keys.pop()
return new Proxy(runtimeConfig, {
get (target, p: string, receiver) {
if (p !== 'public' && !(p in target) && !p.startsWith('__v') /* vue check for reactivity, e.g. `__v_isRef` */) {
get (target, p, receiver) {
if (typeof p === 'string' && p !== 'public' && !(p in target) && !p.startsWith('__v') /* vue check for reactivity, e.g. `__v_isRef` */) {
console.warn(`[nuxt] Could not access \`${p}\`. The only available runtime config keys on the client side are ${keys.join(', ')} and ${lastKey}. See \`https://nuxt.com/docs/guide/going-further/runtime-config\` for more information.`)
}
return Reflect.get(target, p, receiver)

View File

@ -1,4 +1,4 @@
import { defineComponent, h, ref } from 'vue'
import { defineComponent, getCurrentInstance, h, ref } from 'vue'
import NuxtIsland from '#app/components/nuxt-island'
import { useRoute } from '#app/composables/router'
import { isPrerendered } from '#app/composables/payload'
@ -11,6 +11,7 @@ export const createServerComponent = (name: string) => {
props: { lazy: Boolean },
emits: ['error'],
setup (props, { attrs, slots, expose, emit }) {
const vm = getCurrentInstance()
const islandRef = ref<null | typeof NuxtIsland>(null)
expose({
@ -22,6 +23,7 @@ export const createServerComponent = (name: string) => {
name,
lazy: props.lazy,
props: attrs,
scopeId: vm?.vnode.scopeId,
ref: islandRef,
onError: (err) => {
emit('error', err)

View File

@ -388,7 +388,10 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}
// Init nitro
const nitro = await createNitro(nitroConfig)
const nitro = await createNitro(nitroConfig, {
// @ts-expect-error this will be valid in a future version of Nitro
compatibilityDate: nuxt.options.compatibilityDate,
})
// Trigger Nitro reload when SPA loading template changes
const spaLoadingTemplateFilePath = await spaLoadingTemplatePath(nuxt)

View File

@ -455,6 +455,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
type: resource.module ? 'module' : null,
src: renderer.rendererContext.buildAssetsURL(resource.file),
defer: resource.module ? null : true,
// if we are rendering script tag payloads that import an async payload
// we need to ensure this resolves before executing the Nuxt entry
tagPosition: (_PAYLOAD_EXTRACTION && !process.env.NUXT_JSON_PAYLOADS) ? 'bodyClose' : 'head',
crossorigin: '',
})),
}, headEntryOptions)

View File

@ -112,6 +112,8 @@ export const pluginsDeclaration: NuxtTemplate = {
const pluginsName = (await annotatePlugins(ctx.nuxt, ctx.app.plugins)).filter(p => p.name).map(p => `'${p.name}'`)
const isV4 = ctx.nuxt.options.future.compatibilityVersion === 4
return `// Generated by Nuxt'
import type { Plugin } from '#app'
@ -131,9 +133,10 @@ declare module '#app' {
}
declare module '#app/defaults' {
type DefaultAsyncDataErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
type DefaultAsyncDataValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
type DefaultErrorValue = ${ctx.nuxt.options.future.compatibilityVersion === 4 ? 'undefined' : 'null'}
type DefaultAsyncDataErrorValue = ${isV4 ? 'undefined' : 'null'}
type DefaultAsyncDataValue = ${isV4 ? 'undefined' : 'null'}
type DefaultErrorValue = ${isV4 ? 'undefined' : 'null'}
type DedupeOption = ${isV4 ? '\'cancel\' | \'defer\'' : 'boolean | \'cancel\' | \'defer\''}
}
declare module 'vue' {

View File

@ -123,15 +123,16 @@ export const scriptsStubsPreset = {
'useScriptFathomAnalytics',
'useScriptMatomoAnalytics',
'useScriptGoogleTagManager',
'useScriptGoogleAdsense',
'useScriptSegment',
'useScriptFacebookPixel',
'useScriptMetaPixel',
'useScriptXPixel',
'useScriptIntercom',
'useScriptHotjar',
'useScriptStripe',
'useScriptLemonSqueezy',
'useScriptVimeoPlayer',
'useScriptYouTubeIframe',
'useScriptYouTubePlayer',
'useScriptGoogleMaps',
'useScriptNpm',
],

View File

@ -1,6 +1,6 @@
import { existsSync, readdirSync } from 'node:fs'
import { mkdir, readFile } from 'node:fs/promises'
import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, logger, updateTemplates, useNitro } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, logger, resolvePath, updateTemplates, useNitro } from '@nuxt/kit'
import { dirname, join, relative, resolve } from 'pathe'
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
import type { Nuxt, NuxtApp, NuxtPage } from 'nuxt/schema'
@ -61,8 +61,12 @@ export default defineNuxtModule({
}
const pages = await resolvePagesRoutes()
await nuxt.callHook('pages:extend', pages)
if (pages.length) { return true }
if (pages.length) {
if (nuxt.apps.default) {
nuxt.apps.default.pages = pages
}
return true
}
return false
}
@ -75,7 +79,6 @@ export default defineNuxtModule({
nuxt.hook('app:templates', async (app) => {
app.pages = await resolvePagesRoutes()
await nuxt.callHook('pages:extend', app.pages)
if (!nuxt.options.ssr && app.pages.some(p => p.mode === 'server')) {
logger.warn('Using server pages with `ssr: false` is not supported with auto-detected component islands. Set `experimental.componentIslands` to `true`.')
@ -153,10 +156,9 @@ export default defineNuxtModule({
logs: nuxt.options.debug,
async beforeWriteFiles (rootPage) {
rootPage.children.forEach(child => child.delete())
let pages = nuxt.apps.default?.pages
if (!pages) {
pages = await resolvePagesRoutes()
await nuxt.callHook('pages:extend', pages)
const pages = nuxt.apps.default?.pages || await resolvePagesRoutes()
if (nuxt.apps.default) {
nuxt.apps.default.pages = pages
}
function addPage (parent: EditableTreeNode, page: NuxtPage) {
// @ts-expect-error TODO: either fix types upstream or figure out another
@ -342,6 +344,7 @@ export default defineNuxtModule({
}
if (nuxt.options.experimental.appManifest) {
const componentStubPath = await resolvePath(resolve(runtimeDir, 'component-stub'))
// Add all redirect paths as valid routes to router; we will handle these in a client-side middleware
// when the app manifest is enabled.
nuxt.hook('pages:extend', (routes) => {
@ -356,7 +359,7 @@ export default defineNuxtModule({
routes.push({
_sync: true,
path: path.replace(/\/[^/]*\*\*/, '/:pathMatch(.*)'),
file: resolve(runtimeDir, 'component-stub'),
file: componentStubPath,
})
}
})

View File

@ -2,7 +2,7 @@ import { runInNewContext } from 'node:vm'
import fs from 'node:fs'
import { extname, normalize, relative, resolve } from 'pathe'
import { encodePath, joinURL, withLeadingSlash } from 'ufo'
import { logger, resolveFiles, useNuxt } from '@nuxt/kit'
import { logger, resolveFiles, resolvePath, useNuxt } from '@nuxt/kit'
import { genArrayFromRaw, genDynamicImport, genImport, genSafeVariableName } from 'knitwork'
import escapeRE from 'escape-string-regexp'
import { filename } from 'pathe/utils'
@ -58,21 +58,30 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
scannedFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath, 'en-US'))
const allRoutes = await generateRoutesFromFiles(uniqueBy(scannedFiles, 'relativePath'), {
shouldExtractBuildMeta: nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages,
shouldUseServerComponents: !!nuxt.options.experimental.componentIslands,
vfs: nuxt.vfs,
})
return uniqueBy(allRoutes, 'path')
const pages = uniqueBy(allRoutes, 'path')
const shouldAugment = nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages
if (shouldAugment) {
const augmentedPages = await augmentPages(pages, nuxt.vfs)
await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs, augmentedPages)
augmentedPages.clear()
} else {
await nuxt.callHook('pages:extend', pages)
}
return pages
}
type GenerateRoutesFromFilesOptions = {
shouldExtractBuildMeta?: boolean
shouldUseServerComponents?: boolean
vfs?: Record<string, string>
}
export async function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): Promise<NuxtPage[]> {
export function generateRoutesFromFiles (files: ScannedFile[], options: GenerateRoutesFromFilesOptions = {}): NuxtPage[] {
const routes: NuxtPage[] = []
for (const file of files) {
@ -124,17 +133,27 @@ export async function generateRoutesFromFiles (files: ScannedFile[], options: Ge
}
}
if (options.shouldExtractBuildMeta && options.vfs) {
const fileContent = file.absolutePath in options.vfs ? options.vfs[file.absolutePath] : fs.readFileSync(file.absolutePath, 'utf-8')
Object.assign(route, await getRouteMeta(fileContent, file.absolutePath))
}
parent.push(route)
}
return prepareRoutes(routes)
}
export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, augmentedPages = new Set<string>()) {
for (const route of routes) {
if (route.file && !augmentedPages.has(route.file)) {
const fileContent = route.file in vfs ? vfs[route.file] : fs.readFileSync(await resolvePath(route.file), 'utf-8')
Object.assign(route, await getRouteMeta(fileContent, route.file))
augmentedPages.add(route.file)
}
if (route.children && route.children.length > 0) {
await augmentPages(route.children, vfs)
}
}
return augmentedPages
}
const SFC_SCRIPT_RE = /<script[^>]*>([\s\S]*?)<\/script[^>]*>/i
export function extractScriptContent (html: string) {
const match = html.match(SFC_SCRIPT_RE)

View File

@ -6,8 +6,9 @@ import * as VueFunctions from 'vue'
import type { Import } from 'unimport'
import { createUnimport } from 'unimport'
import type { Plugin } from 'vite'
import { registry as scriptRegistry } from '@nuxt/scripts/registry'
import { TransformPlugin } from '../src/imports/transform'
import { defaultPresets } from '../src/imports/presets'
import { defaultPresets, scriptsStubsPreset } from '../src/imports/presets'
describe('imports:transform', () => {
const imports: Import[] = [
@ -193,3 +194,21 @@ describe('imports:vue', () => {
})
}
})
describe('imports:nuxt/scripts', () => {
const scripts = scriptRegistry().map(s => s.import?.name).filter(Boolean)
const globalScripts = new Set([
'useScript',
'useAnalyticsPageEvent',
'useElementScriptTrigger',
'useConsentScriptTrigger',
])
it.each(scriptsStubsPreset.imports)(`should register %s from @nuxt/scripts`, (name) => {
if (globalScripts.has(name)) { return }
expect(scripts).toContain(name)
})
it.each(scripts)(`should register %s from @nuxt/scripts`, (name) => {
expect(scriptsStubsPreset.imports).toContain(name)
})
})

View File

@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import type { NuxtPage } from 'nuxt/schema'
import { generateRoutesFromFiles, normalizeRoutes, pathToNitroGlob } from '../src/pages/utils'
import { augmentPages, generateRoutesFromFiles, normalizeRoutes, pathToNitroGlob } from '../src/pages/utils'
import { generateRouteKey } from '../src/pages/runtime/utils'
describe('pages:generateRoutesFromFiles', () => {
@ -568,11 +568,12 @@ describe('pages:generateRoutesFromFiles', () => {
) as Record<string, string>
try {
result = await generateRoutesFromFiles(test.files.map(file => ({
result = generateRoutesFromFiles(test.files.map(file => ({
shouldUseServerComponents: true,
absolutePath: file.path,
relativePath: file.path.replace(/^(pages|layer\/pages)\//, ''),
})), { shouldExtractBuildMeta: true, vfs })
})))
await augmentPages(result, vfs)
} catch (error: any) {
expect(error.message).toEqual(test.error)
}

View File

@ -1,6 +1,6 @@
{
"name": "@nuxt/schema",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",
@ -58,11 +58,12 @@
"vue": "3.4.27",
"vue-bundle-renderer": "2.1.0",
"vue-loader": "17.4.2",
"vue-router": "4.3.2",
"vue-router": "4.3.3",
"webpack": "5.91.0",
"webpack-dev-middleware": "7.2.1"
},
"dependencies": {
"compatx": "^0.1.3",
"consola": "^3.2.3",
"defu": "^6.1.4",
"hookable": "^5.5.3",
@ -71,8 +72,8 @@
"scule": "^1.3.0",
"std-env": "^3.7.0",
"ufo": "^1.5.3",
"unimport": "^3.7.2",
"uncrypto": "^0.1.3",
"unimport": "^3.7.2",
"untyped": "^1.4.2"
},
"engines": {

View File

@ -20,6 +20,18 @@ export default defineUntypedSchema({
*/
extends: null,
/**
* Specify a compatibility date for your app.
*
* This is used to control the behavior of presets in Nitro, Nuxt Image
* and other modules that may change behavior without a major version bump.
*
* We plan to improve the tooling around this feature in the future.
*
* @type {typeof import('compatx').CompatibilityDateSpec}
*/
compatibilityDate: undefined,
/**
* Extend project from a local or remote source.
*

View File

@ -385,7 +385,7 @@ export default defineUntypedSchema({
*
* https://github.com/nuxt/nuxt/issues/24770
*/
scanPageMeta: false,
scanPageMeta: true,
/**
* Automatically share payload _data_ between pages that are prerendered. This can result in a significant

View File

@ -1,4 +1,3 @@
import type { Defu } from 'defu'
import type { NuxtHooks } from './hooks'
import type { Nuxt } from './nuxt'
import type { NuxtCompatibility } from './compatibility'
@ -27,7 +26,8 @@ export interface ModuleMeta {
/** The options received. */
export type ModuleOptions = Record<string, any>
export type ModuleSetupInstallResult = {
/** Optional result for nuxt modules */
export interface ModuleSetupReturn {
/**
* Timing information for the initial setup
*/
@ -39,37 +39,19 @@ export type ModuleSetupInstallResult = {
}
type Awaitable<T> = T | Promise<T>
type _ModuleSetupReturn = Awaitable<void | false | ModuleSetupReturn>
type Prettify<T> = {
[K in keyof T]: T[K];
} & {}
export type ModuleSetupReturn = Awaitable<false | void | ModuleSetupInstallResult>
export type ResolvedModuleOptions<TOptions extends ModuleOptions, TOptionsDefaults extends Partial<TOptions>> = Prettify<
Defu<
Partial<TOptions>,
[Partial<TOptions>, TOptionsDefaults]
>
>
/** Module definition passed to 'defineNuxtModule(...)' or 'defineNuxtModule().with(...)'. */
export interface ModuleDefinition<
TOptions extends ModuleOptions,
TOptionsDefaults extends Partial<TOptions> = Partial<TOptions>,
> {
/** Input module passed to defineNuxtModule. */
export interface ModuleDefinition<T extends ModuleOptions = ModuleOptions> {
meta?: ModuleMeta
defaults?: TOptionsDefaults | ((nuxt: Nuxt) => TOptionsDefaults)
schema?: TOptions
defaults?: T | ((nuxt: Nuxt) => T)
schema?: T
hooks?: Partial<NuxtHooks>
setup?: (this: void, resolvedOptions: ResolvedModuleOptions<TOptions, TOptionsDefaults>, nuxt: Nuxt) => ModuleSetupReturn
setup?: (this: void, resolvedOptions: T, nuxt: Nuxt) => _ModuleSetupReturn
}
export interface NuxtModule<
TOptions extends ModuleOptions = ModuleOptions,
TOptionsDefaults extends Partial<TOptions> = Partial<TOptions>,
> {
(this: void, resolvedOptions: ResolvedModuleOptions<TOptions, TOptionsDefaults>, nuxt: Nuxt): ModuleSetupReturn
getOptions?: (inlineOptions?: Partial<TOptions>, nuxt?: Nuxt) => Promise<ResolvedModuleOptions<TOptions, TOptionsDefaults>>
export interface NuxtModule<T extends ModuleOptions = ModuleOptions> {
(this: void, inlineOptions: T, nuxt: Nuxt): _ModuleSetupReturn
getOptions?: (inlineOptions?: T, nuxt?: Nuxt) => Promise<T>
getMeta?: () => Promise<ModuleMeta>
}

View File

@ -26,11 +26,11 @@
"execa": "9.2.0",
"globby": "14.0.1",
"html-minifier": "4.0.0",
"jiti": "1.21.0",
"jiti": "1.21.6",
"knitwork": "1.1.0",
"lodash-es": "4.17.21",
"pathe": "1.1.2",
"prettier": "3.3.1",
"prettier": "3.3.2",
"scule": "1.3.0",
"unocss": "0.60.4",
"vite": "5.2.13"

View File

@ -1,6 +1,6 @@
{
"name": "@nuxt/vite-builder",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",

View File

@ -1,6 +1,6 @@
{
"name": "@nuxt/webpack-builder",
"version": "3.11.2",
"version": "3.12.1",
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/nuxt.git",

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,6 @@
"main"
],
"ignoreDeps": [
"jiti",
"@vitejs/plugin-vue",
"nuxt",
"nuxt3",

View File

@ -16,7 +16,7 @@ for PKG in packages/* ; do
if [[ $PKG == "packages/test-utils" ]] ; then
continue
fi
if [[ $p == "packages/ui-templates" ]] ; then
if [[ $PKG == "packages/ui-templates" ]] ; then
continue
fi
pushd $PKG

View File

@ -32,7 +32,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(`"1338k"`)

View File

@ -11,7 +11,7 @@
"devDependencies": {
"ofetch": "latest",
"unplugin-vue-router": "^0.7.0",
"vitest": "1.5.3",
"vitest": "1.6.0",
"vue": "latest",
"vue-router": "latest"
}

View File

@ -4,8 +4,8 @@ import type { FetchError } from 'ofetch'
import type { NavigationFailure, RouteLocationNormalized, RouteLocationRaw, Router, useRouter as vueUseRouter } from '#vue-router'
import type { AppConfig, RuntimeValue, UpperSnakeCase } from 'nuxt/schema'
import { defineNuxtConfig } from 'nuxt/config'
import { defineNuxtModule } from 'nuxt/kit'
import { defineNuxtConfig } from 'nuxt/config'
import { callWithNuxt, isVue3 } from '#app'
import type { NuxtError } from '#app'
import type { NavigateToOptions } from '#app/composables/router'
@ -244,19 +244,13 @@ describe('modules', () => {
defineNuxtConfig({ undeclaredKey: { other: false } })
})
it('correctly typed resolved options in defineNuxtModule setup using `.with()`', () => {
defineNuxtModule<{
foo?: string
baz: number
}>().with({
it('preserves options in defineNuxtModule setup without `.with()`', () => {
defineNuxtModule<{ foo?: string, baz: number }>({
defaults: {
foo: 'bar',
baz: 100,
},
setup: (resolvedOptions) => {
expectTypeOf(resolvedOptions).toEqualTypeOf<{
foo: string
baz?: number | undefined
}>()
expectTypeOf(resolvedOptions).toEqualTypeOf<{ foo?: string, baz: number }>()
},
})
})

View File

@ -16,9 +16,15 @@ export default defineNuxtModule({
}, {
path: '/big-page-1',
file: resolver.resolve('./pages/big-page.vue'),
meta: {
layout: false,
},
}, {
path: '/big-page-2',
file: resolver.resolve('./pages/big-page.vue'),
meta: {
layout: false,
},
})
})
},

View File

@ -15,7 +15,7 @@ if (count || data.value !== 1) {
}
timeout = 100
const p = refresh({ dedupe: true, _initial: false })
const p = refresh({ dedupe: 'cancel', _initial: false })
if (import.meta.client && (count !== 0 || data.value !== 1)) {
throw new Error('count should start at 0')

View File

@ -1,6 +1,6 @@
import { beforeEach } from 'node:test'
import { describe, expect, it, vi } from 'vitest'
import { h, nextTick } from 'vue'
import { defineComponent, h, nextTick, popScopeId, pushScopeId } from 'vue'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { createServerComponent } from '../../packages/nuxt/src/components/runtime/server-component'
import { createSimpleRemoteIslandProvider } from '../fixtures/remote-provider'
@ -133,6 +133,19 @@ describe('runtime server component', () => {
expect(wrapper.emitted('error')).toHaveLength(1)
vi.mocked(fetch).mockReset()
})
it('expect NuxtIsland to have parent scopeId', async () => {
const wrapper = await mountSuspended(defineComponent({
render () {
pushScopeId('data-v-654e2b21')
const vnode = h(createServerComponent('dummyName'))
popScopeId()
return vnode
},
}))
expect(wrapper.find('*').attributes()).toHaveProperty('data-v-654e2b21')
})
})
describe('client components', () => {
@ -186,7 +199,7 @@ describe('client components', () => {
expect(fetch).toHaveBeenCalledOnce()
expect(wrapper.html()).toMatchInlineSnapshot(`
"<div data-island-uid="4">hello<div data-island-uid="4" data-island-component="Client-12345">
"<div data-island-uid="5">hello<div data-island-uid="5" data-island-component="Client-12345">
<div>client component</div>
</div>
</div>
@ -212,7 +225,7 @@ describe('client components', () => {
await wrapper.vm.$.exposed!.refresh()
await nextTick()
expect(wrapper.html()).toMatchInlineSnapshot(`
"<div data-island-uid="4">hello<div>
"<div data-island-uid="5">hello<div>
<div>fallback</div>
</div>
</div>"
@ -305,7 +318,7 @@ describe('client components', () => {
})
expect(fetch).toHaveBeenCalledOnce()
expect(wrapper.html()).toMatchInlineSnapshot(`
"<div data-island-uid="6">hello<div data-island-uid="6" data-island-component="ClientWithSlot-12345">
"<div data-island-uid="7">hello<div data-island-uid="7" data-island-component="ClientWithSlot-12345">
<div class="client-component">
<div style="display: contents" data-island-uid="" data-island-slot="default">
<div>slot in client component</div>

View File

@ -283,3 +283,34 @@ describe('plugin dependsOn', () => {
])
})
})
describe('plugin hooks', () => {
it('registers hooks before executing plugins', async () => {
const nuxtApp = useNuxtApp()
const sequence: string[] = []
const plugins = [
defineNuxtPlugin({
name: 'A',
setup (nuxt) {
sequence.push('start A')
nuxt.callHook('a:setup')
},
}),
defineNuxtPlugin({
name: 'B',
hooks: {
'a:setup': () => {
sequence.push('listen B')
},
},
}),
]
await applyPlugins(nuxtApp, plugins)
expect(sequence).toMatchObject([
'start A',
'listen B',
])
})
})