feat: support `<script client>` in mpa mode

pull/395/head
Evan You 3 years ago
parent b94b163a3a
commit e0b69973f8

@ -21,7 +21,7 @@ export async function build(
}
try {
const [clientResult, serverResult, pageToHashMap] = await bundle(
const { clientResult, serverResult, pageToHashMap } = await bundle(
siteConfig,
buildOptions
)
@ -36,7 +36,9 @@ export async function build(
(chunk) => chunk.type === 'chunk' && chunk.isEntry
) as OutputChunk)
const cssChunk = (clientResult || serverResult).output.find(
const cssChunk = (
siteConfig.mpa ? serverResult : clientResult
).output.find(
(chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as OutputAsset

@ -0,0 +1,45 @@
import { build } from 'vite'
import { SiteConfig } from '..'
const virtualEntry = 'client.js'
export async function buildMPAClient(
js: Record<string, string>,
config: SiteConfig
) {
const files = Object.keys(js)
const themeFiles = files.filter((f) => !f.endsWith('.md'))
const pages = files.filter((f) => f.endsWith('.md'))
return build({
root: config.srcDir,
base: config.site.base,
logLevel: 'warn',
build: {
emptyOutDir: false,
outDir: config.outDir,
rollupOptions: {
input: [virtualEntry, ...pages]
}
},
plugins: [
{
name: 'vitepress-mpa-client',
resolveId(id) {
if (id === virtualEntry) {
return id
}
},
load(id) {
if (id === virtualEntry) {
return themeFiles
.map((file) => `import ${JSON.stringify(file)}`)
.join('\n')
} else if (id in js) {
return js[id]
}
}
}
]
})
}

@ -7,6 +7,7 @@ import { SiteConfig } from '../config'
import { RollupOutput } from 'rollup'
import { build, BuildOptions, UserConfig as ViteUserConfig } from 'vite'
import { createVitePressPlugin } from '../plugin'
import { buildMPAClient } from './buildMPAClient'
export const okMark = '\x1b[32m✓\x1b[0m'
export const failMark = '\x1b[31m✖\x1b[0m'
@ -15,9 +16,14 @@ export const failMark = '\x1b[31m✖\x1b[0m'
export async function bundle(
config: SiteConfig,
options: BuildOptions
): Promise<[RollupOutput, RollupOutput, Record<string, string>]> {
): Promise<{
clientResult: RollupOutput
serverResult: RollupOutput
pageToHashMap: Record<string, string>
}> {
const { root, srcDir } = config
const pageToHashMap = Object.create(null)
const clientJSMap = Object.create(null)
// define custom rollup input
// this is a multi-entry build - every page is considered an entry chunk
@ -39,7 +45,13 @@ export async function bundle(
root: srcDir,
base: config.site.base,
logLevel: 'warn',
plugins: createVitePressPlugin(root, config, ssr, pageToHashMap),
plugins: createVitePressPlugin(
root,
config,
ssr,
pageToHashMap,
clientJSMap
),
// @ts-ignore
ssr: {
noExternal: ['vitepress']
@ -112,9 +124,13 @@ export async function bundle(
if (fs.existsSync(publicDir)) {
await fs.copy(publicDir, config.outDir)
}
// build <script client> bundle
if (Object.keys(clientJSMap).length) {
clientResult = (await buildMPAClient(clientJSMap, config)) as RollupOutput
}
}
return [clientResult, serverResult, pageToHashMap]
return { clientResult, serverResult, pageToHashMap }
}
const adComponentRE = /(?:Carbon|BuySell)Ads/

@ -4,15 +4,16 @@ import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { HeadConfig } from '../shared'
import { normalizePath } from 'vite'
import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
import { slash } from '../utils/slash'
const escape = require('escape-html')
export async function renderPage(
config: SiteConfig,
page: string, // foo.md
result: RollupOutput,
appChunk: OutputChunk,
cssChunk: OutputAsset,
result: RollupOutput | null,
appChunk: OutputChunk | undefined,
cssChunk: OutputAsset | undefined,
pageToHashMap: Record<string, string>,
hashMapString: string
) {
@ -39,20 +40,26 @@ export async function renderPage(
))
const pageData = JSON.parse(__pageData)
const preloadLinks = config.mpa
? ''
: [
// resolve imports for index.js + page.md.js and inject script tags for
// them as well so we fetch everything as early as possible without having
// to wait for entry chunks to parse
...resolvePageImports(config, page, result, appChunk),
pageClientJsFileName,
appChunk.fileName
]
.map((file) => {
return `<link rel="modulepreload" href="${siteData.base}${file}">`
})
.join('\n ')
const preloadLinks = (
config.mpa
? appChunk
? [appChunk.fileName]
: []
: result && appChunk
? [
// resolve imports for index.js + page.md.js and inject script tags for
// them as well so we fetch everything as early as possible without having
// to wait for entry chunks to parse
...resolvePageImports(config, page, result, appChunk),
pageClientJsFileName,
appChunk.fileName
]
: []
)
.map((file) => {
return `<link rel="modulepreload" href="${siteData.base}${file}">`
})
.join('\n ')
const stylesheetLink = cssChunk
? `<link rel="stylesheet" href="${siteData.base}${cssChunk.fileName}">`
@ -69,6 +76,23 @@ export async function renderPage(
...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}">
@ -87,10 +111,16 @@ export async function renderPage(
<div id="app">${content}</div>
${
config.mpa
? ``
: `<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>` +
`<script type="module" async src="${siteData.base}${appChunk.fileName}"></script>`
}</body>
? ''
: `<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))

@ -45,6 +45,7 @@ export interface UserConfig<ThemeConfig = any> {
/**
* Enable MPA / zero-JS mode
* @experimental
*/
mpa?: boolean
}

@ -133,6 +133,7 @@ export function createMarkdownToVueRenderFn(
const scriptRE = /<\/script>/
const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/
const scriptClientRe = /<\s*script[^>]*\bclient\b[^>]*/
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/
@ -142,7 +143,11 @@ function genPageDataCode(tags: string[], data: PageData) {
)}`
const existingScriptIndex = tags.findIndex((tag) => {
return scriptRE.test(tag) && !scriptSetupRE.test(tag)
return (
scriptRE.test(tag) &&
!scriptSetupRE.test(tag) &&
!scriptClientRe.test(tag)
)
})
if (existingScriptIndex > -1) {

@ -16,6 +16,11 @@ const staticInjectMarkerRE =
const staticStripRE = /__VP_STATIC_START__.*?__VP_STATIC_END__/g
const staticRestoreRE = /__VP_STATIC_(START|END)__/g
// matches client-side js blocks in MPA mode.
// in the future we may add different execution strategies like visible or
// media queries.
const scriptClientRE = /<script\b[^>]*client\b[^>]*>([^]*?)<\/script>/
const isPageChunk = (
chunk: OutputAsset | OutputChunk
): chunk is OutputChunk & { facadeModuleId: string } =>
@ -28,7 +33,12 @@ const isPageChunk = (
export function createVitePressPlugin(
root: string,
{
siteConfig: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>,
clientJSMap?: Record<string, string>
): Plugin[] {
const {
srcDir,
configPath,
alias,
@ -37,10 +47,8 @@ export function createVitePressPlugin(
vue: userVuePluginOptions,
vite: userViteConfig,
pages
}: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>
): Plugin[] {
} = siteConfig
let markdownToVue: (
src: string,
file: string,
@ -52,6 +60,15 @@ export function createVitePressPlugin(
...userVuePluginOptions
})
const processClientJS = (code: string, id: string) => {
return scriptClientRE.test(code)
? code.replace(scriptClientRE, (_, content) => {
if (ssr && clientJSMap) clientJSMap[id] = content
return `\n`.repeat(_.split('\n').length - 1)
})
: code
}
let siteData = site
let hasDeadLinks = false
let config: ResolvedConfig
@ -109,7 +126,9 @@ export function createVitePressPlugin(
},
transform(code, id) {
if (id.endsWith('.md')) {
if (id.endsWith('.vue')) {
return processClientJS(code, id)
} else if (id.endsWith('.md')) {
// transform .md files into vueSrc so plugin-vue can handle it
const { vueSrc, deadLinks, includes } = markdownToVue(
code,
@ -124,7 +143,7 @@ export function createVitePressPlugin(
this.addWatchFile(i)
})
}
return vueSrc
return processClientJS(vueSrc, id)
}
},

Loading…
Cancel
Save