@markuplint/ml-config 4.8.15 → 5.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.ja.md +65 -33
- package/ARCHITECTURE.md +73 -41
- package/CHANGELOG.md +39 -0
- package/SKILL.md +3 -3
- package/docs/maintenance.ja.md +16 -18
- package/docs/maintenance.md +16 -18
- package/lib/index.d.ts +1 -1
- package/lib/index.js +0 -1
- package/lib/merge-config.d.ts +8 -6
- package/lib/merge-config.js +86 -27
- package/lib/types.d.ts +103 -24
- package/lib/utils.d.ts +18 -4
- package/lib/utils.js +27 -17
- package/package.json +10 -8
package/ARCHITECTURE.ja.md
CHANGED
|
@@ -87,8 +87,8 @@ type RuleConfig<T, O> = {
|
|
|
87
87
|
|
|
88
88
|
### NodeRule / ChildNodeRule
|
|
89
89
|
|
|
90
|
-
- `NodeRule` -- CSS セレクタ、正規表現セレクタ、ARIA ロール、カテゴリ、obsolete
|
|
91
|
-
- `ChildNodeRule` -- `NodeRule` と類似だが子ノードを対象とする。`inheritance`
|
|
90
|
+
- `NodeRule` -- CSS セレクタ、正規表現セレクタ、ARIA ロール、カテゴリ、obsolete フラグで対象ノードを限定し、ルール設定をオーバーライド。オプションの `name`(`/` を含む必要あり)で named nodeRule として仮想ルールに展開可能。オプションの `specConformance`(`'normative'` または `'non-normative'`)を下流ツールやレポート向けのメタデータとして設定
|
|
91
|
+
- `ChildNodeRule` -- `NodeRule` と類似だが子ノードを対象とする。`inheritance` フラグで子孫への継承を制御。`name` と `specConformance`(メタデータ)による named nodeRule 展開もサポート
|
|
92
92
|
|
|
93
93
|
### Pretender 型
|
|
94
94
|
|
|
@@ -124,21 +124,21 @@ flowchart TD
|
|
|
124
124
|
|
|
125
125
|
### プロパティ別マージ戦略テーブル
|
|
126
126
|
|
|
127
|
-
| プロパティ | 戦略
|
|
128
|
-
| ---------------- |
|
|
129
|
-
| `plugins` | 結合+重複排除+正規化
|
|
130
|
-
| `parser` | オブジェクト
|
|
131
|
-
| `parserOptions` | オブジェクト
|
|
132
|
-
| `specs` | オブジェクト
|
|
133
|
-
| `excludeFiles` | 結合+重複排除
|
|
134
|
-
| `severity` | オブジェクト
|
|
135
|
-
| `pretenders` |
|
|
136
|
-
| `rules` | ルール別マージ
|
|
137
|
-
| `nodeRules` |
|
|
138
|
-
| `childNodeRules` |
|
|
139
|
-
| `overrideMode` | 右辺優先
|
|
140
|
-
| `overrides` | キー別再帰マージ
|
|
141
|
-
| `extends` | 結合→削除
|
|
127
|
+
| プロパティ | 戦略 | ヘルパー関数 | 詳細 |
|
|
128
|
+
| ---------------- | -------------------------- | ---------------------------------------------------- | --------------------------------------------- |
|
|
129
|
+
| `plugins` | 結合+重複排除+正規化 | `concatArray(uniquely=true, comparePropName='name')` | 同名プラグインは settings を shallow merge |
|
|
130
|
+
| `parser` | オブジェクト shallow merge | `mergeObject()` | `{...a, ...b}` で右辺優先 |
|
|
131
|
+
| `parserOptions` | オブジェクト shallow merge | `mergeObject()` | 同上 |
|
|
132
|
+
| `specs` | オブジェクト shallow merge | `mergeObject()` | 同上 |
|
|
133
|
+
| `excludeFiles` | 結合+重複排除 | `concatArray(uniquely=true)` | 単純な値の重複排除 |
|
|
134
|
+
| `severity` | オブジェクト shallow merge | `mergeObject()` | parser と同様 |
|
|
135
|
+
| `pretenders` | セマンティックマージ | `mergePretenders()` | files/imports: 上書き、data: 追加 |
|
|
136
|
+
| `rules` | ルール別マージ | `mergeRules()` → `mergeRule()` | **最も複雑 -- 次節で詳述** |
|
|
137
|
+
| `nodeRules` | 結合+名前で重複排除 | `concatArray(uniquely=true, comparePropName='name')` | 名前付きエントリは重複排除、無名は追加 |
|
|
138
|
+
| `childNodeRules` | 結合+名前で重複排除 | `concatArray(uniquely=true, comparePropName='name')` | nodeRules と同様 |
|
|
139
|
+
| `overrideMode` | 右辺優先 | `b.overrideMode ?? a.overrideMode` | 単純な優先順位 |
|
|
140
|
+
| `overrides` | キー別再帰マージ | `mergeOverrides()` | 各キーに対して `mergeConfig()` を再帰呼び出し |
|
|
141
|
+
| `extends` | 結合→削除 | `concatArray()` | マージ後に結果から削除 |
|
|
142
142
|
|
|
143
143
|
### mergeRule() -- ルールマージの詳細
|
|
144
144
|
|
|
@@ -146,7 +146,7 @@ flowchart TD
|
|
|
146
146
|
mergeRule(a: Nullable<AnyRule>, b: AnyRule): AnyRule
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
最も複雑なマージロジックを処理する関数です。両方の入力はまず `optimizeRule()`
|
|
149
|
+
最も複雑なマージロジックを処理する関数です。両方の入力はまず `optimizeRule()` で正規化されます。
|
|
150
150
|
|
|
151
151
|
```mermaid
|
|
152
152
|
flowchart TD
|
|
@@ -159,18 +159,16 @@ flowchart TD
|
|
|
159
159
|
ChkBUndef -->|Yes| RetA["return a"]
|
|
160
160
|
ChkBUndef -->|No| ChkBVal{"b は Value?\n(primitive/null/array)"}
|
|
161
161
|
ChkBVal -->|Yes| ChkAVal{"a は Value?"}
|
|
162
|
-
ChkAVal -->|Yes|
|
|
163
|
-
|
|
164
|
-
ChkBothArr -->|No| RetBVal["return b\n(右辺優先)"]
|
|
165
|
-
ChkAVal -->|No| MergeValObj["a の severity/reason を保持\nvalue を置換\n(配列は連結)"]
|
|
162
|
+
ChkAVal -->|Yes| RetBVal["return b\n(右辺優先、\n配列も上書き)"]
|
|
163
|
+
ChkAVal -->|No| MergeValObj["a の severity/reason を保持\nvalue を b で上書き"]
|
|
166
164
|
ChkBVal -->|No| MergeObj["severity: b ?? a\nvalue: b ?? a\noptions: mergeObject(a, b)\nreason: b ?? a"]
|
|
167
165
|
```
|
|
168
166
|
|
|
169
167
|
**重要な設計判断:**
|
|
170
168
|
|
|
171
169
|
1. **`false` は絶対無効化** -- override が `false`(または `{value: false}`)なら、base が何であっても結果は常に `false`
|
|
172
|
-
2.
|
|
173
|
-
3. **options は
|
|
170
|
+
2. **配列値は上書き** -- `["a","b"]` + `["c","d"]` は `["c","d"]` になる(右辺優先)。ESLint や Biome と一貫した動作
|
|
171
|
+
3. **options は shallow merge** -- severity、value、reason は右辺優先だが、options のみ `mergeObject()`(`{...a, ...b}` による shallow merge)を使用
|
|
174
172
|
|
|
175
173
|
### ヘルパー関数
|
|
176
174
|
|
|
@@ -180,12 +178,12 @@ flowchart TD
|
|
|
180
178
|
|
|
181
179
|
- `uniquely=false` -- 単純な連結、重複排除なし
|
|
182
180
|
- `uniquely=true`、`comparePropName` なし -- 完全一致で重複排除
|
|
183
|
-
- `uniquely=true`、`comparePropName` あり --
|
|
181
|
+
- `uniquely=true`、`comparePropName` あり -- 指定プロパティ名で重複排除。同名オブジェクトはオブジェクトスプレッドで shallow merge(例: プラグインの settings)
|
|
184
182
|
- 空の結果には `undefined` を返す
|
|
185
183
|
|
|
186
184
|
#### mergeObject(a, b)
|
|
187
185
|
|
|
188
|
-
`
|
|
186
|
+
`{...a, ...b}` による shallow merge。トップレベルで右辺の値が優先。結果から undefined プロパティを除去。
|
|
189
187
|
|
|
190
188
|
#### mergeOverrides(a, b)
|
|
191
189
|
|
|
@@ -193,7 +191,7 @@ flowchart TD
|
|
|
193
191
|
|
|
194
192
|
#### mergePretenders(a, b)
|
|
195
193
|
|
|
196
|
-
配列形式の pretenders を正規化形式 `PretenderDetails`(`{data: [...]}
|
|
194
|
+
配列形式の pretenders を正規化形式 `PretenderDetails`(`{data: [...]}`)に変換してからセマンティックマージ: `files`/`imports` は上書き(右辺優先)、`data` は追加(連結)。
|
|
197
195
|
|
|
198
196
|
## テンプレートレンダリングシステム
|
|
199
197
|
|
|
@@ -217,11 +215,11 @@ Mustache テンプレート文字列を提供されたデータでレンダリ
|
|
|
217
215
|
|
|
218
216
|
## ユーティリティ関数
|
|
219
217
|
|
|
220
|
-
| 関数 | 目的
|
|
221
|
-
| --------------------- |
|
|
222
|
-
| `cleanOptions()` |
|
|
223
|
-
| `isRuleConfigValue()` | 型ガード: プリミティブ、`null`、配列(= `RuleConfig` オブジェクトではない)に対して `true` を返す
|
|
224
|
-
| `deleteUndefProp()` | プレーンオブジェクトから `undefined` 値のプロパティをすべて in-place で削除
|
|
218
|
+
| 関数 | 目的 |
|
|
219
|
+
| --------------------- | ------------------------------------------------------------------------------------------------- |
|
|
220
|
+
| `cleanOptions()` | 標準フィールド(`severity`、`value`、`options`、`reason`)を抽出して undefined プロパティを除去 |
|
|
221
|
+
| `isRuleConfigValue()` | 型ガード: プリミティブ、`null`、配列(= `RuleConfig` オブジェクトではない)に対して `true` を返す |
|
|
222
|
+
| `deleteUndefProp()` | プレーンオブジェクトから `undefined` 値のプロパティをすべて in-place で削除 |
|
|
225
223
|
|
|
226
224
|
## 主要ソースファイル
|
|
227
225
|
|
|
@@ -239,7 +237,6 @@ Mustache テンプレート文字列を提供されたデータでレンダリ
|
|
|
239
237
|
| `@markuplint/ml-ast` | `ParserOptions` 型(型のみ) |
|
|
240
238
|
| `@markuplint/selector` | `RegexSelector` 型(再エクスポート) |
|
|
241
239
|
| `@markuplint/shared` | `Nullable` ユーティリティ型 |
|
|
242
|
-
| `deepmerge` | `mergeObject()` の deep merge 実装 |
|
|
243
240
|
| `is-plain-object` | `deleteUndefProp()` でのプレーンオブジェクト判定 |
|
|
244
241
|
| `mustache` | `provideValue()` のテンプレートレンダリングエンジン |
|
|
245
242
|
| `type-fest` | `Writable` ユーティリティ型 |
|
|
@@ -277,6 +274,41 @@ flowchart LR
|
|
|
277
274
|
- **`@markuplint/ml-core`** -- マージ済みの `OptimizedConfig` を受け取り、パース済みドキュメントにルールを適用
|
|
278
275
|
- **`@markuplint/rules`** -- `Rule<T,O>` と `RuleConfig<T,O>` 型を使用してルール実装を定義
|
|
279
276
|
|
|
277
|
+
## 設計判断
|
|
278
|
+
|
|
279
|
+
### Flat Config を採用しない理由
|
|
280
|
+
|
|
281
|
+
ESLint の Flat Config アプローチを評価した結果、markuplint には不適と判断しました:
|
|
282
|
+
|
|
283
|
+
- **ESLint 自体が 2025年3月に Flat Config へ `extends` を再追加** -- 純粋な Flat アプローチは JavaScript ツールでも不十分だった
|
|
284
|
+
- **markuplint は JSON ベース** -- Flat Config は JavaScript を前提とする。HTML/マークアップ開発者(主要ユーザー層)にとって、JSON のスキーマ検証と言語非依存性は大きなメリット
|
|
285
|
+
- **markuplint の `nodeRules`/`childNodeRules` は CSS セレクタベース** -- Flat Config のファイルパターンモデルに対応する仕組みがない
|
|
286
|
+
- **ESLint v9 への移行はコミュニティに大きな痛みを与えた** -- 段階的な移行には自動化ツールと数年のエコシステム適応が必要だった
|
|
287
|
+
|
|
288
|
+
**結論:** JSON ベースの `extends` マージ戦略の改善が markuplint にとって最適なアプローチ。
|
|
289
|
+
|
|
290
|
+
### マージ戦略の原則
|
|
291
|
+
|
|
292
|
+
配列の扱いには論理的な区別があります:
|
|
293
|
+
|
|
294
|
+
| 配列の種類 | 例 | マージ動作 | 根拠 |
|
|
295
|
+
| ------------------------------ | -------------------------------------- | ---------- | ---------------------- |
|
|
296
|
+
| **トップレベルのコレクション** | `plugins`, `excludeFiles`, `nodeRules` | 累積 | 独立したアイテムの集合 |
|
|
297
|
+
| **ルール値** | `["allowed-tag-1", "allowed-tag-2"]` | 上書き | 1つのルールの設定値 |
|
|
298
|
+
|
|
299
|
+
これは ESLint と Biome の動作と一貫しており、ルール値はより具体的な設定で常に上書きされます。
|
|
300
|
+
|
|
301
|
+
### Deep Merge ではなく Shallow Merge
|
|
302
|
+
|
|
303
|
+
| ツール | ルール options のマージ | 根拠 |
|
|
304
|
+
| ---------- | ----------------------------- | -------------------------------------------------------------------------- |
|
|
305
|
+
| ESLint | 完全リプレース | 最もシンプルだが驚きがある |
|
|
306
|
+
| Biome | Deep merge(Merge trait経由) | 完全な柔軟性、高い複雑性 |
|
|
307
|
+
| markuplint | **Shallow merge** | 中間地点: トップレベルのキーはマージ、ネストされたオブジェクトはリプレース |
|
|
308
|
+
|
|
309
|
+
`deepmerge` ライブラリを削除し、シンプルなオブジェクトスプレッド(`{...a, ...b}`)に置き換えました。markuplint の設定でマージされるすべてのオブジェクト(parser、specs、parserOptions、severity、プラグイン設定、ルールオプション)はフラットなキーバリューマップであるため、これで十分です。
|
|
310
|
+
|
|
280
311
|
## ドキュメントマップ
|
|
281
312
|
|
|
313
|
+
- [マイグレーションガイド](../../docs/migration/v4-v5/config.ja.md) -- メジャーバージョン間の破壊的変更
|
|
282
314
|
- [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、レシピ、トラブルシューティング
|
package/ARCHITECTURE.md
CHANGED
|
@@ -87,8 +87,8 @@ type RuleConfig<T, O> = {
|
|
|
87
87
|
|
|
88
88
|
### NodeRule / ChildNodeRule
|
|
89
89
|
|
|
90
|
-
- `NodeRule` -- Targets specific nodes by CSS selector, regex selector, ARIA roles, categories, or obsolete flag, then overrides their rule settings
|
|
91
|
-
- `ChildNodeRule` -- Similar to `NodeRule` but targets child nodes; includes an `inheritance` flag to control whether overrides propagate to descendants
|
|
90
|
+
- `NodeRule` -- Targets specific nodes by CSS selector, regex selector, ARIA roles, categories, or obsolete flag, then overrides their rule settings. Supports an optional `name` (must contain `/`) for named nodeRule expansion into virtual rules, and an optional `specConformance` (`'normative'` or `'non-normative'`) as metadata for downstream tools and reporting
|
|
91
|
+
- `ChildNodeRule` -- Similar to `NodeRule` but targets child nodes; includes an `inheritance` flag to control whether overrides propagate to descendants. Also supports `name` and `specConformance` (metadata) for named nodeRule expansion
|
|
92
92
|
|
|
93
93
|
### Pretender Types
|
|
94
94
|
|
|
@@ -124,21 +124,21 @@ flowchart TD
|
|
|
124
124
|
|
|
125
125
|
### Per-Property Merge Strategy Table
|
|
126
126
|
|
|
127
|
-
| Property | Strategy | Helper Function | Details
|
|
128
|
-
| ---------------- | -------------------------------- | ---------------------------------------------------- |
|
|
129
|
-
| `plugins` | Concat + deduplicate + normalize | `concatArray(uniquely=true, comparePropName='name')` | Same-name plugins have their settings
|
|
130
|
-
| `parser` | Object
|
|
131
|
-
| `parserOptions` | Object
|
|
132
|
-
| `specs` | Object
|
|
133
|
-
| `excludeFiles` | Concat + deduplicate | `concatArray(uniquely=true)` | Simple value deduplication
|
|
134
|
-
| `severity` | Object
|
|
135
|
-
| `pretenders` |
|
|
136
|
-
| `rules` | Per-rule merge | `mergeRules()` then `mergeRule()` | **Most complex -- see next section**
|
|
137
|
-
| `nodeRules` | Concat
|
|
138
|
-
| `childNodeRules` | Concat
|
|
139
|
-
| `overrideMode` | Right-side wins | `b.overrideMode ?? a.overrideMode` | Simple precedence
|
|
140
|
-
| `overrides` | Per-key recursive merge | `mergeOverrides()` | Calls `mergeConfig()` recursively for each key
|
|
141
|
-
| `extends` | Concat then delete | `concatArray()` | Removed from result after merge
|
|
127
|
+
| Property | Strategy | Helper Function | Details |
|
|
128
|
+
| ---------------- | -------------------------------- | ---------------------------------------------------- | ---------------------------------------------------- |
|
|
129
|
+
| `plugins` | Concat + deduplicate + normalize | `concatArray(uniquely=true, comparePropName='name')` | Same-name plugins have their settings shallow-merged |
|
|
130
|
+
| `parser` | Object shallow merge | `mergeObject()` | Right-side wins via `{...a, ...b}` |
|
|
131
|
+
| `parserOptions` | Object shallow merge | `mergeObject()` | Same as above |
|
|
132
|
+
| `specs` | Object shallow merge | `mergeObject()` | Same as above |
|
|
133
|
+
| `excludeFiles` | Concat + deduplicate | `concatArray(uniquely=true)` | Simple value deduplication |
|
|
134
|
+
| `severity` | Object shallow merge | `mergeObject()` | Same as parser |
|
|
135
|
+
| `pretenders` | Semantic merge | `mergePretenders()` | files/imports: override, data: append |
|
|
136
|
+
| `rules` | Per-rule merge | `mergeRules()` then `mergeRule()` | **Most complex -- see next section** |
|
|
137
|
+
| `nodeRules` | Concat + deduplicate by name | `concatArray(uniquely=true, comparePropName='name')` | Named entries deduplicated; unnamed entries appended |
|
|
138
|
+
| `childNodeRules` | Concat + deduplicate by name | `concatArray(uniquely=true, comparePropName='name')` | Same as nodeRules |
|
|
139
|
+
| `overrideMode` | Right-side wins | `b.overrideMode ?? a.overrideMode` | Simple precedence |
|
|
140
|
+
| `overrides` | Per-key recursive merge | `mergeOverrides()` | Calls `mergeConfig()` recursively for each key |
|
|
141
|
+
| `extends` | Concat then delete | `concatArray()` | Removed from result after merge |
|
|
142
142
|
|
|
143
143
|
### mergeRule() -- Rule Merge Details
|
|
144
144
|
|
|
@@ -146,7 +146,7 @@ flowchart TD
|
|
|
146
146
|
mergeRule(a: Nullable<AnyRule>, b: AnyRule): AnyRule
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
This function handles the most complex merge logic. Both inputs are first normalized via `optimizeRule()
|
|
149
|
+
This function handles the most complex merge logic. Both inputs are first normalized via `optimizeRule()`.
|
|
150
150
|
|
|
151
151
|
```mermaid
|
|
152
152
|
flowchart TD
|
|
@@ -159,18 +159,16 @@ flowchart TD
|
|
|
159
159
|
ChkBUndef -->|Yes| RetA["return a"]
|
|
160
160
|
ChkBUndef -->|No| ChkBVal{"b is Value?\n(primitive/null/array)"}
|
|
161
161
|
ChkBVal -->|Yes| ChkAVal{"a is Value?"}
|
|
162
|
-
ChkAVal -->|Yes|
|
|
163
|
-
|
|
164
|
-
ChkBothArr -->|No| RetBVal["return b\n(right-side wins)"]
|
|
165
|
-
ChkAVal -->|No| MergeValObj["Keep a's severity/reason\nReplace value\n(arrays concatenated)"]
|
|
162
|
+
ChkAVal -->|Yes| RetBVal["return b\n(right-side wins,\narrays override)"]
|
|
163
|
+
ChkAVal -->|No| MergeValObj["Keep a's severity/reason\nOverride value with b"]
|
|
166
164
|
ChkBVal -->|No| MergeObj["severity: b ?? a\nvalue: b ?? a\noptions: mergeObject(a, b)\nreason: b ?? a"]
|
|
167
165
|
```
|
|
168
166
|
|
|
169
167
|
**Key Design Decisions:**
|
|
170
168
|
|
|
171
169
|
1. **`false` is absolute disable** -- If the override is `false` (or `{value: false}`), the result is always `false`, regardless of what the base config says
|
|
172
|
-
2. **Array values
|
|
173
|
-
3. **options uses
|
|
170
|
+
2. **Array values override** -- `["a","b"]` + `["c","d"]` results in `["c","d"]` (right-side wins), consistent with ESLint and Biome behavior
|
|
171
|
+
3. **options uses shallow merge** -- While severity, value, and reason use right-side-wins precedence, options uses `mergeObject()` (shallow merge via `{...a, ...b}`)
|
|
174
172
|
|
|
175
173
|
### Helper Functions
|
|
176
174
|
|
|
@@ -180,12 +178,12 @@ Concatenates two arrays with optional deduplication:
|
|
|
180
178
|
|
|
181
179
|
- `uniquely=false` -- Simple concatenation, no deduplication
|
|
182
180
|
- `uniquely=true`, no `comparePropName` -- Exact-match deduplication
|
|
183
|
-
- `uniquely=true`, with `comparePropName` -- Deduplicates by the specified property name; when two objects share the same name, they are merged via
|
|
181
|
+
- `uniquely=true`, with `comparePropName` -- Deduplicates by the specified property name; when two objects share the same name, they are shallow-merged via object spread (e.g., plugin settings)
|
|
184
182
|
- Returns `undefined` for empty results
|
|
185
183
|
|
|
186
184
|
#### mergeObject(a, b)
|
|
187
185
|
|
|
188
|
-
|
|
186
|
+
Shallow merges two objects via `{...a, ...b}`. Right-side values take precedence at the top level. Removes undefined properties from the result.
|
|
189
187
|
|
|
190
188
|
#### mergeOverrides(a, b)
|
|
191
189
|
|
|
@@ -193,7 +191,7 @@ Collects the union of all keys from both override records. For each key, calls `
|
|
|
193
191
|
|
|
194
192
|
#### mergePretenders(a, b)
|
|
195
193
|
|
|
196
|
-
Converts array-form pretenders to the normalized `PretenderDetails` form (`{data: [...]}`)
|
|
194
|
+
Converts array-form pretenders to the normalized `PretenderDetails` form (`{data: [...]}`) then applies semantic merge: `files`/`imports` are overridden (right-side wins), `data` is appended (concatenated).
|
|
197
195
|
|
|
198
196
|
## Template Rendering System
|
|
199
197
|
|
|
@@ -217,11 +215,11 @@ This function is used by `nodeRules` and `childNodeRules` with `regexSelector`,
|
|
|
217
215
|
|
|
218
216
|
## Utility Functions
|
|
219
217
|
|
|
220
|
-
| Function | Purpose
|
|
221
|
-
| --------------------- |
|
|
222
|
-
| `cleanOptions()` |
|
|
223
|
-
| `isRuleConfigValue()` | Type guard: returns `true` for primitives, `null`, and arrays (i.e., not a `RuleConfig` object)
|
|
224
|
-
| `deleteUndefProp()` | Removes all properties with `undefined` values from a plain object in-place
|
|
218
|
+
| Function | Purpose |
|
|
219
|
+
| --------------------- | ------------------------------------------------------------------------------------------------- |
|
|
220
|
+
| `cleanOptions()` | Extracts standard fields (`severity`, `value`, `options`, `reason`), removes undefined properties |
|
|
221
|
+
| `isRuleConfigValue()` | Type guard: returns `true` for primitives, `null`, and arrays (i.e., not a `RuleConfig` object) |
|
|
222
|
+
| `deleteUndefProp()` | Removes all properties with `undefined` values from a plain object in-place |
|
|
225
223
|
|
|
226
224
|
## Key Source Files
|
|
227
225
|
|
|
@@ -234,15 +232,14 @@ This function is used by `nodeRules` and `childNodeRules` with `regexSelector`,
|
|
|
234
232
|
|
|
235
233
|
## External Dependencies
|
|
236
234
|
|
|
237
|
-
| Dependency | Purpose
|
|
238
|
-
| ---------------------- |
|
|
239
|
-
| `@markuplint/ml-ast` | `ParserOptions` type (type-only)
|
|
240
|
-
| `@markuplint/selector` | `RegexSelector` type (re-exported)
|
|
241
|
-
| `@markuplint/shared` | `Nullable` utility type
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
244
|
-
| `
|
|
245
|
-
| `type-fest` | `Writable` utility type |
|
|
235
|
+
| Dependency | Purpose |
|
|
236
|
+
| ---------------------- | ---------------------------------------------- |
|
|
237
|
+
| `@markuplint/ml-ast` | `ParserOptions` type (type-only) |
|
|
238
|
+
| `@markuplint/selector` | `RegexSelector` type (re-exported) |
|
|
239
|
+
| `@markuplint/shared` | `Nullable` utility type |
|
|
240
|
+
| `is-plain-object` | Plain object detection in `deleteUndefProp()` |
|
|
241
|
+
| `mustache` | Template rendering engine for `provideValue()` |
|
|
242
|
+
| `type-fest` | `Writable` utility type |
|
|
246
243
|
|
|
247
244
|
## Integration Points
|
|
248
245
|
|
|
@@ -277,6 +274,41 @@ flowchart LR
|
|
|
277
274
|
- **`@markuplint/ml-core`** -- Receives the merged `OptimizedConfig` and applies rules to the parsed document
|
|
278
275
|
- **`@markuplint/rules`** -- Uses `Rule<T,O>` and `RuleConfig<T,O>` types to define rule implementations
|
|
279
276
|
|
|
277
|
+
## Design Decisions
|
|
278
|
+
|
|
279
|
+
### Why Not Flat Config?
|
|
280
|
+
|
|
281
|
+
ESLint's Flat Config approach was evaluated and rejected for markuplint:
|
|
282
|
+
|
|
283
|
+
- **ESLint itself re-added `extends` to Flat Config in March 2025** -- The pure flat approach proved insufficient even for JavaScript tooling
|
|
284
|
+
- **markuplint is JSON-based** -- Flat Config assumes JavaScript. HTML/markup developers (the primary audience) benefit from JSON's schema validation and language-agnostic editing
|
|
285
|
+
- **markuplint's `nodeRules`/`childNodeRules` are CSS-selector-based** -- These have no equivalent in Flat Config's file-pattern model
|
|
286
|
+
- **ESLint v9 migration caused significant community pain** -- The gradual transition required automated migration tools and years of ecosystem adaptation
|
|
287
|
+
|
|
288
|
+
**Conclusion:** Improving the JSON-based `extends` merge strategy is the optimal approach for markuplint.
|
|
289
|
+
|
|
290
|
+
### Merge Strategy Principles
|
|
291
|
+
|
|
292
|
+
There is a logical distinction between how arrays are handled:
|
|
293
|
+
|
|
294
|
+
| Array Type | Examples | Merge Behavior | Rationale |
|
|
295
|
+
| ------------------------- | -------------------------------------- | -------------- | -------------------------------------- |
|
|
296
|
+
| **Top-level collections** | `plugins`, `excludeFiles`, `nodeRules` | Accumulate | Independent items forming a collection |
|
|
297
|
+
| **Rule values** | `["allowed-tag-1", "allowed-tag-2"]` | Override | A single rule's configuration value |
|
|
298
|
+
|
|
299
|
+
This aligns with ESLint and Biome, where rule values are always overridden by the more specific config.
|
|
300
|
+
|
|
301
|
+
### Shallow Merge over Deep Merge
|
|
302
|
+
|
|
303
|
+
| Tool | Rule options merge | Rationale |
|
|
304
|
+
| ---------- | ---------------------------- | --------------------------------------------------------------------- |
|
|
305
|
+
| ESLint | Complete replacement | Simplest, but can be surprising |
|
|
306
|
+
| Biome | Deep merge (via Merge trait) | Full flexibility, higher complexity |
|
|
307
|
+
| markuplint | **Shallow merge** | Middle ground: top-level keys are merged, nested objects are replaced |
|
|
308
|
+
|
|
309
|
+
The `deepmerge` library was removed in favor of simple object spread (`{...a, ...b}`). This is sufficient because all merged objects in markuplint config (parser, specs, parserOptions, severity, plugin settings, rule options) are flat key-value maps.
|
|
310
|
+
|
|
280
311
|
## Documentation Map
|
|
281
312
|
|
|
313
|
+
- [Migration Guide](../../docs/migration/v4-v5/config.md) -- Breaking changes between major versions
|
|
282
314
|
- [Maintenance Guide](docs/maintenance.md) -- Commands, recipes, and troubleshooting
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,45 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [5.0.0-alpha.0](https://github.com/markuplint/markuplint/compare/v4.14.1...v5.0.0-alpha.0) (2026-02-20)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- use explicit `export type` for type-only re-exports ([7c77c05](https://github.com/markuplint/markuplint/commit/7c77c05619518c8d18a183132040f5b2cd0ab6ec))
|
|
11
|
+
|
|
12
|
+
### Code Refactoring
|
|
13
|
+
|
|
14
|
+
- **ml-config:** remove deprecated rule types ([e5d2b2d](https://github.com/markuplint/markuplint/commit/e5d2b2d6b5d7f6a060e1ea2160be97ad3ca02084))
|
|
15
|
+
|
|
16
|
+
- refactor(ml-config)!: change pretenders merge behavior ([e7b00ab](https://github.com/markuplint/markuplint/commit/e7b00abd80dd75a6060697b30d59d0371ae3694b))
|
|
17
|
+
- refactor(ml-config)!: change rule value array merge to override ([05c23ac](https://github.com/markuplint/markuplint/commit/05c23ace31a3429233b3411c8b95ae62438be6e5)), closes [#1104](https://github.com/markuplint/markuplint/issues/1104)
|
|
18
|
+
- refactor(ml-config)!: replace deepmerge with shallow merge ([15b4945](https://github.com/markuplint/markuplint/commit/15b494546b9016189a790b2ea49fcc2bb38c85c4))
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- **ml-config:** add name and specConformance properties to nodeRule types ([af53042](https://github.com/markuplint/markuplint/commit/af5304218f7a207a1d8e61464c81c42d5ee1bf01))
|
|
23
|
+
- **ml-config:** add named rule group types, merge logic, and type guards ([9f625fd](https://github.com/markuplint/markuplint/commit/9f625fdcd9bc821d2be53668ac0eb676597aa935))
|
|
24
|
+
- **ml-config:** add ruleCommonSettings.ariaVersion option ([f2cd713](https://github.com/markuplint/markuplint/commit/f2cd7132311c00c22d68c4685b4a280b77ee6463))
|
|
25
|
+
|
|
26
|
+
### BREAKING CHANGES
|
|
27
|
+
|
|
28
|
+
- Pretender files/imports are now overridden instead
|
|
29
|
+
of deep-merged. Pretender data continues to be appended.
|
|
30
|
+
|
|
31
|
+
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
32
|
+
|
|
33
|
+
- Rule value arrays are now overridden instead of
|
|
34
|
+
- Object properties in config are now shallow-merged
|
|
35
|
+
instead of deep-merged. Nested objects within parser, specs, etc.
|
|
36
|
+
will be replaced entirely by the overriding config.
|
|
37
|
+
|
|
38
|
+
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
39
|
+
|
|
40
|
+
- **ml-config:** RuleV2, RuleConfigV2, AnyRuleV2 types are removed.
|
|
41
|
+
The deprecated `option` field is no longer supported; use `options`.
|
|
42
|
+
|
|
43
|
+
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
44
|
+
|
|
6
45
|
## [4.8.15](https://github.com/markuplint/markuplint/compare/@markuplint/ml-config@4.8.14...@markuplint/ml-config@4.8.15) (2026-02-10)
|
|
7
46
|
|
|
8
47
|
**Note:** Version bump only for package @markuplint/ml-config
|
package/SKILL.md
CHANGED
|
@@ -49,7 +49,7 @@ Add a new property to the Config type and implement its merge strategy. Follow r
|
|
|
49
49
|
|
|
50
50
|
1. Read `src/merge-config.ts`
|
|
51
51
|
2. Add the merge logic inside `mergeConfig()`, choosing the appropriate strategy:
|
|
52
|
-
- `mergeObject()` for object
|
|
52
|
+
- `mergeObject()` for object shallow merge
|
|
53
53
|
- `concatArray()` for array concatenation
|
|
54
54
|
- `b.prop ?? a.prop` for simple right-side precedence
|
|
55
55
|
3. If the property needs format conversion (like plugins or pretenders), implement a conversion helper
|
|
@@ -94,8 +94,8 @@ Modify the rule merge logic in `mergeRule()`. Follow recipe #3 in `docs/maintena
|
|
|
94
94
|
|
|
95
95
|
1. Make changes to `mergeRule()`, paying attention to:
|
|
96
96
|
- The `false` absolute disable behavior
|
|
97
|
-
- Array
|
|
98
|
-
-
|
|
97
|
+
- Array values override (right-side wins)
|
|
98
|
+
- Shallow merge for options via `mergeObject()`
|
|
99
99
|
- Right-side precedence for severity, value, reason
|
|
100
100
|
|
|
101
101
|
### Step 3: Verify
|
package/docs/maintenance.ja.md
CHANGED
|
@@ -61,7 +61,7 @@ expect(exchangeValueOnRule({ value: '{{ var }}' }, { var: 'x' })).toStrictEqual(
|
|
|
61
61
|
- トップレベル専用(`$schema`、`extends` のように)なら、`NoInherit` ユニオン型にプロパティ名を追加
|
|
62
62
|
- ファイルパターンごとにオーバーライド可能にする場合はそのまま(`Omit<Config, NoInherit>` 経由で `Config` から継承)
|
|
63
63
|
5. `src/merge-config.ts` を読み、`mergeConfig()` 関数の config オブジェクト内にマージロジックを追加:
|
|
64
|
-
- オブジェクト
|
|
64
|
+
- オブジェクト shallow merge: `newProp: mergeObject(a.newProp, b.newProp)`
|
|
65
65
|
- 配列結合: `newProp: concatArray(a.newProp, b.newProp)`
|
|
66
66
|
- 配列結合+重複排除: `newProp: concatArray(a.newProp, b.newProp, true)`
|
|
67
67
|
- 単純な右辺優先: `newProp: b.newProp ?? a.newProp`(スプレッドで処理されるが、明示的な方が分かりやすい)
|
|
@@ -74,7 +74,7 @@ expect(exchangeValueOnRule({ value: '{{ var }}' }, { var: 'x' })).toStrictEqual(
|
|
|
74
74
|
1. `src/merge-config.ts` を読み、`mergeConfig()` 関数内のプロパティを確認
|
|
75
75
|
2. 現在の戦略を特定(`ARCHITECTURE.md` の戦略テーブルを参照)
|
|
76
76
|
3. マージ呼び出しを置換。利用可能な戦略:
|
|
77
|
-
- `mergeObject(a.prop, b.prop)` -- 右辺優先の
|
|
77
|
+
- `mergeObject(a.prop, b.prop)` -- 右辺優先の shallow merge
|
|
78
78
|
- `concatArray(a.prop, b.prop)` -- 単純な配列結合
|
|
79
79
|
- `concatArray(a.prop, b.prop, true)` -- 重複排除付き結合
|
|
80
80
|
- `concatArray(a.prop, b.prop, true, 'name')` -- 名前付きプロパティで重複排除、同名オブジェクトをマージ
|
|
@@ -88,15 +88,15 @@ expect(exchangeValueOnRule({ value: '{{ var }}' }, { var: 'x' })).toStrictEqual(
|
|
|
88
88
|
|
|
89
89
|
1. `src/merge-config.ts` を読み、`mergeRule()` 関数を確認
|
|
90
90
|
2. 現在のフローを理解:
|
|
91
|
-
- `optimizeRule()`
|
|
91
|
+
- `optimizeRule()` が両方の入力を正規化
|
|
92
92
|
- `false` チェック: override が `false` または `{value: false}` なら常に `false` を返す
|
|
93
93
|
- `undefined` チェック: 片方がない場合はもう片方を返す
|
|
94
|
-
- 値型チェック: override が直接値(primitive/null/array
|
|
95
|
-
- オブジェクト型マージ: severity/value/reason は右辺優先、options は
|
|
94
|
+
- 値型チェック: override が直接値(primitive/null/array)ならベース値を上書き
|
|
95
|
+
- オブジェクト型マージ: severity/value/reason は右辺優先、options は shallow merge
|
|
96
96
|
3. 変更を加える際、主要な不変条件を保持:
|
|
97
97
|
- `false` は常に絶対無効化になる必要がある
|
|
98
|
-
-
|
|
99
|
-
- `options` は `mergeObject()` による
|
|
98
|
+
- 配列値は上書き(右辺優先)であり、連結ではない
|
|
99
|
+
- `options` は `mergeObject()` による shallow merge が必要
|
|
100
100
|
4. `src/merge-config.spec.ts` の既存テストが通ることを確認
|
|
101
101
|
5. 変更後の動作に対する新しいテストケースを追加
|
|
102
102
|
6. ビルド: `yarn build --scope @markuplint/ml-config`
|
|
@@ -107,10 +107,10 @@ expect(exchangeValueOnRule({ value: '{{ var }}' }, { var: 'x' })).toStrictEqual(
|
|
|
107
107
|
1. `src/types.ts` を読み、`Pretender`、`PretenderDetails`、`OriginalNode` を確認
|
|
108
108
|
2. 適切な型に新しいフィールドを追加
|
|
109
109
|
3. `src/merge-config.ts` を読み、`mergePretenders()` を確認:
|
|
110
|
-
- 配列形式を `{data: [...]}` に変換(`
|
|
111
|
-
- `
|
|
112
|
-
- `
|
|
113
|
-
- `
|
|
110
|
+
- 配列形式を `{data: [...]}` に変換(`toPretenderDetails()`)
|
|
111
|
+
- `files`/`imports` は上書き(右辺優先)
|
|
112
|
+
- `data` 配列は連結(追加)
|
|
113
|
+
- `PretenderDetails` の新しいフィールドは `mergePretenders()` 内で明示的に処理が必要
|
|
114
114
|
4. `src/merge-config.spec.ts` にテストケースを追加
|
|
115
115
|
5. ビルド: `yarn build --scope @markuplint/ml-config`
|
|
116
116
|
6. テスト: `yarn test --scope @markuplint/ml-config`
|
|
@@ -145,20 +145,18 @@ yarn test --scope @markuplint/ml-config
|
|
|
145
145
|
2. 両方の入力に値がある場合にヘルパー関数が `undefined` を返していないか検証
|
|
146
146
|
3. `concatArray()` は空配列に対して `undefined` を返す -- 入力が空でないことを確認
|
|
147
147
|
|
|
148
|
-
###
|
|
148
|
+
### ルール値が予期しない上書きになる
|
|
149
149
|
|
|
150
|
-
**症状:**
|
|
150
|
+
**症状:** ルールの配列値がベースの値を保持せず、完全に置き換えられる。
|
|
151
151
|
|
|
152
|
-
**原因:** `mergeRule()`
|
|
152
|
+
**原因:** `mergeRule()` は設計上、配列値を上書きする(右辺優先)。これは ESLint や Biome と一貫した動作。
|
|
153
153
|
|
|
154
|
-
**解決策:**
|
|
154
|
+
**解決策:** これは期待される動作です。両方の値が必要な場合は、単一の設定で手動で配列を統合:
|
|
155
155
|
|
|
156
156
|
```json
|
|
157
|
-
{ "value": ["
|
|
157
|
+
{ "value": ["base-tag-1", "base-tag-2", "new-tag-1"], "options": {} }
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
これにより連結ではなく値が完全に置換される。
|
|
161
|
-
|
|
162
160
|
### プラグインの settings がマージされない
|
|
163
161
|
|
|
164
162
|
**症状:** 同名プラグインの2つの設定で、片方の settings しか反映されない。
|
package/docs/maintenance.md
CHANGED
|
@@ -61,7 +61,7 @@ expect(exchangeValueOnRule({ value: '{{ var }}' }, { var: 'x' })).toStrictEqual(
|
|
|
61
61
|
- If it should be top-level only (like `$schema`, `extends`), add the property name to the `NoInherit` union type
|
|
62
62
|
- If it should be overridable per file pattern, leave it as-is (it inherits from `Config` via `Omit<Config, NoInherit>`)
|
|
63
63
|
5. Read `src/merge-config.ts` and add the merge logic inside the `mergeConfig()` function's config object:
|
|
64
|
-
- For object
|
|
64
|
+
- For object shallow merge: `newProp: mergeObject(a.newProp, b.newProp)`
|
|
65
65
|
- For array concatenation: `newProp: concatArray(a.newProp, b.newProp)`
|
|
66
66
|
- For array with deduplication: `newProp: concatArray(a.newProp, b.newProp, true)`
|
|
67
67
|
- For simple right-side precedence: `newProp: b.newProp ?? a.newProp` (handled by the spread, but explicit is clearer)
|
|
@@ -74,7 +74,7 @@ expect(exchangeValueOnRule({ value: '{{ var }}' }, { var: 'x' })).toStrictEqual(
|
|
|
74
74
|
1. Read `src/merge-config.ts` and locate the property in the `mergeConfig()` function
|
|
75
75
|
2. Identify the current strategy (see the strategy table in `ARCHITECTURE.md`)
|
|
76
76
|
3. Replace the merge call. Available strategies:
|
|
77
|
-
- `mergeObject(a.prop, b.prop)` --
|
|
77
|
+
- `mergeObject(a.prop, b.prop)` -- Shallow merge with right-side precedence
|
|
78
78
|
- `concatArray(a.prop, b.prop)` -- Simple array concatenation
|
|
79
79
|
- `concatArray(a.prop, b.prop, true)` -- Concat with deduplication
|
|
80
80
|
- `concatArray(a.prop, b.prop, true, 'name')` -- Concat with deduplication by named property, merging same-name objects
|
|
@@ -88,15 +88,15 @@ expect(exchangeValueOnRule({ value: '{{ var }}' }, { var: 'x' })).toStrictEqual(
|
|
|
88
88
|
|
|
89
89
|
1. Read `src/merge-config.ts` and locate the `mergeRule()` function
|
|
90
90
|
2. Understand the current flow:
|
|
91
|
-
- `optimizeRule()` normalizes both inputs
|
|
91
|
+
- `optimizeRule()` normalizes both inputs
|
|
92
92
|
- `false` check: override `false` or `{value: false}` always returns `false`
|
|
93
93
|
- `undefined` checks: missing side returns the other side
|
|
94
|
-
- Value type check: if override is a direct value (primitive/null/array), it replaces
|
|
95
|
-
- Object type merge: severity/value/reason use right-side precedence, options use
|
|
94
|
+
- Value type check: if override is a direct value (primitive/null/array), it replaces the base value
|
|
95
|
+
- Object type merge: severity/value/reason use right-side precedence, options use shallow merge
|
|
96
96
|
3. Make changes, preserving the key invariants:
|
|
97
97
|
- `false` must always result in absolute disable
|
|
98
|
-
- Array values must
|
|
99
|
-
- `options` must use
|
|
98
|
+
- Array values must override (right-side wins), not concatenate
|
|
99
|
+
- `options` must use shallow merge via `mergeObject()`
|
|
100
100
|
4. Verify existing tests pass in `src/merge-config.spec.ts`
|
|
101
101
|
5. Add new test cases for the modified behavior
|
|
102
102
|
6. Build: `yarn build --scope @markuplint/ml-config`
|
|
@@ -107,10 +107,10 @@ expect(exchangeValueOnRule({ value: '{{ var }}' }, { var: 'x' })).toStrictEqual(
|
|
|
107
107
|
1. Read `src/types.ts` and locate `Pretender`, `PretenderDetails`, and `OriginalNode`
|
|
108
108
|
2. Add new fields to the appropriate type
|
|
109
109
|
3. Read `src/merge-config.ts` and check `mergePretenders()`:
|
|
110
|
-
- It converts array form to `{data: [...]}` via `
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
- New fields on `
|
|
110
|
+
- It converts array form to `{data: [...]}` via `toPretenderDetails()`
|
|
111
|
+
- `files`/`imports` are overridden (right-side wins)
|
|
112
|
+
- `data` arrays are concatenated (appended)
|
|
113
|
+
- New fields on `PretenderDetails` need explicit handling in `mergePretenders()`
|
|
114
114
|
4. Add test cases in `src/merge-config.spec.ts`
|
|
115
115
|
5. Build: `yarn build --scope @markuplint/ml-config`
|
|
116
116
|
6. Test: `yarn test --scope @markuplint/ml-config`
|
|
@@ -145,20 +145,18 @@ yarn test --scope @markuplint/ml-config
|
|
|
145
145
|
2. Verify the helper function does not return `undefined` when both inputs have values
|
|
146
146
|
3. `concatArray()` returns `undefined` for empty arrays -- ensure the inputs are not empty
|
|
147
147
|
|
|
148
|
-
### Rule values
|
|
148
|
+
### Rule values unexpectedly replaced
|
|
149
149
|
|
|
150
|
-
**Symptom:** A rule's array value
|
|
150
|
+
**Symptom:** A rule's array value is replaced entirely instead of keeping both base and override values.
|
|
151
151
|
|
|
152
|
-
**Cause:** `mergeRule()`
|
|
152
|
+
**Cause:** `mergeRule()` overrides array values by design (right-side wins). This is consistent with ESLint and Biome behavior.
|
|
153
153
|
|
|
154
|
-
**Solution:**
|
|
154
|
+
**Solution:** This is the expected behavior. If you need both sets of values, manually combine the arrays in a single config:
|
|
155
155
|
|
|
156
156
|
```json
|
|
157
|
-
{ "value": ["
|
|
157
|
+
{ "value": ["base-tag-1", "base-tag-2", "new-tag-1"], "options": {} }
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
This replaces the value entirely instead of concatenating.
|
|
161
|
-
|
|
162
160
|
### Plugin settings are not merged
|
|
163
161
|
|
|
164
162
|
**Symptom:** Two configs with the same plugin name result in settings from only one side.
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/merge-config.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type { Config, AnyRule,
|
|
1
|
+
import type { Config, AnyRule, OptimizedConfig } from './types.js';
|
|
2
2
|
import type { Nullable } from '@markuplint/shared';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Merges two markuplint configurations into an optimized result.
|
|
5
5
|
*
|
|
6
6
|
* Plugins, arrays, and rules are merged with specific strategies:
|
|
7
|
-
* - Plugins are concatenated and deduplicated by name
|
|
7
|
+
* - Plugins are concatenated and deduplicated by name (settings shallow-merged)
|
|
8
8
|
* - Arrays (excludeFiles, nodeRules, childNodeRules) are concatenated
|
|
9
9
|
* - Rules are merged per-key with right-side precedence
|
|
10
|
+
* - Objects (parser, specs, etc.) are shallow-merged
|
|
10
11
|
* - The `extends` property is removed from the result when `b` is provided
|
|
11
12
|
*
|
|
12
13
|
* @param a - The base configuration
|
|
@@ -18,11 +19,12 @@ export declare function mergeConfig(a: Config, b?: Config): OptimizedConfig;
|
|
|
18
19
|
* Merges two rule configurations with right-side precedence.
|
|
19
20
|
*
|
|
20
21
|
* If `b` is `false`, the rule is unconditionally disabled.
|
|
21
|
-
* If `b` is a direct value, it
|
|
22
|
-
* If both are full config objects, their properties are merged
|
|
22
|
+
* If `b` is a direct value (including arrays), it overrides `a`.
|
|
23
|
+
* If both are full config objects, their properties are merged
|
|
24
|
+
* (severity/value/reason: right-side wins, options: shallow-merged).
|
|
23
25
|
*
|
|
24
26
|
* @param a - The base rule configuration (may be `null` or `undefined`)
|
|
25
27
|
* @param b - The rule configuration to merge on top
|
|
26
28
|
* @returns The merged rule configuration
|
|
27
29
|
*/
|
|
28
|
-
export declare function mergeRule(a: Nullable<AnyRule
|
|
30
|
+
export declare function mergeRule(a: Nullable<AnyRule>, b: AnyRule): AnyRule;
|
package/lib/merge-config.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { deleteUndefProp, cleanOptions, isRuleConfigValue } from './utils.js';
|
|
1
|
+
import { deleteUndefProp, cleanOptions, isRuleConfigValue, isNamedRuleGroup } from './utils.js';
|
|
3
2
|
/**
|
|
4
|
-
*
|
|
3
|
+
* Merges two markuplint configurations into an optimized result.
|
|
5
4
|
*
|
|
6
5
|
* Plugins, arrays, and rules are merged with specific strategies:
|
|
7
|
-
* - Plugins are concatenated and deduplicated by name
|
|
6
|
+
* - Plugins are concatenated and deduplicated by name (settings shallow-merged)
|
|
8
7
|
* - Arrays (excludeFiles, nodeRules, childNodeRules) are concatenated
|
|
9
8
|
* - Rules are merged per-key with right-side precedence
|
|
9
|
+
* - Objects (parser, specs, etc.) are shallow-merged
|
|
10
10
|
* - The `extends` property is removed from the result when `b` is provided
|
|
11
11
|
*
|
|
12
12
|
* @param a - The base configuration
|
|
@@ -19,6 +19,7 @@ export function mergeConfig(a, b) {
|
|
|
19
19
|
const config = {
|
|
20
20
|
...a,
|
|
21
21
|
...b,
|
|
22
|
+
ruleCommonSettings: mergeObject(a.ruleCommonSettings, b.ruleCommonSettings),
|
|
22
23
|
plugins: concatArray(a.plugins, b.plugins, true, 'name')?.map(plugin => {
|
|
23
24
|
if (typeof plugin === 'string') {
|
|
24
25
|
return {
|
|
@@ -36,8 +37,8 @@ export function mergeConfig(a, b) {
|
|
|
36
37
|
rules: mergeRules(
|
|
37
38
|
// TODO: Deep merge
|
|
38
39
|
a.rules, b.rules),
|
|
39
|
-
nodeRules: concatArray(a.nodeRules, b.nodeRules),
|
|
40
|
-
childNodeRules: concatArray(a.childNodeRules, b.childNodeRules),
|
|
40
|
+
nodeRules: concatArray(a.nodeRules, b.nodeRules, true, 'name'),
|
|
41
|
+
childNodeRules: concatArray(a.childNodeRules, b.childNodeRules, true, 'name'),
|
|
41
42
|
overrideMode: b.overrideMode ?? a.overrideMode,
|
|
42
43
|
overrides: mergeOverrides(a.overrides, b.overrides),
|
|
43
44
|
extends: concatArray(toReadonlyArray(a.extends), toReadonlyArray(b.extends)),
|
|
@@ -53,8 +54,9 @@ export function mergeConfig(a, b) {
|
|
|
53
54
|
* Merges two rule configurations with right-side precedence.
|
|
54
55
|
*
|
|
55
56
|
* If `b` is `false`, the rule is unconditionally disabled.
|
|
56
|
-
* If `b` is a direct value, it
|
|
57
|
-
* If both are full config objects, their properties are merged
|
|
57
|
+
* If `b` is a direct value (including arrays), it overrides `a`.
|
|
58
|
+
* If both are full config objects, their properties are merged
|
|
59
|
+
* (severity/value/reason: right-side wins, options: shallow-merged).
|
|
58
60
|
*
|
|
59
61
|
* @param a - The base rule configuration (may be `null` or `undefined`)
|
|
60
62
|
* @param b - The rule configuration to merge on top
|
|
@@ -77,13 +79,9 @@ export function mergeRule(a, b) {
|
|
|
77
79
|
}
|
|
78
80
|
if (isRuleConfigValue(oB)) {
|
|
79
81
|
if (isRuleConfigValue(oA)) {
|
|
80
|
-
if (Array.isArray(oA) && Array.isArray(oB)) {
|
|
81
|
-
return [...oA, ...oB];
|
|
82
|
-
}
|
|
83
82
|
return oB;
|
|
84
83
|
}
|
|
85
|
-
const
|
|
86
|
-
const res = cleanOptions({ ...oA, value });
|
|
84
|
+
const res = cleanOptions({ ...oA, value: oB });
|
|
87
85
|
deleteUndefProp(res);
|
|
88
86
|
return res;
|
|
89
87
|
}
|
|
@@ -104,13 +102,25 @@ function mergePretenders(a, b) {
|
|
|
104
102
|
if (!a && !b) {
|
|
105
103
|
return;
|
|
106
104
|
}
|
|
107
|
-
const aDetails = a ?
|
|
108
|
-
const bDetails = b ?
|
|
109
|
-
|
|
105
|
+
const aDetails = a ? toPretenderDetails(a) : undefined;
|
|
106
|
+
const bDetails = b ? toPretenderDetails(b) : undefined;
|
|
107
|
+
if (!aDetails) {
|
|
108
|
+
return bDetails;
|
|
109
|
+
}
|
|
110
|
+
if (!bDetails) {
|
|
111
|
+
return aDetails;
|
|
112
|
+
}
|
|
113
|
+
// files/imports: override (right-side wins)
|
|
114
|
+
// data: append (concatenate)
|
|
115
|
+
const details = {
|
|
116
|
+
files: bDetails.files ?? aDetails.files,
|
|
117
|
+
imports: bDetails.imports ?? aDetails.imports,
|
|
118
|
+
data: concatArray(aDetails.data, bDetails.data),
|
|
119
|
+
};
|
|
110
120
|
deleteUndefProp(details);
|
|
111
121
|
return details;
|
|
112
122
|
}
|
|
113
|
-
function
|
|
123
|
+
function toPretenderDetails(pretenders) {
|
|
114
124
|
if (isReadonlyArray(pretenders)) {
|
|
115
125
|
return {
|
|
116
126
|
data: pretenders,
|
|
@@ -148,7 +158,7 @@ function mergeObject(a, b) {
|
|
|
148
158
|
if (b == null) {
|
|
149
159
|
return a ?? undefined;
|
|
150
160
|
}
|
|
151
|
-
const res =
|
|
161
|
+
const res = { ...a, ...b };
|
|
152
162
|
deleteUndefProp(res);
|
|
153
163
|
return res;
|
|
154
164
|
}
|
|
@@ -180,12 +190,7 @@ function concatArray(a, b, uniquely = false, comparePropName) {
|
|
|
180
190
|
return;
|
|
181
191
|
}
|
|
182
192
|
const existed = newArray[existedIndex];
|
|
183
|
-
|
|
184
|
-
if (!merged) {
|
|
185
|
-
newArray.push(item);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
newArray.splice(existedIndex, 1, merged);
|
|
193
|
+
newArray.splice(existedIndex, 1, { ...existed, ...item });
|
|
189
194
|
}
|
|
190
195
|
// eslint-disable-next-line unicorn/no-array-for-each
|
|
191
196
|
a?.forEach(concat);
|
|
@@ -205,6 +210,15 @@ function getName(item, comparePropName) {
|
|
|
205
210
|
}
|
|
206
211
|
return null;
|
|
207
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Merges two Rules dictionaries. Keys containing `/` use named rule group
|
|
215
|
+
* merge semantics ({@link mergeNamedRuleGroupEntry}); other keys use standard
|
|
216
|
+
* rule merge semantics ({@link mergeRule}).
|
|
217
|
+
*
|
|
218
|
+
* @param a - The base rules (lower priority)
|
|
219
|
+
* @param b - The override rules (higher priority)
|
|
220
|
+
* @returns The merged rules, or `undefined` if both inputs are nullish
|
|
221
|
+
*/
|
|
208
222
|
function mergeRules(a, b) {
|
|
209
223
|
if (a == null) {
|
|
210
224
|
return b && optimizeRules(b);
|
|
@@ -214,17 +228,62 @@ function mergeRules(a, b) {
|
|
|
214
228
|
}
|
|
215
229
|
const res = optimizeRules(a);
|
|
216
230
|
for (const [key, rule] of Object.entries(b)) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
res[key] =
|
|
231
|
+
if (key.includes('/')) {
|
|
232
|
+
// Named rule group key: special merge semantics
|
|
233
|
+
res[key] = mergeNamedRuleGroupEntry(res[key], rule);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const merged = mergeRule(res[key], rule);
|
|
237
|
+
if (merged != null) {
|
|
238
|
+
res[key] = merged;
|
|
239
|
+
}
|
|
220
240
|
}
|
|
221
241
|
}
|
|
222
242
|
deleteUndefProp(res);
|
|
223
243
|
return Object.freeze(res);
|
|
224
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Merges a named rule group entry (key containing `/`).
|
|
247
|
+
*
|
|
248
|
+
* - `false` disables the group entirely
|
|
249
|
+
* - A partial override object (e.g., `{ severity: "warning" }`) is merged into the existing NamedRuleGroup
|
|
250
|
+
* - Otherwise, right side wins
|
|
251
|
+
*
|
|
252
|
+
* @param a - The existing entry from a lower-priority config (may be a NamedRuleGroup)
|
|
253
|
+
* @param b - The overriding entry from a higher-priority config
|
|
254
|
+
* @returns The merged entry
|
|
255
|
+
*/
|
|
256
|
+
function mergeNamedRuleGroupEntry(a, b) {
|
|
257
|
+
// false disables the group
|
|
258
|
+
if (b === false) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
// Partial override: object without `rules` merging into an existing NamedRuleGroup
|
|
262
|
+
// Only merge valid NamedRuleGroup keys to avoid contamination from RuleConfig keys
|
|
263
|
+
if (typeof b === 'object' && b !== null && !isNamedRuleGroup(b) && a !== undefined && isNamedRuleGroup(a)) {
|
|
264
|
+
const bObj = b;
|
|
265
|
+
const override = {};
|
|
266
|
+
if ('severity' in bObj) {
|
|
267
|
+
override.severity = bObj.severity;
|
|
268
|
+
}
|
|
269
|
+
if ('specConformance' in bObj) {
|
|
270
|
+
override.specConformance = bObj.specConformance;
|
|
271
|
+
}
|
|
272
|
+
const merged = { ...a, ...override };
|
|
273
|
+
deleteUndefProp(merged);
|
|
274
|
+
return merged;
|
|
275
|
+
}
|
|
276
|
+
// Right side wins for everything else
|
|
277
|
+
return b;
|
|
278
|
+
}
|
|
225
279
|
function optimizeRules(rules) {
|
|
226
280
|
const res = {};
|
|
227
281
|
for (const [key, rule] of Object.entries(rules)) {
|
|
282
|
+
// Pass through NamedRuleGroup entries without optimization
|
|
283
|
+
if (key.includes('/') && isNamedRuleGroup(rule)) {
|
|
284
|
+
res[key] = rule;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
228
287
|
const _rule = optimizeRule(rule);
|
|
229
288
|
if (_rule != null) {
|
|
230
289
|
res[key] = _rule;
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ParserOptions } from '@markuplint/ml-ast';
|
|
2
|
+
import type { ARIAVersion } from '@markuplint/ml-spec';
|
|
2
3
|
import type { RegexSelector } from '@markuplint/selector';
|
|
3
4
|
import type { Nullable } from '@markuplint/shared';
|
|
4
5
|
export type { RegexSelector } from '@markuplint/selector';
|
|
@@ -8,6 +9,7 @@ export type { RegexSelector } from '@markuplint/selector';
|
|
|
8
9
|
*/
|
|
9
10
|
export type Config = {
|
|
10
11
|
readonly $schema?: string;
|
|
12
|
+
readonly ruleCommonSettings?: RuleCommonSettings;
|
|
11
13
|
readonly extends?: string | readonly string[];
|
|
12
14
|
readonly plugins?: readonly (PluginConfig | string)[];
|
|
13
15
|
readonly parser?: ParserConfig;
|
|
@@ -22,6 +24,13 @@ export type Config = {
|
|
|
22
24
|
readonly overrideMode?: 'merge' | 'reset';
|
|
23
25
|
readonly overrides?: Readonly<Record<string, OverrideConfig>>;
|
|
24
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Common settings applied globally to all rules.
|
|
29
|
+
* These values serve as fallbacks when individual rules do not specify their own.
|
|
30
|
+
*/
|
|
31
|
+
export type RuleCommonSettings = {
|
|
32
|
+
readonly ariaVersion?: ARIAVersion;
|
|
33
|
+
};
|
|
25
34
|
/**
|
|
26
35
|
* A primitive scalar value that can appear in rule configuration.
|
|
27
36
|
*/
|
|
@@ -84,6 +93,10 @@ export type SpecConfig = {
|
|
|
84
93
|
export type SeverityOptions = {
|
|
85
94
|
readonly parseError?: Severity | 'off' | boolean;
|
|
86
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* Normalized form of pretender configuration used after merging.
|
|
98
|
+
* Contains optional file references, import paths, and inline pretender data.
|
|
99
|
+
*/
|
|
87
100
|
export type PretenderDetails = {
|
|
88
101
|
/**
|
|
89
102
|
* @experimental
|
|
@@ -102,6 +115,11 @@ export type PretenderFileData = {
|
|
|
102
115
|
readonly version: string;
|
|
103
116
|
readonly data: readonly Pretender[];
|
|
104
117
|
};
|
|
118
|
+
/**
|
|
119
|
+
* Defines a mapping from a custom element (matched by CSS selector) to a standard
|
|
120
|
+
* HTML element for linting purposes, allowing rules to treat custom components
|
|
121
|
+
* as if they were native elements.
|
|
122
|
+
*/
|
|
105
123
|
export type Pretender = {
|
|
106
124
|
/**
|
|
107
125
|
* Target node selectors
|
|
@@ -265,23 +283,48 @@ export interface PretenderScanOptions {
|
|
|
265
283
|
* @template O - The type of the rule's options
|
|
266
284
|
*/
|
|
267
285
|
export type Rule<T extends RuleConfigValue, O extends PlainData = undefined> = RuleConfig<T, O> | Readonly<T> | boolean;
|
|
268
|
-
/**
|
|
269
|
-
* @deprecated
|
|
270
|
-
*/
|
|
271
|
-
export type RuleV2<T extends RuleConfigValue, O extends PlainData = undefined> = RuleConfigV2<T, O> | Readonly<T> | boolean;
|
|
272
286
|
/**
|
|
273
287
|
* A rule setting with any value and option types.
|
|
274
288
|
*/
|
|
275
289
|
export type AnyRule = Rule<RuleConfigValue, PlainData>;
|
|
276
290
|
/**
|
|
277
|
-
*
|
|
291
|
+
* A named rule group in the `rules` section.
|
|
292
|
+
* Keys containing `/` in `rules` are treated as named rule groups.
|
|
293
|
+
* Each group wraps one or more base rules under a namespace,
|
|
294
|
+
* enabling per-check control and spec conformance metadata.
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```jsonc
|
|
298
|
+
* {
|
|
299
|
+
* "rules": {
|
|
300
|
+
* "a11y/id-duplication": {
|
|
301
|
+
* "specConformance": "normative",
|
|
302
|
+
* "rules": { "id-duplication": true }
|
|
303
|
+
* }
|
|
304
|
+
* }
|
|
305
|
+
* }
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
export type NamedRuleGroup = {
|
|
309
|
+
readonly specConformance?: SpecConformance;
|
|
310
|
+
/** User-applied severity override for all rules in the group */
|
|
311
|
+
readonly severity?: Severity;
|
|
312
|
+
readonly rules: BaseRules;
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* A dictionary mapping base rule names to their configurations.
|
|
316
|
+
* Does not accept {@link NamedRuleGroup} entries.
|
|
317
|
+
* Used inside {@link NamedRuleGroup}, {@link NodeRule}, and {@link ChildNodeRule}.
|
|
278
318
|
*/
|
|
279
|
-
export type
|
|
319
|
+
export type BaseRules = {
|
|
320
|
+
readonly [ruleName: string]: AnyRule;
|
|
321
|
+
};
|
|
280
322
|
/**
|
|
281
323
|
* A dictionary mapping rule names to their configurations.
|
|
324
|
+
* Keys containing `/` may be {@link NamedRuleGroup} entries.
|
|
282
325
|
*/
|
|
283
326
|
export type Rules = {
|
|
284
|
-
readonly [ruleName: string]: AnyRule;
|
|
327
|
+
readonly [ruleName: string]: AnyRule | NamedRuleGroup;
|
|
285
328
|
};
|
|
286
329
|
/**
|
|
287
330
|
* Full configuration for a single rule, specifying severity, value, options, and reason.
|
|
@@ -299,25 +342,18 @@ export type RuleConfig<T extends RuleConfigValue, O extends PlainData = undefine
|
|
|
299
342
|
/** A human-readable reason for this rule configuration, included in violation messages */
|
|
300
343
|
readonly reason?: string;
|
|
301
344
|
};
|
|
302
|
-
/**
|
|
303
|
-
* @deprecated
|
|
304
|
-
*/
|
|
305
|
-
export type RuleConfigV2<T extends RuleConfigValue, O extends PlainData = undefined> = {
|
|
306
|
-
readonly severity?: Severity;
|
|
307
|
-
readonly value?: Readonly<T>;
|
|
308
|
-
readonly reason?: string;
|
|
309
|
-
/**
|
|
310
|
-
* Old property
|
|
311
|
-
*
|
|
312
|
-
* @deprecated
|
|
313
|
-
* @see {this.options}
|
|
314
|
-
*/
|
|
315
|
-
readonly option?: Readonly<O>;
|
|
316
|
-
};
|
|
317
345
|
/**
|
|
318
346
|
* The severity level of a lint violation.
|
|
319
347
|
*/
|
|
320
348
|
export type Severity = 'error' | 'warning' | 'info';
|
|
349
|
+
/**
|
|
350
|
+
* The spec conformance classification of a rule, based on RFC 2119 keyword strength.
|
|
351
|
+
*
|
|
352
|
+
* - `'normative'` — derived from MUST/REQUIRED requirements in the HTML spec
|
|
353
|
+
* - `'non-normative'` — derived from SHOULD/RECOMMENDED requirements in the HTML spec
|
|
354
|
+
* - `undefined` — plugin/preset recommendation or user-defined (no spec backing)
|
|
355
|
+
*/
|
|
356
|
+
export type SpecConformance = 'normative' | 'non-normative';
|
|
321
357
|
/**
|
|
322
358
|
* The value portion of a rule configuration. Can be a primitive scalar,
|
|
323
359
|
* an array of scalars or objects, or `null` to represent no value.
|
|
@@ -325,23 +361,58 @@ export type Severity = 'error' | 'warning' | 'info';
|
|
|
325
361
|
export type RuleConfigValue = PrimitiveScalar | readonly (PrimitiveScalar | Readonly<Record<string, any>>)[] | null;
|
|
326
362
|
/**
|
|
327
363
|
* A rule override that targets specific nodes by CSS selector, regex selector, ARIA roles, or categories.
|
|
364
|
+
*
|
|
365
|
+
* When a `name` is provided (must contain `/`), this becomes a **named nodeRule**
|
|
366
|
+
* that creates a virtual rule instance running independently from the base rule.
|
|
367
|
+
* Named nodeRules can be individually enabled/disabled via `rules["name/here"]: false`.
|
|
328
368
|
*/
|
|
329
369
|
export type NodeRule = {
|
|
370
|
+
/**
|
|
371
|
+
* Alias name for this nodeRule, creating a virtual rule.
|
|
372
|
+
* Must contain `/` (e.g., `"a11y/img-has-alt"`).
|
|
373
|
+
* With a single non-false entry, this name is used directly.
|
|
374
|
+
* With multiple non-false entries, derived names (`name/baseRuleName`)
|
|
375
|
+
* are generated automatically, and this name becomes the group name.
|
|
376
|
+
*/
|
|
377
|
+
readonly name?: string;
|
|
378
|
+
/**
|
|
379
|
+
* The spec conformance classification of this rule.
|
|
380
|
+
* Included in violations as metadata for downstream tools and reporting.
|
|
381
|
+
*/
|
|
382
|
+
readonly specConformance?: SpecConformance;
|
|
330
383
|
readonly selector?: string;
|
|
331
384
|
readonly regexSelector?: RegexSelector;
|
|
332
385
|
readonly categories?: readonly string[];
|
|
333
386
|
readonly roles?: readonly string[];
|
|
334
387
|
readonly obsolete?: boolean;
|
|
335
|
-
|
|
388
|
+
/** Base rule settings. Does not accept {@link NamedRuleGroup} entries. */
|
|
389
|
+
readonly rules?: BaseRules;
|
|
336
390
|
};
|
|
337
391
|
/**
|
|
338
392
|
* A rule override that targets child nodes of elements matching the selector.
|
|
393
|
+
*
|
|
394
|
+
* When a `name` is provided (must contain `/`), this becomes a **named childNodeRule**
|
|
395
|
+
* that creates a virtual rule instance, just like named nodeRules.
|
|
339
396
|
*/
|
|
340
397
|
export type ChildNodeRule = {
|
|
398
|
+
/**
|
|
399
|
+
* Alias name for this childNodeRule, creating a virtual rule.
|
|
400
|
+
* Must contain `/` (e.g., `"a11y/heading-in-section"`).
|
|
401
|
+
* With a single non-false entry, this name is used directly.
|
|
402
|
+
* With multiple non-false entries, derived names (`name/baseRuleName`)
|
|
403
|
+
* are generated automatically, and this name becomes the group name.
|
|
404
|
+
*/
|
|
405
|
+
readonly name?: string;
|
|
406
|
+
/**
|
|
407
|
+
* The spec conformance classification of this rule.
|
|
408
|
+
* Included in violations as metadata for downstream tools and reporting.
|
|
409
|
+
*/
|
|
410
|
+
readonly specConformance?: SpecConformance;
|
|
341
411
|
readonly selector?: string;
|
|
342
412
|
readonly regexSelector?: RegexSelector;
|
|
343
413
|
readonly inheritance?: boolean;
|
|
344
|
-
|
|
414
|
+
/** Base rule settings. Does not accept {@link NamedRuleGroup} entries. */
|
|
415
|
+
readonly rules?: BaseRules;
|
|
345
416
|
};
|
|
346
417
|
/**
|
|
347
418
|
* A violation report from a rule. Can report against a scope (node-based)
|
|
@@ -386,10 +457,18 @@ export type Scope<T extends RuleConfigValue, O extends PlainData = undefined> =
|
|
|
386
457
|
* A fully resolved lint violation with all information needed for reporting.
|
|
387
458
|
*/
|
|
388
459
|
export type Violation = {
|
|
460
|
+
/** The base rule ID (always the underlying rule name, for backwards compatibility) */
|
|
389
461
|
readonly ruleId: string;
|
|
462
|
+
/**
|
|
463
|
+
* The display name of the rule. Present only on virtual rules (named nodeRules).
|
|
464
|
+
* For regular rules, use `ruleId` as the display name.
|
|
465
|
+
*/
|
|
466
|
+
readonly name?: string;
|
|
390
467
|
readonly severity: Severity;
|
|
391
468
|
readonly message: string;
|
|
392
469
|
readonly reason?: string;
|
|
470
|
+
/** The normative level of the rule that produced this violation */
|
|
471
|
+
readonly specConformance?: SpecConformance;
|
|
393
472
|
readonly line: number;
|
|
394
473
|
readonly col: number;
|
|
395
474
|
readonly raw: string;
|
package/lib/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyRule,
|
|
1
|
+
import type { AnyRule, NamedRuleGroup, PlainData, RuleConfig, RuleConfigValue, Severity } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Renders a Mustache template with the provided data.
|
|
4
4
|
*
|
|
@@ -18,16 +18,30 @@ export declare function provideValue(template: string, data: Readonly<Record<str
|
|
|
18
18
|
* @param data - Key-value pairs for template variable replacement
|
|
19
19
|
* @returns The rule with all template strings rendered, or `undefined` if rendering fails
|
|
20
20
|
*/
|
|
21
|
-
export declare function exchangeValueOnRule(rule: AnyRule
|
|
21
|
+
export declare function exchangeValueOnRule(rule: AnyRule, data: Readonly<Record<string, string>>): AnyRule | undefined;
|
|
22
22
|
/**
|
|
23
23
|
* Normalizes a rule configuration by extracting the standard fields
|
|
24
24
|
* (`severity`, `value`, `options`, `reason`) and removing `undefined` properties.
|
|
25
|
-
* Also handles the deprecated `option` field by mapping it to `options`.
|
|
26
25
|
*
|
|
27
26
|
* @param rule - The rule configuration to normalize
|
|
28
27
|
* @returns A clean rule configuration with only defined properties
|
|
29
28
|
*/
|
|
30
|
-
export declare function cleanOptions(rule: RuleConfig<RuleConfigValue, PlainData>
|
|
29
|
+
export declare function cleanOptions(rule: RuleConfig<RuleConfigValue, PlainData>): RuleConfig<RuleConfigValue, PlainData>;
|
|
30
|
+
/**
|
|
31
|
+
* Type guard that checks whether a value is a {@link NamedRuleGroup}.
|
|
32
|
+
* A NamedRuleGroup is an object with a `rules` property (and optionally `specConformance` and `severity`).
|
|
33
|
+
*
|
|
34
|
+
* @param v - The value to check
|
|
35
|
+
* @returns `true` if `v` is a named rule group
|
|
36
|
+
*/
|
|
37
|
+
export declare function isNamedRuleGroup(v: unknown): v is NamedRuleGroup;
|
|
38
|
+
/**
|
|
39
|
+
* Type guard that checks whether a string is a valid {@link Severity} value.
|
|
40
|
+
*
|
|
41
|
+
* @param v - The string to check
|
|
42
|
+
* @returns `true` if `v` is "error", "warning", or "info"
|
|
43
|
+
*/
|
|
44
|
+
export declare function isSeverity(v: string): v is Severity;
|
|
31
45
|
/**
|
|
32
46
|
* Type guard that checks whether a value is a {@link RuleConfigValue}
|
|
33
47
|
* (i.e. a primitive, `null`, or an array) rather than a full {@link RuleConfig} object.
|
package/lib/utils.js
CHANGED
|
@@ -44,7 +44,7 @@ export function exchangeValueOnRule(rule, data) {
|
|
|
44
44
|
value: exchangeValue(result.value, data),
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
const options =
|
|
47
|
+
const options = result.options;
|
|
48
48
|
if (options != null && options !== '' && options !== 0) {
|
|
49
49
|
const newOptions = exchangeOption(options, data);
|
|
50
50
|
result = {
|
|
@@ -69,7 +69,6 @@ export function exchangeValueOnRule(rule, data) {
|
|
|
69
69
|
/**
|
|
70
70
|
* Normalizes a rule configuration by extracting the standard fields
|
|
71
71
|
* (`severity`, `value`, `options`, `reason`) and removing `undefined` properties.
|
|
72
|
-
* Also handles the deprecated `option` field by mapping it to `options`.
|
|
73
72
|
*
|
|
74
73
|
* @param rule - The rule configuration to normalize
|
|
75
74
|
* @returns A clean rule configuration with only defined properties
|
|
@@ -78,12 +77,37 @@ export function cleanOptions(rule) {
|
|
|
78
77
|
const res = {
|
|
79
78
|
severity: rule.severity,
|
|
80
79
|
value: rule.value,
|
|
81
|
-
options:
|
|
80
|
+
options: rule.options,
|
|
82
81
|
reason: rule.reason,
|
|
83
82
|
};
|
|
84
83
|
deleteUndefProp(res);
|
|
85
84
|
return res;
|
|
86
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Type guard that checks whether a value is a {@link NamedRuleGroup}.
|
|
88
|
+
* A NamedRuleGroup is an object with a `rules` property (and optionally `specConformance` and `severity`).
|
|
89
|
+
*
|
|
90
|
+
* @param v - The value to check
|
|
91
|
+
* @returns `true` if `v` is a named rule group
|
|
92
|
+
*/
|
|
93
|
+
export function isNamedRuleGroup(v) {
|
|
94
|
+
return (v != null &&
|
|
95
|
+
typeof v === 'object' &&
|
|
96
|
+
!Array.isArray(v) &&
|
|
97
|
+
'rules' in v &&
|
|
98
|
+
v.rules != null &&
|
|
99
|
+
typeof v.rules === 'object' &&
|
|
100
|
+
!Array.isArray(v.rules));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Type guard that checks whether a string is a valid {@link Severity} value.
|
|
104
|
+
*
|
|
105
|
+
* @param v - The string to check
|
|
106
|
+
* @returns `true` if `v` is "error", "warning", or "info"
|
|
107
|
+
*/
|
|
108
|
+
export function isSeverity(v) {
|
|
109
|
+
return v === 'error' || v === 'warning' || v === 'info';
|
|
110
|
+
}
|
|
87
111
|
/**
|
|
88
112
|
* Type guard that checks whether a value is a {@link RuleConfigValue}
|
|
89
113
|
* (i.e. a primitive, `null`, or an array) rather than a full {@link RuleConfig} object.
|
|
@@ -120,20 +144,6 @@ export function deleteUndefProp(obj) {
|
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Return options from `options` or `option`
|
|
125
|
-
*
|
|
126
|
-
* @param rule
|
|
127
|
-
* @returns
|
|
128
|
-
*/
|
|
129
|
-
function extractOptions(rule) {
|
|
130
|
-
if ('options' in rule && rule.options != null) {
|
|
131
|
-
return rule.options;
|
|
132
|
-
}
|
|
133
|
-
if ('option' in rule && rule.option != null) {
|
|
134
|
-
return rule.option;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
147
|
function exchangeValue(rule, data) {
|
|
138
148
|
if (rule == null) {
|
|
139
149
|
return rule;
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/ml-config",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-alpha.0",
|
|
4
4
|
"description": "JSON Schema and TypeScript types of markuplint configure JSON",
|
|
5
5
|
"repository": "git@github.com:markuplint/markuplint.git",
|
|
6
6
|
"author": "Yusuke Hirao <yusukehirao@me.com>",
|
|
7
7
|
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
8
11
|
"type": "module",
|
|
9
12
|
"exports": {
|
|
10
13
|
".": {
|
|
@@ -24,15 +27,14 @@
|
|
|
24
27
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
25
28
|
},
|
|
26
29
|
"dependencies": {
|
|
27
|
-
"@markuplint/ml-ast": "
|
|
28
|
-
"@markuplint/ml-spec": "
|
|
29
|
-
"@markuplint/selector": "
|
|
30
|
-
"@markuplint/shared": "
|
|
30
|
+
"@markuplint/ml-ast": "5.0.0-alpha.0",
|
|
31
|
+
"@markuplint/ml-spec": "5.0.0-alpha.0",
|
|
32
|
+
"@markuplint/selector": "5.0.0-alpha.0",
|
|
33
|
+
"@markuplint/shared": "5.0.0-alpha.0",
|
|
31
34
|
"@types/mustache": "4.2.6",
|
|
32
|
-
"deepmerge": "4.3.1",
|
|
33
35
|
"is-plain-object": "5.0.0",
|
|
34
36
|
"mustache": "4.2.0",
|
|
35
|
-
"type-fest": "4.
|
|
37
|
+
"type-fest": "5.4.4"
|
|
36
38
|
},
|
|
37
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "13dcfc84ec83d87360c720e253383b60767e1b56"
|
|
38
40
|
}
|