feat(docs): add static generation and search indexes
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { lazy } from "react"
|
||||
|
||||
import {
|
||||
defaultDocLocale,
|
||||
docLocales,
|
||||
@@ -12,23 +14,17 @@ import {
|
||||
} from "./types"
|
||||
import { trans, type Translation } from "../lib/locale"
|
||||
|
||||
type DocModule = {
|
||||
type DocContentModule = {
|
||||
default: React.ComponentType<{ components?: MdxComponents }>
|
||||
frontmatter: Record<string, unknown>
|
||||
}
|
||||
|
||||
type LocalizedText = Translation<string>
|
||||
|
||||
type PackageDefinition = {
|
||||
key: string
|
||||
label: string
|
||||
description: LocalizedText
|
||||
description: Translation<string>
|
||||
}
|
||||
|
||||
type ParsedDocPage = DocPage & {
|
||||
packageKey: string
|
||||
searchText: string
|
||||
}
|
||||
type ParsedDocPage = DocPage & { packageKey: string }
|
||||
|
||||
const packageDefinitions: readonly PackageDefinition[] = [
|
||||
{
|
||||
@@ -81,18 +77,23 @@ const packageDefinitions: readonly PackageDefinition[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const docModules = import.meta.glob<DocModule>(
|
||||
const docModuleLoaders = import.meta.glob<DocContentModule>(
|
||||
"../../../packages/*/docs/*/**/*.mdx",
|
||||
{ eager: true }
|
||||
{ eager: false }
|
||||
)
|
||||
|
||||
const docSources = import.meta.glob<string>(
|
||||
const docFrontmatter = import.meta.glob<Record<string, unknown>>(
|
||||
"../../../packages/*/docs/*/**/*.mdx",
|
||||
{ eager: true, import: "default", query: "?docs-search-raw" }
|
||||
{ eager: true, import: "frontmatter" }
|
||||
)
|
||||
|
||||
const parsedPages = Object.entries(docModules).map(([path, module]) =>
|
||||
parseDocPage(path, module, docSources[path] ?? "")
|
||||
const parsedPages = Object.entries(docFrontmatter).map(
|
||||
([path, frontmatter]) => {
|
||||
const load = docModuleLoaders[path]
|
||||
if (!load) throw new Error(`Missing documentation module for "${path}".`)
|
||||
|
||||
return parseDocPage(path, frontmatter, lazy(load))
|
||||
}
|
||||
)
|
||||
|
||||
const packagesByLocale: Record<DocLocale, readonly DocPackage[]> = {
|
||||
@@ -147,7 +148,7 @@ export function getLocalizedHref(pathname: string, locale: DocLocale) {
|
||||
}
|
||||
|
||||
export function resolveDocLocale(pathname: string): DocLocale {
|
||||
const firstSegment = pathname.split("/").filter(Boolean)[0]
|
||||
const firstSegment = pathname.split("/").find(Boolean)
|
||||
return isDocLocale(firstSegment) ? firstSegment : defaultDocLocale
|
||||
}
|
||||
|
||||
@@ -193,8 +194,17 @@ export function getDocSectionLabel(section: DocSection, locale: DocLocale) {
|
||||
return trans(locale, labels[section])
|
||||
}
|
||||
|
||||
export function getDocSearchText(page: DocPage) {
|
||||
return (page as ParsedDocPage).searchText
|
||||
export function getStaticDocRoutes() {
|
||||
return docLocales.flatMap((locale) => {
|
||||
const home = getDocsHomeHref(locale)
|
||||
return [
|
||||
home,
|
||||
`${home === "/" ? "" : home}/settings`,
|
||||
...getDocPackages(locale).flatMap((docPackage) =>
|
||||
docPackage.pages.map((page) => getDocHref(docPackage, page))
|
||||
),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function createDocPackage(
|
||||
@@ -205,7 +215,7 @@ function createDocPackage(
|
||||
.filter(
|
||||
(page) => page.packageKey === definition.key && page.locale === locale
|
||||
)
|
||||
.sort(
|
||||
.toSorted(
|
||||
(left, right) =>
|
||||
docSections.indexOf(left.section) -
|
||||
docSections.indexOf(right.section) ||
|
||||
@@ -241,8 +251,8 @@ function createDocPackage(
|
||||
|
||||
function parseDocPage(
|
||||
path: string,
|
||||
module: DocModule,
|
||||
source: string
|
||||
frontmatter: Record<string, unknown>,
|
||||
Content: DocPage["Content"]
|
||||
): ParsedDocPage {
|
||||
const match = path.match(
|
||||
/\/packages\/([^/]+)\/docs\/([^/]+)\/(overview|guide|advanced|examples)(?:\/([^/]+))?\.mdx$/
|
||||
@@ -269,14 +279,12 @@ function parseDocPage(
|
||||
throw new Error(`Documentation file is in the wrong directory: "${path}".`)
|
||||
}
|
||||
|
||||
const frontmatter = module.frontmatter
|
||||
return {
|
||||
Content: module.default,
|
||||
Content,
|
||||
description: readString(frontmatter, "description"),
|
||||
locale: localeValue,
|
||||
order: readNumber(frontmatter, "order"),
|
||||
packageKey,
|
||||
searchText: source,
|
||||
section: sectionValue,
|
||||
slug: nestedSlug ?? "overview",
|
||||
title: readString(frontmatter, "title"),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { DocLocale, DocSearchEntry } from "./types"
|
||||
|
||||
const indexes = new Map<DocLocale, Promise<readonly DocSearchEntry[]>>()
|
||||
|
||||
export function loadDocSearchIndex(locale: DocLocale) {
|
||||
const existing = indexes.get(locale)
|
||||
if (existing) return existing
|
||||
|
||||
const index = (
|
||||
import.meta.env.DEV
|
||||
? import("./search-index.source").then((module) =>
|
||||
module.getDocSearchIndex(locale)
|
||||
)
|
||||
: fetch(
|
||||
`${import.meta.env.BASE_URL}search/${encodeURIComponent(locale)}.json`
|
||||
).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Unable to load the ${locale} documentation search index.`
|
||||
)
|
||||
}
|
||||
return (await response.json()) as readonly DocSearchEntry[]
|
||||
})
|
||||
).catch((error: unknown) => {
|
||||
indexes.delete(locale)
|
||||
throw error
|
||||
})
|
||||
|
||||
indexes.set(locale, index)
|
||||
return index
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createDocSearchIndex } from "./search-index"
|
||||
import type { DocLocale } from "./types"
|
||||
|
||||
const docSources = import.meta.glob<string>(
|
||||
"../../../packages/*/docs/*/**/*.mdx",
|
||||
{ eager: true, import: "default", query: "?docs-search-raw" }
|
||||
)
|
||||
|
||||
export function getDocSearchIndex(locale: DocLocale) {
|
||||
return createDocSearchIndex(locale, docSources)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { getDocHref, getDocPackages, getDocSectionLabel } from "./registry"
|
||||
import type { DocLocale, DocSearchEntry, DocTocItem } from "./types"
|
||||
|
||||
export function createDocSearchIndex(
|
||||
locale: DocLocale,
|
||||
sources: Readonly<Record<string, string>>
|
||||
): readonly DocSearchEntry[] {
|
||||
const sourcesByPage = new Map(
|
||||
Object.entries(sources).map(([path, source]) => [
|
||||
getSourcePageKey(path),
|
||||
stripFrontmatter(source),
|
||||
])
|
||||
)
|
||||
|
||||
return getDocPackages(locale).flatMap((docPackage) =>
|
||||
docPackage.pages.map((page) => ({
|
||||
body:
|
||||
sourcesByPage.get(
|
||||
`${docPackage.key}/${locale}/${page.section}/${page.slug}`
|
||||
) ?? "",
|
||||
description: page.description,
|
||||
href: getDocHref(docPackage, page),
|
||||
id: `${page.section}/${page.slug}`,
|
||||
packageKey: docPackage.key,
|
||||
packageLabel: docPackage.label,
|
||||
sectionLabel: getDocSectionLabel(page.section, locale),
|
||||
title: page.title,
|
||||
toc: page.toc.flatMap(flattenTocTitles),
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
function getSourcePageKey(path: string) {
|
||||
const match = path.match(
|
||||
/\/packages\/([^/]+)\/docs\/([^/]+)\/(overview|guide|advanced|examples)(?:\/([^/]+))?\.mdx$/
|
||||
)
|
||||
if (!match) throw new Error(`Invalid documentation search source: "${path}".`)
|
||||
|
||||
const [, packageKey, locale, section, nestedSlug] = match
|
||||
return `${packageKey}/${locale}/${section}/${nestedSlug ?? "overview"}`
|
||||
}
|
||||
|
||||
function flattenTocTitles(item: DocTocItem): readonly string[] {
|
||||
return [item.title, ...(item.items?.flatMap(flattenTocTitles) ?? [])]
|
||||
}
|
||||
|
||||
function stripFrontmatter(source: string) {
|
||||
return source.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, "")
|
||||
}
|
||||
@@ -30,6 +30,18 @@ export type DocPage = {
|
||||
toc: readonly DocTocItem[]
|
||||
}
|
||||
|
||||
export type DocSearchEntry = {
|
||||
body: string
|
||||
description: string
|
||||
href: string
|
||||
id: string
|
||||
packageKey: string
|
||||
packageLabel: string
|
||||
sectionLabel: string
|
||||
title: string
|
||||
toc: readonly string[]
|
||||
}
|
||||
|
||||
export type DocPackage = {
|
||||
key: string
|
||||
label: string
|
||||
|
||||
Reference in New Issue
Block a user