refactor: simplify code and comments around the icons pipeline

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/5407/head
Divyansh Singh 2 weeks ago
parent 4eb940510b
commit 98fafe4a01

@ -12,15 +12,10 @@ import { parseIconName, type SSGContext } from '../../shared'
import { withBase } from '../utils' import { withBase } from '../utils'
/** /**
* Renders an iconify icon through vitepress's icon pipeline: during SSR the * Resolves an icon name (`collection:name`, e.g. `simple-icons:github`) to
* name is registered so the build emits its CSS rule; in dev the icon is * its `vpi-<collection>-<name>` class. During SSR the name is registered so
* resolved from locally installed collections, without network access. * the build emits its CSS rule; in dev the SVG is served on demand and
* * applied to `el` inline.
* Accepts a fully qualified `collection:name` for any `@iconify-json/*`
* collection in the project's dependencies (e.g. `simple-icons:github`).
* Returns the class to render (`vpi-<collection>-<name>`); pass the
* template ref of the element carrying it so dev can apply the on-demand
* fallback.
*/ */
export function useIcon( export function useIcon(
icon: MaybeRefOrGetter<string | { svg: string } | undefined>, icon: MaybeRefOrGetter<string | { svg: string } | undefined>,
@ -43,9 +38,8 @@ export function useIcon(
// unparseable names are registered too — the build warns about them // unparseable names are registered too — the build warns about them
if (typeof value === 'string') ctx?.vpIcons.add(value) if (typeof value === 'string') ctx?.vpIcons.add(value)
} else if (import.meta.env.DEV) { } else if (import.meta.env.DEV) {
// dev has no generated stylesheet, so a `vpi-<collection>-<name>` class // dev has no generated stylesheet — the icon is always fetched from the
// never has a rule — the icon always comes from the dev server, tracked // dev server, re-resolved when the name changes
// per name so a reactive icon prop re-resolves
let applied: string | undefined let applied: string | undefined
onMounted(() => { onMounted(() => {
watchPostEffect(() => { watchPostEffect(() => {
@ -66,8 +60,7 @@ export function useIcon(
'--icon', '--icon',
`url('${withBase(`/@vpicons/${name.collection}/${name.icon}.svg`)}')` `url('${withBase(`/@vpicons/${name.collection}/${name.icon}.svg`)}')`
) )
// a theme without the default theme's icon rules gets the mask // inline the mask setup for themes without the default icon rules
// machinery inline, so dev works before any styling exists
const styles = getComputedStyle(span) const styles = getComputedStyle(span)
if ((styles.maskImage || styles.webkitMaskImage) === 'none') { if ((styles.maskImage || styles.webkitMaskImage) === 'none') {
Object.assign(span.style, { Object.assign(span.style, {

@ -39,14 +39,13 @@ export async function renderPage(
usedIcons: Set<string> usedIcons: Set<string>
) { ) {
const routePath = `/${page.replace(/\.md$/, '')}` const routePath = `/${page.replace(/\.md$/, '')}`
const relativeBase = isRelativeBase(config.site.base) const relativeBase = isRelativeBase(config.site.base)
const pageBase = relativeBase ? relativePathToRoot(page) : config.site.base const pageBase = relativeBase ? relativePathToRoot(page) : config.site.base
// user hooks must never see the build sentinel // user hooks must never see the build sentinel
const desentinel = (value: string) => const desentinel = (value: string) =>
relativeBase ? value.replaceAll(RELATIVE_BASE_SENTINEL, pageBase) : value relativeBase ? value.replaceAll(RELATIVE_BASE_SENTINEL, pageBase) : value
// render page
const context = await render(routePath) const context = await render(routePath)
if (relativeBase) { if (relativeBase) {
context.content = desentinel(context.content) context.content = desentinel(context.content)
@ -56,9 +55,9 @@ export async function renderPage(
} }
} }
} }
// SSR filled the original context's icon set — drain it before postRender,
// which may return a fresh object without it; a hook can still contribute // collect the icons rendered during SSR; postRender may replace the
// additional icons through the object it returns // context and contribute more
context.vpIcons?.forEach((icon) => usedIcons.add(icon)) context.vpIcons?.forEach((icon) => usedIcons.add(icon))
const rendered = (await config.postRender?.(context)) ?? context const rendered = (await config.postRender?.(context)) ?? context
@ -70,10 +69,6 @@ export async function renderPage(
const pageName = sanitizeFileName(page.replace(/\//g, '_')) const pageName = sanitizeFileName(page.replace(/\//g, '_'))
// server build doesn't need hash // server build doesn't need hash
const pageServerJsFileName = pageName + '.js' const pageServerJsFileName = pageName + '.js'
// for any initial page load, we only need the lean version of the page js
// since the static content is already on the page!
const pageHash = pageToHashMap[pageName.toLowerCase()]
const pageClientJsFileName = `${config.assetsDir}/${pageName}.${pageHash}.lean.js`
let pageData: PageData let pageData: PageData
let hasCustom404 = true let hasCustom404 = true
@ -102,26 +97,27 @@ export async function renderPage(
: '' : ''
const pageAssets = relativeBase ? assets.map(desentinel) : assets const pageAssets = relativeBase ? assets.map(desentinel) : assets
const title: string = createTitle(siteData, pageData) const title = createTitle(siteData, pageData)
const description: string = pageData.description || siteData.description const description = pageData.description || siteData.description
const stylesheetLink = cssChunk const dir = pageData.frontmatter.dir || siteData.dir || 'ltr'
? `<link rel="preload stylesheet" href="${assetUrl(cssChunk.fileName)}" as="style">` const isDefault404 = page === '404.md' && !hasCustom404
: ''
// the initial load only needs the lean page js — the static content is
// already in the HTML
const pageHash = pageToHashMap[pageName.toLowerCase()]
const pageClientJsFileName = `${config.assetsDir}/${pageName}.${pageHash}.lean.js`
let preloadLinks = let preloadLinks: string[] = []
config.mpa || (!hasCustom404 && page === '404.md') if (result && appChunk && !config.mpa && !isDefault404) {
? [] preloadLinks = [
: result && appChunk
? [
...new Set([ ...new Set([
// resolve imports for index.js + page.md.js and inject script tags // the imports of index.js + page.md.js as well, so everything
// for them as well so we fetch everything as early as possible // fetches without waiting for the entry chunks to parse
// without having to wait for entry chunks to parse
...(await resolvePageImports(config, page, result, appChunk)), ...(await resolvePageImports(config, page, result, appChunk)),
pageClientJsFileName pageClientJsFileName
]) ])
] ]
: [] }
let prefetchLinks: string[] = [] let prefetchLinks: string[] = []
@ -163,21 +159,27 @@ export async function renderPage(
) )
] ]
const head = mergeHead( const transformContext = (head: HeadConfig[]) => ({
headBeforeTransform,
(await config.transformHead?.({
page, page,
siteConfig: config, siteConfig: config,
siteData, siteData,
pageData, pageData,
title, title,
description, description,
head: headBeforeTransform, head,
content, content,
assets: pageAssets assets: pageAssets
})) || [] })
const head = mergeHead(
headBeforeTransform,
(await config.transformHead?.(transformContext(headBeforeTransform))) || []
) )
const stylesheetLink = cssChunk
? `<link rel="preload stylesheet" href="${assetUrl(cssChunk.fileName)}" as="style">`
: ''
let inlinedScript = '' let inlinedScript = ''
if (config.mpa && result) { if (config.mpa && result) {
const matchingChunk = result.output.find( const matchingChunk = result.output.find(
@ -197,20 +199,18 @@ export async function renderPage(
} }
} }
const dir = pageData.frontmatter.dir || siteData.dir || 'ltr'
const html = `<!DOCTYPE html> const html = `<!DOCTYPE html>
<html lang="${siteData.lang}" dir="${dir}"> <html lang="${siteData.lang}" dir="${dir}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
${ ${
isMetaViewportOverridden(head) hasNamedMeta(head, 'viewport')
? '' ? ''
: '<meta name="viewport" content="width=device-width,initial-scale=1">' : '<meta name="viewport" content="width=device-width,initial-scale=1">'
} }
<title>${escapeHtml(title)}</title> <title>${escapeHtml(title)}</title>
${ ${
isDescriptionOverridden(head) hasNamedMeta(head, 'description')
? '' ? ''
: `<meta name="description" content="${escapeHtml(description)}">` : `<meta name="description" content="${escapeHtml(description)}">`
} }
@ -245,17 +245,7 @@ export async function renderPage(
const transformedHtml = await config.transformHtml?.( const transformedHtml = await config.transformHtml?.(
finalHtml, finalHtml,
htmlFileName, htmlFileName,
{ transformContext(head)
page,
siteConfig: config,
siteData,
pageData,
title,
description,
head,
content,
assets: pageAssets
}
) )
await writeFile(htmlFileName, transformedHtml || finalHtml) await writeFile(htmlFileName, transformedHtml || finalHtml)
} }
@ -267,35 +257,28 @@ async function resolvePageImports(
appChunk: Rolldown.OutputChunk appChunk: Rolldown.OutputChunk
) { ) {
page = config.rewrites.inv[page] || page page = config.rewrites.inv[page] || page
// find the page's js chunk and inject script tags for its imports so that
// they start fetching as early as possible
let srcPath = path.resolve(config.srcDir, page) let srcPath = path.resolve(config.srcDir, page)
try { try {
if (!config.vite?.resolve?.preserveSymlinks) { if (!config.vite?.resolve?.preserveSymlinks) {
srcPath = await realpath(srcPath) srcPath = await realpath(srcPath)
} }
} catch (e) { } catch {
// if the page is a virtual page generated by a dynamic route this would // virtual pages generated by dynamic routes have no file on disk
// fail, which is expected
} }
srcPath = normalizePath(srcPath) srcPath = normalizePath(srcPath)
const pageChunk = result.output.find( const pageChunk = result.output.find(
(chunk): chunk is Rolldown.OutputChunk => (chunk): chunk is Rolldown.OutputChunk =>
chunk.type === 'chunk' && chunk.facadeModuleId === srcPath chunk.type === 'chunk' && chunk.facadeModuleId === srcPath
) )
return [ // dynamic imports are intentionally not preloaded
...appChunk.imports, return [...appChunk.imports, ...(pageChunk?.imports || [])]
// ...appChunk.dynamicImports,
...(pageChunk?.imports || [])
// ...pageChunk.dynamicImports
]
} }
async function renderHead(head: HeadConfig[]): Promise<string> { async function renderHead(head: HeadConfig[]): Promise<string> {
const tags = await Promise.all( const tags = await Promise.all(
head.map(async ([tag, attrs = {}, innerHTML = '']) => { head.map(async ([tag, attrs = {}, innerHTML = '']) => {
const openTag = `<${tag}${renderAttrs(attrs)}>` const openTag = `<${tag}${renderAttrs(attrs)}>`
if (tag !== 'link' && tag !== 'meta') { if (tag === 'link' || tag === 'meta') return openTag
if ( if (
tag === 'script' && tag === 'script' &&
(attrs.type === undefined || attrs.type.includes('javascript')) (attrs.type === undefined || attrs.type.includes('javascript'))
@ -303,9 +286,6 @@ async function renderHead(head: HeadConfig[]): Promise<string> {
innerHTML = (await minify('inline-script.js', innerHTML)).code innerHTML = (await minify('inline-script.js', innerHTML)).code
} }
return `${openTag}${innerHTML}</${tag}>` return `${openTag}${innerHTML}</${tag}>`
} else {
return openTag
}
}) })
) )
return tags.join('\n ') return tags.join('\n ')
@ -313,27 +293,18 @@ async function renderHead(head: HeadConfig[]): Promise<string> {
function renderAttrs(attrs: Record<string, string>): string { function renderAttrs(attrs: Record<string, string>): string {
return Object.keys(attrs) return Object.keys(attrs)
.map((key) => { .map((key) =>
if (isBooleanAttr(key)) return ` ${key}` isBooleanAttr(key) ? ` ${key}` : ` ${key}="${escapeHtml(attrs[key])}"`
return ` ${key}="${escapeHtml(attrs[key] as string)}"` )
})
.join('') .join('')
} }
function filterOutHeadDescription(head: HeadConfig[] = []) { function filterOutHeadDescription(head: HeadConfig[] = []) {
return head.filter(([type, attrs]) => { return head.filter(
return !(type === 'meta' && attrs?.name === 'description') ([type, attrs]) => !(type === 'meta' && attrs?.name === 'description')
}) )
}
function isDescriptionOverridden(head: HeadConfig[] = []) {
return head.some(([type, attrs]) => {
return type === 'meta' && attrs?.name === 'description'
})
} }
function isMetaViewportOverridden(head: HeadConfig[] = []) { function hasNamedMeta(head: HeadConfig[], name: string) {
return head.some(([type, attrs]) => { return head.some(([type, attrs]) => type === 'meta' && attrs?.name === name)
return type === 'meta' && attrs?.name === 'viewport'
})
} }

@ -12,8 +12,8 @@ type IconifyJSON = Parameters<typeof getIconsCSSData>[0]
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
// collections vitepress itself depends on (simple-icons today) — resolvable // collections vitepress itself depends on, resolvable even when the project
// through vitepress even when the project doesn't install them // doesn't install them
const ownCollections = new Set( const ownCollections = new Set(
Object.keys(dependencies) Object.keys(dependencies)
.filter((dep) => dep.startsWith('@iconify-json/')) .filter((dep) => dep.startsWith('@iconify-json/'))
@ -21,9 +21,8 @@ const ownCollections = new Set(
) )
/** /**
* Replaced with the content hash (or stripped together with the link tag when * Placeholder for the stylesheet's content hash, replaced once all pages
* no icons are used) after all pages have rendered the icon set, and hence * have rendered and the icon set is complete.
* the hash, is only complete once every page's SSR pass has run.
*/ */
export const VP_ICONS_HASH_PLACEHOLDER = '__VP_ICONS_HASH__' export const VP_ICONS_HASH_PLACEHOLDER = '__VP_ICONS_HASH__'
@ -31,11 +30,9 @@ export function vpIconsFileName(hash: string): string {
return `vp-icons.${hash}.css` return `vp-icons.${hash}.css`
} }
// mirrors theme-default/styles/icons.css at zero specificity so any theme's // mirrors theme-default/styles/icons.css at zero specificity, so any theme's
// rules win; always emitted — no reliable way exists to tell whether a // rules win and duplication is inert; the `--icon` default keeps unresolved
// bundle carries the default theme's copy, and duplication is inert. The // icons invisible instead of solid currentColor boxes
// `--icon` default keeps an unresolved icon invisible instead of painting a
// solid currentColor box.
const BASE_RULES = const BASE_RULES =
":where([class^='vpi-'],[class*=' vpi-'])" + ":where([class^='vpi-'],[class*=' vpi-'])" +
`{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E");` + `{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E");` +
@ -60,8 +57,7 @@ async function loadCollection(
const key = `${root}\0${name}` const key = `${root}\0${name}`
let cached = collectionCache.get(key) let cached = collectionCache.get(key)
if (!cached) { if (!cached) {
// resolvable from anywhere in the project's tree; falls back to // falls back to vitepress's own dependencies for the collections it ships
// vitepress's own dependencies for the collections it ships
cached = loadCollectionFromFS(name, false, '@iconify-json', root) cached = loadCollectionFromFS(name, false, '@iconify-json', root)
.catch(() => undefined) .catch(() => undefined)
.then( .then(
@ -72,8 +68,7 @@ async function loadCollection(
: undefined) : undefined)
) )
collectionCache.set(key, cached) collectionCache.set(key, cached)
// don't memoize a miss — the user may install the collection while the // don't cache misses — the collection may be installed during dev
// dev server is running
cached.then((data) => { cached.then((data) => {
if (!data) collectionCache.delete(key) if (!data) collectionCache.delete(key)
}) })
@ -81,6 +76,14 @@ async function loadCollection(
return cached return cached
} }
const collectionMissingMessage = (collection: string) =>
`icon collection "${collection}" is not installed — ` +
`run \`npm add -D @iconify-json/${collection}\` in your project`
const iconMissingMessage = (collection: string, icon: string) =>
`icon "${icon}" was not found in the "${collection}" collection — ` +
`check https://icones.js.org/collection/${collection} for valid names.`
export async function generateIconsCSS( export async function generateIconsCSS(
root: string, root: string,
icons: Set<string>, icons: Set<string>,
@ -123,9 +126,6 @@ export async function generateIconsCSS(
return false return false
}) })
if (!found.length) continue if (!found.length) continue
// no commonSelector: `css` then holds only per-icon rules, and the
// common declarations land in `common`, which the theme's static rules
// (or BASE_RULES) replace
const cssData = getIconsCSSData(data, found, { const cssData = getIconsCSSData(data, found, {
iconSelector: '.vpi-{prefix}-{name}', iconSelector: '.vpi-{prefix}-{name}',
varName: 'icon', varName: 'icon',
@ -135,19 +135,12 @@ export async function generateIconsCSS(
chunks.push(formatCSS(cssData.css, format)) chunks.push(formatCSS(cssData.css, format))
} }
if (!chunks.length) return { css: '', warnings } return {
css: chunks.length ? BASE_RULES + '\n' + chunks.join('') : '',
return { css: BASE_RULES + '\n' + chunks.join(''), warnings } warnings
}
} }
const collectionMissingMessage = (collection: string) =>
`icon collection "${collection}" is not installed — ` +
`run \`npm add -D @iconify-json/${collection}\` in your project`
const iconMissingMessage = (collection: string, icon: string) =>
`icon "${icon}" was not found in the "${collection}" collection — ` +
`check https://icones.js.org/collection/${collection} for valid names.`
/** single-icon SVG for the dev-server endpoint */ /** single-icon SVG for the dev-server endpoint */
export async function resolveIconSVG( export async function resolveIconSVG(
root: string, root: string,

@ -8,9 +8,7 @@ const iconRequestRE = /\/@vpicons\/([a-z0-9-]+)\/([a-z0-9-]+)\.svg$/
/** /**
* Serves `/@vpicons/<collection>/<name>.svg` in dev from locally installed * Serves `/@vpicons/<collection>/<name>.svg` in dev from locally installed
* `@iconify-json/*` collections, so icons render without the generated * `@iconify-json/*` collections (requested on demand by `useIcon`).
* stylesheet and without any network access (the `useIcon` composable
* requests these on demand).
*/ */
export function iconsPlugin(siteConfig: SiteConfig): Plugin { export function iconsPlugin(siteConfig: SiteConfig): Plugin {
const warned = new Set<string>() const warned = new Set<string>()

@ -121,27 +121,38 @@ export interface UserConfig<
*/ */
assetsDir?: string assetsDir?: string
/** /**
* URL prefix the built assets (everything under `assetsDir`) are served * URL prefix for built assets (everything under `assetsDir`), e.g. a CDN.
* from, e.g. a CDN. Must be an absolute URL, a protocol-relative URL, or *
* a root-absolute path, and must mirror the layout of `outDir`: each URL * Must be one of:
* is this prefix plus the file's output-relative path. Pages, `withBase` * - an absolute URL
* links, `public/` files and `hashmap.json` stay on `base`. A * - a protocol-relative URL
* cross-origin prefix must send CORS headers, as the generated tags are * - a root-absolute path
* marked `crossorigin`. Applies to builds and preview, not dev. *
* The prefix must mirror `outDir` layout: each asset URL = this prefix +
* file output-relative path.
*
* These still use `base`:
* - pages
* - `withBase` links
* - `public/` files
* - `hashmap.json`
*
* If the prefix is cross-origin, it must serve CORS headers, because
* generated tags are marked `crossorigin`.
*
* Applies to builds and preview (not dev).
*
* @example 'https://cdn.example.com/' * @example 'https://cdn.example.com/'
*/ */
assetsBase?: string assetsBase?: string
/** /**
* Options for the generated icon styles (a hashed `vp-icons.*.css` asset * Options for the generated icon stylesheet (`vp-icons.*.css`).
* holding every iconify icon rendered during SSR).
*/ */
icons?: { icons?: {
/** /**
* Icons to include in the generated stylesheet in addition to the ones * Fully qualified `collection:name` icons to include in addition to
* collected while rendering pages needed for icons that only render * the ones collected during SSR for icons that only render
* client-side (e.g. inside `<ClientOnly>`), which SSR collection cannot * client-side (e.g. inside `<ClientOnly>`).
* see. Names are fully qualified as `collection:name`, for any
* `@iconify-json/*` collection in the project's dependencies.
* @example ['mdi:home', 'simple-icons:discord'] * @example ['mdi:home', 'simple-icons:discord']
*/ */
include?: string[] include?: string[]
@ -237,10 +248,10 @@ export interface UserConfig<
*/ */
cleanUrls?: boolean cleanUrls?: boolean
/** /**
* Use web fonts instead of emitting font files to dist. The active * Use web fonts instead of emitting font files to dist. Requires the
* theme must import a file named `fonts.(s)css` for this to work. If * active theme to import a file named `fonts.(s)css`, with its web font
* you are a theme author, to support this, place your web font import * imports placed between `webfont-marker-begin` and `webfont-marker-end`
* between `webfont-marker-begin` and `webfont-marker-end` comments. * comments.
* @experimental * @experimental
* @default true in webcontainers, else false * @default true in webcontainers, else false
*/ */

@ -34,12 +34,9 @@ export const APPEARANCE_KEY = 'vitepress-theme-appearance'
const iconNameRE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ const iconNameRE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
/** /**
* Parse a fully qualified `collection:name` icon name (any installed * Parses a fully qualified `collection:name` icon name, corresponding to
* `@iconify-json/*` collection). The corresponding class is * the `vpi-<collection>-<name>` class. Returns null for anything else,
* `vpi-<collection>-<name>`. Returns null for anything else bare names * keeping malformed input out of generated selectors and class attributes.
* included which also keeps malformed input out of generated selectors
* and class attributes. (`socialLinks` additionally accepts bare
* simple-icons names; the theme qualifies them before they get here.)
*/ */
export function parseIconName( export function parseIconName(
name: string name: string
@ -53,9 +50,8 @@ export function parseIconName(
} }
/** /**
* Placeholder base used by SSR when base is relative. * Placeholder prepended to SSR-emitted URLs when base is relative, later
* It is prepended to emitted URLs, then replaced with the ../ prefix * replaced with each page's `../` prefix back to the site root.
* from each file back to the site root.
*/ */
export const RELATIVE_BASE_SENTINEL = '/__VP_BASE__/' export const RELATIVE_BASE_SENTINEL = '/__VP_BASE__/'
@ -164,7 +160,8 @@ export function getLocaleForPath(
} }
/** /**
* this merges the locales data to the main data by the route * Resolves the site data for a route, layering the matched locale and
* additional configs over the root config.
*/ */
export function resolveSiteDataByRoute( export function resolveSiteDataByRoute(
siteData: SiteData, siteData: SiteData,
@ -176,8 +173,8 @@ export function resolveSiteDataByRoute(
siteData.locales[localeIndex] ?? ({} as (typeof siteData.locales)[string]) siteData.locales[localeIndex] ?? ({} as (typeof siteData.locales)[string])
Object.assign(localeConfig, { localeIndex }) Object.assign(localeConfig, { localeIndex })
// additional configs are colocated with sources, so resolve them by the // additional configs are colocated with sources — resolve them by source
// source path (filePath) rather than the rewritten one // path rather than the rewritten one
const additionalConfigs = resolveAdditionalConfig( const additionalConfigs = resolveAdditionalConfig(
siteData, siteData,
filePath || relativePath filePath || relativePath
@ -364,7 +361,7 @@ function resolveAdditionalConfig(
return configs.filter((config) => config !== undefined) return configs.filter((config) => config !== undefined)
} }
// This helps users to understand which configuration files are active // logs the config layers active for a page (dev only)
function reportConfigLayers(path: string, layers: Partial<SiteData>[]) { function reportConfigLayers(path: string, layers: Partial<SiteData>[]) {
const summaryTitle = `Config Layers for ${path}:` const summaryTitle = `Config Layers for ${path}:`
@ -380,9 +377,8 @@ function reportConfigLayers(path: string, layers: Partial<SiteData>[]) {
} }
/** /**
* Creates a deep, merged view of multiple objects without mutating originals. * Creates a readonly proxy behaving like a deep merge of the given layers,
* Returns a readonly proxy behaving like a merged object of the input objects. * without mutating them. Earlier layers take precedence.
* Layers are merged in descending precedence, i.e. earlier layer is on top.
*/ */
export function stackView<T extends ObjectType>(..._layers: Partial<T>[]): T { export function stackView<T extends ObjectType>(..._layers: Partial<T>[]): T {
const layers = _layers.filter((layer) => isObject(layer)) const layers = _layers.filter((layer) => isObject(layer))

45
types/shared.d.ts vendored

@ -36,10 +36,10 @@ export interface PageData {
*/ */
relativePath: string relativePath: string
/** /**
* The path of the actual source file relative to the source directory. * The path of the actual source file relative to the source directory:
* Differs from `relativePath` when path rewrites are in use, points to * differs from `relativePath` when rewrites are in use, points to the
* the route template for dynamic routes, and is an empty string if the * route template for dynamic routes, and is empty for virtual pages
* page is virtual (e.g. the 404 page). * (e.g. the 404 page).
*/ */
filePath: string filePath: string
/** /**
@ -247,11 +247,10 @@ export interface SiteData<ThemeConfig = any> {
prefetchLinks: boolean prefetchLinks: boolean
} }
/** /**
* Config overrides applied to pages by source directory: either a dict * Config overrides applied to pages by source directory (before
* mapping a directory (e.g. `/guide/`) to overrides, where deeper * rewrites): a dict mapping a directory (e.g. `/guide/`) to overrides,
* directories take precedence, or a function returning the overrides to * deeper directories taking precedence, or a function returning the
* apply for a page. Directories are resolved against the source paths of * overrides for a page.
* pages, before rewrites.
*/ */
additionalConfig?: additionalConfig?:
AdditionalConfigDict<ThemeConfig> | AdditionalConfigLoader<ThemeConfig> AdditionalConfigDict<ThemeConfig> | AdditionalConfigLoader<ThemeConfig>
@ -364,11 +363,9 @@ export interface SSGContext extends SSRContext {
*/ */
content: string content: string
/** /**
* The icons used on the page, collected during SSR so that only the * The icons used on the page, registered during SSR (via `useIcon`) so
* styles of used icons are emitted into the generated stylesheet. * that only their styles are emitted. Names are fully qualified as
* Names are fully qualified as `collection:name`, for any * `collection:name`.
* `@iconify-json/*` collection in the project's dependencies. Theme
* components register icons here (see the `useIcon` composable).
*/ */
vpIcons: Set<string> vpIcons: Set<string>
} }
@ -455,13 +452,10 @@ export interface ContainerOptions {
cautionLabel?: string cautionLabel?: string
/** /**
* Additional containers to register, mapping the container name to its * Additional containers to register, mapping the container name to its
* default title. Registered names work both as `::: name` blocks and as * default title. Names must be lowercase (letters, numbers, hyphens,
* GitHub-style alerts (`> [!NAME]`), and are styleable in the theme via * underscores), work as both `::: name` blocks and `> [!NAME]` alerts,
* `.custom-block.name`. Names must be lowercase and may only contain * and are styleable via `.custom-block.name`. Locale overrides may only
* letters, numbers, hyphens, and underscores. * change the titles of root-registered names.
*
* In locale-specific overrides only the titles of containers registered
* at the root level can be changed - new names cannot be added there.
*/ */
customContainers?: Record<string, string> customContainers?: Record<string, string>
} }
@ -483,9 +477,8 @@ export interface CodeCopyButtonOptions {
} }
/** /**
* Build-time markdown strings that can be overridden per locale. Set them * Markdown strings overridable per locale via `locales.<index>.markdown`,
* under `locales.<index>.markdown` in the site config; values fall back to * falling back to the root `markdown` options when unset.
* the root `markdown` options when a locale leaves them unset.
*/ */
export interface MarkdownLocaleOptions { export interface MarkdownLocaleOptions {
/** /**
@ -544,8 +537,8 @@ export type AdditionalConfigLoader<ThemeConfig = any> = (
filePath: string filePath: string
) => AdditionalConfig<ThemeConfig>[] | void ) => AdditionalConfig<ThemeConfig>[] | void
// Manually declaring all properties as rollup-plugin-dts // all properties are declared manually as rollup-plugin-dts cannot merge
// is unable to merge augmented module declarations // augmented module declarations
/** /**
* The environment object passed to `markdown-it` when rendering a page. * The environment object passed to `markdown-it` when rendering a page.
*/ */

Loading…
Cancel
Save