commit-sheriff 1.4.0 → 1.6.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/README.md CHANGED
@@ -56,11 +56,10 @@ Different repos tend to drift into different commit-message and branch-naming co
56
56
  npm install --save-dev commit-sheriff husky
57
57
  ```
58
58
 
59
- `lint-staged`, `eslint`, and `prettier` are optional — install them too if you want the pre-commit hook to lint/format staged files:
60
-
61
- ```bash
62
- npm install --save-dev lint-staged eslint prettier
63
- ```
59
+ `npx commit-sheriff init` installs `lint-staged`, `eslint`, and `prettier` for you automatically
60
+ (via `npm install --save-dev`) if they're missing, so the pre-commit lint/format step is actually
61
+ functional right away — you don't need to install them yourself first. This only happens as part
62
+ of `init` (see below), not from installing `commit-sheriff` itself.
64
63
 
65
64
  ## Usage
66
65
 
@@ -77,9 +76,18 @@ Run this once per repo, from the repo root (where `package.json` lives). This wo
77
76
  3. Creates a default `.commitsheriffrc.json` file at the repo root **only if one doesn't already exist** (and only if there's no legacy `package.json` → `commitGuard` block either — see [Configuration](#configuration-commitsheriffrcjson)) — re-running `init` never overwrites your customized config.
78
77
  4. Adds a default `lint-staged` block to `package.json` **only if one doesn't already exist**.
79
78
  5. Adds a `"prepare": "husky"` npm script if missing, so hooks are (re)installed automatically after `npm install`.
80
- 6. If `react` and `typescript` are both present in your `dependencies`/`devDependencies`, also runs [`add-eslint-react`](#advanced-eslint-module-boundary-template-typescriptreact) automatically, which **overwrites** `.eslintrc.js` and `.prettierrc.js` with the latest template. Otherwise it's skipped and you can add it manually later.
79
+ 6. Installs `lint-staged`, `eslint`, and `prettier` as devDependencies (via `npm install --save-dev`) if any of them are missing, so the pre-commit lint/format step actually runs instead of silently no-oping.
80
+ 7. If `react` and `typescript` are both present in your `dependencies`/`devDependencies`, also runs [`add-eslint-react`](#advanced-eslint-module-boundary-template-typescriptreact) automatically, which writes the ESLint flat-config module-boundary template (as `eslint.config.mjs` if you have none yet, or as a separate `eslint.config.commit-sheriff.mjs` if you already have one), **overwrites** `.prettierrc.js` with the latest template, and installs the extra plugins it needs. Otherwise it's skipped and you can add it manually later.
81
81
 
82
- Re-running `npx commit-sheriff init` later (e.g. after updating the package) is safe for your project-specific settings: it refreshes the hook scripts and (on react+typescript projects) the ESLint/Prettier templates to the latest version, but leaves your `.commitsheriffrc.json` / `lint-staged` config alone. If you've hand-edited `.eslintrc.js` or `.prettierrc.js`, back them up first — those two get overwritten on every re-run.
82
+ Re-running `npx commit-sheriff init` later (e.g. after updating the package) is safe for your project-specific settings: it refreshes the hook scripts and (on react+typescript projects) the ESLint/Prettier templates to the latest version, but leaves your `.commitsheriffrc.json` / `lint-staged` config alone. If you've hand-edited `.prettierrc.js`, or `eslint.config.mjs` in the no-existing-config case, back them up first — those get overwritten on every re-run (the side-file case, `eslint.config.commit-sheriff.mjs`, is also overwritten each time, but your own `eslint.config.mjs` is never touched there).
83
+
84
+ **Why the auto-install matters**: the pre-commit hook only runs `lint-staged` if it can actually
85
+ find the package — if it's missing, older versions of this hook **silently skipped linting
86
+ entirely**, so a broken/incomplete setup looked identical to a working one until someone noticed
87
+ bad code slipping through. The hook itself is now hardened too: if `package.json` has a
88
+ `lint-staged` config block but the package isn't actually installed (e.g. `node_modules` got
89
+ wiped, or someone added the config by hand), the commit now **fails loudly** with instructions,
90
+ instead of passing silently.
83
91
 
84
92
  ## Commit message format
85
93
 
@@ -227,7 +235,7 @@ Allowed `type` words in **commit messages**, following [Conventional Commits](ht
227
235
  In addition to the branch name check, `pre-commit` conditionally runs (skipped when the project doesn't have the relevant tool):
228
236
 
229
237
  - `npm run type-check` — if a `type-check` script exists in `package.json`
230
- - `npx lint-staged` — if `lint-staged` is listed as a dependency
238
+ - `npx lint-staged` — if `package.json` has a `lint-staged` config block. If the block exists but the `lint-staged` package itself isn't actually installed, the commit **fails with instructions** rather than silently skipping the check (this used to silently no-op — see [What `init` does](#what-init-does)).
231
239
  - `npx vitest related --run --passWithNoTests <staged .ts/.tsx files>` — if `vitest` is listed as a dependency and staged `.ts`/`.tsx` files exist (bounded to 60s via `timeout`/`gtimeout` when available)
232
240
 
233
241
  Any failure here aborts the commit.
@@ -257,37 +265,73 @@ other through a public barrel file, never through deep/internal paths:
257
265
  npx commit-sheriff add-eslint-react
258
266
  ```
