Appearance
vitepress 命令源码摘要
- 调试入口文件:
src/node/cli.ts,在目标位置打上断点。 - 将
VSCode终端调至JavaScript Debug模式,输如下命令,唤起断点调试。
bash
pnpm run docs-dev
# "docs-dev": "wait-on -d 100 dist/node/cli.js && node ./bin/vitepress dev docs"处理命令行入参
ts
const argv: any = minimist(process.argv.slice(2)); // _: ['dev', 'docs']
const command = argv._[0];
const root = argv._[command ? 1 : 0];
if (root) {
argv.root = root;
}主流程摘要
ts
if (!command || command === "dev") {
const createDevServer = async () => {
const server = await createServer(root, argv, async () => {
await server.close();
await createDevServer();
});
await server.listen();
logVersion(server.config.logger);
server.printUrls();
};
createDevServer().catch((err) => {});
} else {
logVersion();
if (command === "build") {
build(root, argv).catch((err) => {
createLogger().error(`${c.red(`build error:`)}\n${err.stack}`);
process.exit(1);
});
} else if (command === "serve" || command === "preview") {
serve(argv).catch((err) => {
createLogger().error(
`${c.red(`failed to start server. error:`)}\n${err.stack}`
);
process.exit(1);
});
} else if (command === "init") {
init();
} else {
createLogger().error(c.red(`unknown command "${command}".`));
process.exit(1);
}
}dev-createServer 创建服务
- 实际上,
vitepress最后用到的都是vite的api。 - 如
createViteServer就是 vite/createServer
ts
export async function createServer(
root: string = process.cwd(),
serverOptions: ServerOptions = {},
recreateServer?: () => Promise<void>
) {
const config = await resolveConfig(root) {
// async function resolveConfig(
// root: string = process.cwd(),
// command: 'serve' | 'build' = 'serve',
// mode = 'development'
// )
// normalizePath来自vite
root = normalizePath(path.resolve(root))
// 解析用户配置
// const [userConfig, configPath, configDeps] =
await resolveUserConfig(
root,
command,
mode
) {
// 加载用户配置
const configPath = supportedConfigExtensions.flatMap((ext) => [
resolve(root, `config/index.${ext}`),
resolve(root, `config.${ext}`)
]).find(fs.pathExistsSync)
let userConfig: RawConfigExports = {}
let configDeps: string[] = []
if (!configPath) {
debug(`no config file found.`)
} else {
// 获取配置导出信息。loadConfigFromFile来自vite
const configExports = await loadConfigFromFile(
{ command, mode },
configPath,
root
)
// 分配配置信息和依赖
if (configExports) {
userConfig = configExports.config
configDeps = configExports.dependencies.map((file) =>
normalizePath(path.resolve(file))
)
}
}
return [await resolveConfigExtends(userConfig), configPath, configDeps]
}
// 解析、格式化站点配置数据
const site = await resolveSiteData(root, userConfig) {
userConfig = userConfig || (await resolveUserConfig(root, command, mode))[0]
return {
lang: userConfig.lang || 'en-US',
dir: userConfig.dir || 'ltr',
title: userConfig.title || 'VitePress',
titleTemplate: userConfig.titleTemplate,
description: userConfig.description || 'A VitePress site',
base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/',
head: resolveSiteDataHead(userConfig),
appearance: userConfig.appearance ?? true,
themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {},
scrollOffset: userConfig.scrollOffset || 90,
cleanUrls: !!userConfig.cleanUrls
}
}
const srcDir = normalizePath(path.resolve(root, userConfig.srcDir || '.'))
const outDir = ...
const cacheDir = ...
// 解析主题
const userThemeDir = resolve(root, 'theme')
const themeDir = (await fs.pathExists(userThemeDir)) ? userThemeDir : DEFAULT_THEME_PATH
// 解析页面、路由
const { pages, dynamicRoutes, rewrites } = await resolvePages(srcDir, userConfig) {
const allMarkdownFiles = (await fg(['**.md'], {
cwd: srcDir,
ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
})
).sort()
const pages: string[] = []
const dynamicRouteFiles: string[] = []
allMarkdownFiles.forEach((file) => {
dynamicRouteRE.lastIndex = 0
;(dynamicRouteRE.test(file) ? dynamicRouteFiles : pages).push(file)
})
const dynamicRoutes = await resolveDynamicRoutes(srcDir, dynamicRouteFiles)
pages.push(...dynamicRoutes.routes.map((r) => r.path))
const rewrites = resolveRewrites(pages, userConfig.rewrites)
return { pages, dynamicRoutes, rewrites }
}
const config: SiteConfig = {
root,
srcDir,
site,
themeDir,
pages,
dynamicRoutes,
...
}
global.VITEPRESS_CONFIG = config
return config
}
// createViteServer来自vite
return createViteServer({
root: config.srcDir,
base: config.site.base,
cacheDir: config.cacheDir,
plugins: await createVitePressPlugin(config, false, {}, {}, recreateServer) {
const { vue: userVuePluginOptions } = siteConfig
let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
const vuePlugin = await import('@vitejs/plugin-vue').then((r) =>
r.default({
include: [/\.vue$/, /\.md$/],
...userVuePluginOptions
})
)
// 插件,详见markdown 转 html 插件
const vitePressPlugin: Plugin = {
name: 'vitepress',
...
}
return { vitePressPlugin, vuePlugin }
},
server: serverOptions,
customLogger: config.logger,
});
}build-renderPage
最终的 build 来自 vite。【vite 的 build】
ts
export async function build(
root?: string,
buildOptions: BuildOptions & { base?: string; mpa?: string } = {}
) {
process.env.NODE_ENV = "production";
const siteConfig = await resolveConfig(root, "build", "production");
try {
const { clientResult, serverResult, pageToHashMap } = await bundle(
siteConfig,
buildOptions
) {
const input: Record<string, string> = {}
config.pages.forEach((file) => {
const alias = config.rewrites.map[file] || file
input[slash(alias).replace(/\//g, '_')] = path.resolve(config.srcDir, file)
}
const resolveViteConfig = async (ssr: boolean) => {
root: config.srcDir,
build: {
rollupOptions: {
input: {
...input,
app: path.resolve(APP_PATH, ssr ? 'ssr.js' : 'index.js')
},
output: {
assetFileNames: 'assets/[name].[hash].[ext]',
}
}
}
...
}
try {
[clientResult, serverResult] = await (Promise.all([
// build来自vite
config.mpa ? null : build(await resolveViteConfig(false)),
build(await resolveViteConfig(true))
]) as Promise<[RollupOutput, RollupOutput]>)
} catch (e) {}
return { clientResult, serverResult, pageToHashMap }
}
const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).toString());
try {
const appChunk = xxx
const cssChunk = (siteConfig.mpa ? serverResult : clientResult).output.find(...)
const assets = (siteConfig.mpa ? serverResult : clientResult).output.filter(...)
if (isDefaultTheme) {...}
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
const siteDataString = xxx
await Promise.all(
["404.md", ...siteConfig.pages]
.map((page) => siteConfig.rewrites.map[page] || page)
.map((page) =>
renderPage(render,siteConfig,page,clientResult,appChunk,
cssChunk,assets,pageToHashMap,hashMapString,
siteDataString,additionalHeadTags
) {
// 提取title、head、link等html页面信息
const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath)
const pageName = sanitizeFileName(page.replace(/\//g, '_'))
let pageData: PageData
try {
const { __pageData } = await import(
pathToFileURL(path.join(config.tempDir, pageServerJsFileName)).toString()
)
pageData = __pageData
} catch (e) {}
const title: string = createTitle(siteData, pageData)
let preloadLinks = xxx
const head = mergeHead(...)
// 构建、填充 html 字符串
const html = `<!DOCTYPE html><html lang="${siteData.lang}"xxx</html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
// 确认目录存在
await fs.ensureDir(path.dirname(htmlFileName))
// 文件写入
await fs.writeFile(htmlFileName, transformedHtml || html)
}
)
);
} catch (e) {}
} finally {
// 删除临时文件目录
if (!process.env.DEBUG) {
fs.rmSync(siteConfig.tempDir, { recursive: true, force: true });
}
}
// buildEnd 钩子
await siteConfig.buildEnd?.(siteConfig);
siteConfig.logger.info(
`build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.`
);
}markdown 内容转 vue 插件
markdown-it插件将 markdown 内容转换成 vue 类型信息。
ts
const vitePressPlugin: Plugin = {
name: "vitepress",
async configResolved(resolvedConfig) {
config = resolvedConfig;
markdownToVue = await createMarkdownToVueRenderFn(
srcDir,
markdown,
pages,
config.define,
config.command === "build",
config.base,
lastUpdated,
cleanUrls,
siteConfig
) {
const md = await createMarkdownRenderer(srcDir,options,base,siteConfig?.logger) {
const theme = options.theme ?? 'material-theme-palenight'
const md = MarkdownIt({ html: true, linkify: true, ... })
md.linkify.set({ fuzzyLink: false })
md.use(xxxPlugin).use(xxxPlugin)
}
pages = pages.map((p) => slash(p.replace(/\.md$/, '')))
return async (
src: string,
file: string,
publicDir: string
): Promise<MarkdownCompileResult> => {
const fileOrig = file
const html = md.render(src, env)
const result = {...}
return result
}
}
},
async 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 } = await markdownToVue(
code, id, config.publicDir )
allDeadLinks.push(...deadLinks)
if (includes.length) {
includes.forEach((i) => {
this.addWatchFile(i)
})
}
return processClientJS(vueSrc, id)
}
},
};辅助信息集锦
ts
async function resolveConfigExtends(
config: RawConfigExports
): Promise<UserConfig> {
const resolved = await (typeof config === "function" ? config() : config);
if (resolved.extends) {
const base = await resolveConfigExtends(resolved.extends);
return mergeConfig(base, resolved);
}
return resolved;
}