wip: build compat for vite 2.0

pull/198/head
Evan You 4 years ago
parent 02391593bc
commit d02cd6b9b5

@ -29,7 +29,7 @@ export function pathToFile(path: string): string {
// client production build needs to account for page hash, which is
// injected directly in the page's html
const pageHash = __VP_HASH_MAP__[pagePath]
pagePath = `${base}_assets/${pagePath}.${pageHash}.js`
pagePath = `${base}assets/${pagePath}.${pageHash}.js`
} else {
// ssr build uses much simpler name mapping
pagePath = `./${pagePath.slice(1).replace(/\//g, '_')}.md.js`

@ -1,24 +1,16 @@
import fs from 'fs-extra'
import { bundle, okMark, failMark } from './bundle'
import { BuildOptions as ViteBuildOptions } from 'vite'
import { BuildOptions } from 'vite'
import { resolveConfig } from '../config'
import { renderPage } from './render'
import { OutputChunk, OutputAsset } from 'rollup'
import ora from 'ora'
export type BuildOptions = Pick<
Partial<ViteBuildOptions>,
| 'root'
| 'rollupInputOptions'
| 'rollupOutputOptions'
| 'rollupPluginVueOptions'
>
export async function build(buildOptions: BuildOptions = {}) {
export async function build(root: string, buildOptions: BuildOptions = {}) {
const start = Date.now()
process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(buildOptions.root)
const siteConfig = await resolveConfig(root)
try {
const [clientResult, , pageToHashMap] = await bundle(
@ -30,12 +22,11 @@ export async function build(buildOptions: BuildOptions = {}) {
spinner.start('rendering pages...')
try {
const appChunk = clientResult.assets.find(
(chunk) =>
chunk.type === 'chunk' && chunk.fileName.match(/^app\.\w+\.js$/)
const appChunk = clientResult.output.find(
(chunk) => chunk.type === 'chunk' && chunk.isEntry && chunk
) as OutputChunk
const cssChunk = clientResult.assets.find(
const cssChunk = clientResult.output.find(
(chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as OutputAsset

@ -1,109 +1,23 @@
import ora from 'ora'
import path from 'path'
import slash from 'slash'
import fs from 'fs-extra'
import { APP_PATH, createResolver, SITE_DATA_REQUEST_PATH } from '../resolver'
import { BuildOptions } from './build'
import { resolveUserConfig, SiteConfig } from '../config'
import { Plugin, OutputAsset, OutputChunk } from 'rollup'
import { createMarkdownToVueRenderFn } from '../markdownToVue'
import { build, BuildOptions as ViteBuildOptions } from 'vite'
import ora from 'ora'
import { APP_PATH } from '../alias'
import { SiteConfig } from '../config'
import { RollupOutput, ExternalOption } from 'rollup'
import { build, BuildOptions, UserConfig as ViteUserConfig } from 'vite'
import { createVitePressPlugin } from '../plugin'
export const okMark = '\x1b[32m✓\x1b[0m'
export const failMark = '\x1b[31m✖\x1b[0m'
const hashRE = /\.(\w+)\.js$/
const staticInjectMarkerRE = /\b(const _hoisted_\d+ = \/\*#__PURE__\*\/createStaticVNode)\("(.*)", (\d+)\)/g
const staticStripRE = /__VP_STATIC_START__.*?__VP_STATIC_END__/g
const staticRestoreRE = /__VP_STATIC_(START|END)__/g
const isPageChunk = (
chunk: OutputAsset | OutputChunk
): chunk is OutputChunk & { facadeModuleId: string } =>
!!(
chunk.type === 'chunk' &&
chunk.isEntry &&
chunk.facadeModuleId &&
chunk.facadeModuleId.endsWith('.md')
)
// bundles the VitePress app for both client AND server.
export async function bundle(
config: SiteConfig,
options: BuildOptions
): Promise<[BuildResult, BuildResult, Record<string, string>]> {
): Promise<[RollupOutput, RollupOutput, Record<string, string>]> {
const root = config.root
const userConfig = await resolveUserConfig(root)
const resolver = createResolver(config.themeDir, userConfig)
const markdownToVue = createMarkdownToVueRenderFn(root, userConfig.markdown)
let isClientBuild = true
const pageToHashMap = Object.create(null)
const VitePressPlugin: Plugin = {
name: 'vitepress',
resolveId(id) {
if (id === SITE_DATA_REQUEST_PATH) {
return id
}
},
async load(id) {
if (id === SITE_DATA_REQUEST_PATH) {
return `export default ${JSON.stringify(JSON.stringify(config.site))}`
}
// compile md into vue src
if (id.endsWith('.md')) {
const content = await fs.readFile(id, 'utf-8')
const { vueSrc } = markdownToVue(content, id)
return vueSrc
}
},
renderChunk(code, chunk) {
if (isClientBuild && isPageChunk(chunk as OutputChunk)) {
// For each page chunk, inject marker for start/end of static strings.
// we do this here because in generateBundle the chunks would have been
// minified and we won't be able to safely locate the strings.
// Using a regexp relies on specific output from Vue compiler core,
// which is a reasonable trade-off considering the massive perf win over
// a full AST parse.
code = code.replace(
staticInjectMarkerRE,
'$1("__VP_STATIC_START__$2__VP_STATIC_END__", $3)'
)
return code
}
return null
},
generateBundle(_options, bundle) {
if (!isClientBuild) {
return
}
// for each .md entry chunk, adjust its name to its correct path.
for (const name in bundle) {
const chunk = bundle[name]
if (isPageChunk(chunk)) {
// record page -> hash relations
const hash = chunk.fileName.match(hashRE)![1]
const pageName = chunk.fileName.replace(hashRE, '')
pageToHashMap[pageName] = hash
// inject another chunk with the content stripped
bundle[name + '-lean'] = {
...chunk,
fileName: chunk.fileName.replace(/\.js$/, '.lean.js'),
code: chunk.code.replace(staticStripRE, ``)
}
// remove static markers from original code
chunk.code = chunk.code.replace(staticRestoreRE, '')
}
}
}
}
// define custom rollup input
// this is a multi-entry build - every page is considered an entry chunk
// the loading is done via filename conversion rules so that the
@ -118,49 +32,57 @@ export async function bundle(
})
// resolve options to pass to vite
const { rollupInputOptions = {}, rollupOutputOptions = {} } = options
const viteOptions: Partial<ViteBuildOptions> = {
...options,
base: config.site.base,
resolvers: [resolver],
outDir: config.outDir,
// let rollup-plugin-vue compile .md files as well
rollupPluginVueOptions: {
include: /\.(vue|md)$/
},
rollupInputOptions: {
...rollupInputOptions,
input,
// important so that each page chunk and the index export things for each
// other
preserveEntrySignatures: 'allow-extension',
plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])]
},
rollupOutputOptions: {
...rollupOutputOptions,
chunkFileNames(chunk): string {
if (/runtime-dom/.test(chunk.name)) {
return `framework.[hash].js`
const { rollupOptions } = options
const resolveViteConfig = (ssr: boolean): ViteUserConfig => ({
logLevel: 'warn',
plugins: createVitePressPlugin(root, config, ssr, pageToHashMap),
build: {
...options,
base: config.site.base,
outDir: ssr ? config.tempDir : config.outDir,
cssCodeSplit: !ssr,
rollupOptions: {
...rollupOptions,
input,
external: ssr
? resolveExternal(rollupOptions?.external)
: rollupOptions?.external,
// important so that each page chunk and the index export things for each
// other
preserveEntrySignatures: 'allow-extension',
output: {
...rollupOptions?.output,
...(ssr
? {
format: 'cjs',
exports: 'named',
entryFileNames: '[name].js'
}
: {
chunkFileNames(chunk): string {
if (!chunk.isEntry && /runtime/.test(chunk.name)) {
return `assets/framework.[hash].js`
}
return `assets/[name].[hash].js`
}
})
}
return `[name].[hash].js`
}
},
silent: !process.env.DEBUG,
minify: !process.env.DEBUG
}
},
minify: false //ssr ? false : !process.env.DEBUG
}
})
let clientResult, serverResult
let clientResult: RollupOutput
let serverResult: RollupOutput
const spinner = ora()
spinner.start('building client + server bundles...')
try {
;[clientResult, serverResult] = await Promise.all([
build(viteOptions),
ssrBuild({
...viteOptions,
outDir: config.tempDir
})
])
;[clientResult, serverResult] = await (Promise.all([
build(resolveViteConfig(false)),
build(resolveViteConfig(true))
]) as Promise<[RollupOutput, RollupOutput]>)
} catch (e) {
spinner.stopAndPersist({
symbol: failMark
@ -171,5 +93,26 @@ export async function bundle(
symbol: okMark
})
return [clientResult[0], serverResult[0], pageToHashMap]
return [clientResult, serverResult, pageToHashMap]
}
function resolveExternal(
userExternal: ExternalOption | undefined
): ExternalOption {
const required = ['vue', /^@vue\//]
if (!userExternal) {
return required
}
if (Array.isArray(userExternal)) {
return [...required, ...userExternal]
} else if (typeof userExternal === 'function') {
return (src, importer, isResolved) => {
if (src === 'vue' || /^@vue\//.test(src)) {
return true
}
return userExternal(src, importer, isResolved)
}
} else {
return [...required, userExternal]
}
}

@ -2,21 +2,20 @@ import path from 'path'
import fs from 'fs-extra'
import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { HeadConfig } from '../../../types/shared'
import { BuildResult } from 'vite'
import { OutputChunk, OutputAsset } from 'rollup'
import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
const escape = require('escape-html')
export async function renderPage(
config: SiteConfig,
page: string, // foo.md
result: BuildResult,
result: RollupOutput,
appChunk: OutputChunk,
cssChunk: OutputAsset,
pageToHashMap: Record<string, string>,
hashMapString: string
) {
const { createApp } = require(path.join(config.tempDir, `_assets/app.js`))
const { createApp } = require(path.join(config.tempDir, `app.js`))
const { app, router } = createApp()
const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath)
@ -30,17 +29,15 @@ export async function renderPage(
// for any initial page load, we only need the lean version of the page js
// since the static content is already on the page!
const pageHash = pageToHashMap[pageName]
const pageClientJsFileName = pageName + `.` + pageHash + '.lean.js'
const pageClientJsFileName = `assets/${pageName}.${pageHash}.lean.js`
// resolve page data so we can render head tags
const { __pageData } = require(path.join(
config.tempDir,
`_assets`,
pageServerJsFileName
))
const pageData = JSON.parse(__pageData)
const assetPath = `${siteData.base}_assets/`
const preloadLinks = [
// 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
@ -50,7 +47,7 @@ export async function renderPage(
appChunk.fileName
]
.map((file) => {
return `<link rel="modulepreload" href="${assetPath}${file}">`
return `<link rel="modulepreload" href="${siteData.base}${file}">`
})
.join('\n ')
@ -64,7 +61,7 @@ export async function renderPage(
${pageData.title ? pageData.title + ` | ` : ``}${siteData.title}
</title>
<meta name="description" content="${siteData.description}">
<link rel="stylesheet" href="${assetPath}${cssChunk.fileName}">
<link rel="stylesheet" href="${siteData.base}${cssChunk.fileName}">
${preloadLinks}
${renderHead(siteData.head)}
${renderHead(pageData.frontmatter.head)}
@ -72,7 +69,9 @@ export async function renderPage(
<body>
<div id="app">${content}</div>
<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>
<script type="module" async src="${assetPath}${appChunk.fileName}"></script>
<script type="module" async src="${siteData.base}${
appChunk.fileName
}"></script>
</body>
</html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
@ -83,14 +82,14 @@ export async function renderPage(
function resolvePageImports(
config: SiteConfig,
page: string,
result: BuildResult,
result: RollupOutput,
indexChunk: OutputChunk
) {
// find the page's js chunk and inject script tags for its imports so that
// they are start fetching as early as possible
const srcPath = path.resolve(config.root, page)
const pageChunk = result.assets.find(
const pageChunk = result.output.find(
(chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
) as OutputChunk
return Array.from(new Set([...indexChunk.imports, ...pageChunk.imports]))

@ -21,7 +21,7 @@ if (!command || command === 'dev') {
process.exit(1)
})
} else if (command === 'build') {
build(argv).catch((err) => {
build(root, argv).catch((err) => {
console.error(chalk.red(`build error:\n`), err)
process.exit(1)
})

@ -2,7 +2,7 @@ import path from 'path'
import fs from 'fs-extra'
import chalk from 'chalk'
import globby from 'globby'
import { APP_PATH, createAlias, DEFAULT_THEME_PATH } from './resolver'
import { createAlias, APP_PATH, DEFAULT_THEME_PATH } from './alias'
import { SiteData, HeadConfig, LocaleConfig } from '../../types/shared'
import { MarkdownOptions } from './markdown/markdown'
import { AliasOptions } from 'vite'

@ -2,18 +2,37 @@ import path from 'path'
import { Plugin } from 'vite'
import { SiteConfig, resolveSiteData } from './config'
import { createMarkdownToVueRenderFn } from './markdownToVue'
import { APP_PATH, SITE_DATA_REQUEST_PATH } from './resolver'
import { APP_PATH, SITE_DATA_REQUEST_PATH } from './alias'
import createVuePlugin from '@vitejs/plugin-vue'
import slash from 'slash'
import { OutputAsset, OutputChunk } from 'rollup'
const hashRE = /\.(\w+)\.js$/
const staticInjectMarkerRE = /\b(const _hoisted_\d+ = \/\*#__PURE__\*\/createStaticVNode)\("(.*)", (\d+)\)/g
const staticStripRE = /__VP_STATIC_START__.*?__VP_STATIC_END__/g
const staticRestoreRE = /__VP_STATIC_(START|END)__/g
const isPageChunk = (
chunk: OutputAsset | OutputChunk
): chunk is OutputChunk & { facadeModuleId: string } =>
!!(
chunk.type === 'chunk' &&
chunk.isEntry &&
chunk.facadeModuleId &&
chunk.facadeModuleId.endsWith('.md')
)
export function createVitePressPlugin(
root: string,
{ configPath, aliases, markdown, site: initialSiteData }: SiteConfig
{ configPath, aliases, markdown, site: initialSiteData }: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>
): Plugin[] {
const markdownToVue = createMarkdownToVueRenderFn(root, markdown)
const vuePlugin = createVuePlugin({
include: [/\.vue$/, /\.md$/]
include: [/\.vue$/, /\.md$/],
ssr
})
let siteData = initialSiteData
@ -51,6 +70,7 @@ export function createVitePressPlugin(
// serve our index.html after vite history fallback
const indexPath = `/@fs/${path.join(APP_PATH, 'index.html')}`
return () => {
// @ts-ignore
server.app.use((req, _, next) => {
if (req.url!.endsWith('.html')) {
req.url = indexPath
@ -60,6 +80,55 @@ export function createVitePressPlugin(
}
},
renderChunk(code, chunk) {
if (!ssr && isPageChunk(chunk as OutputChunk)) {
// For each page chunk, inject marker for start/end of static strings.
// we do this here because in generateBundle the chunks would have been
// minified and we won't be able to safely locate the strings.
// Using a regexp relies on specific output from Vue compiler core,
// which is a reasonable trade-off considering the massive perf win over
// a full AST parse.
code = code.replace(
staticInjectMarkerRE,
'$1("__VP_STATIC_START__$2__VP_STATIC_END__", $3)'
)
return code
}
return null
},
generateBundle(_options, bundle) {
if (ssr) {
// ssr build:
// delete all asset chunks
for (const name in bundle) {
if (bundle[name].type === 'asset') {
delete bundle[name]
}
}
} else {
// client build:
// for each .md entry chunk, adjust its name to its correct path.
for (const name in bundle) {
const chunk = bundle[name]
if (isPageChunk(chunk)) {
// record page -> hash relations
const hash = chunk.fileName.match(hashRE)![1]
pageToHashMap![chunk.name] = hash
// inject another chunk with the content stripped
bundle[name + '-lean'] = {
...chunk,
fileName: chunk.fileName.replace(/\.js$/, '.lean.js'),
code: chunk.code.replace(staticStripRE, ``)
}
// remove static markers from original code
chunk.code = chunk.code.replace(staticRestoreRE, '')
}
}
}
},
async handleHotUpdate(file, mods, read, server) {
// handle config hmr
if (file === configPath) {

Loading…
Cancel
Save