import path from 'path' ; import fs from 'fs-extra' ; import { SiteConfig , resolveSiteDataByRoute } from '../config' ; import { HeadConfig } from '../shared' ; import { normalizePath , transformWithEsbuild } from 'vite' ; import { RollupOutput , OutputChunk , OutputAsset } from 'rollup' ; import { slash } from '../utils/slash' ; import escape from 'escape-html' ; export async function renderPage ( config , page , result , appChunk , cssChunk , pageToHashMap , hashMapString ) { const { createApp } = require ( path . join ( config . tempDir , `app.js` ) ) ; const { app , router } = createApp ( ) ; const routePath = `/${ page . replace ( /\.md$/ , '' ) }` ; const siteData = resolveSiteDataByRoute ( config . site , routePath ) ; router . go ( routePath ) ; let rendererPath ; try { rendererPath = require . resolve ( 'vue/server-renderer' , { paths : [ config . root ] } ) ; } catch ( e ) { rendererPath = require . resolve ( 'vue/server-renderer' ) ; } const content = await require ( rendererPath ) . renderToString ( app ) ; const pageName = page . replace ( /\//g , '_' ) ; const pageServerJsFileName = pageName + '.js' ; const pageHash = pageToHashMap [ pageName . toLowerCase ( ) ] ; const pageClientJsFileName = `assets/${ pageName }.${ pageHash }.lean.js` ; const { __pageData } = require ( path . join ( config . tempDir , pageServerJsFileName ) ) ; const pageData = JSON . parse ( __pageData ) ; let preloadLinks = config . mpa ? appChunk ? [ appChunk . fileName ] : [ ] : result && appChunk ? [ ... new Set ( [ ... resolvePageImports ( config , page , result , appChunk ) , pageClientJsFileName , appChunk . fileName ] ) ] : [ ] ; let prefetchLinks = [ ] ; const { shouldPreload } = config ; if ( shouldPreload ) { prefetchLinks = preloadLinks . filter ( ( link ) => ! shouldPreload ( link , page ) ) ; preloadLinks = preloadLinks . filter ( ( link ) => shouldPreload ( link , page ) ) ; } const preloadLinksString = preloadLinks . map ( ( file ) => { return `<link rel="modulepreload" href="${ siteData . base }${ file }">` ; } ) . join ( '\n    ' ) ; const prefetchLinkString = prefetchLinks . map ( ( file ) => { return `<link rel="prefetch" href="${ siteData . base }${ file }">` ; } ) . join ( '\n    ' ) ; const stylesheetLink = cssChunk ? `<link rel="stylesheet" href="${ siteData . base }${ cssChunk . fileName }">` : '' ; const title = pageData . title && pageData . title !== 'Home' ? `${ pageData . title } | ${ siteData . title }` : siteData . title ; const head = addSocialTags ( title , ... siteData . head , ... filterOutHeadDescription ( pageData . frontmatter . head ) ) ; let inlinedScript = '' ; if ( config . mpa && result ) { const matchingChunk = result . output . find ( ( chunk ) => chunk . type === 'chunk' && chunk . facadeModuleId === slash ( path . join ( config . srcDir , page ) ) ) as OutputChunk ; if ( matchingChunk ) { if ( ! matchingChunk . code . includes ( 'import' ) ) { inlinedScript = `<script type="module">${ matchingChunk . code }</script>` ; fs . removeSync ( path . resolve ( config . outDir , matchingChunk . fileName ) ) ; } else { inlinedScript = `<script type="module" src="${ siteData . base }${ matchingChunk . fileName }"></script>` ; } } } const html = `
<!DOCTYPE html>
<html lang="${ siteData . lang }">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>${ title }</title>
    <meta name="description" content="${ pageData . description || siteData . description }">
    ${ stylesheetLink }
    ${ preloadLinksString }
    ${ prefetchLinkString }
    ${ await renderHead ( head ) }
  </head>
  <body>
    <div id="app">${ content }</div>
    ${ config . mpa ? '' : `<script>__VP_HASH_MAP__ = JSON.parse(${ hashMapString })</script>` }
    ${ appChunk ? `<script type="module" async src="${ siteData . base }${ appChunk . fileName }"></script>` : `` }
    ${ inlinedScript }
  </body>
</html>` . trim ( ) ; const htmlFileName = path . join ( config . outDir , page . replace ( /\.md$/ , '.html' ) ) ; await fs . ensureDir ( path . dirname ( htmlFileName ) ) ; await fs . writeFile ( htmlFileName , html ) ; } function resolvePageImports ( config , page , result , appChunk ) { const srcPath = normalizePath ( fs . realpathSync ( path . resolve ( config . srcDir , page ) ) ) ; const pageChunk = result . output . find ( ( chunk ) => chunk . type === 'chunk' && chunk . facadeModuleId === srcPath ) as OutputChunk ; return [ ... appChunk . imports , ... appChunk . dynamicImports , ... pageChunk . imports , ... pageChunk . dynamicImports ] ; } function renderHead ( head ) { return Promise . all ( head . map ( async ( [ tag , attrs = { } , innerHTML = '' ] ) => { const openTag = `<${ tag }${ renderAttrs ( attrs ) }>` ; if ( tag !== 'link' && tag !== 'meta' ) { if ( tag === 'script' ) { innerHTML = ( await transformWithEsbuild ( innerHTML , 'inline-script.js' , { minify : true } ) ) . code . trim ( ) ; } return `${ openTag }${ innerHTML }</${ tag }>` ; } else { return openTag ; } } ) ) . then ( ( tags ) => tags . join ( '\n  ' ) ) ; } function renderAttrs ( attrs ) { return Object . keys ( attrs ) . map ( ( key ) => { return ` ${ key }="${ escape ( attrs [ key ] ) }"` ; } ) . join ( '' ) ; } function isMetaDescription ( headConfig ) { const [ type , attrs ] = headConfig ; return type === 'meta' && attrs ?. name === 'description' ; } function filterOutHeadDescription ( head ) { return head ? head . filter ( ( h ) => ! isMetaDescription ( h ) ) : [ ] ; } function hasTag ( head , tag ) { const [ tagType , tagAttrs ] = tag ; const [ attr , value ] = Object . entries ( tagAttrs ) [ 0 ] ; return head . some ( ( [ type , attrs ] ) => type === tagType && attrs [ attr ] === value ) ; } function addSocialTags ( title , ... head ) { const tags = [ [ 'meta' , { name : 'twitter:title' , content : title } ] , [ 'meta' , { property : 'og:title' , content : title } ] ] ; tags . filter ( ( tagAttrs ) => { if ( ! hasTag ( head , tagAttrs ) ) head . push ( tagAttrs ) ; } ) ; return head ; }