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
|
#!/usr/bin/env node
|
||||||
const chalk = require('chalk')
|
require('../dist/node/cli')
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
@ -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, ServerOptions } from 'vite'
|
||||||
import {
|
import { resolveConfig } from './config'
|
||||||
createServer as createViteServer,
|
import { createVitePressPlugin } from './plugin'
|
||||||
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'
|
|
||||||
|
|
||||||
const debug = require('debug')('vitepress:serve')
|
export async function createServer(
|
||||||
const debugHmr = require('debug')('vitepress:hmr')
|
root: string = process.cwd(),
|
||||||
|
serverOptions: ServerOptions = {}
|
||||||
function createVitePressPlugin({
|
) {
|
||||||
configPath,
|
const config = await resolveConfig(root)
|
||||||
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)
|
|
||||||
|
|
||||||
return createViteServer({
|
return createViteServer({
|
||||||
...options,
|
root,
|
||||||
configureServer: createVitePressPlugin(config),
|
// logLevel: 'warn',
|
||||||
resolvers: [config.resolver]
|
plugins: createVitePressPlugin(root, config),
|
||||||
|
server: serverOptions
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in new issue