pull/1/head
Evan You 5 years ago
parent b94a0865b0
commit a512fc7925

@ -1,5 +1,5 @@
import { h } from 'vue' import { h } from 'vue'
import { useRoute } from '../composables/router' import { useRoute } from '../router'
export const Content = { export const Content = {
setup() { setup() {

@ -1,4 +1,4 @@
import { ref, provide, inject } from 'vue' import { inject } from 'vue'
/** /**
* @typedef {{ * @typedef {{
@ -11,24 +11,15 @@ import { ref, provide, inject } from 'vue'
/** /**
* @type {import('vue').InjectionKey<PageDataRef>} * @type {import('vue').InjectionKey<PageDataRef>}
*/ */
const pageDataSymbol = Symbol() export const pageDataSymbol = Symbol()
export function initPageData() {
const data = ref()
provide(pageDataSymbol, data)
return data
}
/** /**
* @returns {PageDataRef} * @returns {PageDataRef}
*/ */
export function usePageData() { export function usePageData() {
const data = inject(pageDataSymbol) const data = inject(pageDataSymbol)
if (__DEV__ && !data) { if (!data) {
throw new Error( throw new Error('usePageData() is called without provider.')
'usePageData() is called without initPageData() in an ancestor component.'
)
} }
// @ts-ignore
return data return data
} }

@ -2,4 +2,4 @@
// so the user can do `import { usePageData } from 'vitepress'` // so the user can do `import { usePageData } from 'vitepress'`
export { useSiteData } from './composables/siteData' export { useSiteData } from './composables/siteData'
export { usePageData } from './composables/pageData' export { usePageData } from './composables/pageData'
export { useRouter, useRoute } from './composables/router' export { useRouter, useRoute } from './router'

@ -1,8 +1,8 @@
import { createApp, h, readonly } from 'vue' import { createApp as createClientApp, createSSRApp, ref, readonly } from 'vue'
import { Content } from './components/Content' import { Content } from './components/Content'
import { initRouter } from './composables/router' import { createRouter, RouterSymbol } from './router'
import { useSiteData } from './composables/siteData' import { useSiteData } from './composables/siteData'
import { initPageData, usePageData } from './composables/pageData' import { usePageData, pageDataSymbol } from './composables/pageData'
import Theme from '/@theme/index' import Theme from '/@theme/index'
import { hot } from '@hmr' import { hot } from '@hmr'
@ -10,72 +10,80 @@ const inBrowser = typeof window !== 'undefined'
const NotFound = Theme.NotFound || (() => '404 Not Found') const NotFound = Theme.NotFound || (() => '404 Not Found')
const App = { export function createApp() {
setup() { const pageDataRef = ref()
const pageDataRef = initPageData()
initRouter((route) => { // hot reload pageData
let pagePath = route.path.replace(/\.html$/, '') if (__DEV__ && inBrowser) {
if (pagePath.endsWith('/')) { hot.on('vitepress:pageData', (data) => {
pagePath += 'index' if (
} data.path.replace(/\.md$/, '') ===
if (__DEV__) { location.pathname.replace(/\.html$/, '')
// awlays force re-fetch content in dev ) {
pagePath += `.md?t=${Date.now()}` pageDataRef.value = data.pageData
} else {
// in production, each .md file is built into a .md.js file following
// the path conversion scheme.
// /foo/bar.html -> /js/foo_bar.md.js
// TODO handle base
pagePath = `/js/${pagePath.slice(1).replace(/\//g, '_')}.md.js`
} }
})
}
if (inBrowser) { const router = createRouter((route) => {
// in browser: native dynamic import let pagePath = route.path.replace(/\.html$/, '')
return import(pagePath).then((m) => { if (pagePath.endsWith('/')) {
pageDataRef.value = readonly(m.__pageData) pagePath += 'index'
return m.default }
}) if (__DEV__) {
} else { // awlays force re-fetch content in dev
// SSR, sync require pagePath += `.md?t=${Date.now()}`
return require(pagePath).default } else {
} // in production, each .md file is built into a .md.js file following
}, NotFound) // the path conversion scheme.
// /foo/bar.html -> /js/foo_bar.md.js
// TODO handle base
pagePath = pagePath.slice(1).replace(/\//g, '_') + '.md.js'
}
if (__DEV__ && inBrowser) { if (inBrowser) {
// hot reload pageData // in browser: native dynamic import
hot.on('vitepress:pageData', (data) => { // js files are stored in a sub directory
if ( return import('./js/' + pagePath).then(page => {
data.path.replace(/\.md$/, '') === pageDataRef.value = readonly(page.__pageData)
location.pathname.replace(/\.html$/, '') return page.default
) {
pageDataRef.value = data.pageData
}
}) })
} else {
// SSR, sync require
const page = require('./' + pagePath)
console.log('setting page data')
pageDataRef.value = page.__pageData
return page.default
} }
}, NotFound)
return () => h(Theme.Layout) const app = __DEV__
} ? createClientApp(Theme.Layout)
} : createSSRApp(Theme.Layout)
// TODO use createSSRApp in production app.provide(RouterSymbol, router)
const app = createApp(App) app.provide(pageDataSymbol, pageDataRef)
app.component('Content', Content) app.component('Content', Content)
app.mixin({ app.mixin({
beforeCreate() { beforeCreate() {
const siteRef = useSiteData() const siteRef = useSiteData()
const pageRef = usePageData() const pageRef = usePageData()
Object.defineProperties(this, { Object.defineProperties(this, {
$site: { $site: {
get: () => siteRef.value get: () => siteRef.value
}, },
$page: { $page: {
get: () => pageRef.value get: () => pageRef.value
} }
}) })
} }
}) })
return { app, router }
}
app.mount('#app') if (inBrowser) {
createApp().app.mount('#app')
}

@ -1,4 +1,4 @@
import { reactive, provide, inject, nextTick, markRaw } from 'vue' import { reactive, inject, nextTick, markRaw } from 'vue'
/** /**
* @typedef {import('vue').Component} Component * @typedef {import('vue').Component} Component
@ -17,7 +17,7 @@ import { reactive, provide, inject, nextTick, markRaw } from 'vue'
/** /**
* @type {import('vue').InjectionKey<Router>} * @type {import('vue').InjectionKey<Router>}
*/ */
const RouterSymbol = Symbol() export const RouterSymbol = Symbol()
/** /**
* @returns {Route} * @returns {Route}
@ -32,7 +32,7 @@ const getDefaultRoute = () => ({
* @param {Component} [fallbackComponent] * @param {Component} [fallbackComponent]
* @returns {Router} * @returns {Router}
*/ */
export function initRouter(loadComponent, fallbackComponent) { export function createRouter(loadComponent, fallbackComponent) {
const route = reactive(getDefaultRoute()) const route = reactive(getDefaultRoute())
const inBrowser = typeof window !== 'undefined' const inBrowser = typeof window !== 'undefined'
@ -159,9 +159,9 @@ export function initRouter(loadComponent, fallbackComponent) {
go go
} }
provide(RouterSymbol, router) if (inBrowser) {
loadPage(location.href)
loadPage(location.href) }
return router return router
} }
@ -171,10 +171,8 @@ export function initRouter(loadComponent, fallbackComponent) {
*/ */
export function useRouter() { export function useRouter() {
const router = inject(RouterSymbol) const router = inject(RouterSymbol)
if (__DEV__ && !router) { if (!router) {
throw new Error( throw new Error('useRouter() is called without provider.')
'useRouter() is called without initRouter() in an ancestor component.'
)
} }
// @ts-ignore // @ts-ignore
return router return router

@ -22,6 +22,8 @@
"author": "Evan You", "author": "Evan You",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-sfc": "^3.0.0-beta.4",
"@vue/server-renderer": "^3.0.0-beta.4",
"debug": "^4.1.1", "debug": "^4.1.1",
"diacritics": "^1.3.0", "diacritics": "^1.3.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
@ -36,7 +38,8 @@
"minimist": "^1.2.5", "minimist": "^1.2.5",
"prismjs": "^1.20.0", "prismjs": "^1.20.0",
"slash": "^3.0.0", "slash": "^3.0.0",
"vite": "^0.6.0" "vite": "^0.6.0",
"vue": "^3.0.0-beta.4"
}, },
"devDependencies": { "devDependencies": {
"@types/lru-cache": "^5.1.0", "@types/lru-cache": "^5.1.0",

@ -1,11 +1,22 @@
import { promises as fs } from 'fs'
import { bundle } from './bundle' import { bundle } from './bundle'
import { BuildOptions as ViteBuildOptions } from 'vite' import { BuildOptions as ViteBuildOptions } from 'vite'
import { resolveConfig } from '../config'
import { renderPage } from './render'
export type BuildOptions = Pick< export type BuildOptions = Pick<
ViteBuildOptions, ViteBuildOptions,
'root' | 'rollupInputOptions' | 'rollupOutputOptions' 'root' | 'rollupInputOptions' | 'rollupOutputOptions'
> >
export async function build(options: BuildOptions = {}) { export async function build(buildOptions: BuildOptions = {}) {
await bundle(options) const siteConfig = await resolveConfig(buildOptions.root)
try {
const result = await bundle(siteConfig, buildOptions)
for (const page of siteConfig.pages) {
await renderPage(siteConfig, page, result)
}
} finally {
await fs.rmdir(siteConfig.tempDir, { recursive: true })
}
} }

@ -1,25 +1,23 @@
import path from 'path' import path from 'path'
import globby from 'globby'
import slash from 'slash' import slash from 'slash'
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import { APP_PATH, createResolver } from '../utils/pathResolver' import { APP_PATH, createResolver } from '../utils/pathResolver'
import { build } from 'vite' import { build, BuildOptions as ViteBuildOptions, BuildResult } from 'vite'
import { BuildOptions } from './build' import { BuildOptions } from './build'
import { resolveConfig } from '../resolveConfig' import { SiteConfig } from '../config'
import { Plugin } from 'rollup' import { Plugin } from 'rollup'
import { createMarkdownToVueRenderFn } from '../markdownToVue' import { createMarkdownToVueRenderFn } from '../markdownToVue'
// bundles the VitePress app for both client AND server. // bundles the VitePress app for both client AND server.
export async function bundle(options: BuildOptions) { export async function bundle(
const root = options.root || process.cwd() config: SiteConfig,
const config = await resolveConfig(root) options: BuildOptions
const resolver = createResolver(config.themePath) ): Promise<BuildResult[]> {
const root = config.root
const resolver = createResolver(config.themeDir)
const markdownToVue = createMarkdownToVueRenderFn(root) const markdownToVue = createMarkdownToVueRenderFn(root)
const { const { rollupInputOptions = {}, rollupOutputOptions = {} } = options
rollupInputOptions = {},
rollupOutputOptions = {}
} = options
const VitePressPlugin: Plugin = { const VitePressPlugin: Plugin = {
name: 'vitepress', name: 'vitepress',
@ -32,17 +30,8 @@ export async function bundle(options: BuildOptions) {
if (id === '/@siteData') { if (id === '/@siteData') {
return `export default ${JSON.stringify(JSON.stringify(config.site))}` return `export default ${JSON.stringify(JSON.stringify(config.site))}`
} }
// generate facade module for .md files
// and request virtual .md.vue file
if (id.endsWith('.md')) {
return (
`import Comp, { __pageData } from "${id + '.vue'}"\n` +
`export default Comp\n` +
`export { __pageData }`
)
}
// compile md into vue src for .md.vue virtual files // compile md into vue src for .md.vue virtual files
if (id.endsWith('.md.vue')) { if (id.endsWith('.md')) {
const filePath = id.replace(/\.vue$/, '') const filePath = id.replace(/\.vue$/, '')
const content = await fs.readFile(filePath, 'utf-8') const content = await fs.readFile(filePath, 'utf-8')
const lastUpdated = (await fs.stat(filePath)).mtimeMs const lastUpdated = (await fs.stat(filePath)).mtimeMs
@ -76,34 +65,59 @@ export async function bundle(options: BuildOptions) {
} }
} }
const pages = ( // convert page files to absolute paths
await globby(['**.md'], { cwd: root, ignore: ['node_modules'] }) const pages = config.pages.map(file => path.resolve(root, file))
).map((file) => path.resolve(root, file))
await build({ // let rollup-plugin-vue compile .md files as well
const rollupPluginVueOptions = {
include: /\.(vue|md)$/
}
const sharedOptions: ViteBuildOptions = {
...options, ...options,
cdn: false, cdn: false,
silent: true, silent: true,
resolvers: [resolver], resolvers: [resolver],
srcRoots: [APP_PATH, config.themePath], srcRoots: [APP_PATH, config.themeDir],
cssFileName: 'css/style.css', cssFileName: 'css/style.css',
rollupPluginVueOptions,
rollupInputOptions: { rollupInputOptions: {
...rollupInputOptions, ...rollupInputOptions,
input: [path.resolve(APP_PATH, 'index.js'), ...pages], input: [path.resolve(APP_PATH, 'index.js'), ...pages],
plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])] plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])]
}, },
rollupOutputOptions: [ rollupOutputOptions: {
{ ...rollupOutputOptions,
dir: path.resolve(root, '.vitepress/dist'), dir: config.outDir
...rollupOutputOptions },
},
{
dir: path.resolve(root, '.vitepress/temp'),
...rollupOutputOptions,
format: 'cjs',
exports: 'named'
}
],
debug: !!process.env.DEBUG debug: !!process.env.DEBUG
}
const clientResult = await build({
...sharedOptions,
rollupOutputOptions: {
...rollupOutputOptions,
dir: config.outDir
}
}) })
const serverResult = await build({
...sharedOptions,
rollupPluginVueOptions: {
...rollupPluginVueOptions,
target: 'node'
},
rollupInputOptions: {
...sharedOptions.rollupInputOptions,
external: ['vue', '@vue/server-renderer']
},
rollupOutputOptions: {
...rollupOutputOptions,
dir: config.tempDir,
format: 'cjs',
exports: 'named'
}
})
return [clientResult, serverResult]
} }

@ -0,0 +1,17 @@
import path from 'path'
import { SiteConfig } from '../config'
import { BuildResult } from 'vite'
import { renderToString } from '@vue/server-renderer'
export async function renderPage(
config: SiteConfig,
page: string, // foo.md
result: BuildResult[]
) {
const { createApp } = require(path.join(config.tempDir, 'js/index.js'))
const { app, router } = createApp()
const routePath = `/${page.replace(/\.md$/, '')}`
router.go(routePath)
const html = await renderToString(app)
console.log(html)
}

@ -1,7 +1,8 @@
import path from 'path' import path from 'path'
import chalk from 'chalk' import chalk from 'chalk'
import globby from 'globby'
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import { createResolver } from './utils/pathResolver' import { createResolver, APP_PATH } from './utils/pathResolver'
import { Resolver } from 'vite' import { Resolver } from 'vite'
const debug = require('debug')('vitepress:config') const debug = require('debug')('vitepress:config')
@ -24,32 +25,44 @@ export interface SiteData<ThemeConfig = any> {
themeConfig: ThemeConfig themeConfig: ThemeConfig
} }
export interface ResolvedConfig<ThemeConfig = any> { export interface SiteConfig<ThemeConfig = any> {
root: string
site: SiteData<ThemeConfig> site: SiteData<ThemeConfig>
themePath: string configPath: string
themeDir: string
outDir: string
tempDir: string
resolver: Resolver resolver: Resolver
pages: string[]
} }
export const getConfigPath = (root: string) => const resolve = (root: string, file: string) =>
path.join(root, '.vitepress/config.js') path.join(root, `.vitepress`, file)
export async function resolveConfig(root: string): Promise<ResolvedConfig> { export async function resolveConfig(
root: string = process.cwd()
): Promise<SiteConfig> {
const site = await resolveSiteData(root) const site = await resolveSiteData(root)
// resolve theme path // resolve theme path
const userThemePath = path.join(root, '.vitepress/theme') const userThemeDir = resolve(root, 'theme')
let themePath: string let themeDir: string
try { try {
await fs.stat(userThemePath) await fs.stat(userThemeDir)
themePath = userThemePath themeDir = userThemeDir
} catch (e) { } catch (e) {
themePath = path.join(__dirname, '../lib/theme-default') themeDir = path.join(__dirname, '../lib/theme-default')
} }
const config: ResolvedConfig = { const config: SiteConfig = {
root,
site, site,
themePath, themeDir,
resolver: createResolver(themePath) pages: await globby(['**.md'], { cwd: root, ignore: ['node_modules'] }),
configPath: resolve(root, 'config.js'),
outDir: resolve(root, 'dist'),
tempDir: path.resolve(APP_PATH, 'temp'),
resolver: createResolver(themeDir)
} }
return config return config
@ -57,7 +70,7 @@ export async function resolveConfig(root: string): Promise<ResolvedConfig> {
export async function resolveSiteData(root: string): Promise<SiteData> { export async function resolveSiteData(root: string): Promise<SiteData> {
// load user config // load user config
const configPath = getConfigPath(root) const configPath = resolve(root, 'config.js')
let hasUserConfig = false let hasUserConfig = false
try { try {
await fs.stat(configPath) await fs.stat(configPath)

@ -7,19 +7,18 @@ import {
} from 'vite' } from 'vite'
import { import {
resolveConfig, resolveConfig,
ResolvedConfig, SiteConfig,
getConfigPath,
resolveSiteData resolveSiteData
} from './resolveConfig' } from './config'
import { createMarkdownToVueRenderFn } from './markdownToVue' import { createMarkdownToVueRenderFn } from './markdownToVue'
import { APP_PATH, SITE_DATA_REQUEST_PATH } from './utils/pathResolver' import { APP_PATH, SITE_DATA_REQUEST_PATH } from './utils/pathResolver'
const debug = require('debug')('vitepress:serve') const debug = require('debug')('vitepress:serve')
const debugHmr = require('debug')('vitepress:hmr') const debugHmr = require('debug')('vitepress:hmr')
function createVitePressPlugin(config: ResolvedConfig): Plugin { function createVitePressPlugin(config: SiteConfig): Plugin {
const { const {
themePath, themeDir,
site: initialSiteData, site: initialSiteData,
resolver: vitepressResolver resolver: vitepressResolver
} = config } = config
@ -33,9 +32,9 @@ function createVitePressPlugin(config: ResolvedConfig): Plugin {
} }
// watch theme files if it's outside of project root // watch theme files if it's outside of project root
if (path.relative(root, themePath).startsWith('..')) { if (path.relative(root, themeDir).startsWith('..')) {
debugHmr(`watching theme dir outside of project root: ${themePath}`) debugHmr(`watching theme dir outside of project root: ${themeDir}`)
watcher.add(themePath) watcher.add(themeDir)
} }
// hot reload .md files as .vue files // hot reload .md files as .vue files
@ -68,10 +67,9 @@ function createVitePressPlugin(config: ResolvedConfig): Plugin {
// parsing the object literal as JavaScript. // parsing the object literal as JavaScript.
let siteData = initialSiteData let siteData = initialSiteData
let stringifiedData = JSON.stringify(JSON.stringify(initialSiteData)) let stringifiedData = JSON.stringify(JSON.stringify(initialSiteData))
const configPath = getConfigPath(root) watcher.add(config.configPath)
watcher.add(configPath)
watcher.on('change', async (file) => { watcher.on('change', async (file) => {
if (file === configPath) { if (file === config.configPath) {
const newData = await resolveSiteData(root) const newData = await resolveSiteData(root)
stringifiedData = JSON.stringify(JSON.stringify(newData)) stringifiedData = JSON.stringify(JSON.stringify(newData))
if (newData.base !== siteData.base) { if (newData.base !== siteData.base) {
@ -129,7 +127,7 @@ function createVitePressPlugin(config: ResolvedConfig): Plugin {
} }
export async function createServer(options: ServerConfig = {}) { export async function createServer(options: ServerConfig = {}) {
const config = await resolveConfig(options.root || process.cwd()) const config = await resolveConfig(options.root)
return createViteServer({ return createViteServer({
...options, ...options,

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "../dist", "outDir": "../dist",
"module": "commonjs", "module": "commonjs",
"lib": ["ESNext"], "lib": ["ESNext", "DOM"],
"sourceMap": false, "sourceMap": false,
"target": "esnext", "target": "esnext",
"moduleResolution": "node", "moduleResolution": "node",

@ -11,14 +11,14 @@ export const SITE_DATA_REQUEST_PATH = '/@siteData'
// so that we can resolve custom requests that start with /@app or /@theme // 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 // 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. // vite HMR can send the correct update notifications to the client.
export function createResolver(themePath: string): Resolver { export function createResolver(themeDir: string): Resolver {
return { return {
requestToFile(publicPath) { requestToFile(publicPath) {
if (publicPath.startsWith('/@app')) { if (publicPath.startsWith('/@app')) {
return path.join(APP_PATH, publicPath.replace(/^\/@app\/?/, '')) return path.join(APP_PATH, publicPath.replace(/^\/@app\/?/, ''))
} }
if (publicPath.startsWith('/@theme')) { if (publicPath.startsWith('/@theme')) {
return path.join(themePath, publicPath.replace(/^\/@theme\/?/, '')) return path.join(themeDir, publicPath.replace(/^\/@theme\/?/, ''))
} }
if (publicPath === SITE_DATA_REQUEST_PATH) { if (publicPath === SITE_DATA_REQUEST_PATH) {
return SITE_DATA_REQUEST_PATH return SITE_DATA_REQUEST_PATH
@ -28,8 +28,8 @@ export function createResolver(themePath: string): Resolver {
if (filePath.startsWith(APP_PATH)) { if (filePath.startsWith(APP_PATH)) {
return `/@app/${path.relative(APP_PATH, filePath)}` return `/@app/${path.relative(APP_PATH, filePath)}`
} }
if (filePath.startsWith(themePath)) { if (filePath.startsWith(themeDir)) {
return `/@theme/${path.relative(themePath, filePath)}` return `/@theme/${path.relative(themeDir, filePath)}`
} }
if (filePath === SITE_DATA_REQUEST_PATH) { if (filePath === SITE_DATA_REQUEST_PATH) {
return SITE_DATA_REQUEST_PATH return SITE_DATA_REQUEST_PATH

@ -320,6 +320,14 @@
"@vue/shared" "3.0.0-beta.4" "@vue/shared" "3.0.0-beta.4"
csstype "^2.6.8" csstype "^2.6.8"
"@vue/server-renderer@^3.0.0-beta.4":
version "3.0.0-beta.4"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.0.0-beta.4.tgz#08bbd34f8917f01298cfb70a0b000349868f770b"
integrity sha512-CQGaKWV4UL4EjpquQHWZWlXBYgYcqFWIcEIHckJAMiZTsI0UEE77B/2KTQEBHmdNHhISRTl4+KHP3gf3+cJHcA==
dependencies:
"@vue/compiler-ssr" "3.0.0-beta.4"
"@vue/shared" "3.0.0-beta.4"
"@vue/shared@3.0.0-beta.4": "@vue/shared@3.0.0-beta.4":
version "3.0.0-beta.4" version "3.0.0-beta.4"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.0-beta.4.tgz#2c1f18896d598549a9241641b406f0886a710494" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.0-beta.4.tgz#2c1f18896d598549a9241641b406f0886a710494"

Loading…
Cancel
Save