mirror of https://github.com/vuejs/vitepress
commit
07d227b5d2
@ -0,0 +1,33 @@
|
||||
import { deeplyParseHeader } from 'node/utils/parseHeader'
|
||||
|
||||
test('deeplyParseHeader', () => {
|
||||
const asserts: Record<string, string> = {
|
||||
// Remove tail html
|
||||
'# `H1` <Comp></Comp>': '# H1',
|
||||
'# *H1* <Comp/>': '# H1',
|
||||
|
||||
// Reserve code-wrapped tail html
|
||||
'# `H1` `<Comp></Comp>`': '# H1 <Comp></Comp>',
|
||||
'# *H1* `<Comp/>`': '# H1 <Comp/>',
|
||||
|
||||
// Remove leading html
|
||||
'# <Comp></Comp> `H1`': '# H1',
|
||||
'# <Comp/> *H1*': '# H1',
|
||||
|
||||
// Reserve code-wrapped leading html
|
||||
'# `<Comp></Comp>` `H1`': '# <Comp></Comp> H1',
|
||||
'# `<Comp/>` *H1*': '# <Comp/> H1',
|
||||
|
||||
// Remove middle html
|
||||
'# `H1` <Comp></Comp> `H2`': '# H1 H2',
|
||||
'# `H1` <Comp/> `H2`': '# H1 H2',
|
||||
|
||||
// Reserve middle html
|
||||
'# `H1` `<Comp></Comp>` `H2`': '# H1 <Comp></Comp> H2',
|
||||
'# `H1` `<Comp/>` `H2`': '# H1 <Comp/> H2'
|
||||
}
|
||||
|
||||
Object.keys(asserts).forEach((input) => {
|
||||
expect(deeplyParseHeader(input)).toBe(asserts[input])
|
||||
})
|
||||
})
|
@ -0,0 +1,38 @@
|
||||
import { parseHeader } from 'node/utils/parseHeader'
|
||||
|
||||
describe('parseHeader', () => {
|
||||
test('should unescape html', () => {
|
||||
const input = `<div :id="'app'">`
|
||||
expect(parseHeader(input)).toBe(`<div :id="'app'">`)
|
||||
})
|
||||
|
||||
test('should remove markdown tokens correctly', () => {
|
||||
const asserts: Record<string, string> = {
|
||||
// vuepress #238
|
||||
'[vue](vuejs.org)': 'vue',
|
||||
'`vue`': 'vue',
|
||||
'*vue*': 'vue',
|
||||
'**vue**': 'vue',
|
||||
'***vue***': 'vue',
|
||||
_vue_: 'vue',
|
||||
'\\_vue\\_': '_vue_',
|
||||
'\\*vue\\*': '*vue*',
|
||||
'\\!vue\\!': '!vue!',
|
||||
|
||||
// vuepress #2688
|
||||
'[vue](vuejs.org) / [vue](vuejs.org)': 'vue / vue',
|
||||
'[\\<ins>](vuejs.org)': '<ins>',
|
||||
|
||||
// vuepress #564 For multiple markdown tokens
|
||||
'`a` and `b`': 'a and b',
|
||||
'***bold and italic***': 'bold and italic',
|
||||
'**bold** and *italic*': 'bold and italic',
|
||||
|
||||
// escaping \$
|
||||
'\\$vue': '$vue'
|
||||
}
|
||||
Object.keys(asserts).forEach((input) => {
|
||||
expect(parseHeader(input)).toBe(asserts[input])
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,56 @@
|
||||
import { removeNonCodeWrappedHTML } from 'node/utils/parseHeader'
|
||||
|
||||
test('removeNonCodeWrappedHTML', () => {
|
||||
const asserts: Record<string, string> = {
|
||||
// Remove tail html
|
||||
'# H1 <Comp></Comp>': '# H1 ',
|
||||
'# H1<Comp></Comp>': '# H1',
|
||||
'# H1 <Comp a="b"></Comp>': '# H1 ',
|
||||
'# H1<Comp a="b"></Comp>': '# H1',
|
||||
'# H1 <Comp/>': '# H1 ',
|
||||
'# H1<Comp/>': '# H1',
|
||||
'# H1 <Comp a="b"/>': '# H1 ',
|
||||
'# H1<Comp a="b"/>': '# H1',
|
||||
|
||||
// Reserve code-wrapped tail html
|
||||
'# H1 `<Comp></Comp>`': '# H1 `<Comp></Comp>`',
|
||||
'# H1 `<Comp a="b"></Comp>`': '# H1 `<Comp a="b"></Comp>`',
|
||||
'# H1 `<Comp/>`': '# H1 `<Comp/>`',
|
||||
'# H1 `<Comp a="b"/>`': '# H1 `<Comp a="b"/>`',
|
||||
|
||||
// Remove leading html
|
||||
'# <Comp></Comp> H1': '# H1',
|
||||
'# <Comp></Comp>H1': '# H1',
|
||||
'# <Comp a="b"></Comp> H1': '# H1',
|
||||
'# <Comp a="b"></Comp>H1': '# H1',
|
||||
'# <Comp/> H1': '# H1',
|
||||
'# <Comp/>H1': '# H1',
|
||||
'# <Comp a="b"/> H1': '# H1',
|
||||
'# <Comp a="b"/>H1': '# H1',
|
||||
|
||||
// Reserve code-wrapped leading html
|
||||
'# `<Comp></Comp>` H1': '# `<Comp></Comp>` H1',
|
||||
'# `<Comp a="b"></Comp>` H1': '# `<Comp a="b"></Comp>` H1',
|
||||
'# `<Comp/>` H1': '# `<Comp/>` H1',
|
||||
'# `<Comp a="b"/>` H1': '# `<Comp a="b"/>` H1',
|
||||
|
||||
// Remove middle html
|
||||
'# H1 <Comp></Comp> H2': '# H1 H2',
|
||||
'# H1 <Comp a="b"></Comp> H2': '# H1 H2',
|
||||
'# H1 <Comp/> H2': '# H1 H2',
|
||||
'# H1 <Comp a="b"/> H2': '# H1 H2',
|
||||
|
||||
// Reserve code-wrapped middle html
|
||||
'# H1 `<Comp></Comp>` H2': '# H1 `<Comp></Comp>` H2',
|
||||
'# H1 `<Comp a="b"></Comp>` H2': '# H1 `<Comp a="b"></Comp>` H2',
|
||||
'# H1 `<Comp/>` H2': '# H1 `<Comp/>` H2',
|
||||
'# H1 `<Comp a="b"/>` H2': '# H1 `<Comp a="b"/>` H2',
|
||||
|
||||
// vuepress #2688
|
||||
'# \\<ins>': '# \\<ins>'
|
||||
}
|
||||
|
||||
Object.keys(asserts).forEach((input) => {
|
||||
expect(removeNonCodeWrappedHTML(input)).toBe(asserts[input])
|
||||
})
|
||||
})
|
@ -1,44 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
const chalk = require('chalk')
|
||||
const argv = require('minimist')(process.argv.slice(2))
|
||||
|
||||
console.log(chalk.cyan(`vitepress v${require('../package.json').version}`))
|
||||
console.log(chalk.cyan(`vite v${require('vite/package.json').version}`))
|
||||
|
||||
const command = argv._[0]
|
||||
const root = argv._[command ? 1 : 0]
|
||||
if (root) {
|
||||
argv.root = root
|
||||
}
|
||||
|
||||
if (!command || command === 'dev') {
|
||||
const port = argv.port || 3000
|
||||
require('../dist/node')
|
||||
.createServer(argv)
|
||||
.then((server) => {
|
||||
server.listen(port, () => {
|
||||
console.log(`listening at http://localhost:${port}`)
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(chalk.red(`failed to start server. error:\n`), err)
|
||||
process.exit(1)
|
||||
})
|
||||
} else if (command === 'build') {
|
||||
require('../dist/node')
|
||||
.build(argv)
|
||||
.catch((err) => {
|
||||
console.error(chalk.red(`build error:\n`), err)
|
||||
process.exit(1)
|
||||
})
|
||||
} else if (command === 'serve') {
|
||||
require('../dist/node')
|
||||
.serve(argv)
|
||||
.catch((err) => {
|
||||
console.error(chalk.red(`failed to start server. error:\n`), err)
|
||||
process.exit(1)
|
||||
})
|
||||
} else {
|
||||
console.log(chalk.red(`unknown command "${command}".`))
|
||||
process.exit(1)
|
||||
}
|
||||
require('../dist/node/cli')
|
||||
|
@ -0,0 +1,3 @@
|
||||
/assets/*
|
||||
cache-control: max-age=31536000
|
||||
cache-control: immutable
|
@ -1,29 +0,0 @@
|
||||
// exports in this file are exposed to themes and md files via 'vitepress'
|
||||
// so the user can do `import { useRoute, useSiteData } from 'vitepress'`
|
||||
|
||||
// generic types
|
||||
export type { Router, Route } from './router'
|
||||
|
||||
// theme types
|
||||
export * from './theme'
|
||||
|
||||
// composables
|
||||
export { useRouter, useRoute } from './router'
|
||||
export { useSiteData } from './composables/siteData'
|
||||
export { useSiteDataByRoute } from './composables/siteDataByRoute'
|
||||
export { usePageData } from './composables/pageData'
|
||||
export { useFrontmatter } from './composables/frontmatter'
|
||||
|
||||
// utilities
|
||||
export { inBrowser, joinPath } from './utils'
|
||||
|
||||
// components
|
||||
export { Content } from './components/Content'
|
||||
|
||||
import { ComponentOptions } from 'vue'
|
||||
import _Debug from './components/Debug.vue'
|
||||
const Debug = _Debug as ComponentOptions
|
||||
export { Debug }
|
||||
|
||||
// default theme
|
||||
export { default as defaultTheme } from '/@default-theme/index'
|
@ -1,2 +0,0 @@
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/@app/index.js"></script>
|
@ -0,0 +1,29 @@
|
||||
// exports in this file are exposed to themes and md files via 'vitepress'
|
||||
// so the user can do `import { useRoute, useSiteData } from 'vitepress'`
|
||||
|
||||
// generic types
|
||||
export type { Router, Route } from './app/router'
|
||||
|
||||
// theme types
|
||||
export * from './app/theme'
|
||||
|
||||
// composables
|
||||
export { useRouter, useRoute } from './app/router'
|
||||
export { useSiteData } from './app/composables/siteData'
|
||||
export { useSiteDataByRoute } from './app/composables/siteDataByRoute'
|
||||
export { usePageData } from './app/composables/pageData'
|
||||
export { useFrontmatter } from './app/composables/frontmatter'
|
||||
|
||||
// utilities
|
||||
export { inBrowser, joinPath } from './app/utils'
|
||||
|
||||
// components
|
||||
export { Content } from './app/components/Content'
|
||||
|
||||
import { ComponentOptions } from 'vue'
|
||||
import _Debug from './app/components/Debug.vue'
|
||||
const Debug = _Debug as ComponentOptions
|
||||
export { Debug }
|
||||
|
||||
// default theme
|
||||
export { default as DefaultTheme } from './theme-default'
|
@ -0,0 +1,54 @@
|
||||
import path from 'path'
|
||||
import { Alias, AliasOptions } from 'vite'
|
||||
import { UserConfig } from './config'
|
||||
|
||||
const PKG_ROOT = path.join(__dirname, '../../')
|
||||
export const APP_PATH = path.join(__dirname, '../client/app')
|
||||
export const SHARED_PATH = path.join(__dirname, '../client/shared')
|
||||
export const DEFAULT_THEME_PATH = path.join(
|
||||
__dirname,
|
||||
'../client/theme-default'
|
||||
)
|
||||
|
||||
// special virtual file
|
||||
// we can't directly import '/@siteData' because
|
||||
// - it's not an actual file so we can't use tsconfig paths to redirect it
|
||||
// - TS doesn't allow shimming a module that starts with '/'
|
||||
export const SITE_DATA_ID = '@siteData'
|
||||
export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
|
||||
|
||||
export function resolveAliases(
|
||||
root: string,
|
||||
themeDir: string,
|
||||
userConfig: UserConfig
|
||||
): AliasOptions {
|
||||
const paths: Record<string, string> = {
|
||||
...userConfig.alias,
|
||||
'/@theme': themeDir,
|
||||
'/@shared': SHARED_PATH,
|
||||
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
|
||||
}
|
||||
|
||||
const aliases: Alias[] = [
|
||||
...Object.keys(paths).map((p) => ({
|
||||
find: p,
|
||||
replacement: paths[p]
|
||||
})),
|
||||
{
|
||||
find: /^vitepress$/,
|
||||
replacement: path.join(__dirname, '../client/index')
|
||||
},
|
||||
// alias for local linked development
|
||||
{ find: /^vitepress\//, replacement: PKG_ROOT + '/' },
|
||||
// make sure it always use the same vue dependency that comes with
|
||||
// vitepress itself
|
||||
{
|
||||
find: /^vue$/,
|
||||
replacement: require.resolve(
|
||||
'@vue/runtime-dom/dist/runtime-dom.esm-bundler.js'
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return aliases
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import chalk from 'chalk'
|
||||
import minimist from 'minimist'
|
||||
import { createServer, build, serve } from '.'
|
||||
|
||||
const argv: any = minimist(process.argv.slice(2))
|
||||
|
||||
console.log(chalk.cyan(`vitepress v${require('../../package.json').version}`))
|
||||
console.log(chalk.cyan(`vite v${require('vite/package.json').version}`))
|
||||
|
||||
const command = argv._[0]
|
||||
const root = argv._[command ? 1 : 0]
|
||||
if (root) {
|
||||
argv.root = root
|
||||
}
|
||||
|
||||
if (!command || command === 'dev') {
|
||||
createServer(root, argv)
|
||||
.then((server) => server.listen())
|
||||
.catch((err) => {
|
||||
console.error(chalk.red(`failed to start server. error:\n`), err)
|
||||
process.exit(1)
|
||||
})
|
||||
} else if (command === 'build') {
|
||||
build(root, argv).catch((err) => {
|
||||
console.error(chalk.red(`build error:\n`), err)
|
||||
process.exit(1)
|
||||
})
|
||||
} else if (command === 'serve') {
|
||||
serve(argv).catch((err) => {
|
||||
console.error(chalk.red(`failed to start server. error:\n`), err)
|
||||
process.exit(1)
|
||||
})
|
||||
} else {
|
||||
console.log(chalk.red(`unknown command "${command}".`))
|
||||
process.exit(1)
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
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 './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, themeDir, site }: SiteConfig,
|
||||
ssr = false,
|
||||
pageToHashMap?: Record<string, string>
|
||||
): Plugin[] {
|
||||
const markdownToVue = createMarkdownToVueRenderFn(root, markdown)
|
||||
|
||||
const vuePlugin = createVuePlugin({
|
||||
include: [/\.vue$/, /\.md$/],
|
||||
ssr
|
||||
})
|
||||
|
||||
let siteData = site
|
||||
|
||||
const vitePressPlugin: Plugin = {
|
||||
name: 'vitepress',
|
||||
|
||||
config() {
|
||||
return {
|
||||
alias: aliases,
|
||||
transformInclude: /\.md$/,
|
||||
define: {
|
||||
__CARBON__: !!site.themeConfig.carbonAds?.carbon,
|
||||
__BSA__: !!site.themeConfig.carbonAds?.custom,
|
||||
__ALGOLIA__: !!site.themeConfig.algolia
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
resolveId(id) {
|
||||
if (id === SITE_DATA_REQUEST_PATH) {
|
||||
return SITE_DATA_REQUEST_PATH
|
||||
}
|
||||
},
|
||||
|
||||
load(id) {
|
||||
if (id === SITE_DATA_REQUEST_PATH) {
|
||||
return `export default ${JSON.stringify(JSON.stringify(siteData))}`
|
||||
}
|
||||
},
|
||||
|
||||
transform(code, id) {
|
||||
if (id.endsWith('.md')) {
|
||||
// transform .md files into vueSrc so plugin-vue can handle it
|
||||
return markdownToVue(code, id).vueSrc
|
||||
}
|
||||
},
|
||||
|
||||
configureServer(server) {
|
||||
// serve our index.html after vite history fallback
|
||||
return () => {
|
||||
// @ts-ignore
|
||||
server.app.use((req, res, next) => {
|
||||
if (req.url!.endsWith('.html')) {
|
||||
res.statusCode = 200
|
||||
res.end(
|
||||
`<div id="app"></div>\n` +
|
||||
`<script type="module" src="/@fs/${APP_PATH}/index.js"></script>`
|
||||
)
|
||||
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] = 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)
|
||||
|
||||
// notify the client to update page data
|
||||
server.ws.send({
|
||||
type: 'custom',
|
||||
event: 'vitepress:pageData',
|
||||
data: {
|
||||
path: `/${slash(path.relative(root, file))}`,
|
||||
pageData
|
||||
}
|
||||
})
|
||||
|
||||
// reload the content component
|
||||
return vuePlugin.handleHotUpdate!({
|
||||
...ctx,
|
||||
read: () => vueSrc
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [vitePressPlugin, vuePlugin]
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import path from 'path'
|
||||
import { Resolver } from 'vite'
|
||||
import { UserConfig } from './config'
|
||||
|
||||
export const APP_PATH = path.join(__dirname, '../client/app')
|
||||
export const SHARED_PATH = path.join(__dirname, '../client/shared')
|
||||
export const DEFAULT_THEME_PATH = path.join(
|
||||
__dirname,
|
||||
'../client/theme-default'
|
||||
)
|
||||
|
||||
// special virtual file
|
||||
// we can't directly import '/@siteData' because
|
||||
// - it's not an actual file so we can't use tsconfig paths to redirect it
|
||||
// - TS doesn't allow shimming a module that starts with '/'
|
||||
export const SITE_DATA_ID = '@siteData'
|
||||
export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
|
||||
|
||||
// this is a path resolver that is passed to vite
|
||||
// so that we can resolve custom requests that start with /@app or /@theme
|
||||
// we also need to map file paths back to their public served paths so that
|
||||
// vite HMR can send the correct update notifications to the client.
|
||||
export function createResolver(
|
||||
themeDir: string,
|
||||
userConfig: UserConfig
|
||||
): Resolver {
|
||||
return {
|
||||
alias: {
|
||||
...userConfig.alias,
|
||||
'/@app/': APP_PATH,
|
||||
'/@theme/': themeDir,
|
||||
'/@default-theme/': DEFAULT_THEME_PATH,
|
||||
'/@shared/': SHARED_PATH,
|
||||
vitepress: '/@app/exports.js',
|
||||
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
|
||||
},
|
||||
requestToFile(publicPath) {
|
||||
if (publicPath === SITE_DATA_REQUEST_PATH) {
|
||||
return SITE_DATA_REQUEST_PATH
|
||||
}
|
||||
},
|
||||
fileToRequest(filePath) {
|
||||
if (filePath === SITE_DATA_REQUEST_PATH) {
|
||||
return SITE_DATA_REQUEST_PATH
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,139 +1,17 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
createServer as createViteServer,
|
||||
cachedRead,
|
||||
ServerConfig,
|
||||
ServerPlugin
|
||||
} from 'vite'
|
||||
import { resolveConfig, SiteConfig, resolveSiteData } from './config'
|
||||
import { createMarkdownToVueRenderFn } from './markdownToVue'
|
||||
import { APP_PATH, SITE_DATA_REQUEST_PATH } from './resolver'
|
||||
import { existsSync } from 'fs'
|
||||
import { createServer as createViteServer, ServerOptions } from 'vite'
|
||||
import { resolveConfig } from './config'
|
||||
import { createVitePressPlugin } from './plugin'
|
||||
|
||||
const debug = require('debug')('vitepress:serve')
|
||||
const debugHmr = require('debug')('vitepress:hmr')
|
||||
|
||||
function createVitePressPlugin({
|
||||
configPath,
|
||||
markdown,
|
||||
site: initialSiteData
|
||||
}: SiteConfig): ServerPlugin {
|
||||
return ({ app, root, watcher, resolver }) => {
|
||||
const markdownToVue = createMarkdownToVueRenderFn(root, markdown)
|
||||
|
||||
// hot reload .md files as .vue files
|
||||
watcher.on('change', async (file) => {
|
||||
if (file.endsWith('.md')) {
|
||||
debugHmr(`reloading ${file}`)
|
||||
const content = await cachedRead(null, file)
|
||||
const timestamp = Date.now()
|
||||
const { pageData, vueSrc } = markdownToVue(
|
||||
content.toString(),
|
||||
file,
|
||||
timestamp,
|
||||
// do not inject pageData on HMR
|
||||
// it leads to vite to think <script> has changed and reloads the
|
||||
// component instead of re-rendering.
|
||||
// pageData needs separate HMR logic anyway (see below)
|
||||
false
|
||||
)
|
||||
|
||||
// notify the client to update page data
|
||||
watcher.send({
|
||||
type: 'custom',
|
||||
id: 'vitepress:pageData',
|
||||
customData: {
|
||||
path: resolver.fileToRequest(file),
|
||||
pageData
|
||||
}
|
||||
})
|
||||
|
||||
// reload the content component
|
||||
watcher.handleVueReload(file, timestamp, vueSrc)
|
||||
}
|
||||
})
|
||||
|
||||
// hot reload handling for siteData
|
||||
// the data is stringified twice so it is sent to the client as a string
|
||||
// it is then parsed on the client via JSON.parse() which is faster than
|
||||
// parsing the object literal as JavaScript.
|
||||
let siteData = initialSiteData
|
||||
let stringifiedData = JSON.stringify(JSON.stringify(initialSiteData))
|
||||
watcher.add(configPath)
|
||||
watcher.on('change', async (file) => {
|
||||
if (file === configPath) {
|
||||
const newData = await resolveSiteData(root)
|
||||
stringifiedData = JSON.stringify(JSON.stringify(newData))
|
||||
if (newData.base !== siteData.base) {
|
||||
console.warn(
|
||||
`[vitepress]: config.base has changed. Please restart the dev server.`
|
||||
)
|
||||
}
|
||||
siteData = newData
|
||||
watcher.handleJSReload(SITE_DATA_REQUEST_PATH)
|
||||
}
|
||||
})
|
||||
|
||||
// inject Koa middleware
|
||||
app.use(async (ctx, next) => {
|
||||
// serve siteData (which is a virtual file)
|
||||
if (ctx.path === SITE_DATA_REQUEST_PATH) {
|
||||
ctx.type = 'js'
|
||||
ctx.body = `export default ${stringifiedData}`
|
||||
debug(ctx.url)
|
||||
return
|
||||
}
|
||||
|
||||
// handle .md -> vue transforms
|
||||
if (ctx.path.endsWith('.md')) {
|
||||
const file = resolver.requestToFile(ctx.path)
|
||||
if (!existsSync(file)) {
|
||||
return next()
|
||||
}
|
||||
|
||||
await cachedRead(ctx, file)
|
||||
|
||||
// let vite know this is supposed to be treated as vue file
|
||||
ctx.vue = true
|
||||
|
||||
const { vueSrc, pageData } = markdownToVue(
|
||||
ctx.body,
|
||||
file,
|
||||
ctx.lastModified.getTime(),
|
||||
false
|
||||
)
|
||||
ctx.body = vueSrc
|
||||
debug(ctx.url, ctx.status)
|
||||
|
||||
await next()
|
||||
|
||||
// make sure this is the main <script> block
|
||||
if (!ctx.query.type) {
|
||||
// inject pageData to generated script
|
||||
ctx.body += `\nexport const __pageData = ${JSON.stringify(
|
||||
JSON.stringify(pageData)
|
||||
)}`
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await next()
|
||||
|
||||
// serve our index.html after vite history fallback
|
||||
if (ctx.url.endsWith('.html')) {
|
||||
await cachedRead(ctx, path.join(APP_PATH, 'index.html'))
|
||||
ctx.status = 200
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function createServer(options: ServerConfig = {}) {
|
||||
const config = await resolveConfig(options.root)
|
||||
export async function createServer(
|
||||
root: string = process.cwd(),
|
||||
serverOptions: ServerOptions = {}
|
||||
) {
|
||||
const config = await resolveConfig(root)
|
||||
|
||||
return createViteServer({
|
||||
...options,
|
||||
configureServer: createVitePressPlugin(config),
|
||||
resolvers: [config.resolver]
|
||||
root,
|
||||
// logLevel: 'warn',
|
||||
plugins: createVitePressPlugin(root, config),
|
||||
server: serverOptions
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in new issue