Skip to content

Commit 25a076e

Browse files
fix(proxy): Copilot CR — 抽共享模块 + Map 防原型链污染
1. 抽 convertSlugToPinyin 到 lib/leetcode-slug.ts,lib/source.ts 和生成脚本 共用同一实现,消除双点维护(Copilot CR comment #1) 2. proxy.ts 里把 SLUG_MAP 从 plain object 换成 Map,实测 __proto__/constructor 等原型链 key 会被错误命中(拿到 [object Object] / Object 构造函数), Map 天然隔离原型链(Copilot CR comment #2) 3. 脚本从 .mjs 改为 .mts 以便 import TypeScript 共享模块,用 Node 24 的 原生 TS 支持执行,prebuild 走 node 不走 tsx(tsx v4.21 在 Node 24 下 会把 TS 转成 CJS 导致命名 ESM 导出丢失)
1 parent 45a5326 commit 25a076e

5 files changed

Lines changed: 44 additions & 44 deletions

File tree

lib/leetcode-slug.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { pinyin } from "pinyin-pro";
2+
3+
/**
4+
* 把 leetcode 目录下含中文的文件名 / slug 片段转成纯 ASCII 拼音 slug。
5+
*
6+
* 为什么抽出来独立成文件:
7+
* 1. 运行时 (`lib/source.ts` 里的 transformer) 要用它把 Fumadocs 预生成的 slugs 拼音化
8+
* 2. 构建时 (`scripts/generate-leetcode-slug-map.mts`) 要用它生成「中文 → 拼音」字面映射
9+
* 给 proxy.ts 做 301 查表
10+
* 两处算法必须完全一致,否则 301 跳过去找不到页面。复制粘贴迟早忘记同步,
11+
* 因此抽成唯一真源。
12+
*
13+
* 规则:
14+
* - 无中文 → 原样返回(保留纯英文 slug 的连字符、数字等)
15+
* - 有中文 → 拼音化 + 去掉所有非 `[a-z0-9]` 字符,再用 `-` 连接
16+
*/
17+
export function convertSlugToPinyin(text: string): string {
18+
// Fumadocs 内部的 slugs 可能被 encode 过(%E6%BC...),先 decode 再判断汉字
19+
const decodedText = decodeURIComponent(text);
20+
if (!/[\u4e00-\u9fa5]/.test(decodedText)) return text;
21+
22+
return pinyin(decodedText, {
23+
toneType: "none",
24+
type: "array",
25+
nonZh: "consecutive",
26+
})
27+
.map((t) => t.toLowerCase().replace(/[^a-z0-9]/g, ""))
28+
.filter(Boolean)
29+
.join("-");
30+
}

lib/source.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,6 @@
11
import { docs } from "@/.source";
22
import { loader, getSlugs } from "fumadocs-core/source";
3-
import { pinyin } from "pinyin-pro";
4-
5-
// 拼音转换工具,仅针对中文部分转换,保留原本的标点和数字
6-
function convertSlugToPinyin(text: string) {
7-
// 核心修复点:Fumadocs 内部生成的 slugs 可能是被 encode 处理过的(%E6%BC...),需要先解码再判断汉字
8-
const decodedText = decodeURIComponent(text);
9-
10-
if (!/[\u4e00-\u9fa5]/.test(decodedText)) return text;
11-
12-
return pinyin(decodedText, {
13-
toneType: "none",
14-
type: "array",
15-
nonZh: "consecutive",
16-
})
17-
.map((t) => t.toLowerCase().replace(/[^a-z0-9]/g, "")) // 进一步清理非字母数字字符
18-
.filter(Boolean)
19-
.join("-");
20-
}
3+
import { convertSlugToPinyin } from "./leetcode-slug";
214

225
export const source = loader({
236
baseUrl: "/docs",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"dev": "next dev",
7-
"prebuild": "node scripts/escape-angles.mjs && tsx scripts/generate-leaderboard.mjs && node scripts/generate-leetcode-slug-map.mjs",
7+
"prebuild": "node scripts/escape-angles.mjs && tsx scripts/generate-leaderboard.mjs && node scripts/generate-leetcode-slug-map.mts",
88
"build": "next build",
99
"start": "next start -p 3000",
1010
"test": "vitest run",

proxy.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ import leetcodeSlugMap from "@/generated/leetcode-slug-map.json";
1818
* 为什么不走 next.config 的 redirects:
1919
* path-to-regexp 对方括号 / 空格 / 中文的处理不稳,不如 middleware 字面匹配可靠。
2020
*/
21-
const SLUG_MAP = leetcodeSlugMap as Record<string, string>;
21+
// 用 Map 而不是 plain object,杜绝 __proto__ / constructor 这类原型链 key 被当成命中
22+
// 导致 redirect 目标异常(例如 mapped 返回 Object 构造函数)。
23+
const SLUG_MAP = new Map<string, string>(
24+
Object.entries(leetcodeSlugMap as Record<string, string>),
25+
);
2226
const LEETCODE_NEW_BASE = "/docs/career/interview-prep/leetcode";
2327
const LEETCODE_OLD_BASE = "/docs/CommunityShare/Leetcode";
2428

@@ -47,7 +51,7 @@ function redirectLeetcodeIfNeeded(req: NextRequest): NextResponse | null {
4751
rawSlug = rest;
4852
}
4953

50-
const mapped = SLUG_MAP[rawSlug];
54+
const mapped = SLUG_MAP.get(rawSlug);
5155
const targetSlug = mapped ?? rawSlug;
5256

5357
// 新路径 + ASCII slug 命中原样:放行,不绕圈
Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
* proxy.ts (Next 16 middleware) 要在 edge 端 O(1) 查表把旧 URL 301 到正确拼音路径,
1010
* 又不能把 pinyin-pro 的整本字典塞进 edge bundle,所以构建时先把映射固化成 JSON。
1111
*
12-
* 生成规则必须和 lib/source.ts 的 convertSlugToPinyin 完全一致,否则链接对不上。
12+
* 算法从 lib/leetcode-slug.ts 里 import,运行时和构建时共用同一实现,
13+
* 消除双点维护。脚本必须用 tsx 执行(见 package.json prebuild)。
1314
*/
1415
import fs from "node:fs";
1516
import path from "node:path";
1617
import { fileURLToPath } from "node:url";
17-
import { pinyin } from "pinyin-pro";
18+
import { convertSlugToPinyin } from "../lib/leetcode-slug.ts";
1819

1920
const __filename = fileURLToPath(import.meta.url);
2021
const __dirname = path.dirname(__filename);
@@ -25,31 +26,13 @@ const LEETCODE_DIR = path.join(
2526
);
2627
const OUTPUT_FILE = path.join(PROJECT_ROOT, "generated/leetcode-slug-map.json");
2728

28-
/**
29-
* 与 lib/source.ts 中 convertSlugToPinyin 保持同步。
30-
* 入参:单个 slug 片段(一般是文件名 stem)。
31-
* 无中文直接原样返回;有中文则按拼音 + 非字母数字清洗 + 连字符拼接。
32-
*/
33-
function convertSlugToPinyin(text) {
34-
const decodedText = decodeURIComponent(text);
35-
if (!/[\u4e00-\u9fa5]/.test(decodedText)) return text;
36-
return pinyin(decodedText, {
37-
toneType: "none",
38-
type: "array",
39-
nonZh: "consecutive",
40-
})
41-
.map((t) => t.toLowerCase().replace(/[^a-z0-9]/g, ""))
42-
.filter(Boolean)
43-
.join("-");
44-
}
45-
4629
/**
4730
* 从文件名去掉 locale / 扩展名后缀,还原 Fumadocs 会当 slug 的 stem。
4831
* 2309兼具大小写的最好英文字母_translated.md → 2309兼具大小写的最好英文字母_translated
4932
* 2241-design-an-atm-machine.zh.md → 2241-design-an-atm-machine
5033
* [146]LRU 缓存_translated.md → [146]LRU 缓存_translated
5134
*/
52-
function stripSuffix(filename) {
35+
function stripSuffix(filename: string): string {
5336
let stem = filename.replace(/\.(md|mdx)$/i, "");
5437
stem = stem.replace(/\.(en|zh)$/i, "");
5538
return stem;
@@ -65,8 +48,8 @@ function main() {
6548
.readdirSync(LEETCODE_DIR)
6649
.filter((f) => /\.(md|mdx)$/i.test(f));
6750

68-
const map = {};
69-
const collisions = [];
51+
const map: Record<string, string> = {};
52+
const collisions: { stem: string; existing: string; incoming: string }[] = [];
7053

7154
for (const file of files) {
7255
const stem = stripSuffix(file);

0 commit comments

Comments
 (0)