Merge branch 'main' into main

pull/4893/head
Evorp 3 days ago committed by GitHub
commit c4466b97df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,3 +1,50 @@
## [2.0.0-alpha.12](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.11...v2.0.0-alpha.12) (2025-08-20)
### Bug Fixes
- **hmr:** don't load config twice on server restart ([d1a8061](https://github.com/vuejs/vitepress/commit/d1a8061eb438c730ccc62ce2d7158dbe89cc5292))
- **hmr:** no need for server restart on theme change ([d3a1567](https://github.com/vuejs/vitepress/commit/d3a15673bd0846c7837bcc4ff5a2e3239a02f1f9))
- **hmr:** hmr not working for snippet imports in dynamic routes ([914467e](https://github.com/vuejs/vitepress/commit/914467e17fb759a9722951a3fd7568eb3bc4d4e6))
- **theme:** fix local nav alignment and increase touch area ([43b36c0](https://github.com/vuejs/vitepress/commit/43b36c0c19c2b4696f8c38fdaf4318786ea7ae8e))
- **theme:** nav background doesn't extend fully and gap after sidebar with non-overlay scrollbars ([7df3052](https://github.com/vuejs/vitepress/commit/7df30525121a28a46cc6c802f3155ccff8effaca)), closes [#4653](https://github.com/vuejs/vitepress/issues/4653)
- **theme:** use clipboard-check instead of clipboard-copy for code copied icon ([1c8815d](https://github.com/vuejs/vitepress/commit/1c8815d53ed2d56b07938260df6566f1514f4bfc))
### Features
- add markdown-it-cjk-friendly ([9fc8462](https://github.com/vuejs/vitepress/commit/9fc8462726ccf1cdb78b6171c9f1f5964e79ca22)), closes [#3762](https://github.com/vuejs/vitepress/issues/3762) [#4752](https://github.com/vuejs/vitepress/issues/4752)
- make postcssIsolateStyles idempotent ([0944777](https://github.com/vuejs/vitepress/commit/094477789328b80cff45cd973efa16b6a4db0a27))
### BREAKING CHANGES
- [markdown-it-cjk-friendly](https://www.npmjs.com/package/markdown-it-cjk-friendly) is enabled by default. This intentionally deviates from the official commonmark spec for the benefit of CJK users. **For most users, no change is required.** If you were using hacks to patch `scanDelims`, you can remove those. To disable the plugin, set `markdown: { cjkFriendly: false }` in your vitepress config.
- `includeFiles` option in `postcssIsolateStyles` now defaults to `[/vp-doc\.css/, /base\.css/]`. You can remove explicit `includeFiles` if you were using it just to run it on `vp-doc.css`. To revert back to older behavior pass `includeFiles: [/base\.css/]`. The underlying implementation is changed and `transform` and `exclude` options are no longer supported. Use `postcss-prefix-selector` directly if you've advanced use cases.
## [2.0.0-alpha.11](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.10...v2.0.0-alpha.11) (2025-08-14)
### Bug Fixes
- hmr working only once for markdown files ([8d8a5ac](https://github.com/vuejs/vitepress/commit/8d8a5ac281f090cd097bece792d9dd3ef00e5545)), closes [#4909](https://github.com/vuejs/vitepress/issues/4909)
- html entities encoded twice in toc plugin ([8abbe29](https://github.com/vuejs/vitepress/commit/8abbe298d545de17d34a9bc1eb72af4c5a4b41b8)), closes [#4908](https://github.com/vuejs/vitepress/issues/4908)
## [2.0.0-alpha.10](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.9...v2.0.0-alpha.10) (2025-08-11)
### Bug Fixes
- **client:** base not stripped from relativePath in 404 pages ([b840877](https://github.com/vuejs/vitepress/commit/b840877aa83a5a24ffc1222e8a5a3dbf3e5105e8)), closes [#4850](https://github.com/vuejs/vitepress/issues/4850)
- hmr of style blocks in dynamic routes ([#4903](https://github.com/vuejs/vitepress/issues/4903)) ([3d0fafb](https://github.com/vuejs/vitepress/commit/3d0fafba545f4b5028cf43d86027dd44dab14421))
- make paths in `watchedFiles` absolute as mentioned in the docs ([318c14f](https://github.com/vuejs/vitepress/commit/318c14fa7c9fb949d74b7d9fae416e917766cf05))
- module graph causing unnecessary route regeneration on every update ([fc267ae](https://github.com/vuejs/vitepress/commit/fc267ae6b787e163d41666e090089821377ead43))
- preserve externally added dynamic routes and pages ([fc267ae](https://github.com/vuejs/vitepress/commit/fc267ae6b787e163d41666e090089821377ead43))
- **search:** input placeholder being cut off in smaller viewports ([162c6a6](https://github.com/vuejs/vitepress/commit/162c6a69bf56945daa20d126aa034c59ee0c8a2e))
- **search:** style tweaks for when searches are empty ([8b23217](https://github.com/vuejs/vitepress/commit/8b232171cc321bd3dc86b4357622815269f0b6f4))
- **types:** externalize markdown-it types ([5bf835b](https://github.com/vuejs/vitepress/commit/5bf835b5074e9567852d552bfb5115c6456026e8))
- **types:** pass generics deeply to user config ([777e2ca](https://github.com/vuejs/vitepress/commit/777e2caaacd93ce41b046f6c9d5ba80cc43ba37c))
### Features
- add source param to the deadlink check fn ([#4870](https://github.com/vuejs/vitepress/issues/4870)) ([8c027c2](https://github.com/vuejs/vitepress/commit/8c027c2a7c443074fd0d4890f7736b444f9254aa))
- **theme:** add `rel="me"` to social links by default ([#4873](https://github.com/vuejs/vitepress/issues/4873)) ([34886c6](https://github.com/vuejs/vitepress/commit/34886c667d1305a79d64c957f8c52931ea122f47))
## [2.0.0-alpha.9](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.8...v2.0.0-alpha.9) (2025-07-26)
### Bug Fixes

@ -0,0 +1,77 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`node/postcss/isolateStyles > transforms selectors and skips keyframes 1`] = `
"
/* simple classes */
.example:not(:where(.vp-raw, .vp-raw *)) { color: red; }
.class-a:not(:where(.vp-raw, .vp-raw *)) { color: coral; }
.class-b:not(:where(.vp-raw, .vp-raw *)) { color: deepskyblue; }
/* escaped colon in class */
.baz\\:not\\(.bar\\):not(:where(.vp-raw, .vp-raw *)) { display: block; }
.disabled\\:opacity-50:not(:where(.vp-raw, .vp-raw *)):disabled { opacity: .5; }
/* pseudos (class + element) */
.button:not(:where(.vp-raw, .vp-raw *)):hover { color: pink; }
.button:not(:where(.vp-raw, .vp-raw *)):focus:hover { color: hotpink; }
.item:not(:where(.vp-raw, .vp-raw *))::before { content: '•'; }
:not(:where(.vp-raw, .vp-raw *))::first-letter { color: pink; }
:not(:where(.vp-raw, .vp-raw *))::before { content: ''; }
/* universal + :not */
*:not(:where(.vp-raw, .vp-raw *)) { background-color: red; }
*:not(:where(.vp-raw, .vp-raw *)):not(.b) { text-transform: uppercase; }
/* combinators */
.foo:hover .bar:not(:where(.vp-raw, .vp-raw *)) { background: blue; }
ul > li.active:not(:where(.vp-raw, .vp-raw *)) { color: green; }
a + b ~ c:not(:where(.vp-raw, .vp-raw *)) { color: orange; }
/* ids + attribute selectors */
#wow:not(:where(.vp-raw, .vp-raw *)) { color: yellow; }
[data-world] .d:not(:where(.vp-raw, .vp-raw *)) { padding: 10px 20px; }
/* :root and chained tags */
:not(:where(.vp-raw, .vp-raw *)):root { --bs-blue: #0d6efd; }
:root .a:not(:where(.vp-raw, .vp-raw *)) { --bs-green: #bada55; }
html:not(:where(.vp-raw, .vp-raw *)) { margin: 0; }
body:not(:where(.vp-raw, .vp-raw *)) { padding: 0; }
html body div:not(:where(.vp-raw, .vp-raw *)) { color: blue; }
/* grouping with commas */
.a:not(:where(.vp-raw, .vp-raw *)), .b:not(:where(.vp-raw, .vp-raw *)) { color: red; }
/* multiple repeated groups to ensure stability */
.a:not(:where(.vp-raw, .vp-raw *)), .b:not(:where(.vp-raw, .vp-raw *)) { color: coral; }
.a:not(:where(.vp-raw, .vp-raw *)) { animation: glow 1s linear infinite alternate; }
/* nested blocks */
.foo:not(:where(.vp-raw, .vp-raw *)) {
svg:not(:where(.vp-raw, .vp-raw *)) { display: none; }
.bar:not(:where(.vp-raw, .vp-raw *)) { display: inline; }
}
/* standalone pseudos */
:not(:where(.vp-raw, .vp-raw *)):first-child { color: pink; }
:not(:where(.vp-raw, .vp-raw *)):hover { color: blue; }
:not(:where(.vp-raw, .vp-raw *)):active { color: red; }
/* keyframes (should be ignored) */
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes glow {
from { color: coral; }
to { color: red; }
}
@-moz-keyframes glow {
from { color: coral; }
to { color: red; }
}
@-o-keyframes glow {
from { color: coral; }
to { color: red; }
}
"
`;

@ -1,44 +1,93 @@
import {
postcssIsolateStyles,
splitSelectorPseudo
} from 'node/postcss/isolateStyles'
// helper to run plugin transform on selector
function apply(
prefixPlugin: ReturnType<typeof postcssIsolateStyles>,
selector: string
) {
// `prepare` is available on the runtime plugin but missing from the types, thus cast to `any`
const { Rule } = (prefixPlugin as any).prepare({
root: { source: { input: { file: 'foo/base.css' } } }
})
const rule = { selectors: [selector] }
Rule(rule, { result: {} })
return rule.selectors[0]
import { postcssIsolateStyles } from 'node/postcss/isolateStyles'
import postcss from 'postcss'
const INPUT_CSS = `
/* simple classes */
.example { color: red; }
.class-a { color: coral; }
.class-b { color: deepskyblue; }
/* escaped colon in class */
.baz\\:not\\(.bar\\) { display: block; }
.disabled\\:opacity-50:disabled { opacity: .5; }
/* pseudos (class + element) */
.button:hover { color: pink; }
.button:focus:hover { color: hotpink; }
.item::before { content: '•'; }
::first-letter { color: pink; }
::before { content: ''; }
/* universal + :not */
* { background-color: red; }
*:not(.b) { text-transform: uppercase; }
/* combinators */
.foo:hover .bar { background: blue; }
ul > li.active { color: green; }
a + b ~ c { color: orange; }
/* ids + attribute selectors */
#wow { color: yellow; }
[data-world] .d { padding: 10px 20px; }
/* :root and chained tags */
:root { --bs-blue: #0d6efd; }
:root .a { --bs-green: #bada55; }
html { margin: 0; }
body { padding: 0; }
html body div { color: blue; }
/* grouping with commas */
.a, .b { color: red; }
/* multiple repeated groups to ensure stability */
.a, .b { color: coral; }
.a { animation: glow 1s linear infinite alternate; }
/* nested blocks */
.foo {
svg { display: none; }
.bar { display: inline; }
}
describe('node/postcss/isolateStyles', () => {
const plugin = postcssIsolateStyles()
/* standalone pseudos */
:first-child { color: pink; }
:hover { color: blue; }
:active { color: red; }
test('splitSelectorPseudo skips escaped colon', () => {
const input = '.foo\\:bar'
const [selector, pseudo] = splitSelectorPseudo(input)
expect(selector).toBe(input)
expect(pseudo).toBe('')
})
/* keyframes (should be ignored) */
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes glow {
from { color: coral; }
to { color: red; }
}
@-moz-keyframes glow {
from { color: coral; }
to { color: red; }
}
@-o-keyframes glow {
from { color: coral; }
to { color: red; }
}
`
test('splitSelectorPseudo splits on pseudo selectors', () => {
const input = '.button:hover'
const [selector, pseudo] = splitSelectorPseudo(input)
expect(selector).toBe('.button')
expect(pseudo).toBe(':hover')
describe('node/postcss/isolateStyles', () => {
test('transforms selectors and skips keyframes', () => {
const out = run(INPUT_CSS)
expect(out.css).toMatchSnapshot()
})
it('postcssIsolateStyles inserts :not(...) in the right place', () => {
const input = '.disabled\\:opacity-50:disabled'
const result = apply(plugin, input)
expect(result).toBe(
'.disabled\\:opacity-50:not(:where(.vp-raw, .vp-raw *)):disabled'
)
test('idempotent (running twice produces identical CSS)', () => {
const first = run(INPUT_CSS).css
const second = run(first).css
expect(second).toBe(first)
})
})
function run(css: string, from = 'src/styles/vp-doc.css') {
return postcss([postcssIsolateStyles()]).process(css, { from })
}

@ -124,13 +124,13 @@ export default defineConfig({
},
locales: {
root: { label: 'English' },
zh: { label: '简体中文' },
pt: { label: 'Português' },
ru: { label: 'Русский' },
es: { label: 'Español' },
ko: { label: '한국어' },
fa: { label: 'فارسی' }
root: { label: 'English', lang: 'en-US', dir: 'ltr' },
zh: { label: '简体中文', lang: 'zh-Hans', dir: 'ltr' },
pt: { label: 'Português', lang: 'pt-BR', dir: 'ltr' },
ru: { label: 'Русский', lang: 'ru-RU', dir: 'ltr' },
es: { label: 'Español', lang: 'es', dir: 'ltr' },
ko: { label: '한국어', lang: 'ko-KR', dir: 'ltr' },
fa: { label: 'فارسی', lang: 'fa-IR', dir: 'rtl' }
},
vite: {

@ -5,7 +5,6 @@ const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export default defineAdditionalConfig({
lang: 'en-US',
description: 'Vite & Vue powered static site generator.',
themeConfig: {

@ -18,23 +18,19 @@ VitePress can be used on its own, or be installed into an existing project. In b
::: code-group
```sh [npm]
$ npm add -D vitepress
$ npm add -D vitepress@next
```
```sh [pnpm]
$ pnpm add -D vitepress
$ pnpm add -D vitepress@next
```
```sh [yarn]
$ yarn add -D vitepress
```
```sh [yarn (pnp)]
$ yarn add -D vitepress vue
$ yarn add -D vitepress@next vue
```
```sh [bun]
$ bun add -D vitepress
$ bun add -D vitepress@next
```
:::

@ -277,11 +277,11 @@ Wraps in a `<div class="vp-raw">`
}
```
It uses [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) under the hood. You can pass its options like this:
You can pass its options like this:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // defaults to /base\.css/
includeFiles: [/custom\.css/] // defaults to [/vp-doc\.css/, /base\.css/]
})
```

@ -50,8 +50,8 @@ Unlike many traditional SSGs where each navigation results in a full page reload
## What About VuePress?
VitePress is the spiritual successor of VuePress. The original VuePress was based on Vue 2 and webpack. With Vue 3 and Vite under the hood, VitePress provides significantly better DX, better production performance, a more polished default theme, and a more flexible customization API.
VitePress is the spiritual successor of VuePress 1. The original VuePress 1 was based on Vue 2 and webpack. With Vue 3 and Vite under the hood, VitePress provides significantly better DX, better production performance, a more polished default theme, and a more flexible customization API.
The API difference between VitePress and VuePress mostly lies in theming and customization. If you are using VuePress 1 with the default theme, it should be relatively straightforward to migrate to VitePress.
The API difference between VitePress and VuePress 1 mostly lies in theming and customization. If you are using VuePress 1 with the default theme, it should be relatively straightforward to migrate to VitePress.
There has also been effort invested into VuePress 2, which also supports Vue 3 and Vite with more compatibility with VuePress 1. However, maintaining two SSGs in parallel isn't sustainable, so the Vue team has decided to focus on VitePress as the main recommended SSG in the long run.
Maintaining two SSGs in parallel isn't sustainable, so the Vue team has decided to focus on VitePress as the main recommended SSG in the long run. Now VuePress 1 has been deprecated, and VuePress 2 has been handed over to the VuePress community team for further development and maintenance.

@ -24,7 +24,7 @@ export default {
}
```
:::details Dynamic (Async) Config
::: details Dynamic (Async) Config
If you need to dynamically generate the config, you can also default export a function. For example:

@ -5,7 +5,6 @@ const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export default defineAdditionalConfig({
lang: 'es-CO',
description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.',
themeConfig: {
@ -25,7 +24,7 @@ export default defineAdditionalConfig({
footer: {
message: 'Liberado bajo la licencia MIT',
copyright: `Derechos reservados © 2019-${new Date().getFullYear()} Evan You`
copyright: 'Todos los derechos reservados © 2019-PRESENTE Evan You'
},
docFooter: {

@ -18,19 +18,19 @@ VitePress puede ser usado solo, o ser instalado en un proyecto ya existente. En
::: code-group
```sh [npm]
$ npm add -D vitepress
$ npm add -D vitepress@next
```
```sh [pnpm]
$ pnpm add -D vitepress
$ pnpm add -D vitepress@next
```
```sh [yarn]
$ yarn add -D vitepress
$ yarn add -D vitepress@next
```
```sh [bun]
$ bun add -D vitepress
$ bun add -D vitepress@next
```
:::

@ -256,11 +256,11 @@ La clase `vp-raw` también puede ser usada directamente en elementos. El aislami
}
```
El utiliza [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) internamente. Puede pasar opciones así:
Puede pasar opciones así:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // o padrão é /base\.css/
includeFiles: [/custom\.css/] // o padrão é [/vp-doc\.css/, /base\.css/]
})
```

@ -24,7 +24,7 @@ export default {
}
```
:::details Configuración dinámica (Assíncrona)
::: details Configuración dinámica (Assíncrona)
Si necesitas generar dinamicamente la configuración, también puedes exportar por defecto una función. Por ejemplo:
@ -594,7 +594,7 @@ export default {
`transformHead` es un enlace de compilación para transformar el encabezado antes de generar cada página. Esto le permite agregar entradas de encabezado que no se pueden agregar estáticamente a la configuración de VitePress. Sólo necesita devolver entradas adicionales, que se fusionarán automáticamente con las existentes.
:::warning
::: warning
No mutes ningún elemento dentro `context`.
:::
@ -662,7 +662,7 @@ export default {
- Tipo: `(code: string, id: string, context: TransformContext) => Awaitable<string | void>`
`transformHtml` es un gancho de compilación para transformar el contenido de cada página antes de guardarla en el disco.
:::warning
::: warning
No mute ningún elemento dentro del `context`. Además, modificar el contenido HTML puede provocar problemas de hidratación en tiempo de ejecución.
:::
@ -679,7 +679,7 @@ export default {
`transformPageData` es un gancho para transformar los datos de cada página. Puedes hacer mutaciones directamente en `pageData` o devolver valores modificados que se fusionarán con los datos de la página.
:::warning
::: warning
No mute ningún elemento dentro del `context` y tenga cuidado ya que esto puede afectar el rendimiento del servidor de desarrollo, especialmente si tiene algunas solicitudes de red o cálculos pesados (como generar imágenes) en el gancho. Puede consultar `process.env.NODE_ENV === 'production'` para ver la lógica condicional.
:::

@ -5,9 +5,7 @@ const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export default defineAdditionalConfig({
lang: 'fa-IR',
description: 'ژنراتور استاتیک وب‌سایت با Vite و Vue',
dir: 'rtl',
// prettier-ignore
head: [

@ -18,23 +18,19 @@
::: code-group
```sh [npm]
$ npm add -D vitepress
$ npm add -D vitepress@next
```
```sh [pnpm]
$ pnpm add -D vitepress
$ pnpm add -D vitepress@next
```
```sh [yarn]
$ yarn add -D vitepress
```
```sh [yarn (pnp)]
$ yarn add -D vitepress vue
$ yarn add -D vitepress@next vue
```
```sh [bun]
$ bun add -D vitepress
$ bun add -D vitepress@next
```
:::

@ -255,11 +255,11 @@ export default defineConfig({
}
```
این از [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) استفاده می‌کند. می‌توانید گزینه‌های آن را به این صورت پاس بدهید:
می‌توانید گزینه‌های آن را به این صورت پاس بدهید:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // به طور پیش‌فرض /base\.css/
includeFiles: [/custom\.css/] // به طور پیش‌فرض [/vp-doc\.css/, /base\.css/]
})
```

@ -24,7 +24,7 @@ export default {
}
```
:::details تنظیمات پویا (غیرهمزمان)
::: details تنظیمات پویا (غیرهمزمان)
اگر نیاز دارید به طور پویا تنظیمات را تولید کنید، می‌توانید یک تابع صادر کنید. به عنوان مثال:
@ -354,7 +354,7 @@ export default {
وقتی تنظیم شود به `true`، ویت‌پرس `.html` انتهایی را از URL ها حذف می‌کند. همچنین ببینید [تولید URL تمیز](../guide/routing#generating-clean-url).
::: هشدار نیاز به پشتیبانی سرور
::: warning هشدار نیاز به پشتیبانی سرور
فعال کردن این ممکن است نیاز به پیکربندی اضافی در پلتفرم میزبان شما داشته باشد. برای اینکه کار کند، سرور شما باید بتواند `/foo.html` را زمانی که `/foo` بازدید می‌شود **بدون ریدایرکت** سرو کند.
:::

@ -5,7 +5,6 @@ const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export default defineAdditionalConfig({
lang: 'ko-KR',
description: 'Vite 및 Vue 기반 정적 사이트 생성기.',
themeConfig: {

@ -19,23 +19,19 @@ VitePress는 단독으로 사용하거나 기존 프로젝트에 설치할 수
::: code-group
```sh [npm]
$ npm add -D vitepress
$ npm add -D vitepress@next
```
```sh [pnpm]
$ pnpm add -D vitepress
$ pnpm add -D vitepress@next
```
```sh [yarn]
$ yarn add -D vitepress
```
```sh [yarn (pnp)]
$ yarn add -D vitepress vue
$ yarn add -D vitepress@next vue
```
```sh [bun]
$ bun add -D vitepress
$ bun add -D vitepress@next
```
:::

@ -255,11 +255,11 @@ export default defineConfig({
}
```
이것은 기본적으로 [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector)를 사용합니다. 다음과 같이 옵션을 전달할 수 있습니다:
다음과 같이 옵션을 전달할 수 있습니다:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // 기본값은 /base\.css/
includeFiles: [/custom\.css/] // 기본값은 [/vp-doc\.css/, /base\.css/]
})
```

@ -15,7 +15,7 @@
"open-cli": "^8.0.0",
"postcss-rtlcss": "^5.7.1",
"vitepress": "workspace:*",
"vitepress-plugin-group-icons": "^1.6.1",
"vitepress-plugin-llms": "^1.7.1"
"vitepress-plugin-group-icons": "^1.6.3",
"vitepress-plugin-llms": "^1.7.3"
}
}

@ -5,7 +5,6 @@ const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export default defineAdditionalConfig({
lang: 'pt-BR',
description: 'Gerador de Site Estático desenvolvido com Vite e Vue.',
themeConfig: {

@ -18,23 +18,19 @@ VitePress pode ser usado sozinho, ou ser instalado em um projeto já existente.
::: code-group
```sh [npm]
$ npm add -D vitepress
$ npm add -D vitepress@next
```
```sh [pnpm]
$ pnpm add -D vitepress
$ pnpm add -D vitepress@next
```
```sh [yarn]
$ yarn add -D vitepress
```
```sh [yarn (pnp)]
$ yarn add -D vitepress vue
$ yarn add -D vitepress@next vue
```
```sh [bun]
$ bun add -D vitepress
$ bun add -D vitepress@next
```
:::

@ -255,11 +255,11 @@ A classe `vp-raw` também pode ser usada diretamente em elementos. O isolamento
}
```
Ele utiliza [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) internamente. Você pode passar opções assim:
Você pode passar opções assim:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // o padrão é /base\.css/
includeFiles: [/custom\.css/] // o padrão é [/vp-doc\.css/, /base\.css/]
})
```

@ -24,7 +24,7 @@ export default {
}
```
:::details Configuração Dinâmica (Assíncrona)
::: details Configuração Dinâmica (Assíncrona)
Se você precisar gerar dinamicamente a configuração, também pode exportar por padrão uma função. Por exemplo:
@ -594,7 +594,7 @@ export default {
`transformHead` é um gancho de compilação para transformar o cabeçalho antes de gerar cada página. Isso permite adicionar entradas no cabeçalho que não podem ser adicionadas estaticamente à configuração VitePress. Você só precisa retornar entradas extras, que serão mescladas automaticamente com as existentes.
:::warning
::: warning
Não faça mutações em qualquer item dentro de `context`.
:::
@ -662,7 +662,7 @@ export default {
- Tipo: `(code: string, id: string, context: TransformContext) => Awaitable<string | void>`
`transformHtml` é um gancho de compilação para transformar o conteúdo de cada página antes de salvá-lo no disco.
:::warning
::: warning
Não faça mutações em qualquer item dentro de `context`. Além disso, modificar o conteúdo HTML pode causar problemas de hidratação em tempo de execução.
:::
@ -679,7 +679,7 @@ export default {
`transformPageData` é um gancho para transformar os dados de cada página. Você pode fazer mutações diretamente em `pageData` ou retornar valores alterados que serão mesclados nos dados da página.
:::warning
::: warning
Não faça mutações em qualquer item dentro de `context` e tenha cuidado pois isso pode impactar no desempenho do servidor de desenvolvimento, especialmente se você tiver algumas solicitações de rede ou computações pesadas (como gerar imagens) no gancho. Você pode verificar `process.env.NODE_ENV === 'production'` para lógica condicional.
:::

@ -5,7 +5,6 @@ const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export default defineAdditionalConfig({
lang: 'ru-RU',
description: 'Генератор статических сайтов на основе Vite и Vue.',
themeConfig: {

@ -18,23 +18,19 @@ VitePress можно использовать самостоятельно ил
::: code-group
```sh [npm]
$ npm add -D vitepress
$ npm add -D vitepress@next
```
```sh [pnpm]
$ pnpm add -D vitepress
$ pnpm add -D vitepress@next
```
```sh [yarn]
$ yarn add -D vitepress
```
```sh [yarn (pnp)]
$ yarn add -D vitepress vue
$ yarn add -D vitepress@next vue
```
```sh [bun]
$ bun add -D vitepress
$ bun add -D vitepress@next
```
:::

@ -281,11 +281,11 @@ console.log('Привет, VitePress!')
}
```
Он использует [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) под капотом. Вы можете передать ему параметры следующим образом:
Вы можете передать ему параметры следующим образом:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // по умолчанию /base\.css/
includeFiles: [/custom\.css/] // по умолчанию [/vp-doc\.css/, /base\.css/]
})
```

@ -1,6 +1,6 @@
# Что такое VitePress? {#what-is-vitepress}
VitePress — это [Генератор статических сайтов](https://en.wikipedia.org/wiki/Static_site_generator) (ГСС), предназначенный для быстрого создания сайтов, ориентированных на контент. В двух словах, VitePress берёт ваш исходный контент, написанный в [Markdown](https://ru.wikipedia.org/wiki/Markdown), применяет к нему тему и генерирует статические HTML-страницы, которые можно легко развернуть в любом месте.
VitePress — это [Генератор статических сайтов](https://en.wikipedia.org/wiki/Static_site_generator) (ГСС), предназначенный для быстрого создания сайтов, ориентированных на контент. В двух словах, VitePress берёт ваш исходный контент, написанный на [Markdown](https://ru.wikipedia.org/wiki/Markdown), применяет к нему тему и генерирует статические HTML-страницы, которые можно легко развернуть в любом месте.
<div class="tip custom-block" style="padding-top: 8px">
@ -50,8 +50,8 @@ VitePress стремится обеспечить отличные возмож
## Что насчёт VuePress? {#what-about-vuepress}
VitePress — это духовный наследник VuePress. Оригинальный VuePress был основан на Vue 2 и webpack. Благодаря Vue 3 и Vite под капотом, VitePress обеспечивает значительно лучший опыт разработки, лучшую производительность, более отточенную тему по умолчанию и более гибкий API для настройки.
VitePress — это духовный преемник VuePress 1. Оригинальный VuePress 1 был основан на Vue 2 и webpack. С Vue 3 и Vite под капотом VitePress обеспечивает значительно лучший опыт разработки (DX), лучшую производительность в продакшене, более отточенную стандартную тему и более гибкий API для кастомизации.
Разница в API между VitePress и VuePress заключается в основном в тематическом оформлении и настройке. Если вы используете VuePress 1 с темой по умолчанию, то переход на VitePress будет относительно простым.
Различия в API между VitePress и VuePress 1 в основном касаются тем и кастомизации. Если вы используете VuePress 1 со стандартной темой, миграция на VitePress должна быть относительно простой.
Также были приложены усилия для создания VuePress 2, который также поддерживает Vue 3 и Vite с большей совместимостью с VuePress 1. Однако поддерживать два генератора параллельно не представляется возможным, поэтому команда Vue решила сосредоточиться на VitePress как основном рекомендуемом генераторе статических сайтов в долгосрочной перспективе.
Поддержка двух генераторов статических сайтов (SSG) параллельно не является устойчивой, поэтому команда Vue решила сосредоточиться на VitePress как на основном рекомендуемом SSG в долгосрочной перспективе. Теперь VuePress 1 признан устаревшим, а VuePress 2 передан команде сообщества VuePress для дальнейшей разработки и поддержки.

@ -24,7 +24,7 @@ export default {
}
```
:::details Динамическая (асинхронная) конфигурация
::: details Динамическая (асинхронная) конфигурация
Если вам нужно генерировать конфигурацию динамически, вы также можете экспортировать функцию по умолчанию. Например:

@ -5,7 +5,6 @@ const require = createRequire(import.meta.url)
const pkg = require('vitepress/package.json')
export default defineAdditionalConfig({
lang: 'zh-Hans',
description: '由 Vite 和 Vue 驱动的静态站点生成器',
themeConfig: {
@ -25,7 +24,7 @@ export default defineAdditionalConfig({
footer: {
message: '基于 MIT 许可发布',
copyright: `版权所有 © 2019-${new Date().getFullYear()} 尤雨溪`
copyright: '版权所有 © 2019-至今 尤雨溪'
},
docFooter: {

@ -14,7 +14,7 @@ VitePress 默认的主题已经针对文档进行了优化,并且可以进行
这些高级自定义配置将需要使用自定义主题来“拓展”默认主题。
:::tip
::: tip
在继续之前,请确保首先阅读[自定义主题](./custom-theme)以了解其工作原理。
:::

@ -18,23 +18,19 @@ VitePress 可以单独使用,也可以安装到现有项目中。在这两种
::: code-group
```sh [npm]
$ npm add -D vitepress
$ npm add -D vitepress@next
```
```sh [pnpm]
$ pnpm add -D vitepress
$ pnpm add -D vitepress@next
```
```sh [yarn]
$ yarn add -D vitepress
```
```sh [yarn (pnp)]
$ yarn add -D vitepress vue
$ yarn add -D vitepress@next vue
```
```sh [bun]
$ bun add -D vitepress
$ bun add -D vitepress@next
```
:::
@ -73,7 +69,7 @@ $ bun vitepress init
<<< @/snippets/init.ansi
:::tip Vue 作为 peer dependency
::: tip Vue 作为 peer dependency
如果打算使用 Vue 组件或 API 进行自定义,还应该明确地将 `vue` 安装为 dependency。
:::
@ -96,7 +92,7 @@ $ bun vitepress init
`docs` 目录作为 VitePress 站点的项目**根目录**。`.vitepress` 目录是 VitePress 配置文件、开发服务器缓存、构建输出和可选主题自定义代码的位置。
:::tip
::: tip
默认情况下VitePress 将其开发服务器缓存存储在 `.vitepress/cache` 中,并将生产构建输出存储在 `.vitepress/dist` 中。如果使用 Git应该将它们添加到 `.gitignore` 文件中。也可以手动[配置](../reference/site-config#outdir)这些位置。
:::

@ -255,11 +255,11 @@ Wraps in a `<div class="vp-raw">`
}
```
它在底层使用 [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector)。你可以像这样传递它的选项:
你可以像这样传递它的选项:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // 默认为 /base\.css/
includeFiles: [/custom\.css/] // 默认为 [/vp-doc\.css/, /base\.css/]
})
```

@ -123,7 +123,7 @@ src/getting-started.md --> /getting-started.html
## 生成简洁的 URL {#generating-clean-url}
:::warning 需要服务器支持
::: warning 需要服务器支持
要使 VitePress 提供简洁 URL需要服务器端支持。
:::

@ -67,7 +67,7 @@ The count is: {{ count }}
</style>
```
:::warning 避免在 Markdown 中使用 `<style scoped>`
::: warning 避免在 Markdown 中使用 `<style scoped>`
在 Markdown 中使用时,`<style scoped>` 需要为当前页面的每个元素添加特殊属性,这将显著增加页面的大小。当我们需要局部范围的样式时 `<style module>` 是首选。
:::

@ -50,8 +50,8 @@ VitePress 旨在使用 Markdown 生成内容时提供出色的开发体验。
## VuePress 又是什么? {#what-about-vuepress}
VitePress 灵感来源于 VuePress。最初的 VuePress 基于 Vue 2 和 webpack。借助 Vue 3 和 ViteVitePress 提供了更好的开发体验、更好的生产性能、更精美的默认主题和更灵活的自定义 API。
VitePress 灵感来源于 VuePress。最初的 VuePress 1 基于 Vue 2 和 webpack。VitePress 则基于 Vue 3 和 Vite 开发,提供了更好的开发体验、更好的生产性能、更精美的默认主题和更灵活的自定义 API。
VitePress 和 VuePress 之间的 API 区别主要在于主题和自定义。如果使用的是带有默认主题的 VuePress 1迁移到 VitePress 应该相对简单
VitePress 和 VuePress 1 的 API 区别主要在于主题和自定义。如果使用的是 VuePress 1 的默认主题应该可以很方便地迁移到 VitePress。
VuePress 2 我们也投入了精力,它也支持 Vue 3 和 Vite与 VuePress 1 的兼容性更好。但是,并行维护两个 SSG 是难以持续的,因此 Vue 团队决定将重点放在 VitePress作为长期的主要 SSG 选择推荐
并行维护两个 SSG 是难以持续的,因此 Vue 团队决定将 VitePress 作为长期维护并推荐的 SSG。现在 VuePress 1 已被弃用VuePress 2 已移交给 VuePress 社区团队进行进一步开发和维护

@ -24,7 +24,7 @@ export default {
}
```
:::details 异步的动态配置
::: details 异步的动态配置
如果需要动态生成配置,也可以默认导出一个函数,例如:

@ -1,6 +1,6 @@
{
"name": "vitepress",
"version": "2.0.0-alpha.9",
"version": "2.0.0-alpha.12",
"description": "Vite & Vue powered static site generator",
"keywords": [
"vite",
@ -95,34 +95,35 @@
"*": "prettier --experimental-cli --ignore-unknown --write"
},
"dependencies": {
"@docsearch/css": "^4.0.0-beta.5",
"@docsearch/js": "^4.0.0-beta.5",
"@iconify-json/simple-icons": "^1.2.44",
"@shikijs/core": "^3.8.1",
"@shikijs/transformers": "^3.8.1",
"@shikijs/types": "^3.8.1",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/devtools-api": "^7.7.7",
"@vue/shared": "^3.5.18",
"@vueuse/core": "^13.5.0",
"@vueuse/integrations": "^13.5.0",
"@docsearch/css": "^4.0.0-beta.8",
"@docsearch/js": "^4.0.0-beta.8",
"@iconify-json/simple-icons": "^1.2.49",
"@shikijs/core": "^3.12.0",
"@shikijs/transformers": "^3.12.0",
"@shikijs/types": "^3.12.0",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/devtools-api": "^8.0.1",
"@vue/shared": "^3.5.20",
"@vueuse/core": "^13.8.0",
"@vueuse/integrations": "^13.8.0",
"focus-trap": "^7.6.5",
"mark.js": "8.11.1",
"minisearch": "^7.1.2",
"shiki": "^3.8.1",
"vite": "^7.0.6",
"vue": "^3.5.18"
"shiki": "^3.12.0",
"vite": "^7.1.3",
"vue": "^3.5.20"
},
"devDependencies": {
"@clack/prompts": "^1.0.0-alpha.1",
"@iconify/utils": "^2.3.0",
"@mdit-vue/plugin-component": "^2.1.4",
"@mdit-vue/plugin-frontmatter": "^2.1.4",
"@mdit-vue/plugin-headers": "^2.1.4",
"@mdit-vue/plugin-sfc": "^2.1.4",
"@mdit-vue/plugin-title": "^2.1.4",
"@mdit-vue/plugin-toc": "^2.1.4",
"@mdit-vue/shared": "^2.1.4",
"@clack/prompts": "^1.0.0-alpha.4",
"@iconify/utils": "^3.0.1",
"@mdit-vue/plugin-component": "^3.0.2",
"@mdit-vue/plugin-frontmatter": "^3.0.2",
"@mdit-vue/plugin-headers": "^3.0.2",
"@mdit-vue/plugin-sfc": "^3.0.2",
"@mdit-vue/plugin-title": "^3.0.2",
"@mdit-vue/plugin-toc": "^3.0.2",
"@mdit-vue/shared": "^3.0.2",
"@polka/compression": "^1.0.0-next.28",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.6",
@ -134,67 +135,66 @@
"@types/fs-extra": "^11.0.4",
"@types/lodash.template": "^4.5.3",
"@types/mark.js": "^8.11.12",
"@types/markdown-it": "^14.1.2",
"@types/markdown-it-attrs": "^4.1.3",
"@types/markdown-it-container": "^2.0.10",
"@types/markdown-it-emoji": "^3.0.1",
"@types/minimist": "^1.2.5",
"@types/node": "^24.1.0",
"@types/picomatch": "^4.0.1",
"@types/postcss-prefix-selector": "^1.16.3",
"@types/node": "^24.3.0",
"@types/picomatch": "^4.0.2",
"@types/prompts": "^2.4.9",
"chokidar": "^4.0.3",
"conventional-changelog-cli": "^5.0.0",
"cross-spawn": "^7.0.6",
"debug": "^4.4.1",
"esbuild": "^0.25.8",
"esbuild": "^0.25.9",
"execa": "^9.6.0",
"fs-extra": "^11.3.0",
"fs-extra": "^11.3.1",
"get-port": "^7.1.0",
"gray-matter": "^4.0.3",
"lint-staged": "^16.1.2",
"lint-staged": "^16.1.5",
"lodash.template": "^4.5.0",
"lru-cache": "^11.1.0",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-async": "^2.2.0",
"markdown-it-attrs": "^4.3.1",
"markdown-it-cjk-friendly": "^1.2.0",
"markdown-it-container": "^4.0.0",
"markdown-it-emoji": "^3.0.0",
"markdown-it-mathjax3": "^4.3.2",
"minimist": "^1.2.8",
"nanoid": "^5.1.5",
"ora": "^8.2.0",
"oxc-minify": "^0.78.0",
"oxc-minify": "^0.82.3",
"p-map": "^7.0.3",
"package-directory": "^8.1.0",
"path-to-regexp": "^6.3.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.3",
"playwright-chromium": "^1.54.1",
"playwright-chromium": "^1.55.0",
"polka": "^1.0.0-next.28",
"postcss": "^8.5.6",
"postcss-prefix-selector": "^2.1.1",
"postcss-selector-parser": "^7.1.0",
"prettier": "^3.6.2",
"prompts": "^2.4.2",
"punycode": "^2.3.1",
"rimraf": "^6.0.1",
"rollup": "^4.45.1",
"rollup": "^4.49.0",
"rollup-plugin-dts": "6.1.1",
"rollup-plugin-esbuild": "^6.2.1",
"semver": "^7.7.2",
"simple-git-hooks": "^2.13.0",
"simple-git-hooks": "^2.13.1",
"sirv": "^3.0.1",
"sitemap": "^8.0.0",
"tinyglobby": "^0.2.14",
"typescript": "^5.8.3",
"vitest": "^4.0.0-beta.4",
"vue-tsc": "^3.0.4",
"typescript": "^5.9.2",
"vitest": "4.0.0-beta.4",
"vue-tsc": "^3.0.6",
"wait-on": "^8.0.4"
},
"peerDependencies": {
"markdown-it-mathjax3": "^4",
"oxc-minify": "^0.78.0",
"oxc-minify": "^0.82.3",
"postcss": "^8"
},
"peerDependenciesMeta": {
@ -208,5 +208,5 @@
"optional": true
}
},
"packageManager": "pnpm@10.13.1"
"packageManager": "pnpm@10.14.0"
}

File diff suppressed because it is too large Load Diff

@ -56,8 +56,7 @@ const esmBuild: RollupOptions = {
const typesExternal = [
...external,
/\/vitepress\/(?!(dist|node_modules|vitepress)\/).*\.d\.ts$/,
'source-map-js',
'fast-glob'
/^markdown-it(?:\/|$)/
]
const dtsNode = dts({

@ -57,18 +57,9 @@ export interface VitePressData<T = any> {
// site data is a singleton
export const siteDataRef: Ref<SiteData> = shallowRef(
(import.meta.env.PROD ? siteData : readonly(siteData)) as SiteData
readonly(siteData) as SiteData
)
// hmr
if (import.meta.hot) {
import.meta.hot.accept('@siteData', (m) => {
if (m) {
siteDataRef.value = m.default
}
})
}
// per-app data
export function initData(route: Route): VitePressData {
const site = computed(() =>

@ -54,7 +54,7 @@ export interface Router {
export const RouterSymbol: InjectionKey<Router> = Symbol()
// we are just using URL to parse the pathname and hash - the base doesn't
// matter and is only passed to support same-host hrefs.
// matter and is only passed to support same-host hrefs
const fakeHost = 'http://a.com'
const getDefaultRoute = (): Route => ({
@ -261,35 +261,57 @@ export function scrollTo(hash: string, smooth = false, scrollPosition = 0) {
return
}
let target: Element | null = null
let target: HTMLElement | null = null
try {
target = document.getElementById(decodeURIComponent(hash).slice(1))
} catch (e) {
console.warn(e)
}
if (!target) return
if (target) {
const targetPadding = parseInt(
window.getComputedStyle(target).paddingTop,
10
)
const targetTop =
window.scrollY +
const targetTop =
window.scrollY +
target.getBoundingClientRect().top -
getScrollOffset() +
targetPadding
Number.parseInt(window.getComputedStyle(target).paddingTop, 10) || 0
const behavior = window.matchMedia('(prefers-reduced-motion)').matches
? 'instant'
: // only smooth scroll if distance is smaller than screen height
smooth && Math.abs(targetTop - window.scrollY) <= window.innerHeight
? 'smooth'
: 'auto'
const scrollToTarget = () => {
window.scrollTo({ left: 0, top: targetTop, behavior })
// focus the target element for better accessibility
target.focus({ preventScroll: true })
function scrollToTarget() {
// only smooth scroll if distance is smaller than screen height.
if (!smooth || Math.abs(targetTop - window.scrollY) > window.innerHeight)
window.scrollTo(0, targetTop)
else window.scrollTo({ left: 0, top: targetTop, behavior: 'smooth' })
// return if focus worked
if (document.activeElement === target) return
// element has tabindex already, likely not focusable
// because of some other reason, bail out
if (target.hasAttribute('tabindex')) return
const restoreTabindex = () => {
target.removeAttribute('tabindex')
target.removeEventListener('blur', restoreTabindex)
}
requestAnimationFrame(scrollToTarget)
// temporarily make the target element focusable
target.setAttribute('tabindex', '-1')
target.addEventListener('blur', restoreTabindex)
// try to focus again
target.focus({ preventScroll: true })
// remove tabindex and event listener if focus still not worked
if (document.activeElement !== target) restoreTabindex()
}
requestAnimationFrame(scrollToTarget)
}
function handleHMR(route: Route): void {
@ -313,7 +335,7 @@ function shouldHotReload(payload: PageDataPayload): boolean {
function normalizeHref(href: string): string {
const url = new URL(href, fakeHost)
url.pathname = url.pathname.replace(/(^|\/)index(\.html)?$/, '$1')
// ensure correct deep link so page refresh lands on correct files.
// ensure correct deep link so page refresh lands on correct files
if (siteDataRef.value.cleanUrls) {
url.pathname = url.pathname.replace(/\.html$/, '')
} else if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {

@ -78,7 +78,7 @@ function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
}
})
docsearch(options)
docsearch(options as any)
}
function getRelativePath(url: string) {

@ -1,26 +1,38 @@
<script setup lang="ts">
import { ref, computed, watchEffect, onMounted } from 'vue'
import { useNavigatorLanguage } from '@vueuse/core'
import { computed, onMounted, shallowRef, useTemplateRef, watchEffect } from 'vue'
import { useData } from '../composables/data'
const { theme, page, lang } = useData()
const { theme, page, lang: pageLang } = useData()
const { language: browserLang } = useNavigatorLanguage()
const date = computed(
() => new Date(page.value.lastUpdated!)
)
const timeRef = useTemplateRef('timeRef')
const date = computed(() => new Date(page.value.lastUpdated!))
const isoDatetime = computed(() => date.value.toISOString())
const datetime = ref('')
const datetime = shallowRef('')
// set time on mounted hook to avoid hydration mismatch due to
// potential differences in timezones of the server and clients
onMounted(() => {
watchEffect(() => {
const lang = theme.value.lastUpdated?.formatOptions?.forceLocale
? pageLang.value
: browserLang.value
datetime.value = new Intl.DateTimeFormat(
theme.value.lastUpdated?.formatOptions?.forceLocale ? lang.value : undefined,
lang,
theme.value.lastUpdated?.formatOptions ?? {
dateStyle: 'short',
timeStyle: 'short'
dateStyle: 'medium',
timeStyle: 'medium'
}
).format(date.value)
if (lang && pageLang.value !== lang) {
timeRef.value?.setAttribute('lang', lang)
} else {
timeRef.value?.removeAttribute('lang')
}
})
})
</script>
@ -28,7 +40,7 @@ onMounted(() => {
<template>
<p class="VPLastUpdated">
{{ theme.lastUpdated?.text || theme.lastUpdatedText || 'Last updated' }}:
<time :datetime="isoDatetime">{{ datetime }}</time>
<time ref="timeRef" :datetime="isoDatetime">{{ datetime }}</time>
</p>
</template>

@ -5,18 +5,12 @@ defineProps<{
headers: DefaultTheme.OutlineItem[]
root?: boolean
}>()
function onClick({ target: el }: Event) {
const id = (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.getElementById(decodeURIComponent(id))
heading?.focus({ preventScroll: true })
}
</script>
<template>
<ul class="VPDocOutlineItem" :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title } in headers">
<a class="outline-link" :href="link" @click="onClick" :title>
<a class="outline-link" :href="link" :title>
{{ title }}
</a>
<template v-if="children?.length">

@ -32,7 +32,7 @@ const classes = computed(() => {
VPLocalNav: true,
'has-sidebar': hasSidebar.value,
empty: !hasLocalNav.value,
fixed: !hasLocalNav.value && !hasSidebar.value,
fixed: !hasLocalNav.value && !hasSidebar.value
}
})
</script>
@ -113,7 +113,6 @@ const classes = computed(() => {
.menu {
display: flex;
align-items: center;
padding: 12px 24px 11px;
line-height: 24px;
font-size: 12px;
font-weight: 500;
@ -126,12 +125,6 @@ const classes = computed(() => {
transition: color 0.25s;
}
@media (min-width: 768px) {
.menu {
padding: 0 32px;
}
}
@media (min-width: 960px) {
.menu {
display: none;
@ -143,12 +136,14 @@ const classes = computed(() => {
font-size: 14px;
}
.VPOutlineDropdown {
.menu,
:deep(.VPLocalNavOutlineDropdown > button) {
padding: 12px 24px 11px;
}
@media (min-width: 768px) {
.VPOutlineDropdown {
.menu,
:deep(.VPLocalNavOutlineDropdown > button) {
padding: 12px 32px 11px;
}
}

@ -92,16 +92,6 @@ function scrollToTop() {
</template>
<style scoped>
.VPLocalNavOutlineDropdown {
padding: 12px 20px 11px;
}
@media (min-width: 960px) {
.VPLocalNavOutlineDropdown {
padding: 12px 36px 11px;
}
}
.VPLocalNavOutlineDropdown button {
display: block;
font-size: 12px;

@ -9,11 +9,14 @@ defineProps<{
}>()
const { page } = useData()
defineOptions({ inheritAttrs: false })
</script>
<template>
<div class="VPMenuLink">
<VPLink
v-bind="$attrs"
:class="{
active: isActive(
page.relativePath,
@ -46,6 +49,7 @@ const { page } = useData()
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
text-align: left;
white-space: nowrap;
transition:
background-color 0.25s,

@ -170,16 +170,22 @@ watchPostEffect(() => {
.VPNavBar.has-sidebar .content {
position: relative;
z-index: 1;
padding-right: 32px;
padding-left: var(--vp-sidebar-width);
}
.VPNavBar.has-sidebar .content-body {
padding-right: 32px;
}
}
@media (min-width: 1440px) {
.VPNavBar.has-sidebar .content {
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
.VPNavBar.has-sidebar .content-body {
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);
}
}
.content-body {

@ -31,7 +31,7 @@ const hasExtraContent = computed(
<p class="trans-title">{{ currentLang.label }}</p>
<template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" />
<VPMenuLink :item="locale" :lang="locale.lang" :dir="locale.dir" />
</template>
</div>

@ -19,7 +19,7 @@ const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
<p class="title">{{ currentLang.label }}</p>
<template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" />
<VPMenuLink :item="locale" :lang="locale.lang" :dir="locale.dir" />
</template>
</div>
</VPFlyout>

@ -25,7 +25,14 @@ function toggle() {
<ul class="list">
<li v-for="locale in localeLinks" :key="locale.link" class="item">
<VPLink class="link" :href="locale.link">{{ locale.text }}</VPLink>
<VPLink
class="link"
:href="locale.link"
:lang="locale.lang"
:dir="locale.dir"
>
{{ locale.text }}
</VPLink>
</li>
</ul>
</div>

@ -111,8 +111,8 @@ watch(
@media (min-width: 1440px) {
.VPSidebar {
padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
padding-left: max(32px, calc((100vw - (var(--vp-layout-max-width) - 64px)) / 2));
width: calc((100vw - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
}
}

@ -8,39 +8,18 @@ const route = useRoute()
const backToTop = ref()
watch(() => route.path, () => backToTop.value.focus())
function focusOnTargetAnchor({ target }: Event) {
const el = document.getElementById(
decodeURIComponent((target as HTMLAnchorElement).hash).slice(1)
)
if (el) {
const removeTabIndex = () => {
el.removeAttribute('tabindex')
el.removeEventListener('blur', removeTabIndex)
}
el.setAttribute('tabindex', '-1')
el.addEventListener('blur', removeTabIndex)
el.focus()
window.scrollTo(0, 0)
}
}
</script>
<template>
<span ref="backToTop" tabindex="-1" />
<a
href="#VPContent"
class="VPSkipLink visually-hidden"
@click="focusOnTargetAnchor"
>
<a href="#VPContent" class="VPSkipLink visually-hidden">
{{ theme.skipToContentLabel || 'Skip to content' }}
</a>
</template>
<style scoped>
.VPSkipLink {
position: fixed;
top: 8px;
left: 8px;
padding: 8px 16px;

@ -25,7 +25,9 @@ export function useLangs({ correspondingLink = false } = {}) {
currentLang.value.link.length - 1
),
!site.value.cleanUrls
) + hash.value
) + hash.value,
lang: value.lang,
dir: value.dir
}
)
)

@ -23,7 +23,9 @@ export function getHeaders(
range: DefaultTheme.Config['outline']
): DefaultTheme.OutlineItem[] {
const headers = [
...document.querySelectorAll('.VPDoc :where(h1,h2,h3,h4,h5,h6)')
...document.querySelectorAll(
'.VPDoc h1, .VPDoc h2, .VPDoc h3, .VPDoc h4, .VPDoc h5, .VPDoc h6'
)
]
.filter((el) => el.id && el.hasChildNodes())
.map((el) => {

@ -226,7 +226,7 @@
}
.vp-doc .custom-block div[class*='language-'] {
margin: 8px 0;
margin: 8px 0 !important;
border-radius: 8px;
}

@ -88,6 +88,6 @@
:root {
/* clipboard */
--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/g%3E%3C/svg%3E");
/* clipboard-copy */
--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2M16 4h2a2 2 0 0 1 2 2v4m1 4H11'/%3E%3Cpath d='m15 10l-4 4l4 4'/%3E%3C/g%3E%3C/svg%3E");
/* clipboard-check */
--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14l2 2l4-4'/%3E%3C/g%3E%3C/svg%3E");
}

@ -2,7 +2,6 @@ import { createRequire } from 'node:module'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Alias, AliasOptions } from 'vite'
import type { SiteConfig } from './config'
const require = createRequire(import.meta.url)
const PKG_ROOT = resolve(fileURLToPath(import.meta.url), '../..')
@ -20,20 +19,8 @@ export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
const vueRuntimePath = 'vue/dist/vue.runtime.esm-bundler.js'
export function resolveAliases(
{ root, themeDir }: SiteConfig,
ssr: boolean
): AliasOptions {
const paths: Record<string, string> = {
'@theme': themeDir,
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
}
export function resolveAliases(root: string, ssr: boolean): AliasOptions {
const aliases: Alias[] = [
...Object.keys(paths).map((p) => ({
find: p,
replacement: paths[p]
})),
{
find: /^vitepress$/,
replacement: join(DIST_CLIENT_PATH, '/index.js')

@ -1,9 +1,16 @@
import minimist from 'minimist'
import c from 'picocolors'
import { createLogger, type Logger } from 'vite'
import { build, createServer, serve } from '.'
import {
build,
createServer,
disposeMdItInstance,
resolveConfig,
serve
} from '.'
import { version } from '../../package.json'
import { init } from './init/init'
import { clearCache } from './markdownToVue'
import { bindShortcuts } from './shortcuts'
if (process.env.DEBUG) {
@ -40,30 +47,38 @@ if (!command || command === 'dev') {
argv.optimizeDeps = { force: true }
}
let config = await resolveConfig(root, argv).catch(
logErrorAndExit.bind(null, `failed to resolve config. error:`)
)
const createDevServer = async (isRestart = true) => {
const server = await createServer(root, argv, async () => {
const server = await createServer(root, argv, restartServer, config)
function restartServer() {
if (!restartPromise) {
restartPromise = (async () => {
try {
config = await resolveConfig(root, argv)
} catch (err: any) {
logError(`failed to resolve config. error:`, err)
return
}
disposeMdItInstance()
clearCache()
await server.close()
await createDevServer()
})().finally(() => {
restartPromise = undefined
})
}
return restartPromise
})
}
await server.listen(undefined, isRestart)
logVersion(server.config.logger)
server.printUrls()
bindShortcuts(server, createDevServer)
bindShortcuts(server, restartServer)
}
createDevServer(false).catch((err) => {
createLogger().error(
`${c.red(`failed to start server. error:`)}\n${err.message}\n${err.stack}`
)
process.exit(1)
})
createDevServer(false).catch(
logErrorAndExit.bind(null, `failed to start server. error:`)
)
} else if (command === 'init') {
createLogger().info('', { clear: true })
init(argv.root)
@ -74,21 +89,30 @@ if (!command || command === 'dev') {
onAfterConfigResolve(siteConfig) {
logVersion(siteConfig.logger)
}
}).catch((err) => {
createLogger().error(
`${c.red(`build error:`)}\n${err.message}\n${err.stack}`
)
process.exit(1)
})
}).catch(logErrorAndExit.bind(null, `build error:`))
} else if (command === 'serve' || command === 'preview') {
serve(argv).catch((err) => {
createLogger().error(
`${c.red(`failed to start server. error:`)}\n${err.message}\n${err.stack}`
)
process.exit(1)
})
serve(argv).catch(
logErrorAndExit.bind(null, `failed to start server. error:`)
)
} else {
createLogger().error(c.red(`unknown command "${command}".`))
process.exit(1)
logErrorAndExit(`unknown command "${command}".`)
}
}
function logErrorAndExit(message: string, err?: any): never {
logError(message, err)
process.exit(1)
}
function logError(message: string, err?: any) {
const logger = createLogger()
logger.error(
[
c.red(message),
err && 'message' in err && err.message,
err && 'stack' in err && err.stack
]
.filter(Boolean)
.join('\n')
)
}

@ -168,7 +168,7 @@ export async function resolveConfig(
global.VITEPRESS_CONFIG = config
// resolve pages after setting global, so that path loaders can access it
Object.assign(config, await resolvePages(srcDir, userConfig, logger, true))
await resolvePages(config, true)
return config as SiteConfig
}
@ -233,7 +233,7 @@ export async function resolveUserConfig(
root: string,
command: 'serve' | 'build',
mode: string
): Promise<[UserConfig, string | undefined, string[]]> {
): Promise<[UserConfig, configPath: string | undefined, configDeps: string[]]> {
// load user config
const configPath = supportedConfigExtensions
.flatMap((ext) => [

@ -22,6 +22,7 @@ import type {
import anchorPlugin from 'markdown-it-anchor'
import { MarkdownItAsync, type Options } from 'markdown-it-async'
import attrsPlugin from 'markdown-it-attrs'
import mditCjkFriendly from 'markdown-it-cjk-friendly'
import { full as emojiPlugin } from 'markdown-it-emoji'
import type { BuiltinLanguage, BuiltinTheme, Highlighter } from 'shiki'
import type { Logger } from 'vite'
@ -188,6 +189,13 @@ export interface MarkdownOptions extends Options {
* @see https://vitepress.dev/guide/markdown#github-flavored-alerts
*/
gfmAlerts?: boolean
/**
* Allows disabling the CJK-friendly plugin.
* This plugin adds support for emphasis marks (**bold**) in Japanese, Chinese, and Korean text.
* @default true
* @see https://github.com/tats-u/markdown-cjk-friendly
*/
cjkFriendly?: boolean
}
export type MarkdownRenderer = MarkdownItAsync
@ -320,7 +328,11 @@ export async function createMarkdownRenderer(
.use(titlePlugin)
.use(tocPlugin, {
slugify,
...options.toc
...options.toc,
format: (s) => {
const title = s.replaceAll('&amp;', '&') // encoded twice because of restoreEntities
return options.toc?.format?.(title) ?? title
}
} as TocPluginOptions)
if (options.math) {
@ -348,6 +360,10 @@ export async function createMarkdownRenderer(
}
}
if (options.cjkFriendly !== false) {
md.use(mditCjkFriendly)
}
// apply user config
if (options.config) {
await options.config(md)

@ -32,14 +32,14 @@ export interface MarkdownCompileResult {
includes: string[]
}
export function clearCache(id?: string) {
if (!id) {
export function clearCache(relativePath?: string) {
if (!relativePath) {
cache.clear()
return
}
id = JSON.stringify({ id }).slice(1)
cache.find((_, key) => key.endsWith(id!) && cache.delete(key))
relativePath = JSON.stringify({ relativePath }).slice(1)
cache.find((_, key) => key.endsWith(relativePath!) && cache.delete(key))
}
let __pages: string[] = []
@ -114,12 +114,7 @@ export async function createMarkdownToVueRenderFn(
file = rewrites.get(file) || file
const relativePath = slash(path.relative(srcDir, file))
const cacheKey = JSON.stringify({
src,
ts,
file: relativePath,
id: fileOrig
})
const cacheKey = JSON.stringify({ src, ts, relativePath })
if (isBuild || options.cache !== false) {
const cached = cache.get(cacheKey)
if (cached) {
@ -167,7 +162,7 @@ export async function createMarkdownToVueRenderFn(
// validate data.links
const deadLinks: MarkdownCompileResult['deadLinks'] = []
const recordDeadLink = (url: string) => {
deadLinks.push({ url, file: path.relative(srcDir, fileOrig) })
deadLinks.push({ url, file: fileOrig })
}
function shouldIgnoreDeadLink(url: string) {

@ -4,6 +4,7 @@ import {
mergeConfig,
normalizePath,
searchForWorkspaceRoot,
type EnvironmentModuleNode,
type Plugin,
type ResolvedConfig,
type Rollup,
@ -13,16 +14,11 @@ import {
APP_PATH,
DEFAULT_THEME_PATH,
DIST_CLIENT_PATH,
SITE_DATA_ID,
SITE_DATA_REQUEST_PATH,
resolveAliases
} from './alias'
import {
isAdditionalConfigFile,
resolvePages,
resolveUserConfig,
type SiteConfig
} from './config'
import { disposeMdItInstance } from './markdown/markdown'
import { isAdditionalConfigFile, resolvePages, type SiteConfig } from './config'
import {
clearCache,
createMarkdownToVueRenderFn,
@ -42,7 +38,8 @@ declare module 'vite' {
}
}
const themeRE = /\/\.vitepress\/theme\/index\.(m|c)?(j|t)s$/
const themeRE = /(?:^|\/)\.vitepress\/theme\/index\.(m|c)?(j|t)s$/
const startsWithThemeRE = /^@theme(?:\/|$)/
const docsearchRE = /\/@docsearch\/css\/dist\/style.css(?:$|\?)/
const hashRE = /\.([-\w]+)\.js$/
@ -73,7 +70,7 @@ export async function createVitePressPlugin(
ssr = false,
pageToHashMap?: Record<string, string>,
clientJSMap?: Record<string, string>,
recreateServer?: () => Promise<void>
restartServer?: () => Promise<void>
) {
const {
srcDir,
@ -130,7 +127,7 @@ export async function createVitePressPlugin(
config() {
const baseConfig: UserConfig = {
resolve: {
alias: resolveAliases(siteConfig, ssr)
alias: resolveAliases(siteConfig.root, ssr)
},
define: {
__VP_LOCAL_SEARCH__: site.themeConfig?.search?.provider === 'local',
@ -146,10 +143,7 @@ export async function createVitePressPlugin(
include: [
'vue',
'vitepress > @vue/devtools-api',
'vitepress > @vueuse/core',
siteConfig.themeDir === DEFAULT_THEME_PATH
? '@theme/index'
: undefined
'vitepress > @vueuse/core'
].filter((d) => d != null),
exclude: ['@docsearch/js', 'vitepress']
},
@ -169,10 +163,17 @@ export async function createVitePressPlugin(
: baseConfig
},
resolveId(id) {
if (id === SITE_DATA_REQUEST_PATH) {
resolveId(id, importer, resolveOptions) {
if (id === SITE_DATA_ID) {
return SITE_DATA_REQUEST_PATH
}
if (startsWithThemeRE.test(id)) {
return this.resolve(
siteConfig.themeDir + id.slice(6),
importer,
Object.assign({ skipSelf: true }, resolveOptions)
)
}
},
load(id) {
@ -200,6 +201,7 @@ export async function createVitePressPlugin(
return processClientJS(code, id)
}
if (id.endsWith('.md')) {
const relativePath = path.posix.relative(srcDir, id)
// transform .md files into vueSrc so plugin-vue can handle it
const { vueSrc, deadLinks, includes, pageData } = await markdownToVue(
code,
@ -209,7 +211,7 @@ export async function createVitePressPlugin(
allDeadLinks.push(...deadLinks)
if (includes.length) {
includes.forEach((i) => {
;(importerMap[slash(i)] ??= new Set()).add(id)
;(importerMap[slash(i)] ??= new Set()).add(relativePath)
this.addWatchFile(i)
})
}
@ -217,7 +219,6 @@ export async function createVitePressPlugin(
this.environment.mode === 'dev' &&
this.environment.name === 'client'
) {
const relativePath = path.posix.relative(srcDir, id)
const payload: PageDataPayload = {
path: `/${siteConfig.rewrites.map[relativePath] || relativePath}`,
pageData
@ -259,40 +260,6 @@ export async function createVitePressPlugin(
configDeps.forEach((file) => server.watcher.add(file))
}
const onFileAddDelete = async (added: boolean, _file: string) => {
const file = slash(_file)
// restart server on theme file creation / deletion
if (themeRE.test(file)) {
siteConfig.logger.info(
c.green(
`${path.relative(process.cwd(), _file)} ${added ? 'created' : 'deleted'}, restarting server...\n`
),
{ clear: true, timestamp: true }
)
await recreateServer?.()
}
// update pages, dynamicRoutes and rewrites on md file creation / deletion
if (file.endsWith('.md')) {
Object.assign(
siteConfig,
await resolvePages(
siteConfig.srcDir,
siteConfig.userConfig,
siteConfig.logger
)
)
}
if (!added && importerMap[file]) {
delete importerMap[file]
}
}
server.watcher
.on('add', onFileAddDelete.bind(null, true))
.on('unlink', onFileAddDelete.bind(null, false))
// serve our index.html after vite history fallback
return () => {
server.middlewares.use(async (req, res, next) => {
@ -377,8 +344,18 @@ export async function createVitePressPlugin(
}
},
async hotUpdate({ file }) {
async hotUpdate({ file, type }) {
if (this.environment.name !== 'client') return
const relativePath = path.posix.relative(srcDir, file)
// update pages, dynamicRoutes and rewrites on md file creation / deletion
if (file.endsWith('.md') && type !== 'update') {
await resolvePages(siteConfig)
}
if (type === 'delete') {
delete importerMap[relativePath]
}
if (
file === configPath ||
@ -392,36 +369,43 @@ export async function createVitePressPlugin(
{ clear: true, timestamp: true }
)
try {
await resolveUserConfig(siteConfig.root, 'serve', 'development')
} catch (err: any) {
siteConfig.logger.error(err)
return
}
return restartServer?.()
}
disposeMdItInstance()
clearCache()
await recreateServer?.()
return
if (themeRE.test(relativePath) && type !== 'update') {
siteConfig.themeDir =
type === 'create' ? path.posix.dirname(file) : DEFAULT_THEME_PATH
siteConfig.logger.info(c.green('page reload ') + c.dim(relativePath), {
clear: true,
timestamp: true
})
this.environment.moduleGraph.invalidateAll()
this.environment.hot.send({ type: 'full-reload' })
return []
}
}
}
const hmrFix: Plugin = {
name: 'vitepress:hmr-fix',
async hotUpdate({ file, modules }) {
async hotUpdate({ file, modules: existingMods }) {
if (this.environment.name !== 'client') return
const modules: EnvironmentModuleNode[] = []
const importers = [...(importerMap[slash(file)] || [])]
if (importers.length > 0) {
return [
...modules,
...importers.map((id) => {
clearCache(id)
return this.environment.moduleGraph.getModuleById(id)
})
].filter((mod) => mod !== undefined)
if (file.endsWith('.md')) {
const mod = this.environment.moduleGraph.getModuleById(file)
mod && modules.push(mod)
}
importerMap[slash(file)]?.forEach((relativePath) => {
clearCache(relativePath)
const mod = this.environment.moduleGraph.getModuleById(
path.posix.join(srcDir, relativePath)
)
mod && modules.push(mod)
})
return modules.length ? [...existingMods, ...modules] : undefined
}
}

@ -5,6 +5,7 @@ import pm from 'picomatch'
import {
loadConfigFromFile,
normalizePath,
type EnvironmentModuleGraph,
type EnvironmentModuleNode,
type Logger,
type Plugin
@ -61,6 +62,7 @@ const pathLoaderRE = /\.paths\.m?[jt]s$/
const routeModuleCache = new Map<string, ResolvedRouteModule>()
let moduleGraph = new ModuleGraph()
let discoveredPages = new Set<string>()
/**
* Helper for defining routes with type inference
@ -69,20 +71,21 @@ export function defineRoutes(loader: RouteModule): RouteModule {
return loader
}
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
export async function resolvePages(
srcDir: string,
userConfig: UserConfig,
logger: Logger,
siteConfig: Optional<SiteConfig, 'pages' | 'dynamicRoutes' | 'rewrites'>,
rebuildCache = false
): Promise<Pick<SiteConfig, 'pages' | 'dynamicRoutes' | 'rewrites'>> {
): Promise<void> {
if (rebuildCache) {
moduleGraph = new ModuleGraph()
routeModuleCache.clear()
discoveredPages.clear()
}
const allMarkdownFiles = await glob(['**/*.md'], {
cwd: srcDir,
ignore: userConfig.srcExclude
cwd: siteConfig.srcDir,
ignore: siteConfig.userConfig.srcExclude
})
const pages: string[] = []
@ -94,22 +97,33 @@ export async function resolvePages(
})
const dynamicRoutes = await resolveDynamicRoutes(
srcDir,
siteConfig.srcDir,
dynamicRouteFiles,
logger
siteConfig.logger
)
pages.push(...dynamicRoutes.map((r) => r.path))
const rewrites = resolveRewrites(pages, userConfig.rewrites)
const externalDynamicRoutes =
siteConfig.dynamicRoutes?.filter((r) => !discoveredPages.has(r.path)) || []
const externalPages =
siteConfig.pages?.filter((p) => !discoveredPages.has(p)) || []
return {
pages,
dynamicRoutes,
const finalDynamicRoutes = [...dynamicRoutes, ...externalDynamicRoutes].sort(
(a, b) => a.path.localeCompare(b.path)
)
const finalPages = [...pages, ...externalPages].sort()
const rewrites = resolveRewrites(pages, siteConfig.userConfig.rewrites)
Object.assign(siteConfig, {
pages: finalPages,
dynamicRoutes: finalDynamicRoutes,
rewrites,
// @ts-expect-error internal flag to reload resolution cache in ../markdownToVue.ts
__dirty: true
}
} satisfies Partial<SiteConfig>)
discoveredPages = new Set(pages)
}
export const dynamicRoutesPlugin = async (
@ -117,6 +131,7 @@ export const dynamicRoutesPlugin = async (
): Promise<Plugin> => {
return {
name: 'vitepress:dynamic-routes',
enforce: 'pre',
resolveId(id) {
if (!id.endsWith('.md')) return
@ -164,11 +179,7 @@ export const dynamicRoutesPlugin = async (
const normalizedFile = normalizePath(file)
// Trigger update if a module or its dependencies changed.
for (const id of moduleGraph.delete(normalizedFile)) {
routeModuleCache.delete(id)
const mod = this.environment.moduleGraph.getModuleById(id)
if (mod) modules.push(mod)
}
modules.push(...getModules(normalizedFile, this.environment.moduleGraph))
// Also check if the file matches any custom watch patterns.
let watchedFileChanged = false
@ -179,11 +190,7 @@ export const dynamicRoutesPlugin = async (
) {
route.routes = undefined
watchedFileChanged = true
for (const id of moduleGraph.delete(file)) {
const mod = this.environment.moduleGraph.getModuleById(id)
if (mod) modules.push(mod)
}
modules.push(...getModules(file, this.environment.moduleGraph, false))
}
}
@ -193,10 +200,7 @@ export const dynamicRoutesPlugin = async (
pathLoaderRE.test(normalizedFile)
) {
// path loader module or deps updated, reset loaded routes
Object.assign(
config,
await resolvePages(config.srcDir, config.userConfig, config.logger)
)
await resolvePages(config)
}
return modules.length ? [...existingMods, ...modules] : undefined
@ -345,3 +349,16 @@ async function resolveDynamicRoutes(
return resolvedRoutes
}
function getModules(
id: string,
envModuleGraph: EnvironmentModuleGraph,
deleteFromRouteModuleCache = true
) {
const modules: EnvironmentModuleNode[] = []
for (const file of moduleGraph.delete(id)) {
deleteFromRouteModuleCache && routeModuleCache.delete(file)
modules.push(...(envModuleGraph.getModulesByFile(file)?.values() ?? []))
}
return modules
}

@ -30,7 +30,7 @@ export async function localSearchPlugin(
name: 'vitepress:local-search',
resolveId(id) {
if (id.startsWith(LOCAL_SEARCH_INDEX_ID)) {
return `/${id}`
return LOCAL_SEARCH_INDEX_REQUEST_PATH
}
},
load(id) {
@ -183,7 +183,7 @@ export async function localSearchPlugin(
records.push(
`${JSON.stringify(
locale
)}: () => import('@localSearchIndex${locale}')`
)}: () => import('${LOCAL_SEARCH_INDEX_ID}${locale}')`
)
}
return `export default {${records.join(',')}}`

@ -1,21 +1,38 @@
import postcssPrefixSelector from 'postcss-prefix-selector'
import type { Plugin } from 'postcss'
import selectorParser from 'postcss-selector-parser'
export function postcssIsolateStyles(
options: Parameters<typeof postcssPrefixSelector>[0] = {}
): ReturnType<typeof postcssPrefixSelector> {
return postcssPrefixSelector({
prefix: ':not(:where(.vp-raw, .vp-raw *))',
includeFiles: [/base\.css/],
transform(prefix, _selector) {
// split selector from its pseudo part if the trailing colon is not escaped
const [selector, pseudo] = splitSelectorPseudo(_selector)
return selector + prefix + pseudo
},
...options
})
type Options = {
includeFiles?: RegExp[]
ignoreFiles?: RegExp[]
prefix?: string
}
export function splitSelectorPseudo(selector: string): [string, string] {
const [base, pseudo = ''] = selector.split(/(?<!\\)(:\S*)$/)
return [base, pseudo]
export function postcssIsolateStyles({
includeFiles = [/vp-doc\.css/, /base\.css/],
ignoreFiles,
prefix = ':not(:where(.vp-raw, .vp-raw *))'
}: Options = {}): Plugin {
const prefixNodes = selectorParser().astSync(prefix).first.nodes
return /* prettier-ignore */ {
postcssPlugin: 'postcss-isolate-styles',
Once(root) {
const file = root.source?.input.file
if (file && includeFiles?.length && !includeFiles.some((re) => re.test(file))) return
if (file && ignoreFiles?.length && ignoreFiles.some((re) => re.test(file))) return
root.walkRules((rule) => {
if (!rule.selector || rule.selector.includes(prefix)) return
if (rule.parent?.type === 'atrule' && /\bkeyframes$/i.test(rule.parent.name)) return
rule.selector = selectorParser((selectors) => {
selectors.each((sel) => {
if (!sel.nodes.length) return
const insertionIndex = sel.nodes.findLastIndex((n) => n.type !== 'pseudo') + 1
sel.nodes.splice(insertionIndex, 0, ...prefixNodes.map((n) => n.clone() as any))
})
}).processSync(rule.selector)
})
}
}
}

@ -1,25 +1,24 @@
import { createServer as createViteServer, type ServerOptions } from 'vite'
import { resolveConfig } from './config'
import { resolveConfig, type SiteConfig } from './config'
import { createVitePressPlugin } from './plugin'
export async function createServer(
root: string = process.cwd(),
root: string = process.cwd(), // for backwards compatibility
serverOptions: ServerOptions & { base?: string } = {},
recreateServer?: () => Promise<void>
restartServer?: () => Promise<void>,
config?: SiteConfig // new code should pass config directly
) {
const config = await resolveConfig(root)
config ??= await resolveConfig(root)
if (serverOptions.base) {
config.site.base = serverOptions.base
delete serverOptions.base
}
const { base, ...server } = serverOptions
config.site.base = base ?? config.site.base
return createViteServer({
root: config.srcDir,
base: config.site.base,
cacheDir: config.cacheDir,
plugins: await createVitePressPlugin(config, false, {}, {}, recreateServer),
server: serverOptions,
plugins: await createVitePressPlugin(config, false, {}, {}, restartServer),
server,
customLogger: config.logger,
configFile: config.vite?.configFile
})

@ -1,22 +1,19 @@
import c from 'picocolors'
import type { ViteDevServer } from 'vite'
import { disposeMdItInstance } from './markdown/markdown'
import { clearCache } from './markdownToVue'
type CreateDevServer = () => Promise<void>
import type { Awaitable } from './shared'
export type CLIShortcut = {
key: string
description: string
action(
server: ViteDevServer,
createDevServer: CreateDevServer
): void | Promise<void>
restartServer: () => Promise<void>
): Awaitable<void>
}
export function bindShortcuts(
server: ViteDevServer,
createDevServer: CreateDevServer
restartServer: () => Promise<void>
): void {
if (!server.httpServer || !process.stdin.isTTY || process.env.CI) {
return
@ -59,7 +56,7 @@ export function bindShortcuts(
if (!shortcut) return
actionRunning = true
await shortcut.action(server, createDevServer)
await shortcut.action(server, restartServer)
actionRunning = false
}
@ -77,15 +74,12 @@ const SHORTCUTS: CLIShortcut[] = [
{
key: 'r',
description: 'restart the server',
async action(server, createDevServer) {
async action(server, restartServer) {
server.config.logger.info(c.green(`restarting server...\n`), {
clear: true,
timestamp: true
})
disposeMdItInstance()
clearCache()
await server.close()
await createDevServer()
await restartServer()
}
},
{

@ -54,6 +54,8 @@ export class ModuleGraph {
*/
delete(module: string): Set<string> {
const deleted = new Set<string>()
if (!this.nodes.has(module)) return deleted
const stack: string[] = [module]
// Traverse the reverse dependency graph (using dependents).

Loading…
Cancel
Save