wip: dev works with vite 2

pull/198/head
Evan You 4 years ago
parent f05fe8b7f6
commit bf8fd3a6f3

@ -20,7 +20,7 @@ Now, with Vite and Vue 3, it is time to rethink what a "Vue-powered static site
## Improvements Over VuePress
There're couple of things that are improved from VuePress.
There're couple of things that are improved from VuePress....
### It Uses Vue 3

@ -29,6 +29,7 @@
"release": "node scripts/release.js",
"docs": "run-p dev docs-dev",
"docs-dev": "node ./bin/vitepress dev docs",
"debug": "node --inspect-brk ./bin/vitepress dev docs",
"docs-build": "yarn build && node ./bin/vitepress build docs",
"docs-serve": "yarn docs-build && node ./bin/vitepress serve --root docs"
},
@ -64,8 +65,8 @@
"dependencies": {
"@docsearch/css": "^1.0.0-alpha.28",
"@docsearch/js": "^1.0.0-alpha.28",
"@vue/compiler-sfc": "^3.0.3",
"@vue/server-renderer": "^3.0.3",
"@vue/compiler-sfc": "^3.0.4",
"@vue/server-renderer": "^3.0.4",
"chalk": "^4.1.0",
"debug": "^4.1.1",
"diacritics": "^1.3.0",
@ -83,8 +84,8 @@
"ora": "^5.1.0",
"prismjs": "^1.20.0",
"slash": "^3.0.0",
"vite": "^1.0.0-rc.13",
"vue": "^3.0.3"
"vite": "^2.0.0-alpha.1",
"vue": "^3.0.4"
},
"devDependencies": {
"@types/fs-extra": "^9.0.1",

@ -16,7 +16,7 @@ function parse(data: string): SiteData {
// hmr
if (import.meta.hot) {
import.meta.hot!.acceptDeps('/@siteData', (m) => {
import.meta.hot!.accept('/@siteData', (m) => {
siteDataRef.value = parse(m.default)
})
}

@ -1,6 +1,6 @@
import fs from 'fs-extra'
import { bundle, okMark, failMark } from './bundle'
import { BuildConfig as ViteBuildOptions } from 'vite'
import { BuildOptions as ViteBuildOptions } from 'vite'
import { resolveConfig } from '../config'
import { renderPage } from './render'
import { OutputChunk, OutputAsset } from 'rollup'

@ -6,12 +6,7 @@ import { BuildOptions } from './build'
import { resolveUserConfig, SiteConfig } from '../config'
import { Plugin, OutputAsset, OutputChunk } from 'rollup'
import { createMarkdownToVueRenderFn } from '../markdownToVue'
import {
build,
ssrBuild,
BuildConfig as ViteBuildOptions,
BuildResult
} from 'vite'
import { build, BuildOptions as ViteBuildOptions } from 'vite'
import ora from 'ora'
export const okMark = '\x1b[32m✓\x1b[0m'
@ -60,9 +55,7 @@ export async function bundle(
// compile md into vue src
if (id.endsWith('.md')) {
const content = await fs.readFile(id, 'utf-8')
// TODO use git timestamp
const lastUpdated = (await fs.stat(id)).mtimeMs
const { vueSrc } = markdownToVue(content, id, lastUpdated)
const { vueSrc } = markdownToVue(content, id)
return vueSrc
}
},

@ -14,11 +14,12 @@ if (root) {
}
if (!command || command === 'dev') {
const port = argv.port || 3000
createServer(argv)
createServer(root, argv)
.then((server) => {
server.listen(port, () => {
console.log(`listening at http://localhost:${port}`)
return server.listen().then(() => {
console.log(
`listening at http://localhost:${server.config.server.port}`
)
})
})
.catch((err) => {

@ -2,10 +2,10 @@ import path from 'path'
import fs from 'fs-extra'
import chalk from 'chalk'
import globby from 'globby'
import { createResolver, APP_PATH, DEFAULT_THEME_PATH } from './resolver'
import { Resolver } from 'vite'
import { APP_PATH, createAlias, DEFAULT_THEME_PATH } from './resolver'
import { SiteData, HeadConfig, LocaleConfig } from '../../types/shared'
import { MarkdownOptions } from './markdown/markdown'
import { AliasOptions } from 'vite'
export { resolveSiteDataByRoute } from './shared/config'
const debug = require('debug')('vitepress:config')
@ -30,7 +30,7 @@ export interface SiteConfig<ThemeConfig = any> {
themeDir: string
outDir: string
tempDir: string
resolver: Resolver
aliases: AliasOptions
pages: string[]
markdown?: MarkdownOptions
}
@ -58,8 +58,8 @@ export async function resolveConfig(
configPath: resolve(root, 'config.js'),
outDir: resolve(root, 'dist'),
tempDir: path.resolve(APP_PATH, 'temp'),
resolver: createResolver(themeDir, userConfig),
markdown: userConfig.markdown
markdown: userConfig.markdown,
aliases: createAlias(themeDir, userConfig)
}
return config

@ -1,12 +1,19 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import LRUCache from 'lru-cache'
import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown'
import { deeplyParseHeader } from './utils/parseHeader'
import { PageData, HeadConfig } from '../../types/shared'
import slash from 'slash'
const debug = require('debug')('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
const cache = new LRUCache<string, MarkdownCompileCachedResult>({ max: 1024 })
interface MarkdownCompileCachedResult extends MarkdownCompileResult {
tagsWithPageData: string
tagsWithoutPageData: string
}
interface MarkdownCompileResult {
vueSrc: string
@ -22,44 +29,63 @@ export function createMarkdownToVueRenderFn(
return (
src: string,
file: string,
lastUpdated: number,
injectData = true
) => {
file = path.relative(root, file)
): MarkdownCompileResult => {
const relativePath = slash(path.relative(root, file))
const cached = cache.get(src)
if (cached) {
debug(`[cache hit] ${file}`)
return cached
debug(`[cache hit] ${relativePath}`)
return pickResult(cached, injectData)
}
const start = Date.now()
const { content, data: frontmatter } = matter(src)
const { html, data } = md.render(content)
// TODO validate data.links?
const vueSrc = `\n<template><div>${html}</div></template>`
// inject page data
// TODO validate data.links?
const pageData: PageData = {
title: inferTitle(frontmatter, content),
description: inferDescription(frontmatter),
frontmatter,
headers: data.headers,
relativePath: file.replace(/\\/g, '/'),
lastUpdated
relativePath,
// TODO use git timestamp?
lastUpdated: fs.statSync(file).mtimeMs
}
const additionalBlocks = injectData
? injectPageData(data.hoistedTags || [], pageData)
: data.hoistedTags || []
const tagsWithPageData = genPageDataCode(
data.hoistedTags || [],
pageData
).join('\n')
const vueSrc =
additionalBlocks.join('\n') + `\n<template><div>${html}</div></template>`
const tagsWithoutPageData = (data.hoistedTags || []).join('\n')
debug(`[render] ${file} in ${Date.now() - start}ms.`)
const result = { vueSrc, pageData }
const result = {
vueSrc,
pageData,
tagsWithPageData,
tagsWithoutPageData
}
cache.set(src, result)
return result
return pickResult(result, injectData)
}
}
function pickResult(
res: MarkdownCompileCachedResult,
injectData: boolean
): MarkdownCompileResult {
return {
vueSrc:
res.vueSrc +
(injectData ? res.tagsWithPageData : res.tagsWithoutPageData),
pageData: res.pageData
}
}
@ -68,7 +94,7 @@ const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/
function injectPageData(tags: string[], data: PageData) {
function genPageDataCode(tags: string[], data: PageData) {
const code = `\nexport const __pageData = ${JSON.stringify(
JSON.stringify(data)
)}`

@ -0,0 +1,107 @@
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 './resolver'
import createVuePlugin from '@vitejs/plugin-vue'
import slash from 'slash'
export function createVitePressPlugin(
root: string,
{ configPath, aliases, markdown, site: initialSiteData }: SiteConfig
): Plugin[] {
const markdownToVue = createMarkdownToVueRenderFn(root, markdown)
const vuePlugin = createVuePlugin({
include: [/\.vue$/, /\.md$/]
})
let siteData = initialSiteData
let stringifiedData = JSON.stringify(JSON.stringify(initialSiteData))
const vitePressPlugin: Plugin = {
name: 'vitepress',
config() {
return {
alias: aliases,
transformInclude: /\.md$/
}
},
resolveId(id) {
if (id === SITE_DATA_REQUEST_PATH) {
return SITE_DATA_REQUEST_PATH
}
},
load(id) {
if (id === SITE_DATA_REQUEST_PATH) {
return `export default ${stringifiedData}`
}
},
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
const indexPath = `/@fs/${path.join(APP_PATH, 'index.html')}`
return () => {
server.app.use((req, _, next) => {
if (req.url!.endsWith('.html')) {
req.url = indexPath
}
next()
})
}
},
async handleHotUpdate(file, mods, read, server) {
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
return
}
// hot reload .md files as .vue files
if (file.endsWith('.md')) {
const content = await read()
const { pageData, vueSrc } = markdownToVue(
content.toString(),
file,
// do not inject pageData on HMR
// it leads to plugin-vue 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
server.ws.send({
type: 'custom',
event: 'vitepress:pageData',
data: {
path: `/${slash(path.relative(root, file))}`,
pageData
}
})
// reload the content component
return vuePlugin.handleHotUpdate!(file, mods, () => vueSrc, server)
}
}
}
return [vitePressPlugin, vuePlugin]
}

@ -1,5 +1,5 @@
import path from 'path'
import { Resolver } from 'vite'
import { AliasOptions } from 'vite'
import { UserConfig } from './config'
export const APP_PATH = path.join(__dirname, '../client/app')
@ -20,29 +20,17 @@ export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
// 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(
export function createAlias(
themeDir: string,
userConfig: UserConfig
): Resolver {
): AliasOptions {
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
}
}
...userConfig.alias,
'/@app': APP_PATH,
'/@theme': themeDir,
'/@default-theme': DEFAULT_THEME_PATH,
'/@shared': SHARED_PATH,
vitepress: `${APP_PATH}/exports.js`,
[SITE_DATA_ID]: 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
})
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save