commit-sheriff 1.7.0 → 1.7.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
@@ -285,12 +285,27 @@ merge step required, ever:
285
285
  - `eslint.config.mjs` — the entry point ESLint actually reads, generated fresh every run to import
286
286
  the file above. **If you already had an `eslint.config.mjs` (e.g. Next.js's own
287
287
  `create-next-app`-generated one), it is fully overwritten** — not merged, not left alone. If
288
- `next` is detected in your `dependencies`, the generated entry point folds in
289
- `next/core-web-vitals`/`next/typescript` automatically (via the same `FlatCompat` pattern Next's
290
- own generated config uses) so you don't lose Next-specific linting; `eslint-config-next` is
288
+ `next` is detected in your `dependencies`, the generated entry point folds in Next's own ESLint
289
+ rules automatically so you don't lose Next-specific linting; `eslint-config-next` is
291
290
  auto-installed too if it's missing. Any other hand-written customizations in your previous
292
291
  `eslint.config.mjs` are not preserved — back it up under a different name first if you need them.
293
292
 
293
+ `eslint-config-next` changed shape between major versions, so the generated entry point checks
294
+ the actually-installed version and picks the matching pattern automatically:
295
+ - **v16+** (current): ships a native flat-config array
296
+ (`import nextCoreWebVitals from "eslint-config-next/core-web-vitals"`, spread directly). Mixing
297
+ this with our own module-boundary plugins (which resolve the same plugin _names_ — `react`,
298
+ `jsx-a11y`, `import`, etc. — through a separate loader) would otherwise throw `Cannot redefine
299
+ plugin ...`; the generated config strips those redundant re-registrations automatically so both
300
+ rule sets apply cleanly.
301
+ - **v15 and earlier**: still ships the legacy eslintrc-style object, so the generated config uses
302
+ `FlatCompat.extends("next/core-web-vitals", "next/typescript")` instead — the same bridge
303
+ `create-next-app@15` itself generates.
304
+
305
+ (An earlier version of this tool hardcoded the v15-style pattern unconditionally, which crashed
306
+ with `TypeError: Converting circular structure to JSON` on projects using `eslint-config-next@16+`
307
+ — fixed by detecting the installed version instead of assuming one.)
308
+
294
309
  `.prettierrc.js` is written/refreshed the same way — **always overwritten** with the latest
295
310
  template (same behavior as the `commit-msg`/`pre-commit` hooks). This trade-off is intentional:
296
311
  zero manual steps for the common case, at the cost of not preserving bespoke prior ESLint/Prettier
@@ -253,6 +253,25 @@ function writeFileOverwrite(destName, content) {
253
253
  return dest;
254
254
  }
255
255
 
