@markuplint/create-rule 4.7.21 → 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.
@@ -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
- output(pluginName || 'core', file.test ? '🖍 ' : (icons[file.name] ?? '🛡 '), file.fileName, path.resolve(file.destDir, file.fileName + file.ext));
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
- function output(name, icon, title, path) {
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 = (path) => ' ' + font.cyanBright(path);
77
- process.stdout.write(_marker + _title(icon, title) + _file(path));
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);