Files
simple-react-app-kit/docs/src/content/registry.ts
T
2026-09-20 16:44:22 +08:00

372 lines
11 KiB
TypeScript

import { lazy } from "react"
import {
defaultDocLocale,
docLocales,
docSections,
type DocLocale,
type DocLocation,
type DocPackage,
type DocPage,
type DocSection,
type DocTocItem,
type MdxComponents,
} from "./types"
import { trans, type Translation } from "../lib/locale"
type DocContentModule = {
default: React.ComponentType<{ components?: MdxComponents }>
}
type PackageDefinition = {
key: string
label: string
description: Translation<string>
}
type ParsedDocPage = DocPage & { packageKey: string }
const packageDefinitions: readonly PackageDefinition[] = [
{
key: "blocks",
label: "Blocks",
description: {
zh: "构建应用外壳,并接入导航、通知、聊天和外观控制。",
us: "Build an application shell and add navigation, notifications, chat, and appearance controls.",
},
},
{
key: "i18n",
label: "I18n",
description: {
zh: "接入应用自有词典、本地化运行时、开发工具和完整翻译工作流。",
us: "Add application-owned catalogs, localized runtime rendering, development tools, and translation workflows.",
},
},
{
key: "lexical",
label: "Lexical",
description: {
zh: "从根节点和内容区域开始,逐步构建支持自定义操作与媒体的富文本编辑器。",
us: "Build a rich-text editor progressively from its root and content surface to custom actions and media.",
},
},
{
key: "media",
label: "Media",
description: {
zh: "通过存储 Adapter 构建媒体浏览、上传、目录管理和资源选择流程。",
us: "Build media browsing, uploads, folder management, and asset selection with a storage adapter.",
},
},
{
key: "preferences",
label: "Preferences",
description: {
zh: "使用 SSR 快照、持久化回调和可组合副作用管理类型安全且可扩展的应用偏好。",
us: "Manage typed, extensible application preferences with SSR snapshots, persistence callbacks, and composable effects.",
},
},
{
key: "search",
label: "Search",
description: {
zh: "通过可替换的 Adapter、分组结果、分页和搜索历史构建应用级搜索体验。",
us: "Build application search with a replaceable adapter, grouped results, pagination, and search history.",
},
},
{
key: "ui",
label: "UI",
description: {
zh: "配置共享 UI 层,并学习组合组件、Hooks、图标和本地化词典。",
us: "Set up the shared UI layer and learn how to compose its components, hooks, icons, and locale catalogs.",
},
},
]
const docModuleLoaders = import.meta.glob<DocContentModule>(
"../../../packages/*/docs/*/**/*.mdx",
{ eager: false }
)
const docFrontmatter = import.meta.glob<Record<string, unknown>>(
"../../../packages/*/docs/*/**/*.mdx",
{ eager: true, import: "frontmatter" }
)
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[]> = {
"zh-Hans": packageDefinitions.map((definition) =>
createDocPackage(definition, "zh-Hans")
),
"en-US": packageDefinitions.map((definition) =>
createDocPackage(definition, "en-US")
),
}
validateLocalizedPagePairs(packagesByLocale)
export function getDocPackages(locale: DocLocale = defaultDocLocale) {
return packagesByLocale[locale]
}
export function getDocHref(docPackage: DocPackage, page?: DocPage) {
const prefix = getLocalePrefix(docPackage.locale)
const target = page ?? getOverviewPage(docPackage)
const pagePath =
target.section === "overview"
? ""
: `/${target.section}/${encodeURIComponent(target.slug)}`
return `${prefix}/articles/${docPackage.key}${pagePath}`
}
export function getDocsHomeHref(locale: DocLocale) {
return getLocalePrefix(locale) || "/"
}
export function getLocalizedHref(pathname: string, locale: DocLocale) {
if (/^(?:\/(?:en-US|zh-Hans))?\/settings\/?$/.test(pathname)) {
return `${getLocalePrefix(locale)}/settings`
}
const location = resolveDocLocation(pathname)
if (!location) return getDocsHomeHref(locale)
const docPackage = getDocPackages(locale).find(
(item) => item.key === location.package.key
)
const page = docPackage?.pages.find(
(item) =>
item.section === location.page.section && item.slug === location.page.slug
)
return docPackage && page
? getDocHref(docPackage, page)
: getDocsHomeHref(locale)
}
export function resolveDocLocale(pathname: string): DocLocale {
const firstSegment = pathname.split("/").find(Boolean)
return isDocLocale(firstSegment) ? firstSegment : defaultDocLocale
}
export function resolveDocLocation(pathname: string): DocLocation | undefined {
const segments = pathname.split("/").filter(Boolean)
let locale: DocLocale = defaultDocLocale
if (isDocLocale(segments[0])) {
locale = segments.shift() as DocLocale
}
const [resource, packageKey, sectionValue, pageSlug, ...extra] = segments
if (resource !== "articles" || extra.length > 0) return undefined
const docPackage = getDocPackages(locale).find(
(item) => item.key === packageKey
)
if (!docPackage) return undefined
if (sectionValue === undefined) {
const page = getOverviewPage(docPackage)
return { locale, package: docPackage, page }
}
if (sectionValue === "overview" || !isDocSection(sectionValue) || !pageSlug) {
return undefined
}
const page = docPackage.pages.find(
(item) => item.section === sectionValue && item.slug === pageSlug
)
return page ? { locale, package: docPackage, page } : undefined
}
export function getDocSectionLabel(section: DocSection, locale: DocLocale) {
const labels: Record<DocSection, Translation> = {
overview: { zh: "概览", us: "Overview" },
guide: { zh: "指南", us: "Guide" },
advanced: { zh: "进阶", us: "Advanced" },
examples: { zh: "示例", us: "Examples" },
}
return trans(locale, labels[section])
}
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(
definition: PackageDefinition,
locale: DocLocale
): DocPackage {
const pages = parsedPages
.filter(
(page) => page.packageKey === definition.key && page.locale === locale
)
.toSorted(
(left, right) =>
docSections.indexOf(left.section) -
docSections.indexOf(right.section) ||
left.order - right.order ||
left.slug.localeCompare(right.slug)
)
const overviewCount = pages.filter(
(page) => page.section === "overview"
).length
if (overviewCount !== 1) {
throw new Error(
`${definition.key}/${locale} must contain exactly one docs/${locale}/overview.mdx file.`
)
}
for (const section of docSections.slice(1)) {
if (!pages.some((page) => page.section === section)) {
throw new Error(
`${definition.key}/${locale} must contain at least one ${section} document.`
)
}
}
return {
key: definition.key,
label: definition.label,
description: trans(locale, definition.description),
locale,
pages,
}
}
function parseDocPage(
path: string,
frontmatter: Record<string, unknown>,
Content: DocPage["Content"]
): ParsedDocPage {
const match = path.match(
/\/packages\/([^/]+)\/docs\/([^/]+)\/(overview|guide|advanced|examples)(?:\/([^/]+))?\.mdx$/
)
if (!match) {
throw new Error(
`Invalid documentation path "${path}". Expected docs/<locale>/overview.mdx or docs/<locale>/<guide|advanced|examples>/*.mdx.`
)
}
const [, packageKey, localeValue, sectionValue, nestedSlug] = match
if (!isDocLocale(localeValue)) {
throw new Error(
`Unsupported documentation locale "${localeValue}" in "${path}".`
)
}
if (!isDocSection(sectionValue)) {
throw new Error(`Unsupported documentation section in "${path}".`)
}
if (
(sectionValue === "overview" && nestedSlug !== undefined) ||
(sectionValue !== "overview" && nestedSlug === undefined)
) {
throw new Error(`Documentation file is in the wrong directory: "${path}".`)
}
return {
Content,
description: readString(frontmatter, "description"),
locale: localeValue,
order: readNumber(frontmatter, "order"),
packageKey,
section: sectionValue,
slug: nestedSlug ?? "overview",
title: readString(frontmatter, "title"),
toc: readToc(frontmatter.toc),
}
}
function validateLocalizedPagePairs(
packages: Record<DocLocale, readonly DocPackage[]>
) {
for (const definition of packageDefinitions) {
const keysByLocale = docLocales.map((locale) => {
const docPackage = packages[locale].find(
(item) => item.key === definition.key
)!
return docPackage.pages.map((page) => `${page.section}/${page.slug}`)
})
if (keysByLocale[0]?.join("\n") !== keysByLocale[1]?.join("\n")) {
throw new Error(
`${definition.key} documentation must contain matching zh-Hans and en-US page paths.`
)
}
}
}
function getOverviewPage(docPackage: DocPackage) {
return docPackage.pages.find((page) => page.section === "overview")!
}
function getLocalePrefix(locale: DocLocale) {
return locale === defaultDocLocale ? "" : `/${locale}`
}
function isDocLocale(value: unknown): value is DocLocale {
return docLocales.some((locale) => locale === value)
}
function isDocSection(value: unknown): value is DocSection {
return docSections.some((section) => section === value)
}
function readString(frontmatter: Record<string, unknown>, key: string) {
const value = frontmatter[key]
if (typeof value === "string" && value.trim()) return value
throw new Error(`Documentation frontmatter requires a string "${key}".`)
}
function readNumber(frontmatter: Record<string, unknown>, key: string) {
const value = frontmatter[key]
if (typeof value === "number" && Number.isFinite(value)) return value
throw new Error(`Documentation frontmatter requires a number "${key}".`)
}
function readToc(value: unknown): readonly DocTocItem[] {
if (value === undefined) return []
if (!Array.isArray(value)) {
throw new Error('Documentation frontmatter "toc" must be an array.')
}
return value.map((item) => {
if (!isRecord(item)) {
throw new Error('Each documentation "toc" entry must be an object.')
}
return {
id: readString(item, "id"),
title: readString(item, "title"),
items: item.items === undefined ? undefined : readToc(item.items),
}
})
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}