pull/4663/merge
Yuxuan Zhang 4 months ago committed by GitHub
commit a137a6d6bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -54,7 +54,7 @@ describe('static data file support in vite 3', () => {
document.querySelector('pre#basic')?.textContent === document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: false }, { b: true }], null, 2), JSON.stringify([{ a: false }, { b: true }], null, 2),
undefined, undefined,
{ timeout: 3000 } { timeout: 5000 }
) )
} finally { } finally {
await fs.writeFile(a, JSON.stringify({ a: true }, null, 2) + '\n') await fs.writeFile(a, JSON.stringify({ a: true }, null, 2) + '\n')
@ -69,7 +69,7 @@ describe('static data file support in vite 3', () => {
document.querySelector('pre#basic')?.textContent === document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: true }], null, 2), JSON.stringify([{ a: true }], null, 2),
undefined, undefined,
{ timeout: 3000 } { timeout: 5000 }
) )
err = false err = false
} finally { } finally {
@ -85,7 +85,7 @@ describe('static data file support in vite 3', () => {
document.querySelector('pre#basic')?.textContent === document.querySelector('pre#basic')?.textContent ===
JSON.stringify([{ a: true }, { b: false }], null, 2), JSON.stringify([{ a: true }, { b: false }], null, 2),
undefined, undefined,
{ timeout: 3000 } { timeout: 5000 }
) )
} finally { } finally {
await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n') await fs.writeFile(b, JSON.stringify({ b: true }, null, 2) + '\n')

@ -107,6 +107,7 @@
"@vueuse/core": "^13.1.0", "@vueuse/core": "^13.1.0",
"@vueuse/integrations": "^13.1.0", "@vueuse/integrations": "^13.1.0",
"focus-trap": "^7.6.4", "focus-trap": "^7.6.4",
"living-object": "0.0.8",
"mark.js": "8.11.1", "mark.js": "8.11.1",
"minisearch": "^7.1.2", "minisearch": "^7.1.2",
"shiki": "^3.4.0", "shiki": "^3.4.0",

@ -55,6 +55,9 @@ importers:
focus-trap: focus-trap:
specifier: ^7.6.4 specifier: ^7.6.4
version: 7.6.4 version: 7.6.4
living-object:
specifier: 0.0.8
version: 0.0.8
mark.js: mark.js:
specifier: 8.11.1 specifier: 8.11.1
version: 8.11.1 version: 8.11.1
@ -2200,6 +2203,10 @@ packages:
resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
living-object@0.0.8:
resolution: {integrity: sha512-o4SGS1iyo3H6m/OAbrxty0RFgXNIkx0vojHDGcAk/RjQ+noB0lsK2znmq19BZ+jyOS7P0jj5zywxgVNuKt5/Gg==}
engines: {node: '>=16.0.0'}
local-pkg@1.1.1: local-pkg@1.1.1:
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -5123,6 +5130,8 @@ snapshots:
rfdc: 1.4.1 rfdc: 1.4.1
wrap-ansi: 9.0.0 wrap-ansi: 9.0.0
living-object@0.0.8: {}
local-pkg@1.1.1: local-pkg@1.1.1:
dependencies: dependencies:
mlly: 1.7.4 mlly: 1.7.4

@ -1,7 +1,17 @@
import { defineComponent, onMounted, ref } from 'vue' import { defineComponent, onMounted, ref } from 'vue'
export const ClientOnly = defineComponent({ export const ClientOnly = defineComponent({
setup(_, { slots }) { props: {
isClientOnly: {
type: Boolean,
default: true
}
},
setup(props, { slots }) {
// Programmatically determine if this component should be
// client-only based on the presence of the isClientOnly attribute.
if (!props.isClientOnly) return () => slots.default?.(props) || null
const show = ref(false) const show = ref(false)
onMounted(() => { onMounted(() => {

@ -2,29 +2,32 @@
import { useData } from '../composables/data' import { useData } from '../composables/data'
import VPNavBarMenuLink from './VPNavBarMenuLink.vue' import VPNavBarMenuLink from './VPNavBarMenuLink.vue'
import VPNavBarMenuGroup from './VPNavBarMenuGroup.vue' import VPNavBarMenuGroup from './VPNavBarMenuGroup.vue'
import { isClientOnly } from '../../shared'
const { theme } = useData() const { theme } = useData()
</script> </script>
<template> <template>
<nav <ClientOnly :is-client-only="isClientOnly(theme.nav)">
v-if="theme.nav" <nav
aria-labelledby="main-nav-aria-label" v-if="theme.nav"
class="VPNavBarMenu" aria-labelledby="main-nav-aria-label"
> class="VPNavBarMenu"
<span id="main-nav-aria-label" class="visually-hidden"> >
Main Navigation <span id="main-nav-aria-label" class="visually-hidden">
</span> Main Navigation
<template v-for="item in theme.nav" :key="JSON.stringify(item)"> </span>
<VPNavBarMenuLink v-if="'link' in item" :item /> <template v-for="item in theme.nav" :key="JSON.stringify(item)">
<component <VPNavBarMenuLink v-if="'link' in item" :item />
v-else-if="'component' in item" <component
:is="item.component" v-else-if="'component' in item"
v-bind="item.props" :is="item.component"
/> v-bind="item.props"
<VPNavBarMenuGroup v-else :item /> />
</template> <VPNavBarMenuGroup v-else :item />
</nav> </template>
</nav>
</ClientOnly>
</template> </template>
<style scoped> <style scoped>

@ -5,7 +5,7 @@ import { ref, watch } from 'vue'
import { useLayout } from '../composables/layout' import { useLayout } from '../composables/layout'
import VPSidebarGroup from './VPSidebarGroup.vue' import VPSidebarGroup from './VPSidebarGroup.vue'
const { sidebarGroups, hasSidebar } = useLayout() const { sidebarGroups, hasSidebar, isSidebarClientOnly } = useLayout()
const props = defineProps<{ const props = defineProps<{
open: boolean open: boolean
@ -38,27 +38,18 @@ watch(
</script> </script>
<template> <template>
<aside <aside v-if="hasSidebar" class="VPSidebar" :class="{ open }" ref="navEl" @click.stop>
v-if="hasSidebar"
class="VPSidebar"
:class="{ open }"
ref="navEl"
@click.stop
>
<div class="curtain" /> <div class="curtain" />
<nav <nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
class="nav"
id="VPSidebarNav"
aria-labelledby="sidebar-aria-label"
tabindex="-1"
>
<span class="visually-hidden" id="sidebar-aria-label"> <span class="visually-hidden" id="sidebar-aria-label">
Sidebar Navigation Sidebar Navigation
</span> </span>
<slot name="sidebar-nav-before" /> <slot name="sidebar-nav-before" />
<VPSidebarGroup :items="sidebarGroups" :key /> <ClientOnly :is-client-only="isSidebarClientOnly">
<VPSidebarGroup :items="sidebarGroups" :key />
</ClientOnly>
<slot name="sidebar-nav-after" /> <slot name="sidebar-nav-after" />
</nav> </nav>
</aside> </aside>

@ -2,6 +2,7 @@
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref } from 'vue'
import VPSidebarItem from './VPSidebarItem.vue' import VPSidebarItem from './VPSidebarItem.vue'
import { isClientOnly } from '../../shared'
defineProps<{ defineProps<{
items: DefaultTheme.SidebarItem[] items: DefaultTheme.SidebarItem[]
@ -33,7 +34,9 @@ onBeforeUnmount(() => {
class="group" class="group"
:class="{ 'no-transition': disableTransition }" :class="{ 'no-transition': disableTransition }"
> >
<VPSidebarItem :item :depth="0" /> <ClientOnly :is-client-only="isClientOnly(item)">
<VPSidebarItem :item :depth="0" />
</ClientOnly>
</div> </div>
</template> </template>

@ -3,6 +3,7 @@ import type { DefaultTheme } from 'vitepress/theme'
import { computed } from 'vue' import { computed } from 'vue'
import { useSidebarItemControl } from '../composables/sidebar' import { useSidebarItemControl } from '../composables/sidebar'
import VPLink from './VPLink.vue' import VPLink from './VPLink.vue'
import { isClientOnly } from '../../shared'
const props = defineProps<{ const props = defineProps<{
item: DefaultTheme.SidebarItem item: DefaultTheme.SidebarItem
@ -55,56 +56,58 @@ function onCaretClick() {
</script> </script>
<template> <template>
<component :is="sectionTag" class="VPSidebarItem" :class="classes"> <ClientOnly :is-client-only="isClientOnly(item)">
<div <component :is="sectionTag" class="VPSidebarItem" :class="classes">
v-if="item.text"
class="item"
:role="itemRole"
v-on="
item.items
? { click: onItemInteraction, keydown: onItemInteraction }
: {}
"
:tabindex="item.items && 0"
>
<div class="indicator" />
<VPLink
v-if="item.link"
:tag="linkTag"
class="link"
:href="item.link"
:rel="item.rel"
:target="item.target"
>
<component :is="textTag" class="text" v-html="item.text" />
</VPLink>
<component v-else :is="textTag" class="text" v-html="item.text" />
<div <div
v-if="item.collapsed != null && item.items && item.items.length" v-if="item.text"
class="caret" class="item"
role="button" :role="itemRole"
aria-label="toggle section" v-on="
@click="onCaretClick" item.items
@keydown.enter="onCaretClick" ? { click: onItemInteraction, keydown: onItemInteraction }
tabindex="0" : {}
"
:tabindex="item.items && 0"
> >
<span class="vpi-chevron-right caret-icon" /> <div class="indicator" />
<VPLink
v-if="item.link"
:tag="linkTag"
class="link"
:href="item.link"
:rel="item.rel"
:target="item.target"
>
<component :is="textTag" class="text" v-html="item.text" />
</VPLink>
<component v-else :is="textTag" class="text" v-html="item.text" />
<div
v-if="item.collapsed != null && item.items && item.items.length"
class="caret"
role="button"
aria-label="toggle section"
@click="onCaretClick"
@keydown.enter="onCaretClick"
tabindex="0"
>
<span class="vpi-chevron-right caret-icon" />
</div>
</div>
<div v-if="item.items && item.items.length" class="items">
<template v-if="depth < 5">
<VPSidebarItem
v-for="i in item.items"
:key="i.text"
:item="i"
:depth="depth + 1"
/>
</template>
</div> </div>
</div> </component>
</ClientOnly>
<div v-if="item.items && item.items.length" class="items">
<template v-if="depth < 5">
<VPSidebarItem
v-for="i in item.items"
:key="i.text"
:item="i"
:depth="depth + 1"
/>
</template>
</div>
</component>
</template> </template>
<style scoped> <style scoped>

@ -5,6 +5,7 @@ import { getSidebar, getSidebarGroups } from '../support/sidebar'
import { useData } from './data' import { useData } from './data'
import { getHeaders } from './outline' import { getHeaders } from './outline'
import { useCloseSidebarOnEscape } from './sidebar' import { useCloseSidebarOnEscape } from './sidebar'
import { isClientOnly } from '../../shared'
const headers = shallowRef<DefaultTheme.OutlineItem[]>([]) const headers = shallowRef<DefaultTheme.OutlineItem[]>([])
const sidebar = shallowRef<DefaultTheme.SidebarItem[]>([]) const sidebar = shallowRef<DefaultTheme.SidebarItem[]>([])
@ -28,6 +29,10 @@ export function useLayout(): ReturnType<typeof expected> {
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value) const isSidebarEnabled = computed(() => hasSidebar.value && is960.value)
const isSidebarClientOnly = computed(
() => isClientOnly(theme.value.sidebar) || isClientOnly(sidebar.value)
)
const sidebarGroups = computed(() => { const sidebarGroups = computed(() => {
return hasSidebar.value ? getSidebarGroups(sidebar.value) : [] return hasSidebar.value ? getSidebarGroups(sidebar.value) : []
}) })
@ -55,6 +60,7 @@ export function useLayout(): ReturnType<typeof expected> {
sidebarGroups, sidebarGroups,
hasSidebar, hasSidebar,
isSidebarEnabled, isSidebarEnabled,
isSidebarClientOnly,
hasAside, hasAside,
leftAside, leftAside,
headers: shallowReadonly(headers), headers: shallowReadonly(headers),

@ -1,5 +1,5 @@
import type { DefaultTheme } from 'vitepress/theme' import type { DefaultTheme } from 'vitepress/theme'
import { isActive } from '../../shared' import { isActive, propagateClientOnly } from '../../shared'
import { ensureStartingSlash } from './utils' import { ensureStartingSlash } from './utils'
export interface SidebarLink { export interface SidebarLink {
@ -108,12 +108,13 @@ export function hasActiveLink(
: false : false
} }
function addBase(items: SidebarItem[], _base?: string): SidebarItem[] { function addBase(items: SidebarItem[], _base?: string) {
return [...items].map((_item) => { const result = [...items].map((_item) => {
const item = { ..._item } const item = { ..._item }
const base = item.base || _base const base = item.base || _base
if (base && item.link) item.link = base + item.link if (base && item.link) item.link = base + item.link
if (item.items) item.items = addBase(item.items, base) if (item.items) item.items = addBase(item.items, base)
return item return propagateClientOnly(item, _item)
}) })
return propagateClientOnly(items, result)
} }

@ -12,11 +12,11 @@ import type { BuildOptions, Rollup } from 'vite'
import { resolveConfig, type SiteConfig } from '../config' import { resolveConfig, type SiteConfig } from '../config'
import { clearCache } from '../markdownToVue' import { clearCache } from '../markdownToVue'
import { slash, type Awaitable, type HeadConfig } from '../shared' import { slash, type Awaitable, type HeadConfig } from '../shared'
import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize'
import { task } from '../utils/task' import { task } from '../utils/task'
import { bundle } from './bundle' import { bundle } from './bundle'
import { generateSitemap } from './generateSitemap' import { generateSitemap } from './generateSitemap'
import { renderPage } from './render' import { renderPage } from './render'
import LivingObject from 'living-object'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
@ -216,15 +216,11 @@ function generateMetadataScript(
// It's also embedded as a string and JSON.parsed from the client because // It's also embedded as a string and JSON.parsed from the client because
// it's faster than embedding as JS object literal. // it's faster than embedding as JS object literal.
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap)) const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
const siteDataString = JSON.stringify( const siteDataString = new LivingObject({ ...config.site, head: [] })
JSON.stringify(serializeFunctions({ ...config.site, head: [] })) .compile()
) .complete((root) => `window.__VP_SITE_DATA__=${root}`)
const metadataContent = `window.__VP_HASH_MAP__=JSON.parse(${hashMapString});${ const metadataContent = `{window.__VP_HASH_MAP__=JSON.parse(${hashMapString});${siteDataString};}`
siteDataString.includes('_vp-fn_')
? `${deserializeFunctions};window.__VP_SITE_DATA__=deserializeFunctions(JSON.parse(${siteDataString}));`
: `window.__VP_SITE_DATA__=JSON.parse(${siteDataString});`
}`
if (!config.metaChunk) { if (!config.metaChunk) {
return { html: `<script>${metadataContent}</script>`, inHead: false } return { html: `<script>${metadataContent}</script>`, inHead: false }

@ -15,7 +15,7 @@ import type { DefaultTheme } from './defaultTheme'
import { resolvePages } from './plugins/dynamicRoutesPlugin' import { resolvePages } from './plugins/dynamicRoutesPlugin'
import { import {
APPEARANCE_KEY, APPEARANCE_KEY,
VP_SOURCE_KEY, VP_CONFIG_SOURCE,
isObject, isObject,
slash, slash,
type AdditionalConfig, type AdditionalConfig,
@ -28,6 +28,7 @@ import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig'
export { resolvePages } from './plugins/dynamicRoutesPlugin' export { resolvePages } from './plugins/dynamicRoutesPlugin'
export { resolveSiteDataByRoute } from './shared' export { resolveSiteDataByRoute } from './shared'
export * from './siteConfig' export * from './siteConfig'
export { clientOnly, isClientOnly } from './shared'
const debug = _debug('vitepress:config') const debug = _debug('vitepress:config')
@ -218,7 +219,7 @@ async function gatherAdditionalConfig(
) )
if (mode === 'development') { if (mode === 'development') {
;(configExports.config as any)[VP_SOURCE_KEY] = '/' + slash(file) ;(configExports.config as any)[VP_CONFIG_SOURCE] = '/' + slash(file)
} }
return [id, configExports.config as AdditionalConfig] as const return [id, configExports.config as AdditionalConfig] as const

@ -33,7 +33,7 @@ import { rewritesPlugin } from './plugins/rewritesPlugin'
import { staticDataPlugin } from './plugins/staticDataPlugin' import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin' import { webFontsPlugin } from './plugins/webFontsPlugin'
import { slash, type PageDataPayload } from './shared' import { slash, type PageDataPayload } from './shared'
import { deserializeFunctions, serializeFunctions } from './utils/fnSerialize' import { stringify as serializeSiteData } from 'living-object'
declare module 'vite' { declare module 'vite' {
interface UserConfig { interface UserConfig {
@ -184,8 +184,7 @@ export async function createVitePressPlugin(
return `export default window.__VP_SITE_DATA__` return `export default window.__VP_SITE_DATA__`
} }
} }
data = serializeFunctions(data) return serializeSiteData(data, { target: 'module' })
return `${deserializeFunctions};export default deserializeFunctions(JSON.parse(${JSON.stringify(JSON.stringify(data))}))`
} }
}, },

@ -1,42 +0,0 @@
export function serializeFunctions(value: any, key?: string): any {
if (Array.isArray(value)) {
return value.map((v) => serializeFunctions(v))
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).reduce((acc, key) => {
if (key[0] === '_') return acc
acc[key] = serializeFunctions(value[key], key)
return acc
}, {} as any)
} else if (typeof value === 'function') {
let serialized = value.toString()
if (
key &&
(serialized.startsWith(key) || serialized.startsWith('async ' + key))
) {
serialized = serialized.replace(key, 'function')
}
return `_vp-fn_${serialized}`
} else {
return value
}
}
/*
export function deserializeFunctions(value: any): any {
if (Array.isArray(value)) {
return value.map(deserializeFunctions)
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).reduce((acc, key) => {
acc[key] = deserializeFunctions(value[key])
return acc
}, {} as any)
} else if (typeof value === 'string' && value.startsWith('_vp-fn_')) {
return new Function(`return ${value.slice(7)}`)()
} else {
return value
}
}
*/
export const deserializeFunctions =
'function deserializeFunctions(r){return Array.isArray(r)?r.map(deserializeFunctions):typeof r=="object"&&r!==null?Object.keys(r).reduce((t,n)=>(t[n]=deserializeFunctions(r[n]),t),{}):typeof r=="string"&&r.startsWith("_vp-fn_")?new Function(`return ${r.slice(7)}`)():r}'

@ -25,7 +25,11 @@ export type {
export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i
export const APPEARANCE_KEY = 'vitepress-theme-appearance' export const APPEARANCE_KEY = 'vitepress-theme-appearance'
export const VP_SOURCE_KEY = '[VP_SOURCE]' // Global symbols will be preserved by living-object.
export const VP_CONFIG_SOURCE = Symbol.for('VP:ConfigSource')
export const VP_CLIENT_ONLY = Symbol.for('VP:ClientOnly')
// Private symbols are distinct in each runtime environment.
// This should only be used internally.
const UnpackStackView = Symbol('stack-view:unpack') const UnpackStackView = Symbol('stack-view:unpack')
const HASH_RE = /#.*$/ const HASH_RE = /#.*$/
@ -111,7 +115,7 @@ export function resolveSiteDataByRoute(
const additionalConfigs = resolveAdditionalConfig(siteData, relativePath) const additionalConfigs = resolveAdditionalConfig(siteData, relativePath)
if (inBrowser && (import.meta as any).env?.DEV) { if (inBrowser && (import.meta as any).env?.DEV) {
;(localeConfig as any)[VP_SOURCE_KEY] = `locale config (${localeIndex})` ;(localeConfig as any)[VP_CONFIG_SOURCE] = `locale config (${localeIndex})`
reportConfigLayers(relativePath, [ reportConfigLayers(relativePath, [
...additionalConfigs, ...additionalConfigs,
localeConfig, localeConfig,
@ -297,7 +301,7 @@ function reportConfigLayers(path: string, layers: Partial<SiteData>[]) {
const summary = layers.map((c, i, arr) => { const summary = layers.map((c, i, arr) => {
const n = i + 1 const n = i + 1
if (n === arr.length) return `${n}. .vitepress/config (root)` if (n === arr.length) return `${n}. .vitepress/config (root)`
return `${n}. ${(c as any)?.[VP_SOURCE_KEY] ?? '(Unknown Source)'}` return `${n}. ${(c as any)?.[VP_CONFIG_SOURCE] ?? '(Unknown Source)'}`
}) })
console.debug( console.debug(
@ -353,3 +357,19 @@ type ObjectType = Record<PropertyKey, any>
export function isObject(value: unknown): value is ObjectType { export function isObject(value: unknown): value is ObjectType {
return Object.prototype.toString.call(value) === '[object Object]' return Object.prototype.toString.call(value) === '[object Object]'
} }
export function clientOnly<T extends object>(object: T) {
;(object as any)[VP_CLIENT_ONLY] = true
return object
}
export function isClientOnly<T extends object>(object?: T): boolean {
return (object as any)?.[VP_CLIENT_ONLY] ?? false
}
export function propagateClientOnly<T extends object>(src: T, dst: T): T {
if (isClientOnly(src)) {
clientOnly(dst)
}
return dst
}

1
theme.d.ts vendored

@ -20,6 +20,7 @@ export declare const useLayout: () => {
sidebarGroups: ComputedRef<DefaultTheme.SidebarItem[]> sidebarGroups: ComputedRef<DefaultTheme.SidebarItem[]>
hasSidebar: ComputedRef<boolean> hasSidebar: ComputedRef<boolean>
isSidebarEnabled: ComputedRef<boolean> isSidebarEnabled: ComputedRef<boolean>
isSidebarClientOnly: ComputedRef<boolean>
hasAside: ComputedRef<boolean> hasAside: ComputedRef<boolean>
leftAside: ComputedRef<boolean> leftAside: ComputedRef<boolean>

Loading…
Cancel
Save