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
```
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`.

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

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

2
.gitignore vendored

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

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

2
client.d.ts vendored

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

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

@ -211,3 +211,28 @@ export default {
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
```
::: 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.
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
```
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.

@ -31,30 +31,13 @@ $ yarn add --dev vitepress vue
::: 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:
On Yarn v2/v3, add this inside your rc file (`.yarnrc.yml` by default):
```yaml
packageExtensions:
'@docsearch/react@*':
peerDependenciesMeta:
'@types/react':
optional: true
'react':
optional: true
'react-dom':
optional: true
```
On PNPM, add this in your `package.json`:
If using PNPM, add this in your `package.json`:
```json
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"@algolia/client-search",
"@types/react",
"react",
"react-dom"
"@algolia/client-search"
]
}
}
@ -90,7 +73,7 @@ Serve the documentation site in the local server.
$ 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
@ -104,7 +87,7 @@ Let's add another page to the site. Create a file name `getting-started.md` alon
└─ 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.

@ -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`.
- 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.
- 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.
- `lastUpdated` option is now split into `config.lastUpdated` and `themeConfig.lastUpdatedText`.
- `carbonAds.carbon` is changed to `carbonAds.code`.

@ -4,7 +4,7 @@
### 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.
@ -14,7 +14,7 @@ Hence, now you can render images without `img` tag.
```
::: 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.

@ -38,6 +38,7 @@ interface Theme {
Layout: Component // Vue 3 component
NotFound?: Component
enhanceApp?: (ctx: EnhanceAppContext) => void
setup?: () => void
}
interface EnhanceAppContext {
@ -65,6 +66,11 @@ export default {
// router is VitePress' custom router. `siteData` is
// 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,
"name": "vitepress-docs"
"devDependencies": {
"vitepress": "workspace:*"
}
}

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

@ -1,5 +1,5 @@
import { siteDataRef } from './data'
import { inBrowser, EXTERNAL_URL_RE } from '../shared'
import { siteDataRef } from './data.js'
import { inBrowser, EXTERNAL_URL_RE } from '../shared.js'
export { inBrowser }
@ -35,7 +35,8 @@ export function pathToFile(path: string): string {
// /foo/bar.html -> ./foo_bar.md
if (inBrowser) {
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
// injected directly in the page's html
const pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]

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

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

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

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

@ -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.js'
import { EXTERNAL_URL_RE } from '../../shared.js'
const props = defineProps<{
tag?: string

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -20,7 +20,7 @@ export function useFlyout(options: UseFlyoutOptions) {
listeners++
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
options.onFocus?.()
} else {

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

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

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

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

@ -1,5 +1,5 @@
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

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

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

@ -2,6 +2,7 @@ import { createRequire } from 'module'
import { resolve, join } from 'path'
import { fileURLToPath } from 'url'
import { Alias, AliasOptions } from 'vite'
import { SiteConfig } from './config'
const require = createRequire(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'
export function resolveAliases(root: string, themeDir: string): AliasOptions {
export function resolveAliases(
{ root, themeDir }: SiteConfig,
ssr: boolean
): AliasOptions {
const paths: Record<string, string> = {
'/@theme': themeDir,
'@theme': themeDir,
[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[] = [
...Object.keys(paths).map((p) => ({
find: p,
@ -42,24 +36,30 @@ export function resolveAliases(root: string, themeDir: string): AliasOptions {
})),
{
find: /^vitepress$/,
replacement: join(DIST_CLIENT_PATH, '/index')
replacement: join(DIST_CLIENT_PATH, '/index.js')
},
{
find: /^vitepress\/theme$/,
replacement: join(DIST_CLIENT_PATH, '/theme-default/index')
},
// 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
replacement: join(DIST_CLIENT_PATH, '/theme-default/index.js')
}
]
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
}

@ -6,15 +6,18 @@ import { OutputChunk, OutputAsset } from 'rollup'
import { resolveConfig } from '../config'
import { renderPage } from './render'
import { bundle, okMark, failMark } from './bundle'
import { createRequire } from 'module'
import { pathToFileURL } from 'url'
export async function build(
root: string,
root?: string,
buildOptions: BuildOptions & { base?: string; mpa?: string } = {}
) {
const start = Date.now()
process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(root, 'build', 'production')
const unlinkVue = linkVue(siteConfig.root)
if (buildOptions.base) {
siteConfig.site.base = buildOptions.base
@ -32,6 +35,9 @@ export async function build(
buildOptions
)
const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).toString())
const spinner = ora()
spinner.start('rendering pages...')
@ -58,6 +64,7 @@ export async function build(
for (const page of pages) {
await renderPage(
render,
siteConfig,
page,
clientResult,
@ -84,10 +91,25 @@ export async function build(
pageToHashMap
)
} finally {
await fs.remove(siteConfig.tempDir)
unlinkVue()
if (!process.env.DEBUG)
fs.rmSync(siteConfig.tempDir, { recursive: true, force: true })
}
await siteConfig.buildEnd?.(siteConfig)
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
// the loading is done via filename conversion rules so that the
// metadata doesn't need to be included in the main chunk.
const input: Record<string, string> = {
app: path.resolve(APP_PATH, 'index.js')
}
const input: Record<string, string> = {}
config.pages.forEach((file) => {
// page filename conversion
// foo/bar.md -> foo_bar.md
@ -40,64 +38,70 @@ export async function bundle(
// resolve options to pass to vite
const { rollupOptions } = options
const resolveViteConfig = async (ssr: boolean): Promise<ViteUserConfig> => ({
root: config.srcDir,
base: config.site.base,
logLevel: 'warn',
plugins: await createVitePressPlugin(
config,
ssr,
pageToHashMap,
clientJSMap
),
// @ts-ignore
ssr: {
noExternal: ['vitepress']
},
build: {
...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'
}
}
})
}
const resolveViteConfig = async (ssr: boolean): Promise<ViteUserConfig> => {
// use different entry based on ssr or not
input['app'] = path.resolve(APP_PATH, ssr ? 'ssr.js' : 'index.js')
return {
root: config.srcDir,
base: config.site.base,
logLevel: 'warn',
plugins: await createVitePressPlugin(
config,
ssr,
pageToHashMap,
clientJSMap
),
ssr: {
noExternal: ['vitepress', '@docsearch/css']
},
// minify with esbuild in MPA mode (for CSS)
minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG
build: {
...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 serverResult: RollupOutput
@ -136,7 +140,7 @@ export async function bundle(
}
// build <script client> bundle
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[] = []
): boolean {
if (cache.has(id)) {
return cache.get(id) as boolean
return !!cache.get(id)
}
if (importStack.includes(id)) {
// circular deps!

@ -1,4 +1,3 @@
import { createRequire } from 'module'
import fs from 'fs-extra'
import path from 'path'
import { pathToFileURL } from 'url'
@ -16,9 +15,8 @@ import {
import { slash } from '../utils/slash'
import { SiteConfig, resolveSiteDataByRoute } from '../config'
const require = createRequire(import.meta.url)
export async function renderPage(
render: (path: string) => Promise<string>,
config: SiteConfig,
page: string, // foo.md
result: RollupOutput | null,
@ -27,29 +25,11 @@ export async function renderPage(
pageToHashMap: Record<string, 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 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
const content = await import(pathToFileURL(rendererPath).toString()).then(
(r) => r.renderToString(app)
)
const content = await render(routePath)
const pageName = page.replace(/\//g, '_')
// server build doesn't need hash
@ -176,7 +156,15 @@ export async function renderPage(
${inlinedScript}
</body>
</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))
const transformedHtml = await config.transformHtml?.(html, htmlFileName, {
siteConfig: config,

@ -4,7 +4,6 @@ import c from 'picocolors'
import fg from 'fast-glob'
import {
normalizePath,
AliasOptions,
UserConfig as ViteConfig,
mergeConfig as mergeViteConfig,
loadConfigFromFile
@ -17,9 +16,10 @@ import {
DefaultTheme,
APPEARANCE_KEY,
createLangDictionary,
CleanUrlsMode,
PageData
} from './shared'
import { resolveAliases, DEFAULT_THEME_PATH } from './alias'
import { DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug'
@ -61,7 +61,7 @@ export interface UserConfig<ThemeConfig = any> {
scrollOffset?: number | string
/**
* Enable MPA / zero-JS mode
* Enable MPA / zero-JS mode.
* @experimental
*/
mpa?: boolean
@ -73,6 +73,19 @@ export interface UserConfig<ThemeConfig = any> {
*/
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.
* @param siteConfig The resolved configuration.
@ -112,6 +125,7 @@ export interface SiteConfig<ThemeConfig = any>
| 'mpa'
| 'lastUpdated'
| 'ignoreDeadLinks'
| 'cleanUrls'
| 'buildEnd'
| 'transformHtml'
> {
@ -123,7 +137,6 @@ export interface SiteConfig<ThemeConfig = any>
themeDir: string
outDir: string
tempDir: string
alias: AliasOptions
pages: string[]
}
@ -193,12 +206,12 @@ export async function resolveConfig(
tempDir: resolve(root, '.temp'),
markdown: userConfig.markdown,
lastUpdated: userConfig.lastUpdated,
alias: resolveAliases(root, themeDir),
vue: userConfig.vue,
vite: userConfig.vite,
shouldPreload: userConfig.shouldPreload,
mpa: !!userConfig.mpa,
ignoreDeadLinks: userConfig.ignoreDeadLinks,
cleanUrls: userConfig.cleanUrls || 'disabled',
buildEnd: userConfig.buildEnd,
transformHtml: userConfig.transformHtml
}
@ -206,7 +219,7 @@ export async function resolveConfig(
return config
}
const supportedConfigExtensions = ['js', 'ts', 'mjs', 'mts']
const supportedConfigExtensions = ['js', 'ts', 'cjs', 'mjs', 'cts', 'mts']
async function resolveUserConfig(
root: string,
@ -299,7 +312,8 @@ export async function resolveSiteData(
themeConfig: userConfig.themeConfig || {},
locales: userConfig.locales || {},
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 { MarkdownRenderer } from '../markdown'
import { URL } from 'url'
import { EXTERNAL_URL_RE } from '../../shared'
import { EXTERNAL_URL_RE, CleanUrlsMode } from '../../shared'
const indexRE = /(^|.*\/)index.md(#?.*)$/i
@ -37,7 +37,7 @@ export const linkPlugin = (
// links to files (other than html/md)
!/\.(?!html|md)\w+($|\?)/i.test(url)
) {
normalizeHref(hrefAttr)
normalizeHref(hrefAttr, env.cleanUrl)
}
// encode vite-specific replace strings in case they appear in URLs
@ -50,7 +50,10 @@ export const linkPlugin = (
return self.renderToken(tokens, idx, options)
}
function normalizeHref(hrefAttr: [string, string]) {
function normalizeHref(
hrefAttr: [string, string],
shouldCleanUrls: CleanUrlsMode
) {
let url = hrefAttr[1]
const indexMatch = url.match(indexRE)
@ -59,12 +62,19 @@ export const linkPlugin = (
url = path + hash
} else {
let cleanUrl = url.replace(/[?#].*$/, '')
// .md -> .html
// transform foo.md -> foo[.html]
if (cleanUrl.endsWith('.md')) {
cleanUrl = cleanUrl.replace(/\.md$/, '.html')
cleanUrl = cleanUrl.replace(
/\.md$/,
shouldCleanUrls === 'disabled' ? '.html' : ''
)
}
// ./foo -> ./foo.html
if (!cleanUrl.endsWith('.html') && !cleanUrl.endsWith('/')) {
// transform ./foo -> ./foo[.html]
if (
shouldCleanUrls === 'disabled' &&
!cleanUrl.endsWith('.html') &&
!cleanUrl.endsWith('/')
) {
cleanUrl += '.html'
}
const parsed = new URL(url, 'http://a.com')

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

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

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

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

1
theme.d.ts vendored

@ -16,3 +16,4 @@ declare const 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/client/index'
export * from '../dist/node/index.js'
export * from '../dist/client/index.js'

8
types/shared.d.ts vendored

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

Loading…
Cancel
Save