Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

65 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Docusaurus 主题覆盖验证仓库(pnpm workspace)

使用 pnpm + Docusaurus 初始化的仓库,用于验证主题覆盖机制,并给出「多主题链式覆盖」的可落地方案。

这份 README 只管仓库级的事实:怎么跑、有哪些分包、什么东西放在哪。 每个方案与包的细节在它自己的 README 里,这里不抄第二份。

快速开始

pnpm install
pnpm start          # 本地预览 http://localhost:3000
pnpm build          # 编译需要编译的分包 + 构建静态站点
pnpm build:core     # 只用 tsc 编译那九个「构建包」(核心包 + theme/theme-* 那八个主题包)到各自的 dist(即包出口);theme/theme-iframe 的 lib/ 不在里面,那份共享源码不发编译产物
pnpm lint:type      # tsc -p tsconfig.json:唯一把站点根与 *.config.mjs 纳进来的一道(提交钩子也跑)
pnpm typecheck      # tsc -b:demo 与 test 两份 composite 项目
pnpm lint:eslint    # eslint
pnpm test           # vitest 单元测试(解析语义 + 不变量 + 输入校验 + 真实 resolver 端到端)
pnpm test:coverage  # 同上 + 覆盖率(红线 100% 只挂在这里:pnpm test 全绿 ≠ CI 全绿)
pnpm test:e2e       # 构建产物起服务,Playwright 在真浏览器里点开每个主题的示例页(只本地跑)
pnpm verify:chain   # 读真实 build 产物,回读主页那条四层渲染链(src/theme > theme-c > theme-b > theme-a)
pnpm verify         # 一键全链路(组成见 package.json 的 verify;本表不复述,避免两处说法漂移)

