feat(nuxt): namespace __NUXT__ when using multi-app (#27263)

This commit is contained in:
Nicolas Payot 2024-08-19 23:16:03 +02:00 committed by Daniel Roe
parent 2fcadda2e5
commit 1eb9b0a402
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
9 changed files with 62 additions and 34 deletions

View File

@ -9,7 +9,7 @@ import { useRoute } from './router'
import { getAppManifest, getRouteRules } from './manifest' import { getAppManifest, getRouteRules } from './manifest'
// @ts-expect-error virtual import // @ts-expect-error virtual import
import { appManifest, payloadExtraction, renderJsonPayloads } from '#build/nuxt.config.mjs' import { appId, appManifest, multiApp, payloadExtraction, renderJsonPayloads } from '#build/nuxt.config.mjs'
interface LoadPayloadOptions { interface LoadPayloadOptions {
fresh?: boolean fresh?: boolean
@ -107,7 +107,7 @@ export async function getNuxtClientPayload () {
return payloadCache return payloadCache
} }
const el = document.getElementById('__NUXT_DATA__') const el = multiApp ? document.querySelector(`[data-nuxt-data="${appId}"]`) as HTMLElement : document.getElementById('__NUXT_DATA__')
if (!el) { if (!el) {
return {} as Partial<NuxtPayload> return {} as Partial<NuxtPayload>
} }
@ -119,7 +119,7 @@ export async function getNuxtClientPayload () {
payloadCache = { payloadCache = {
...inlineData, ...inlineData,
...externalData, ...externalData,
...window.__NUXT__, ...(multiApp ? window.__NUXT__?.[appId] : window.__NUXT__),
} }
if (payloadCache!.config?.public) { if (payloadCache!.config?.public) {

View File

@ -15,7 +15,7 @@ import plugins from '#build/plugins'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import RootComponent from '#build/root-component.mjs' import RootComponent from '#build/root-component.mjs'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { vueAppRootContainer } from '#build/nuxt.config.mjs' import { appId, multiApp, vueAppRootContainer } from '#build/nuxt.config.mjs'
let entry: (ssrContext?: CreateOptions['ssrContext']) => Promise<App<Element>> let entry: (ssrContext?: CreateOptions['ssrContext']) => Promise<App<Element>>
@ -50,9 +50,10 @@ if (import.meta.client) {
entry = async function initApp () { entry = async function initApp () {
if (vueAppPromise) { return vueAppPromise } if (vueAppPromise) { return vueAppPromise }
const isSSR = Boolean( const isSSR = Boolean(
window.__NUXT__?.serverRendered || (multiApp ? window.__NUXT__?.[appId] : window.__NUXT__)?.serverRendered ??
document.getElementById('__NUXT_DATA__')?.dataset.ssr === 'true', (multiApp ? document.querySelector(`[data-nuxt-data="${appId}"]`) as HTMLElement : document.getElementById('__NUXT_DATA__'))?.dataset.ssr === 'true',
) )
const vueApp = isSSR ? createSSRApp(RootComponent) : createApp(RootComponent) const vueApp = isSSR ? createSSRApp(RootComponent) : createApp(RootComponent)

View File

@ -21,7 +21,7 @@ import type { RouteAnnouncer } from '../app/composables/route-announcer'
import type { ViewTransition } from './plugins/view-transitions.client' import type { ViewTransition } from './plugins/view-transitions.client'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appId } from '#build/nuxt.config.mjs' import { appId, multiApp } from '#build/nuxt.config.mjs'
// TODO: temporary module for backwards compatibility // TODO: temporary module for backwards compatibility
import type { DefaultAsyncDataErrorValue, DefaultErrorValue } from '#app/defaults' import type { DefaultAsyncDataErrorValue, DefaultErrorValue } from '#app/defaults'
@ -315,19 +315,22 @@ export function createNuxtApp (options: CreateOptions) {
nuxtApp.payload.serverRendered = true nuxtApp.payload.serverRendered = true
} }
if (import.meta.client) {
const __NUXT__ = multiApp ? window.__NUXT__?.[nuxtApp._id] : window.__NUXT__
// TODO: remove/refactor in https://github.com/nuxt/nuxt/issues/25336 // TODO: remove/refactor in https://github.com/nuxt/nuxt/issues/25336
if (import.meta.client && window.__NUXT__) { if (__NUXT__) {
for (const key in window.__NUXT__) { for (const key in __NUXT__) {
switch (key) { switch (key) {
case 'data': case 'data':
case 'state': case 'state':
case '_errors': case '_errors':
// Preserve reactivity for non-rich payload support // Preserve reactivity for non-rich payload support
Object.assign(nuxtApp.payload[key], window.__NUXT__[key]) Object.assign(nuxtApp.payload[key], __NUXT__[key])
break break
default: default:
nuxtApp.payload[key] = window.__NUXT__[key] nuxtApp.payload[key] = __NUXT__[key]
}
} }
} }
} }

View File

@ -40,7 +40,8 @@ export default defineNuxtPlugin(async (nuxtApp) => {
} }
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const content = document.getElementById('__NUXT_LOGS__')?.textContent const nuxtLogsElement = document.querySelector(`[data-nuxt-logs="${nuxtApp._name}"]`)
const content = nuxtLogsElement?.textContent
const logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : [] const logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : []
await nuxtApp.hooks.callHook('dev:ssr-logs', logs) await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
} }

View File

@ -26,7 +26,7 @@ declare global {
} }
interface Window { interface Window {
__NUXT__?: Record<string, any> __NUXT__?: Record<string, any> | Record<string, Record<string, any>>
useNuxtApp?: typeof useNuxtApp useNuxtApp?: typeof useNuxtApp
} }
} }

View File

@ -13,6 +13,8 @@ import type { NitroApp } from '#internal/nitro/app'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { rootDir } from '#internal/dev-server-logs-options' import { rootDir } from '#internal/dev-server-logs-options'
// @ts-expect-error virtual file
import { appId } from '#internal/nuxt.config.mjs'
const devReducers: Record<string, (data: any) => any> = { const devReducers: Record<string, (data: any) => any> = {
VNode: data => isVNode(data) ? { type: data.type, props: data.props } : undefined, VNode: data => isVNode(data) ? { type: data.type, props: data.props } : undefined,
@ -75,7 +77,7 @@ export default (nitroApp: NitroApp) => {
const ctx = asyncContext.tryUse() const ctx = asyncContext.tryUse()
if (!ctx) { return } if (!ctx) { return }
try { try {
htmlContext.bodyAppend.unshift(`<script type="application/json" id="__NUXT_LOGS__">${stringify(ctx.logs, { ...devReducers, ...ctx.event.context._payloadReducers })}</script>`) htmlContext.bodyAppend.unshift(`<script type="application/json" data-nuxt-logs="${appId}">${stringify(ctx.logs, { ...devReducers, ...ctx.event.context._payloadReducers })}</script>`)
} catch (e) { } catch (e) {
const shortError = e instanceof Error && 'toString' in e ? ` Received \`${e.toString()}\`.` : '' const shortError = e instanceof Error && 'toString' in e ? ` Received \`${e.toString()}\`.` : ''
console.warn(`[nuxt] Failed to stringify dev server logs.${shortError} You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.`) console.warn(`[nuxt] Failed to stringify dev server logs.${shortError} You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.`)

View File

@ -32,7 +32,7 @@ import { renderSSRHeadOptions } from '#internal/unhead.config.mjs'
import type { NuxtPayload, NuxtSSRContext } from '#app' import type { NuxtPayload, NuxtSSRContext } from '#app'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { appHead, appRootAttrs, appRootTag, appTeleportAttrs, appTeleportTag, componentIslands } from '#internal/nuxt.config.mjs' import { appHead, appId, appRootAttrs, appRootTag, appTeleportAttrs, appTeleportTag, componentIslands, multiApp } from '#internal/nuxt.config.mjs'
// @ts-expect-error virtual file // @ts-expect-error virtual file
import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths' import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths'
@ -428,10 +428,10 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
head.push({ head.push({
script: _PAYLOAD_EXTRACTION script: _PAYLOAD_EXTRACTION
? process.env.NUXT_JSON_PAYLOADS ? process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL }) ? renderPayloadJsonScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL }) : renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
: process.env.NUXT_JSON_PAYLOADS : process.env.NUXT_JSON_PAYLOADS
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload }) ? renderPayloadJsonScript({ ssrContext, data: ssrContext.payload })
: renderPayloadScript({ ssrContext, data: ssrContext.payload }), : renderPayloadScript({ ssrContext, data: ssrContext.payload }),
}, { }, {
...headEntryOptions, ...headEntryOptions,
@ -587,21 +587,27 @@ function renderPayloadResponse (ssrContext: NuxtSSRContext) {
} satisfies RenderResponse } satisfies RenderResponse
} }
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] { function renderPayloadJsonScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : '' const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
const payload: Script = { const payload: Script = {
'type': 'application/json', 'type': 'application/json',
'id': opts.id,
'innerHTML': contents, 'innerHTML': contents,
'data-nuxt-data': appId,
'data-ssr': !(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR), 'data-ssr': !(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR),
} }
if (!multiApp) {
payload.id = '__NUXT_DATA__'
}
if (opts.src) { if (opts.src) {
payload['data-src'] = opts.src payload['data-src'] = opts.src
} }
const config = uneval(opts.ssrContext.config)
return [ return [
payload, payload,
{ {
innerHTML: `window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}`, innerHTML: multiApp
? `window.__NUXT__=window.__NUXT__||{};window.__NUXT__[${JSON.stringify(appId)}]={config:${config}}`
: `window.__NUXT__={};window.__NUXT__.config=${config}`,
}, },
] ]
} }
@ -609,17 +615,22 @@ function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] { function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
opts.data.config = opts.ssrContext.config opts.data.config = opts.ssrContext.config
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
const nuxtData = devalue(opts.data)
if (_PAYLOAD_EXTRACTION) { if (_PAYLOAD_EXTRACTION) {
const singleAppPayload = `import p from "${opts.src}";window.__NUXT__={...p,...(${nuxtData})}`
const multiAppPayload = `import p from "${opts.src}";window.__NUXT__=window.__NUXT__||{};window.__NUXT__[${JSON.stringify(appId)}]={...p,...(${nuxtData})}`
return [ return [
{ {
type: 'module', type: 'module',
innerHTML: `import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})}`, innerHTML: multiApp ? multiAppPayload : singleAppPayload,
}, },
] ]
} }
const singleAppPayload = `window.__NUXT__=${nuxtData}`
const multiAppPayload = `window.__NUXT__=window.__NUXT__||{};window.__NUXT__[${JSON.stringify(appId)}]=${nuxtData}`
return [ return [
{ {
innerHTML: `window.__NUXT__=${devalue(opts.data)}`, innerHTML: multiApp ? multiAppPayload : singleAppPayload,
}, },
] ]
} }

