Merge branch 'main' into chore/docs-add-externalIcon

pull/2572/head
Divyansh Singh 3 years ago committed by GitHub
commit e7a5b0c258
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,6 +14,7 @@ concurrency:
jobs:
action:
if: github.repository == 'vuejs/vitepress'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4

@ -7,6 +7,7 @@ on:
jobs:
release:
if: github.repository == 'vuejs/vitepress'
runs-on: ubuntu-latest
steps:

@ -1,3 +1,9 @@
# [1.0.0-beta.5](https://github.com/vuejs/vitepress/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2023-07-03)
### Bug Fixes
- **types:** `Sidebar` was exported multiple times breaking the config ([#2573](https://github.com/vuejs/vitepress/issues/2573)) ([a99dcf9](https://github.com/vuejs/vitepress/commit/a99dcf94436d6cbbd53ef5481a6ec5ffd8d887d2))
# [1.0.0-beta.4](https://github.com/vuejs/vitepress/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2023-07-02)
### Bug Fixes

@ -65,7 +65,7 @@ describe('Table of Contents', () => {
test('render toc', async () => {
const items = page.locator('#table-of-contents + nav ul li')
const count = await items.count()
expect(count).toBe(33)
expect(count).toBe(35)
})
})
@ -242,6 +242,15 @@ describe('Markdown File Inclusion', () => {
expect(await h1.getAttribute('id')).toBe('foo-1')
})
test('render markdown using nested inclusion inside sub folder', async () => {
const h1 = page.locator('#after-foo + h1')
expect(await h1.getAttribute('id')).toBe('inside-sub-folder')
const h2 = page.locator('#after-foo + h1 + h2')
expect(await h2.getAttribute('id')).toBe('sub-sub')
const h3 = page.locator('#after-foo + h1 + h2 + h3')
expect(await h3.getAttribute('id')).toBe('sub-sub-sub')
})
test('support selecting range', async () => {
const h2 = page.locator('#markdown-file-inclusion-with-range + h2')
expect(trim(await h2.textContent())).toBe('Region')

@ -1,3 +1,5 @@
<!--@include: ./foo.md-->
### After Foo
<!--@include: ./subfolder/inside-subfolder.md-->

@ -0,0 +1,3 @@
# Inside sub folder
<!--@include: ./subsub/subsub.md-->

@ -0,0 +1,3 @@
## Sub sub
<!--@include: ./subsubsub/subsubsub.md-->

@ -45,7 +45,7 @@ The following guides are based on some shared assumptions:
}
```
Now the `docs:preview` method will launch the server at `http://localhost:8080`.
Now the `docs:preview` method will launch the server at `http://localhost:8080`.
## Setting a Public Base Path
@ -65,7 +65,7 @@ This `4f283b18` hash is generated from the content of this file. The same hashed
Cache-Control: max-age=31536000,immutable
```
:::details Example Netlify `_headers` file
::: details Example Netlify `_headers` file
```
/assets/*
@ -79,7 +79,7 @@ Note: the `_headers` file should be placed in the [public directory](/guide/asse
:::
:::details Example Vercel config in `vercel.json`
::: details Example Vercel config in `vercel.json`
```json
{
@ -119,66 +119,87 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
### GitHub Pages
1. In your theme config file, `docs/.vitepress/config.js`, set the `base` property to the name of your GitHub repository. If you plan to deploy your site to `https://foo.github.io/bar/`, then you should set base to `'/bar/'`. It should always start and end with a slash.
2. Create a file named `deploy.yml` inside `.github/workflows` directory of your project with the following content:
1. Create a file named `deploy.yml` inside `.github/workflows` directory of your project with some content like this:
```yaml
name: Deploy
# Sample workflow for building and deploying a VitePress site to GitHub Pages
#
name: Deploy VitePress site to Pages
on:
workflow_dispatch: {}
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
# using the `master` branch as the default branch.
push:
branches:
- main
branches: [main]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
deploy:
# Build job
build:
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
fetch-depth: 0 # Not needed if lastUpdated is not enabled
# - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16
cache: npm
- run: npm ci
- name: Build
run: npm run docs:build
- uses: actions/configure-pages@v2
- uses: actions/upload-pages-artifact@v1
node-version: 18
cache: npm # or pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Install dependencies
run: npm ci # or pnpm install / yarn install
- name: Build with VitePress
run: npm run docs:build # or pnpm docs:build / yarn docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: docs/.vitepress/dist
- name: Deploy
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v2
```
::: tip
Please replace the corresponding branch name. For example, if the branch you want to build is `master`, then you should replace `main` with `master` in the above file.
::: warning
Make sure the `base` option in your VitePress is properly configured. See [Setting a Public Base Path](#setting-a-public-base-path) for more details.
:::
3. In your repository's Settings under Pages menu item, select `GitHub Actions` in Build and deployment's Source.
4. Now commit your code and push it to the `main` branch.
2. In your repository's settings under "Pages" menu item, select "GitHub Actions" in "Build and deployment > Source".
5. Wait for actions to complete.
6. In your repository's Settings under Pages menu item, click `Visit site`, then you can see your site. Your docs will automatically deploy each time you push.
3. Push your changes to the `main` branch and wait for the GitHub Actions workflow to complete. You should see your site deployed to `https://<username>.github.io/[repository]/` or `https://<custom-domain>/` depending on your settings. Your site will automatically be deployed on every push to the `main` branch.
### GitLab Pages
1. Set `outDir` in `docs/.vitepress/config.js` to `../public`.
2. Still in your config file, `docs/.vitepress/config.js`, set the `base` property to the name of your GitLab repository. If you plan to deploy your site to `https://foo.gitlab.io/bar/`, then you should set base to `'/bar/'`. It should always start and end with a slash.
1. Set `outDir` in VitePress config to `../public`. Configure `base` option to `'/<repository>/'` if you want to deploy to `https://<username>.gitlab.io/<repository>/`.
3. Create a file called `.gitlab-ci.yml` in the root of your project with the content below. This will build and deploy your site whenever you make changes to your content:
2. Create a file named `.gitlab-ci.yml` in the root of your project with the content below. This will build and deploy your site whenever you make changes to your content:
```yaml
image: node:16
@ -187,25 +208,7 @@ Don't enable options like _Auto Minify_ for HTML code. It will remove comments f
paths:
- node_modules/
script:
- npm install
- npm run docs:build
artifacts:
paths:
- public
only:
- main
```
4. Alternatively, if you want to use an _alpine_ version of node, you have to install `git` manually. In that case, the code above modifies to this:
```yaml
image: node:16-alpine
pages:
cache:
paths:
- node_modules/
before_script:
- apk add git
script:
# - apk add git # Uncomment this if you're using small docker images like alpine and have lastUpdated enabled
- npm install
- npm run docs:build
artifacts:

@ -10,7 +10,7 @@ However, there are a number of cases where configuration alone won't be enough.
These advanced customizations will require using a custom theme that "extends" the default theme.
:::tip
::: tip
Before proceeding, make sure to first read [Using a Custom Theme](./custom-theme) to understand how custom themes work.
:::
@ -58,7 +58,7 @@ export default DefaultTheme
}
```
:::warning
::: warning
If you are using optional components like the [Team Page](/reference/default-theme-team-page) components, make sure to also import them from `vitepress/theme-without-fonts`!
:::

@ -69,7 +69,7 @@ You will be greeted with a few simple questions:
<img src="./vitepress-init.png" alt="vitepress init screenshot" style="border-radius:8px">
</p>
:::tip Vue as Peer Dependency
::: tip Vue as Peer Dependency
If you intend to perform customization that uses Vue components or APIs, you should also explicitly install `vue` as a peer dependency.
:::
@ -92,7 +92,7 @@ Assuming you chose to scaffold the VitePress project in `./docs`, the generated
The `docs` directory is considered the **project root** of the VitePress site. The `.vitepress` directory is a reserved location for VitePress' config file, dev server cache, build output, and optional theme customization code.
:::tip
::: tip
By default, VitePress stores its dev server cache in `.vitepress/cache`, and the production build output in `.vitepress/dist`. If using Git, you should add them to your `.gitignore` file. These locations can also be [configured](../reference/site-config#outdir).
:::

@ -97,7 +97,7 @@ Learn more about linking to assets such images in [Asset Handling](asset-handlin
## Generating Clean URL
:::warning Server Support Required
::: warning Server Support Required
To serve clean URLs with VitePress, server-side support is required.
:::
@ -172,7 +172,7 @@ export default {
The rewrite paths are compiled using the `path-to-regexp` package - consult [its documentation](https://github.com/pillarjs/path-to-regexp#parameters) for more advanced syntax.
:::warning Relative Links with Rewrites
::: warning Relative Links with Rewrites
When rewrites are enabled, **relative links should be based on the rewritten paths**. For example, in order to create a relative link from `packages/pkg-a/src/pkg-a-code.md` to `packages/pkg-b/src/pkg-b-code.md`, you should use:

@ -4,7 +4,7 @@ In VitePress, each Markdown file is compiled into HTML and then processed as a [
It's worth noting that VitePress leverages Vue's compiler to automatically detect and optimize the purely static parts of the Markdown content. Static contents are optimized into single placeholder nodes and eliminated from the page's JavaScript payload for initial visits. They are also skipped during client-side hydration. In short, you only pay for the dynamic parts on any given page.
:::tip SSR Compatibility
::: tip SSR Compatibility
All Vue usage needs to be SSR-compatible. See [SSR Compatibility](./ssr-compat) for details and common workarounds.
:::
@ -67,7 +67,7 @@ The count is: {{ count }}
</style>
```
:::warning Avoid `<style scoped>` in Markdown
::: warning Avoid `<style scoped>` in Markdown
When used in Markdown, `<style scoped>` requires adding special attributes to every element on the current page, which will significantly bloat the page size. `<style module>` is preferred when locally-scoped styling is needed in a page.
:::

@ -12,7 +12,7 @@ Just want to try it out? Skip to the [Quickstart](./getting-started).
- **Documentation**
VitePress ships with a default theme designed for technical documentation, especially those that need to embed interactive demos. It powers this page you are reading right now, along with the documentation for [Vite](https://vitejs.dev/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Rollup](https://rollupjs.org/), [Mermaid](https://mermaid.js.org/), [Wikimedia Codex](https://doc.wikimedia.org/codex/latest/), and many more.
VitePress ships with a default theme designed for technical documentation. It powers this page you are reading right now, along with the documentation for [Vite](https://vitejs.dev/), [Rollup](https://rollupjs.org/), [Pinia](https://pinia.vuejs.org/), [VueUse](https://vueuse.org/), [Vitest](https://vitest.dev/), [D3](https://d3js.org/), [UnoCSS](https://unocss.dev/), [Iconify](https://iconify.design/) and [many more](https://www.vuetelescope.com/explore?framework.slug=vitepress).
The [official Vue.js documentation](https://vuejs.org/) is also based on VitePress, but uses a custom theme shared between multiple translations.

@ -11,7 +11,7 @@ hero:
actions:
- theme: brand
text: Get Started
link: /guide/what-is-vitepress
link: /guide/getting-started
- theme: alt
text: View on GitHub
link: https://github.com/vuejs/vitepress
@ -20,10 +20,10 @@ features:
- icon: 📝
title: Focus on Your Content
details: Effortlessly create beautiful documentation sites with just markdown.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><g fill="none"><path fill="url(#a)" d="m29.884 6.146-13.142 23.5a.714.714 0 0 1-1.244.005L2.096 6.148a.714.714 0 0 1 .746-1.057l13.156 2.352a.714.714 0 0 0 .253 0l12.881-2.348a.714.714 0 0 1 .752 1.05z"/><path fill="url(#b)" d="M22.264 2.007 12.54 3.912a.357.357 0 0 0-.288.33l-.598 10.104a.357.357 0 0 0 .437.369l2.707-.625a.357.357 0 0 1 .43.42l-.804 3.939a.357.357 0 0 0 .454.413l1.672-.508a.357.357 0 0 1 .454.414l-1.279 6.187c-.08.387.435.598.65.267l.143-.222 7.925-15.815a.357.357 0 0 0-.387-.51l-2.787.537a.357.357 0 0 1-.41-.45l1.818-6.306a.357.357 0 0 0-.412-.45z"/><defs><linearGradient id="a" x1="6" x2="235" y1="33" y2="344" gradientTransform="translate(1.34 1.894) scale(.07142)" gradientUnits="userSpaceOnUse"><stop stop-color="#41D1FF"/><stop offset="1" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="194.651" x2="236.076" y1="8.818" y2="292.989" gradientTransform="translate(1.34 1.894) scale(.07142)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFEA83"/><stop offset=".083" stop-color="#FFDD35"/><stop offset="1" stop-color="#FFA800"/></linearGradient></defs></g></svg>
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 256.32"><defs><linearGradient id="a" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"/><stop offset="100%" stop-color="#BD34FE"/></linearGradient><linearGradient id="b" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"/><stop offset="8.333%" stop-color="#FFDD35"/><stop offset="100%" stop-color="#FFA800"/></linearGradient></defs><path fill="url(#a)" d="M255.153 37.938 134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"/><path fill="url(#b)" d="M185.432.063 96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028 72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"/></svg>
title: Enjoy the Vite DX
details: Instant server start, lightning fast hot updates, and leverage Vite ecosystem plugins.
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path fill="#41b883" d="M24.4 3.925H30l-14 24.15L2 3.925h10.71l3.29 5.6 3.22-5.6Z"/><path fill="#41b883" d="m2 3.925 14 24.15 14-24.15h-5.6L16 18.415 7.53 3.925Z"/><path fill="#35495e" d="M7.53 3.925 16 18.485l8.4-14.56h-5.18L16 9.525l-3.29-5.6Z"/></svg>
- icon: <svg xmlns="http://www.w3.org/2000/svg" width="30" viewBox="0 0 256 220.8"><path fill="#41B883" d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36Z"/><path fill="#41B883" d="m0 0 128 220.8L256 0h-51.2L128 132.48 50.56 0H0Z"/><path fill="#35495E" d="M50.56 0 128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56Z"/></svg>
title: Customize with Vue
details: Use Vue syntax and components directly in markdown, or build custom themes with Vue.
- icon: 🚀

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>Plain HTML page | VitePress</title>

@ -2,6 +2,10 @@
The update time of the last content will be displayed in the lower right corner of the page. To enable it, add `lastUpdated` options to your config.
::: tip
You need to commit the markdown file to see the updated time.
:::
## Site-Level Config
```js

@ -1,3 +1,7 @@
---
outline: deep
---
# Search
## Local Search
@ -58,6 +62,42 @@ export default defineConfig({
})
```
### miniSearch options
You can configure MiniSearch like this:
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
search: {
provider: 'local',
options: {
miniSearch: {
/**
* @type {Pick<import('minisearch').Options, 'extractField' | 'tokenize' | 'processTerm'>}
*/
options: {
/* ... */
},
/**
* @type {import('minisearch').SearchOptions}
* @default
* { fuzzy: 0.2, prefix: true, boost: { title: 4, text: 2, titles: 1 } }
*/
searchOptions: {
/* ... */
}
}
}
}
}
})
```
Learn more in [MiniSearch docs](https://lucaong.github.io/minisearch/classes/_minisearch_.minisearch.html).
## Algolia Search
VitePress supports searching your docs site using [Algolia DocSearch](https://docsearch.algolia.com/docs/what-is-docsearch). Refer their getting started guide. In your `.vitepress/config.ts` you'll need to provide at least the following to make it work:
@ -145,6 +185,114 @@ export default defineConfig({
[These options](https://github.com/vuejs/vitepress/blob/main/types/docsearch.d.ts) can be overridden. Refer official Algolia docs to learn more about them.
### Crawler Config
Here is an example config based on what this site uses:
```ts
new Crawler({
appId: '...',
apiKey: '...',
rateLimit: 8,
startUrls: ['https://vitepress.dev/'],
renderJavaScript: false,
sitemaps: [],
exclusionPatterns: [],
ignoreCanonicalTo: false,
discoveryPatterns: ['https://vitepress.dev/**'],
schedule: 'at 05:10 on Saturday',
actions: [
{
indexName: 'vitepress',
pathsToMatch: ['https://vitepress.dev/**'],
recordExtractor: ({ $, helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: '.content h1',
content: '.content p, .content li',
lvl0: {
selectors: '',
defaultValue: 'Documentation'
},
lvl2: '.content h2',
lvl3: '.content h3',
lvl4: '.content h4',
lvl5: '.content h5'
},
indexHeadings: true
})
}
}
],
initialIndexSettings: {
vitepress: {
attributesForFaceting: ['type', 'lang'],
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
attributesToHighlight: ['hierarchy', 'hierarchy_camel', 'content'],
attributesToSnippet: ['content:10'],
camelCaseAttributes: ['hierarchy', 'hierarchy_radio', 'content'],
searchableAttributes: [
'unordered(hierarchy_radio_camel.lvl0)',
'unordered(hierarchy_radio.lvl0)',
'unordered(hierarchy_radio_camel.lvl1)',
'unordered(hierarchy_radio.lvl1)',
'unordered(hierarchy_radio_camel.lvl2)',
'unordered(hierarchy_radio.lvl2)',
'unordered(hierarchy_radio_camel.lvl3)',
'unordered(hierarchy_radio.lvl3)',
'unordered(hierarchy_radio_camel.lvl4)',
'unordered(hierarchy_radio.lvl4)',
'unordered(hierarchy_radio_camel.lvl5)',
'unordered(hierarchy_radio.lvl5)',
'unordered(hierarchy_radio_camel.lvl6)',
'unordered(hierarchy_radio.lvl6)',
'unordered(hierarchy_camel.lvl0)',
'unordered(hierarchy.lvl0)',
'unordered(hierarchy_camel.lvl1)',
'unordered(hierarchy.lvl1)',
'unordered(hierarchy_camel.lvl2)',
'unordered(hierarchy.lvl2)',
'unordered(hierarchy_camel.lvl3)',
'unordered(hierarchy.lvl3)',
'unordered(hierarchy_camel.lvl4)',
'unordered(hierarchy.lvl4)',
'unordered(hierarchy_camel.lvl5)',
'unordered(hierarchy.lvl5)',
'unordered(hierarchy_camel.lvl6)',
'unordered(hierarchy.lvl6)',
'content'
],
distinct: true,
attributeForDistinct: 'url',
customRanking: [
'desc(weight.pageRank)',
'desc(weight.level)',
'asc(weight.position)'
],
ranking: [
'words',
'filters',
'typo',
'attribute',
'proximity',
'exact',
'custom'
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: '</span>',
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: 'allOptional'
}
}
})
```
<style>
img[src="/search.png"] {
width: 100%;

@ -186,7 +186,7 @@ export default {
Returns sidebar-related data. The returned object has the following type:
```ts
export interface Sidebar {
export interface DocSidebar {
isOpen: Ref<boolean>
sidebar: ComputedRef<DefaultTheme.SidebarItem[]>
sidebarGroups: ComputedRef<DefaultTheme.SidebarItem[]>

@ -290,6 +290,19 @@ export default {
}
```
### assetsDir
- Type: `string`
- Default: `assets`
The directory for assets files. See also: [assetsDir](https://vitejs.dev/config/build-options.html#build-assetsdir).
```ts
export default {
assetsDir: 'static'
}
```
### cacheDir
- Type: `string`

@ -1,9 +1,9 @@
{
"name": "vitepress",
"version": "1.0.0-beta.4",
"version": "1.0.0-beta.5",
"description": "Vite & Vue powered static site generator",
"type": "module",
"packageManager": "pnpm@8.6.5",
"packageManager": "pnpm@8.6.9",
"main": "dist/node/index.js",
"types": "types/index.d.ts",
"exports": {
@ -96,11 +96,11 @@
"@vueuse/core": "^10.2.1",
"@vueuse/integrations": "^10.2.1",
"body-scroll-lock": "4.0.0-beta.0",
"focus-trap": "^7.4.3",
"focus-trap": "^7.5.2",
"mark.js": "8.11.1",
"minisearch": "^6.1.0",
"shiki": "^0.14.3",
"vite": "4.4.0-beta.3",
"vite": "^4.4.4",
"vue": "^3.3.4"
},
"devDependencies": {
@ -113,7 +113,7 @@
"@mdit-vue/plugin-toc": "^0.12.0",
"@mdit-vue/shared": "^0.12.0",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^25.0.2",
"@rollup/plugin-commonjs": "^25.0.3",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-replace": "^5.0.2",
@ -130,14 +130,15 @@
"@types/markdown-it-emoji": "^2.0.2",
"@types/micromatch": "^4.0.2",
"@types/minimist": "^1.2.2",
"@types/node": "^20.3.3",
"@types/node": "^20.4.2",
"@types/prompts": "^2.4.4",
"@vue/shared": "^3.3.4",
"chokidar": "^3.5.3",
"compression": "^1.7.4",
"conventional-changelog-cli": "^2",
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
"esbuild": "^0.18.11",
"esbuild": "^0.18.14",
"escape-html": "^1.0.3",
"execa": "^7.1.1",
"fast-glob": "^3.3.0",
@ -160,23 +161,23 @@
"path-to-regexp": "^6.2.1",
"picocolors": "^1.0.0",
"pkg-dir": "^7.0.0",
"playwright-chromium": "^1.35.1",
"playwright-chromium": "^1.36.1",
"polka": "1.0.0-next.22",
"prettier": "^2.8.8",
"prettier": "^3.0.0",
"prompts": "^2.4.2",
"punycode": "^2.3.0",
"rimraf": "^5.0.1",
"rollup": "^3.26.0",
"rollup": "^3.26.3",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",
"semver": "^7.5.3",
"semver": "^7.5.4",
"shiki-processor": "^0.1.3",
"simple-git-hooks": "^2.8.1",
"sirv": "^2.0.3",
"supports-color": "^9.4.0",
"typescript": "^5.1.6",
"vitest": "^0.32.2",
"vue-tsc": "^1.8.3",
"vitest": "^0.33.0",
"vue-tsc": "^1.8.5",
"wait-on": "^7.0.1"
},
"simple-git-hooks": {

File diff suppressed because it is too large Load Diff

@ -37,9 +37,15 @@ export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
// update title and description
document.title = createTitle(siteData, pageData)
document
.querySelector(`meta[name=description]`)!
.setAttribute('content', pageDescription || siteData.description)
const description = pageDescription || siteData.description
let metaDescriptionElement = document.querySelector(
`meta[name=description]`
)
if (metaDescriptionElement) {
metaDescriptionElement.setAttribute('content', description)
} else {
createHeadElement(['meta', { name: 'description', content: description }])
}
updateHeadTags(
mergeHead(siteData.head, filterOutHeadDescription(frontmatterHead))

@ -58,7 +58,10 @@ export function pathToFile(path: string) {
pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]
}
if (!pageHash) return null
pagePath = `${base}assets/${pagePath}.${pageHash}.js`
pagePath = `${base}${__ASSETS_DIR__.replace(
/"(.+)"/,
'$1'
)}/${pagePath}.${pageHash}.js`
} else {
// ssr build uses much simpler name mapping
pagePath = `./${sanitizeFileName(

@ -3,6 +3,7 @@ declare const __VP_LOCAL_SEARCH__: boolean
declare const __ALGOLIA__: boolean
declare const __CARBON__: boolean
declare const __VUE_PROD_DEVTOOLS__: boolean
declare const __ASSETS_DIR__: string
declare module '*.vue' {
import type { DefineComponent } from 'vue'

@ -1,6 +1,5 @@
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { useSidebar } from 'vitepress/theme'
import { computed, provide, useSlots, watch } from 'vue'
import VPBackdrop from './components/VPBackdrop.vue'
import VPContent from './components/VPContent.vue'
@ -10,7 +9,7 @@ import VPNav from './components/VPNav.vue'
import VPSidebar from './components/VPSidebar.vue'
import VPSkipLink from './components/VPSkipLink.vue'
import { useData } from './composables/data'
import { useCloseSidebarOnEscape } from './composables/sidebar'
import { useCloseSidebarOnEscape, useSidebar } from './composables/sidebar'
const {
isOpen: isSidebarOpen,

@ -1,7 +1,7 @@
<script setup lang="ts">
import { useSidebar } from 'vitepress/theme'
import NotFound from '../NotFound.vue'
import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar'
import VPDoc from './VPDoc.vue'
import VPHome from './VPHome.vue'
import VPPage from './VPPage.vue'

@ -1,8 +1,8 @@
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { useSidebar } from 'vitepress/theme'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar'
import VPDocAside from './VPDocAside.vue'
import VPDocFooter from './VPDocFooter.vue'
import VPDocOutlineDropdown from './VPDocOutlineDropdown.vue'
@ -205,5 +205,6 @@ const pageName = computed(() =>
.external-link-icon-enabled :is(.vp-doc a[href*='://'], .vp-doc a[target='_blank'])::after {
content: '';
color: currentColor;
}
</style>

@ -41,20 +41,20 @@ const showFooter = computed(() => {
</div>
</div>
<div v-if="control.prev?.link || control.next?.link" class="prev-next">
<nav v-if="control.prev?.link || control.next?.link" class="prev-next">
<div class="pager">
<a v-if="control.prev?.link" class="pager-link prev" :href="normalizeLink(control.prev.link)">
<span class="desc" v-html="theme.docFooter?.prev || 'Previous page'"></span>
<span class="title" v-html="control.prev.text"></span>
</a>
</div>
<div class="pager" :class="{ 'has-prev': control.prev?.link }">
<div class="pager">
<a v-if="control.next?.link" class="pager-link next" :href="normalizeLink(control.next.link)">
<span class="desc" v-html="theme.docFooter?.next || 'Next page'"></span>
<span class="title" v-html="control.next.text"></span>
</a>
</div>
</div>
</nav>
</footer>
</template>
@ -101,29 +101,14 @@ const showFooter = computed(() => {
.prev-next {
border-top: 1px solid var(--vp-c-divider);
padding-top: 24px;
display: grid;
grid-row-gap: 8px;
}
@media (min-width: 640px) {
.prev-next {
display: flex;
}
}
.pager.has-prev {
padding-top: 8px;
}
@media (min-width: 640px) {
.pager {
display: flex;
flex-direction: column;
flex-shrink: 0;
width: 50%;
}
.pager.has-prev {
padding-top: 0;
padding-left: 16px;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 16px;
}
}

@ -14,7 +14,7 @@ defineProps<{
</script>
<template>
<VPLink class="VPFeature" :href="link" :no-icon="true">
<VPLink class="VPFeature" :href="link" :no-icon="true" :tag="link ? 'a' : 'div'">
<article class="box">
<VPImage
v-if="typeof icon === 'object'"

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useSidebar } from 'vitepress/theme'
import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar'
const { theme } = useData()
const { hasSidebar } = useSidebar()

@ -27,10 +27,10 @@ const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
<div class="main">
<slot name="home-hero-info">
<h1 v-if="name" class="name">
<span class="clip">{{ name }}</span>
<span v-html="name" class="clip"></span>
</h1>
<p v-if="text" class="text">{{ text }}</p>
<p v-if="tagline" class="tagline">{{ tagline }}</p>
<p v-if="text" v-html="text" class="text"></p>
<p v-if="tagline" v-html="tagline" class="tagline"></p>
</slot>
<div v-if="actions" class="actions">

@ -19,7 +19,11 @@ const isExternal = computed(() => props.href && EXTERNAL_URL_RE.test(props.href)
<component
:is="tag"
class="VPLink"
:class="{ link: href, 'vp-external-link-icon': isExternal && !noIcon }"
:class="{
link: href,
'vp-external-link-icon': isExternal,
'no-icon': noIcon
}"
:href="href ? normalizeLink(href) : undefined"
:target="target || (isExternal ? '_blank' : undefined)"
:rel="rel || (isExternal ? 'noreferrer' : undefined)"

@ -1,9 +1,9 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { onContentUpdated } from 'vitepress'
import { useSidebar } from 'vitepress/theme'
import { computed, shallowRef } from 'vue'
import { useData } from '../composables/data'
import { useSidebar } from '../composables/sidebar'
import { getHeaders, type MenuItem } from '../composables/outline'
import VPLocalNavOutlineDropdown from './VPLocalNavOutlineDropdown.vue'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'

@ -80,8 +80,12 @@ const searchIndex = computedAsync(async () =>
searchOptions: {
fuzzy: 0.2,
prefix: true,
boost: { title: 4, text: 2, titles: 1 }
}
boost: { title: 4, text: 2, titles: 1 },
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.searchOptions)
},
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.options)
}
)
)
@ -396,8 +400,16 @@ function formMarkRegex(terms: Set<string>) {
<div class="backdrop" @click="$emit('close')" />
<div class="shell">
<form class="search-bar" @pointerup="onSearchBarClick($event)" @submit.prevent="">
<label :title="placeholder" id="localsearch-label" for="localsearch-input">
<form
class="search-bar"
@pointerup="onSearchBarClick($event)"
@submit.prevent=""
>
<label
:title="placeholder"
id="localsearch-label"
for="localsearch-input"
>
<svg
class="search-icon"
width="18"
@ -454,7 +466,9 @@ function formMarkRegex(terms: Set<string>) {
class="toggle-layout-button"
:class="{ 'detailed-list': showDetailedList }"
:title="$t('modal.displayDetails')"
@click="selectedIndex > -1 && (showDetailedList = !showDetailedList)"
@click="
selectedIndex > -1 && (showDetailedList = !showDetailedList)
"
>
<svg
width="18"
@ -527,7 +541,11 @@ function formMarkRegex(terms: Set<string>) {
<div>
<div class="titles">
<span class="title-icon">#</span>
<span v-for="(t, index) in p.titles" :key="index" class="title">
<span
v-for="(t, index) in p.titles"
:key="index"
class="title"
>
<span class="text" v-html="t" />
<svg width="18" height="18" viewBox="0 0 24 24">
<path
@ -547,7 +565,7 @@ function formMarkRegex(terms: Set<string>) {
<div v-if="showDetailedList" class="excerpt-wrapper">
<div v-if="p.text" class="excerpt" inert>
<div class="vp-doc" v-html="p.text" />
<div class="vp-doc" v-html="p.text" />
</div>
<div class="excerpt-gradient-bottom" />
<div class="excerpt-gradient-top" />
@ -726,7 +744,7 @@ function formMarkRegex(terms: Set<string>) {
}
.search-actions button.clear-button:disabled {
opacity: 0.37;
opacity: 0.37;
}
.search-keyboard-shortcuts {

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { useSidebar } from 'vitepress/theme'
import { computed } from 'vue'
import { useSidebar } from '../composables/sidebar'
import VPNavBarAppearance from './VPNavBarAppearance.vue'
import VPNavBarExtra from './VPNavBarExtra.vue'
import VPNavBarHamburger from './VPNavBarHamburger.vue'

@ -1,7 +1,7 @@
<script setup lang="ts">
import { useSidebar } from 'vitepress/theme'
import { useData } from '../composables/data'
import { useLangs } from '../composables/langs'
import { useSidebar } from '../composables/sidebar'
import { normalizeLink } from '../support/utils'
import VPImage from './VPImage.vue'
@ -47,6 +47,6 @@ const { currentLang } = useLangs()
:deep(.logo) {
margin-right: 8px;
height: 24px;
height: var(--vp-nav-logo-height);
}
</style>

@ -27,7 +27,7 @@ function unlockBodyScroll() {
@enter="lockBodyScroll"
@after-leave="unlockBodyScroll"
>
<div v-if="open" class="VPNavScreen" ref="screen">
<div v-if="open" class="VPNavScreen" ref="screen" id="VPNavScreen">
<div class="container">
<slot name="nav-screen-content-before" />
<VPNavScreenMenu class="menu" />

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock'
import { useSidebar } from 'vitepress/theme'
import { ref, watchPostEffect } from 'vue'
import { useSidebar } from '../composables/sidebar'
import VPSidebarItem from './VPSidebarItem.vue'
const { sidebarGroups, hasSidebar } = useSidebar()

@ -193,6 +193,7 @@ function onCaretClick() {
color: var(--vp-c-text-3);
cursor: pointer;
transition: color 0.25s;
flex-shrink: 0;
}
.item:hover .caret {

@ -17,7 +17,7 @@ const svg = computed(() => {
<template>
<a
class="VPSocialLink"
class="VPSocialLink no-icon"
:href="link"
:aria-label="ariaLabel ?? (typeof icon === 'string' ? icon : '')"
target="_blank"

@ -1,6 +1,6 @@
import { useMediaQuery } from '@vueuse/core'
import { useSidebar } from 'vitepress/theme'
import { computed } from 'vue'
import { useSidebar } from './sidebar'
export function useAside() {
const { hasSidebar } = useSidebar()

@ -42,7 +42,9 @@
font-weight: 500;
user-select: none;
opacity: 0;
transition: color 0.25s, opacity 0.25s;
transition:
color 0.25s,
opacity 0.25s;
}
.vp-doc .header-anchor:before {
@ -245,7 +247,9 @@
padding: 3px 6px;
color: var(--vp-c-text-code);
background-color: var(--vp-c-mute);
transition: color 0.5s, background-color 0.5s;
transition:
color 0.5s,
background-color 0.5s;
}
.vp-doc h1 > code,
@ -362,12 +366,16 @@
.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) {
filter: blur(0.095rem);
opacity: 0.4;
transition: filter 0.35s, opacity 0.35s;
transition:
filter 0.35s,
opacity 0.35s;
}
.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) {
opacity: 0.7;
transition: filter 0.35s, opacity 0.35s;
transition:
filter 0.35s,
opacity 0.35s;
}
.vp-doc [class*='language-']:hover .has-focused-lines .line:not(.has-focus) {
@ -415,7 +423,9 @@
line-height: var(--vp-code-line-height);
font-size: var(--vp-code-font-size);
color: var(--vp-code-line-number-color);
transition: border-color 0.5s, color 0.5s;
transition:
border-color 0.5s,
color 0.5s;
}
.vp-doc [class*='language-'] > button.copy {
@ -437,7 +447,10 @@
background-position: 50%;
background-size: 20px;
background-repeat: no-repeat;
transition: border-color 0.25s, background-color 0.25s, opacity 0.25s;
transition:
border-color 0.25s,
background-color 0.25s,
opacity 0.25s;
}
.vp-doc [class*='language-']:hover > button.copy,
@ -492,7 +505,9 @@
font-size: 12px;
font-weight: 500;
color: var(--vp-c-code-dimm);
transition: color 0.4s, opacity 0.4s;
transition:
color 0.4s,
opacity 0.4s;
}
.vp-doc [class*='language-']:hover > button.copy + span.lang,
@ -523,17 +538,15 @@
max-width: calc((100% - 24px) / 2) !important;
}
:is(
.vp-external-link-icon,
.vp-doc a[href*='://'],
.vp-doc a[target='_blank']
)::after {
/* prettier-ignore */
:is(.vp-external-link-icon, .vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(.no-icon)::after {
display: inline-block;
margin-top: -1px;
margin-left: 4px;
width: 11px;
height: 11px;
background: currentColor;
color: var(--vp-c-text-3);
flex-shrink: 0;
--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");
-webkit-mask-image: var(--icon);

@ -361,6 +361,7 @@
--vp-nav-height: 64px;
--vp-nav-bg-color: var(--vp-c-bg);
--vp-nav-screen-bg-color: var(--vp-c-bg);
--vp-nav-logo-height: 24px;
}
/**

@ -1,3 +1,4 @@
import { createHash } from 'crypto'
import fs from 'fs-extra'
import { createRequire } from 'module'
import ora from 'ora'
@ -7,9 +8,9 @@ import { rimraf } from 'rimraf'
import type { OutputAsset, OutputChunk } from 'rollup'
import { pathToFileURL } from 'url'
import type { BuildOptions } from 'vite'
import { resolveConfig } from '../config'
import type { HeadConfig } from '../shared'
import { serializeFunctions } from '../utils/fnSerialize'
import { resolveConfig, type SiteConfig } from '../config'
import { slash, type HeadConfig } from '../shared'
import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize'
import { bundle, failMark, okMark } from './bundle'
import { renderPage } from './render'
@ -42,7 +43,7 @@ export async function build(
const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).toString())
const spinner = ora()
const spinner = ora({ discardStdin: false })
spinner.start('rendering pages...')
try {
@ -56,12 +57,12 @@ export async function build(
) as OutputChunk)
const cssChunk = (
siteConfig.mpa ? serverResult : clientResult
siteConfig.mpa ? serverResult : clientResult!
).output.find(
(chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as OutputAsset
const assets = (siteConfig.mpa ? serverResult : clientResult).output
const assets = (siteConfig.mpa ? serverResult : clientResult!).output
.filter(
(chunk) => chunk.type === 'asset' && !chunk.fileName.endsWith('.css')
)
@ -79,6 +80,8 @@ export async function build(
chunk.moduleIds.some((id) => id.includes('client/theme-default'))
)
const metadataScript = generateMetadataScript(pageToHashMap, siteConfig)
if (isDefaultTheme) {
const fontURL = assets.find((file) =>
/inter-roman-latin\.\w+\.woff2/.test(file)
@ -97,15 +100,6 @@ export async function build(
}
}
// We embed the hash map and site config strings into each page directly
// so that it doesn't alter the main chunk's hash on every build.
// It's also embedded as a string and JSON.parsed from the client because
// it's faster than embedding as JS object literal.
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
const siteDataString = JSON.stringify(
JSON.stringify(serializeFunctions({ ...siteConfig.site, head: [] }))
)
await Promise.all(
['404.md', ...siteConfig.pages]
.map((page) => siteConfig.rewrites.map[page] || page)
@ -119,8 +113,7 @@ export async function build(
cssChunk,
assets,
pageToHashMap,
hashMapString,
siteDataString,
metadataScript,
additionalHeadTags
)
)
@ -168,3 +161,51 @@ function linkVue() {
}
return () => {}
}
function generateMetadataScript(
pageToHashMap: Record<string, string>,
config: SiteConfig
) {
if (config.mpa) {
return { html: '', inHead: false }
}
// We embed the hash map and site config strings into each page directly
// so that it doesn't alter the main chunk's hash on every build.
// It's also embedded as a string and JSON.parsed from the client because
// it's faster than embedding as JS object literal.
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
const siteDataString = JSON.stringify(
JSON.stringify(serializeFunctions({ ...config.site, head: [] }))
)
const metadataContent = `window.__VP_HASH_MAP__=JSON.parse(${hashMapString});${
siteDataString.includes('_vp-fn_')
? `${deserializeFunctions.toString()};window.__VP_SITE_DATA__=deserializeFunctions(JSON.parse(${siteDataString}));`
: `window.__VP_SITE_DATA__=JSON.parse(${siteDataString});`
}`
if (!config.metaChunk) {
return { html: `<script>${metadataContent}</script>`, inHead: false }
}
const metadataFile = path.join(
config.assetsDir,
'chunks',
`metadata.${createHash('sha256')
.update(metadataContent)
.digest('hex')
.slice(0, 8)}.js`
)
const resolvedMetadataFile = path.join(config.outDir, metadataFile)
const metadataFileURL = slash(`${config.site.base}${metadataFile}`)
fs.ensureDirSync(path.dirname(resolvedMetadataFile))
fs.writeFileSync(resolvedMetadataFile, metadataContent)
return {
html: `<script type="module" src="${metadataFileURL}"></script>`,
inHead: true
}
}

@ -31,7 +31,7 @@ export async function bundle(
config: SiteConfig,
options: BuildOptions
): Promise<{
clientResult: RollupOutput
clientResult: RollupOutput | null
serverResult: RollupOutput
pageToHashMap: Record<string, string>
}> {
@ -94,19 +94,19 @@ export async function bundle(
output: {
sanitizeFileName,
...rollupOptions?.output,
assetFileNames: 'assets/[name].[hash].[ext]',
assetFileNames: `${config.assetsDir}/[name].[hash].[ext]`,
...(ssr
? {
entryFileNames: '[name].js',
chunkFileNames: '[name].[hash].js'
}
: {
entryFileNames: 'assets/[name].[hash].js',
entryFileNames: `${config.assetsDir}/[name].[hash].js`,
chunkFileNames(chunk) {
// avoid ads chunk being intercepted by adblock
return /(?:Carbon|BuySell)Ads/.test(chunk.name)
? 'assets/chunks/ui-custom.[hash].js'
: 'assets/chunks/[name].[hash].js'
? `${config.assetsDir}/chunks/ui-custom.[hash].js`
: `${config.assetsDir}/chunks/[name].[hash].js`
},
manualChunks(id, ctx) {
if (lazyDefaultThemeComponentsRE.test(id)) {
@ -142,16 +142,16 @@ export async function bundle(
}
})
let clientResult: RollupOutput
let clientResult: RollupOutput | null
let serverResult: RollupOutput
const spinner = ora()
const spinner = ora({ discardStdin: false })
spinner.start('building client + server bundles...')
try {
;[clientResult, serverResult] = await (Promise.all([
config.mpa ? null : build(await resolveViteConfig(false)),
build(await resolveViteConfig(true))
]) as Promise<[RollupOutput, RollupOutput]>)
clientResult = config.mpa
? null
: ((await build(await resolveViteConfig(false))) as RollupOutput)
serverResult = (await build(await resolveViteConfig(true))) as RollupOutput
} catch (e) {
spinner.stopAndPersist({
symbol: failMark

@ -1,3 +1,4 @@
import { isBooleanAttr } from '@vue/shared'
import escape from 'escape-html'
import fs from 'fs-extra'
import path from 'path'
@ -6,8 +7,8 @@ import { pathToFileURL } from 'url'
import { normalizePath, transformWithEsbuild } from 'vite'
import type { SiteConfig } from '../config'
import {
createTitle,
EXTERNAL_URL_RE,
createTitle,
mergeHead,
notFoundPageData,
resolveSiteDataByRoute,
@ -17,19 +18,17 @@ import {
type PageData,
type SSGContext
} from '../shared'
import { deserializeFunctions } from '../utils/fnSerialize'
export async function renderPage(
render: (path: string) => Promise<SSGContext>,
config: SiteConfig,
page: string, // foo.md
result: RollupOutput | null,
appChunk: OutputChunk | undefined,
cssChunk: OutputAsset | undefined,
appChunk: OutputChunk | null,
cssChunk: OutputAsset | null,
assets: string[],
pageToHashMap: Record<string, string>,
hashMapString: string,
siteDataString: string,
metadataScript: { html: string; inHead: boolean },
additionalHeadTags: HeadConfig[]
) {
const routePath = `/${page.replace(/\.md$/, '')}`
@ -45,7 +44,7 @@ export async function renderPage(
// for any initial page load, we only need the lean version of the page js
// since the static content is already on the page!
const pageHash = pageToHashMap[pageName.toLowerCase()]
const pageClientJsFileName = `assets/${pageName}.${pageHash}.lean.js`
const pageClientJsFileName = `${config.assetsDir}/${pageName}.${pageHash}.lean.js`
let pageData: PageData
let hasCustom404 = true
@ -149,15 +148,7 @@ export async function renderPage(
}
}
let metadataScript = `__VP_HASH_MAP__ = JSON.parse(${hashMapString})\n`
if (siteDataString.includes('_vp-fn_')) {
metadataScript += `${deserializeFunctions.toString()}\n__VP_SITE_DATA__ = deserializeFunctions(JSON.parse(${siteDataString}))`
} else {
metadataScript += `__VP_SITE_DATA__ = JSON.parse(${siteDataString})`
}
const html = `
<!DOCTYPE html>
const html = `<!DOCTYPE html>
<html lang="${siteData.lang}" dir="${siteData.dir}">
<head>
<meta charset="utf-8">
@ -165,21 +156,22 @@ export async function renderPage(
<title>${title}</title>
<meta name="description" content="${description}">
${stylesheetLink}
${metadataScript.inHead ? metadataScript.html : ''}
${
appChunk
? `<script type="module" src="${siteData.base}${appChunk.fileName}"></script>`
: ``
: ''
}
${await renderHead(head)}
</head>
<body>${teleports?.body || ''}
<div id="app">${content}</div>
${config.mpa ? '' : `<script>${metadataScript}</script>`}
${metadataScript.inHead ? '' : metadataScript.html}
${inlinedScript}
</body>
</html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
</html>`
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
await fs.ensureDir(path.dirname(htmlFileName))
const transformedHtml = await config.transformHtml?.(html, htmlFileName, {
page,
@ -223,8 +215,8 @@ function resolvePageImports(
]
}
function renderHead(head: HeadConfig[]): Promise<string> {
return Promise.all(
async function renderHead(head: HeadConfig[]): Promise<string> {
const tags = await Promise.all(
head.map(async ([tag, attrs = {}, innerHTML = '']) => {
const openTag = `<${tag}${renderAttrs(attrs)}>`
if (tag !== 'link' && tag !== 'meta') {
@ -243,13 +235,15 @@ function renderHead(head: HeadConfig[]): Promise<string> {
return openTag
}
})
).then((tags) => tags.join('\n '))
)
return tags.join('\n ')
}
function renderAttrs(attrs: Record<string, string>): string {
return Object.keys(attrs)
.map((key) => {
return ` ${key}="${escape(attrs[key])}"`
if (isBooleanAttr(key)) return ` ${key}`
return ` ${key}="${escape(attrs[key] as string)}"`
})
.join('')
}

@ -73,6 +73,9 @@ export async function resolveConfig(
})
const site = await resolveSiteData(root, userConfig)
const srcDir = normalizePath(path.resolve(root, userConfig.srcDir || '.'))
const assetsDir = userConfig.assetsDir
? userConfig.assetsDir.replace(/\//g, '')
: 'assets'
const outDir = userConfig.outDir
? normalizePath(path.resolve(root, userConfig.outDir))
: resolve(root, 'dist')
@ -94,6 +97,7 @@ export async function resolveConfig(
const config: SiteConfig = {
root,
srcDir,
assetsDir,
site,
themeDir,
pages,
@ -111,6 +115,7 @@ export async function resolveConfig(
vite: userConfig.vite,
shouldPreload: userConfig.shouldPreload,
mpa: !!userConfig.mpa,
metaChunk: !!userConfig.metaChunk,
ignoreDeadLinks: userConfig.ignoreDeadLinks,
cleanUrls: !!userConfig.cleanUrls,
useWebFonts:

@ -36,4 +36,5 @@ export interface MarkdownEnv {
relativePath: string
cleanUrls: boolean
links?: string[]
includes?: string[]
}

@ -50,6 +50,7 @@ export interface MarkdownOptions extends MarkdownIt.Options {
languages?: ILanguageRegistration[]
toc?: TocPluginOptions
externalLinks?: Record<string, string>
cache?: boolean
}
export type MarkdownRenderer = MarkdownIt

@ -140,41 +140,43 @@ export const snippetPlugin = (md: MarkdownIt, srcDir: string) => {
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => {
const [tokens, idx, , { loader }] = args
const [tokens, idx, , { includes }] = args
const token = tokens[idx]
// @ts-ignore
const [src, regionName] = token.src ?? []
if (src) {
if (loader) {
loader.addDependency(src)
}
const isAFile = fs.lstatSync(src).isFile()
if (fs.existsSync(src) && isAFile) {
let content = fs.readFileSync(src, 'utf8')
if (regionName) {
const lines = content.split(/\r?\n/)
const region = findRegion(lines, regionName)
if (region) {
content = dedent(
lines
.slice(region.start, region.end)
.filter((line: string) => !region.regexp.test(line.trim()))
.join('\n')
)
}
}
if (!src) return fence(...args)
token.content = content
} else {
token.content = isAFile
? `Code snippet path not found: ${src}`
: `Invalid code snippet option`
token.info = ''
if (includes) {
includes.push(src)
}
const isAFile = fs.lstatSync(src).isFile()
if (!fs.existsSync(src) || !isAFile) {
token.content = isAFile
? `Code snippet path not found: ${src}`
: `Invalid code snippet option`
token.info = ''
return fence(...args)
}
let content = fs.readFileSync(src, 'utf8')
if (regionName) {
const lines = content.split(/\r?\n/)
const region = findRegion(lines, regionName)
if (region) {
content = dedent(
lines
.slice(region.start, region.end)
.filter((line) => !region.regexp.test(line.trim()))
.join('\n')
)
}
}
token.content = content
return fence(...args)
}

@ -67,10 +67,12 @@ export async function createMarkdownToVueRenderFn(
const relativePath = slash(path.relative(srcDir, file))
const cacheKey = JSON.stringify({ src, file })
const cached = cache.get(cacheKey)
if (cached) {
debug(`[cache hit] ${relativePath}`)
return cached
if (isBuild || options.cache !== false) {
const cached = cache.get(cacheKey)
if (cached) {
debug(`[cache hit] ${relativePath}`)
return cached
}
}
const start = Date.now()
@ -88,7 +90,7 @@ export async function createMarkdownToVueRenderFn(
// resolve includes
let includes: string[] = []
function processIncludes(src: string): string {
function processIncludes(src: string, file: string): string {
return src.replace(includesRE, (m: string, m1: string) => {
if (!m1.length) return m
@ -98,7 +100,7 @@ export async function createMarkdownToVueRenderFn(
try {
const includePath = atPresent
? path.join(srcDir, m1.slice(m1[1] === '/' ? 2 : 1))
: path.join(path.dirname(fileOrig), m1)
: path.join(path.dirname(file), m1)
let content = fs.readFileSync(includePath, 'utf-8')
if (range) {
const [, startLine, endLine] = range
@ -112,20 +114,21 @@ export async function createMarkdownToVueRenderFn(
}
includes.push(slash(includePath))
// recursively process includes in the content
return processIncludes(content)
return processIncludes(content, includePath)
} catch (error) {
return m // silently ignore error if file is not present
}
})
}
src = processIncludes(src)
src = processIncludes(src, fileOrig)
// reset env before render
const env: MarkdownEnv = {
path: file,
relativePath,
cleanUrls
cleanUrls,
includes
}
const html = md.render(src, env)
const {
@ -243,7 +246,9 @@ export async function createMarkdownToVueRenderFn(
deadLinks,
includes
}
cache.set(cacheKey, result)
if (isBuild || options.cache !== false) {
cache.set(cacheKey, result)
}
return result
}
}
@ -317,14 +322,16 @@ function injectPageDataCode(
code +
(hasDefaultExport
? ``
: `\nexport default {name:'${data.relativePath}'}`) +
: `\nexport default {name:${JSON.stringify(data.relativePath)}}`) +
`</script>`
)
} else {
tags.unshift(
`<script ${isUsingTS ? 'lang="ts"' : ''}>${code}\nexport default {name:'${
`<script ${
isUsingTS ? 'lang="ts"' : ''
}>${code}\nexport default {name:${JSON.stringify(
data.relativePath
}'}</script>`
)}}</script>`
)
}

@ -129,7 +129,8 @@ export async function createVitePressPlugin(
__ALGOLIA__:
site.themeConfig?.search?.provider === 'algolia' ||
!!site.themeConfig?.algolia, // legacy
__CARBON__: !!site.themeConfig?.carbonAds
__CARBON__: !!site.themeConfig?.carbonAds,
__ASSETS_DIR__: JSON.stringify(siteConfig.assetsDir)
},
optimizeDeps: {
// force include vue to avoid duplicated copies when linked + optimized

@ -1,12 +1,12 @@
import path from 'node:path'
import type { Plugin, ViteDevServer } from 'vite'
import MiniSearch from 'minisearch'
import fs from 'fs-extra'
import _debug from 'debug'
import fs from 'fs-extra'
import MiniSearch from 'minisearch'
import path from 'path'
import type { Plugin, ViteDevServer } from 'vite'
import type { SiteConfig } from '../config'
import type { MarkdownEnv } from '../markdown'
import { createMarkdownRenderer } from '../markdown'
import { resolveSiteDataByRoute, slash } from '../shared'
import { resolveSiteDataByRoute, slash, type DefaultTheme } from '../shared'
const debug = _debug('vitepress:local-search')
@ -21,7 +21,7 @@ interface IndexObject {
}
export async function localSearchPlugin(
siteConfig: SiteConfig
siteConfig: SiteConfig<DefaultTheme.Config>
): Promise<Plugin> {
if (siteConfig.site.themeConfig?.search?.provider !== 'local') {
return {
@ -63,7 +63,9 @@ export async function localSearchPlugin(
if (!index) {
index = new MiniSearch<IndexObject>({
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles']
storeFields: ['title', 'titles'],
...(siteConfig.site.themeConfig?.search?.provider === 'local' &&
siteConfig.site.themeConfig.search.options?.miniSearch?.options)
})
indexByLocales.set(locale, index)
}

@ -28,7 +28,8 @@ export async function serve(options: ServeOptions = {}) {
const config = await resolveConfig(options.root, 'serve', 'production')
const base = trimChar(options?.base ?? config?.site?.base ?? '', '/')
const notAnAsset = (pathname: string) => !pathname.includes('/assets/')
const notAnAsset = (pathname: string) =>
!pathname.includes(`/${config.assetsDir}/`)
const notFound = fs.readFileSync(path.resolve(config.outDir, './404.html'))
const onNoMatch: IOptions['onNoMatch'] = (req, res) => {
res.statusCode = 404

@ -59,6 +59,7 @@ export interface UserConfig<ThemeConfig = any>
srcDir?: string
srcExclude?: string[]
outDir?: string
assetsDir?: string
cacheDir?: string
shouldPreload?: (link: string, page: string) => boolean
@ -97,6 +98,12 @@ export interface UserConfig<ThemeConfig = any>
*/
mpa?: boolean
/**
* Extracts metadata to a separate chunk.
* @experimental
*/
metaChunk?: boolean
/**
* Don't fail builds due to dead links.
*
@ -175,6 +182,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'vite'
| 'shouldPreload'
| 'mpa'
| 'metaChunk'
| 'lastUpdated'
| 'ignoreDeadLinks'
| 'cleanUrls'
@ -192,6 +200,7 @@ export interface SiteConfig<ThemeConfig = any>
configDeps: string[]
themeDir: string
outDir: string
assetsDir: string
cacheDir: string
tempDir: string
pages: string[]

@ -5,7 +5,7 @@ export function getGitTimestamp(file: string) {
return new Promise<number>((resolve, reject) => {
const cwd = dirname(file)
const fileName = basename(file)
const child = spawn('git', ['log', '-1', '--pretty="%ci"', fileName], {
const child = spawn('git', ['log', '-1', '--pretty="%ai"', fileName], {
cwd
})
let output = ''

@ -1 +1,2 @@
export * from './theme'
export { default } from './theme'

2
theme.d.ts vendored

@ -20,4 +20,4 @@ declare const theme: {
export default theme
export type { DefaultTheme } from './types/default-theme.js'
export const useSidebar: () => DefaultTheme.SideBar
export const useSidebar: () => DefaultTheme.DocSidebar

@ -1,3 +1,5 @@
import type { Options as MiniSearchOptions } from 'minisearch'
import type { ComputedRef, Ref } from 'vue'
import type { DocSearchProps } from './docsearch.js'
import type { LocalSearchTranslations } from './local-search.js'
import type { PageData } from './shared.js'
@ -222,7 +224,7 @@ export namespace DefaultTheme {
/**
* ReturnType of `useSidebar`
*/
export interface Sidebar {
export interface DocSidebar {
isOpen: Ref<boolean>
sidebar: ComputedRef<SidebarItem[]>
sidebarGroups: ComputedRef<SidebarItem[]>
@ -334,6 +336,20 @@ export namespace DefaultTheme {
translations?: LocalSearchTranslations
locales?: Record<string, Partial<Omit<LocalSearchOptions, 'locales'>>>
miniSearch?: {
/**
* @see https://lucaong.github.io/minisearch/modules/_minisearch_.html#options
*/
options?: Pick<
MiniSearchOptions,
'extractField' | 'tokenize' | 'processTerm'
>
/**
* @see https://lucaong.github.io/minisearch/modules/_minisearch_.html#searchoptions-1
*/
searchOptions?: MiniSearchOptions['searchOptions']
}
}
// algolia -------------------------------------------------------------------

Loading…
Cancel
Save