mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
feat(nuxt): render all head tags on server with unhead
(#22179)
This commit is contained in:
parent
a2b5d31270
commit
9b09b4d112
@ -10,6 +10,7 @@ import type { H3Event } from 'h3'
|
|||||||
import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema'
|
import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema'
|
||||||
import type { RenderResponse } from 'nitropack'
|
import type { RenderResponse } from 'nitropack'
|
||||||
|
|
||||||
|
import type { MergeHead, VueHeadClient } from '@unhead/vue'
|
||||||
// eslint-disable-next-line import/no-restricted-paths
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
|
import type { NuxtIslandContext } from '../core/runtime/nitro/renderer'
|
||||||
import type { RouteMiddleware } from '../../app'
|
import type { RouteMiddleware } from '../../app'
|
||||||
@ -18,15 +19,6 @@ import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
|||||||
|
|
||||||
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
|
const nuxtAppCtx = /* #__PURE__ */ getContext<NuxtApp>('nuxt-app')
|
||||||
|
|
||||||
type NuxtMeta = {
|
|
||||||
htmlAttrs?: string
|
|
||||||
headAttrs?: string
|
|
||||||
bodyAttrs?: string
|
|
||||||
headTags?: string
|
|
||||||
bodyScriptsPrepend?: string
|
|
||||||
bodyScripts?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type HookResult = Promise<void> | void
|
type HookResult = Promise<void> | void
|
||||||
|
|
||||||
type AppRenderedContext = { ssrContext: NuxtApp['ssrContext'], renderResult: null | Awaited<ReturnType<ReturnType<typeof createRenderer>['renderToString']>> }
|
type AppRenderedContext = { ssrContext: NuxtApp['ssrContext'], renderResult: null | Awaited<ReturnType<ReturnType<typeof createRenderer>['renderToString']>> }
|
||||||
@ -59,10 +51,10 @@ export interface NuxtSSRContext extends SSRContext {
|
|||||||
error?: boolean
|
error?: boolean
|
||||||
nuxt: _NuxtApp
|
nuxt: _NuxtApp
|
||||||
payload: NuxtPayload
|
payload: NuxtPayload
|
||||||
|
head: VueHeadClient<MergeHead>
|
||||||
/** This is used solely to render runtime config with SPA renderer. */
|
/** This is used solely to render runtime config with SPA renderer. */
|
||||||
config?: Pick<RuntimeConfig, 'public' | 'app'>
|
config?: Pick<RuntimeConfig, 'public' | 'app'>
|
||||||
teleports?: Record<string, string>
|
teleports?: Record<string, string>
|
||||||
renderMeta?: () => Promise<NuxtMeta> | NuxtMeta
|
|
||||||
islandContext?: NuxtIslandContext
|
islandContext?: NuxtIslandContext
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_renderResponse?: Partial<RenderResponse>
|
_renderResponse?: Partial<RenderResponse>
|
||||||
|
@ -8,8 +8,6 @@ import escapeRE from 'escape-string-regexp'
|
|||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import fsExtra from 'fs-extra'
|
import fsExtra from 'fs-extra'
|
||||||
import { dynamicEventHandler } from 'h3'
|
import { dynamicEventHandler } from 'h3'
|
||||||
import { createHeadCore } from '@unhead/vue'
|
|
||||||
import { renderSSRHead } from '@unhead/ssr'
|
|
||||||
import type { Nuxt } from 'nuxt/schema'
|
import type { Nuxt } from 'nuxt/schema'
|
||||||
// @ts-expect-error TODO: add legacy type support for subpath imports
|
// @ts-expect-error TODO: add legacy type support for subpath imports
|
||||||
import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs'
|
import { template as defaultSpaLoadingTemplate } from '@nuxt/ui-templates/templates/spa-loading-icon.mjs'
|
||||||
@ -205,12 +203,6 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
|||||||
// Resolve user-provided paths
|
// Resolve user-provided paths
|
||||||
nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!)
|
nitroConfig.srcDir = resolve(nuxt.options.rootDir, nuxt.options.srcDir, nitroConfig.srcDir!)
|
||||||
|
|
||||||
// Add head chunk for SPA renders
|
|
||||||
const head = createHeadCore()
|
|
||||||
head.push(nuxt.options.app.head)
|
|
||||||
const headChunk = await renderSSRHead(head)
|
|
||||||
nitroConfig.virtual!['#head-static'] = `export default ${JSON.stringify(headChunk)}`
|
|
||||||
|
|
||||||
// Add fallback server for `ssr: false`
|
// Add fallback server for `ssr: false`
|
||||||
if (!nuxt.options.ssr) {
|
if (!nuxt.options.ssr) {
|
||||||
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
|
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { createRenderer, renderResourceHeaders } from 'vue-bundle-renderer/runtime'
|
import {
|
||||||
|
createRenderer,
|
||||||
|
getPrefetchLinks,
|
||||||
|
getPreloadLinks,
|
||||||
|
getRequestDependencies,
|
||||||
|
renderResourceHeaders
|
||||||
|
} from 'vue-bundle-renderer/runtime'
|
||||||
import type { RenderResponse } from 'nitropack'
|
import type { RenderResponse } from 'nitropack'
|
||||||
import type { Manifest } from 'vite'
|
import type { Manifest } from 'vite'
|
||||||
import type { H3Event } from 'h3'
|
import type { H3Event } from 'h3'
|
||||||
@ -9,14 +15,17 @@ import destr from 'destr'
|
|||||||
import { joinURL, withoutTrailingSlash } from 'ufo'
|
import { joinURL, withoutTrailingSlash } from 'ufo'
|
||||||
import { renderToString as _renderToString } from 'vue/server-renderer'
|
import { renderToString as _renderToString } from 'vue/server-renderer'
|
||||||
import { hash } from 'ohash'
|
import { hash } from 'ohash'
|
||||||
|
import { renderSSRHead } from '@unhead/ssr'
|
||||||
|
|
||||||
import { defineRenderHandler, getRouteRules, useRuntimeConfig } from '#internal/nitro'
|
import { defineRenderHandler, getRouteRules, useRuntimeConfig } from '#internal/nitro'
|
||||||
import { useNitroApp } from '#internal/nitro/app'
|
import { useNitroApp } from '#internal/nitro/app'
|
||||||
|
|
||||||
|
import type { Link, Script } from '@unhead/vue'
|
||||||
|
import { createServerHead } from '@unhead/vue'
|
||||||
// eslint-disable-next-line import/no-restricted-paths
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import type { NuxtPayload, NuxtSSRContext } from '#app/nuxt'
|
import type { NuxtPayload, NuxtSSRContext } from '#app/nuxt'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { appRootId, appRootTag } from '#internal/nuxt.config.mjs'
|
import { appHead, appRootId, appRootTag } from '#internal/nuxt.config.mjs'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { buildAssetsURL, publicAssetsURL } from '#paths'
|
import { buildAssetsURL, publicAssetsURL } from '#paths'
|
||||||
|
|
||||||
@ -71,9 +80,6 @@ const getEntryIds: () => Promise<string[]> = () => getClientManifest().then(r =>
|
|||||||
r._globalCSS
|
r._globalCSS
|
||||||
).map(r => r.src!))
|
).map(r => r.src!))
|
||||||
|
|
||||||
// @ts-expect-error virtual file
|
|
||||||
const getStaticRenderedHead = (): Promise<NuxtMeta> => import('#head-static').then(r => r.default || r)
|
|
||||||
|
|
||||||
// @ts-expect-error file will be produced after app build
|
// @ts-expect-error file will be produced after app build
|
||||||
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
|
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
|
||||||
|
|
||||||
@ -140,7 +146,6 @@ const getSPARenderer = lazyCachedFunction(async () => {
|
|||||||
public: config.public,
|
public: config.public,
|
||||||
app: config.app
|
app: config.app
|
||||||
}
|
}
|
||||||
ssrContext!.renderMeta = ssrContext!.renderMeta ?? getStaticRenderedHead
|
|
||||||
return Promise.resolve(result)
|
return Promise.resolve(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,6 +226,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
// Get route options (currently to apply `ssr: false`)
|
// Get route options (currently to apply `ssr: false`)
|
||||||
const routeOptions = getRouteRules(event)
|
const routeOptions = getRouteRules(event)
|
||||||
|
|
||||||
|
const head = createServerHead()
|
||||||
|
head.push(appHead)
|
||||||
|
|
||||||
// Initialize ssr context
|
// Initialize ssr context
|
||||||
const ssrContext: NuxtSSRContext = {
|
const ssrContext: NuxtSSRContext = {
|
||||||
url,
|
url,
|
||||||
@ -231,6 +239,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
event.context.nuxt?.noSSR ||
|
event.context.nuxt?.noSSR ||
|
||||||
routeOptions.ssr === false ||
|
routeOptions.ssr === false ||
|
||||||
(process.env.prerender ? PRERENDER_NO_SSR_ROUTES.has(url) : false),
|
(process.env.prerender ? PRERENDER_NO_SSR_ROUTES.has(url) : false),
|
||||||
|
head,
|
||||||
error: !!ssrError,
|
error: !!ssrError,
|
||||||
nuxt: undefined!, /* NuxtApp */
|
nuxt: undefined!, /* NuxtApp */
|
||||||
payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload,
|
payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload,
|
||||||
@ -288,9 +297,6 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
PAYLOAD_CACHE!.set(withoutTrailingSlash(url), renderPayloadResponse(ssrContext))
|
PAYLOAD_CACHE!.set(withoutTrailingSlash(url), renderPayloadResponse(ssrContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render meta
|
|
||||||
const renderedMeta = await ssrContext.renderMeta?.() ?? {}
|
|
||||||
|
|
||||||
if (process.env.NUXT_INLINE_STYLES && !islandContext) {
|
if (process.env.NUXT_INLINE_STYLES && !islandContext) {
|
||||||
const source = ssrContext.modules ?? ssrContext._registeredComponents
|
const source = ssrContext.modules ?? ssrContext._registeredComponents
|
||||||
if (source) {
|
if (source) {
|
||||||
@ -303,45 +309,81 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
// Render inline styles
|
// Render inline styles
|
||||||
const inlinedStyles = (process.env.NUXT_INLINE_STYLES || Boolean(islandContext))
|
const inlinedStyles = (process.env.NUXT_INLINE_STYLES || Boolean(islandContext))
|
||||||
? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? [])
|
? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? [])
|
||||||
: ''
|
: []
|
||||||
|
|
||||||
const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.experimentalNoScripts
|
const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.experimentalNoScripts
|
||||||
|
|
||||||
// Create render context
|
// Setup head
|
||||||
const htmlContext: NuxtRenderHTMLContext = {
|
const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext)
|
||||||
island: Boolean(islandContext),
|
// 1.Extracted payload preloading
|
||||||
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
|
if (_PAYLOAD_EXTRACTION) {
|
||||||
head: normalizeChunks([
|
head.push({
|
||||||
renderedMeta.headTags,
|
link: [
|
||||||
process.env.NUXT_JSON_PAYLOADS
|
process.env.NUXT_JSON_PAYLOADS
|
||||||
? _PAYLOAD_EXTRACTION ? `<link rel="preload" as="fetch" crossorigin="anonymous" href="${payloadURL}">` : null
|
? { rel: 'preload', as: 'fetch', crossorigin: 'anonymous', href: payloadURL }
|
||||||
: _PAYLOAD_EXTRACTION ? `<link rel="modulepreload" href="${payloadURL}">` : null,
|
: { rel: 'modulepreload', href: payloadURL }
|
||||||
NO_SCRIPTS ? null : _rendered.renderResourceHints(),
|
]
|
||||||
_rendered.renderStyles(),
|
})
|
||||||
inlinedStyles,
|
}
|
||||||
ssrContext.styles
|
|
||||||
]),
|
if (!NO_SCRIPTS) {
|
||||||
bodyAttrs: normalizeChunks([renderedMeta.bodyAttrs!]),
|
// 2. Resource Hints
|
||||||
bodyPrepend: normalizeChunks([
|
// @todo add priorities based on Capo
|
||||||
renderedMeta.bodyScriptsPrepend,
|
head.push({
|
||||||
ssrContext.teleports?.body
|
link: getPreloadLinks(ssrContext, renderer.rendererContext) as Link[]
|
||||||
]),
|
})
|
||||||
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceServerOnlyComponentsSlots(ssrContext, _rendered.html) : _rendered.html],
|
head.push({
|
||||||
bodyAppend: normalizeChunks([
|
link: getPrefetchLinks(ssrContext, renderer.rendererContext) as Link[]
|
||||||
NO_SCRIPTS
|
})
|
||||||
? undefined
|
// 3. Payloads
|
||||||
: (_PAYLOAD_EXTRACTION
|
head.push({
|
||||||
|
script: _PAYLOAD_EXTRACTION
|
||||||
? process.env.NUXT_JSON_PAYLOADS
|
? process.env.NUXT_JSON_PAYLOADS
|
||||||
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
||||||
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
: renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL })
|
||||||
: process.env.NUXT_JSON_PAYLOADS
|
: process.env.NUXT_JSON_PAYLOADS
|
||||||
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
|
? renderPayloadJsonScript({ id: '__NUXT_DATA__', ssrContext, data: ssrContext.payload })
|
||||||
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
|
: renderPayloadScript({ ssrContext, data: ssrContext.payload })
|
||||||
|
}, {
|
||||||
|
// this should come before another end of body scripts
|
||||||
|
tagPosition: 'bodyClose',
|
||||||
|
tagPriority: 'high'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Styles
|
||||||
|
head.push({
|
||||||
|
link: Object.values(styles)
|
||||||
|
.map(resource =>
|
||||||
|
({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) })
|
||||||
),
|
),
|
||||||
routeOptions.experimentalNoScripts ? undefined : _rendered.renderScripts(),
|
style: inlinedStyles
|
||||||
// Note: bodyScripts may contain tags other than <script>
|
})
|
||||||
renderedMeta.bodyScripts
|
|
||||||
])
|
// 4. Scripts
|
||||||
|
if (!routeOptions.experimentalNoScripts) {
|
||||||
|
head.push({
|
||||||
|
script: Object.values(scripts).map(resource => (<Script> {
|
||||||
|
type: resource.module ? 'module' : null,
|
||||||
|
src: renderer.rendererContext.buildAssetsURL(resource.file),
|
||||||
|
defer: resource.module ? null : true,
|
||||||
|
crossorigin: ''
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove certain tags for nuxt islands
|
||||||
|
const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)
|
||||||
|
|
||||||
|
// Create render context
|
||||||
|
const htmlContext: NuxtRenderHTMLContext = {
|
||||||
|
island: Boolean(islandContext),
|
||||||
|
htmlAttrs: [htmlAttrs],
|
||||||
|
head: normalizeChunks([headTags, ssrContext.styles]),
|
||||||
|
bodyAttrs: [bodyAttrs],
|
||||||
|
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
|
||||||
|
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceServerOnlyComponentsSlots(ssrContext, _rendered.html) : _rendered.html],
|
||||||
|
bodyAppend: [bodyTags]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow hooking into the rendered result
|
// Allow hooking into the rendered result
|
||||||
@ -349,21 +391,21 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
|
|
||||||
// Response for component islands
|
// Response for component islands
|
||||||
if (process.env.NUXT_COMPONENT_ISLANDS && islandContext) {
|
if (process.env.NUXT_COMPONENT_ISLANDS && islandContext) {
|
||||||
const _tags = htmlContext.head.flatMap(head => extractHTMLTags(head))
|
const islandHead: NuxtIslandResponse['head'] = {
|
||||||
const head: NuxtIslandResponse['head'] = {
|
link: [],
|
||||||
link: _tags.filter(tag => tag.tagName === 'link' && tag.attrs.rel === 'stylesheet' && tag.attrs.href.includes('scoped') && !tag.attrs.href.includes('pages/')).map(tag => ({
|
style: []
|
||||||
key: 'island-link-' + hash(tag.attrs.href),
|
}
|
||||||
...tag.attrs
|
for (const tag of await head.resolveTags()) {
|
||||||
})),
|
if (tag.tag === 'link' && tag.props.rel === 'stylesheet' && tag.props.href.includes('scoped') && !tag.props.href.includes('pages/')) {
|
||||||
style: _tags.filter(tag => tag.tagName === 'style' && tag.innerHTML).map(tag => ({
|
islandHead.link.push({ ...tag.props, key: 'island-link-' + hash(tag.props.href) })
|
||||||
key: 'island-style-' + hash(tag.innerHTML),
|
}
|
||||||
innerHTML: tag.innerHTML
|
if (tag.tag === 'style' && tag.innerHTML) {
|
||||||
}))
|
islandHead.style.push({ key: 'island-style-' + hash(tag.innerHTML), innerHTML: tag.innerHTML })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const islandResponse: NuxtIslandResponse = {
|
const islandResponse: NuxtIslandResponse = {
|
||||||
id: islandContext.id,
|
id: islandContext.id,
|
||||||
head,
|
head: islandHead,
|
||||||
html: getServerComponentHTML(htmlContext.body),
|
html: getServerComponentHTML(htmlContext.body),
|
||||||
state: ssrContext.payload.state
|
state: ssrContext.payload.state
|
||||||
}
|
}
|
||||||
@ -429,33 +471,17 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) {
|
|||||||
</html>`
|
</html>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move to external library
|
|
||||||
const HTML_TAG_RE = /<(?<tag>[a-z]+)(?<rawAttrs> [^>]*)?>(?:(?<innerHTML>[\s\S]*?)<\/\k<tag>)?/g
|
|
||||||
const HTML_TAG_ATTR_RE = /(?<name>[a-z]+)="(?<value>[^"]*)"/g
|
|
||||||
function extractHTMLTags (html: string) {
|
|
||||||
const tags: { tagName: string, attrs: Record<string, string>, innerHTML: string }[] = []
|
|
||||||
for (const tagMatch of html.matchAll(HTML_TAG_RE)) {
|
|
||||||
const attrs: Record<string, string> = {}
|
|
||||||
for (const attrMatch of tagMatch.groups!.rawAttrs?.matchAll(HTML_TAG_ATTR_RE) || []) {
|
|
||||||
attrs[attrMatch.groups!.name] = attrMatch.groups!.value
|
|
||||||
}
|
|
||||||
const innerHTML = tagMatch.groups!.innerHTML || ''
|
|
||||||
tags.push({ tagName: tagMatch.groups!.tag, attrs, innerHTML })
|
|
||||||
}
|
|
||||||
return tags
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderInlineStyles (usedModules: Set<string> | string[]) {
|
async function renderInlineStyles (usedModules: Set<string> | string[]) {
|
||||||
const styleMap = await getSSRStyles()
|
const styleMap = await getSSRStyles()
|
||||||
const inlinedStyles = new Set<string>()
|
const inlinedStyles = new Set<string>()
|
||||||
for (const mod of usedModules) {
|
for (const mod of usedModules) {
|
||||||
if (mod in styleMap) {
|
if (mod in styleMap) {
|
||||||
for (const style of await styleMap[mod]()) {
|
for (const style of await styleMap[mod]()) {
|
||||||
inlinedStyles.add(`<style>${style}</style>`)
|
inlinedStyles.add(style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Array.from(inlinedStyles).join('')
|
return Array.from(inlinedStyles).map(style => ({ innerHTML: style }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
||||||
@ -472,25 +498,41 @@ function renderPayloadResponse (ssrContext: NuxtSSRContext) {
|
|||||||
} satisfies RenderResponse
|
} satisfies RenderResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }) {
|
function renderPayloadJsonScript (opts: { id: string, ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
|
||||||
const attrs = [
|
|
||||||
'type="application/json"',
|
|
||||||
`id="${opts.id}"`,
|
|
||||||
`data-ssr="${!(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)}"`,
|
|
||||||
opts.src ? `data-src="${opts.src}"` : ''
|
|
||||||
].filter(Boolean)
|
|
||||||
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
|
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
|
||||||
return `<script ${attrs.join(' ')}>${contents}</script>` +
|
const payload: Script = {
|
||||||
`<script>window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}</script>`
|
type: 'application/json',
|
||||||
|
id: opts.id,
|
||||||
|
innerHTML: contents,
|
||||||
|
'data-ssr': !(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)
|
||||||
|
}
|
||||||
|
if (opts.src) {
|
||||||
|
payload['data-src'] = opts.src
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
innerHTML: `window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}`
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }) {
|
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
|
||||||
opts.data.config = opts.ssrContext.config
|
opts.data.config = opts.ssrContext.config
|
||||||
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
|
const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
|
||||||
if (_PAYLOAD_EXTRACTION) {
|
if (_PAYLOAD_EXTRACTION) {
|
||||||
return `<script type="module">import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})}</script>`
|
return [
|
||||||
|
{
|
||||||
|
type: 'module',
|
||||||
|
innerHTML: `import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})`
|
||||||
}
|
}
|
||||||
return `<script>window.__NUXT__=${devalue(opts.data)}</script>`
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
innerHTML: `window.__NUXT__=${devalue(opts.data)}`
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitPayload (ssrContext: NuxtSSRContext) {
|
function splitPayload (ssrContext: NuxtSSRContext) {
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import { createHead as createClientHead, createServerHead } from '@unhead/vue'
|
import { createHead as createClientHead } from '@unhead/vue'
|
||||||
import { renderSSRHead } from '@unhead/ssr'
|
|
||||||
import { defineNuxtPlugin } from '#app/nuxt'
|
import { defineNuxtPlugin } from '#app/nuxt'
|
||||||
// @ts-expect-error untyped
|
|
||||||
import { appHead } from '#build/nuxt.config.mjs'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin({
|
export default defineNuxtPlugin({
|
||||||
name: 'nuxt:head',
|
name: 'nuxt:head',
|
||||||
setup (nuxtApp) {
|
setup (nuxtApp) {
|
||||||
const createHead = process.server ? createServerHead : createClientHead
|
const head = process.server ? nuxtApp.ssrContext!.head : createClientHead()
|
||||||
const head = createHead()
|
// nuxt.config appHead is set server-side within the renderer
|
||||||
head.push(appHead)
|
|
||||||
|
|
||||||
nuxtApp.vueApp.use(head)
|
nuxtApp.vueApp.use(head)
|
||||||
|
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
@ -28,17 +23,5 @@ export default defineNuxtPlugin({
|
|||||||
// unpause the DOM once the mount suspense is resolved
|
// unpause the DOM once the mount suspense is resolved
|
||||||
nuxtApp.hooks.hook('app:suspense:resolve', unpauseDom)
|
nuxtApp.hooks.hook('app:suspense:resolve', unpauseDom)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.server) {
|
|
||||||
nuxtApp.ssrContext!.renderMeta = async () => {
|
|
||||||
const meta = await renderSSRHead(head)
|
|
||||||
return {
|
|
||||||
...meta,
|
|
||||||
bodyScriptsPrepend: meta.bodyTagsOpen,
|
|
||||||
// resolves naming difference with NuxtMeta and Unhead
|
|
||||||
bodyScripts: meta.bodyTags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -19,7 +19,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
for (const outputDir of ['.output', '.output-inline']) {
|
for (const outputDir of ['.output', '.output-inline']) {
|
||||||
it('default client bundle size', async () => {
|
it('default client bundle size', async () => {
|
||||||
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
|
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
|
||||||
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.5k"')
|
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"97.3k"')
|
||||||
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
"_nuxt/entry.js",
|
"_nuxt/entry.js",
|
||||||
@ -32,7 +32,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
const serverDir = join(rootDir, '.output/server')
|
const serverDir = join(rootDir, '.output/server')
|
||||||
|
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.5k"')
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"64.2k"')
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2330k"')
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"2330k"')
|
||||||
|
Loading…
Reference in New Issue
Block a user