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

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

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

@ -2,4 +2,4 @@
// so the user can do `import { usePageData } from 'vitepress'`
export { useSiteData } from './composables/siteData'
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 { initRouter } from './composables/router'
import { createRouter, RouterSymbol } from './router'
import { useSiteData } from './composables/siteData'
import { initPageData, usePageData } from './composables/pageData'
import { usePageData, pageDataSymbol } from './composables/pageData'
import Theme from '/@theme/index'
import { hot } from '@hmr'
@ -10,72 +10,80 @@ const inBrowser = typeof window !== 'undefined'
const NotFound = Theme.NotFound || (() => '404 Not Found')
const App = {
setup() {
const pageDataRef = initPageData()
export function createApp() {
const pageDataRef = ref()
initRouter((route) => {
let pagePath = route.path.replace(/\.html$/, '')
if (pagePath.endsWith('/')) {
pagePath += 'index'
}
if (__DEV__) {
// awlays force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`
} 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`
// hot reload pageData
if (__DEV__ && inBrowser) {
hot.on('vitepress:pageData', (data) => {
if (
data.path.replace(/\.md$/, '') ===
location.pathname.replace(/\.html$/, '')
) {
pageDataRef.value = data.pageData
}
})
}
if (inBrowser) {
// in browser: native dynamic import
return import(pagePath).then((m) => {
pageDataRef.value = readonly(m.__pageData)
return m.default
})
} else {
// SSR, sync require
return require(pagePath).default
}
}, NotFound)
const router = createRouter((route) => {
let pagePath = route.path.replace(/\.html$/, '')
if (pagePath.endsWith('/')) {
pagePath += 'index'
}
if (__DEV__) {
// awlays force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`
} 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 = pagePath.slice(1).replace(/\//g, '_') + '.md.js'
}
if (__DEV__ && inBrowser) {
// hot reload pageData
hot.on('vitepress:pageData', (data) => {
if (
data.path.replace(/\.md$/, '') ===
location.pathname.replace(/\.html$/, '')
) {
pageDataRef.value = data.pageData
}
if (inBrowser) {
// in browser: native dynamic import
// js files are stored in a sub directory
return import('./js/' + pagePath).then(page => {
pageDataRef.value = readonly(page.__pageData)
return page.default
})
} 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
const app = createApp(App)
app.provide(RouterSymbol, router)
app.provide(pageDataSymbol, pageDataRef)
app.component('Content', Content)
app.component('Content', Content)
app.mixin({
beforeCreate() {
const siteRef = useSiteData()
const pageRef = usePageData()
Object.defineProperties(this, {
$site: {
get: () => siteRef.value
},
$page: {
get: () => pageRef.value
}
})
}
})
app.mixin({
beforeCreate() {
const siteRef = useSiteData()
const pageRef = usePageData()
Object.defineProperties(this, {
$site: {
get: () => siteRef.value
},
$page: {
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
@ -17,7 +17,7 @@ import { reactive, provide, inject, nextTick, markRaw } from 'vue'
/**
* @type {import('vue').InjectionKey<Router>}
*/
const RouterSymbol = Symbol()
export const RouterSymbol = Symbol()
/**
* @returns {Route}
@ -32,7 +32,7 @@ const getDefaultRoute = () => ({
* @param {Component} [fallbackComponent]
* @returns {Router}
*/
export function initRouter(loadComponent, fallbackComponent) {
export function createRouter(loadComponent, fallbackComponent) {
const route = reactive(getDefaultRoute())
const inBrowser = typeof window !== 'undefined'
@ -159,9 +159,9 @@ export function initRouter(loadComponent, fallbackComponent) {
go
}
provide(RouterSymbol, router)
loadPage(location.href)
if (inBrowser) {
loadPage(location.href)
}
return router
}
@ -171,10 +171,8 @@ export function initRouter(loadComponent, fallbackComponent) {
*/
export function useRouter() {
const router = inject(RouterSymbol)
if (__DEV__ && !router) {
throw new Error(
'useRouter() is called without initRouter() in an ancestor component.'
)
if (!router) {
throw new Error('useRouter() is called without provider.')
}
// @ts-ignore
return router

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

@ -1,11 +1,22 @@
import { promises as fs } from 'fs'
import { bundle } from './bundle'
import { BuildOptions as ViteBuildOptions } from 'vite'
import { resolveConfig } from '../config'
import { renderPage } from './render'
export type BuildOptions = Pick<
ViteBuildOptions,
'root' | 'rollupInputOptions' | 'rollupOutputOptions'
>
export async function build(options: BuildOptions = {}) {
await bundle(options)
export async function build(buildOptions: BuildOptions = {}) {
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 globby from 'globby'
import slash from 'slash'
import { promises as fs } from 'fs'
import { APP_PATH, createResolver } from '../utils/pathResolver'
import { build } from 'vite'
import { build, BuildOptions as ViteBuildOptions, BuildResult } from 'vite'
import { BuildOptions } from './build'
import { resolveConfig } from '../resolveConfig'
import { SiteConfig } from '../config'
import { Plugin } from 'rollup'
import { createMarkdownToVueRenderFn } from '../markdownToVue'
// bundles the VitePress app for both client AND server.
export async function bundle(options: BuildOptions) {
const root = options.root || process.cwd()
const config = await resolveConfig(root)
const resolver = createResolver(config.themePath)
export async function bundle(
config: SiteConfig,
options: BuildOptions
): Promise<BuildResult[]> {
const root = config.root
const resolver = createResolver(config.themeDir)
const markdownToVue = createMarkdownToVueRenderFn(root)
const {
rollupInputOptions = {},
rollupOutputOptions = {}
} = options
const { rollupInputOptions = {}, rollupOutputOptions = {} } = options
const VitePressPlugin: Plugin = {
name: 'vitepress',
@ -32,17 +30,8 @@ export async function bundle(options: BuildOptions) {
if (id === '/@siteData') {
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
if (id.endsWith('.md.vue')) {
if (id.endsWith('.md')) {
const filePath = id.replace(/\.vue$/, '')
const content = await fs.readFile(filePath, 'utf-8')
const lastUpdated = (await fs.stat(filePath)).mtimeMs
@ -76,34 +65,59 @@ export async function bundle(options: BuildOptions) {
}
}
const pages = (
await globby(['**.md'], { cwd: root, ignore: ['node_modules'] })
).map((file) => path.resolve(root, file))
// convert page files to absolute paths
const pages = config.pages.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,
cdn: false,
silent: true,
resolvers: [resolver],
srcRoots: [APP_PATH, config.themePath],
srcRoots: [APP_PATH, config.themeDir],
cssFileName: 'css/style.css',
rollupPluginVueOptions,
rollupInputOptions: {
...rollupInputOptions,
input: [path.resolve(APP_PATH, 'index.js'), ...pages],
plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])]
},
rollupOutputOptions: [
{
dir: path.resolve(root, '.vitepress/dist'),
...rollupOutputOptions
},
{
dir: path.resolve(root, '.vitepress/temp'),
...rollupOutputOptions,
format: 'cjs',
exports: 'named'
}
],
rollupOutputOptions: {
...rollupOutputOptions,
dir: config.outDir
},
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 chalk from 'chalk'
import globby from 'globby'
import { promises as fs } from 'fs'
import { createResolver } from './utils/pathResolver'
import { createResolver, APP_PATH } from './utils/pathResolver'
import { Resolver } from 'vite'
const debug = require('debug')('vitepress:config')
@ -24,32 +25,44 @@ export interface SiteData<ThemeConfig = any> {
themeConfig: ThemeConfig
}
export interface ResolvedConfig<ThemeConfig = any> {
export interface SiteConfig<ThemeConfig = any> {
root: string
site: SiteData<ThemeConfig>
themePath: string
configPath: string
themeDir: string
outDir: string
tempDir: string
resolver: Resolver
pages: string[]
}
export const getConfigPath = (root: string) =>
path.join(root, '.vitepress/config.js')
const resolve = (root: string, file: string) =>
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)
// resolve theme path
const userThemePath = path.join(root, '.vitepress/theme')
let themePath: string
const userThemeDir = resolve(root, 'theme')
let themeDir: string
try {
await fs.stat(userThemePath)
themePath = userThemePath
await fs.stat(userThemeDir)
themeDir = userThemeDir
} catch (e) {
themePath = path.join(__dirname, '../lib/theme-default')
themeDir = path.join(__dirname, '../lib/theme-default')
}
const config: ResolvedConfig = {
const config: SiteConfig = {
root,
site,
themePath,
resolver: createResolver(themePath)
themeDir,
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
@ -57,7 +70,7 @@ export async function resolveConfig(root: string): Promise<ResolvedConfig> {
export async function resolveSiteData(root: string): Promise<SiteData> {
// load user config
const configPath = getConfigPath(root)
const configPath = resolve(root, 'config.js')
let hasUserConfig = false
try {
await fs.stat(configPath)

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

@ -2,7 +2,7 @@
"compilerOptions": {
"outDir": "../dist",
"module": "commonjs",
"lib": ["ESNext"],
"lib": ["ESNext", "DOM"],
"sourceMap": false,
"target": "esnext",
"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
// 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(themePath: string): Resolver {
export function createResolver(themeDir: string): Resolver {
return {
requestToFile(publicPath) {
if (publicPath.startsWith('/@app')) {
return path.join(APP_PATH, publicPath.replace(/^\/@app\/?/, ''))
}
if (publicPath.startsWith('/@theme')) {
return path.join(themePath, publicPath.replace(/^\/@theme\/?/, ''))
return path.join(themeDir, publicPath.replace(/^\/@theme\/?/, ''))
}
if (publicPath === SITE_DATA_REQUEST_PATH) {
return SITE_DATA_REQUEST_PATH
@ -28,8 +28,8 @@ export function createResolver(themePath: string): Resolver {
if (filePath.startsWith(APP_PATH)) {
return `/@app/${path.relative(APP_PATH, filePath)}`
}
if (filePath.startsWith(themePath)) {
return `/@theme/${path.relative(themePath, filePath)}`
if (filePath.startsWith(themeDir)) {
return `/@theme/${path.relative(themeDir, filePath)}`
}
if (filePath === SITE_DATA_REQUEST_PATH) {
return SITE_DATA_REQUEST_PATH

@ -320,6 +320,14 @@
"@vue/shared" "3.0.0-beta.4"
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":
version "3.0.0-beta.4"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.0-beta.4.tgz#2c1f18896d598549a9241641b406f0886a710494"

Loading…
Cancel
Save