mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +00:00
feat(nuxt): experimental option for rich json payloads (#19205)
Co-authored-by: Daniel Roe <daniel@roe.dev>
This commit is contained in:
parent
d4f718d8cd
commit
9e503be0f2
@ -84,9 +84,7 @@ await nuxtApp.callHook('my-plugin:init')
|
|||||||
|
|
||||||
### `payload`
|
### `payload`
|
||||||
|
|
||||||
`payload` exposes data and state variables from server side to client side and makes them available in the `window.__NUXT__` object that is accessible from the browser.
|
`payload` exposes data and state variables from server side to client side. The following keys will be available on the client after they have been passed from the server side:
|
||||||
|
|
||||||
`payload` exposes the following keys on the client side after they are stringified and passed from the server side:
|
|
||||||
|
|
||||||
- **serverRendered** (boolean) - Indicates if response is server-side-rendered.
|
- **serverRendered** (boolean) - Indicates if response is server-side-rendered.
|
||||||
- **data** (object) - When you fetch the data from an API endpoint using either `useFetch` or `useAsyncData`, resulting payload can be accessed from the `payload.data`. This data is cached and helps you prevent fetching the same data in case an identical request is made more than once.
|
- **data** (object) - When you fetch the data from an API endpoint using either `useFetch` or `useAsyncData`, resulting payload can be accessed from the `payload.data`. This data is cached and helps you prevent fetching the same data in case an identical request is made more than once.
|
||||||
@ -115,6 +113,24 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
::alert
|
||||||
|
Normally `payload` must contain only plain JavaScript objects. But by setting `experimental.renderJsonPayloads`, it is possible to use more advanced types, such as `ref`, `reactive`, `shallowRef`, `shallowReactive` and `NuxtError`.
|
||||||
|
|
||||||
|
You can also add your own types. In future you will be able to add your own types easily with [object-syntax plugins](https://github.com/nuxt/nuxt/issues/14628). For now, you must add your plugin which calls both `definePayloadReducer` and `definePayloadReviver` via a custom module:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
modules: [
|
||||||
|
function (_options, nuxt) {
|
||||||
|
// TODO: support directly via object syntax plugins: https://github.com/nuxt/nuxt/issues/14628
|
||||||
|
nuxt.hook('modules:done', () => {
|
||||||
|
nuxt.options.plugins.unshift('~/plugins/custom-type-plugin')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
::
|
||||||
|
|
||||||
### `isHydrating`
|
### `isHydrating`
|
||||||
|
|
||||||
Use `nuxtApp.isHydrating` (boolean) to check if the Nuxt app is hydrating on the client side.
|
Use `nuxtApp.isHydrating` (boolean) to check if the Nuxt app is hydrating on the client side.
|
||||||
|
@ -74,6 +74,7 @@
|
|||||||
"cookie-es": "^0.5.0",
|
"cookie-es": "^0.5.0",
|
||||||
"defu": "^6.1.2",
|
"defu": "^6.1.2",
|
||||||
"destr": "^1.2.2",
|
"destr": "^1.2.2",
|
||||||
|
"devalue": "^4.3.0",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"fs-extra": "^11.1.1",
|
"fs-extra": "^11.1.1",
|
||||||
|
@ -118,7 +118,7 @@ export function useAsyncData<
|
|||||||
nuxt._asyncData[key] = {
|
nuxt._asyncData[key] = {
|
||||||
data: ref(getCachedData() ?? options.default?.() ?? null),
|
data: ref(getCachedData() ?? options.default?.() ?? null),
|
||||||
pending: ref(!hasCachedData()),
|
pending: ref(!hasCachedData()),
|
||||||
error: ref(nuxt.payload._errors[key] ? createError(nuxt.payload._errors[key]) : null)
|
error: toRef(nuxt.payload._errors, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: Else, somehow check for conflicting keys with different defaults or fetcher
|
// TODO: Else, somehow check for conflicting keys with different defaults or fetcher
|
||||||
|
@ -28,6 +28,6 @@ export { onNuxtReady } from './ready'
|
|||||||
export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter } from './router'
|
export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter } from './router'
|
||||||
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
|
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
|
||||||
export { preloadComponents, prefetchComponents, preloadRouteComponents } from './preload'
|
export { preloadComponents, prefetchComponents, preloadRouteComponents } from './preload'
|
||||||
export { isPrerendered, loadPayload, preloadPayload } from './payload'
|
export { isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver } from './payload'
|
||||||
export type { ReloadNuxtAppOptions } from './chunk'
|
export type { ReloadNuxtAppOptions } from './chunk'
|
||||||
export { reloadNuxtApp } from './chunk'
|
export { reloadNuxtApp } from './chunk'
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { joinURL, hasProtocol } from 'ufo'
|
import { joinURL, hasProtocol } from 'ufo'
|
||||||
|
import { parse } from 'devalue'
|
||||||
import { useHead } from '@unhead/vue'
|
import { useHead } from '@unhead/vue'
|
||||||
|
import { getCurrentInstance } from 'vue'
|
||||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||||
|
|
||||||
|
// @ts-expect-error virtual import
|
||||||
|
import { renderJsonPayloads } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
interface LoadPayloadOptions {
|
interface LoadPayloadOptions {
|
||||||
fresh?: boolean
|
fresh?: boolean
|
||||||
hash?: string
|
hash?: string
|
||||||
@ -36,6 +41,7 @@ export function preloadPayload (url: string, opts: LoadPayloadOptions = {}) {
|
|||||||
|
|
||||||
// --- Internal ---
|
// --- Internal ---
|
||||||
|
|
||||||
|
const extension = renderJsonPayloads ? 'json' : 'js'
|
||||||
function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
||||||
const u = new URL(url, 'http://localhost')
|
const u = new URL(url, 'http://localhost')
|
||||||
if (u.search) {
|
if (u.search) {
|
||||||
@ -45,15 +51,19 @@ function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
|||||||
throw new Error('Payload URL must not include hostname: ' + url)
|
throw new Error('Payload URL must not include hostname: ' + url)
|
||||||
}
|
}
|
||||||
const hash = opts.hash || (opts.fresh ? Date.now() : '')
|
const hash = opts.hash || (opts.fresh ? Date.now() : '')
|
||||||
return joinURL(useRuntimeConfig().app.baseURL, u.pathname, hash ? `_payload.${hash}.js` : '_payload.js')
|
return joinURL(useRuntimeConfig().app.baseURL, u.pathname, hash ? `_payload.${hash}.${extension}` : `_payload.${extension}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _importPayload (payloadURL: string) {
|
async function _importPayload (payloadURL: string) {
|
||||||
if (process.server) { return null }
|
if (process.server) { return null }
|
||||||
const res = await import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).catch((err) => {
|
try {
|
||||||
|
return renderJsonPayloads
|
||||||
|
? parsePayload(await fetch(payloadURL).then(res => res.text()))
|
||||||
|
: await import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).then(r => r.default || r)
|
||||||
|
} catch (err) {
|
||||||
console.warn('[nuxt] Cannot load payload ', payloadURL, err)
|
console.warn('[nuxt] Cannot load payload ', payloadURL, err)
|
||||||
})
|
}
|
||||||
return res?.default || null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPrerendered () {
|
export function isPrerendered () {
|
||||||
@ -61,3 +71,63 @@ export function isPrerendered () {
|
|||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
return !!nuxtApp.payload.prerenderedAt
|
return !!nuxtApp.payload.prerenderedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let payloadCache: any = null
|
||||||
|
export async function getNuxtClientPayload () {
|
||||||
|
if (process.server) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (payloadCache) {
|
||||||
|
return payloadCache
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.getElementById('__NUXT_DATA__')
|
||||||
|
if (!el) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inlineData = parsePayload(el.textContent || '')
|
||||||
|
|
||||||
|
const externalData = el.dataset.src ? await _importPayload(el.dataset.src) : undefined
|
||||||
|
|
||||||
|
payloadCache = {
|
||||||
|
...inlineData,
|
||||||
|
...externalData,
|
||||||
|
...window.__NUXT__
|
||||||
|
}
|
||||||
|
|
||||||
|
return payloadCache
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePayload (payload: string) {
|
||||||
|
return parse(payload, useNuxtApp()._payloadRevivers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an experimental function for configuring passing rich data from server -> client.
|
||||||
|
*/
|
||||||
|
export function definePayloadReducer (
|
||||||
|
name: string,
|
||||||
|
reduce: (data: any) => any
|
||||||
|
) {
|
||||||
|
if (process.server) {
|
||||||
|
useNuxtApp().ssrContext!._payloadReducers[name] = reduce
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an experimental function for configuring passing rich data from server -> client.
|
||||||
|
*
|
||||||
|
* This function _must_ be called in a Nuxt plugin that is `unshift`ed to the beginning of the Nuxt plugins array.
|
||||||
|
*/
|
||||||
|
export function definePayloadReviver (
|
||||||
|
name: string,
|
||||||
|
revive: (data: string) => any | undefined
|
||||||
|
) {
|
||||||
|
if (process.dev && getCurrentInstance()) {
|
||||||
|
console.warn('[nuxt] [definePayloadReviver] This function must be called in a Nuxt plugin that is `unshift`ed to the beginning of the Nuxt plugins array.')
|
||||||
|
}
|
||||||
|
if (process.client) {
|
||||||
|
useNuxtApp()._payloadRevivers[name] = revive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -52,7 +52,10 @@ if (process.client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry = async function initApp () {
|
entry = async function initApp () {
|
||||||
const isSSR = Boolean(window.__NUXT__?.serverRendered)
|
const isSSR = Boolean(
|
||||||
|
window.__NUXT__?.serverRendered ||
|
||||||
|
document.getElementById('__NUXT_DATA__')?.dataset.ssr === 'true'
|
||||||
|
)
|
||||||
const vueApp = isSSR ? createSSRApp(RootComponent) : createApp(RootComponent)
|
const vueApp = isSSR ? createSSRApp(RootComponent) : createApp(RootComponent)
|
||||||
|
|
||||||
const nuxt = createNuxtApp({ vueApp })
|
const nuxt = createNuxtApp({ vueApp })
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-use-before-define */
|
/* eslint-disable no-use-before-define */
|
||||||
import { getCurrentInstance, reactive } from 'vue'
|
import { getCurrentInstance, shallowReactive, reactive } from 'vue'
|
||||||
import type { App, onErrorCaptured, VNode, Ref } from 'vue'
|
import type { App, onErrorCaptured, VNode, Ref } from 'vue'
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
import type { Hookable, HookCallback } from 'hookable'
|
import type { Hookable, HookCallback } from 'hookable'
|
||||||
@ -12,6 +12,7 @@ import type { RuntimeConfig, AppConfigInput, AppConfig } from 'nuxt/schema'
|
|||||||
// eslint-disable-next-line import/no-restricted-paths
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
|
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
|
||||||
import type { RouteMiddleware } from '../../app'
|
import type { RouteMiddleware } from '../../app'
|
||||||
|
import type { NuxtError } from '../app/composables/error'
|
||||||
|
|
||||||
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
|
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
|
||||||
|
|
||||||
@ -58,6 +59,8 @@ export interface NuxtSSRContext extends SSRContext {
|
|||||||
teleports?: Record<string, string>
|
teleports?: Record<string, string>
|
||||||
renderMeta?: () => Promise<NuxtMeta> | NuxtMeta
|
renderMeta?: () => Promise<NuxtMeta> | NuxtMeta
|
||||||
islandContext?: NuxtIslandContext
|
islandContext?: NuxtIslandContext
|
||||||
|
/** @internal */
|
||||||
|
_payloadReducers: Record<string, (data: any) => any>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface _NuxtApp {
|
interface _NuxtApp {
|
||||||
@ -99,6 +102,9 @@ interface _NuxtApp {
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
_islandPromises?: Record<string, Promise<any>>
|
_islandPromises?: Record<string, Promise<any>>
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_payloadRevivers: Record<string, (data: any) => any>
|
||||||
|
|
||||||
// Nuxt injections
|
// Nuxt injections
|
||||||
$config: RuntimeConfig
|
$config: RuntimeConfig
|
||||||
|
|
||||||
@ -111,7 +117,6 @@ interface _NuxtApp {
|
|||||||
prerenderedAt?: number
|
prerenderedAt?: number
|
||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
state: Record<string, any>
|
state: Record<string, any>
|
||||||
rendered?: Function
|
|
||||||
error?: Error | {
|
error?: Error | {
|
||||||
url: string
|
url: string
|
||||||
statusCode: number
|
statusCode: number
|
||||||
@ -120,6 +125,7 @@ interface _NuxtApp {
|
|||||||
description: string
|
description: string
|
||||||
data?: any
|
data?: any
|
||||||
} | null
|
} | null
|
||||||
|
_errors: Record<string, NuxtError | undefined>
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
static: {
|
static: {
|
||||||
@ -152,11 +158,11 @@ export function createNuxtApp (options: CreateOptions) {
|
|||||||
get nuxt () { return __NUXT_VERSION__ },
|
get nuxt () { return __NUXT_VERSION__ },
|
||||||
get vue () { return nuxtApp.vueApp.version }
|
get vue () { return nuxtApp.vueApp.version }
|
||||||
},
|
},
|
||||||
payload: reactive({
|
payload: shallowReactive({
|
||||||
data: {},
|
data: shallowReactive({}),
|
||||||
state: {},
|
state: shallowReactive({}),
|
||||||
_errors: {},
|
_errors: shallowReactive({}),
|
||||||
...(process.client ? window.__NUXT__ : { serverRendered: true })
|
...(process.client ? window.__NUXT__ ?? {} : { serverRendered: true })
|
||||||
}),
|
}),
|
||||||
static: {
|
static: {
|
||||||
data: {}
|
data: {}
|
||||||
@ -182,6 +188,7 @@ export function createNuxtApp (options: CreateOptions) {
|
|||||||
},
|
},
|
||||||
_asyncDataPromises: {},
|
_asyncDataPromises: {},
|
||||||
_asyncData: {},
|
_asyncData: {},
|
||||||
|
_payloadRevivers: {},
|
||||||
...options
|
...options
|
||||||
} as any as NuxtApp
|
} as any as NuxtApp
|
||||||
|
|
||||||
@ -217,7 +224,11 @@ export function createNuxtApp (options: CreateOptions) {
|
|||||||
if (nuxtApp.ssrContext) {
|
if (nuxtApp.ssrContext) {
|
||||||
nuxtApp.ssrContext.nuxt = nuxtApp
|
nuxtApp.ssrContext.nuxt = nuxtApp
|
||||||
}
|
}
|
||||||
// Expose to server renderer to create window.__NUXT__
|
// Expose payload types
|
||||||
|
if (nuxtApp.ssrContext) {
|
||||||
|
nuxtApp.ssrContext._payloadReducers = {}
|
||||||
|
}
|
||||||
|
// Expose to server renderer to create payload
|
||||||
nuxtApp.ssrContext = nuxtApp.ssrContext || {} as any
|
nuxtApp.ssrContext = nuxtApp.ssrContext || {} as any
|
||||||
if (nuxtApp.ssrContext!.payload) {
|
if (nuxtApp.ssrContext!.payload) {
|
||||||
Object.assign(nuxtApp.payload, nuxtApp.ssrContext!.payload)
|
Object.assign(nuxtApp.payload, nuxtApp.ssrContext!.payload)
|
||||||
@ -225,7 +236,7 @@ export function createNuxtApp (options: CreateOptions) {
|
|||||||
nuxtApp.ssrContext!.payload = nuxtApp.payload
|
nuxtApp.ssrContext!.payload = nuxtApp.payload
|
||||||
|
|
||||||
// Expose client runtime-config to the payload
|
// Expose client runtime-config to the payload
|
||||||
nuxtApp.payload.config = {
|
nuxtApp.ssrContext!.config = {
|
||||||
public: options.ssrContext!.runtimeConfig.public,
|
public: options.ssrContext!.runtimeConfig.public,
|
||||||
app: options.ssrContext!.runtimeConfig.app
|
app: options.ssrContext!.runtimeConfig.app
|
||||||
}
|
}
|
||||||
|
23
packages/nuxt/src/app/plugins/revive-payload.client.ts
Normal file
23
packages/nuxt/src/app/plugins/revive-payload.client.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { reactive, ref, shallowRef, shallowReactive } from 'vue'
|
||||||
|
import { definePayloadReviver, getNuxtClientPayload } from '#app/composables/payload'
|
||||||
|
import { createError } from '#app/composables/error'
|
||||||
|
import { callWithNuxt, defineNuxtPlugin } from '#app/nuxt'
|
||||||
|
|
||||||
|
const revivers = {
|
||||||
|
NuxtError: (data: any) => createError(data),
|
||||||
|
EmptyShallowRef: (data: any) => shallowRef(JSON.parse(data)),
|
||||||
|
EmptyRef: (data: any) => ref(JSON.parse(data)),
|
||||||
|
ShallowRef: (data: any) => shallowRef(data),
|
||||||
|
ShallowReactive: (data: any) => shallowReactive(data),
|
||||||
|
Ref: (data: any) => ref(data),
|
||||||
|
Reactive: (data: any) => reactive(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||||
|
for (const reviver in revivers) {
|
||||||
|
definePayloadReviver(reviver, revivers[reviver as keyof typeof revivers])
|
||||||
|
}
|
||||||
|
Object.assign(nuxtApp.payload, await callWithNuxt(nuxtApp, getNuxtClientPayload, []))
|
||||||
|
// For backwards compatibility - TODO: remove later
|
||||||
|
window.__NUXT__ = nuxtApp.payload
|
||||||
|
})
|
21
packages/nuxt/src/app/plugins/revive-payload.server.ts
Normal file
21
packages/nuxt/src/app/plugins/revive-payload.server.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { isShallow, isRef, isReactive, toRaw } from 'vue'
|
||||||
|
import { definePayloadReducer } from '#app/composables/payload'
|
||||||
|
import { isNuxtError } from '#app/composables/error'
|
||||||
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
|
/* Defining a plugin that will be used by the Nuxt framework. */
|
||||||
|
|
||||||
|
const reducers = {
|
||||||
|
NuxtError: (data: any) => isNuxtError(data) && data.toJSON(),
|
||||||
|
EmptyShallowRef: (data: any) => isRef(data) && isShallow(data) && !data.value && JSON.stringify(data.value),
|
||||||
|
EmptyRef: (data: any) => isRef(data) && !data.value && JSON.stringify(data.value),
|
||||||
|
ShallowRef: (data: any) => isRef(data) && isShallow(data) && data.value,
|
||||||
|
ShallowReactive: (data: any) => isReactive(data) && isShallow(data) && toRaw(data),
|
||||||
|
Ref: (data: any) => isRef(data) && data.value,
|
||||||
|
Reactive: (data: any) => isReactive(data) && toRaw(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
for (const reducer in reducers) {
|
||||||
|
definePayloadReducer(reducer, reducers[reducer as keyof typeof reducers])
|
||||||
|
}
|
||||||
|
})
|
@ -172,6 +172,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
|||||||
'process.env.NUXT_NO_SCRIPTS': !!nuxt.options.experimental.noScripts && !nuxt.options.dev,
|
'process.env.NUXT_NO_SCRIPTS': !!nuxt.options.experimental.noScripts && !nuxt.options.dev,
|
||||||
'process.env.NUXT_INLINE_STYLES': !!nuxt.options.experimental.inlineSSRStyles,
|
'process.env.NUXT_INLINE_STYLES': !!nuxt.options.experimental.inlineSSRStyles,
|
||||||
'process.env.NUXT_PAYLOAD_EXTRACTION': !!nuxt.options.experimental.payloadExtraction,
|
'process.env.NUXT_PAYLOAD_EXTRACTION': !!nuxt.options.experimental.payloadExtraction,
|
||||||
|
'process.env.NUXT_JSON_PAYLOADS': !!nuxt.options.experimental.renderJsonPayloads,
|
||||||
'process.env.NUXT_COMPONENT_ISLANDS': !!nuxt.options.experimental.componentIslands,
|
'process.env.NUXT_COMPONENT_ISLANDS': !!nuxt.options.experimental.componentIslands,
|
||||||
'process.dev': nuxt.options.dev,
|
'process.dev': nuxt.options.dev,
|
||||||
__VUE_PROD_DEVTOOLS__: false
|
__VUE_PROD_DEVTOOLS__: false
|
||||||
|
@ -281,6 +281,13 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/restore-state.client'))
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/restore-state.client'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nuxt.options.experimental.renderJsonPayloads) {
|
||||||
|
nuxt.hook('modules:done', () => {
|
||||||
|
nuxt.options.plugins.unshift(resolve(nuxt.options.appDir, 'plugins/revive-payload.client'))
|
||||||
|
nuxt.options.plugins.unshift(resolve(nuxt.options.appDir, 'plugins/revive-payload.server'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Track components used to render for webpack
|
// Track components used to render for webpack
|
||||||
if (nuxt.options.builder === '@nuxt/webpack-builder') {
|
if (nuxt.options.builder === '@nuxt/webpack-builder') {
|
||||||
addPlugin(resolve(nuxt.options.appDir, 'plugins/preload.server'))
|
addPlugin(resolve(nuxt.options.appDir, 'plugins/preload.server'))
|
||||||
|
@ -4,6 +4,7 @@ import type { Manifest } from 'vite'
|
|||||||
import type { H3Event } from 'h3'
|
import type { H3Event } from 'h3'
|
||||||
import { appendHeader, getQuery, writeEarlyHints, readBody, createError } from 'h3'
|
import { appendHeader, getQuery, writeEarlyHints, readBody, createError } from 'h3'
|
||||||
import devalue from '@nuxt/devalue'
|
import devalue from '@nuxt/devalue'
|
||||||
|
import { stringify, uneval } from 'devalue'
|
||||||
import destr from 'destr'
|
import destr from 'destr'
|
||||||
import { joinURL, withoutTrailingSlash } from 'ufo'
|
import { joinURL, withoutTrailingSlash } from 'ufo'
|
||||||
import { renderToString as _renderToString } from 'vue/server-renderer'
|
import { renderToString as _renderToString } from 'vue/server-renderer'
|
||||||
@ -121,6 +122,7 @@ const getSPARenderer = lazyCachedFunction(async () => {
|
|||||||
const renderToString = (ssrContext: NuxtSSRContext) => {
|
const renderToString = (ssrContext: NuxtSSRContext) => {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
ssrContext!.payload = {
|
ssrContext!.payload = {
|
||||||
|
_errors: {},
|
||||||
serverRendered: false,
|
serverRendered: false,
|
||||||
config: {
|
config: {
|
||||||
public: config.public,
|
public: config.public,
|
||||||
@ -160,7 +162,7 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
|
|||||||
|
|
||||||
const PAYLOAD_CACHE = (process.env.NUXT_PAYLOAD_EXTRACTION && process.env.prerender) ? new Map() : null // TODO: Use LRU cache
|
const PAYLOAD_CACHE = (process.env.NUXT_PAYLOAD_EXTRACTION && process.env.prerender) ? new Map() : null // TODO: Use LRU cache
|
||||||
const ISLAND_CACHE = (process.env.NUXT_COMPONENT_ISLANDS && process.env.prerender) ? new Map() : null // TODO: Use LRU cache
|
const ISLAND_CACHE = (process.env.NUXT_COMPONENT_ISLANDS && process.env.prerender) ? new Map() : null // TODO: Use LRU cache
|
||||||
const PAYLOAD_URL_RE = /\/_payload(\.[a-zA-Z0-9]+)?.js(\?.*)?$/
|
const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload(\.[a-zA-Z0-9]+)?.json(\?.*)?$/ : /\/_payload(\.[a-zA-Z0-9]+)?.js(\?.*)?$/
|
||||||
const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag} id="${appRootId}">([\\s\\S]*)</${appRootTag}>$`)
|
const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag} id="${appRootId}">([\\s\\S]*)</${appRootTag}>$`)
|
||||||
|
|
||||||
const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html'])
|
const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html'])
|
||||||
@ -219,12 +221,13 @@ export default defineRenderHandler(async (event) => {
|
|||||||
error: !!ssrError,
|
error: !!ssrError,
|
||||||
nuxt: undefined!, /* NuxtApp */
|
nuxt: undefined!, /* NuxtApp */
|
||||||
payload: (ssrError ? { error: ssrError } : {}) as NuxtSSRContext['payload'],
|
payload: (ssrError ? { error: ssrError } : {}) as NuxtSSRContext['payload'],
|
||||||
|
_payloadReducers: {},
|
||||||
islandContext
|
islandContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether we are prerendering route
|
// Whether we are prerendering route
|
||||||
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR
|
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR
|
||||||
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(useRuntimeConfig().app.baseURL, url, '_payload.js') : undefined
|
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(useRuntimeConfig().app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') : undefined
|
||||||
if (process.env.prerender) {
|
if (process.env.prerender) {
|
||||||
ssrContext.payload.prerenderedAt = Date.now()
|
ssrContext.payload.prerenderedAt = Date.now()
|
||||||
}
|
}
|
||||||
@ -260,7 +263,7 @@ export default defineRenderHandler(async (event) => {
|
|||||||
|
|
||||||
if (_PAYLOAD_EXTRACTION) {
|
if (_PAYLOAD_EXTRACTION) {
|
||||||
// Hint nitro to prerender payload for this route
|
// Hint nitro to prerender payload for this route
|
||||||
appendHeader(event, 'x-nitro-prerender', joinURL(url, '_payload.js'))
|
appendHeader(event, 'x-nitro-prerender', joinURL(url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js'))
|
||||||
// Use same ssr context to generate payload for this route
|
// Use same ssr context to generate payload for this route
|
||||||
PAYLOAD_CACHE!.set(withoutTrailingSlash(url), renderPayloadResponse(ssrContext))
|
PAYLOAD_CACHE!.set(withoutTrailingSlash(url), renderPayloadResponse(ssrContext))
|
||||||
}
|
}
|
||||||
@ -279,7 +282,9 @@ export default defineRenderHandler(async (event) => {
|
|||||||
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
|
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
|
||||||
head: normalizeChunks([
|
head: normalizeChunks([
|
||||||
renderedMeta.headTags,
|
renderedMeta.headTags,
|
||||||
_PAYLOAD_EXTRACTION ? `<link rel="modulepreload" href="${payloadURL}">` : null,
|
process.env.NUXT_JSON_PAYLOADS
|
||||||
|
? _PAYLOAD_EXTRACTION ? `<link rel="modulepreload" href="${payloadURL}">` : null
|
||||||
|
: _PAYLOAD_EXTRACTION ? `<link rel="preload" as="fetch" crossorigin="anonymous" href="${payloadURL}">` : null,
|
||||||
_rendered.renderResourceHints(),
|
_rendered.renderResourceHints(),
|
||||||
_rendered.renderStyles(),
|
_rendered.renderStyles(),
|
||||||
inlinedStyles,
|
inlinedStyles,
|
||||||
@ -295,8 +300,12 @@ export default defineRenderHandler(async (event) => {
|
|||||||
process.env.NUXT_NO_SCRIPTS
|
process.env.NUXT_NO_SCRIPTS
|
||||||
? undefined
|
? undefined
|
||||||
: (_PAYLOAD_EXTRACTION
|
: (_PAYLOAD_EXTRACTION
|
||||||
? `<script type="module">import p from "${payloadURL}";window.__NUXT__={...p,...(${devalue(splitPayload(ssrContext).initial)})}</script>`
|
? process.env.NUXT_JSON_PAYLOADS
|
||||||
: `<script>window.__NUXT__=${devalue(ssrContext.payload)}</script>`
|
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
||||||
|
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
||||||
|
: process.env.NUXT_JSON_PAYLOADS
|
||||||
|
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
|
||||||
|
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
|
||||||
),
|
),
|
||||||
_rendered.renderScripts(),
|
_rendered.renderScripts(),
|
||||||
// Note: bodyScripts may contain tags other than <script>
|
// Note: bodyScripts may contain tags other than <script>
|
||||||
@ -420,16 +429,39 @@ async function renderInlineStyles (usedModules: Set<string> | string[]) {
|
|||||||
|
|
||||||
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
||||||
return <RenderResponse> {
|
return <RenderResponse> {
|
||||||
body: `export default ${devalue(splitPayload(ssrContext).payload)}`,
|
body: process.env.NUXT_JSON_PAYLOADS
|
||||||
|
? stringify(splitPayload(ssrContext).payload, ssrContext._payloadReducers)
|
||||||
|
: `export default ${devalue(splitPayload(ssrContext).payload)}`,
|
||||||
statusCode: ssrContext.event.node.res.statusCode,
|
statusCode: ssrContext.event.node.res.statusCode,
|
||||||
statusMessage: ssrContext.event.node.res.statusMessage,
|
statusMessage: ssrContext.event.node.res.statusMessage,
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'text/javascript;charset=UTF-8',
|
'content-type': process.env.NUXT_JSON_PAYLOADS ? 'application/json;charset=utf-8' : 'text/javascript;charset=utf-8',
|
||||||
'x-powered-by': 'Nuxt'
|
'x-powered-by': 'Nuxt'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }) {
|
||||||
|
const attrs = [
|
||||||
|
'type="application/json"',
|
||||||
|
`id="${opts.id}"`,
|
||||||
|
`data-ssr="${!(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)}"`,
|
||||||
|
opts.src ? `data-src="${opts.src}"` : ''
|
||||||
|
].filter(Boolean)
|
||||||
|
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
|
||||||
|
return `<script ${attrs.join(' ')}>${contents}</script>` +
|
||||||
|
`<script>window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}</script>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }) {
|
||||||
|
opts.data.config = opts.ssrContext.config
|
||||||
|
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
|
||||||
|
if (_PAYLOAD_EXTRACTION) {
|
||||||
|
return `<script type="module">import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})}</script>`
|
||||||
|
}
|
||||||
|
return `<script>window.__NUXT__=${devalue(opts.data)}</script>`
|
||||||
|
}
|
||||||
|
|
||||||
function splitPayload (ssrContext: NuxtSSRContext) {
|
function splitPayload (ssrContext: NuxtSSRContext) {
|
||||||
const { data, prerenderedAt, ...initial } = ssrContext.payload
|
const { data, prerenderedAt, ...initial } = ssrContext.payload
|
||||||
return {
|
return {
|
||||||
|
@ -296,6 +296,7 @@ export const nuxtConfigTemplate = {
|
|||||||
getContents: (ctx: TemplateContext) => {
|
getContents: (ctx: TemplateContext) => {
|
||||||
return [
|
return [
|
||||||
...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`),
|
...Object.entries(ctx.nuxt.options.app).map(([k, v]) => `export const ${camelCase('app-' + k)} = ${JSON.stringify(v)}`),
|
||||||
|
`export const renderJsonPayloads = ${!!ctx.nuxt.options.experimental.renderJsonPayloads}`,
|
||||||
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`
|
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`
|
||||||
].join('\n\n')
|
].join('\n\n')
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,9 @@ const appPreset = defineUnimportPreset({
|
|||||||
'prefetchComponents',
|
'prefetchComponents',
|
||||||
'loadPayload',
|
'loadPayload',
|
||||||
'preloadPayload',
|
'preloadPayload',
|
||||||
'isPrerendered'
|
'isPrerendered',
|
||||||
|
'definePayloadReducer',
|
||||||
|
'definePayloadReviver'
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -170,6 +170,7 @@ export default defineUntypedSchema({
|
|||||||
$resolve: async (val, get) => defu(val || {},
|
$resolve: async (val, get) => defu(val || {},
|
||||||
await get('dev') ? {} : {
|
await get('dev') ? {} : {
|
||||||
vue: ['onBeforeMount', 'onMounted', 'onBeforeUpdate', 'onRenderTracked', 'onRenderTriggered', 'onActivated', 'onDeactivated', 'onBeforeUnmount'],
|
vue: ['onBeforeMount', 'onMounted', 'onBeforeUpdate', 'onRenderTracked', 'onRenderTriggered', 'onActivated', 'onDeactivated', 'onBeforeUnmount'],
|
||||||
|
'#app': ['definePayloadReviver']
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -177,6 +178,7 @@ export default defineUntypedSchema({
|
|||||||
$resolve: async (val, get) => defu(val || {},
|
$resolve: async (val, get) => defu(val || {},
|
||||||
await get('dev') ? {} : {
|
await get('dev') ? {} : {
|
||||||
vue: ['onServerPrefetch', 'onRenderTracked', 'onRenderTriggered'],
|
vue: ['onServerPrefetch', 'onRenderTracked', 'onRenderTriggered'],
|
||||||
|
'#app': ['definePayloadReducer']
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,10 @@ export default defineUntypedSchema({
|
|||||||
*/
|
*/
|
||||||
noScripts: false,
|
noScripts: false,
|
||||||
|
|
||||||
|
// TODO: enable by default in v3.5
|
||||||
|
/** Render JSON payloads with support for revivifying complex types. */
|
||||||
|
renderJsonPayloads: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable vue server renderer endpoint within nitro.
|
* Disable vue server renderer endpoint within nitro.
|
||||||
*/
|
*/
|
||||||
|
@ -563,6 +563,9 @@ importers:
|
|||||||
destr:
|
destr:
|
||||||
specifier: ^1.2.2
|
specifier: ^1.2.2
|
||||||
version: 1.2.2
|
version: 1.2.2
|
||||||
|
devalue:
|
||||||
|
specifier: ^4.3.0
|
||||||
|
version: 4.3.0
|
||||||
escape-string-regexp:
|
escape-string-regexp:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.0
|
version: 5.0.0
|
||||||
@ -4419,6 +4422,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
|
resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
/devalue@4.3.0:
|
||||||
|
resolution: {integrity: sha512-n94yQo4LI3w7erwf84mhRUkUJfhLoCZiLyoOZ/QFsDbcWNZePrLwbQpvZBUG2TNxwV3VjCKPxkiiQA6pe3TrTA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/diff@4.0.2:
|
/diff@4.0.2:
|
||||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
|
@ -7,7 +7,7 @@ import { setup, fetch, $fetch, startServer, isDev, createPage, url } from '@nuxt
|
|||||||
import { $fetchComponent } from '@nuxt/test-utils/experimental'
|
import { $fetchComponent } from '@nuxt/test-utils/experimental'
|
||||||
|
|
||||||
import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
|
import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer'
|
||||||
import { expectNoClientErrors, expectWithPolling, renderPage, withLogs } from './utils'
|
import { expectNoClientErrors, expectWithPolling, parseData, parsePayload, renderPage, withLogs } from './utils'
|
||||||
|
|
||||||
const isWebpack = process.env.TEST_BUILDER === 'webpack'
|
const isWebpack = process.env.TEST_BUILDER === 'webpack'
|
||||||
|
|
||||||
@ -43,7 +43,9 @@ describe('server api', () => {
|
|||||||
|
|
||||||
describe('route rules', () => {
|
describe('route rules', () => {
|
||||||
it('should enable spa mode', async () => {
|
it('should enable spa mode', async () => {
|
||||||
expect(await $fetch('/route-rules/spa')).toContain('serverRendered:false')
|
const { script, attrs } = parseData(await $fetch('/route-rules/spa'))
|
||||||
|
expect(script.serverRendered).toEqual(false)
|
||||||
|
expect(attrs['data-ssr']).toEqual('false')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -324,6 +326,23 @@ describe('pages', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('rich payloads', () => {
|
||||||
|
it('correctly serializes and revivifies complex types', async () => {
|
||||||
|
const html = await $fetch('/json-payload')
|
||||||
|
for (const test of [
|
||||||
|
'Date: true',
|
||||||
|
'Recursive objects: true',
|
||||||
|
'Shallow reactive: true',
|
||||||
|
'Shallow ref: true',
|
||||||
|
'Reactive: true',
|
||||||
|
'Ref: true',
|
||||||
|
'Error: true'
|
||||||
|
]) {
|
||||||
|
expect(html).toContain(test)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('nuxt links', () => {
|
describe('nuxt links', () => {
|
||||||
it('handles trailing slashes', async () => {
|
it('handles trailing slashes', async () => {
|
||||||
const html = await $fetch('/nuxt-link/trailing-slash')
|
const html = await $fetch('/nuxt-link/trailing-slash')
|
||||||
@ -467,7 +486,8 @@ describe('legacy async data', () => {
|
|||||||
it('should work with defineNuxtComponent', async () => {
|
it('should work with defineNuxtComponent', async () => {
|
||||||
const html = await $fetch('/legacy/async-data')
|
const html = await $fetch('/legacy/async-data')
|
||||||
expect(html).toContain('<div>Hello API</div>')
|
expect(html).toContain('<div>Hello API</div>')
|
||||||
expect(html).toContain('{hello:"Hello API"}')
|
const { script } = parseData(html)
|
||||||
|
expect(script.data['options:asyncdata:/legacy/async-data'].hello).toEqual('Hello API')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1205,10 +1225,28 @@ describe.runIf(isDev() && !isWebpack)('vite plugins', () => {
|
|||||||
|
|
||||||
describe.skipIf(isDev() || isWindows)('payload rendering', () => {
|
describe.skipIf(isDev() || isWindows)('payload rendering', () => {
|
||||||
it('renders a payload', async () => {
|
it('renders a payload', async () => {
|
||||||
const payload = await $fetch('/random/a/_payload.js', { responseType: 'text' })
|
const payload = await $fetch('/random/a/_payload.json', { responseType: 'text' })
|
||||||
expect(payload).toMatch(
|
const data = parsePayload(payload)
|
||||||
/export default \{data:\{hey:\{[^}]*\},rand_a:\[[^\]]*\],".*":\{html:".*server-only component.*",head:\{link:\[\],style:\[\]\}\}\},prerenderedAt:\d*\}/
|
expect(typeof data.prerenderedAt).toEqual('number')
|
||||||
)
|
|
||||||
|
const [_key, serverData] = Object.entries(data.data).find(([key]) => key.startsWith('ServerOnlyComponent'))!
|
||||||
|
expect(serverData).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"head": {
|
||||||
|
"link": [],
|
||||||
|
"style": [],
|
||||||
|
},
|
||||||
|
"html": "<div> server-only component </div>",
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(data.data).toMatchObject({
|
||||||
|
hey: {
|
||||||
|
baz: 'qux',
|
||||||
|
foo: 'bar'
|
||||||
|
},
|
||||||
|
rand_a: expect.arrayContaining([expect.anything()])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not fetch a prefetched payload', async () => {
|
it('does not fetch a prefetched payload', async () => {
|
||||||
@ -1222,10 +1260,8 @@ describe.skipIf(isDev() || isWindows)('payload rendering', () => {
|
|||||||
await page.goto(url('/random/a'))
|
await page.goto(url('/random/a'))
|
||||||
await page.waitForLoadState('networkidle')
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
const importSuffix = isDev() && !isWebpack ? '?import' : ''
|
|
||||||
|
|
||||||
// We are manually prefetching other payloads
|
// We are manually prefetching other payloads
|
||||||
expect(requests).toContain('/random/c/_payload.js')
|
expect(requests).toContain('/random/c/_payload.json')
|
||||||
|
|
||||||
// We are not triggering API requests in the payload
|
// We are not triggering API requests in the payload
|
||||||
expect(requests).not.toContain(expect.stringContaining('/api/random'))
|
expect(requests).not.toContain(expect.stringContaining('/api/random'))
|
||||||
@ -1240,7 +1276,7 @@ describe.skipIf(isDev() || isWindows)('payload rendering', () => {
|
|||||||
expect(requests).not.toContain(expect.stringContaining('/__nuxt_island'))
|
expect(requests).not.toContain(expect.stringContaining('/__nuxt_island'))
|
||||||
|
|
||||||
// We are fetching a payload we did not prefetch
|
// We are fetching a payload we did not prefetch
|
||||||
expect(requests).toContain('/random/b/_payload.js' + importSuffix)
|
expect(requests).toContain('/random/b/_payload.json')
|
||||||
|
|
||||||
// We are not refetching payloads we've already prefetched
|
// We are not refetching payloads we've already prefetched
|
||||||
// expect(requests.filter(p => p.includes('_payload')).length).toBe(1)
|
// expect(requests.filter(p => p.includes('_payload')).length).toBe(1)
|
||||||
|
@ -40,10 +40,10 @@ describe.skipIf(isWindows || process.env.ECOSYSTEM_CI)('minimal nuxt application
|
|||||||
|
|
||||||
it('default server bundle size', async () => {
|
it('default server bundle size', async () => {
|
||||||
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"91k"')
|
expect(roundToKilobytes(stats.server.totalBytes)).toMatchInlineSnapshot('"92k"')
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2632k"')
|
expect(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2648k"')
|
||||||
|
|
||||||
const packages = modules.files
|
const packages = modules.files
|
||||||
.filter(m => m.endsWith('package.json'))
|
.filter(m => m.endsWith('package.json'))
|
||||||
@ -66,6 +66,7 @@ describe.skipIf(isWindows || process.env.ECOSYSTEM_CI)('minimal nuxt application
|
|||||||
"cookie-es",
|
"cookie-es",
|
||||||
"defu",
|
"defu",
|
||||||
"destr",
|
"destr",
|
||||||
|
"devalue",
|
||||||
"estree-walker",
|
"estree-walker",
|
||||||
"h3",
|
"h3",
|
||||||
"hookable",
|
"hookable",
|
||||||
|
7
test/fixtures/basic/nuxt.config.ts
vendored
7
test/fixtures/basic/nuxt.config.ts
vendored
@ -108,6 +108,12 @@ export default defineNuxtConfig({
|
|||||||
addVitePlugin(plugin.vite())
|
addVitePlugin(plugin.vite())
|
||||||
addWebpackPlugin(plugin.webpack())
|
addWebpackPlugin(plugin.webpack())
|
||||||
},
|
},
|
||||||
|
function (_options, nuxt) {
|
||||||
|
// TODO: support directly via object syntax plugins: https://github.com/nuxt/nuxt/issues/14628
|
||||||
|
nuxt.hook('modules:done', () => {
|
||||||
|
nuxt.options.plugins.unshift('~/plugins/custom-type-registration')
|
||||||
|
})
|
||||||
|
},
|
||||||
function (_options, nuxt) {
|
function (_options, nuxt) {
|
||||||
const routesToDuplicate = ['/async-parent', '/fixed-keyed-child-parent', '/keyed-child-parent', '/with-layout', '/with-layout2']
|
const routesToDuplicate = ['/async-parent', '/fixed-keyed-child-parent', '/keyed-child-parent', '/with-layout', '/with-layout2']
|
||||||
const stripLayout = (page: NuxtPage): NuxtPage => ({
|
const stripLayout = (page: NuxtPage): NuxtPage => ({
|
||||||
@ -184,6 +190,7 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
renderJsonPayloads: true,
|
||||||
respectNoSSRHeader: true,
|
respectNoSSRHeader: true,
|
||||||
clientFallback: true,
|
clientFallback: true,
|
||||||
restoreState: true,
|
restoreState: true,
|
||||||
|
26
test/fixtures/basic/pages/json-payload.vue
vendored
Normal file
26
test/fixtures/basic/pages/json-payload.vue
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const state = useState(() => shallowRef({} as Record<string, any>))
|
||||||
|
|
||||||
|
if (process.server) {
|
||||||
|
const r = ref('')
|
||||||
|
state.value.ref = r
|
||||||
|
state.value.shallowReactive = shallowReactive({ nested: { ref: r } })
|
||||||
|
state.value.shallowRef = shallowRef(false)
|
||||||
|
state.value.reactive = reactive({ ref: r })
|
||||||
|
state.value.error = createError({ message: 'error' })
|
||||||
|
state.value.date = new Date()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<pre>{{ state }}</pre>
|
||||||
|
Date: {{ state.date instanceof Date }} <br>
|
||||||
|
Error: {{ isNuxtError(state.error) }} <hr>
|
||||||
|
Shallow reactive: {{ isReactive(state.shallowReactive) && isShallow(state.shallowReactive) }} <br>
|
||||||
|
Shallow ref: {{ isShallow(state.shallowRef) }} <br>
|
||||||
|
Reactive: {{ isReactive(state.reactive) }} <br>
|
||||||
|
Ref: {{ isRef(state.ref) }} <hr>
|
||||||
|
Recursive objects: {{ state.ref === state.shallowReactive.nested.ref }} <br>
|
||||||
|
</div>
|
||||||
|
</template>
|
7
test/fixtures/basic/plugins/custom-type-assertion.client.ts
vendored
Normal file
7
test/fixtures/basic/plugins/custom-type-assertion.client.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
if (nuxtApp.payload.blinkable !== '<revivified-blink>') {
|
||||||
|
throw createError({
|
||||||
|
message: 'Custom type in Nuxt payload was not revived correctly'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
7
test/fixtures/basic/plugins/custom-type-registration.ts
vendored
Normal file
7
test/fixtures/basic/plugins/custom-type-registration.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
definePayloadReducer('BlinkingText', data => data === '<original-blink>' && '_')
|
||||||
|
definePayloadReviver('BlinkingText', () => '<revivified-blink>')
|
||||||
|
if (process.server) {
|
||||||
|
nuxtApp.payload.blinkable = '<original-blink>'
|
||||||
|
}
|
||||||
|
})
|
@ -1,5 +1,8 @@
|
|||||||
import { expect } from 'vitest'
|
import { expect } from 'vitest'
|
||||||
import type { Page } from 'playwright'
|
import type { Page } from 'playwright'
|
||||||
|
import { parse } from 'devalue'
|
||||||
|
import { shallowReactive, shallowRef, reactive, ref } from 'vue'
|
||||||
|
import { createError } from 'h3'
|
||||||
import { createPage, getBrowser, url, useTestContext } from '@nuxt/test-utils'
|
import { createPage, getBrowser, url, useTestContext } from '@nuxt/test-utils'
|
||||||
|
|
||||||
export async function renderPage (path = '/') {
|
export async function renderPage (path = '/') {
|
||||||
@ -89,3 +92,29 @@ export async function withLogs (callback: (page: Page, logs: string[]) => Promis
|
|||||||
await page.close()
|
await page.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const revivers = {
|
||||||
|
NuxtError: (data: any) => createError(data),
|
||||||
|
EmptyShallowRef: (data: any) => shallowRef(JSON.parse(data)),
|
||||||
|
EmptyRef: (data: any) => ref(JSON.parse(data)),
|
||||||
|
ShallowRef: (data: any) => shallowRef(data),
|
||||||
|
ShallowReactive: (data: any) => shallowReactive(data),
|
||||||
|
Ref: (data: any) => ref(data),
|
||||||
|
Reactive: (data: any) => reactive(data),
|
||||||
|
// test fixture reviver only
|
||||||
|
BlinkingText: () => '<revivified-blink>'
|
||||||
|
}
|
||||||
|
export function parsePayload (payload: string) {
|
||||||
|
return parse(payload || '', revivers)
|
||||||
|
}
|
||||||
|
export function parseData (html: string) {
|
||||||
|
const { script, attrs } = html.match(/<script type="application\/json" id="__NUXT_DATA__"(?<attrs>[^>]+)>(?<script>.*?)<\/script>/)?.groups || {}
|
||||||
|
const _attrs: Record<string, string> = {}
|
||||||
|
for (const attr of attrs.matchAll(/( |^)(?<key>[\w-]+)+="(?<value>[^"]+)"/g)) {
|
||||||
|
_attrs[attr!.groups!.key] = attr!.groups!.value
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
script: parsePayload(script || ''),
|
||||||
|
attrs: _attrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user