diff --git a/bin/vitepress.js b/bin/vitepress.js index 9c711072..0d88de16 100755 --- a/bin/vitepress.js +++ b/bin/vitepress.js @@ -13,7 +13,7 @@ if (!command || command === 'dev') { if (root) { argv.root = root } - require('../dist') + require('../dist/node') .createServer(argv) .then((server) => { server.listen(port, () => { @@ -24,7 +24,7 @@ if (!command || command === 'dev') { console.error(chalk.red(`failed to start server. error:\n`), err) }) } else if (command === 'build') { - require('../dist') + require('../dist/node') .build(argv) .catch((err) => { console.error(chalk.red(`build error:\n`), err) diff --git a/lib/app/composables/pageData.js b/lib/app/composables/pageData.js deleted file mode 100644 index 18d3f5cc..00000000 --- a/lib/app/composables/pageData.js +++ /dev/null @@ -1,21 +0,0 @@ -import { inject } from 'vue' - -/** - * @typedef {import('vue').Ref} PageDataRef - */ - -/** - * @type {import('vue').InjectionKey} - */ -export const pageDataSymbol = Symbol() - -/** - * @returns {PageDataRef} - */ -export function usePageData() { - const data = inject(pageDataSymbol) - if (!data) { - throw new Error('usePageData() is called without provider.') - } - return data -} diff --git a/lib/app/composables/siteData.js b/lib/app/composables/siteData.js deleted file mode 100644 index ad3b6f39..00000000 --- a/lib/app/composables/siteData.js +++ /dev/null @@ -1,25 +0,0 @@ -import serialized from '@siteData' -import { hot } from 'vite/hmr' -import { ref, readonly } from 'vue' - -/** - * @param {string} data - * @returns {any} - */ -const parse = (data) => readonly(JSON.parse(data)) - -/** - * @type {import('vue').Ref} - */ -export const siteDataRef = ref(parse(serialized)) - -export function useSiteData() { - return siteDataRef -} - -// hmr -if (__DEV__) { - hot.accept('/@siteData', (m) => { - siteDataRef.value = parse(m.default) - }) -} diff --git a/lib/jsconfig.json b/lib/jsconfig.json deleted file mode 100644 index 515a4ddd..00000000 --- a/lib/jsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "lib": ["ESNext", "DOM"], - "moduleResolution": "node", - "checkJs": true, - "noUnusedLocals": true, - "strictNullChecks": true, - "noImplicitAny": true, - "paths": { - "/@app/*": ["app/*"], - "/@theme/*": ["theme-default/*"], - "vitepress": ["app/exports.js"], - "src": ["../dist/index.d.ts"] - } - }, - "include": ["."] -} diff --git a/lib/theme-default/index.js b/lib/theme-default/index.js deleted file mode 100644 index d983b32b..00000000 --- a/lib/theme-default/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import Layout from './Layout.vue' - -/** - * @typedef {{ - * app: import('vue').App - * router: import('../app/router').Router - * siteData: import('vue').Ref - * }} EnhanceAppContext - * - * @type {{ - * Layout: import('vue').ComponentOptions - * NotFound?: import('vue').ComponentOptions - * enhanceApp?: (ctx: EnhanceAppContext) => void - * }} - */ -const Theme = { - Layout -} - -export default Theme diff --git a/package.json b/package.json index 2ab72ea4..b1637cf9 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,16 @@ "name": "vitepress", "version": "0.1.1", "description": "", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "dist/node/index.js", + "types": "index.d.ts", "bin": { "vitepress": "bin/vitepress.js" }, "files": [ "bin", "lib", - "dist" + "dist", + "types" ], "keywords": [ "vite", @@ -26,10 +27,13 @@ }, "homepage": "https://github.com/vuejs/vitepress/tree/master/#readme", "scripts": { - "dev": "tsc -w -p src", - "build": "rm -rf dist && tsc -p src", + "dev": "run-p dev-client dev-node", + "dev-client": "tsc -w -p src/client", + "dev-node": "tsc -w -p src/node", + "build": "rm -rf dist && tsc -p src/client && tsc -p src/node", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", - "prepublishOnly": "yarn build && yarn changelog" + "prepublishOnly": "yarn build && yarn changelog", + "postpublish": "git add CHANGELOG.md && git commit -m 'chore: changelog [ci skip]'" }, "engines": { "node": ">=10.0.0" @@ -75,6 +79,7 @@ "@types/node": "^13.13.4", "conventional-changelog-cli": "^2.0.31", "lint-staged": "^10.2.1", + "npm-run-all": "^4.1.5", "prettier": "^2.0.5", "typescript": "^3.8.3", "yorkie": "^2.0.0" diff --git a/lib/app/components/Content.js b/src/client/app/components/Content.ts similarity index 100% rename from lib/app/components/Content.js rename to src/client/app/components/Content.ts diff --git a/lib/app/components/Debug.vue b/src/client/app/components/Debug.vue similarity index 100% rename from lib/app/components/Debug.vue rename to src/client/app/components/Debug.vue diff --git a/lib/app/composables/head.js b/src/client/app/composables/head.ts similarity index 64% rename from lib/app/composables/head.js rename to src/client/app/composables/head.ts index a106ba87..6c41143a 100644 --- a/lib/app/composables/head.js +++ b/src/client/app/composables/head.ts @@ -1,20 +1,13 @@ import { watchEffect } from 'vue' import { siteDataRef } from './siteData' +import { PageDataRef } from './pageData' +import { HeadConfig } from '../../../../types/shared' -/** - * @param {import('./pageData').PageDataRef} pageDataRef - */ -export function useUpdateHead(pageDataRef) { - /** - * @type {HTMLElement[]} - */ - const metaTags = Array.from(document.querySelectorAll('meta')) +export function useUpdateHead(pageDataRef: PageDataRef) { + const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta')) let isFirstUpdate = true - /** - * @param {import('src').HeadConfig[]} newTags - */ - const updateHeadTags = (newTags) => { + const updateHeadTags = (newTags: HeadConfig[]) => { if (!__DEV__ && isFirstUpdate) { // in production, the initial meta tags are already pre-rendered so we // skip the first update. @@ -38,20 +31,20 @@ export function useUpdateHead(pageDataRef) { const pageTitle = pageData && pageData.title document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title updateHeadTags([ - ['meta', { - name: 'description', - content: siteData.description - }], + [ + 'meta', + { + name: 'description', + content: siteData.description + } + ], ...siteData.head, - ...(pageData && pageData.frontmatter.head || []) + ...((pageData && pageData.frontmatter.head) || []) ]) }) } -/** - * @param {import('src').HeadConfig} item - */ -function createHeadElement([tag, attrs, innerHTML]) { +function createHeadElement([tag, attrs, innerHTML]: HeadConfig) { const el = document.createElement(tag) for (const key in attrs) { el.setAttribute(key, attrs[key]) diff --git a/src/client/app/composables/pageData.ts b/src/client/app/composables/pageData.ts new file mode 100644 index 00000000..0b989dd3 --- /dev/null +++ b/src/client/app/composables/pageData.ts @@ -0,0 +1,14 @@ +import { PageData } from '../../../../types/shared' +import { inject, InjectionKey, Ref } from 'vue' + +export type PageDataRef = Ref + +export const pageDataSymbol: InjectionKey = Symbol() + +export function usePageData(): PageDataRef { + const data = inject(pageDataSymbol) + if (!data) { + throw new Error('usePageData() is called without provider.') + } + return data +} diff --git a/src/client/app/composables/siteData.ts b/src/client/app/composables/siteData.ts new file mode 100644 index 00000000..494e935f --- /dev/null +++ b/src/client/app/composables/siteData.ts @@ -0,0 +1,19 @@ +import serialized from '@siteData' +import { ref, readonly, Ref } from 'vue' +import { SiteData } from '../../../../types/shared' +import { hot } from 'vite/hmr' + +const parse = (data: string) => readonly(JSON.parse(data) as SiteData) + +export const siteDataRef: Ref = ref(parse(serialized)) + +export function useSiteData() { + return siteDataRef +} + +// hmr +if (__DEV__) { + hot.accept('/@siteData', (m) => { + siteDataRef.value = parse(m.default) + }) +} diff --git a/lib/app/exports.js b/src/client/app/exports.ts similarity index 64% rename from lib/app/exports.js rename to src/client/app/exports.ts index f5fd172b..de39bee1 100644 --- a/lib/app/exports.js +++ b/src/client/app/exports.ts @@ -1,9 +1,18 @@ // exports in this file are exposed to themes and md files via 'vitepress' // so the user can do `import { usePageData } from 'vitepress'` + +// theme types +export * from './theme' + +// composables export { useSiteData } from './composables/siteData' export { usePageData } from './composables/pageData' export { useRouter, useRoute } from './router' +// components export { Content } from './components/Content' -import Debug from './components/Debug.vue' + +import _Debug from './components/Debug.vue' +import { ComponentOptions } from 'vue' +const Debug = _Debug as ComponentOptions export { Debug } diff --git a/lib/app/index.html b/src/client/app/index.html similarity index 100% rename from lib/app/index.html rename to src/client/app/index.html diff --git a/lib/app/index.js b/src/client/app/index.ts similarity index 98% rename from lib/app/index.js rename to src/client/app/index.ts index b47f4339..04b69aeb 100644 --- a/lib/app/index.js +++ b/src/client/app/index.ts @@ -35,10 +35,7 @@ export function createApp() { } let isInitialPageLoad = inBrowser - /** - * @type string - */ - let initialPath + let initialPath: string const router = createRouter((route) => { let pagePath = route.path.replace(/\.html$/, '') diff --git a/lib/app/router.js b/src/client/app/router.ts similarity index 68% rename from lib/app/router.js rename to src/client/app/router.ts index 635b22e8..d1928760 100644 --- a/lib/app/router.js +++ b/src/client/app/router.ts @@ -1,46 +1,31 @@ import { reactive, inject, nextTick, markRaw } from 'vue' +import type { Component, InjectionKey } from 'vue' -/** - * @typedef {import('vue').Component} Component - * - * @typedef {{ - * path: string - * contentComponent: Component | null - * }} Route - * - * @typedef {{ - * route: Route - * go: (href?: string) => Promise - * }} Router - */ +export interface Route { + path: string + contentComponent: Component | null +} + +export interface Router { + route: Route + go: (href?: string) => Promise +} -/** - * @type {import('vue').InjectionKey} - */ -export const RouterSymbol = Symbol() +export const RouterSymbol: InjectionKey = Symbol() -/** - * @returns {Route} - */ -const getDefaultRoute = () => ({ +const getDefaultRoute = (): Route => ({ path: '/', contentComponent: null }) -/** - * @param {(route: Route) => Component | Promise} loadComponent - * @param {Component} [fallbackComponent] - * @returns {Router} - */ -export function createRouter(loadComponent, fallbackComponent) { +export function createRouter( + loadComponent: (route: Route) => Component | Promise, + fallbackComponent?: Component +): Router { const route = reactive(getDefaultRoute()) const inBrowser = typeof window !== 'undefined' - /** - * @param {string} [href] - * @returns {Promise} - */ - function go(href) { + function go(href?: string) { href = href || (inBrowser ? location.href : '/') if (inBrowser) { // save scroll position before changing url @@ -50,12 +35,7 @@ export function createRouter(loadComponent, fallbackComponent) { return loadPage(href) } - /** - * @param {string} href - * @param {number} scrollPosition - * @returns {Promise} - */ - async function loadPage(href, scrollPosition = 0) { + async function loadPage(href: string, scrollPosition = 0) { // we are just using URL to parse the pathname and hash - the base doesn't // matter and is only passed to support same-host hrefs. const targetLoc = new URL(href, `http://vuejs.org`) @@ -77,10 +57,7 @@ export function createRouter(loadComponent, fallbackComponent) { await nextTick() if (targetLoc.hash && !scrollPosition) { - /** - * @type {HTMLElement | null} - */ - const target = document.querySelector(targetLoc.hash) + const target = document.querySelector(targetLoc.hash) as HTMLElement if (target) { scrollPosition = target.offsetTop } @@ -108,11 +85,8 @@ export function createRouter(loadComponent, fallbackComponent) { if (inBrowser) { window.addEventListener( 'click', - /** - * @param {*} e - */ (e) => { - const link = e.target.closest('a') + const link = (e.target as Element).closest('a') if (link) { const { href, target } = link const targetUrl = new URL(href) @@ -142,32 +116,18 @@ export function createRouter(loadComponent, fallbackComponent) { { capture: true } ) - window.addEventListener( - 'popstate', - /** - * @param {*} e - */ - (e) => { - loadPage(location.href, (e.state && e.state.scrollPosition) || 0) - } - ) + window.addEventListener('popstate', (e) => { + loadPage(location.href, (e.state && e.state.scrollPosition) || 0) + }) } - /** - * @type {Router} - */ - const router = { + return { route, go } - - return router } -/** - * @return {Router} - */ -export function useRouter() { +export function useRouter(): Router { const router = inject(RouterSymbol) if (!router) { throw new Error('useRouter() is called without provider.') @@ -176,9 +136,6 @@ export function useRouter() { return router } -/** - * @returns {Route} - */ -export function useRoute() { +export function useRoute(): Route { return useRouter().route } diff --git a/src/client/app/theme.ts b/src/client/app/theme.ts new file mode 100644 index 00000000..6559a5d7 --- /dev/null +++ b/src/client/app/theme.ts @@ -0,0 +1,15 @@ +import { App, Ref, ComponentOptions } from 'vue' +import { Router } from './router' +import { SiteData } from '../../../types/shared' + +export interface EnhanceAppContext { + app: App + router: Router + siteData: Ref +} + +export interface Theme { + Layout: ComponentOptions + NotFound?: ComponentOptions + enhanceApp?: (ctx: EnhanceAppContext) => void +} diff --git a/lib/shim.d.ts b/src/client/shim.d.ts similarity index 100% rename from lib/shim.d.ts rename to src/client/shim.d.ts diff --git a/lib/theme-default/Layout.vue b/src/client/theme-default/Layout.vue similarity index 100% rename from lib/theme-default/Layout.vue rename to src/client/theme-default/Layout.vue diff --git a/src/client/theme-default/index.ts b/src/client/theme-default/index.ts new file mode 100644 index 00000000..f90d88f6 --- /dev/null +++ b/src/client/theme-default/index.ts @@ -0,0 +1,22 @@ +import { App, Ref, ComponentOptions } from 'vue' +import Layout from './Layout.vue' +import { Router } from '/@app/router' +import { SiteData } from '../../../types/shared' + +export interface EnhanceAppContext { + app: App + router: Router + siteData: Ref +} + +export interface Theme { + Layout: ComponentOptions + NotFound?: ComponentOptions + enhanceApp?: (ctx: EnhanceAppContext) => void +} + +const theme: Theme = { + Layout +} + +export default theme diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json new file mode 100644 index 00000000..401a2861 --- /dev/null +++ b/src/client/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "../../dist/client", + "module": "esnext", + "lib": ["ESNext", "DOM"], + "paths": { + "/@app/*": ["app/*"], + "/@theme/*": ["theme-default/*"], + "vitepress": ["app/exports.ts"] + } + }, + "include": [".", "../../types/shared.d.ts"] +} diff --git a/src/markdown/plugins/slugify.ts b/src/markdown/plugins/slugify.ts deleted file mode 100644 index 483ed66e..00000000 --- a/src/markdown/plugins/slugify.ts +++ /dev/null @@ -1,22 +0,0 @@ -// string.js slugify drops non ascii chars so we have to -// use a custom implementation here -const removeDiacritics = require('diacritics').remove -// eslint-disable-next-line no-control-regex -const rControl = /[\u0000-\u001f]/g -const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g - -export const slugify = (str: string): string => { - return removeDiacritics(str) - // Remove control characters - .replace(rControl, '') - // Replace special characters - .replace(rSpecial, '-') - // Remove continous separators - .replace(/\-{2,}/g, '-') - // Remove prefixing and trailing separtors - .replace(/^\-+|\-+$/g, '') - // ensure it doesn't start with a number (#121) - .replace(/^(\d)/, '_$1') - // lowercase - .toLowerCase() -} diff --git a/src/build/build.ts b/src/node/build/build.ts similarity index 100% rename from src/build/build.ts rename to src/node/build/build.ts diff --git a/src/build/bundle.ts b/src/node/build/bundle.ts similarity index 100% rename from src/build/bundle.ts rename to src/node/build/bundle.ts diff --git a/src/build/render.ts b/src/node/build/render.ts similarity index 97% rename from src/build/render.ts rename to src/node/build/render.ts index d4d9da1e..a11b2313 100644 --- a/src/build/render.ts +++ b/src/node/build/render.ts @@ -1,6 +1,7 @@ import path from 'path' import fs from 'fs-extra' -import { SiteConfig, HeadConfig } from '../config' +import { SiteConfig } from '../config' +import { HeadConfig } from '../../../types/shared' import { BuildResult } from 'vite' import { renderToString } from '@vue/server-renderer' import { OutputChunk, OutputAsset } from 'rollup' diff --git a/src/config.ts b/src/node/config.ts similarity index 81% rename from src/config.ts rename to src/node/config.ts index e7eb0680..88386706 100644 --- a/src/config.ts +++ b/src/node/config.ts @@ -4,14 +4,10 @@ import chalk from 'chalk' import globby from 'globby' import { createResolver, APP_PATH } from './resolver' import { Resolver } from 'vite' -import { Header } from './markdown/plugins/header' +import { SiteData, HeadConfig } from '../../types/shared' const debug = require('debug')('vitepress:config') -export type HeadConfig = - | [string, Record] - | [string, Record, string] - export interface UserConfig { base?: string title?: string @@ -32,21 +28,6 @@ export interface SiteConfig { pages: string[] } -export interface SiteData { - title: string - description: string - base: string - head: HeadConfig[] - themeConfig: ThemeConfig -} - -export interface PageData { - title: string - frontmatter: Record - headers: Header[] - lastUpdated: number -} - const resolve = (root: string, file: string) => path.join(root, `.vitepress`, file) @@ -59,7 +40,7 @@ export async function resolveConfig( const userThemeDir = resolve(root, 'theme') const themeDir = (await fs.pathExists(userThemeDir)) ? userThemeDir - : path.join(__dirname, '../lib/theme-default') + : path.join(__dirname, '../client/theme-default') const config: SiteConfig = { root, diff --git a/src/index.ts b/src/node/index.ts similarity index 100% rename from src/index.ts rename to src/node/index.ts diff --git a/src/markdown/markdown.ts b/src/node/markdown/markdown.ts similarity index 96% rename from src/markdown/markdown.ts rename to src/node/markdown/markdown.ts index 533a8c99..df7ee460 100644 --- a/src/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -10,7 +10,8 @@ import { snippetPlugin } from './plugins/snippet' import { hoistPlugin } from './plugins/hoist' import { preWrapperPlugin } from './plugins/preWrapper' import { linkPlugin } from './plugins/link' -import { extractHeaderPlugin, Header } from './plugins/header' +import { extractHeaderPlugin } from './plugins/header' +import { Header } from '../../../types/shared' const emoji = require('markdown-it-emoji') const anchor = require('markdown-it-anchor') diff --git a/src/markdown/plugins/component.ts b/src/node/markdown/plugins/component.ts similarity index 96% rename from src/markdown/plugins/component.ts rename to src/node/markdown/plugins/component.ts index 0502a21d..f12ef1b2 100644 --- a/src/markdown/plugins/component.ts +++ b/src/node/markdown/plugins/component.ts @@ -32,12 +32,7 @@ export const componentPlugin = (md: MarkdownIt) => { md.block.ruler.at('html_block', htmlBlock) } -const htmlBlock: RuleBlock = ( - state, - startLine, - endLine, - silent -): boolean => { +const htmlBlock: RuleBlock = (state, startLine, endLine, silent): boolean => { let i, nextLine, lineText let pos = state.bMarks[startLine] + state.tShift[startLine] let max = state.eMarks[startLine] diff --git a/src/markdown/plugins/containers.ts b/src/node/markdown/plugins/containers.ts similarity index 100% rename from src/markdown/plugins/containers.ts rename to src/node/markdown/plugins/containers.ts diff --git a/src/markdown/plugins/header.ts b/src/node/markdown/plugins/header.ts similarity index 76% rename from src/markdown/plugins/header.ts rename to src/node/markdown/plugins/header.ts index 9187ec78..df44d2ca 100644 --- a/src/markdown/plugins/header.ts +++ b/src/node/markdown/plugins/header.ts @@ -3,23 +3,14 @@ import { MarkdownParsedData } from '../markdown' import { deeplyParseHeader } from '../../utils/parseHeader' import { slugify } from './slugify' -export interface Header { - level: number - title: string - slug: string -} - -export const extractHeaderPlugin = ( - md: MarkdownIt & { __data: MarkdownParsedData }, - include = ['h2', 'h3'] -) => { +export const extractHeaderPlugin = (md: MarkdownIt, include = ['h2', 'h3']) => { md.renderer.rules.heading_open = (tokens, i, options, env, self) => { const token = tokens[i] if (include.includes(token.tag)) { const title = tokens[i + 1].content const idAttr = token.attrs!.find(([name]) => name === 'id') const slug = idAttr && idAttr[1] - const data = md.__data + const data = (md as any).__data as MarkdownParsedData const headers = data.headers || (data.headers = []) headers.push({ level: parseInt(token.tag.slice(1), 10), diff --git a/src/markdown/plugins/highlight.ts b/src/node/markdown/plugins/highlight.ts similarity index 100% rename from src/markdown/plugins/highlight.ts rename to src/node/markdown/plugins/highlight.ts diff --git a/src/markdown/plugins/highlightLines.ts b/src/node/markdown/plugins/highlightLines.ts similarity index 59% rename from src/markdown/plugins/highlightLines.ts rename to src/node/markdown/plugins/highlightLines.ts index e882be6c..501da1fc 100644 --- a/src/markdown/plugins/highlightLines.ts +++ b/src/node/markdown/plugins/highlightLines.ts @@ -21,29 +21,31 @@ export const highlightLinePlugin = (md: MarkdownIt) => { const lineNumbers = RE.exec(rawInfo)![1] .split(',') - .map(v => v.split('-').map(v => parseInt(v, 10))) + .map((v) => v.split('-').map((v) => parseInt(v, 10))) const code = options.highlight ? options.highlight(token.content, langName) : token.content const rawCode = code.replace(wrapperRE, '') - const highlightLinesCode = rawCode.split('\n').map((split, index) => { - const lineNumber = index + 1 - const inRange = lineNumbers.some(([start, end]) => { - if (start && end) { - return lineNumber >= start && lineNumber <= end + const highlightLinesCode = rawCode + .split('\n') + .map((split, index) => { + const lineNumber = index + 1 + const inRange = lineNumbers.some(([start, end]) => { + if (start && end) { + return lineNumber >= start && lineNumber <= end + } + return lineNumber === start + }) + if (inRange) { + return `
 
` } - return lineNumber === start + return '
' }) - if (inRange) { - return `
 
` - } - return '
' - }).join('') - - const highlightLinesWrapperCode = - `
${highlightLinesCode}
` + .join('') + + const highlightLinesWrapperCode = `
${highlightLinesCode}
` return highlightLinesWrapperCode + code } diff --git a/src/markdown/plugins/hoist.ts b/src/node/markdown/plugins/hoist.ts similarity index 72% rename from src/markdown/plugins/hoist.ts rename to src/node/markdown/plugins/hoist.ts index 43007842..2a152d0e 100644 --- a/src/markdown/plugins/hoist.ts +++ b/src/node/markdown/plugins/hoist.ts @@ -3,12 +3,13 @@ import { MarkdownParsedData } from '../markdown' // hoist