
用 dependency-cruiser 為 TypeScript 倉庫強制實施 Deep Modules入口文件邊界的完整落地指南【免費下載鏈接】skillsSkills for Real Engineers. Straight from my .agents directory.項目地址: https://gitcode.com/GitHub_Trending/skills13/skills本指南基于skills/in-progress/setup-ts-deep-modules/SKILL.md及其配套的 dependency-cruiser.config.cjs講述如何把 TypeScript 倉庫中的每個包改造成“深模塊”deep module把大量行為隱藏在少量入口文件之后讓包的根目錄文件成為唯一對外通道。讀完本文你將掌握完整的接線流程、四條邊界規則的底層正則原理、如何用一次“故意破壞”驗證規則真正生效以及如何把這一約定固化到 Agent 的工作流中。什么是 Deep Module小接口背后的大行為先建立共享詞匯。本倉庫的 codebase-design 技能為“深模塊”提供了精確的術語體系setup-ts-deep-modules 全程使用這套語言模塊Module任何同時擁有接口與實現的東西刻意與規模無關可以是一個函數、一個類、一個包也可以是一個跨層切片接口Interface調用方正確使用模塊所需知道的一切不只是類型簽名還包括不變量、順序約束、錯誤模式、所需配置與性能特征深度Depth接口處的杠桿率即調用方或測試每學習一份接口所能調用的行為量。深模塊 小接口 大量實現淺模塊 大接口 幾乎空的實現要避免縫Seam接口所坐落的位置可以在不改動原處的情況下改變行為杠桿Leverage調用方從深度中獲得的好處一份實現回饋給 N 個調用點和 M 個測試局部性Locality維護者從深度中獲得的好處變更、缺陷、知識與驗證集中在一處而不是擴散到所有調用方。正如 codebase-design 指出的深度是接口的屬性而非實現的屬性一個深模塊內部可以繼續由許多小而可替換的部件組成它們只是不屬于接口而已。setup-ts-deep-modules 所做的正是把“每個包都是深模塊”這一設計目標變成可被 CI 強制執行的工程約束。本技能強制塑造的目錄形態技能要求倉庫形成如下的統一形態src/packages/ name/ index.ts ← 入口文件公開。外部只能從這里 import。 client.ts ← 另一個入口文件。一個包可以暴露多個入口。 lib/ ← 實現對外隱藏內部文件之間可自由互相 import。 tests/ ← 與代碼同目錄的測試與 fixtures子目錄屬于私有。核心判斷標準有三條公開面 包的根目錄文件而不是某個指定的index.ts按慣例實現放在lib/測試放在tests/讓每個包都有相同的兩文件夾形態規則本身是通用的任何子文件夾里的任何東西都是私有的因此將來新增文件夾時永遠不需要改配置。注意這里明確區分了“入口文件”與“桶文件barrel”入口文件而不是桶文件。因為公開面是每一個根目錄文件一個包可以暴露多個小而精的入口index.ts、client.ts、server.ts而不是把一切塞進一個巨型index.ts。鼓勵保持入口文件小而隱藏實現明確不鼓勵那些把整棵子樹重新導出的 barrel 文件。分層哪些包可以依賴哪些包是一個獨立的關注點配置文件里已為它留好了注釋形式的占位見后文。四條邊界規則全部 error 級別規則一共四條加上一條禁環全部以error級別生效意味著任何違反都會讓lint:boundaries失敗入口邊界Entry-point boundary包外代碼應用代碼或另一個包只允許導入該包的入口文件根目錄文件絕不能觸碰其子文件夾里的任何東西。包內自由Intra-package freedom一個包自己的文件之間可以自由互相導入。測試走入口Tests through the entry pointspkg/tests/下的文件可以導入任意包的入口文件以及自己的tests/fixtures但絕不能導入任何包的子文件夾內部實現包括自己包的。跨包的集成測試沒問題深導入不行。禁環No cycles不允許出現依賴環。第四條規則在配置中以no-circular呈現其余三條分別對應配置中的entrypoint-boundary-from-app、entrypoint-boundary-across-packages、tests-through-entrypoints與tests-folder-is-private。注意實際上配置里是5 條 forbidden 規則除了技能正文列出的四條還多出一條tests-folder-is-private一個包的tests/目錄只允許測試自身訪問防止其他代碼誤導入測試 fixtures。這四條加一條共同構成了完整的邊界體系。七步接線流程技能把整個落地過程拆成七個可驗證的步驟每步都帶明確的 “Done when” 驗收條件。第 1 步探測環境包管理器pnpm-lock.yaml→ pnpmyarn.lock→ yarnbun.lockb→ bun否則 npm。后續所有命令都要用探測到的那個管理器pnpm/yarn/npm run/bunx。包根目錄如果存在src/就用src/packages否則用packages。若倉庫已有明顯不同的慣例應與用戶確認。已有配置檢查是否存在.dependency-cruiser.*文件。若已存在不要覆蓋把四條規則與 options 合并進去并明確告訴用戶你添加了什么。驗收包管理器、包根目錄、已有配置狀態三者都已確定。第 2 步安裝 dependency-cruiser用探測到的包管理器把dependency-cruiser安裝為 devDependency。驗收dependency-cruiser出現在devDependencies中。第 3 步編寫配置把倉庫自帶的 dependency-cruiser.config.cjs 復制到倉庫根目錄并命名為.dependency-cruiser.cjs然后把PACKAGES_ROOT設為第 1 步探測到的根目錄。規則基于路徑深度且與擴展名無關因此除此之外無需任何適配。驗收.dependency-cruiser.cjs存在、PACKAGES_ROOT正確、四條禁止規則齊全。第 4 步接入檢查命令新增lint:boundaries腳本depcruise packages-root或depcruise src。把它并入倉庫已有的總檢查命令那個已經跑 typecheck 的check/ci/validate腳本。不要改動 tsconfig也不要添加路徑別名。如果沒有總檢查腳本就只加lint:boundaries并告知用戶應把它納入 CI。驗收lint:boundaries存在并與 typecheck 在同一條命令中執行。第 5 步搭建示例包在packages-root/example/創建可提交的“復制即用”模板index.ts一個入口文件導出一個委托給內部文件的函數讓包看起來有深度而不是一個透傳殼lib/impl.ts子文件夾中的內部文件被index.ts導入外部不可達tests/example.test.ts只導入../index入口文件針對公開函數做斷言。明確告訴用戶這是一個可復制或刪除的起始模板。驗收示例包存在行為通過根目錄入口暴露impl藏在子文件夾中。第 6 步證明規則真的會咬人這是整個技能的完成標準一個在違規時不報錯的配置毫無價值。操作分三步運行lint:boundaries干凈示例必須通過臨時在tests/example.test.ts里加一個深導入例如import { thing } from ../lib/impl再次運行lint:boundaries必須以tests-through-entrypoints失敗撤銷深導入再運行一次必須通過。驗收觀察到了 通過 → 深導入失敗 → 再通過 的全過程。若第 2 步沒有失敗說明規則沒有正確接線必須修復后才能結束。第 7 步記錄約定并讓 Agent 能發現它在packages 文件夾內packages-root/README.md放在它所管轄的包旁邊寫一個README.md覆蓋src/packages/name/的布局根目錄是入口、lib/是實現、tests/是測試、只通過包的入口文件根目錄文件導入、以及如何運行lint:boundaries。明確反對 barrel 文件寧可暴露多個小入口也不要通過一個 index 重新導出整棵子樹。內容保持在復制即用代碼片段 四條規則各一段的篇幅。然后從倉庫的 Agent 指令文件存在CLAUDE.md就用它否則用AGENTS.md兩者都沒有就新建AGENTS.md中加一個上下文指針。一行就夠例如Packages are deep modules: see [src/packages/README.md](https://link.gitcode.com/i/13e81aed4c1ce9bd479dca9b07ef04fb) before adding or importing one.這就是讓 Agent 主動發現邊界規則、而不是撞上它才后悔的關鍵一步。驗收packages-root/README.md存在且反對 barrel倉庫的CLAUDE.md/AGENTS.md鏈接到了它。配置文件逐行拆解正則如何區分內外倉庫自帶的 dependency-cruiser.config.cjs 是整個方案的引擎值得逐段理解。PACKAGES_ROOT 與派生正則/** Where packages live. One immediate child dir per package (flat, no nesting). */ const PACKAGES_ROOT src/packages; // --- derived patterns (no need to edit) ------------------------------------- const R PACKAGES_ROOT; /** * A packages private internals: anything nested inside a package subfolder. * The packages root files are its entry points and are NOT matched here: * they stay importable from outside. */ const PACKAGE_INTERNALS ^${R}/[^/]/[^/]/;唯一的編輯點是PACKAGES_ROOT。PACKAGE_INTERNALS這個正則表達的就是深度決定公私的核心哲學^src/packages/錨定包根目錄[^/]匹配第一個目錄層級即包名第二個[^/]匹配包內的第一層子文件夾如lib、tests結尾的/匹配子文件夾下的內容。因此凡是匹配PACKAGE_INTERNALS的就是私有內部實現而包根目錄文件如index.ts由于后面沒有第二層目錄不匹配該模式從而保持對外可導入。五條 forbidden 規則逐一解讀entrypoint-boundary-from-app應用代碼只能走入口from: { pathNot: ^${R}/ }, // 導入方不在任何包內 to: { path: PACKAGE_INTERNALS },任何位于包樹之外的文件不得導入任何包內部。entrypoint-boundary-across-packages跨包只能走入口包內自由from: { path: ^${R}/([^/])/, pathNot: ^${R}/[^/]/tests/ }, // 導入方在包 $1 內且非測試 to: { path: PACKAGE_INTERNALS, pathNot: ^${R}/$1/, // 同一包 → 包內自由 },這里的關鍵是 dependency-cruiser 的組匹配反向引用$1from的捕獲組捕獲了導入方所屬的包名to.pathNot用它放行導入自己包內部的情況。正如技能 Notes 所強調的這個$1反向引用正是自己人進得去、外人進不來的機制所在不要把它拆散成逐包的手寫規則。tests-through-entrypoints測試同樣走入口from: { path: ^${R}/([^/])/tests/ }, // 測試文件屬于包 $1 to: { path: PACKAGE_INTERNALS, pathNot: ^${R}/$1/tests/, // 自己的 tests/ fixtures → 允許 },測試可以導入任意包的入口文件、以及自己tests/目錄下的 fixtures但連自己包的lib/都不許深導入——這與 codebase-design 的接口即測試面the interface is the test surface原則一脈相承調用方和測試跨越同一條縫想測試接口背后的東西說明模塊的形狀可能錯了。tests-folder-is-privatetests 文件夾只對測試開放from: { pathNot: ^${R}/[^/]/tests/ }, // 導入方不是測試 to: { path: ^${R}/[^/]/tests/ },防止業務代碼順手 import 測試 fixtures堵住測試代碼泄漏進生產路徑的口子。no-circular禁環from: {}, to: { circular: true },若只想限制包內出現環可在注釋提示下把作用域收窄到^${R}/。options 與分層占位options: { doNotFollow: { path: node_modules }, tsConfig: { fileName: tsconfig.json }, enhancedResolveOptions: { extensions: [.ts, .tsx, .js, .jsx, .json], }, },doNotFollow跳過node_modules避免噪音與誤報tsConfig讓 dependency-cruiser 使用tsconfig.json做模塊解析enhancedResolveOptions.extensions聲明參與解析的擴展名集合。配置文件末尾還預留了**分層layering**的注釋占位。技能明確區分兩個正交的關注點**接口隱藏interface-hiding**控制怎么導入必須走入口分層控制哪個包可以依賴哪個。倉庫當前把分層留作注釋模板例如// { // name: ui-may-not-depend-on-billing, // severity: error, // from: { path: ^${R}/ui/ }, // to: { path: ^${R}/billing/ }, // },需要時可自行取消注釋并填入真實的包名。三條重要設計約束技能 Notes 部分點明了三個容易忽略的設計決策公開 vs 私有由深度決定而非枚舉包根目錄文件是入口任何子文件夾內容都是私有的。慣用的子文件夾是lib/實現與tests/但規則并不硬編碼它們任何子文件夾都是私有的所以新增文件夾永遠不需要改配置新增入口也只需添加一個根目錄文件無需 barrel。包是扁平flat的根目錄下只有一層直接子目錄即一個包。包的內部可以任意嵌套多深但一個包內部不能再包含另一個包。用.cjs而非.js這樣即使倉庫是type: module配置里的module.exports也能正常工作。同理不要使用路徑別名去繞過邊界——第 4 步明確要求不要改動 tsconfig也不要添加 path aliases否則邊界的意義會被別名擊穿。與深模塊設計體系的銜接setup-ts-deep-modules 是本倉庫深模塊體系中的落地工具與設計側的能力形成閉環詞匯與判斷標準來自 codebase-design它回答了什么樣的模塊算深、縫該放在哪里深化方法論在 DEEPENING.md按依賴類別進程內、本地可替換、遠程但自有的 Ports Adapters、真正的外部 Mock決定如何跨縫測試并強調測試應跨越接口斷言可觀察結果而不是內部狀態——這正是 setup-ts-deep-modules 讓測試只能走入口的深層動機接口的多種候選形態探索見 DESIGN-IT-TWICE.md并行設計若干激進不同的接口再按深度、局部性與縫的位置對比取舍。值得說明的是該技能目前位于倉庫的in-progress/beta桶中根據 in-progress/README.md 的說明處于 beta 的技能不會進入插件與頂層 README可以按npx skillslatest add mattpocock/skills --skillsetup-ts-deep-modules的方式單獨安裝。其配套的 Agent 聲明 agents/openai.yaml 將allow_implicit_invocation設為falsedisable-model-invocation: true表明它是用戶主動調用的技能而非模型可自行觸發的隱式技能。落地后你應該擁有什么完成七步之后倉庫將獲得四項可驗證的成果一個可運行的邊界檢查lint:boundaries與 typecheck 同命令執行任何深導入、跨包觸底、測試直取內部、依賴成環都會讓 CI 紅牌一個統一的包形態根目錄入口 lib/實現 tests/測試任何新包都能照抄 example 模板一份面向未來的約定文檔packages-root/README.md明確反對 barrel、倡導多入口一條 Agent 可發現的路徑CLAUDE.md/AGENTS.md中的一行指針讓后續所有編碼 Agent 在動手前先讀到邊界規則而不是在違規報錯后才被迫理解它。最終這套配置讓深度優先從設計口號變成持續集成的硬約束接口即邊界邊界即 CICI 即文化。【免費下載鏈接】skillsSkills for Real Engineers. Straight from my .agents directory.項目地址: https://gitcode.com/GitHub_Trending/skills13/skills創作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考