View File

@ -304,9 +304,16 @@ declare module 'nitropack' {
export const clientConfigTemplate: NuxtTemplate = { export const clientConfigTemplate: NuxtTemplate = {
filename: 'nitro.client.mjs', filename: 'nitro.client.mjs',
getContents: () => ` getContents: ({ nuxt }) => {
export const useRuntimeConfig = () => window?.__NUXT__?.config || {} const appId = JSON.stringify(nuxt.options.appId)
`, return [
'export const useRuntimeConfig = () => ',
(!nuxt.options.future.multiApp
? 'window?.__NUXT__?.config || {}'
: `window?.__NUXT__?.[${appId}]?.config || {}`)
|| {},
].join('\n')
},
} }
export const appConfigDeclarationTemplate: NuxtTemplate = { export const appConfigDeclarationTemplate: NuxtTemplate = {
@ -454,6 +461,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
`export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`, `export const viewTransition = ${ctx.nuxt.options.experimental.viewTransition}`,
`export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`, `export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`,
`export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`, `export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`,
`export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`,
].join('\n\n') ].join('\n\n')
}, },
} }

View File

@ -114,7 +114,9 @@ export function parseData (html: string) {
attrs: {}, attrs: {},
} }
} }
const { script, attrs = '' } = html.match(/<script type="application\/json" id="__NUXT_DATA__"(?<attrs>[^>]+)>(?<script>.*?)<\/script>/)?.groups || {}
const regexp = /<script type="application\/json" data-nuxt-data="[^"]+"(?<attrs>[^>]+)>(?<script>.*?)<\/script>/
const { script, attrs = '' } = html.match(regexp)?.groups || {}
const _attrs: Record<string, string> = {} const _attrs: Record<string, string> = {}
for (const attr of attrs.matchAll(/( |^)(?<key>[\w-]+)="(?<value>[^"]+)"/g)) { for (const attr of attrs.matchAll(/( |^)(?<key>[\w-]+)="(?<value>[^"]+)"/g)) {
_attrs[attr!.groups!.key!] = attr!.groups!.value! _attrs[attr!.groups!.key!] = attr!.groups!.value!