@markuplint/create-rule 4.7.22 → 4.8.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 +194 -0
- package/ARCHITECTURE.md +194 -0
- package/CHANGELOG.md +5 -3
- package/README.md +125 -0
- package/SKILL.md +74 -0
- package/docs/maintenance.ja.md +135 -0
- package/docs/maintenance.md +135 -0
- package/lib/cli.d.ts +9 -0
- package/lib/cli.js +262 -4
- package/lib/create-rule-helper-error.d.ts +5 -0
- package/lib/create-rule-helper-error.js +5 -0
- package/lib/create-rule-helper.d.ts +7 -0
- package/lib/create-rule-helper.js +7 -0
- package/lib/create-rule-package.d.ts +11 -0
- package/lib/create-rule-package.js +11 -0
- package/lib/create-rule-to-core.d.ts +19 -0
- package/lib/create-rule-to-core.js +22 -0
- package/lib/create-rule-to-project.d.ts +8 -0
- package/lib/create-rule-to-project.js +8 -0
- package/lib/fs-exists.d.ts +9 -0
- package/lib/fs-exists.js +9 -0
- package/lib/glob.d.ts +9 -0
- package/lib/glob.js +9 -0
- package/lib/install-scaffold.d.ts +14 -0
- package/lib/install-scaffold.js +14 -0
- package/lib/is-markuplint-repo.d.ts +7 -0
- package/lib/is-markuplint-repo.js +7 -0
- package/lib/read-package-json.d.ts +7 -0
- package/lib/read-package-json.js +7 -0
- package/lib/search-core-repository.d.ts +8 -0
- package/lib/search-core-repository.js +8 -0
- package/lib/transfer.d.ts +19 -0
- package/lib/transfer.js +13 -0
- package/lib/types.d.ts +42 -0
- package/package.json +7 -7
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @markuplint/create-rule メンテナンスガイド
|
|
2
|
+
|
|
3
|
+
## 概要
|
|
4
|
+
|
|
5
|
+
`@markuplint/create-rule` パッケージは、新しい markuplint ルールのボイラープレートファイルを生成する CLI スキャフォルディングツールです。3つのモードをサポートしています:
|
|
6
|
+
|
|
7
|
+
- **プロジェクトに追加** — 現在のプロジェクトにローカルプラグインディレクトリを作成
|
|
8
|
+
- **パッケージとして公開** — スタンドアロンの npm パッケージをスキャフォルド
|
|
9
|
+
- **コアに貢献** — モノレポ内の `@markuplint/rules` にルールを追加
|
|
10
|
+
|
|
11
|
+
### ファイル構成
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
packages/@markuplint/create-rule/
|
|
15
|
+
├── bin/
|
|
16
|
+
│ └── create-rule.mjs # CLI 実行ファイル
|
|
17
|
+
├── src/
|
|
18
|
+
│ ├── cli.ts # 対話ウィザード
|
|
19
|
+
│ ├── types.ts # 型定義
|
|
20
|
+
│ ├── create-rule-helper.ts # 目的ベースルーター
|
|
21
|
+
│ ├── create-rule-to-project.ts
|
|
22
|
+
│ ├── create-rule-package.ts
|
|
23
|
+
│ ├── create-rule-to-core.ts
|
|
24
|
+
│ ├── install-scaffold.ts # スキャフォルドインストーラー
|
|
25
|
+
│ └── transfer.ts # テンプレート処理
|
|
26
|
+
└── scaffold/
|
|
27
|
+
├── core/ # コアルールテンプレート
|
|
28
|
+
├── project/ # プロジェクトプラグインテンプレート
|
|
29
|
+
└── package/ # パッケージテンプレート
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## テンプレートファイルの編集
|
|
33
|
+
|
|
34
|
+
テンプレートファイルは `scaffold/{core,project,package}/` に配置されています。ユーザーが CLI を実行すると、これらのファイルがプレースホルダーを実際の値に置換してコピーされます。
|
|
35
|
+
|
|
36
|
+
### プレースホルダー一覧
|
|
37
|
+
|
|
38
|
+
| プレースホルダー | 置換内容 | 入力例 | 出力例 |
|
|
39
|
+
| ----------------- | ------------------------------ | -------------- | -------------- |
|
|
40
|
+
| `__pluginName__` | プラグイン名(そのまま) | `my-plugin` | `my-plugin` |
|
|
41
|
+
| `__pluginName__c` | プラグイン名(キャメルケース) | `my-plugin` | `myPlugin` |
|
|
42
|
+
| `__ruleName__` | ルール名(そのまま) | `no-empty-alt` | `no-empty-alt` |
|
|
43
|
+
| `__ruleName__c` | ルール名(キャメルケース) | `no-empty-alt` | `noEmptyAlt` |
|
|
44
|
+
| `__description__` | ルールの説明(コアのみ) | — | — |
|
|
45
|
+
| `__category__` | ルールカテゴリ(コアのみ) | — | — |
|
|
46
|
+
| `__severity__` | デフォルト重大度(コアのみ) | — | — |
|
|
47
|
+
|
|
48
|
+
### キャメルケース変換
|
|
49
|
+
|
|
50
|
+
`__<name>__c` サフィックスはキャメルケース変換をトリガーします。ハイフンが除去され、次の文字が大文字化されます。例えば、`__ruleName__c` に値 `no-empty-alt` を指定すると `noEmptyAlt` になります。これは生成コード内の変数名に使用されます。
|
|
51
|
+
|
|
52
|
+
### ファイル名の置換
|
|
53
|
+
|
|
54
|
+
テンプレートファイル名に含まれる `__ruleName__` は実際のルール名に置換されます。例えば、`rules/__ruleName__.ts` は `rules/no-empty-alt.ts` になります。
|
|
55
|
+
|
|
56
|
+
### TypeScript から JavaScript へのトランスパイル
|
|
57
|
+
|
|
58
|
+
ユーザーが JavaScript を選択した場合、すべての `.ts` テンプレートファイルが TypeScript コンパイラ API で `.js` にトランスパイルされます。テンプレートを編集する際は、生成される TypeScript がトランスパイル後も有効な JavaScript になることを確認してください。
|
|
59
|
+
|
|
60
|
+
### Prettier フォーマット
|
|
61
|
+
|
|
62
|
+
生成されるすべてのファイルは Prettier でフォーマットされます。テンプレート内の `// prettier-ignore` コメントはフォーマット前に自動削除されます。これにより、プレースホルダー式がリフォーマットされないようにテンプレート内で `// prettier-ignore` を使用できます。
|
|
63
|
+
|
|
64
|
+
## CLI フローの変更
|
|
65
|
+
|
|
66
|
+
対話的な質問シーケンスは `src/cli.ts` で定義されています。新しい質問を追加するには:
|
|
67
|
+
|
|
68
|
+
1. `@markuplint/cli-utils` のヘルパー(`input()`、`select()`、`confirm()`)を使って質問を追加
|
|
69
|
+
2. `src/types.ts` に対応する型を追加(例: `CreateRuleCreatorParams` の新しいフィールド)
|
|
70
|
+
3. `createRuleHelper()` の呼び出しに値を渡す
|
|
71
|
+
4. 値を `replacer` オプションに渡す必要がある場合は `install-scaffold.ts` を更新
|
|
72
|
+
5. 値がスキャフォルド戦略の動作に影響する場合は、関連する戦略ファイルを更新
|
|
73
|
+
|
|
74
|
+
## i18n の活用
|
|
75
|
+
|
|
76
|
+
ルール実装を書く際(テンプレートでも実際のルールでも)、ハードコードされた文字列ではなく、ルールコンテキストの `t()` 翻訳関数を使用してすべてのユーザー向けメッセージを生成してください。
|
|
77
|
+
|
|
78
|
+
### 翻訳関数の使い方
|
|
79
|
+
|
|
80
|
+
`t()` 関数はすべてのルールの `verify` コンテキストで利用可能です:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
async verify({ document, report, t }) {
|
|
84
|
+
await document.walkOn('Element', el => {
|
|
85
|
+
report({
|
|
86
|
+
scope: el,
|
|
87
|
+
// t() で文テンプレートとキーワード引数を使用
|
|
88
|
+
message: t('{0} is {1:c}', 'attribute', 'deprecated'),
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
文テンプレートとキーワードは `@markuplint/i18n` で定義されています。このアプローチには以下の利点があります:
|
|
95
|
+
|
|
96
|
+
- 日本語(およびその他のサポート言語)への自動翻訳
|
|
97
|
+
- すべてのルールで一貫したメッセージフォーマット
|
|
98
|
+
- 補語形式のサポート(日本語の述語接続に使う `:c` フラグ)
|
|
99
|
+
|
|
100
|
+
### 新しいキーワードやフレーズの追加
|
|
101
|
+
|
|
102
|
+
ルールに必要なキーワードや文テンプレートが存在しない場合は、`@markuplint/i18n` に追加してください。手順は [i18n メンテナンススキル](../i18n/SKILL.md) を参照してください。主なポイント:
|
|
103
|
+
|
|
104
|
+
1. キーワードを `locales/ja.json` と `$schema.json` に追加(3ファイル同期ルール)
|
|
105
|
+
2. 文テンプレートは `{0}`, `{1}` プレースホルダーで設計
|
|
106
|
+
3. 補語形式には `{0:c}`、翻訳スキップには `{0*}` を使用
|
|
107
|
+
|
|
108
|
+
## 既存ルールの参考
|
|
109
|
+
|
|
110
|
+
新しいルールを作成する際は、`packages/@markuplint/rules/src/` の既存の実装をパターンやベストプラクティスの参考にしてください。
|
|
111
|
+
|
|
112
|
+
### 推奨例
|
|
113
|
+
|
|
114
|
+
シンプルなルール:
|
|
115
|
+
|
|
116
|
+
- `id-duplication` — 要素走査と重複検出のシンプルな実装
|
|
117
|
+
- `class-naming` — 正規表現パターンを使った属性値チェック
|
|
118
|
+
|
|
119
|
+
i18n を多用するルール:
|
|
120
|
+
|
|
121
|
+
- `deprecated-attr` — 補語キーワード(`{0:c}`)と複数の文テンプレートを使用
|
|
122
|
+
- `required-attr` — 要素/属性コンテキストでのキーワードベースメッセージを実装
|
|
123
|
+
|
|
124
|
+
`packages/@markuplint/rules/src/` ディレクトリを参照して、作成しようとしているルールに類似した実装を探してください。
|
|
125
|
+
|
|
126
|
+
## fix 関数について
|
|
127
|
+
|
|
128
|
+
`fix` 関数は `RuleSeed` のオプションプロパティで、違反の自動修正を可能にします。しかし、多くの既存ルールでは `fix` の実装が不完全または未実装です。新しいルールを作成する際は、まず `verify` 関数に注力してください。`fix` は自動修正の動作が明確で単純な場合にのみ追加してください。
|
|
129
|
+
|
|
130
|
+
## コマンドリファレンス
|
|
131
|
+
|
|
132
|
+
| コマンド | 説明 |
|
|
133
|
+
| -------------------------------------------- | ------------------ |
|
|
134
|
+
| `yarn test --scope @markuplint/create-rule` | テスト実行 |
|
|
135
|
+
| `yarn build --scope @markuplint/create-rule` | パッケージのビルド |
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @markuplint/create-rule Maintenance Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `@markuplint/create-rule` package is a CLI scaffolding tool that generates boilerplate files for new markuplint rules. It supports three modes:
|
|
6
|
+
|
|
7
|
+
- **Add to project** — Creates a local plugin directory in the current project
|
|
8
|
+
- **Publish as package** — Scaffolds a standalone npm package
|
|
9
|
+
- **Contribute to core** — Adds a rule to `@markuplint/rules` within the monorepo
|
|
10
|
+
|
|
11
|
+
### File Structure
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
packages/@markuplint/create-rule/
|
|
15
|
+
├── bin/
|
|
16
|
+
│ └── create-rule.mjs # CLI executable
|
|
17
|
+
├── src/
|
|
18
|
+
│ ├── cli.ts # Interactive wizard
|
|
19
|
+
│ ├── types.ts # Type definitions
|
|
20
|
+
│ ├── create-rule-helper.ts # Purpose-based router
|
|
21
|
+
│ ├── create-rule-to-project.ts
|
|
22
|
+
│ ├── create-rule-package.ts
|
|
23
|
+
│ ├── create-rule-to-core.ts
|
|
24
|
+
│ ├── install-scaffold.ts # Scaffold installer
|
|
25
|
+
│ └── transfer.ts # Template processing
|
|
26
|
+
└── scaffold/
|
|
27
|
+
├── core/ # Core rule templates
|
|
28
|
+
├── project/ # Project plugin templates
|
|
29
|
+
└── package/ # Package templates
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Editing Template Files
|
|
33
|
+
|
|
34
|
+
Template files live in `scaffold/{core,project,package}/`. When a user runs the CLI, these files are copied to the destination with placeholders replaced by actual values.
|
|
35
|
+
|
|
36
|
+
### Placeholder Reference
|
|
37
|
+
|
|
38
|
+
| Placeholder | Replaced with | Example input | Example output |
|
|
39
|
+
| ----------------- | ---------------------------- | -------------- | -------------- |
|
|
40
|
+
| `__pluginName__` | Plugin name (as-is) | `my-plugin` | `my-plugin` |
|
|
41
|
+
| `__pluginName__c` | Plugin name (camelCase) | `my-plugin` | `myPlugin` |
|
|
42
|
+
| `__ruleName__` | Rule name (as-is) | `no-empty-alt` | `no-empty-alt` |
|
|
43
|
+
| `__ruleName__c` | Rule name (camelCase) | `no-empty-alt` | `noEmptyAlt` |
|
|
44
|
+
| `__description__` | Rule description (core only) | — | — |
|
|
45
|
+
| `__category__` | Rule category (core only) | — | — |
|
|
46
|
+
| `__severity__` | Default severity (core only) | — | — |
|
|
47
|
+
|
|
48
|
+
### CamelCase Conversion
|
|
49
|
+
|
|
50
|
+
The `__<name>__c` suffix triggers camelCase conversion: hyphens are removed and the following letter is uppercased. For example, `__ruleName__c` with value `no-empty-alt` becomes `noEmptyAlt`. This is used for variable names in generated code.
|
|
51
|
+
|
|
52
|
+
### File Name Replacement
|
|
53
|
+
|
|
54
|
+
Template file names containing `__ruleName__` are renamed to the actual rule name. For example, `rules/__ruleName__.ts` becomes `rules/no-empty-alt.ts`.
|
|
55
|
+
|
|
56
|
+
### TypeScript-to-JavaScript Transpilation
|
|
57
|
+
|
|
58
|
+
When the user selects JavaScript, all `.ts` template files are transpiled to `.js` using the TypeScript compiler API. Keep this in mind when editing templates: the generated TypeScript must also produce valid JavaScript after transpilation.
|
|
59
|
+
|
|
60
|
+
### Prettier Formatting
|
|
61
|
+
|
|
62
|
+
All generated files are formatted with Prettier. Any `// prettier-ignore` comments in templates are automatically stripped before formatting. This means `// prettier-ignore` can be used in templates to preserve formatting of placeholder expressions that would otherwise be reformatted.
|
|
63
|
+
|
|
64
|
+
## Changing the CLI Flow
|
|
65
|
+
|
|
66
|
+
The interactive question sequence is defined in `src/cli.ts`. To add a new question:
|
|
67
|
+
|
|
68
|
+
1. Add the question using helpers from `@markuplint/cli-utils` (`input()`, `select()`, `confirm()`)
|
|
69
|
+
2. Add the corresponding type to `src/types.ts` (e.g., a new field on `CreateRuleCreatorParams`)
|
|
70
|
+
3. Pass the value through the `createRuleHelper()` call
|
|
71
|
+
4. Update `install-scaffold.ts` if the value needs to be passed to the `replacer` options
|
|
72
|
+
5. Update the relevant scaffold strategy files if the value affects their behavior
|
|
73
|
+
|
|
74
|
+
## Leveraging i18n
|
|
75
|
+
|
|
76
|
+
When writing rule implementations (whether in templates or actual rules), use the `t()` translator function from the rule context for all user-facing messages rather than hardcoded strings.
|
|
77
|
+
|
|
78
|
+
### Using the Translator
|
|
79
|
+
|
|
80
|
+
The `t()` function is available in the `verify` context of every rule:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
async verify({ document, report, t }) {
|
|
84
|
+
await document.walkOn('Element', el => {
|
|
85
|
+
report({
|
|
86
|
+
scope: el,
|
|
87
|
+
// Use t() with a sentence template and keyword arguments
|
|
88
|
+
message: t('{0} is {1:c}', 'attribute', 'deprecated'),
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The sentence templates and keywords are defined in `@markuplint/i18n`. This approach provides:
|
|
95
|
+
|
|
96
|
+
- Automatic translation to Japanese (and other supported languages)
|
|
97
|
+
- Consistent message formatting across all rules
|
|
98
|
+
- Complement form support (`:c` flag for Japanese predicate attachment)
|
|
99
|
+
|
|
100
|
+
### Adding New Keywords or Sentences
|
|
101
|
+
|
|
102
|
+
If your rule needs a keyword or sentence template that does not exist yet, add it to `@markuplint/i18n`. See the [i18n maintenance skill](../i18n/SKILL.md) for the procedure. The key points are:
|
|
103
|
+
|
|
104
|
+
1. Add the keyword to `locales/ja.json` and `$schema.json` (three-file sync rule)
|
|
105
|
+
2. Design sentence templates with `{0}`, `{1}` placeholders
|
|
106
|
+
3. Use `{0:c}` for complement forms and `{0*}` to skip translation
|
|
107
|
+
|
|
108
|
+
## Referencing Existing Rules
|
|
109
|
+
|
|
110
|
+
When creating a new rule, refer to existing implementations in `packages/@markuplint/rules/src/` for patterns and best practices.
|
|
111
|
+
|
|
112
|
+
### Recommended Examples
|
|
113
|
+
|
|
114
|
+
Simple rules to start with:
|
|
115
|
+
|
|
116
|
+
- `id-duplication` — Straightforward element traversal with duplicate detection
|
|
117
|
+
- `class-naming` — Attribute value checking with regex patterns
|
|
118
|
+
|
|
119
|
+
Rules with rich i18n usage:
|
|
120
|
+
|
|
121
|
+
- `deprecated-attr` — Uses complement keywords (`{0:c}`) and multiple sentence templates
|
|
122
|
+
- `required-attr` — Demonstrates keyword-based messages with element/attribute context
|
|
123
|
+
|
|
124
|
+
Browse the `packages/@markuplint/rules/src/` directory to find rules similar to what you are building.
|
|
125
|
+
|
|
126
|
+
## About the `fix` Function
|
|
127
|
+
|
|
128
|
+
The `fix` function is an optional property of `RuleSeed` that enables auto-fixing violations. However, many existing rules have incomplete or missing `fix` implementations. When creating a new rule, focus on the `verify` function first. Add `fix` only if the auto-fix behavior is straightforward and well-defined.
|
|
129
|
+
|
|
130
|
+
## Command Reference
|
|
131
|
+
|
|
132
|
+
| Command | Description |
|
|
133
|
+
| -------------------------------------------- | ----------------- |
|
|
134
|
+
| `yarn test --scope @markuplint/create-rule` | Run tests |
|
|
135
|
+
| `yarn build --scope @markuplint/create-rule` | Build the package |
|
package/lib/cli.d.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry point for creating a new markuplint rule.
|
|
3
|
+
*
|
|
4
|
+
* Supports two modes:
|
|
5
|
+
* - **Non-interactive**: When CLI options are provided, creates the rule directly.
|
|
6
|
+
* - **Interactive**: When no options are provided, starts the guided wizard.
|
|
7
|
+
*
|
|
8
|
+
* @returns Resolves when the rule has been fully scaffolded and dependencies installed.
|
|
9
|
+
*/
|
|
1
10
|
export declare function createRule(): Promise<void>;
|
package/lib/cli.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
2
3
|
import { input, installModule, select, confirm, font, header, xterm } from '@markuplint/cli-utils';
|
|
3
4
|
import { createRuleHelper } from './create-rule-helper.js';
|
|
4
5
|
import { isMarkuplintRepo } from './is-markuplint-repo.js';
|
|
6
|
+
const KEBAB_CASE = /^[a-z][\da-z]*(?:-[a-z][\da-z]*)*$/i;
|
|
7
|
+
const CATEGORIES = ['validation', 'a11y', 'naming-convention', 'maintainability', 'style'];
|
|
8
|
+
const SEVERITIES = ['error', 'warning'];
|
|
9
|
+
const PURPOSE_MAP = {
|
|
10
|
+
project: 'ADD_TO_PROJECT',
|
|
11
|
+
package: 'PUBLISH_AS_PACKAGE',
|
|
12
|
+
core: 'CONTRIBUTE_TO_CORE',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Icon mapping for scaffold output display, keyed by base file name.
|
|
16
|
+
*/
|
|
5
17
|
const icons = {
|
|
6
18
|
README: '📝',
|
|
7
19
|
index: '📜',
|
|
@@ -9,7 +21,245 @@ const icons = {
|
|
|
9
21
|
package: '🎁',
|
|
10
22
|
tsconfig: '💎',
|
|
11
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Prints the CLI usage information, options, and examples to stdout.
|
|
26
|
+
*/
|
|
27
|
+
function printHelp() {
|
|
28
|
+
process.stdout.write(`
|
|
29
|
+
Usage: create-rule [options]
|
|
30
|
+
|
|
31
|
+
Without options, starts the interactive wizard.
|
|
32
|
+
With options, creates a rule non-interactively.
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
-p, --purpose <type> Purpose: project, package, or core (required)
|
|
36
|
+
-n, --plugin-name <name> Plugin/directory name (required for project/package)
|
|
37
|
+
-r, --rule-name <name> Rule name in kebab-case (required)
|
|
38
|
+
-l, --lang <lang> Language: ts or js (default: ts, ignored for core)
|
|
39
|
+
-t, --test Generate test files (default: true)
|
|
40
|
+
--no-test Skip test file generation
|
|
41
|
+
-d, --description <text> Rule description (required for core)
|
|
42
|
+
-c, --category <cat> Category (required for core):
|
|
43
|
+
validation, a11y, naming-convention,
|
|
44
|
+
maintainability, style
|
|
45
|
+
-s, --severity <level> Severity: error or warning (required for core)
|
|
46
|
+
--json Output result as JSON
|
|
47
|
+
-h, --help Show this help message
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
# Add a rule to this project
|
|
51
|
+
create-rule -p project -n my-plugin -r no-empty-alt
|
|
52
|
+
|
|
53
|
+
# Create a publishable package
|
|
54
|
+
create-rule -p package -n my-plugin -r no-empty-alt -l js --no-test
|
|
55
|
+
|
|
56
|
+
# Contribute to core
|
|
57
|
+
create-rule -p core -r no-empty-alt -d "Disallow empty alt" -c a11y -s error
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Sentinel class thrown when `--help` is requested to signal a
|
|
62
|
+
* successful early exit without using `process.exit`.
|
|
63
|
+
*/
|
|
64
|
+
class HelpRequested {
|
|
65
|
+
constructor() {
|
|
66
|
+
this.code = 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Error thrown when CLI arguments are invalid or missing.
|
|
71
|
+
* Includes a usage hint directing users to `--help`.
|
|
72
|
+
*/
|
|
73
|
+
class UsageHintError extends Error {
|
|
74
|
+
constructor(message) {
|
|
75
|
+
super(`${message}\nRun 'create-rule --help' for usage.`);
|
|
76
|
+
this.name = 'UsageHintError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parses `process.argv` into validated {@link CreateRuleHelperParams}.
|
|
81
|
+
*
|
|
82
|
+
* @returns The parsed parameters and output format flag, or `null` when
|
|
83
|
+
* no arguments are provided (indicating interactive mode).
|
|
84
|
+
* @throws {HelpRequested} When `--help` is passed.
|
|
85
|
+
* @throws {UsageHintError} When required options are missing or values are invalid.
|
|
86
|
+
*/
|
|
87
|
+
function parseCliArgs() {
|
|
88
|
+
const { values } = parseArgs({
|
|
89
|
+
options: {
|
|
90
|
+
purpose: { type: 'string', short: 'p' },
|
|
91
|
+
'plugin-name': { type: 'string', short: 'n' },
|
|
92
|
+
'rule-name': { type: 'string', short: 'r' },
|
|
93
|
+
lang: { type: 'string', short: 'l' },
|
|
94
|
+
test: { type: 'boolean', short: 't', default: true },
|
|
95
|
+
'no-test': { type: 'boolean', default: false },
|
|
96
|
+
description: { type: 'string', short: 'd' },
|
|
97
|
+
category: { type: 'string', short: 'c' },
|
|
98
|
+
severity: { type: 'string', short: 's' },
|
|
99
|
+
json: { type: 'boolean', default: false },
|
|
100
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
101
|
+
},
|
|
102
|
+
strict: true,
|
|
103
|
+
});
|
|
104
|
+
if (values.help) {
|
|
105
|
+
printHelp();
|
|
106
|
+
throw new HelpRequested();
|
|
107
|
+
}
|
|
108
|
+
// No arguments → interactive mode
|
|
109
|
+
if (!values.purpose && !values['rule-name'] && !values['plugin-name']) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// Validate purpose
|
|
113
|
+
if (!values.purpose) {
|
|
114
|
+
throw new UsageHintError('--purpose is required in non-interactive mode');
|
|
115
|
+
}
|
|
116
|
+
const purpose = PURPOSE_MAP[values.purpose];
|
|
117
|
+
if (!purpose) {
|
|
118
|
+
throw new UsageHintError(`Invalid --purpose "${values.purpose}". Must be one of: project, package, core`);
|
|
119
|
+
}
|
|
120
|
+
// Validate rule name
|
|
121
|
+
if (!values['rule-name']) {
|
|
122
|
+
throw new UsageHintError('--rule-name is required');
|
|
123
|
+
}
|
|
124
|
+
const ruleName = values['rule-name'];
|
|
125
|
+
if (!KEBAB_CASE.test(ruleName)) {
|
|
126
|
+
throw new UsageHintError(`Invalid --rule-name "${ruleName}". Must be kebab-case (e.g., "no-empty-alt")`);
|
|
127
|
+
}
|
|
128
|
+
// Validate plugin name
|
|
129
|
+
let pluginName = '';
|
|
130
|
+
if (purpose !== 'CONTRIBUTE_TO_CORE') {
|
|
131
|
+
if (!values['plugin-name']) {
|
|
132
|
+
throw new UsageHintError('--plugin-name is required for project/package purpose');
|
|
133
|
+
}
|
|
134
|
+
pluginName = values['plugin-name'];
|
|
135
|
+
if (!KEBAB_CASE.test(pluginName)) {
|
|
136
|
+
throw new UsageHintError(`Invalid --plugin-name "${pluginName}". Must be kebab-case (e.g., "my-plugin")`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Language
|
|
140
|
+
let lang;
|
|
141
|
+
if (purpose === 'CONTRIBUTE_TO_CORE') {
|
|
142
|
+
lang = 'TYPESCRIPT';
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
switch (values.lang ?? 'ts') {
|
|
146
|
+
case 'ts': {
|
|
147
|
+
lang = 'TYPESCRIPT';
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case 'js': {
|
|
151
|
+
lang = 'JAVASCRIPT';
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
default: {
|
|
155
|
+
throw new UsageHintError(`Invalid --lang "${values.lang}". Must be "ts" or "js"`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Test
|
|
160
|
+
const needTest = purpose === 'CONTRIBUTE_TO_CORE' ? true : !values['no-test'];
|
|
161
|
+
// Core-specific params
|
|
162
|
+
let core;
|
|
163
|
+
if (purpose === 'CONTRIBUTE_TO_CORE') {
|
|
164
|
+
if (!values.description) {
|
|
165
|
+
throw new UsageHintError('--description is required for core purpose');
|
|
166
|
+
}
|
|
167
|
+
if (!values.category) {
|
|
168
|
+
throw new UsageHintError('--category is required for core purpose');
|
|
169
|
+
}
|
|
170
|
+
if (!CATEGORIES.includes(values.category)) {
|
|
171
|
+
throw new UsageHintError(`Invalid --category "${values.category}". Must be one of: ${CATEGORIES.join(', ')}`);
|
|
172
|
+
}
|
|
173
|
+
if (!values.severity) {
|
|
174
|
+
throw new UsageHintError('--severity is required for core purpose');
|
|
175
|
+
}
|
|
176
|
+
if (!SEVERITIES.includes(values.severity)) {
|
|
177
|
+
throw new UsageHintError(`Invalid --severity "${values.severity}". Must be one of: ${SEVERITIES.join(', ')}`);
|
|
178
|
+
}
|
|
179
|
+
core = {
|
|
180
|
+
description: values.description,
|
|
181
|
+
category: values.category,
|
|
182
|
+
severity: values.severity,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
params: { purpose, pluginName, ruleName, lang, needTest, core },
|
|
187
|
+
json: values.json ?? false,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* CLI entry point for creating a new markuplint rule.
|
|
192
|
+
*
|
|
193
|
+
* Supports two modes:
|
|
194
|
+
* - **Non-interactive**: When CLI options are provided, creates the rule directly.
|
|
195
|
+
* - **Interactive**: When no options are provided, starts the guided wizard.
|
|
196
|
+
*
|
|
197
|
+
* @returns Resolves when the rule has been fully scaffolded and dependencies installed.
|
|
198
|
+
*/
|
|
12
199
|
export async function createRule() {
|
|
200
|
+
let parsed;
|
|
201
|
+
try {
|
|
202
|
+
parsed = parseCliArgs();
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error instanceof HelpRequested) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
if (parsed) {
|
|
211
|
+
await createRuleNonInteractive(parsed.params, parsed.json);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
await createRuleInteractive();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Creates a rule non-interactively from pre-validated CLI options.
|
|
219
|
+
* Runs the scaffold, prints results (or JSON), and installs dependencies.
|
|
220
|
+
*
|
|
221
|
+
* @param params - The validated rule creation parameters.
|
|
222
|
+
* @param json - When `true`, outputs the result as JSON instead of
|
|
223
|
+
* the human-readable file list.
|
|
224
|
+
*/
|
|
225
|
+
async function createRuleNonInteractive(params, json) {
|
|
226
|
+
const result = await createRuleHelper(params);
|
|
227
|
+
if (!json) {
|
|
228
|
+
process.stdout.write(header('Create a rule'));
|
|
229
|
+
process.stdout.write('\n\n');
|
|
230
|
+
}
|
|
231
|
+
if (json) {
|
|
232
|
+
const output = {
|
|
233
|
+
files: result.files.map(file => ({
|
|
234
|
+
name: file.fileName + file.ext,
|
|
235
|
+
path: path.resolve(file.destDir, file.fileName + file.ext),
|
|
236
|
+
test: file.test,
|
|
237
|
+
})),
|
|
238
|
+
dependencies: result.dependencies,
|
|
239
|
+
devDependencies: result.devDependencies,
|
|
240
|
+
};
|
|
241
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
for (const file of result.files) {
|
|
245
|
+
printFile(params.pluginName || 'core', file.test ? '🖍 ' : (icons[file.name] ?? '🛡 '), file.fileName, path.resolve(file.destDir, file.fileName + file.ext));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (result.dependencies.length > 0) {
|
|
249
|
+
await installModule(result.dependencies);
|
|
250
|
+
}
|
|
251
|
+
if (result.devDependencies.length > 0) {
|
|
252
|
+
await installModule(result.devDependencies, true);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Interactive CLI wizard for creating a new markuplint rule.
|
|
257
|
+
*
|
|
258
|
+
* Guides the user through selecting a purpose, naming the plugin and rule,
|
|
259
|
+
* choosing a language, and optionally generating tests. After scaffolding,
|
|
260
|
+
* it prints the generated files and installs any required dependencies.
|
|
261
|
+
*/
|
|
262
|
+
async function createRuleInteractive() {
|
|
13
263
|
process.stdout.write(header('Create a rule'));
|
|
14
264
|
process.stdout.write('\n');
|
|
15
265
|
process.stdout.write('\n');
|
|
@@ -61,7 +311,7 @@ export async function createRule() {
|
|
|
61
311
|
const needTest = purpose === 'CONTRIBUTE_TO_CORE' ? true : await confirm('Do you need the test?', { initial: true });
|
|
62
312
|
const result = await createRuleHelper({ purpose, pluginName, ruleName, lang, needTest, core });
|
|
63
313
|
for (const file of result.files) {
|
|
64
|
-
|
|
314
|
+
printFile(pluginName || 'core', file.test ? '🖍 ' : (icons[file.name] ?? '🛡 '), file.fileName, path.resolve(file.destDir, file.fileName + file.ext));
|
|
65
315
|
}
|
|
66
316
|
if (result.dependencies.length > 0) {
|
|
67
317
|
await installModule(result.dependencies);
|
|
@@ -70,10 +320,18 @@ export async function createRule() {
|
|
|
70
320
|
await installModule(result.devDependencies, true);
|
|
71
321
|
}
|
|
72
322
|
}
|
|
73
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Prints a single scaffolded file entry to stdout with a check mark, icon, and file path.
|
|
325
|
+
*
|
|
326
|
+
* @param name - The plugin or module name used as a prefix.
|
|
327
|
+
* @param icon - The icon character to display next to the file name.
|
|
328
|
+
* @param title - The display title (typically the file name).
|
|
329
|
+
* @param filePath - The absolute path to the generated file.
|
|
330
|
+
*/
|
|
331
|
+
function printFile(name, icon, title, filePath) {
|
|
74
332
|
const _marker = xterm(39)('✔') + ' ';
|
|
75
333
|
const _title = (icon, title) => `${icon} ` + font.bold(`${name}/${title}`);
|
|
76
|
-
const _file = (
|
|
77
|
-
process.stdout.write(_marker + _title(icon, title) + _file(
|
|
334
|
+
const _file = (filePath) => ' ' + font.cyanBright(filePath);
|
|
335
|
+
process.stdout.write(_marker + _title(icon, title) + _file(filePath));
|
|
78
336
|
process.stdout.write('\n');
|
|
79
337
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error class for failures that occur during rule scaffolding.
|
|
3
|
+
* Thrown when preconditions are not met (e.g., directory already exists,
|
|
4
|
+
* core options missing, or repository not found).
|
|
5
|
+
*/
|
|
1
6
|
export declare class CreateRuleHelperError extends Error {
|
|
2
7
|
name: string;
|
|
3
8
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error class for failures that occur during rule scaffolding.
|
|
3
|
+
* Thrown when preconditions are not met (e.g., directory already exists,
|
|
4
|
+
* core options missing, or repository not found).
|
|
5
|
+
*/
|
|
1
6
|
export class CreateRuleHelperError extends Error {
|
|
2
7
|
constructor() {
|
|
3
8
|
super(...arguments);
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { CreateRuleHelperParams, CreateRuleHelperResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Dispatches rule creation to the appropriate scaffolding function based on the
|
|
4
|
+
* specified purpose. Acts as the central entry point for programmatic rule creation.
|
|
5
|
+
*
|
|
6
|
+
* @param params - The helper parameters including purpose, plugin name, rule name, language, and options.
|
|
7
|
+
* @returns The scaffold result containing generated files and dependencies.
|
|
8
|
+
*/
|
|
2
9
|
export declare function createRuleHelper(params: CreateRuleHelperParams): Promise<CreateRuleHelperResult>;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { createRulePackage } from './create-rule-package.js';
|
|
2
2
|
import { createRuleToCore } from './create-rule-to-core.js';
|
|
3
3
|
import { createRuleToProject } from './create-rule-to-project.js';
|
|
4
|
+
/**
|
|
5
|
+
* Dispatches rule creation to the appropriate scaffolding function based on the
|
|
6
|
+
* specified purpose. Acts as the central entry point for programmatic rule creation.
|
|
7
|
+
*
|
|
8
|
+
* @param params - The helper parameters including purpose, plugin name, rule name, language, and options.
|
|
9
|
+
* @returns The scaffold result containing generated files and dependencies.
|
|
10
|
+
*/
|
|
4
11
|
export async function createRuleHelper(params) {
|
|
5
12
|
switch (params.purpose) {
|
|
6
13
|
case 'ADD_TO_PROJECT': {
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import type { CreateRuleCreatorParams, CreateRuleHelperResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Scaffolds a new markuplint rule as a standalone publishable npm package
|
|
4
|
+
* in the current working directory.
|
|
5
|
+
*
|
|
6
|
+
* Validates that the current directory is empty before proceeding.
|
|
7
|
+
* Generates a complete package structure including `package.json`.
|
|
8
|
+
*
|
|
9
|
+
* @param params - The rule creation parameters (plugin name, rule name, language, test preference).
|
|
10
|
+
* @returns The scaffold result containing generated files and dependencies.
|
|
11
|
+
* @throws {CreateRuleHelperError} If the current directory is not empty.
|
|
12
|
+
*/
|
|
2
13
|
export declare function createRulePackage({ pluginName, ruleName, lang, needTest, }: CreateRuleCreatorParams): Promise<CreateRuleHelperResult>;
|
|
@@ -2,6 +2,17 @@ import path from 'node:path';
|
|
|
2
2
|
import { CreateRuleHelperError } from './create-rule-helper-error.js';
|
|
3
3
|
import { glob } from './glob.js';
|
|
4
4
|
import { installScaffold } from './install-scaffold.js';
|
|
5
|
+
/**
|
|
6
|
+
* Scaffolds a new markuplint rule as a standalone publishable npm package
|
|
7
|
+
* in the current working directory.
|
|
8
|
+
*
|
|
9
|
+
* Validates that the current directory is empty before proceeding.
|
|
10
|
+
* Generates a complete package structure including `package.json`.
|
|
11
|
+
*
|
|
12
|
+
* @param params - The rule creation parameters (plugin name, rule name, language, test preference).
|
|
13
|
+
* @returns The scaffold result containing generated files and dependencies.
|
|
14
|
+
* @throws {CreateRuleHelperError} If the current directory is not empty.
|
|
15
|
+
*/
|
|
5
16
|
export async function createRulePackage({ pluginName, ruleName, lang, needTest, }) {
|
|
6
17
|
const newRuleDir = path.resolve(process.cwd(), '*');
|
|
7
18
|
const files = await glob(newRuleDir);
|