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: matrix:
node-version: [14, 16] node-version: [14, 16]
steps: steps:
- uses: actions/checkout@v1 - name: Checkout
- name: Use Node.js ${{ matrix.node-version }} uses: actions/checkout@v2
uses: actions/setup-node@v1
- 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: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node_version }}
- name: install and test cache: "pnpm"
run: |
yarn install - name: Install deps
yarn test run: pnpm install
- name: build
run: | - name: Build
yarn 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) ## [0.16.1](https://github.com/vuejs/vitepress/compare/v0.16.0...v0.16.1) (2021-08-11)
### Features ### 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/)' spiritual successor, built on top of [vite](https://github.com/vuejs/vite).
---
VitePress is [VuePress](http://vuepress.vuejs.org/)' little brother, built on top of [vite](https://github.com/vuejs/vite).
## Documentation ## Documentation
@ -17,7 +13,7 @@ To check out docs, visit [vitepress.vuejs.org](https://vitepress.vuejs.org).
## Changelog ## 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 ## 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/>` ### `<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. 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`. 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 ## Heroku

@ -60,7 +60,6 @@ Outbound links automatically get `target="_blank" rel="noopener noreferrer"`:
title: Blogging Like a Hacker title: Blogging Like a Hacker
lang: en-US lang: en-US
--- ---
``` ```
This data will be available to the rest of the page, along with all custom and theming components. 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`: 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 ```js
const anchor = require('markdown-it-anchor')
module.exports = { module.exports = {
markdown: { markdown: {
// options for markdown-it-anchor // 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] }, toc: { includeLevel: [1, 2] },
config: (md) => { 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 ```js
// .vitepress/theme/index.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 ```vue
<!-- .vitepress/theme/Layout.vue --> <!-- .vitepress/theme/Layout.vue -->

@ -2,8 +2,11 @@
"private": true, "private": true,
"name": "vitepress-example-minimal", "name": "vitepress-example-minimal",
"scripts": { "scripts": {
"dev": "node ../../bin/vitepress dev", "dev": "vitepress dev",
"build": "node ../../bin/vitepress build", "build": "vitepress build",
"serve": "node ../../bin/vitepress serve" "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", "name": "vitepress",
"version": "0.16.1", "version": "0.19.2",
"description": "Vite & Vue powered static site generator", "description": "Vite & Vue powered static site generator",
"main": "dist/node/index.js", "main": "dist/node/index.js",
"typings": "types/index.d.ts", "typings": "types/index.d.ts",
@ -9,19 +9,24 @@
}, },
"files": [ "files": [
"bin", "bin",
"lib",
"dist", "dist",
"types" "types"
], ],
"scripts": { "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-start": "run-p dev-client dev-node dev-watch",
"dev-client": "tsc -w -p src/client", "dev-client": "tsc -w -p src/client",
"dev-node": "tsc -w -p src/node", "dev-node": "tsc -w -p src/node",
"dev-shared": "node scripts/copyShared", "dev-shared": "node scripts/copyShared",
"dev-watch": "node scripts/watchAndCopy", "dev-watch": "node scripts/watchAndCopy",
"build": "rimraf -rf dist && node scripts/copyShared && tsc -p src/client && tsc -p src/node && node scripts/copyClient", "build": "run-s build-prepare build-client build-node build-types",
"lint": "yarn lint:js && yarn lint:ts", "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:js": "prettier --check --write \"{bin,docs,scripts,src}/**/*.js\"",
"lint:ts": "prettier --check --write --parser typescript \"{__tests__,src,docs,types}/**/*.ts\"", "lint:ts": "prettier --check --write --parser typescript \"{__tests__,src,docs,types}/**/*.ts\"",
"test": "jest", "test": "jest",
@ -30,8 +35,9 @@
"docs": "run-p dev docs-dev", "docs": "run-p dev docs-dev",
"docs-dev": "node ./bin/vitepress dev docs", "docs-dev": "node ./bin/vitepress dev docs",
"docs-debug": "node --inspect-brk ./bin/vitepress dev docs", "docs-debug": "node --inspect-brk ./bin/vitepress dev docs",
"docs-build": "yarn build && node ./bin/vitepress build docs", "docs-build": "pnpm run build && node ./bin/vitepress build docs",
"docs-serve": "node ./bin/vitepress serve docs" "docs-serve": "node ./bin/vitepress serve docs",
"ci-docs": "run-s build docs-build"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
@ -65,52 +71,62 @@
"dependencies": { "dependencies": {
"@docsearch/css": "^1.0.0-alpha.28", "@docsearch/css": "^1.0.0-alpha.28",
"@docsearch/js": "^1.0.0-alpha.28", "@docsearch/js": "^1.0.0-alpha.28",
"@types/markdown-it": "^12.0.1", "@vitejs/plugin-vue": "^1.9.0",
"@vitejs/plugin-vue": "^1.4.0", "@vue/runtime-dom": "^3.2.18",
"@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",
"prismjs": "^1.23.0", "prismjs": "^1.23.0",
"sirv": "^1.0.12", "vite": "^2.5.0",
"vite": "^2.4.4", "vue": "^3.2.13"
"vue": "^3.2.1"
}, },
"devDependencies": { "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/compression": "^1.7.0",
"@types/debug": "^4.1.7",
"@types/fs-extra": "^9.0.11", "@types/fs-extra": "^9.0.11",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",
"@types/koa": "^2.13.1", "@types/koa": "^2.13.1",
"@types/koa-static": "^4.0.1", "@types/koa-static": "^4.0.1",
"@types/lru-cache": "^5.1.0", "@types/lru-cache": "^5.1.0",
"@types/markdown-it": "^12.0.1",
"@types/node": "^15.6.1", "@types/node": "^15.6.1",
"@types/polka": "^0.5.3",
"@types/postcss-load-config": "^3.0.1", "@types/postcss-load-config": "^3.0.1",
"chalk": "^4.1.1",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"compression": "^1.7.4",
"conventional-changelog-cli": "^2.1.1", "conventional-changelog-cli": "^2.1.1",
"debug": "^4.3.2",
"diacritics": "^1.3.0",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"esbuild": "^0.12.28",
"escape-html": "^1.0.3",
"execa": "^5.0.0", "execa": "^5.0.0",
"fs-extra": "^10.0.0",
"globby": "^11.0.3",
"gray-matter": "^4.0.3",
"jest": "^27.0.1", "jest": "^27.0.1",
"lint-staged": "^11.0.0", "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", "npm-run-all": "^4.1.5",
"ora": "^5.4.0",
"pnpm": "^6.15.1",
"polka": "^0.5.2",
"prettier": "^2.3.0", "prettier": "^2.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^2.38.5", "rollup": "^2.56.3",
"rollup-plugin-esbuild": "^4.5.0",
"semver": "^7.3.5", "semver": "^7.3.5",
"sirv": "^1.0.12",
"ts-jest": "^27.0.1", "ts-jest": "^27.0.1",
"typescript": "^4.3.2", "typescript": "^4.3.2",
"yorkie": "^2.0.0" "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. // Build the package.
step('\nBuilding the package...') step('\nBuilding the package...')
await run('yarn', ['build']) await run('pnpm', ['build'])
// Generate the changelog. // Generate the changelog.
step('\nGenerating the changelog...') step('\nGenerating the changelog...')
await run('yarn', ['changelog']) await run('pnpm', ['changelog'])
await run('yarn', ['prettier', '--write', 'CHANGELOG.md']) await run('pnpm', ['prettier', '--write', 'CHANGELOG.md'])
const { yes: changelogOk } = await prompt({ const { yes: changelogOk } = await prompt({
type: 'confirm', type: 'confirm',
@ -82,13 +82,7 @@ async function main() {
// Publish the package. // Publish the package.
step('\nPublishing the package...') step('\nPublishing the package...')
await run('yarn', [ await run('pnpm', ['publish', '--ignore-scripts', '--no-git-checks'])
'publish',
'--new-version',
targetVersion,
'--no-commit-hooks',
'--no-git-tag-version'
])
// Push to GitHub. // Push to GitHub.
step('\nPushing 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 fs = require('fs-extra')
const chokidar = require('chokidar') const chokidar = require('chokidar')
const { normalizePath } = require('vite')
function toClientAndNode(method, file) { function toClientAndNode(method, file) {
file = normalizePath(file)
if (method === 'copy') { if (method === 'copy') {
fs.copy(file, file.replace(/^src\/shared\//, 'src/node/')) fs.copy(file, file.replace(/^src\/shared\//, 'src/node/'))
fs.copy(file, file.replace(/^src\/shared\//, 'src/client/')) fs.copy(file, file.replace(/^src\/shared\//, 'src/client/'))
@ -12,7 +14,7 @@ function toClientAndNode(method, file) {
} }
function toDist(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. // copy shared files to the client and node directory whenever they change.

@ -1,13 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch, reactive } from 'vue'
import { useData } from '../data' import { useData } from '../data'
const data = useData() const data = useData()
const el = ref<HTMLElement | null>(null) const el = ref<HTMLElement | null>(null)
const open = ref(false) const open = ref(false)
// FIXME: remove in next Vue release
const tempData = reactive(data)
watch(open, (value) => { watch(open, (value) => {
if (value === false) { if (!value) {
el.value!.scrollTop = 0 el.value!.scrollTop = 0
} }
}) })
@ -16,7 +19,7 @@ watch(open, (value) => {
<template> <template>
<div class="debug" :class="{ open }" ref="el" @click="open = !open"> <div class="debug" :class="{ open }" ref="el" @click="open = !open">
<p class="title">Debug</p> <p class="title">Debug</p>
<pre class="block">{{ data }}</pre> <pre class="block">{{ tempData }}</pre>
</div> </div>
</template> </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 { Route } from './router'
import serializedSiteData from '@siteData' import serializedSiteData from '@siteData'
import { resolveSiteDataByRoute, PageData, SiteData } from '../shared' import { resolveSiteDataByRoute, PageData, SiteData } from '../shared'
@ -20,7 +20,7 @@ export interface VitePressData<T = any> {
// site data is a singleton // site data is a singleton
export type SiteDataRef<T = any> = Ref<SiteData<T>> 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 { function parse(data: string): SiteData {
return readonly(JSON.parse(data)) as SiteData return readonly(JSON.parse(data)) as SiteData
@ -46,9 +46,11 @@ export function initData(route: Route): VitePressData {
frontmatter: computed(() => route.data.frontmatter), frontmatter: computed(() => route.data.frontmatter),
lang: computed(() => site.value.lang), lang: computed(() => site.value.lang),
localePath: computed(() => { localePath: computed(() => {
const { locales, lang } = site.value const { langs, lang } = site.value
const path = Object.keys(locales).find((lp) => locales[lp].lang === lang) const path = Object.keys(langs).find(
return withBase((locales && path) || '/') (langPath) => langs[langPath].lang === lang
)
return withBase(path || '/')
}), }),
title: computed(() => { title: computed(() => {
return route.data.title return route.data.title

@ -77,7 +77,9 @@ export function createRouter(
route.path = pendingPath route.path = pendingPath
route.component = markRaw(comp) 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) { if (inBrowser) {
nextTick(() => { nextTick(() => {
@ -94,7 +96,7 @@ export function createRouter(
}) })
} }
} }
} catch (err) { } catch (err: any) {
if (!err.message.match(/fetch/)) { if (!err.message.match(/fetch/)) {
console.error(err) console.error(err)
} }
@ -131,6 +133,8 @@ export function createRouter(
// scroll between hash anchors in the same page // scroll between hash anchors in the same page
if (hash && hash !== currentUrl.hash) { if (hash && hash !== currentUrl.hash) {
history.pushState(null, '', 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 // use smooth scroll when clicking on header anchor links
scrollTo(link, hash, link.classList.contains('header-anchor')) scrollTo(link, hash, link.classList.contains('header-anchor'))
} }

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

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

@ -2,11 +2,14 @@
<div class="theme"> <div class="theme">
<h1>404</h1> <h1>404</h1>
<blockquote>{{ getMsg() }}</blockquote> <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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useData } from 'vitepress'
const { site } = useData()
const msgs = [ const msgs = [
`There's nothing here.`, `There's nothing here.`,
`How did we get here?`, `How did we get here?`,

@ -45,6 +45,10 @@ onMounted(() => {
function load() { function load() {
if (typeof _bsa !== 'undefined' && _bsa) { 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}`, { _bsa.init('default', code, `placement:${placement}`, {
target: '.bsa-cpc', target: '.bsa-cpc',
align: 'horizontal', align: 'horizontal',

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

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

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

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

@ -1,57 +1,30 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useRoute, useData, inBrowser } from 'vitepress' import { useData, useRoute } from 'vitepress'
import type { DefaultTheme } from '../config' import type { DefaultTheme } from '../config'
export function useLocaleLinks() { export function useLanguageLinks() {
const route = useRoute() const { site, localePath, theme } = useData()
const { site } = useData()
return computed(() => { return computed(() => {
const theme = site.value.themeConfig as DefaultTheme.Config const langs = site.value.langs
const locales = theme.locales const localePaths = Object.keys(langs)
if (!locales) { // one language
if (localePaths.length < 2) {
return null return null
} }
const localeKeys = Object.keys(locales) const route = useRoute()
if (localeKeys.length <= 1) { // intentionally remove the leading slash because each locale has one
return null const currentPath = route.path.replace(localePath.value, '')
}
// 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}`
}
})
const currentLangKey = currentLangBase ? currentLangBase : '/' const candidates = localePaths.map((localePath) => ({
text: langs[localePath].label,
link: `${localePath}${currentPath}`
}))
const selectText = locales[currentLangKey].selectText const selectText = theme.value.selectText || 'Languages'
? locales[currentLangKey].selectText
: 'Languages'
return { return {
text: selectText, text: selectText,

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

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

@ -6,14 +6,22 @@ import { renderPage } from './render'
import { OutputChunk, OutputAsset } from 'rollup' import { OutputChunk, OutputAsset } from 'rollup'
import ora from 'ora' 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() const start = Date.now()
process.env.NODE_ENV = 'production' process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(root) const siteConfig = await resolveConfig(root)
if (buildOptions.mpa) {
siteConfig.mpa = true
delete buildOptions.mpa
}
try { try {
const [clientResult, , pageToHashMap] = await bundle( const { clientResult, serverResult, pageToHashMap } = await bundle(
siteConfig, siteConfig,
buildOptions buildOptions
) )
@ -22,11 +30,15 @@ export async function build(root: string, buildOptions: BuildOptions = {}) {
spinner.start('rendering pages...') spinner.start('rendering pages...')
try { try {
const appChunk = clientResult.output.find( const appChunk =
(chunk) => chunk.type === 'chunk' && chunk.isEntry clientResult &&
) as OutputChunk (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') (chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as OutputAsset ) 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 ora from 'ora'
import path from 'path' import path from 'path'
import fs from 'fs-extra'
import { slash } from '../utils/slash' import { slash } from '../utils/slash'
import { APP_PATH } from '../alias' import { APP_PATH } from '../alias'
import { SiteConfig } from '../config' import { SiteConfig } from '../config'
import { RollupOutput } from 'rollup' import { RollupOutput } from 'rollup'
import { build, BuildOptions, UserConfig as ViteUserConfig } from 'vite' import { build, BuildOptions, UserConfig as ViteUserConfig } from 'vite'
import { createVitePressPlugin } from '../plugin' import { createVitePressPlugin } from '../plugin'
import { buildMPAClient } from './buildMPAClient'
export const okMark = '\x1b[32m✓\x1b[0m' export const okMark = '\x1b[32m✓\x1b[0m'
export const failMark = '\x1b[31m✖\x1b[0m' export const failMark = '\x1b[31m✖\x1b[0m'
@ -14,9 +16,14 @@ export const failMark = '\x1b[31m✖\x1b[0m'
export async function bundle( export async function bundle(
config: SiteConfig, config: SiteConfig,
options: BuildOptions options: BuildOptions
): Promise<[RollupOutput, RollupOutput, Record<string, string>]> { ): Promise<{
clientResult: RollupOutput
serverResult: RollupOutput
pageToHashMap: Record<string, string>
}> {
const { root, srcDir } = config const { root, srcDir } = config
const pageToHashMap = Object.create(null) const pageToHashMap = Object.create(null)
const clientJSMap = Object.create(null)
// define custom rollup input // define custom rollup input
// this is a multi-entry build - every page is considered an entry chunk // this is a multi-entry build - every page is considered an entry chunk
@ -38,7 +45,13 @@ export async function bundle(
root: srcDir, root: srcDir,
base: config.site.base, base: config.site.base,
logLevel: 'warn', logLevel: 'warn',
plugins: createVitePressPlugin(root, config, ssr, pageToHashMap), plugins: createVitePressPlugin(
root,
config,
ssr,
pageToHashMap,
clientJSMap
),
// @ts-ignore // @ts-ignore
ssr: { ssr: {
noExternal: ['vitepress'] noExternal: ['vitepress']
@ -64,12 +77,15 @@ export async function bundle(
if (!chunk.isEntry && /runtime/.test(chunk.name)) { if (!chunk.isEntry && /runtime/.test(chunk.name)) {
return `assets/framework.[hash].js` 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...') spinner.start('building client + server bundles...')
try { try {
;[clientResult, serverResult] = await (Promise.all([ ;[clientResult, serverResult] = await (Promise.all([
build(resolveViteConfig(false)), config.mpa ? null : build(resolveViteConfig(false)),
build(resolveViteConfig(true)) build(resolveViteConfig(true))
]) as Promise<[RollupOutput, RollupOutput]>) ]) as Promise<[RollupOutput, RollupOutput]>)
} catch (e) { } catch (e) {
@ -93,5 +109,28 @@ export async function bundle(
symbol: okMark 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 { HeadConfig } from '../shared'
import { normalizePath } from 'vite' import { normalizePath } from 'vite'
import { RollupOutput, OutputChunk, OutputAsset } from 'rollup' import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
import { slash } from '../utils/slash'
const escape = require('escape-html') import escape from 'escape-html'
export async function renderPage( export async function renderPage(
config: SiteConfig, config: SiteConfig,
page: string, // foo.md page: string, // foo.md
result: RollupOutput, result: RollupOutput | null,
appChunk: OutputChunk, appChunk: OutputChunk | undefined,
cssChunk: OutputAsset, cssChunk: OutputAsset | undefined,
pageToHashMap: Record<string, string>, pageToHashMap: Record<string, string>,
hashMapString: string hashMapString: string
) { ) {
@ -22,7 +22,7 @@ export async function renderPage(
const siteData = resolveSiteDataByRoute(config.site, routePath) const siteData = resolveSiteDataByRoute(config.site, routePath)
router.go(routePath) router.go(routePath)
// lazy require server-renderer for production build // 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, '_') const pageName = page.replace(/\//g, '_')
// server build doesn't need hash // server build doesn't need hash
@ -39,14 +39,22 @@ export async function renderPage(
)) ))
const pageData = JSON.parse(__pageData) const pageData = JSON.parse(__pageData)
const preloadLinks = [ const preloadLinks = (
// resolve imports for index.js + page.md.js and inject script tags for config.mpa
// them as well so we fetch everything as early as possible without having ? appChunk
// to wait for entry chunks to parse ? [appChunk.fileName]
...resolvePageImports(config, page, result, appChunk), : []
pageClientJsFileName, : result && appChunk
appChunk.fileName ? [
] // 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) => { .map((file) => {
return `<link rel="modulepreload" href="${siteData.base}${file}">` return `<link rel="modulepreload" href="${siteData.base}${file}">`
}) })
@ -67,6 +75,23 @@ export async function renderPage(
...filterOutHeadDescription(pageData.frontmatter.head) ...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 = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
<html lang="${siteData.lang}"> <html lang="${siteData.lang}">
@ -83,10 +108,17 @@ export async function renderPage(
</head> </head>
<body> <body>
<div id="app">${content}</div> <div id="app">${content}</div>
<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script> ${
<script type="module" async src="${siteData.base}${ config.mpa
appChunk.fileName ? ''
}"></script> : `<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>`
}
${
appChunk
? `<script type="module" async src="${siteData.base}${appChunk.fileName}"></script>`
: ``
}
${inlinedScript}
</body> </body>
</html>`.trim() </html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) 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)) const argv: any = minimist(process.argv.slice(2))
console.log(chalk.cyan(`vitepress v${require('../../package.json').version}`)) 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 command = argv._[0]
const root = argv._[command ? 1 : 0] const root = argv._[command ? 1 : 0]

@ -2,17 +2,31 @@ import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import chalk from 'chalk' import chalk from 'chalk'
import globby from 'globby' 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 { 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 { resolveAliases, APP_PATH, DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown' import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug'
export { resolveSiteDataByRoute } from './shared' export { resolveSiteDataByRoute } from './shared'
const debug = require('debug')('vitepress:config') const debug = _debug('vitepress:config')
export type { MarkdownOptions }
export interface UserConfig<ThemeConfig = any> { export interface UserConfig<ThemeConfig = any> {
extends?: RawConfigExports
lang?: string lang?: string
base?: string base?: string
title?: string title?: string
@ -22,28 +36,29 @@ export interface UserConfig<ThemeConfig = any> {
locales?: Record<string, LocaleConfig> locales?: Record<string, LocaleConfig>
markdown?: MarkdownOptions markdown?: MarkdownOptions
/** /**
* Opitons to pass on to @vitejs/plugin-vue * Opitons to pass on to `@vitejs/plugin-vue`
*/ */
vue?: VuePluginOptions vue?: VuePluginOptions
/** /**
* Vite config * Vite config
*/ */
vite?: ViteConfig vite?: ViteConfig
customData?: any
srcDir?: string srcDir?: string
srcExclude?: string[] srcExclude?: string[]
/** /**
* @deprecated use `srcExclude` instead * Enable MPA / zero-JS mode
*/ * @experimental
exclude?: string[]
/**
* @deprecated use `vue` instead
*/ */
vueOptions?: VuePluginOptions mpa?: boolean
} }
export type RawConfigExports =
| UserConfig
| Promise<UserConfig>
| (() => UserConfig | Promise<UserConfig>)
export interface SiteConfig<ThemeConfig = any> { export interface SiteConfig<ThemeConfig = any> {
root: string root: string
srcDir: string srcDir: string
@ -57,6 +72,7 @@ export interface SiteConfig<ThemeConfig = any> {
markdown: MarkdownOptions | undefined markdown: MarkdownOptions | undefined
vue: VuePluginOptions | undefined vue: VuePluginOptions | undefined
vite: ViteConfig | undefined vite: ViteConfig | undefined
mpa: boolean
} }
const resolve = (root: string, file: string) => const resolve = (root: string, file: string) =>
@ -66,22 +82,7 @@ export async function resolveConfig(
root: string = process.cwd() root: string = process.cwd()
): Promise<SiteConfig> { ): Promise<SiteConfig> {
const userConfig = await resolveUserConfig(root) 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 site = await resolveSiteData(root, userConfig)
const srcDir = path.resolve(root, userConfig.srcDir || '.') const srcDir = path.resolve(root, userConfig.srcDir || '.')
// resolve theme path // resolve theme path
@ -90,22 +91,33 @@ export async function resolveConfig(
? userThemeDir ? userThemeDir
: DEFAULT_THEME_PATH : 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 = { const config: SiteConfig = {
root, root,
srcDir, srcDir,
site, site,
themeDir, themeDir,
pages: await globby(['**.md'], { pages,
cwd: srcDir,
ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
}),
configPath: resolve(root, 'config.js'), configPath: resolve(root, 'config.js'),
outDir: resolve(root, 'dist'), outDir: resolve(root, 'dist'),
tempDir: path.resolve(APP_PATH, 'temp'), tempDir: path.resolve(APP_PATH, 'temp'),
markdown: userConfig.markdown, markdown: userConfig.markdown,
alias: resolveAliases(themeDir), alias: resolveAliases(themeDir),
vue: userConfig.vue, vue: userConfig.vue,
vite: userConfig.vite vite: userConfig.vite,
mpa: !!userConfig.mpa
} }
return config return config
@ -117,16 +129,53 @@ export async function resolveUserConfig(root: string): Promise<UserConfig> {
const hasUserConfig = await fs.pathExists(configPath) const hasUserConfig = await fs.pathExists(configPath)
// always delete cache first before loading config // always delete cache first before loading config
delete require.cache[require.resolve(configPath)] delete require.cache[require.resolve(configPath)]
const userConfig: UserConfig | (() => UserConfig) = hasUserConfig const userConfig: RawConfigExports = hasUserConfig ? require(configPath) : {}
? require(configPath)
: {}
if (hasUserConfig) { if (hasUserConfig) {
debug(`loaded config at ${chalk.yellow(configPath)}`) debug(`loaded config at ${chalk.yellow(configPath)}`)
} else { } else {
debug(`no config file found.`) 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( export async function resolveSiteData(
@ -142,6 +191,6 @@ export async function resolveSiteData(
head: userConfig.head || [], head: userConfig.head || [],
themeConfig: userConfig.themeConfig || {}, themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}, locales: userConfig.locales || {},
customData: userConfig.customData || {} langs: createLangDictionary(userConfig)
} }
} }

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

@ -1,13 +1,11 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import { RuleBlock } from 'markdown-it/lib/parser_block' 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 // Replacing the default htmlBlock rule to allow using custom components at
// root level // 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, // An array of opening and corresponding closing sequences for html tags,
// last argument defines whether it can terminate a paragraph or not // last argument defines whether it can terminate a paragraph or not
const HTML_SEQUENCES: [RegExp, RegExp, boolean][] = [ const HTML_SEQUENCES: [RegExp, RegExp, boolean][] = [

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

@ -1,7 +1,9 @@
const chalk = require('chalk') import chalk from 'chalk'
const prism = require('prismjs') 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 loadLanguages = require('prismjs/components/index')
const escapeHtml = require('escape-html')
// required to make embedded highlighting work... // required to make embedded highlighting work...
loadLanguages(['markup', 'css', 'javascript']) loadLanguages(['markup', 'css', 'javascript'])

@ -32,6 +32,13 @@ export const linkPlugin = (
) { ) {
normalizeHref(hrefAttr) 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) return self.renderToken(tokens, idx, options)
} }

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

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

@ -1,11 +1,11 @@
import path from 'path' import path from 'path'
import { mergeConfig, Plugin, ResolvedConfig } from 'vite' import { defineConfig, mergeConfig, Plugin, ResolvedConfig } from 'vite'
import { SiteConfig, resolveSiteData } from './config' import { SiteConfig, resolveSiteData } from './config'
import { import {
createMarkdownToVueRenderFn, createMarkdownToVueRenderFn,
MarkdownCompileResult MarkdownCompileResult
} from './markdownToVue' } 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 createVuePlugin from '@vitejs/plugin-vue'
import { slash } from './utils/slash' import { slash } from './utils/slash'
import { OutputAsset, OutputChunk } from 'rollup' import { OutputAsset, OutputChunk } from 'rollup'
@ -16,6 +16,11 @@ const staticInjectMarkerRE =
const staticStripRE = /__VP_STATIC_START__.*?__VP_STATIC_END__/g const staticStripRE = /__VP_STATIC_START__.*?__VP_STATIC_END__/g
const staticRestoreRE = /__VP_STATIC_(START|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 = ( const isPageChunk = (
chunk: OutputAsset | OutputChunk chunk: OutputAsset | OutputChunk
): chunk is OutputChunk & { facadeModuleId: string } => ): chunk is OutputChunk & { facadeModuleId: string } =>
@ -28,7 +33,12 @@ const isPageChunk = (
export function createVitePressPlugin( export function createVitePressPlugin(
root: string, root: string,
{ siteConfig: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>,
clientJSMap?: Record<string, string>
): Plugin[] {
const {
srcDir, srcDir,
configPath, configPath,
alias, alias,
@ -37,10 +47,8 @@ export function createVitePressPlugin(
vue: userVuePluginOptions, vue: userVuePluginOptions,
vite: userViteConfig, vite: userViteConfig,
pages pages
}: SiteConfig, } = siteConfig
ssr = false,
pageToHashMap?: Record<string, string>
): Plugin[] {
let markdownToVue: ( let markdownToVue: (
src: string, src: string,
file: string, file: string,
@ -52,6 +60,15 @@ export function createVitePressPlugin(
...userVuePluginOptions ...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 siteData = site
let hasDeadLinks = false let hasDeadLinks = false
let config: ResolvedConfig let config: ResolvedConfig
@ -71,7 +88,7 @@ export function createVitePressPlugin(
}, },
config() { config() {
const baseConfig = { const baseConfig = defineConfig({
resolve: { resolve: {
alias alias
}, },
@ -84,8 +101,13 @@ export function createVitePressPlugin(
// force include vue to avoid duplicated copies when linked + optimized // force include vue to avoid duplicated copies when linked + optimized
include: ['vue'], include: ['vue'],
exclude: ['@docsearch/js'] exclude: ['@docsearch/js']
},
server: {
fs: {
allow: [DIST_CLIENT_PATH, srcDir, process.cwd()]
}
} }
} })
return userViteConfig return userViteConfig
? mergeConfig(userViteConfig, baseConfig) ? mergeConfig(userViteConfig, baseConfig)
: baseConfig : baseConfig
@ -104,13 +126,24 @@ export function createVitePressPlugin(
}, },
transform(code, id) { 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 // 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) { if (deadLinks.length) {
hasDeadLinks = true 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 sirv from 'sirv'
import compression from 'compression' import compression from 'compression'
import { resolveConfig } from '../config' import { resolveConfig } from '../config'
import polka from 'polka'
export interface ServeOptions { export interface ServeOptions {
root?: string root?: string
@ -26,7 +27,7 @@ export async function serve(options: ServeOptions = {}) {
} }
}) })
require('polka')() polka()
.use(compress, serve) .use(compress, serve)
.listen(port, (err: any) => { .listen(port, (err: any) => {
if (err) throw err 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. // 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 // so that we can write HTML/Vue in the header. One exception is the HTML
// wrapped by <code>(markdown token: '`') tag. // wrapped by <code>(markdown token: '`') tag.
import emojiData from 'markdown-it-emoji/lib/data/full.json'
const parseEmojis = (str: string) => { const parseEmojis = (str: string) => {
const emojiData = require('markdown-it-emoji/lib/data/full.json')
return String(str).replace( return String(str).replace(
/:(.+?):/g, /:(.+?):/g,
(placeholder, key) => emojiData[key] || placeholder (placeholder, key) => emojiData[key] || placeholder

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

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

25
types/shared.d.ts vendored

@ -11,13 +11,36 @@ export interface LocaleConfig {
export interface SiteData<ThemeConfig = any> { export interface SiteData<ThemeConfig = any> {
base: string base: string
/**
* Language of the site as it should be set on the `html` element.
* @example `en-US`, `zh-CN`
*/
lang: string lang: string
title: string title: string
description: string description: string
head: HeadConfig[] head: HeadConfig[]
themeConfig: ThemeConfig themeConfig: ThemeConfig
locales: Record<string, LocaleConfig> 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 = export type HeadConfig =

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