Skip to content

Commit

Permalink
feat: support <script client> in mpa mode
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Sep 14, 2021
1 parent b94b163 commit e0b6997
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 34 deletions.
6 changes: 4 additions & 2 deletions src/node/build/build.ts
Expand Up @@ -21,7 +21,7 @@ export async function build(
}

try {
const [clientResult, serverResult, pageToHashMap] = await bundle(
const { clientResult, serverResult, pageToHashMap } = await bundle(
siteConfig,
buildOptions
)
Expand All @@ -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

Expand Down
45 changes: 45 additions & 0 deletions src/node/build/buildMPAClient.ts
@@ -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]
}
}
}
]
})
}
22 changes: 19 additions & 3 deletions src/node/build/bundle.ts
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -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']
Expand Down Expand Up @@ -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/
72 changes: 51 additions & 21 deletions src/node/build/render.ts
Expand Up @@ -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
) {
Expand All @@ -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}">`
Expand All @@ -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}">
Expand All @@ -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))
Expand Down
1 change: 1 addition & 0 deletions src/node/config.ts
Expand Up @@ -45,6 +45,7 @@ export interface UserConfig<ThemeConfig = any> {

/**
* Enable MPA / zero-JS mode
* @experimental
*/
mpa?: boolean
}
Expand Down
7 changes: 6 additions & 1 deletion src/node/markdownToVue.ts
Expand Up @@ -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/

Expand All @@ -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) {
Expand Down
33 changes: 26 additions & 7 deletions src/node/plugin.ts
Expand Up @@ -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 } =>
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -124,7 +143,7 @@ export function createVitePressPlugin(
this.addWatchFile(i)
})
}
return vueSrc
return processClientJS(vueSrc, id)
}
},

Expand Down

0 comments on commit e0b6997

Please sign in to comment.