when plugins have thrown an error but we're not yet rendering the error page
+const abortRender = import.meta.server && error.value && !nuxtApp.ssrContext.error
onErrorCaptured((err, target, info) => {
nuxtApp.hooks.callHook('vue:error', err, target, info).catch(hookError => console.error('[nuxt] Error in `vue:error` hook', hookError))
if (import.meta.server || (isNuxtError(err) && (err.fatal || err.unhandled))) {
diff --git a/packages/nuxt/src/app/components/nuxt-route-announcer.ts b/packages/nuxt/src/app/components/nuxt-route-announcer.ts
new file mode 100644
index 0000000000..035e9e9e50
--- /dev/null
+++ b/packages/nuxt/src/app/components/nuxt-route-announcer.ts
@@ -0,0 +1,48 @@
+import { defineComponent, h } from 'vue'
+import type { Politeness } from '#app/composables/route-announcer'
+import { useRouteAnnouncer } from '#app/composables/route-announcer'
+
+export default defineComponent({
+ name: 'NuxtRouteAnnouncer',
+ props: {
+ atomic: {
+ type: Boolean,
+ default: false,
+ },
+ politeness: {
+ type: String as () => Politeness,
+ default: 'polite',
+ },
+ },
+ setup (props, { slots, expose }) {
+ const { set, polite, assertive, message, politeness } = useRouteAnnouncer({ politeness: props.politeness })
+
+ expose({
+ set, polite, assertive, message, politeness,
+ })
+
+ return () => h('span', {
+ class: 'nuxt-route-announcer',
+ style: {
+ position: 'absolute',
+ },
+ }, h('span', {
+ 'role': 'alert',
+ 'aria-live': politeness.value,
+ 'aria-atomic': props.atomic,
+ 'style': {
+ 'border': '0',
+ 'clip': 'rect(0 0 0 0)',
+ 'clip-path': 'inset(50%)',
+ 'height': '1px',
+ 'width': '1px',
+ 'overflow': 'hidden',
+ 'position': 'absolute',
+ 'white-space': 'nowrap',
+ 'word-wrap': 'normal',
+ 'margin': '-1px',
+ 'padding': '0',
+ },
+ }, slots.default ? slots.default({ message: message.value }) : message.value))
+ },
+})
diff --git a/packages/nuxt/src/app/components/nuxt-stubs.ts b/packages/nuxt/src/app/components/nuxt-stubs.ts
index c2adc56412..5a3d20c10d 100644
--- a/packages/nuxt/src/app/components/nuxt-stubs.ts
+++ b/packages/nuxt/src/app/components/nuxt-stubs.ts
@@ -4,14 +4,14 @@ function renderStubMessage (name: string) {
throw createError({
fatal: true,
statusCode: 500,
- statusMessage: `${name} is provided by @nuxt/image. Check your console to install it or run 'npx nuxi@latest module add @nuxt/image'`
+ statusMessage: `${name} is provided by @nuxt/image. Check your console to install it or run 'npx nuxi@latest module add @nuxt/image'`,
})
}
export const NuxtImg = {
- setup: () => renderStubMessage('
')
+ setup: () => renderStubMessage(''),
}
export const NuxtPicture = {
- setup: () => renderStubMessage('')
+ setup: () => renderStubMessage(''),
}
diff --git a/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts b/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts
index 34a5dcca91..e459781dd2 100644
--- a/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts
+++ b/packages/nuxt/src/app/components/nuxt-teleport-island-component.ts
@@ -1,14 +1,16 @@
-import type { Component } from 'vue'
-import { Teleport, defineComponent, h } from 'vue'
+import type { Component, InjectionKey } from 'vue'
+import { Teleport, defineComponent, h, inject, provide } from 'vue'
import { useNuxtApp } from '../nuxt'
// @ts-expect-error virtual file
import { paths } from '#build/components-chunk'
type ExtendedComponent = Component & {
- __file: string,
+ __file: string
__name: string
}
+export const NuxtTeleportIslandSymbol = Symbol('NuxtTeleportIslandComponent') as InjectionKey
+
/**
* component only used with componentsIsland
* this teleport the component in SSR only if it needs to be hydrated on client
@@ -19,43 +21,37 @@ export default defineComponent({
props: {
to: {
type: String,
- required: true
+ required: true,
},
nuxtClient: {
type: Boolean,
- default: false
+ default: false,
},
- /**
- * ONLY used in dev mode since we use build:manifest result in production
- * do not pass any value in production
- */
- rootDir: {
- type: String,
- default: null
- }
},
setup (props, { slots }) {
const nuxtApp = useNuxtApp()
- if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient) { return () => slots.default!() }
+ // 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)
const islandContext = nuxtApp.ssrContext!.islandContext!
return () => {
const slot = slots.default!()[0]
- const slotType = (slot.type as ExtendedComponent)
+ const slotType = slot.type as ExtendedComponent
const name = (slotType.__name || slotType.name) as string
islandContext.components[props.to] = {
- chunk: import.meta.dev ? '_nuxt/' + paths[name] : paths[name],
- props: slot.props || {}
+ chunk: import.meta.dev ? nuxtApp.$config.app.buildAssetsDir + paths[name] : paths[name],
+ props: slot.props || {},
}
return [h('div', {
- style: 'display: contents;',
+ 'style': 'display: contents;',
'data-island-uid': '',
- 'data-island-component': props.to
+ 'data-island-component': props.to,
}, []), h(Teleport, { to: props.to }, slot)]
}
- }
+ },
})
diff --git a/packages/nuxt/src/app/components/nuxt-teleport-island-slot.ts b/packages/nuxt/src/app/components/nuxt-teleport-island-slot.ts
index 6001f26097..9f9fefc8b1 100644
--- a/packages/nuxt/src/app/components/nuxt-teleport-island-slot.ts
+++ b/packages/nuxt/src/app/components/nuxt-teleport-island-slot.ts
@@ -1,6 +1,8 @@
-import { Teleport, defineComponent, h } from 'vue'
+import type { VNode } from 'vue'
+import { Teleport, createVNode, defineComponent, h, inject } from 'vue'
import { useNuxtApp } from '../nuxt'
-
+import { NuxtTeleportIslandSymbol } from './nuxt-teleport-island-component'
+
/**
* component only used within islands for slot teleport
*/
@@ -9,40 +11,53 @@ export default defineComponent({
name: 'NuxtTeleportIslandSlot',
props: {
name: {
- type: String,
- required: true
+ type: String,
+ required: true,
},
/**
* must be an array to handle v-for
*/
props: {
- type: Object as () => Array
- }
+ type: Object as () => Array,
+ },
},
setup (props, { slots }) {
const nuxtApp = useNuxtApp()
const islandContext = nuxtApp.ssrContext?.islandContext
-
- if(!islandContext) {
- return () => slots.default?.()
+ if (!islandContext) {
+ return () => slots.default?.()[0]
}
+ const componentName = inject(NuxtTeleportIslandSymbol, false)
islandContext.slots[props.name] = {
- props: (props.props || []) as unknown[]
+ props: (props.props || []) as unknown[],
}
return () => {
- const vnodes = [h('div', {
- style: 'display: contents;',
- 'data-island-uid': '',
- 'data-island-slot': props.name,
- })]
+ const vnodes: VNode[] = []
+
+ if (nuxtApp.ssrContext?.islandContext && slots.default) {
+ vnodes.push(h('div', {
+ 'style': 'display: contents;',
+ 'data-island-uid': '',
+ 'data-island-slot': props.name,
+ }, {
+ // Teleport in slot to not be hydrated client-side with the staticVNode
+ default: () => [createVNode(Teleport, { to: `island-slot=${componentName};${props.name}` }, slots.default?.())],
+ }))
+ } else {
+ vnodes.push(h('div', {
+ 'style': 'display: contents;',
+ 'data-island-uid': '',
+ 'data-island-slot': props.name,
+ }))
+ }
if (slots.fallback) {
- vnodes.push(h(Teleport, { to: `island-fallback=${props.name}`}, slots.fallback()))
+ vnodes.push(h(Teleport, { to: `island-fallback=${props.name}` }, slots.fallback()))
}
return vnodes
}
- }
+ },
})
diff --git a/packages/nuxt/src/app/components/route-provider.ts b/packages/nuxt/src/app/components/route-provider.ts
index 6f57fb82f6..de4ae64a47 100644
--- a/packages/nuxt/src/app/components/route-provider.ts
+++ b/packages/nuxt/src/app/components/route-provider.ts
@@ -7,15 +7,15 @@ export const RouteProvider = defineComponent({
props: {
vnode: {
type: Object as () => VNode,
- required: true
+ required: true,
},
route: {
type: Object as () => RouteLocationNormalizedLoaded,
- required: true
+ required: true,
},
vnodeRef: Object as () => Ref,
renderKey: String,
- trackRootNodes: Boolean
+ trackRootNodes: Boolean,
},
setup (props) {
// Prevent reactivity when the page will be rerendered in a different suspense fork
@@ -26,7 +26,7 @@ export const RouteProvider = defineComponent({
const route = {} as RouteLocation
for (const key in props.route) {
Object.defineProperty(route, key, {
- get: () => previousKey === props.renderKey ? props.route[key as keyof RouteLocationNormalizedLoaded] : previousRoute[key as keyof RouteLocationNormalizedLoaded]
+ get: () => previousKey === props.renderKey ? props.route[key as keyof RouteLocationNormalizedLoaded] : previousRoute[key as keyof RouteLocationNormalizedLoaded],
})
}
@@ -52,5 +52,5 @@ export const RouteProvider = defineComponent({
return h(props.vnode, { ref: props.vnodeRef })
}
- }
+ },
})
diff --git a/packages/nuxt/src/app/components/server-placeholder.ts b/packages/nuxt/src/app/components/server-placeholder.ts
index eaa38e3282..accbfb9857 100644
--- a/packages/nuxt/src/app/components/server-placeholder.ts
+++ b/packages/nuxt/src/app/components/server-placeholder.ts
@@ -4,5 +4,5 @@ export default defineComponent({
name: 'ServerPlaceholder',
render () {
return createElementBlock('div')
- }
+ },
})
diff --git a/packages/nuxt/src/app/components/test-component-wrapper.ts b/packages/nuxt/src/app/components/test-component-wrapper.ts
index fd185150cc..8b0da24381 100644
--- a/packages/nuxt/src/app/components/test-component-wrapper.ts
+++ b/packages/nuxt/src/app/components/test-component-wrapper.ts
@@ -1,4 +1,3 @@
-import { parseURL } from 'ufo'
import { defineComponent, h } from 'vue'
import { parseQuery } from 'vue-router'
import { resolve } from 'pathe'
@@ -10,18 +9,18 @@ export default (url: string) => defineComponent({
name: 'NuxtTestComponentWrapper',
async setup (props, { attrs }) {
- const query = parseQuery(parseURL(url).search)
+ const query = parseQuery(new URL(url, 'http://localhost').search)
const urlProps = query.props ? destr>(query.props as string) : {}
const path = resolve(query.path as string)
if (!path.startsWith(devRootDir)) {
throw new Error(`[nuxt] Cannot access path outside of project root directory: \`${path}\`.`)
}
- const comp = await import(/* @vite-ignore */ query.path as string).then(r => r.default)
+ const comp = await import(/* @vite-ignore */ path as string).then(r => r.default)
return () => [
- h('div', 'Component Test Wrapper for ' + query.path),
+ h('div', 'Component Test Wrapper for ' + path),
h('div', { id: 'nuxt-component-root' }, [
- h(comp, { ...attrs, ...props, ...urlProps })
- ])
+ h(comp, { ...attrs, ...props, ...urlProps }),
+ ]),
]
- }
+ },
})
diff --git a/packages/nuxt/src/app/components/utils.ts b/packages/nuxt/src/app/components/utils.ts
index 11108cb583..a5e918a89d 100644
--- a/packages/nuxt/src/app/components/utils.ts
+++ b/packages/nuxt/src/app/components/utils.ts
@@ -36,7 +36,7 @@ export function isChangingPage (to: RouteLocationNormalized, from: RouteLocation
if (generateRouteKey(to) !== generateRouteKey(from)) { return true }
const areComponentsSame = to.matched.every((comp, index) =>
- comp.components && comp.components.default === from.matched[index]?.components?.default
+ comp.components && comp.components.default === from.matched[index]?.components?.default,
)
if (areComponentsSame) {
return false
@@ -44,7 +44,6 @@ export function isChangingPage (to: RouteLocationNormalized, from: RouteLocation
return true
}
-// eslint-disable-next-line no-use-before-define
export type SSRBuffer = SSRBufferItem[] & { hasAsync?: boolean }
export type SSRBufferItem = string | SSRBuffer | Promise
@@ -71,28 +70,10 @@ export function createBuffer () {
if (isPromise(item) || (isArray(item) && item.hasAsync)) {
buffer.hasAsync = true
}
- }
+ },
}
}
-const TRANSLATE_RE = /&(nbsp|amp|quot|lt|gt);/g
-const NUMSTR_RE = /(\d+);/gi
-export function decodeHtmlEntities (html: string) {
- const translateDict = {
- nbsp: ' ',
- amp: '&',
- quot: '"',
- lt: '<',
- gt: '>'
- } as const
- return html.replace(TRANSLATE_RE, function (_, entity: keyof typeof translateDict) {
- return translateDict[entity]
- }).replace(NUMSTR_RE, function (_, numStr: string) {
- const num = parseInt(numStr, 10)
- return String.fromCharCode(num)
- })
-}
-
/**
* helper for NuxtIsland to generate a correct array for scoped data
*/
@@ -113,7 +94,7 @@ export function vforToArray (source: any): any[] {
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
return Array.from(source as Iterable, item =>
- item
+ item,
)
} else {
const keys = Object.keys(source)
diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts
index 79a7813ba9..8ca773a338 100644
--- a/packages/nuxt/src/app/composables/asyncData.ts
+++ b/packages/nuxt/src/app/composables/asyncData.ts
@@ -1,4 +1,4 @@
-import { getCurrentInstance, onBeforeMount, onServerPrefetch, onUnmounted, ref, shallowRef, toRef, unref, watch } from 'vue'
+import { computed, getCurrentInstance, getCurrentScope, onBeforeMount, onScopeDispose, onServerPrefetch, onUnmounted, ref, shallowRef, toRef, unref, watch } from 'vue'
import type { Ref, WatchSource } from 'vue'
import type { NuxtApp } from '../nuxt'
import { useNuxtApp } from '../nuxt'
@@ -8,39 +8,44 @@ import { createError } from './error'
import { onNuxtReady } from './ready'
// @ts-expect-error virtual file
-import { asyncDataDefaults } from '#build/nuxt.config.mjs'
+import { asyncDataDefaults, resetAsyncDataToUndefined } from '#build/nuxt.config.mjs'
+
+// TODO: temporary module for backwards compatibility
+import type { DedupeOption, DefaultAsyncDataErrorValue, DefaultAsyncDataValue } from '#app/defaults'
export type AsyncDataRequestStatus = 'idle' | 'pending' | 'success' | 'error'
-export type _Transform = (input: Input) => Output
+export type _Transform = (input: Input) => Output | Promise