@@ -18,44 +18,60 @@ if (!["serve", "build"].includes(mode)) {
1818}
1919
2020/**
21- * MkDocs docs_dir=docs:页面内相对链接不应再带 ./docs/ 前缀。
22- * 把 README(仓根路径)原样贴进 docs/*.md 会导致站内导航全断,且默认
23- * unrecognized_links=ignore 时 CI 不会拦。在此做契约检查。
21+ * MkDocs docs_dir=docs:手写页相对链接解析后必须落在 docs/ 内。
22+ * 同时拦住两类误用(单一权威规则,避免再为每种前缀打洞):
23+ * 1) README 仓根路径误贴:](./docs/guide/…) → 站内变成双重 docs/
24+ * 2) 指向仓内其它目录:](../../sfmc/…) → Pages 上 404;应改 GitHub 绝对 URL
25+ * TypeDoc 生成目录跳过;mkdocs exclude(plan/archive/reviews)仍检查,防草稿误贴。
2426 */
25- function assertNoRepoRootDocsLinks ( docsDir ) {
27+ function assertDocsRelativeLinksStayInDocs ( docsDir ) {
2628 const bad = [ ] ;
29+ const linkRe = / \] \( ( [ ^ ) \s ] + ) (?: \s + " [ ^ " ] * " ) ? \) / g;
2730 const stack = [ docsDir ] ;
2831 while ( stack . length ) {
2932 const dir = stack . pop ( ) ;
3033 for ( const ent of readdirSync ( dir , { withFileTypes : true } ) ) {
3134 const full = path . join ( dir , ent . name ) ;
3235 if ( ent . isDirectory ( ) ) {
33- // TypeDoc 输出目录跳过;plan/archive 由 mkdocs exclude
3436 const rel = path . relative ( docsDir , full ) . replace ( / \\ / g, "/" ) ;
35- if ( rel === "reference/sdk" || ent . name === "plan" || ent . name === "archive" ) continue ;
37+ // 仅跳过 TypeDoc 输出(生成物,非手写契约)
38+ if ( rel === "reference/sdk" ) continue ;
3639 stack . push ( full ) ;
3740 continue ;
3841 }
3942 if ( ! ent . name . endsWith ( ".md" ) ) continue ;
4043 const text = readFileSync ( full , "utf8" ) ;
41- // Markdown 链接目标:](./docs/...) 或 ](docs/...)
42- if ( / \] \( \. ? \/ ? d o c s \/ / . test ( text ) ) {
43- bad . push ( path . relative ( root , full ) ) ;
44+ const fileRel = path . relative ( root , full ) . replace ( / \\ / g, "/" ) ;
45+ let m ;
46+ linkRe . lastIndex = 0 ;
47+ while ( ( m = linkRe . exec ( text ) ) ) {
48+ const raw = m [ 1 ] . replace ( / ^ < | > $ / g, "" ) ;
49+ // 锚点 / 协议链接 / 协议相对 URL 不参与 docs_dir 解析
50+ if ( ! raw || raw . startsWith ( "#" ) || / ^ [ a - z ] [ a - z 0 - 9 + . - ] * : / i. test ( raw ) || raw . startsWith ( "//" ) ) {
51+ continue ;
52+ }
53+ const targetPath = raw . split ( "#" ) [ 0 ] . split ( "?" ) [ 0 ] ;
54+ if ( ! targetPath ) continue ;
55+ const resolved = path . resolve ( path . dirname ( full ) , targetPath ) ;
56+ const relToDocs = path . relative ( docsDir , resolved ) ;
57+ if ( relToDocs . startsWith ( ".." ) || path . isAbsolute ( relToDocs ) ) {
58+ bad . push ( `${ fileRel } : ](${ raw } )` ) ;
59+ }
4460 }
4561 }
4662 }
4763 if ( bad . length ) {
4864 console . error (
4965 [
50- "[docs-mkdocs] 发现仓根相对路径 docs/…(MkDocs 下应写成 ./guide/、./dev/ 等 ):" ,
51- ...bad . map ( ( f ) => ` - ${ f } ` ) ,
66+ "[docs-mkdocs] 相对链接逃出 docs/(站内应写 ./guide/ 等;仓外目标用 GitHub 绝对 URL ):" ,
67+ ...bad . map ( ( line ) => ` - ${ line } ` ) ,
5268 ] . join ( "\n" )
5369 ) ;
5470 process . exit ( 1 ) ;
5571 }
5672}
5773
58- assertNoRepoRootDocsLinks ( path . join ( root , "docs" ) ) ;
74+ assertDocsRelativeLinksStayInDocs ( path . join ( root , "docs" ) ) ;
5975
6076// 先生成 API 文档
6177const gen = spawnSync ( process . execPath , [ path . join ( root , "tools" , "docs-typedoc.mjs" ) ] , {
0 commit comments