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 // client production build needs to account for page hash, which is
// injected directly in the page's html // injected directly in the page's html
const pageHash = __VP_HASH_MAP__[pagePath] const pageHash = __VP_HASH_MAP__[pagePath]
pagePath = `${base}_assets/${pagePath}.${pageHash}.js` pagePath = `${base}assets/${pagePath}.${pageHash}.js`
} else { } else {
// ssr build uses much simpler name mapping // ssr build uses much simpler name mapping
pagePath = `./${pagePath.slice(1).replace(/\//g, '_')}.md.js` pagePath = `./${pagePath.slice(1).replace(/\//g, '_')}.md.js`

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

@ -1,109 +1,23 @@
import ora from 'ora'
import path from 'path' import path from 'path'
import slash from 'slash' import slash from 'slash'
import fs from 'fs-extra' import { APP_PATH } from '../alias'
import { APP_PATH, createResolver, SITE_DATA_REQUEST_PATH } from '../resolver' import { SiteConfig } from '../config'
import { BuildOptions } from './build' import { RollupOutput, ExternalOption } from 'rollup'
import { resolveUserConfig, SiteConfig } from '../config' import { build, BuildOptions, UserConfig as ViteUserConfig } from 'vite'
import { Plugin, OutputAsset, OutputChunk } from 'rollup' import { createVitePressPlugin } from '../plugin'
import { createMarkdownToVueRenderFn } from '../markdownToVue'
import { build, BuildOptions as ViteBuildOptions } from 'vite'
import ora from 'ora'
export const okMark = '\x1b[32m✓\x1b[0m' export const okMark = '\x1b[32m✓\x1b[0m'
export const failMark = '\x1b[31m✖\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. // bundles the VitePress app for both client AND server.
export async function bundle( export async function bundle(
config: SiteConfig, config: SiteConfig,
options: BuildOptions options: BuildOptions
): Promise<[BuildResult, BuildResult, Record<string, string>]> { ): Promise<[RollupOutput, RollupOutput, Record<string, string>]> {
const root = config.root 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 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 // define custom rollup input
// this is a multi-entry build - every page is considered an entry chunk // this is a multi-entry build - every page is considered an entry chunk
// the loading is done via filename conversion rules so that the // 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 // resolve options to pass to vite
const { rollupInputOptions = {}, rollupOutputOptions = {} } = options const { rollupOptions } = options
const viteOptions: Partial<ViteBuildOptions> = {
const resolveViteConfig = (ssr: boolean): ViteUserConfig => ({
logLevel: 'warn',
plugins: createVitePressPlugin(root, config, ssr, pageToHashMap),
build: {
...options, ...options,
base: config.site.base, base: config.site.base,
resolvers: [resolver], outDir: ssr ? config.tempDir : config.outDir,
outDir: config.outDir, cssCodeSplit: !ssr,
// let rollup-plugin-vue compile .md files as well rollupOptions: {
rollupPluginVueOptions: { ...rollupOptions,
include: /\.(vue|md)$/
},
rollupInputOptions: {
...rollupInputOptions,
input, input,
external: ssr
? resolveExternal(rollupOptions?.external)
: rollupOptions?.external,
// important so that each page chunk and the index export things for each // important so that each page chunk and the index export things for each
// other // other
preserveEntrySignatures: 'allow-extension', preserveEntrySignatures: 'allow-extension',
plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])] output: {
}, ...rollupOptions?.output,
rollupOutputOptions: { ...(ssr
...rollupOutputOptions, ? {
format: 'cjs',
exports: 'named',
entryFileNames: '[name].js'
}
: {
chunkFileNames(chunk): string { chunkFileNames(chunk): string {
if (/runtime-dom/.test(chunk.name)) { if (!chunk.isEntry && /runtime/.test(chunk.name)) {
return `framework.[hash].js` return `assets/framework.[hash].js`
} }
return `[name].[hash].js` return `assets/[name].[hash].js`
}
})
} }
}, },
silent: !process.env.DEBUG, minify: false //ssr ? false : !process.env.DEBUG
minify: !process.env.DEBUG
} }
})
let clientResult, serverResult let clientResult: RollupOutput
let serverResult: RollupOutput
const spinner = ora() const spinner = ora()
spinner.start('building client + server bundles...') spinner.start('building client + server bundles...')
try { try {
;[clientResult, serverResult] = await Promise.all([ ;[clientResult, serverResult] = await (Promise.all([
build(viteOptions), build(resolveViteConfig(false)),
ssrBuild({ build(resolveViteConfig(true))
...viteOptions, ]) as Promise<[RollupOutput, RollupOutput]>)
outDir: config.tempDir
})
])
} catch (e) { } catch (e) {
spinner.stopAndPersist({ spinner.stopAndPersist({
symbol: failMark symbol: failMark
@ -171,5 +93,26 @@ export async function bundle(
symbol: okMark 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 fs from 'fs-extra'
import { SiteConfig, resolveSiteDataByRoute } from '../config' import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { HeadConfig } from '../../../types/shared' import { HeadConfig } from '../../../types/shared'
import { BuildResult } from 'vite' import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
import { OutputChunk, OutputAsset } from 'rollup'
const escape = require('escape-html') const escape = require('escape-html')
export async function renderPage( export async function renderPage(
config: SiteConfig, config: SiteConfig,
page: string, // foo.md page: string, // foo.md
result: BuildResult, result: RollupOutput,
appChunk: OutputChunk, appChunk: OutputChunk,
cssChunk: OutputAsset, cssChunk: OutputAsset,
pageToHashMap: Record<string, string>, pageToHashMap: Record<string, string>,
hashMapString: 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 { app, router } = createApp()
const routePath = `/${page.replace(/\.md$/, '')}` const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath) 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 // for any initial page load, we only need the lean version of the page js
// since the static content is already on the page! // since the static content is already on the page!
const pageHash = pageToHashMap[pageName] 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 // resolve page data so we can render head tags
const { __pageData } = require(path.join( const { __pageData } = require(path.join(
config.tempDir, config.tempDir,
`_assets`,
pageServerJsFileName pageServerJsFileName
)) ))
const pageData = JSON.parse(__pageData) const pageData = JSON.parse(__pageData)
const assetPath = `${siteData.base}_assets/`
const preloadLinks = [ const preloadLinks = [
// resolve imports for index.js + page.md.js and inject script tags for // 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 // them as well so we fetch everything as early as possible without having
@ -50,7 +47,7 @@ export async function renderPage(
appChunk.fileName appChunk.fileName
] ]
.map((file) => { .map((file) => {
return `<link rel="modulepreload" href="${assetPath}${file}">` return `<link rel="modulepreload" href="${siteData.base}${file}">`
}) })
.join('\n ') .join('\n ')
@ -64,7 +61,7 @@ export async function renderPage(
${pageData.title ? pageData.title + ` | ` : ``}${siteData.title} ${pageData.title ? pageData.title + ` | ` : ``}${siteData.title}
</title> </title>
<meta name="description" content="${siteData.description}"> <meta name="description" content="${siteData.description}">
<link rel="stylesheet" href="${assetPath}${cssChunk.fileName}"> <link rel="stylesheet" href="${siteData.base}${cssChunk.fileName}">
${preloadLinks} ${preloadLinks}
${renderHead(siteData.head)} ${renderHead(siteData.head)}
${renderHead(pageData.frontmatter.head)} ${renderHead(pageData.frontmatter.head)}
@ -72,7 +69,9 @@ export async function renderPage(
<body> <body>
<div id="app">${content}</div> <div id="app">${content}</div>
<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script> <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> </body>
</html>`.trim() </html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
@ -83,14 +82,14 @@ export async function renderPage(
function resolvePageImports( function resolvePageImports(
config: SiteConfig, config: SiteConfig,
page: string, page: string,
result: BuildResult, result: RollupOutput,
indexChunk: OutputChunk indexChunk: OutputChunk
) { ) {
// find the page's js chunk and inject script tags for its imports so that // find the page's js chunk and inject script tags for its imports so that
// they are start fetching as early as possible // they are start fetching as early as possible
const srcPath = path.resolve(config.root, page) const srcPath = path.resolve(config.root, page)
const pageChunk = result.assets.find( const pageChunk = result.output.find(
(chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath (chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
) as OutputChunk ) as OutputChunk
return Array.from(new Set([...indexChunk.imports, ...pageChunk.imports])) return Array.from(new Set([...indexChunk.imports, ...pageChunk.imports]))

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

@ -2,7 +2,7 @@ import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import chalk from 'chalk' import chalk from 'chalk'
import globby from 'globby' 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 { SiteData, HeadConfig, LocaleConfig } from '../../types/shared'
import { MarkdownOptions } from './markdown/markdown' import { MarkdownOptions } from './markdown/markdown'
import { AliasOptions } from 'vite' import { AliasOptions } from 'vite'

@ -2,18 +2,37 @@ import path from 'path'
import { Plugin } from 'vite' import { Plugin } from 'vite'
import { SiteConfig, resolveSiteData } from './config' import { SiteConfig, resolveSiteData } from './config'
import { createMarkdownToVueRenderFn } from './markdownToVue' 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 createVuePlugin from '@vitejs/plugin-vue'
import slash from 'slash' 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( export function createVitePressPlugin(
root: string, root: string,
{ configPath, aliases, markdown, site: initialSiteData }: SiteConfig { configPath, aliases, markdown, site: initialSiteData }: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>
): Plugin[] { ): Plugin[] {
const markdownToVue = createMarkdownToVueRenderFn(root, markdown) const markdownToVue = createMarkdownToVueRenderFn(root, markdown)
const vuePlugin = createVuePlugin({ const vuePlugin = createVuePlugin({
include: [/\.vue$/, /\.md$/] include: [/\.vue$/, /\.md$/],
ssr
}) })
let siteData = initialSiteData let siteData = initialSiteData
@ -51,6 +70,7 @@ export function createVitePressPlugin(
// serve our index.html after vite history fallback // serve our index.html after vite history fallback
const indexPath = `/@fs/${path.join(APP_PATH, 'index.html')}` const indexPath = `/@fs/${path.join(APP_PATH, 'index.html')}`
return () => { return () => {
// @ts-ignore
server.app.use((req, _, next) => { server.app.use((req, _, next) => {
if (req.url!.endsWith('.html')) { if (req.url!.endsWith('.html')) {
req.url = indexPath 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) { async handleHotUpdate(file, mods, read, server) {
// handle config hmr // handle config hmr
if (file === configPath) { if (file === configPath) {

Loading…
Cancel
Save