diff --git a/bin/vitepress.js b/bin/vitepress.js index aeff1e4f..d270d565 100755 --- a/bin/vitepress.js +++ b/bin/vitepress.js @@ -13,8 +13,12 @@ if (!command || command === 'dev') { if (root) { argv.root = root } - require('../dist').createServer(argv).listen(port, () => { - console.log(`listening at http://localhost:${port}`) + require('../dist').createServer(argv).then(server => { + server.listen(port, () => { + console.log(`listening at http://localhost:${port}`) + }) + }).catch(err => { + console.error(`failed to start server. error: `, err) }) } else if (command === 'build') { require('../dist').build(argv).catch(err => { diff --git a/lib/app/index.js b/lib/app/index.js index 1a766189..5267b384 100644 --- a/lib/app/index.js +++ b/lib/app/index.js @@ -1,7 +1,7 @@ import { createApp, h } from 'vue' -import { Layout } from '/@theme/index.js' +import { Layout } from '/@theme/index' import { Content } from './Content' -import { useRouter } from './router.js' +import { useRouter } from './router' const App = { setup() { diff --git a/lib/jsconfig.json b/lib/jsconfig.json new file mode 100644 index 00000000..6f38c88b --- /dev/null +++ b/lib/jsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "moduleResolution": "node", + "checkJs": true, + "noUnusedLocals": true, + "strictNullChecks": true, + "noImplicitAny": true, + "paths": { + "/@app/*": ["lib/app/*"], + "/@theme/*": ["lib/theme-default/*"] + } + }, + "include": ["."] +} diff --git a/package.json b/package.json index b2df3349..ba425bf1 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dist" ], "scripts": { - "dev": "tsc -w -p ." + "dev": "tsc -w -p src" }, "keywords": [ "vite", diff --git a/src/index.ts b/src/index.ts index d43e1129..d82ad5da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export * from './server/server' +export * from './server' export * from './build/build' diff --git a/src/resolveConfig.ts b/src/resolveConfig.ts new file mode 100644 index 00000000..975be2da --- /dev/null +++ b/src/resolveConfig.ts @@ -0,0 +1,78 @@ +import path from 'path' +import { promises as fs } from 'fs' +import { createResolver } from './utils/pathResolver' +import { Resolver } from 'vite' + +const debug = require('debug')('vitepress:config') + +export interface UserConfig> { + base?: string + title?: string + description?: string + head?: + | [string, Record] + | [string, Record, string] + themeConfig?: ThemeConfig + // TODO locales support etc. +} + +export interface SiteData> { + title: string + description: string + base: string + themeConfig: ThemeConfig + pages: PageData[] +} + +export interface PageData { + path: string +} + +export interface ResolvedConfig> { + site: SiteData + root: string // project root on file system + themePath: string + resolver: Resolver +} + +export async function resolveConfig(root: string): Promise { + // 1. load user config + const configPath = path.join(root, '.vitepress/config.js') + let hasUserConfig = false + try { + await fs.stat(configPath) + hasUserConfig = true + debug(`loading user config at ${configPath}`) + } catch (e) {} + const userConfig: UserConfig = hasUserConfig ? require(configPath) : {} + + // 2. TODO scan pages data + + // 3. resolve site data + const site: SiteData = { + title: userConfig.title || 'VitePress', + description: userConfig.description || 'A VitePress site', + base: userConfig.base || '/', + themeConfig: userConfig.themeConfig || {}, + pages: [] + } + + // 4. resolve theme path + const userThemePath = path.join(root, '.vitepress/theme') + let themePath: string + try { + await fs.stat(userThemePath) + themePath = userThemePath + } catch (e) { + themePath = path.join(__dirname, '../lib/theme-default') + } + + const config: ResolvedConfig = { + root, + site, + themePath, + resolver: createResolver(themePath) + } + + return config +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 00000000..0b39e6c7 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,77 @@ +import path from 'path' +import { + createServer as createViteServer, + cachedRead, + Plugin, + ServerConfig +} from 'vite' +import { resolveConfig, ResolvedConfig } from './resolveConfig' +import { createMarkdownToVueRenderFn } from './markdown/markdownToVue' +import { APP_PATH } from './utils/pathResolver' + +const debug = require('debug')('vitepress:serve') +const debugHmr = require('debug')('vitepress:hmr') + +function createVitePressPlugin({ + themePath, + resolver: vitepressResolver +}: ResolvedConfig): Plugin { + return ({ app, root, watcher, resolver }) => { + const markdownToVue = createMarkdownToVueRenderFn(root) + + // 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) + } + + // 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) + watcher.handleVueReload(file, Date.now(), markdownToVue(content, file)) + } + }) + + // inject Koa middleware + app.use(async (ctx, next) => { + // handle .md -> vue transforms + if (ctx.path.endsWith('.md')) { + const file = resolver.publicToFile(ctx.path) + await cachedRead(ctx, file) + // let vite know this is supposed to be treated as vue file + ctx.vue = true + ctx.body = markdownToVue(ctx.body, file) + debug(ctx.url, ctx.status) + return next() + } + + // detect and serve vitepress @app / @theme files + const file = vitepressResolver.publicToFile(ctx.path, root) + if (file) { + await cachedRead(ctx, file) + debug(ctx.url, ctx.status) + await next() + return + } + + await next() + + // serve our index.html after vite history fallback + if (ctx.url === '/index.html') { + await cachedRead(ctx, path.join(APP_PATH, 'index-dev.html')) + } + }) + } +} + +export async function createServer(options: ServerConfig = {}) { + const config = await resolveConfig(options.root || process.cwd()) + + return createViteServer({ + ...options, + plugins: [createVitePressPlugin(config)], + resolvers: [config.resolver] + }) +} diff --git a/src/server/resolver.ts b/src/server/resolver.ts deleted file mode 100644 index 7b1eaf99..00000000 --- a/src/server/resolver.ts +++ /dev/null @@ -1,26 +0,0 @@ -import path from 'path' -import { Resolver } from "vite" - -// built ts files are placed into /dist -export const APP_PATH = path.join(__dirname, '../../lib/app') -// TODO detect user configured theme -export const THEME_PATH = path.join(__dirname, '../../lib/theme-default') - -export const VitePressResolver: Resolver = { - publicToFile(publicPath) { - if (publicPath.startsWith('/@app')) { - return path.join(APP_PATH, publicPath.replace(/^\/@app\/?/, '')) - } - if (publicPath.startsWith('/@theme')) { - return path.join(THEME_PATH, publicPath.replace(/^\/@theme\/?/, '')) - } - }, - fileToPublic(filePath) { - if (filePath.startsWith(APP_PATH)) { - return `/@app/${path.relative(APP_PATH, filePath)}` - } - if (filePath.startsWith(THEME_PATH)) { - return `/@theme/${path.relative(THEME_PATH, filePath)}` - } - } -} diff --git a/src/server/server.ts b/src/server/server.ts deleted file mode 100644 index 167f4a1b..00000000 --- a/src/server/server.ts +++ /dev/null @@ -1,70 +0,0 @@ -import path from 'path' -import { - createServer as createViteServer, - cachedRead, - Plugin, - ServerConfig -} from 'vite' -import { createMarkdownToVueRenderFn } from '../markdown/markdownToVue' -import { VitePressResolver, THEME_PATH, APP_PATH } from './resolver' - -const debug = require('debug')('vitepress:serve') -const debugHmr = require('debug')('vitepress:hmr') - -const VitePressPlugin: Plugin = ({ app, root, watcher, resolver }) => { - const markdownToVue = createMarkdownToVueRenderFn(root) - - // watch theme files if it's outside of project root - if (path.relative(root, THEME_PATH).startsWith('..')) { - debugHmr(`watching theme dir outside of project root: ${THEME_PATH}`) - watcher.add(THEME_PATH) - } - - // 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) - watcher.handleVueReload(file, Date.now(), markdownToVue(content, file)) - } - }) - - // inject Koa middleware - app.use(async (ctx, next) => { - // handle .md -> vue transforms - if (ctx.path.endsWith('.md')) { - const file = resolver.publicToFile(ctx.path) - await cachedRead(ctx, file) - // let vite know this is supposed to be treated as vue file - ctx.vue = true - ctx.body = markdownToVue(ctx.body, file) - debug(ctx.url) - return next() - } - - // detect and serve vitepress @app / @theme files - const file = VitePressResolver.publicToFile(ctx.path, root) - if (file) { - ctx.type = path.extname(file) - await cachedRead(ctx, file) - - debug(ctx.url) - return next() - } - - await next() - - // serve our index.html after vite history fallback - if (ctx.url === '/index.html') { - await cachedRead(ctx, path.join(APP_PATH, 'index-dev.html')) - } - }) -} - -export function createServer(options: ServerConfig = {}) { - return createViteServer({ - ...options, - plugins: [VitePressPlugin], - resolvers: [VitePressResolver] - }) -} diff --git a/tsconfig.json b/src/tsconfig.json similarity index 66% rename from tsconfig.json rename to src/tsconfig.json index db0aaf48..a045e381 100644 --- a/tsconfig.json +++ b/src/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "baseUrl": ".", - "outDir": "./dist", + "outDir": "../dist", "module": "commonjs", - "lib": ["ESNext", "DOM"], + "lib": ["ESNext"], "sourceMap": false, "target": "esnext", "moduleResolution": "node", @@ -16,11 +16,7 @@ "strictNullChecks": true, "noImplicitAny": true, "removeComments": false, - "preserveSymlinks": true, - "paths": { - "/@app/*": ["lib/app/*"], - "/@theme/*": ["lib/theme-default/*"] - } + "preserveSymlinks": true }, - "include": ["src", "lib"] + "include": ["."] } diff --git a/src/utils/pathResolver.ts b/src/utils/pathResolver.ts new file mode 100644 index 00000000..81cf023d --- /dev/null +++ b/src/utils/pathResolver.ts @@ -0,0 +1,30 @@ +import path from 'path' +import { Resolver } from "vite" + +// built ts files are placed into /dist +export const APP_PATH = path.join(__dirname, '../../lib/app') + +// 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(themePath: string): Resolver { + return { + publicToFile(publicPath) { + if (publicPath.startsWith('/@app')) { + return path.join(APP_PATH, publicPath.replace(/^\/@app\/?/, '')) + } + if (publicPath.startsWith('/@theme')) { + return path.join(themePath, publicPath.replace(/^\/@theme\/?/, '')) + } + }, + fileToPublic(filePath) { + if (filePath.startsWith(APP_PATH)) { + return `/@app/${path.relative(APP_PATH, filePath)}` + } + if (filePath.startsWith(themePath)) { + return `/@theme/${path.relative(themePath, filePath)}` + } + } + } +}