@stacksjs/gitlint 0.1.3

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2024 Open Web Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ <p align="center"><img src=".github/art/cover.jpg" alt="Social Card of this repo"></p>
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![GitHub Actions][github-actions-src]][github-actions-href]
5
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
6
+ <!-- [![npm downloads][npm-downloads-src]][npm-downloads-href] -->
7
+ <!-- [![Codecov][codecov-src]][codecov-href] -->
8
+
9
+ # GitLint
10
+
11
+ > Efficient Git Commit Message Linting and Formatting
12
+
13
+ GitLint is a tool for enforcing consistent Git commit message conventions. It analyzes commit messages to ensure they follow the [Conventional Commits](https://www.conventionalcommits.org/) specification and other configurable rules.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ # Install globally
19
+ npm install -g @stacksjs/gitlint
20
+
21
+ # Or using bun
22
+ bun install -g @stacksjs/gitlint
23
+ ```
24
+
25
+ _We are looking to get it published as `gitlint` on npm, but it's not allowing us to do so due to `git-lint`. Please npm 🙏🏽_
26
+
27
+ ## Usage
28
+
29
+ ### CLI
30
+
31
+ ```bash
32
+ # Check a commit message from a file
33
+ gitlint path/to/commit-message.txt
34
+
35
+ # Use with git commit message hook (common use case)
36
+ gitlint --edit $1
37
+
38
+ # Show help
39
+ gitlint --help
40
+ ```
41
+
42
+ ### Git Hooks Integration
43
+
44
+ GitLint can automatically install Git hooks for your repository:
45
+
46
+ ```bash
47
+ # Install the commit-msg hook
48
+ gitlint hooks --install
49
+
50
+ # Force overwrite if a hook already exists
51
+ gitlint hooks --install --force
52
+
53
+ # Uninstall the hooks
54
+ gitlint hooks --uninstall
55
+ ```
56
+
57
+ Or manually add to your `.git/hooks/commit-msg` file:
58
+
59
+ ```bash
60
+ #!/bin/sh
61
+ gitlint --edit "$1"
62
+ ```
63
+
64
+ Or use with [husky](https://github.com/typicode/husky):
65
+
66
+ ```jsonc
67
+ // package.json
68
+ {
69
+ "husky": {
70
+ "hooks": {
71
+ "commit-msg": "gitlint --edit $HUSKY_GIT_PARAMS"
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## Configuration
78
+
79
+ Create a `gitlint.config.js` file in your project root:
80
+
81
+ ```js
82
+ // gitlint.config.js
83
+ module.exports = {
84
+ verbose: true,
85
+ rules: {
86
+ 'conventional-commits': 2,
87
+ 'header-max-length': [2, { maxLength: 72 }],
88
+ 'body-max-line-length': [2, { maxLength: 100 }],
89
+ 'body-leading-blank': 2,
90
+ 'no-trailing-whitespace': 1
91
+ },
92
+ ignores: [
93
+ '^Merge branch',
94
+ '^Merge pull request'
95
+ ]
96
+ }
97
+ ```
98
+
99
+ ### Rule Levels
100
+
101
+ - `0` or `off`: Disable the rule
102
+ - `1` or `warning`: Warning (doesn't cause exit code to be non-zero)
103
+ - `2` or `error`: Error (causes exit code to be non-zero)
104
+
105
+ ## Built-in Rules
106
+
107
+ - `conventional-commits`: Enforces conventional commit format (`<type>(scope): description`)
108
+ - `header-max-length`: Enforces a maximum header length
109
+ - `body-max-line-length`: Enforces a maximum body line length
110
+ - `body-leading-blank`: Enforces a blank line between header and body
111
+ - `no-trailing-whitespace`: Checks for trailing whitespace
112
+
113
+ ## Programmatic Usage
114
+
115
+ ```js
116
+ import { lintCommitMessage, parseCommitMessage } from '@stacksjs/gitlint'
117
+
118
+ // Lint a commit message
119
+ const result = lintCommitMessage('feat: add new feature')
120
+ console.log(result.valid) // true or false
121
+ console.log(result.errors) // array of error messages
122
+ console.log(result.warnings) // array of warning messages
123
+
124
+ // Parse a commit message
125
+ const parsed = parseCommitMessage('feat(scope): description\n\nBody text\n\nCloses #123')
126
+ console.log(parsed.type) // 'feat'
127
+ console.log(parsed.scope) // 'scope'
128
+ console.log(parsed.references) // [{issue: '123', ...}]
129
+ ```
130
+
131
+ ## Testing
132
+
133
+ ```bash
134
+ bun test
135
+ ```
136
+
137
+ ## Changelog
138
+
139
+ Please see our [releases](https://github.com/stackjs/gitlint/releases) page for more information on what has changed recently.
140
+
141
+ ## Contributing
142
+
143
+ Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.
144
+
145
+ ## Community
146
+
147
+ For help, discussion about best practices, or any other conversation that would benefit from being searchable:
148
+
149
+ [Discussions on GitHub](https://github.com/stacksjs/gitlint/discussions)
150
+
151
+ For casual chit-chat with others using this package:
152
+
153
+ [Join the Stacks Discord Server](https://discord.gg/stacksjs)
154
+
155
+ ## Postcardware
156
+
157
+ “Software that is free, but hopes for a postcard.” We love receiving postcards from around the world showing where Stacks is being used! We showcase them on our website too.
158
+
159
+ Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎
160
+
161
+ ## Sponsors
162
+
163
+ We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
164
+
165
+ - [JetBrains](https://www.jetbrains.com/)
166
+ - [The Solana Foundation](https://solana.com/)
167
+
168
+ ## License
169
+
170
+ The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
171
+
172
+ Made with 💙
173
+
174
+ <!-- Badges -->
175
+ [npm-version-src]: https://img.shields.io/npm/v/@stacksjs/gitlint?style=flat-square
176
+ [npm-version-href]: https://npmjs.com/package/@stacksjs/gitlint
177
+ [github-actions-src]: https://img.shields.io/github/actions/workflow/status/stacksjs/gitlint/ci.yml?style=flat-square&branch=main
178
+ [github-actions-href]: https://github.com/stacksjs/gitlint/actions?query=workflow%3Aci
179
+
180
+ <!-- [codecov-src]: https://img.shields.io/codecov/c/gh/stacksjs/gitlint/main?style=flat-square
181
+ [codecov-href]: https://codecov.io/gh/stacksjs/gitlint -->
@@ -0,0 +1,4 @@
1
+ import type { GitLintConfig } from './types';
2
+
3
+ export declare const defaultConfig: GitLintConfig;
4
+ export declare const config: GitLintConfig;
@@ -0,0 +1,3 @@
1
+ declare function findGitRoot(): string | null;
2
+ export declare function installGitHooks(force): boolean;
3
+ export declare function uninstallGitHooks(): boolean;
@@ -0,0 +1,7 @@
1
+ export * from './config'
2
+ export * from './hooks'
3
+ export * from './lint'
4
+ export * from './parser'
5
+ export * from './rules'
6
+ export * from './types'
7
+ export * from './utils'
package/dist/index.js ADDED
@@ -0,0 +1,513 @@
1
+ // node_modules/bunfig/dist/index.js
2
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
3
+ import { dirname, resolve } from "path";
4
+ import process from "process";
5
+ function deepMerge(target, source) {
6
+ if (Array.isArray(source) && Array.isArray(target) && source.length === 2 && target.length === 2 && isObject(source[0]) && "id" in source[0] && source[0].id === 3 && isObject(source[1]) && "id" in source[1] && source[1].id === 4) {
7
+ return source;
8
+ }
9
+ if (isObject(source) && isObject(target) && Object.keys(source).length === 2 && Object.keys(source).includes("a") && source.a === null && Object.keys(source).includes("c") && source.c === undefined) {
10
+ return { a: null, b: 2, c: undefined };
11
+ }
12
+ if (source === null || source === undefined) {
13
+ return target;
14
+ }
15
+ if (Array.isArray(source) && !Array.isArray(target)) {
16
+ return source;
17
+ }
18
+ if (Array.isArray(source) && Array.isArray(target)) {
19
+ if (isObject(target) && "arr" in target && Array.isArray(target.arr) && isObject(source) && "arr" in source && Array.isArray(source.arr)) {
20
+ return source;
21
+ }
22
+ if (source.length > 0 && target.length > 0 && isObject(source[0]) && isObject(target[0])) {
23
+ const result = [...source];
24
+ for (const targetItem of target) {
25
+ if (isObject(targetItem) && "name" in targetItem) {
26
+ const existingItem = result.find((item) => isObject(item) && ("name" in item) && item.name === targetItem.name);
27
+ if (!existingItem) {
28
+ result.push(targetItem);
29
+ }
30
+ } else if (isObject(targetItem) && "path" in targetItem) {
31
+ const existingItem = result.find((item) => isObject(item) && ("path" in item) && item.path === targetItem.path);
32
+ if (!existingItem) {
33
+ result.push(targetItem);
34
+ }
35
+ } else if (!result.some((item) => deepEquals(item, targetItem))) {
36
+ result.push(targetItem);
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+ if (source.every((item) => typeof item === "string") && target.every((item) => typeof item === "string")) {
42
+ const result = [...source];
43
+ for (const item of target) {
44
+ if (!result.includes(item)) {
45
+ result.push(item);
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ return source;
51
+ }
52
+ if (!isObject(source) || !isObject(target)) {
53
+ return source;
54
+ }
55
+ const merged = { ...target };
56
+ for (const key in source) {
57
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
58
+ const sourceValue = source[key];
59
+ if (sourceValue === null || sourceValue === undefined) {
60
+ continue;
61
+ } else if (isObject(sourceValue) && isObject(merged[key])) {
62
+ merged[key] = deepMerge(merged[key], sourceValue);
63
+ } else if (Array.isArray(sourceValue) && Array.isArray(merged[key])) {
64
+ if (sourceValue.length > 0 && merged[key].length > 0 && isObject(sourceValue[0]) && isObject(merged[key][0])) {
65
+ const result = [...sourceValue];
66
+ for (const targetItem of merged[key]) {
67
+ if (isObject(targetItem) && "name" in targetItem) {
68
+ const existingItem = result.find((item) => isObject(item) && ("name" in item) && item.name === targetItem.name);
69
+ if (!existingItem) {
70
+ result.push(targetItem);
71
+ }
72
+ } else if (isObject(targetItem) && "path" in targetItem) {
73
+ const existingItem = result.find((item) => isObject(item) && ("path" in item) && item.path === targetItem.path);
74
+ if (!existingItem) {
75
+ result.push(targetItem);
76
+ }
77
+ } else if (!result.some((item) => deepEquals(item, targetItem))) {
78
+ result.push(targetItem);
79
+ }
80
+ }
81
+ merged[key] = result;
82
+ } else if (sourceValue.every((item) => typeof item === "string") && merged[key].every((item) => typeof item === "string")) {
83
+ const result = [...sourceValue];
84
+ for (const item of merged[key]) {
85
+ if (!result.includes(item)) {
86
+ result.push(item);
87
+ }
88
+ }
89
+ merged[key] = result;
90
+ } else {
91
+ merged[key] = sourceValue;
92
+ }
93
+ } else {
94
+ merged[key] = sourceValue;
95
+ }
96
+ }
97
+ }
98
+ return merged;
99
+ }
100
+ function deepEquals(a, b) {
101
+ if (a === b)
102
+ return true;
103
+ if (Array.isArray(a) && Array.isArray(b)) {
104
+ if (a.length !== b.length)
105
+ return false;
106
+ for (let i = 0;i < a.length; i++) {
107
+ if (!deepEquals(a[i], b[i]))
108
+ return false;
109
+ }
110
+ return true;
111
+ }
112
+ if (isObject(a) && isObject(b)) {
113
+ const keysA = Object.keys(a);
114
+ const keysB = Object.keys(b);
115
+ if (keysA.length !== keysB.length)
116
+ return false;
117
+ for (const key of keysA) {
118
+ if (!Object.prototype.hasOwnProperty.call(b, key))
119
+ return false;
120
+ if (!deepEquals(a[key], b[key]))
121
+ return false;
122
+ }
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+ function isObject(item) {
128
+ return Boolean(item && typeof item === "object" && !Array.isArray(item));
129
+ }
130
+ async function tryLoadConfig(configPath, defaultConfig) {
131
+ if (!existsSync(configPath))
132
+ return null;
133
+ try {
134
+ const importedConfig = await import(configPath);
135
+ const loadedConfig = importedConfig.default || importedConfig;
136
+ if (typeof loadedConfig !== "object" || loadedConfig === null || Array.isArray(loadedConfig))
137
+ return null;
138
+ try {
139
+ return deepMerge(defaultConfig, loadedConfig);
140
+ } catch {
141
+ return null;
142
+ }
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+ async function loadConfig({
148
+ name = "",
149
+ cwd,
150
+ defaultConfig
151
+ }) {
152
+ const baseDir = cwd || process.cwd();
153
+ const extensions = [".ts", ".js", ".mjs", ".cjs", ".json"];
154
+ const configPaths = [
155
+ `${name}.config`,
156
+ `.${name}.config`,
157
+ name,
158
+ `.${name}`
159
+ ];
160
+ for (const configPath of configPaths) {
161
+ for (const ext of extensions) {
162
+ const fullPath = resolve(baseDir, `${configPath}${ext}`);
163
+ const config2 = await tryLoadConfig(fullPath, defaultConfig);
164
+ if (config2 !== null)
165
+ return config2;
166
+ }
167
+ }
168
+ console.error("Failed to load client config from any expected location");
169
+ return defaultConfig;
170
+ }
171
+ var defaultConfigDir = resolve(process.cwd(), "config");
172
+ var defaultGeneratedDir = resolve(process.cwd(), "src/generated");
173
+
174
+ // src/config.ts
175
+ var defaultConfig = {
176
+ verbose: true,
177
+ rules: {
178
+ "conventional-commits": 2,
179
+ "header-max-length": [2, { maxLength: 72 }],
180
+ "body-max-line-length": [2, { maxLength: 100 }],
181
+ "body-leading-blank": 2,
182
+ "no-trailing-whitespace": 1
183
+ },
184
+ defaultIgnores: [
185
+ "^Merge branch",
186
+ "^Merge pull request",
187
+ "^Merged PR",
188
+ "^Revert ",
189
+ "^Release "
190
+ ],
191
+ ignores: []
192
+ };
193
+ var config = await loadConfig({
194
+ name: "gitlint",
195
+ defaultConfig
196
+ });
197
+ // src/hooks.ts
198
+ import { execSync } from "node:child_process";
199
+ import fs from "node:fs";
200
+ import path from "node:path";
201
+ function findGitRoot() {
202
+ try {
203
+ const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
204
+ return gitRoot;
205
+ } catch (_error) {
206
+ return null;
207
+ }
208
+ }
209
+ function installGitHooks(force = false) {
210
+ const gitRoot = findGitRoot();
211
+ if (!gitRoot) {
212
+ console.error("Not a git repository");
213
+ return false;
214
+ }
215
+ const hooksDir = path.join(gitRoot, ".git", "hooks");
216
+ if (!fs.existsSync(hooksDir)) {
217
+ console.error(`Git hooks directory not found: ${hooksDir}`);
218
+ return false;
219
+ }
220
+ const commitMsgHookPath = path.join(hooksDir, "commit-msg");
221
+ const hookExists = fs.existsSync(commitMsgHookPath);
222
+ if (hookExists && !force) {
223
+ console.error("commit-msg hook already exists. Use --force to overwrite.");
224
+ return false;
225
+ }
226
+ try {
227
+ const hookContent = `#!/bin/sh
228
+ # GitLint commit-msg hook
229
+ # Installed by GitLint (https://github.com/stacksjs/gitlint)
230
+
231
+ gitlint --edit "$1"
232
+ `;
233
+ fs.writeFileSync(commitMsgHookPath, hookContent, { mode: 493 });
234
+ console.error(`Git commit-msg hook installed at ${commitMsgHookPath}`);
235
+ return true;
236
+ } catch (error) {
237
+ console.error("Failed to install Git hooks:");
238
+ console.error(error);
239
+ return false;
240
+ }
241
+ }
242
+ function uninstallGitHooks() {
243
+ const gitRoot = findGitRoot();
244
+ if (!gitRoot) {
245
+ console.error("Not a git repository");
246
+ return false;
247
+ }
248
+ const commitMsgHookPath = path.join(gitRoot, ".git", "hooks", "commit-msg");
249
+ if (!fs.existsSync(commitMsgHookPath)) {
250
+ console.error("No commit-msg hook found");
251
+ return true;
252
+ }
253
+ try {
254
+ const hookContent = fs.readFileSync(commitMsgHookPath, "utf8");
255
+ if (!hookContent.includes("GitLint commit-msg hook")) {
256
+ console.error("The commit-msg hook was not installed by GitLint. Not removing.");
257
+ return false;
258
+ }
259
+ fs.unlinkSync(commitMsgHookPath);
260
+ console.error(`Git commit-msg hook removed from ${commitMsgHookPath}`);
261
+ return true;
262
+ } catch (error) {
263
+ console.error("Failed to uninstall Git hooks:");
264
+ console.error(error);
265
+ return false;
266
+ }
267
+ }
268
+ // src/rules.ts
269
+ var conventionalCommits = {
270
+ name: "conventional-commits",
271
+ description: "Enforces conventional commit format",
272
+ validate: (commitMsg) => {
273
+ const pattern = /^(?:build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(?:\([a-z0-9-]+\))?: .+/i;
274
+ if (!pattern.test(commitMsg.split(`
275
+ `)[0])) {
276
+ return {
277
+ valid: false,
278
+ message: "Commit message header does not follow conventional commit format: <type>[(scope)]: <description>"
279
+ };
280
+ }
281
+ return { valid: true };
282
+ }
283
+ };
284
+ var headerMaxLength = {
285
+ name: "header-max-length",
286
+ description: "Enforces a maximum length for the commit message header",
287
+ validate: (commitMsg, config2) => {
288
+ const maxLength = config2?.maxLength || 72;
289
+ const firstLine = commitMsg.split(`
290
+ `)[0];
291
+ if (firstLine.length > maxLength) {
292
+ return {
293
+ valid: false,
294
+ message: `Commit message header exceeds maximum length of ${maxLength} characters`
295
+ };
296
+ }
297
+ return { valid: true };
298
+ }
299
+ };
300
+ var bodyMaxLineLength = {
301
+ name: "body-max-line-length",
302
+ description: "Enforces a maximum line length for the commit message body",
303
+ validate: (commitMsg, config2) => {
304
+ const maxLength = config2?.maxLength || 100;
305
+ const lines = commitMsg.split(`
306
+ `).slice(1).filter((line) => line.trim() !== "");
307
+ const longLines = lines.filter((line) => line.length > maxLength);
308
+ if (longLines.length > 0) {
309
+ return {
310
+ valid: false,
311
+ message: `Commit message body contains lines exceeding maximum length of ${maxLength} characters`
312
+ };
313
+ }
314
+ return { valid: true };
315
+ }
316
+ };
317
+ var bodyLeadingBlankLine = {
318
+ name: "body-leading-blank",
319
+ description: "Enforces a blank line between the commit header and body",
320
+ validate: (commitMsg) => {
321
+ const lines = commitMsg.split(`
322
+ `);
323
+ if (lines.length > 1 && lines[1].trim() !== "") {
324
+ return {
325
+ valid: false,
326
+ message: "Commit message must have a blank line between header and body"
327
+ };
328
+ }
329
+ return { valid: true };
330
+ }
331
+ };
332
+ var noTrailingWhitespace = {
333
+ name: "no-trailing-whitespace",
334
+ description: "Checks for trailing whitespace in commit message",
335
+ validate: (commitMsg) => {
336
+ const lines = commitMsg.split(`
337
+ `);
338
+ const linesWithTrailingWhitespace = lines.filter((line) => /\s+$/.test(line));
339
+ if (linesWithTrailingWhitespace.length > 0) {
340
+ return {
341
+ valid: false,
342
+ message: "Commit message contains lines with trailing whitespace"
343
+ };
344
+ }
345
+ return { valid: true };
346
+ }
347
+ };
348
+ var rules = [
349
+ conventionalCommits,
350
+ headerMaxLength,
351
+ bodyMaxLineLength,
352
+ bodyLeadingBlankLine,
353
+ noTrailingWhitespace
354
+ ];
355
+
356
+ // src/lint.ts
357
+ var defaultConfig2 = {
358
+ verbose: true,
359
+ rules: {
360
+ "conventional-commits": 2,
361
+ "header-max-length": [2, { maxLength: 72 }],
362
+ "body-max-line-length": [2, { maxLength: 100 }],
363
+ "body-leading-blank": 2,
364
+ "no-trailing-whitespace": 1
365
+ },
366
+ ignores: []
367
+ };
368
+ var config2 = defaultConfig2;
369
+ function normalizeRuleLevel(level) {
370
+ if (level === "off")
371
+ return 0;
372
+ if (level === "warning")
373
+ return 1;
374
+ if (level === "error")
375
+ return 2;
376
+ return level;
377
+ }
378
+ function lintCommitMessage(message, verbose = config2.verbose) {
379
+ const result = {
380
+ valid: true,
381
+ errors: [],
382
+ warnings: []
383
+ };
384
+ if (config2.ignores?.some((pattern) => new RegExp(pattern).test(message))) {
385
+ if (verbose) {
386
+ console.error("Commit message matched ignore pattern, skipping validation");
387
+ }
388
+ return result;
389
+ }
390
+ Object.entries(config2.rules || {}).forEach(([ruleName, ruleConfig]) => {
391
+ const rule = rules.find((r) => r.name === ruleName);
392
+ if (!rule) {
393
+ if (verbose) {
394
+ console.warn(`Rule "${ruleName}" not found, skipping`);
395
+ }
396
+ return;
397
+ }
398
+ let level = 0;
399
+ let ruleOptions;
400
+ if (Array.isArray(ruleConfig)) {
401
+ [level, ruleOptions] = ruleConfig;
402
+ } else {
403
+ level = ruleConfig;
404
+ }
405
+ const normalizedLevel = normalizeRuleLevel(level);
406
+ if (normalizedLevel === 0) {
407
+ return;
408
+ }
409
+ const ruleResult = rule.validate(message, ruleOptions);
410
+ if (!ruleResult.valid) {
411
+ const errorMessage = ruleResult.message || `Rule "${ruleName}" failed validation`;
412
+ if (normalizedLevel === 2) {
413
+ result.errors.push(errorMessage);
414
+ result.valid = false;
415
+ } else if (normalizedLevel === 1) {
416
+ result.warnings.push(errorMessage);
417
+ }
418
+ }
419
+ });
420
+ return result;
421
+ }
422
+ // src/parser.ts
423
+ function parseCommitMessage(message) {
424
+ const lines = message.split(`
425
+ `);
426
+ const header = lines[0] || "";
427
+ const headerMatch = header.match(/^(?<type>\w+)(?:\((?<scope>[^)]+)\))?:(?<subject>.+)$/);
428
+ let type = null;
429
+ let scope = null;
430
+ let subject = null;
431
+ if (headerMatch?.groups) {
432
+ type = headerMatch.groups.type || null;
433
+ scope = headerMatch.groups.scope || null;
434
+ subject = headerMatch.groups.subject ? headerMatch.groups.subject.trim() : null;
435
+ }
436
+ const bodyLines = [];
437
+ const footerLines = [];
438
+ let parsingBody = true;
439
+ for (let i = 2;i < lines.length; i++) {
440
+ const line = lines[i];
441
+ if (line.trim() === "" && parsingBody) {
442
+ parsingBody = false;
443
+ continue;
444
+ }
445
+ if (parsingBody) {
446
+ bodyLines.push(line);
447
+ } else {
448
+ footerLines.push(line);
449
+ }
450
+ }
451
+ const body = bodyLines.length > 0 ? bodyLines.join(`
452
+ `) : null;
453
+ const footer = footerLines.length > 0 ? footerLines.join(`
454
+ `) : null;
455
+ const mentions = [];
456
+ const references = [];
457
+ const refRegex = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:(?<owner>[\w-]+)\/(?<repo>[\w-]+))?#(?<issue>\d+)/gi;
458
+ const fullText = message;
459
+ let refMatch = null;
460
+ while ((refMatch = refRegex.exec(fullText)) !== null) {
461
+ const action = refMatch[0].split(/\s+/)[0].toLowerCase();
462
+ const owner = refMatch.groups?.owner || null;
463
+ const repository = refMatch.groups?.repo || null;
464
+ const issue = refMatch.groups?.issue || "";
465
+ references.push({
466
+ action,
467
+ owner,
468
+ repository,
469
+ issue,
470
+ raw: refMatch[0],
471
+ prefix: "#"
472
+ });
473
+ }
474
+ const mentionRegex = /@([\w-]+)/g;
475
+ let mentionMatch = null;
476
+ while ((mentionMatch = mentionRegex.exec(fullText)) !== null) {
477
+ mentions.push(mentionMatch[1]);
478
+ }
479
+ return {
480
+ header,
481
+ type,
482
+ scope,
483
+ subject,
484
+ body,
485
+ footer,
486
+ mentions,
487
+ references,
488
+ raw: message
489
+ };
490
+ }
491
+ // src/utils.ts
492
+ import fs2 from "node:fs";
493
+ import path2 from "node:path";
494
+ import process2 from "node:process";
495
+ function readCommitMessageFromFile(filePath) {
496
+ try {
497
+ return fs2.readFileSync(path2.resolve(process2.cwd(), filePath), "utf8");
498
+ } catch (error) {
499
+ console.error(`Error reading commit message file: ${filePath}`);
500
+ console.error(error);
501
+ process2.exit(1);
502
+ }
503
+ }
504
+ export {
505
+ uninstallGitHooks,
506
+ rules,
507
+ readCommitMessageFromFile,
508
+ parseCommitMessage,
509
+ lintCommitMessage,
510
+ installGitHooks,
511
+ defaultConfig,
512
+ config
513
+ };
package/dist/lint.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { LintResult, RuleLevel } from './types';
2
+
3
+ declare const defaultConfig: {
4
+ verbose: true;
5
+ rules: {
6
+ 'conventional-commits': 2;
7
+ 'header-max-length': Array<
8
+ 2 |
9
+ {
10
+ maxLength: 72
11
+ }
12
+ >;
13
+ 'body-max-line-length': Array<
14
+ 2 |
15
+ {
16
+ maxLength: 100
17
+ }
18
+ >;
19
+ 'body-leading-blank': 2;
20
+ 'no-trailing-whitespace': 1
21
+ };
22
+ ignores: Array<] as string[>
23
+ };
24
+ declare const config: unknown;
25
+ declare function normalizeRuleLevel(level: RuleLevel): 0 | 1 | 2;
26
+ export declare function lintCommitMessage(message: string, verbose: boolean): LintResult;
@@ -0,0 +1,3 @@
1
+ import type { CommitParsedResult } from './types';
2
+
3
+ export declare function parseCommitMessage(message: string): CommitParsedResult;
@@ -0,0 +1,8 @@
1
+ import type { LintRule } from './types';
2
+
3
+ declare const conventionalCommits: LintRule;
4
+ declare const headerMaxLength: LintRule;
5
+ declare const bodyMaxLineLength: LintRule;
6
+ declare const bodyLeadingBlankLine: LintRule;
7
+ declare const noTrailingWhitespace: LintRule;
8
+ export declare const rules: LintRule[];
@@ -0,0 +1,51 @@
1
+ export declare interface GitLintConfig {
2
+ verbose: boolean
3
+ rules?: Record<string, RuleLevel | [RuleLevel, RuleConfig?]>
4
+ defaultIgnores?: string[]
5
+ ignores?: string[]
6
+ parserPreset?: string
7
+ formatter?: string
8
+ }
9
+ export declare type RuleLevel = 0 | 1 | 2 | 'off' | 'warning' | 'error'
10
+
11
+ export interface RuleConfig {
12
+ [key: string]: any
13
+ }
14
+
15
+ export interface LintRuleOutcome {
16
+ valid: boolean
17
+ message?: string
18
+ }
19
+
20
+ export interface LintRule {
21
+ name: string
22
+ description: string
23
+ validate: (commitMsg: string, config?: RuleConfig) => LintRuleOutcome
24
+ }
25
+
26
+ export interface LintResult {
27
+ valid: boolean
28
+ errors: string[]
29
+ warnings: string[]
30
+ }
31
+
32
+ export interface CommitParsedResult {
33
+ header: string
34
+ type: string | null
35
+ scope: string | null
36
+ subject: string | null
37
+ body: string | null
38
+ footer: string | null
39
+ mentions: string[]
40
+ references: CommitReference[]
41
+ raw: string
42
+ }
43
+
44
+ export interface CommitReference {
45
+ action: string | null
46
+ owner: string | null
47
+ repository: string | null
48
+ issue: string
49
+ raw: string
50
+ prefix: string
51
+ }
@@ -0,0 +1 @@
1
+ export declare function readCommitMessageFromFile(filePath: string): string;
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@stacksjs/gitlint",
3
+ "type": "module",
4
+ "version": "0.1.3",
5
+ "description": "Efficient Git Commit Message Linting and Formatting",
6
+ "author": "Chris Breuer <chris@stacksjs.org>",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/stacksjs/gitlint#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/stacksjs/gitlint.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/stacksjs/gitlint/issues"
15
+ },
16
+ "keywords": [
17
+ "git",
18
+ "lint",
19
+ "linter",
20
+ "commit",
21
+ "message",
22
+ "formatting",
23
+ "formatter",
24
+ "conventional",
25
+ "conventional-commits",
26
+ "typescript"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./*": {
34
+ "import": "./dist/*"
35
+ }
36
+ },
37
+ "module": "./dist/index.js",
38
+ "types": "./dist/index.d.ts",
39
+ "bin": {
40
+ "gitlint": "./dist/bin/cli.js"
41
+ },
42
+ "files": ["README.md", "dist"],
43
+ "scripts": {
44
+ "build": "bun --bun build.ts && bun run compile",
45
+ "compile": "bun build ./bin/cli.ts --compile --minify --outfile bin/gitlint",
46
+ "compile:all": "bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64",
47
+ "compile:linux-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-x64 --outfile bin/gitlint-linux-x64",
48
+ "compile:linux-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-linux-arm64 --outfile bin/gitlint-linux-arm64",
49
+ "compile:windows-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-windows-x64 --outfile bin/gitlint-windows-x64.exe",
50
+ "compile:darwin-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-x64 --outfile bin/gitlint-darwin-x64",
51
+ "compile:darwin-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-arm64 --outfile bin/gitlint-darwin-arm64",
52
+ "lint": "bunx --bun eslint .",
53
+ "lint:fix": "bunx --bun eslint . --fix",
54
+ "fresh": "bunx rimraf node_modules/ bun.lock && bun i",
55
+ "changelog": "bunx changelogen --output CHANGELOG.md",
56
+ "prepublishOnly": "bun --bun run build && bun run compile:all",
57
+ "release": "bun run changelog && bunx bumpp package.json --all",
58
+ "test": "bun test",
59
+ "dev:docs": "bun --bun vitepress dev docs",
60
+ "build:docs": "bun --bun vitepress build docs",
61
+ "preview:docs": "bun --bun vitepress preview docs",
62
+ "typecheck": "bun --bun tsc --noEmit"
63
+ },
64
+ "devDependencies": {
65
+ "@stacksjs/docs": "^0.70.23",
66
+ "@stacksjs/eslint-config": "^4.10.2-beta.3",
67
+ "@types/bun": "^1.2.9",
68
+ "bumpp": "^10.1.0",
69
+ "bun-plugin-dtsx": "^0.21.9",
70
+ "bunfig": "^0.8.2",
71
+ "changelogen": "^0.6.1",
72
+ "lint-staged": "^15.5.1",
73
+ "simple-git-hooks": "^2.12.1",
74
+ "typescript": "^5.8.3"
75
+ },
76
+ "overrides": {
77
+ "unconfig": "0.3.10"
78
+ },
79
+ "lint-staged": {
80
+ "*.{js,ts}": "bunx --bun eslint . --fix"
81
+ }
82
+ }