Merge branch 'master' into fix/layout-shift-ads

pull/176/head
Eduardo San Martin Morote 5 years ago committed by GitHub
commit ccde772445
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,65 +1,191 @@
## [0.9.1](https://github.com/vuejs/vitepress/compare/v0.9.0...v0.9.1) (2020-12-05)
## [0.11.4](https://github.com/vuejs/vitepress/compare/v0.11.3...v0.11.4) (2021-01-19)
- Latest Vite beta.32 compat (internal changes).
## [0.11.3](https://github.com/vuejs/vitepress/compare/v0.11.2...v0.11.3) (2021-01-13)
### Bug Fixes
- ignore non-html links in router and prefetch ([3e6e61b](https://github.com/vuejs/vitepress/commit/3e6e61bcea8d4a34079428fcce3ecd25af1ae4f7))
## [0.11.2](https://github.com/vuejs/vitepress/compare/v0.11.1...v0.11.2) (2021-01-12)
### Bug Fixes
- aria label id ([a0f463a](https://github.com/vuejs/vitepress/commit/a0f463af8fd828d24d9a01c3d808d85af8a71c9f))
### Performance Improvements
- generate preload directives for dynamicImport chunks too ([b9fc0cb](https://github.com/vuejs/vitepress/commit/b9fc0cb78d43949b417376498939daa892a33334))
## [0.11.1](https://github.com/vuejs/vitepress/compare/v0.11.0...v0.11.1) (2021-01-12)
### Features
- render content on home page ([ca631c7](https://github.com/vuejs/vitepress/commit/ca631c7f516ad6c643d252dd81e03e29fb3b9e05))
# [0.11.0](https://github.com/vuejs/vitepress/compare/v0.10.8...v0.11.0) (2021-01-12)
### Code Refactoring
- move default theme to 'vitepress/theme' ([a79e1e1](https://github.com/vuejs/vitepress/commit/a79e1e1916a71271728e6fe7c2b734fc2f209518))
### Features
- support customData in config ([4072dc5](https://github.com/vuejs/vitepress/commit/4072dc5f7ede381709fce49e9a29d6af4f7ab81a))
### BREAKING CHANGES
- the default theme is now exposed via 'vitepress/theme',
instead of a named export from 'vitepress'. This change fixes the case where
when a completely custom theme is used, importing anything from 'vitepress'
also imports the entire default theme.
## [0.10.8](https://github.com/vuejs/vitepress/compare/v0.10.7...v0.10.8) (2021-01-11)
### Bug Fixes
- resolve page hash case-insenstively, close [#202](https://github.com/vuejs/vitepress/issues/202) ([#203](https://github.com/vuejs/vitepress/issues/203)) ([bac1ce2](https://github.com/vuejs/vitepress/commit/bac1ce2d01469ff7586437f43b0d665b1c5eb278))
## [0.10.7](https://github.com/vuejs/vitepress/compare/v0.10.6...v0.10.7) (2021-01-05)
### Features
Bump to Vite 2.0.0-beta.8
### Bug Fixes
- scrollbar when using line highlight ([#200](https://github.com/vuejs/vitepress/issues/200)) ([b6ba8a9](https://github.com/vuejs/vitepress/commit/b6ba8a943cc0488410a438c6c2f277c1c33a90bf))
## [0.10.6](https://github.com/vuejs/vitepress/compare/v0.10.5...v0.10.6) (2021-01-04)
### Bug Fixes
- bump vite and fix win32 path resolving ([#198](https://github.com/vuejs/vitepress/issues/198)) ([da2c4f6](https://github.com/vuejs/vitepress/commit/da2c4f694e6dd2d11ff061b8eb7cae2354ae930d))
## [0.10.5](https://github.com/vuejs/vitepress/compare/v0.10.4...v0.10.5) (2021-01-02)
### Bug Fixes
- vite 2.0.0-beta.2 compat ([991a443](https://github.com/vuejs/vitepress/commit/991a443c70c6173aa0100fcccf57f3565e9e38d9))
## [0.10.4](https://github.com/vuejs/vitepress/compare/v0.10.3...v0.10.4) (2021-01-01)
### Bug Fixes
- ensure the same vue dep in all cases ([d6b8568](https://github.com/vuejs/vitepress/commit/d6b8568c52d51d66423a32293879f8bb57756954))
- respect root during build ([055e3fd](https://github.com/vuejs/vitepress/commit/055e3fd043b6ec425f1b0a0cf529bc1ff66acda5))
## [0.10.3](https://github.com/vuejs/vitepress/compare/v0.10.2...v0.10.3) (2021-01-01)
### Bug Fixes
- always define theme globals ([8769b4b](https://github.com/vuejs/vitepress/commit/8769b4b49f398c5244354fbb93fcbecdb9b9c638))
- avoid unexpected vite define replacements in markdown content ([a41928e](https://github.com/vuejs/vitepress/commit/a41928ef83eaf9dcb68be26b1e1f8a3edadfb74a))
- loosen navLink active matching ([8a2ff33](https://github.com/vuejs/vitepress/commit/8a2ff33bf8043b5b0ec21826d7962d7e6337e394))
### Features
- **theme-default:** nav.item.activeMatch ([e262ef6](https://github.com/vuejs/vitepress/commit/e262ef63d89b2bc90c7e42bfc302ba6c602fab16))
- add altAction for home ([9a17ddf](https://github.com/vuejs/vitepress/commit/9a17ddfdfb3cf7afd70d28d697245a298de090e1))
## [0.10.2](https://github.com/vuejs/vitepress/compare/v0.10.1...v0.10.2) (2020-12-31)
### Bug Fixes
- adjust multi sidebar matching logic ([7e4b16e](https://github.com/vuejs/vitepress/commit/7e4b16ee524efc87c150a3d57a3215aac76b3669))
## [0.10.1](https://github.com/vuejs/vitepress/compare/v0.10.0...v0.10.1) (2020-12-30)
### Bug Fixes
* **theme:** the actionLink miss withBase ([#168](https://github.com/vuejs/vitepress/issues/168)) ([#169](https://github.com/vuejs/vitepress/issues/169)) ([ffaca73](https://github.com/vuejs/vitepress/commit/ffaca73992675cef789fe8e13dd8132ae14bbd53))
* align $title with vuepress ([#158](https://github.com/vuejs/vitepress/issues/158)) ([#163](https://github.com/vuejs/vitepress/issues/163)) ([30740d3](https://github.com/vuejs/vitepress/commit/30740d3516e3f7cce0e083faa90a732d9916f9af))
* fix h2 anchor hover ([9bd79e8](https://github.com/vuejs/vitepress/commit/9bd79e8de1827251796d1647b5d258818a94f3b3)), closes [#174](https://github.com/vuejs/vitepress/issues/174)
* fix inline code not inheriting the parent font size ([f5a570f](https://github.com/vuejs/vitepress/commit/f5a570f640c539d96cfa2104613521a70cf2f199))
* fix link prefetch ([ade6ddd](https://github.com/vuejs/vitepress/commit/ade6dddbb5ea72cc7569fcfc46f5e6a362af58ce))
* hydration mismatch when home page having action link ([a7686b7](https://github.com/vuejs/vitepress/commit/a7686b7691a3e3d7d10226fd4f7971929701965a))
* make home page look better ([#154](https://github.com/vuejs/vitepress/issues/154)) ([a084cd3](https://github.com/vuejs/vitepress/commit/a084cd3f782f2aaf78a6542b0c86f67676580a73))
* prevLinks and nextLinks config type ([#165](https://github.com/vuejs/vitepress/issues/165)) ([1b6981a](https://github.com/vuejs/vitepress/commit/1b6981a9157588bc4e29e591ba8a0d9ca5c9c9e8))
* siteData passed to enhanceApp being siteDataByRoute ([#159](https://github.com/vuejs/vitepress/issues/159)) ([01d2837](https://github.com/vuejs/vitepress/commit/01d2837474caef19daaf0be4b3c283dbe85a09da))
- disable css code split ([04dc058](https://github.com/vuejs/vitepress/commit/04dc058cd9977b47eb29c6d2d043e33d92802af8))
- minify ([e3d7fc0](https://github.com/vuejs/vitepress/commit/e3d7fc035376d6d73e350be661925058c84828a8))
### Features
- production ready serve ([2d77eaf](https://github.com/vuejs/vitepress/commit/2d77eafe3b05e9fe76031af9a6c5386c4c6586ac))
### Performance Improvements
- avoid including optional features in build when not used ([c878e6d](https://github.com/vuejs/vitepress/commit/c878e6d3b56ecbd71bd75ff4360446d6dacbd70b))
# [0.10.0](https://github.com/vuejs/vitepress/compare/v0.9.2...v0.10.0) (2020-12-30)
- Upgrade to Vite 2.0
### Bug Fixes
- port fixes to parseHeader utils from vuepress ([#172](https://github.com/vuejs/vitepress/issues/172)) ([dd312ce](https://github.com/vuejs/vitepress/commit/dd312ce86bf9daf4b169e025d4215c05e2ad63c5))
- revert datetime handling ([a1daf2b](https://github.com/vuejs/vitepress/commit/a1daf2b8a012a8a248b3a832d80d6933778087d0))
- style pollution on custom theme ([#190](https://github.com/vuejs/vitepress/issues/190)) ([46e99ba](https://github.com/vuejs/vitepress/commit/46e99babc2d1a0e456d47081c2e7beb961bcd1d5))
- temporarily disable slot usage causing hydration mismatch ([0239159](https://github.com/vuejs/vitepress/commit/02391593bcdd21b621a60aaa0ea2c8cf2ef450d8))
- **md:** avoid normalising markdown "mailto:" links ([#173](https://github.com/vuejs/vitepress/issues/173)) ([18d18d2](https://github.com/vuejs/vitepress/commit/18d18d2eb15a569113ca68ccbb9ba52dfd46c80a))
## [0.9.2](https://github.com/vuejs/vitepress/compare/v0.9.1...v0.9.2) (2020-12-10)
Fix build files
## [0.9.1](https://github.com/vuejs/vitepress/compare/v0.9.0...v0.9.1) (2020-12-05)
### Bug Fixes
- **theme:** the actionLink miss withBase ([#168](https://github.com/vuejs/vitepress/issues/168)) ([#169](https://github.com/vuejs/vitepress/issues/169)) ([ffaca73](https://github.com/vuejs/vitepress/commit/ffaca73992675cef789fe8e13dd8132ae14bbd53))
- align $title with vuepress ([#158](https://github.com/vuejs/vitepress/issues/158)) ([#163](https://github.com/vuejs/vitepress/issues/163)) ([30740d3](https://github.com/vuejs/vitepress/commit/30740d3516e3f7cce0e083faa90a732d9916f9af))
- fix h2 anchor hover ([9bd79e8](https://github.com/vuejs/vitepress/commit/9bd79e8de1827251796d1647b5d258818a94f3b3)), closes [#174](https://github.com/vuejs/vitepress/issues/174)
- fix inline code not inheriting the parent font size ([f5a570f](https://github.com/vuejs/vitepress/commit/f5a570f640c539d96cfa2104613521a70cf2f199))
- fix link prefetch ([ade6ddd](https://github.com/vuejs/vitepress/commit/ade6dddbb5ea72cc7569fcfc46f5e6a362af58ce))
- hydration mismatch when home page having action link ([a7686b7](https://github.com/vuejs/vitepress/commit/a7686b7691a3e3d7d10226fd4f7971929701965a))
- make home page look better ([#154](https://github.com/vuejs/vitepress/issues/154)) ([a084cd3](https://github.com/vuejs/vitepress/commit/a084cd3f782f2aaf78a6542b0c86f67676580a73))
- prevLinks and nextLinks config type ([#165](https://github.com/vuejs/vitepress/issues/165)) ([1b6981a](https://github.com/vuejs/vitepress/commit/1b6981a9157588bc4e29e591ba8a0d9ca5c9c9e8))
- siteData passed to enhanceApp being siteDataByRoute ([#159](https://github.com/vuejs/vitepress/issues/159)) ([01d2837](https://github.com/vuejs/vitepress/commit/01d2837474caef19daaf0be4b3c283dbe85a09da))
### Features
* built-in ClientOnly component ([8809d2d](https://github.com/vuejs/vitepress/commit/8809d2dbfc6818ba1618fa43368a45130d940890))
* **default-theme:** support customLayout: true in frontmatter ([f32771f](https://github.com/vuejs/vitepress/commit/f32771fe8646701410ba4b231a2b0ce38230ab64))
* add `$withBase` global app function ([15e18df](https://github.com/vuejs/vitepress/commit/15e18df01e6e5ca8af605365896fee0024244b37))
* add Algolia DocSearch ([#40](https://github.com/vuejs/vitepress/issues/40)) ([#153](https://github.com/vuejs/vitepress/issues/153)) ([5bb4730](https://github.com/vuejs/vitepress/commit/5bb4730f7f48153ae006d7878431d0b58b0fffee))
* add native support for carbon ads ([#86](https://github.com/vuejs/vitepress/issues/86)) ([9d6b8ca](https://github.com/vuejs/vitepress/commit/9d6b8cadcc6cd59bde6b9b20037f9038190672ce))
* support customizing default theme via slots ([b8e892e](https://github.com/vuejs/vitepress/commit/b8e892e94a2fd2cedf7b25651548a08a758ccbdb))
* add more global and computed properties (#152) ([c6bdcfb](https://github.com/vuejs/vitepress/commit/c6bdcfbf4f14916f20a7192b44941d33d4bee51e)), closes [#152](https://github.com/vuejs/vitepress/issues/152)
- built-in ClientOnly component ([8809d2d](https://github.com/vuejs/vitepress/commit/8809d2dbfc6818ba1618fa43368a45130d940890))
- **default-theme:** support customLayout: true in frontmatter ([f32771f](https://github.com/vuejs/vitepress/commit/f32771fe8646701410ba4b231a2b0ce38230ab64))
- add `$withBase` global app function ([15e18df](https://github.com/vuejs/vitepress/commit/15e18df01e6e5ca8af605365896fee0024244b37))
- add Algolia DocSearch ([#40](https://github.com/vuejs/vitepress/issues/40)) ([#153](https://github.com/vuejs/vitepress/issues/153)) ([5bb4730](https://github.com/vuejs/vitepress/commit/5bb4730f7f48153ae006d7878431d0b58b0fffee))
- add native support for carbon ads ([#86](https://github.com/vuejs/vitepress/issues/86)) ([9d6b8ca](https://github.com/vuejs/vitepress/commit/9d6b8cadcc6cd59bde6b9b20037f9038190672ce))
- support customizing default theme via slots ([b8e892e](https://github.com/vuejs/vitepress/commit/b8e892e94a2fd2cedf7b25651548a08a758ccbdb))
- add more global and computed properties (#152) ([c6bdcfb](https://github.com/vuejs/vitepress/commit/c6bdcfbf4f14916f20a7192b44941d33d4bee51e)), closes [#152](https://github.com/vuejs/vitepress/issues/152)
# [0.9.0](https://github.com/vuejs/vitepress/compare/v0.8.1...v0.9.0) (2020-11-24)
### Bug Fixes
* avoid 300ms click delay on touch devices ([621ca3e](https://github.com/vuejs/vitepress/commit/621ca3e26f65e13e504724aef76aba3f3361ce81))
* fix nested list having too much margin ([b0cf2be](https://github.com/vuejs/vitepress/commit/b0cf2be5614505731a3b6dcebeab949c3639c2b2))
* fix sidebar active status not working as expected ([#140](https://github.com/vuejs/vitepress/issues/140)) ([#149](https://github.com/vuejs/vitepress/issues/149)) ([0b181e7](https://github.com/vuejs/vitepress/commit/0b181e7582ea4be7dca51ec399c697e32b7116f3))
* make code block look prettier ([#146](https://github.com/vuejs/vitepress/issues/146)) ([242fcc1](https://github.com/vuejs/vitepress/commit/242fcc1098f606f13e7d8e123c081e73a3d89366))
* some color in code block not working as expected ([#143](https://github.com/vuejs/vitepress/issues/143)) ([da09266](https://github.com/vuejs/vitepress/commit/da09266f5eede3796bb150ccd9d6a173e90354a4))
- avoid 300ms click delay on touch devices ([621ca3e](https://github.com/vuejs/vitepress/commit/621ca3e26f65e13e504724aef76aba3f3361ce81))
- fix nested list having too much margin ([b0cf2be](https://github.com/vuejs/vitepress/commit/b0cf2be5614505731a3b6dcebeab949c3639c2b2))
- fix sidebar active status not working as expected ([#140](https://github.com/vuejs/vitepress/issues/140)) ([#149](https://github.com/vuejs/vitepress/issues/149)) ([0b181e7](https://github.com/vuejs/vitepress/commit/0b181e7582ea4be7dca51ec399c697e32b7116f3))
- make code block look prettier ([#146](https://github.com/vuejs/vitepress/issues/146)) ([242fcc1](https://github.com/vuejs/vitepress/commit/242fcc1098f606f13e7d8e123c081e73a3d89366))
- some color in code block not working as expected ([#143](https://github.com/vuejs/vitepress/issues/143)) ([da09266](https://github.com/vuejs/vitepress/commit/da09266f5eede3796bb150ccd9d6a173e90354a4))
### Features
* add "last updated" feature ([40d204b](https://github.com/vuejs/vitepress/commit/40d204b2f68b90bd2c5e9940cd128c4c16cd5274))
- add "last updated" feature ([40d204b](https://github.com/vuejs/vitepress/commit/40d204b2f68b90bd2c5e9940cd128c4c16cd5274))
## [0.8.1](https://github.com/vuejs/vitepress/compare/v0.8.0...v0.8.1) (2020-11-20)
### Bug Fixes
* fix "next and prev link" not working when `link` has extention ([6dcf6b3](https://github.com/vuejs/vitepress/commit/6dcf6b3796bb3d6e703fddd79c6b0c0a7adfd567))
* fix "next and prev links" not working when the `base` option is set ([#139](https://github.com/vuejs/vitepress/issues/139)) ([018a9b4](https://github.com/vuejs/vitepress/commit/018a9b46d924d0d08f7ff67f18a813348c84ab0a))
- fix "next and prev link" not working when `link` has extention ([6dcf6b3](https://github.com/vuejs/vitepress/commit/6dcf6b3796bb3d6e703fddd79c6b0c0a7adfd567))
- fix "next and prev links" not working when the `base` option is set ([#139](https://github.com/vuejs/vitepress/issues/139)) ([018a9b4](https://github.com/vuejs/vitepress/commit/018a9b46d924d0d08f7ff67f18a813348c84ab0a))
# [0.8.0](https://github.com/vuejs/vitepress/compare/v0.7.4...v0.8.0) (2020-11-20)
### Bug Fixes
* exit process with non-zero code on error ([fb09f8e](https://github.com/vuejs/vitepress/commit/fb09f8e638c06aec32494f731554fb1b989daaf0))
* fix edit link and prev and next links display ([#97](https://github.com/vuejs/vitepress/issues/97)) ([c3b7172](https://github.com/vuejs/vitepress/commit/c3b71729513592112e233165782e60c9c5b425c4))
* fix next and prev links not working ([#130](https://github.com/vuejs/vitepress/issues/130)) ([fdd498b](https://github.com/vuejs/vitepress/commit/fdd498be70cc09a4331dadd17c4a5339318f21bf))
* display header-anchor links when using keyboard navigation ([ddc3640](https://github.com/vuejs/vitepress/commit/ddc3640ce66f606894b31e1b7ebeacaaf7b0f1b5))
* show top part of scrollbar in sidebar ([#129](https://github.com/vuejs/vitepress/issues/129)) ([1ba209a](https://github.com/vuejs/vitepress/commit/1ba209a4d2b606bee1abb7ec1d383467d98cf198))
- exit process with non-zero code on error ([fb09f8e](https://github.com/vuejs/vitepress/commit/fb09f8e638c06aec32494f731554fb1b989daaf0))
- fix edit link and prev and next links display ([#97](https://github.com/vuejs/vitepress/issues/97)) ([c3b7172](https://github.com/vuejs/vitepress/commit/c3b71729513592112e233165782e60c9c5b425c4))
- fix next and prev links not working ([#130](https://github.com/vuejs/vitepress/issues/130)) ([fdd498b](https://github.com/vuejs/vitepress/commit/fdd498be70cc09a4331dadd17c4a5339318f21bf))
- display header-anchor links when using keyboard navigation ([ddc3640](https://github.com/vuejs/vitepress/commit/ddc3640ce66f606894b31e1b7ebeacaaf7b0f1b5))
- show top part of scrollbar in sidebar ([#129](https://github.com/vuejs/vitepress/issues/129)) ([1ba209a](https://github.com/vuejs/vitepress/commit/1ba209a4d2b606bee1abb7ec1d383467d98cf198))
### Features
* add ability to configure markdown options ([#127](https://github.com/vuejs/vitepress/issues/127)) ([#128](https://github.com/vuejs/vitepress/issues/128)) ([463a03a](https://github.com/vuejs/vitepress/commit/463a03a9815ce8fc9f55293dda07bc211ef4f62b))
* add serve command ([#136](https://github.com/vuejs/vitepress/issues/136)) ([67868bd](https://github.com/vuejs/vitepress/commit/67868bd9281077a4ce708e666bf61a7824afb8b2))
* better build command output ([e435eec](https://github.com/vuejs/vitepress/commit/e435eec94a841ab0e1c14d59bb13608d5ad6a011))
- add ability to configure markdown options ([#127](https://github.com/vuejs/vitepress/issues/127)) ([#128](https://github.com/vuejs/vitepress/issues/128)) ([463a03a](https://github.com/vuejs/vitepress/commit/463a03a9815ce8fc9f55293dda07bc211ef4f62b))
- add serve command ([#136](https://github.com/vuejs/vitepress/issues/136)) ([67868bd](https://github.com/vuejs/vitepress/commit/67868bd9281077a4ce708e666bf61a7824afb8b2))
- better build command output ([e435eec](https://github.com/vuejs/vitepress/commit/e435eec94a841ab0e1c14d59bb13608d5ad6a011))
## [0.7.4](https://github.com/vuejs/vitepress/compare/v0.7.3...v0.7.4) (2020-11-11)

@ -16,8 +16,8 @@ describe('client/theme-default/support/sideBar', () => {
it('gets the correct sidebar items from the given path', () => {
const sidebar = {
'/': [{ text: 'R', link: 'r' }],
'/guide/': [{ text: 'G', link: 'g' }]
'/guide/': [{ text: 'G', link: 'g' }],
'/': [{ text: 'R', link: 'r' }]
}
expect(getSideBarConfig(sidebar, '/')).toEqual(sidebar['/'])
@ -31,7 +31,8 @@ describe('client/theme-default/support/sideBar', () => {
}
expect(getSideBarConfig(s, '/guide/')).toEqual(s['/guide/'])
expect(getSideBarConfig(s, '/guide')).toEqual(s['/guide/'])
// no ending slash should not match
expect(getSideBarConfig(s, '/guide')).not.toEqual(s['/guide/'])
expect(getSideBarConfig(s, 'guide/')).toEqual(s['/guide/'])
expect(getSideBarConfig(s, 'guide/nested')).toEqual(s['/guide/'])
expect(getSideBarConfig(s, '/guide/nested')).toEqual(s['/guide/'])

@ -0,0 +1,33 @@
import { deeplyParseHeader } from 'node/utils/parseHeader'
test('deeplyParseHeader', () => {
const asserts: Record<string, string> = {
// Remove tail html
'# `H1` <Comp></Comp>': '# H1',
'# *H1* <Comp/>': '# H1',
// Reserve code-wrapped tail html
'# `H1` `<Comp></Comp>`': '# H1 <Comp></Comp>',
'# *H1* `<Comp/>`': '# H1 <Comp/>',
// Remove leading html
'# <Comp></Comp> `H1`': '# H1',
'# <Comp/> *H1*': '# H1',
// Reserve code-wrapped leading html
'# `<Comp></Comp>` `H1`': '# <Comp></Comp> H1',
'# `<Comp/>` *H1*': '# <Comp/> H1',
// Remove middle html
'# `H1` <Comp></Comp> `H2`': '# H1 H2',
'# `H1` <Comp/> `H2`': '# H1 H2',
// Reserve middle html
'# `H1` `<Comp></Comp>` `H2`': '# H1 <Comp></Comp> H2',
'# `H1` `<Comp/>` `H2`': '# H1 <Comp/> H2'
}
Object.keys(asserts).forEach((input) => {
expect(deeplyParseHeader(input)).toBe(asserts[input])
})
})

@ -0,0 +1,38 @@
import { parseHeader } from 'node/utils/parseHeader'
describe('parseHeader', () => {
test('should unescape html', () => {
const input = `&lt;div :id=&quot;&#39;app&#39;&quot;&gt;`
expect(parseHeader(input)).toBe(`<div :id="'app'">`)
})
test('should remove markdown tokens correctly', () => {
const asserts: Record<string, string> = {
// vuepress #238
'[vue](vuejs.org)': 'vue',
'`vue`': 'vue',
'*vue*': 'vue',
'**vue**': 'vue',
'***vue***': 'vue',
_vue_: 'vue',
'\\_vue\\_': '_vue_',
'\\*vue\\*': '*vue*',
'\\!vue\\!': '!vue!',
// vuepress #2688
'[vue](vuejs.org) / [vue](vuejs.org)': 'vue / vue',
'[\\<ins>](vuejs.org)': '<ins>',
// vuepress #564 For multiple markdown tokens
'`a` and `b`': 'a and b',
'***bold and italic***': 'bold and italic',
'**bold** and *italic*': 'bold and italic',
// escaping \$
'\\$vue': '$vue'
}
Object.keys(asserts).forEach((input) => {
expect(parseHeader(input)).toBe(asserts[input])
})
})
})

@ -0,0 +1,56 @@
import { removeNonCodeWrappedHTML } from 'node/utils/parseHeader'
test('removeNonCodeWrappedHTML', () => {
const asserts: Record<string, string> = {
// Remove tail html
'# H1 <Comp></Comp>': '# H1 ',
'# H1<Comp></Comp>': '# H1',
'# H1 <Comp a="b"></Comp>': '# H1 ',
'# H1<Comp a="b"></Comp>': '# H1',
'# H1 <Comp/>': '# H1 ',
'# H1<Comp/>': '# H1',
'# H1 <Comp a="b"/>': '# H1 ',
'# H1<Comp a="b"/>': '# H1',
// Reserve code-wrapped tail html
'# H1 `<Comp></Comp>`': '# H1 `<Comp></Comp>`',
'# H1 `<Comp a="b"></Comp>`': '# H1 `<Comp a="b"></Comp>`',
'# H1 `<Comp/>`': '# H1 `<Comp/>`',
'# H1 `<Comp a="b"/>`': '# H1 `<Comp a="b"/>`',
// Remove leading html
'# <Comp></Comp> H1': '# H1',
'# <Comp></Comp>H1': '# H1',
'# <Comp a="b"></Comp> H1': '# H1',
'# <Comp a="b"></Comp>H1': '# H1',
'# <Comp/> H1': '# H1',
'# <Comp/>H1': '# H1',
'# <Comp a="b"/> H1': '# H1',
'# <Comp a="b"/>H1': '# H1',
// Reserve code-wrapped leading html
'# `<Comp></Comp>` H1': '# `<Comp></Comp>` H1',
'# `<Comp a="b"></Comp>` H1': '# `<Comp a="b"></Comp>` H1',
'# `<Comp/>` H1': '# `<Comp/>` H1',
'# `<Comp a="b"/>` H1': '# `<Comp a="b"/>` H1',
// Remove middle html
'# H1 <Comp></Comp> H2': '# H1 H2',
'# H1 <Comp a="b"></Comp> H2': '# H1 H2',
'# H1 <Comp/> H2': '# H1 H2',
'# H1 <Comp a="b"/> H2': '# H1 H2',
// Reserve code-wrapped middle html
'# H1 `<Comp></Comp>` H2': '# H1 `<Comp></Comp>` H2',
'# H1 `<Comp a="b"></Comp>` H2': '# H1 `<Comp a="b"></Comp>` H2',
'# H1 `<Comp/>` H2': '# H1 `<Comp/>` H2',
'# H1 `<Comp a="b"/>` H2': '# H1 `<Comp a="b"/>` H2',
// vuepress #2688
'# \\<ins>': '# \\<ins>'
}
Object.keys(asserts).forEach((input) => {
expect(removeNonCodeWrappedHTML(input)).toBe(asserts[input])
})
})

@ -1,44 +1,2 @@
#!/usr/bin/env node
const chalk = require('chalk')
const argv = require('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]
if (root) {
argv.root = root
}
if (!command || command === 'dev') {
const port = argv.port || 3000
require('../dist/node')
.createServer(argv)
.then((server) => {
server.listen(port, () => {
console.log(`listening at http://localhost:${port}`)
})
})
.catch((err) => {
console.error(chalk.red(`failed to start server. error:\n`), err)
process.exit(1)
})
} else if (command === 'build') {
require('../dist/node')
.build(argv)
.catch((err) => {
console.error(chalk.red(`build error:\n`), err)
process.exit(1)
})
} else if (command === 'serve') {
require('../dist/node')
.serve(argv)
.catch((err) => {
console.error(chalk.red(`failed to start server. error:\n`), err)
process.exit(1)
})
} else {
console.log(chalk.red(`unknown command "${command}".`))
process.exit(1)
}
require('../dist/node/cli')

@ -23,8 +23,12 @@ module.exports = {
},
nav: [
{ text: 'Guide', link: '/' },
{ text: 'Config Reference', link: '/config/basics' },
{ text: 'Guide', link: '/', activeMatch: '^/$|^/guide/' },
{
text: 'Config Reference',
link: '/config/basics',
activeMatch: '^/config/'
},
{
text: 'Release Notes',
link: 'https://github.com/vuejs/vitepress/releases'
@ -32,9 +36,9 @@ module.exports = {
],
sidebar: {
'/': getGuideSidebar(),
'/guide/': getGuideSidebar(),
'/config/': getConfigSidebar()
'/config/': getConfigSidebar(),
'/': getGuideSidebar()
}
}
}

@ -34,7 +34,7 @@ $ yarn docs:build
$ yarn docs:serve
```
The `serve` command will boot up local static web server that serves the files from `.vitepress/dist` at http://localhost:3000. It's an easy way to check if the production build looks OK in your local environment.
The `serve` command will boot up local static web server that serves the files from `.vitepress/dist` at http://localhost:5000. It's an easy way to check if the production build looks OK in your local environment.
You may configure the port of the server py passing `--port` flag as an argument.

@ -64,6 +64,14 @@ Reference of `$page.frontmatter`.
}
```
## $lang
The language of the current page. Default: `en-US`.
## $localePath
The locale path prefix for the current page. Default: `/`.
## $title
Value of the `<title>` label used for the current page.

@ -20,7 +20,7 @@ Now, with Vite and Vue 3, it is time to rethink what a "Vue-powered static site
## Improvements Over VuePress
There're couple of things that are improved from VuePress.
There're couple of things that are improved from VuePress....
### It Uses Vue 3

@ -0,0 +1,3 @@
/assets/*
cache-control: max-age=31536000
cache-control: immutable

@ -1,6 +1,6 @@
{
"name": "vitepress",
"version": "0.9.1",
"version": "0.11.4",
"description": "Vite & Vue powered static site generator",
"main": "dist/node/index.js",
"typings": "types/index.d.ts",
@ -29,11 +29,12 @@
"release": "node scripts/release.js",
"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": "yarn docs-build && node ./bin/vitepress serve --root docs"
"docs-serve": "node ./bin/vitepress serve docs"
},
"engines": {
"node": ">=10.0.0"
"node": ">=12.0.0"
},
"gitHooks": {
"pre-commit": "lint-staged"
@ -64,9 +65,11 @@
"dependencies": {
"@docsearch/css": "^1.0.0-alpha.28",
"@docsearch/js": "^1.0.0-alpha.28",
"@vue/compiler-sfc": "^3.0.3",
"@vue/server-renderer": "^3.0.3",
"@vitejs/plugin-vue": "^1.1.0",
"@vue/compiler-sfc": "^3.0.5",
"@vue/server-renderer": "^3.0.5",
"chalk": "^4.1.0",
"compression": "^1.7.4",
"debug": "^4.1.1",
"diacritics": "^1.3.0",
"escape-html": "^1.0.3",
@ -81,12 +84,15 @@
"markdown-it-table-of-contents": "^0.4.4",
"minimist": "^1.2.5",
"ora": "^5.1.0",
"polka": "^0.5.2",
"prismjs": "^1.20.0",
"sirv": "^1.0.10",
"slash": "^3.0.0",
"vite": "^1.0.0-rc.13",
"vue": "^3.0.3"
"vite": "^2.0.0-beta.32",
"vue": "^3.0.5"
},
"devDependencies": {
"@types/compression": "^1.7.0",
"@types/fs-extra": "^9.0.1",
"@types/jest": "^26.0.15",
"@types/koa": "^2.11.6",
@ -100,8 +106,6 @@
"enquirer": "^2.3.6",
"execa": "^4.1.0",
"jest": "^26.6.3",
"koa": "^2.13.0",
"koa-static": "^5.0.0",
"lint-staged": "^10.3.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5",
@ -109,7 +113,7 @@
"rollup": "^2.33.3",
"semver": "^7.3.2",
"ts-jest": "^26.4.4",
"typescript": "^3.8.3",
"typescript": "^4.1.3",
"yorkie": "^2.0.0"
}
}

@ -8,8 +8,6 @@ const currentVersion = require('../package.json').version
const versionIncrements = ['patch', 'minor', 'major']
const tags = ['latest', 'next']
const inc = (i) => semver.inc(currentVersion, i)
const bin = (name) => path.resolve(__dirname, `../node_modules/.bin/${name}`)
const run = (bin, args, opts = {}) =>
@ -43,19 +41,10 @@ async function main() {
throw new Error(`Invalid target version: ${targetVersion}`)
}
const { tag } = await prompt({
type: 'select',
name: 'tag',
message: 'Select tag type',
choices: tags
})
console.log(tag)
const { yes: tagOk } = await prompt({
type: 'confirm',
name: 'yes',
message: `Releasing v${targetVersion} with the "${tag}" tag. Confirm?`
message: `Releasing v${targetVersion}. Confirm?`
})
if (!tagOk) {
@ -73,6 +62,7 @@ async function main() {
// Generate the changelog.
step('\nGenerating the changelog...')
await run('yarn', ['changelog'])
await run('yarn', ['prettier', '--write', 'CHANGELOG.md'])
const { yes: changelogOk } = await prompt({
type: 'confirm',
@ -84,17 +74,16 @@ async function main() {
return
}
// Commit changes to the Git.
// Commit changes to the Git and create a tag.
step('\nCommitting changes...')
await run('git', ['add', '-A'])
await run('git', ['add', 'CHANGELOG.md', 'package.json'])
await run('git', ['commit', '-m', `release: v${targetVersion}`])
await run('git', ['tag', `v${targetVersion}`])
// Publish the package.
step('\nPublishing the package...')
await run('yarn', [
'publish',
'--tag',
tag,
'--new-version',
targetVersion,
'--no-commit-hooks',
@ -103,7 +92,6 @@ async function main() {
// Push to GitHub.
step('\nPushing to GitHub...')
await run('git', ['tag', `v${targetVersion}`])
await run('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
await run('git', ['push'])
}

@ -8,11 +8,9 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
<script setup lang="ts">
import { ref, watch } from 'vue'
export default defineComponent({
setup() {
const el = ref<HTMLElement | null>(null)
const open = ref(false)
@ -21,13 +19,6 @@ export default defineComponent({
el.value!.scrollTop = 0
}
})
return {
el,
open
}
}
})
</script>
<style scoped>

@ -7,7 +7,7 @@ export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
let isFirstUpdate = true
const updateHeadTags = (newTags: HeadConfig[]) => {
if (process.env.NODE_ENV === 'production' && isFirstUpdate) {
if (import.meta.env.PROD && isFirstUpdate) {
// in production, the initial meta tags are already pre-rendered so we
// skip the first update.
isFirstUpdate = false

@ -75,6 +75,11 @@ export function usePrefetch() {
rIC(() => {
document.querySelectorAll('#app a').forEach((link) => {
const { target, hostname, pathname } = link as HTMLAnchorElement
const extMatch = pathname.match(/\.\w+$/)
if (extMatch && extMatch[0] !== '.html') {
return
}
if (
// only prefetch same tab navigation, since a new tab will load
// the lean js chunk instead.

@ -16,7 +16,7 @@ function parse(data: string): SiteData {
// hmr
if (import.meta.hot) {
import.meta.hot!.acceptDeps('/@siteData', (m) => {
import.meta.hot!.accept('/@siteData', (m) => {
siteDataRef.value = parse(m.default)
})
}

@ -1,29 +0,0 @@
// exports in this file are exposed to themes and md files via 'vitepress'
// so the user can do `import { useRoute, useSiteData } from 'vitepress'`
// generic types
export type { Router, Route } from './router'
// theme types
export * from './theme'
// composables
export { useRouter, useRoute } from './router'
export { useSiteData } from './composables/siteData'
export { useSiteDataByRoute } from './composables/siteDataByRoute'
export { usePageData } from './composables/pageData'
export { useFrontmatter } from './composables/frontmatter'
// utilities
export { inBrowser, joinPath } from './utils'
// components
export { Content } from './components/Content'
import { ComponentOptions } from 'vue'
import _Debug from './components/Debug.vue'
const Debug = _Debug as ComponentOptions
export { Debug }
// default theme
export { default as defaultTheme } from '/@default-theme/index'

@ -1,2 +0,0 @@
<div id="app"></div>
<script type="module" src="/@app/index.js"></script>

@ -1,3 +1,4 @@
import 'vite/dynamic-import-polyfill'
import { App, createApp as createClientApp, createSSRApp, h } from 'vue'
import { inBrowser, pathToFile } from './utils'
import { Router, RouterSymbol, createRouter } from './router'
@ -54,7 +55,7 @@ export function createApp() {
}
function newApp(): App {
return process.env.NODE_ENV === 'production'
return import.meta.env.PROD
? createSSRApp(VitePressApp)
: createClientApp(VitePressApp)
}
@ -85,6 +86,7 @@ function newRouter(): Router {
}
// SSR: sync require
// @ts-ignore
return require(pageFilePath)
}, NotFound)
}
@ -111,7 +113,7 @@ function shouldHotReload(payload: any): boolean {
if (inBrowser) {
const { app, router } = createApp()
// wait unitl page component is fetched before mounting
// wait until page component is fetched before mounting
router.go().then(() => {
app.mount('#app')
})

@ -1,9 +1,8 @@
import { App } from 'vue'
import { App, defineAsyncComponent } from 'vue'
import { joinPath } from './utils'
import { SiteDataRef } from './composables/siteData'
import { PageDataRef } from './composables/pageData'
import { Content } from './components/Content'
import Debug from './components/Debug.vue'
import { ClientOnly } from './components/ClientOnly'
export function mixinGlobalComputed(
@ -43,6 +42,25 @@ export function mixinGlobalComputed(
}
},
$lang: {
get() {
return siteByRoute.value.lang
}
},
$localePath: {
get() {
const { locales } = site.value
const { lang } = siteByRoute.value
const path = Object.keys(locales).find(
(lp) => locales[lp].lang === lang
)
return (locales && path) || '/'
}
},
$title: {
get() {
return page.value.title
@ -66,9 +84,12 @@ export function mixinGlobalComputed(
}
export function mixinGlobalComponents(app: App) {
const isProd = process.env.NODE_ENV === 'production'
app.component('Content', Content)
app.component('ClientOnly', ClientOnly)
app.component('Debug', isProd ? () => null : Debug)
app.component(
'Debug',
import.meta.env.PROD
? () => null
: defineAsyncComponent(() => import('./components/Debug.vue'))
)
}

@ -23,7 +23,7 @@ const getDefaultRoute = (): Route => ({
path: '/',
component: null,
// this will be set upon initial page load, which is before
// the app is mounted, so it's guaranteed to be avaiable in
// the app is mounted, so it's guaranteed to be available in
// components
data: null as any
})
@ -114,6 +114,7 @@ export function createRouter(
if (link) {
const { href, protocol, hostname, pathname, hash, target } = link
const currentUrl = window.location
const extMatch = pathname.match(/\.\w+$/)
// only intercept inbound links
if (
!e.ctrlKey &&
@ -122,7 +123,8 @@ export function createRouter(
!e.metaKey &&
target !== `_blank` &&
protocol === currentUrl.protocol &&
hostname === currentUrl.hostname
hostname === currentUrl.hostname &&
!(extMatch && extMatch[0] !== '.html')
) {
e.preventDefault()
if (pathname === currentUrl.pathname) {

@ -17,7 +17,7 @@ export function pathToFile(path: string): string {
}
if (import.meta.env.DEV) {
// awlays force re-fetch content in dev
// always force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`
} else {
// in production, each .md file is built into a .md.js file following
@ -28,8 +28,8 @@ export function pathToFile(path: string): string {
pagePath = pagePath.slice(base.length).replace(/\//g, '_') + '.md'
// client production build needs to account for page hash, which is
// injected directly in the page's html
const pageHash = __VP_HASH_MAP__[pagePath]
pagePath = `${base}_assets/${pagePath}.${pageHash}.js`
const pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]
pagePath = `${base}assets/${pagePath}.${pageHash}.js`
} else {
// ssr build uses much simpler name mapping
pagePath = `./${pagePath.slice(1).replace(/\//g, '_')}.md.js`

@ -0,0 +1,26 @@
// exports in this file are exposed to themes and md files via 'vitepress'
// so the user can do `import { useRoute, useSiteData } from 'vitepress'`
// generic types
export type { Router, Route } from './app/router'
// theme types
export type { Theme, EnhanceAppContext } from './app/theme'
// composables
export { useRouter, useRoute } from './app/router'
export { useSiteData } from './app/composables/siteData'
export { useSiteDataByRoute } from './app/composables/siteDataByRoute'
export { usePageData } from './app/composables/pageData'
export { useFrontmatter } from './app/composables/frontmatter'
// utilities
export { inBrowser, joinPath } from './app/utils'
// components
export { Content } from './app/components/Content'
import { ComponentOptions } from 'vue'
import _Debug from './app/components/Debug.vue'
const Debug = _Debug as ComponentOptions
export { Debug }

@ -1,5 +1,7 @@
declare const __VP_HASH_MAP__: Record<string, string>
declare const __CARBON__: boolean
declare const __BSA__: boolean
declare const __ALGOLIA__: boolean
declare module '*.vue' {
import { ComponentOptions } from 'vue'
const comp: ComponentOptions
@ -18,5 +20,6 @@ declare module '@docsearch/js' {
}
declare module '@docsearch/css' {
export default string
const css: string
export default css
}

@ -80,16 +80,20 @@ import type { DefaultTheme } from './config'
import NavBar from './components/NavBar.vue'
import SideBar from './components/SideBar.vue'
import Page from './components/Page.vue'
const Home = defineAsyncComponent(() => import('./components/Home.vue'))
const CarbonAds = defineAsyncComponent(
const NoopComponent = () => null
const CarbonAds = __CARBON__ ? defineAsyncComponent(
() => import('./components/CarbonAds.vue')
)
const BuySellAds = defineAsyncComponent(
) : NoopComponent
const BuySellAds = __BSA__ ? defineAsyncComponent(
() => import('./components/BuySellAds.vue')
)
const AlgoliaSearchBox = defineAsyncComponent(
) : NoopComponent
const AlgoliaSearchBox = __ALGOLIA__ ? defineAsyncComponent(
() => import('./components/AlgoliaSearchBox.vue')
)
) : NoopComponent
// generic state
const route = useRoute()

@ -2,13 +2,8 @@
<div class="algolia-search-box" id="docsearch" />
</template>
<script lang="ts">
// TODO: @vue/compiler-sfc currently has a bug that removes `import 'foo'`
// statements in <script setup> so we put it here for now
import '@docsearch/css'
</script>
<script setup lang="ts">
import '@docsearch/css'
import { useRoute, useRouter } from 'vitepress'
import { defineProps, getCurrentInstance, onMounted, watch } from 'vue'
import docsearch from '@docsearch/js'
@ -144,6 +139,7 @@ function initialize(userOptions: any) {
@media (min-width: 720px) {
.algolia-search-box {
padding-left: 8px;
min-width: 176.3px; /* avoid layout shift */
}
}

@ -28,6 +28,7 @@ onMounted(() => {
max-width: 280px;
font-size: 0.75rem;
background-color: rgba(255, 255, 255, 0.8);
min-height: 105.38px; /* avoid layout shift on mobile */
}
.carbon-ads::after {
@ -43,6 +44,8 @@ onMounted(() => {
margin: -8px -8px 24px 24px;
padding: 8px;
width: 146px;
max-width: 100%;
min-height: 200px;
}
}
@ -54,7 +57,7 @@ onMounted(() => {
}
}
.carbon-ads ::v-deep(.carbon-img) {
.carbon-ads :deep(.carbon-img) {
float: left;
margin-right: 0.75rem;
max-width: 100px;
@ -62,7 +65,7 @@ onMounted(() => {
}
@media (min-width: 420px) {
.carbon-ads ::v-deep(.carbon-img) {
.carbon-ads :deep(.carbon-img) {
float: none;
display: block;
margin-right: 0;
@ -70,24 +73,24 @@ onMounted(() => {
}
}
.carbon-ads ::v-deep(.carbon-img img) {
.carbon-ads :deep(.carbon-img img) {
display: block;
width: 100%;
}
@media (min-width: 420px) {
.carbon-ads ::v-deep(.carbon-text) {
.carbon-ads :deep(.carbon-text) {
padding-top: 8px;
}
}
.carbon-ads ::v-deep(.carbon-text) {
.carbon-ads :deep(.carbon-text) {
display: block;
font-weight: 400;
color: var(--c-text-light);
}
.carbon-ads ::v-deep(.carbon-poweredby) {
.carbon-ads :deep(.carbon-poweredby) {
display: block;
padding-top: 2px;
font-weight: 400;

@ -3,6 +3,9 @@
<HomeHero />
<slot name="hero" />
<HomeFeatures />
<div class="home-content">
<Content />
</div>
<slot name="features" />
<HomeFooter />
<slot name="footer" />
@ -19,4 +22,17 @@ import HomeFooter from './HomeFooter.vue'
.home {
padding-top: var(--header-height);
}
.home-content {
max-width: 960px;
margin: 0px auto;
padding: 0 1.5rem;
}
@media (max-width: 720px) {
.home-content {
max-width: 392px;
padding: 0;
}
}
</style>

@ -1,17 +1,23 @@
<template>
<header v-if="showHero" class="home-hero">
<figure v-if="$frontmatter.heroImage" class="figure">
<img
class="image"
:src="$withBase($frontmatter.heroImage)"
:alt="$frontmatter.heroAlt"
>
<img class="image" :src="$withBase($frontmatter.heroImage)" :alt="$frontmatter.heroAlt" />
</figure>
<h1 v-if="hasHeroText" class="title">{{ heroText }}</h1>
<h1 v-if="hasHeroText" id="main-title" class="title">{{ heroText }}</h1>
<p v-if="hasTagline" class="description">{{ tagline }}</p>
<NavLink v-if="hasAction" :item="action" class="action" />
<NavLink
v-if="hasAction"
:item="{ link: data.actionLink, text: data.actionText }"
class="action"
/>
<NavLink
v-if="hasAltAction"
:item="{ link: data.altActionLink, text: data.altActionText }"
class="action alt"
/>
</header>
</template>
@ -37,10 +43,7 @@ const hasTagline = computed(() => data.value.tagline !== null)
const tagline = computed(() => data.value.tagline || site.value.description)
const hasAction = computed(() => data.value.actionLink && data.value.actionText)
const action = computed(() => ({
link: data.value.actionLink,
text: data.value.actionText,
}))
const hasAltAction = computed(() => data.value.altActionLink && data.value.altActionText)
</script>
<style scoped>
@ -93,7 +96,7 @@ const action = computed(() => ({
.description {
margin: 0;
margin-top: .25rem;
margin-top: 0.25rem;
line-height: 1.3;
font-size: 1.2rem;
color: var(--c-text-light);
@ -108,24 +111,36 @@ const action = computed(() => ({
.action {
margin-top: 1.5rem;
display: inline-block;
}
.action.alt {
margin-left: 1.5rem;
}
@media (min-width: 420px) {
.action {
margin-top: 2rem;
display: inline-block;
}
}
.action :deep(.item) {
display: inline-block;
border-radius: 4px;
border-radius: 6px;
padding: 0 20px;
line-height: 48px;
line-height: 44px;
font-size: 1rem;
font-weight: 500;
color: #ffffff;
background-color: var(--c-brand);
transition: background-color .1s ease;
border: 2px solid var(--c-brand);
transition: background-color 0.1s ease;
}
.action.alt :deep(.item) {
background-color: #fff;
color: var(--c-brand);
}
.action :deep(.item:hover) {
@ -137,7 +152,7 @@ const action = computed(() => ({
@media (min-width: 420px) {
.action :deep(.item) {
padding: 0 24px;
line-height: 56px;
line-height: 52px;
font-size: 1.2rem;
font-weight: 500;
}

@ -5,17 +5,13 @@
</p>
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted } from 'vue'
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useSiteDataByRoute, usePageData } from 'vitepress'
export default defineComponent({
setup() {
const site = useSiteDataByRoute()
const page = usePageData()
const datetime = ref('')
const hasLastUpdated = computed(() => {
const lu = site.value.themeConfig.lastUpdated
@ -24,21 +20,15 @@ export default defineComponent({
const prefix = computed(() => {
const p = site.value.themeConfig.lastUpdated
return p === true ? 'Last Updated' : p
})
const datetime = ref('')
onMounted(() => {
// locale string might be different based on end user
// and will lead to potential hydration mismatch if calculated at build time
datetime.value = new Date(page.value.lastUpdated).toLocaleString('en-US')
})
return {
hasLastUpdated,
prefix,
datetime
}
}
})
</script>
<style scoped>
@ -46,7 +36,7 @@ export default defineComponent({
display: inline-block;
margin: 0;
line-height: 1.4;
font-size: .9rem;
font-size: 0.9rem;
color: var(--c-text-light);
}

@ -9,17 +9,9 @@
</footer>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import EditLink from './EditLink.vue'
import LastUpdated from './LastUpdated.vue'
export default defineComponent({
components: {
EditLink,
LastUpdated
}
})
</script>
<style scoped>

@ -113,7 +113,7 @@ function isAnchorActive(
}
function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeout: NodeJS.Timeout
let timeout: number
let called = false
return () => {

@ -11,10 +11,22 @@ export function useNavLink(item: DefaultTheme.NavItemWithLink) {
const isExternal = isExternalCheck(item.link)
const props = computed(() => {
const routePath = normalizePath(route.path)
let active = false
if (item.activeMatch) {
active = new RegExp(item.activeMatch).test(routePath)
} else {
const itemPath = normalizePath(withBase(item.link))
active =
itemPath === '/'
? itemPath === routePath
: routePath.startsWith(itemPath)
}
return {
class: {
active:
normalizePath(withBase(item.link)) === normalizePath(route.path),
active,
isExternal
},
href: isExternal ? item.link : withBase(item.link),
@ -31,14 +43,9 @@ export function useNavLink(item: DefaultTheme.NavItemWithLink) {
}
function normalizePath(path: string): string {
path = path
return path
.replace(/#.*$/, '')
.replace(/\?.*$/, '')
.replace(/\.html$/, '')
if (path.endsWith('/')) {
path += 'index'
}
return path
.replace(/\.(html|md)$/, '')
.replace(/\/index$/, '/')
}

@ -15,7 +15,7 @@ export namespace DefaultTheme {
* Customize the header label. Defaults to GitHub/Gitlab/Bitbucket
* depending on the provided repo.
*
* @exampe `"Contribute!"`
* @example `"Contribute!"`
*/
repoLabel?: string
@ -80,6 +80,7 @@ export namespace DefaultTheme {
target?: string
rel?: string
ariaLabel?: string
activeMatch?: string
}
export interface NavItemWithLink extends NavItemBase {

@ -85,6 +85,7 @@ li > div[class*='language-'] {
font-family: var(--code-font-family);
font-size: var(--code-font-size);
user-select: none;
overflow: hidden;
}
.highlight-lines .highlighted {

@ -1,10 +1,5 @@
import { DefaultTheme } from '../config'
import {
isArray,
ensureSlash,
ensureStartingSlash,
removeExtention
} from '../utils'
import { isArray, ensureStartingSlash, removeExtention } from '../utils'
export function isSideBarConfig(
sidebar: DefaultTheme.SideBarConfig | DefaultTheme.MultiSideBarConfig
@ -32,15 +27,11 @@ export function getSideBarConfig(
return sidebar
}
// get the very first segment of the path to compare with nulti sidebar keys
// and make sure it's surrounded by slash
path = removeExtention(path)
path = ensureStartingSlash(path).split('/')[1] || '/'
path = ensureSlash(path)
path = ensureStartingSlash(path)
for (const dir in sidebar) {
// make sure the multi sidebar key is surrounded by slash too
if (path === ensureSlash(dir)) {
// make sure the multi sidebar key starts with slash too
if (path.startsWith(ensureStartingSlash(dir))) {
return sidebar[dir]
}
}

@ -3,15 +3,15 @@
"compilerOptions": {
"baseUrl": ".",
"outDir": "../../dist/client",
"target": "esnext",
"module": "esnext",
"lib": ["ESNext", "DOM"],
"types": ["vite"],
"types": ["vite/client"],
"paths": {
"/@theme/*": ["theme-default/*"],
"/@default-theme/*": ["theme-default/*"],
"/@shared/*": ["shared/*"],
"/@types/*": ["../../types/*"],
"vitepress": ["app/exports.ts"]
"/@theme/*": ["theme-default/*"],
"vitepress": ["index.ts"]
}
},
"include": [

@ -0,0 +1,57 @@
import path from 'path'
import { Alias, AliasOptions } from 'vite'
import { UserConfig } from './config'
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'
)
// special virtual file
// we can't directly import '/@siteData' because
// - it's not an actual file so we can't use tsconfig paths to redirect it
// - TS doesn't allow shimming a module that starts with '/'
export const SITE_DATA_ID = '@siteData'
export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
export function resolveAliases(
themeDir: string,
userConfig: UserConfig
): AliasOptions {
const paths: Record<string, string> = {
...userConfig.alias,
'/@theme': themeDir,
'/@shared': SHARED_PATH,
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
}
const aliases: Alias[] = [
...Object.keys(paths).map((p) => ({
find: p,
replacement: paths[p]
})),
{
find: /^vitepress$/,
replacement: path.join(__dirname, '../client/index')
},
{
find: /^vitepress\/theme$/,
replacement: path.join(__dirname, '../client/theme-default/index')
},
// alias for local linked development
{ find: /^vitepress\//, replacement: PKG_ROOT + '/' },
// make sure it always use the same vue dependency that comes with
// vitepress itself
{
find: /^vue$/,
replacement: require.resolve(
'@vue/runtime-dom/dist/runtime-dom.esm-bundler.js'
)
}
]
return aliases
}

@ -1,24 +1,16 @@
import fs from 'fs-extra'
import { bundle, okMark, failMark } from './bundle'
import { BuildConfig as ViteBuildOptions } from 'vite'
import { BuildOptions } from 'vite'
import { resolveConfig } from '../config'
import { renderPage } from './render'
import { OutputChunk, OutputAsset } from 'rollup'
import ora from 'ora'
export type BuildOptions = Pick<
Partial<ViteBuildOptions>,
| 'root'
| 'rollupInputOptions'
| 'rollupOutputOptions'
| 'rollupPluginVueOptions'
>
export async function build(buildOptions: BuildOptions = {}) {
export async function build(root: string, buildOptions: BuildOptions = {}) {
const start = Date.now()
process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(buildOptions.root)
const siteConfig = await resolveConfig(root)
try {
const [clientResult, , pageToHashMap] = await bundle(
@ -30,12 +22,11 @@ export async function build(buildOptions: BuildOptions = {}) {
spinner.start('rendering pages...')
try {
const appChunk = clientResult.assets.find(
(chunk) =>
chunk.type === 'chunk' && chunk.fileName.match(/^app\.\w+\.js$/)
const appChunk = clientResult.output.find(
(chunk) => chunk.type === 'chunk' && chunk.isEntry && chunk
) as OutputChunk
const cssChunk = clientResult.assets.find(
const cssChunk = clientResult.output.find(
(chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as OutputAsset
@ -43,7 +34,7 @@ export async function build(buildOptions: BuildOptions = {}) {
// alter the main chunk's hash on every build. It's also embedded as a
// string and JSON.parsed from the client because it's faster than embedding
// as JS object literal.
const hashMapStirng = JSON.stringify(JSON.stringify(pageToHashMap))
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
for (const page of siteConfig.pages) {
await renderPage(
@ -53,7 +44,7 @@ export async function build(buildOptions: BuildOptions = {}) {
appChunk,
cssChunk,
pageToHashMap,
hashMapStirng
hashMapString
)
}
} catch (e) {

@ -1,116 +1,23 @@
import ora from 'ora'
import path from 'path'
import slash from 'slash'
import fs from 'fs-extra'
import { APP_PATH, createResolver, SITE_DATA_REQUEST_PATH } from '../resolver'
import { BuildOptions } from './build'
import { resolveUserConfig, SiteConfig } from '../config'
import { Plugin, OutputAsset, OutputChunk } from 'rollup'
import { createMarkdownToVueRenderFn } from '../markdownToVue'
import {
build,
ssrBuild,
BuildConfig as ViteBuildOptions,
BuildResult
} from 'vite'
import ora from 'ora'
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'
export const okMark = '\x1b[32m✓\x1b[0m'
export const failMark = '\x1b[31m✖\x1b[0m'
const hashRE = /\.(\w+)\.js$/
const staticInjectMarkerRE = /\b(const _hoisted_\d+ = \/\*#__PURE__\*\/createStaticVNode)\("(.*)", (\d+)\)/g
const staticStripRE = /__VP_STATIC_START__.*?__VP_STATIC_END__/g
const staticRestoreRE = /__VP_STATIC_(START|END)__/g
const isPageChunk = (
chunk: OutputAsset | OutputChunk
): chunk is OutputChunk & { facadeModuleId: string } =>
!!(
chunk.type === 'chunk' &&
chunk.isEntry &&
chunk.facadeModuleId &&
chunk.facadeModuleId.endsWith('.md')
)
// bundles the VitePress app for both client AND server.
export async function bundle(
config: SiteConfig,
options: BuildOptions
): Promise<[BuildResult, BuildResult, Record<string, string>]> {
): Promise<[RollupOutput, RollupOutput, Record<string, string>]> {
const root = config.root
const userConfig = await resolveUserConfig(root)
const resolver = createResolver(config.themeDir, userConfig)
const markdownToVue = createMarkdownToVueRenderFn(root, userConfig.markdown)
let isClientBuild = true
const pageToHashMap = Object.create(null)
const VitePressPlugin: Plugin = {
name: 'vitepress',
resolveId(id) {
if (id === SITE_DATA_REQUEST_PATH) {
return id
}
},
async load(id) {
if (id === SITE_DATA_REQUEST_PATH) {
return `export default ${JSON.stringify(JSON.stringify(config.site))}`
}
// compile md into vue src
if (id.endsWith('.md')) {
const content = await fs.readFile(id, 'utf-8')
// TODO use git timestamp
const lastUpdated = (await fs.stat(id)).mtimeMs
const { vueSrc } = markdownToVue(content, id, lastUpdated)
return vueSrc
}
},
renderChunk(code, chunk) {
if (isClientBuild && isPageChunk(chunk as OutputChunk)) {
// For each page chunk, inject marker for start/end of static strings.
// we do this here because in generateBundle the chunks would have been
// minified and we won't be able to safely locate the strings.
// Using a regexp relies on specific output from Vue compiler core,
// which is a reasonable trade-off considering the massive perf win over
// a full AST parse.
code = code.replace(
staticInjectMarkerRE,
'$1("__VP_STATIC_START__$2__VP_STATIC_END__", $3)'
)
return code
}
return null
},
generateBundle(_options, bundle) {
if (!isClientBuild) {
return
}
// for each .md entry chunk, adjust its name to its correct path.
for (const name in bundle) {
const chunk = bundle[name]
if (isPageChunk(chunk)) {
// record page -> hash relations
const hash = chunk.fileName.match(hashRE)![1]
const pageName = chunk.fileName.replace(hashRE, '')
pageToHashMap[pageName] = hash
// inject another chunk with the content stripped
bundle[name + '-lean'] = {
...chunk,
fileName: chunk.fileName.replace(/\.js$/, '.lean.js'),
code: chunk.code.replace(staticStripRE, ``)
}
// remove static markers from orginal code
chunk.code = chunk.code.replace(staticRestoreRE, '')
}
}
}
}
// define custom rollup input
// this is a multi-entry build - every page is considered an entry chunk
// the loading is done via filename conversion rules so that the
@ -125,49 +32,56 @@ export async function bundle(
})
// resolve options to pass to vite
const { rollupInputOptions = {}, rollupOutputOptions = {} } = options
const viteOptions: Partial<ViteBuildOptions> = {
const { rollupOptions } = options
const resolveViteConfig = (ssr: boolean): ViteUserConfig => ({
root,
logLevel: 'warn',
plugins: createVitePressPlugin(root, config, ssr, pageToHashMap),
ssr: {
noExternal: ['vitepress']
},
build: {
...options,
emptyOutDir: true,
ssr,
base: config.site.base,
resolvers: [resolver],
outDir: config.outDir,
// let rollup-plugin-vue compile .md files as well
rollupPluginVueOptions: {
include: /\.(vue|md)$/
},
rollupInputOptions: {
...rollupInputOptions,
outDir: ssr ? config.tempDir : config.outDir,
cssCodeSplit: false,
rollupOptions: {
...rollupOptions,
input,
// important so that each page chunk and the index export things for each
// other
preserveEntrySignatures: 'allow-extension',
plugins: [VitePressPlugin, ...(rollupInputOptions.plugins || [])]
},
rollupOutputOptions: {
...rollupOutputOptions,
chunkFileNames: (chunk) => {
if (/runtime-dom/.test(chunk.name)) {
return `framework.[hash].js`
output: {
...rollupOptions?.output,
...(ssr
? {}
: {
chunkFileNames(chunk): string {
if (!chunk.isEntry && /runtime/.test(chunk.name)) {
return `assets/framework.[hash].js`
}
return `[name].[hash].js`
return `assets/[name].[hash].js`
}
})
}
},
silent: !process.env.DEBUG,
minify: !process.env.DEBUG
minify: ssr ? false : !process.env.DEBUG
}
})
let clientResult, serverResult
let clientResult: RollupOutput
let serverResult: RollupOutput
const spinner = ora()
spinner.start('building client + server bundles...')
try {
;[clientResult, serverResult] = await Promise.all([
build(viteOptions),
ssrBuild({
...viteOptions,
outDir: config.tempDir
})
])
;[clientResult, serverResult] = await (Promise.all([
build(resolveViteConfig(false)),
build(resolveViteConfig(true))
]) as Promise<[RollupOutput, RollupOutput]>)
} catch (e) {
spinner.stopAndPersist({
symbol: failMark
@ -178,5 +92,5 @@ export async function bundle(
symbol: okMark
})
return [clientResult[0], serverResult[0], pageToHashMap]
return [clientResult, serverResult, pageToHashMap]
}

@ -2,21 +2,21 @@ import path from 'path'
import fs from 'fs-extra'
import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { HeadConfig } from '../../../types/shared'
import { BuildResult } from 'vite'
import { OutputChunk, OutputAsset } from 'rollup'
import { normalizePath } from 'vite'
import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
const escape = require('escape-html')
export async function renderPage(
config: SiteConfig,
page: string, // foo.md
result: BuildResult,
result: RollupOutput,
appChunk: OutputChunk,
cssChunk: OutputAsset,
pageToHashMap: Record<string, string>,
hashMapStirng: string
hashMapString: string
) {
const { createApp } = require(path.join(config.tempDir, `_assets/app.js`))
const { createApp } = require(path.join(config.tempDir, `app.js`))
const { app, router } = createApp()
const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath)
@ -29,18 +29,16 @@ export async function renderPage(
const pageServerJsFileName = pageName + '.js'
// for any initial page load, we only need the lean version of the page js
// since the static content is already on the page!
const pageHash = pageToHashMap[pageName]
const pageClientJsFileName = pageName + `.` + pageHash + '.lean.js'
const pageHash = pageToHashMap[pageName.toLowerCase()]
const pageClientJsFileName = `assets/${pageName}.${pageHash}.lean.js`
// resolve page data so we can render head tags
const { __pageData } = require(path.join(
config.tempDir,
`_assets`,
pageServerJsFileName
))
const pageData = JSON.parse(__pageData)
const assetPath = `${siteData.base}_assets/`
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
@ -50,7 +48,7 @@ export async function renderPage(
appChunk.fileName
]
.map((file) => {
return `<link rel="modulepreload" href="${assetPath}${file}">`
return `<link rel="modulepreload" href="${siteData.base}${file}">`
})
.join('\n ')
@ -64,15 +62,17 @@ export async function renderPage(
${pageData.title ? pageData.title + ` | ` : ``}${siteData.title}
</title>
<meta name="description" content="${siteData.description}">
<link rel="stylesheet" href="${assetPath}${cssChunk.fileName}">
<link rel="stylesheet" href="${siteData.base}${cssChunk.fileName}">
${preloadLinks}
${renderHead(siteData.head)}
${renderHead(pageData.frontmatter.head)}
</head>
<body>
<div id="app">${content}</div>
<script>__VP_HASH_MAP__ = JSON.parse(${hashMapStirng})</script>
<script type="module" async src="${assetPath}${appChunk.fileName}"></script>
<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>
<script type="module" async src="${siteData.base}${
appChunk.fileName
}"></script>
</body>
</html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
@ -83,17 +83,23 @@ export async function renderPage(
function resolvePageImports(
config: SiteConfig,
page: string,
result: BuildResult,
result: RollupOutput,
indexChunk: OutputChunk
) {
// find the page's js chunk and inject script tags for its imports so that
// they are start fetching as early as possible
const srcPath = path.resolve(config.root, page)
const pageChunk = result.assets.find(
const srcPath = normalizePath(path.resolve(config.root, page))
const pageChunk = result.output.find(
(chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
) as OutputChunk
return Array.from(new Set([...indexChunk.imports, ...pageChunk.imports]))
return Array.from(
new Set([
...indexChunk.imports,
...indexChunk.dynamicImports,
...pageChunk.imports,
...pageChunk.dynamicImports
])
)
}
function renderHead(head: HeadConfig[]) {

@ -0,0 +1,36 @@
import chalk from 'chalk'
import minimist from 'minimist'
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]
if (root) {
argv.root = root
}
if (!command || command === 'dev') {
createServer(root, argv)
.then((server) => server.listen())
.catch((err) => {
console.error(chalk.red(`failed to start server. error:\n`), err)
process.exit(1)
})
} else if (command === 'build') {
build(root, argv).catch((err) => {
console.error(chalk.red(`build error:\n`), err)
process.exit(1)
})
} else if (command === 'serve') {
serve(argv).catch((err) => {
console.error(chalk.red(`failed to start server. error:\n`), err)
process.exit(1)
})
} else {
console.log(chalk.red(`unknown command "${command}".`))
process.exit(1)
}

@ -2,10 +2,10 @@ import path from 'path'
import fs from 'fs-extra'
import chalk from 'chalk'
import globby from 'globby'
import { createResolver, APP_PATH, DEFAULT_THEME_PATH } from './resolver'
import { Resolver } from 'vite'
import { resolveAliases, APP_PATH, DEFAULT_THEME_PATH } from './alias'
import { SiteData, HeadConfig, LocaleConfig } from '../../types/shared'
import { MarkdownOptions } from './markdown/markdown'
import { AliasOptions } from 'vite'
export { resolveSiteDataByRoute } from './shared/config'
const debug = require('debug')('vitepress:config')
@ -20,7 +20,7 @@ export interface UserConfig<ThemeConfig = any> {
locales?: Record<string, LocaleConfig>
alias?: Record<string, string>
markdown?: MarkdownOptions
// TODO locales support etc.
customData?: any
}
export interface SiteConfig<ThemeConfig = any> {
@ -30,7 +30,7 @@ export interface SiteConfig<ThemeConfig = any> {
themeDir: string
outDir: string
tempDir: string
resolver: Resolver
alias: AliasOptions
pages: string[]
markdown?: MarkdownOptions
}
@ -58,8 +58,8 @@ export async function resolveConfig(
configPath: resolve(root, 'config.js'),
outDir: resolve(root, 'dist'),
tempDir: path.resolve(APP_PATH, 'temp'),
resolver: createResolver(themeDir, userConfig),
markdown: userConfig.markdown
markdown: userConfig.markdown,
alias: resolveAliases(themeDir, userConfig)
}
return config
@ -91,6 +91,7 @@ export async function resolveSiteData(root: string): Promise<SiteData> {
base: userConfig.base ? userConfig.base.replace(/([^/])$/, '$1/') : '/',
head: userConfig.head || [],
themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}
locales: userConfig.locales || {},
customData: userConfig.customData || {}
}
}

@ -37,7 +37,7 @@ export const highlight = (str: string, lang: string) => {
} catch (e) {
console.warn(
chalk.yellow(
`[vuepress] Syntax highlight for language "${lang}" is not supported.`
`[vitepress] Syntax highlight for language "${lang}" is not supported.`
)
)
}

@ -23,7 +23,12 @@ export const linkPlugin = (
Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val)
})
} else if (!url.startsWith('#')) {
} else if (
// internal anchor links
!url.startsWith('#') &&
// mail links
!url.startsWith('mailto:')
) {
normalizeHref(hrefAttr)
}
}

@ -12,7 +12,7 @@ export const slugify = (str: string): string => {
.replace(rControl, '')
// Replace special characters
.replace(rSpecial, '-')
// Remove continous separators
// Remove continuos separators
.replace(/\-{2,}/g, '-')
// Remove prefixing and trailing separtors
.replace(/^\-+|\-+$/g, '')

@ -1,9 +1,11 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import LRUCache from 'lru-cache'
import { createMarkdownRenderer, MarkdownOptions } from './markdown/markdown'
import { deeplyParseHeader } from './utils/parseHeader'
import { PageData, HeadConfig } from '../../types/shared'
import slash from 'slash'
const debug = require('debug')('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
@ -19,45 +21,46 @@ export function createMarkdownToVueRenderFn(
) {
const md = createMarkdownRenderer(options)
return (
src: string,
file: string,
lastUpdated: number,
injectData = true
) => {
file = path.relative(root, file)
return (src: string, file: string): MarkdownCompileResult => {
const relativePath = slash(path.relative(root, file))
const cached = cache.get(src)
if (cached) {
debug(`[cache hit] ${file}`)
debug(`[cache hit] ${relativePath}`)
return cached
}
const start = Date.now()
const { content, data: frontmatter } = matter(src)
const { html, data } = md.render(content)
let { html, data } = md.render(content)
// TODO validate data.links?
// avoid env variables being replaced by vite
html = html
.replace(/import\.meta/g, 'import.<wbr/>meta')
.replace(/process\.env/g, 'process.<wbr/>env')
// inject page data
// TODO validate data.links?
const pageData: PageData = {
title: inferTitle(frontmatter, content),
description: inferDescription(frontmatter),
frontmatter,
headers: data.headers,
relativePath: file.replace(/\\/g, '/'),
lastUpdated
relativePath,
// TODO use git timestamp?
lastUpdated: Math.round(fs.statSync(file).mtimeMs)
}
const additionalBlocks = injectData
? injectPageData(data.hoistedTags || [], pageData)
: data.hoistedTags || []
const vueSrc =
additionalBlocks.join('\n') + `\n<template><div>${html}</div></template>`
genPageDataCode(data.hoistedTags || [], pageData).join('\n') +
`\n<template><div>${html}</div></template>`
debug(`[render] ${file} in ${Date.now() - start}ms.`)
const result = { vueSrc, pageData }
const result = {
vueSrc,
pageData
}
cache.set(src, result)
return result
}
@ -68,7 +71,7 @@ const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/
function injectPageData(tags: string[], data: PageData) {
function genPageDataCode(tags: string[], data: PageData) {
const code = `\nexport const __pageData = ${JSON.stringify(
JSON.stringify(data)
)}`

@ -0,0 +1,177 @@
import path from 'path'
import { Plugin } from 'vite'
import { SiteConfig, resolveSiteData } from './config'
import { createMarkdownToVueRenderFn } from './markdownToVue'
import { APP_PATH, SITE_DATA_REQUEST_PATH } from './alias'
import createVuePlugin from '@vitejs/plugin-vue'
import slash from 'slash'
import { OutputAsset, OutputChunk } from 'rollup'
const hashRE = /\.(\w+)\.js$/
const staticInjectMarkerRE = /\b(const _hoisted_\d+ = \/\*#__PURE__\*\/createStaticVNode)\("(.*)", (\d+)\)/g
const staticStripRE = /__VP_STATIC_START__.*?__VP_STATIC_END__/g
const staticRestoreRE = /__VP_STATIC_(START|END)__/g
const isPageChunk = (
chunk: OutputAsset | OutputChunk
): chunk is OutputChunk & { facadeModuleId: string } =>
!!(
chunk.type === 'chunk' &&
chunk.isEntry &&
chunk.facadeModuleId &&
chunk.facadeModuleId.endsWith('.md')
)
export function createVitePressPlugin(
root: string,
{ configPath, alias, markdown, site }: SiteConfig,
ssr = false,
pageToHashMap?: Record<string, string>
): Plugin[] {
const markdownToVue = createMarkdownToVueRenderFn(root, markdown)
const vuePlugin = createVuePlugin({
include: [/\.vue$/, /\.md$/]
})
let siteData = site
const vitePressPlugin: Plugin = {
name: 'vitepress',
config() {
return {
alias,
define: {
__CARBON__: !!site.themeConfig.carbonAds?.carbon,
__BSA__: !!site.themeConfig.carbonAds?.custom,
__ALGOLIA__: !!site.themeConfig.algolia
}
}
},
resolveId(id) {
if (id === SITE_DATA_REQUEST_PATH) {
return SITE_DATA_REQUEST_PATH
}
},
load(id) {
if (id === SITE_DATA_REQUEST_PATH) {
return `export default ${JSON.stringify(JSON.stringify(siteData))}`
}
},
transform(code, id) {
if (id.endsWith('.md')) {
// transform .md files into vueSrc so plugin-vue can handle it
return markdownToVue(code, id).vueSrc
}
},
configureServer(server) {
// serve our index.html after vite history fallback
return () => {
server.middlewares.use((req, res, next) => {
if (req.url!.endsWith('.html')) {
res.statusCode = 200
res.end(
`<div id="app"></div>\n` +
`<script type="module" src="/@fs/${APP_PATH}/index.js"></script>`
)
return
}
next()
})
}
},
renderChunk(code, chunk) {
if (!ssr && isPageChunk(chunk as OutputChunk)) {
// For each page chunk, inject marker for start/end of static strings.
// we do this here because in generateBundle the chunks would have been
// minified and we won't be able to safely locate the strings.
// Using a regexp relies on specific output from Vue compiler core,
// which is a reasonable trade-off considering the massive perf win over
// a full AST parse.
code = code.replace(
staticInjectMarkerRE,
'$1("__VP_STATIC_START__$2__VP_STATIC_END__", $3)'
)
return code
}
return null
},
generateBundle(_options, bundle) {
if (ssr) {
// ssr build:
// delete all asset chunks
for (const name in bundle) {
if (bundle[name].type === 'asset') {
delete bundle[name]
}
}
} else {
// client build:
// for each .md entry chunk, adjust its name to its correct path.
for (const name in bundle) {
const chunk = bundle[name]
if (isPageChunk(chunk)) {
// record page -> hash relations
const hash = chunk.fileName.match(hashRE)![1]
pageToHashMap![chunk.name.toLowerCase()] = hash
// inject another chunk with the content stripped
bundle[name + '-lean'] = {
...chunk,
fileName: chunk.fileName.replace(/\.js$/, '.lean.js'),
code: chunk.code.replace(staticStripRE, ``)
}
// remove static markers from original code
chunk.code = chunk.code.replace(staticRestoreRE, '')
}
}
}
},
async handleHotUpdate(ctx) {
// handle config hmr
const { file, read, server } = ctx
if (file === configPath) {
const newData = await resolveSiteData(root)
if (newData.base !== siteData.base) {
console.warn(
`[vitepress]: config.base has changed. Please restart the dev server.`
)
}
siteData = newData
return [server.moduleGraph.getModuleById(SITE_DATA_REQUEST_PATH)!]
}
// hot reload .md files as .vue files
if (file.endsWith('.md')) {
const content = await read()
const { pageData, vueSrc } = markdownToVue(content, file)
// notify the client to update page data
server.ws.send({
type: 'custom',
event: 'vitepress:pageData',
data: {
path: `/${slash(path.relative(root, file))}`,
pageData
}
})
// reload the content component
return vuePlugin.handleHotUpdate!({
...ctx,
read: () => vueSrc
})
}
}
}
return [vitePressPlugin, vuePlugin]
}

@ -1,48 +0,0 @@
import path from 'path'
import { Resolver } from 'vite'
import { UserConfig } from './config'
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'
)
// special virtual file
// we can't directly import '/@siteData' becase
// - it's not an actual file so we can't use tsconfig paths to redirect it
// - TS doesn't allow shimming a module that starts with '/'
export const SITE_DATA_ID = '@siteData'
export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
// this is a path resolver that is passed to vite
// so that we can resolve custom requests that start with /@app or /@theme
// we also need to map file paths back to their public served paths so that
// vite HMR can send the correct update notifications to the client.
export function createResolver(
themeDir: string,
userConfig: UserConfig
): Resolver {
return {
alias: {
...userConfig.alias,
'/@app/': APP_PATH,
'/@theme/': themeDir,
'/@default-theme/': DEFAULT_THEME_PATH,
'/@shared/': SHARED_PATH,
vitepress: '/@app/exports.js',
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
},
requestToFile(publicPath) {
if (publicPath === SITE_DATA_REQUEST_PATH) {
return SITE_DATA_REQUEST_PATH
}
},
fileToRequest(filePath) {
if (filePath === SITE_DATA_REQUEST_PATH) {
return SITE_DATA_REQUEST_PATH
}
}
}
}

@ -1,5 +1,5 @@
import Koa from 'koa'
import koaServe from 'koa-static'
import sirv from 'sirv'
import compression from 'compression'
import { resolveConfig } from '../config'
export interface ServeOptions {
@ -8,14 +8,28 @@ export interface ServeOptions {
}
export async function serve(options: ServeOptions = {}) {
const port = options.port !== undefined ? options.port : 3000
const port = options.port !== undefined ? options.port : 5000
const site = await resolveConfig(options.root)
const app = new Koa()
app.use(koaServe(site.outDir))
app.listen(port)
const compress = compression()
const serve = sirv(site.outDir, {
etag: true,
single: true,
maxAge: 31536000,
immutable: true,
setHeaders(res, pathname) {
if (!pathname.includes('/assets/')) {
// force server validation for non-asset files since they are not
// fingerprinted.
res.setHeader('cache-control', 'no-cache')
}
}
})
console.log(`listening at http://localhost:${port}`)
require('polka')()
.use(compress, serve)
.listen(port, (err: any) => {
if (err) throw err
console.log(`Built site served at http://localhost:${port}.\n`)
})
}

@ -1,139 +1,17 @@
import path from 'path'
import {
createServer as createViteServer,
cachedRead,
ServerConfig,
ServerPlugin
} from 'vite'
import { resolveConfig, SiteConfig, resolveSiteData } from './config'
import { createMarkdownToVueRenderFn } from './markdownToVue'
import { APP_PATH, SITE_DATA_REQUEST_PATH } from './resolver'
import { existsSync } from 'fs'
import { createServer as createViteServer, ServerOptions } from 'vite'
import { resolveConfig } from './config'
import { createVitePressPlugin } from './plugin'
const debug = require('debug')('vitepress:serve')
const debugHmr = require('debug')('vitepress:hmr')
function createVitePressPlugin({
configPath,
markdown,
site: initialSiteData
}: SiteConfig): ServerPlugin {
return ({ app, root, watcher, resolver }) => {
const markdownToVue = createMarkdownToVueRenderFn(root, markdown)
// hot reload .md files as .vue files
watcher.on('change', async (file) => {
if (file.endsWith('.md')) {
debugHmr(`reloading ${file}`)
const content = await cachedRead(null, file)
const timestamp = Date.now()
const { pageData, vueSrc } = markdownToVue(
content.toString(),
file,
timestamp,
// do not inject pageData on HMR
// it leads to vite to think <script> has changed and reloads the
// component instead of re-rendering.
// pageData needs separate HMR logic anyway (see below)
false
)
// notify the client to update page data
watcher.send({
type: 'custom',
id: 'vitepress:pageData',
customData: {
path: resolver.fileToRequest(file),
pageData
}
})
// reload the content component
watcher.handleVueReload(file, timestamp, vueSrc)
}
})
// hot reload handling for siteData
// the data is stringified twice so it is sent to the client as a string
// it is then parsed on the client via JSON.parse() which is faster than
// parsing the object literal as JavaScript.
let siteData = initialSiteData
let stringifiedData = JSON.stringify(JSON.stringify(initialSiteData))
watcher.add(configPath)
watcher.on('change', async (file) => {
if (file === configPath) {
const newData = await resolveSiteData(root)
stringifiedData = JSON.stringify(JSON.stringify(newData))
if (newData.base !== siteData.base) {
console.warn(
`[vitepress]: config.base has changed. Please restart the dev server.`
)
}
siteData = newData
watcher.handleJSReload(SITE_DATA_REQUEST_PATH)
}
})
// inject Koa middleware
app.use(async (ctx, next) => {
// serve siteData (which is a virtual file)
if (ctx.path === SITE_DATA_REQUEST_PATH) {
ctx.type = 'js'
ctx.body = `export default ${stringifiedData}`
debug(ctx.url)
return
}
// handle .md -> vue transforms
if (ctx.path.endsWith('.md')) {
const file = resolver.requestToFile(ctx.path)
if (!existsSync(file)) {
return next()
}
await cachedRead(ctx, file)
// let vite know this is supposed to be treated as vue file
ctx.vue = true
const { vueSrc, pageData } = markdownToVue(
ctx.body,
file,
ctx.lastModified.getTime(),
false
)
ctx.body = vueSrc
debug(ctx.url, ctx.status)
await next()
// make sure this is the main <script> block
if (!ctx.query.type) {
// inject pageData to generated script
ctx.body += `\nexport const __pageData = ${JSON.stringify(
JSON.stringify(pageData)
)}`
}
return
}
await next()
// serve our index.html after vite history fallback
if (ctx.url.endsWith('.html')) {
await cachedRead(ctx, path.join(APP_PATH, 'index.html'))
ctx.status = 200
}
})
}
}
export async function createServer(options: ServerConfig = {}) {
const config = await resolveConfig(options.root)
export async function createServer(
root: string = process.cwd(),
serverOptions: ServerOptions = {}
) {
const config = await resolveConfig(root)
return createViteServer({
...options,
configureServer: createVitePressPlugin(config),
resolvers: [config.resolver]
root,
// logLevel: 'warn',
plugins: createVitePressPlugin(root, config),
server: serverOptions
})
}

@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "es2019",
"baseUrl": ".",
"outDir": "../../dist/node",
"module": "commonjs",

@ -5,7 +5,7 @@
//
// But header's parsing in the markdown content is done by the markdown
// loader based on markdown-it. markdown-it parser will will always keep
// HTML in headers, so in VuePress, after being parsed by the markdiwn
// HTML in headers, so in VuePress, after being parsed by the markdown
// 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.
@ -28,9 +28,9 @@ const unescapeHtml = (html: string) =>
const removeMarkdownTokens = (str: string) =>
String(str)
.replace(/\[(.*)\]\(.*\)/, '$1') // []()
.replace(/(\[(.[^\]]+)\]\((.[^)]+)\))/g, '$2') // []()
.replace(/(`|\*{1,3}|_)(.*?[^\\])\1/g, '$2') // `{t}` | *{t}* | **{t}** | ***{t}*** | _{t}_
.replace(/(\\)(\*|_|`|\!)/g, '$2') // remove escape char '\'
.replace(/(\\)(\*|_|`|\!|<|\$)/g, '$2') // remove escape char '\'
const trim = (str: string) => str.trim()
@ -39,7 +39,7 @@ const trim = (str: string) => str.trim()
// Input: "<a> b", Output: "b"
// Input: "`<a>` b", Output: "`<a>` b"
export const removeNonCodeWrappedHTML = (str: string) => {
return String(str).replace(/(^|[^><`])<.*>([^><`]|$)/g, '$1$2')
return String(str).replace(/(^|[^><`\\])<.*>([^><`]|$)/g, '$1$2')
}
const compose = (...processors: ((str: string) => string)[]) => {

3
theme.d.ts vendored

@ -0,0 +1,3 @@
// so that users can do `import DefaultTheme from 'vitepress/theme'`
import DefaultTheme from './dist/client/theme-default/index'
export default DefaultTheme

@ -2,7 +2,6 @@
"compilerOptions": {
"baseUrl": ".",
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",
"strict": true,
"declaration": true,
@ -18,7 +17,7 @@
"tests/*": ["__tests__/*"],
"/@shared/*": ["src/client/shared/*"],
"/@types/*": ["types/*"],
"vitepress": ["src/client/app/exports.ts"]
"vitepress": ["src/client/index.ts"]
}
},
"include": [

3
types/index.d.ts vendored

@ -1,5 +1,4 @@
export * from './shared'
export * from '../dist/node/index'
export * from '../dist/client/app/exports'
export * from '../dist/client/index'
export * from '../dist/client/theme-default/config'
export { default as defaultTheme } from '../dist/client/theme-default/index'

1
types/shared.d.ts vendored

@ -17,6 +17,7 @@ export interface SiteData<ThemeConfig = any> {
head: HeadConfig[]
themeConfig: ThemeConfig
locales: Record<string, LocaleConfig>
customData: any
}
export type HeadConfig =

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