mirror of https://github.com/vuejs/vitepress
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
289 lines
7.9 KiB
289 lines
7.9 KiB
import path from 'path'
|
|
import { defineConfig, mergeConfig, Plugin, ResolvedConfig } from 'vite'
|
|
import { SiteConfig, resolveSiteData } from './config'
|
|
import {
|
|
createMarkdownToVueRenderFn,
|
|
MarkdownCompileResult
|
|
} from './markdownToVue'
|
|
import { DIST_CLIENT_PATH, APP_PATH, SITE_DATA_REQUEST_PATH } from './alias'
|
|
import { slash } from './utils/slash'
|
|
import { OutputAsset, OutputChunk } from 'rollup'
|
|
import { staticDataPlugin } from './staticDataPlugin'
|
|
|
|
const hashRE = /\.(\w+)\.js$/
|
|
const staticInjectMarkerRE =
|
|
/\b(const _hoisted_\d+ = \/\*(?:#|@)__PURE__\*\/\s*createStaticVNode)\("(.*)", (\d+)\)/g
|
|
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 } =>
|
|
!!(
|
|
chunk.type === 'chunk' &&
|
|
chunk.isEntry &&
|
|
chunk.facadeModuleId &&
|
|
chunk.facadeModuleId.endsWith('.md')
|
|
)
|
|
|
|
export function createVitePressPlugin(
|
|
root: string,
|
|
siteConfig: SiteConfig,
|
|
ssr = false,
|
|
pageToHashMap?: Record<string, string>,
|
|
clientJSMap?: Record<string, string>
|
|
) {
|
|
const {
|
|
srcDir,
|
|
configPath,
|
|
alias,
|
|
markdown,
|
|
site,
|
|
vue: userVuePluginOptions,
|
|
vite: userViteConfig,
|
|
pages
|
|
} = siteConfig
|
|
|
|
let markdownToVue: (
|
|
src: string,
|
|
file: string,
|
|
publicDir: string
|
|
) => MarkdownCompileResult
|
|
|
|
// lazy require plugin-vue to respect NODE_ENV in @vue/compiler-x
|
|
const vuePlugin = require('@vitejs/plugin-vue')({
|
|
include: [/\.vue$/, /\.md$/],
|
|
...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
|
|
|
|
const vitePressPlugin: Plugin = {
|
|
name: 'vitepress',
|
|
|
|
configResolved(resolvedConfig) {
|
|
config = resolvedConfig
|
|
markdownToVue = createMarkdownToVueRenderFn(
|
|
srcDir,
|
|
markdown,
|
|
pages,
|
|
config.define,
|
|
config.command === 'build'
|
|
)
|
|
},
|
|
|
|
config() {
|
|
const baseConfig = defineConfig({
|
|
resolve: {
|
|
alias
|
|
},
|
|
define: {
|
|
__CARBON__: !!site.themeConfig.carbonAds?.carbon,
|
|
__BSA__: !!site.themeConfig.carbonAds?.custom,
|
|
__ALGOLIA__: !!site.themeConfig.algolia
|
|
},
|
|
optimizeDeps: {
|
|
// force include vue to avoid duplicated copies when linked + optimized
|
|
include: ['vue'],
|
|
exclude: ['@docsearch/js']
|
|
},
|
|
server: {
|
|
fs: {
|
|
allow: [DIST_CLIENT_PATH, srcDir, process.cwd()]
|
|
}
|
|
}
|
|
})
|
|
return userViteConfig
|
|
? mergeConfig(userViteConfig, baseConfig)
|
|
: baseConfig
|
|
},
|
|
|
|
resolveId(id) {
|
|
if (id === SITE_DATA_REQUEST_PATH) {
|
|
return SITE_DATA_REQUEST_PATH
|
|
}
|
|
},
|
|
|
|
load(id) {
|
|
if (id === SITE_DATA_REQUEST_PATH) {
|
|
let data = siteData
|
|
// head info is not needed by the client in production build
|
|
if (config.command === 'build') {
|
|
data = { ...siteData, head: [] }
|
|
}
|
|
return `export default ${JSON.stringify(JSON.stringify(data))}`
|
|
}
|
|
},
|
|
|
|
transform(code, id) {
|
|
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,
|
|
id,
|
|
config.publicDir
|
|
)
|
|
if (deadLinks.length) {
|
|
hasDeadLinks = true
|
|
}
|
|
if (includes.length) {
|
|
includes.forEach((i) => {
|
|
this.addWatchFile(i)
|
|
})
|
|
}
|
|
return processClientJS(vueSrc, id)
|
|
}
|
|
},
|
|
|
|
renderStart() {
|
|
if (hasDeadLinks) {
|
|
throw new Error(`One or more pages contain dead links.`)
|
|
}
|
|
},
|
|
|
|
configureServer(server) {
|
|
if (configPath) {
|
|
server.watcher.add(configPath)
|
|
}
|
|
|
|
// serve our index.html after vite history fallback
|
|
return () => {
|
|
server.middlewares.use((req, res, next) => {
|
|
if (req.url!.endsWith('.html')) {
|
|
res.statusCode = 200
|
|
res.end(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title></title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<meta name="description" content="">
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
<script type="module" src="/@fs/${APP_PATH}/index.js"></script>
|
|
</body>
|
|
</html>`)
|
|
return
|
|
}
|
|
next()
|
|
})
|
|
}
|
|
},
|
|
|
|
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.toLowerCase()] = 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(ctx) {
|
|
// handle config hmr
|
|
const { file, read, server } = ctx
|
|
if (file === configPath) {
|
|
const newData = await resolveSiteData(root)
|
|
if (newData.base !== siteData.base) {
|
|
console.warn(
|
|
`[vitepress]: config.base has changed. Please restart the dev server.`
|
|
)
|
|
}
|
|
siteData = newData
|
|
return [server.moduleGraph.getModuleById(SITE_DATA_REQUEST_PATH)!]
|
|
}
|
|
|
|
// hot reload .md files as .vue files
|
|
if (file.endsWith('.md')) {
|
|
const content = await read()
|
|
const { pageData, vueSrc } = markdownToVue(
|
|
content,
|
|
file,
|
|
config.publicDir
|
|
)
|
|
|
|
// notify the client to update page data
|
|
server.ws.send({
|
|
type: 'custom',
|
|
event: 'vitepress:pageData',
|
|
data: {
|
|
path: `/${slash(path.relative(srcDir, file))}`,
|
|
pageData
|
|
}
|
|
})
|
|
|
|
// overwrite src so vue plugin can handle the HMR
|
|
ctx.read = () => vueSrc
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
vitePressPlugin,
|
|
vuePlugin,
|
|
...(userViteConfig?.plugins || []),
|
|
staticDataPlugin
|
|
]
|
|
}
|