259
267
 
260
- This copies two templates into your repo root, **always overwriting** whatever is already
261
- there at those exact filenames (same behavior as the `commit-msg`/`pre-commit` hooks re-running
262
- it refreshes both files to the latest version of the template):
263
-
264
- - `.eslintrc.js` — legacy-format ESLint config (TypeScript + React + a11y + import + sonarjs +
265
- testing-library/jest/vitest rules) with a module-boundary `no-restricted-imports` rule.
266
- - `.prettierrc.js` matching Prettier config.
267
-
268
- If you've customized either file by hand, back it up under a different name before re-running
269
- `add-eslint-react` (or `init` on a react+typescript project, which calls it automatically) —
270
- your edits will be replaced.
268
+ This ships as a **flat config** (`eslint.config.*`, ESLint 9+ format), not the legacy
269
+ `.eslintrc.js` format see [why](#why-flat-config) below. Where it lands depends on what your
270
+ project already has:
271
+
272
+ - **No `eslint.config.*` yet** written directly as `eslint.config.mjs` (your main config).
273
+ - **You already have one** (e.g. Next.js's own `create-next-app`-generated `eslint.config.mjs`)
274
+ → left untouched; the module-boundary rules go into a separate
275
+ `eslint.config.commit-sheriff.mjs` instead, printed with a snippet to import + spread it into
276
+ your existing config:
277
+
278
+ ```js
279
+ import moduleBoundaries from "./eslint.config.commit-sheriff.mjs";
280
+ export default [
281
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
282
+ ...moduleBoundaries,
283
+ ];
284
+ ```
285
+
286
+ Either way, `.prettierrc.js` is also written/refreshed, **always overwriting** whatever is
287
+ already there at that filename (same behavior as the `commit-msg`/`pre-commit` hooks — re-running
288
+ `add-eslint-react` refreshes it to the latest version of the template). If you've customized it
289
+ by hand, back it up under a different name first — your edits will be replaced. The same applies
290
+ to `eslint.config.mjs` when it's the one written directly (the no-existing-config case above); the
291
+ side-file case never touches your own config.
271
292
 
272
293
  The module list for the boundary rule isn't hardcoded — it's read straight from the `modules`
273
294
  array in your root `.commitsheriffrc.json` (the same list the commit-msg/branch-name hooks use
274
295
  for the `[MODULE]` tag), lowercased. For older installs still using `package.json`'s
275
296
  `commitGuard.modules`, that's used as a fallback. If neither is set, a small illustrative default
276
- (`auth`, `billing`, `reports`, `settings`) is used — open `.eslintrc.js` and replace it, or just
277
- fill in `modules` in `.commitsheriffrc.json` and it picks it up automatically.
297
+ (`auth`, `billing`, `reports`, `settings`) is used — open the generated config and replace it, or
298
+ just fill in `modules` in `.commitsheriffrc.json` and it picks it up automatically.
278
299
 
279
300
  This template assumes a `src/app/<module>/...` folder layout with `<module>.public.ts` barrel
280
- files; adjust the paths inside `.eslintrc.js` if your structure differs.
301
+ files; adjust the paths inside the generated config if your structure differs.
281
302
 
282
- It doesn't install any dependencies for you install what your project needs, e.g.:
303
+ It installs whatever it needs automatically (via `npm install --save-dev`, same as `init` does for
304
+ `lint-staged`/`eslint`/`prettier`), skipping anything already present:
283
305
 
284
306
  ```bash
285
- npm install --save-dev typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin \
286
- eslint-plugin-sonarjs eslint-plugin-import eslint-plugin-prettier eslint-config-prettier \
287
- eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y \
288
- eslint-plugin-testing-library eslint-plugin-vitest eslint-plugin-jest prettier
307
+ npm install --save-dev eslint@^9 typescript @typescript-eslint/parser \
308
+ @typescript-eslint/eslint-plugin @eslint/js @eslint/eslintrc eslint-plugin-sonarjs \
309
+ eslint-plugin-import eslint-plugin-prettier eslint-config-prettier eslint-plugin-react \
310
+ eslint-plugin-react-hooks eslint-plugin-jsx-a11y eslint-plugin-testing-library \
311
+ @vitest/eslint-plugin eslint-plugin-jest prettier
289
312
  ```
290
313
 
314
+ `eslint` is pinned to `^9` on purpose rather than left to resolve to whatever `latest` is — plugins
315
+ like `eslint-plugin-react` have historically taken a while to support each new ESLint major, so an
316
+ unpinned install can grab a version the rest of the toolchain doesn't work with yet.
317
+
318
+ If the install fails (offline, registry issue, etc.), it prints this exact command so you can run
319
+ it yourself.
320
+
321
+ #### Why flat config?
322
+
323
+ ESLint 9+ looks for `eslint.config.*` first and, if one is found, **ignores `.eslintrc.js`
324
+ entirely** — there's no fallback. Since frameworks like Next.js already scaffold their own
325
+ `eslint.config.mjs`, a legacy-only `.eslintrc.js` template would sit right next to it and never
326
+ actually run (this was a real bug: an unused `useState` import went completely unflagged because
327
+ the module-boundary config was silently dead). The flat config here uses `FlatCompat` (the same
328
+ official migration helper Next.js's own generated config uses internally) so the exact same rule
329
+ set works natively under ESLint 9+, in both the standalone and merge-into-existing-config cases.
330
+
331
+ A legacy `.eslintrc.js` version of this template (`eslintrc.module-boundaries.js`) still ships
332
+ inside the package for reference/manual use on pure ESLint 8 projects with no flat config support
333
+ at all, but the CLI no longer writes it by default.
334
+
291
335
  ## Examples
292
336
 
293
337
  **Minimal project (no modules)** — `.commitsheriffrc.json`:
@@ -91,6 +91,46 @@ function ensureConfigFile() {
91
91
  console.log(`✔ ${CONFIG_FILE_NAME} yazıldı`);
92
92
  }
93
93
 
94
+ // Installs whatever in `packages` isn't already a listed dependency/devDependency, so the hooks
95
+ // this tool wires up are actually functional right after `init`/`add-eslint-react` — not just
96
+ // documented as a copy-paste command the user has to remember to run separately. Falls back to
97
+ // printing the manual command if the install itself fails (offline, registry auth issue, etc.),
98
+ // so the user isn't left silently thinking it worked.
99
+ // Strips a trailing "@version" from an install spec to get the plain package name for looking it
100
+ // up in package.json — careful with scoped packages ("@eslint/js" has no version, but
101
+ // "@vitest/eslint-plugin@^1" does; only the *last* "@" is ever the version separator).
102
+ function packageNameOf(spec) {
103
+ const atIndex = spec.lastIndexOf("@");
104
+ return atIndex > 0 ? spec.slice(0, atIndex) : spec;
105
+ }
106
+
107
+ function ensureDevDeps(packages) {
108
+ const { pkg } = readPackageJson();
109
+ const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
110
+ const missing = packages.filter((spec) => !deps[packageNameOf(spec)]);
111
+ if (!missing.length) {
112
+ console.log("• Lazımi devDependencies artıq quraşdırılıb, toxunulmadı");
113
+ return;
114
+ }
115
+ console.log(`→ Quraşdırılır: ${missing.join(" ")}`);
116
+ try {
117
+ // --legacy-peer-deps: some ESLint plugins lag behind the newest ESLint major in their
118
+ // declared peerDependencies (e.g. eslint-plugin-import still capping at ^9 while npm already
119
+ // resolves plain "eslint" to 10.x), which makes npm's default strict peer resolution abort
120
+ // the whole install with ERESOLVE even though the versions work fine together in practice.
121
+ execSync(`npm install --save-dev --legacy-peer-deps ${missing.join(" ")}`, {
122
+ cwd,
123
+ stdio: "inherit",
124
+ });
125
+ console.log("✔ devDependencies quraşdırıldı");
126
+ } catch {
127
+ console.log(
128
+ "\n Avtomatik quraşdırma alınmadı (şəbəkə/npm xətası ola bilər) — özünüz işlədin:\n" +
129
+ ` npm install --save-dev ${missing.join(" ")}\n`,
130
+ );
131
+ }
132
+ }
133
+
94
134
  function mergeConfig() {
95
135
  const { pkgPath, pkg } = readPackageJson();
96
136
  let changed = false;
@@ -114,6 +154,18 @@ function mergeConfig() {
114
154
  if (changed) {
115
155
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
116
156
  }
157
+
158
+ // Without this, the pre-commit hook's `lint-staged` step silently no-ops when the package
159
+ // itself isn't installed (it only checks whether *some* config/listing exists), so lint/format
160
+ // checks would never actually run on commit even though everything *looks* set up. `eslint`
161
+ // and `prettier` are included too since the default lint-staged config above already assumes
162
+ // both are runnable — otherwise the very first commit would fail on "command not found" instead
163
+ // of an actual lint/format problem. `eslint` is pinned to ^9 (not left to resolve to whatever
164
+ // "latest" is) because the plugin ecosystem this tool depends on (eslint-plugin-react, etc.)
165
+ // regularly lags behind ESLint's newest major for months — installing an unpinned "latest" has
166
+ // broken real installs (e.g. eslint-plugin-react crashing under ESLint 10's changed rule-context
167
+ // API) within days of a new major landing on npm.
168
+ ensureDevDeps(["lint-staged", "eslint@^9", "prettier"]);
117
169
  }
118
170
 
119
171
  function isReactTypescriptProject() {
@@ -145,9 +197,14 @@ function init() {
145
197
  }
146
198
 
147
199
  const ESLINT_REACT_DEV_DEPS = [
200
+ // Pinned (not left to resolve to "latest") — see the comment on the eslint@^9 install in
201
+ // mergeConfig() for why: the plugin ecosystem here regularly lags behind ESLint's newest major.
202
+ "eslint@^9",
148
203
  "typescript",
149
204
  "@typescript-eslint/parser",
150
205
  "@typescript-eslint/eslint-plugin",
206
+ "@eslint/js",
207
+ "@eslint/eslintrc",
151
208
  "eslint-plugin-sonarjs",
152
209
  "eslint-plugin-import",
153
210
  "eslint-plugin-prettier",
@@ -156,32 +213,78 @@ const ESLINT_REACT_DEV_DEPS = [
156
213
  "eslint-plugin-react-hooks",
157
214
  "eslint-plugin-jsx-a11y",
158
215
  "eslint-plugin-testing-library",
159
- "eslint-plugin-vitest",
216
+ "@vitest/eslint-plugin",
160
217
  "eslint-plugin-jest",
161
218
  "prettier",
162
219
  ];
163
220
 
221
+ const FLAT_CONFIG_CANDIDATES = [
222
+ "eslint.config.js",
223
+ "eslint.config.mjs",
224
+ "eslint.config.cjs",
225
+ "eslint.config.ts",
226
+ ];
227
+ const FLAT_CONFIG_SIDE_FILE = "eslint.config.commit-sheriff.mjs";
228
+
229
+ function hasFlatEslintConfig() {
230
+ return FLAT_CONFIG_CANDIDATES.some((name) => fs.existsSync(path.join(cwd, name)));
231
+ }
232
+
164
233
  function copyTemplateOverwrite(templateName, destName) {
165
234
  const src = path.join(__dirname, "..", "templates", templateName);
166
235
  const dest = path.join(cwd, destName);
167
236
  const existed = fs.existsSync(dest);
168
237
  fs.copyFileSync(src, dest);
169
238
  console.log(existed ? `✔ ${destName} yeniləndi (üzərinə yazıldı)` : `✔ ${destName} yazıldı`);
239
+ return dest;
170
240
  }
171
241
 
172
242
  function addEslintReact() {
173
- copyTemplateOverwrite("eslintrc.module-boundaries.js", ".eslintrc.js");
243
+ // ESLint 9+ prioritizes flat config (`eslint.config.*`) and completely ignores a legacy
244
+ // `.eslintrc.js` when one is present — no fallback. Frameworks like Next.js already scaffold
245
+ // their own `eslint.config.mjs`, so if we wrote only `.eslintrc.js` here it would silently
246
+ // never run for the majority of modern projects. We therefore ship the flat-config template
247
+ // as the primary artifact, and decide where it lands based on what the project already has.
248
+ const flatConfigAlreadyExists = hasFlatEslintConfig();
249
+
250
+ if (flatConfigAlreadyExists) {
251
+ copyTemplateOverwrite("eslint.config.module-boundaries.mjs", FLAT_CONFIG_SIDE_FILE);
252
+ console.log(
253
+ `\n• Layihədə artıq öz eslint.config.* faylınız var — ona toxunulmadı.\n` +
254
+ ` Modul-sərhəd qaydaları ayrıca ${FLAT_CONFIG_SIDE_FILE} faylına yazıldı.\n` +
255
+ ` Özünüzün eslint.config.* faylınıza əlavə edin, məsələn:\n\n` +
256
+ ` import moduleBoundaries from "./${FLAT_CONFIG_SIDE_FILE}";\n` +
257
+ ` export default [\n` +
258
+ ` ...compat.extends("next/core-web-vitals", "next/typescript"),\n` +
259
+ ` ...moduleBoundaries,\n` +
260
+ ` ];\n`,
261
+ );
262
+ } else {
263
+ copyTemplateOverwrite("eslint.config.module-boundaries.mjs", "eslint.config.mjs");
264
+ console.log(
265
+ "\n• Layihədə eslint.config.* yox idi — eslint.config.mjs olaraq birbaşa yazıldı.\n",
266
+ );
267
+ }
174
268
  copyTemplateOverwrite("prettier.module-boundaries.js", ".prettierrc.js");
175
269
  console.log(
176
- "\nBu şablon TypeScript/React + modul-sərhəd (module-boundary) qaydaları üçündür.\n" +
270
+ "Bu şablon TypeScript/React + modul-sərhəd (module-boundary) qaydaları üçündür.\n" +
177
271
  `Modul siyahısı ${CONFIG_FILE_NAME}.modules-dən avtomatik oxunur (boşdursa nümunə\n` +
178
- "siyahı istifadə olunur — .eslintrc.js içindəki şərhi oxuyun).\n\n" +
179
- "Diqqət: .eslintrc.js və .prettierrc.js hər dəfə şablonun ən son versiyası ilə\n" +
180
- "üzərinə yazılır (commit-msg/pre-commit hook-ları kimi) — özünüzə uyğun etdiyiniz\n" +
181
- "dəyişiklikləri qorumaq istəsəniz, əvvəlcə fərqli adla backup götürün.\n\n" +
182
- "Lazımi devDependencies (özünüz quraşdırın, layihənizin real versiyalarına uyğun):\n" +
183
- ` npm install --save-dev ${ESLINT_REACT_DEV_DEPS.join(" ")}\n`,
272
+ "siyahı istifadə olunur — şablon faylı içindəki şərhi oxuyun).\n\n" +
273
+ "Diqqət: eslint.config.* (ya yuxarıdakı side-file) və .prettierrc.js hər dəfə\n" +
274
+ "şablonun ən son versiyası ilə üzərinə yazılır (commit-msg/pre-commit hook-ları kimi)\n" +
275
+ "— özünüzə uyğun etdiyiniz dəyişiklikləri qorumaq istəsəniz, əvvəlcə fərqli adla\n" +
276
+ "backup götürün.\n",
184
277
  );
278
+ ensureDevDeps(ESLINT_REACT_DEV_DEPS);
279
+ if (flatConfigAlreadyExists) {
280
+ console.log(
281
+ `\nUnutmayın: ${FLAT_CONFIG_SIDE_FILE} avtomatik import olunmur — onu öz\n` +
282
+ "eslint.config.* faylınıza əlavə etməsəniz, bu qaydalar heç vaxt işə düşməyəcək\n" +
283
+ "(yuxarıdakı nümunəyə baxın). Həmçinin, əgər layihənizdə hələ də köhnə .eslintrc.js\n" +
284
+ "varsa, onu silin — flat config varkən ESLint onu oxumur, üstəlik özü də indi\n" +
285
+ "adi .js fayl kimi lint xətaları törədə bilər.\n",
286
+ );
287
+ }
185
288
  }
186
289
 
187
290
  if (command === "init") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commit-sheriff",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
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": {
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Advanced ESLint config (flat config format) — enforces module boundaries in a modular
3
+ * TypeScript/React app. This is the flat-config (ESLint 9+, `eslint.config.*`) counterpart of
4
+ * the rules previously shipped only as a legacy `.eslintrc.js`.
5
+ *
6
+ * Why this exists: ESLint 9+ looks for `eslint.config.*` (flat config) first and, if one is
7
+ * found, ignores `.eslintrc.js` entirely — it does NOT fall back to it. Frameworks like Next.js
8
+ * (`create-next-app`) already scaffold their own `eslint.config.mjs`, so a legacy `.eslintrc.js`
9
+ * sitting next to it is silently dead: none of its rules (including the module-boundary ones)
10
+ * ever run. This file avoids that trap by speaking flat config natively.
11
+ *
12
+ * Assumptions this template makes (edit as needed):
13
+ * - Feature modules live under `src/app/<module>/` (e.g. `src/app/auth/`).
14
+ * - Each module exposes its public surface via a `<module>.public.ts` barrel file; anything
15
+ * else under `src/app/<module>/modules/**` or `src/app/<module>/shared/**` is internal and
16
+ * must not be imported directly from another module.
17
+ * - Shared/base code lives under `src/app/_shared/**` and `src/packages/**` and must never
18
+ * import from a feature module (that would invert the dependency direction).
19
+ * - `src/packages/routing/AppRoutes.tsx` and `src/packages/routing/config/modules.registry.ts`
20
+ * are the one place allowed to import every module directly (that's the whole point of a
21
+ * router/registry), so the boundary rule is turned off just for those two files.
22
+ *
23
+ * Module list: instead of hardcoding module names, this reads `modules` from the repo's
24
+ * `.commitsheriffrc.json` (the same list used by the commit-sheriff commit-msg/branch-name
25
+ * hooks — see README), so the two conventions (module tags in commits/branches, and module
26
+ * boundaries in imports) stay in sync automatically. For older installs that still keep their
27
+ * config under `package.json`'s `commitGuard` block, that location is used as a fallback. If
28
+ * neither is found, a small illustrative default is used instead — replace it with your real
29
+ * module names.
30
+ *
31
+ * How to use this file:
32
+ * - If your project has NO `eslint.config.*` yet, `commit-sheriff add-eslint-react` writes
33
+ * this content directly as your `eslint.config.mjs` — nothing else to do.
34
+ * - If your project ALREADY has an `eslint.config.*` (e.g. Next.js's own), this content is
35
+ * instead written to `eslint.config.commit-sheriff.mjs` alongside it, and you import + spread
36
+ * it into your existing config so nothing gets silently overwritten:
37
+ *
38
+ * import moduleBoundaries from "./eslint.config.commit-sheriff.mjs";
39
+ * export default [
40
+ * ...compat.extends("next/core-web-vitals", "next/typescript"),
41
+ * ...moduleBoundaries,
42
+ * ];
43
+ */
44
+
45
+ import { fileURLToPath } from "node:url";
46
+ import path from "node:path";
47
+ import fs from "node:fs";
48
+ import js from "@eslint/js";
49
+ import { FlatCompat } from "@eslint/eslintrc";
50
+
51
+ const __filename = fileURLToPath(import.meta.url);
52
+ const __dirname = path.dirname(__filename);
53
+
54
+ // `recommendedConfig`/`allConfig` are required so FlatCompat can resolve the bare
55
+ // "eslint:recommended" / "eslint:all" strings used in `extends` below — without them it throws
56
+ // "Missing parameter 'recommendedConfig' in FlatCompat constructor" as soon as `.config()` runs.
57
+ const compat = new FlatCompat({
58
+ baseDirectory: __dirname,
59
+ recommendedConfig: js.configs.recommended,
60
+ allConfig: js.configs.all,
61
+ });
62
+
63
+ function loadAppModules() {
64
+ try {
65
+ const rcPath = path.join(__dirname, ".commitsheriffrc.json");
66
+ const rc = JSON.parse(fs.readFileSync(rcPath, "utf8"));
67
+ if (Array.isArray(rc.modules) && rc.modules.length) {
68
+ return rc.modules.map((m) => String(m).toLowerCase());
69
+ }
70
+ } catch {
71
+ // fall through to the legacy package.json lookup below
72
+ }
73
+ try {
74
+ const pkgPath = path.join(__dirname, "package.json");
75
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
76
+ const modules = pkg.commitGuard && pkg.commitGuard.modules;
77
+ if (Array.isArray(modules) && modules.length) {
78
+ return modules.map((m) => String(m).toLowerCase());
79
+ }
80
+ } catch {
81
+ // fall through to the illustrative default below
82
+ }
83
+ return ["auth", "billing", "reports", "settings"]; // example only — replace with your modules
84
+ }
85
+
86
+ const APP_MODULES = loadAppModules();
87
+ const MODULE_INTERNALS = ["modules", "shared"];
88
+
89
+ const restrictImports = (group, message) => ({
90
+ "no-restricted-imports": ["error", { patterns: [{ group, message }] }],
91
+ });
92
+
93
+ const moduleBoundaryConfigs = [
94
+ // A module reaches another module's internals only through `<module>.public.ts`, so the
95
+ // surface between modules stays explicit. `routing/` stays open on purpose: route enums are
96
+ // dependency-free leaves, while the barrel pulls in services that instantiate on import.
97
+ ...APP_MODULES.map((appModule) => ({
98
+ files: [`src/app/${appModule}/**`],
99
+ rules: restrictImports(
100
+ APP_MODULES.filter((other) => other !== appModule).flatMap((other) =>
101
+ MODULE_INTERNALS.map((internal) => `~/app/${other}/${internal}/**`),
102
+ ),
103
+ "Cross-module import: use the other module's '<module>.public.ts' barrel instead of a deep path.",
104
+ ),
105
+ })),
106
+ {
107
+ files: ["src/app/_shared/**", "src/packages/**"],
108
+ rules: restrictImports(
109
+ APP_MODULES.map((appModule) => `~/app/${appModule}/**`),
110
+ "Base layers must not depend on a feature module: move the shared piece down into _shared or packages instead.",
111
+ ),
112
+ },
113
+ {
114
+ files: [
115
+ "src/packages/routing/AppRoutes.tsx",
116
+ "src/packages/routing/config/modules.registry.ts",
117
+ ],
118
+ rules: { "no-restricted-imports": "off" },
119
+ },
120
+ ];
121
+
122
+ const moduleBoundaryConfig = [
123
+ {
124
+ ignores: [
125
+ "coverage/**",
126
+ "dist/**",
127
+ "build/**",
128
+ "node_modules/**",
129
+ "**/*.d.ts",
130
+ "**/*.config.js",
131
+ "**/*.config.mjs",
132
+ // Root-level CommonJS config dotfiles (this template's own .prettierrc.js among them) aren't
133
+ // part of the TS project's `include`, so `parserOptions.project: true` below fails to parse
134
+ // them with a "TSConfig does not include this file" error unless they're excluded here.
135
+ ".eslintrc.js",
136
+ ".prettierrc.js",
137
+ ".prettierrc.cjs",
138
+ ],
139
+ },
140
+ // FlatCompat translates the same legacy-style `extends`/`env`/`parserOptions`/`plugins`/`rules`
141
+ // block ESLint's own docs recommend for migrating a single rich config to flat format — this
142
+ // keeps every rule identical to the previous .eslintrc.js version, just in flat shape.
143
+ ...compat.config({
144
+ parser: "@typescript-eslint/parser",
145
+ parserOptions: {
146
+ ecmaVersion: "latest",
147
+ sourceType: "module",
148
+ ecmaFeatures: { jsx: true },
149
+ project: true,
150
+ },
151
+ env: { browser: true, es2021: true, node: true },
152
+ settings: {
153
+ react: { version: "detect" },
154
+ linkComponents: [
155
+ { name: "RouterLink", linkAttribute: ["href", "to"] },
156
+ { name: "Link", linkAttribute: ["href", "to"] },
157
+ ],
158
+ jest: { version: "29.0" },
159
+ },
160
+ extends: [
161
+ "eslint:recommended",
162
+ "plugin:@typescript-eslint/recommended",
163
+ "plugin:sonarjs/recommended-legacy",
164
+ "plugin:import/recommended",
165
+ "plugin:import/typescript",
166
+ "plugin:prettier/recommended",
167
+ "plugin:react/recommended",
168
+ "plugin:react-hooks/recommended",
169
+ "plugin:jsx-a11y/recommended",
170
+ "plugin:testing-library/react",
171
+ "plugin:@vitest/legacy-recommended",
172
+ "plugin:jest/recommended",
173
+ ],
174
+ plugins: [
175
+ "@typescript-eslint",
176
+ "sonarjs",
177
+ "import",
178
+ "prettier",
179
+ "react",
180
+ "react-hooks",
181
+ "jsx-a11y",
182
+ "testing-library",
183
+ "@vitest",
184
+ "jest",
185
+ ],
186
+ rules: {
187
+ "prettier/prettier": ["error", { endOfLine: "auto" }],
188
+ /** React-specific rules */
189
+ "react/hook-use-state": ["warn", { allowDestructuredState: true }],
190
+ "react/react-in-jsx-scope": ["off"],
191
+ "react/no-array-index-key": ["error"],
192
+ "react/display-name": ["off"],
193
+ "react/prop-types": ["off"],
194
+ /** Accessibility rules */
195
+ "jsx-a11y/anchor-is-valid": [
196
+ "error",
197
+ {
198
+ components: ["Link", "RouterLink"],
199
+ specialLink: ["to"],
200
+ aspects: ["noHref", "invalidHref", "preferButton"],
201
+ },
202
+ ],
203
+ /** Best practices and coding style */
204
+ "no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
205
+ "no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
206
+ "prefer-const": ["error"],
207
+ "no-use-before-define": ["error", { functions: false, classes: true, variables: true }],
208
+ "no-nested-ternary": ["error"],
209
+ /** SonarJS rules */
210
+ "sonarjs/cognitive-complexity": ["error"],
211
+ "sonarjs/no-inverted-boolean-check": ["off"],
212
+ "sonarjs/no-duplicate-string": ["off"],
213
+ /** Import rules */
214
+ "import/no-anonymous-default-export": "off",
215
+ "import/no-cycle": ["off"],
216
+ "import/namespace": ["off"],
217
+ "import/no-named-as-default": ["off"],
218
+ "import/no-unresolved": ["off", { ignore: ["^src/"] }],
219
+ /** TypeScript-specific rules */
220
+ "@typescript-eslint/explicit-module-boundary-types": ["off"],
221
+ "@typescript-eslint/no-explicit-any": ["error"],
222
+ "@typescript-eslint/no-unsafe-return": ["warn"],
223
+ "@typescript-eslint/no-unsafe-call": ["warn"],
224
+ "@typescript-eslint/no-unsafe-member-access": ["warn"],
225
+ "@typescript-eslint/no-unsafe-assignment": ["warn"],
226
+ "@typescript-eslint/prefer-optional-chain": ["error"],
227
+ "@typescript-eslint/prefer-nullish-coalescing": ["warn"],
228
+ "@typescript-eslint/no-unused-vars": [
229
+ "error",
230
+ {
231
+ argsIgnorePattern: "^_",
232
+ varsIgnorePattern: "^_",
233
+ caughtErrorsIgnorePattern: "^_",
234
+ },
235
+ ],
236
+ "@typescript-eslint/no-restricted-types": [
237
+ "error",
238
+ {
239
+ types: {
240
+ Array: {
241
+ message: "Use yourType[] instead. So for Array<string> you need to use string[]",
242
+ },
243
+ },
244
+ },
245
+ ],
246
+ "@typescript-eslint/consistent-type-definitions": ["warn", "interface"],
247
+ "@typescript-eslint/naming-convention": [
248
+ "error",
249
+ {
250
+ selector: "variable",
251
+ types: ["boolean"],
252
+ format: ["PascalCase"],
253
+ prefix: ["is", "should", "has", "can", "did", "will", "are"],
254
+ },
255
+ ],
256
+ /** File size constraints */
257
+ "max-lines": ["warn", { max: 250, skipComments: true, skipBlankLines: true }],
258
+ },
259
+ }),
260
+ {
261
+ files: ["*.js", "**/*.styles.ts"],
262
+ rules: {
263
+ "@typescript-eslint/no-unsafe-member-access": "off",
264
+ "@typescript-eslint/no-unsafe-call": "off",
265
+ "@typescript-eslint/no-unsafe-assignment": "off",
266
+ "@typescript-eslint/no-var-requires": "off",
267
+ },
268
+ },
269
+ {
270
+ files: ["**/*.styles.ts"],
271
+ rules: {
272
+ "@typescript-eslint/no-unsafe-return": "off",
273
+ },
274
+ },
275
+ ...moduleBoundaryConfigs,
276
+ ];
277
+
278
+ export default moduleBoundaryConfig;
@@ -222,7 +222,7 @@ module.exports = {
222
222
  caughtErrorsIgnorePattern: "^_",
223
223
  },
224
224
  ],
225
- "@typescript-eslint/ban-types": [
225
+ "@typescript-eslint/no-restricted-types": [
226
226
  "error",
227
227
  {
228
228
  types: {
@@ -66,13 +66,28 @@ if [ "$HAS_TYPE_CHECK" = "1" ]; then
66
66
  npm run type-check || exit 1
67
67
  fi
68
68
 
69
- HAS_LINT_STAGED=$(node -e "
70
- const p = require('./package.json');
71
- const deps = Object.assign({}, p.dependencies, p.devDependencies);
72
- process.stdout.write(deps['lint-staged'] ? '1' : '0');
69
+ # Two separate signals on purpose: whether the project *wants* lint-staged (has a config block
70
+ # in package.json — commit-sheriff's own `init` writes one by default) vs. whether the package is
71
+ # *actually installed* right now. If a config exists but the package isn't resolvable, that's not
72
+ # "this project doesn't use lint-staged" it's a broken setup (e.g. someone ran `npm prune`,
73
+ # skipped `npm install`, or deleted node_modules) and used to fail silently, letting broken commits
74
+ # through with zero lint/format enforcement. Now it fails loudly instead.
75
+ HAS_LINT_CONFIG=$(node -e "
76
+ process.stdout.write(require('./package.json')['lint-staged'] ? '1' : '0');
73
77
  " 2>/dev/null)
74
- if [ "$HAS_LINT_STAGED" = "1" ]; then
75
- npx lint-staged || exit 1
78
+ if [ "$HAS_LINT_CONFIG" = "1" ]; then
79
+ if node -e "require.resolve('lint-staged')" 2>/dev/null; then
80
+ npx lint-staged || exit 1
81
+ else
82
+ echo ""
83
+ echo " package.json-da 'lint-staged' konfiqurasiyası var, amma paketin özü"
84
+ echo " node_modules-də tapılmadı — lint/format yoxlanışı keçirilə bilmədi."
85
+ echo ""
86
+ echo " Düzəltmək üçün:"
87
+ echo " npm install --save-dev lint-staged"
88
+ echo ""
89
+ exit 1
90
+ fi
76
91
  fi
77
92
 
78
93
  # ── Related tests (only if vitest is installed) ─────────────────────────────