From e17d2b8ae30bfb3c73630bd7eb2c374321e56609 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 18:35:59 +0000 Subject: [PATCH 1/2] refactor: centralize PageData type and use it for page.data casts - Created `app/types/doc.ts` to host shared `PageData` and `DateLike` types. - Updated `app/sitemap.ts` to use imported types instead of local definitions. - Refactored `lib/search-index.ts` to use `PageData` instead of `PageDataShape`. - Updated `app/docs/[...slug]/page.tsx` to use `PageData` for `page.data` casts, simplifying the code. - Ensured consistent property access across the codebase. Co-authored-by: longsizhuo <114939201+longsizhuo@users.noreply.github.com> --- app/docs/[...slug]/page.tsx | 10 ++++---- app/sitemap.ts | 26 +-------------------- app/types/doc.ts | 46 +++++++++++++++++++++++++++++++++++++ lib/search-index.ts | 16 +++---------- 4 files changed, 54 insertions(+), 44 deletions(-) create mode 100644 app/types/doc.ts diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index 960c092..ab699a1 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -18,6 +18,7 @@ import { PageFeedback } from "@/app/components/PageFeedback"; import { DocHistoryPanel } from "@/app/components/DocHistoryPanel"; import { DocShareButton } from "@/app/components/DocShareButton"; import { cookies } from "next/headers"; +import { type PageData } from "@/app/types/doc"; // Extract clean text content from MDX - no longer used on client/page side // content fetching moved to API route for performance @@ -51,7 +52,7 @@ function getPageWithLocale( return { page: originalPage, isFallback: false }; const originalLang = - (originalPage?.data as { lang?: string } | undefined)?.lang ?? null; + (originalPage?.data as PageData | undefined)?.lang ?? null; // 已经是目标语言,直接返回 if (originalLang === locale) return { page: originalPage, isFallback: false }; @@ -83,11 +84,8 @@ export default async function DocPage({ params }: Param) { // 统一通过工具函数生成 Edit 链接,内部已处理中文目录编码 const editUrl = buildDocsEditUrl(page.path); - const docIdFromPage = - (page.data as { docId?: string; frontmatter?: { docId?: string } }) - ?.docId ?? - (page.data as { docId?: string; frontmatter?: { docId?: string } }) - ?.frontmatter?.docId; + const data = page.data as PageData; + const docIdFromPage = data.docId ?? data.frontmatter?.docId; const contributorsEntry = getDocContributorsByPath(page.file.path) || diff --git a/app/sitemap.ts b/app/sitemap.ts index 85cc8db..262722b 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -24,36 +24,12 @@ import leaderboard from "@/generated/site-leaderboard.json"; // SITE_URL 由 lib/site-url.ts 统一提供(从 NEXT_PUBLIC_SITE_URL 读 + 归一化), // 这里和 app/robots.ts 共用一份,避免两边 drift。 import { SITE_URL } from "@/lib/site-url"; +import { type PageData, type DateLike } from "@/app/types/doc"; /** * 定义 `source.getPages()` 返回的单个页面对象的类型别名 */ type SourcePage = ReturnType[number]; -/** * 定义可以被解析为日期的宽松类型 - */ -type DateLike = string | number | Date | undefined | null; - -/** - * (FIX) 定义一个用于 page.data 的基础类型, - * 以避免在 isDraftOrHidden 和 extractDateFromPage 中使用 'any'。 - */ -type PageData = { - date?: DateLike; - updated?: DateLike; - updatedAt?: DateLike; - lastUpdated?: DateLike; - draft?: boolean; - hidden?: boolean; - frontmatter?: { - date?: DateLike; - updated?: DateLike; - updatedAt?: DateLike; - lastUpdated?: DateLike; - draft?: boolean; - hidden?: boolean; - }; -}; - /** * Next.js 会调用的默认导出函数,用于生成整个站点的 Sitemap。 * * @returns {MetadataRoute.Sitemap} 一个包含所有站点地图条目的数组。 diff --git a/app/types/doc.ts b/app/types/doc.ts new file mode 100644 index 0000000..6f0f20b --- /dev/null +++ b/app/types/doc.ts @@ -0,0 +1,46 @@ +import type { StructuredData } from "fumadocs-core/mdx-plugins"; + +/** + * 定义可以被解析为日期的宽松类型 + */ +export type DateLike = string | number | Date | undefined | null; + +/** + * 定义用于 page.data 的基础类型, + * 包含 Fumadocs 自动生成的字段以及常见的前置元数据 (frontmatter)。 + */ +export interface PageData { + title?: string; + description?: string; + date?: DateLike; + updated?: DateLike; + updatedAt?: DateLike; + lastUpdated?: DateLike; + draft?: boolean; + hidden?: boolean; + docId?: string; + lang?: string; + structuredData?: StructuredData; + load?: () => Promise<{ structuredData: StructuredData }>; + /** + * 允许访问 frontmatter 原始对象(Fumadocs 默认会将字段打平到 data 根部, + * 但部分逻辑可能仍显式访问 .frontmatter)。 + */ + frontmatter?: { + title?: string; + description?: string; + date?: DateLike; + updated?: DateLike; + updatedAt?: DateLike; + lastUpdated?: DateLike; + draft?: boolean; + hidden?: boolean; + docId?: string; + lang?: string; + [key: string]: any; + }; + /** + * 允许通过索引访问其他动态属性 + */ + [key: string]: any; +} diff --git a/lib/search-index.ts b/lib/search-index.ts index f0af15f..c76198c 100644 --- a/lib/search-index.ts +++ b/lib/search-index.ts @@ -4,26 +4,16 @@ import type { AdvancedIndex } from "fumadocs-core/search/server"; import type { StructuredData } from "fumadocs-core/mdx-plugins"; import { source } from "@/lib/source"; import { basename, extname } from "path"; +import { type PageData } from "@/app/types/doc"; type Page = ReturnType[number]; -/** - * fumadocs page.data 在构建产物里的 runtime shape。 - * 老路径:structuredData 直接 inline;新路径:通过 load() 异步拉。 - */ -interface PageDataShape { - structuredData?: StructuredData; - load?: () => Promise<{ structuredData: StructuredData }>; - title?: string; - description?: string; -} - /** * 把一个 fumadocs 页面转成 Orama 索引项(复用 fumadocs-core 默认实现逻辑), * 单独抽出来是因为我们需要分片(zh / en),用 createSearchAPI 手动传 indexes。 */ export async function pageToIndex(page: Page): Promise { - const data = page.data as PageDataShape; + const data = page.data as PageData; let structuredData: StructuredData | undefined; if (data.structuredData) { @@ -52,6 +42,6 @@ export async function pageToIndex(page: Page): Promise { * 翻译版 frontmatter 会声明 `lang: "en"` 且通常 `translatedFrom: "zh"`。 */ export function isEnglishPage(page: Page): boolean { - const lang = (page.data as { lang?: string }).lang; + const lang = (page.data as PageData).lang; return lang === "en"; } From 0221ac9ed364d3246b7574df25ed1b02c827e4dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:29:12 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(types):=20PageData=20=E5=B9=B2=E6=8E=89?= =?UTF-8?q?=E9=A1=B6=E5=B1=82=20any=20index=20signature=20=E8=AE=A9=20buil?= =?UTF-8?q?d=20lint=20=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #323 build 挂在 lint:app/types/doc.ts 两处 [key: string]: any 触发 @typescript-eslint/no-explicit-any error。 修法权衡 - frontmatter 嵌套对象:any → unknown(真的动态字段,调用方需显式 narrow) - PageData 顶层:直接干掉 [key: string]: any 索引签名 为什么不是 unknown:试过 unknown,所有 5 处 `as PageData` cast 都炸 TS2352 "neither type sufficiently overlaps" —— Fumadocs 的 page.data 由 zod DocOut 推出,本身没有 index signature;PageData 顶层挂一个就跟 zod 类型形成"对方有但我没有"的不兼容。要么所有 cast 改 `as unknown as PageData` (等于把 escape hatch 散到 5 处),要么干脆不挂顶层 index signature (escape hatch 收窄到一处 frontmatter 子对象)。后者更干净。 调用方(page.tsx / sitemap.ts / search-index.ts)的现有访问字段 (docId / lang / draft / hidden / DateLike 候选)都已经在 PageData 顶层 显式声明,去掉 index signature 不影响任何现有代码,tsc + lint 均过。 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- app/types/doc.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/types/doc.ts b/app/types/doc.ts index 6f0f20b..557c216 100644 --- a/app/types/doc.ts +++ b/app/types/doc.ts @@ -37,10 +37,10 @@ export interface PageData { hidden?: boolean; docId?: string; lang?: string; - [key: string]: any; + [key: string]: unknown; }; - /** - * 允许通过索引访问其他动态属性 - */ - [key: string]: any; + // 故意不挂顶层 [key: string]: unknown 索引签名 —— Fumadocs 的 page.data 由 + // zod DocOut 推出,没有 index signature;如果在 PageData 上挂一个,as PageData + // 会触发 TS2352 "neither type sufficiently overlaps"。所有需要的字段都已 + // 在上面显式声明;真要拓展加新字段,往这里加显式 ? 字段,别走 escape hatch。 }