Merge branch 'main' into fix-local-search-breakpoint-5213

pull/5217/head
Divyansh Singh 23 hours ago committed by GitHub
commit def89717fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -19,7 +19,7 @@ Hi! We're really excited that you are interested in contributing to VitePress. B
- It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging.
- Commit messages must follow the [commit message convention](./commit-convention.md) so that changelogs can be automatically generated.
- Commit messages must follow the [commit message convention](/.github/commit-convention.md) so that changelogs can be automatically generated.
## Development Setup

@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node_version: [20, 22, 24, latest]
node_version: [22, 24, 26]
include:
- os: windows-latest
node_version: 24

@ -1,3 +1,46 @@
## [2.0.0-alpha.18](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.17...v2.0.0-alpha.18) (2026-07-06)
### Bug Fixes
- **build:** apply `base` to links with download attribute ([#5186](https://github.com/vuejs/vitepress/issues/5186)) ([01987c4](https://github.com/vuejs/vitepress/commit/01987c4c0838478a8fda81f427775f28476c5557))
- **build:** normalize rewrite drive letters ([#5245](https://github.com/vuejs/vitepress/issues/5245)) ([5c50b99](https://github.com/vuejs/vitepress/commit/5c50b99724815a6fb3d2311e801dcad4aeb9b412))
- **build:** show dead link line numbers ([#5230](https://github.com/vuejs/vitepress/issues/5230)) ([c37bde6](https://github.com/vuejs/vitepress/commit/c37bde6308fb9f202e224f5eb38c0fac35468ea6))
- compose markdown config when extending configs ([#5236](https://github.com/vuejs/vitepress/issues/5236)) ([04f4fba](https://github.com/vuejs/vitepress/commit/04f4fbadbdc05c75dbf3564c21404540d570d43b))
- delete undefined values while merging sidepanel props ([cc30b10](https://github.com/vuejs/vitepress/commit/cc30b10b60beb862f85915055dc97651703bf250))
- disable pluginTimings and invalidAnnotation for now ([9315fc1](https://github.com/vuejs/vitepress/commit/9315fc182229f13ee793dc44107947b2fe6ab50e))
- don't invalidate framework chunk when a new asset is added ([c0e2e18](https://github.com/vuejs/vitepress/commit/c0e2e1809464c48435a22a0aa9468ff5e562791d))
- escape description in head ([d96bf1d](https://github.com/vuejs/vitepress/commit/d96bf1dc616599609d8a24af7183aee6a7b9ae07))
- index rewritten local search pages by locale ([#5241](https://github.com/vuejs/vitepress/issues/5241)) ([80cf265](https://github.com/vuejs/vitepress/commit/80cf2650aa5fa4b49093509f60766cc5b28c19bc))
- keep translation links in the current tab ([#5158](https://github.com/vuejs/vitepress/issues/5158)) ([202ee70](https://github.com/vuejs/vitepress/commit/202ee7026054ac5c721bbdbf196426628b0c9b18))
- normalize `/index` to `/` ([856858d](https://github.com/vuejs/vitepress/commit/856858d26a78f2e19e2c8ee23c2e85a95dbfdd29)), closes [#5165](https://github.com/vuejs/vitepress/issues/5165)
- preserve Agent Studio DocSearch options ([#5254](https://github.com/vuejs/vitepress/issues/5254)) ([f29ffdb](https://github.com/vuejs/vitepress/commit/f29ffdbb33022eb41327fec836f9cfbc16bf01d8))
- preserve external sidebar links with base ([#5243](https://github.com/vuejs/vitepress/issues/5243)) ([ddf178a](https://github.com/vuejs/vitepress/commit/ddf178a170967527bafe7c9b262fb66aa10ec9de))
- prevent DocSearch SVG clipping in WebKit ([#5240](https://github.com/vuejs/vitepress/issues/5240)) ([a357e5e](https://github.com/vuejs/vitepress/commit/a357e5ef67ed266861877eb08da753b2141a784f))
- strip frontmatter before heading includes ([#5246](https://github.com/vuejs/vitepress/issues/5246)) ([e68fade](https://github.com/vuejs/vitepress/commit/e68fade75d2259b10695e85277f5483a084e3ae7))
- **theme:** avatars misaligned in team member cards ([6730fb8](https://github.com/vuejs/vitepress/commit/6730fb84620c852b516de59f6f7c39c4f04f0e37)), closes [#5160](https://github.com/vuejs/vitepress/issues/5160)
- **theme:** correct mixed LTR/RTL text rendering in code blocks ([73f7b0b](https://github.com/vuejs/vitepress/commit/73f7b0b984853758d41e897dd43e5e93a1066266))
- **theme:** keep external link icon inline ([#5232](https://github.com/vuejs/vitepress/issues/5232)) ([756a88c](https://github.com/vuejs/vitepress/commit/756a88cfa2f8f71400362327d2255cb8b5ccacfa))
- **theme:** prevent `sub` and `sup` elements from affecting line height ([19357f9](https://github.com/vuejs/vitepress/commit/19357f9d337472572a500b8d2af8ef97932bfdda)), closes [#5173](https://github.com/vuejs/vitepress/issues/5173)
- use resolveDynamicComponent instead of resolveComponent ([9da1e3e](https://github.com/vuejs/vitepress/commit/9da1e3e70f41b7b8cb81f791307c778d65854f7a))
### Features
- add macOS local search navigation shortcuts ([#5237](https://github.com/vuejs/vitepress/issues/5237)) ([e635e9e](https://github.com/vuejs/vitepress/commit/e635e9e5ea2876a293954970c6a00d6e09cb50a5))
- add support for `format` option in Carbon options ([#5188](https://github.com/vuejs/vitepress/issues/5188)) ([6ee01bf](https://github.com/vuejs/vitepress/commit/6ee01bf30534cbf7847fe8c94fbf23efd9637f68))
- allow custom i18n routing ([#5239](https://github.com/vuejs/vitepress/issues/5239)) ([eef5742](https://github.com/vuejs/vitepress/commit/eef57427f4861939ff27f07d71587e68118064e2))
- allow VPContent to use custom components ([#5176](https://github.com/vuejs/vitepress/issues/5176)) ([c0b38d5](https://github.com/vuejs/vitepress/commit/c0b38d52c270e4efb59effa8ab7207535c48ec05))
- **markdown:** expose Shiki color replacements in markdown options ([#5153](https://github.com/vuejs/vitepress/issues/5153)) ([fccc617](https://github.com/vuejs/vitepress/commit/fccc6171024f1b0087dd963dbe5a6236081b5d3d))
- migrate to vite 8 ([228eef1](https://github.com/vuejs/vitepress/commit/228eef187ae33d1676a79e466d289c0e6e9ab321))
- show local search loading state ([#5252](https://github.com/vuejs/vitepress/issues/5252)) ([7e2273a](https://github.com/vuejs/vitepress/commit/7e2273a3e470da2777c2510b51262e85879f2ee9))
- support scroll-margin / scroll-padding ([6cce766](https://github.com/vuejs/vitepress/commit/6cce76685da39f8b5c75da047f847f82f70b9c4e))
- support social link target option ([#5242](https://github.com/vuejs/vitepress/issues/5242)) ([d0159c8](https://github.com/vuejs/vitepress/commit/d0159c8a850cbd2a010a3d44bdeb97c3db651d0e))
### BREAKING CHANGES
- VitePress now uses Vite 8. If you are using Vite plugins in your config, please check the [Vite 8 migration guide](https://vite.dev/guide/migration) for any breaking changes that may affect you.
- Node 20 support is dropped. v22 or higher is needed.
- `scrollOffset` from config is removed. Users wanting to customize scroll offset should customize `scroll-margin-top` via CSS instead. `smoothScroll` support from `router.go` is also removed as it didn't work as expected for most users. Users wanting smooth scrolling should set `scroll-behavior: smooth` in CSS, ideally inside a `@media (prefers-reduced-motion: no-preference)` block.
## [2.0.0-alpha.17](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.16...v2.0.0-alpha.17) (2026-03-19)
### Bug Fixes

@ -162,6 +162,14 @@ export default defineConfig({
themeConfig: {
nav,
sidebar,
socialLinks: [
{
icon: 'github',
link: '/home',
ariaLabel: 'Home social link',
target: '_self'
}
],
search: {
provider: 'local',
options: {

@ -1,4 +1,4 @@
import fs from 'node:fs/promises'
import { writeFile, unlink } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
describe('static data file support in vite 3', () => {
@ -48,20 +48,20 @@ describe('static data file support in vite 3', () => {
const b = fileURLToPath(new URL('./data/b.json', import.meta.url))
try {
await fs.writeFile(a, JSON.stringify({ a: false }, null, 2) + '\n')
await writeFile(a, JSON.stringify({ a: false }, null, 2) + '\n')
await page.waitForFunction(
() =>
document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: false }, { b: true }], null, 2)
)
} finally {
await fs.writeFile(a, JSON.stringify({ a: true }, null, 2) + '\n')
await writeFile(a, JSON.stringify({ a: true }, null, 2) + '\n')
}
let err = true
try {
await fs.unlink(b)
await unlink(b)
await page.waitForFunction(
() =>
document.querySelector('pre#basic')?.textContent ===
@ -70,19 +70,19 @@ describe('static data file support in vite 3', () => {
err = false
} finally {
if (err) {
await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
await writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
}
}
try {
await fs.writeFile(b, JSON.stringify({ b: false }, null, 2) + '\n')
await writeFile(b, JSON.stringify({ b: false }, null, 2) + '\n')
await page.waitForFunction(
() =>
document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: true }, { b: false }], null, 2)
)
} finally {
await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
await writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')
}
})

@ -32,4 +32,13 @@ describe('render correct content', async () => {
const outlineLinksCount = await outlineLinksLocator.count()
expect(outlineLinksCount).toEqual(4)
})
test('social link target override', async () => {
const socialLink = page.locator(
'.VPNavBarSocialLinks a[aria-label="Home social link"]'
)
expect(await socialLink.getAttribute('href')).toBe('/home')
expect(await socialLink.getAttribute('target')).toBe('_self')
})
})

@ -3,15 +3,69 @@ describe('local search', () => {
await goto('/')
})
test.runIf(!process.env.VITE_TEST_BUILD)(
'shows progress while loading search index',
async () => {
const indexRoute = /@localSearchIndexroot/
let delayedIndex = false
await page.route(indexRoute, async (route) => {
delayedIndex = true
await new Promise((resolve) => setTimeout(resolve, 800))
await route.continue()
})
try {
await page.locator('.VPNavBarSearchButton').click()
const loading = page.locator('.search-loading')
const results = page.locator('.results')
await page.waitForFunction(() =>
document
.querySelector('.search-loading')
?.classList.contains('active')
)
expect(delayedIndex).toBe(true)
expect(await loading.getAttribute('role')).toBe('status')
expect(await loading.getAttribute('aria-label')).toBe(
'Loading search results'
)
expect(await results.getAttribute('aria-busy')).toBe('true')
await page.waitForFunction(
() =>
!document
.querySelector('.search-loading')
?.classList.contains('active')
)
expect(await results.getAttribute('aria-busy')).toBe('false')
} finally {
await page.unroute(indexRoute)
}
}
)
test('exclude content from search results', async () => {
await page.locator('.VPNavBarSearchButton').click()
const input = await page.waitForSelector('input#localsearch-input')
await input.type('local')
await page.waitForSelector('ul#localsearch-list', { state: 'visible' })
const searchResults = page.locator('#localsearch-list')
await page.waitForFunction(() => {
const options = [
...document.querySelectorAll('#localsearch-list li[role=option]')
]
return (
options.length === 1 &&
options[0].textContent?.includes('Local search included')
)
})
expect(await searchResults.locator('li[role=option]').count()).toBe(1)
expect(

@ -1,6 +1,6 @@
import fs from 'fs-extra'
import getPort from 'get-port'
import { nanoid } from 'nanoid'
import { rm } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath, URL } from 'node:url'
import { chromium } from 'playwright-chromium'
@ -33,12 +33,12 @@ const variations = themes.flatMap((theme) =>
afterAll(async () => {
await page.close()
await browser.close()
await fs.remove(tempDir)
await rm(tempDir, { recursive: true, force: true })
})
test.each(variations)('init %s', async (_, { theme, useTs }) => {
const root = getTempRoot()
await fs.remove(root)
await rm(root, { recursive: true, force: true })
scaffold({ root, theme, useTs, injectNpmScripts: false })
const port = await getPort()

@ -1,7 +1,7 @@
import { ref } from 'vue'
import { resolveLocaleLink } from 'client/theme-default/composables/langs'
import type { VitePressData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { resolveLocaleLink } from 'client/theme-default/composables/langs'
import { ref } from 'vue'
function createData(
themeConfig: DefaultTheme.Config,

@ -1,5 +1,6 @@
import {
buildAskAiConfig,
buildSidePanelProps,
hasAskAi,
hasKeywordSearch,
mergeLangFacetFilters,
@ -192,5 +193,110 @@ describe('client/theme-default/support/docsearch', () => {
)
expect(result.searchParameters?.facetFilters).toEqual(['lang:en'])
})
test('preserves Agent Studio search parameters by index', () => {
const result = buildAskAiConfig(
{
assistantId: 'assistant123',
agentStudio: true,
searchParameters: {
index: {
distinct: false
}
}
} as any,
{
appId: 'app',
apiKey: 'key',
indexName: 'index',
searchParameters: {
facetFilters: ['tag:docs']
}
} as any,
'en'
)
expect(result.searchParameters).toEqual({
index: {
distinct: false
}
})
expect(result.searchParameters).not.toHaveProperty('facetFilters')
})
test('does not add legacy facet filters to Agent Studio config', () => {
const result = buildAskAiConfig(
{
assistantId: 'assistant123',
agentStudio: true
} as any,
{
appId: 'app',
apiKey: 'key',
indexName: 'index',
searchParameters: {
facetFilters: ['tag:docs']
}
} as any,
'en'
)
expect(result.searchParameters).toBeUndefined()
})
})
describe('buildSidePanelProps', () => {
test('passes resolved Ask AI options to the side panel', () => {
const result = buildSidePanelProps(
{
assistantId: 'assistant123',
agentStudio: true,
searchParameters: {
index: {
facetFilters: ['lang:en']
}
},
suggestedQuestions: true,
useStagingEnv: true,
sidePanel: {
button: {
variant: 'inline'
},
panel: {
width: 420,
suggestedQuestions: true
}
}
} as any,
{
appId: 'app',
apiKey: 'key',
indexName: 'index'
} as any
)
expect(result).toEqual({
container: '#vp-docsearch-sidepanel',
appId: 'app',
apiKey: 'key',
indexName: 'index',
assistantId: 'assistant123',
agentStudio: true,
searchParameters: {
index: {
facetFilters: ['lang:en']
}
},
suggestedQuestions: true,
useStagingEnv: true,
button: {
variant: 'inline'
},
panel: {
width: 420,
suggestedQuestions: true
}
})
})
})
})

@ -1,4 +1,8 @@
import { getSidebar, hasActiveLink } from 'client/theme-default/support/sidebar'
import {
getFlatSideBarLinks,
getSidebar,
hasActiveLink
} from 'client/theme-default/support/sidebar'
describe('client/theme-default/support/sidebar', () => {
describe('getSidebar', () => {
@ -103,6 +107,75 @@ describe('client/theme-default/support/sidebar', () => {
)
})
})
test('applies base only to internal links', () => {
expect(
getSidebar(
{
'/en/': {
base: '/en/',
items: [
{
text: 'Guide',
items: [
{ text: 'Intro', link: 'intro' },
{ text: 'Root', link: '/root' },
{ text: 'External', link: 'https://example.com/' }
]
}
]
}
},
'/en/intro'
)
).toStrictEqual([
{
text: 'Guide',
items: [
{ text: 'Intro', link: '/en/intro' },
{ text: 'Root', link: '/en/root' },
{ text: 'External', link: 'https://example.com/' }
]
}
])
})
})
describe('getFlatSideBarLinks', () => {
test('flattens nested items and preserves link metadata', () => {
const sidebar = [
{
text: 'Group',
items: [
{ text: 'Intro', link: '/intro' },
{
text: 'External',
link: 'https://example.com/',
target: '_self',
rel: 'noopener',
docFooterText: 'Go external'
}
]
}
]
expect(getFlatSideBarLinks(sidebar)).toStrictEqual([
{
text: 'Intro',
link: '/intro',
docFooterText: undefined,
rel: undefined,
target: undefined
},
{
text: 'External',
link: 'https://example.com/',
docFooterText: 'Go external',
rel: 'noopener',
target: '_self'
}
])
})
})
describe('hasActiveLink', () => {

@ -1,5 +1,5 @@
import { mergeConfig } from 'node/config'
import type { MarkdownItAsync } from 'markdown-it-async'
import { mergeConfig } from 'node/config'
describe('node/config', () => {
test('merges markdown config hooks from extended configs', async () => {

@ -1,8 +1,8 @@
import { resolveConfig } from 'node/config'
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { resolveConfig } from 'node/config'
import { createMarkdownToVueRenderFn } from 'node/markdownToVue'
describe('node/markdownToVue', () => {
let root: string | undefined
@ -66,4 +66,94 @@ describe('node/markdownToVue', () => {
line: 8
})
})
test('selects included heading sections after frontmatter', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-include-'))
const file = path.join(root, 'index.md')
const source = path.join(root, 'source.md')
await writeFile(
source,
[
'---',
'description: Source description',
'---',
'# Intro',
'',
'intro text',
'',
'## Shared',
'',
'shared before target',
'',
'## Target',
'',
'target text',
'',
'### Child',
'',
'child text',
'',
'## Shared',
'',
'shared after target',
''
].join('\n')
)
const src = '<!--@include: ./source.md#target-->'
await writeFile(file, src)
const siteConfig = await resolveConfig(root, 'build', 'production')
const render = await createMarkdownToVueRenderFn(
siteConfig.srcDir,
{ cache: false },
'/',
false,
false,
siteConfig
)
const result = await render(src, file, 'public')
expect(result.vueSrc).toContain('<p>target text</p>')
expect(result.vueSrc).toContain('<h3 id="child"')
expect(result.vueSrc).toContain('<p>child text</p>')
expect(result.vueSrc).not.toContain('Source description')
expect(result.vueSrc).not.toContain('intro text')
expect(result.vueSrc).not.toContain('shared before target')
expect(result.vueSrc).not.toContain('shared after target')
})
test('applies rewrites with mismatched Windows drive letter case', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-rewrite-'))
const file = path.join(root, 'index.md')
await writeFile(file, '# Home\n')
const siteConfig = await resolveConfig(root, 'build', 'production')
siteConfig.srcDir = 'c:/site/docs'
siteConfig.pages = ['en/index.md']
siteConfig.rewrites = {
map: { 'en/index.md': 'index.md' },
inv: { 'index.md': 'en/index.md' }
}
;(siteConfig as any).__dirty = true
const render = await createMarkdownToVueRenderFn(
siteConfig.srcDir,
{ cache: false },
'/',
false,
false,
siteConfig
)
const result = await render(
'# Home\n',
'C:/site/docs/en/index.md',
'public'
)
expect(result.pageData.relativePath).toBe('index.md')
})
})

@ -0,0 +1,102 @@
import MiniSearch from 'minisearch'
import { resolveConfig } from 'node/config'
import { localSearchPlugin } from 'node/plugins/localSearchPlugin'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
describe('node/plugins/localSearchPlugin', () => {
let root: string | undefined
let nodeEnv: string | undefined
beforeEach(() => {
nodeEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
})
afterEach(async () => {
if (nodeEnv === undefined) {
delete process.env.NODE_ENV
} else {
process.env.NODE_ENV = nodeEnv
}
if (root) {
await rm(root, { recursive: true, force: true })
root = undefined
}
})
test('indexes rewritten pages by rewritten locale path', async () => {
root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-'))
const configDir = path.join(root, '.vitepress')
await mkdir(configDir)
await writeFile(
path.join(root, 'index.md'),
'# English home\n\nrootonlytoken\n'
)
await writeFile(
path.join(root, 'zh.md'),
'# Chinese home\n\nlocaleonlytoken\n'
)
await writeFile(
path.join(configDir, 'config.ts'),
[
'export default {',
' rewrites: {',
" 'index.md': 'guide.md',",
" 'zh.md': 'zh/guide.md'",
' },',
' locales: {',
" root: { label: 'English', lang: 'en' },",
" zh: { label: 'Chinese', lang: 'zh' }",
' },',
' themeConfig: {',
" search: { provider: 'local' }",
' }',
'}'
].join('\n')
)
const siteConfig = await resolveConfig(root, 'build', 'production')
const plugin = await localSearchPlugin(siteConfig)
const indexModule = (await plugin.load?.call(
{} as never,
'/@localSearchIndex'
)) as string
expect(indexModule).toContain(
'"root": () => import(\'@localSearchIndexroot\')'
)
expect(indexModule).toContain('"zh": () => import(\'@localSearchIndexzh\')')
const rootIndex = loadIndex(
(await plugin.load?.call({} as never, '/@localSearchIndexroot')) as string
)
const zhIndex = loadIndex(
(await plugin.load?.call({} as never, '/@localSearchIndexzh')) as string
)
expect(rootIndex.search('rootonlytoken')).toMatchObject([
{ id: '/guide.html#english-home' }
])
expect(rootIndex.search('localeonlytoken')).toEqual([])
expect(zhIndex.search('localeonlytoken')).toMatchObject([
{ id: '/zh/guide.html#chinese-home' }
])
expect(zhIndex.search('rootonlytoken')).toEqual([])
})
})
function loadIndex(serializedModule: string) {
const serializedIndex = JSON.parse(
serializedModule.slice('export default '.length)
)
return MiniSearch.loadJSON(serializedIndex, {
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles']
})
}

@ -1,4 +1,4 @@
import { createRequire } from 'module'
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)

@ -67,6 +67,24 @@ export default {
}
```
The `router` value is the same VitePress router instance returned by [`useRouter()`](../reference/runtime-api#userouter). To listen for route changes, assign handlers on the router:
```ts [.vitepress/theme/index.ts]
export default {
enhanceApp({ router }) {
router.onBeforeRouteChange = (to) => {
console.log('navigating to', to)
}
router.onAfterRouteChange = (to) => {
console.log('navigated to', to)
}
}
}
```
Return `false` from `onBeforeRouteChange` or `onBeforePageLoad` to cancel navigation.
The default export is the only contract for a custom theme, and only the `Layout` property is required. So technically, a VitePress theme can be as simple as a single Vue component.
Inside your layout component, it works just like a normal Vite + Vue 3 application. Do note the theme also needs to be [SSR-compatible](./ssr-compat).

@ -12,7 +12,7 @@ You can try VitePress directly in your browser on [StackBlitz](https://vitepress
### Prerequisites
- [Node.js](https://nodejs.org/) version 20 or higher.
- [Node.js](https://nodejs.org/) version 22 or higher.
- Terminal for accessing VitePress via its command line interface (CLI).
- Text Editor with [Markdown](https://en.wikipedia.org/wiki/Markdown) syntax support.
- [VSCode](https://code.visualstudio.com/) is recommended, along with the [official Vue extension](https://marketplace.visualstudio.com/items?itemName=Vue.volar).

@ -329,7 +329,7 @@ The paths loader module is run in Node.js and only executed during build time. Y
Generating paths from local files:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -253,6 +253,7 @@ export default {
// You can add any icon from simple-icons (https://simpleicons.org/):
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
{ icon: 'twitter', link: '...' },
{ icon: 'discord', link: '/community', target: '_self' },
// You can also add custom icons by passing SVG as string:
{
icon: {
@ -272,6 +273,7 @@ interface SocialLink {
icon: string | { svg: string }
link: string
ariaLabel?: string
target?: string
}
```

@ -291,6 +291,12 @@ export default defineConfig({
})
```
Use `askAi.sidePanel.panel.suggestedQuestions` for side panel suggested
questions. Algolia's standalone Ask AI examples also mention
`askAi.suggestedQuestions`, but that top-level option is not enough for
VitePress side panel mode and does not make the integrated keyword-search
modal display suggested questions on first open.
If you need to disable the keyboard shortcut, use the `keyboardShortcuts` option at the sidepanel root level:
```ts

@ -124,6 +124,18 @@ interface Router {
}
```
Assign route-change handlers on the router instance:
```ts
const router = useRouter()
router.onBeforeRouteChange = (to) => {
console.log('navigating to', to)
}
```
For custom themes, the same router is available from [`enhanceApp`](../guide/custom-theme#theme-interface).
## `withBase` <Badge type="info" text="helper" />
- **Type**: `(path: string) => string`

@ -1,4 +1,4 @@
import { createRequire } from 'module'
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)

@ -12,7 +12,7 @@ Puede experimentar VitePress directamente en su navegador en [StackBlitz](https:
### Prerrequisitos {#prerequisites}
- [Node.js](https://nodejs.org/) versión 20 o superior.
- [Node.js](https://nodejs.org/) versión 22 o superior.
- Terminal para acessar VitePress a través de su interfaz de linea de comando (CLI).
- Editor de texto con soporte a sintaxis [Markdown](https://en.wikipedia.org/wiki/Markdown).
- [VSCode](https://code.visualstudio.com/) es recomendado, junto con la [extensión oficial Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar).

@ -290,7 +290,7 @@ El módulo de carga de paths es ejecutado en Node.js y apenas durante el momento
Generando paths a partir de archivos locales:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -1,4 +1,4 @@
import { createRequire } from 'module'
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)

@ -12,7 +12,7 @@ description: نصب و راه‌اندازی ویت‌پرس. یاد بگیری
### پیش‌نیازها {#prerequisites}
- [Node.js](https://nodejs.org/) نسخه 20 یا بالاتر.
- [Node.js](https://nodejs.org/) نسخه 22 یا بالاتر.
- ترمینال برای دسترسی به ویت‌پرس از طریق رابط خط فرمان (CLI).
- ویرایشگر متنی با پشتیبانی از [Markdown](https://en.wikipedia.org/wiki/Markdown).
- [VSCode](https://code.visualstudio.com/) به همراه [افزونه رسمی Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar).

@ -295,7 +295,7 @@ export default {
تولید مسیرها از فایل‌های محلی:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -1,4 +1,4 @@
import { createRequire } from 'module'
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)

@ -12,7 +12,7 @@ description: VitePressのインストール、セットアップ、開発サー
### 前提条件 {#prerequisites}
- [Node.js](https://nodejs.org/) バージョン 20 以上
- [Node.js](https://nodejs.org/) バージョン 22 以上
- VitePress をコマンドラインインターフェース (CLI) で操作するためのターミナル
- [Markdown](https://en.wikipedia.org/wiki/Markdown) 構文に対応したテキストエディタ
- 推奨: [VSCode](https://code.visualstudio.com/) と [公式 Vue 拡張](https://marketplace.visualstudio.com/items?itemName=Vue.volar)

@ -304,7 +304,7 @@ export default {
ローカルファイルから生成する例:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -1,4 +1,4 @@
import { createRequire } from 'module'
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)

@ -12,7 +12,7 @@ description: VitePress를 시작하세요. 문서 사이트를 설치하고, 스
### 사전 준비 사항 {#prerequisites}
- [Node.js](https://nodejs.org/) 버전 20 이상.
- [Node.js](https://nodejs.org/) 버전 22 이상.
- VitePress를 명령줄 인터페이스(CLI)를 통해 접근하기 위한 터미널.
- [마크다운](https://en.wikipedia.org/wiki/Markdown) 문법 지원이 있는 텍스트 에디터.
- [VSCode](https://code.visualstudio.com/)와 [공식 Vue 확장 프로그램](https://marketplace.visualstudio.com/items?itemName=Vue.volar) 사용을 권장합니다.

@ -291,7 +291,7 @@ export default {
로컬 파일에서 경로 생성:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -1,4 +1,4 @@
import { createRequire } from 'module'
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)

@ -12,7 +12,7 @@ Você pode experimentar VitePress diretamente no seu navegador em [StackBlitz](h
### Pré-requisitos {#prerequisites}
- [Node.js](https://nodejs.org/) na versão 20 ou superior.
- [Node.js](https://nodejs.org/) na versão 22 ou superior.
- Terminal para acessar VitePress através da sua interface de linha de comando (CLI).
- Editor de texto com suporte a sintaxe [Markdown](https://en.wikipedia.org/wiki/Markdown).
- [VSCode](https://code.visualstudio.com/) é recomendado, junto com a [extensão oficial Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar).

@ -290,7 +290,7 @@ O módulo de carregamento de caminhos é executado no Node.js e apenas durante o
Gerando caminhos a partir de arquivos locais:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -1,4 +1,4 @@
import { createRequire } from 'module'
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)
@ -189,7 +189,7 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
clearButtonAriaLabel: 'Очистить запрос',
closeButtonText: 'Закрыть',
closeButtonAriaLabel: 'Закрыть',
placeholderText: 'Поиск по документации или задайте вопрос Ask AI',
placeholderText: 'Искать в документации или задать вопрос Ask AI',
placeholderTextAskAi: 'Задайте другой вопрос...',
placeholderTextAskAiStreaming: 'Отвечаю...',
searchInputLabel: 'Поиск',

@ -1,5 +1,5 @@
---
description: Подключите VitePress к безголовой CMS с помощью динамических маршрутов и загрузчиков данных.
description: Подключите VitePress к CMS без встроенного интерфейса с помощью динамических маршрутов и загрузчиков данных.
outline: deep
---

@ -67,6 +67,24 @@ export default {
}
```
Значение `router` представляет собой тот же экземпляр маршрутизатора VitePress, который возвращает [`useRouter()`](../reference/runtime-api#userouter). Чтобы отслеживать изменения маршрутов, назначьте обработчики для маршрутизатора:
```ts [.vitepress/theme/index.ts]
export default {
enhanceApp({ router }) {
router.onBeforeRouteChange = (to) => {
console.log('navigating to', to)
}
router.onAfterRouteChange = (to) => {
console.log('navigated to', to)
}
}
}
```
Верните `false` из `onBeforeRouteChange` или `onBeforePageLoad`, чтобы отменить переход.
Экспорт по умолчанию является единственным контрактом для пользовательской темы, и только свойство `Layout` является обязательным. Таким образом, технически тема VitePress может быть простой, как один компонент Vue.
Внутри компонент макета работает так же, как и обычное приложение Vite + Vue 3. Обратите внимание, что тема также должна быть [SSR-совместимой](./ssr-compat).
@ -156,7 +174,7 @@ const { page, frontmatter } = useData()
## Распространение пользовательской темы {#distributing-a-custom-theme}
Самый простой способ распространить пользовательскую тему — предоставить её в виде [репозитория шаблонов на GitHub](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository).
Самый простой способ распространить пользовательскую тему — предоставить её в виде [репозитория шаблонов на GitHub](https://docs.github.com/ru/repositories/creating-and-managing-repositories/creating-a-template-repository).
Если вы хотите распространить тему в виде пакета npm, выполните следующие действия:

@ -166,6 +166,13 @@ Cache-Control: max-age=31536000,immutable
with:
node-version: 24
cache: npm # или pnpm / yarn
- name: Cache VitePress
uses: actions/cache@v4
with:
path: docs/.vitepress/cache
key: ${{ runner.os }}-vitepress-${{ hashFiles('docs/**', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb') }}
restore-keys: |
${{ runner.os }}-vitepress-
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies

@ -12,7 +12,7 @@ description: Начните работу с VitePress. Узнайте, как у
### Требования {#prerequisites}
- [Node.js](https://nodejs.org/) версии 20 или выше.
- [Node.js](https://nodejs.org/) версии 22 или выше.
- Терминал для доступа к VitePress через интерфейс командной строки (CLI).
- Текстовый редактор с поддержкой синтаксиса [Markdown](https://ru.wikipedia.org/wiki/Markdown).
- Рекомендуется использовать [VSCode](https://code.visualstudio.com/), а также [официальное расширение Vue](https://marketplace.visualstudio.com/items?itemName=Vue.volar).
@ -37,6 +37,10 @@ $ yarn add -D vitepress@next vue
$ bun add -D vitepress@next
```
```sh [deno]
$ deno add -D vitepress@next
```
:::
::: tip ПРИМЕЧАНИЕ

@ -329,7 +329,7 @@ export default {
Генерация путей из локальных файлов:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -25,10 +25,28 @@ export default {
## i18nRouting
- Тип: `boolean`
- Тип: `boolean | ((data: VitePressData<DefaultTheme.Config>, hash: string, targetLocale: string) => string)`
При смене локали на `ru` URL изменится с `/foo` (или `/en/foo/`) на `/ru/foo`. Вы можете отключить это поведение, установив для параметра `themeConfig.i18nRouting` значение `false`.
Установите для `themeConfig.i18nRouting` функцию, чтобы настроить ссылки для переключения локали. Эта функция получает текущие данные VitePress, текущий хеш и ключ целевой локали, а затем возвращает ссылку для перехода на неё.
```ts
import { defineConfig } from 'vitepress'
export default defineConfig({
themeConfig: {
i18nRouting(data, hash, targetLocale) {
const target = data.site.value.locales[targetLocale]
const targetLink =
target.link || (targetLocale === 'root' ? '/' : `/${targetLocale}/`)
return `${targetLink}${data.page.value.relativePath.replace(/\.md$/, '')}${hash}`
}
}
})
```
## logo
- Тип: `ThemeableImage`
@ -235,6 +253,7 @@ export default {
// Можно добавить любую иконку из simple-icons (https://simpleicons.org/):
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' },
{ icon: 'twitter', link: '...' },
{ icon: 'discord', link: '/community', target: '_self' },
// Можно добавить пользовательские иконки, передав SVG в виде строки:
{
icon: {
@ -254,6 +273,7 @@ interface SocialLink {
icon: string | { svg: string }
link: string
ariaLabel?: string
target?: string
}
```

@ -276,7 +276,6 @@ export default defineConfig({
askAi: {
assistantId: 'XXXYYY',
sidePanel: {
// Отражает API @docsearch/sidepanel-js SidepanelProps
panel: {
variant: 'floating', // или 'inline'
side: 'right',
@ -292,6 +291,8 @@ export default defineConfig({
})
```
Используйте `askAi.sidePanel.panel.suggestedQuestions` для настройки рекомендуемых вопросов в боковой панели. В примерах автономного Ask AI от Algolia также упоминается `askAi.suggestedQuestions`, однако одного этого параметра верхнего уровня недостаточно для режима боковой панели VitePress, и он не позволяет встроенному модальному окну поиска по ключевым словам отображать рекомендуемые вопросы при первом открытии.
Если вам нужно отключить сочетание клавиш, используйте опцию `keyboardShortcuts` боковой панели:
```ts

@ -62,6 +62,8 @@ interface PageData {
}
```
`page.headers` заполняется только в том случае, если включён параметр [markdown.headers](./site-config#markdown). Без него это свойство остаётся пустым массивом. Оглавление в теме по умолчанию получает заголовки из уже отрендеренного содержимого страницы, поэтому оно может отображаться, даже если `page.headers` пуст.
**Пример:**
```vue
@ -122,6 +124,18 @@ interface Router {
}
```
Назначьте обработчики изменения маршрутов для экземпляра маршрутизатора:
```ts
const router = useRouter()
router.onBeforeRouteChange = (to) => {
console.log('переход к', to)
}
```
В пользовательских темах этот же экземпляр маршрутизатора доступен через [`enhanceApp`](../guide/custom-theme#theme-interface).
## `withBase` <Badge type="info" text="хелпер" /> {#withbase}
- **Тип**: `(path: string) => string`

@ -134,13 +134,43 @@ export default defineConfigWithTheme<ThemeConfig>({
Вы можете настроить базовый экземпляр [Markdown-It](https://github.com/markdown-it/markdown-it) с помощью опции [markdown](#markdown) в конфигурации VitePress.
### Переопределение на уровне страницы {#page-level-overrides}
Некоторые настройки можно переопределить для отдельных страниц с помощью метаданных.
Подробности см. в разделе [Конфигурация метаданных](./frontmatter-config).
### Переопределение на уровне директории {#directory-level-overrides}
Некоторые параметры конфигурации можно переопределить на уровне директории, что позволяет всем страницам в этой директории использовать общие настройки без необходимости повторять их в блоке метаданных каждой страницы.
Для этого добавьте файл с именем `config.ts` (или `.js`, `.mjs` или `.mts`) в соответствующую директорию. Этот файл должен экспортировать объект конфигурации с помощью `export default`, аналогично основному файлу конфигурации.
Вложенные директории наследуют настройки от родительской директории, при этом переопределения конфигурации объединяются соответствующим образом.
Вспомогательную функцию `defineAdditionalConfig` можно использовать для получения подсказок TypeScript по доступным параметрам, однако, как и в случае с `defineConfig`, её использование необязательно.
Например, для сайта с несколькими языками может потребоваться разное значение `description` для каждого языка. Мы можем добавить файл `es/config.ts` со следующим содержимым:
```ts
import { defineAdditionalConfig } from 'vitepress'
export default defineAdditionalConfig({
description: 'Generador de Sitios Estáticos desarrollado con Vite y Vue.'
})
```
Этот `description` затем будет использоваться для всех страниц в директории `es`.
В качестве альтернативы, при использовании встроенных возможностей i18n настройки для директории локали можно переопределить через параметр `locales` в основном файле конфигурации. Подробности см. в разделе [Интернационализация](../guide/i18n).
## Метаданные сайта {#site-metadata}
### title {#title}
- Тип: `string`
- По умолчанию: `VitePress`
- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#title)
- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#title) или на [уровне директории](#directory-level-overrides)
Название для сайта. При использовании темы по умолчанию оно будет отображаться в панели навигации.
@ -161,7 +191,7 @@ export default {
### titleTemplate {##titletemplate}
- Тип: `string | boolean`
- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#titletemplate)
- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#titletemplate) или на [уровне директории](#directory-level-overrides)
Позволяет настраивать суффикс заголовка каждой страницы или весь заголовок. Например:
@ -194,7 +224,7 @@ export default {
- Тип: `string`
- По умолчанию: `A VitePress site`
- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#description)
- Можно переопределить для каждой страницы с помощью [метаданных](./frontmatter-config#description) или на [уровне директории](#directory-level-overrides)
Описание для сайта. Это будет отображаться как тег `<meta>` в HTML-странице.
@ -208,7 +238,7 @@ export default {
- Тип: `HeadConfig[]`
- По умолчанию: `[]`
- Можно добавлять на страницу через [метаданные](./frontmatter-config#head)
- Можно добавлять на страницу через [метаданные](./frontmatter-config#head) или на [уровне директории](#directory-level-overrides)
Дополнительные элементы для отображения в теге `<head>` в HTML-странице. Добавленные пользователем теги выводятся перед закрывающим тегом `head`, после тегов VitePress.
@ -320,6 +350,7 @@ export default {
- Тип: `string`
- По умолчанию: `en-US`
- Может быть переопределено [на уровне директории](#directory-level-overrides)
Атрибут lang для сайта. Будет выглядеть как тег `<html lang="en-US">` в HTML-странице.
@ -530,6 +561,8 @@ export default {
Проверьте [объявление типа и jsdocs](https://github.com/vuejs/vitepress/blob/main/src/node/markdown/markdown.ts) на наличие всех доступных опций.
Установите `markdown.headers` в значение `true` или передайте параметры [`@mdit-vue/plugin-headers`](https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers), чтобы собирать заголовки в [`useData().page.headers`](./runtime-api#usedata). Этот параметр отключён по умолчанию.
### vite {#vite}
- Тип: `import('vite').UserConfig`
@ -607,7 +640,7 @@ interface SSGContext {
- Тип: `(context: TransformContext) => Awaitable<HeadConfig[]>`
`transformHead` — это хук сборки для преобразования заголовка перед генерацией каждой страницы. Это позволит вам добавить в конфигурацию VitePress записи, которые не могут быть добавлены статически. Вам нужно только вернуть дополнительные записи, они будут автоматически объединены с существующими.
`transformHead` — это хук сборки для добавления дополнительных тегов в `<head>` каждой страницы. Он позволяет добавлять элементы в head, которые невозможно статически добавить в конфигурацию VitePress. Вам нужно только вернуть дополнительные элементы — они будут автоматически объединены с уже существующими.
::: warning ПРЕДУПРЕЖДЕНИЕ
Не мутируйте ничего внутри `context`.
@ -635,44 +668,38 @@ interface TransformContext {
}
```
Обратите внимание, что этот хук вызывается только при статической генерации сайта. Он не вызывается во время разработки. Если вам нужно добавить динамические записи в голову во время разработки, вместо этого вы можете использовать хук [`transformPageData`](#transformpagedata):
Этот хук вызывается только при выполнении сборки и не вызывается в режиме разработки.
```ts
export default {
transformPageData(pageData) {
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'meta',
{
name: 'og:title',
content:
pageData.frontmatter.layout === 'home'
? `VitePress`
: `${pageData.title} | VitePress`
}
])
}
}
```
Дополнительные теги будут добавлены в статические HTML-файлы, созданные во время сборки. Они не будут обновляться при навигации на стороне клиента.
#### Пример: Добавление канонического URL-адреса `<link>` {#example-adding-a-canonical-url-link}
Во многих случаях более подходящим решением будет использование хука [`transformPageData`](#transformpagedata). Этот хук также применяется как при клиентской навигации, так и в режиме разработки. Однако если генерация тегов head требует значительных вычислительных ресурсов, `transformHead` позволяет избежать этих затрат во время разработки.
#### Пример: добавление мета-тега `og:image` {#example-adding-og-image-meta}
```ts
export default {
transformPageData(pageData) {
const canonicalUrl = `https://example.com/${pageData.relativePath}`
.replace(/index\.md$/, '')
.replace(/\.md$/, '.html')
async transformHead(context) {
if (context.page === '404.md') {
return
}
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'link',
{ rel: 'canonical', href: canonicalUrl }
])
// Детали реализации `generatePageImage` зависят от ваших требований.
// Здесь мы предполагаем, что она создаёт подходящее изображение
// для каждой страницы и возвращает URL изображения.
const imageUrl = await generatePageImage(context)
return [[
'meta',
{ name: 'og:image', content: imageUrl }
]]
}
}
```
Здесь мы предполагаем, что URL изображения является динамическим и требует много времени для генерации. Использование `transformHead` позволяет избежать этих затрат во время разработки.
Для более простых случаев может быть достаточно использовать параметр [`head`](./frontmatter-config#head) в метаданных или [`transformPageData`](#transformpagedata).
### transformHtml {#transformhtml}
- Тип: `(code: string, id: string, context: TransformContext) => Awaitable<string | void>`
@ -721,3 +748,39 @@ interface TransformPageContext {
siteConfig: SiteConfig
}
```
#### Пример: добавление `<meta name="og:title">` {#example-adding-a-meta-name-og-title}
```ts
export default {
transformPageData(pageData) {
const title = pageData.frontmatter.layout === 'home'
? 'VitePress'
: `${pageData.title} | VitePress`
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'meta',
{ name: 'og:title', content: title }
])
}
}
```
#### Пример: добавление `<link>` с каноническим URL {#example-adding-a-canonical-url-link}
```ts
export default {
transformPageData(pageData) {
const canonicalUrl = `https://example.com/${pageData.relativePath}`
.replace(/index\.md$/, '')
.replace(/\.md$/, '.html')
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push([
'link',
{ rel: 'canonical', href: canonicalUrl }
])
}
}
```

@ -1,4 +1,4 @@
import { createRequire } from 'module'
import { createRequire } from 'node:module'
import { defineAdditionalConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)
@ -209,7 +209,7 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
closeText: '关闭',
backToSearchText: '返回搜索',
closeKeyAriaLabel: 'Esc 键',
poweredByText: '由…提供支持'
poweredByText: '搜索提供'
},
errorScreen: {
titleText: '无法获取结果',
@ -301,7 +301,7 @@ function searchOptions(): Partial<DefaultTheme.AlgoliaSearchOptions> {
'我会搜索你的文档,快速帮你找到设置指南、功能细节和故障排除提示。'
},
logo: {
poweredByText: '由…提供支持'
poweredByText: '搜索提供'
}
}
}

@ -12,7 +12,7 @@ description: 快速上手 VitePress。了解如何安装、创建项目结构并
### 前置准备 {#prerequisites}
- [Node.js](https://nodejs.org/) 20 及以上版本。
- [Node.js](https://nodejs.org/) 22 及以上版本。
- 通过命令行界面 (CLI) 访问 VitePress 的终端。
- 支持 [Markdown](https://en.wikipedia.org/wiki/Markdown) 语法的编辑器。
- 推荐 [VSCode](https://code.visualstudio.com/) 及其[官方 Vue 扩展](https://marketplace.visualstudio.com/items?itemName=Vue.volar)。

@ -291,7 +291,7 @@ export default {
从本地文件生成路径:
```js
import fs from 'fs'
import fs from 'node:fs'
export default {
paths() {

@ -1,6 +1,6 @@
{
"name": "vitepress",
"version": "2.0.0-alpha.17",
"version": "2.0.0-alpha.18",
"description": "Vite & Vue powered static site generator",
"keywords": [
"vite",
@ -56,11 +56,11 @@
"dev:start": "pnpm --stream '/^dev:(client|node|watch)$/'",
"dev:client": "tsc --sourcemap -w --preserveWatchOutput -p src/client",
"dev:node": "DEV=true pnpm build:node -w",
"dev:shared": "node scripts/copyShared",
"dev:watch": "node scripts/watchAndCopy",
"dev:shared": "node scripts/copyShared.ts",
"dev:watch": "node scripts/watchAndCopy.ts",
"build": "pnpm build:prepare && pnpm build:client && pnpm build:node",
"build:prepare": "pnpm clean && node scripts/copyShared",
"build:client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient",
"build:prepare": "pnpm clean && node scripts/copyShared.ts",
"build:client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient.ts",
"build:node": "tsc -p src/node --noEmit && rollup --config rollup.config.ts --configPlugin esbuild",
"test": "pnpm --aggregate-output --reporter=append-only '/^test:(unit|e2e|init)$/'",
"test:unit": "vitest run -r __tests__/unit",
@ -87,7 +87,7 @@
"format:fail": "prettier --experimental-cli --check .",
"check": "pnpm format:fail && pnpm build && pnpm test",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"release": "node scripts/release.js"
"release": "node scripts/release.ts"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
@ -113,7 +113,7 @@
"mark.js": "8.11.1",
"minisearch": "^7.2.0",
"shiki": "^4.3.0",
"vite": "^7.3.6",
"vite": "^8.1.3",
"vue": "^3.5.39"
},
"devDependencies": {
@ -135,7 +135,6 @@
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@types/cross-spawn": "^6.0.6",
"@types/fs-extra": "^11.0.4",
"@types/lodash.template": "^4.5.3",
"@types/mark.js": "^8.11.12",
"@types/markdown-it-attrs": "^4.1.3",
@ -145,12 +144,12 @@
"@types/node": "^25.9.4",
"@types/picomatch": "^4.0.3",
"@types/prompts": "^2.4.9",
"@types/semver": "^7.7.1",
"chokidar": "^5.0.0",
"conventional-changelog": "^7.2.1",
"conventional-changelog-angular": "^8.3.1",
"cross-spawn": "^7.0.6",
"esbuild": "^0.27.7",
"fs-extra": "^11.3.5",
"get-port": "^7.2.0",
"gray-matter": "^4.0.3",
"lint-staged": "^16.4.0",
@ -168,7 +167,6 @@
"nanoid": "^5.1.16",
"obug": "^2.1.3",
"ora": "^9.4.1",
"oxc-minify": "^0.98.0",
"p-map": "^7.0.4",
"package-directory": "^8.2.0",
"path-to-regexp": "^6.3.0",
@ -190,13 +188,12 @@
"sitemap": "^9.0.1",
"tinyglobby": "^0.2.17",
"typescript": "^5.9.3",
"vitest": "4.0.0-beta.4",
"vitest": "^4.1.9",
"vue-tsc": "^3.3.5",
"wait-on": "^9.0.10"
},
"peerDependencies": {
"markdown-it-mathjax3": "^4",
"oxc-minify": "*",
"postcss": "^8"
},
"peerDependenciesMeta": {
@ -205,9 +202,6 @@
},
"postcss": {
"optional": true
},
"oxc-minify": {
"optional": true
}
},
"packageManager": "pnpm@10.33.0"

File diff suppressed because it is too large Load Diff

@ -13,7 +13,6 @@ onlyBuiltDependencies:
overrides:
ora>string-width: ^5
vite: npm:rolldown-vite@latest
patchedDependencies:
'@types/markdown-it-attrs': patches/@types__markdown-it-attrs@4.1.3.patch

@ -3,7 +3,7 @@ import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import * as fs from 'node:fs/promises'
import { rm } from 'node:fs/promises'
import { builtinModules, createRequire } from 'node:module'
import { type RollupOptions, defineConfig } from 'rollup'
import dts from 'rollup-plugin-dts'
@ -33,7 +33,7 @@ const plugins = [
}),
commonjs(),
nodeResolve({ preferBuiltins: false }),
esbuild({ target: 'node20' }),
esbuild({ target: 'node22' }),
json()
]
@ -88,7 +88,7 @@ const clientTypes: RollupOptions = {
name: 'cleanup',
async closeBundle() {
if (PROD) {
await fs.rm('dist/client-types', { recursive: true })
await rm('dist/client-types', { recursive: true })
}
}
}

@ -1,11 +1,11 @@
import { copy } from 'fs-extra'
import { cp } from 'node:fs/promises'
import { globSync } from 'tinyglobby'
function toDest(file) {
function toDest(file: string) {
return file.replace(/^src\//, 'dist/')
}
globSync(['src/client/**']).forEach((file) => {
if (/(\.ts|tsconfig\.json)$/.test(file)) return
copy(file, toDest(file))
cp(file, toDest(file))
})

@ -1,9 +0,0 @@
import { copy } from 'fs-extra'
import { globSync } from 'tinyglobby'
globSync(['src/shared/**/*.ts']).forEach(async (file) => {
await Promise.all([
copy(file, file.replace(/^src\/shared\//, 'src/node/')),
copy(file, file.replace(/^src\/shared\//, 'src/client/'))
])
})

@ -0,0 +1,9 @@
import { cp } from 'node:fs/promises'
import { globSync } from 'tinyglobby'
globSync(['src/shared/**/*.ts']).forEach(async (file) => {
await Promise.all([
cp(file, file.replace(/^src\/shared\//, 'src/node/')),
cp(file, file.replace(/^src\/shared\//, 'src/client/'))
])
})

@ -1,8 +1,9 @@
import { spawn } from 'node:child_process'
import { readFileSync, writeFileSync } from 'node:fs'
import { spawn } from 'cross-spawn'
import type { SpawnOptions } from 'node:child_process'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import c from 'picocolors'
import prompts from 'prompts'
import semver from 'semver'
@ -12,17 +13,16 @@ const { version: currentVersion } = createRequire(import.meta.url)(
)
const { inc: _inc, valid } = semver
const versionIncrements = ['patch', 'minor', 'major']
const versionIncrements = ['patch', 'minor', 'major'] as const
const tags = ['latest', 'next']
const tags = ['latest', 'next'] as const
const dir = fileURLToPath(new URL('.', import.meta.url))
const inc = (i) => _inc(currentVersion, i)
const run = (bin, args, opts = {}) =>
new Promise((resolve, reject) => {
const inc = (i: semver.ReleaseType) => _inc(currentVersion, i)
const run = (bin: string, args: string[], opts: SpawnOptions = {}) =>
new Promise<void>((resolve, reject) => {
const child = spawn(bin, args, {
stdio: 'inherit',
shell: process.platform === 'win32',
...opts
})
@ -37,10 +37,10 @@ const run = (bin, args, opts = {}) =>
}
})
})
const step = (msg) => console.log(c.cyan(msg))
const step = (msg: string) => console.log(c.cyan(msg))
async function main() {
let targetVersion
let targetVersion: string
const versions = versionIncrements
.map((i) => `${i} (${inc(i)})`)
@ -50,7 +50,7 @@ async function main() {
type: 'select',
name: 'release',
message: 'Select release type',
choices: versions
choices: versions.map((title, value) => ({ title, value }))
})
if (release === 3) {
@ -63,7 +63,7 @@ async function main() {
})
).version
} else {
targetVersion = versions[release].match(/\((.*)\)/)[1]
targetVersion = versions[release].match(/\((.*)\)/)![1]
}
if (!valid(targetVersion)) {
@ -74,7 +74,7 @@ async function main() {
type: 'select',
name: 'tag',
message: 'Select tag type',
choices: tags
choices: tags.map((title, value) => ({ title, value }))
})
const { yes: tagOk } = await prompts({
@ -132,13 +132,13 @@ async function main() {
await run('git', ['push'])
}
function updatePackage(version) {
function updatePackage(version: string) {
const pkgPath = resolve(resolve(dir, '..'), 'package.json')
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
pkg.version = version
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
main().catch((err) => console.error(err))

@ -1,36 +0,0 @@
import { watch } from 'chokidar'
import { copy, remove } from 'fs-extra'
import { normalizePath } from 'vite'
function toClientAndNode(method, file) {
file = normalizePath(file)
if (method === 'copy') {
copy(file, file.replace(/^src\/shared\//, 'src/node/'))
copy(file, file.replace(/^src\/shared\//, 'src/client/'))
} else if (method === 'remove') {
remove(file.replace(/^src\/shared\//, 'src/node/'))
remove(file.replace(/^src\/shared\//, 'src/client/'))
}
}
function toDist(file) {
return normalizePath(file).replace(/^src\//, 'dist/')
}
// copy shared files to the client and node directory whenever they change.
watch('src/shared', {
ignored: (path, stats) => stats?.isFile() && !path.endsWith('.ts')
})
.on('change', (file) => toClientAndNode('copy', file))
.on('add', (file) => toClientAndNode('copy', file))
.on('unlink', (file) => toClientAndNode('remove', file))
// copy non ts files, such as an html or css, to the dist directory whenever
// they change.
watch('src/client', {
ignored: (path, stats) =>
stats?.isFile() && (path.endsWith('.ts') || path.endsWith('tsconfig.json'))
})
.on('change', (file) => copy(file, toDist(file)))
.on('add', (file) => copy(file, toDist(file)))
.on('unlink', (file) => remove(toDist(file)))

@ -0,0 +1,37 @@
import { watch } from 'chokidar'
import { cp, rm } from 'node:fs/promises'
import { normalizePath } from 'vite'
function toClientAndNode(method: 'copy' | 'remove', file: string) {
file = normalizePath(file)
if (method === 'copy') {
cp(file, file.replace(/^src\/shared\//, 'src/node/'))
cp(file, file.replace(/^src\/shared\//, 'src/client/'))
} else if (method === 'remove') {
rm(file.replace(/^src\/shared\//, 'src/node/'), { force: true })
rm(file.replace(/^src\/shared\//, 'src/client/'), { force: true })
}
}
function toDist(file: string) {
return normalizePath(file).replace(/^src\//, 'dist/')
}
// copy shared files to the client and node directory whenever they change.
watch('src/shared', {
ignored: (path, stats) => !!stats?.isFile() && !path.endsWith('.ts')
})
.on('change', (file) => toClientAndNode('copy', file))
.on('add', (file) => toClientAndNode('copy', file))
.on('unlink', (file) => toClientAndNode('remove', file))
// copy non ts files, such as an html or css, to the dist directory whenever
// they change.
watch('src/client', {
ignored: (path, stats) =>
!!stats?.isFile() &&
(path.endsWith('.ts') || path.endsWith('tsconfig.json'))
})
.on('change', (file) => cp(file, toDist(file)))
.on('add', (file) => cp(file, toDist(file)))
.on('unlink', (file) => rm(toDist(file), { force: true }))

@ -2,8 +2,8 @@
// so the user can do `import { useRoute, useData } from 'vitepress'`
// generic types
export type { VitePressData } from './shared'
export type { Route, Router } from './app/router'
export type { VitePressData } from './shared'
// theme types
export type { EnhanceAppContext, Theme } from './app/theme'

@ -1,12 +1,16 @@
<script setup lang="ts">
import type { DocSearchInstance, DocSearchProps } from '@docsearch/js'
import type { SidepanelInstance, SidepanelProps } from '@docsearch/sidepanel-js'
import type { SidepanelInstance } from '@docsearch/sidepanel-js'
import { inBrowser, useRouter } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { nextTick, onUnmounted, watch } from 'vue'
import type { DocSearchAskAi } from '../../../../types/docsearch'
import { useData } from '../composables/data'
import { resolveMode, validateCredentials } from '../support/docsearch'
import {
buildSidePanelProps,
resolveMode,
validateCredentials
} from '../support/docsearch'
import '../styles/docsearch.css'
@ -105,12 +109,7 @@ async function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
if (currentInitialize !== initializeCount) return
sidepanelInstance = sidepanel({
...(askAi.sidePanel === true ? {} : askAi.sidePanel),
container: '#vp-docsearch-sidepanel',
indexName: askAi.indexName ?? userOptions.indexName,
appId: askAi.appId ?? userOptions.appId,
apiKey: askAi.apiKey ?? userOptions.apiKey,
assistantId: askAi.assistantId,
...buildSidePanelProps(askAi, userOptions),
onOpen: focusInput,
onClose: onClose.bind(null, 'sidepanel'),
onReady: () => {
@ -122,7 +121,7 @@ async function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
keyboardShortcuts: {
'Ctrl/Cmd+I': false
}
} as SidepanelProps)
})
}
const options = {

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { normalizeLink } from '../support/utils'
import { EXTERNAL_URL_RE } from '../../shared'
import { normalizeLink } from '../support/utils'
interface Props {
tag?: string

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { ref, watch, onMounted } from 'vue'
import { onMounted, ref, watch } from 'vue'
import { useAside } from '../composables/aside'
import { useData } from '../composables/data'

@ -1,7 +1,7 @@
<script setup lang="ts">
import { useData } from '../composables/data'
import VPDocAsideOutline from './VPDocAsideOutline.vue'
import VPDocAsideCarbonAds from './VPDocAsideCarbonAds.vue'
import VPDocAsideOutline from './VPDocAsideOutline.vue'
const { theme } = useData()
</script>

@ -1,6 +1,6 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import type { DefaultTheme } from 'vitepress/theme'
import { defineAsyncComponent } from 'vue'
defineProps<{
carbonAds: DefaultTheme.CarbonAdsOptions

@ -1,9 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout'
import { resolveTitle, useActiveAnchor } from '../composables/outline'
import VPDocOutlineItem from './VPDocOutlineItem.vue'
import { useLayout } from '../composables/layout'
const { theme } = useData()

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Sponsors } from './VPSponsors.vue'
import type { Sponsor } from './VPSponsorsGrid.vue'
import VPSponsors from './VPSponsors.vue'
import type { Sponsor } from './VPSponsorsGrid.vue'
defineProps<{
tier?: string

@ -3,8 +3,8 @@ import { computed } from 'vue'
import { useData } from '../composables/data'
import { useEditLink } from '../composables/edit-link'
import { usePrevNext } from '../composables/prev-next'
import VPLink from './VPLink.vue'
import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue'
import VPLink from './VPLink.vue'
const { theme, page, frontmatter } = useData()
@ -53,6 +53,8 @@ const showFooter = computed(
v-if="control.prev?.link"
class="pager-link prev"
:href="control.prev.link"
:target="control.prev.target"
:rel="control.prev.rel"
>
<span
class="desc"
@ -66,6 +68,8 @@ const showFooter = computed(
v-if="control.next?.link"
class="pager-link next"
:href="control.next.link"
:target="control.next.target"
:rel="control.next.rel"
>
<span
class="desc"

@ -1,8 +1,8 @@
<script setup lang="ts">
import VPHomeHero from './VPHomeHero.vue'
import VPHomeFeatures from './VPHomeFeatures.vue'
import VPHomeContent from './VPHomeContent.vue'
import { useData } from '../composables/data'
import VPHomeContent from './VPHomeContent.vue'
import VPHomeFeatures from './VPHomeFeatures.vue'
import VPHomeHero from './VPHomeHero.vue'
const { frontmatter, theme } = useData()
</script>

@ -2,14 +2,16 @@
import { computed } from 'vue'
import { isLinkExternal, normalizeLink } from '../support/utils'
const props = defineProps<{
const props = withDefaults(defineProps<{
tag?: string
href?: string
noIcon?: boolean
external?: boolean
target?: string
rel?: string
}>()
}>(), {
external: undefined,
})
const tag = computed(() => props.tag ?? (props.href ? 'a' : 'span'))
const isExternal = computed(() =>

@ -67,25 +67,34 @@ const { activate } = useFocusTrap(el, {
escapeDeactivates: true
})
const { localeIndex, theme } = vitePressData
const searchIndex = computedAsync(async () =>
markRaw(
MiniSearch.loadJSON<Result>(
(await searchIndexData.value[localeIndex.value]?.())?.default,
{
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
searchOptions: {
fuzzy: 0.2,
prefix: true,
boost: { title: 4, text: 2, titles: 1 },
const isSearchIndexLoading = ref(false)
const isSearching = ref(false)
const showSearchSpinner = computed(() => {
return isSearchIndexLoading.value || isSearching.value
})
const searchIndex = computedAsync(
async () =>
markRaw(
MiniSearch.loadJSON<Result>(
(await searchIndexData.value[localeIndex.value]?.())?.default,
{
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
searchOptions: {
fuzzy: 0.2,
prefix: true,
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?.searchOptions)
},
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.options)
}
)
)
theme.value.search.options?.miniSearch?.options)
}
)
),
undefined,
isSearchIndexLoading
)
const disableQueryPersistence = computed(() => {
@ -145,9 +154,15 @@ watchDebounced(
let canceled = false
onCleanup(() => {
canceled = true
isSearching.value = false
})
if (!index) return
if (!index) {
results.value = []
return
}
isSearching.value = true
// Search
results.value = index
@ -232,6 +247,7 @@ watchDebounced(
}
// FIXME: without this whole page scrolls to the bottom
resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' })
isSearching.value = false
},
{ debounce: 200, immediate: true }
)
@ -487,6 +503,14 @@ function onMouseMove(e: MouseEvent) {
type="search"
/>
<div class="search-actions">
<span
class="search-loading"
:class="{ active: showSearchSpinner }"
:role="showSearchSpinner ? 'status' : undefined"
aria-live="polite"
:aria-label="showSearchSpinner ? 'Loading search results' : undefined"
/>
<button
v-if="!disableDetailedView"
class="toggle-layout-button"
@ -516,6 +540,7 @@ function onMouseMove(e: MouseEvent) {
ref="resultsEl"
:id="results?.length ? 'localsearch-list' : undefined"
:role="results?.length ? 'listbox' : undefined"
:aria-busy="showSearchSpinner ? 'true' : 'false'"
:aria-labelledby="results?.length ? 'localsearch-label' : undefined"
class="results"
@mousemove="onMouseMove"
@ -724,6 +749,34 @@ function onMouseMove(e: MouseEvent) {
opacity: 0.37;
}
.search-loading {
visibility: hidden;
margin: 8px;
width: 18px;
height: 18px;
flex: none;
border: 2px solid var(--vp-c-divider);
border-top-color: var(--vp-c-brand-1);
border-radius: 50%;
}
.search-loading.active {
visibility: visible;
animation: local-search-loading 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.search-loading.active {
animation: none;
}
}
@keyframes local-search-loading {
to {
transform: rotate(360deg);
}
}
.search-keyboard-shortcuts {
font-size: 0.8rem;
opacity: 75%;

@ -1,8 +1,8 @@
<script lang="ts" setup generic="T extends DefaultTheme.NavItemWithLink">
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import { useData } from '../composables/data'
import VPLink from './VPLink.vue'
const props = defineProps<{

@ -1,11 +1,11 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useData } from '../composables/data'
import { useLangs } from '../composables/langs'
import VPFlyout from './VPFlyout.vue'
import VPMenuLink from './VPMenuLink.vue'
import VPSwitchAppearance from './VPSwitchAppearance.vue'
import VPSocialLinks from './VPSocialLinks.vue'
import { useData } from '../composables/data'
import { useLangs } from '../composables/langs'
import VPSwitchAppearance from './VPSwitchAppearance.vue'
const { site, theme } = useData()
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useData } from '../composables/data'
import VPNavBarMenuLink from './VPNavBarMenuLink.vue'
import VPNavBarMenuGroup from './VPNavBarMenuGroup.vue'
import VPNavBarMenuLink from './VPNavBarMenuLink.vue'
const { theme } = useData()
</script>

@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import { useData } from '../composables/data'
import VPFlyout from './VPFlyout.vue'
const props = defineProps<{

@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import { useData } from '../composables/data'
import VPLink from './VPLink.vue'
const props = defineProps<{

@ -1,8 +1,8 @@
<script lang="ts" setup>
import VPFlyout from './VPFlyout.vue'
import VPMenuLink from './VPMenuLink.vue'
import { useData } from '../composables/data'
import { useLangs } from '../composables/langs'
import VPFlyout from './VPFlyout.vue'
import VPMenuLink from './VPMenuLink.vue'
const { theme } = useData()
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useData } from '../composables/data'
import VPNavScreenMenuLink from './VPNavScreenMenuLink.vue'
import VPNavScreenMenuGroup from './VPNavScreenMenuGroup.vue'
import VPNavScreenMenuLink from './VPNavScreenMenuLink.vue'
const { theme } = useData()
</script>

@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed, inject } from 'vue'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import { useData } from '../composables/data'
import { navInjectionKey } from '../composables/nav'
import VPLink from './VPLink.vue'

@ -1,8 +1,8 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed, inject } from 'vue'
import { useData } from '../composables/data'
import { isActive } from '../../shared'
import { useData } from '../composables/data'
import { navInjectionKey } from '../composables/nav'
import VPLink from './VPLink.vue'

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

@ -1,12 +1,13 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed, nextTick, onMounted, ref, useSSRContext } from 'vue'
import type { SSGContext } from '../../shared'
import { isExternal, type SSGContext } from '../../shared'
const props = defineProps<{
icon: DefaultTheme.SocialLinkIcon
link: string
ariaLabel?: string
target?: string
me: boolean
}>()
@ -45,7 +46,7 @@ if (import.meta.env.SSR) {
class="VPSocialLink no-icon"
:href="link"
:aria-label="ariaLabel ?? (typeof icon === 'string' ? icon : '')"
target="_blank"
:target="target ?? (isExternal(link) ? '_blank' : undefined)"
:rel="me ? 'me noopener' : 'noopener'"
v-html="svg"
></a>

@ -13,11 +13,12 @@ withDefaults(defineProps<{
<template>
<div class="VPSocialLinks">
<VPSocialLink
v-for="{ link, icon, ariaLabel } in links"
v-for="{ link, icon, ariaLabel, target } in links"
:key="link"
:icon
:link
:ariaLabel
:target
:me
/>
</div>

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { GridSize } from '../composables/sponsor-grid'
import type { Sponsor } from './VPSponsorsGrid.vue'
import { computed } from 'vue'
import VPSponsorsGrid from './VPSponsorsGrid.vue'
export interface Sponsors {

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { GridSize } from '../composables/sponsor-grid'
import { ref } from 'vue'
import type { GridSize } from '../composables/sponsor-grid'
import { useSponsorsGrid } from '../composables/sponsor-grid'
export interface Sponsor {

@ -85,10 +85,12 @@ export function useActiveAnchor(
const onScroll = throttleAndDebounce(setActiveLink, 100)
let prevActiveLink: HTMLAnchorElement | null = null
let ignoreScrollOnce: boolean = false
onMounted(() => {
requestAnimationFrame(setActiveLink)
window.addEventListener('scroll', onScroll)
container.value.addEventListener('click', onClick)
})
onUpdated(() => {
@ -98,13 +100,33 @@ export function useActiveAnchor(
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
container.value.removeEventListener('click', onClick)
})
function onClick(e: MouseEvent) {
if (!isAsideEnabled.value) {
return
}
const hash =
e.target instanceof Element ? e.target.closest('a')?.hash : null
if (hash) {
ignoreScrollOnce = true
activateLink(hash)
}
}
function setActiveLink() {
if (!isAsideEnabled.value) {
return
}
if (ignoreScrollOnce) {
ignoreScrollOnce = false
return
}
const scrollY = window.scrollY
const innerHeight = window.innerHeight
const offsetHeight = document.body.offsetHeight
@ -159,7 +181,7 @@ export function useActiveAnchor(
prevActiveLink = null
} else {
prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]`
`a[href$="${decodeURIComponent(hash)}"]`
)
}

@ -40,7 +40,15 @@ export function usePrevNext() {
link:
(typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.link
: undefined) ?? candidates[index - 1]?.link
: undefined) ?? candidates[index - 1]?.link,
target:
(typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.target
: undefined) ?? candidates[index - 1]?.target,
rel:
(typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.rel
: undefined) ?? candidates[index - 1]?.rel
},
next: hideNext
? undefined
@ -56,11 +64,19 @@ export function usePrevNext() {
link:
(typeof frontmatter.value.next === 'object'
? frontmatter.value.next.link
: undefined) ?? candidates[index + 1]?.link
: undefined) ?? candidates[index + 1]?.link,
target:
(typeof frontmatter.value.next === 'object'
? frontmatter.value.next.target
: undefined) ?? candidates[index + 1]?.target,
rel:
(typeof frontmatter.value.next === 'object'
? frontmatter.value.next.rel
: undefined) ?? candidates[index + 1]?.rel
}
} as {
prev?: { text?: string; link?: string }
next?: { text?: string; link?: string }
prev?: { text?: string; link?: string; target?: string; rel?: string }
next?: { text?: string; link?: string; target?: string; rel?: string }
}
})
}

@ -41,10 +41,6 @@
--docsearch-modal-shadow: none;
}
:is(.DocSearch-Container, .DocSearch-Sidepanel) svg {
overflow: visible;
}
.DocSearch-AskAiScreen-RelatedSources-Item-Link {
padding: 8px 12px 8px 10px;
}

@ -1,3 +1,4 @@
import type { SidepanelProps } from '@docsearch/sidepanel-js'
import type { DefaultTheme } from 'vitepress/theme'
import type { DocSearchAskAi } from '../../../../types/docsearch'
import { isObject } from '../../shared'
@ -19,6 +20,14 @@ export interface ResolvedMode {
useSidePanel: boolean
}
// FIXME: remove when https://github.com/algolia/docsearch/pull/2906 is released
export type ResolvedSidePanelProps = SidepanelProps & {
agentStudio?: boolean
searchParameters?: DocSearchAskAi['searchParameters']
suggestedQuestions?: boolean
useStagingEnv?: boolean
}
/**
* Resolves the effective mode based on config and available features.
*
@ -168,10 +177,8 @@ export function buildAskAiConfig(
!isAskAiString && askAiProp.searchParameters
? { ...askAiProp.searchParameters }
: undefined
const isAgentStudio = !isAskAiString && askAiProp.agentStudio === true
// If Ask AI defines its own facetFilters, merge lang filtering into those.
// Otherwise, reuse the keyword search facetFilters so Ask AI follows the
// same language filtering behavior by default.
const askAiFacetFiltersSource =
askAiSearchParameters?.facetFilters ??
options.searchParameters?.facetFilters
@ -180,10 +187,12 @@ export function buildAskAiConfig(
lang
)
const mergedAskAiSearchParameters = {
...askAiSearchParameters,
facetFilters: askAiFacetFilters.length ? askAiFacetFilters : undefined
}
const mergedAskAiSearchParameters = isAgentStudio
? askAiSearchParameters
: {
...askAiSearchParameters,
facetFilters: askAiFacetFilters.length ? askAiFacetFilters : undefined
}
const result: Record<string, any> = {
...(isAskAiString ? {} : askAiProp),
@ -194,13 +203,37 @@ export function buildAskAiConfig(
}
// Keep `searchParameters` undefined unless it has at least one key.
if (Object.values(mergedAskAiSearchParameters).some((v) => v != null)) {
if (
mergedAskAiSearchParameters &&
Object.values(mergedAskAiSearchParameters).some((v) => v != null)
) {
result.searchParameters = mergedAskAiSearchParameters
}
return result
}
/**
* Builds the DocSearch side panel config from the resolved Ask AI options.
*/
export function buildSidePanelProps(
askAi: DocSearchAskAi,
options: DefaultTheme.AlgoliaSearchOptions
): ResolvedSidePanelProps {
const { sidePanel, ...askAiRest } = JSON.parse(
JSON.stringify(askAi)
) as DocSearchAskAi
return {
container: '#vp-docsearch-sidepanel',
indexName: options.indexName,
appId: options.appId,
apiKey: options.apiKey,
...askAiRest,
...(sidePanel && sidePanel !== true ? sidePanel : {})
} as ResolvedSidePanelProps
}
/**
* Resolves Algolia search options for the given language,
* merging in locale-specific overrides and language facet filters.

@ -1,11 +1,13 @@
import type { DefaultTheme } from 'vitepress/theme'
import { isActive } from '../../shared'
import { isActive, isExternal } from '../../shared'
import { ensureStartingSlash } from './utils'
export interface SidebarLink {
text: string
link: string
docFooterText?: string
rel?: string
target?: string
}
type SidebarItem = DefaultTheme.SidebarItem
@ -75,7 +77,9 @@ export function getFlatSideBarLinks(sidebar: SidebarItem[]): SidebarLink[] {
links.push({
text: item.text,
link: item.link,
docFooterText: item.docFooterText
docFooterText: item.docFooterText,
rel: item.rel,
target: item.target
})
}
@ -112,7 +116,7 @@ function addBase(items: SidebarItem[], _base?: string): SidebarItem[] {
return [...items].map((_item) => {
const item = { ..._item }
const base = item.base || _base
if (base && item.link)
if (base && item.link && !isExternal(item.link))
item.link = base + item.link.replace(/^\//, base.endsWith('/') ? '' : '/')
if (item.items) item.items = addBase(item.items, base)
return item

@ -1,17 +1,16 @@
import { getIconsCSS } from '@iconify/utils'
import fs from 'fs-extra'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import pMap from 'p-map'
import { packageDirectorySync } from 'package-directory'
import * as vite from 'vite'
import type { BuildOptions, Rollup } from 'vite'
import type { BuildOptions, Rolldown } from 'vite'
import { resolveConfig, type SiteConfig } from '../config'
import { clearCache } from '../markdownToVue'
import { slash, type Awaitable, type HeadConfig } from '../shared'
import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize'
import { nativeImport } from '../utils/nativeImport'
import { task } from '../utils/task'
import { bundle } from './bundle'
import { generateSitemap } from './generateSitemap'
@ -29,19 +28,6 @@ export async function build(
) {
const start = Date.now()
// @ts-ignore only exists for rolldown-vite
if (vite.rolldownVersion) {
try {
await import('oxc-minify')
} catch {
throw new Error(
'`oxc-minify` is not installed.' +
' vitepress requires `oxc-minify` to be installed when rolldown-vite is used.' +
' Please run `npm install oxc-minify`.'
)
}
}
process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(root, 'build', 'production')
@ -76,43 +62,45 @@ export async function build(
}
const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).href)
const { render } = await nativeImport(entryPath)
await task('rendering pages', async () => {
const appChunk =
clientResult &&
(clientResult.output.find(
(chunk) =>
chunk.type === 'chunk' &&
chunk.isEntry &&
chunk.facadeModuleId?.endsWith('.js')
) as Rollup.OutputChunk)
const cssChunk = (
siteConfig.mpa ? serverResult : clientResult!
).output.find(
(chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as Rollup.OutputAsset
const assets = (siteConfig.mpa ? serverResult : clientResult!).output
.filter(
(chunk) => chunk.type === 'asset' && !chunk.fileName.endsWith('.css')
)
.map((asset) => siteConfig.site.base + asset.fileName)
const clientOutput: (Rolldown.OutputChunk | Rolldown.OutputAsset)[] =
clientResult?.output || []
const appChunk = clientOutput.find(
(chunk): chunk is Rolldown.OutputChunk =>
chunk.type === 'chunk' &&
chunk.isEntry &&
!!chunk.facadeModuleId?.endsWith('.js')
)
// default theme special handling: inject font preload
// custom themes will need to use `transformHead` to inject this
const additionalHeadTags: HeadConfig[] = []
const isDefaultTheme =
clientResult &&
clientResult.output.some(
(chunk) =>
chunk.type === 'chunk' &&
// @ts-ignore only exists for rolldown-vite
(vite.rolldownVersion || chunk.name === 'theme') && // FIXME: remove when rolldown-vite supports manualChunks
chunk.moduleIds.some((id) => id.includes('client/theme-default'))
)
const isDefaultTheme = clientOutput.some(
(chunk): chunk is Rolldown.OutputChunk =>
chunk.type === 'chunk' &&
chunk.name === 'theme' &&
chunk.moduleIds.some((id) => id.includes('client/theme-default'))
)
// ----
const resultOutput: (Rolldown.OutputChunk | Rolldown.OutputAsset)[] =
(siteConfig.mpa ? serverResult : clientResult)?.output || []
const cssChunk = resultOutput.find(
(chunk): chunk is Rolldown.OutputAsset =>
chunk.type === 'asset' && chunk.fileName.endsWith('.css')
)
// prettier-ignore
const assets = resultOutput.filter(
(chunk): chunk is Rolldown.OutputAsset =>
chunk.type === 'asset' && !chunk.fileName.endsWith('.css')
).map((asset) => siteConfig.site.base + asset.fileName)
// ----
const additionalHeadTags: HeadConfig[] = []
const metadataScript = generateMetadataScript(pageToHashMap, siteConfig)
if (isDefaultTheme) {
@ -169,9 +157,9 @@ export async function build(
// emit page hash map for the case where a user session is open
// when the site got redeployed (which invalidates current hash map)
fs.writeJSONSync(
fs.writeFileSync(
path.join(siteConfig.outDir, 'hashmap.json'),
pageToHashMap
JSON.stringify(pageToHashMap)
)
} finally {
unlinkVue()
@ -200,7 +188,8 @@ function linkVue() {
// if user did not install vue by themselves, link VitePress' version
if (!fs.existsSync(dest)) {
const src = path.dirname(createRequire(import.meta.url).resolve('vue'))
fs.ensureSymlinkSync(src, dest, 'junction')
fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.symlinkSync(src, dest, 'junction')
return () => {
fs.unlinkSync(dest)
}
@ -248,7 +237,7 @@ function generateMetadataScript(
const resolvedMetadataFile = path.join(config.outDir, metadataFile)
const metadataFileURL = slash(`${config.site.base}${metadataFile}`)
fs.ensureDirSync(path.dirname(resolvedMetadataFile))
fs.mkdirSync(path.dirname(resolvedMetadataFile), { recursive: true })
fs.writeFileSync(resolvedMetadataFile, metadataContent)
return {

@ -1,4 +1,4 @@
import { build, type Rollup } from 'vite'
import { build, type Rolldown } from 'vite'
import type { SiteConfig } from '..'
const virtualEntry = 'client.js'
@ -6,7 +6,7 @@ const virtualEntry = 'client.js'
export async function buildMPAClient(
js: Record<string, string>,
config: SiteConfig
): Promise<Rollup.RollupOutput> {
): Promise<Rolldown.RolldownOutput> {
const files = Object.keys(js)
const themeFiles = files.filter((f) => !f.endsWith('.md'))
const pages = files.filter((f) => f.endsWith('.md'))
@ -19,7 +19,7 @@ export async function buildMPAClient(
build: {
emptyOutDir: false,
outDir: config.outDir,
rollupOptions: {
rolldownOptions: {
input: [virtualEntry, ...pages]
}
},
@ -42,5 +42,5 @@ export async function buildMPAClient(
}
}
]
}) as Promise<Rollup.RollupOutput>
}) as Promise<Rolldown.RolldownOutput>
}

@ -1,12 +1,13 @@
import fs from 'fs-extra'
import fs from 'node:fs'
import { cp } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import * as vite from 'vite'
import pMap from 'p-map'
import {
build,
normalizePath,
type BuildOptions,
type Rollup,
type Rolldown,
type InlineConfig as ViteInlineConfig
} from 'vite'
import { APP_PATH } from '../alias'
@ -16,7 +17,7 @@ import { escapeRegExp, sanitizeFileName, slash } from '../shared'
import { task } from '../utils/task'
import { buildMPAClient } from './buildMPAClient'
// https://github.com/vitejs/vite/blob/d2aa0969ee316000d3b957d7e879f001e85e369e/packages/vite/src/node/plugins/splitVendorChunk.ts#L14
// https://github.com/vitejs/vite/blob/a55d0b34400e3360c4100d05e422ae9cf10fa07b/packages/vite/src/node/constants.ts#L50
const CSS_LANGS_RE =
/\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/
@ -39,14 +40,14 @@ export async function bundle(
config: SiteConfig,
options: BuildOptions
): Promise<{
clientResult: Rollup.RollupOutput | null
serverResult: Rollup.RollupOutput
clientResult: Rolldown.RolldownOutput | null
serverResult: Rolldown.RolldownOutput
pageToHashMap: Record<string, string>
}> {
const pageToHashMap = Object.create(null) as Record<string, string>
const clientJSMap = Object.create(null) as Record<string, string>
// define custom rollup input
// define custom rolldown input
// this is a multi-entry build - every page is considered an entry chunk
// the loading is done via filename conversion rules so that the
// metadata doesn't need to be included in the main chunk.
@ -65,7 +66,11 @@ export async function bundle(
)
// resolve options to pass to vite
const { rollupOptions } = options
const {
rollupOptions,
rolldownOptions = rollupOptions,
...restOptions
} = options
const resolveViteConfig = async (
ssr: boolean
@ -84,26 +89,26 @@ export async function bundle(
noExternal: ['vitepress', '@docsearch/css']
},
build: {
...options,
...restOptions,
emptyOutDir: true,
ssr,
ssrEmitAssets: config.mpa,
minify: ssr ? !!config.mpa : (options.minify ?? !process.env.DEBUG),
outDir: ssr ? config.tempDir : config.outDir,
cssCodeSplit: false,
rollupOptions: {
...rollupOptions,
rolldownOptions: {
...rolldownOptions,
input: {
// use different entry based on ssr or not
app: path.resolve(APP_PATH, ssr ? 'ssr.js' : 'index.js'),
...input
},
// important so that each page chunk and the index export things for each
// other
// important so that each page chunk and the index export things for
// each other
preserveEntrySignatures: 'allow-extension',
output: {
sanitizeFileName,
...rollupOptions?.output,
...rolldownOptions?.output,
assetFileNames: `${config.assetsDir}/[name].[hash].[ext]`,
...(ssr
? {
@ -118,34 +123,27 @@ export async function bundle(
? `${config.assetsDir}/chunks/ui-custom.[hash].js`
: `${config.assetsDir}/chunks/[name].[hash].js`
},
// @ts-ignore skip setting it for rolldown-vite since it doesn't support `manualChunks`
...(vite.rolldownVersion
? undefined
: {
manualChunks(
id: string,
ctx: Pick<Rollup.PluginContext, 'getModuleInfo'>
) {
codeSplitting: {
groups: [
{
name(id, ctx) {
const getModuleInfo = ctx.getModuleInfo.bind(ctx)
// avoid emitting multiple files for assets
// see: https://github.com/rolldown/rolldown/issues/4246
if (getModuleInfo(id)?.meta['vite:asset']) {
return 'assets'
}
// move known framework code into a stable chunk so that
// custom theme changes do not invalidate hash for all pages
if (
id.startsWith('\0vite') ||
ctx.getModuleInfo(id)?.meta['vite:asset']
) {
return 'framework'
}
if (id.includes('plugin-vue:export-helper')) {
return 'framework'
}
if (
id.includes(`${clientDir}/app`) &&
id !== `${clientDir}/app/index.js`
) {
return 'framework'
}
if (
isEagerChunk(id, ctx.getModuleInfo) &&
/@vue\/(runtime|shared|reactivity)/.test(id)
id.includes('plugin-vue:export-helper') ||
(id.includes(`${clientDir}/app`) &&
id !== `${clientDir}/app/index.js`) ||
(isEagerChunk(id, getModuleInfo) &&
/@vue\/(runtime|shared|reactivity)/.test(id))
) {
return 'framework'
}
@ -155,7 +153,7 @@ export async function bundle(
!excludedModules.some((i) => id.includes(i))) &&
staticImportedByEntry(
id,
ctx.getModuleInfo,
getModuleInfo,
cacheTheme,
themeEntryRE
)
@ -163,43 +161,55 @@ export async function bundle(
return 'theme'
}
}
})
}
]
}
})
},
checks: {
invalidAnnotation: false, // FIXME: remove when vueuse releases a new version
pluginTimings: false,
...rolldownOptions?.checks
}
}
},
configFile: config.vite?.configFile
})
let clientResult!: Rollup.RollupOutput | null
let serverResult!: Rollup.RollupOutput
let clientResult: Rolldown.RolldownOutput | null = null
let serverResult!: Rolldown.RolldownOutput
// prettier-ignore
await task('building client + server bundles', async () => {
clientResult = config.mpa
? null
: ((await build(await resolveViteConfig(false))) as Rollup.RollupOutput)
serverResult = (await build(
await resolveViteConfig(true)
)) as Rollup.RollupOutput
if (!config.mpa) clientResult =
(await build(await resolveViteConfig(false))) as Rolldown.RolldownOutput
serverResult =
(await build(await resolveViteConfig(true))) as Rolldown.RolldownOutput
})
if (config.mpa) {
// in MPA mode, we need to copy over the non-js asset files from the
// server build since there is no client-side build.
await Promise.all(
serverResult.output.map(async (chunk) => {
await pMap(
serverResult.output,
async (chunk) => {
if (!chunk.fileName.endsWith('.js')) {
const tempPath = path.resolve(config.tempDir, chunk.fileName)
const outPath = path.resolve(config.outDir, chunk.fileName)
await fs.copy(tempPath, outPath)
await cp(tempPath, outPath)
}
})
},
{ concurrency: config.buildConcurrency }
)
// also copy over public dir
const publicDir = path.resolve(config.srcDir, 'public')
if (fs.existsSync(publicDir)) {
await fs.copy(publicDir, config.outDir)
// dereference symlinks like vite's own publicDir copy does, and so that
// copying over an existing symlinked file does not fail with EEXIST
await cp(publicDir, config.outDir, { recursive: true, dereference: true })
}
// build <script client> bundle
if (Object.keys(clientJSMap).length) {
clientResult = await buildMPAClient(clientJSMap, config)
@ -223,7 +233,7 @@ const cacheTheme = new Map<string, boolean>()
/**
* Check if a module is statically imported by at least one entry.
*/
function isEagerChunk(id: string, getModuleInfo: Rollup.GetModuleInfo) {
function isEagerChunk(id: string, getModuleInfo: Rolldown.GetModuleInfo) {
if (
id.includes('node_modules') &&
!CSS_LANGS_RE.test(id) &&
@ -235,7 +245,7 @@ function isEagerChunk(id: string, getModuleInfo: Rollup.GetModuleInfo) {
function staticImportedByEntry(
id: string,
getModuleInfo: Rollup.GetModuleInfo,
getModuleInfo: Rolldown.GetModuleInfo,
cache: Map<string, boolean>,
entryRE: RegExp | null = null,
importStack: string[] = []

@ -1,5 +1,5 @@
import fs from 'fs-extra'
import matter from 'gray-matter'
import fs from 'node:fs'
import path from 'node:path'
import {
SitemapStream,

@ -1,9 +1,8 @@
import { isBooleanAttr } from '@vue/shared'
import fs from 'fs-extra'
import fs from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import * as vite from 'vite'
import { normalizePath, transformWithEsbuild, type Rollup } from 'vite'
import { minifySync, normalizePath, type Rolldown } from 'vite'
import { version } from '../../../package.json'
import type { SiteConfig } from '../config'
import {
@ -19,14 +18,15 @@ import {
type PageData,
type SSGContext
} from '../shared'
import { nativeImport } from '../utils/nativeImport'
export async function renderPage(
render: (path: string) => Promise<SSGContext>,
config: SiteConfig,
page: string, // foo.md
result: Rollup.RollupOutput | null,
appChunk: Rollup.OutputChunk | null,
cssChunk: Rollup.OutputAsset | null,
result: Rolldown.RolldownOutput | null | undefined,
appChunk: Rolldown.OutputChunk | null | undefined,
cssChunk: Rolldown.OutputAsset | null | undefined,
assets: string[],
pageToHashMap: Record<string, string>,
metadataScript: { html: string; inHead: boolean },
@ -57,8 +57,8 @@ export async function renderPage(
try {
// resolve page data so we can render head tags
const { __pageData } = await import(
pathToFileURL(path.join(config.tempDir, pageServerJsFileName)).href
const { __pageData } = await nativeImport(
path.join(config.tempDir, pageServerJsFileName)
)
pageData = __pageData
} catch (e) {
@ -140,14 +140,16 @@ export async function renderPage(
let inlinedScript = ''
if (config.mpa && result) {
const matchingChunk = result.output.find(
(chunk) =>
(chunk): chunk is Rolldown.OutputChunk =>
chunk.type === 'chunk' &&
chunk.facadeModuleId === slash(path.join(config.srcDir, page))
) as Rollup.OutputChunk
)
if (matchingChunk) {
if (!matchingChunk.code.includes('import')) {
inlinedScript = `<script type="module">${matchingChunk.code}</script>`
fs.removeSync(path.resolve(config.outDir, matchingChunk.fileName))
fs.rmSync(path.resolve(config.outDir, matchingChunk.fileName), {
force: true
})
} else {
inlinedScript = `<script type="module" src="${siteData.base}${matchingChunk.fileName}"></script>`
}
@ -190,7 +192,7 @@ export async function renderPage(
</html>`
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html'))
await fs.ensureDir(path.dirname(htmlFileName))
await mkdir(path.dirname(htmlFileName), { recursive: true })
const transformedHtml = await config.transformHtml?.(html, htmlFileName, {
page,
siteConfig: config,
@ -202,14 +204,14 @@ export async function renderPage(
content,
assets
})
await fs.writeFile(htmlFileName, transformedHtml || html)
await writeFile(htmlFileName, transformedHtml || html)
}
function resolvePageImports(
config: SiteConfig,
page: string,
result: Rollup.RollupOutput,
appChunk: Rollup.OutputChunk
result: Rolldown.RolldownOutput,
appChunk: Rolldown.OutputChunk
) {
page = config.rewrites.inv[page] || page
// find the page's js chunk and inject script tags for its imports so that
@ -225,12 +227,13 @@ function resolvePageImports(
}
srcPath = normalizePath(srcPath)
const pageChunk = result.output.find(
(chunk) => chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
) as Rollup.OutputChunk
(chunk): chunk is Rolldown.OutputChunk =>
chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
)
return [
...appChunk.imports,
// ...appChunk.dynamicImports,
...pageChunk.imports
...(pageChunk?.imports || [])
// ...pageChunk.dynamicImports
]
}
@ -244,7 +247,7 @@ async function renderHead(head: HeadConfig[]): Promise<string> {
tag === 'script' &&
(attrs.type === undefined || attrs.type.includes('javascript'))
) {
innerHTML = await minifyScript(innerHTML, 'inline-script.js')
innerHTML = minifySync('inline-script.js', innerHTML).code
}
return `${openTag}${innerHTML}</${tag}>`
} else {
@ -264,17 +267,6 @@ function renderAttrs(attrs: Record<string, string>): string {
.join('')
}
async function minifyScript(code: string, filename: string): Promise<string> {
// @ts-ignore use oxc-minify when rolldown-vite is used
if (vite.rolldownVersion) {
const oxcMinify = await import('oxc-minify')
return (await oxcMinify.minify(filename, code)).code.trim()
}
return (
await transformWithEsbuild(code, filename, { minify: true })
).code.trim()
}
function filterOutHeadDescription(head: HeadConfig[] = []) {
return head.filter(([type, attrs]) => {
return !(type === 'meta' && attrs?.name === 'description')

@ -1,6 +1,6 @@
import { createDebug } from 'obug'
import fs from 'fs-extra'
import fs from 'node:fs'
import path from 'node:path'
import { createDebug } from 'obug'
import c from 'picocolors'
import {
createLogger,
@ -12,6 +12,7 @@ import {
} from 'vite'
import { DEFAULT_THEME_PATH } from './alias'
import type { DefaultTheme } from './defaultTheme'
import type { MarkdownOptions } from './markdown/markdown'
import { resolvePages } from './plugins/dynamicRoutesPlugin'
import {
APPEARANCE_KEY,
@ -23,7 +24,6 @@ import {
type HeadConfig,
type SiteData
} from './shared'
import type { MarkdownOptions } from './markdown/markdown'
import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig'
import { glob } from './utils/glob'
@ -124,7 +124,7 @@ export async function resolveConfig(
// resolve theme path
const userThemeDir = resolve(root, 'theme')
const themeDir = (await fs.pathExists(userThemeDir))
const themeDir = fs.existsSync(userThemeDir)
? userThemeDir
: DEFAULT_THEME_PATH
@ -240,7 +240,7 @@ export async function resolveUserConfig(
resolve(root, `config/index.${ext}`),
resolve(root, `config.${ext}`)
])
.find(fs.pathExistsSync)
.find(fs.existsSync)
let userConfig: RawConfigExports = {}
let configDeps: string[] = []

@ -1,5 +1,5 @@
import fs from 'fs-extra'
import matter from 'gray-matter'
import fs from 'node:fs'
import path from 'node:path'
import pMap from 'p-map'
import { normalizePath } from 'vite'

@ -7,8 +7,8 @@ import {
select,
text
} from '@clack/prompts'
import fs from 'fs-extra'
import template from 'lodash.template'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import c from 'picocolors'
@ -200,7 +200,8 @@ export function scaffold({
const content = fs.readFileSync(filePath, 'utf-8')
const compiled = template(content)(data)
fs.outputFileSync(targetPath, compiled)
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
fs.writeFileSync(targetPath, compiled)
}
const filesToScaffold = [

@ -1,6 +1,6 @@
import fs from 'fs-extra'
import type { MarkdownItAsync } from 'markdown-it-async'
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
import fs from 'node:fs'
import path from 'node:path'
import type { MarkdownEnv } from '../../shared'

@ -1,8 +1,8 @@
import { resolveTitleFromToken } from '@mdit-vue/shared'
import { createDebug } from 'obug'
import fs from 'fs-extra'
import { LRUCache } from 'lru-cache'
import fs from 'node:fs'
import path from 'node:path'
import { createDebug } from 'obug'
import type { SiteConfig } from './config'
import {
createMarkdownRenderer,
@ -47,6 +47,10 @@ let __dynamicRoutes = new Map<string, [string, string]>()
let __rewrites = new Map<string, string>()
let __ts: number
function normalizeDriveLetter(file: string) {
return file.replace(/^[a-z]:/i, (drive) => drive.toLowerCase())
}
function getResolutionCache(siteConfig: SiteConfig) {
// @ts-expect-error internal
if (siteConfig.__dirty) {
@ -61,8 +65,8 @@ function getResolutionCache(siteConfig: SiteConfig) {
__rewrites = new Map(
Object.entries(siteConfig.rewrites.map).map(([key, value]) => [
slash(path.join(siteConfig.srcDir, key)),
slash(path.join(siteConfig.srcDir, value!))
normalizeDriveLetter(slash(path.join(siteConfig.srcDir, key))),
normalizeDriveLetter(slash(path.join(siteConfig.srcDir, value!)))
])
)
@ -110,7 +114,7 @@ export async function createMarkdownToVueRenderFn(
getPageDataTransformer(dynamicRoute?.[1]!)
].filter((fn) => fn != null)
file = rewrites.get(file) || file
file = rewrites.get(normalizeDriveLetter(file)) || file
const relativePath = slash(path.relative(srcDir, file))
const cacheKey = JSON.stringify({ src, ts, relativePath })

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

Loading…
Cancel
Save