Merge branch 'main' into fix/multiple-algolia

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

@ -1,32 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'bug: pending triage'
assignees: ''
---
<!--
NOTE:
VitePress is still WIP, and it is not compatible with VuePress.
Please do not open issue about default theme missing features or something doesn't work like VuePress.
-->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**System Info**
- vitepress version:
- vite version:
- Node version:
- OS version:
**Additional context**
Add any other context about the problem here.

@ -0,0 +1,60 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve
labels: ["bug: pending triage"]
body:
- type: markdown
attributes:
value: |
"Thanks for taking the time to fill out this bug report!
VitePress is still WIP, and it is not compatible with VuePress.
Please do not open issue about default theme missing features or something doesn't work like VuePress."
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior
placeholder: Reproduction
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
placeholder: Expected behavior
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --npmPackages vitepress --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Follow our [Code of Conduct](https://vuejs.org/coc)
required: true
- label: Read the [docs](https://vitepress.vuejs.org/).
required: true
- label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
required: true

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

@ -0,0 +1,45 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
body:
- type: markdown
attributes:
value: |
Thanks for your interest in the project and taking the time to fill out this feature report!
- type: textarea
id: feature-description
attributes:
label: Is your feature request related to a problem? Please describe.
description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Follow our [Code of Conduct](https://vuejs.org/coc)
required: true
- label: Read the [docs](https://vitepress.vuejs.org/).
required: true
- label: Read the [Contributing Guidelines](https://github.com/vuejs/vitepress/blob/master/.github/contributing.md).
required: true
- label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
required: true

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

4
.gitignore vendored

@ -1,6 +1,6 @@
/coverage
/src/client/shared
/src/node/shared
/src/client/shared.ts
/src/node/shared.ts
*.log
.DS_Store
.vite_opt_cache

@ -1,3 +1,217 @@
## [0.19.2](https://github.com/vuejs/vitepress/compare/v0.19.1...v0.19.2) (2021-09-28)
### Bug Fixes
- encode urls that conflict w/ vite built-in replacements ([3940625](https://github.com/vuejs/vitepress/commit/3940625121455b7ad6e5ea8ebb3e1cf2faf9c7fc))
## [0.19.1](https://github.com/vuejs/vitepress/compare/v0.19.0...v0.19.1) (2021-09-21)
- Fix build
# [0.19.0](https://github.com/vuejs/vitepress/compare/v0.18.1...v0.19.0) (2021-09-21)
### Features
- upgrade vue, simplify deps ([9030486](https://github.com/vuejs/vitepress/commit/9030486409f10a59115d874b9365f71348ed76c2))
- use `markdown-it-attrs` for markdown-it plugins ([#393](https://github.com/vuejs/vitepress/issues/393)) ([610e9b7](https://github.com/vuejs/vitepress/commit/610e9b7111462d3aace878017fa4d359cd2ae7ea))
## [0.18.1](https://github.com/vuejs/vitepress/compare/v0.18.0...v0.18.1) (2021-09-16)
### Bug Fixes
- ensure stable pages entry order across builds ([929bcf5](https://github.com/vuejs/vitepress/commit/929bcf50ee634d9fe73adbe2aae5f7038b048e5a))
# [0.18.0](https://github.com/vuejs/vitepress/compare/v0.17.3...v0.18.0) (2021-09-14)
### Features
- map mode + remove deprecated options ([b94b163](https://github.com/vuejs/vitepress/commit/b94b163a3a931fe03e69547391d6ac22eb41b789))
- support `<script client>` in mpa mode ([e0b6997](https://github.com/vuejs/vitepress/commit/e0b69973f840bfa281fae209da1f1c674c1301a8))
## [0.17.3](https://github.com/vuejs/vitepress/compare/v0.17.2...v0.17.3) (2021-09-09)
### Bug Fixes
- emit prevented hashchange event ([4fb387d](https://github.com/vuejs/vitepress/commit/4fb387d94ea9d7ae28a871929cbbc57e931b8d7a))
## [0.17.2](https://github.com/vuejs/vitepress/compare/v0.17.1...v0.17.2) (2021-09-08)
### Bug Fixes
- improve fs allow ([2e9264f](https://github.com/vuejs/vitepress/commit/2e9264f03259354e7739e2a56a7c1306fb167843))
### Features
- support config.extends ([f749b27](https://github.com/vuejs/vitepress/commit/f749b272d4603a3b8eaf251b0feebe2d33da3983))
## [0.17.1](https://github.com/vuejs/vitepress/compare/v0.17.0...v0.17.1) (2021-09-08)
### Bug Fixes
- avoid using spread for client code ([03abee7](https://github.com/vuejs/vitepress/commit/03abee7f7c0fac95806f31ff5761b9e912a1f232))
- **default-theme:** use description as tagline by default ([b94c827](https://github.com/vuejs/vitepress/commit/b94c82710a7b230a918790ac0b6aa1d2f5afc1c3))
- handle case when there is no themeConfig ([034c737](https://github.com/vuejs/vitepress/commit/034c7375ad2de4b42c0ac861c2dd18183511771d))
### Performance Improvements
- minor optimizations ([96bcdda](https://github.com/vuejs/vitepress/commit/96bcddabedac9af4e1c817ed651bb4ce692c75e7))
# [0.17.0](https://github.com/vuejs/vitepress/compare/v0.16.1...v0.17.0) (2021-08-31)
### Bug Fixes
- allow vite server access to theme and local files ([9b9fdc7](https://github.com/vuejs/vitepress/commit/9b9fdc710a6cedb3e278805eb07bed669ca2075e))
- **code:** code block highlight bug in ul ([#352](https://github.com/vuejs/vitepress/issues/352)) ([9245226](https://github.com/vuejs/vitepress/commit/9245226b16f6113c722e5e8c7b876bea1cf1c255))
- **css:** remove 720px breakpoint in home layout ([#347](https://github.com/vuejs/vitepress/issues/347)) ([0c1a1f2](https://github.com/vuejs/vitepress/commit/0c1a1f2ef43cd7d995f3e9d43f19be8b3f961cb1))
- **i18n:** fix locales reading, add site.langs ([#353](https://github.com/vuejs/vitepress/issues/353)) ([bc78adb](https://github.com/vuejs/vitepress/commit/bc78adb468bce8ce2d4e2543423adacc9351cf51)), closes [/vuepress.vuejs.org/guide/i18n.html#site-level-i18](https://github.com//vuepress.vuejs.org/guide/i18n.html/issues/site-level-i18) [/v2.vuepress.vuejs.org/guide/i18n.html#site-i18](https://github.com//v2.vuepress.vuejs.org/guide/i18n.html/issues/site-i18)
- include emoji text in nav link to match toc ([#284](https://github.com/vuejs/vitepress/issues/284)) ([80ff360](https://github.com/vuejs/vitepress/commit/80ff36066ef6a4ed4a18548993bc5d8d9a6dab58))
- use useData() instead of $site ([#365](https://github.com/vuejs/vitepress/issues/365)) ([1e64773](https://github.com/vuejs/vitepress/commit/1e6477393308a5d8bd03a614cecf9573466f6e6c))
### Features
- support function config ([e74c5f0](https://github.com/vuejs/vitepress/commit/e74c5f06d1d5890fad6dd728df9bf85dcfda87d1))
- support partial include directive ([7b3a9e5](https://github.com/vuejs/vitepress/commit/7b3a9e59b44e9e354692eed6c1ca453be9cb7a86))
- upgrade markdown-it-anchor ([#350](https://github.com/vuejs/vitepress/issues/350)) ([26b5aa9](https://github.com/vuejs/vitepress/commit/26b5aa931f1935bd67dcd1d511461ff5fa8a00ec))
### BREAKING CHANGES
- the `markdown.anchor` option is updated. Refer to
valeriangalliat/markdown-it-anchor#permalinks for
instructions to upgrade your existing `markdown.anchor.permalink`
option. **This doesn't affect you if you weren't changing the header
permalinks behavior**.
## [0.16.1](https://github.com/vuejs/vitepress/compare/v0.16.0...v0.16.1) (2021-08-11)
### Features
- info custom container ([4925fb5](https://github.com/vuejs/vitepress/commit/4925fb5c29c59b7e17d050ab4346f71afc0463cd))
# [0.16.0](https://github.com/vuejs/vitepress/compare/v0.15.6...v0.16.0) (2021-08-10)
This version uses Vue 3.2.0.
### Bug Fixes
- override target and rel links attribute in config ([#332](https://github.com/vuejs/vitepress/issues/332)) ([9d98dbb](https://github.com/vuejs/vitepress/commit/9d98dbbe60d477a78d6dc0e80d16fdddedcd4ed5))
- **edit-link:** let frontmatter overwrite global editLink ([#340](https://github.com/vuejs/vitepress/issues/340)) ([cfbba80](https://github.com/vuejs/vitepress/commit/cfbba80a0a6e33bcb2ca3d4450fb9624dcd6d140))
## [0.15.6](https://github.com/vuejs/vitepress/compare/v0.15.5...v0.15.6) (2021-07-02)
### Bug Fixes
- automatically escape vite user defined variables in markdown ([3cec536](https://github.com/vuejs/vitepress/commit/3cec536c1f3d5d027ee16cd0629f84461e565096))
- skip external URLs in `withBase` ([#328](https://github.com/vuejs/vitepress/issues/328)) ([53bb961](https://github.com/vuejs/vitepress/commit/53bb961a925cbafe53730450c5b069e255b54e03))
## [0.15.5](https://github.com/vuejs/vitepress/compare/v0.15.4...v0.15.5) (2021-06-23)
### Bug Fixes
- **nav:** display nav if locales are present ([#321](https://github.com/vuejs/vitepress/issues/321)) ([e76e6ec](https://github.com/vuejs/vitepress/commit/e76e6ecd54f8a202a9d5051afd72553080f898c9))
- **search:** correctly detect multilang ([c046905](https://github.com/vuejs/vitepress/commit/c046905b032a765352ff6bb9944f72db76c5cf45)), closes [#316](https://github.com/vuejs/vitepress/issues/316)
### Performance Improvements
- only update necessary head tags in prod ([e6bb5a4](https://github.com/vuejs/vitepress/commit/e6bb5a4806bb16f1ace26f27f43b5ed83885bf1a))
## [0.15.4](https://github.com/vuejs/vitepress/compare/v0.15.3...v0.15.4) (2021-06-19)
### Bug Fixes
- avoid scroll behavior reliance on .nav-bar class ([9b35dfc](https://github.com/vuejs/vitepress/commit/9b35dfcde4c00e6f10b2631103f95e97cbf4af9e))
## [0.15.3](https://github.com/vuejs/vitepress/compare/v0.15.2...v0.15.3) (2021-06-15)
### Bug Fixes
- avoid error when theme does not have .nav-bar class ([a9d5800](https://github.com/vuejs/vitepress/commit/a9d580069fff90298b197808379cd0c4b756d463))
- avoid resetting head tags on hmr/page switch ([f52f20e](https://github.com/vuejs/vitepress/commit/f52f20e02f6908411ad9cddb341583456a3c2a8c))
- watch config file when using srcDir ([348f19a](https://github.com/vuejs/vitepress/commit/348f19a537930b1d4c7272e05e91edcb72219f34))
## [0.15.2](https://github.com/vuejs/vitepress/compare/v0.15.1...v0.15.2) (2021-06-15)
### Bug Fixes
- force optimize vue to avoid duplication when linked ([eefba39](https://github.com/vuejs/vitepress/commit/eefba398b0e5a4b5afb47ce6e06b0c39a6be55d2))
## [0.15.1](https://github.com/vuejs/vitepress/compare/v0.15.0...v0.15.1) (2021-06-14)
### Features
- support passing vite config in vitepress config file via `vite` option ([3737b10](https://github.com/vuejs/vitepress/commit/3737b1055dc1145dc70b10994564c6d83affd15d))
- support srcDir config option ([aaf4910](https://github.com/vuejs/vitepress/commit/aaf4910d938f4449fdab576ffd0ae853b5aace24))
### Performance Improvements
- avoid double resolve user config on startup ([5733fc6](https://github.com/vuejs/vitepress/commit/5733fc625ea33ab1b07ddfd4f8412e15473d8cca))
### BREAKING CHANGES
- Some config options have changed.
- `vueOptions` renamed to `vue`
- `alias` option has been removed. Use `vite.resovle.alias` instead.
# [0.15.0](https://github.com/vuejs/vitepress/compare/v0.14.1...v0.15.0) (2021-06-14)
### Bug Fixes
- fix frontmatter sidebarDepth for headers ([424a4ca](https://github.com/vuejs/vitepress/commit/424a4ca379f028e3542e2e9598cb5beacaf50067))
- fix vue code block type indication ([76fa173](https://github.com/vuejs/vitepress/commit/76fa1733fff4e3aa4356df08272e4811db996dab))
### Features
- more efficient `useData()` method that exposes all data ([0661063](https://github.com/vuejs/vitepress/commit/0661063d29c0e1dce108cac608be0ff754d2d4c1))
### BREAKING CHANGES
- The following methods are removed.
- `useSiteData`
- `useSiteDataByRoute`
- `usePageData`
- `useFrontmatter`
Instead, use the new `useData()` method:
```js
// before
import { useSiteDataByRoute, usePageData } from 'vitepress'
const site = useSiteDataByRoute()
const page = usePageData()
const theme = computed(() => site.value.themeConfig)
// after
import { useData } from 'vitepress'
const { site, page, theme } = useData()
```
All destructured values are computed refs injected from app root
so they are created only once globally.
- All global mixin properties (e.g. `$site`) except `$frontmatter` are removed. Always use `useData()` to retrieve VitePress data in Vue components.
## [0.14.1](https://github.com/vuejs/vitepress/compare/v0.14.0...v0.14.1) (2021-06-08)
### Bug Fixes
- functional templates with vue v3.1 ([#312](https://github.com/vuejs/vitepress/issues/312)) ([8988aad](https://github.com/vuejs/vitepress/commit/8988aadbcbd781a81df0a8d1a4a6964d324c58a3))
# [0.14.0](https://github.com/vuejs/vitepress/compare/v0.13.2...v0.14.0) (2021-05-27)
### Bug Fixes
- chinese filenames can't build ([#217](https://github.com/vuejs/vitepress/issues/217)) ([#262](https://github.com/vuejs/vitepress/issues/262)) ([b940397](https://github.com/vuejs/vitepress/commit/b940397cd0e5135e7433bac6fc99da8553915053))
- **theme:** set search box min-width for >=751px ([#286](https://github.com/vuejs/vitepress/issues/286)) ([9589a5d](https://github.com/vuejs/vitepress/commit/9589a5d0e6458da07054a84d2df6ef99a5ad1dbd))
- detect public folder for dead link ([#290](https://github.com/vuejs/vitepress/issues/290)) ([3aa185f](https://github.com/vuejs/vitepress/commit/3aa185fa9f9dd49e32cfd60f96a30da8616e419e))
- remove unnecessary 'vite/dynamic-import-polyfill' ([6b4a4aa](https://github.com/vuejs/vitepress/commit/6b4a4aa7a6cd2f2e044a5cc54c5bf72a50f9df67))
### Features
- Vite version bumped to `^2.3.4`
- exclude option ([#281](https://github.com/vuejs/vitepress/issues/281)) ([71a5e1c](https://github.com/vuejs/vitepress/commit/71a5e1c2a2b552ced8a994dc60201c4be89b4ac9))
- Render titles for social sharing and improve home page sharing ([#263](https://github.com/vuejs/vitepress/issues/263)) ([e651f97](https://github.com/vuejs/vitepress/commit/e651f977d6b9f50fef25f6d736f8d4880c997305))
## [0.13.2](https://github.com/vuejs/vitepress/compare/v0.13.1...v0.13.2) (2021-04-26)
### Bug Fixes

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

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

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

@ -61,9 +61,8 @@ function getGuideSidebar() {
text: 'Advanced',
children: [
{ text: 'Frontmatter', link: '/guide/frontmatter' },
{ text: 'Global Computed', link: '/guide/global-computed' },
{ text: 'Global Component', link: '/guide/global-component' },
{ text: 'Customization', link: '/guide/customization' },
{ text: 'Theming', link: '/guide/theming' },
{ text: 'API Reference', link: '/guide/api' },
{
text: 'Differences from Vuepress',
link: '/guide/differences-from-vuepress'

@ -1,5 +1,9 @@
# App Config: Basics
::: tip
The config reference is incomplete since the config format may still receive further changes. For a complete reference of the current available options, refer to [config.ts](https://github.com/vuejs/vitepress/blob/master/src/node/config.ts#L15).
:::
## base
- Type: `string`
@ -33,7 +37,7 @@ module.exports = {
- Type: `string`
- Default: `VitePress`
Title for the site. This will be the prefix for all page titles, and displayed in the navbar.
Title for the site. This will be the suffix for all page titles, and displayed in the navbar.
```js
module.exports = {

@ -0,0 +1,95 @@
# API Reference
## Helper Methods
The following methods are globally importable from `vitepress` and are typically used in custom theme Vue components. However, they are also usable inside `.md` pages because markdown files are compiled into Vue single-file components.
Methods that start with `use*` indicates that it is a [Vue 3 Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html) function that can only be used inside `setup()` or `<script setup>`.
### `useData`
Returns page-specific data. The returned object has the following type:
```ts
interface VitePressData {
site: Ref<SiteData>
page: Ref<PageData>
theme: Ref<any> // themeConfig from .vitepress/config.js
frontmatter: Ref<PageData['frontmatter']>
title: Ref<string>
description: Ref<string>
lang: Ref<string>
localePath: Ref<string>
}
```
**Example:**
```vue
<script setup>
import { useData } from 'vitepress'
const { theme } = useData()
</script>
<template>
<h1>{{ theme.heroText }}</h1>
</template>
```
### `useRoute`
Returns the current route object with the following type:
```ts
interface Route {
path: string
data: PageData
component: Component | null
}
```
### `useRouter`
Returns the VitePress router instance so you can programmatically navigate to another page.
```ts
interface Router {
route: Route
go: (href?: string) => Promise<void>
}
```
### `withBase`
- **Type**: `(path: string) => string`
Appends the configured [`base`](/config/basics.html#base) to a given URL path. Also see [Base URL](/guide/assets.html#base-url).
## Global Components
VitePress comes with few built-in component that can be used globally. You may use these components in your markdown or your custom theme configuration.
### `<Content/>`
The `<Content/>` component displays the rendered markdown contents. Useful [when creating your own theme](https://vitepress.vuejs.org/guide/customization.html).
```vue
<template>
<h1>Custom Layout!</h1>
<Content />
</template>
```
### `<ClientOnly/>`
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.
If you are using or demoing components that are not SSR-friendly (for example, contain custom directives), you can wrap them inside the `ClientOnly` component.
```html
<ClientOnly>
<NonSSRFriendlyComponent />
</ClientOnly>
```

@ -26,10 +26,30 @@ Note that you should reference files placed in `public` using root absolute path
If your site is deployed to a non-root URL, you will need to set the `base` option in `.vitepress/config.js`. For example, if you plan to deploy your site to `https://foo.github.io/bar/`, then `base` should be set to `'/bar/'` (it should always start and end with a slash).
With a base URL, to reference an image in `public`, you'd have to use URLs like `/bar/image.png`. But this is brittle if you ever decide to change the base. To help with that, VitePress provides a built-in helper `$withBase` (injected onto Vue's prototype) that generates the correct path:
All your static asset paths are automatically processed to adjust for different `base` config values. For example, if you have an absolute reference to an asset under `public` in your markdown:
```html
<img :src="$withBase('/foo.png')" alt="foo" />
```md
![An image](/image-inside-public.png)
```
You do **not** need to update it when you change the `base` config value in this case.
However, if you are authoring a theme component that links to assets dynamically, e.g. an image whose `src` is based on a theme config value:
```vue
<img :src="theme.logoPath" />
```
Note you can use the above syntax not only in theme components, but in your Markdown files as well.
In this case it is recommended to wrap the path with the [`withBase` helper](/guide/api.html#withbase) provided by VitePress:
```vue
<script setup>
import { withBase, useData } from 'vitepress'
const { theme } = useData()
</script>
<template>
<img :src="withBase(theme.logoPath)" />
</template>
```

@ -1,51 +0,0 @@
# Customization
You can develop your custom theme by adding the `.vitepress/theme/index.js` file.
```bash
.
├─ docs
│ ├─ .vitepress
│ │ ├─ theme
│ │ │ └─ index.js
│ │ └─ config.js
│ └─ index.md
└─ package.json
```
In `.vitepress/theme/index.js`, you must export theme object and register your own theme layout. Let's say you have your own `Layout` component like this.
```vue
<!-- .vitepress/theme/Layout.vue -->
<template>
<h1>Custom Layout!</h1>
<Content /><!-- make sure to include markdown outlet -->
</template>
```
Then include it in `.vitepress/theme/index.js`.
```js
// .vitepress/theme/index.js
import Layout from './Layout.vue'
export default {
Layout,
NotFound: () => 'custom 404', // <- this is a Vue 3 functional component
enhanceApp({ app, router, siteData }) {
// app is the Vue 3 app instance from `createApp()`. router is VitePress'
// custom router. `siteData`` is a `ref`` of current site-level metadata.
}
}
```
If you want to extend the default theme, you can import it from `vitepress/theme`.
```js
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
export default {
...DefaultTheme
}
```

@ -1,3 +1,7 @@
---
sidebarDepth: 3
---
# Deploying
The following guides are based on some shared assumptions:
@ -198,7 +202,7 @@ pages:
3. Deploy to surge by typing `surge docs/.vitepress/dist`.
You can also deploy to a [custom domain](http://surge.sh/help/adding-a-custom-domain) by adding `surge docs/.vitepress/dist yourdomain.com`.
You can also deploy to a [custom domain](https://surge.sh/help/adding-a-custom-domain) by adding `surge docs/.vitepress/dist yourdomain.com`.
## Heroku

@ -1,3 +1,6 @@
---
sidebarDepth: 2
---
# Differences from VuePress
VitePress and VuePress have different [design goals](../index.md). Both projects share similar config naming conventions. VitePress aims to have the bare minimum features needed for authoring docs. Other features are pushed to Themes. On the other hand, VuePress has more features out-of-the-box or enabled by its ecosystem of plugins.
@ -22,8 +25,8 @@ In case you decide to move your project to VitePress, this is a list of differen
- Components in `.vitepress/components` [are not auto registered as global components](https://vuepress.vuejs.org/)
- Differences
- [Public files](https://vuepress.vuejs.org/guide/assets.html#public-files) that are directly copied to dist root moved from `.vitepress/public/` is `public/`
- [styling](https://vuepress.vuejs.org/config/#styling) `.vitepress/styles/index.styl` and `.vitepress/styles/palette.styl` is `.vitepress/style.styl`
- [App Level Enhancements](https://vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements) API, app enhancements `.vitepress/enhanceApp.js` is `.vitepress/theme/index.js`.
- [styling](https://vuepress.vuejs.org/config/#styling) `.vitepress/styles/index.styl` and `.vitepress/styles/palette.styl` is not supported. See [Customizing CSS](/guide/theming.html#customizing-css).
- [App Level Enhancements](https://vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements) API, app enhancements `.vitepress/enhanceApp.js` is now done in `.vitepress/theme/index.js`. See [Extending the Default Theme](/guide/theming.html#extending-the-default-theme).
## Markdown
@ -31,7 +34,6 @@ In case you decide to move your project to VitePress, this is a list of differen
- Support for [toml in frontmatter](https://vuepress.vuejs.org/guide/frontmatter.html#alternative-frontmatter-formats)
- [details block](https://vuepress.vuejs.org/guide/markdown.html#custom-containers)
- [markdown slots](https://vuepress.vuejs.org/guide/markdown-slot.html)
guide/using-vue.html#using-components).
- `~` prefix to explicitly specify a url is a [webpack module request](https://vuepress.vuejs.org/guide/assets.html#relative-urls)
## Site Config
@ -67,7 +69,7 @@ In case you decide to move your project to VitePress, this is a list of differen
- `algolia` is `search.algolia`
- `searchPlaceholder` is `search.placeholder`
# Default Theme
## Default Theme
- Missing
- [`<code-group>` and `<code-block>`](https://vuepress.vuejs.org/theme/default-theme-config.html#code-groups-and-code-blocks)

@ -9,7 +9,7 @@ editLink: true
---
```
Between the triple-dashed lines, you can set [predefined variables](#predefined-variables), or even create custom ones of your own. These variables can be used via the <code>$frontmatter</code> variable.
Between the triple-dashed lines, you can set [predefined variables](#predefined-variables), or even create custom ones of your own. These variables can be used via the special <code>$frontmatter</code> variable.
Heres an example of how you could use it in your Markdown file:

@ -1,89 +0,0 @@
# Global Computed
In VitePress, some core [computed properties](https://v3.vuejs.org/guide/computed.html#computed-properties) can be used by the default theme or custom themes. Or directly in Markdown pages using vue, for example using `$frontmatter.title` to access the title defined in the frontmatter section of the page.
## $site
This is the `$site` value of the site you're currently reading:
```js
{
base: '/',
lang: 'en-US',
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
head: [],
locales: {},
themeConfig: $themeConfig
}
```
## $themeConfig
Refers to `$site.themeConfig`.
```js
{
locales: {},
repo: 'vuejs/vitepress',
docsDir: 'docs',
editLinks: true,
editLinkText: 'Edit this page on GitHub',
lastUpdated: 'Last Updated',
nav: [...],
sidebar: { ... }
}
```
## $page
This is the `$page` value of the page you're currently reading:
```js
{
relativePath: 'guide/global-computed.md',
title: 'Global Computed',
headers: [
{ level: 2, title: '$site', slug: 'site' },
{ level: 2, title: '$page', slug: '$page' },
...
],
frontmatter: $frontmatter,
lastUpdated: 1606297645000
}
```
## $frontmatter
Reference of `$page.frontmatter`.
```js
{
title: 'Docs with VitePress',
editLink: true
}
```
## $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.
## $description
The content value of the `<meta name= "description" content= "...">` for the current page.
## $withBase
Helper method to generate correct path by prepending the `base` path configured in `.vitepress/config.js`. It's useful when you want to link to [public files with base path](./assets#public-files).
```html
<img :src="$withBase('/foo.png')" alt="foo" />
```

@ -1,3 +1,7 @@
---
sidebarDepth: 3
---
# Markdown Extensions
## Header Anchors
@ -28,9 +32,9 @@ For example, given the following directory structure:
And providing you are in `foo/one.md`:
```md
[Home](/) <!-- sends the user to the root README.md -->
[Home](/) <!-- sends the user to the root index.md -->
[foo](/foo/) <!-- sends the user to index.html of directory foo -->
[foo heading](./#heading) <!-- anchors user to a heading in the foo README file -->
[foo heading](./#heading) <!-- anchors user to a heading in the foo index file -->
[bar - three](../bar/three) <!-- you can omit extention -->
[bar - three](../bar/three.md) <!-- you can append .md -->
[bar - four](../bar/four.html) <!-- or you can append .html -->
@ -56,7 +60,6 @@ Outbound links automatically get `target="_blank" rel="noopener noreferrer"`:
title: Blogging Like a Hacker
lang: en-US
---
```
This data will be available to the rest of the page, along with all custom and theming components.
@ -124,6 +127,10 @@ Custom containers can be defined by their types, titles, and contents.
This is a tip
:::
::: info
This is an info box
:::
::: warning
This is a warning
:::
@ -139,6 +146,10 @@ This is a dangerous warning
This is a tip
:::
::: info
This is an info box
:::
::: warning
This is a warning
:::
@ -360,7 +371,7 @@ It also supports [line highlighting](#line-highlighting-in-code-blocks):
<!--lint enable strong-marker-->
::: tip
The value of `@` corresponds to `process.cwd()`.
The value of `@` corresponds to the source root. By default it's the VitePress project root, unless `srcDir` is configured.
:::
You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/codebasics#_folding) to only include the corresponding part of the code file. You can provide a custom region name after a `#` following the filepath (`snippet` by default):
@ -392,12 +403,17 @@ You can also use a [VS Code region](https://code.visualstudio.com/docs/editor/co
VitePress uses [markdown-it](https://github.com/markdown-it/markdown-it) as the Markdown renderer. A lot of the extensions above are implemented via custom plugins. You can further customize the `markdown-it` instance using the `markdown` option in `.vitepress/config.js`:
```js
const anchor = require('markdown-it-anchor')
module.exports = {
markdown: {
// options for markdown-it-anchor
anchor: { permalink: false },
// https://github.com/valeriangalliat/markdown-it-anchor#permalinks
anchor: {
permalink: anchor.permalink.headerLink()
},
// options for markdown-it-toc
// options for markdown-it-table-of-contents
toc: { includeLevel: [1, 2] },
config: (md) => {

@ -0,0 +1,157 @@
# Theming
## Using a Custom Theme
You can enable a custom theme by adding the `.vitepress/theme/index.js` file (the "theme entry file").
```bash
.
├─ docs
│ ├─ .vitepress
│ │ ├─ theme
│ │ │ └─ index.js
│ │ └─ config.js
│ └─ index.md
└─ package.json
```
A VitePress custom theme is simply an object containing three properties and is defined as follows:
```ts
interface Theme {
Layout: Component // Vue 3 component
NotFound?: Component
enhanceApp?: (ctx: EnhanceAppContext) => void
}
interface EnhanceAppContext {
app: App // Vue 3 app instance
router: Router // VitePress router instance
siteData: Ref<SiteData>
}
```
The theme entry file should export the theme as its default export:
```js
// .vitepress/theme/index.js
import Layout from './Layout.vue'
export default {
Layout,
NotFound: () => 'custom 404', // <- this is a Vue 3 functional component
enhanceApp({ app, router, siteData }) {
// app is the Vue 3 app instance from `createApp()`. router is VitePress'
// custom router. `siteData`` is a `ref`` of current site-level metadata.
}
}
```
...where the `Layout` component could look like this:
```vue
<!-- .vitepress/theme/Layout.vue -->
<template>
<h1>Custom Layout!</h1>
<Content /><!-- this is where markdown content will be rendered -->
</template>
```
The default export is the only contract for a custom theme. Inside your custom theme, it works just like a normal Vite + Vue 3 application. Do note the theme also needs to be [SSR-compatible](/guide/using-vue.html#browser-api-access-restrictions).
To distribute a theme, simply export the object in your package entry. To consume an external theme, import and re-export it from the custom theme entry:
```js
// .vitepress/theme/index.js
import Theme from 'awesome-vitepress-theme'
export default Theme
```
## Extending the Default Theme
If you want to extend and customize the default theme, you can import it from `vitepress/theme` and augment it in a custom theme entry. Here are some examples of common customizations:
### Registering Global Components
```js
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
export default {
...DefaultTheme,
enhanceApp({ app }) {
// register global components
app.component('MyGlobalComponent' /* ... */)
}
}
```
Since we are using Vite, you can also leverage Vite's [glob import feature](https://vitejs.dev/guide/features.html#glob-import) to auto register a directory of components.
### Customizing CSS
The default theme CSS is customizable by overriding root level CSS variables:
```js
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './custom.css'
export default DefaultTheme
```
```css
/* .vitepress/theme/custom.css */
:root {
--c-brand: #646cff;
--c-brand-light: #747bff;
}
```
See [default theme CSS variables](https://github.com/vuejs/vitepress/blob/master/src/client/theme-default/styles/vars.css) that can be overridden.
### Layout Slots
The default theme's `<Layout/>` component has a few slots that can be used to inject content at certain locations of the page. Here's an example of injecting a component into the top of the sidebar:
```js
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import MyLayout from './MyLayout.vue'
export default {
...DefaultTheme,
// override the Layout with a wrapper component that injects the slots
Layout: MyLayout
}
```
```vue
<!--.vitepress/theme/MyLayout.vue-->
<script setup>
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
</script>
<template>
<Layout>
<template #sidebar-top>
My custom sidebar top content
</template>
</Layout>
</template>
```
Full list of slots available in the default theme layout:
- `navbar-search`
- `sidebar-top`
- `sidebar-bottom`
- `page-top-ads`
- `page-top`
- `page-bottom`
- `page-bottom-ads`
- Only when `home: true` is enabled via frontmatter:
- `home-hero`
- `home-features`
- `home-footer`

@ -1,59 +1,12 @@
# Using Vue in Markdown
## Browser API Access Restrictions
Because VitePress applications are server-rendered in Node.js when generating static builds, any Vue usage must conform to the [universal code requirements](https://ssr.vuejs.org/en/universal.html). In short, make sure to only access Browser / DOM APIs in `beforeMount` or `mounted` hooks.
If you are using or demoing components that are not SSR-friendly (for example, contain custom directives), you can wrap them inside the built-in `<ClientOnly>` component:
``` md
<ClientOnly>
<NonSSRFriendlyComponent/>
</ClientOnly>
```
Note this does not fix components or libraries that access Browser APIs **on import**. To use code that assumes a browser environment on import, you need to dynamically import them in proper lifecycle hooks:
``` vue
<script>
export default {
mounted () {
import('./lib-that-access-window-on-import').then(module => {
// use code
})
}
}
</script>
```
If your module `export default` a Vue component, you can register it dynamically:
```vue
<template>
<component v-if="dynamicComponent" :is="dynamicComponent"></component>
</template>
---
sidebarDepth: 3
---
<script>
export default {
data() {
return {
dynamicComponent: null
}
},
mounted () {
import('./lib-that-access-window-on-import').then(module => {
this.dynamicComponent = module.default
})
}
}
</script>
```
**Also see:**
# Using Vue in Markdown
- [Vue.js > Dynamic Components](https://v3.vuejs.org/guide/component-dynamic-async.html)
In VitePress, each markdown file is compiled into HTML and then processed as a Vue Single-File Component. This means you can use any Vue features inside the markdown, including dynamic templating, using Vue components, or arbitrary in-page Vue component logic by adding a `<script>` tag.
It is also important to know that VitePress leverages Vue 3's compiler to automatically detect and optimize the purely static parts of the markdown. Static contents are optimized into single placeholder nodes and eliminated from the page's JavaScript payload. They are also skipped during client-side hydration. In short, you only pay for the dynamic parts on any given page.
## Templating
@ -63,7 +16,7 @@ Each Markdown file is first compiled into HTML and then passed on as a Vue compo
**Input**
``` md
```md
{{ 1 + 1 }}
```
@ -77,7 +30,7 @@ Directives also work:
**Input**
``` md
```md
<span v-for="i in 3">{{ i }} </span>
```
@ -87,17 +40,22 @@ Directives also work:
### Access to Site & Page Data
The compiled component has access to the [site metadata and computed properties](./global-computed.md). For example:
You can use the [`useData` helper](/guide/api.html#usedata) in a `<script>` block and expose the data to the page.
**Input**
``` md
{{ $page }}
```md
<script setup>
import { useData } from 'vitepress'
const { page } = useData()
</script>
<pre>{{ page }}</pre>
```
**Output**
``` json
```json
{
"path": "/using-vue.html",
"title": "Using Vue in Markdown",
@ -111,7 +69,7 @@ By default, fenced code blocks are automatically wrapped with `v-pre`. To displa
**Input**
``` md
```md
::: v-pre
`{{ This will be displayed as-is }}`
:::
@ -132,6 +90,10 @@ When you need to have more flexibility, VitePress allows you to extend your auth
If your components are going to be used in only a few places, the recommended way to use them is to importing the components in the file where it is used.
```md
<script setup>
import CustomComponent from '../components/CustomComponent.vue'
</script>
# Docs
This is a .md using a custom component
@ -141,15 +103,11 @@ This is a .md using a custom component
## More docs
...
<script setup>
import CustomComponent from '../components/CustomComponent.vue'
</script>
```
### Registering global components in the theme
If the components are going to be used across several pages in the docs, they can be registered globally in the theme (or as part of extending the default VitePress theme). Check out the [Customization Guide](./customization.md) for more information.
If the components are going to be used across several pages in the docs, they can be registered globally in the theme (or as part of extending the default VitePress theme). Check out the [Theming Guide](./theming.md) for more information.
In `.vitepress/theme/index.js`, the `enhanceApp` function receives the Vue `app` instance so you can [register components](https://v3.vuejs.org/guide/component-registration.html#component-registration) as you would do in a regular Vue application.
@ -180,9 +138,9 @@ Make sure a custom components name either contains a hyphen or is in PascalCa
You can use Vue components in the headers, but note the difference between the following syntaxes:
| Markdown | Output HTML | Parsed Header |
|--------|-------------|----------------|
| <pre v-pre><code> # text &lt;Tag/&gt; </code></pre> | `<h1>text <Tag/></h1>` | `text` |
| Markdown | Output HTML | Parsed Header |
| ------------------------------------------------------- | ----------------------------------------- | ------------- |
| <pre v-pre><code> # text &lt;Tag/&gt; </code></pre> | `<h1>text <Tag/></h1>` | `text` |
| <pre v-pre><code> # text \`&lt;Tag/&gt;\` </code></pre> | `<h1>text <code>&lt;Tag/&gt;</code></h1>` | `text <Tag/>` |
The HTML wrapped by `<code>` will be displayed as-is; only the HTML that is **not** wrapped will be parsed by Vue.
@ -208,7 +166,7 @@ npm install -D stylus
Then you can use the following in Markdown and theme components:
``` vue
```vue
<style lang="sass">
.title
font-size: 20px
@ -247,3 +205,57 @@ VitePress provides Built-In Vue Components like `ClientOnly` and `OutboundLink`,
**Also see:**
- [Using Components In Headers](#using-components-in-headers)
## Browser API Access Restrictions
Because VitePress applications are server-rendered in Node.js when generating static builds, any Vue usage must conform to the [universal code requirements](https://ssr.vuejs.org/en/universal.html). In short, make sure to only access Browser / DOM APIs in `beforeMount` or `mounted` hooks.
If you are using or demoing components that are not SSR-friendly (for example, contain custom directives), you can wrap them inside the built-in `<ClientOnly>` component:
```md
<ClientOnly>
<NonSSRFriendlyComponent/>
</ClientOnly>
```
Note this does not fix components or libraries that access Browser APIs **on import**. To use code that assumes a browser environment on import, you need to dynamically import them in proper lifecycle hooks:
```vue
<script>
export default {
mounted() {
import('./lib-that-access-window-on-import').then((module) => {
// use code
})
}
}
</script>
```
If your module `export default` a Vue component, you can register it dynamically:
```vue
<template>
<component v-if="dynamicComponent" :is="dynamicComponent"></component>
</template>
<script>
export default {
data() {
return {
dynamicComponent: null
}
},
mounted() {
import('./lib-that-access-window-on-import').then((module) => {
this.dynamicComponent = module.default
})
}
}
</script>
```
**Also see:**
- [Vue.js > Dynamic Components](https://v3.vuejs.org/guide/component-dynamic-async.html)

@ -1,26 +1,28 @@
---
sidebarDepth: 2
---
# What is VitePress?
::: warning WARNING
VitePress 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 currently in 0.x status. It is already suitable for out-of-the-box documentation use, but the config and theming API may still change between minor releases.
:::
VitePress is [VuePress](https://vuepress.vuejs.org/)' little brother, built on top of [Vite](https://github.com/vitejs/vite).
## Motivation
We love VuePress, but being built on top of Webpack, the time it takes to spin up the dev server for a simple doc site with a few pages is just becoming unbearable. Even HMR updates can take up to seconds to reflect in the browser!
As a reference, the [Composition API RFC repo](https://github.com/vuejs/composition-api-rfc) is just two pages, but it takes 4 seconds to spin up the server and almost 2 seconds for any edit to reflect in the browser.
We love VuePress v1, but being built on top of Webpack, the time it takes to spin up the dev server for a simple doc site with a few pages is just becoming unbearable. Even HMR updates can take up to seconds to reflect in the browser!
Fundamentally, this is because VuePress is a Webpack app under the hood. Even with just two pages, it's a full on Webpack project (including all the theme source files) being compiled. It gets even worse when the project has many pages every page must first be fully compiled before the server can even display anything!
Fundamentally, this is because VuePress v1 is a Webpack app under the hood. Even with just two pages, it's a full on Webpack project (including all the theme source files) being compiled. It gets even worse when the project has many pages every page must first be fully compiled before the server can even display anything!
Incidentally, Vite solves these problems really well: nearly instant server start, an on-demand compilation that only compiles the page being served, and lightning-fast HMR. Plus, there are a few additional design issues I have noted in VuePress over time but never had the time to fix due to the amount of refactoring it would require.
Incidentally, Vite solves these problems really well: nearly instant server start, an on-demand compilation that only compiles the page being served, and lightning-fast HMR. Plus, there are a few additional design issues I have noted in VuePress v1 over time but never had the time to fix due to the amount of refactoring it would require.
Now, with Vite and Vue 3, it is time to rethink what a "Vue-powered static site generator" can really be.
## Improvements Over VuePress
## Improvements Over VuePress v1
There're couple of things that are improved from VuePress....
There're couple of things that are improved from VuePress v1....
### It Uses Vue 3
@ -49,4 +51,6 @@ VitePress is future oriented: VitePress only targets browsers that support nativ
## Will This Become The Next VuePress in The Future?
Probably not. It's currently under a different name so that we don't over commit to the compatibility with the current VuePress ecosystem (mostly themes and plugins). We'll see how close we can get without compromising the design goals listed above. But the overall idea is that VitePress will have a drastically more minimal theming API (preferring JavaScript APIs instead of file layout conventions) and likely no plugins (all customization is done in themes).
We already have [vuepress-next](https://github.com/vuepress/vuepress-next), which would be the next major version of VuePress. It also makes lots of improvements over VuePress v1, and also supports Vite now.
VitePress is not compatible with the current VuePress ecosystem (mostly themes and plugins). The overall idea is that VitePress will have a drastically more minimal theming API (preferring JavaScript APIs instead of file layout conventions) and likely no plugins (all customization is done in themes).

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

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

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

File diff suppressed because it is too large Load Diff

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

@ -2,6 +2,6 @@ const fs = require('fs-extra')
const glob = require('globby')
glob.sync('src/shared/**/*.ts').forEach((file) => {
fs.copy(file, file.replace(/^src\//, 'src/node/'))
fs.copy(file, file.replace(/^src\//, 'src/client/'))
fs.copy(file, file.replace(/^src\/shared\//, 'src/node/'))
fs.copy(file, file.replace(/^src\/shared\//, 'src/client/'))
})

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

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

@ -1,18 +1,20 @@
const fs = require('fs-extra')
const chokidar = require('chokidar')
const { normalizePath } = require('vite')
function toClientAndNode(method, file) {
file = normalizePath(file)
if (method === 'copy') {
fs.copy(file, file.replace(/^src\//, 'src/node/'))
fs.copy(file, file.replace(/^src\//, 'src/client/'))
fs.copy(file, file.replace(/^src\/shared\//, 'src/node/'))
fs.copy(file, file.replace(/^src\/shared\//, 'src/client/'))
} else if (method === 'remove') {
fs.remove(file.replace(/^src\//, 'src/node/'))
fs.remove(file.replace(/^src\//, 'src/client/'))
fs.remove(file.replace(/^src\/shared\//, 'src/node/'))
fs.remove(file.replace(/^src\/shared\//, 'src/client/'))
}
}
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.

@ -5,6 +5,9 @@ export const Content = defineComponent({
name: 'VitePressContent',
setup() {
const route = useRoute()
return () => (route.component ? h(route.component) : null)
return () =>
h('div', { style: { position: 'relative' } }, [
route.component ? h(route.component) : null
])
}
})

@ -1,26 +1,28 @@
<template>
<div class="debug" :class="{ open }" ref="el" @click="open = !open">
<p class="title">Debug</p>
<pre class="block">$page {{ $page }}</pre>
<pre class="block">$siteByRoute {{ $siteByRoute }}</pre>
<pre class="block">$site {{ $site }}</pre>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref, watch, reactive } from 'vue'
import { useData } from '../data'
const data = useData()
const el = ref<HTMLElement | null>(null)
const open = ref(false)
// FIXME: remove in next Vue release
const tempData = reactive(data)
watch(open, (value) => {
if (value === false) {
if (!value) {
el.value!.scrollTop = 0
}
})
</script>
<template>
<div class="debug" :class="{ open }" ref="el" @click="open = !open">
<p class="title">Debug</p>
<pre class="block">{{ tempData }}</pre>
</div>
</template>
<style scoped>
.debug {
box-sizing: border-box;

@ -1,9 +0,0 @@
import { Ref, computed } from 'vue'
import { usePageData } from './pageData'
export type FrontmatterRef = Ref<Record<string, any>>
export function useFrontmatter() {
const pageData = usePageData()
return computed(() => pageData.value.frontmatter)
}

@ -1,11 +1,11 @@
import { watchEffect, Ref } from 'vue'
import { HeadConfig, SiteData } from '../../../../types/shared'
import { HeadConfig, SiteData } from '../../shared'
import { Route } from '../router'
export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta'))
let managedHeadTags: HTMLElement[] = []
let isFirstUpdate = true
const updateHeadTags = (newTags: HeadConfig[]) => {
if (import.meta.env.PROD && isFirstUpdate) {
// in production, the initial meta tags are already pre-rendered so we
@ -13,15 +13,44 @@ export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
isFirstUpdate = false
return
}
metaTags.forEach((el) => document.head.removeChild(el))
metaTags.length = 0
if (newTags && newTags.length) {
newTags.forEach((headConfig) => {
const el = createHeadElement(headConfig)
document.head.appendChild(el)
metaTags.push(el)
})
const newEls: HTMLElement[] = []
const commonLength = Math.min(managedHeadTags.length, newTags.length)
for (let i = 0; i < commonLength; i++) {
let el = managedHeadTags[i]
const [tag, attrs, innerHTML = ''] = newTags[i]
if (el.tagName.toLocaleLowerCase() === tag) {
for (const key in attrs) {
if (el.getAttribute(key) !== attrs[key]) {
el.setAttribute(key, attrs[key])
}
}
for (let i = 0; i < el.attributes.length; i++) {
const name = el.attributes[i].name
if (!(name in attrs)) {
el.removeAttribute(name)
}
}
if (el.innerHTML !== innerHTML) {
el.innerHTML = innerHTML
}
} else {
document.head.removeChild(el)
el = createHeadElement(newTags[i])
document.head.append(el)
}
newEls.push(el)
}
managedHeadTags
.slice(commonLength)
.forEach((el) => document.head.removeChild(el))
newTags.slice(commonLength).forEach((headConfig) => {
const el = createHeadElement(headConfig)
document.head.appendChild(el)
newEls.push(el)
})
managedHeadTags = newEls
}
watchEffect(() => {
@ -30,25 +59,17 @@ export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
const pageTitle = pageData && pageData.title
const pageDescription = pageData && pageData.description
const frontmatterHead = pageData && pageData.frontmatter.head
// update title and description
document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title
document
.querySelector(`meta[name=description]`)!
.setAttribute('content', pageDescription || siteData.description)
updateHeadTags([
['meta', { charset: 'utf-8' }],
[
'meta',
{
name: 'viewport',
content: 'width=device-width,initial-scale=1'
}
],
[
'meta',
{
name: 'description',
content: pageDescription || siteData.description
}
],
...siteData.head,
...((frontmatterHead && filterOutHeadDescription(frontmatterHead)) || [])
// site head can only change during dev
...(import.meta.env.DEV ? siteData.head : []),
...(frontmatterHead ? filterOutHeadDescription(frontmatterHead) : [])
])
})
}

@ -1,11 +0,0 @@
import { Ref, computed } from 'vue'
import { PageData } from '/@types/shared'
import { Route, useRoute } from '../router'
export type PageDataRef = Ref<PageData>
export function usePageData(route?: Route) {
const r = route || useRoute()
return computed(() => r.data)
}

@ -1,22 +0,0 @@
import serialized from '@siteData'
import { SiteData } from '/@types/shared'
import { Ref, ref, readonly } from 'vue'
export type SiteDataRef<T = any> = Ref<SiteData<T>>
export const siteDataRef: Ref<SiteData> = ref(parse(serialized))
export function useSiteData<T = any>() {
return siteDataRef as Ref<SiteData<T>>
}
function parse(data: string): SiteData {
return readonly(JSON.parse(data)) as SiteData
}
// hmr
if (import.meta.hot) {
import.meta.hot!.accept('/@siteData', (m) => {
siteDataRef.value = parse(m.default)
})
}

@ -1,12 +0,0 @@
import { computed } from 'vue'
import { resolveSiteDataByRoute } from '/@shared/config'
import { siteDataRef } from './siteData'
import { Route, useRoute } from '../router'
export function useSiteDataByRoute(route?: Route) {
const r = route || useRoute()
return computed(() => {
return resolveSiteDataByRoute(siteDataRef.value, r.path)
})
}

@ -0,0 +1,72 @@
import { InjectionKey, Ref, shallowRef, readonly, computed, inject } from 'vue'
import { Route } from './router'
import serializedSiteData from '@siteData'
import { resolveSiteDataByRoute, PageData, SiteData } from '../shared'
import { withBase } from './utils'
export const dataSymbol: InjectionKey<VitePressData> = Symbol()
export interface VitePressData<T = any> {
site: Ref<SiteData<T>>
page: Ref<PageData>
theme: Ref<T>
frontmatter: Ref<PageData['frontmatter']>
title: Ref<string>
description: Ref<string>
lang: Ref<string>
localePath: Ref<string>
}
// site data is a singleton
export type SiteDataRef<T = any> = Ref<SiteData<T>>
export const siteDataRef: Ref<SiteData> = shallowRef(parse(serializedSiteData))
function parse(data: string): SiteData {
return readonly(JSON.parse(data)) as SiteData
}
// hmr
if (import.meta.hot) {
import.meta.hot!.accept('/@siteData', (m) => {
siteDataRef.value = parse(m.default)
})
}
// per-app data
export function initData(route: Route): VitePressData {
const site = computed(() =>
resolveSiteDataByRoute(siteDataRef.value, route.path)
)
return {
site,
theme: computed(() => site.value.themeConfig),
page: computed(() => route.data),
frontmatter: computed(() => route.data.frontmatter),
lang: computed(() => site.value.lang),
localePath: computed(() => {
const { langs, lang } = site.value
const path = Object.keys(langs).find(
(langPath) => langs[langPath].lang === lang
)
return withBase(path || '/')
}),
title: computed(() => {
return route.data.title
? route.data.title + ' | ' + site.value.title
: site.value.title
}),
description: computed(() => {
return route.data.description || site.value.description
})
}
}
export function useData<T = any>(): VitePressData<T> {
const data = inject(dataSymbol)
if (!data) {
throw new Error('vitepress data not properly injected in app')
}
return data
}

@ -1,33 +1,33 @@
import 'vite/dynamic-import-polyfill'
import {
App,
createApp as createClientApp,
createSSRApp,
defineAsyncComponent,
h,
onMounted,
watch
} from 'vue'
import { inBrowser, pathToFile } from './utils'
import { Router, RouterSymbol, createRouter } from './router'
import { mixinGlobalComputed, mixinGlobalComponents } from './mixin'
import { siteDataRef } from './composables/siteData'
import { useSiteDataByRoute } from './composables/siteDataByRoute'
import { usePageData } from './composables/pageData'
import { siteDataRef, useData } from './data'
import { useUpdateHead } from './composables/head'
import Theme from '/@theme/index'
import { usePrefetch } from './composables/preFetch'
import { dataSymbol, initData } from './data'
import { Content } from './components/Content'
import { ClientOnly } from './components/ClientOnly'
const NotFound = Theme.NotFound || (() => '404 Not Found')
const VitePressApp = {
name: 'VitePressApp',
setup() {
const siteData = useSiteDataByRoute()
const { site } = useData()
// change the language on the HTML element based on the current lang
onMounted(() => {
watch(
() => siteData.value.lang,
() => site.value.lang,
(lang: string) => {
document.documentElement.lang = lang
},
@ -52,16 +52,30 @@ export function createApp() {
app.provide(RouterSymbol, router)
const siteDataByRouteRef = useSiteDataByRoute(router.route)
const pageDataRef = usePageData(router.route)
const data = initData(router.route)
app.provide(dataSymbol, data)
if (inBrowser) {
// dynamically update head tags
useUpdateHead(router.route, siteDataByRouteRef)
useUpdateHead(router.route, data.site)
}
mixinGlobalComputed(app, siteDataRef, siteDataByRouteRef, pageDataRef)
mixinGlobalComponents(app)
// install global components
app.component('Content', Content)
app.component('ClientOnly', ClientOnly)
app.component(
'Debug',
import.meta.env.PROD
? () => null
: defineAsyncComponent(() => import('./components/Debug.vue'))
)
// expose $frontmatter
Object.defineProperty(app.config.globalProperties, '$frontmatter', {
get() {
return data.frontmatter.value
}
})
if (Theme.enhanceApp) {
Theme.enhanceApp({

@ -1,95 +0,0 @@
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 { ClientOnly } from './components/ClientOnly'
export function mixinGlobalComputed(
app: App,
site: SiteDataRef,
siteByRoute: SiteDataRef,
page: PageDataRef
): void {
Object.defineProperties(app.config.globalProperties, {
$site: {
get() {
return site.value
}
},
$siteByRoute: {
get() {
return siteByRoute.value
}
},
$themeConfig: {
get() {
return siteByRoute.value.themeConfig
}
},
$page: {
get() {
return page.value
}
},
$frontmatter: {
get() {
return page.value.frontmatter
}
},
$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
? page.value.title + ' | ' + siteByRoute.value.title
: siteByRoute.value.title
}
},
$description: {
get() {
return page.value.description || siteByRoute.value.description
}
},
$withBase: {
value(path: string) {
return joinPath(site.value.base, path)
}
}
})
}
export function mixinGlobalComponents(app: App) {
app.component('Content', Content)
app.component('ClientOnly', ClientOnly)
app.component(
'Debug',
import.meta.env.PROD
? () => null
: defineAsyncComponent(() => import('./components/Debug.vue'))
)
}

@ -1,6 +1,6 @@
import { reactive, inject, markRaw, nextTick, readonly } from 'vue'
import type { Component, InjectionKey } from 'vue'
import { PageData } from '../../../types/shared'
import { PageData } from '../shared'
import { inBrowser } from './utils'
export interface Route {
@ -77,7 +77,9 @@ export function createRouter(
route.path = pendingPath
route.component = markRaw(comp)
route.data = readonly(JSON.parse(__pageData)) as PageData
route.data = import.meta.env.PROD
? markRaw(JSON.parse(__pageData))
: (readonly(JSON.parse(__pageData)) as PageData)
if (inBrowser) {
nextTick(() => {
@ -94,7 +96,7 @@ export function createRouter(
})
}
}
} catch (err) {
} catch (err: any) {
if (!err.message.match(/fetch/)) {
console.error(err)
}
@ -131,6 +133,8 @@ export function createRouter(
// scroll between hash anchors in the same page
if (hash && hash !== currentUrl.hash) {
history.pushState(null, '', hash)
// still emit the event so we can listen to it in themes
window.dispatchEvent(new Event('hashchange'))
// use smooth scroll when clicking on header anchor links
scrollTo(link, hash, link.classList.contains('header-anchor'))
}
@ -172,13 +176,11 @@ export function useRoute(): Route {
}
function scrollTo(el: HTMLElement, hash: string, smooth = false) {
const pageOffset = (document.querySelector('.nav-bar') as HTMLElement)
.offsetHeight
const target = el.classList.contains('.header-anchor')
? el
: document.querySelector(decodeURIComponent(hash))
if (target) {
const targetTop = (target as HTMLElement).offsetTop - pageOffset - 15
const targetTop = (target as HTMLElement).offsetTop
// only smooth scroll if distance is smaller than screen height.
if (!smooth || Math.abs(targetTop - window.scrollY) > window.innerHeight) {
window.scrollTo(0, targetTop)

@ -1,6 +1,6 @@
import { App, Ref, ComponentOptions } from 'vue'
import { App, Ref, Component } from 'vue'
import { Router } from './router'
import { SiteData } from '../../../types/shared'
import { SiteData } from '../shared'
export interface EnhanceAppContext {
app: App
@ -9,7 +9,7 @@ export interface EnhanceAppContext {
}
export interface Theme {
Layout: ComponentOptions
NotFound?: ComponentOptions
Layout: Component
NotFound?: Component
enhanceApp?: (ctx: EnhanceAppContext) => void
}

@ -1,4 +1,5 @@
import { inBrowser } from '/@shared/config'
import { siteDataRef } from './data'
import { inBrowser, EXTERNAL_URL_RE } from '../shared'
export { inBrowser }
@ -9,11 +10,18 @@ export function joinPath(base: string, path: string): string {
return `${base}${path}`.replace(/\/+/g, '/')
}
export function withBase(path: string) {
return EXTERNAL_URL_RE.test(path)
? path
: joinPath(siteDataRef.value.base, path)
}
/**
* Converts a url path to the corresponding js chunk filename.
*/
export function pathToFile(path: string): string {
let pagePath = path.replace(/\.html$/, '')
pagePath = decodeURIComponent(pagePath)
if (pagePath.endsWith('/')) {
pagePath += 'index'
}

@ -3,19 +3,24 @@
// generic types
export type { Router, Route } from './app/router'
export type { VitePressData } from './app/data'
// theme types
export type { Theme, EnhanceAppContext } from './app/theme'
// shared types
export type {
PageData,
SiteData,
HeadConfig,
Header,
LocaleConfig
} from '../../types/shared'
// composables
export { useData } from './app/data'
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'
export { inBrowser, withBase } from './app/utils'
// components
export { Content } from './app/components/Content'

@ -1,3 +1,85 @@
<script setup lang="ts">
import { ref, computed, watch, defineAsyncComponent } from 'vue'
import { useRoute, useData } from 'vitepress'
import { isSideBarEmpty, getSideBarConfig } from './support/sideBar'
// components
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 NoopComponent = () => null
const CarbonAds = __CARBON__
? defineAsyncComponent(() => import('./components/CarbonAds.vue'))
: NoopComponent
const BuySellAds = __BSA__
? defineAsyncComponent(() => import('./components/BuySellAds.vue'))
: NoopComponent
const AlgoliaSearchBox = __ALGOLIA__
? defineAsyncComponent(() => import('./components/AlgoliaSearchBox.vue'))
: NoopComponent
// generic state
const route = useRoute()
const { site, page, theme, frontmatter } = useData()
// custom layout
const isCustomLayout = computed(() => !!frontmatter.value.customLayout)
// home
const enableHome = computed(() => !!frontmatter.value.home)
// automatic multilang check for AlgoliaSearchBox
const isMultiLang = computed(() => Object.keys(site.value.langs).length > 1)
// navbar
const showNavbar = computed(() => {
const themeConfig = theme.value
if (frontmatter.value.navbar === false || themeConfig.navbar === false) {
return false
}
return (
site.value.title || themeConfig.logo || themeConfig.repo || themeConfig.nav
)
})
// sidebar
const openSideBar = ref(false)
const showSidebar = computed(() => {
if (frontmatter.value.home || frontmatter.value.sidebar === false) {
return false
}
return !isSideBarEmpty(
getSideBarConfig(theme.value.sidebar, route.data.relativePath)
)
})
const toggleSidebar = (to?: boolean) => {
openSideBar.value = typeof to === 'boolean' ? to : !openSideBar.value
}
const hideSidebar = toggleSidebar.bind(null, false)
// close the sidebar when navigating to a different location
watch(route, hideSidebar)
// TODO: route only changes when the pathname changes
// listening to hashchange does nothing because it's prevented in router
// page classes
const pageClasses = computed(() => {
return [
{
'no-navbar': !showNavbar.value,
'sidebar-open': openSideBar.value,
'no-sidebar': !showSidebar.value
}
]
})
</script>
<template>
<div class="theme" :class="pageClasses">
<NavBar v-if="showNavbar" @toggle="toggleSidebar">
@ -70,102 +152,6 @@
<Debug />
</template>
<script setup lang="ts">
import { ref, computed, watch, defineAsyncComponent } from 'vue'
import {
useRoute,
useSiteData,
usePageData,
useSiteDataByRoute
} from 'vitepress'
import { isSideBarEmpty, getSideBarConfig } from './support/sideBar'
import type { DefaultTheme } from './config'
// components
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 NoopComponent = () => null
const CarbonAds = __CARBON__
? defineAsyncComponent(() => import('./components/CarbonAds.vue'))
: NoopComponent
const BuySellAds = __BSA__
? defineAsyncComponent(() => import('./components/BuySellAds.vue'))
: NoopComponent
const AlgoliaSearchBox = __ALGOLIA__
? defineAsyncComponent(() => import('./components/AlgoliaSearchBox.vue'))
: NoopComponent
// generic state
const route = useRoute()
const siteData = useSiteData<DefaultTheme.Config>()
const siteRouteData = useSiteDataByRoute()
const theme = computed(() => siteData.value.themeConfig)
const page = usePageData()
// custom layout
const isCustomLayout = computed(() => !!route.data.frontmatter.customLayout)
// home
const enableHome = computed(() => !!route.data.frontmatter.home)
// navbar
const showNavbar = computed(() => {
const { themeConfig } = siteRouteData.value
const { frontmatter } = route.data
if (frontmatter.navbar === false || themeConfig.navbar === false) {
return false
}
return (
siteData.value.title ||
themeConfig.logo ||
themeConfig.repo ||
themeConfig.nav
)
})
// sidebar
const openSideBar = ref(false)
const showSidebar = computed(() => {
const { frontmatter } = route.data
if (frontmatter.home || frontmatter.sidebar === false) {
return false
}
const { themeConfig } = siteRouteData.value
return !isSideBarEmpty(
getSideBarConfig(themeConfig.sidebar, route.data.relativePath)
)
})
const toggleSidebar = (to?: boolean) => {
openSideBar.value = typeof to === 'boolean' ? to : !openSideBar.value
}
const hideSidebar = toggleSidebar.bind(null, false)
// close the sidebar when navigating to a different location
watch(route, hideSidebar)
// TODO: route only changes when the pathname changes
// listening to hashchange does nothing because it's prevented in router
// page classes
const pageClasses = computed(() => {
return [
{
'no-navbar': !showNavbar.value,
'sidebar-open': openSideBar.value,
'no-sidebar': !showSidebar.value
}
]
})
</script>
<style>
#ads-container {
margin: 0 auto;

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

@ -1,17 +1,10 @@
<template>
<div class="algolia-search-box" id="docsearch" />
</template>
<script setup lang="ts">
import '@docsearch/css'
import { useRoute, useRouter } from 'vitepress'
import { defineProps, getCurrentInstance, onMounted, watch } from 'vue'
import docsearch from '@docsearch/js'
import { useRoute, useRouter, useData } from 'vitepress'
import { getCurrentInstance, onMounted, watch } from 'vue'
import type { DefaultTheme } from '../config'
import type { DocSearchHit } from '@docsearch/react/dist/esm/types'
import { useSiteDataByRoute } from 'vitepress'
const siteData = useSiteDataByRoute()
const props = defineProps<{
options: DefaultTheme.AlgoliaSearchOptions
@ -57,10 +50,12 @@ function update(options: any) {
}
}
const { lang } = useData()
// if the user has multiple locales, the search results should be filtered
// based on the language
const facetFilters: string[] = props.multilang
? ['language:' + siteData.value.lang]
? ['language:' + lang.value]
: []
if (props.options.searchParameters?.facetFilters) {
@ -68,7 +63,7 @@ if (props.options.searchParameters?.facetFilters) {
}
watch(
() => siteData.value.lang,
lang,
(newLang, oldLang) => {
const index = facetFilters.findIndex(
(filter) => filter === 'language:' + oldLang
@ -161,6 +156,10 @@ function initialize(userOptions: any) {
}
</script>
<template>
<div class="algolia-search-box" id="docsearch" />
</template>
<style>
.algolia-search-box {
padding-top: 1px;
@ -169,13 +168,12 @@ function initialize(userOptions: any) {
@media (min-width: 720px) {
.algolia-search-box {
padding-left: 8px;
min-width: 176.3px; /* avoid layout shift */
}
}
@media (min-width: 751px) {
.algolia-search-box {
padding-left: 8px;
min-width: 176.3px; /* avoid layout shift */
}
.algolia-search-box .DocSearch-Button-Placeholder {

@ -1,11 +1,5 @@
<template>
<div class="buy-sell-ads">
<div class="bsa-cpc" />
</div>
</template>
<script setup lang="ts">
import { defineProps, onMounted } from 'vue'
import { onMounted } from 'vue'
// global _bsa
const ID = 'bsa-cpc-script'
@ -51,6 +45,10 @@ onMounted(() => {
function load() {
if (typeof _bsa !== 'undefined' && _bsa) {
const parent = document.querySelector('.bsa-cpc')!
// cleanup any existing ad to avoid them stacking
parent.innerHTML = ''
_bsa.init('default', code, `placement:${placement}`, {
target: '.bsa-cpc',
align: 'horizontal',
@ -60,6 +58,12 @@ function load() {
}
</script>
<template>
<div class="buy-sell-ads">
<div class="bsa-cpc" />
</div>
</template>
<style scoped>
.buy-sell-ads {
margin: 0 auto;

@ -1,9 +1,5 @@
<template>
<div class="carbon-ads" ref="el" />
</template>
<script setup lang="ts">
import { defineProps, ref, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
const { code, placement } = defineProps<{
code: string
@ -20,10 +16,15 @@ onMounted(() => {
})
</script>
<template>
<div class="carbon-ads" ref="el" />
</template>
<style scoped>
.carbon-ads {
border-radius: 4px;
margin: 0 auto;
padding: 8px;
max-width: 280px;
font-size: 0.75rem;
background-color: var(--c-bg-accent);
@ -41,7 +42,6 @@ onMounted(() => {
z-index: 1;
float: right;
margin: -8px -8px 24px 24px;
padding: 8px;
width: 146px;
max-width: 100%;
min-height: 200px;

@ -1,3 +1,10 @@
<script setup lang="ts">
import { useEditLink } from '../composables/editLink'
import OutboundLink from './icons/OutboundLink.vue'
const { url, text } = useEditLink()
</script>
<template>
<div class="edit-link">
<a
@ -12,13 +19,6 @@
</div>
</template>
<script setup lang="ts">
import { useEditLink } from '../composables/editLink'
import OutboundLink from './icons/OutboundLink.vue'
const { url, text } = useEditLink()
</script>
<style scoped>
.link {
display: inline-block;

@ -1,3 +1,9 @@
<script setup lang="ts">
import HomeHero from './HomeHero.vue'
import HomeFeatures from './HomeFeatures.vue'
import HomeFooter from './HomeFooter.vue'
</script>
<template>
<main class="home" aria-labelledby="main-title">
<HomeHero />
@ -12,12 +18,6 @@
</main>
</template>
<script setup lang="ts">
import HomeHero from './HomeHero.vue'
import HomeFeatures from './HomeFeatures.vue'
import HomeFooter from './HomeFooter.vue'
</script>
<style scoped>
.home {
padding-top: var(--header-height);
@ -28,11 +28,4 @@ import HomeFooter from './HomeFooter.vue'
margin: 0px auto;
padding: 0 1.5rem;
}
@media (max-width: 720px) {
.home-content {
max-width: 392px;
padding: 0;
}
}
</style>

@ -1,3 +1,23 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from 'vitepress'
const { frontmatter } = useData()
const hasFeatures = computed(() => {
return frontmatter.value.features && frontmatter.value.features.length > 0
})
interface Feature {
title?: string
details?: string
}
const features = computed<Feature[]>(() => {
return frontmatter.value.features ? frontmatter.value.features : []
})
</script>
<template>
<div v-if="hasFeatures" class="home-features">
<div class="wrapper">
@ -17,21 +37,6 @@
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useSiteDataByRoute, useFrontmatter } from 'vitepress'
const data = useFrontmatter()
const hasFeatures = computed(() => {
return data.value.features && data.value.features.length > 0
})
const features = computed(() => {
return data.value.features ? data.value.features : []
})
</script>
<style scoped>
.home-features {
margin: 0 auto;

@ -1,7 +1,13 @@
<script setup lang="ts">
import { useData } from 'vitepress'
const { frontmatter } = useData()
</script>
<template>
<footer v-if="$frontmatter.footer" class="footer">
<footer v-if="frontmatter.footer" class="footer">
<div class="container">
<p class="text">{{ $frontmatter.footer }}</p>
<p class="text">{{ frontmatter.footer }}</p>
</div>
</footer>
</template>

@ -1,59 +1,52 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData, withBase } from 'vitepress'
import NavLink from './NavLink.vue'
const { site, frontmatter } = useData()
const showHero = computed(() => {
const { heroImage, heroText, tagline, actionLink, actionText } =
frontmatter.value
return heroImage || heroText || tagline || (actionLink && actionText)
})
const heroText = computed(() => frontmatter.value.heroText || site.value.title)
const tagline = computed(
() => frontmatter.value.tagline || site.value.description
)
</script>
<template>
<header v-if="showHero" class="home-hero">
<figure v-if="$frontmatter.heroImage" class="figure">
<figure v-if="frontmatter.heroImage" class="figure">
<img
class="image"
:src="$withBase($frontmatter.heroImage)"
:alt="$frontmatter.heroAlt"
:src="withBase(frontmatter.heroImage)"
:alt="frontmatter.heroAlt"
/>
</figure>
<h1 v-if="hasHeroText" id="main-title" class="title">{{ heroText }}</h1>
<p v-if="hasTagline" class="description">{{ tagline }}</p>
<h1 v-if="heroText" id="main-title" class="title">{{ heroText }}</h1>
<p v-if="tagline" class="tagline">{{ tagline }}</p>
<NavLink
v-if="hasAction"
:item="{ link: data.actionLink, text: data.actionText }"
v-if="frontmatter.actionLink && frontmatter.actionText"
:item="{ link: frontmatter.actionLink, text: frontmatter.actionText }"
class="action"
/>
<NavLink
v-if="hasAltAction"
:item="{ link: data.altActionLink, text: data.altActionText }"
v-if="frontmatter.altActionLink && frontmatter.altActionText"
:item="{
link: frontmatter.altActionLink,
text: frontmatter.altActionText
}"
class="action alt"
/>
</header>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useSiteDataByRoute, useFrontmatter } from 'vitepress'
import NavLink from './NavLink.vue'
const site = useSiteDataByRoute()
const data = useFrontmatter()
const showHero = computed(() => {
return (
data.value.heroImage ||
hasHeroText.value ||
hasTagline.value ||
hasAction.value
)
})
const hasHeroText = computed(() => data.value.heroText !== null)
const heroText = computed(() => data.value.heroText || site.value.title)
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 hasAltAction = computed(
() => data.value.altActionLink && data.value.altActionText
)
</script>
<style scoped>
.home-hero {
margin: 2.5rem 0 2.75rem;
@ -102,7 +95,7 @@ const hasAltAction = computed(
}
}
.description {
.tagline {
margin: 0;
margin-top: 0.25rem;
line-height: 1.3;
@ -111,7 +104,7 @@ const hasAltAction = computed(
}
@media (min-width: 420px) {
.description {
.tagline {
line-height: 1.2;
font-size: 1.6rem;
}

@ -1,25 +1,17 @@
<template>
<p v-if="hasLastUpdated" class="last-updated">
<span class="prefix">{{ prefix }}:</span>
<span class="datetime">{{ datetime }}</span>
</p>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useSiteDataByRoute, usePageData } from 'vitepress'
import { useData } from 'vitepress'
const site = useSiteDataByRoute()
const page = usePageData()
const { theme, page } = useData()
const hasLastUpdated = computed(() => {
const lu = site.value.themeConfig.lastUpdated
const lu = theme.value.lastUpdated
return lu !== undefined && lu !== false
})
const prefix = computed(() => {
const p = site.value.themeConfig.lastUpdated
const p = theme.value.lastUpdated
return p === true ? 'Last Updated' : p
})
@ -31,6 +23,13 @@ onMounted(() => {
})
</script>
<template>
<p v-if="hasLastUpdated" class="last-updated">
<span class="prefix">{{ prefix }}:</span>
<span class="datetime">{{ datetime }}</span>
</p>
</template>
<style scoped>
.last-updated {
display: inline-block;

@ -1,3 +1,11 @@
<script setup lang="ts">
import NavBarTitle from './NavBarTitle.vue'
import NavLinks from './NavLinks.vue'
import ToggleSideBarButton from './ToggleSideBarButton.vue'
defineEmits(['toggle'])
</script>
<template>
<header class="nav-bar">
<ToggleSideBarButton @toggle="$emit('toggle')" />
@ -14,15 +22,6 @@
</header>
</template>
<script setup lang="ts">
import { defineEmit } from 'vue'
import NavBarTitle from './NavBarTitle.vue'
import NavLinks from './NavLinks.vue'
import ToggleSideBarButton from './ToggleSideBarButton.vue'
defineEmit(['toggle'])
</script>
<style scoped>
.nav-bar {
position: fixed;

@ -1,16 +1,21 @@
<script setup lang="ts">
import { withBase, useData } from 'vitepress'
const { site, theme, localePath } = useData()
</script>
<template>
<a
class="nav-bar-title"
:href="$withBase($localePath)"
:aria-label="`${$siteByRoute.title}, back to home`"
:href="localePath"
:aria-label="`${site.title}, back to home`"
>
<img
v-if="$themeConfig.logo"
v-if="theme.logo"
class="logo"
:src="$withBase($themeConfig.logo)"
:src="withBase(theme.logo)"
alt="Logo"
/>
{{ $site.title }}
{{ site.title }}
</a>
</template>

@ -1,20 +1,5 @@
<template>
<div class="nav-dropdown-link" :class="{ open }">
<button class="button" :aria-label="item.ariaLabel" @click="toggle">
<span class="button-text">{{ item.text }}</span>
<span class="button-arrow" :class="open ? 'down' : 'right'" />
</button>
<ul class="dialog">
<li v-for="item in item.items" :key="item.text" class="dialog-item">
<NavDropdownLinkItem :item="item" />
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue'
import { ref, watch } from 'vue'
import { useRoute } from 'vitepress'
import type { DefaultTheme } from '../config'
import NavDropdownLinkItem from './NavDropdownLinkItem.vue'
@ -39,6 +24,21 @@ function toggle() {
}
</script>
<template>
<div class="nav-dropdown-link" :class="{ open }">
<button class="button" :aria-label="item.ariaLabel" @click="toggle">
<span class="button-text">{{ item.text }}</span>
<span class="button-arrow" :class="open ? 'down' : 'right'" />
</button>
<ul class="dialog">
<li v-for="item in item.items" :key="item.text" class="dialog-item">
<NavDropdownLinkItem :item="item" />
</li>
</ul>
</div>
</template>
<style scoped>
.nav-dropdown-link {
position: relative;

@ -1,15 +1,5 @@
<template>
<div class="nav-dropdown-link-item">
<a class="item" v-bind="linkProps">
<span class="arrow" />
<span class="text">{{ item.text }}</span>
<span class="icon"><OutboundLink v-if="isExternal" /></span>
</a>
</div>
</template>
<script setup lang="ts">
import { defineProps, toRefs } from 'vue'
import { toRefs } from 'vue'
import type { DefaultTheme } from '../config'
import { useNavLink } from '../composables/navLink'
import OutboundLink from './icons/OutboundLink.vue'
@ -23,6 +13,16 @@ const propsRefs = toRefs(props)
const { props: linkProps, isExternal } = useNavLink(propsRefs.item)
</script>
<template>
<div class="nav-dropdown-link-item">
<a class="item" v-bind="linkProps">
<span class="arrow" />
<span class="text">{{ item.text }}</span>
<span class="icon"><OutboundLink v-if="isExternal" /></span>
</a>
</div>
</template>
<style scoped>
.item {
display: block;

@ -1,13 +1,5 @@
<template>
<div class="nav-link">
<a class="item" v-bind="linkProps">
{{ item.text }} <OutboundLink v-if="isExternal" />
</a>
</div>
</template>
<script setup lang="ts">
import { defineProps, toRefs } from 'vue'
import { toRefs } from 'vue'
import type { DefaultTheme } from '../config'
import { useNavLink } from '../composables/navLink'
import OutboundLink from './icons/OutboundLink.vue'
@ -21,6 +13,14 @@ const propsRefs = toRefs(props)
const { props: linkProps, isExternal } = useNavLink(propsRefs.item)
</script>
<template>
<div class="nav-link">
<a class="item" v-bind="linkProps">
{{ item.text }} <OutboundLink v-if="isExternal" />
</a>
</div>
</template>
<style scoped>
.item {
display: block;

@ -1,7 +1,21 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from 'vitepress'
import { useLanguageLinks } from '../composables/nav'
import { useRepo } from '../composables/repo'
import NavLink from './NavLink.vue'
import NavDropdownLink from './NavDropdownLink.vue'
const { theme } = useData()
const localeLinks = useLanguageLinks()
const repo = useRepo()
const show = computed(() => theme.value.nav || repo.value || localeLinks.value)
</script>
<template>
<nav v-if="show" class="nav-links">
<template v-if="links">
<div v-for="item in links" :key="item.text" class="item">
<template v-if="theme.nav">
<div v-for="item in theme.nav" :key="item.text" class="item">
<NavDropdownLink v-if="item.items" :item="item" />
<NavLink v-else :item="item" />
</div>
@ -17,23 +31,6 @@
</nav>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useSiteDataByRoute } from 'vitepress'
import { useLocaleLinks } from '../composables/nav'
import { useRepo } from '../composables/repo'
import NavLink from './NavLink.vue'
import NavDropdownLink from './NavDropdownLink.vue'
const site = useSiteDataByRoute()
const localeLinks = useLocaleLinks()
const repo = useRepo()
const show = computed(() => links.value || repo.value)
const links = computed(() => site.value.themeConfig.nav)
</script>
<style scoped>
.nav-links {
padding: 0.75rem 0;

@ -1,14 +1,23 @@
<script setup lang="ts">
import { withBase } from 'vitepress'
import { useNextAndPrevLinks } from '../composables/nextAndPrevLinks'
import ArrowLeft from './icons/ArrowLeft.vue'
import ArrowRight from './icons/ArrowRight.vue'
const { hasLinks, prev, next } = useNextAndPrevLinks()
</script>
<template>
<div v-if="hasLinks" class="next-and-prev-link">
<div class="container">
<div class="prev">
<a v-if="prev" class="link" :href="$withBase(prev.link)">
<a v-if="prev" class="link" :href="withBase(prev.link)">
<ArrowLeft class="icon icon-prev" />
<span class="text">{{ prev.text }}</span>
</a>
</div>
<div class="next">
<a v-if="next" class="link" :href="$withBase(next.link)">
<a v-if="next" class="link" :href="withBase(next.link)">
<span class="text">{{ next.text }}</span>
<ArrowRight class="icon icon-next" />
</a>
@ -17,14 +26,6 @@
</div>
</template>
<script setup lang="ts">
import { useNextAndPrevLinks } from '../composables/nextAndPrevLinks'
import ArrowLeft from './icons/ArrowLeft.vue'
import ArrowRight from './icons/ArrowRight.vue'
const { hasLinks, prev, next } = useNextAndPrevLinks()
</script>
<style scoped>
.next-and-prev-link {
padding-top: 1rem;

@ -1,14 +1,15 @@
<script setup lang="ts">
import PageFooter from './PageFooter.vue'
import NextAndPrevLinks from './NextAndPrevLinks.vue'
</script>
<template>
<main class="page">
<div class="container">
<slot name="top" />
<div class="content">
<Content />
</div>
<Content class="content" />
<PageFooter />
<NextAndPrevLinks />
<slot name="bottom" />
@ -16,11 +17,6 @@
</main>
</template>
<script setup lang="ts">
import PageFooter from './PageFooter.vue'
import NextAndPrevLinks from './NextAndPrevLinks.vue'
</script>
<style scoped>
.page {
padding-top: var(--header-height);

@ -1,3 +1,8 @@
<script setup lang="ts">
import EditLink from './EditLink.vue'
import LastUpdated from './LastUpdated.vue'
</script>
<template>
<footer class="page-footer">
<div class="edit">
@ -9,11 +14,6 @@
</footer>
</template>
<script setup lang="ts">
import EditLink from './EditLink.vue'
import LastUpdated from './LastUpdated.vue'
</script>
<style scoped>
.page-footer {
padding-top: 1rem;

@ -1,3 +1,10 @@
<script setup lang="ts">
import NavLinks from './NavLinks.vue'
import SideBarLinks from './SideBarLinks.vue'
defineProps<{ open: boolean }>()
</script>
<template>
<aside class="sidebar" :class="{ open }">
<NavLinks class="nav" />
@ -10,16 +17,6 @@
</aside>
</template>
<script setup lang="ts">
import { defineProps } from 'vue'
import NavLinks from './NavLinks.vue'
import SideBarLinks from './SideBarLinks.vue'
defineProps({
open: { type: Boolean, required: true }
})
</script>
<style scoped>
.sidebar {
position: fixed;

@ -1,6 +1,6 @@
import { FunctionalComponent, h, VNode } from 'vue'
import { useRoute, useSiteData } from 'vitepress'
import { Header } from '/@types/shared'
import { useRoute, useData } from 'vitepress'
import { Header } from '../../shared'
import { DefaultTheme } from '../config'
import { joinUrl, isActive } from '../utils'
@ -10,16 +10,22 @@ interface HeaderWithChildren extends Header {
export const SideBarLink: FunctionalComponent<{
item: DefaultTheme.SideBarItem
depth?: number
}> = (props) => {
const route = useRoute()
const site = useSiteData()
const { site, frontmatter } = useData()
const depth = props.depth || 1
const maxDepth = frontmatter.value.sidebarDepth || Infinity
const headers = route.data.headers
const text = props.item.text
const link = resolveLink(site.value.base, props.item.link)
const children = (props.item as DefaultTheme.SideBarGroup).children
const active = isActive(route, props.item.link)
const childItems = createChildren(active, children, headers)
const childItems =
depth < maxDepth
? createChildren(active, children, headers, depth + 1)
: null
return h('li', { class: 'sidebar-link' }, [
h(
@ -50,20 +56,21 @@ function resolveLink(base: string, path?: string): string | undefined {
function createChildren(
active: boolean,
children?: DefaultTheme.SideBarItem[],
headers?: Header[]
headers?: Header[],
depth = 1
): VNode | null {
if (children && children.length > 0) {
return h(
'ul',
{ class: 'sidebar-links' },
children.map((c) => {
return h(SideBarLink, { item: c })
return h(SideBarLink, { item: c, depth })
})
)
}
return active && headers
? createChildren(false, resolveHeaders(headers))
? createChildren(false, resolveHeaders(headers), undefined, depth)
: null
}

@ -1,12 +1,12 @@
<template>
<ul v-if="items.length > 0" class="sidebar-links">
<SideBarLink v-for="item of items" :key="item.text" :item="item" />
</ul>
</template>
<script setup lang="ts">
import { useSideBar } from '../composables/sideBar'
import { SideBarLink } from './SideBarLink'
const items = useSideBar()
</script>
<template>
<ul v-if="items.length > 0" class="sidebar-links">
<SideBarLink v-for="item of items" :item="item" />
</ul>
</template>

@ -1,3 +1,9 @@
<script lang="ts">
export default {
emits: ['toggle']
}
</script>
<template>
<div class="sidebar-button" @click="$emit('toggle')">
<svg
@ -16,12 +22,6 @@
</div>
</template>
<script>
export default {
emits: ['toggle']
}
</script>
<style>
.sidebar-button {
position: absolute;

@ -1,4 +1,4 @@
<template functional>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M19,11H7.4l5.3-5.3c0.4-0.4,0.4-1,0-1.4s-1-0.4-1.4,0l-7,7c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.2-0.1,0.5,0,0.8c0.1,0.1,0.1,0.2,0.2,0.3l7,7c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3c0.4-0.4,0.4-1,0-1.4L7.4,13H19c0.6,0,1-0.4,1-1S19.6,11,19,11z"

@ -1,4 +1,4 @@
<template functional>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M19.9,12.4c0.1-0.2,0.1-0.5,0-0.8c-0.1-0.1-0.1-0.2-0.2-0.3l-7-7c-0.4-0.4-1-0.4-1.4,0s-0.4,1,0,1.4l5.3,5.3H5c-0.6,0-1,0.4-1,1s0.4,1,1,1h11.6l-5.3,5.3c-0.4,0.4-0.4,1,0,1.4c0.2,0.2,0.5,0.3,0.7,0.3s0.5-0.1,0.7-0.3l7-7C19.8,12.6,19.9,12.5,19.9,12.4z"

@ -1,4 +1,4 @@
<template functional>
<template>
<svg
class="icon outbound"
xmlns="http://www.w3.org/2000/svg"

@ -1,25 +1,25 @@
import { computed } from 'vue'
import { useSiteDataByRoute, usePageData } from 'vitepress'
import { endingSlashRE, isNullish, isExternal } from '../utils'
import { useData } from 'vitepress'
import { endingSlashRE, isExternal } from '../utils'
const bitbucketRE = /bitbucket.org/
export function useEditLink() {
const site = useSiteDataByRoute()
const page = usePageData()
const { page, theme, frontmatter } = useData()
const url = computed(() => {
const showEditLink = isNullish(page.value.frontmatter.editLink)
? site.value.themeConfig.editLinks
: page.value.frontmatter.editLink
const {
repo,
docsDir = '',
docsBranch = 'master',
docsRepo = repo
} = site.value.themeConfig
docsRepo = repo,
editLinks
} = theme.value
const showEditLink =
frontmatter.value.editLink != null
? frontmatter.value.editLink
: editLinks
const { relativePath } = page.value
if (!showEditLink || !relativePath || !repo) {
@ -30,7 +30,7 @@ export function useEditLink() {
})
const text = computed(() => {
return site.value.themeConfig.editLinkText || 'Edit this page'
return theme.value.editLinkText || 'Edit this page'
})
return {

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

@ -1,12 +1,10 @@
import { computed, Ref } from 'vue'
import { useRoute } from 'vitepress'
import type { DefaultTheme } from '../config'
import { useRoute, withBase } from 'vitepress'
import { isExternal as isExternalCheck } from '../utils'
import { useUrl } from '../composables/url'
import type { DefaultTheme } from '../config'
export function useNavLink(item: Ref<DefaultTheme.NavItemWithLink>) {
const route = useRoute()
const { withBase } = useUrl()
const isExternal = isExternalCheck(item.value.link)
@ -30,8 +28,8 @@ export function useNavLink(item: Ref<DefaultTheme.NavItemWithLink>) {
isExternal
},
href: isExternal ? item.value.link : withBase(item.value.link),
target: item.value.target || isExternal ? `_blank` : null,
rel: item.value.rel || isExternal ? `noopener noreferrer` : null,
target: item.value.target || (isExternal ? `_blank` : null),
rel: item.value.rel || (isExternal ? `noopener noreferrer` : null),
'aria-label': item.value.ariaLabel
}
})

@ -1,18 +1,17 @@
import { computed } from 'vue'
import { useSiteDataByRoute, usePageData } from 'vitepress'
import { useData } from 'vitepress'
import { isArray, ensureStartingSlash, removeExtention } from '../utils'
import { getSideBarConfig, getFlatSideBarLinks } from '../support/sideBar'
export function useNextAndPrevLinks() {
const site = useSiteDataByRoute()
const page = usePageData()
const { page, theme } = useData()
const path = computed(() => {
return removeExtention(ensureStartingSlash(page.value.relativePath))
})
const candidates = computed(() => {
const config = getSideBarConfig(site.value.themeConfig.sidebar, path.value)
const config = getSideBarConfig(theme.value.sidebar, path.value)
return isArray(config) ? getFlatSideBarLinks(config) : []
})
@ -25,7 +24,7 @@ export function useNextAndPrevLinks() {
const next = computed(() => {
if (
site.value.themeConfig.nextLinks !== false &&
theme.value.nextLinks !== false &&
index.value > -1 &&
index.value < candidates.value.length - 1
) {
@ -34,7 +33,7 @@ export function useNextAndPrevLinks() {
})
const prev = computed(() => {
if (site.value.themeConfig.prevLinks !== false && index.value > 0) {
if (theme.value.prevLinks !== false && index.value > 0) {
return candidates.value[index.value - 1]
}
})

@ -1,13 +1,14 @@
import { computed } from 'vue'
import { useSiteDataByRoute } from 'vitepress'
import { useData } from 'vitepress'
import type { DefaultTheme } from '../config'
import { EXTERNAL_URL_RE } from '../../shared'
export const platforms = ['GitHub', 'GitLab', 'Bitbucket'].map((platform) => {
return [platform, new RegExp(platform, 'i')] as const
})
export function useRepo() {
const site = useSiteDataByRoute()
const { site } = useData()
return computed(() => {
const theme = site.value.themeConfig as DefaultTheme.Config
@ -26,7 +27,7 @@ export function useRepo() {
function getRepoUrl(repo: string): string {
// if the full url is not provided, default to GitHub repo
return /^https?:/.test(repo) ? repo : `https://github.com/${repo}`
return EXTERNAL_URL_RE.test(repo) ? repo : `https://github.com/${repo}`
}
function getRepoText(url: string, text?: string): string {

@ -1,13 +1,13 @@
import { computed } from 'vue'
import { useRoute, useSiteDataByRoute } from 'vitepress'
import { Header } from '/@types/shared'
import { useRoute, useData } from 'vitepress'
import { Header } from '../../shared'
import { useActiveSidebarLinks } from '../composables/activeSidebarLink'
import { getSideBarConfig } from '../support/sideBar'
import { DefaultTheme } from '../config'
export function useSideBar() {
const route = useRoute()
const site = useSiteDataByRoute()
const { site } = useData()
useActiveSidebarLinks()

@ -1,13 +0,0 @@
import { useSiteData, joinPath } from 'vitepress'
export function useUrl() {
const site = useSiteData()
function withBase(path: string): string {
return joinPath(site.value.base, path)
}
return {
withBase
}
}

@ -26,6 +26,7 @@ div[class*='language-'] {
li > div[class*='language-'] {
border-radius: 6px 0 0 6px;
margin: 1rem -1.5rem 1rem -1.25rem;
line-height: initial;
}
@media (min-width: 420px) {
@ -116,7 +117,7 @@ div[class*='language-'].line-numbers-mode {
/* Language marker */
[class*='language-']:before {
div[class*='language-']:before {
position: absolute;
top: 0.6em;
right: 1em;
@ -125,93 +126,97 @@ div[class*='language-'].line-numbers-mode {
color: #888;
}
[class~='language-html']:before,
[class~='language-markup']:before {
div[class~='language-html']:before,
div[class~='language-markup']:before {
content: 'html';
}
[class~='language-md']:before,
[class~='language-markdown']:before {
div[class~='language-md']:before,
div[class~='language-markdown']:before {
content: 'md';
}
[class~='language-css']:before {
div[class~='language-css']:before {
content: 'css';
}
[class~='language-sass']:before {
div[class~='language-sass']:before {
content: 'sass';
}
[class~='language-scss']:before {
div[class~='language-scss']:before {
content: 'scss';
}
[class~='language-less']:before {
div[class~='language-less']:before {
content: 'less';
}
[class~='language-stylus']:before {
div[class~='language-stylus']:before {
content: 'styl';
}
[class~='language-js']:before,
[class~='language-javascript']:before {
div[class~='language-js']:before,
div[class~='language-javascript']:before {
content: 'js';
}
[class~='language-ts']:before,
[class~='language-typescript']:before {
div[class~='language-ts']:before,
div[class~='language-typescript']:before {
content: 'ts';
}
[class~='language-json']:before {
div[class~='language-json']:before {
content: 'json';
}
[class~='language-rb']:before,
[class~='language-ruby']:before {
div[class~='language-rb']:before,
div[class~='language-ruby']:before {
content: 'rb';
}
[class~='language-py']:before,
[class~='language-python']:before {
div[class~='language-py']:before,
div[class~='language-python']:before {
content: 'py';
}
[class~='language-sh']:before,
[class~='language-bash']:before {
div[class~='language-sh']:before,
div[class~='language-bash']:before {
content: 'sh';
}
[class~='language-php']:before {
div[class~='language-php']:before {
content: 'php';
}
[class~='language-go']:before {
div[class~='language-go']:before {
content: 'go';
}
[class~='language-rust']:before {
div[class~='language-rust']:before {
content: 'rust';
}
[class~='language-java']:before {
div[class~='language-java']:before {
content: 'java';
}
[class~='language-c']:before {
div[class~='language-c']:before {
content: 'c';
}
[class~='language-yaml']:before {
div[class~='language-yaml']:before {
content: 'yaml';
}
[class~='language-dockerfile']:before {
div[class~='language-dockerfile']:before {
content: 'dockerfile';
}
div[class~='language-vue']:before {
content: 'vue';
}
/**
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML.
* Based on https://github.com/chriskempson/tomorrow-theme

@ -1,4 +1,5 @@
.custom-block.tip,
.custom-block.info,
.custom-block.warning,
.custom-block.danger {
margin: 1rem 0;
@ -12,6 +13,11 @@
border-color: var(--c-brand);
}
.custom-block.info {
background-color: #f3f5f7;
border-color: var(--c-text-light-2);
}
.custom-block.warning {
border-color: #e7c000;
color: #6b5900;

@ -8,8 +8,6 @@
"lib": ["ESNext", "DOM"],
"types": ["vite/client"],
"paths": {
"/@shared/*": ["shared/*"],
"/@types/*": ["../../types/*"],
"/@theme/*": ["theme-default/*"],
"vitepress": ["index.ts"]
}

@ -1,14 +1,11 @@
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'
)
export const DIST_CLIENT_PATH = path.join(__dirname, '../client')
export const APP_PATH = path.join(DIST_CLIENT_PATH, 'app')
export const SHARED_PATH = path.join(DIST_CLIENT_PATH, 'shared')
export const DEFAULT_THEME_PATH = path.join(DIST_CLIENT_PATH, 'theme-default')
// special virtual file
// we can't directly import '/@siteData' because
@ -17,12 +14,8 @@ export const DEFAULT_THEME_PATH = path.join(
export const SITE_DATA_ID = '@siteData'
export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
export function resolveAliases(
themeDir: string,
userConfig: UserConfig
): AliasOptions {
export function resolveAliases(themeDir: string): AliasOptions {
const paths: Record<string, string> = {
...userConfig.alias,
'/@theme': themeDir,
'/@shared': SHARED_PATH,
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH

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

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

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

@ -1,18 +1,18 @@
import path from 'path'
import fs from 'fs-extra'
import { SiteConfig, resolveSiteDataByRoute } from '../config'
import { HeadConfig } from '../../../types/shared'
import { HeadConfig } from '../shared'
import { normalizePath } from 'vite'
import { RollupOutput, OutputChunk, OutputAsset } from 'rollup'
const escape = require('escape-html')
import { slash } from '../utils/slash'
import escape from 'escape-html'
export async function renderPage(
config: SiteConfig,
page: string, // foo.md
result: RollupOutput,
appChunk: OutputChunk,
cssChunk: OutputAsset,
result: RollupOutput | null,
appChunk: OutputChunk | undefined,
cssChunk: OutputAsset | undefined,
pageToHashMap: Record<string, string>,
hashMapString: string
) {
@ -22,7 +22,7 @@ export async function renderPage(
const siteData = resolveSiteDataByRoute(config.site, routePath)
router.go(routePath)
// lazy require server-renderer for production build
const content = await require('@vue/server-renderer').renderToString(app)
const content = await require('vue/server-renderer').renderToString(app)
const pageName = page.replace(/\//g, '_')
// server build doesn't need hash
@ -38,16 +38,23 @@ export async function renderPage(
pageServerJsFileName
))
const pageData = JSON.parse(__pageData)
const frontmatterHead = pageData.frontmatter.head
const preloadLinks = [
// resolve imports for index.js + page.md.js and inject script tags for
// them as well so we fetch everything as early as possible without having
// to wait for entry chunks to parse
...resolvePageImports(config, page, result, appChunk),
pageClientJsFileName,
appChunk.fileName
]
const preloadLinks = (
config.mpa
? appChunk
? [appChunk.fileName]
: []
: result && appChunk
? [
// resolve imports for index.js + page.md.js and inject script tags for
// them as well so we fetch everything as early as possible without having
// to wait for entry chunks to parse
...resolvePageImports(config, page, result, appChunk),
pageClientJsFileName,
appChunk.fileName
]
: []
)
.map((file) => {
return `<link rel="modulepreload" href="${siteData.base}${file}">`
})
@ -57,29 +64,61 @@ export async function renderPage(
? `<link rel="stylesheet" href="${siteData.base}${cssChunk.fileName}">`
: ''
const title: string =
pageData.title && pageData.title !== 'Home'
? `${pageData.title} | ${siteData.title}`
: siteData.title
const head = addSocialTags(
title,
...siteData.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 = `
<!DOCTYPE html>
<html lang="${siteData.lang}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>
${pageData.title ? pageData.title + ` | ` : ``}${siteData.title}
</title>
<title>${title}</title>
<meta name="description" content="${
pageData.description || siteData.description
}">
${stylesheetLink}
${preloadLinks}
${renderHead(siteData.head)}
${renderHead(frontmatterHead && filterOutHeadDescription(frontmatterHead))}
${renderHead(head)}
</head>
<body>
<div id="app">${content}</div>
<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>
<script type="module" async src="${siteData.base}${
appChunk.fileName
}"></script>
${
config.mpa
? ''
: `<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>`
}
${
appChunk
? `<script type="module" async src="${siteData.base}${appChunk.fileName}"></script>`
: ``
}
${inlinedScript}
</body>
</html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
@ -96,7 +135,7 @@ function resolvePageImports(
// 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 = normalizePath(
fs.realpathSync(path.resolve(config.root, page))
fs.realpathSync(path.resolve(config.srcDir, page))
)
const pageChunk = result.output.find(
(chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
@ -112,9 +151,6 @@ function resolvePageImports(
}
function renderHead(head: HeadConfig[]) {
if (!head || !head.length) {
return ''
}
return head
.map(([tag, attrs = {}, innerHTML = '']) => {
const openTag = `<${tag}${renderAttrs(attrs)}>`
@ -136,13 +172,27 @@ function renderAttrs(attrs: Record<string, string>): string {
}
function isMetaDescription(headConfig: HeadConfig) {
return (
headConfig[0] === 'meta' &&
headConfig[1] &&
headConfig[1].name === 'description'
)
const [type, attrs] = headConfig
return type === 'meta' && attrs?.name === 'description'
}
function filterOutHeadDescription(head: HeadConfig[] | undefined) {
return head ? head.filter((h) => !isMetaDescription(h)) : []
}
function hasTag(head: HeadConfig[], tag: HeadConfig) {
const [tagType, tagAttrs] = tag
const [attr, value] = Object.entries(tagAttrs)[0] // First key
return head.some(([type, attrs]) => type === tagType && attrs[attr] === value)
}
function filterOutHeadDescription(head: HeadConfig[]) {
return head.filter((h) => !isMetaDescription(h))
function addSocialTags(title: string, ...head: HeadConfig[]) {
const tags: HeadConfig[] = [
['meta', { name: 'twitter:title', content: title }],
['meta', { property: 'og:title', content: title }]
]
tags.filter((tagAttrs) => {
if (!hasTag(head, tagAttrs)) head.push(tagAttrs)
})
return head
}

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

@ -2,16 +2,31 @@ import path from 'path'
import fs from 'fs-extra'
import chalk from 'chalk'
import globby from 'globby'
import { AliasOptions } from 'vite'
import {
normalizePath,
AliasOptions,
UserConfig as ViteConfig,
mergeConfig as mergeViteConfig
} from 'vite'
import { Options as VuePluginOptions } from '@vitejs/plugin-vue'
import { SiteData, HeadConfig, LocaleConfig } from '../../types/shared'
export { resolveSiteDataByRoute } from './shared/config'
import {
SiteData,
HeadConfig,
LocaleConfig,
createLangDictionary
} from './shared'
import { resolveAliases, APP_PATH, DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug'
const debug = require('debug')('vitepress:config')
export { resolveSiteDataByRoute } from './shared'
const debug = _debug('vitepress:config')
export type { MarkdownOptions }
export interface UserConfig<ThemeConfig = any> {
extends?: RawConfigExports
lang?: string
base?: string
title?: string
@ -19,14 +34,34 @@ export interface UserConfig<ThemeConfig = any> {
head?: HeadConfig[]
themeConfig?: ThemeConfig
locales?: Record<string, LocaleConfig>
alias?: Record<string, string>
markdown?: MarkdownOptions
customData?: any
vueOptions?: VuePluginOptions
/**
* Opitons to pass on to `@vitejs/plugin-vue`
*/
vue?: VuePluginOptions
/**
* Vite config
*/
vite?: ViteConfig
srcDir?: string
srcExclude?: string[]
/**
* Enable MPA / zero-JS mode
* @experimental
*/
mpa?: boolean
}
export type RawConfigExports =
| UserConfig
| Promise<UserConfig>
| (() => UserConfig | Promise<UserConfig>)
export interface SiteConfig<ThemeConfig = any> {
root: string
srcDir: string
site: SiteData<ThemeConfig>
configPath: string
themeDir: string
@ -34,18 +69,21 @@ export interface SiteConfig<ThemeConfig = any> {
tempDir: string
alias: AliasOptions
pages: string[]
markdown?: MarkdownOptions
vueOptions?: VuePluginOptions
markdown: MarkdownOptions | undefined
vue: VuePluginOptions | undefined
vite: ViteConfig | undefined
mpa: boolean
}
const resolve = (root: string, file: string) =>
path.resolve(root, `.vitepress`, file)
normalizePath(path.resolve(root, `.vitepress`, file))
export async function resolveConfig(
root: string = process.cwd()
): Promise<SiteConfig> {
const userConfig = await resolveUserConfig(root)
const site = await resolveSiteData(root)
const site = await resolveSiteData(root, userConfig)
const srcDir = path.resolve(root, userConfig.srcDir || '.')
// resolve theme path
const userThemeDir = resolve(root, 'theme')
@ -53,41 +91,98 @@ export async function resolveConfig(
? userThemeDir
: DEFAULT_THEME_PATH
// Important: globby/fast-glob doesn't guarantee order of the returned files.
// We must sort the pages so the input list to rollup is stable across
// builds - otherwise different input order could result in different exports
// order in shared chunks which in turns invalidates the hash of every chunk!
// JavaScript built-in sort() is mandated to be stable as of ES2019 and
// supported in Node 12+, which is required by Vite.
const pages = (
await globby(['**.md'], {
cwd: srcDir,
ignore: ['**/node_modules', ...(userConfig.srcExclude || [])]
})
).sort()
const config: SiteConfig = {
root,
srcDir,
site,
themeDir,
pages: await globby(['**.md'], { cwd: root, ignore: ['node_modules'] }),
pages,
configPath: resolve(root, 'config.js'),
outDir: resolve(root, 'dist'),
tempDir: path.resolve(APP_PATH, 'temp'),
markdown: userConfig.markdown,
alias: resolveAliases(themeDir, userConfig),
vueOptions: userConfig.vueOptions
alias: resolveAliases(themeDir),
vue: userConfig.vue,
vite: userConfig.vite,
mpa: !!userConfig.mpa
}
return config
}
export async function resolveUserConfig(root: string) {
export async function resolveUserConfig(root: string): Promise<UserConfig> {
// load user config
const configPath = resolve(root, 'config.js')
const hasUserConfig = await fs.pathExists(configPath)
// always delete cache first before loading config
delete require.cache[configPath]
const userConfig: UserConfig = hasUserConfig ? require(configPath) : {}
delete require.cache[require.resolve(configPath)]
const userConfig: RawConfigExports = hasUserConfig ? require(configPath) : {}
if (hasUserConfig) {
debug(`loaded config at ${chalk.yellow(configPath)}`)
} else {
debug(`no config file found.`)
}
return resolveConfigExtends(userConfig)
}
return 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
}
export async function resolveSiteData(root: string): Promise<SiteData> {
const userConfig = await resolveUserConfig(root)
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
}
function isObject(value: unknown): value is Record<string, any> {
return Object.prototype.toString.call(value) === '[object Object]'
}
export async function resolveSiteData(
root: string,
userConfig?: UserConfig
): Promise<SiteData> {
userConfig = userConfig || (await resolveUserConfig(root))
return {
lang: userConfig.lang || 'en-US',
title: userConfig.title || 'VitePress',
@ -96,6 +191,6 @@ export async function resolveSiteData(root: string): Promise<SiteData> {
head: userConfig.head || [],
themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {},
customData: userConfig.customData || {}
langs: createLangDictionary(userConfig)
}
}

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

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

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

@ -1,10 +1,10 @@
import MarkdownIt from 'markdown-it'
import Token from 'markdown-it/lib/token'
const container = require('markdown-it-container')
import container from 'markdown-it-container'
export const containerPlugin = (md: MarkdownIt) => {
md.use(...createContainer('tip', 'TIP'))
.use(...createContainer('info', 'INFO'))
.use(...createContainer('warning', 'WARNING'))
.use(...createContainer('danger', 'WARNING'))
// explicitly escape Vue syntax

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

@ -24,7 +24,7 @@ export const highlightLinePlugin = (md: MarkdownIt) => {
.map((v) => v.split('-').map((v) => parseInt(v, 10)))
const code = options.highlight
? options.highlight(token.content, langName)
? options.highlight(token.content, langName, '')
: token.content
const rawCode = code.replace(wrapperRE, '')

@ -5,6 +5,7 @@
import MarkdownIt from 'markdown-it'
import { MarkdownParsedData } from '../markdown'
import { URL } from 'url'
import { EXTERNAL_URL_RE } from '../../shared'
const indexRE = /(^|.*\/)index.md(#?.*)$/i
@ -18,7 +19,7 @@ export const linkPlugin = (
if (hrefIndex >= 0) {
const hrefAttr = token.attrs![hrefIndex]
const url = hrefAttr[1]
const isExternal = /^https?:/.test(url)
const isExternal = EXTERNAL_URL_RE.test(url)
if (isExternal) {
Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val)
@ -31,6 +32,13 @@ export const linkPlugin = (
) {
normalizeHref(hrefAttr)
}
// encode vite-specific replace strings in case they appear in URLs
// this also excludes them from build-time replacements (which injects
// <wbr/> and will break URLs)
hrefAttr[1] = hrefAttr[1]
.replace(/\bimport\.meta/g, 'import%2Emeta')
.replace(/\bprocess\.env/g, 'process%2Eenv')
}
return self.renderToken(tokens, idx, options)
}

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

@ -78,7 +78,7 @@ function findRegion(lines: Array<string>, regionName: string) {
return null
}
export const snippetPlugin = (md: MarkdownIt, root: string) => {
export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
const parser: RuleBlock = (state, startLine, endLine, silent) => {
const CH = '<'.charCodeAt(0)
const pos = state.bMarks[startLine] + state.tShift[startLine]
@ -107,12 +107,13 @@ export const snippetPlugin = (md: MarkdownIt, root: string) => {
*
* captures: ['/path/to/file.extension', 'extension', '#region', '{meta}']
*/
const rawPathRegexp = /^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?({\d+(?:[,-]\d+)*}))?$/
const rawPathRegexp =
/^(.+(?:\.([a-z]+)))(?:(#[\w-]+))?(?: ?({\d+(?:[,-]\d+)*}))?$/
const rawPath = state.src
.slice(start, end)
.trim()
.replace(/^@/, root)
.replace(/^@/, srcDir)
.trim()
const [filename = '', extension = '', region = '', meta = ''] = (
rawPathRegexp.exec(rawPath) || []

@ -4,29 +4,48 @@ 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 { PageData, HeadConfig } from './shared'
import { slash } from './utils/slash'
import chalk from 'chalk'
import _debug from 'debug'
const debug = require('debug')('vitepress:md')
const debug = _debug('vitepress:md')
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 })
const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g
interface MarkdownCompileResult {
export interface MarkdownCompileResult {
vueSrc: string
pageData: PageData
deadLinks: string[]
includes: string[]
}
export function createMarkdownToVueRenderFn(
root: string,
srcDir: string,
options: MarkdownOptions = {},
pages: string[]
pages: string[],
userDefines: Record<string, any> | undefined,
isBuild = false
) {
const md = createMarkdownRenderer(root, options)
const md = createMarkdownRenderer(srcDir, options)
pages = pages.map((p) => slash(p.replace(/\.md$/, '')))
return (src: string, file: string): MarkdownCompileResult => {
const relativePath = slash(path.relative(root, file))
const userDefineRegex = userDefines
? new RegExp(
`\\b(${Object.keys(userDefines)
.map((key) => key.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'))
.join('|')})`,
'g'
)
: null
return (
src: string,
file: string,
publicDir: string
): MarkdownCompileResult => {
const relativePath = slash(path.relative(srcDir, file))
const dir = path.dirname(file)
const cached = cache.get(src)
if (cached) {
@ -36,13 +55,33 @@ export function createMarkdownToVueRenderFn(
const start = Date.now()
// resolve includes
let includes: string[] = []
src = src.replace(includesRE, (_, m1) => {
const includePath = path.join(dir, m1)
console.log(includePath)
const content = fs.readFileSync(includePath, 'utf-8')
includes.push(slash(includePath))
return content
})
const { content, data: frontmatter } = matter(src)
let { html, data } = md.render(content)
// avoid env variables being replaced by vite
html = html
.replace(/import\.meta/g, 'import.<wbr/>meta')
.replace(/process\.env/g, 'process.<wbr/>env')
if (isBuild) {
// avoid env variables being replaced by vite
html = html
.replace(/\bimport\.meta/g, 'import.<wbr/>meta')
.replace(/\bprocess\.env/g, 'process.<wbr/>env')
// also avoid replacing vite user defines
if (userDefineRegex) {
html = html.replace(
userDefineRegex,
(_) => `${_[0]}<wbr/>${_.slice(1)}`
)
}
}
// validate data.links
const deadLinks = []
@ -54,9 +93,12 @@ export function createMarkdownToVueRenderFn(
const resolved = slash(
url.startsWith('/')
? url.slice(1)
: path.relative(root, path.resolve(dir, url))
: path.relative(srcDir, path.resolve(dir, url))
)
if (!pages.includes(resolved)) {
if (
!pages.includes(resolved) &&
!fs.existsSync(path.resolve(dir, publicDir, `${resolved}.html`))
) {
console.warn(
chalk.yellow(
`\n(!) Found dead link ${chalk.cyan(
@ -88,7 +130,8 @@ export function createMarkdownToVueRenderFn(
const result = {
vueSrc,
pageData,
deadLinks
deadLinks,
includes
}
cache.set(src, result)
return result
@ -97,6 +140,7 @@ export function createMarkdownToVueRenderFn(
const scriptRE = /<\/script>/
const scriptSetupRE = /<\s*script[^>]*\bsetup\b[^>]*/
const scriptClientRe = /<\s*script[^>]*\bclient\b[^>]*/
const defaultExportRE = /((?:^|\n|;)\s*)export(\s*)default/
const namedDefaultExportRE = /((?:^|\n|;)\s*)export(.+)as(\s*)default/
@ -106,7 +150,11 @@ function genPageDataCode(tags: string[], data: PageData) {
)}`
const existingScriptIndex = tags.findIndex((tag) => {
return scriptRE.test(tag) && !scriptSetupRE.test(tag)
return (
scriptRE.test(tag) &&
!scriptSetupRE.test(tag) &&
!scriptClientRe.test(tag)
)
})
if (existingScriptIndex > -1) {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save