feat(build): switch to vite 3, support clean urls and esm mode (#856)

pull/1189/head
Divyansh Singh 2 years ago committed by GitHub
parent b36656a925
commit 0048e2bf1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -48,7 +48,7 @@ The easiest way to start testing out VitePress is to tweak the VitePress docs. Y
$ pnpm run docs $ pnpm run docs
``` ```
After executing the above command, visit http://localhost:3000 and try modifying the source code. You'll get live update. After executing the above command, visit http://localhost:5173 and try modifying the source code. You'll get live update.
If you don't need docs site up and running, you may start VitePress local dev environment with `pnpm run dev`. If you don't need docs site up and running, you may start VitePress local dev environment with `pnpm run dev`.

@ -1,17 +1,18 @@
name: Release
on: on:
push: push:
tags: tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Create Release
jobs: jobs:
build: release:
name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout
uses: actions/checkout@master uses: actions/checkout@v3
- name: Create Release for Tag - name: Create Release for Tag
id: release_tag id: release_tag
uses: yyx990803/release-tag@master uses: yyx990803/release-tag@master

@ -16,17 +16,17 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [14, 16] node-version: [14, 16, 18]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2.0.1 uses: pnpm/action-setup@v2
with:
version: 7.0.1
- name: Set node version to ${{ matrix.node_version }} - name: Set node version to ${{ matrix.node_version }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3

2
.gitignore vendored

@ -10,3 +10,5 @@ dist
node_modules node_modules
pnpm-global pnpm-global
TODOs.md TODOs.md
.temp
*.tgz

@ -1,4 +1,3 @@
/docs
*.md *.md
*.vue *.vue
dist dist

2
client.d.ts vendored

@ -2,4 +2,4 @@
// be able to reference vite/client in project root. // be able to reference vite/client in project root.
/// <reference types="vite/client" /> /// <reference types="vite/client" />
export * from './dist/client' export * from './dist/client/index.js'

@ -1,5 +1,4 @@
import { defineConfig } from '../../src/node' import { defineConfig } from '../../src/node'
import { version } from '../../package.json' import { version } from '../../package.json'
export default defineConfig({ export default defineConfig({
@ -8,6 +7,7 @@ export default defineConfig({
description: 'Vite & Vue powered static site generator.', description: 'Vite & Vue powered static site generator.',
lastUpdated: true, lastUpdated: true,
cleanUrls: 'without-subfolders',
themeConfig: { themeConfig: {
nav: nav(), nav: nav(),
@ -58,9 +58,9 @@ function nav() {
{ {
text: 'Contributing', text: 'Contributing',
link: 'https://github.com/vuejs/vitepress/blob/main/.github/contributing.md' link: 'https://github.com/vuejs/vitepress/blob/main/.github/contributing.md'
}, }
], ]
}, }
] ]
} }

@ -211,3 +211,28 @@ export default {
titleTemplate: 'Vite & Vue powered static site generator' titleTemplate: 'Vite & Vue powered static site generator'
} }
``` ```
## cleanUrls (Experimental)
- Type: `'disabled' | 'without-subfolders' | 'with-subfolders'`
- Default: `'disabled'`
Allows removing trailing `.html` from URLs and, optionally, generating clean directory structure. Available modes:
| Mode | Page | Generated Page | URL |
| :--------------------: | :-------: | :---------------: | :---------: |
| `'disabled'` | `/foo.md` | `/foo.html` | `/foo.html` |
| `'without-subfolders'` | `/foo.md` | `/foo.html` | `/foo` |
| `'with-subfolders'` | `/foo.md` | `/foo/index.html` | `/foo` |
::: warning
Enabling this may require additional configuration on your hosting platform. For it to work, your server must serve the generated page on requesting the URL (see above table) **without a redirect**.
:::
```ts
export default {
cleanUrls: 'with-subfolders'
}
```

@ -11,6 +11,10 @@ Place your configuration file at `.vitepress/config.js`. This is where all ViteP
└─ package.json └─ package.json
``` ```
::: tip
You can also use any of `.ts`, `.cjs`, `.mjs`, `.cts`, `.mts` as the config file extension.
:::
VitePress comes with 2 types of configs. One is the [App Configs](./app-configs) which configures the site's fundamental features such as setting title of the site, or customize how markdown parser works. Second is the [Theme Config](./theme-configs) which configures the theme of the site, for example, adding a sidebar, or add features such as "Edit this page on GitHub" link. VitePress comes with 2 types of configs. One is the [App Configs](./app-configs) which configures the site's fundamental features such as setting title of the site, or customize how markdown parser works. Second is the [Theme Config](./theme-configs) which configures the theme of the site, for example, adding a sidebar, or add features such as "Edit this page on GitHub" link.
There's also another configuration you may do in [Frontmatter](./frontmatter-configs). Frontmatter configs can override global configs defined on App Configs or Theme Configs for that specific page. However, there're several options that are only available at frontmatter as well. There's also another configuration you may do in [Frontmatter](./frontmatter-configs). Frontmatter configs can override global configs defined on App Configs or Theme Configs for that specific page. However, there're several options that are only available at frontmatter as well.

@ -37,7 +37,7 @@ If your site is to be served at a subdirectory (`https://example.com/subdir/`),
$ yarn docs:serve $ yarn docs:serve
``` ```
The `serve` command will boot up a local static web server that will serve the files from `.vitepress/dist` at `http://localhost:5000`. It's an easy way to check if the production build looks fine in your local environment. The `serve` command will boot up a local static web server that will serve the files from `.vitepress/dist` at `http://localhost:4173`. It's an easy way to check if the production build looks fine in your local environment.
- You can configure the port of the server by passing `--port` as an argument. - You can configure the port of the server by passing `--port` as an argument.

@ -31,30 +31,13 @@ $ yarn add --dev vitepress vue
::: details Getting missing peer deps warnings? ::: details Getting missing peer deps warnings?
`@docsearch/js` has certain issues with its peer dependencies. If you see some commands failing due to them, you can try this workaround for now: `@docsearch/js` has certain issues with its peer dependencies. If you see some commands failing due to them, you can try this workaround for now:
On Yarn v2/v3, add this inside your rc file (`.yarnrc.yml` by default): If using PNPM, add this in your `package.json`:
```yaml
packageExtensions:
'@docsearch/react@*':
peerDependenciesMeta:
'@types/react':
optional: true
'react':
optional: true
'react-dom':
optional: true
```
On PNPM, add this in your `package.json`:
```json ```json
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [ "ignoreMissing": [
"@algolia/client-search", "@algolia/client-search"
"@types/react",
"react",
"react-dom"
] ]
} }
} }
@ -90,7 +73,7 @@ Serve the documentation site in the local server.
$ yarn docs:dev $ yarn docs:dev
``` ```
VitePress will start a hot-reloading development server at `http://localhost:3000`. VitePress will start a hot-reloading development server at `http://localhost:5173`.
## Step. 4: Add more pages ## Step. 4: Add more pages
@ -104,7 +87,7 @@ Let's add another page to the site. Create a file name `getting-started.md` alon
└─ package.json └─ package.json
``` ```
Then, try to access `http://localhost:3000/getting-started` and you should see the content of `getting-started` is shown. Then, try to access `http://localhost:5173/getting-started.html` and you should see the content of `getting-started.md` is shown.
This is how VitePress works basically. The directory structure corresponds with the URL path. You add files, and just try to access it. This is how VitePress works basically. The directory structure corresponds with the URL path. You add files, and just try to access it.

