commit-sheriff 1.0.0 → 1.1.1
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/README.md
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Shared [Husky](https://typicode.github.io/husky/) git hooks for any project — enforces a consistent **branch naming format** and **commit message format** (ticket + type, optionally module), and runs **lint-staged** / type-check / related tests before every commit.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quick start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
npm i commit-sheriff
|
|
8
9
|
npx commit-sheriff init
|
|
10
|
+
npx commit-sheriff add-eslint-react # optional — TypeScript/React + module-boundary ESLint config
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
## Table of contents
|
|
12
14
|
|
|
15
|
+
- [Quick start](#quick-start)
|
|
13
16
|
- [Why](#why)
|
|
14
17
|
- [Requirements](#requirements)
|
|
15
18
|
- [Install](#install)
|
|
@@ -25,6 +28,7 @@ npx commit-sheriff init
|
|
|
25
28
|
- [`types`](#types)
|
|
26
29
|
- [Pre-commit checks](#pre-commit-checks)
|
|
27
30
|
- [lint-staged](#lint-staged)
|
|
31
|
+
- [Advanced: ESLint module-boundary template (TypeScript/React)](#advanced-eslint-module-boundary-template-typescriptreact)
|
|
28
32
|
- [Examples](#examples)
|
|
29
33
|
- [Updating](#updating)
|
|
30
34
|
- [Skipping / bypassing hooks](#skipping--bypassing-hooks)
|
|
@@ -241,6 +245,40 @@ The default config added by `init` (only if you don't already have one):
|
|
|
241
245
|
|
|
242
246
|
Adjust freely — `commit-sheriff` doesn't overwrite it once present.
|
|
243
247
|
|
|
248
|
+
## Advanced: ESLint module-boundary template (TypeScript/React)
|
|
249
|
+
|
|
250
|
+
Optional, separate from `init` — for projects that split feature code into modules (e.g.
|
|
251
|
+
`src/app/auth/`, `src/app/billing/`) and want ESLint to enforce that modules only talk to each
|
|
252
|
+
other through a public barrel file, never through deep/internal paths:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
npx commit-sheriff add-eslint-react
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
This copies two templates into your repo root, **without overwriting** anything already there:
|
|
259
|
+
|
|
260
|
+
- `.eslintrc.js` — legacy-format ESLint config (TypeScript + React + a11y + import + sonarjs +
|
|
261
|
+
testing-library/jest/vitest rules) with a module-boundary `no-restricted-imports` rule.
|
|
262
|
+
- `.prettierrc.js` — matching Prettier config.
|
|
263
|
+
|
|
264
|
+
The module list for the boundary rule isn't hardcoded — it's read straight from your
|
|
265
|
+
`commitGuard.modules` array in `package.json` (the same list the commit-msg/branch-name hooks
|
|
266
|
+
use for the `[MODULE]` tag), lowercased. If you haven't set `commitGuard.modules`, a small
|
|
267
|
+
illustrative default (`auth`, `billing`, `reports`, `settings`) is used — open `.eslintrc.js`
|
|
268
|
+
and replace it, or just fill in `commitGuard.modules` and it picks it up automatically.
|
|
269
|
+
|
|
270
|
+
This template assumes a `src/app/<module>/...` folder layout with `<module>.public.ts` barrel
|
|
271
|
+
files; adjust the paths inside `.eslintrc.js` if your structure differs.
|
|
272
|
+
|
|
273
|
+
It doesn't install any dependencies for you — install what your project needs, e.g.:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
npm install --save-dev typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin \
|
|
277
|
+
eslint-plugin-sonarjs eslint-plugin-import eslint-plugin-prettier eslint-config-prettier \
|
|
278
|
+
eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y \
|
|
279
|
+
eslint-plugin-testing-library eslint-plugin-vitest eslint-plugin-jest prettier
|
|
280
|
+
```
|
|
281
|
+
|
|
244
282
|
## Examples
|
|
245
283
|
|
|
246
284
|
**Minimal project (no modules):**
|
package/bin/commit-sheriff.js
CHANGED
|
@@ -111,9 +111,54 @@ function init() {
|
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
const ESLINT_REACT_DEV_DEPS = [
|
|
115
|
+
"typescript",
|
|
116
|
+
"@typescript-eslint/parser",
|
|
117
|
+
"@typescript-eslint/eslint-plugin",
|
|
118
|
+
"eslint-plugin-sonarjs",
|
|
119
|
+
"eslint-plugin-import",
|
|
120
|
+
"eslint-plugin-prettier",
|
|
121
|
+
"eslint-config-prettier",
|
|
122
|
+
"eslint-plugin-react",
|
|
123
|
+
"eslint-plugin-react-hooks",
|
|
124
|
+
"eslint-plugin-jsx-a11y",
|
|
125
|
+
"eslint-plugin-testing-library",
|
|
126
|
+
"eslint-plugin-vitest",
|
|
127
|
+
"eslint-plugin-jest",
|
|
128
|
+
"prettier",
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
function copyTemplateIfMissing(templateName, destName) {
|
|
132
|
+
const src = path.join(__dirname, "..", "templates", templateName);
|
|
133
|
+
const dest = path.join(cwd, destName);
|
|
134
|
+
if (fs.existsSync(dest)) {
|
|
135
|
+
console.log(`• ${destName} artıq var, toxunulmadı`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
fs.copyFileSync(src, dest);
|
|
139
|
+
console.log(`✔ ${destName} yazıldı`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function addEslintReact() {
|
|
143
|
+
copyTemplateIfMissing("eslintrc.module-boundaries.js", ".eslintrc.js");
|
|
144
|
+
copyTemplateIfMissing("prettier.module-boundaries.js", ".prettierrc.js");
|
|
145
|
+
console.log(
|
|
146
|
+
"\nBu şablon TypeScript/React + modul-sərhəd (module-boundary) qaydaları üçündür.\n" +
|
|
147
|
+
"Modul siyahısı package.json → commitGuard.modules-dən avtomatik oxunur (boşdursa nümunə\n" +
|
|
148
|
+
"siyahı istifadə olunur — .eslintrc.js içindəki şərhi oxuyun).\n\n" +
|
|
149
|
+
"Lazımi devDependencies (özünüz quraşdırın, layihənizin real versiyalarına uyğun):\n" +
|
|
150
|
+
` npm install --save-dev ${ESLINT_REACT_DEV_DEPS.join(" ")}\n`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
114
154
|
if (command === "init") {
|
|
115
155
|
init();
|
|
156
|
+
} else if (command === "add-eslint-react") {
|
|
157
|
+
addEslintReact();
|
|
116
158
|
} else {
|
|
117
159
|
console.log("İstifadə: npx commit-sheriff init");
|
|
160
|
+
console.log(
|
|
161
|
+
" : npx commit-sheriff add-eslint-react (opsional, TS/React modul-sərhəd ESLint şablonu)",
|
|
162
|
+
);
|
|
118
163
|
process.exit(command ? 1 : 0);
|
|
119
164
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commit-sheriff",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Shared husky commit-msg / pre-commit hooks enforcing ticket-based commit messages and branch naming",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,12 @@
|
|
|
23
23
|
"conventional-commits",
|
|
24
24
|
"lint-staged",
|
|
25
25
|
"branch-naming",
|
|
26
|
-
"ticket"
|
|
26
|
+
"ticket",
|
|
27
|
+
"eslint",
|
|
28
|
+
"prettier",
|
|
29
|
+
"module-boundaries",
|
|
30
|
+
"typescript",
|
|
31
|
+
"react"
|
|
27
32
|
],
|
|
28
33
|
"author": "",
|
|
29
34
|
"license": "ISC",
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced ESLint config — enforces module boundaries in a modular TypeScript/React app.
|
|
3
|
+
*
|
|
4
|
+
* This is an OPTIONAL, opt-in template (not installed by `commit-sheriff init`). Copy it to
|
|
5
|
+
* your repo root as `.eslintrc.js` via `npx commit-sheriff add-eslint-react`, then adjust the
|
|
6
|
+
* assumptions below to match your project structure.
|
|
7
|
+
*
|
|
8
|
+
* Assumptions this template makes (edit as needed):
|
|
9
|
+
* - Feature modules live under `src/app/<module>/` (e.g. `src/app/auth/`).
|
|
10
|
+
* - Each module exposes its public surface via a `<module>.public.ts` barrel file; anything
|
|
11
|
+
* else under `src/app/<module>/modules/**` or `src/app/<module>/shared/**` is internal and
|
|
12
|
+
* must not be imported directly from another module.
|
|
13
|
+
* - Shared/base code lives under `src/app/_shared/**` and `src/packages/**` and must never
|
|
14
|
+
* import from a feature module (that would invert the dependency direction).
|
|
15
|
+
* - `src/packages/routing/AppRoutes.tsx` and `src/packages/routing/config/modules.registry.ts`
|
|
16
|
+
* are the one place allowed to import every module directly (that's the whole point of a
|
|
17
|
+
* router/registry), so the boundary rule is turned off just for those two files.
|
|
18
|
+
*
|
|
19
|
+
* Module list: instead of hardcoding module names, this reads `commitGuard.modules` from
|
|
20
|
+
* package.json (the same list used by the commit-sheriff commit-msg/branch-name hooks — see
|
|
21
|
+
* README), so the two conventions (module tags in commits/branches, and module boundaries in
|
|
22
|
+
* imports) stay in sync automatically. If `commitGuard.modules` is empty/absent, a small
|
|
23
|
+
* illustrative default is used instead — replace it with your real module names.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const path = require("path");
|
|
28
|
+
|
|
29
|
+
function loadAppModules() {
|
|
30
|
+
try {
|
|
31
|
+
const pkgPath = path.join(__dirname, "package.json");
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
33
|
+
const modules = pkg.commitGuard && pkg.commitGuard.modules;
|
|
34
|
+
if (Array.isArray(modules) && modules.length) {
|
|
35
|
+
return modules.map((m) => String(m).toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// fall through to the illustrative default below
|
|
39
|
+
}
|
|
40
|
+
return ["auth", "billing", "reports", "settings"]; // example only — replace with your modules
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const APP_MODULES = loadAppModules();
|
|
44
|
+
const MODULE_INTERNALS = ["modules", "shared"];
|
|
45
|
+
|
|
46
|
+
const restrictImports = (group, message) => ({
|
|
47
|
+
"no-restricted-imports": ["error", { patterns: [{ group, message }] }],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const moduleBoundaryOverrides = [
|
|
51
|
+
// A module reaches another module's internals only through `<module>.public.ts`, so the
|
|
52
|
+
// surface between modules stays explicit. `routing/` stays open on purpose: route enums are
|
|
53
|
+
// dependency-free leaves, while the barrel pulls in services that instantiate on import.
|
|
54
|
+
...APP_MODULES.map((appModule) => ({
|
|
55
|
+
files: [`src/app/${appModule}/**`],
|
|
56
|
+
rules: restrictImports(
|
|
57
|
+
APP_MODULES.filter((other) => other !== appModule).flatMap((other) =>
|
|
58
|
+
MODULE_INTERNALS.map((internal) => `~/app/${other}/${internal}/**`),
|
|
59
|
+
),
|
|
60
|
+
"Cross-module import: use the other module's '<module>.public.ts' barrel instead of a deep path.",
|
|
61
|
+
),
|
|
62
|
+
})),
|
|
63
|
+
{
|
|
64
|
+
files: ["src/app/_shared/**", "src/packages/**"],
|
|
65
|
+
rules: restrictImports(
|
|
66
|
+
APP_MODULES.map((appModule) => `~/app/${appModule}/**`),
|
|
67
|
+
"Base layers must not depend on a feature module: move the shared piece down into _shared or packages instead.",
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
files: [
|
|
72
|
+
"src/packages/routing/AppRoutes.tsx",
|
|
73
|
+
"src/packages/routing/config/modules.registry.ts",
|
|
74
|
+
],
|
|
75
|
+
rules: { "no-restricted-imports": "off" },
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
root: true,
|
|
81
|
+
ignorePatterns: [
|
|
82
|
+
"coverage/**",
|
|
83
|
+
"dist/**",
|
|
84
|
+
"build/**",
|
|
85
|
+
"node_modules/**",
|
|
86
|
+
"**/*.d.ts",
|
|
87
|
+
"**/*.config.js",
|
|
88
|
+
],
|
|
89
|
+
env: {
|
|
90
|
+
browser: true,
|
|
91
|
+
es2021: true,
|
|
92
|
+
node: true,
|
|
93
|
+
},
|
|
94
|
+
parser: "@typescript-eslint/parser",
|
|
95
|
+
parserOptions: {
|
|
96
|
+
ecmaVersion: "latest",
|
|
97
|
+
sourceType: "module",
|
|
98
|
+
ecmaFeatures: {
|
|
99
|
+
jsx: true,
|
|
100
|
+
},
|
|
101
|
+
project: true,
|
|
102
|
+
},
|
|
103
|
+
settings: {
|
|
104
|
+
react: {
|
|
105
|
+
version: "detect",
|
|
106
|
+
},
|
|
107
|
+
linkComponents: [
|
|
108
|
+
{ name: "RouterLink", linkAttribute: ["href", "to"] },
|
|
109
|
+
{ name: "Link", linkAttribute: ["href", "to"] },
|
|
110
|
+
],
|
|
111
|
+
jest: {
|
|
112
|
+
version: "29.0",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
overrides: [
|
|
116
|
+
{
|
|
117
|
+
files: ["*.js", "**/*.styles.ts"],
|
|
118
|
+
rules: {
|
|
119
|
+
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
120
|
+
"@typescript-eslint/no-unsafe-call": "off",
|
|
121
|
+
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
122
|
+
"@typescript-eslint/no-var-requires": "off",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
files: ["**/*.styles.ts"],
|
|
127
|
+
rules: {
|
|
128
|
+
"@typescript-eslint/no-unsafe-return": "off",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
...moduleBoundaryOverrides,
|
|
132
|
+
],
|
|
133
|
+
extends: [
|
|
134
|
+
"eslint:recommended",
|
|
135
|
+
"plugin:@typescript-eslint/recommended",
|
|
136
|
+
"plugin:sonarjs/recommended-legacy",
|
|
137
|
+
"plugin:import/recommended",
|
|
138
|
+
"plugin:import/typescript",
|
|
139
|
+
"plugin:prettier/recommended",
|
|
140
|
+
"plugin:react/recommended",
|
|
141
|
+
"plugin:react-hooks/recommended",
|
|
142
|
+
"plugin:jsx-a11y/recommended",
|
|
143
|
+
"plugin:testing-library/react",
|
|
144
|
+
"plugin:@vitest/legacy-recommended",
|
|
145
|
+
"plugin:jest/recommended",
|
|
146
|
+
],
|
|
147
|
+
plugins: [
|
|
148
|
+
"@typescript-eslint",
|
|
149
|
+
"sonarjs",
|
|
150
|
+
"import",
|
|
151
|
+
"prettier",
|
|
152
|
+
"react",
|
|
153
|
+
"react-hooks",
|
|
154
|
+
"jsx-a11y",
|
|
155
|
+
"testing-library",
|
|
156
|
+
"@vitest",
|
|
157
|
+
"jest",
|
|
158
|
+
],
|
|
159
|
+
rules: {
|
|
160
|
+
"prettier/prettier": [
|
|
161
|
+
"error",
|
|
162
|
+
{
|
|
163
|
+
endOfLine: "auto",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
/** React-specific rules */
|
|
167
|
+
"react/hook-use-state": ["warn", { allowDestructuredState: true }],
|
|
168
|
+
"react/react-in-jsx-scope": ["off"],
|
|
169
|
+
"react/no-array-index-key": ["error"],
|
|
170
|
+
"react/display-name": ["off"],
|
|
171
|
+
"react/prop-types": ["off"],
|
|
172
|
+
/** Accessibility rules */
|
|
173
|
+
"jsx-a11y/anchor-is-valid": [
|
|
174
|
+
"error",
|
|
175
|
+
{
|
|
176
|
+
components: ["Link", "RouterLink"],
|
|
177
|
+
specialLink: ["to"],
|
|
178
|
+
aspects: ["noHref", "invalidHref", "preferButton"],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
/** Best practices and coding style */
|
|
182
|
+
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
|
|
183
|
+
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
|
184
|
+
"prefer-const": ["error"],
|
|
185
|
+
"no-use-before-define": ["error", { functions: false, classes: true, variables: true }],
|
|
186
|
+
"no-nested-ternary": ["error"],
|
|
187
|
+
/** SonarJS rules */
|
|
188
|
+
"sonarjs/cognitive-complexity": ["error"],
|
|
189
|
+
"sonarjs/no-inverted-boolean-check": ["off"],
|
|
190
|
+
"sonarjs/no-duplicate-string": ["off"],
|
|
191
|
+
/** Import rules */
|
|
192
|
+
"import/no-anonymous-default-export": "off",
|
|
193
|
+
"import/no-cycle": ["off"],
|
|
194
|
+
"import/namespace": ["off"],
|
|
195
|
+
"import/no-named-as-default": ["off"],
|
|
196
|
+
"import/no-unresolved": ["off", { ignore: ["^src/"] }],
|
|
197
|
+
/** TypeScript-specific rules */
|
|
198
|
+
"@typescript-eslint/explicit-module-boundary-types": ["off"],
|
|
199
|
+
"@typescript-eslint/no-explicit-any": ["error"],
|
|
200
|
+
"@typescript-eslint/no-unsafe-return": ["warn"],
|
|
201
|
+
"@typescript-eslint/no-unsafe-call": ["warn"],
|
|
202
|
+
"@typescript-eslint/no-unsafe-member-access": ["warn"],
|
|
203
|
+
"@typescript-eslint/no-unsafe-assignment": ["warn"],
|
|
204
|
+
"@typescript-eslint/prefer-optional-chain": ["error"],
|
|
205
|
+
"@typescript-eslint/prefer-nullish-coalescing": ["warn"],
|
|
206
|
+
"@typescript-eslint/no-unused-vars": [
|
|
207
|
+
"error",
|
|
208
|
+
{
|
|
209
|
+
argsIgnorePattern: "^_",
|
|
210
|
+
varsIgnorePattern: "^_",
|
|
211
|
+
caughtErrorsIgnorePattern: "^_",
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
"@typescript-eslint/ban-types": [
|
|
215
|
+
"error",
|
|
216
|
+
{
|
|
217
|
+
types: {
|
|
218
|
+
Array: {
|
|
219
|
+
message: "Use yourType[] instead. So for Array<string> you need to use string[]",
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
"@typescript-eslint/consistent-type-definitions": ["warn", "interface"],
|
|
225
|
+
"@typescript-eslint/naming-convention": [
|
|
226
|
+
"error",
|
|
227
|
+
{
|
|
228
|
+
selector: "variable",
|
|
229
|
+
types: ["boolean"],
|
|
230
|
+
format: ["PascalCase"],
|
|
231
|
+
prefix: ["is", "should", "has", "can", "did", "will", "are"],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
/** File size constraints */
|
|
235
|
+
"max-lines": ["warn", { max: 250, skipComments: true, skipBlankLines: true }],
|
|
236
|
+
},
|
|
237
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prettier config to pair with `eslintrc.module-boundaries.js`.
|
|
3
|
+
* Copy target: `.prettierrc.js` (via `npx commit-sheriff add-eslint-react`).
|
|
4
|
+
*/
|
|
5
|
+
module.exports = {
|
|
6
|
+
semi: true,
|
|
7
|
+
singleQuote: true,
|
|
8
|
+
trailingComma: "all",
|
|
9
|
+
tabWidth: 2,
|
|
10
|
+
printWidth: 100,
|
|
11
|
+
endOfLine: "auto",
|
|
12
|
+
};
|