256
+ // eslint-config-next changed shape across majors: up through v15 it ships an eslintrc-style
257
+ // object (`module.exports = { extends: [...] }`) meant to be pulled in via FlatCompat's legacy
258
+ // `compat.extends("next/core-web-vitals", "next/typescript")` bridge (exactly what
259
+ // `create-next-app` itself generates for v15 projects). From v16 on it ships a native flat-config
260
+ // array as the default export of `eslint-config-next/core-web-vitals` / `/typescript` instead —
261
+ // running the *old* FlatCompat-string pattern against a v16 install crashes with
262
+ // "TypeError: Converting circular structure to JSON" deep inside `@eslint/eslintrc`'s config
263
+ // validator (confirmed by reproducing it in a scratch project — the crash reproduces with only
264
+ // Next's own extends, no module-boundary rules involved at all). `create-next-app@latest` itself
265
+ // already generates the new native-import form for v16, so we mirror that here rather than the
266
+ // older FlatCompat form once eslint-config-next resolves to 16+.
267
+ function installedVersion(pkgName) {
268
+ try {
269
+ return require(path.join(cwd, "node_modules", pkgName, "package.json")).version;
270
+ } catch {
271
+ return null;
272
+ }
273
+ }
274
+
256
275
  function buildEslintEntryConfig(hasNext) {
257
276
  if (!hasNext) {
258
277
  return (
@@ -260,9 +279,44 @@ function buildEslintEntryConfig(hasNext) {
260
279
  `export default [...moduleBoundaries];\n`
261
280
  );
262
281
  }
263
- // Next.js scaffolds its own eslint.config.mjs with `next/core-web-vitals`/`next/typescript` —
264
- // those are folded in here (via the same FlatCompat pattern Next's own generated config uses)
265
- // so overwriting the file doesn't silently drop Next-specific linting.
282
+
283
+ const version = installedVersion("eslint-config-next");
284
+ const major = version ? parseInt(version.split(".")[0], 10) : null;
285
+ // Default to the modern (16+) form when the version can't be determined (e.g. install failed
286
+ // offline) — that's what any newly-scaffolded Next project will have going forward.
287
+ const useNativeFlatExport = major === null || major >= 16;
288
+
289
+ if (useNativeFlatExport) {
290
+ // eslint-config-next's native flat array already registers plugins like "react",
291
+ // "react-hooks", "import", "jsx-a11y" and "@typescript-eslint". Our own module-boundary
292
+ // template is built through @eslint/eslintrc's FlatCompat, which independently resolves
293
+ // those same plugin *names* into separate object instances — even though they load the same
294
+ // underlying package, ESLint's flat config throws "Cannot redefine plugin ..." when a plugin
295
+ // name is registered twice with two different object references (confirmed by reproducing it
296
+ // in a scratch Next 16 project). Only one registration per plugin name is actually needed, so
297
+ // any config entry from our own template that re-declares a plugin Next.js already registered
298
+ // has that redundant "plugins" key stripped below — its "rules" keep working against the
299
+ // plugin instance Next.js already loaded.
300
+ return (
301
+ `import nextCoreWebVitals from "eslint-config-next/core-web-vitals";\n` +
302
+ `import nextTypescript from "eslint-config-next/typescript";\n` +
303
+ `import moduleBoundaries from "./${RULES_FILE}";\n\n` +
304
+ `const nextConfigs = [...nextCoreWebVitals, ...nextTypescript];\n` +
305
+ `const pluginsNextAlreadyRegisters = new Set(\n` +
306
+ ` nextConfigs.flatMap((entry) => (entry.plugins ? Object.keys(entry.plugins) : [])),\n` +
307
+ `);\n` +
308
+ `const dedupedModuleBoundaries = moduleBoundaries.map((entry) => {\n` +
309
+ ` if (!entry.plugins) return entry;\n` +
310
+ ` const ownPlugins = Object.fromEntries(\n` +
311
+ ` Object.entries(entry.plugins).filter(([name]) => !pluginsNextAlreadyRegisters.has(name)),\n` +
312
+ ` );\n` +
313
+ ` const { plugins, ...rest } = entry;\n` +
314
+ ` return Object.keys(ownPlugins).length ? { ...rest, plugins: ownPlugins } : rest;\n` +
315
+ `});\n\n` +
316
+ `export default [...nextConfigs, ...dedupedModuleBoundaries];\n`
317
+ );
318
+ }
319
+
266
320
  return (
267
321
  `import { fileURLToPath } from "node:url";\n` +
268
322
  `import path from "node:path";\n` +
@@ -284,6 +338,13 @@ function addEslintReact() {
284
338
  const hasNext = Boolean(deps.next);
285
339
 
286
340
  copyTemplateOverwrite("eslint.config.module-boundaries.mjs", RULES_FILE);
341
+
342
+ // eslint-config-next must actually be installed (and its version readable) *before* generating
343
+ // the entry config below, so the version check above sees real, resolved data instead of "not
344
+ // installed yet".
345
+ const extraDeps = hasNext && !deps["eslint-config-next"] ? ["eslint-config-next"] : [];
346
+ ensureDevDeps(ESLINT_REACT_DEV_DEPS.concat(extraDeps));
347
+
287
348
  writeFileOverwrite("eslint.config.mjs", buildEslintEntryConfig(hasNext));
288
349
  copyTemplateOverwrite("prettier.module-boundaries.js", ".prettierrc.js");
289
350
 
@@ -303,9 +364,6 @@ function addEslintReact() {
303
364
  : ""),
304
365
  );
305
366
 
306
- const extraDeps = hasNext && !deps["eslint-config-next"] ? ["eslint-config-next"] : [];
307
- ensureDevDeps(ESLINT_REACT_DEV_DEPS.concat(extraDeps));
308
-
309
367
  if (fs.existsSync(path.join(cwd, ".eslintrc.js"))) {
310
368
  console.log(
311
369
  "\nDiqqət: layihənizdə hələ də köhnə .eslintrc.js var — silin. Flat config varkən\n" +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commit-sheriff",
3
- "version": "1.7.0",
3
+ "version": "1.7.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": {