From 6e3d7e970d2447bc9267eb974c8318527f046264 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 28 Jun 2022 15:37:50 +0100 Subject: [PATCH 01/73] refactor: split out renderMeta into separate function --- packages/nuxt/src/core/runtime/nitro/renderer.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 3562c6cabe9..8eed98d444b 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -148,18 +148,22 @@ export default eventHandler(async (event) => { await ssrContext.nuxt.hooks.callHook('app:rendered') } + await renderMeta(rendered, ssrContext) + const html = await renderHTML(ssrContext.payload, rendered, ssrContext) event.res.setHeader('Content-Type', 'text/html;charset=UTF-8') return html }) -async function renderHTML (payload: any, rendered: RenderResult, ssrContext: NuxtSSRContext) { - const state = `` - +async function renderMeta (rendered: RenderResult, ssrContext: NuxtSSRContext) { rendered.meta = rendered.meta || {} if (ssrContext.renderMeta) { Object.assign(rendered.meta, await ssrContext.renderMeta()) } +} + +function renderHTML (payload: any, rendered: RenderResult, ssrContext: NuxtSSRContext) { + const state = `` return htmlTemplate({ HTML_ATTRS: (rendered.meta.htmlAttrs || ''), From 488d3ba016614fba15c603d3a726563032369320 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 28 Jun 2022 16:05:14 +0100 Subject: [PATCH 02/73] feat: support rendering individual components --- .../nuxt/src/app/components/nuxt-root.vue | 7 +++++- .../src/app/components/render-components.ts | 14 ++++++++++++ packages/nuxt/src/app/nuxt.ts | 4 ++++ .../nuxt/src/core/runtime/nitro/renderer.ts | 22 ++++++++++++++++++- 4 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 packages/nuxt/src/app/components/render-components.ts diff --git a/packages/nuxt/src/app/components/nuxt-root.vue b/packages/nuxt/src/app/components/nuxt-root.vue index 9279c810129..5bc2b4e78ac 100644 --- a/packages/nuxt/src/app/components/nuxt-root.vue +++ b/packages/nuxt/src/app/components/nuxt-root.vue @@ -1,12 +1,14 @@ diff --git a/packages/nuxt/src/app/components/render-components.ts b/packages/nuxt/src/app/components/render-components.ts new file mode 100644 index 00000000000..4ae3ba66c72 --- /dev/null +++ b/packages/nuxt/src/app/components/render-components.ts @@ -0,0 +1,14 @@ +import { createBlock, defineComponent, h, resolveComponent, Teleport } from 'vue' + +export default defineComponent({ + props: { + components: Array as () => Array<{ name: string, props?: Record }> + }, + setup (props) { + return () => props.components.map( + (c, index) => createBlock(Teleport as any, { to: `render-target-${index}` }, [ + h(resolveComponent(c.name), c.props) + ]) + ) + } +}) diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index e24f425684f..33b5c70a93b 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -62,6 +62,9 @@ interface _NuxtApp { payload: _NuxtApp['payload'] teleports?: Record renderMeta?: () => Promise | NuxtMeta + render?: { + components: Array<{ name: string, props?: Record }> + } } payload: { serverRendered?: boolean @@ -96,6 +99,7 @@ export function createNuxtApp (options: CreateOptions) { data: {}, state: {}, _errors: {}, + ...options.ssrContext?.payload || {}, ...(process.client ? window.__NUXT__ : { serverRendered: true }) }), isHydrating: process.client, diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 8eed98d444b..c954b5a1cd2 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -111,6 +111,7 @@ const getSPARenderer = lazyCachedFunction(async () => { export default eventHandler(async (event) => { // Whether we're rendering an error page const ssrError = event.req.url?.startsWith('/__nuxt_error') ? useQuery(event) : null + const customRender = event.req.url?.startsWith('/__nuxt_render') ? useQuery(event) : null const url = ssrError?.url as string || event.req.url! // Initialize ssr context @@ -122,8 +123,11 @@ export default eventHandler(async (event) => { runtimeConfig: useRuntimeConfig(), noSSR: !!event.req.headers['x-nuxt-no-ssr'], error: ssrError, + render: customRender + ? { components: JSON.parse(customRender.components as string || '[]') } + : undefined, nuxt: undefined, /* NuxtApp */ - payload: undefined + payload: customRender ? { state: JSON.parse(customRender.state as string || '{}') } : undefined } // Render app @@ -150,6 +154,22 @@ export default eventHandler(async (event) => { await renderMeta(rendered, ssrContext) + // Render server components + if (ssrContext.teleports?.['render-target-0']) { + const components = Object.entries(ssrContext.teleports) + .filter(([key]) => key.startsWith('render-target')) + .map(([, value]) => ({ html: value.replace(/$/, '') })) + + event.res.setHeader('Content-Type', 'application/json') + + return { + rendered: components, + state: ssrContext.payload, + // TODO: head integration + meta: {} + } + } + const html = await renderHTML(ssrContext.payload, rendered, ssrContext) event.res.setHeader('Content-Type', 'text/html;charset=UTF-8') return html From aa98041749582f937bbba51a4729ec5c8e77f3e7 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 1 Jul 2022 11:38:29 +0100 Subject: [PATCH 03/73] feat: render styles/scripts and extract type --- packages/nuxt/src/app/nuxt.ts | 7 +++++++ packages/nuxt/src/core/runtime/nitro/renderer.ts | 12 +++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 33b5c70a93b..b60c88de978 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -241,3 +241,10 @@ export function useRuntimeConfig (): RuntimeConfig { function defineGetter (obj: Record, key: K, val: V) { Object.defineProperty(obj, key, { get: () => val }) } + +export interface ComponentRenderResult { + state: Record + rendered: Array<{ html: string }> + style?: string + script?: string +} diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index c954b5a1cd2..02ee2957bf6 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -3,7 +3,7 @@ import { eventHandler, useQuery } from 'h3' import devalue from '@nuxt/devalue' import { renderToString as _renderToString } from 'vue/server-renderer' -import type { NuxtApp } from '#app' +import type { NuxtApp, ComponentRenderResult } from '#app' // @ts-ignore import { useRuntimeConfig } from '#internal/nitro' @@ -162,12 +162,14 @@ export default eventHandler(async (event) => { event.res.setHeader('Content-Type', 'application/json') - return { + const result: ComponentRenderResult = { rendered: components, - state: ssrContext.payload, - // TODO: head integration - meta: {} + state: ssrContext.payload.state, + style: rendered.renderStyles() + (ssrContext.styles || ''), + script: (rendered.meta.bodyScriptsPrepend || '') + rendered.renderScripts() + (rendered.meta.bodyScripts || '') } + + return result } const html = await renderHTML(ssrContext.payload, rendered, ssrContext) From 109d48a6da09e3d34e7db0ca01abf3d44e72aea8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 1 Jul 2022 11:41:36 +0100 Subject: [PATCH 04/73] fix: workaround upstream vue issue --- packages/nuxt/src/app/components/render-components.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/render-components.ts b/packages/nuxt/src/app/components/render-components.ts index 4ae3ba66c72..2ca31f2146c 100644 --- a/packages/nuxt/src/app/components/render-components.ts +++ b/packages/nuxt/src/app/components/render-components.ts @@ -4,7 +4,12 @@ export default defineComponent({ props: { components: Array as () => Array<{ name: string, props?: Record }> }, - setup (props) { + async setup (props) { + // TODO: https://github.com/vuejs/core/issues/6207 + await Promise.all(props.components.map((c) => { + const component = resolveComponent(c.name) + return component && typeof component === 'object' && component.__asyncLoader?.() + })) return () => props.components.map( (c, index) => createBlock(Teleport as any, { to: `render-target-${index}` }, [ h(resolveComponent(c.name), c.props) From 7547e03371ba3fe15bc6e9c68f4d1141ebde6660 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 1 Jul 2022 11:56:58 +0100 Subject: [PATCH 05/73] fix: suppress vue-router warning --- packages/nuxt/src/pages/module.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index 65518a6a380..4aaba02a23d 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -108,7 +108,12 @@ export default defineNuxtModule({ const pages = await resolvePagesRoutes() await nuxt.callHook('pages:extend', pages) const { routes, imports } = normalizeRoutes(pages) - return [...imports, `export default ${routes}`].join('\n') + return [ + ...imports, + `const routes = ${routes}`, + "if (process.server) { routes.push({ name: '_nuxt_render', path: '/__nuxt_render', component: { render: () => '' } }) }", + 'export default routes' + ].join('\n') } }) From fdb09e21b075856e3b0d199ed48969889ddaa708 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 1 Jul 2022 11:59:50 +0100 Subject: [PATCH 06/73] feat: support POST requests as well --- packages/nuxt/src/core/runtime/nitro/renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 02ee2957bf6..639e4ec7e93 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -1,5 +1,5 @@ import { createRenderer } from 'vue-bundle-renderer' -import { eventHandler, useQuery } from 'h3' +import { eventHandler, useBody, useQuery } from 'h3' import devalue from '@nuxt/devalue' import { renderToString as _renderToString } from 'vue/server-renderer' @@ -111,7 +111,7 @@ const getSPARenderer = lazyCachedFunction(async () => { export default eventHandler(async (event) => { // Whether we're rendering an error page const ssrError = event.req.url?.startsWith('/__nuxt_error') ? useQuery(event) : null - const customRender = event.req.url?.startsWith('/__nuxt_render') ? useQuery(event) : null + const customRender = event.req.url?.startsWith('/__nuxt_render') ? event.req.method === 'GET' ? useQuery(event) : await useBody(event) : null const url = ssrError?.url as string || event.req.url! // Initialize ssr context From 12d2592edfc0296feae0434c933a01f32ab5fe22 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 1 Jul 2022 12:07:37 +0100 Subject: [PATCH 07/73] refactor: use `destr` to parse --- .../nuxt/src/core/runtime/nitro/renderer.ts | 19 +++++++++++++------ playground/pages/index.vue | 9 +++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 playground/pages/index.vue diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 639e4ec7e93..911ea95082e 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -1,6 +1,7 @@ import { createRenderer } from 'vue-bundle-renderer' -import { eventHandler, useBody, useQuery } from 'h3' +import { CompatibilityEvent, eventHandler, useBody, useQuery } from 'h3' import devalue from '@nuxt/devalue' +import destr from 'destr' import { renderToString as _renderToString } from 'vue/server-renderer' import type { NuxtApp, ComponentRenderResult } from '#app' @@ -108,10 +109,18 @@ const getSPARenderer = lazyCachedFunction(async () => { return { renderToString } }) +function parseRenderQuery (event: CompatibilityEvent) { + const query = useQuery(event) + return { + components: destr(query.components), + state: destr(query.state) + } +} + export default eventHandler(async (event) => { // Whether we're rendering an error page const ssrError = event.req.url?.startsWith('/__nuxt_error') ? useQuery(event) : null - const customRender = event.req.url?.startsWith('/__nuxt_render') ? event.req.method === 'GET' ? useQuery(event) : await useBody(event) : null + const customRender = event.req.url?.startsWith('/__nuxt_render') ? event.req.method === 'GET' ? parseRenderQuery(event) : await useBody(event) : null const url = ssrError?.url as string || event.req.url! // Initialize ssr context @@ -123,11 +132,9 @@ export default eventHandler(async (event) => { runtimeConfig: useRuntimeConfig(), noSSR: !!event.req.headers['x-nuxt-no-ssr'], error: ssrError, - render: customRender - ? { components: JSON.parse(customRender.components as string || '[]') } - : undefined, nuxt: undefined, /* NuxtApp */ - payload: customRender ? { state: JSON.parse(customRender.state as string || '{}') } : undefined + render: customRender ? { components: customRender.components } : undefined, + payload: customRender ? { state: customRender.state } : undefined } // Render app diff --git a/playground/pages/index.vue b/playground/pages/index.vue new file mode 100644 index 00000000000..ee3538963c3 --- /dev/null +++ b/playground/pages/index.vue @@ -0,0 +1,9 @@ + + + From be7ab204fff2439a7dfa138132836777d15b431f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 1 Jul 2022 12:08:41 +0100 Subject: [PATCH 08/73] refactor: clarity --- packages/nuxt/src/core/runtime/nitro/renderer.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 911ea95082e..568dac6b82e 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -120,7 +120,11 @@ function parseRenderQuery (event: CompatibilityEvent) { export default eventHandler(async (event) => { // Whether we're rendering an error page const ssrError = event.req.url?.startsWith('/__nuxt_error') ? useQuery(event) : null - const customRender = event.req.url?.startsWith('/__nuxt_render') ? event.req.method === 'GET' ? parseRenderQuery(event) : await useBody(event) : null + const customRender = event.req.url?.startsWith('/__nuxt_render') + ? event.req.method === 'GET' + ? parseRenderQuery(event) + : await useBody(event) + : null const url = ssrError?.url as string || event.req.url! // Initialize ssr context From 4759e418d882513910f92ef4d678ee7539709ffa Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 1 Jul 2022 12:12:08 +0100 Subject: [PATCH 09/73] fix: remove setHeader for `application/json` --- packages/nuxt/src/core/runtime/nitro/error.ts | 1 - packages/nuxt/src/core/runtime/nitro/renderer.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/error.ts b/packages/nuxt/src/core/runtime/nitro/error.ts index 381832fb500..570427331a3 100644 --- a/packages/nuxt/src/core/runtime/nitro/error.ts +++ b/packages/nuxt/src/core/runtime/nitro/error.ts @@ -30,7 +30,6 @@ export default async function errorhandler (_error, event) { // JSON response if (isJsonRequest(event)) { - event.res.setHeader('Content-Type', 'application/json') event.res.end(JSON.stringify(errorObject)) return } diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 568dac6b82e..704409faf1d 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -171,8 +171,6 @@ export default eventHandler(async (event) => { .filter(([key]) => key.startsWith('render-target')) .map(([, value]) => ({ html: value.replace(/$/, '') })) - event.res.setHeader('Content-Type', 'application/json') - const result: ComponentRenderResult = { rendered: components, state: ssrContext.payload.state, From 8d6e23aaa432e617198f56f6b61690024e1ddd2a Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 1 Jul 2022 12:13:12 +0100 Subject: [PATCH 10/73] refactor: styles & scripts --- packages/nuxt/src/app/nuxt.ts | 4 ++-- packages/nuxt/src/core/runtime/nitro/renderer.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index b60c88de978..65aa73b8b6e 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -245,6 +245,6 @@ function defineGetter (obj: Record, export interface ComponentRenderResult { state: Record rendered: Array<{ html: string }> - style?: string - script?: string + styles?: string + scripts?: string } diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 704409faf1d..def9abf2b51 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -174,8 +174,8 @@ export default eventHandler(async (event) => { const result: ComponentRenderResult = { rendered: components, state: ssrContext.payload.state, - style: rendered.renderStyles() + (ssrContext.styles || ''), - script: (rendered.meta.bodyScriptsPrepend || '') + rendered.renderScripts() + (rendered.meta.bodyScripts || '') + styles: rendered.renderStyles() + (ssrContext.styles || ''), + scripts: (rendered.meta.bodyScriptsPrepend || '') + rendered.renderScripts() + (rendered.meta.bodyScripts || '') } return result From 4a1db3bb41d90781eeb6e4cf93d70efbf9110bf4 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Jul 2022 10:59:05 +0100 Subject: [PATCH 11/73] feat: add `isIndividualRender` composable --- packages/nuxt/src/app/components/nuxt-root.vue | 4 ++-- packages/nuxt/src/app/composables/index.ts | 2 +- packages/nuxt/src/app/composables/router.ts | 8 ++++---- packages/nuxt/src/app/composables/ssr.ts | 2 ++ packages/nuxt/src/auto-imports/presets.ts | 1 + 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-root.vue b/packages/nuxt/src/app/components/nuxt-root.vue index 5bc2b4e78ac..aca4c0c912f 100644 --- a/packages/nuxt/src/app/components/nuxt-root.vue +++ b/packages/nuxt/src/app/components/nuxt-root.vue @@ -7,7 +7,7 @@ diff --git a/packages/nuxt/src/app/composables/index.ts b/packages/nuxt/src/app/composables/index.ts index 2780a248ca6..7b8dcc3a34d 100644 --- a/packages/nuxt/src/app/composables/index.ts +++ b/packages/nuxt/src/app/composables/index.ts @@ -8,6 +8,6 @@ export { useFetch, useLazyFetch } from './fetch' export type { FetchResult, UseFetchOptions } from './fetch' export { useCookie } from './cookie' export type { CookieOptions, CookieRef } from './cookie' -export { useRequestHeaders, useRequestEvent } from './ssr' +export { isIndividualRender, useRequestHeaders, useRequestEvent } from './ssr' export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, navigateTo, useRoute, useActiveRoute, useRouter } from './router' export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router' diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index 1ec7e83a62b..78e10e70609 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -1,18 +1,18 @@ import type { Router, RouteLocationNormalizedLoaded, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, NavigationFailure } from 'vue-router' import { sendRedirect } from 'h3' import { joinURL } from 'ufo' -import { useNuxtApp, useRuntimeConfig } from '#app' +import { useNuxtApp, useRuntimeConfig, isIndividualRender } from '#app' export const useRouter = () => { - return useNuxtApp()?.$router as Router + return isIndividualRender() ? null : useNuxtApp()?.$router as Router } export const useRoute = () => { - return useNuxtApp()._route as RouteLocationNormalizedLoaded + return isIndividualRender() ? null : useNuxtApp()._route as RouteLocationNormalizedLoaded } export const useActiveRoute = () => { - return useNuxtApp()._activeRoute as RouteLocationNormalizedLoaded + return isIndividualRender() ? null : useNuxtApp()._activeRoute as RouteLocationNormalizedLoaded } export interface RouteMiddleware { diff --git a/packages/nuxt/src/app/composables/ssr.ts b/packages/nuxt/src/app/composables/ssr.ts index 971a05186c5..d1abdbfb00e 100644 --- a/packages/nuxt/src/app/composables/ssr.ts +++ b/packages/nuxt/src/app/composables/ssr.ts @@ -15,3 +15,5 @@ export function useRequestHeaders (include?) { export function useRequestEvent (nuxtApp: NuxtApp = useNuxtApp()): CompatibilityEvent { return nuxtApp.ssrContext?.event as CompatibilityEvent } + +export const isIndividualRender = () => process.server && useNuxtApp().ssrContext.url.startsWith('/__nuxt_render') diff --git a/packages/nuxt/src/auto-imports/presets.ts b/packages/nuxt/src/auto-imports/presets.ts index cf2fd30dea5..16018d58491 100644 --- a/packages/nuxt/src/auto-imports/presets.ts +++ b/packages/nuxt/src/auto-imports/presets.ts @@ -35,6 +35,7 @@ export const appPreset = defineUnimportPreset({ 'useCookie', 'useRequestHeaders', 'useRequestEvent', + 'isIndividualRender', 'useRouter', 'useRoute', 'useActiveRoute', From b1c32db454c0fc36925d8ee6be681b09980521fe Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Jul 2022 10:59:38 +0100 Subject: [PATCH 12/73] feat: support passing url in `customRender` payload --- packages/nuxt/src/core/runtime/nitro/renderer.ts | 7 ++++--- packages/nuxt/src/pages/module.ts | 7 +------ packages/nuxt/src/pages/runtime/router.ts | 10 +++++++--- playground/pages/index.vue | 9 --------- 4 files changed, 12 insertions(+), 21 deletions(-) delete mode 100644 playground/pages/index.vue diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index def9abf2b51..2d0661befbd 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -112,6 +112,7 @@ const getSPARenderer = lazyCachedFunction(async () => { function parseRenderQuery (event: CompatibilityEvent) { const query = useQuery(event) return { + ...query, components: destr(query.components), state: destr(query.state) } @@ -125,7 +126,7 @@ export default eventHandler(async (event) => { ? parseRenderQuery(event) : await useBody(event) : null - const url = ssrError?.url as string || event.req.url! + const url: string = ssrError?.url as string || customRender?.url || event.req.url! // Initialize ssr context const ssrContext: NuxtSSRContext = { @@ -137,8 +138,8 @@ export default eventHandler(async (event) => { noSSR: !!event.req.headers['x-nuxt-no-ssr'], error: ssrError, nuxt: undefined, /* NuxtApp */ - render: customRender ? { components: customRender.components } : undefined, - payload: customRender ? { state: customRender.state } : undefined + render: customRender ? { components: customRender.components || [] } : undefined, + payload: customRender ? { state: customRender.state || {} } : undefined } // Render app diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index 4aaba02a23d..65518a6a380 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -108,12 +108,7 @@ export default defineNuxtModule({ const pages = await resolvePagesRoutes() await nuxt.callHook('pages:extend', pages) const { routes, imports } = normalizeRoutes(pages) - return [ - ...imports, - `const routes = ${routes}`, - "if (process.server) { routes.push({ name: '_nuxt_render', path: '/__nuxt_render', component: { render: () => '' } }) }", - 'export default routes' - ].join('\n') + return [...imports, `export default ${routes}`].join('\n') } }) diff --git a/packages/nuxt/src/pages/runtime/router.ts b/packages/nuxt/src/pages/runtime/router.ts index 824dddeeaea..333d4e58702 100644 --- a/packages/nuxt/src/pages/runtime/router.ts +++ b/packages/nuxt/src/pages/runtime/router.ts @@ -9,7 +9,7 @@ import { import { createError } from 'h3' import { withoutBase, isEqual } from 'ufo' import NuxtPage from './page' -import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, throwError, clearError, navigateTo, useError } from '#app' +import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, throwError, clearError, navigateTo, useError, isIndividualRender } from '#app' // @ts-ignore import routes from '#build/routes' // @ts-ignore @@ -49,17 +49,21 @@ function createCurrentLocation ( } export default defineNuxtPlugin(async (nuxtApp) => { + const baseURL = useRuntimeConfig().app.baseURL + const initialURL = process.server ? nuxtApp.ssrContext.url : createCurrentLocation(baseURL, window.location) + + // Self-disable if we are rendering a component with no URL context + if (isIndividualRender()) { return } + nuxtApp.vueApp.component('NuxtPage', NuxtPage) // TODO: remove before release - present for backwards compatibility & intentionally undocumented nuxtApp.vueApp.component('NuxtNestedPage', NuxtPage) nuxtApp.vueApp.component('NuxtChild', NuxtPage) - const baseURL = useRuntimeConfig().app.baseURL const routerHistory = process.client ? createWebHistory(baseURL) : createMemoryHistory(baseURL) - const initialURL = process.server ? nuxtApp.ssrContext.url : createCurrentLocation(baseURL, window.location) const router = createRouter({ ...routerOptions, history: routerHistory, diff --git a/playground/pages/index.vue b/playground/pages/index.vue deleted file mode 100644 index ee3538963c3..00000000000 --- a/playground/pages/index.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - From 11c8e14125d49732a7d92c46975dde092b744c4d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Jul 2022 11:07:08 +0100 Subject: [PATCH 13/73] test: add test suite for server rendering individual components --- test/basic.test.ts | 67 +++++++++++++++++++ .../basic/components/global/PureComponent.vue | 22 ++++++ .../components/global/RouteComponent.vue | 5 ++ test/fixtures/basic/nuxt.config.ts | 4 ++ test/fixtures/basic/plugins/my-plugin.ts | 2 +- 5 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/basic/components/global/PureComponent.vue create mode 100644 test/fixtures/basic/components/global/RouteComponent.vue diff --git a/test/basic.test.ts b/test/basic.test.ts index b4fcb492bf6..25a5ada977a 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -2,7 +2,9 @@ import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' // import { isWindows } from 'std-env' import { setup, fetch, $fetch, startServer } from '@nuxt/test-utils' +import { withQuery } from 'ufo' import { expectNoClientErrors } from './utils' +import { ComponentRenderResult } from '#app' await setup({ rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), @@ -334,6 +336,71 @@ describe('automatically keyed composables', () => { }) }) +describe('selective rendering of global components', () => { + it('renders components with route', async () => { + const result: ComponentRenderResult = await $fetch(withQuery('/__nuxt_render', { + url: '/foo', + state: JSON.stringify({}), + components: JSON.stringify([ + { name: 'RouteComponent' } + ]) + })) + expect(result.rendered[0].html).toMatchInlineSnapshot(` + "
    Route: /foo
+        
" + `) + expect(result.state).toMatchInlineSnapshot(` + { + "error": null, + } + `) + expect(Object.keys(result)).toMatchInlineSnapshot(` + [ + "rendered", + "state", + "styles", + "scripts", + ] + `) + }) + it('renders pure components', async () => { + const result: ComponentRenderResult = await $fetch(withQuery('/__nuxt_render', { + components: JSON.stringify([ + { + name: 'PureComponent', + props: { + bool: false, + number: 3487, + str: 'something', + obj: { foo: 42, bar: false, me: 'hi' } + } + } + ]) + })) + expect(result.rendered[0].html).toMatchInlineSnapshot(` + "
    false
+          3487
+          "something"
+          {"foo":42,"bar":false,"me":"hi"}
+          Was router enabled: false
+        
" + `) + expect(result.state).toMatchInlineSnapshot(` + { + "error": null, + } + `) + expect(Object.keys(result)).toMatchInlineSnapshot(` + [ + "rendered", + "state", + "styles", + "scripts", + ] + `) + }) +}) + describe('dynamic paths', () => { if (process.env.NUXT_TEST_DEV) { // TODO: diff --git a/test/fixtures/basic/components/global/PureComponent.vue b/test/fixtures/basic/components/global/PureComponent.vue new file mode 100644 index 00000000000..a64a1de43d3 --- /dev/null +++ b/test/fixtures/basic/components/global/PureComponent.vue @@ -0,0 +1,22 @@ + + + diff --git a/test/fixtures/basic/components/global/RouteComponent.vue b/test/fixtures/basic/components/global/RouteComponent.vue new file mode 100644 index 00000000000..a5f3defe1ab --- /dev/null +++ b/test/fixtures/basic/components/global/RouteComponent.vue @@ -0,0 +1,5 @@ + diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 93c21696cd7..099dbd5d477 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -2,6 +2,10 @@ import { defineNuxtConfig } from 'nuxt' import { addComponent } from '@nuxt/kit' export default defineNuxtConfig({ + components: [ + { path: '~/components/global', global: true }, + '~/components' + ], buildDir: process.env.NITRO_BUILD_DIR, builder: process.env.TEST_WITH_WEBPACK ? 'webpack' : 'vite', extends: [ diff --git a/test/fixtures/basic/plugins/my-plugin.ts b/test/fixtures/basic/plugins/my-plugin.ts index 9f59aea0224..627d9db911e 100644 --- a/test/fixtures/basic/plugins/my-plugin.ts +++ b/test/fixtures/basic/plugins/my-plugin.ts @@ -2,7 +2,7 @@ export default defineNuxtPlugin(() => { useHead({ titleTemplate: '%s - Fixture' }) - const path = useRoute().path + const path = useRoute()?.path return { provide: { myPlugin: () => 'Injected by my-plugin', From f8324683fa914562fb3afc2c26987bb498c8b413 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Jul 2022 11:09:39 +0100 Subject: [PATCH 14/73] test: improve snapshot readability --- test/basic.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/basic.test.ts b/test/basic.test.ts index 25a5ada977a..9663d1d8cee 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -377,11 +377,11 @@ describe('selective rendering of global components', () => { } ]) })) - expect(result.rendered[0].html).toMatchInlineSnapshot(` + expect(result.rendered[0].html.replace(/"/g, '"')).toMatchInlineSnapshot(` "
    false
           3487
-          "something"
-          {"foo":42,"bar":false,"me":"hi"}
+          \\"something\\"
+          {\\"foo\\":42,\\"bar\\":false,\\"me\\":\\"hi\\"}
           Was router enabled: false
         
" `) From 63978b4c737f833dc267d55e819c20095fe478f2 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Jul 2022 11:10:58 +0100 Subject: [PATCH 15/73] chore: revert changes --- packages/nuxt/src/pages/runtime/router.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/pages/runtime/router.ts b/packages/nuxt/src/pages/runtime/router.ts index 333d4e58702..ed15b541cc5 100644 --- a/packages/nuxt/src/pages/runtime/router.ts +++ b/packages/nuxt/src/pages/runtime/router.ts @@ -49,9 +49,6 @@ function createCurrentLocation ( } export default defineNuxtPlugin(async (nuxtApp) => { - const baseURL = useRuntimeConfig().app.baseURL - const initialURL = process.server ? nuxtApp.ssrContext.url : createCurrentLocation(baseURL, window.location) - // Self-disable if we are rendering a component with no URL context if (isIndividualRender()) { return } @@ -60,10 +57,12 @@ export default defineNuxtPlugin(async (nuxtApp) => { nuxtApp.vueApp.component('NuxtNestedPage', NuxtPage) nuxtApp.vueApp.component('NuxtChild', NuxtPage) + const baseURL = useRuntimeConfig().app.baseURL const routerHistory = process.client ? createWebHistory(baseURL) : createMemoryHistory(baseURL) + const initialURL = process.server ? nuxtApp.ssrContext.url : createCurrentLocation(baseURL, window.location) const router = createRouter({ ...routerOptions, history: routerHistory, From be889e8d81d107b04178f667b6773bbc9042698f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Jul 2022 11:12:22 +0100 Subject: [PATCH 16/73] fix: disable universal router too --- packages/nuxt/src/app/plugins/router.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index 50e5d6bae1e..539837e6ca5 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -1,7 +1,7 @@ import { reactive, h } from 'vue' import { parseURL, parseQuery, withoutBase, isEqual, joinURL } from 'ufo' import { createError } from 'h3' -import { defineNuxtPlugin } from '..' +import { defineNuxtPlugin, isIndividualRender } from '..' import { callWithNuxt } from '../nuxt' import { clearError, navigateTo, throwError, useRuntimeConfig } from '#app' // @ts-ignore @@ -88,6 +88,9 @@ interface Router { } export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => { + // Self-disable if we are rendering a component with no URL context + if (isIndividualRender()) { return } + const initialURL = process.client ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash : nuxtApp.ssrContext.url From 685fc4b6796c6de410e1ec4b648379f503aa3ab7 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 8 Jul 2022 11:48:45 +0100 Subject: [PATCH 17/73] refactor: remove `isIndividualRender` and detect directly from app flag --- packages/nuxt/src/app/composables/index.ts | 2 +- packages/nuxt/src/app/composables/router.ts | 11 +++++++---- packages/nuxt/src/app/composables/ssr.ts | 2 -- packages/nuxt/src/app/nuxt.ts | 3 +++ packages/nuxt/src/app/plugins/router.ts | 4 ++-- packages/nuxt/src/auto-imports/presets.ts | 1 - packages/nuxt/src/pages/runtime/router.ts | 4 ++-- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/nuxt/src/app/composables/index.ts b/packages/nuxt/src/app/composables/index.ts index 7b8dcc3a34d..2780a248ca6 100644 --- a/packages/nuxt/src/app/composables/index.ts +++ b/packages/nuxt/src/app/composables/index.ts @@ -8,6 +8,6 @@ export { useFetch, useLazyFetch } from './fetch' export type { FetchResult, UseFetchOptions } from './fetch' export { useCookie } from './cookie' export type { CookieOptions, CookieRef } from './cookie' -export { isIndividualRender, useRequestHeaders, useRequestEvent } from './ssr' +export { useRequestHeaders, useRequestEvent } from './ssr' export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, navigateTo, useRoute, useActiveRoute, useRouter } from './router' export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router' diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index 78e10e70609..32ea4da87b8 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -1,18 +1,21 @@ import type { Router, RouteLocationNormalizedLoaded, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, NavigationFailure } from 'vue-router' import { sendRedirect } from 'h3' import { joinURL } from 'ufo' -import { useNuxtApp, useRuntimeConfig, isIndividualRender } from '#app' +import { useNuxtApp, useRuntimeConfig } from '#app' export const useRouter = () => { - return isIndividualRender() ? null : useNuxtApp()?.$router as Router + const nuxtApp = useNuxtApp() + return !nuxtApp || nuxtApp?._isIndividualRender ? null : nuxtApp.$router as Router } export const useRoute = () => { - return isIndividualRender() ? null : useNuxtApp()._route as RouteLocationNormalizedLoaded + const nuxtApp = useNuxtApp() + return nuxtApp._isIndividualRender ? null : nuxtApp._route as RouteLocationNormalizedLoaded } export const useActiveRoute = () => { - return isIndividualRender() ? null : useNuxtApp()._activeRoute as RouteLocationNormalizedLoaded + const nuxtApp = useNuxtApp() + return nuxtApp._isIndividualRender ? null : nuxtApp._activeRoute as RouteLocationNormalizedLoaded } export interface RouteMiddleware { diff --git a/packages/nuxt/src/app/composables/ssr.ts b/packages/nuxt/src/app/composables/ssr.ts index d1abdbfb00e..971a05186c5 100644 --- a/packages/nuxt/src/app/composables/ssr.ts +++ b/packages/nuxt/src/app/composables/ssr.ts @@ -15,5 +15,3 @@ export function useRequestHeaders (include?) { export function useRequestEvent (nuxtApp: NuxtApp = useNuxtApp()): CompatibilityEvent { return nuxtApp.ssrContext?.event as CompatibilityEvent } - -export const isIndividualRender = () => process.server && useNuxtApp().ssrContext.url.startsWith('/__nuxt_render') diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 65aa73b8b6e..8bc19b1b283 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -46,6 +46,8 @@ interface _NuxtApp { [key: string]: any + /** @private */ + _isIndividualRender?: boolean _asyncDataPromises?: Record> ssrContext?: SSRContext & { @@ -103,6 +105,7 @@ export function createNuxtApp (options: CreateOptions) { ...(process.client ? window.__NUXT__ : { serverRendered: true }) }), isHydrating: process.client, + _isIndividualRender: !!options.ssrContext?.render, _asyncDataPromises: {}, ...options } as any as NuxtApp diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index 539837e6ca5..6249b939006 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -1,7 +1,7 @@ import { reactive, h } from 'vue' import { parseURL, parseQuery, withoutBase, isEqual, joinURL } from 'ufo' import { createError } from 'h3' -import { defineNuxtPlugin, isIndividualRender } from '..' +import { defineNuxtPlugin } from '..' import { callWithNuxt } from '../nuxt' import { clearError, navigateTo, throwError, useRuntimeConfig } from '#app' // @ts-ignore @@ -89,7 +89,7 @@ interface Router { export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => { // Self-disable if we are rendering a component with no URL context - if (isIndividualRender()) { return } + if (nuxtApp._isIndividualRender) { return } const initialURL = process.client ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash diff --git a/packages/nuxt/src/auto-imports/presets.ts b/packages/nuxt/src/auto-imports/presets.ts index 16018d58491..cf2fd30dea5 100644 --- a/packages/nuxt/src/auto-imports/presets.ts +++ b/packages/nuxt/src/auto-imports/presets.ts @@ -35,7 +35,6 @@ export const appPreset = defineUnimportPreset({ 'useCookie', 'useRequestHeaders', 'useRequestEvent', - 'isIndividualRender', 'useRouter', 'useRoute', 'useActiveRoute', diff --git a/packages/nuxt/src/pages/runtime/router.ts b/packages/nuxt/src/pages/runtime/router.ts index ed15b541cc5..5e92a8fe045 100644 --- a/packages/nuxt/src/pages/runtime/router.ts +++ b/packages/nuxt/src/pages/runtime/router.ts @@ -9,7 +9,7 @@ import { import { createError } from 'h3' import { withoutBase, isEqual } from 'ufo' import NuxtPage from './page' -import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, throwError, clearError, navigateTo, useError, isIndividualRender } from '#app' +import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, throwError, clearError, navigateTo, useError } from '#app' // @ts-ignore import routes from '#build/routes' // @ts-ignore @@ -50,7 +50,7 @@ function createCurrentLocation ( export default defineNuxtPlugin(async (nuxtApp) => { // Self-disable if we are rendering a component with no URL context - if (isIndividualRender()) { return } + if (nuxtApp._isIndividualRender) { return } nuxtApp.vueApp.component('NuxtPage', NuxtPage) // TODO: remove before release - present for backwards compatibility & intentionally undocumented From 08b82743fb3961c201b3b40b9080ee03f71f6c17 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 8 Jul 2022 12:04:29 +0100 Subject: [PATCH 18/73] fix: use based on url --- packages/nuxt/src/app/nuxt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 8bc19b1b283..5b841add6cd 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -105,7 +105,7 @@ export function createNuxtApp (options: CreateOptions) { ...(process.client ? window.__NUXT__ : { serverRendered: true }) }), isHydrating: process.client, - _isIndividualRender: !!options.ssrContext?.render, + _isIndividualRender: process.server && options.ssrContext?.url.startsWith('/__nuxt_render'), _asyncDataPromises: {}, ...options } as any as NuxtApp From 5696fd875660b0d6ad2e9c3734830de9ea5e65c8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 8 Jul 2022 12:22:09 +0100 Subject: [PATCH 19/73] test: update state snapshots --- test/basic.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/basic.test.ts b/test/basic.test.ts index 9663d1d8cee..322c1a2f725 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -351,7 +351,7 @@ describe('selective rendering of global components', () => { `) expect(result.state).toMatchInlineSnapshot(` { - "error": null, + "$serror": null, } `) expect(Object.keys(result)).toMatchInlineSnapshot(` @@ -387,7 +387,7 @@ describe('selective rendering of global components', () => { `) expect(result.state).toMatchInlineSnapshot(` { - "error": null, + "$serror": null, } `) expect(Object.keys(result)).toMatchInlineSnapshot(` From f180149486c5b95f66610ec646119f0441866d7a Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 14 Jul 2022 19:54:54 +0200 Subject: [PATCH 20/73] revert back json header we are directly closing request here --- packages/nuxt/src/core/runtime/nitro/error.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nuxt/src/core/runtime/nitro/error.ts b/packages/nuxt/src/core/runtime/nitro/error.ts index 570427331a3..381832fb500 100644 --- a/packages/nuxt/src/core/runtime/nitro/error.ts +++ b/packages/nuxt/src/core/runtime/nitro/error.ts @@ -30,6 +30,7 @@ export default async function errorhandler (_error, event) { // JSON response if (isJsonRequest(event)) { + event.res.setHeader('Content-Type', 'application/json') event.res.end(JSON.stringify(errorObject)) return } From 4a8e5f960e96237d864b868d7aa986e4de321a7e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 14 Jul 2022 20:05:40 +0200 Subject: [PATCH 21/73] fix: always repond json in custom render mode --- packages/nuxt/src/core/runtime/nitro/renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 2d0661befbd..0ee1f685b23 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -167,8 +167,8 @@ export default eventHandler(async (event) => { await renderMeta(rendered, ssrContext) // Render server components - if (ssrContext.teleports?.['render-target-0']) { - const components = Object.entries(ssrContext.teleports) + if (customRender) { + const components = Object.entries(ssrContext.teleports || []) .filter(([key]) => key.startsWith('render-target')) .map(([, value]) => ({ html: value.replace(/$/, '') })) From cc273c3cbdc2bf4ade558742f6c3040bce35de38 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 14 Jul 2022 20:35:06 +0200 Subject: [PATCH 22/73] refactor: rename to isolated --- packages/nuxt/src/app/composables/router.ts | 6 +++--- packages/nuxt/src/app/nuxt.ts | 4 ++-- packages/nuxt/src/app/plugins/router.ts | 2 +- packages/nuxt/src/core/runtime/nitro/renderer.ts | 14 ++++++-------- packages/nuxt/src/pages/runtime/router.ts | 2 +- test/basic.test.ts | 4 ++-- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index 32ea4da87b8..4dcc8b8099b 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -5,17 +5,17 @@ import { useNuxtApp, useRuntimeConfig } from '#app' export const useRouter = () => { const nuxtApp = useNuxtApp() - return !nuxtApp || nuxtApp?._isIndividualRender ? null : nuxtApp.$router as Router + return !nuxtApp || nuxtApp?._isolatedRender ? null : nuxtApp.$router as Router } export const useRoute = () => { const nuxtApp = useNuxtApp() - return nuxtApp._isIndividualRender ? null : nuxtApp._route as RouteLocationNormalizedLoaded + return nuxtApp._isolatedRender ? null : nuxtApp._route as RouteLocationNormalizedLoaded } export const useActiveRoute = () => { const nuxtApp = useNuxtApp() - return nuxtApp._isIndividualRender ? null : nuxtApp._activeRoute as RouteLocationNormalizedLoaded + return nuxtApp._isolatedRender ? null : nuxtApp._activeRoute as RouteLocationNormalizedLoaded } export interface RouteMiddleware { diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 34f9047fcf3..c2b330e3761 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -47,7 +47,7 @@ interface _NuxtApp { [key: string]: any /** @private */ - _isIndividualRender?: boolean + _isolatedRender?: boolean _asyncDataPromises?: Record> ssrContext?: SSRContext & { @@ -105,7 +105,7 @@ export function createNuxtApp (options: CreateOptions) { ...(process.client ? window.__NUXT__ : { serverRendered: true }) }), isHydrating: process.client, - _isIndividualRender: process.server && options.ssrContext?.url.startsWith('/__nuxt_render'), + _isolatedRender: process.server && options.ssrContext?.url.startsWith('/__nuxt_isolated_render'), _asyncDataPromises: {}, ...options } as any as NuxtApp diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index 6249b939006..8a283a3a384 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -89,7 +89,7 @@ interface Router { export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => { // Self-disable if we are rendering a component with no URL context - if (nuxtApp._isIndividualRender) { return } + if (nuxtApp._isolatedRender) { return } const initialURL = process.client ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 0ee1f685b23..dfb33e510e0 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -121,12 +121,10 @@ function parseRenderQuery (event: CompatibilityEvent) { export default eventHandler(async (event) => { // Whether we're rendering an error page const ssrError = event.req.url?.startsWith('/__nuxt_error') ? useQuery(event) : null - const customRender = event.req.url?.startsWith('/__nuxt_render') - ? event.req.method === 'GET' - ? parseRenderQuery(event) - : await useBody(event) + const isolatedRenderCtx = event.req.url?.startsWith('/__nuxt_isolated_render') + ? event.req.method === 'GET' ? parseRenderQuery(event) : await useBody(event) : null - const url: string = ssrError?.url as string || customRender?.url || event.req.url! + const url: string = ssrError?.url as string || isolatedRenderCtx?.url || event.req.url! // Initialize ssr context const ssrContext: NuxtSSRContext = { @@ -138,8 +136,8 @@ export default eventHandler(async (event) => { noSSR: !!event.req.headers['x-nuxt-no-ssr'], error: ssrError, nuxt: undefined, /* NuxtApp */ - render: customRender ? { components: customRender.components || [] } : undefined, - payload: customRender ? { state: customRender.state || {} } : undefined + render: isolatedRenderCtx ? { components: isolatedRenderCtx.components || [] } : undefined, + payload: isolatedRenderCtx ? { state: isolatedRenderCtx.state || {} } : undefined } // Render app @@ -167,7 +165,7 @@ export default eventHandler(async (event) => { await renderMeta(rendered, ssrContext) // Render server components - if (customRender) { + if (isolatedRenderCtx) { const components = Object.entries(ssrContext.teleports || []) .filter(([key]) => key.startsWith('render-target')) .map(([, value]) => ({ html: value.replace(/$/, '') })) diff --git a/packages/nuxt/src/pages/runtime/router.ts b/packages/nuxt/src/pages/runtime/router.ts index 5e92a8fe045..c5a0e218111 100644 --- a/packages/nuxt/src/pages/runtime/router.ts +++ b/packages/nuxt/src/pages/runtime/router.ts @@ -50,7 +50,7 @@ function createCurrentLocation ( export default defineNuxtPlugin(async (nuxtApp) => { // Self-disable if we are rendering a component with no URL context - if (nuxtApp._isIndividualRender) { return } + if (nuxtApp._isolatedRender) { return } nuxtApp.vueApp.component('NuxtPage', NuxtPage) // TODO: remove before release - present for backwards compatibility & intentionally undocumented diff --git a/test/basic.test.ts b/test/basic.test.ts index 322c1a2f725..caaad60e3c9 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -338,7 +338,7 @@ describe('automatically keyed composables', () => { describe('selective rendering of global components', () => { it('renders components with route', async () => { - const result: ComponentRenderResult = await $fetch(withQuery('/__nuxt_render', { + const result: ComponentRenderResult = await $fetch(withQuery('/__nuxt_isolated_render', { url: '/foo', state: JSON.stringify({}), components: JSON.stringify([ @@ -364,7 +364,7 @@ describe('selective rendering of global components', () => { `) }) it('renders pure components', async () => { - const result: ComponentRenderResult = await $fetch(withQuery('/__nuxt_render', { + const result: ComponentRenderResult = await $fetch(withQuery('/__nuxt_isolated_render', { components: JSON.stringify([ { name: 'PureComponent', From 0efce9a2a1703183c49013351e092f827d1a1ec4 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 1 Aug 2022 12:36:58 +0100 Subject: [PATCH 23/73] refactor: only render island components (via `~/components/islands` or `.island.vue`) --- packages/nuxt/src/app/components/nuxt-root.vue | 5 ++++- .../nuxt/src/app/components/render-components.ts | 9 ++++++--- packages/nuxt/src/components/module.ts | 8 ++++++-- packages/nuxt/src/components/scan.ts | 8 +++++--- packages/nuxt/src/components/templates.ts | 12 ++++++++++++ packages/schema/src/types/components.ts | 7 +++++-- test/basic.test.ts | 8 ++++++-- .../components/{global => islands}/PureComponent.vue | 6 ++++++ .../{global => islands}/RouteComponent.vue | 0 test/fixtures/basic/nuxt.config.ts | 4 ---- 10 files changed, 50 insertions(+), 17 deletions(-) rename test/fixtures/basic/components/{global => islands}/PureComponent.vue (88%) rename test/fixtures/basic/components/{global => islands}/RouteComponent.vue (100%) diff --git a/packages/nuxt/src/app/components/nuxt-root.vue b/packages/nuxt/src/app/components/nuxt-root.vue index 783b4bb2a70..6c1f3c1e91a 100644 --- a/packages/nuxt/src/app/components/nuxt-root.vue +++ b/packages/nuxt/src/app/components/nuxt-root.vue @@ -8,10 +8,13 @@ diff --git a/packages/nuxt/src/app/components/render-components.ts b/packages/nuxt/src/app/components/render-components.ts deleted file mode 100644 index 245c92807df..00000000000 --- a/packages/nuxt/src/app/components/render-components.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createBlock, defineComponent, h, Teleport } from 'vue' - -// @ts-ignore -import * as islandComponents from '#build/components-islands.mjs' - -export default defineComponent({ - props: { - components: Array as () => Array<{ name: string, props?: Record }> - }, - async setup (props) { - // TODO: https://github.com/vuejs/core/issues/6207 - await Promise.all(props.components.map((c) => { - const component = islandComponents[c.name] - return component && typeof component === 'object' && component.__asyncLoader?.() - })) - return () => props.components.map( - (c, index) => createBlock(Teleport as any, { to: `render-target-${index}` }, [ - h(islandComponents[c.name] || 'span', c.props) - ]) - ) - } -}) diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index 20df3c000a7..1dab018f4b1 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -5,25 +5,19 @@ import { joinURL } from 'ufo' import { useNuxtApp, useRuntimeConfig } from '#app' export const useRouter = () => { - const nuxtApp = useNuxtApp() - return !nuxtApp || nuxtApp?._islandRender ? null : nuxtApp.$router as Router + return useNuxtApp()?.$router as Router } -export const useRoute = (): RouteLocationNormalizedLoaded | null => { - const nuxtApp = useNuxtApp() - if (nuxtApp._islandRender) { - return null - } +export const useRoute = (): RouteLocationNormalizedLoaded => { if (getCurrentInstance()) { - return inject('_route', nuxtApp._route) + return inject('_route', useNuxtApp()._route) } - return useNuxtApp()._route as RouteLocationNormalizedLoaded + return useNuxtApp()._route } /** @deprecated Use `useRoute` instead. */ -export const useActiveRoute = (): RouteLocationNormalizedLoaded | null => { - const nuxtApp = useNuxtApp() - return nuxtApp._islandRender ? null : nuxtApp._route as RouteLocationNormalizedLoaded +export const useActiveRoute = (): RouteLocationNormalizedLoaded => { + return useNuxtApp()._route } export interface RouteMiddleware { diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index acf18266a71..789ae0852ff 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -56,8 +56,9 @@ export interface NuxtSSRContext extends SSRContext { payload: _NuxtApp['payload'] teleports?: Record renderMeta?: () => Promise | NuxtMeta - render?: { - components: Array<{ name: string, props?: Record }> + islandContext?: { + name: string + props?: Record } } @@ -73,9 +74,6 @@ interface _NuxtApp { _asyncDataPromises: Record | undefined> - /** @private */ - _islandRender?: boolean - ssrContext?: NuxtSSRContext payload: { serverRendered?: boolean @@ -122,7 +120,6 @@ export function createNuxtApp (options: CreateOptions) { ...(process.client ? window.__NUXT__ : { serverRendered: true }) }), isHydrating: process.client, - _islandRender: process.server && options.ssrContext?.url.startsWith('/__nuxt_island'), _asyncDataPromises: {}, ...options } as any as NuxtApp diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index 342da07ad27..59cf70fc6a2 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -87,9 +87,6 @@ interface Router { } export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => { - // Self-disable if we are rendering a component with no URL context - if (nuxtApp._islandRender) { return } - const initialURL = process.client ? withoutBase(window.location.pathname, useRuntimeConfig().app.baseURL) + window.location.search + window.location.hash : nuxtApp.ssrContext!.url diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 961e807f71d..9ce01209fc7 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -20,7 +20,13 @@ export interface NuxtRenderHTMLContext { bodyAppend: string[] } -export interface NuxtComponentRenderResult { +export interface NuxtIslandRequest { + format?: 'html' | 'json' + name: string + props?: Record +} + +export interface NuxtIslandResponse { state: Record rendered: Array<{ html: string }> styles?: string @@ -106,22 +112,21 @@ const getSPARenderer = lazyCachedFunction(async () => { return { renderToString } }) -function parseRenderQuery (event: CompatibilityEvent) { - const query = getQuery(event) +async function readIslandsRequest (event: CompatibilityEvent): Promise { + const query = event.req.method === 'GET' ? getQuery(event) : await readBody(event) + // TODO: Validate name and props return { - ...query, - components: destr(query.components), - state: destr(query.state) + format: query.format, + name: query.name, + props: destr(query.props) || {} } } export default defineRenderHandler(async (event) => { // Whether we're rendering an error page const ssrError = event.req.url?.startsWith('/__nuxt_error') ? getQuery(event) as Exclude : null - const isolatedRenderCtx = event.req.url?.startsWith('/__nuxt_island') - ? event.req.method === 'GET' ? parseRenderQuery(event) : await readBody(event) - : null - const url: string = ssrError?.url as string || isolatedRenderCtx?.url || event.req.url! + const islandContext = event.req.url?.startsWith('/__nuxt_island') ? await readIslandsRequest(event) : undefined + const url: string = ssrError?.url as string || event.req.url! // Initialize ssr context const ssrContext: NuxtSSRContext = { @@ -133,10 +138,9 @@ export default defineRenderHandler(async (event) => { noSSR: !!event.req.headers['x-nuxt-no-ssr'], error: !!ssrError, nuxt: undefined!, /* NuxtApp */ - render: isolatedRenderCtx ? { components: isolatedRenderCtx.components || [] } : undefined, + islandContext, payload: { - ...ssrError ? { error: ssrError } : {}, - ...isolatedRenderCtx ? { state: isolatedRenderCtx.state || {} } : {} + ...ssrError ? { error: ssrError } : {} } as NuxtSSRContext['payload'] } @@ -162,36 +166,6 @@ export default defineRenderHandler(async (event) => { const nitroApp = useNitroApp() - // Render server components - if (isolatedRenderCtx) { - const components = Object.entries(ssrContext.teleports || []) - .filter(([key]) => key.startsWith('render-target')) - .map(([, value]) => ({ html: value.replace(/$/, '') })) - - const result: NuxtComponentRenderResult = { - rendered: components, - state: ssrContext.payload.state, - styles: _rendered.renderStyles() + (ssrContext.styles || ''), - scripts: (renderedMeta.bodyScriptsPrepend || '') + _rendered.renderScripts() + (renderedMeta.bodyScripts || '') - } - - // Allow hooking into the rendered result - await nitroApp.hooks.callHook('nuxt:component:rendered', result) - - // Construct JSON response - const response: RenderResponse = { - body: JSON.stringify(result), - statusCode: event.res.statusCode, - statusMessage: event.res.statusMessage, - headers: { - 'content-type': 'application/json;charset=utf-8', - 'x-powered-by': 'Nuxt' - } - } - - return response - } - // Create render context const htmlContext: NuxtRenderHTMLContext = { htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]), @@ -207,8 +181,10 @@ export default defineRenderHandler(async (event) => { ssrContext.teleports?.body ]), body: [ - // TODO: Rename to _rendered.body in next vue-bundle-renderer - _rendered.html + islandContext + ? ssrContext.teleports!['nuxt-island'] + .replace(/$/, '') + : _rendered.html ], bodyAppend: normalizeChunks([ ``, @@ -221,6 +197,20 @@ export default defineRenderHandler(async (event) => { // Allow hooking into the rendered result await nitroApp.hooks.callHook('render:html', htmlContext, { event }) + // Response for component islands + if (islandContext && islandContext.format !== 'html') { + const response: RenderResponse = { + body: JSON.stringify(htmlContext, null, 2), + statusCode: event.res.statusCode, + statusMessage: event.res.statusMessage, + headers: { + 'content-type': 'application/json;charset=utf-8', + 'x-powered-by': 'Nuxt' + } + } + return response + } + // Construct HTML response const response: RenderResponse = { body: renderHTMLDocument(htmlContext), @@ -264,3 +254,11 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) { ${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)} ` } + +function renderIslandComponent (html: NuxtRenderHTMLContext) { + return ` + +${joinTags(html.head)} +${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)} +` +} diff --git a/packages/nuxt/src/pages/runtime/router.ts b/packages/nuxt/src/pages/runtime/router.ts index c5b12409f53..0b50bd67a9d 100644 --- a/packages/nuxt/src/pages/runtime/router.ts +++ b/packages/nuxt/src/pages/runtime/router.ts @@ -49,9 +49,6 @@ function createCurrentLocation ( } export default defineNuxtPlugin(async (nuxtApp) => { - // Self-disable if we are rendering a component with no URL context - if (nuxtApp._islandRender) { return } - nuxtApp.vueApp.component('NuxtPage', NuxtPage) // TODO: remove before release - present for backwards compatibility & intentionally undocumented nuxtApp.vueApp.component('NuxtNestedPage', NuxtPage) From 9e18a8884122b29c73bf5e4709544f4c91c7fe71 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Aug 2022 10:14:53 +0200 Subject: [PATCH 29/73] fix lint and update test --- .../nuxt/src/core/runtime/nitro/renderer.ts | 14 +- test/basic.test.ts | 124 +++++++++--------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 9ce01209fc7..6be69677866 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -255,10 +255,10 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) { ` } -function renderIslandComponent (html: NuxtRenderHTMLContext) { - return ` - -${joinTags(html.head)} -${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)} -` -} +// function renderIslandComponent (html: NuxtRenderHTMLContext) { +// return ` +// +// ${joinTags(html.head)} +// ${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)} +// ` +// } diff --git a/test/basic.test.ts b/test/basic.test.ts index 827b6bd50ed..5d74a847a6c 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' // import { isWindows } from 'std-env' import { setup, fetch, $fetch, startServer } from '@nuxt/test-utils' import { withQuery } from 'ufo' -import type { NuxtComponentRenderResult } from '../packages/nuxt/src/core/runtime/nitro/renderer' +import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer' import { expectNoClientErrors } from './utils' await setup({ @@ -361,67 +361,6 @@ describe('automatically keyed composables', () => { }) }) -describe('selective rendering of islands components', () => { - it('renders components with route', async () => { - const result: NuxtComponentRenderResult = await $fetch(withQuery('/__nuxt_island', { - url: '/foo', - state: JSON.stringify({}), - components: JSON.stringify([ - { name: 'RouteComponent' } - ]) - })) - expect(result.rendered[0].html).toMatchInlineSnapshot(` - "
    Route: /foo
-        
" - `) - expect(result.state).toMatchInlineSnapshot('{}') - expect(Object.keys(result)).toMatchInlineSnapshot(` - [ - "rendered", - "state", - "styles", - "scripts", - ] - `) - }) - it('renders pure components', async () => { - const result: NuxtComponentRenderResult = await $fetch(withQuery('/__nuxt_island', { - components: JSON.stringify([ - { - name: 'PureComponent', - props: { - bool: false, - number: 3487, - str: 'something', - obj: { foo: 42, bar: false, me: 'hi' } - } - } - ]) - })) - expect(result.rendered[0].html.replace(/"/g, '"').replace(/ data-v-\w+>/, '>')).toMatchInlineSnapshot(` - "
    false
-          3487
-          \\"something\\"
-          {\\"foo\\":42,\\"bar\\":false,\\"me\\":\\"hi\\"}
-          Was router enabled: false
-        
" - `) - expect(result.state).toMatchInlineSnapshot('{}') - // TODO: fix bundle renderer issue with webpack - if (!process.env.TEST_WITH_WEBPACK) { - expect(result.styles).toMatch(/PureComponent[^"']*.css/) - } - expect(Object.keys(result)).toMatchInlineSnapshot(` - [ - "rendered", - "state", - "styles", - "scripts", - ] - `) - }) -}) - describe('dynamic paths', () => { if (process.env.NUXT_TEST_DEV) { // TODO: @@ -545,3 +484,64 @@ describe('app config', () => { expect(html).toContain(JSON.stringify(expectedAppConfig)) }) }) + +describe('component islands', () => { + it('renders components with route', async () => { + const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island', { + url: '/foo', + state: JSON.stringify({}), + components: JSON.stringify([ + { name: 'RouteComponent' } + ]) + })) + expect(result.rendered[0].html).toMatchInlineSnapshot(` + "
    Route: /foo
+        
" + `) + expect(result.state).toMatchInlineSnapshot('{}') + expect(Object.keys(result)).toMatchInlineSnapshot(` + [ + "rendered", + "state", + "styles", + "scripts", + ] + `) + }) + it('renders pure components', async () => { + const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island', { + components: JSON.stringify([ + { + name: 'PureComponent', + props: { + bool: false, + number: 3487, + str: 'something', + obj: { foo: 42, bar: false, me: 'hi' } + } + } + ]) + })) + expect(result.rendered[0].html.replace(/"/g, '"').replace(/ data-v-\w+>/, '>')).toMatchInlineSnapshot(` + "
    false
+          3487
+          \\"something\\"
+          {\\"foo\\":42,\\"bar\\":false,\\"me\\":\\"hi\\"}
+          Was router enabled: false
+        
" + `) + expect(result.state).toMatchInlineSnapshot('{}') + // TODO: fix bundle renderer issue with webpack + if (!process.env.TEST_WITH_WEBPACK) { + expect(result.styles).toMatch(/PureComponent[^"']*.css/) + } + expect(Object.keys(result)).toMatchInlineSnapshot(` + [ + "rendered", + "state", + "styles", + "scripts", + ] + `) + }) +}) From 8b8fa7a7b693357f8adcad0bb906f89fe7bb909c Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Aug 2022 10:35:51 +0200 Subject: [PATCH 30/73] update --- .../nuxt/src/core/runtime/nitro/renderer.ts | 42 ++++++++++++------- test/basic.test.ts | 16 +++---- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 6be69677866..5e8f8ad6d67 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -5,6 +5,8 @@ import { CompatibilityEvent, readBody, getQuery } from 'h3' import devalue from '@nuxt/devalue' import destr from 'destr' import { renderToString as _renderToString } from 'vue/server-renderer' +import { hash } from 'ohash' +import { snakeCase } from 'scule' import type { NuxtApp, NuxtSSRContext } from '#app' import { useRuntimeConfig, useNitroApp, defineRenderHandler } from '#internal/nitro' @@ -20,17 +22,17 @@ export interface NuxtRenderHTMLContext { bodyAppend: string[] } -export interface NuxtIslandRequest { +export interface NuxtIslandContext { + id: string format?: 'html' | 'json' name: string props?: Record } export interface NuxtIslandResponse { + island?: { id: string, html: string } state: Record - rendered: Array<{ html: string }> - styles?: string - scripts?: string + html: NuxtRenderHTMLContext } export interface NuxtRenderResponse { @@ -112,20 +114,23 @@ const getSPARenderer = lazyCachedFunction(async () => { return { renderToString } }) -async function readIslandsRequest (event: CompatibilityEvent): Promise { +async function getIslandContext (event: CompatibilityEvent): Promise { const query = event.req.method === 'GET' ? getQuery(event) : await readBody(event) // TODO: Validate name and props - return { + const ctx: NuxtIslandContext = { + id: null as any, format: query.format, name: query.name, props: destr(query.props) || {} } + ctx.id = 'island_' + snakeCase(ctx.name) + '_' + hash([ctx.props]).toLocaleLowerCase() + return ctx } export default defineRenderHandler(async (event) => { // Whether we're rendering an error page const ssrError = event.req.url?.startsWith('/__nuxt_error') ? getQuery(event) as Exclude : null - const islandContext = event.req.url?.startsWith('/__nuxt_island') ? await readIslandsRequest(event) : undefined + const islandContext = event.req.url?.startsWith('/__nuxt_island') ? await getIslandContext(event) : undefined const url: string = ssrError?.url as string || event.req.url! // Initialize ssr context @@ -180,12 +185,11 @@ export default defineRenderHandler(async (event) => { renderedMeta.bodyScriptsPrepend, ssrContext.teleports?.body ]), - body: [ - islandContext - ? ssrContext.teleports!['nuxt-island'] - .replace(/$/, '') - : _rendered.html - ], + body: islandContext + ? [] + : [ + _rendered.html + ], bodyAppend: normalizeChunks([ ``, _rendered.renderScripts(), @@ -199,8 +203,18 @@ export default defineRenderHandler(async (event) => { // Response for component islands if (islandContext && islandContext.format !== 'html') { + const islandReponse: NuxtIslandResponse = { + island: { + id: islandContext.id, + html: ssrContext.teleports!['nuxt-island'] + .replace(/$/, '') + }, + state: ssrContext.payload.state, + html: htmlContext + } + const response: RenderResponse = { - body: JSON.stringify(htmlContext, null, 2), + body: JSON.stringify(islandReponse, null, 2), statusCode: event.res.statusCode, statusMessage: event.res.statusMessage, headers: { diff --git a/test/basic.test.ts b/test/basic.test.ts index 5d74a847a6c..03461ec6ad7 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -489,12 +489,9 @@ describe('component islands', () => { it('renders components with route', async () => { const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island', { url: '/foo', - state: JSON.stringify({}), - components: JSON.stringify([ - { name: 'RouteComponent' } - ]) + name: 'RouteComponent' })) - expect(result.rendered[0].html).toMatchInlineSnapshot(` + expect(result.island.html).toMatchInlineSnapshot(` "
    Route: /foo
         
" `) @@ -522,7 +519,7 @@ describe('component islands', () => { } ]) })) - expect(result.rendered[0].html.replace(/"/g, '"').replace(/ data-v-\w+>/, '>')).toMatchInlineSnapshot(` + expect(result.island.html.replace(/"/g, '"').replace(/ data-v-\w+>/, '>')).toMatchInlineSnapshot(` "
    false
           3487
           \\"something\\"
@@ -533,14 +530,13 @@ describe('component islands', () => {
     expect(result.state).toMatchInlineSnapshot('{}')
     // TODO: fix bundle renderer issue with webpack
     if (!process.env.TEST_WITH_WEBPACK) {
-      expect(result.styles).toMatch(/PureComponent[^"']*.css/)
+      expect(result.html.head.join(' ')).toMatch(/PureComponent[^"']*.css/)
     }
     expect(Object.keys(result)).toMatchInlineSnapshot(`
       [
-        "rendered",
+        "island",
         "state",
-        "styles",
-        "scripts",
+        "html"
       ]
     `)
   })

From d610fbb4f5f9f78970168c0b8eda703f53341e00 Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 10:43:04 +0200
Subject: [PATCH 31/73] update types

---
 packages/nuxt/src/app/nuxt.ts | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts
index 789ae0852ff..26a8dca0d75 100644
--- a/packages/nuxt/src/app/nuxt.ts
+++ b/packages/nuxt/src/app/nuxt.ts
@@ -6,8 +6,6 @@ import type { RuntimeConfig, AppConfigInput } from '@nuxt/schema'
 import { getContext } from 'unctx'
 import type { SSRContext } from 'vue-bundle-renderer/runtime'
 import type { CompatibilityEvent } from 'h3'
-// eslint-disable-next-line import/no-restricted-paths
-import type { NuxtComponentRenderResult } from '../core/runtime/nitro/renderer'
 
 const nuxtAppCtx = getContext('nuxt-app')
 
@@ -33,7 +31,6 @@ export interface RuntimeNuxtHooks {
   'app:error': (err: any) => HookResult
   'app:error:cleared': (options: { redirect?: string }) => HookResult
   'app:data:refresh': (keys?: string[]) => HookResult
-  'component:rendered': (ctx: NuxtComponentRenderResult) => HookResult
   'page:start': (Component?: VNode) => HookResult
   'page:finish': (Component?: VNode) => HookResult
   'meta:register': (metaRenderers: Array<(nuxt: NuxtApp) => NuxtMeta | Promise>) => HookResult

From 1cb6406871188b2f6b5f2e5f8a13d55af355f0fd Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 11:44:43 +0200
Subject: [PATCH 32/73] remove server-components plugin

to be added in server-only components PR
---
 packages/nuxt/src/components/module.ts        |  6 ----
 .../components/plugins/server-components.ts   | 36 -------------------
 2 files changed, 42 deletions(-)
 delete mode 100644 packages/nuxt/src/components/plugins/server-components.ts

diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts
index 96779b14131..f1a4ead0e84 100644
--- a/packages/nuxt/src/components/module.ts
+++ b/packages/nuxt/src/components/module.ts
@@ -6,7 +6,6 @@ import { componentsPluginTemplate, componentsTemplate, componentsIslandsTemplate
 import { scanComponents } from './scan'
 import { loaderPlugin } from './loader'
 import { TreeShakeTemplatePlugin } from './tree-shake'
-import { stripServerComponentsPlugin } from './plugins/server-components'
 
 const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
 const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch (_e) { return false } }
@@ -135,11 +134,6 @@ export default defineNuxtModule({
     // Register islands import
     addTemplate({ ...componentsIslandsTemplate, filename: 'components-islands.mjs', options: { getComponents } })
 
-    // Strip server component chunks (for example, islands) from client build
-
-    addVitePlugin(stripServerComponentsPlugin.vite({ getComponents, sourcemap: nuxt.options.sourcemap }), { server: false })
-    addWebpackPlugin(stripServerComponentsPlugin.webpack({ getComponents, sourcemap: nuxt.options.sourcemap }), { server: false })
-
     // Scan components and add to plugin
     nuxt.hook('app:templates', async () => {
       const newComponents = await scanComponents(componentDirs, nuxt.options.srcDir!)
diff --git a/packages/nuxt/src/components/plugins/server-components.ts b/packages/nuxt/src/components/plugins/server-components.ts
deleted file mode 100644
index c3cb908b362..00000000000
--- a/packages/nuxt/src/components/plugins/server-components.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-
-import { Component } from '@nuxt/schema'
-import MagicString from 'magic-string'
-import { findStaticImports } from 'mlly'
-import { createUnplugin } from 'unplugin'
-
-interface StripServerComponentsPluginOptions {
-  getComponents: (mode?: 'client' | 'server' | 'all') => Component[]
-  sourcemap?: boolean
-}
-
-export const stripServerComponentsPlugin = createUnplugin((options: StripServerComponentsPluginOptions) => {
-  return {
-    name: 'nuxt:components:strip-server-components',
-    enforce: 'post',
-    transformInclude (id) {
-      const components = options.getComponents()
-      return components.some(c => c.filePath === id && c.mode === 'server' && !components.some(o => o.mode === 'client' && o.pascalName === c.pascalName))
-    },
-    transform (code, id) {
-      const s = new MagicString(code)
-      const imports = findStaticImports(code)
-        .filter(i => i.type === 'static' && i.specifier.match(/\.css$/))
-        .map(i => i.code)
-
-      s.overwrite(0, code.length, imports.join('\n'))
-
-      if (s.hasChanged()) {
-        return {
-          code: s.toString(),
-          map: options.sourcemap && s.generateMap({ source: id, includeContent: true })
-        }
-      }
-    }
-  }
-})

From b41bb844b7190a9383e268cadf50d4d5cb12dfdd Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 11:46:10 +0200
Subject: [PATCH 33/73] add render:island hook

---
 packages/nuxt/src/core/runtime/nitro/renderer.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index 5e8f8ad6d67..40553680d1a 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -14,6 +14,7 @@ import { useRuntimeConfig, useNitroApp, defineRenderHandler } from '#internal/ni
 import { buildAssetsURL } from '#paths'
 
 export interface NuxtRenderHTMLContext {
+  island?: boolean
   htmlAttrs: string[]
   head: string[]
   bodyAttrs: string[]
@@ -173,6 +174,7 @@ export default defineRenderHandler(async (event) => {
 
   // Create render context
   const htmlContext: NuxtRenderHTMLContext = {
+    island: Boolean(islandContext),
     htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
     head: normalizeChunks([
       renderedMeta.headTags,
@@ -213,6 +215,8 @@ export default defineRenderHandler(async (event) => {
       html: htmlContext
     }
 
+    await nitroApp.hooks.callHook('render:island', islandReponse, { event, islandContext })
+
     const response: RenderResponse = {
       body: JSON.stringify(islandReponse, null, 2),
       statusCode: event.res.statusCode,

From 66d61fa3bf94db6e15a36d297beb3d686da1f5bf Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 13:20:37 +0200
Subject: [PATCH 34/73] improved url format for caching and prerendering

---
 .../nuxt/src/core/runtime/nitro/renderer.ts   | 28 +++++++++++--------
 1 file changed, 17 insertions(+), 11 deletions(-)

diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index 40553680d1a..46b79053117 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -6,7 +6,6 @@ import devalue from '@nuxt/devalue'
 import destr from 'destr'
 import { renderToString as _renderToString } from 'vue/server-renderer'
 import { hash } from 'ohash'
-import { snakeCase } from 'scule'
 import type { NuxtApp, NuxtSSRContext } from '#app'
 import { useRuntimeConfig, useNitroApp, defineRenderHandler } from '#internal/nitro'
 
@@ -24,14 +23,15 @@ export interface NuxtRenderHTMLContext {
 }
 
 export interface NuxtIslandContext {
-  id: string
+  id?: string
+  url?: string
   format?: 'html' | 'json'
   name: string
   props?: Record
 }
 
 export interface NuxtIslandResponse {
-  island?: { id: string, html: string }
+  island?: { id?: string, html: string }
   state: Record
   html: NuxtRenderHTMLContext
 }
@@ -116,15 +116,21 @@ const getSPARenderer = lazyCachedFunction(async () => {
 })
 
 async function getIslandContext (event: CompatibilityEvent): Promise {
-  const query = event.req.method === 'GET' ? getQuery(event) : await readBody(event)
-  // TODO: Validate name and props
+  // TODO: Strict validation for url
+  const [_, componentName, hashId] =
+    (event.req.url?.substring('/__nuxt_island'.length + 1) || '').match(/([^:]+):?([^/?]+)?/) || []
+
+  // TODO: Validate context
+  const context = event.req.method === 'GET' ? getQuery(event) : await readBody(event)
+
   const ctx: NuxtIslandContext = {
-    id: null as any,
-    format: query.format,
-    name: query.name,
-    props: destr(query.props) || {}
+    url: '/',
+    ...context,
+    id: hashId,
+    name: componentName,
+    props: destr(context.props) || {}
   }
-  ctx.id = 'island_' + snakeCase(ctx.name) + '_' + hash([ctx.props]).toLocaleLowerCase()
+
   return ctx
 }
 
@@ -132,7 +138,7 @@ export default defineRenderHandler(async (event) => {
   // Whether we're rendering an error page
   const ssrError = event.req.url?.startsWith('/__nuxt_error') ? getQuery(event) as Exclude : null
   const islandContext = event.req.url?.startsWith('/__nuxt_island') ? await getIslandContext(event) : undefined
-  const url: string = ssrError?.url as string || event.req.url!
+  const url: string = ssrError?.url as string || islandContext?.url || event.req.url!
 
   // Initialize ssr context
   const ssrContext: NuxtSSRContext = {

From 6f8a2d9a48ec283b180467723c759cd577415cbb Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 13:20:51 +0200
Subject: [PATCH 35/73] handle 404 components

---
 packages/nuxt/src/app/components/island-renderer.ts | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/packages/nuxt/src/app/components/island-renderer.ts b/packages/nuxt/src/app/components/island-renderer.ts
index 72ca76054c9..e8a6dcd80ad 100644
--- a/packages/nuxt/src/app/components/island-renderer.ts
+++ b/packages/nuxt/src/app/components/island-renderer.ts
@@ -2,6 +2,7 @@ import { createBlock, defineComponent, h, Teleport } from 'vue'
 
 // @ts-ignore
 import * as islandComponents from '#build/components-islands.mjs'
+import { createError } from '#app'
 
 export default defineComponent({
   props: {
@@ -13,9 +14,18 @@ export default defineComponent({
   async setup (props) {
     // TODO: https://github.com/vuejs/core/issues/6207
     const component = islandComponents[props.context.name]
+
+    if (!component) {
+      throw createError({
+        statusCode: 404,
+        statusMessage: `Island component not found: ${JSON.stringify(component)}`
+      })
+    }
+
     if (typeof component === 'object') {
       await component.__asyncLoader?.()
     }
+
     return () => [
       createBlock(Teleport as any, { to: 'nuxt-island' }, [h(component || 'span', props.context.props)])
     ]

From 5372a87740cd4e6f98e6c12febbbf4fb42d4044e Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 13:37:13 +0200
Subject: [PATCH 36/73] feat: `NuxtIsland` component

---
 .../nuxt/src/app/components/nuxt-island.ts    | 77 +++++++++++++++++++
 packages/nuxt/src/app/nuxt.ts                 |  1 -
 packages/nuxt/src/core/nuxt.ts                |  6 ++
 .../components/islands/PureComponent.vue      | 10 +--
 test/fixtures/basic/pages/islands.vue         | 22 ++++++
 5 files changed, 109 insertions(+), 7 deletions(-)
 create mode 100644 packages/nuxt/src/app/components/nuxt-island.ts
 create mode 100644 test/fixtures/basic/pages/islands.vue

diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts
new file mode 100644
index 00000000000..ffb49d81612
--- /dev/null
+++ b/packages/nuxt/src/app/components/nuxt-island.ts
@@ -0,0 +1,77 @@
+import { defineComponent, createStaticVNode, computed, ref, watch } from 'vue'
+import { debounce } from 'perfect-debounce'
+import { hash } from 'ohash'
+// eslint-disable-next-line import/no-restricted-paths
+import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
+import { useHead } from '#app'
+
+export default defineComponent({
+  name: 'NuxtIsland',
+  props: {
+    name: {
+      type: String,
+      required: true
+    },
+    props: {
+      type: Object,
+      default: () => undefined
+    },
+    context: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  async setup (props) {
+    const hashId = computed(() => hash([props.props, props.context]))
+    const html = ref('')
+
+    async function fetchComponent () {
+      const islandResponse = await $fetch(`${props.name}:${hashId.value}`, {
+        baseURL: '/__nuxt_island',
+        params: {
+          ...props.context,
+          props: props.props ? JSON.stringify(props.props) : undefined
+        }
+      })
+      // TODO: Validate response
+      injectHead(islandResponse.html)
+      // Update html
+      html.value = islandResponse.island?.html!
+    }
+
+    if (process.server) {
+      await fetchComponent()
+    }
+
+    if (process.client) {
+      watch(props, debounce(fetchComponent, 100))
+    }
+
+    return () => createStaticVNode(html.value, 1)
+  }
+})
+
+// --- Internal ---
+
+function injectHead (html: NuxtIslandResponse['html']) {
+  const tags = extractTags(html.head.join(''))
+  useHead({
+    link: tags.filter(tag => tag.tag === 'link').map(tag => tag.attrs)
+  })
+}
+
+// Tag parsing utils
+// TOOD: Move to external library
+const HTML_TAG_RE = /<(?[a-z]+)(? [^>]*)>/g
+const HTML_TAG_ATTR_RE = /(?[a-z]+)=(?"[^"]*"|'[^']*'|[^ >]*)/g
+function extractTags (html: string) {
+  const tags: {tag: string, attrs: Record}[] = []
+  for (const tagMatch of html.matchAll(HTML_TAG_RE)) {
+    const attrs = {} as Record
+    for (const attraMatch of tagMatch.groups!.rawAttrs.matchAll(HTML_TAG_ATTR_RE)) {
+      attrs[attraMatch.groups!.name] = attraMatch.groups!.value
+    }
+    tags.push({ tag: tagMatch.groups!.tag, attrs })
+  }
+  return tags
+}
diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts
index 26a8dca0d75..21c88189ebb 100644
--- a/packages/nuxt/src/app/nuxt.ts
+++ b/packages/nuxt/src/app/nuxt.ts
@@ -113,7 +113,6 @@ export function createNuxtApp (options: CreateOptions) {
       data: {},
       state: {},
       _errors: {},
-      ...options.ssrContext?.payload || {},
       ...(process.client ? window.__NUXT__ : { serverRendered: true })
     }),
     isHydrating: process.client,
diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts
index ddd227dc987..dd222bec136 100644
--- a/packages/nuxt/src/core/nuxt.ts
+++ b/packages/nuxt/src/core/nuxt.ts
@@ -134,6 +134,12 @@ async function initNuxt (nuxt: Nuxt) {
     filePath: resolve(nuxt.options.appDir, 'components/nuxt-loading-indicator')
   })
 
+  // Add 
+  addComponent({
+    name: 'NuxtIsland',
+    filePath: resolve(nuxt.options.appDir, 'components/nuxt-island')
+  })
+
   for (const m of modulesToInstall) {
     if (Array.isArray(m)) {
       await installModule(m[0], m[1])
diff --git a/test/fixtures/basic/components/islands/PureComponent.vue b/test/fixtures/basic/components/islands/PureComponent.vue
index 10b052dd942..7423703a43e 100644
--- a/test/fixtures/basic/components/islands/PureComponent.vue
+++ b/test/fixtures/basic/components/islands/PureComponent.vue
@@ -5,10 +5,8 @@ defineProps({
   str: String,
   obj: Object
 })
-let wasRouter = false
-try {
-  wasRouter = !!useRoute()
-} catch (e) {}
+
+const hasRouter = useState('hasRouter', () => !!useRouter())
 
 
 
 
 
diff --git a/test/fixtures/basic/pages/islands.vue b/test/fixtures/basic/pages/islands.vue
new file mode 100644
index 00000000000..acf371297d6
--- /dev/null
+++ b/test/fixtures/basic/pages/islands.vue
@@ -0,0 +1,22 @@
+
+
+

From 7cad46308c38059e372d72538036c18d24203f62 Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 13:45:23 +0200
Subject: [PATCH 37/73] chore: remove unused imports

---
 packages/nuxt/src/components/module.ts           | 2 +-
 packages/nuxt/src/core/runtime/nitro/renderer.ts | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts
index f1a4ead0e84..c74e44b64e2 100644
--- a/packages/nuxt/src/components/module.ts
+++ b/packages/nuxt/src/components/module.ts
@@ -1,6 +1,6 @@
 import { statSync } from 'node:fs'
 import { relative, resolve } from 'pathe'
-import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate, addVitePlugin, addWebpackPlugin } from '@nuxt/kit'
+import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate } from '@nuxt/kit'
 import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema'
 import { componentsPluginTemplate, componentsTemplate, componentsIslandsTemplate, componentsTypeTemplate } from './templates'
 import { scanComponents } from './scan'
diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index 46b79053117..4f20ff18120 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -5,7 +5,6 @@ import { CompatibilityEvent, readBody, getQuery } from 'h3'
 import devalue from '@nuxt/devalue'
 import destr from 'destr'
 import { renderToString as _renderToString } from 'vue/server-renderer'
-import { hash } from 'ohash'
 import type { NuxtApp, NuxtSSRContext } from '#app'
 import { useRuntimeConfig, useNitroApp, defineRenderHandler } from '#internal/nitro'
 

From 752dfc0921380d7b6d3916f5ea521564591ad1fd Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 13:49:03 +0200
Subject: [PATCH 38/73] nullify island renderer for client-side

---
 packages/nuxt/src/app/components/nuxt-root.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/nuxt/src/app/components/nuxt-root.vue b/packages/nuxt/src/app/components/nuxt-root.vue
index 14592c61f55..423112414a4 100644
--- a/packages/nuxt/src/app/components/nuxt-root.vue
+++ b/packages/nuxt/src/app/components/nuxt-root.vue
@@ -13,7 +13,7 @@ import { callWithNuxt, isNuxtError, showError, useError, useRoute, useNuxtApp }
 const ErrorComponent = defineAsyncComponent(() => import('#build/error-component.mjs'))
 const IslandRendererer = process.server
   ? defineAsyncComponent(() => import('./island-renderer').then(r => r.default || r))
-  : () => import('#build/island-renderer.mjs')
+  : () => null
 
 const nuxtApp = useNuxtApp()
 const onResolve = () => nuxtApp.callHook('app:suspense:resolve')

From 2891b0b23052f67e0ba0962e3ca86325726bbaa3 Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 13:58:30 +0200
Subject: [PATCH 39/73] improve url matching

---
 packages/nuxt/src/core/runtime/nitro/renderer.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index 4f20ff18120..c5d94c22add 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -116,8 +116,8 @@ const getSPARenderer = lazyCachedFunction(async () => {
 
 async function getIslandContext (event: CompatibilityEvent): Promise {
   // TODO: Strict validation for url
-  const [_, componentName, hashId] =
-    (event.req.url?.substring('/__nuxt_island'.length + 1) || '').match(/([^:]+):?([^/?]+)?/) || []
+  const url = event.req.url?.substring('/__nuxt_island'.length + 1) || ''
+  const [componentName, hashId] = url.split('?')[0].split(/\//)
 
   // TODO: Validate context
   const context = event.req.method === 'GET' ? getQuery(event) : await readBody(event)

From 71a0c884191d503c5426d2f75d85fcb0f4e0a75e Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 14:05:28 +0200
Subject: [PATCH 40/73] fix hashId matching

---
 packages/nuxt/src/core/runtime/nitro/renderer.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index c5d94c22add..1be276120c9 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -117,7 +117,7 @@ const getSPARenderer = lazyCachedFunction(async () => {
 async function getIslandContext (event: CompatibilityEvent): Promise {
   // TODO: Strict validation for url
   const url = event.req.url?.substring('/__nuxt_island'.length + 1) || ''
-  const [componentName, hashId] = url.split('?')[0].split(/\//)
+  const [componentName, hashId] = url.split('?')[0].split(':')
 
   // TODO: Validate context
   const context = event.req.method === 'GET' ? getQuery(event) : await readBody(event)

From d5b8c1e04845b28f727dbb08f363f8633f1214d2 Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 14:11:52 +0200
Subject: [PATCH 41/73] update tests

---
 test/basic.test.ts                            | 74 ++++++++++---------
 .../components/islands/PureComponent.vue      | 14 ++--
 test/fixtures/basic/pages/islands.vue         | 32 +++++---
 3 files changed, 67 insertions(+), 53 deletions(-)

diff --git a/test/basic.test.ts b/test/basic.test.ts
index 03461ec6ad7..87bf8a58aec 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -487,56 +487,60 @@ describe('app config', () => {
 
 describe('component islands', () => {
   it('renders components with route', async () => {
-    const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island', {
-      url: '/foo',
-      name: 'RouteComponent'
-    }))
-    expect(result.island.html).toMatchInlineSnapshot(`
-      "
    Route: /foo
-        
" + const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo') + expect(result.island).toMatchInlineSnapshot(` + { + "html": "
    Route: /foo
+        
", + } `) expect(result.state).toMatchInlineSnapshot('{}') expect(Object.keys(result)).toMatchInlineSnapshot(` [ - "rendered", + "island", "state", - "styles", - "scripts", + "html", ] `) }) it('renders pure components', async () => { - const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island', { - components: JSON.stringify([ - { - name: 'PureComponent', - props: { - bool: false, - number: 3487, - str: 'something', - obj: { foo: 42, bar: false, me: 'hi' } - } - } - ]) + const result: NuxtIslandResponse = await $fetch(withQuery('/__nuxt_island/PureComponent', { + props: JSON.stringify({ + bool: false, + number: 3487, + str: 'something', + obj: { foo: 42, bar: false, me: 'hi' } + }) })) - expect(result.island.html.replace(/"/g, '"').replace(/ data-v-\w+>/, '>')).toMatchInlineSnapshot(` - "
    false
-          3487
-          \\"something\\"
-          {\\"foo\\":42,\\"bar\\":false,\\"me\\":\\"hi\\"}
-          Was router enabled: false
-        
" + + expect(result.island).toMatchInlineSnapshot(` + { + "html": "
Was router enabled: true
Props:
{
+        \\"number\\": 3487,
+        \\"str\\": \\"something\\",
+        \\"obj\\": {
+          \\"foo\\": 42,
+          \\"bar\\": false,
+          \\"me\\": \\"hi\\"
+        },
+        \\"bool\\": false
+      }
", + } `) - expect(result.state).toMatchInlineSnapshot('{}') - // TODO: fix bundle renderer issue with webpack - if (!process.env.TEST_WITH_WEBPACK) { - expect(result.html.head.join(' ')).toMatch(/PureComponent[^"']*.css/) - } + expect(result.state).toMatchInlineSnapshot(` + { + "$shasRouter": true, + } + `) + + // TODO: Test response differes! + // expect(result.html.head.join(' ')).toMatch(/PureComponent[^"']*.css/) + expect(Object.keys(result)).toMatchInlineSnapshot(` [ "island", "state", - "html" + "html", ] `) }) diff --git a/test/fixtures/basic/components/islands/PureComponent.vue b/test/fixtures/basic/components/islands/PureComponent.vue index 7423703a43e..61a3941cedd 100644 --- a/test/fixtures/basic/components/islands/PureComponent.vue +++ b/test/fixtures/basic/components/islands/PureComponent.vue @@ -1,5 +1,5 @@ From 8994268d674626d7a13164ce0a7df5296f976178 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Aug 2022 14:19:31 +0200 Subject: [PATCH 42/73] use NuxtIslandContext for nuxt app --- packages/nuxt/src/app/nuxt.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 21c88189ebb..3a45c5762eb 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -6,6 +6,8 @@ import type { RuntimeConfig, AppConfigInput } from '@nuxt/schema' import { getContext } from 'unctx' import type { SSRContext } from 'vue-bundle-renderer/runtime' import type { CompatibilityEvent } from 'h3' +// eslint-disable-next-line import/no-restricted-paths +import type { NuxtIslandContext } from '../core/runtime/nitro/renderer' const nuxtAppCtx = getContext('nuxt-app') @@ -53,10 +55,7 @@ export interface NuxtSSRContext extends SSRContext { payload: _NuxtApp['payload'] teleports?: Record renderMeta?: () => Promise | NuxtMeta - islandContext?: { - name: string - props?: Record - } + islandContext?: NuxtIslandContext } interface _NuxtApp { From 3956bfb06ca8c6c9b83616b213af829c39dfbb82 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Aug 2022 14:19:44 +0200 Subject: [PATCH 43/73] refactor: reduce git diff --- .../nuxt/src/core/runtime/nitro/renderer.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 1be276120c9..cd642892c57 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -134,6 +134,8 @@ async function getIslandContext (event: CompatibilityEvent): Promise { + const nitroApp = useNitroApp() + // Whether we're rendering an error page const ssrError = event.req.url?.startsWith('/__nuxt_error') ? getQuery(event) as Exclude : null const islandContext = event.req.url?.startsWith('/__nuxt_island') ? await getIslandContext(event) : undefined @@ -149,10 +151,8 @@ export default defineRenderHandler(async (event) => { noSSR: !!event.req.headers['x-nuxt-no-ssr'], error: !!ssrError, nuxt: undefined!, /* NuxtApp */ - islandContext, - payload: { - ...ssrError ? { error: ssrError } : {} - } as NuxtSSRContext['payload'] + payload: ssrError ? { error: ssrError } as NuxtSSRContext['payload'] : undefined!, + islandContext } // Render app @@ -175,8 +175,6 @@ export default defineRenderHandler(async (event) => { // Render meta const renderedMeta = await ssrContext.renderMeta?.() ?? {} - const nitroApp = useNitroApp() - // Create render context const htmlContext: NuxtRenderHTMLContext = { island: Boolean(islandContext), @@ -277,11 +275,3 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) { ${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)} ` } - -// function renderIslandComponent (html: NuxtRenderHTMLContext) { -// return ` -// -// ${joinTags(html.head)} -// ${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)} -// ` -// } From 1101ebac24b193acb5c2ea1bb797138324cea265 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Aug 2022 14:19:59 +0200 Subject: [PATCH 44/73] fix: add name to hashId to make it always unique --- packages/nuxt/src/app/components/nuxt-island.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index ffb49d81612..fefc6432439 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -22,7 +22,7 @@ export default defineComponent({ } }, async setup (props) { - const hashId = computed(() => hash([props.props, props.context])) + const hashId = computed(() => hash([props.name, props.props, props.context])) const html = ref('') async function fetchComponent () { From 340f858757b8f6065884c37ef0a167ad4cdf7306 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Aug 2022 14:26:36 +0200 Subject: [PATCH 45/73] update snapshot test --- test/basic.test.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/test/basic.test.ts b/test/basic.test.ts index 87bf8a58aec..22c33f854ce 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -513,19 +513,17 @@ describe('component islands', () => { }) })) - expect(result.island).toMatchInlineSnapshot(` - { - "html": "
Was router enabled: true
Props:
{
-        \\"number\\": 3487,
-        \\"str\\": \\"something\\",
-        \\"obj\\": {
-          \\"foo\\": 42,
-          \\"bar\\": false,
-          \\"me\\": \\"hi\\"
+    expect(result.island.html.replace(/data-v-\w+|"/g, '')).toMatchInlineSnapshot(`
+      "
Was router enabled: true
Props:
{
+        number: 3487,
+        str: something,
+        obj: {
+          foo: 42,
+          bar: false,
+          me: hi
         },
-        \\"bool\\": false
-      }
", - } + bool: false + }
" `) expect(result.state).toMatchInlineSnapshot(` { From 811c193a96fbc1980861e3297a3db48b1271842e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Aug 2022 14:37:22 +0200 Subject: [PATCH 46/73] fix: split component islands --- packages/nuxt/src/app/components/island-renderer.ts | 2 +- packages/nuxt/src/components/module.ts | 5 ++--- packages/nuxt/src/components/templates.ts | 9 +++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/nuxt/src/app/components/island-renderer.ts b/packages/nuxt/src/app/components/island-renderer.ts index e8a6dcd80ad..ae4642efaa9 100644 --- a/packages/nuxt/src/app/components/island-renderer.ts +++ b/packages/nuxt/src/app/components/island-renderer.ts @@ -1,7 +1,7 @@ import { createBlock, defineComponent, h, Teleport } from 'vue' // @ts-ignore -import * as islandComponents from '#build/components-islands.mjs' +import * as islandComponents from '#build/components.islands.mjs' import { createError } from '#app' export default defineComponent({ diff --git a/packages/nuxt/src/components/module.ts b/packages/nuxt/src/components/module.ts index c74e44b64e2..d82bc464a4d 100644 --- a/packages/nuxt/src/components/module.ts +++ b/packages/nuxt/src/components/module.ts @@ -119,6 +119,8 @@ export default defineNuxtModule({ addTemplate({ ...componentsTemplate, filename: 'components.server.mjs', options: { getComponents, mode: 'server' } }) // components.client.mjs addTemplate({ ...componentsTemplate, filename: 'components.client.mjs', options: { getComponents, mode: 'client' } }) + // components.islands.mjs + addTemplate({ ...componentsIslandsTemplate, filename: 'components.islands.mjs', options: { getComponents } }) nuxt.hook('vite:extendConfig', (config, { isClient }) => { const mode = isClient ? 'client' : 'server' @@ -131,9 +133,6 @@ export default defineNuxtModule({ } }) - // Register islands import - addTemplate({ ...componentsIslandsTemplate, filename: 'components-islands.mjs', options: { getComponents } }) - // Scan components and add to plugin nuxt.hook('app:templates', async () => { const newComponents = await scanComponents(componentDirs, nuxt.options.srcDir!) diff --git a/packages/nuxt/src/components/templates.ts b/packages/nuxt/src/components/templates.ts index d850f429389..61efe2d700d 100644 --- a/packages/nuxt/src/components/templates.ts +++ b/packages/nuxt/src/components/templates.ts @@ -55,7 +55,7 @@ export const componentsTemplate = { getContents ({ options }: ComponentsTemplateContext) { return [ 'import { defineAsyncComponent } from \'vue\'', - ...options.getComponents(options.mode).flatMap((c) => { + ...options.getComponents(options.mode).filter(c => !c.island).flatMap((c) => { const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']` const comment = createImportMagicComments(c) @@ -64,14 +64,15 @@ export const componentsTemplate = { `export const Lazy${c.pascalName} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))` ] }), - `export const componentNames = ${JSON.stringify(options.getComponents().map(c => c.pascalName))}` + `export const componentNames = ${JSON.stringify(options.getComponents().filter(c => !c.island).map(c => c.pascalName))}` ].join('\n') } } export const componentsIslandsTemplate = { + // components.islands.mjs' getContents ({ options }: ComponentsTemplateContext) { - return options.getComponents().filter(c => c.island === true).map( + return options.getComponents().filter(c => c.island).map( (c) => { const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']` const comment = createImportMagicComments(c) @@ -85,7 +86,7 @@ export const componentsTypeTemplate = { filename: 'components.d.ts', getContents: ({ options, nuxt }: ComponentsTemplateContext) => { const buildDir = nuxt.options.buildDir - const componentTypes = options.getComponents().map(c => [ + const componentTypes = options.getComponents().filter(c => !c.island).map(c => [ c.pascalName, `typeof ${genDynamicImport(isAbsolute(c.filePath) ? relative(buildDir, c.filePath) : c.filePath, { wrapper: false })}['${c.export}']` ]) From 5d157082b663e908cca354ffffca6ad28e1d0625 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Aug 2022 14:50:32 +0200 Subject: [PATCH 47/73] nuxt-island: test client side and handle parallel requests to same --- .../nuxt/src/app/components/nuxt-island.ts | 28 +++++++++++++------ test/fixtures/basic/pages/islands.vue | 9 +++++- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts index fefc6432439..61fdd2ea945 100644 --- a/packages/nuxt/src/app/components/nuxt-island.ts +++ b/packages/nuxt/src/app/components/nuxt-island.ts @@ -3,7 +3,9 @@ import { debounce } from 'perfect-debounce' import { hash } from 'ohash' // eslint-disable-next-line import/no-restricted-paths import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer' -import { useHead } from '#app' +import { useHead, useNuxtApp } from '#app' + +const pKey = '_islandPromises' export default defineComponent({ name: 'NuxtIsland', @@ -22,24 +24,34 @@ export default defineComponent({ } }, async setup (props) { + const nuxtApp = useNuxtApp() const hashId = computed(() => hash([props.name, props.props, props.context])) const html = ref('') - async function fetchComponent () { - const islandResponse = await $fetch(`${props.name}:${hashId.value}`, { + function _fetchComponent () { + // TODO: Validate response + return $fetch(`${props.name}:${hashId.value}`, { baseURL: '/__nuxt_island', params: { ...props.context, props: props.props ? JSON.stringify(props.props) : undefined } }) - // TODO: Validate response - injectHead(islandResponse.html) - // Update html - html.value = islandResponse.island?.html! } - if (process.server) { + async function fetchComponent () { + nuxtApp[pKey] = nuxtApp[pKey] || {} + if (!nuxtApp[pKey][hashId.value]) { + nuxtApp[pKey][hashId.value] = _fetchComponent().finally(() => { + delete nuxtApp[pKey][hashId.value] + }) + } + const res = await nuxtApp[pKey][hashId.value] + injectHead(res.html) + html.value = res.island?.html! + } + + if (process.server || !nuxtApp.isHydrating) { await fetchComponent() } diff --git a/test/fixtures/basic/pages/islands.vue b/test/fixtures/basic/pages/islands.vue index 5e40da4afe4..ca229bc8a48 100644 --- a/test/fixtures/basic/pages/islands.vue +++ b/test/fixtures/basic/pages/islands.vue @@ -5,6 +5,8 @@ const islandProps = ref({ str: 'helo world', obj: { json: 'works' } }) + +const routeIslandVisible = ref(false) @@ -28,5 +34,6 @@ const islandProps = ref({ .box { border: 1px solid black; margin: 3px; + display: flex; } From 82e919c8087ac047c2038b8a02f73efa02027545 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Aug 2022 14:54:37 +0200 Subject: [PATCH 48/73] remove comments from snapshot --- test/basic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/basic.test.ts b/test/basic.test.ts index 22c33f854ce..6b69f437f7e 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -513,7 +513,7 @@ describe('component islands', () => { }) })) - expect(result.island.html.replace(/data-v-\w+|"/g, '')).toMatchInlineSnapshot(` + expect(result.island.html.replace(/data-v-\w+|"|/g, '')).toMatchInlineSnapshot(` "
Was router enabled: true
Props:
{
         number: 3487,
         str: something,

From c2a723f784a47de4a0736254da7b2d8b04e965da Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 18:02:00 +0200
Subject: [PATCH 49/73] refactor: move tag parsing to the server renderer

---
 .../nuxt/src/app/components/nuxt-island.ts    | 34 ++++---------------
 .../nuxt/src/core/runtime/nitro/renderer.ts   | 29 +++++++++++-----
 2 files changed, 27 insertions(+), 36 deletions(-)

diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts
index 61fdd2ea945..aee8aba3279 100644
--- a/packages/nuxt/src/app/components/nuxt-island.ts
+++ b/packages/nuxt/src/app/components/nuxt-island.ts
@@ -46,9 +46,12 @@ export default defineComponent({
           delete nuxtApp[pKey][hashId.value]
         })
       }
-      const res = await nuxtApp[pKey][hashId.value]
-      injectHead(res.html)
-      html.value = res.island?.html!
+      const res: NuxtIslandResponse = await nuxtApp[pKey][hashId.value]
+      // TODO: Use better head meerging
+      useHead({
+        link: res.tags.filter(tag => tag.tag === 'link').map(tag => tag.attrs)
+      })
+      html.value = res.html
     }
 
     if (process.server || !nuxtApp.isHydrating) {
@@ -62,28 +65,3 @@ export default defineComponent({
     return () => createStaticVNode(html.value, 1)
   }
 })
-
-// --- Internal ---
-
-function injectHead (html: NuxtIslandResponse['html']) {
-  const tags = extractTags(html.head.join(''))
-  useHead({
-    link: tags.filter(tag => tag.tag === 'link').map(tag => tag.attrs)
-  })
-}
-
-// Tag parsing utils
-// TOOD: Move to external library
-const HTML_TAG_RE = /<(?[a-z]+)(? [^>]*)>/g
-const HTML_TAG_ATTR_RE = /(?[a-z]+)=(?"[^"]*"|'[^']*'|[^ >]*)/g
-function extractTags (html: string) {
-  const tags: {tag: string, attrs: Record}[] = []
-  for (const tagMatch of html.matchAll(HTML_TAG_RE)) {
-    const attrs = {} as Record
-    for (const attraMatch of tagMatch.groups!.rawAttrs.matchAll(HTML_TAG_ATTR_RE)) {
-      attrs[attraMatch.groups!.name] = attraMatch.groups!.value
-    }
-    tags.push({ tag: tagMatch.groups!.tag, attrs })
-  }
-  return tags
-}
diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index cd642892c57..990cb21ea6c 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -30,9 +30,10 @@ export interface NuxtIslandContext {
 }
 
 export interface NuxtIslandResponse {
-  island?: { id?: string, html: string }
+  id?: string
+  html: string
   state: Record
-  html: NuxtRenderHTMLContext
+  tags: { tag: string, attrs: Record }[]
 }
 
 export interface NuxtRenderResponse {
@@ -209,13 +210,10 @@ export default defineRenderHandler(async (event) => {
   // Response for component islands
   if (islandContext && islandContext.format !== 'html') {
     const islandReponse: NuxtIslandResponse = {
-      island: {
-        id: islandContext.id,
-        html: ssrContext.teleports!['nuxt-island']
-          .replace(/$/, '')
-      },
+      id: islandContext.id,
+      html: ssrContext.teleports!['nuxt-island'].replace(/$/, ''),
       state: ssrContext.payload.state,
-      html: htmlContext
+      tags: htmlContext.head.flatMap(head => extractHTMLTags(head))
     }
 
     await nitroApp.hooks.callHook('render:island', islandReponse, { event, islandContext })
@@ -275,3 +273,18 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) {
 ${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)}
 `
 }
+
+// TOOD: Move to external library
+const HTML_TAG_RE = /<(?[a-z]+)(? [^>]*)>/g
+const HTML_TAG_ATTR_RE = /(?[a-z]+)=(?"[^"]*"|'[^']*'|[^ >]*)/g
+function extractHTMLTags (html: string) {
+  const tags: {tag: string, attrs: Record}[] = []
+  for (const tagMatch of html.matchAll(HTML_TAG_RE)) {
+    const attrs = {} as Record
+    for (const attraMatch of tagMatch.groups!.rawAttrs.matchAll(HTML_TAG_ATTR_RE)) {
+      attrs[attraMatch.groups!.name] = attraMatch.groups!.value
+    }
+    tags.push({ tag: tagMatch.groups!.tag, attrs })
+  }
+  return tags
+}

From 4d1063807216802ce47b3c5cbbb4ec3b9169a845 Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 18:03:45 +0200
Subject: [PATCH 50/73] fix: strip all comments

---
 packages/nuxt/src/core/runtime/nitro/renderer.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index 990cb21ea6c..f7e157d284d 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -211,7 +211,7 @@ export default defineRenderHandler(async (event) => {
   if (islandContext && islandContext.format !== 'html') {
     const islandReponse: NuxtIslandResponse = {
       id: islandContext.id,
-      html: ssrContext.teleports!['nuxt-island'].replace(/$/, ''),
+      html: ssrContext.teleports!['nuxt-island'].replace(//g, ''),
       state: ssrContext.payload.state,
       tags: htmlContext.head.flatMap(head => extractHTMLTags(head))
     }

From b363043e028417e02b7aa2a5bedd30b08f0387d9 Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 18:12:33 +0200
Subject: [PATCH 51/73] update test

---
 test/basic.test.ts | 131 ++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 125 insertions(+), 6 deletions(-)

diff --git a/test/basic.test.ts b/test/basic.test.ts
index 6b69f437f7e..2bd13a80d58 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -488,18 +488,137 @@ describe('app config', () => {
 describe('component islands', () => {
   it('renders components with route', async () => {
     const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo')
-    expect(result.island).toMatchInlineSnapshot(`
+    expect(result).toMatchInlineSnapshot(`
       {
         "html": "
    Route: /foo
         
", + "state": {}, + "tags": [ + { + "attrs": { + "charset": "\\"utf-8\\"", + }, + "tag": "meta", + }, + { + "attrs": { + "content": "\\"width=1024, initial-scale=1\\"", + "name": "\\"viewport\\"", + }, + "tag": "meta", + }, + { + "attrs": { + "content": "\\"2\\"", + "name": "\\"head:count\\"", + }, + "tag": "meta", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/entry.79170466.js\\"", + "rel": "\\"modulepreload\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/TestGlobal.28b642d1.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/WithSuffix.global.a813662c.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/BreaksServer.9d9aeeb9.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/injectAuth.de44cafa.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/override.1835fbc4.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/foo.bcd52142.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/error-component.2a10ec59.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/PascalCase.87deb145.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/custom.4b0b318c.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/override.af11ded7.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + { + "attrs": { + "as": "\\"script\\"", + "href": "\\"/_nuxt/default.8f9ba753.js\\"", + "rel": "\\"prefetch\\"", + }, + "tag": "link", + }, + ], } `) expect(result.state).toMatchInlineSnapshot('{}') expect(Object.keys(result)).toMatchInlineSnapshot(` [ - "island", - "state", "html", + "state", + "tags", ] `) }) @@ -513,7 +632,7 @@ describe('component islands', () => { }) })) - expect(result.island.html.replace(/data-v-\w+|"|/g, '')).toMatchInlineSnapshot(` + expect(result.html.replace(/data-v-\w+|"|/g, '')).toMatchInlineSnapshot(` "
Was router enabled: true
Props:
{
         number: 3487,
         str: something,
@@ -536,9 +655,9 @@ describe('component islands', () => {
 
     expect(Object.keys(result)).toMatchInlineSnapshot(`
       [
-        "island",
-        "state",
         "html",
+        "state",
+        "tags",
       ]
     `)
   })

From 4a8fb3db5ec8315a1c7e5b746278a097a72e68ed Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 18:19:36 +0200
Subject: [PATCH 52/73] fix tag parsing and compress

---
 packages/nuxt/src/app/components/nuxt-island.ts  | 2 +-
 packages/nuxt/src/core/runtime/nitro/renderer.ts | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts
index aee8aba3279..854fedfdf80 100644
--- a/packages/nuxt/src/app/components/nuxt-island.ts
+++ b/packages/nuxt/src/app/components/nuxt-island.ts
@@ -49,7 +49,7 @@ export default defineComponent({
       const res: NuxtIslandResponse = await nuxtApp[pKey][hashId.value]
       // TODO: Use better head meerging
       useHead({
-        link: res.tags.filter(tag => tag.tag === 'link').map(tag => tag.attrs)
+        link: res.tags.filter(tag => tag[0] === 'link').map(tag => tag[1] || {})
       })
       html.value = res.html
     }
diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index f7e157d284d..669a55d0763 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -33,7 +33,7 @@ export interface NuxtIslandResponse {
   id?: string
   html: string
   state: Record
-  tags: { tag: string, attrs: Record }[]
+  tags: [tag: string, attrs?: Record][]
 }
 
 export interface NuxtRenderResponse {
@@ -276,15 +276,15 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) {
 
 // TOOD: Move to external library
 const HTML_TAG_RE = /<(?[a-z]+)(? [^>]*)>/g
-const HTML_TAG_ATTR_RE = /(?[a-z]+)=(?"[^"]*"|'[^']*'|[^ >]*)/g
+const HTML_TAG_ATTR_RE = /(?[a-z]+)="(?[^"]*)"/g
 function extractHTMLTags (html: string) {
-  const tags: {tag: string, attrs: Record}[] = []
+  const tags: [tag: string, attrs?: Record][] = []
   for (const tagMatch of html.matchAll(HTML_TAG_RE)) {
     const attrs = {} as Record
     for (const attraMatch of tagMatch.groups!.rawAttrs.matchAll(HTML_TAG_ATTR_RE)) {
       attrs[attraMatch.groups!.name] = attraMatch.groups!.value
     }
-    tags.push({ tag: tagMatch.groups!.tag, attrs })
+    tags.push([tagMatch.groups!.tag, attrs])
   }
   return tags
 }

From 22dfbcdf883a10e3ea457242ab4bd685fbaac8ce Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 18:22:36 +0200
Subject: [PATCH 53/73] use computed object for head

---
 packages/nuxt/src/app/components/nuxt-island.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts
index 854fedfdf80..43d0fb33417 100644
--- a/packages/nuxt/src/app/components/nuxt-island.ts
+++ b/packages/nuxt/src/app/components/nuxt-island.ts
@@ -2,6 +2,7 @@ import { defineComponent, createStaticVNode, computed, ref, watch } from 'vue'
 import { debounce } from 'perfect-debounce'
 import { hash } from 'ohash'
 // eslint-disable-next-line import/no-restricted-paths
+import type { MetaObject } from '@nuxt/schema'
 import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
 import { useHead, useNuxtApp } from '#app'
 
@@ -27,6 +28,8 @@ export default defineComponent({
     const nuxtApp = useNuxtApp()
     const hashId = computed(() => hash([props.name, props.props, props.context]))
     const html = ref('')
+    const cHead = ref({ link: [] })
+    useHead(cHead)
 
     function _fetchComponent () {
       // TODO: Validate response
@@ -47,10 +50,7 @@ export default defineComponent({
         })
       }
       const res: NuxtIslandResponse = await nuxtApp[pKey][hashId.value]
-      // TODO: Use better head meerging
-      useHead({
-        link: res.tags.filter(tag => tag[0] === 'link').map(tag => tag[1] || {})
-      })
+      cHead.value.link = res.tags.filter(tag => tag[0] === 'link').map(tag => tag[1] || {})
       html.value = res.html
     }
 

From aa7e07a5f7e23d10af2aac4806fd4e64fca88bcf Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 18:28:31 +0200
Subject: [PATCH 54/73] eslint is sometimes not funny either

---
 packages/nuxt/src/app/components/nuxt-island.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/nuxt/src/app/components/nuxt-island.ts b/packages/nuxt/src/app/components/nuxt-island.ts
index 43d0fb33417..333788921db 100644
--- a/packages/nuxt/src/app/components/nuxt-island.ts
+++ b/packages/nuxt/src/app/components/nuxt-island.ts
@@ -1,8 +1,8 @@
 import { defineComponent, createStaticVNode, computed, ref, watch } from 'vue'
 import { debounce } from 'perfect-debounce'
 import { hash } from 'ohash'
-// eslint-disable-next-line import/no-restricted-paths
 import type { MetaObject } from '@nuxt/schema'
+// eslint-disable-next-line import/no-restricted-paths
 import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
 import { useHead, useNuxtApp } from '#app'
 

From d41fd7a933febe1cb0253ae3bdf58d1b3ae94637 Mon Sep 17 00:00:00 2001
From: Pooya Parsa 
Date: Thu, 18 Aug 2022 18:29:38 +0200
Subject: [PATCH 55/73] update fixture

---
 test/basic.test.ts | 202 ++++++++++++++++++++++-----------------------
 1 file changed, 101 insertions(+), 101 deletions(-)

diff --git a/test/basic.test.ts b/test/basic.test.ts
index 2bd13a80d58..fd3088176ed 100644
--- a/test/basic.test.ts
+++ b/test/basic.test.ts
@@ -494,122 +494,122 @@ describe('component islands', () => {
         
", "state": {}, "tags": [ - { - "attrs": { - "charset": "\\"utf-8\\"", + [ + "meta", + { + "charset": "utf-8", }, - "tag": "meta", - }, - { - "attrs": { - "content": "\\"width=1024, initial-scale=1\\"", - "name": "\\"viewport\\"", + ], + [ + "meta", + { + "content": "width=1024, initial-scale=1", + "name": "viewport", }, - "tag": "meta", - }, - { - "attrs": { - "content": "\\"2\\"", - "name": "\\"head:count\\"", + ], + [ + "meta", + { + "content": "2", + "name": "head:count", }, - "tag": "meta", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/entry.79170466.js\\"", - "rel": "\\"modulepreload\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/entry.ed3d904c.js", + "rel": "modulepreload", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/TestGlobal.28b642d1.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/TestGlobal.8b601198.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/WithSuffix.global.a813662c.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/WithSuffix.global.19532f4e.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/BreaksServer.9d9aeeb9.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/BreaksServer.9d9aeeb9.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/injectAuth.de44cafa.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/injectAuth.2083679e.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/override.1835fbc4.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/override.af1cd5ce.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/foo.bcd52142.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/foo.72febae8.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/error-component.2a10ec59.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/error-component.eceba4d2.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/PascalCase.87deb145.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/PascalCase.87deb145.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/custom.4b0b318c.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/custom.daf0d259.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/override.af11ded7.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/override.62df4802.js", + "rel": "prefetch", }, - "tag": "link", - }, - { - "attrs": { - "as": "\\"script\\"", - "href": "\\"/_nuxt/default.8f9ba753.js\\"", - "rel": "\\"prefetch\\"", + ], + [ + "link", + { + "as": "script", + "href": "/_nuxt/default.b7809d21.js", + "rel": "prefetch", }, - "tag": "link", - }, + ], ], } `) From dc7db7f53c4faef6eb2eb4b28d0c4535751a8060 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 26 Sep 2022 09:13:11 +0100 Subject: [PATCH 56/73] chore: typo --- packages/nuxt/src/core/runtime/nitro/renderer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 669a55d0763..b49c9305be5 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -209,17 +209,17 @@ export default defineRenderHandler(async (event) => { // Response for component islands if (islandContext && islandContext.format !== 'html') { - const islandReponse: NuxtIslandResponse = { + const islandResponse: NuxtIslandResponse = { id: islandContext.id, html: ssrContext.teleports!['nuxt-island'].replace(//g, ''), state: ssrContext.payload.state, tags: htmlContext.head.flatMap(head => extractHTMLTags(head)) } - await nitroApp.hooks.callHook('render:island', islandReponse, { event, islandContext }) + await nitroApp.hooks.callHook('render:island', islandResponse, { event, islandContext }) const response: RenderResponse = { - body: JSON.stringify(islandReponse, null, 2), + body: JSON.stringify(islandResponse, null, 2), statusCode: event.res.statusCode, statusMessage: event.res.statusMessage, headers: { From 2dce064db268025e782688094b9e39f07909d4ac Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 26 Sep 2022 09:34:16 +0100 Subject: [PATCH 57/73] fix: respect island url --- packages/nuxt/src/core/runtime/nitro/renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts index 98e0d18232b..66b36fc0afe 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts @@ -152,7 +152,7 @@ export default defineRenderHandler(async (event) => { ? getQuery(event) as Exclude : null const islandContext = event.req.url?.startsWith('/__nuxt_island') ? await getIslandContext(event) : undefined - let url = ssrError?.url as string || event.req.url! + let url = ssrError?.url as string || islandContext?.url || event.req.url! // Whether we are rendering payload route const isRenderingPayload = PAYLOAD_URL_RE.test(url) From bbb12e402f8c35b026910566c69f551e8aabf706 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 26 Sep 2022 09:34:57 +0100 Subject: [PATCH 58/73] test: remove hashes --- test/basic.test.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/basic.test.ts b/test/basic.test.ts index a1f82ba6c7b..aeb8874ee2c 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -2,10 +2,10 @@ import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { joinURL, withQuery } from 'ufo' import { isWindows } from 'std-env' -import { setup, fetch, $fetch, startServer, createPage, url } from '@nuxt/test-utils' import type { NuxtIslandResponse } from '../packages/nuxt/src/core/runtime/nitro/renderer' // eslint-disable-next-line import/order import { expectNoClientErrors, renderPage } from './utils' +import { setup, fetch, $fetch, startServer, createPage, url } from '@nuxt/test-utils' await setup({ rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), @@ -608,6 +608,7 @@ describe('app config', () => { describe('component islands', () => { it('renders components with route', async () => { const result: NuxtIslandResponse = await $fetch('/__nuxt_island/RouteComponent?url=/foo') + result.tags = result.tags.map(([tag, attrs]) => attrs?.href ? [tag, { ...attrs, href: attrs.href.replace(/\.[\w\d]+(\.[\w]+)$/, '$1') }] : [tag, attrs]) expect(result).toMatchInlineSnapshot(` { "html": "
    Route: /foo
@@ -638,7 +639,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/entry.ed3d904c.js",
+              "href": "/_nuxt/entry.js",
               "rel": "modulepreload",
             },
           ],
@@ -646,7 +647,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/TestGlobal.8b601198.js",
+              "href": "/_nuxt/injectAuth.js",
               "rel": "prefetch",
             },
           ],
@@ -654,7 +655,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/WithSuffix.global.19532f4e.js",
+              "href": "/_nuxt/sets-layout.js",
               "rel": "prefetch",
             },
           ],
@@ -662,7 +663,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/BreaksServer.9d9aeeb9.js",
+              "href": "/_nuxt/override.js",
               "rel": "prefetch",
             },
           ],
@@ -670,7 +671,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/injectAuth.2083679e.js",
+              "href": "/_nuxt/foo.js",
               "rel": "prefetch",
             },
           ],
@@ -678,7 +679,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/override.af1cd5ce.js",
+              "href": "/_nuxt/error-component.js",
               "rel": "prefetch",
             },
           ],
@@ -686,7 +687,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/foo.72febae8.js",
+              "href": "/_nuxt/PascalCase.js",
               "rel": "prefetch",
             },
           ],
@@ -694,7 +695,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/error-component.eceba4d2.js",
+              "href": "/_nuxt/custom.js",
               "rel": "prefetch",
             },
           ],
@@ -702,7 +703,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/PascalCase.87deb145.js",
+              "href": "/_nuxt/invalid-root.js",
               "rel": "prefetch",
             },
           ],
@@ -710,7 +711,7 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/custom.daf0d259.js",
+              "href": "/_nuxt/override.js",
               "rel": "prefetch",
             },
           ],
@@ -718,16 +719,15 @@ describe('component islands', () => {
             "link",
             {
               "as": "script",
-              "href": "/_nuxt/override.62df4802.js",
+              "href": "/_nuxt/default.js",
               "rel": "prefetch",
             },
           ],
           [
             "link",
             {
-              "as": "script",
-              "href": "/_nuxt/default.b7809d21.js",
-              "rel": "prefetch",
+              "href": "/_nuxt/entry.css",
+              "rel": "prefetch stylesheet",
             },
           ],
         ],

From ae18933650455718af0e2e39e34687e7f3b6430e Mon Sep 17 00:00:00 2001
From: Daniel Roe 
Date: Mon, 26 Sep 2022 09:35:32 +0100
Subject: [PATCH 59/73] style: revert indent

---
 packages/nuxt/src/core/runtime/nitro/renderer.ts | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/renderer.ts
index 66b36fc0afe..a24520362bb 100644
--- a/packages/nuxt/src/core/runtime/nitro/renderer.ts
+++ b/packages/nuxt/src/core/runtime/nitro/renderer.ts
@@ -249,16 +249,14 @@ export default defineRenderHandler(async (event) => {
     ]),
     body: islandContext
       ? []
-      : [
-        _rendered.html
-      ],
+      : [_rendered.html],
     bodyAppend: normalizeChunks([
       process.env.NUXT_NO_SCRIPTS
         ? undefined
         : (_PAYLOAD_EXTRACTION
-          ? ``
-          : ``
-        ),
+            ? ``
+            : ``
+          ),
       _rendered.renderScripts(),
       // Note: bodyScripts may contain tags other than