chore: migrate build to tsdown (#5396)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
pull/4957/merge
Divyansh Singh 3 weeks ago committed by GitHub
parent 5a368d3198
commit 357b072a47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,8 +3,8 @@ import { defineRoutes } from 'vitepress'
import paths from './paths'
export default defineRoutes({
async paths(watchedFiles: string[]) {
// console.log('watchedFiles', watchedFiles)
async paths(_watchedFiles: string[]) {
// console.log('watchedFiles', _watchedFiles)
return paths
},
watch: ['../data-loading/**/*.json'],

@ -123,13 +123,15 @@ describe('Table of Contents', () => {
})
describe('Custom Containers', () => {
enum CustomBlocks {
Info = 'INFO',
Tip = 'TIP',
Warning = 'WARNING',
Danger = 'DANGER',
Details = 'Details'
}
const CustomBlocks = {
Info: 'INFO',
Tip: 'TIP',
Warning: 'WARNING',
Danger: 'DANGER',
Details: 'Details'
} as const
type CustomBlocks = (typeof CustomBlocks)[keyof typeof CustomBlocks]
const classnameMap = {
[CustomBlocks.Info]: 'info',

@ -52,17 +52,10 @@
"lib"
],
"scripts": {
"clean": "node -e \"require('node:fs').rmSync('./dist',{recursive:!0,force:!0,maxRetries:10})\"",
"dev": "pnpm clean && pnpm dev:shared && pnpm dev:start",
"dev:start": "pnpm --stream '/^dev:(client|node|watch)$/'",
"dev:client": "tsc --sourcemap -w --preserveWatchOutput -p src/client",
"dev:node": "DEV=true pnpm build:node -w",
"dev:shared": "node scripts/copyShared.ts",
"dev:watch": "node scripts/watchAndCopy.ts",
"build": "pnpm build:prepare && pnpm build:client && pnpm build:node && node scripts/genWebTypes.ts",
"build:prepare": "pnpm clean && node scripts/copyShared.ts",
"build:client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient.ts",
"build:node": "tsc -p src/node --noEmit && rollup --config rollup.config.ts",
"dev": "tsdown --watch --sourcemap",
"build": "tsdown && pnpm typecheck && node scripts/genWebTypes.ts && pnpm build:check",
"build:check": "publint && attw --pack . --profile esm-only",
"typecheck": "tsc -p tsconfig.shared.json && vue-tsc -p tsconfig.client.json && tsc -p tsconfig.node.json",
"test": "pnpm --aggregate-output --reporter=append-only '/^test:(types|unit|e2e|init)$/'",
"test:types": "tsc -p __tests__/unit && tsc -p __tests__/e2e && tsc -p __tests__/init && vue-tsc -p docs",
"test:unit": "vitest run -r __tests__/unit",
@ -132,13 +125,9 @@
"@mdit/plugin-emoji": "^1.1.1",
"@mdit/plugin-footnote": "^1.0.2",
"@mdit/plugin-tasklist": "^1.0.2",
"@arethetypeswrong/cli": "^0.18.5",
"@polka/compression": "^1.0.0-next.28",
"@rolldown/pluginutils": "^1.0.1",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@types/cross-spawn": "^6.0.6",
"@types/lodash.template": "^4.5.3",
"@types/mark.js": "^8.11.12",
@ -146,11 +135,11 @@
"@types/node": "^26.2.0",
"@types/picomatch": "^4.0.3",
"@types/semver": "^7.8.0",
"chokidar": "^5.0.0",
"@volar/typescript": "^2.4.28",
"@vue/language-core": "^3.3.11",
"conventional-changelog": "^8.1.1",
"conventional-changelog-angular": "^9.2.1",
"cross-spawn": "^7.0.6",
"esbuild": "^0.27.7",
"get-port": "^7.2.0",
"gray-matter": "^4.0.3",
"image-size": "^2.0.2",
@ -175,18 +164,18 @@
"postcss": "^8.5.6",
"postcss-selector-parser": "^7.1.5",
"prettier": "^3.9.6",
"punycode": "^2.3.1",
"rollup": "^4.62.4",
"rollup-plugin-dts": "6.1.1",
"rollup-plugin-esbuild": "^6.2.1",
"publint": "^0.3.24",
"rolldown": "^1.2.5",
"semver": "^7.8.5",
"simple-git-hooks": "^2.13.1",
"sirv": "^3.0.2",
"sitemap": "^9.0.1",
"tinyglobby": "^0.2.17",
"typescript": "^5.9.3",
"tsdown": "^0.22.14",
"typescript": "^6.0.3",
"vitest": "^4.1.10",
"vue-tsc": "^3.3.9",
"vue-sfc-transformer": "^0.2.5",
"vue-tsc": "^3.3.11",
"wait-on": "^9.1.0"
},
"peerDependencies": {

@ -0,0 +1,18 @@
diff --git a/dist/rolldown.mjs b/dist/rolldown.mjs
index 7e04d96c703656b81220193b1f01426778b6a42f..26e96378fc0ad985237302e673ed0ab07674bf41 100644
--- a/dist/rolldown.mjs
+++ b/dist/rolldown.mjs
@@ -196,10 +196,11 @@ function resolveCache(options) {
async function transpileScript(code, filename = "__sfc.ts") {
const result = await transform(filename, code, {
lang: "ts",
- sourcemap: false
+ sourcemap: false,
+ typescript: { onlyRemoveTypeImports: true }
});
if (result.errors.length) throw new AggregateError(result.errors, `[vue-sfc-transformer] failed to transpile script in ${filename}`);
- return result.code ?? code;
+ return (result.code ?? code).replace(/\n?export \{\};?[\s\n]*$/, "");
}
function vueSfcPlugin(pluginOptions) {
const cwd = pluginOptions.cwd ?? process.cwd();

File diff suppressed because it is too large Load Diff

@ -3,7 +3,6 @@ packages:
- __tests__/*
allowBuilds:
esbuild: true
playwright-chromium: true
simple-git-hooks: true
@ -13,6 +12,12 @@ ignoreWorkspaceRootCheck: true
minimumReleaseAge: 1440
overrides:
esbuild: '-'
patchedDependencies:
vue-sfc-transformer: patches/vue-sfc-transformer.patch
shellEmulator: true
strictPeerDependencies: true

@ -1,115 +0,0 @@
import { rm } from 'node:fs/promises'
import { builtinModules } from 'node:module'
import { fileURLToPath } from 'node:url'
import alias from '@rollup/plugin-alias'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import { type RollupOptions, defineConfig } from 'rollup'
import dts from 'rollup-plugin-dts'
import esbuild from 'rollup-plugin-esbuild'
import pkg from './package.json' with { type: 'json' }
const DEV = !!process.env.DEV
const PROD = !DEV
const external = [
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies),
...builtinModules.flatMap((m) =>
m.includes('punycode') ? [] : [m, `node:${m}`]
)
]
const plugins = [
alias({ entries: { 'readable-stream': 'stream' } }),
replace({
// polyfill broken browser check from bundled deps
'navigator.userAgentData': 'undefined',
'navigator.userAgent': 'undefined',
preventAssignment: true
}),
commonjs(),
nodeResolve({ preferBuiltins: false }),
esbuild({ target: 'node22' }),
json()
]
const esmBuild: RollupOptions = {
input: ['src/node/index.ts', 'src/node/cli.ts'],
output: {
format: 'esm',
entryFileNames: `[name].js`,
chunkFileNames: 'chunk-[hash].js',
dir: 'dist/node',
sourcemap: DEV
},
external,
plugins,
onwarn(warning, warn) {
if (warning.code !== 'EVAL') warn(warning)
}
}
// keep .d.ts files under the repo root (e.g. types/*) external so module
// augmentations in the bundle still target the same files users reference.
// compared on normalized resolved paths so this works regardless of the
// checkout location, path separators, or drive-letter casing.
const normalizePath = (id: string): string => {
const normalized = id.replaceAll('\\', '/')
return process.platform === 'win32' ? normalized.toLowerCase() : normalized
}
const root = normalizePath(fileURLToPath(new URL('.', import.meta.url)))
const typesExternal = (id: string): boolean => {
if (external.includes(id) || /^markdown-it(?:\/|$)/.test(id)) return true
const normalized = normalizePath(id)
return (
normalized.endsWith('.d.ts') &&
normalized.startsWith(root) &&
!normalized.startsWith(`${root}dist/`) &&
!normalized.startsWith(`${root}node_modules/`)
)
}
const dtsNode = dts({
respectExternal: true,
tsconfig: 'src/node/tsconfig.json',
compilerOptions: { preserveSymlinks: false }
})
const nodeTypes: RollupOptions = {
input: 'src/node/index.ts',
output: {
format: 'esm',
file: 'dist/node/index.d.ts'
},
external: typesExternal,
plugins: [dtsNode]
}
const clientTypes: RollupOptions = {
input: 'dist/client-types/index.d.ts',
output: {
format: 'esm',
file: 'dist/client/index.d.ts'
},
external: typesExternal,
plugins: [
dts({ respectExternal: true }),
{
name: 'cleanup',
async closeBundle() {
if (PROD) {
await rm('dist/client-types', { recursive: true })
}
}
}
]
}
export default defineConfig([esmBuild, nodeTypes, clientTypes])

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

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

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

@ -0,0 +1,13 @@
// Cross-environment globals that environment-neutral code may use, declared
// merge-compatibly with lib.dom and @types/node (interface merging plus an
// identically named var). Loaded only by projects without a DOM lib; keep
// members to what shared code actually touches.
interface Console {
debug(...data: unknown[]): void
warn(...data: unknown[]): void
}
declare var console: Console
interface Document {}
declare var document: Document

@ -7,7 +7,8 @@ export function useCodeGroups() {
Array.from(el.children).forEach((child) => {
child.classList.remove('active')
})
activate(el.children[0])
const first = el.children[0]
if (first) activate(first)
})
})
}

@ -6,7 +6,7 @@ const ignoredNodes = ['.vp-copy-ignore', '.diff.remove'].join(', ')
export function useCopyCode() {
if (inBrowser) {
const timeoutIdMap: WeakMap<HTMLElement, NodeJS.Timeout> = new WeakMap()
const timeoutIdMap: WeakMap<HTMLElement, number> = new WeakMap()
window.addEventListener('click', (e) => {
const el = e.target as HTMLElement
if (el.matches('div[class*="language-"] > button.copy')) {
@ -35,7 +35,7 @@ export function useCopyCode() {
copyToClipboard(text).then(() => {
el.classList.add('copied')
clearTimeout(timeoutIdMap.get(el))
const timeoutId = setTimeout(() => {
const timeoutId = window.setTimeout(() => {
el.classList.remove('copied')
el.blur()
timeoutIdMap.delete(el)

@ -82,8 +82,8 @@ export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
function createHeadElement([tag, attrs, innerHTML]: HeadConfig) {
const el = document.createElement(tag)
for (const key in attrs) {
el.setAttribute(key, attrs[key])
for (const [key, value] of Object.entries(attrs)) {
el.setAttribute(key, value)
}
if (innerHTML) {
el.innerHTML = innerHTML

@ -1,3 +1,8 @@
// vite/client rather than vitepress/client: the .vue declaration emit runs
// outside a project and cannot resolve self-references, and client.d.ts would
// pull the built dist into the program, clashing with the sources
/// <reference types="vite/client" />
declare const __VP_HASH_MAP__: Record<string, string>
declare const __VP_LOCAL_SEARCH__: boolean
declare const __ALGOLIA__: boolean
@ -5,12 +10,6 @@ declare const __CARBON__: boolean
declare const __VUE_PROD_DEVTOOLS__: boolean
declare const __ASSETS_DIR__: string
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}
declare module '@siteData' {
import type { SiteData } from 'vitepress'
const data: SiteData

@ -2,7 +2,7 @@
import { useMediaQuery } from '@vueuse/core'
import { useRoute } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { onMounted, ref, watch } from 'vue'
import { onMounted, useTemplateRef, watch } from 'vue'
const route = useRoute()
const props = defineProps<{
@ -12,7 +12,7 @@ const props = defineProps<{
const carbonOptions = props.carbonAds
const isAsideVisible = useMediaQuery('(min-width: 80rem)')
const container = ref()
const container = useTemplateRef('container')
let isInitialized = false
@ -28,7 +28,7 @@ function init() {
s.id = '_carbonads_js'
s.src = `//cdn.carbonads.com/carbon.js?${params.toString()}`
s.async = true
container.value.appendChild(s)
container.value?.appendChild(s)
}
}

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useTemplateRef } from 'vue'
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout'
@ -8,8 +8,8 @@ import VPDocOutlineItem from './VPDocOutlineItem.vue'
const { theme } = useData()
const container = ref()
const marker = ref()
const container = useTemplateRef('container')
const marker = useTemplateRef('marker')
const { headers, hasLocalNav } = useLayout()

@ -1,6 +1,6 @@
<script lang="ts" setup generic="T extends DefaultTheme.NavItem">
import type { DefaultTheme } from 'vitepress/theme'
import { ref } from 'vue'
import { ref, useTemplateRef } from 'vue'
import { useFlyout } from '../composables/flyout'
import VPMenu from './VPMenu.vue'
@ -13,7 +13,7 @@ defineProps<{
}>()
const open = ref(false)
const el = ref<HTMLElement>()
const el = useTemplateRef('el')
useFlyout({ el, onBlur })

@ -2,7 +2,7 @@
import { onKeyStroke } from '@vueuse/core'
import { onContentUpdated } from 'vitepress'
import type { DefaultTheme } from 'vitepress/theme'
import { nextTick, ref, watch } from 'vue'
import { nextTick, ref, useTemplateRef, watch } from 'vue'
import { useData } from '../composables/data'
import { resolveTitle } from '../composables/outline'
@ -17,8 +17,8 @@ const props = defineProps<{
const { theme } = useData()
const open = ref(false)
const vh = ref(0)
const main = ref<HTMLDivElement>()
const items = ref<HTMLDivElement>()
const main = useTemplateRef('main')
const items = useTemplateRef('items')
// lock body scroll while the dropdown is open to prevent scroll chaining
const isLocked = useBodyScrollLock()

@ -21,6 +21,7 @@ import {
onMounted,
ref,
shallowRef,
useTemplateRef,
watch,
watchEffect,
type Ref
@ -38,8 +39,8 @@ const emit = defineEmits<{
(e: 'close'): void
}>()
const el = shallowRef<HTMLElement>()
const resultsEl = shallowRef<HTMLElement>()
const el = useTemplateRef('el')
const resultsEl = useTemplateRef('resultsEl')
/* Search */
@ -75,11 +76,11 @@ const showSearchSpinner = computed(() => {
})
const searchIndex = computedAsync(
async () =>
markRaw(
MiniSearch.loadJSON<Result>(
(await searchIndexData.value[localeIndex.value]?.())?.default,
{
async () => {
const json = (await searchIndexData.value[localeIndex.value]?.())?.default
if (!json) return null
return markRaw(
MiniSearch.loadJSON<Result>(json, {
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
searchOptions: {
@ -91,9 +92,9 @@ const searchIndex = computedAsync(
},
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.options)
}
})
)
),
},
undefined,
isSearchIndexLoading
)
@ -265,7 +266,7 @@ async function fetchExcerpt(id: string) {
/* Search input focus */
const searchInput = ref<HTMLInputElement>()
const searchInput = useTemplateRef('searchInput')
const disableReset = computed(() => {
return filterText.value?.length <= 0
})

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { ref, useTemplateRef, watch } from 'vue'
import { useLayout } from '../composables/layout'
import { useBodyScrollLock } from '../composables/scroll-lock'
@ -12,7 +12,7 @@ const props = defineProps<{
}>()
// a11y: focus Nav element when menu has opened
const navEl = ref<HTMLElement | null>(null)
const navEl = useTemplateRef('navEl')
const isLocked = useBodyScrollLock()
watch(

@ -1,14 +1,14 @@
<script lang="ts" setup>
import { useRoute } from 'vitepress'
import { ref, watch } from 'vue'
import { useTemplateRef, watch } from 'vue'
import { useData } from '../composables/data'
const { theme } = useData()
const route = useRoute()
const backToTop = ref()
const backToTop = useTemplateRef('backToTop')
watch(() => route.path, () => backToTop.value.focus())
watch(() => route.path, () => backToTop.value?.focus())
</script>
<template>

@ -1,6 +1,12 @@
<script lang="ts" setup>
import type { DefaultTheme } from 'vitepress/theme'
import { computed, nextTick, onMounted, ref, useSSRContext } from 'vue'
import {
computed,
nextTick,
onMounted,
useSSRContext,
useTemplateRef
} from 'vue'
import { isExternal, type SSGContext } from '../../shared'
@ -12,7 +18,7 @@ const props = defineProps<{
me: boolean
}>()
const el = ref<HTMLAnchorElement>()
const el = useTemplateRef('el')
onMounted(async () => {
await nextTick()

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useTemplateRef } from 'vue'
import type { GridSize } from '../composables/sponsor-grid'
import { useSponsorsGrid } from '../composables/sponsor-grid'
@ -17,7 +17,7 @@ const props = withDefaults(defineProps<Props>(), {
size: 'medium'
})
const el = ref(null)
const el = useTemplateRef('el')
useSponsorsGrid({ el, size: props.size })
</script>

@ -1,8 +1,8 @@
import { inBrowser } from 'vitepress'
import { onUnmounted, readonly, type Ref, ref, watch } from 'vue'
import { onUnmounted, readonly, type TemplateRef, ref, watch } from 'vue'
interface UseFlyoutOptions {
el: Ref<HTMLElement | undefined>
el: TemplateRef<HTMLElement>
onFocus?(): void
onBlur?(): void
}

@ -1,6 +1,6 @@
import { useMediaQuery } from '@vueuse/core'
import { onContentUpdated, useRoute } from 'vitepress'
import type { DefaultTheme, useLayout as expected } from 'vitepress/theme'
import type { DefaultTheme } from 'vitepress/theme'
import {
computed,
shallowReadonly,
@ -20,7 +20,7 @@ const sidebar = shallowRef<DefaultTheme.SidebarItem[]>([])
const isDesktop = useMediaQuery('(min-width: 60rem)')
export function useLayout(): ReturnType<typeof expected> {
export function useLayout(): DefaultTheme.Layout {
const { frontmatter, theme } = useData()
const isHome = computed(() => {

@ -1,6 +1,6 @@
import { useMediaQuery } from '@vueuse/core'
import type { DefaultTheme } from 'vitepress/theme'
import { onMounted, onUnmounted, onUpdated, type Ref } from 'vue'
import { onMounted, onUnmounted, onUpdated, type TemplateRef } from 'vue'
import { throttleAndDebounce } from '../support/utils'
@ -77,8 +77,8 @@ export function resolveHeaders(
}
export function useActiveAnchor(
container: Ref<HTMLElement>,
marker: Ref<HTMLElement>
container: TemplateRef<HTMLElement>,
marker: TemplateRef<HTMLElement>
): void {
const isAsideVisible = useMediaQuery('(min-width: 80rem)')
@ -90,7 +90,7 @@ export function useActiveAnchor(
onMounted(() => {
requestAnimationFrame(setActiveLink)
window.addEventListener('scroll', onScroll)
container.value.addEventListener('click', onClick)
container.value?.addEventListener('click', onClick)
})
onUpdated(() => {
@ -156,7 +156,7 @@ export function useActiveAnchor(
// page bottom - highlight last link
if (isBottom) {
activateLink(headers[headers.length - 1].link)
activateLink(headers.at(-1)?.link ?? null)
return
}
@ -174,9 +174,9 @@ export function useActiveAnchor(
function activateLink(hash: string | null) {
const activeLink =
hash != null
? container.value.querySelector<HTMLAnchorElement>(
? (container.value?.querySelector<HTMLAnchorElement>(
`a[href$="${decodeURIComponent(hash)}"]`
)
) ?? null)
: null
if (activeLink === prevActiveLink) return
@ -186,16 +186,18 @@ export function useActiveAnchor(
if (activeLink) {
activeLink.classList.add('active')
// the links' offsetParent (.root) sits below the outline title while the
// marker is offset from .content, so re-align their origins
if (marker.value) {
// the links' offsetParent (.root) sits below the outline title while
// the marker is offset from .content, so re-align their origins
marker.value.style.top =
activeLink.offsetTop +
((activeLink.offsetParent as HTMLElement)?.offsetTop ?? 0) +
(activeLink.offsetHeight - marker.value.offsetHeight) / 2 +
'px'
marker.value.style.opacity = '1'
}
activeLink.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
} else {
} else if (marker.value) {
marker.value.style.top = ''
marker.value.style.opacity = '0'
}

@ -1,15 +1,15 @@
import { type Ref, onMounted, onUnmounted } from 'vue'
import { type TemplateRef, onMounted, onUnmounted } from 'vue'
import { throttleAndDebounce } from '../support/utils'
export interface GridSetting {
[size: string]: [number, number][]
export type GridSetting = {
[size in GridSize]: [number, number][]
}
export type GridSize = 'xmini' | 'mini' | 'small' | 'medium' | 'big'
export interface UseSponsorsGridOptions {
el: Ref<HTMLElement | null>
el: TemplateRef<HTMLElement>
size?: GridSize
}
@ -63,7 +63,7 @@ export function useSponsorsGrid({
})
function manage() {
adjustSlots(el.value!, size)
if (el.value) adjustSlots(el.value, size)
}
}

@ -37,7 +37,7 @@ export function getSidebar(
return path.startsWith(ensureStartingSlash(dir))
})
const sidebar = dir ? _sidebar[dir] : []
const sidebar = dir ? (_sidebar[dir] ?? []) : []
return Array.isArray(sidebar)
? addBase(sidebar)
: addBase(sidebar.items, sidebar.base)
@ -51,19 +51,20 @@ export function getSidebarGroups(sidebar: SidebarItem[]): SidebarItem[] {
let lastGroupIndex: number = 0
for (const index in sidebar) {
const item = sidebar[index]
for (const item of sidebar) {
if (item.items) {
lastGroupIndex = groups.push(item)
continue
}
if (!groups[lastGroupIndex]) {
groups.push({ items: [] })
let group = groups[lastGroupIndex]
if (!group) {
group = { items: [] }
groups.push(group)
}
groups[lastGroupIndex]!.items!.push(item)
group.items?.push(item)
}
return groups

@ -4,7 +4,7 @@ import { isExternal, treatAsHtml } from '../../shared'
import { useData } from '../composables/data'
export function throttleAndDebounce(fn: () => void, delay: number): () => void {
let timeoutId: NodeJS.Timeout
let timeoutId: number
let called = false
return () => {
@ -12,8 +12,8 @@ export function throttleAndDebounce(fn: () => void, delay: number): () => void {
if (!called) {
fn()
;(called = true) && setTimeout(() => (called = false), delay)
} else timeoutId = setTimeout(fn, delay)
;(called = true) && window.setTimeout(() => (called = false), delay)
} else timeoutId = window.setTimeout(fn, delay)
}
}

@ -1,14 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/client",
"declaration": true,
"declarationDir": "../../dist/client-types",
"types": ["../../client.d.ts", "@types/node"],
"paths": {
"vitepress": ["./index.ts"],
"vitepress/theme": ["../../theme.d.ts"]
}
},
"include": ["."]
}

@ -18,11 +18,14 @@ import c from 'picocolors'
import { slash } from '../shared'
import { readFile } from '../utils/fs'
export enum ScaffoldThemeType {
Default = 'default theme',
DefaultCustom = 'default theme + customization',
Custom = 'custom theme'
}
export const ScaffoldThemeType = {
Default: 'default theme',
DefaultCustom: 'default theme + customization',
Custom: 'custom theme'
} as const
export type ScaffoldThemeType =
(typeof ScaffoldThemeType)[keyof typeof ScaffoldThemeType]
export interface ScaffoldOptions {
root?: string

@ -122,7 +122,7 @@ export async function createMarkdownToVueRenderFn(
const fileOrig = dynamicRoute?.[0] || file
const transformPageData = [
siteConfig?.transformPageData,
getPageDataTransformer(dynamicRoute?.[1]!)
getPageDataTransformer(dynamicRoute?.[1])
].filter((fn) => fn != null)
file = rewrites.get(normalizeDriveLetter(file)) || file

@ -217,8 +217,9 @@ export const dynamicRoutesPlugin = async (
}
export function getPageDataTransformer(
loaderPath: string
loaderPath?: string
): UserConfig['transformPageData'] | undefined {
if (loaderPath == null) return undefined
return routeModuleCache.get(loaderPath)?.transformPageData
}

@ -61,7 +61,7 @@ export const rewritesPlugin = (config: SiteConfig): Plugin => {
if (config.rewrites.inv[page]) {
req.url = req.url.replace(
encodeURI(page),
encodeURI(config.rewrites.inv[page]!)
encodeURI(config.rewrites.inv[page])
)
}
}

@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/node",
"types": ["node"],
"sourceMap": true
},
"include": ["."]
}

@ -122,7 +122,7 @@ export function resolveSiteDataByRoute(
): SiteData {
const localeIndex = getLocaleForPath(siteData, relativePath)
const { label, link, markdown, ...localeConfig } =
siteData.locales[localeIndex] ?? {}
siteData.locales[localeIndex] ?? ({} as (typeof siteData.locales)[string])
Object.assign(localeConfig, { localeIndex })
// additional configs are colocated with sources, so resolve them by the
@ -253,7 +253,7 @@ export function slash(p: string): string {
export function treatAsHtml(filename: string): boolean {
if (KNOWN_EXTENSIONS.size === 0) {
const extraExts =
(typeof process === 'object' && process.env?.VITE_EXTRA_EXTENSIONS) ||
(globalThis as any).process?.env?.VITE_EXTRA_EXTENSIONS ||
(import.meta as any).env?.VITE_EXTRA_EXTENSIONS ||
''
@ -300,7 +300,7 @@ function resolveAdditionalConfig(
if (typeof additionalConfig === 'function')
return additionalConfig(path) ?? []
const configs: AdditionalConfig[] = []
const configs: (AdditionalConfig | undefined)[] = []
const segments = path.split('/').slice(0, -1) // remove file name
while (segments.length) {

@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["esnext", "dom", "dom.iterable"]
},
"include": ["."]
}

64
theme.d.ts vendored

@ -1,7 +1,7 @@
// so that users can do `import DefaultTheme from 'vitepress/theme'`
import type { EnhanceAppContext } from 'vitepress'
import type { ComputedRef, DefineComponent, ShallowRef } from 'vue'
import type { DefineComponent } from 'vue'
import type { DefaultTheme } from './types/default-theme.js'
@ -14,48 +14,26 @@ declare const theme: {
export default theme
export declare const useLayout: () => {
isHome: ComputedRef<boolean>
sidebar: Readonly<ShallowRef<DefaultTheme.SidebarItem[]>>
sidebarGroups: ComputedRef<DefaultTheme.SidebarItem[]>
hasSidebar: ComputedRef<boolean>
isSidebarEnabled: ComputedRef<boolean>
hasAside: ComputedRef<boolean>
leftAside: ComputedRef<boolean>
/**
* The outline headers of the current page.
*/
headers: Readonly<ShallowRef<DefaultTheme.OutlineItem[]>>
/**
* Whether the current page has a local nav. Local nav is shown when the
* "outline" is present in the page. However, note that the actual
* local nav visibility depends on the screen width as well.
*/
hasLocalNav: ComputedRef<boolean>
}
// TODO: add props for these
export declare const VPBadge: DefineComponent
export declare const VPButton: DefineComponent
export declare const VPDocAsideSponsors: DefineComponent
export declare const VPFeatures: DefineComponent
export declare const VPHomeContent: DefineComponent
export declare const VPHomeFeatures: DefineComponent
export declare const VPHomeHero: DefineComponent
export declare const VPHomeSponsors: DefineComponent
export declare const VPImage: DefineComponent
export declare const VPLink: DefineComponent
export declare const VPNavBarSearch: DefineComponent
export declare const VPSocialLink: DefineComponent
export declare const VPSocialLinks: DefineComponent
export declare const VPSponsors: DefineComponent
export declare const VPTeamMembers: DefineComponent
export declare const VPTeamPage: DefineComponent
export declare const VPTeamPageSection: DefineComponent
export declare const VPTeamPageTitle: DefineComponent
export declare const useLayout: () => DefaultTheme.Layout
export declare const VPBadge: typeof import('./dist/client/theme-default/components/VPBadge.vue').default
export declare const VPButton: typeof import('./dist/client/theme-default/components/VPButton.vue').default
export declare const VPDocAsideSponsors: typeof import('./dist/client/theme-default/components/VPDocAsideSponsors.vue').default
export declare const VPFeatures: typeof import('./dist/client/theme-default/components/VPFeatures.vue').default
export declare const VPHomeContent: typeof import('./dist/client/theme-default/components/VPHomeContent.vue').default
export declare const VPHomeFeatures: typeof import('./dist/client/theme-default/components/VPHomeFeatures.vue').default
export declare const VPHomeHero: typeof import('./dist/client/theme-default/components/VPHomeHero.vue').default
export declare const VPHomeSponsors: typeof import('./dist/client/theme-default/components/VPHomeSponsors.vue').default
export declare const VPImage: typeof import('./dist/client/theme-default/components/VPImage.vue').default
export declare const VPLink: typeof import('./dist/client/theme-default/components/VPLink.vue').default
export declare const VPNavBarSearch: typeof import('./dist/client/theme-default/components/VPNavBarSearch.vue').default
export declare const VPSocialLink: typeof import('./dist/client/theme-default/components/VPSocialLink.vue').default
export declare const VPSocialLinks: typeof import('./dist/client/theme-default/components/VPSocialLinks.vue').default
export declare const VPSponsors: typeof import('./dist/client/theme-default/components/VPSponsors.vue').default
export declare const VPTeamMembers: typeof import('./dist/client/theme-default/components/VPTeamMembers.vue').default
export declare const VPTeamPage: typeof import('./dist/client/theme-default/components/VPTeamPage.vue').default
export declare const VPTeamPageSection: typeof import('./dist/client/theme-default/components/VPTeamPageSection.vue').default
export declare const VPTeamPageTitle: typeof import('./dist/client/theme-default/components/VPTeamPageTitle.vue').default
declare module 'vue' {
interface GlobalComponents {

@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "preserve",
"moduleResolution": "bundler",
"moduleDetection": "force",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"noEmit": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"strict": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true
},
"exclude": [
"**/node_modules/**",
"**/dist/**",
"template",
"bin",
"docs/snippets"
]
}

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"types": ["./client.d.ts"],
"noUncheckedIndexedAccess": true,
"paths": {
"vitepress": ["./src/client/index.ts"],
// default-theme.d.ts instead of theme.d.ts so type checks and the .vue
// declaration emit don't depend on a built dist.
"vitepress/theme": ["./types/default-theme.d.ts"],
// dev-server virtual modules, resolved onto their ambient shims
"@siteData": ["./src/client/shims.d.ts"],
"@theme/index": ["./src/client/shims.d.ts"],
"@localSearchIndex": ["./src/client/shims.d.ts"]
}
},
"include": ["src/client"]
}

@ -1,22 +1,3 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true,
"jsx": "preserve",
"lib": ["esnext", "dom", "dom.iterable"]
},
"exclude": [
"**/node_modules/**",
"**/dist/**",
"template",
"bin",
"docs/snippets"
]
"extends": "./tsconfig.base.json"
}

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"lib": ["ES2023"],
"types": ["node"]
},
"include": ["src/node", "scripts", "tsdown.config.ts", "shared-globals.d.ts"]
}

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"lib": ["ES2023"],
"types": [],
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
},
"include": ["src/shared", "shared-globals.d.ts"]
}

@ -0,0 +1,241 @@
import { readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { defineConfig, type Rolldown, type UserConfig } from 'tsdown'
import { vueSfcPlugin } from 'vue-sfc-transformer/rolldown'
const ROOT = import.meta.dirname
// wipe all of dist (not just the outDirs) once per invocation; the configs
// set `clean: false` so watch-mode rebuilds never clear it mid-session
rmSync(path.join(ROOT, 'dist'), {
recursive: true,
force: true,
maxRetries: 10
})
const normalizePath = (p: string): string => {
const normalized = p.replaceAll('\\', '/')
return process.platform === 'win32' ? normalized.toLowerCase() : normalized
}
const TYPES_DIR = normalizePath(path.join(ROOT, 'types')) + '/'
function isRootTypes(id: string, importer: string | undefined): boolean {
if (!importer || !/^\.\.?\//.test(id)) return false
const resolved = normalizePath(path.resolve(path.dirname(importer), id))
return resolved.startsWith(TYPES_DIR)
}
// keep .d.ts files under types/* external so module augmentations in the
// output still target the same files users reference, spelled with the .js
// extension so they resolve under node16 too
function rootTypesSpecifiers(): Rolldown.Plugin {
return {
name: 'vitepress:root-types-specifiers',
resolveId: {
order: 'pre',
handler(id, importer) {
if (
importer &&
/\.d\.[cm]?ts$/.test(importer) &&
isRootTypes(id, importer)
) {
return { id: id.replace(/\.js$/, '') + '.js', external: 'relative' }
}
}
}
}
}
// src/shared is compiled twice, once per environment, via gitignored copies.
// Copies are written only when their content differs — they are watched
// modules, so an unconditional write on every buildStart would retrigger the
// watcher in an endless loop.
function syncSharedFiles(
dest: 'client' | 'node',
track?: (file: string) => void
): void {
const src = path.join(ROOT, 'src/shared')
for (const entry of readdirSync(src, {
recursive: true,
encoding: 'utf8'
})) {
if (!entry.endsWith('.ts')) continue
const file = path.join(src, entry)
track?.(file)
const content = readFileSync(file)
const copy = path.join(ROOT, 'src', dest, entry)
let stale = true
try {
stale = !content.equals(readFileSync(copy))
} catch {}
if (stale) writeFileSync(copy, content)
}
}
// seed the copies now, before the entry globs below are resolved
syncSharedFiles('client')
syncSharedFiles('node')
// keeps the copies fresh across watch-mode rebuilds
function syncShared(dest: 'client' | 'node'): Rolldown.Plugin {
return {
name: 'vitepress:sync-shared',
buildStart() {
syncSharedFiles(dest, (file) => this.addWatchFile(file))
}
}
}
// Ships styles and fonts as-is, keeps css imports as relative specifiers in
// the output for the consumer's bundler, and drops them from declaration
// files where they would dangle.
function clientAssets(): Rolldown.Plugin {
const src = path.join(ROOT, 'src/client')
return {
name: 'vitepress:client-assets',
resolveId(id) {
if (id.endsWith('.css') && id[0] === '.') {
return { id, external: 'relative' }
}
},
buildStart() {
for (const entry of readdirSync(src, {
recursive: true,
encoding: 'utf8'
})) {
if (!/\.(css|woff2)$/.test(entry)) continue
const file = path.join(src, entry)
this.addWatchFile(file)
this.emitFile({
type: 'asset',
fileName: normalizePath(entry),
source: readFileSync(file)
})
}
}
}
}
// Declarations must resolve under node16, so relative specifiers need the .js
// extension: vue-tsc keeps the SFC's extensionless ones in .d.vue.ts files,
// and rolldown-plugin-dts drops the extension from chunk imports on Windows.
// Dangling side-effect css imports are removed along the way.
function fixDeclarationSpecifiers(): Rolldown.Plugin {
const stripCssImports = (code: string) =>
code.replace(/^\s*import\s+["'][^"']+\.css["'];?\s*\r?\n/gm, '')
const addJsExtensions = (code: string) =>
code.replace(
/(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])(\.\.?\/[^'"]*?)\2/g,
(match, keyword, quote, spec) =>
/\.[^/.]+$/.test(spec) ? match : `${keyword}${quote}${spec}.js${quote}`
)
const fix = (code: string) => addJsExtensions(stripCssImports(code))
return {
name: 'vitepress:fix-declaration-specifiers',
generateBundle(_options, bundle) {
for (const file of Object.values(bundle)) {
if (!/\.d\.(?:vue\.)?ts$/.test(file.fileName)) continue
if (file.type === 'chunk') file.code = fix(file.code)
else if (typeof file.source === 'string') file.source = fix(file.source)
}
}
}
}
// Rebuilds re-emit every output; dropping byte-identical files before they
// are written keeps their mtimes stable, so watchers of dist (the docs dev
// server's vite) only react to files that actually changed.
function skipUnchanged(): Rolldown.Plugin {
return {
name: 'vitepress:skip-unchanged',
generateBundle: {
order: 'post',
handler(options, bundle) {
for (const [key, file] of Object.entries(bundle)) {
const next =
file.type === 'chunk'
? Buffer.from(file.code)
: Buffer.from(file.source)
try {
const existing = readFileSync(
path.resolve(ROOT, options.dir ?? '.', file.fileName)
)
if (existing.equals(next)) delete bundle[key]
} catch {}
}
}
}
}
}
function withStableOutputs(config: UserConfig): UserConfig {
return {
...config,
clean: false,
plugins: [
...(config.plugins as Rolldown.Plugin[]),
fixDeclarationSpecifiers(),
skipUnchanged()
]
}
}
const client: UserConfig = {
entry: ['src/client/**/*.ts', '!src/client/**/*.d.ts'],
outDir: 'dist/client',
platform: 'neutral',
unbundle: true,
fixedExtension: false,
dts: { vue: true },
tsconfig: 'tsconfig.client.json',
deps: {
// self-imports and dev-server virtual modules, resolved at site build time
neverBundle: [
/^vitepress(?:\/|$)/,
'@siteData',
'@theme/index',
'@localSearchIndex'
]
},
plugins: [
syncShared('client'),
rootTypesSpecifiers(),
clientAssets(),
vueSfcPlugin({
srcDir: 'src/client',
cwd: ROOT,
tsconfig: './tsconfig.client.json'
})
],
checks: { pluginTimings: false }
}
const node: UserConfig = {
entry: ['src/node/index.ts', 'src/node/cli.ts'],
outDir: 'dist/node',
platform: 'node',
target: 'node22',
fixedExtension: false,
dts: true,
tsconfig: 'tsconfig.node.json',
// polyfill broken browser check from bundled deps
define: {
'navigator.userAgentData': 'undefined',
'navigator.userAgent': 'undefined'
},
deps: {
// devDependencies are bundled by design
onlyBundle: false,
// markdown-it types are provided by @types/markdown-it (a runtime dep)
dts: { neverBundle: /^markdown-it(?:\/|$)/ }
},
// code-level compression only — no name mangling or whitespace removal
minify: { compress: true, mangle: false, codegen: false },
outputOptions: { chunkFileNames: 'chunk-[hash].js' },
checks: { eval: false, pluginTimings: false },
plugins: [syncShared('node'), rootTypesSpecifiers()]
}
export default defineConfig([client, node].map(withStableOutputs))

@ -1,10 +1,37 @@
import type { Options as _MiniSearchOptions } from 'minisearch'
import type { ComputedRef, ShallowRef } from 'vue'
import type { DocSearchProps } from './docsearch.js'
import type { LocalSearchTranslations } from './local-search.js'
import type { Header, PageData, Route, VitePressData } from './shared.js'
export namespace DefaultTheme {
/**
* The layout state returned by `useLayout` from `vitepress/theme`.
*/
export interface Layout {
isHome: ComputedRef<boolean>
sidebar: Readonly<ShallowRef<SidebarItem[]>>
sidebarGroups: ComputedRef<SidebarItem[]>
hasSidebar: ComputedRef<boolean>
isSidebarEnabled: ComputedRef<boolean>
hasAside: ComputedRef<boolean>
leftAside: ComputedRef<boolean>
/**
* The outline headers of the current page.
*/
headers: Readonly<ShallowRef<OutlineItem[]>>
/**
* Whether the current page has a local nav. Local nav is shown when the
* "outline" is present in the page. However, note that the actual
* local nav visibility depends on the screen width as well.
*/
hasLocalNav: ComputedRef<boolean>
}
export interface Config {
/**
* The logo file of the site.

Loading…
Cancel
Save