Merge branch 'main' into fix/02

pull/364/head
Evan You 4 years ago committed by GitHub
commit 7fc213c9bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,15 +9,25 @@ jobs:
matrix:
node-version: [14, 16]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
- name: Checkout
uses: actions/checkout@v2
- name: Install pnpm
uses: pnpm/action-setup@v2.0.1
with:
version: 6.15.1
- name: Set node version to ${{ matrix.node_version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: install and test
run: |
yarn install
yarn test
- name: build
run: |
yarn build
node-version: ${{ matrix.node_version }}
cache: "pnpm"
- name: Install deps
run: pnpm install
- name: Build
run: pnpm run build
- name: Test
run: pnpm run test

@ -1,3 +1,86 @@
## [0.19.2](https://github.com/vuejs/vitepress/compare/v0.19.1...v0.19.2) (2021-09-28)
### Bug Fixes
- encode urls that conflict w/ vite built-in replacements ([3940625](https://github.com/vuejs/vitepress/commit/3940625121455b7ad6e5ea8ebb3e1cf2faf9c7fc))
## [0.19.1](https://github.com/vuejs/vitepress/compare/v0.19.0...v0.19.1) (2021-09-21)
- Fix build
# [0.19.0](https://github.com/vuejs/vitepress/compare/v0.18.1...v0.19.0) (2021-09-21)
### Features
- upgrade vue, simplify deps ([9030486](https://github.com/vuejs/vitepress/commit/9030486409f10a59115d874b9365f71348ed76c2))
- use `markdown-it-attrs` for markdown-it plugins ([#393](https://github.com/vuejs/vitepress/issues/393)) ([610e9b7](https://github.com/vuejs/vitepress/commit/610e9b7111462d3aace878017fa4d359cd2ae7ea))
## [0.18.1](https://github.com/vuejs/vitepress/compare/v0.18.0...v0.18.1) (2021-09-16)
### Bug Fixes
- ensure stable pages entry order across builds ([929bcf5](https://github.com/vuejs/vitepress/commit/929bcf50ee634d9fe73adbe2aae5f7038b048e5a))
# [0.18.0](https://github.com/vuejs/vitepress/compare/v0.17.3...v0.18.0) (2021-09-14)
### Features
- map mode + remove deprecated options ([b94b163](https://github.com/vuejs/vitepress/commit/b94b163a3a931fe03e69547391d6ac22eb41b789))
- support `<script client>` in mpa mode ([e0b6997](https://github.com/vuejs/vitepress/commit/e0b69973f840bfa281fae209da1f1c674c1301a8))
## [0.17.3](https://github.com/vuejs/vitepress/compare/v0.17.2...v0.17.3) (2021-09-09)
### Bug Fixes
- emit prevented hashchange event ([4fb387d](https://github.com/vuejs/vitepress/commit/4fb387d94ea9d7ae28a871929cbbc57e931b8d7a))
## [0.17.2](https://github.com/vuejs/vitepress/compare/v0.17.1...v0.17.2) (2021-09-08)
### Bug Fixes
- improve fs allow ([2e9264f](https://github.com/vuejs/vitepress/commit/2e9264f03259354e7739e2a56a7c1306fb167843))
### Features
- support config.extends ([f749b27](https://github.com/vuejs/vitepress/commit/f749b272d4603a3b8eaf251b0feebe2d33da3983))
## [0.17.1](https://github.com/vuejs/vitepress/compare/v0.17.0...v0.17.1) (2021-09-08)
### Bug Fixes
- avoid using spread for client code ([03abee7](https://github.com/vuejs/vitepress/commit/03abee7f7c0fac95806f31ff5761b9e912a1f232))
- **default-theme:** use description as tagline by default ([b94c827](https://github.com/vuejs/vitepress/commit/b94c82710a7b230a918790ac0b6aa1d2f5afc1c3))
- handle case when there is no themeConfig ([034c737](https://github.com/vuejs/vitepress/commit/034c7375ad2de4b42c0ac861c2dd18183511771d))
### Performance Improvements
- minor optimizations ([96bcdda](https://github.com/vuejs/vitepress/commit/96bcddabedac9af4e1c817ed651bb4ce692c75e7))
# [0.17.0](https://github.com/vuejs/vitepress/compare/v0.16.1...v0.17.0) (2021-08-31)
### Bug Fixes
- allow vite server access to theme and local files ([9b9fdc7](https://github.com/vuejs/vitepress/commit/9b9fdc710a6cedb3e278805eb07bed669ca2075e))
- **code:** code block highlight bug in ul ([#352](https://github.com/vuejs/vitepress/issues/352)) ([9245226](https://github.com/vuejs/vitepress/commit/9245226b16f6113c722e5e8c7b876bea1cf1c255))
- **css:** remove 720px breakpoint in home layout ([#347](https://github.com/vuejs/vitepress/issues/347)) ([0c1a1f2](https://github.com/vuejs/vitepress/commit/0c1a1f2ef43cd7d995f3e9d43f19be8b3f961cb1))
- **i18n:** fix locales reading, add site.langs ([#353](https://github.com/vuejs/vitepress/issues/353)) ([bc78adb](https://github.com/vuejs/vitepress/commit/bc78adb468bce8ce2d4e2543423adacc9351cf51)), closes [/vuepress.vuejs.org/guide/i18n.html#site-level-i18](https://github.com//vuepress.vuejs.org/guide/i18n.html/issues/site-level-i18) [/v2.vuepress.vuejs.org/guide/i18n.html#site-i18](https://github.com//v2.vuepress.vuejs.org/guide/i18n.html/issues/site-i18)
- include emoji text in nav link to match toc ([#284](https://github.com/vuejs/vitepress/issues/284)) ([80ff360](https://github.com/vuejs/vitepress/commit/80ff36066ef6a4ed4a18548993bc5d8d9a6dab58))
- use useData() instead of $site ([#365](https://github.com/vuejs/vitepress/issues/365)) ([1e64773](https://github.com/vuejs/vitepress/commit/1e6477393308a5d8bd03a614cecf9573466f6e6c))
### Features
- support function config ([e74c5f0](https://github.com/vuejs/vitepress/commit/e74c5f06d1d5890fad6dd728df9bf85dcfda87d1))
- support partial include directive ([7b3a9e5](https://github.com/vuejs/vitepress/commit/7b3a9e59b44e9e354692eed6c1ca453be9cb7a86))
- upgrade markdown-it-anchor ([#350](https://github.com/vuejs/vitepress/issues/350)) ([26b5aa9](https://github.com/vuejs/vitepress/commit/26b5aa931f1935bd67dcd1d511461ff5fa8a00ec))
### BREAKING CHANGES
- the `markdown.anchor` option is updated. Refer to
valeriangalliat/markdown-it-anchor#permalinks for
instructions to upgrade your existing `markdown.anchor.permalink`
option. **This doesn't affect you if you weren't changing the header
permalinks behavior**.
## [0.16.1](https://github.com/vuejs/vitepress/compare/v0.16.0...v0.16.1) (2021-08-11)
### Features

@ -5,11 +5,7 @@
---
**:fire: Note this is early WIP! Currently the focus is on making Vite stable and feature complete first. It is not recommended to use this for anything serious yet.**
---
VitePress is [VuePress](http://vuepress.vuejs.org/)' little brother, built on top of [vite](https://github.com/vuejs/vite).
VitePress is [VuePress](http://vuepress.vuejs.org/)' spiritual successor, built on top of [vite](https://github.com/vuejs/vite).
## Documentation
@ -17,7 +13,7 @@ To check out docs, visit [vitepress.vuejs.org](https://vitepress.vuejs.org).
## Changelog
Detailed changes for each release are documented in the [release notes](https://github.com/vuejs/vitepress/releases).
Detailed changes for each release are documented in the [CHANGELOG](https://github.com/vuejs/vitepress/blob/master/CHANGELOG.md).
## Contribution

@ -0,0 +1,53 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"projectFolder": ".",
"mainEntryPointFilePath": "./dist/temp/index.d.ts",
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "./dist/client/index.d.ts"
},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning",
"addToApiReportFile": true
},
"ae-missing-release-tag": {
"logLevel": "none"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
},
"tsdoc-undefined-tag": {
"logLevel": "none"
}
}
}
}

@ -0,0 +1,53 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"projectFolder": ".",
"mainEntryPointFilePath": "./dist/temp/index.d.ts",
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "./dist/node/index.d.ts"
},
"apiReport": {
"enabled": false
},
"docModel": {
"enabled": false
},
"tsdocMetadata": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "warning",
"addToApiReportFile": true
},
"ae-missing-release-tag": {
"logLevel": "none"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "warning"
},
"tsdoc-undefined-tag": {
"logLevel": "none"
}
}
}
}

@ -82,7 +82,7 @@ The `<Content/>` component displays the rendered markdown contents. Useful [when
### `<ClientOnly/>`
The `<ClientOnly/>` component renderes its slot only at client side.
The `<ClientOnly/>` component renders its slot only at client side.
Because VitePress applications are server-rendered in Node.js when generating static builds, any Vue usage must conform to the universal code requirements. In short, make sure to only access Browser / DOM APIs in beforeMount or mounted hooks.

@ -202,7 +202,7 @@ pages:
3. Deploy to surge by typing `surge docs/.vitepress/dist`.
You can also deploy to a [custom domain](http://surge.sh/help/adding-a-custom-domain) by adding `surge docs/.vitepress/dist yourdomain.com`.
You can also deploy to a [custom domain](https://surge.sh/help/adding-a-custom-domain) by adding `surge docs/.vitepress/dist yourdomain.com`.
## Heroku

@ -60,7 +60,6 @@ Outbound links automatically get `target="_blank" rel="noopener noreferrer"`:
title: Blogging Like a Hacker
lang: en-US
---
```
This data will be available to the rest of the page, along with all custom and theming components.
@ -404,12 +403,17 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`:
```js
const anchor = require('markdown-it-anchor')
module.exports = {
markdown: {
// options for markdown-it-anchor
anchor: { permalink: false },
// https://github.com/valeriangalliat/markdown-it-anchor#permalinks
anchor: {
permalink: anchor.permalink.headerLink()
},
// options for markdown-it-toc
// options for markdown-it-table-of-contents
toc: { includeLevel: [1, 2] },
config: (md) => {

@ -31,7 +31,7 @@ interface EnhanceAppContext {
}
```
The theme entry file shoud export the theme as its default export:
The theme entry file should export the theme as its default export:
```js
// .vitepress/theme/index.js
@ -47,7 +47,7 @@ export default {
}
```
...where the `Layout` component could like this:
...where the `Layout` component could look like this:
```vue
<!-- .vitepress/theme/Layout.vue -->

@ -2,8 +2,11 @@
"private": true,
"name": "vitepress-example-minimal",
"scripts": {
"dev": "node ../../bin/vitepress dev",
"build": "node ../../bin/vitepress build",
"serve": "node ../../bin/vitepress serve"
"dev": "vitepress dev",
"build": "vitepress build",
"serve": "vitepress serve"
},
"devDependencies": {
"vitepress": "workspace:*"
}
}

@ -0,0 +1,7 @@
[build.environment]
NODE_VERSION = "16"
NPM_FLAGS = "--version" # prevent Netlify npm install
[build]
publish = "docs/.vitepress/dist"
command = "npx pnpm i --store=node_modules/.pnpm-store && npm run ci-docs"

@ -1,6 +1,6 @@
{
"name": "vitepress",
"version": "0.16.1",
"version": "0.19.2",
"description": "Vite & Vue powered static site generator",
"main": "dist/node/index.js",
"typings": "types/index.d.ts",
@ -9,19 +9,24 @@
},
"files": [
"bin",
"lib",
"dist",
"types"
],
"scripts": {
"dev": "yarn dev-shared && yarn dev-start",
"dev": "pnpm run dev-shared && pnpm run dev-start",
"dev-start": "run-p dev-client dev-node dev-watch",
"dev-client": "tsc -w -p src/client",
"dev-node": "tsc -w -p src/node",
"dev-shared": "node scripts/copyShared",
"dev-watch": "node scripts/watchAndCopy",
"build": "rimraf -rf dist && node scripts/copyShared && tsc -p src/client && tsc -p src/node && node scripts/copyClient",
"lint": "yarn lint:js && yarn lint:ts",
"build": "run-s build-prepare build-client build-node build-types",
"build-prepare": "rimraf -rf dist && node scripts/copyShared",
"build-client": "tsc -p src/client && node scripts/copyClient",
"build-node": "rollup -c scripts/rollup.config.js",
"build-types": "run-s build-types-client build-types-node",
"build-types-client": "tsc -p src/client --declaration --emitDeclarationOnly --outDir dist/temp && api-extractor run -c api-extractor.client.json && rimraf dist/temp",
"build-types-node": "tsc -p src/node --declaration --emitDeclarationOnly --outDir dist/temp && api-extractor run -c api-extractor.node.json && rimraf dist/temp",
"lint": "pnpm run lint:js && pnpm run lint:ts",
"lint:js": "prettier --check --write \"{bin,docs,scripts,src}/**/*.js\"",
"lint:ts": "prettier --check --write --parser typescript \"{__tests__,src,docs,types}/**/*.ts\"",
"test": "jest",
@ -30,8 +35,9 @@
"docs": "run-p dev docs-dev",
"docs-dev": "node ./bin/vitepress dev docs",
"docs-debug": "node --inspect-brk ./bin/vitepress dev docs",
"docs-build": "yarn build && node ./bin/vitepress build docs",
"docs-serve": "node ./bin/vitepress serve docs"
"docs-build": "pnpm run build && node ./bin/vitepress build docs",
"docs-serve": "node ./bin/vitepress serve docs",
"ci-docs": "run-s build docs-build"
},
"engines": {
"node": ">=12.0.0"
@ -65,52 +71,62 @@
"dependencies": {
"@docsearch/css": "^1.0.0-alpha.28",
"@docsearch/js": "^1.0.0-alpha.28",
"@types/markdown-it": "^12.0.1",
"@vitejs/plugin-vue": "^1.4.0",
"@vue/compiler-sfc": "^3.2.1",
"@vue/server-renderer": "^3.2.1",
"chalk": "^4.1.1",
"compression": "^1.7.4",
"debug": "^4.3.2",
"diacritics": "^1.3.0",
"escape-html": "^1.0.3",
"fs-extra": "^10.0.0",
"globby": "^11.0.3",
"gray-matter": "^4.0.3",
"lru-cache": "^6.0.0",
"markdown-it": "^12.0.6",
"markdown-it-anchor": "^7.1.0",
"markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.0",
"markdown-it-table-of-contents": "^0.5.2",
"minimist": "^1.2.5",
"ora": "^5.4.0",
"polka": "^0.5.2",
"@vitejs/plugin-vue": "^1.9.0",
"@vue/runtime-dom": "^3.2.18",
"prismjs": "^1.23.0",
"sirv": "^1.0.12",
"vite": "^2.4.4",
"vue": "^3.2.1"
"vite": "^2.5.0",
"vue": "^3.2.13"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.18.9",
"@rollup/plugin-alias": "^3.1.5",
"@rollup/plugin-commonjs": "^20.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4",
"@types/compression": "^1.7.0",
"@types/debug": "^4.1.7",
"@types/fs-extra": "^9.0.11",
"@types/jest": "^26.0.23",
"@types/koa": "^2.13.1",
"@types/koa-static": "^4.0.1",
"@types/lru-cache": "^5.1.0",
"@types/markdown-it": "^12.0.1",
"@types/node": "^15.6.1",
"@types/polka": "^0.5.3",
"@types/postcss-load-config": "^3.0.1",
"chalk": "^4.1.1",
"chokidar": "^3.5.1",
"compression": "^1.7.4",
"conventional-changelog-cli": "^2.1.1",
"debug": "^4.3.2",
"diacritics": "^1.3.0",
"enquirer": "^2.3.6",
"esbuild": "^0.12.28",
"escape-html": "^1.0.3",
"execa": "^5.0.0",
"fs-extra": "^10.0.0",
"globby": "^11.0.3",
"gray-matter": "^4.0.3",
"jest": "^27.0.1",
"lint-staged": "^11.0.0",
"lru-cache": "^6.0.0",
"markdown-it": "^12.0.6",
"markdown-it-anchor": "^8.1.2",
"markdown-it-attrs": "^4.0.0",
"markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.0",
"markdown-it-table-of-contents": "^0.5.2",
"minimist": "^1.2.5",
"npm-run-all": "^4.1.5",
"ora": "^5.4.0",
"pnpm": "^6.15.1",
"polka": "^0.5.2",
"prettier": "^2.3.0",
"rimraf": "^3.0.2",
"rollup": "^2.38.5",
"rollup": "^2.56.3",
"rollup-plugin-esbuild": "^4.5.0",
"semver": "^7.3.5",
"sirv": "^1.0.12",
"ts-jest": "^27.0.1",
"typescript": "^4.3.2",
"yorkie": "^2.0.0"

File diff suppressed because it is too large Load Diff

@ -0,0 +1,3 @@
packages:
- examples/*
- docs

@ -57,12 +57,12 @@ async function main() {
// Build the package.
step('\nBuilding the package...')
await run('yarn', ['build'])
await run('pnpm', ['build'])
// Generate the changelog.
step('\nGenerating the changelog...')
await run('yarn', ['changelog'])
await run('yarn', ['prettier', '--write', 'CHANGELOG.md'])
await run('pnpm', ['changelog'])
await run('pnpm', ['prettier', '--write', 'CHANGELOG.md'])
const { yes: changelogOk } = await prompt({
type: 'confirm',
@ -82,13 +82,7 @@ async function main() {
// Publish the package.
step('\nPublishing the package...')
await run('yarn', [
'publish',
'--new-version',
targetVersion,
'--no-commit-hooks',
'--no-git-tag-version'
])
await run('pnpm', ['publish', '--ignore-scripts', '--no-git-checks'])
// Push to GitHub.
step('\nPushing to GitHub...')

@ -0,0 +1,35 @@
import { defineConfig } from 'rollup'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import esbuild from 'rollup-plugin-esbuild'
import json from '@rollup/plugin-json'
import alias from '@rollup/plugin-alias'
import { resolve } from 'path'
const r = (p) => resolve(__dirname, '../', p)
const pkg = require('../package.json')
export default defineConfig({
input: [r('src/node/index.ts'), r('src/node/cli.ts')],
output: {
format: 'cjs',
dir: r('dist/node')
},
external: [...Object.keys(pkg.dependencies), 'buffer', 'punycode'],
plugins: [
alias({
entries: {
'readable-stream': 'stream'
}
}),
commonjs(),
nodeResolve(),
esbuild({
target: 'node12'
}),
json()
],
onwarn(warning, warn) {
if (warning.code !== 'EVAL') warn(warning)
}
})

@ -1,7 +1,9 @@
const fs = require('fs-extra')
const chokidar = require('chokidar')
const { normalizePath } = require('vite')
function toClientAndNode(method, file) {
file = normalizePath(file)
if (method === 'copy') {
fs.copy(file, file.replace(/^src\/shared\//, 'src/node/'))
fs.copy(file, file.replace(/^src\/shared\//, 'src/client/'))
@ -12,7 +14,7 @@ function toClientAndNode(method, file) {
}
function toDist(file) {
return file.replace(/^src\//, 'dist/')
return normalizePath(file).replace(/^src\//, 'dist/')
}
// copy shared files to the client and node directory whenever they change.

@ -1,13 +1,16 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref, watch, reactive } from 'vue'
import { useData } from '../data'
const data = useData()
const el = ref<HTMLElement | null>(null)
const open = ref(false)
// FIXME: remove in next Vue release
const tempData = reactive(data)
watch(open, (value) => {
if (value === false) {
if (!value) {
el.value!.scrollTop = 0
}
})
@ -16,7 +19,7 @@ watch(open, (value) => {
<template>
<div class="debug" :class="{ open }" ref="el" @click="open = !open">
<p class="title">Debug</p>
<pre class="block">{{ data }}</pre>
<pre class="block">{{ tempData }}</pre>
</div>
</template>

@ -1,4 +1,4 @@
import { InjectionKey, Ref, ref, readonly, computed, inject } from 'vue'
import { InjectionKey, Ref, shallowRef, readonly, computed, inject } from 'vue'
import { Route } from './router'
import serializedSiteData from '@siteData'
import { resolveSiteDataByRoute, PageData, SiteData } from '../shared'
@ -20,7 +20,7 @@ export interface VitePressData<T = any> {
// site data is a singleton
export type SiteDataRef<T = any> = Ref<SiteData<T>>
export const siteDataRef: Ref<SiteData> = ref(parse(serializedSiteData))
export const siteDataRef: Ref<SiteData> = shallowRef(parse(serializedSiteData))
function parse(data: string): SiteData {
return readonly(JSON.parse(data)) as SiteData
@ -46,9 +46,11 @@ export function initData(route: Route): VitePressData {
frontmatter: computed(() => route.data.frontmatter),
lang: computed(() => site.value.lang),
localePath: computed(() => {
const { locales, lang } = site.value
const path = Object.keys(locales).find((lp) => locales[lp].lang === lang)
return withBase((locales && path) || '/')
const { langs, lang } = site.value
const path = Object.keys(langs).find(
(langPath) => langs[langPath].lang === lang
)
return withBase(path || '/')
}),
title: computed(() => {
return route.data.title

@ -77,7 +77,9 @@ export function createRouter(
route.path = pendingPath
route.component = markRaw(comp)
route.data = readonly(JSON.parse(__pageData)) as PageData
route.data = import.meta.env.PROD
? markRaw(JSON.parse(__pageData))
: (readonly(JSON.parse(__pageData)) as PageData)
if (inBrowser) {
nextTick(() => {
@ -94,7 +96,7 @@ export function createRouter(
})
}
}
} catch (err) {
} catch (err: any) {
if (!err.message.match(/fetch/)) {
console.error(err)
}
@ -131,6 +133,8 @@ export function createRouter(
// scroll between hash anchors in the same page
if (hash && hash !== currentUrl.hash) {
history.pushState(null, '', hash)
// still emit the event so we can listen to it in themes
window.dispatchEvent(new Event('hashchange'))
// use smooth scroll when clicking on header anchor links
scrollTo(link, hash, link.classList.contains('header-anchor'))
}

@ -3,9 +3,17 @@
// generic types
export type { Router, Route } from './app/router'
export type { VitePressData } from './app/data'
// theme types
export type { Theme, EnhanceAppContext } from './app/theme'
// shared types
export type {
PageData,
SiteData,
HeadConfig,
Header,
LocaleConfig
} from '../../types/shared'
// composables
export { useData } from './app/data'

@ -32,9 +32,7 @@ const isCustomLayout = computed(() => !!frontmatter.value.customLayout)
const enableHome = computed(() => !!frontmatter.value.home)
// automatic multilang check for AlgoliaSearchBox
const isMultiLang = computed(
() => Object.keys(theme.value.locales || {}).length > 0
)
const isMultiLang = computed(() => Object.keys(site.value.langs).length > 1)
// navbar
const showNavbar = computed(() => {

@ -2,11 +2,14 @@
<div class="theme">
<h1>404</h1>
<blockquote>{{ getMsg() }}</blockquote>
<a :href="$site.base" aria-label="go to home">Take me home.</a>
<a :href="site.base" aria-label="go to home">Take me home.</a>
</div>
</template>
<script setup lang="ts">
import { useData } from 'vitepress'
const { site } = useData()
const msgs = [
`There's nothing here.`,
`How did we get here?`,

@ -45,6 +45,10 @@ onMounted(() => {
function load() {
if (typeof _bsa !== 'undefined' && _bsa) {
const parent = document.querySelector('.bsa-cpc')!
// cleanup any existing ad to avoid them stacking
parent.innerHTML = ''
_bsa.init('default', code, `placement:${placement}`, {
target: '.bsa-cpc',
align: 'horizontal',

@ -24,6 +24,7 @@ onMounted(() => {
.carbon-ads {
border-radius: 4px;
margin: 0 auto;
padding: 8px;
max-width: 280px;
font-size: 0.75rem;
background-color: var(--c-bg-accent);
@ -41,7 +42,6 @@ onMounted(() => {
z-index: 1;
float: right;
margin: -8px -8px 24px 24px;
padding: 8px;
width: 146px;
max-width: 100%;
min-height: 200px;

@ -28,11 +28,4 @@ import HomeFooter from './HomeFooter.vue'
margin: 0px auto;
padding: 0 1.5rem;
}
@media (max-width: 720px) {
.home-content {
max-width: 392px;
padding: 0;
}
}
</style>

@ -6,17 +6,15 @@ import NavLink from './NavLink.vue'
const { site, frontmatter } = useData()
const showHero = computed(() => {
const {
heroImage,
heroText,
tagline,
actionLink,
actionText
} = frontmatter.value
const { heroImage, heroText, tagline, actionLink, actionText } =
frontmatter.value
return heroImage || heroText || tagline || (actionLink && actionText)
})
const heroText = computed(() => frontmatter.value.heroText || site.value.title)
const tagline = computed(
() => frontmatter.value.tagline || site.value.description
)
</script>
<template>
@ -30,9 +28,7 @@ const heroText = computed(() => frontmatter.value.heroText || site.value.title)
</figure>
<h1 v-if="heroText" id="main-title" class="title">{{ heroText }}</h1>
<p v-if="frontmatter.tagline" class="description">
{{ frontmatter.tagline }}
</p>
<p v-if="tagline" class="tagline">{{ tagline }}</p>
<NavLink
v-if="frontmatter.actionLink && frontmatter.actionText"
@ -99,7 +95,7 @@ const heroText = computed(() => frontmatter.value.heroText || site.value.title)
}
}
.description {
.tagline {
margin: 0;
margin-top: 0.25rem;
line-height: 1.3;
@ -108,7 +104,7 @@ const heroText = computed(() => frontmatter.value.heroText || site.value.title)
}
@media (min-width: 420px) {
.description {
.tagline {
line-height: 1.2;
font-size: 1.6rem;
}

@ -1,13 +1,13 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from 'vitepress'
import { useLocaleLinks } from '../composables/nav'
import { useLanguageLinks } from '../composables/nav'
import { useRepo } from '../composables/repo'
import NavLink from './NavLink.vue'
import NavDropdownLink from './NavDropdownLink.vue'
const { theme } = useData()
const localeLinks = useLocaleLinks()
const localeLinks = useLanguageLinks()
const repo = useRepo()
const show = computed(() => theme.value.nav || repo.value || localeLinks.value)
</script>

@ -1,57 +1,30 @@
import { computed } from 'vue'
import { useRoute, useData, inBrowser } from 'vitepress'
import { useData, useRoute } from 'vitepress'
import type { DefaultTheme } from '../config'
export function useLocaleLinks() {
const route = useRoute()
const { site } = useData()
export function useLanguageLinks() {
const { site, localePath, theme } = useData()
return computed(() => {
const theme = site.value.themeConfig as DefaultTheme.Config
const locales = theme.locales
const langs = site.value.langs
const localePaths = Object.keys(langs)
if (!locales) {
// one language
if (localePaths.length < 2) {
return null
}
const localeKeys = Object.keys(locales)
const route = useRoute()
if (localeKeys.length <= 1) {
return null
}
// handle site base
const siteBase = inBrowser ? site.value.base : '/'
const siteBaseWithoutSuffix = siteBase.endsWith('/')
? siteBase.slice(0, -1)
: siteBase
// remove site base in browser env
const routerPath = route.path.slice(siteBaseWithoutSuffix.length)
const currentLangBase = localeKeys.find((key) => {
return key === '/' ? false : routerPath.startsWith(key)
})
const currentContentPath = currentLangBase
? routerPath.substring(currentLangBase.length - 1)
: routerPath
const candidates = localeKeys.map((v) => {
const localePath = v.endsWith('/') ? v.slice(0, -1) : v
return {
text: locales[v].label,
link: `${localePath}${currentContentPath}`
}
})
// intentionally remove the leading slash because each locale has one
const currentPath = route.path.replace(localePath.value, '')
const currentLangKey = currentLangBase ? currentLangBase : '/'
const candidates = localePaths.map((localePath) => ({
text: langs[localePath].label,
link: `${localePath}${currentPath}`
}))
const selectText = locales[currentLangKey].selectText
? locales[currentLangKey].selectText
: 'Languages'
const selectText = theme.value.selectText || 'Languages'
return {
text: selectText,

@ -26,6 +26,7 @@ div[class*='language-'] {
li > div[class*='language-'] {
border-radius: 6px 0 0 6px;
margin: 1rem -1.5rem 1rem -1.25rem;
line-height: initial;
}
@media (min-width: 420px) {

@ -2,12 +2,10 @@ import path from 'path'
import { Alias, AliasOptions } from 'vite'
const PKG_ROOT = path.join(__dirname, '../../')
export const APP_PATH = path.join(__dirname, '../client/app')
export const SHARED_PATH = path.join(__dirname, '../client/shared')
export const DEFAULT_THEME_PATH = path.join(
__dirname,
'../client/theme-default'
)
export const DIST_CLIENT_PATH = path.join(__dirname, '../client')
export const APP_PATH = path.join(DIST_CLIENT_PATH, 'app')
export const SHARED_PATH = path.join(DIST_CLIENT_PATH, 'shared')
export const DEFAULT_THEME_PATH = path.join(DIST_CLIENT_PATH, 'theme-default')
// special virtual file
// we can't directly import '/@siteData' because

@ -6,14 +6,22 @@ import { renderPage } from './render'
import { OutputChunk, OutputAsset } from 'rollup'
import ora from 'ora'
export async function build(root: string, buildOptions: BuildOptions = {}) {
export async function build(
root: string,
buildOptions: BuildOptions & { mpa?: string } = {}
) {
const start = Date.now()
process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(root)
if (buildOptions.mpa) {
siteConfig.mpa = true
delete buildOptions.mpa
}
try {
const [clientResult, , pageToHashMap] = await bundle(
const { clientResult, serverResult, pageToHashMap } = await bundle(
siteConfig,
buildOptions
)
@ -22,11 +30,15 @@ export async function build(root: string, buildOptions: BuildOptions = {}) {
spinner.start('rendering pages...')
try {
const appChunk = clientResult.output.find(
(chunk) => chunk.type === 'chunk' && chunk.isEntry
) as OutputChunk
const appChunk =
clientResult &&
(clientResult.output.find(
(chunk) => chunk.type === 'chunk' && chunk.isEntry
) as OutputChunk)
const cssChunk = clientResult.output.find(
const cssChunk = (
siteConfig.mpa ? serverResult : clientResult
).output.find(
(chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as OutputAsset

@ -0,0 +1,46 @@
import { build } from 'vite'
import { SiteConfig } from '..'
import { RollupOutput } from 'rollup'
const virtualEntry = 'client.js'
export async function buildMPAClient(
js: Record<string, string>,
config: SiteConfig
): Promise<RollupOutput> {
const files = Object.keys(js)
const themeFiles = files.filter((f) => !f.endsWith('.md'))
const pages = files.filter((f) => f.endsWith('.md'))
return build({
root: config.srcDir,
base: config.site.base,
logLevel: 'warn',
build: {
emptyOutDir: false,
outDir: config.outDir,
rollupOptions: {
input: [virtualEntry, ...pages]
}
},
plugins: [
{
name: 'vitepress-mpa-client',
resolveId(id) {
if (id === virtualEntry) {
return id
}
},
load(id) {
if (id === virtualEntry) {
return themeFiles
.map((file) => `import ${JSON.stringify(file)}`)
.join('\n')
} else if (id in js) {
return js[id]
}
}
}
]
}) as Promise<RollupOutput>
}

@ -1,11 +1,13 @@
import ora from 'ora'
import path from 'path'
import fs from 'fs-extra'
import { slash } from '../utils/slash'
import { APP_PATH } from '../alias'
import { SiteConfig } from '../config'
import { RollupOutput } from 'rollup'
import { build, BuildOptions, UserConfig as ViteUserConfig } from 'vite'
import { createVitePressPlugin } from '../plugin'
import { buildMPAClient } from './buildMPAClient'
export const okMark = '\x1b[32m✓\x1b[0m'
export const failMark = '\x1b[31m✖\x1b[0m'
@ -14,9 +16,14 @@ export const failMark = '\x1b[31m✖\x1b[0m'
export async function bundle(
config: SiteConfig,
options: BuildOptions
): Promise<[RollupOutput, RollupOutput, Record<string, string>]> {
): Promise<{
clientResult: RollupOutput
serverResult: RollupOutput
pageToHashMap: Record<string, string>
}> {
const { root, srcDir } = config
const pageToHashMap = Object.create(null)
const clientJSMap = Object.create(null)
// define custom rollup input
// this is a multi-entry build - every page is considered an entry chunk
@ -38,7 +45,13 @@ export async function bundle(
root: srcDir,
base: config.site.base,
logLevel: 'warn',
plugins: createVitePressPlugin(root, config, ssr, pageToHashMap),
plugins: createVitePressPlugin(
root,
config,
ssr,
pageToHashMap,
clientJSMap
),
// @ts-ignore
ssr: {
noExternal: ['vitepress']
@ -64,12 +77,15 @@ export async function bundle(
if (!chunk.isEntry && /runtime/.test(chunk.name)) {
return `assets/framework.[hash].js`
}
return `assets/[name].[hash].js`
return adComponentRE.test(chunk.name)
? `assets/ui-custom.[hash].js`
: `assets/[name].[hash].js`
}
})
}
},
minify: ssr ? false : !process.env.DEBUG
// minify with esbuild in MPA mode (for CSS)
minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG
}
})
@ -80,7 +96,7 @@ export async function bundle(
spinner.start('building client + server bundles...')
try {
;[clientResult, serverResult] = await (Promise.all([
build(resolveViteConfig(false)),
config.mpa ? null : build(resolveViteConfig(false)),
build(resolveViteConfig(true))
]) as Promise<[RollupOutput, RollupOutput]>)
} catch (e) {
@ -93,5 +109,28 @@ export async function bundle(
symbol: okMark
})
return [clientResult, serverResult, pageToHashMap]
if (config.mpa) {
// in MPA mode, we need to copy over the non-js asset files from the
// server build since there is no client-side build.
for (const chunk of serverResult.output) {
if (!chunk.fileName.endsWith('.js')) {
const tempPath = path.resolve(config.tempDir, chunk.fileName)
const outPath = path.resolve(config.outDir, chunk.fileName)
await fs.copy(tempPath, outPath)
}
}
// also copy over public dir
const publicDir = path.resolve(config.srcDir, 'public')
if (fs.existsSync(publicDir)) {
await fs.copy(publicDir, config.outDir)
}
// build <script client> bundle
if (Object.keys(clientJSMap).length) {
clientResult = (await buildMPAClient(clientJSMap, config)) as RollupOutput
}
}
return { clientResult, serverResult, pageToHashMap }
}
const adComponentRE = /(?:Carbon|BuySell)Ads/

@ -4,15 +4,15 @@ import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { HeadConfig } from '../shared'
import { normalizePath } from 'vite'
import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
const escape = require('escape-html')
import { slash } from '../utils/slash'
import escape from 'escape-html'
export async function renderPage(
config: SiteConfig,
page: string, // foo.md
result: RollupOutput,
appChunk: OutputChunk,
cssChunk: OutputAsset,
result: RollupOutput | null,
appChunk: OutputChunk | undefined,
cssChunk: OutputAsset | undefined,
pageToHashMap: Record<string, string>,
hashMapString: string
) {
@ -22,7 +22,7 @@ export async function renderPage(
const siteData = resolveSiteDataByRoute(config.site, routePath)
router.go(routePath)
// lazy require server-renderer for production build
const content = await require('@vue/server-renderer').renderToString(app)
const content = await require('vue/server-renderer').renderToString(app)
const pageName = page.replace(/\//g, '_')
// server build doesn't need hash
@ -39,14 +39,22 @@ export async function renderPage(
))
const pageData = JSON.parse(__pageData)
const preloadLinks = [
// resolve imports for index.js + page.md.js and inject script tags for
// them as well so we fetch everything as early as possible without having
// to wait for entry chunks to parse
...resolvePageImports(config, page, result, appChunk),
pageClientJsFileName,
appChunk.fileName
]
const preloadLinks = (
config.mpa
? appChunk
? [appChunk.fileName]
: []
: result && appChunk
? [
// resolve imports for index.js + page.md.js and inject script tags for
// them as well so we fetch everything as early as possible without having
// to wait for entry chunks to parse
...resolvePageImports(config, page, result, appChunk),
pageClientJsFileName,
appChunk.fileName
]
: []
)
.map((file) => {
return `<link rel="modulepreload" href="${siteData.base}${file}">`
})
@ -67,6 +75,23 @@ export async function renderPage(
...filterOutHeadDescription(pageData.frontmatter.head)
)
let inlinedScript = ''
if (config.mpa && result) {
const matchingChunk = result.output.find(
(chunk) =>
chunk.type === 'chunk' &&
chunk.facadeModuleId === slash(path.join(config.srcDir, page))
) as OutputChunk
if (matchingChunk) {
if (!matchingChunk.code.includes('import')) {
inlinedScript = `<script type="module">${matchingChunk.code}</script>`
fs.removeSync(path.resolve(config.outDir, matchingChunk.fileName))
} else {
inlinedScript = `<script type="module" src="${siteData.base}${matchingChunk.fileName}"></script>`
}
}
}
const html = `
<!DOCTYPE html>
<html lang="${siteData.lang}">
@ -83,10 +108,17 @@ export async function renderPage(
</head>
<body>
<div id="app">${content}</div>
<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>
<script type="module" async src="${siteData.base}${
appChunk.fileName
}"></script>
${
config.mpa
? ''
: `<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>`
}
${
appChunk
? `<script type="module" async src="${siteData.base}${appChunk.fileName}"></script>`
: ``
}
${inlinedScript}
</body>
</html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))

@ -5,7 +5,6 @@ import { createServer, build, serve } from '.'
const argv: any = minimist(process.argv.slice(2))
console.log(chalk.cyan(`vitepress v${require('../../package.json').version}`))
console.log(chalk.cyan(`vite v${require('vite/package.json').version}`))
const command = argv._[0]
const root = argv._[command ? 1 : 0]

@ -2,17 +2,31 @@ import path from 'path'
import fs from 'fs-extra'
import chalk from 'chalk'
import globby from 'globby'
import { normalizePath, AliasOptions, UserConfig as ViteConfig } from 'vite'
import {
normalizePath,
AliasOptions,
UserConfig as ViteConfig,
mergeConfig as mergeViteConfig
} from 'vite'
import { Options as VuePluginOptions } from '@vitejs/plugin-vue'
import { SiteData, HeadConfig, LocaleConfig } from './shared'
import {
SiteData,
HeadConfig,
LocaleConfig,
createLangDictionary
} from './shared'
import { resolveAliases, APP_PATH, DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug'
export { resolveSiteDataByRoute } from './shared'
const debug = require('debug')('vitepress:config')
const debug = _debug('vitepress:config')
export type { MarkdownOptions }
export interface UserConfig<ThemeConfig = any> {
extends?: RawConfigExports
lang?: string
base?: string
title?: string
@ -22,28 +36,29 @@ export interface UserConfig<ThemeConfig = any> {
locales?: Record<string, LocaleConfig>
markdown?: MarkdownOptions
/**
* Opitons to pass on to @vitejs/plugin-vue
* Opitons to pass on to `@vitejs/plugin-vue`
*/
vue?: VuePluginOptions
/**
* Vite config
*/
vite?: ViteConfig
customData?: any
srcDir?: string
srcExclude?: string[]
/**
* @deprecated use `srcExclude` instead
*/
exclude?: string[]
/**
* @deprecated use `vue` instead
* Enable MPA / zero-JS mode
* @experimental
*/
vueOptions?: VuePluginOptions
mpa?: boolean
}
export type RawConfigExports =
| UserConfig
| Promise<UserConfig>
| (() => UserConfig | Promise<UserConfig>)
export interface SiteConfig<ThemeConfig = any> {
root: string
srcDir: string
@ -57,6 +72,7 @@ export interface SiteConfig<ThemeConfig = any> {
markdown: MarkdownOptions | undefined
vue: VuePluginOptions | undefined
vite: ViteConfig | undefined
mpa: boolean
}
const resolve = (root: string, file: string) =>
@ -66,22 +82,7 @@ export async function resolveConfig(
root: string = process.cwd()
): Promise<SiteConfig> {
const userConfig = await resolveUserConfig(root)
if (userConfig.vueOptions) {
console.warn(
chalk.yellow(`[vitepress] "vueOptions" option has been renamed to "vue".`)
)
}
if (userConfig.exclude) {
console.warn(
chalk.yellow(
`[vitepress] "exclude" option has been renamed to "ssrExclude".`
)
)
}
const site = await resolveSiteData(root, userConfig)
const srcDir = path.resolve(root, userConfig.srcDir || '.')
// resolve theme path
@ -90,22 +91,33 @@ export async function resolveConfig(
? userThemeDir
: DEFAULT_THEME_PATH
// Important: globby/fast-glob doesn't guarantee order of the returned files.
// We must sort the pages so the input list to rollup is stable across
// builds - otherwise different input order could result in different exports
// order in shared chunks which in turns invalidates the hash of every chunk!
// JavaScript built-in sort() is mandated to be stable as of ES2019 and
// supported in Node 12+, which is required by Vite.
const pages = (
await globby(['**.md'], {
cwd: srcDir,
ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
})
).sort()
const config: SiteConfig = {
root,
srcDir,
site,
themeDir,
pages: await globby(['**.md'], {
cwd: srcDir,
ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
}),
pages,
configPath: resolve(root, 'config.js'),
outDir: resolve(root, 'dist'),
tempDir: path.resolve(APP_PATH, 'temp'),
markdown: userConfig.markdown,
alias: resolveAliases(themeDir),
vue: userConfig.vue,
vite: userConfig.vite
vite: userConfig.vite,
mpa: !!userConfig.mpa
}
return config
@ -117,16 +129,53 @@ export async function resolveUserConfig(root: string): Promise<UserConfig> {
const hasUserConfig = await fs.pathExists(configPath)
// always delete cache first before loading config
delete require.cache[require.resolve(configPath)]
const userConfig: UserConfig | (() => UserConfig) = hasUserConfig
? require(configPath)
: {}
const userConfig: RawConfigExports = hasUserConfig ? require(configPath) : {}
if (hasUserConfig) {
debug(`loaded config at ${chalk.yellow(configPath)}`)
} else {
debug(`no config file found.`)
}
return resolveConfigExtends(userConfig)
}
async function resolveConfigExtends(
config: RawConfigExports
): Promise<UserConfig> {
const resolved = await (typeof config === 'function' ? config() : config)
if (resolved.extends) {
const base = await resolveConfigExtends(resolved.extends)
return mergeConfig(base, resolved)
}
return resolved
}
function mergeConfig(a: UserConfig, b: UserConfig, isRoot = true) {
const merged: Record<string, any> = { ...a }
for (const key in b) {
const value = b[key as keyof UserConfig]
if (value == null) {
continue
}
const existing = merged[key]
if (Array.isArray(existing) && Array.isArray(value)) {
merged[key] = [...existing, ...value]
continue
}
if (isObject(existing) && isObject(value)) {
if (isRoot && key === 'vite') {
merged[key] = mergeViteConfig(existing, value)
} else {
merged[key] = mergeConfig(existing, value, false)
}
continue
}
merged[key] = value
}
return merged
}
return typeof userConfig === 'function' ? userConfig() : userConfig
function isObject(value: unknown): value is Record<string, any> {
return Object.prototype.toString.call(value) === '[object Object]'
}
export async function resolveSiteData(
@ -142,6 +191,6 @@ export async function resolveSiteData(
head: userConfig.head || [],
themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {},
customData: userConfig.customData || {}
langs: createLangDictionary(userConfig)
}
}

@ -2,3 +2,5 @@ export * from './server'
export * from './build/build'
export * from './serve/serve'
export * from './config'
export type { SiteData, HeadConfig, LocaleConfig } from '../../types/shared'

@ -12,18 +12,21 @@ import { preWrapperPlugin } from './plugins/preWrapper'
import { linkPlugin } from './plugins/link'
import { extractHeaderPlugin } from './plugins/header'
import { Header } from '../shared'
const emoji = require('markdown-it-emoji')
const anchor = require('markdown-it-anchor')
const toc = require('markdown-it-table-of-contents')
import anchor from 'markdown-it-anchor'
import attrs from 'markdown-it-attrs'
import emoji from 'markdown-it-emoji'
import toc from 'markdown-it-table-of-contents'
export interface MarkdownOptions extends MarkdownIt.Options {
lineNumbers?: boolean
config?: (md: MarkdownIt) => void
anchor?: {
permalink?: boolean
permalinkBefore?: boolean
permalinkSymbol?: string
permalink?: anchor.AnchorOptions['permalink']
}
attrs?: {
leftDelimiter?: string
rightDelimiter?: string
allowedAttributes?: string[]
}
// https://github.com/Oktavilla/markdown-it-table-of-contents
toc?: any
@ -66,14 +69,16 @@ export const createMarkdownRenderer = (
...options.externalLinks
})
.use(attrs, {
leftDelimiter: '{',
rightDelimiter: '}',
allowedAttributes: [],
...options.attrs
})
// 3rd party plugins
.use(emoji)
.use(anchor, {
slugify,
permalink: true,
permalinkBefore: true,
permalinkSymbol: '#',
permalinkAttrs: () => ({ 'aria-hidden': true }),
permalink: anchor.permalink.ariaHidden({}),
...options.anchor
})
.use(toc, {
@ -82,6 +87,7 @@ export const createMarkdownRenderer = (
format: parseHeader,
...options.toc
})
.use(emoji)
// apply user config
if (options.config) {

@ -1,13 +1,11 @@
import MarkdownIt from 'markdown-it'
import { RuleBlock } from 'markdown-it/lib/parser_block'
import blockNames from 'markdown-it/lib/common/html_blocks'
import { HTML_OPEN_CLOSE_TAG_RE } from 'markdown-it/lib/common/html_re'
// Replacing the default htmlBlock rule to allow using custom components at
// root level
const blockNames: string[] = require('markdown-it/lib/common/html_blocks')
const HTML_OPEN_CLOSE_TAG_RE: RegExp =
require('markdown-it/lib/common/html_re').HTML_OPEN_CLOSE_TAG_RE
// An array of opening and corresponding closing sequences for html tags,
// last argument defines whether it can terminate a paragraph or not
const HTML_SEQUENCES: [RegExp, RegExp, boolean][] = [

@ -1,7 +1,6 @@
import MarkdownIt from 'markdown-it'
import Token from 'markdown-it/lib/token'
const container = require('markdown-it-container')
import container from 'markdown-it-container'
export const containerPlugin = (md: MarkdownIt) => {
md.use(...createContainer('tip', 'TIP'))

@ -1,7 +1,9 @@
const chalk = require('chalk')
const prism = require('prismjs')
import chalk from 'chalk'
import escapeHtml from 'escape-html'
import prism from 'prismjs'
// prism is listed as actual dep so it's ok to require
const loadLanguages = require('prismjs/components/index')
const escapeHtml = require('escape-html')
// required to make embedded highlighting work...
loadLanguages(['markup', 'css', 'javascript'])

@ -32,6 +32,13 @@ export const linkPlugin = (
) {
normalizeHref(hrefAttr)
}
// encode vite-specific replace strings in case they appear in URLs
// this also excludes them from build-time replacements (which injects
// <wbr/> and will break URLs)
hrefAttr[1] = hrefAttr[1]
.replace(/\bimport\.meta/g, 'import%2Emeta')
.replace(/\bprocess\.env/g, 'process%2Eenv')
}
return self.renderToken(tokens, idx, options)
}

@ -1,6 +1,6 @@
// string.js slugify drops non ascii chars so we have to
// use a custom implementation here
const removeDiacritics = require('diacritics').remove
import { remove as removeDiacritics } from 'diacritics'
// eslint-disable-next-line no-control-regex
const rControl = /[\u0000-\u001f]/g
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g
@ -12,9 +12,9 @@ export const slugify = (str: string): string => {
.replace(rControl, '')
// Replace special characters
.replace(rSpecial, '-')
// Remove continuos separators
// Remove continuous separators
.replace(/\-{2,}/g, '-')
// Remove prefixing and trailing separtors
// Remove prefixing and trailing separators
.replace(/^\-+|\-+$/g, '')
// ensure it doesn't start with a number (#121)
.replace(/^(\d)/, '_$1')

@ -7,14 +7,17 @@ import { deeplyParseHeader } from './utils/parseHeader'
import { PageData, HeadConfig } from './shared'
import { slash } from './utils/slash'
import chalk from 'chalk'
import _debug from 'debug'
const debug = require('debug')('vitepress:md')
const debug = _debug('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g
export interface MarkdownCompileResult {
vueSrc: string
pageData: PageData
deadLinks: string[]
includes: string[]
}
export function createMarkdownToVueRenderFn(
@ -27,12 +30,22 @@ export function createMarkdownToVueRenderFn(
const md = createMarkdownRenderer(srcDir, options)
pages = pages.map((p) => slash(p.replace(/\.md$/, '')))
const userDefineRegex = userDefines
? new RegExp(
`\\b(${Object.keys(userDefines)
.map((key) => key.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'))
.join('|')})`,
'g'
)
: null
return (
src: string,
file: string,
publicDir: string
): MarkdownCompileResult => {
const relativePath = slash(path.relative(srcDir, file))
const dir = path.dirname(file)
const cached = cache.get(src)
if (cached) {
@ -42,6 +55,16 @@ export function createMarkdownToVueRenderFn(
const start = Date.now()
// resolve includes
let includes: string[] = []
src = src.replace(includesRE, (_, m1) => {
const includePath = path.join(dir, m1)
console.log(includePath)
const content = fs.readFileSync(includePath, 'utf-8')
includes.push(slash(includePath))
return content
})
const { content, data: frontmatter } = matter(src)
let { html, data } = md.render(content)
@ -52,14 +75,11 @@ export function createMarkdownToVueRenderFn(
.replace(/\bprocess\.env/g, 'process.<wbr/>env')
// also avoid replacing vite user defines
if (userDefines) {
const regex = new RegExp(
`\\b(${Object.keys(userDefines)
.map((key) => key.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'))
.join('|')})`,
'g'
if (userDefineRegex) {
html = html.replace(
userDefineRegex,
(_) => `${_[0]}<wbr/>${_.slice(1)}`
)
html = html.replace(regex, (_) => `${_[0]}<wbr/>${_.slice(1)}`)
}
}
@ -110,7 +130,8 @@ export function createMarkdownToVueRenderFn(
const result = {
vueSrc,
pageData,
deadLinks
deadLinks,
includes
}
cache.set(src, result)
return result
@ -119,6 +140,7 @@ export function createMarkdownToVueRenderFn(
const scriptRE = /<\/script>/
const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/
const scriptClientRe = /<\s*script[^>]*\bclient\b[^>]*/
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/
@ -128,7 +150,11 @@ function genPageDataCode(tags: string[], data: PageData) {
)}`
const existingScriptIndex = tags.findIndex((tag) => {
return scriptRE.test(tag) && !scriptSetupRE.test(tag)
return (
scriptRE.test(tag) &&
!scriptSetupRE.test(tag) &&
!scriptClientRe.test(tag)
)
})
if (existingScriptIndex > -1) {

@ -1,11 +1,11 @@
import path from 'path'
import { mergeConfig, Plugin, ResolvedConfig } from 'vite'
import { defineConfig, mergeConfig, Plugin, ResolvedConfig } from 'vite'
import { SiteConfig, resolveSiteData } from './config'
import {
createMarkdownToVueRenderFn,
MarkdownCompileResult
} from './markdownToVue'
import { APP_PATH, SITE_DATA_REQUEST_PATH } from './alias'
import { DIST_CLIENT_PATH, APP_PATH, SITE_DATA_REQUEST_PATH } from './alias'
import createVuePlugin from '@vitejs/plugin-vue'
import { slash } from './utils/slash'
import { OutputAsset, OutputChunk } from 'rollup'
@ -16,6 +16,11 @@ const staticInjectMarkerRE =
const staticStripRE = /__VP_STATIC_START__.*?__VP_STATIC_END__/g
const staticRestoreRE = /__VP_STATIC_(START|END)__/g
// matches client-side js blocks in MPA mode.
// in the future we may add different execution strategies like visible or
// media queries.
const scriptClientRE = /<script\b[^>]*client\b[^>]*>([^]*?)<\/script>/
const isPageChunk = (
chunk: OutputAsset | OutputChunk
): chunk is OutputChunk & { facadeModuleId: string } =>
@ -28,7 +33,12 @@ const isPageChunk = (
export function createVitePressPlugin(
root: string,
{
siteConfig: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>,
clientJSMap?: Record<string, string>
): Plugin[] {
const {
srcDir,
configPath,
alias,
@ -37,10 +47,8 @@ export function createVitePressPlugin(
vue: userVuePluginOptions,
vite: userViteConfig,
pages
}: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>
): Plugin[] {
} = siteConfig
let markdownToVue: (
src: string,
file: string,
@ -52,6 +60,15 @@ export function createVitePressPlugin(
...userVuePluginOptions
})
const processClientJS = (code: string, id: string) => {
return scriptClientRE.test(code)
? code.replace(scriptClientRE, (_, content) => {
if (ssr && clientJSMap) clientJSMap[id] = content
return `\n`.repeat(_.split('\n').length - 1)
})
: code
}
let siteData = site
let hasDeadLinks = false
let config: ResolvedConfig
@ -71,7 +88,7 @@ export function createVitePressPlugin(
},
config() {
const baseConfig = {
const baseConfig = defineConfig({
resolve: {
alias
},
@ -84,8 +101,13 @@ export function createVitePressPlugin(
// force include vue to avoid duplicated copies when linked + optimized
include: ['vue'],
exclude: ['@docsearch/js']
},
server: {
fs: {
allow: [DIST_CLIENT_PATH, srcDir, process.cwd()]
}
}
}
})
return userViteConfig
? mergeConfig(userViteConfig, baseConfig)
: baseConfig
@ -104,13 +126,24 @@ export function createVitePressPlugin(
},
transform(code, id) {
if (id.endsWith('.md')) {
if (id.endsWith('.vue')) {
return processClientJS(code, id)
} else if (id.endsWith('.md')) {
// transform .md files into vueSrc so plugin-vue can handle it
const { vueSrc, deadLinks } = markdownToVue(code, id, config.publicDir)
const { vueSrc, deadLinks, includes } = markdownToVue(
code,
id,
config.publicDir
)
if (deadLinks.length) {
hasDeadLinks = true
}
return vueSrc
if (includes.length) {
includes.forEach((i) => {
this.addWatchFile(i)
})
}
return processClientJS(vueSrc, id)
}
},

@ -1,6 +1,7 @@
import sirv from 'sirv'
import compression from 'compression'
import { resolveConfig } from '../config'
import polka from 'polka'
export interface ServeOptions {
root?: string
@ -26,7 +27,7 @@ export async function serve(options: ServeOptions = {}) {
}
})
require('polka')()
polka()
.use(compress, serve)
.listen(port, (err: any) => {
if (err) throw err

@ -0,0 +1,43 @@
declare module 'markdown-it-attrs' {
const def: any
export default def
}
declare module 'markdown-it-emoji' {
const def: any
export default def
}
declare module 'markdown-it-table-of-contents' {
const def: any
export default def
}
declare module 'markdown-it-container' {
const def: any
export default def
}
declare module 'escape-html' {
const def: (str: string) => string
export default def
}
declare module 'prismjs' {
const def: any
export default def
}
declare module 'prismjs/components/index' {
const def: any
export default def
}
declare module 'diacritics' {
export const remove: (str: string) => string
}
declare module '*.json' {
const def: any
export default def
}

@ -9,9 +9,9 @@
// loader, the raw HTML in headers will finally be parsed by Vue-loader.
// so that we can write HTML/Vue in the header. One exception is the HTML
// wrapped by <code>(markdown token: '`') tag.
import emojiData from 'markdown-it-emoji/lib/data/full.json'
const parseEmojis = (str: string) => {
const emojiData = require('markdown-it-emoji/lib/data/full.json')
return String(str).replace(
/:(.+?):/g,
(placeholder, key) => emojiData[key] || placeholder

@ -1,4 +1,4 @@
import { SiteData } from '../../types/shared'
import { LocaleConfig, SiteData } from '../../types/shared'
export type {
SiteData,
@ -12,7 +12,7 @@ export const EXTERNAL_URL_RE = /^https?:/i
export const inBrowser = typeof window !== 'undefined'
function findMatchRoot(route: string, roots: string[]) {
function findMatchRoot(route: string, roots: string[]): string | undefined {
// first match to the routes with the most deep level.
roots.sort((a, b) => {
const levelDelta = b.split('/').length - a.split('/').length
@ -24,9 +24,8 @@ function findMatchRoot(route: string, roots: string[]) {
})
for (const r of roots) {
if (route.startsWith(r)) return
if (route.startsWith(r)) return r
}
return undefined
}
function resolveLocales<T>(
@ -37,6 +36,23 @@ function resolveLocales<T>(
return localeRoot ? locales[localeRoot] : undefined
}
export function createLangDictionary(siteData: {
themeConfig?: Record<string, any>
locales?: Record<string, LocaleConfig>
}) {
const { locales } = siteData.themeConfig || {}
const siteLocales = siteData.locales
return locales && siteLocales
? Object.keys(locales).reduce((langs, path) => {
langs[path] = {
label: locales![path].label,
lang: siteLocales[path].lang
}
return langs
}, {} as Record<string, { lang: string; label: string }>)
: {}
}
// this merges the locales data to the main data by the route
export function resolveSiteDataByRoute(
siteData: SiteData,
@ -44,25 +60,24 @@ export function resolveSiteDataByRoute(
): SiteData {
route = cleanRoute(siteData, route)
const localeData = resolveLocales(siteData.locales || {}, route) || {}
const localeThemeConfig =
resolveLocales<any>(
(siteData.themeConfig && siteData.themeConfig.locales) || {},
route
) || {}
const localeData = resolveLocales(siteData.locales || {}, route)
const localeThemeConfig = resolveLocales<any>(
siteData.themeConfig.locales || {},
route
)
return {
...siteData,
...localeData,
themeConfig: {
...siteData.themeConfig,
...localeThemeConfig,
// avoid object rest spread since this is going to run in the browser
// and spread is going to result in polyfill code
return Object.assign({}, siteData, localeData, {
themeConfig: Object.assign({}, siteData.themeConfig, localeThemeConfig, {
// clean the locales to reduce the bundle size
locales: {}
},
lang: localeThemeConfig.lang || siteData.lang,
locales: {}
}
}),
lang: (localeData || siteData).lang,
// clean the locales to reduce the bundle size
locales: {},
langs: createLangDictionary(siteData)
})
}
/**

@ -4,7 +4,6 @@
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"declaration": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"esModuleInterop": true,

25
types/shared.d.ts vendored

@ -11,13 +11,36 @@ export interface LocaleConfig {
export interface SiteData<ThemeConfig = any> {
base: string
/**
* Language of the site as it should be set on the `html` element.
* @example `en-US`, `zh-CN`
*/
lang: string
title: string
description: string
head: HeadConfig[]
themeConfig: ThemeConfig
locales: Record<string, LocaleConfig>
customData: any
/**
* Available locales for the site when it has defined `locales` in its
* `themeConfig`. This object is otherwise empty. Keys are paths like `/` or
* `/zh/`.
*/
langs: Record<
string,
{
/**
* Lang attribute as set on the `<html>` element.
* @example `en-US`, `zh-CN`
*/
lang: string
/**
* Label to display in the language menu.
* @example `English`, `简体中文`
*/
label: string
}
>
}
export type HeadConfig =

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