@ -12,7 +12,7 @@ If you're coming from VitePress 0.x version, there're several breaking changes d
- `children` key is now named `items`. - `children` key is now named `items`.
- Top level item may not contain `link` at the moment. We're planning to bring it back. - Top level item may not contain `link` at the moment. We're planning to bring it back.
- `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` are removed in favor of more flexible api. - `repo`, `repoLabel`, `docsDir`, `docsBranch`, `editLinks`, `editLinkText` are removed in favor of more flexible api.
- For adding GitHub link with icon to the nav, use [Social Links](./theme-nav.html#navigation-links) feature. - For adding GitHub link with icon to the nav, use [Social Links](./theme-nav#navigation-links) feature.
- For adding "Edit this page" feature, use [Edit Link](./theme-edit-link) feature. - For adding "Edit this page" feature, use [Edit Link](./theme-edit-link) feature.
- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`. - `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`.
- `carbonAds.carbon` is changed to `carbonAds.code`. - `carbonAds.carbon` is changed to `carbonAds.code`.

@ -4,7 +4,7 @@
### Images ### Images
Unlike VuePress, VitePress handles [`base`](/guide/asset-handling.html#base-url) of your config automatically when you use static image. Unlike VuePress, VitePress handles [`base`](./asset-handling#base-url) of your config automatically when you use static image.
Hence, now you can render images without `img` tag. Hence, now you can render images without `img` tag.
@ -14,7 +14,7 @@ Hence, now you can render images without `img` tag.
``` ```
::: warning ::: warning
For dynamic images you still need `withBase` as shown in [Base URL guide](/guide/asset-handling.html#base-url). For dynamic images you still need `withBase` as shown in [Base URL guide](./asset-handling#base-url).
::: :::
Use `<img.*withBase\('(.*)'\).*alt="([^"]*)".*>` regex to find and replace it with `![$2]($1)` to replace all the images with `![](...)` syntax. Use `<img.*withBase\('(.*)'\).*alt="([^"]*)".*>` regex to find and replace it with `![$2]($1)` to replace all the images with `![](...)` syntax.

@ -38,6 +38,7 @@ interface Theme {
Layout: Component // Vue 3 component Layout: Component // Vue 3 component
NotFound?: Component NotFound?: Component
enhanceApp?: (ctx: EnhanceAppContext) => void enhanceApp?: (ctx: EnhanceAppContext) => void
setup?: () => void
} }
interface EnhanceAppContext { interface EnhanceAppContext {
@ -65,6 +66,11 @@ export default {
// router is VitePress' custom router. `siteData` is // router is VitePress' custom router. `siteData` is
// a `ref` of current site-level metadata. // a `ref` of current site-level metadata.
} }
setup() {
// this function will be executed inside VitePressApp's
// setup hook. all composition APIs are available here.
}
} }
``` ```

@ -1,4 +1,6 @@
{ {
"private": true, "private": true,
"name": "vitepress-docs" "devDependencies": {
"vitepress": "workspace:*"
}
} }

@ -1,4 +1,4 @@
import { defineConfig } from '../../../src/node' import { defineConfig } from 'vitepress'
export default defineConfig({ export default defineConfig({
title: 'Configured Example', title: 'Configured Example',

@ -3,7 +3,7 @@
"version": "1.0.0-alpha.5", "version": "1.0.0-alpha.5",
"description": "Vite & Vue powered static site generator", "description": "Vite & Vue powered static site generator",
"type": "module", "type": "module",
"packageManager": "pnpm@7.8.0", "packageManager": "pnpm@7.9.0",
"main": "dist/node/index.js", "main": "dist/node/index.js",
"types": "types/index.d.ts", "types": "types/index.d.ts",
"exports": { "exports": {
@ -12,7 +12,8 @@
"import": "./dist/node/index.js", "import": "./dist/node/index.js",
"require": "./dist/node-cjs/index.cjs" "require": "./dist/node-cjs/index.cjs"
}, },
"./dist/client/*": "./dist/client/*", "./dist/*": "./dist/*",
"./package.json": "./package.json",
"./client": { "./client": {
"types": "./client.d.ts", "types": "./client.d.ts",
"default": "./dist/client/index.js" "default": "./dist/client/index.js"
@ -47,9 +48,6 @@
"bugs": { "bugs": {
"url": "https://github.com/vuejs/vitepress/issues" "url": "https://github.com/vuejs/vitepress/issues"
}, },
"engines": {
"node": ">=14.6.0"
},
"scripts": { "scripts": {
"dev": "rimraf dist && run-s dev-shared dev-start", "dev": "rimraf dist && run-s dev-shared dev-start",
"dev-start": "run-p dev-client dev-node dev-watch", "dev-start": "run-p dev-client dev-node dev-watch",
@ -59,7 +57,7 @@
"dev-watch": "node scripts/watchAndCopy", "dev-watch": "node scripts/watchAndCopy",
"build": "run-s build-prepare build-client build-node", "build": "run-s build-prepare build-client build-node",
"build-prepare": "rimraf dist && node scripts/copyShared", "build-prepare": "rimraf dist && node scripts/copyShared",
"build-client": "tsc -p src/client && node scripts/copyClient", "build-client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient",
"build-node": "rollup --config rollup.config.ts --configPlugin esbuild", "build-node": "rollup --config rollup.config.ts --configPlugin esbuild",
"format": "prettier --check --write .", "format": "prettier --check --write .",
"format-fail": "prettier --check .", "format-fail": "prettier --check .",
@ -83,89 +81,86 @@
"ci-docs": "run-s docs-build" "ci-docs": "run-s docs-build"
}, },
"dependencies": { "dependencies": {
"@docsearch/css": "^3.0.0", "@docsearch/css": "^3.2.1",
"@docsearch/js": "^3.0.0", "@docsearch/js": "^3.2.1",
"@vitejs/plugin-vue": "^2.3.2", "@vitejs/plugin-vue": "^3.0.3",
"@vue/devtools-api": "^6.1.4", "@vue/devtools-api": "^6.2.1",
"@vueuse/core": "^8.5.0", "@vueuse/core": "^9.1.0",
"body-scroll-lock": "^4.0.0-beta.0", "body-scroll-lock": "^4.0.0-beta.0",
"shiki": "^0.10.1", "shiki": "^0.11.1",
"vite": "^2.9.7", "vite": "^3.0.8",
"vue": "^3.2.33" "vue": "^3.2.37"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-alias": "^3.1.5", "@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "^20.0.0", "@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4", "@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-replace": "^4.0.0",
"@types/body-scroll-lock": "^3.1.0", "@types/body-scroll-lock": "^3.1.0",
"@types/compression": "^1.7.0", "@types/compression": "^1.7.2",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"@types/diacritics": "^1.3.1", "@types/diacritics": "^1.3.1",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/fs-extra": "^9.0.11", "@types/fs-extra": "^9.0.13",
"@types/koa": "^2.13.1", "@types/koa": "^2.13.5",
"@types/koa-static": "^4.0.1", "@types/koa-static": "^4.0.2",
"@types/lru-cache": "^5.1.0", "@types/markdown-it": "^12.2.3",
"@types/markdown-it": "^12.0.1",
"@types/markdown-it-attrs": "^4.1.0", "@types/markdown-it-attrs": "^4.1.0",
"@types/markdown-it-container": "^2.0.5", "@types/markdown-it-container": "^2.0.5",
"@types/markdown-it-emoji": "^2.0.2", "@types/markdown-it-emoji": "^2.0.2",
"@types/micromatch": "^4.0.2", "@types/micromatch": "^4.0.2",
"@types/minimist": "^1.2.2", "@types/minimist": "^1.2.2",
"@types/node": "^15.6.1", "@types/node": "^18.7.5",
"@types/polka": "^0.5.3", "@types/polka": "^0.5.4",
"@types/prompts": "^2.0.14", "@types/prompts": "^2.0.14",
"chokidar": "^3.5.1", "chokidar": "^3.5.3",
"compression": "^1.7.4", "compression": "^1.7.4",
"conventional-changelog-cli": "^2.1.1", "conventional-changelog-cli": "^2.2.2",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"debug": "^4.3.2", "debug": "^4.3.4",
"diacritics": "^1.3.0", "diacritics": "^1.3.0",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"esbuild": "^0.14.0", "esbuild": "^0.15.3",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"execa": "^6.1.0", "execa": "^6.1.0",
"fast-glob": "^3.2.7", "fast-glob": "^3.2.11",
"fs-extra": "^10.0.0", "fs-extra": "^10.1.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lint-staged": "^11.0.0", "lint-staged": "^13.0.3",
"lru-cache": "^6.0.0", "lru-cache": "^7.13.2",
"markdown-it": "^12.3.2", "markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.4.1", "markdown-it-anchor": "^8.6.4",
"markdown-it-attrs": "^4.1.3", "markdown-it-attrs": "^4.1.4",
"markdown-it-container": "^3.0.0", "markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.0", "markdown-it-emoji": "^2.0.2",
"markdown-it-toc-done-right": "^4.2.0", "markdown-it-toc-done-right": "^4.2.0",
"micromatch": "^4.0.4", "micromatch": "^4.0.5",
"minimist": "^1.2.5", "minimist": "^1.2.6",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ora": "^5.4.0", "ora": "^5.4.1",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"playwright-chromium": "^1.23.4", "playwright-chromium": "^1.25.0",
"polka": "^0.5.2", "polka": "^0.5.2",
"prettier": "^2.3.0", "prettier": "^2.7.1",
"prompts": "^2.4.2", "prompts": "^2.4.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^2.56.3", "rollup": "^2.78.0",
"rollup-plugin-dts": "^4.2.2", "rollup-plugin-dts": "^4.2.2",
"rollup-plugin-esbuild": "^4.8.2", "rollup-plugin-esbuild": "^4.9.3",
"semver": "^7.3.5", "semver": "^7.3.7",
"simple-git-hooks": "^2.7.0", "simple-git-hooks": "^2.8.0",
"sirv": "^1.0.12", "sirv": "^2.0.2",
"supports-color": "^9.2.2", "supports-color": "^9.2.2",
"typescript": "^4.7.2", "typescript": "^4.7.4",
"vitest": "^0.14.2" "vitest": "^0.22.0",
"vue-tsc": "^0.40.1"
}, },
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [ "ignoreMissing": [
"@algolia/client-search", "@algolia/client-search"
"@types/react",
"react",
"react-dom"
] ]
} }
}, },

File diff suppressed because it is too large Load Diff

@ -1,5 +1,5 @@
import { defineComponent, h } from 'vue' import { defineComponent, h } from 'vue'
import { useRoute } from '../router' import { useRoute } from '../router.js'
export const Content = defineComponent({ export const Content = defineComponent({
name: 'VitePressContent', name: 'VitePressContent',

@ -1,6 +1,6 @@
import { watchEffect, Ref } from 'vue' import { watchEffect, Ref } from 'vue'
import { HeadConfig, SiteData, createTitle, mergeHead } from '../../shared' import { HeadConfig, SiteData, createTitle, mergeHead } from '../../shared.js'
import { Route } from '../router' import { Route } from '../router.js'
export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) { export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
let managedHeadTags: HTMLElement[] = [] let managedHeadTags: HTMLElement[] = []

@ -1,9 +1,9 @@
// Customized pre-fetch for page chunks based on // Customized pre-fetch for page chunks based on
// https://github.com/GoogleChromeLabs/quicklink // https://github.com/GoogleChromeLabs/quicklink
import { useRoute } from '../router' import { useRoute } from '../router.js'
import { onMounted, onUnmounted, watch } from 'vue' import { onMounted, onUnmounted, watch } from 'vue'
import { inBrowser, pathToFile } from '../utils' import { inBrowser, pathToFile } from '../utils.js'
const hasFetched = new Set<string>() const hasFetched = new Set<string>()
const createLink = () => document.createElement('link') const createLink = () => document.createElement('link')
@ -49,7 +49,7 @@ export function usePrefetch() {
return return
} }
const rIC = (window as any).requestIdleCallback || setTimeout const rIC = window.requestIdleCallback || setTimeout
let observer: IntersectionObserver | null = null let observer: IntersectionObserver | null = null
const observeLinks = () => { const observeLinks = () => {
@ -73,8 +73,8 @@ export function usePrefetch() {
}) })
rIC(() => { rIC(() => {
document.querySelectorAll('#app a').forEach((link) => { document.querySelectorAll<HTMLAnchorElement>('#app a').forEach((link) => {
const { target, hostname, pathname } = link as HTMLAnchorElement const { target, hostname, pathname } = link
const extMatch = pathname.match(/\.\w+$/) const extMatch = pathname.match(/\.\w+$/)
if (extMatch && extMatch[0] !== '.html') { if (extMatch && extMatch[0] !== '.html') {
return return

@ -1,13 +1,13 @@
import { InjectionKey, Ref, shallowRef, readonly, computed, inject } from 'vue' import { InjectionKey, Ref, shallowRef, readonly, computed, inject } from 'vue'
import { Route } from './router' import { Route } from './router.js'
import siteData from '@siteData' import siteData from '@siteData'
import { import {
PageData, PageData,
SiteData, SiteData,
resolveSiteDataByRoute, resolveSiteDataByRoute,
createTitle createTitle
} from '../shared' } from '../shared.js'
import { withBase } from './utils' import { withBase } from './utils.js'
export const dataSymbol: InjectionKey<VitePressData> = Symbol() export const dataSymbol: InjectionKey<VitePressData> = Symbol()
@ -23,10 +23,8 @@ export interface VitePressData<T = any> {
} }
// site data is a singleton // site data is a singleton
export type SiteDataRef<T = any> = Ref<SiteData<T>>
export const siteDataRef: Ref<SiteData> = shallowRef( export const siteDataRef: Ref<SiteData> = shallowRef(
import.meta.env.PROD ? siteData : readonly(siteData) (import.meta.env.PROD ? siteData : readonly(siteData)) as SiteData
) )
// hmr // hmr

@ -1,7 +1,7 @@
import { setupDevtoolsPlugin } from '@vue/devtools-api' import { setupDevtoolsPlugin } from '@vue/devtools-api'
import type { App } from 'vue' import type { App } from 'vue'
import type { Router } from './router' import type { Router } from './router.js'
import type { VitePressData } from './data' import type { VitePressData } from './data.js'
const COMPONENT_STATE_TYPE = 'VitePress' const COMPONENT_STATE_TYPE = 'VitePress'

@ -2,23 +2,24 @@ import {
App, App,
createApp as createClientApp, createApp as createClientApp,
createSSRApp, createSSRApp,
defineComponent,
h, h,
onMounted, onMounted,
watch watch
} from 'vue' } from 'vue'
import Theme from '/@theme/index' import Theme from '@theme/index'
import { inBrowser, pathToFile } from './utils' import { inBrowser, pathToFile } from './utils.js'
import { Router, RouterSymbol, createRouter } from './router' import { Router, RouterSymbol, createRouter } from './router.js'
import { siteDataRef, useData } from './data' import { siteDataRef, useData } from './data.js'
import { useUpdateHead } from './composables/head' import { useUpdateHead } from './composables/head.js'
import { usePrefetch } from './composables/preFetch' import { usePrefetch } from './composables/preFetch.js'
import { dataSymbol, initData } from './data' import { dataSymbol, initData } from './data.js'
import { Content } from './components/Content' import { Content } from './components/Content.js'
import { ClientOnly } from './components/ClientOnly' import { ClientOnly } from './components/ClientOnly.js'
const NotFound = Theme.NotFound || (() => '404 Not Found') const NotFound = Theme.NotFound || (() => '404 Not Found')
const VitePressApp = { const VitePressApp = defineComponent({
name: 'VitePressApp', name: 'VitePressApp',
setup() { setup() {
const { site } = useData() const { site } = useData()
@ -38,9 +39,11 @@ const VitePressApp = {
// in prod mode, enable intersectionObserver based pre-fetch // in prod mode, enable intersectionObserver based pre-fetch
usePrefetch() usePrefetch()
} }
if (Theme.setup) Theme.setup()
return () => h(Theme.Layout) return () => h(Theme.Layout)
} }
} })
export function createApp() { export function createApp() {
const router = newRouter() const router = newRouter()
@ -73,7 +76,7 @@ export function createApp() {
// setup devtools in dev mode // setup devtools in dev mode
if (import.meta.env.DEV || __VUE_PROD_DEVTOOLS__) { if (import.meta.env.DEV || __VUE_PROD_DEVTOOLS__) {
import('./devtools').then(({ setupDevtools }) => import('./devtools.js').then(({ setupDevtools }) =>
setupDevtools(app, router, data) setupDevtools(app, router, data)
) )
} }

@ -1,9 +1,9 @@
import { reactive, inject, markRaw, nextTick, readonly } from 'vue' import { reactive, inject, markRaw, nextTick, readonly } from 'vue'
import type { Component, InjectionKey } from 'vue' import type { Component, InjectionKey } from 'vue'
import { notFoundPageData } from '../shared' import { notFoundPageData } from '../shared.js'
import type { PageData, PageDataPayload } from '../shared' import type { PageData, PageDataPayload } from '../shared.js'
import { inBrowser, withBase } from './utils' import { inBrowser, withBase } from './utils.js'
import { siteDataRef } from './data' import { siteDataRef } from './data.js'
export interface Route { export interface Route {
path: string path: string
@ -34,17 +34,20 @@ interface PageModule {
} }
export function createRouter( export function createRouter(
loadPageModule: (path: string) => PageModule | Promise<PageModule>, loadPageModule: (path: string) => Promise<PageModule>,
fallbackComponent?: Component fallbackComponent?: Component
): Router { ): Router {
const route = reactive(getDefaultRoute()) const route = reactive(getDefaultRoute())
function go(href: string = inBrowser ? location.href : '/') { function go(href: string = inBrowser ? location.href : '/') {
// ensure correct deep link so page refresh lands on correct files.
const url = new URL(href, fakeHost) const url = new URL(href, fakeHost)
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { if (siteDataRef.value.cleanUrls === 'disabled') {
url.pathname += '.html' // ensure correct deep link so page refresh lands on correct files.
href = url.pathname + url.search + url.hash // if cleanUrls is enabled, the server should handle this
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
url.pathname += '.html'
href = url.pathname + url.search + url.hash
}
} }
if (inBrowser) { if (inBrowser) {
// save scroll position before changing url // save scroll position before changing url
@ -60,16 +63,11 @@ export function createRouter(
const targetLoc = new URL(href, fakeHost) const targetLoc = new URL(href, fakeHost)
const pendingPath = (latestPendingPath = targetLoc.pathname) const pendingPath = (latestPendingPath = targetLoc.pathname)
try { try {
let page = loadPageModule(pendingPath) let page = await loadPageModule(pendingPath)
// only await if it returns a Promise - this allows sync resolution
// on initial render in SSR.
if ('then' in page && typeof page.then === 'function') {
page = await page
}
if (latestPendingPath === pendingPath) { if (latestPendingPath === pendingPath) {
latestPendingPath = null latestPendingPath = null
const { default: comp, __pageData } = page as PageModule const { default: comp, __pageData } = page
if (!comp) { if (!comp) {
throw new Error(`Invalid route component: ${comp}`) throw new Error(`Invalid route component: ${comp}`)
} }
@ -87,7 +85,7 @@ export function createRouter(
try { try {
target = document.querySelector( target = document.querySelector(
decodeURIComponent(targetLoc.hash) decodeURIComponent(targetLoc.hash)
) as HTMLElement )
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
} }
@ -101,7 +99,7 @@ export function createRouter(
} }
} }
} catch (err: any) { } catch (err: any) {
if (!err.message.match(/fetch/) && !href.match(/^[\\/]404\.html$/)) { if (!/fetch/.test(err.message) && !/^\/404(\.html|\/)?$/.test(href)) {
console.error(err) console.error(err)
} }
@ -190,7 +188,6 @@ export function useRouter(): Router {
if (!router) { if (!router) {
throw new Error('useRouter() is called without provider.') throw new Error('useRouter() is called without provider.')
} }
// @ts-ignore
return router return router
} }
@ -199,7 +196,7 @@ export function useRoute(): Route {
} }
function scrollTo(el: HTMLElement, hash: string, smooth = false) { function scrollTo(el: HTMLElement, hash: string, smooth = false) {
let target: Element | null = null let target: HTMLElement | null = null
try { try {
target = el.classList.contains('header-anchor') target = el.classList.contains('header-anchor')
@ -216,12 +213,12 @@ function scrollTo(el: HTMLElement, hash: string, smooth = false) {
document.querySelector(offset)!.getBoundingClientRect().bottom + 24 document.querySelector(offset)!.getBoundingClientRect().bottom + 24
} }
const targetPadding = parseInt( const targetPadding = parseInt(
window.getComputedStyle(target as HTMLElement).paddingTop, window.getComputedStyle(target).paddingTop,
10 10
) )
const targetTop = const targetTop =
window.scrollY + window.scrollY +
(target as HTMLElement).getBoundingClientRect().top - target.getBoundingClientRect().top -
offset + offset +
targetPadding targetPadding
// only smooth scroll if distance is smaller than screen height. // only smooth scroll if distance is smaller than screen height.

@ -0,0 +1,10 @@
// entry for SSR
import { createApp } from './index.js'
// @ts-ignore pending vue release
import { renderToString } from 'vue/server-renderer'
export async function render(path: string) {
const { app, router } = createApp()
await router.go(path)
return renderToString(app)
}

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

@ -1,5 +1,5 @@
import { siteDataRef } from './data' import { siteDataRef } from './data.js'
import { inBrowser, EXTERNAL_URL_RE } from '../shared' import { inBrowser, EXTERNAL_URL_RE } from '../shared.js'
export { inBrowser } export { inBrowser }
@ -35,7 +35,8 @@ export function pathToFile(path: string): string {
// /foo/bar.html -> ./foo_bar.md // /foo/bar.html -> ./foo_bar.md
if (inBrowser) { if (inBrowser) {
const base = import.meta.env.BASE_URL const base = import.meta.env.BASE_URL
pagePath = pagePath.slice(base.length).replace(/\//g, '_') + '.md' pagePath =
(pagePath.slice(base.length).replace(/\//g, '_') || 'index') + '.md'
// client production build needs to account for page hash, which is // client production build needs to account for page hash, which is
// injected directly in the page's html // injected directly in the page's html
const pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()] const pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]

@ -2,10 +2,10 @@
// so the user can do `import { useRoute, useSiteData } from 'vitepress'` // so the user can do `import { useRoute, useSiteData } from 'vitepress'`
// generic types // generic types
export type { Router, Route } from './app/router' export type { Router, Route } from './app/router.js'
export type { VitePressData } from './app/data' export type { VitePressData } from './app/data.js'
// theme types // theme types
export type { Theme, EnhanceAppContext } from './app/theme' export type { Theme, EnhanceAppContext } from './app/theme.js'
// shared types // shared types
export type { export type {
PageData, PageData,
@ -13,14 +13,14 @@ export type {
HeadConfig, HeadConfig,
Header, Header,
LocaleConfig LocaleConfig
} from '../../types/shared' } from '../../types/shared.js'
// composables // composables
export { useData } from './app/data' export { useData } from './app/data.js'
export { useRouter, useRoute } from './app/router' export { useRouter, useRoute } from './app/router.js'
// utilities // utilities
export { inBrowser, withBase } from './app/utils' export { inBrowser, withBase } from './app/utils.js'
// components // components
export { Content } from './app/components/Content' export { Content } from './app/components/Content.js'

@ -10,13 +10,13 @@ declare module '*.vue' {
} }
declare module '@siteData' { declare module '@siteData' {
import type { SiteData } from './shared' import type { SiteData } from 'vitepress'
const data: SiteData const data: SiteData
export default data export default data
} }
// this module's typing is broken. declare module '@theme/index' {
declare module '@docsearch/js' { import type { Theme } from 'vitepress'
function docsearch<T = any>(props: T): void const theme: Theme
export default docsearch export default theme
} }

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { provide, watch } from 'vue' import { provide, watch } from 'vue'
import { useRoute } from 'vitepress' import { useRoute } from 'vitepress'
import { useSidebar, useCloseSidebarOnEscape } from './composables/sidebar' import { useSidebar, useCloseSidebarOnEscape } from './composables/sidebar.js'
import VPSkipLink from './components/VPSkipLink.vue' import VPSkipLink from './components/VPSkipLink.vue'
import VPBackdrop from './components/VPBackdrop.vue' import VPBackdrop from './components/VPBackdrop.vue'
import VPNav from './components/VPNav.vue' import VPNav from './components/VPNav.vue'

@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import docsearch from '@docsearch/js' import { default as docsearch } from '@docsearch/js'
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useRouter, useRoute, useData } from 'vitepress' import { useRouter, useRoute, useData } from 'vitepress'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const { theme } = useData() const { theme, site } = useData()
onMounted(() => { onMounted(() => {
initialize(theme.value.algolia) initialize(theme.value.algolia)
@ -29,14 +29,16 @@ function poll() {
}, 16) }, 16)
} }
type DocSearchProps = Parameters<typeof docsearch>[0]
function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) { function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
// note: multi-lang search support is removed since the theme // note: multi-lang search support is removed since the theme
// doesn't support multiple locales as of now. // doesn't support multiple locales as of now.
const options = Object.assign({}, userOptions, { const options = Object.assign<{}, {}, DocSearchProps>({}, userOptions, {
container: '#docsearch', container: '#docsearch',
navigator: { navigator: {
navigate({ itemUrl }: { itemUrl: string }) { navigate({ itemUrl }) {
const { pathname: hitPathname } = new URL( const { pathname: hitPathname } = new URL(
window.location.origin + itemUrl window.location.origin + itemUrl
) )
@ -51,7 +53,7 @@ function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
} }
}, },
transformItems(items: any[]) { transformItems(items) {
return items.map((item) => { return items.map((item) => {
return Object.assign({}, item, { return Object.assign({}, item, {
url: getRelativePath(item.url) url: getRelativePath(item.url)
@ -59,7 +61,8 @@ function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
}) })
}, },
hitComponent({ hit, children }: { hit: any; children: any }) { // @ts-expect-error vue-tsc thinks this should return Vue JSX but it returns the required React one
hitComponent({ hit, children }) {
return { return {
__v: null, __v: null,
type: 'a', type: 'a',
@ -76,7 +79,12 @@ function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
function getRelativePath(absoluteUrl: string) { function getRelativePath(absoluteUrl: string) {
const { pathname, hash } = new URL(absoluteUrl) const { pathname, hash } = new URL(absoluteUrl)
return pathname + hash return (
pathname.replace(
/\.html$/,
site.value.cleanUrls === 'disabled' ? '.html' : ''
) + hash
)
} }
</script> </script>

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

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted } from 'vue' import { ref, watch, onMounted } from 'vue'
import { useData } from 'vitepress' import { useData } from 'vitepress'
import { useAside } from '../composables/aside' import { useAside } from '../composables/aside.js'
const { theme } = useData() const { theme } = useData()
const carbonOptions = theme.value.carbonAds const carbonOptions = theme.value.carbonAds

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute, useData } from 'vitepress' import { useRoute, useData } from 'vitepress'
import { useCopyCode } from '../composables/copy-code' import { useCopyCode } from '../composables/copy-code.js'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar.js'
import Theme from '/@theme/index' import Theme from '@theme/index'
import VPPage from './VPPage.vue' import VPPage from './VPPage.vue'
import VPHome from './VPHome.vue' import VPHome from './VPHome.vue'
import VPDoc from './VPDoc.vue' import VPDoc from './VPDoc.vue'

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useRoute } from 'vitepress' import { useRoute } from 'vitepress'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar.js'
import VPDocAside from './VPDocAside.vue' import VPDocAside from './VPDocAside.vue'
import VPDocFooter from './VPDocFooter.vue' import VPDocFooter from './VPDocFooter.vue'

@ -5,7 +5,7 @@ import {
resolveHeaders, resolveHeaders,
useOutline, useOutline,
useActiveAnchor useActiveAnchor
} from '../composables/outline' } from '../composables/outline.js'
const { page, frontmatter, theme } = useData() const { page, frontmatter, theme } = useData()
@ -22,9 +22,9 @@ const resolvedHeaders = computed(() => {
function handleClick({ target: el }: Event) { function handleClick({ target: el }: Event) {
const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1] const id = '#' + (el as HTMLAnchorElement).href!.split('#')[1]
const heading = document.querySelector( const heading = document.querySelector<HTMLAnchorElement>(
decodeURIComponent(id) decodeURIComponent(id)
) as HTMLAnchorElement )
heading?.focus() heading?.focus()
} }
</script> </script>

@ -1,18 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Sponsors } from './VPSponsors.vue'
import type { Sponsor } from './VPSponsorsGrid.vue'
import VPSponsors from './VPSponsors.vue' import VPSponsors from './VPSponsors.vue'
export interface Sponsors {
tier?: string
size?: 'xmini' | 'mini' | 'small'
items: Sponsor[]
}
export interface Sponsor {
name: string
img: string
url: string
}
defineProps<{ defineProps<{
tier?: string tier?: string
size?: 'xmini' | 'mini' | 'small' size?: 'xmini' | 'mini' | 'small'
@ -22,11 +12,6 @@ defineProps<{
<template> <template>
<div class="VPDocAsideSponsors"> <div class="VPDocAsideSponsors">
<VPSponsors <VPSponsors mode="aside" :tier="tier" :size="size" :data="data" />
mode="aside"
:tier="tier"
:size="size"
:data="data"
/>
</div> </div>
</template> </template>

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from 'vitepress' import { useData } from 'vitepress'
import { normalizeLink } from '../support/utils' import { normalizeLink } from '../support/utils.js'
import { useEditLink } from '../composables/edit-link' import { useEditLink } from '../composables/edit-link.js'
import { usePrevNext } from '../composables/prev-next' import { usePrevNext } from '../composables/prev-next.js'
import VPIconEdit from './icons/VPIconEdit.vue' import VPIconEdit from './icons/VPIconEdit.vue'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue' import VPDocFooterLastUpdated from './VPDocFooterLastUpdated.vue'

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useFlyout } from '../composables/flyout' import { useFlyout } from '../composables/flyout.js'
import VPIconChevronDown from './icons/VPIconChevronDown.vue' import VPIconChevronDown from './icons/VPIconChevronDown.vue'
import VPIconMoreHorizontal from './icons/VPIconMoreHorizontal.vue' import VPIconMoreHorizontal from './icons/VPIconMoreHorizontal.vue'
import VPMenu from './VPMenu.vue' import VPMenu from './VPMenu.vue'

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

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from 'vue'
import { normalizeLink } from '../support/utils' import { normalizeLink } from '../support/utils.js'
import VPIconExternalLink from './icons/VPIconExternalLink.vue' import VPIconExternalLink from './icons/VPIconExternalLink.vue'
import { EXTERNAL_URL_RE } from '../../shared' import { EXTERNAL_URL_RE } from '../../shared.js'
const props = defineProps<{ const props = defineProps<{
href?: string href?: string

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar.js'
import VPIconAlignLeft from './icons/VPIconAlignLeft.vue' import VPIconAlignLeft from './icons/VPIconAlignLeft.vue'
defineProps<{ defineProps<{

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { useData } from 'vitepress'
import { isActive } from '../support/utils' import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
defineProps<{ defineProps<{

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { provide } from 'vue' import { provide } from 'vue'
import { useNav } from '../composables/nav' import { useNav } from '../composables/nav.js'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar.js'
import VPNavBar from './VPNavBar.vue' import VPNavBar from './VPNavBar.vue'
import VPNavScreen from './VPNavScreen.vue' import VPNavScreen from './VPNavScreen.vue'

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar.js'
import VPNavBarTitle from './VPNavBarTitle.vue' import VPNavBarTitle from './VPNavBarTitle.vue'
import VPNavBarSearch from './VPNavBarSearch.vue' import VPNavBarSearch from './VPNavBarSearch.vue'
import VPNavBarMenu from './VPNavBarMenu.vue' import VPNavBarMenu from './VPNavBarMenu.vue'

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useData } from 'vitepress' import { useData } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { isActive } from '../support/utils' import { isActive } from '../support/utils.js'
import VPFlyout from './VPFlyout.vue' import VPFlyout from './VPFlyout.vue'
defineProps<{ defineProps<{

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { useData } from 'vitepress' import { useData } from 'vitepress'
import { isActive } from '../support/utils' import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
defineProps<{ defineProps<{

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

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watchPostEffect, nextTick } from 'vue' import { ref, watchPostEffect, nextTick } from 'vue'
import { useSidebar } from '../composables/sidebar' import { useSidebar } from '../composables/sidebar.js'
import VPSidebarGroup from './VPSidebarGroup.vue' import VPSidebarGroup from './VPSidebarGroup.vue'
const { sidebar, hasSidebar } = useSidebar() const { sidebar, hasSidebar } = useSidebar()

@ -2,7 +2,7 @@
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { ref, watchEffect } from 'vue' import { ref, watchEffect } from 'vue'
import { useData } from 'vitepress' import { useData } from 'vitepress'
import { isActive } from '../support/utils' import { isActive } from '../support/utils.js'
import VPIconPlusSquare from './icons/VPIconPlusSquare.vue' import VPIconPlusSquare from './icons/VPIconPlusSquare.vue'
import VPIconMinusSquare from './icons/VPIconMinusSquare.vue' import VPIconMinusSquare from './icons/VPIconMinusSquare.vue'
import VPSidebarLink from './VPSidebarLink.vue' import VPSidebarLink from './VPSidebarLink.vue'

@ -2,7 +2,7 @@
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import { useData } from 'vitepress' import { useData } from 'vitepress'
import { isActive } from '../support/utils' import { isActive } from '../support/utils.js'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
withDefaults(defineProps<{ item: DefaultTheme.SidebarItem; depth?: number }>(), { depth: 1 }) withDefaults(defineProps<{ item: DefaultTheme.SidebarItem; depth?: number }>(), { depth: 1 })

@ -8,9 +8,9 @@ const backToTop = ref()
watch(() => route.path, () => backToTop.value.focus()) watch(() => route.path, () => backToTop.value.focus())
function focusOnTargetAnchor({ target }: Event) { function focusOnTargetAnchor({ target }: Event) {
const el = document.querySelector( const el = document.querySelector<HTMLAnchorElement>(
(target as HTMLAnchorElement).hash! (target as HTMLAnchorElement).hash
) as HTMLAnchorElement )
if (el) { if (el) {
const removeTabIndex = () => { const removeTabIndex = () => {

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue' import { computed } from 'vue'
import { icons } from '../support/socialIcons' import { icons } from '../support/socialIcons.js'
const props = defineProps<{ const props = defineProps<{
icon: DefaultTheme.SocialLinkIcon icon: DefaultTheme.SocialLinkIcon

@ -1,40 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import type { GridSize } from '../composables/sponsor-grid.js'
import type { Sponsor } from './VPSponsorsGrid.vue'
import { computed } from 'vue' import { computed } from 'vue'
import VPSponsorsGrid from './VPSponsorsGrid.vue' import VPSponsorsGrid from './VPSponsorsGrid.vue'
export interface Sponsors { export interface Sponsors {
tier?: string tier?: string
size?: 'small' | 'medium' | 'big' size?: GridSize
items: Sponsor[] items: Sponsor[]
} }
export interface Sponsor {
name: string
img: string
url: string
}
const props = defineProps<{ const props = defineProps<{
mode?: 'normal' | 'aside' mode?: 'normal' | 'aside'
tier?: string tier?: string
size?: 'xmini' | 'small' | 'medium' | 'big' size?: GridSize
data: Sponsors[] | Sponsor[] data: Sponsors[] | Sponsor[]
}>() }>()
const sponsors = computed(() => { const sponsors = computed(() => {
const isSponsors = props.data.some((s) => { const isSponsors = props.data.some((s) => {
return !!(s as Sponsors).items return 'items' in s
}) })
if (isSponsors) { if (isSponsors) {
return props.data return props.data as Sponsors[]
} }
return [{ return [
tier: props.tier, { tier: props.tier, size: props.size, items: props.data as Sponsor[] }
size: props.size, ]
items: props.data
}]
}) })
</script> </script>

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { GridSize } from '../composables/sponsor-grid.js'
import { ref } from 'vue' import { ref } from 'vue'
import { useSponsorsGrid } from '../composables/sponsor-grid' import { useSponsorsGrid } from '../composables/sponsor-grid.js'
export interface Sponsor { export interface Sponsor {
name: string name: string
@ -9,7 +10,7 @@ export interface Sponsor {
} }
const props = defineProps<{ const props = defineProps<{
size?: 'xmini' | 'mini' | 'small' | 'medium' | 'big' size?: GridSize
data: Sponsor[] data: Sponsor[]
}>() }>()
@ -19,12 +20,29 @@ useSponsorsGrid({ el, size: props.size })
</script> </script>
<template> <template>
<div class="VPSponsorsGrid vp-sponsor-grid" :class="[props.size ?? 'medium']" ref="el"> <div
<div v-for="sponsor in data" :key="sponsor.tier" class="vp-sponsor-grid-item"> class="VPSponsorsGrid vp-sponsor-grid"
<a class="vp-sponsor-grid-link" :href="sponsor.url" target="_blank" rel="sponsored noopener"> :class="[props.size ?? 'medium']"
ref="el"
>
<div
v-for="sponsor in data"
:key="sponsor.name"
class="vp-sponsor-grid-item"
>
<a
class="vp-sponsor-grid-link"
:href="sponsor.url"
target="_blank"
rel="sponsored noopener"
>
<article class="vp-sponsor-grid-box"> <article class="vp-sponsor-grid-box">
<h4 class="visually-hidden">{{ sponsor.name }}</h4> <h4 class="visually-hidden">{{ sponsor.name }}</h4>
<img class="vp-sponsor-grid-image" :src="sponsor.img" :alt="sponsor.name" /> <img
class="vp-sponsor-grid-image"
:src="sponsor.img"
:alt="sponsor.name"
/>
</article> </article>
</a> </a>
</div> </div>

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { APPEARANCE_KEY } from '../../shared' import { APPEARANCE_KEY } from '../../shared.js'
import VPSwitch from './VPSwitch.vue' import VPSwitch from './VPSwitch.vue'
import VPIconSun from './icons/VPIconSun.vue' import VPIconSun from './icons/VPIconSun.vue'
import VPIconMoon from './icons/VPIconMoon.vue' import VPIconMoon from './icons/VPIconMoon.vue'

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue' import { computed } from 'vue'
import type { DefaultTheme } from '..'
import VPTeamMembersItem from './VPTeamMembersItem.vue' import VPTeamMembersItem from './VPTeamMembersItem.vue'
const props = defineProps<{ const props = defineProps<{

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DefaultTheme } from '..' import type { DefaultTheme } from 'vitepress/theme'
import VPIconHeart from './icons/VPIconHeart.vue' import VPIconHeart from './icons/VPIconHeart.vue'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
import VPSocialLinks from './VPSocialLinks.vue' import VPSocialLinks from './VPSocialLinks.vue'

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

@ -20,7 +20,7 @@ export function useFlyout(options: UseFlyoutOptions) {
listeners++ listeners++
const unwatch = watch(focusedElement, (el) => { const unwatch = watch(focusedElement, (el) => {
if (el === options.el.value || options.el.value?.contains(el as Node)) { if (el === options.el.value || options.el.value?.contains(el!)) {
focus.value = true focus.value = true
options.onFocus?.() options.onFocus?.()
} else { } else {

@ -1,7 +1,7 @@
import { Ref, computed, onMounted, onUpdated, onUnmounted } from 'vue' import { Ref, computed, onMounted, onUpdated, onUnmounted } from 'vue'
import { Header, useData } from 'vitepress' import { Header, useData } from 'vitepress'
import { useAside } from '../composables/aside' import { useAside } from '../composables/aside.js'
import { throttleAndDebounce } from '../support/utils' import { throttleAndDebounce } from '../support/utils.js'
interface HeaderWithChildren extends Header { interface HeaderWithChildren extends Header {
children?: Header[] children?: Header[]
@ -134,7 +134,7 @@ export function useActiveAnchor(
if (hash !== null) { if (hash !== null) {
prevActiveLink = container.value.querySelector( prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]` `a[href="${decodeURIComponent(hash)}"]`
) as HTMLAnchorElement )
} }
const activeLink = prevActiveLink const activeLink = prevActiveLink