Node 版本要求以 package.json 的 engines 为准(当前 ^24.0.0);CI 另外跑 current / lts/* / lts/-1 (见 .github/workflows/bring-it.yaml)。 这条下界的来历是「跟着最老的受支持运行时」:@types/node 与 engines 钉在同一档, 「只在新版才有的 API」才不会在 CI 里悄悄绿。当前 @types/node 是 ^22.18.0,比 engines 低一档。

站点根为什么不是 ESM 包

站点根 package.json 刻意不写 "type": "module",配置用 .mjs 扩展名拿 ESM。 这是修 .docusaurus/ 里 require.resolveWeak 报错的唯一产地:

webpack 按「扩展名 + 最近 package.json 的 type」给模块判型。根上写了 "type": "module", Docusaurus 生成的 .docusaurus/*.js(CJS 风格)就会被判成 javascript/esm, require.resolveWeak(...) 不被改写、原样进产物,而 SSG 注入的 require 没有它 —— pnpm build 当场死在 TypeError: require.resolveWeak is not a function。

这不是本仓库代码的问题(零自定义插件的最小站点 + "type": "module" 同样红)。 不该为它给 webpack 打判型补丁(那会让所有源码被降级成 javascript/auto,import 语义变了 比原报错更难查),也不该让源码为它改回 CJS —— 去掉根上的 "type" 才是对准根因: .docusaurus/ 是构建中间产物,模块格式该由生成它的代码(CJS 风格)决定。

实际落点只有一处,靠扩展名而不是 "type":

文件 为什么是 .mjs
docusaurus.config.mjs Docusaurus 按 .mjs 加载配置;无 "type" 时 .js 会被当 CJS

分包

仓库里的分包按「src/ 要不要先编译」分两种形态,不是按「像不像一个包」:

目录 是什么 形态 细节
plugins/plugin-prev-layer-resolver/ 核心包:@theme-prev/X 解析器 + 层栈自动发现 构建包(src/ → dist/,改完要编译) README · internals
theme/theme-markmap/ 主题包:md 的 frontmatter 写 markmap: true 即在正文上方渲染导图 构建包(只编译入口与 loader;层与组件是源码) README · 示例页
theme/theme-erd/ 主题包:.dbml 文件在正文上方渲染数据库关系图,正文附上固定四种格式加一串由插件选项决定的导出模板 构建包(只编译入口、清单判据与 loader;层与组件是源码) README · internals · 示例页
theme/theme-diff/ 主题包:md 的 frontmatter 写 diff: true(或一个路径)即在正文上方渲染这份文件最近一次提交的差异视图;写成 diff: {old: 旧, new: 新} 时比的是磁盘上那两份文件,不读 git 历史 构建包(只编译入口与 loader;层与组件是源码) README · internals · 示例页 · 两份对照示例页
theme/theme-iframe/ 主题包,也是「整页交给一个框」这一族唯一的外层套壳:判四把 frontmatter 键,命中就把那一页换成一个通用框;lib/ 里另住着三条 API 层共用的跨 iframe 约定与配置期工厂 构建包(同上,src/ 里只有入口那一份;lib/ 免编译) README · internals · 协议
theme/theme-stoplight/ 主题包:md 的 frontmatter 写 oas3: <URL> 时整页换成一个 iframe(外层那格由 theme-iframe 承接),框里是本包自己那条 /stoplight-view 页面,Elements 在那份文档里渲染 API 文档 构建包(同上,src/ 里只有入口那一份,且是调工厂的一次) README · internals
theme/theme-redoc/ 主题包:md 的 frontmatter 写 redoc: <URL> 时整页换成一个 iframe(外层那格由 theme-iframe 承接),框里是本包自己那条 /redoc-view 页面,Redoc 在那份文档里渲染 API 文档 构建包(同上,src/ 里只有入口那一份,且是调工厂的一次) README · internals
theme/theme-scalar/ 主题包:md 的 frontmatter 写 scalar: <URL> 时整页换成一个 iframe(外层那格由 theme-iframe 承接),框里是本包自己那条 /scalar-view 页面,Scalar 在那份文档里渲染 API 文档 构建包(同上,src/ 里只有入口那一份,且是调工厂的一次) README · internals
theme/theme-viewer/ 主题包:文档正文里有图片时,那些图片交给查看器 —— 点开放大、原尺寸、重置、前后切换;没有图片的页面这一层等于透明 构建包(同上,src/ 里只有入口那一份) README · internals · 示例页
demo/theme-{a,b,c}/ 验证 / 调研用主题插件(不属于核心包) 免编译(没有 src/:入口与层都是源码,站点直接吃) README

九个构建包都是 tsc 编译包:Node 侧要跑的源码 .mts 在包内 src/,产物在包内 dist/, dist/index.mjs 就是包出口(exports)—— 所以 pnpm build:core 是「编译这九个包」, pnpm start / pnpm build / pnpm test 都已经把它内置在前面。

主题包的主题层与组件不进这份编译:theme/*/theme/** 与 theme/*/components/** 是未编译的 .tsx / .jsx,随包发布,由用它们的站点的 babel 编译 —— 判据只有一条:谁编译,就不编译第二遍。 这一步不会自动成立:Docusaurus 官方那条 babel 规则把 node_modules 整个排除在外(只给 docusaurus*.js 开例外),所以每个主题包的入口都在 configureWebpack 里用官方 utils.getJSLoader() 补一条只覆盖本包根的 .tsx? 规则(下表「层与组件为什么不编译」)。 demo/theme-*/ 是这个形态的完整版,连入口 index.mts 都由 Docusaurus 的 jiti 直接加载。 theme/theme-iframe/lib/ 里那一份(注册表、判定清单的分发、跨 iframe 共享约定、配置期工厂)是同一判据推到头的 另一格:它没有编译产物 —— 浏览器那几份由用它的包直接引源码;注册表是纯数据,配置期与浏览器期 引的是同一份;唯一跑在 Node 配置期的 api-frame-theme.mts 由 Docusaurus 的 jiti 直接吃这份 源码(.mts 后缀按上游 lint preset 标 Node 环境,与 src/index.mts 同一口径)。exports 用 ./lib/* 把源码交给用它的包,build:core 编译本包时也只产入口那一份。

theme/theme-iframe/、theme/theme-stoplight/、theme/theme-redoc/ 与 theme/theme-scalar/ 已接进站点 (plugins 里的顺序 = @theme/ 层栈的覆写顺序,四者都在 theme-markmap 往外那一侧), 接入位置、触发键与各自验证到哪一步以各包维护者文档 iframe、 stoplight、 redoc、 scalar 为准,用法与限制在各自的 README,这里不抄第二份。 四把键的判定顺序由 theme-iframe 那一层决定(口径在 iframe-protocol.md 的「注册表与分发」一节); theme/theme-iframe/lib/ 不在 plugins 的重名层里 —— 它不是插件也不是主题层,被三条 API 层 当普通依赖用(版本契约写在各自的 peerDependencies,口径在 theme-iframe 的 internals)。插件身份只属于 theme-iframe 包本身。

构建包的编译口径

九份 tsconfig.build.json 是同一份差量:extends: @nice-move/tsconfig/node-bundle.json, 再写 noEmit: false 与 rootDir / outDir,能从 extends 拿的一律不重抄。这张表是这些口径的产地, 包里的注释只留指针:

口径 内容
编译范围 只有 src/,而 src/ 里只放 Node 侧要跑的代码:核心包的解析器、主题包的入口与 webpack loader、类型。JSX 一份都不进 tsc。
层与组件为什么不编译 用它们的站点会编译,编译两遍只是多出一份要 files 点名、会和源码漂移的产物。但「站点会编译」要主动接上:官方 babel 规则按 excludeJS 排除整个 node_modules、只给 docusaurus*.js 开例外,从 npm 装进真项目时未编译的 .tsx 会报 Module parse failed。因此每个主题包的入口都用官方 utils.getJSLoader({ isServer }) 补一条只覆盖本包根的 .tsx? 规则(三条 API 层经 lib/api-frame-theme.mts 的工厂统一补);工作区里 pnpm 的软链把源码解析到 node_modules 之外,两条规则各跑一遍 babel,产物不变。实测根据:demo/theme-*/theme/** 一直是未编译的 .tsx,站点收进层栈照样绿 —— pnpm verify:chain 打印的层栈里就有那一格。
让 tsc 编 JSX 要付什么 多一条产物口径:react.json 给的 react-jsx 会把 JSX 编成 .js,而包是 "type": "module",服务端构建里 babel 先把 import() 变成 require('./x.js') —— 纯 ESM 模块的 parser 不认 require,依赖进不了 bundle,静态渲染阶段报 Cannot find module;要躲它得单独钉 jsx: "preserve"。少编译一层就少一条这种口径。
files 点名与 docs/ 发布只点站点运行时与类型要吃的格子(dist、theme、components、lib、types),各包的 docs/(维护者文档与那份共享约定)不进 files —— 实现记录留在仓库里读,代价与判据见 theme-iframe 的编译口径。
相对 import 统一写源码名(./x.mts / ./x.tsx)。编译过的由 node-bundle.json 的 rewriteRelativeImportExtensions 在产出时改成产物名(./x.mjs);未编译的那份原样交给 webpack,磁盘上就是这个文件,所以两种写法都指得到。allowImportingTsExtensions 不用写 —— base.json 已给。
样式表 不是模块,tsc 也不搬:theme-markmap 与 theme-viewer 那两份留在包根 styles/,走 getClientModules()(viewer 那一份连 viewerjs 自带的样式表一起从这一格交给站点,只是那份写的是 CSS 原生嵌套,得先由该包入口的 configurePostCss() 摊平,口径见它包内 docs/internals.md 的「第三方那份样式表要补的两处」一节);theme-diff 自己没有样式表,只把 @git-diff-view/react 那份第三方样式表从同一格交给站点;iframe 那份 CSS Module 直接放在用它的组件旁边。第三方那份静态样式表(@stoplight/elements)不进这条流水线:由 theme-stoplight 的入口在构建配置期把它从「打进全站那一份 css」改成「单独一个产物」,只由框内那份文档 <link>(口径见它包内 docs/internals.md 的「样式」一节)。
未编译源码的类型检查 由仓库根 tsconfig.json 接手(默认 include **/*,types 同时列 node 与 react / react-dom)。所以构建项目不再需要 jsx、不需要 react 那两条 types。@theme* 那三条 specifier 的声明归解析器包(plugins/plugin-prev-layer-resolver/types/),由 tsconfig.demo/test 列进 files。*.module.css、@docusaurus/router 这类只在构建期存在的依赖,由仓库根一份 module-aliases.d.ts 引用官方 @docusaurus/module-type-aliases 说明(根 tsconfig.json 的 include 收得到它,tsconfig.test.json 按 files 点名)。

