檢測與 config 總表
本頁彙整 blueprint 所有查得到的東西,以及指南各頁沒逐一說明的 config 欄位。
完整型別簽名見 API 文件;本頁的定位是索引地圖。
執行環境需求
- Node —— 最低
^18.18.0 || ^20.9.0 || >=21.1.0。
這個下限是被跑出來的,不是從原始碼讀出來宣稱的:CI 用當前版本的 Node 建置,再把建置產物拿到18.18.0上執行 —— 因為被宣稱的就是這個版本。
建議版本:本專案自己拿來建置與測試的那一版,記在.nvmrc,這裡刻意不抄一份 —— 抄進正文的版本號會在沒人發現的情況下過期。
高於下限的版本都能跑,那一版只是走得最熟的路。 - ESLint 9 或 10,flat config —— 兩個大版本都在每個承載外掛的 peer 範圍內,
所以init安裝eslint時不鎖版本,讓它解析到支援範圍內最新的那個。
CI 會拿整套測試在兩個大版本上各跑一次,所以你解析到的那一版是本專案實際執行過的,不只是宣告允許的。
舊制的.eslintrc是一次遷移決策,不會變成默默導入到一半的狀態。
除此之外沒有別的 —— 套件本身零執行期依賴。
inspect 回報的檢測項目
只要有 error 等級的違規,就以 exit code 1 結束;warn 與 info 只提示、不影響檢核結果。
測試檔案(architecture.testFiles)在這些項目上都豁免,但只豁免到 glob 掃得到的範圍 ——
掃到、而且沒有一條 glob 對得上的檔案,會被當成一般原始碼檢查。
undeclared-folder· error —— 原始碼根目錄下存在未宣告為分層的資料夾flow-violation· error —— 逆向匯入,或透過別名進行的同層匯入deep-import· error —— 別名匯入直接觸及資料夾模組的內部,未經公開入口relative-escape· error —— 相對路徑匯入越出所屬分層、逃逸出原始碼根目錄,或伸進鄰居模組的入口之後。
在folder佈局下,鄰居模組是碰得到的 ——../Sibling就是同層之間互相使用的方式,而且是唯一的方式,因為別名寫法(~app/{自己這層}/Sibling)仍然被擋package-ownership· error —— 從非擁有者分層匯入某分層專屬的套件(或受限的具名匯入)selfonly-reexport· error —— 再匯出標記為selfOnly的依賴 —— 僅可依賴,不可轉手輸出cycle· error —— 模組層級的循環匯入,並列出完整路徑。
每一組獨立的循環都會回報,一組互相依賴的模組算一筆 —— 所以數量就是工作量,不是「先找到的那一個」no-entry· warn —— 資料夾模組缺少公開入口檔 —— 外部無從匯入missing-layer· info —— 已宣告的分層尚無對應資料夾owns-not-installed· info —— 某分層owns的套件不在package.json裡。
禁令已經產生、內容也正確,只是暫時還碰不到任何東西。
把套件裝起來,或是把這筆宣告拿掉,兩種都算解法declaratory-self-only· info ——selfOnly保護的分層還沒有任何檔案 —— 再匯出禁令是宣告性的,要等 code 進來才會真正生效
既有專案可透過 baseline 棘輪,把這份清單轉成「只攔新增的違規」。
被 baseline 記錄的違規,是用「規則 + 路徑 + subject」來識別的 —— subject 指的是 import specifier、循環的成員這類東西,不是訊息文字。
所以某次改版把訊息改得更好懂,不會害你的 gate 變紅。
baseline 檔本身帶著這套識別方式的 "version";
在識別方式改變之前寫下的檔案會被拒收,並附上重記的指令,而不是被拿去重新解讀。
import graph 是怎麼讀出來的
上面每一條跟 import 有關的檢測,都是從一張圖上讀出來的,而那張圖是從原始碼文字掃出來的,不是解析 AST。
算出來的 specifier(import(path)、require(name))、import * as 背後的個別名稱、字串裡長得像 import 的文字,都在它看不到的範圍內。inspect 跟 deps 的輸出都會以這段說明收尾 —— 因為報告乾淨的時候,才是它最要緊的時候。
硬性 gate 沒有這個限制:它們跑在 ESLint 上、走 AST。
所以 inspect 是盤點,你的 lint 才是單一 import 的判決 —— 這也正是「blueprint inspect 本身不等於 gate」的原因。
內嵌 ESLint 外掛
emitLint 在生成的 config 裡內建自訂規則 —— 不用額外安裝。
其中一條是結構規則、永遠開著;其餘由 blueprint.rules 的規則識別碼控制。
plugin 物件本身也有匯出(import { plugin } from '@kekkai/blueprint')—— 這是給「不 spread emitLint、想手動掛某條 blueprint/* 規則」的逃生口,其他人永遠用不到它:
blueprint/relative-escape· 恆常啟用(結構規則)—— inspect 同名檢測的「看得懂深度」孿生版:
兩者呼叫同一個relativeVerdict,所以任一方都不可能得出另一方不會同意的結論blueprint/no-deep-watch·rules.deepWatch—— 禁用deep: true的監聽 —— 每次變更都會遍歷整個資料來源(Vue preset:error)blueprint/use-prefix·rules.usePrefix—— hook 分層匯出的函式必須帶use前綴(分層與前綴皆可設定)blueprint/use-prefix-needs-reactivity·rules.usePrefixReactivity—— 帶use前綴的檔案必須實際呼叫 reactive 或生命週期 APIblueprint/test-filename-matches-source·rules.testFilename—— 測試檔必須有同目錄、同名的原始碼檔案blueprint/no-typedef-only-file·rules.typedefOnlyFile—— JS 檔案不得僅含@typedef宣告(僅套用於.js)
另有三條受管規則 —— 由 layers / owns / alias 轉譯而成、歸生成器管:no-restricted-imports、no-restricted-syntax、no-restricted-globals。
這三條沒辦法透過 lintOverrides 設定;要調整就改 blueprint config 本身。
dependency-flow 禁令、同層禁令與 selfOnly 再匯出 selector 都會透過每個已宣告別名,同時涵蓋裸的分層入口與其下所有路徑。
這不會放寬資料夾模組的公開面:獲准的匯入者仍可使用模組入口,但不能伸進入口後方。
把受管規則併進自己的規則設定
flat config 是取代不是合併 —— 但只發生在「兩筆都命中的檔案」上 ——
所以本來就有設 no-restricted-syntax 的 repo,在那些檔案上不能放著讓後面那筆贏:兩邊的選項必須併成同一筆。
不過也就只有那些檔案。
一筆設定對「不在自己 files 範圍內」的檔案什麼都不做,所以你的設定沒伸到的地方,spread 仍然在替 blueprint 執行它那一筆;
兩邊範圍不一致時,要做的是把合併後的那一筆縮到重疊區,而不是把任一邊放寬去湊另一邊。
你原本那一筆留在原處、繼續守 blueprint 從來沒管過的檔案 —— 而且不用搬。
把合併後那一筆放在陣列最後就好:最後就同時在 spread 之後、也在你原本那筆之後,因為兩筆都命中的地方仍然是後面的贏。
合併那一筆需要的 selfOnly selector,npx blueprint rules --json 會照層帶出來,而且有兩種寫法,只有一種撐得過「貼上」這個動作:
- 要複製的是
jsLiteral—— 這是 selector 的 JS 原始碼形式,連引號一起給。 selectors是 ESLint 實際解析的那個值。
對「用程式組設定」的情境是對的,對「用貼的」則是陷阱:
路徑分隔符在裡面是/的跳脫寫法(直接放裸/會讓 esquery 的正規式提早結束),
而 JavaScript 解析字串常值時會把同一個跳脫吃掉一層 —— 於是貼進去的 selector 在那個裸/就結束了。
不會有語法錯誤、lint 照樣是綠的,禁令則靜靜地什麼都沒擋到。testExemptions是一起附著的,得跟著搬過去。
只靠 selector 重組一筆設定會安靜地把它弄丟,而且是最糟的那種安靜:合併後的那筆照跑,於是禁令開始伸進 glob 掃得到的那些測試檔。
禁令的訊息文字是你自己寫的 —— doctor 驗的是 selector,從來不驗訊息。
還有一條作用範圍要記著,它講的是這條檢查本身、跟你的 config 無關,所以併完之後仍然成立:
doctor 的合併存活檢查比對的是匯入禁令、全域物件與 selfOnly selector —— 不含套件歸屬。
所以一次弄丟套件禁令的合併,在那裡照樣是綠的,那一欄要你自己驗。blueprint rules 會在「你真的有分層持有套件」的情況下,把該跑的指令講出來。
blueprint.rules —— 哪些識別碼會成為檢核關卡
blueprint.rules 裡的識別碼,只有機器查得動的才會轉譯成 lint 關卡。
查得動的集合如下:
maxLines→max-lines· error · 400maxLinesPerFunction→max-lines-per-function· warn · 100maxParams→max-params· warn · 3maxStatements→max-statements· warn · 15complexity→complexity· warn · 12unusedVars→no-unused-vars(TypeScript 專案自動改用 TS 感知版本)· errorexplicitAny→@typescript-eslint/no-explicit-any· errorcodeStyle→@stylistic的customize()整組,加上max-len、linebreak-style與原生curly—— 約 68 條 · errorstatementsPerLine→@stylistic/max-statements-per-line,寫死{ max: 1 }· errorstatementPadding→@stylistic/padding-line-between-statements,帶固定的 17 條設定 · errorimportBlock→import-x/first+import-x/no-duplicates· errorfixtureImports→ 禁止產品程式碼匯入 fixture 目錄 · error(Vue preset)cycles→ inspect 的cycle檢測(模組層級,只在 inspect 執行時診斷;baseline 會保留已記錄的 finding)。生成 config 預設不做持續預防;能接受逐檔重查圖的成本時,可選擇啟用import-x/no-cycle· errordeepWatch/usePrefix/usePrefixReactivity/testFilename/typedefOnlyFile→ 上面外掛那節的規則(見上)
其餘任何識別碼(例如 deadCode)都屬於文件性質:會寫進手冊與 AI Agent 守則,作為 Agent 必須持守的判斷,但不會被說成硬性關卡。
這個劃分就是三種級別落點的機制。
這整份對照隨時問得到工具本人:npx blueprint rules 會印出 catalog,有 config 時還會標註實際宣告的 tier。
在這裡開不起來的關卡,那一列會留著、標記 unavailable here,原因則單獨列在關卡列表上方 —— 例如 JS 專案上的 explicitAny、testFiles: [] 旁邊的 testFilename —— 而不是連個交代都沒有就被拿掉。
這也是為什麼這份 catalog 的列數會比 inspect 與 doctor 印的 N/M 個選用關卡 分母來得多:
那個分母數的是「有東西開得起來」的關卡,而拿兩個數字對照的人會被告知差額落在哪一列,不用自己猜。
有五個關卡靠注入的外掛才活著
這個套件沒有任何 runtime 依賴,
所以上面每一條會 emit 第三方規則的識別碼,都得由你把外掛交給 emitLint。
而外掛缺席的關卡會完全不 emit,同時 lint 照樣是綠的 —— 讀起來跟一次乾淨的合併一模一樣。
生成的 config 三個外掛都接好了,init 也會裝;
手動合併的 config 要自己把參數帶過去:
import stylistic from '@stylistic/eslint-plugin';
import imports from 'eslint-plugin-import-x';
import tseslint from 'typescript-eslint';
export default [
/* …你原本的設定 */
...emitLint(blueprint, { typescript: tseslint.plugin, stylistic, imports }),
];explicitAny要typescript。
跟unusedVars不一樣,這條沒有原生規則可以退回去 ——any是 TypeScript 才有的東西,
所以在 JS 專案裡這個關卡沒有意義,inspect會直接把它從涵蓋率的分母移掉,
而不是回報一個沒人開得起來的關卡。codeStyle、statementsPerLine、statementPadding要stylistic。
ESLint 自己的排版規則,在它把排版交給@stylistic那次就被標為 deprecated 並凍結了,
照原本的識別碼 emit 等於塞一批隨時會被移除的規則給使用者。codeStyle還會去讀外掛的configs.customize()factory,讀不到就直接拋錯,
而不是安靜地什麼都不管。importBlock要imports。
ESLint 原生和@stylistic都沒有任何規則會合併重複的 import。
這裡是 ESLint 在管排版
codeStyle 不是包在 formatter 外面的便利層 —— 它就是 formatter。
兩個後果值得直說:
- 紅字本身就是完整的執行機制。
不需要編輯器整合、不需要存檔掛鉤、也不假設誰用哪個編輯器:
agent 跑 lint、讀到紅字、自己修好。
約 68 條裡只有 5 條沒有自動修正,所以eslint --fix會清掉第一輪的絕大部分,
剩下的才是真的需要判斷的部分。 - 本來就有自己 formatter 的 repo,屬於「工具重疊」那一類。
排版的所有權留一個,並把選了哪個記錄下來。
兩邊設在同一個 key 上的規則是機械性衝突 —— flat config 是取代而不是合併。
codeStyle 裡面有三個細節是刻意的,不是順手加的:
statementsPerLine是讓maxLines有意義的那條。maxLines數的是程式行(空行與註解跳過),
所以一個沒有限制「一行能裝多少」的行數預算,把敘述壓成一行就過得去 —— 根本不用拆檔案。{ max: 1 }寫死就是為了這件事:這個關卡的旋鈕是 tier。curly堵的是同一條路的下一層:沒有它,if (x) return;會被算成一個敘述而溜過去。max-len不放過純字串,而且沒有自動修正。
一個「行裡有字串就豁免」的長度上限不是上限;
而過長的行要的是重構,不是重排。linebreak-style是unix,而它的紅字通常不是在講那個檔案。
會炸的是混用換行,所以立場是全部 LF ——
但違規的成因通常在 git 的autocrlf或缺少.gitattributes。
去那邊修,不然下次 checkout 就把自動修正蓋回去了。
可調參數:indent(2)、quotes(single)、semi(true)、maxLen(90),
寫在 gate 上,例如 codeStyle: { tier: 'error', indent: 4, maxLen: 120 }。
其餘都是固定的 —— 想要不一樣的括號風格就把這個關卡關掉,自己宣告一套。
一個實戰會咬人的範圍細節:emit.lint.severity 只蓋結構家族(no-restricted-imports / -syntax / -globals 與 blueprint/relative-escape)。
上面每條規則都吃自己的 blueprint.rules tier —— severity 設 warn 不會讓 maxLines 或 unusedVars 變安靜。
快速上手範例以外的 config 欄位
快速上手的 defineBlueprint 範例涵蓋核心欄位。
其餘欄位一覽如下 —— 完整結構見 API 文件:
承重的那一塊
結構規則全部從這裡編出來。
這些鍵比上面那份關卡目錄更早存在,也因此一直只在範例裡露臉 —— 定義該有個家。
architecture.alias—— 專案的匯入根,例如~app。
必填、沒有預設值:猜錯的別名會讓非法匯入靜靜通過,因為每一條結構禁令的樣式都是拿這個字串組出來的architecture.layers—— 有順序的分層清單。
順序就是流向:一個分層只能匯入排在它後面的分層。
因此宣告本身說不出回頭邊,無環的是「宣告的分層圖」;這不會持續阻止模組匯入 cycle,後者只在blueprint inspect執行時診斷layer.does—— 一句話說明這層的程式碼是幹嘛的。
寫進手冊與 Agent 守則;沒有規則會強制它layer.mustNot—— 這層不該做的事,用白話寫。
去處相同、同樣不強制:規則判斷不了的時候,審查者與 Agent 讀的就是這幾句layer.allowedImporters—— 收窄「誰可以匯入這一層」。
不寫的話,排在前面的分層都可以;寫了就只有清單上的可以,而且每一個都必須是更早宣告的分層 —— 所以收窄永遠不可能生出一條回頭的邊。
條目可帶selfOnly(可以依賴這層,但不得再往外轉出)與description(手冊關係圖上那條邊的標籤)layer.owns—— 這層獨佔的基元,其他分層一律被擋。
直接給字串代表整個套件('axios');物件形式可帶imports(只鎖特定具名匯入,如['createContext'])、pattern(把名稱當成 glob 群組)、exempt(豁免的檔案樣式)。{ global: 'fetch' }則是獨佔一個全域變數而不是套件architecture.module—— 共用的模組形狀:layout(folder= 一個模組一個資料夾、外面只看得到公開入口;flat= 單檔)、entry(入口檔名,預設index)、private(藏在入口後面的子部分)。folder之下,鄰居模組只能透過它的入口碰到(../Sibling),其餘皆不可 —— 伸進入口後面不行,走別名也不行
調校
architecture.sourceRoot—— 分層所在目錄(相對於專案根目錄)。預設src;根目錄式佈局(如無src/的 Next.js)設為.。Lint、inspect、init scaffold、deps target 與產生的 agent placement guidance 都會從此根目錄解析來源路徑。Config 尚未建立時,survey 可由 TypeScript includes 推導根目錄式佈局;若 workspace 有多個 application root,則會要求明確選擇此欄位。architecture.additionalAliases——alias以外、同樣納入所有結構禁令的額外匯入根。Alias 可指向 source root、其上層,或src/shared之類的單一已宣告 layer。
一份 blueprint 只描述一條有序的 layer 軸。它可以治理 app → features 這類根目錄資料夾的外層關係,但不會同時將每個 feature 內重複的 ui/application/infrastructure/domain 視為第二條獨立分層軸。
architecture.testFiles—— 豁免於結構規則與度量關卡的測試檔樣式(預設*.test.*/*.spec.*)。
填[]代表不豁免任何檔 —— 測試檔跟著它那層的規則走 —— 同時也把testFilename這個關卡關掉:
那條規則的範圍就是這些測試檔樣式,空清單等於沒有檔可以讓它檢查。blueprint rules會在該關卡旁邊講明。
宣告了、卻對不上任何檔的 glob,賠掉的是豁免、不是關卡 ——
這一輪讀到的檔案沒有一個因它而豁免。architecture.layerFiles—— 框架預設樣式不適用時,逐層指定檔案樣式architecture.layerFilesIgnore—— 從產出的 lint 與由 lint 執行的inspectfindings 中全域排除的檔案樣式。這些檔案仍會接受 undeclared folder、cycle 等只由inspect執行的檢查;coverage 會將它們列為刻意忽略,而不會宣稱 lint 已涵蓋
lint 與 inspect 共通的可攜 glob 語法,是以 / 分隔的路徑搭配 **、*、?, 以及 *.{ts,tsx} 這類單層 brace alternatives;layerFiles 另會把 {layer} 替換成每個已宣告的分層名稱。Negation、character classes、extglobs 與巢狀 braces 不在共通語法內。維持在這個可攜子集合,lint 與 inspect 才會選到同一批檔案。
architecture.naming—— 依概念設定的命名慣例(如{ hook: 'useX + reactivity' })—— 寫入手冊與守則layer.module—— 逐層覆寫共用的模組形狀 —— 例如某一分層採資料夾模組、其餘維持單檔layer.lintOverrides—— 逐層的 ESLint 調整(三條受管規則除外)emit.agents—— Agent 守則的發佈目標:claude、agents、gemini、copilot、cursor、windsurf(可逐目標指定path)。預設['claude', 'agents'];空陣列就不產出。縮窄清單後,下一次 init 會自動移除「整份都是自己產出」的過期守則檔(被人手改過的只提醒、不動手)emit.handbook/emit.lint—— 手冊輸出路徑 · 結構規則的等級(度量規則吃自己的rulestier)
命令列旗標
init——--agent claude|codex(啟動編寫用的 Agent CLI)·--preset(強制建 preset)·--authoring(即使小 repo 也強制產 playbook;與--preset相反)·--framework vue|react·--no-install·--dry-runsurvey——--alias <name>(tsconfig paths 偵測不到別名時指定)·--source-root <path>(在 workspace 中選擇一個 application)·--jsoninspect——--baseline·--update-baseline·--framework vue|react·--jsonimpact——--jsondeps [module]——--framework vue|react·--jsonrules——--jsondoctor——--json
所有指令都支援 --help;CLI 本身支援 --version。