@ -1,7 +1,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useData } from 'vitepress' import { useData } from 'vitepress'
import { isActive } from '../support/utils' import { isActive } from '../support/utils.js'
import { getSidebar, getFlatSideBarLinks } from '../support/sidebar' import { getSidebar, getFlatSideBarLinks } from '../support/sidebar.js'
export function usePrevNext() { export function usePrevNext() {
const { page, theme, frontmatter } = useData() const { page, theme, frontmatter } = useData()

@ -1,6 +1,6 @@
import { computed, onMounted, onUnmounted, Ref, ref, watchEffect } from 'vue' import { computed, onMounted, onUnmounted, Ref, ref, watchEffect } from 'vue'
import { useData, useRoute } from 'vitepress' import { useData, useRoute } from 'vitepress'
import { getSidebar } from '../support/sidebar' import { getSidebar } from '../support/sidebar.js'
export function useSidebar() { export function useSidebar() {
const route = useRoute() const route = useRoute()

@ -1,5 +1,5 @@
import { Ref, onMounted, onUnmounted } from 'vue' import { Ref, onMounted, onUnmounted } from 'vue'
import { throttleAndDebounce } from '../support/utils' import { throttleAndDebounce } from '../support/utils.js'
export interface GridSetting { export interface GridSetting {
[size: string]: [number, number][] [size: string]: [number, number][]

@ -1,5 +1,5 @@
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { ensureStartingSlash } from './utils' import { ensureStartingSlash } from './utils.js'
/** /**
* Get the `Sidebar` from sidebar option. This method will ensure to get correct * Get the `Sidebar` from sidebar option. This method will ensure to get correct

@ -1,6 +1,6 @@
import { ref } from 'vue' import { ref } from 'vue'
import { withBase } from 'vitepress' import { withBase, useData } from 'vitepress'
import { EXTERNAL_URL_RE } from '../../shared' import { EXTERNAL_URL_RE } from '../../shared.js'
export const HASH_RE = /#.*$/ export const HASH_RE = /#.*$/
export const EXT_RE = /(index)?\.(md|html)$/ export const EXT_RE = /(index)?\.(md|html)$/
@ -13,7 +13,7 @@ export function isExternal(path: string): boolean {
} }
export function throttleAndDebounce(fn: () => void, delay: number): () => void { export function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeout: number let timeout: any
let called = false let called = false
return () => { return () => {
@ -74,12 +74,16 @@ export function normalizeLink(url: string): string {
return url return url
} }
const { site } = useData()
const { pathname, search, hash } = new URL(url, 'http://example.com') const { pathname, search, hash } = new URL(url, 'http://example.com')
const normalizedPath = const normalizedPath =
pathname.endsWith('/') || pathname.endsWith('.html') pathname.endsWith('/') || pathname.endsWith('.html')
? url ? url
: `${pathname.replace(/(\.md)?$/, '.html')}${search}${hash}` : `${pathname.replace(
/(\.md)?$/,
site.value.cleanUrls === 'disabled' ? '.html' : ''
)}${search}${hash}`
return withBase(normalizedPath) return withBase(normalizedPath)
} }

@ -5,15 +5,15 @@
"outDir": "../../dist/client", "outDir": "../../dist/client",
"target": "esnext", "target": "esnext",
"module": "esnext", "module": "esnext",
"moduleResolution": "nodenext",
"declaration": true, "declaration": true,
"declarationDir": "../../dist/client-types", "declarationDir": "../../dist/client-types",
"jsx": "preserve", "jsx": "preserve",
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
"types": ["vite/client"], "types": ["vite/client"],
"paths": { "paths": {
"/@theme/*": ["theme-default/*"],
"vitepress": ["index.ts"], "vitepress": ["index.ts"],
"vitepress/theme": ["../../types/default-theme.d"] "vitepress/theme": ["../../theme.d.ts"]
} }
}, },
"include": ["."] "include": ["."]

@ -2,6 +2,7 @@ import { createRequire } from 'module'
import { resolve, join } from 'path' import { resolve, join } from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { Alias, AliasOptions } from 'vite' import { Alias, AliasOptions } from 'vite'
import { SiteConfig } from './config'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const PKG_ROOT = resolve(fileURLToPath(import.meta.url), '../..') const PKG_ROOT = resolve(fileURLToPath(import.meta.url), '../..')
@ -19,22 +20,15 @@ export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
const vueRuntimePath = 'vue/dist/vue.runtime.esm-bundler.js' const vueRuntimePath = 'vue/dist/vue.runtime.esm-bundler.js'
export function resolveAliases(root: string, themeDir: string): AliasOptions { export function resolveAliases(
{ root, themeDir }: SiteConfig,
ssr: boolean
): AliasOptions {
const paths: Record<string, string> = { const paths: Record<string, string> = {
'/@theme': themeDir,
'@theme': themeDir, '@theme': themeDir,
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH [SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
} }
// prioritize vue installed in project root and fallback to
// vue that comes with vitepress itself.
let vuePath
try {
vuePath = require.resolve(vueRuntimePath, { paths: [root] })
} catch (e) {
vuePath = require.resolve(vueRuntimePath)
}
const aliases: Alias[] = [ const aliases: Alias[] = [
...Object.keys(paths).map((p) => ({ ...Object.keys(paths).map((p) => ({
find: p, find: p,
@ -42,24 +36,30 @@ export function resolveAliases(root: string, themeDir: string): AliasOptions {
})), })),
{ {
find: /^vitepress$/, find: /^vitepress$/,
replacement: join(DIST_CLIENT_PATH, '/index') replacement: join(DIST_CLIENT_PATH, '/index.js')
}, },
{ {
find: /^vitepress\/theme$/, find: /^vitepress\/theme$/,
replacement: join(DIST_CLIENT_PATH, '/theme-default/index') replacement: join(DIST_CLIENT_PATH, '/theme-default/index.js')
},
// alias for local linked development
{
find: /^vitepress\//,
replacement: PKG_ROOT + '/'
},
// make sure it always use the same vue dependency that comes
// with vitepress itself
{
find: /^vue$/,
replacement: vuePath
} }
] ]
if (!ssr) {
// Prioritize vue installed in project root and fallback to
// vue that comes with vitepress itself.
// Only do this when not running SSR build, since `vue` needs to be
// externalized during SSR
let vuePath
try {
vuePath = require.resolve(vueRuntimePath, { paths: [root] })
} catch (e) {
vuePath = require.resolve(vueRuntimePath)
}
aliases.push({
find: /^vue$/,
replacement: vuePath
})
}
return aliases return aliases
} }

@ -6,15 +6,18 @@ import { OutputChunk, OutputAsset } from 'rollup'
import { resolveConfig } from '../config' import { resolveConfig } from '../config'
import { renderPage } from './render' import { renderPage } from './render'
import { bundle, okMark, failMark } from './bundle' import { bundle, okMark, failMark } from './bundle'
import { createRequire } from 'module'
import { pathToFileURL } from 'url'
export async function build( export async function build(
root: string, root?: string,
buildOptions: BuildOptions & { base?: string; mpa?: string } = {} buildOptions: BuildOptions & { base?: string; mpa?: string } = {}
) { ) {
const start = Date.now() const start = Date.now()
process.env.NODE_ENV = 'production' process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(root, 'build', 'production') const siteConfig = await resolveConfig(root, 'build', 'production')
const unlinkVue = linkVue(siteConfig.root)
if (buildOptions.base) { if (buildOptions.base) {
siteConfig.site.base = buildOptions.base siteConfig.site.base = buildOptions.base
@ -32,6 +35,9 @@ export async function build(
buildOptions buildOptions
) )
const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).toString())
const spinner = ora() const spinner = ora()
spinner.start('rendering pages...') spinner.start('rendering pages...')
@ -58,6 +64,7 @@ export async function build(
for (const page of pages) { for (const page of pages) {
await renderPage( await renderPage(
render,
siteConfig, siteConfig,
page, page,
clientResult, clientResult,
@ -84,10 +91,25 @@ export async function build(
pageToHashMap pageToHashMap
) )
} finally { } finally {
await fs.remove(siteConfig.tempDir) unlinkVue()
if (!process.env.DEBUG)
fs.rmSync(siteConfig.tempDir, { recursive: true, force: true })
} }
await siteConfig.buildEnd?.(siteConfig) await siteConfig.buildEnd?.(siteConfig)
console.log(`build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.`) console.log(`build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.`)
} }
function linkVue(root: string) {
const dest = path.resolve(root, 'node_modules/vue')
// 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)
return () => {
fs.unlinkSync(dest)
}
}
return () => {}
}

@ -28,9 +28,7 @@ export async function bundle(
// this is a multi-entry build - every page is considered an entry chunk // this is a multi-entry build - every page is considered an entry chunk
// the loading is done via filename conversion rules so that the // the loading is done via filename conversion rules so that the
// metadata doesn't need to be included in the main chunk. // metadata doesn't need to be included in the main chunk.
const input: Record<string, string> = { const input: Record<string, string> = {}
app: path.resolve(APP_PATH, 'index.js')
}
config.pages.forEach((file) => { config.pages.forEach((file) => {
// page filename conversion // page filename conversion
// foo/bar.md -> foo_bar.md // foo/bar.md -> foo_bar.md
@ -40,64 +38,70 @@ export async function bundle(
// resolve options to pass to vite // resolve options to pass to vite
const { rollupOptions } = options const { rollupOptions } = options
const resolveViteConfig = async (ssr: boolean): Promise<ViteUserConfig> => ({ const resolveViteConfig = async (ssr: boolean): Promise<ViteUserConfig> => {
root: config.srcDir, // use different entry based on ssr or not
base: config.site.base, input['app'] = path.resolve(APP_PATH, ssr ? 'ssr.js' : 'index.js')
logLevel: 'warn', return {
plugins: await createVitePressPlugin( root: config.srcDir,
config, base: config.site.base,
ssr, logLevel: 'warn',
pageToHashMap, plugins: await createVitePressPlugin(
clientJSMap config,
), ssr,
// @ts-ignore pageToHashMap,
ssr: { clientJSMap
noExternal: ['vitepress'] ),
}, ssr: {
build: { noExternal: ['vitepress', '@docsearch/css']
...options,
emptyOutDir: true,
ssr,
outDir: ssr ? config.tempDir : config.outDir,
cssCodeSplit: false,
rollupOptions: {
...rollupOptions,
input,
// important so that each page chunk and the index export things for each
// other
preserveEntrySignatures: 'allow-extension',
output: {
...rollupOptions?.output,
...(ssr
? {}
: {
chunkFileNames(chunk) {
// avoid ads chunk being intercepted by adblock
return /(?:Carbon|BuySell)Ads/.test(chunk.name)
? `assets/chunks/ui-custom.[hash].js`
: `assets/chunks/[name].[hash].js`
},
manualChunks(id, ctx) {
// move known framework code into a stable chunk so that
// custom theme changes do not invalidate hash for all pages
if (id.includes('plugin-vue:export-helper')) {
return 'framework'
}
if (
isEagerChunk(id, ctx) &&
(/@vue\/(runtime|shared|reactivity)/.test(id) ||
/vitepress\/dist\/client/.test(id))
) {
return 'framework'
}
}
})
}
}, },
// minify with esbuild in MPA mode (for CSS) build: {
minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG ...options,
emptyOutDir: true,
ssr,
outDir: ssr ? config.tempDir : config.outDir,
cssCodeSplit: false,
rollupOptions: {
...rollupOptions,
input,
// important so that each page chunk and the index export things for each
// other
preserveEntrySignatures: 'allow-extension',
output: {
...rollupOptions?.output,
...(ssr
? {
entryFileNames: `[name].js`,
chunkFileNames: `[name].[hash].js`
}
: {
chunkFileNames(chunk) {
// avoid ads chunk being intercepted by adblock
return /(?:Carbon|BuySell)Ads/.test(chunk.name)
? `assets/chunks/ui-custom.[hash].js`
: `assets/chunks/[name].[hash].js`
},
manualChunks(id, ctx) {
// move known framework code into a stable chunk so that
// custom theme changes do not invalidate hash for all pages
if (id.includes('plugin-vue:export-helper')) {
return 'framework'
}
if (
isEagerChunk(id, ctx) &&
(/@vue\/(runtime|shared|reactivity)/.test(id) ||
/vitepress\/dist\/client/.test(id))
) {
return 'framework'
}
}
})
}
},
// minify with esbuild in MPA mode (for CSS)
minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG
}
} }
}) }
let clientResult: RollupOutput let clientResult: RollupOutput
let serverResult: RollupOutput let serverResult: RollupOutput
@ -136,7 +140,7 @@ export async function bundle(
} }
// build <script client> bundle // build <script client> bundle
if (Object.keys(clientJSMap).length) { if (Object.keys(clientJSMap).length) {
clientResult = (await buildMPAClient(clientJSMap, config)) as RollupOutput clientResult = await buildMPAClient(clientJSMap, config)
} }
} }
@ -165,7 +169,7 @@ function staticImportedByEntry(
importStack: string[] = [] importStack: string[] = []
): boolean { ): boolean {
if (cache.has(id)) { if (cache.has(id)) {
return cache.get(id) as boolean return !!cache.get(id)
} }
if (importStack.includes(id)) { if (importStack.includes(id)) {
// circular deps! // circular deps!

@ -1,4 +1,3 @@
import { createRequire } from 'module'
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import { pathToFileURL } from 'url' import { pathToFileURL } from 'url'
@ -16,9 +15,8 @@ import {
import { slash } from '../utils/slash' import { slash } from '../utils/slash'
import { SiteConfig, resolveSiteDataByRoute } from '../config' import { SiteConfig, resolveSiteDataByRoute } from '../config'
const require = createRequire(import.meta.url)
export async function renderPage( export async function renderPage(
render: (path: string) => Promise<string>,
config: SiteConfig, config: SiteConfig,
page: string, // foo.md page: string, // foo.md
result: RollupOutput | null, result: RollupOutput | null,
@ -27,29 +25,11 @@ export async function renderPage(
pageToHashMap: Record<string, string>, pageToHashMap: Record<string, string>,
hashMapString: string hashMapString: string
) { ) {
const { createApp } = await import(
pathToFileURL(path.join(config.tempDir, `app.js`)).toString()
)
const { app, router } = createApp()
const routePath = `/${page.replace(/\.md$/, '')}` const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath) const siteData = resolveSiteDataByRoute(config.site, routePath)
router.go(routePath)
// lazy require server-renderer for production build
// prioritize project root over vitepress' own dep
let rendererPath
try {
rendererPath = require.resolve('vue/server-renderer', {
paths: [config.root]
})
} catch (e) {
rendererPath = require.resolve('vue/server-renderer')
}
// render page // render page
const content = await import(pathToFileURL(rendererPath).toString()).then( const content = await render(routePath)
(r) => r.renderToString(app)
)
const pageName = page.replace(/\//g, '_') const pageName = page.replace(/\//g, '_')
// server build doesn't need hash // server build doesn't need hash
@ -176,7 +156,15 @@ export async function renderPage(
${inlinedScript} ${inlinedScript}
</body> </body>
</html>`.trim() </html>`.trim()
const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) const createSubDirectory =
config.cleanUrls === 'with-subfolders' &&
!/(^|\/)(index|404).md$/.test(page)
const htmlFileName = path.join(
config.outDir,
page.replace(/\.md$/, createSubDirectory ? '/index.html' : '.html')
)
await fs.ensureDir(path.dirname(htmlFileName)) await fs.ensureDir(path.dirname(htmlFileName))
const transformedHtml = await config.transformHtml?.(html, htmlFileName, { const transformedHtml = await config.transformHtml?.(html, htmlFileName, {
siteConfig: config, siteConfig: config,

@ -4,7 +4,6 @@ import c from 'picocolors'
import fg from 'fast-glob' import fg from 'fast-glob'
import { import {
normalizePath, normalizePath,
AliasOptions,
UserConfig as ViteConfig, UserConfig as ViteConfig,
mergeConfig as mergeViteConfig, mergeConfig as mergeViteConfig,
loadConfigFromFile loadConfigFromFile
@ -17,9 +16,10 @@ import {
DefaultTheme, DefaultTheme,
APPEARANCE_KEY, APPEARANCE_KEY,
createLangDictionary, createLangDictionary,
CleanUrlsMode,
PageData PageData
} from './shared' } from './shared'
import { resolveAliases, DEFAULT_THEME_PATH } from './alias' import { DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown' import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug' import _debug from 'debug'
@ -61,7 +61,7 @@ export interface UserConfig<ThemeConfig = any> {
scrollOffset?: number | string scrollOffset?: number | string
/** /**
* Enable MPA / zero-JS mode * Enable MPA / zero-JS mode.
* @experimental * @experimental
*/ */
mpa?: boolean mpa?: boolean
@ -73,6 +73,19 @@ export interface UserConfig<ThemeConfig = any> {
*/ */
ignoreDeadLinks?: boolean ignoreDeadLinks?: boolean
/**
* @experimental
* Remove '.html' from URLs and generate clean directory structure.
*
* Available Modes:
* - `disabled`: generates `/foo.html` for every `/foo.md` and shows `/foo.html` in browser
* - `without-subfolders`: generates `/foo.html` for every `/foo.md` but shows `/foo` in browser
* - `with-subfolders`: generates `/foo/index.html` for every `/foo.md` and shows `/foo` in browser
*
* @default 'disabled'
*/
cleanUrls?: CleanUrlsMode
/** /**
* Build end hook: called when SSG finish. * Build end hook: called when SSG finish.
* @param siteConfig The resolved configuration. * @param siteConfig The resolved configuration.
@ -112,6 +125,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'mpa' | 'mpa'
| 'lastUpdated' | 'lastUpdated'
| 'ignoreDeadLinks' | 'ignoreDeadLinks'
| 'cleanUrls'
| 'buildEnd' | 'buildEnd'
| 'transformHtml' | 'transformHtml'
> { > {
@ -123,7 +137,6 @@ export interface SiteConfig<ThemeConfig = any>
themeDir: string themeDir: string
outDir: string outDir: string
tempDir: string tempDir: string
alias: AliasOptions
pages: string[] pages: string[]
} }
@ -193,12 +206,12 @@ export async function resolveConfig(
tempDir: resolve(root, '.temp'), tempDir: resolve(root, '.temp'),
markdown: userConfig.markdown, markdown: userConfig.markdown,
lastUpdated: userConfig.lastUpdated, lastUpdated: userConfig.lastUpdated,
alias: resolveAliases(root, themeDir),
vue: userConfig.vue, vue: userConfig.vue,
vite: userConfig.vite, vite: userConfig.vite,
shouldPreload: userConfig.shouldPreload, shouldPreload: userConfig.shouldPreload,
mpa: !!userConfig.mpa, mpa: !!userConfig.mpa,
ignoreDeadLinks: userConfig.ignoreDeadLinks, ignoreDeadLinks: userConfig.ignoreDeadLinks,
cleanUrls: userConfig.cleanUrls || 'disabled',
buildEnd: userConfig.buildEnd, buildEnd: userConfig.buildEnd,
transformHtml: userConfig.transformHtml transformHtml: userConfig.transformHtml
} }
@ -206,7 +219,7 @@ export async function resolveConfig(
return config return config
} }
const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts'] const supportedConfigExtensions = ['js', 'ts', 'cjs', 'mjs', 'cts', 'mts']
async function resolveUserConfig( async function resolveUserConfig(
root: string, root: string,
@ -299,7 +312,8 @@ export async function resolveSiteData(
themeConfig: userConfig.themeConfig || {}, themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {}, locales: userConfig.locales || {},
langs: createLangDictionary(userConfig), langs: createLangDictionary(userConfig),
scrollOffset: userConfig.scrollOffset || 90 scrollOffset: userConfig.scrollOffset || 90,
cleanUrls: userConfig.cleanUrls || 'disabled'
} }
} }

@ -5,7 +5,7 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import { MarkdownRenderer } from '../markdown' import { MarkdownRenderer } from '../markdown'
import { URL } from 'url' import { URL } from 'url'
import { EXTERNAL_URL_RE } from '../../shared' import { EXTERNAL_URL_RE, CleanUrlsMode } from '../../shared'
const indexRE = /(^|.*\/)index.md(#?.*)$/i const indexRE = /(^|.*\/)index.md(#?.*)$/i
@ -37,7 +37,7 @@ export const linkPlugin = (
// links to files (other than html/md) // links to files (other than html/md)
!/\.(?!html|md)\w+($|\?)/i.test(url) !/\.(?!html|md)\w+($|\?)/i.test(url)
) { ) {
normalizeHref(hrefAttr) normalizeHref(hrefAttr, env.cleanUrl)
} }
// encode vite-specific replace strings in case they appear in URLs // encode vite-specific replace strings in case they appear in URLs
@ -50,7 +50,10 @@ export const linkPlugin = (
return self.renderToken(tokens, idx, options) return self.renderToken(tokens, idx, options)
} }
function normalizeHref(hrefAttr: [string, string]) { function normalizeHref(
hrefAttr: [string, string],
shouldCleanUrls: CleanUrlsMode
) {
let url = hrefAttr[1] let url = hrefAttr[1]
const indexMatch = url.match(indexRE) const indexMatch = url.match(indexRE)
@ -59,12 +62,19 @@ export const linkPlugin = (
url = path + hash url = path + hash
} else { } else {
let cleanUrl = url.replace(/[?#].*$/, '') let cleanUrl = url.replace(/[?#].*$/, '')
// .md -> .html // transform foo.md -> foo[.html]
if (cleanUrl.endsWith('.md')) { if (cleanUrl.endsWith('.md')) {
cleanUrl = cleanUrl.replace(/\.md$/, '.html') cleanUrl = cleanUrl.replace(
/\.md$/,
shouldCleanUrls === 'disabled' ? '.html' : ''
)
} }
// ./foo -> ./foo.html // transform ./foo -> ./foo[.html]
if (!cleanUrl.endsWith('.html') && !cleanUrl.endsWith('/')) { if (
shouldCleanUrls === 'disabled' &&
!cleanUrl.endsWith('.html') &&
!cleanUrl.endsWith('/')
) {
cleanUrl += '.html' cleanUrl += '.html'
} }
const parsed = new URL(url, 'http://a.com') const parsed = new URL(url, 'http://a.com')

@ -3,7 +3,7 @@ import path from 'path'
import c from 'picocolors' import c from 'picocolors'
import matter from 'gray-matter' import matter from 'gray-matter'
import LRUCache from 'lru-cache' import LRUCache from 'lru-cache'
import { PageData, HeadConfig, EXTERNAL_URL_RE } from './shared' import { PageData, HeadConfig, EXTERNAL_URL_RE, CleanUrlsMode } from './shared'
import { slash } from './utils/slash' import { slash } from './utils/slash'
import { deeplyParseHeader } from './utils/parseHeader' import { deeplyParseHeader } from './utils/parseHeader'
import { getGitTimestamp } from './utils/getGitTimestamp' import { getGitTimestamp } from './utils/getGitTimestamp'
@ -22,7 +22,7 @@ export interface MarkdownCompileResult {
} }
export function clearCache() { export function clearCache() {
cache.reset() cache.clear()
} }
export async function createMarkdownToVueRenderFn( export async function createMarkdownToVueRenderFn(
@ -32,7 +32,8 @@ export async function createMarkdownToVueRenderFn(
userDefines: Record<string, any> | undefined, userDefines: Record<string, any> | undefined,
isBuild = false, isBuild = false,
base = '/', base = '/',
includeLastUpdatedData = false includeLastUpdatedData = false,
cleanUrls: CleanUrlsMode = 'disabled'
) { ) {
const md = await createMarkdownRenderer(srcDir, options, base) const md = await createMarkdownRenderer(srcDir, options, base)
pages = pages.map((p) => slash(p.replace(/\.md$/, ''))) pages = pages.map((p) => slash(p.replace(/\.md$/, '')))
@ -74,7 +75,12 @@ export async function createMarkdownToVueRenderFn(
md.__path = file md.__path = file
md.__relativePath = relativePath md.__relativePath = relativePath
const html = md.render(content) const html = md.render(content, {
path: file,
relativePath,
cleanUrls,
frontmatter
})
const data = md.__data const data = md.__data
// validate data.links // validate data.links

@ -3,7 +3,12 @@ import c from 'picocolors'
import { defineConfig, mergeConfig, Plugin, ResolvedConfig } from 'vite' import { defineConfig, mergeConfig, Plugin, ResolvedConfig } from 'vite'
import { SiteConfig } from './config' import { SiteConfig } from './config'
import { createMarkdownToVueRenderFn, clearCache } from './markdownToVue' import { createMarkdownToVueRenderFn, clearCache } from './markdownToVue'
import { DIST_CLIENT_PATH, APP_PATH, SITE_DATA_REQUEST_PATH } from './alias' import {
DIST_CLIENT_PATH,
APP_PATH,
SITE_DATA_REQUEST_PATH,
resolveAliases
} from './alias'
import { slash } from './utils/slash' import { slash } from './utils/slash'
import { OutputAsset, OutputChunk } from 'rollup' import { OutputAsset, OutputChunk } from 'rollup'
import { staticDataPlugin } from './staticDataPlugin' import { staticDataPlugin } from './staticDataPlugin'
@ -41,13 +46,14 @@ export async function createVitePressPlugin(
srcDir, srcDir,
configPath, configPath,
configDeps, configDeps,
alias,
markdown, markdown,
site, site,
vue: userVuePluginOptions, vue: userVuePluginOptions,
vite: userViteConfig, vite: userViteConfig,
pages, pages,
ignoreDeadLinks ignoreDeadLinks,
lastUpdated,
cleanUrls
} = siteConfig } = siteConfig
let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>> let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
@ -85,14 +91,15 @@ export async function createVitePressPlugin(
config.define, config.define,
config.command === 'build', config.command === 'build',
config.base, config.base,
siteConfig.lastUpdated lastUpdated,
cleanUrls
) )
}, },
config() { config() {
const baseConfig = defineConfig({ const baseConfig = defineConfig({
resolve: { resolve: {
alias alias: resolveAliases(siteConfig, ssr)
}, },
define: { define: {
__ALGOLIA__: !!site.themeConfig.algolia, __ALGOLIA__: !!site.themeConfig.algolia,
@ -101,7 +108,7 @@ export async function createVitePressPlugin(
optimizeDeps: { optimizeDeps: {
// force include vue to avoid duplicated copies when linked + optimized // force include vue to avoid duplicated copies when linked + optimized
include: ['vue'], include: ['vue'],
exclude: ['@docsearch/js'] exclude: ['@docsearch/js', 'vitepress']
}, },
server: { server: {
fs: { fs: {
@ -220,6 +227,14 @@ export async function createVitePressPlugin(
delete bundle[name] delete bundle[name]
} }
} }
if (config.ssr?.format === 'esm') {
this.emitFile({
type: 'asset',
fileName: 'package.json',
source: '{ "private": true, "type": "module" }'
})
}
} else { } else {
// client build: // client build:
// for each .md entry chunk, adjust its name to its correct path. // for each .md entry chunk, adjust its name to its correct path.

@ -24,7 +24,7 @@ export interface ServeOptions {
} }
export async function serve(options: ServeOptions = {}) { export async function serve(options: ServeOptions = {}) {
const port = options.port !== undefined ? options.port : 5000 const port = options.port !== undefined ? options.port : 4173
const site = await resolveConfig(options.root, 'serve', 'production') const site = await resolveConfig(options.root, 'serve', 'production')
const base = trimChar(options?.base ?? site?.site?.base ?? '', '/') const base = trimChar(options?.base ?? site?.site?.base ?? '', '/')

@ -1,3 +1,4 @@
import { setDefaultResultOrder } from 'node:dns'
import { createServer as createViteServer, ServerOptions } from 'vite' import { createServer as createViteServer, ServerOptions } from 'vite'
import { resolveConfig } from './config' import { resolveConfig } from './config'
import { createVitePressPlugin } from './plugin' import { createVitePressPlugin } from './plugin'
@ -14,6 +15,8 @@ export async function createServer(
delete serverOptions.base delete serverOptions.base
} }
setDefaultResultOrder('verbatim')
return createViteServer({ return createViteServer({
root: config.srcDir, root: config.srcDir,
base: config.site.base, base: config.site.base,

@ -3,7 +3,7 @@ import {
PageData, PageData,
LocaleConfig, LocaleConfig,
HeadConfig HeadConfig
} from '../../types/shared' } from '../../types/shared.js'
export type { export type {
SiteData, SiteData,
@ -12,13 +12,13 @@ export type {
LocaleConfig, LocaleConfig,
Header, Header,
DefaultTheme, DefaultTheme,
PageDataPayload PageDataPayload,
} from '../../types/shared' CleanUrlsMode
} from '../../types/shared.js'
export const EXTERNAL_URL_RE = /^[a-z]+:/i export const EXTERNAL_URL_RE = /^[a-z]+:/i
export const APPEARANCE_KEY = 'vitepress-theme-appearance' export const APPEARANCE_KEY = 'vitepress-theme-appearance'
// @ts-ignore
export const inBrowser = typeof window !== 'undefined' export const inBrowser = typeof window !== 'undefined'
export const notFoundPageData: PageData = { export const notFoundPageData: PageData = {

1
theme.d.ts vendored

@ -16,3 +16,4 @@ declare const theme: {
} }
export default theme export default theme
export type { DefaultTheme } from './types/default-theme.js'

4
types/index.d.ts vendored

@ -1,2 +1,2 @@
export * from '../dist/node/index' export * from '../dist/node/index.js'
export * from '../dist/client/index' export * from '../dist/client/index.js'

8
types/shared.d.ts vendored

@ -1,6 +1,6 @@
// types shared between server and client // types shared between server and client
export { DefaultTheme } from './default-theme' export type { DefaultTheme } from './default-theme.js'
export interface PageData { export interface PageData {
relativePath: string relativePath: string
@ -18,8 +18,14 @@ export interface Header {
slug: string slug: string
} }
export type CleanUrlsMode =
| 'disabled'
| 'without-subfolders'
| 'with-subfolders'
export interface SiteData<ThemeConfig = any> { export interface SiteData<ThemeConfig = any> {
base: string base: string
cleanUrls?: CleanUrlsMode
/** /**
* Language of the site as it should be set on the `html` element. * Language of the site as it should be set on the `html` element.

Loading…
Cancel
Save