其余目录不是包,只是仓库根的一部分:

目录 职责 细节
docs/ 站点文档源:intro.md 就是主页(卡片列表),每个主题包一份示例页(*-demo.md,ERD 是 erd-demo.dbml) docs/intro.md
test/ 核心包单元测试(按契约面组织) README
src/ 只剩站点层 swizzle src/theme/(主页由 docs 承担,站点侧没有页面层) README
static/ 原样复制到站点根的静态文件(三条 API 示例页各取一份 OpenAPI 文档,图片查看器示例页那两张图在 img/) —

上表的 plugins/*、theme/*、demo/* 是 pnpm workspace 包(各自有 package.json, 也在 pnpm-workspace.yaml 的 packages: 里);其余是仓库根的目录,不是包 —— 判据是 「有没有 package.json」。

分包要被解析,判据是用它的那个 importer 声明了它:packages: 里收了、目录里有 package.json 都不算,站点根不写 "@show-docs/theme-a": "workspace:^",pnpm 就不会建链接, 一路顺到 docusaurus build 才炸 Cannot find module。test/plugin.test.mts 有一条用例 把核心包与 demo 的三个主题按 Node 的 ESM 规则解析一遍,把这一步提前到 pnpm test (清单里的 theme/* 还没进那条用例的目录列表)。

方案在哪份文档里

  • 多主题链式覆盖(@theme-prev/X 怎么解析、怎么接入、有哪些约束): plugins/plugin-prev-layer-resolver/README.md; 机制为什么这样设计、实测与内部契约清单在同包 docs/internals.md
  • 导图主题(frontmatter 触发、渲染位置、明暗跟随全站主题): 行为见 docs/markmap-demo.md,用法见 theme/theme-markmap/README.md;实现见 theme/theme-markmap/(入口与 loader 在 src/,层与组件在 theme/、components/)
  • 关系图主题(.dbml 触发、渲染位置、正文里附哪些导出、那些导出由谁生成): 行为见 docs/erd-demo.dbml,用法见 theme/theme-erd/README.md;实现(入口、清单判据与 loader 在 src/,层与组件在 theme/、components/,@dbml/core 实测到的形状与导出模板插拔在实现上的 三个约束)在同包 docs/internals.md
  • 差异视图主题(frontmatter 触发、比的是哪一份或哪两份、取不到差异时怎么办): 行为见 docs/diff-demo.md 与 两份文件对照那一页,用法见 theme/theme-diff/README.md;实现(入口与 loader 在 src/, 层与组件在 theme/、components/,git 那几步与取不到差异的几种落点)在同包 docs/internals.md
  • iframe 主题(frontmatter 触发、URL 怎么算、在层栈里放哪一格、命中时丢掉的是哪一块): 行为见 docs/iframe-demo.md,用法见 theme/theme-iframe/README.md;实现与包内组成在同包 docs/internals.md,跨 iframe 的口径在 iframe-protocol.md
  • Stoplight 主题(oas3: 触发、渲染为什么挪到本包自己那条 /stoplight-view 页面): 行为见 docs/stoplight-demo.md(示例取 static/openapi-stoplight.yaml),用法见 theme/theme-stoplight/README.md;实现(含那份静态样式表 怎么只出现在框内那一页、浏览器 bundle 要补哪几样 Node 侧依赖)在同包 docs/internals.md
  • Redoc 主题(redoc: 触发、渲染为什么挪到本包自己那条 /redoc-view 页面): 行为见 docs/redoc-demo.md(示例取 static/openapi-redoc.yaml),用法见 theme/theme-redoc/README.md;实现(含样式为什么不需要构建期 处理而暗色表仍要在运行期改挂)在同包 docs/internals.md
  • Scalar 主题(scalar: 触发、渲染为什么挪到本包自己那条 /scalar-view 页面): 行为见 docs/scalar-demo.md(示例取 static/openapi-scalar.yaml),用法见 theme/theme-scalar/README.md;实现(含它的静态样式表为什么 拦不住而只能靠 .scalar-app 那层前缀、亮暗为什么落在 <body> 那两个类名上)在同包 docs/internals.md
  • 图片查看器主题(这一层没有开关,判的是正文里有没有图片;查看器接管的是哪一格): 行为见 docs/viewer-demo.md(示例取 static/img/ 那两张图),用法见 theme/theme-viewer/README.md;实现(入口在 src/,层与组件在 theme/、components/,两处样式收在 styles/,第三方那份样式表要补的那两处)在同包 docs/internals.md
  • 把渲染器关进一个框之后要对齐的那两格(地址栏里 # 那一段与站点的亮暗模式,两边怎么跨文档 送、为什么要各推一次): theme/theme-iframe/docs/iframe-protocol.md —— 上面三条 API 层共用同一份实现
  • 链是否真的成立:pnpm build && pnpm verify:chain,判据清单以脚本输出为准
  • 每个主题在浏览器里是否真的工作:pnpm test:e2e(跑的是构建产物,只本地跑)。用例是 Given/When/Then 句子(Gherkin 写在 test/e2e/features/,步骤实现在 test/e2e/steps/), 用例清单与首次要装的浏览器见 test/README.md

目录结构

「职责」比「位置」重要 —— 放错地方的代价不是不好看,而是行为变了(把 specifier 写在层外文件里 就是硬失败,不是「有时灵有时不灵」)。先按「你想改什么」找,再在对应文档里看细节。

你想改什么 动哪里 细节
层栈怎么推导、specifier 怎么解析 plugins/plugin-prev-layer-resolver/ README · internals
导图怎么触发、渲染在哪 theme/theme-markmap/ README · 示例页
关系图怎么触发、渲染在哪 theme/theme-erd/ README · internals · 示例页
上一次改动怎么看 theme/theme-diff/ README · internals · 示例页 · 两份对照示例页
整页交给外部站点或外部文档 theme/theme-iframe/(跨 iframe 那两格在同包 lib/ 与 docs/iframe-protocol.md)、theme/theme-stoplight/、theme/theme-redoc/、theme/theme-scalar/ —— 四包的内部实现与实测记录在各包 docs/internals.md 示例页 · stoplight 示例页 · redoc 示例页 · scalar 示例页 · iframe · 跨 iframe 约定 · stoplight · redoc · scalar
正文图片怎么被接管 theme/theme-viewer/ README · internals · 示例页
某层包在组件外面的样子 demo/theme-{a,b,c}/theme/、src/theme/ demo · src/theme
验证「链真的成立」 demo/scripts/verify-chain.mts demo
站点主页与文档 docs/(intro.md 带 slug: /,路由前缀在配置里挪到根) docs/intro.md
单测、浏览器用例与夹具 test/(e2e 的句子在 test/e2e/features/,实现在 test/e2e/steps/) README
@theme* 的类型声明 plugins/plugin-prev-layer-resolver/types/ README「类型声明」

仓库级只有这些不在分包里的东西:

dist/.tsbuildinfo/                 # `tsc -b` 的增量信息(git 忽略);构建包的产物在各包自己目录里
tsconfig.json                      # 共享编译选项(noEmit);demo / test 各一份 extends,八个构建包在包内自带 tsconfig.build.json
tsconfig.demo.json / tsconfig.test.json  # 两份 composite 项目,由 pnpm typecheck(tsc -b)跑
docusaurus.config.mjs              # 站点配置(.mjs:站点根不是 ESM 包,靠扩展名拿 ESM;侧栏由 docs 插件按目录自动生成;docs 的路由前缀是 '/',所以主页就是 docs/intro.md)
pnpm-workspace.yaml                # pnpm 12 的配置落点:分包清单(packages:)+ 安装期依赖(configDependencies)+ 依赖安装脚本放行名单(allowBuilds)+ 扁平化(shamefullyHoist)
eslint.config.mjs / syncpack.config.mjs / vitest.config.mts / playwright.config.mts  # 四道检查各自的落点(后者同时是 BDD 的落点:`defineBddConfig()` 说清 `.feature` 与步骤在哪,`testDir` 由它给出)
.features-gen/                       # `bddgen` 从 `.feature` 生成的 Playwright 文件(git 忽略,别手改)
.github/workflows/                 # CI(GitHub Actions)

关于 src/:它现在只有主题层那一种东西 —— src/theme/ 是层栈最外圈的哨兵槽位。 站点不再有自己的页面层:主页就是 docs/intro.md(配置把 docs 的路由前缀挪到根,那份 frontmatter 写 slug: /)。历史上 src/pages/ 在的那一格讲的是「specifier 写在层外文件里 必定硬失败」,那条口径的产地在 插件 README 的使用约束①。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages