From 700fad192edef1f5d4681d714d3eaebbd77eab95 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sun, 16 Jul 2023 19:32:07 +0530 Subject: [PATCH] feat(build): add `metaChunk` option to extract metadata to separate chunk (#2626) Co-authored-by: Bojan Rajh --- src/node/build/build.ts | 69 ++++++++++++++++++++++++++++++++-------- src/node/build/render.ts | 30 +++++++---------- src/node/config.ts | 1 + src/node/siteConfig.ts | 7 ++++ 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/node/build/build.ts b/src/node/build/build.ts index ed71ffc3..2d778b70 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -1,3 +1,4 @@ +import { createHash } from 'crypto' import fs from 'fs-extra' import { createRequire } from 'module' import ora from 'ora' @@ -7,9 +8,9 @@ import { rimraf } from 'rimraf' import type { OutputAsset, OutputChunk } from 'rollup' import { pathToFileURL } from 'url' import type { BuildOptions } from 'vite' -import { resolveConfig } from '../config' -import type { HeadConfig } from '../shared' -import { serializeFunctions } from '../utils/fnSerialize' +import { resolveConfig, type SiteConfig } from '../config' +import { slash, type HeadConfig } from '../shared' +import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize' import { bundle, failMark, okMark } from './bundle' import { renderPage } from './render' @@ -79,6 +80,8 @@ export async function build( chunk.moduleIds.some((id) => id.includes('client/theme-default')) ) + const metadataScript = generateMetadataScript(pageToHashMap, siteConfig) + if (isDefaultTheme) { const fontURL = assets.find((file) => /inter-roman-latin\.\w+\.woff2/.test(file) @@ -97,15 +100,6 @@ export async function build( } } - // We embed the hash map and site config strings into each page directly - // so that it doesn't alter the main chunk's hash on every build. - // It's also embedded as a string and JSON.parsed from the client because - // it's faster than embedding as JS object literal. - const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap)) - const siteDataString = JSON.stringify( - JSON.stringify(serializeFunctions({ ...siteConfig.site, head: [] })) - ) - await Promise.all( ['404.md', ...siteConfig.pages] .map((page) => siteConfig.rewrites.map[page] || page) @@ -119,8 +113,7 @@ export async function build( cssChunk, assets, pageToHashMap, - hashMapString, - siteDataString, + metadataScript, additionalHeadTags ) ) @@ -168,3 +161,51 @@ function linkVue() { } return () => {} } + +function generateMetadataScript( + pageToHashMap: Record, + config: SiteConfig +) { + if (config.mpa) { + return { html: '', inHead: false } + } + + // We embed the hash map and site config strings into each page directly + // so that it doesn't alter the main chunk's hash on every build. + // It's also embedded as a string and JSON.parsed from the client because + // it's faster than embedding as JS object literal. + const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap)) + const siteDataString = JSON.stringify( + JSON.stringify(serializeFunctions({ ...config.site, head: [] })) + ) + + const metadataContent = `window.__VP_HASH_MAP__=JSON.parse(${hashMapString});${ + siteDataString.includes('_vp-fn_') + ? `${deserializeFunctions.toString()};window.__VP_SITE_DATA__=deserializeFunctions(JSON.parse(${siteDataString}));` + : `window.__VP_SITE_DATA__=JSON.parse(${siteDataString});` + }` + + if (!config.metaChunk) { + return { html: ``, inHead: false } + } + + const metadataFile = path.join( + config.assetsDir, + 'chunks', + `metadata.${createHash('sha256') + .update(metadataContent) + .digest('hex') + .slice(0, 8)}.js` + ) + + const resolvedMetadataFile = path.join(config.outDir, metadataFile) + const metadataFileURL = slash(`${config.site.base}${metadataFile}`) + + fs.ensureDirSync(path.dirname(resolvedMetadataFile)) + fs.writeFileSync(resolvedMetadataFile, metadataContent) + + return { + html: ``, + inHead: true + } +} diff --git a/src/node/build/render.ts b/src/node/build/render.ts index e391ed39..40675408 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -18,7 +18,6 @@ import { type PageData, type SSGContext } from '../shared' -import { deserializeFunctions } from '../utils/fnSerialize' export async function renderPage( render: (path: string) => Promise, @@ -29,8 +28,7 @@ export async function renderPage( cssChunk: OutputAsset | null, assets: string[], pageToHashMap: Record, - hashMapString: string, - siteDataString: string, + metadataScript: { html: string; inHead: boolean }, additionalHeadTags: HeadConfig[] ) { const routePath = `/${page.replace(/\.md$/, '')}` @@ -150,15 +148,7 @@ export async function renderPage( } } - let metadataScript = `__VP_HASH_MAP__ = JSON.parse(${hashMapString})\n` - if (siteDataString.includes('_vp-fn_')) { - metadataScript += `${deserializeFunctions.toString()}\n__VP_SITE_DATA__ = deserializeFunctions(JSON.parse(${siteDataString}))` - } else { - metadataScript += `__VP_SITE_DATA__ = JSON.parse(${siteDataString})` - } - - const html = ` - + const html = ` @@ -166,21 +156,22 @@ export async function renderPage( ${title} ${stylesheetLink} + ${metadataScript.inHead ? metadataScript.html : ''} ${ appChunk ? `` - : `` + : '' } ${await renderHead(head)} ${teleports?.body || ''}
${content}
- ${config.mpa ? '' : ``} + ${metadataScript.inHead ? '' : metadataScript.html} ${inlinedScript} -`.trim() - const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) +` + const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) await fs.ensureDir(path.dirname(htmlFileName)) const transformedHtml = await config.transformHtml?.(html, htmlFileName, { page, @@ -224,8 +215,8 @@ function resolvePageImports( ] } -function renderHead(head: HeadConfig[]): Promise { - return Promise.all( +async function renderHead(head: HeadConfig[]): Promise { + const tags = await Promise.all( head.map(async ([tag, attrs = {}, innerHTML = '']) => { const openTag = `<${tag}${renderAttrs(attrs)}>` if (tag !== 'link' && tag !== 'meta') { @@ -244,7 +235,8 @@ function renderHead(head: HeadConfig[]): Promise { return openTag } }) - ).then((tags) => tags.join('\n ')) + ) + return tags.join('\n ') } function renderAttrs(attrs: Record): string { diff --git a/src/node/config.ts b/src/node/config.ts index bb187a9c..6144d876 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -115,6 +115,7 @@ export async function resolveConfig( vite: userConfig.vite, shouldPreload: userConfig.shouldPreload, mpa: !!userConfig.mpa, + metaChunk: !!userConfig.metaChunk, ignoreDeadLinks: userConfig.ignoreDeadLinks, cleanUrls: !!userConfig.cleanUrls, useWebFonts: diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index 152eca1e..6a1cea00 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -98,6 +98,12 @@ export interface UserConfig */ mpa?: boolean + /** + * Extracts metadata to a separate chunk. + * @experimental + */ + metaChunk?: boolean + /** * Don't fail builds due to dead links. * @@ -176,6 +182,7 @@ export interface SiteConfig | 'vite' | 'shouldPreload' | 'mpa' + | 'metaChunk' | 'lastUpdated' | 'ignoreDeadLinks' | 'cleanUrls'