@krizaka/config 0.8.0 → 0.8.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
@@ -136,7 +136,7 @@ Every message names the home and ends « Missing function? add it to the package
136
136
  to the Krizaka package, never worked around with the library. Run it with `depcruise --output-type err-long <folders>`
137
137
  to read the message (`err` prints the rule name only). Beside the domains, the preset owes every repository
138
138
  `no-circular`, `not-to-unresolvable` (declare the dependency, or fix the path; set `options.tsConfig` for path aliases)
139
- and `not-to-dev-dep` (tests, stories, scripts and tool configs may import devDependencies). Run `depcruise` from the
139
+ and `not-to-dev-dep` (tests, stories, scripts and tool configs may import devDependencies; a package that is also a `peerDependency` is installed by the consumer, so production code may import it). Run `depcruise` from the
140
140
  workspace that owns the `package.json`.
141
141
 
142
142
  #### Known violations (they only shrink)
@@ -57,9 +57,10 @@ function createConfig({ exemptFrom = [] } = {}) {
57
57
  {
58
58
  name: "not-to-dev-dep",
59
59
  severity: "error",
60
- comment: "Production code imports a devDependency: it is not installed in production. Move the package to `dependencies`, or the import to a test.",
60
+ comment: "Production code imports a devDependency: it is not installed in production. Move the package to `dependencies` (or `peerDependencies`, for a library whose consumer installs it), or the import to a test.",
61
61
  from: { pathNot: [NODE_MODULES, DEV_FILES] },
62
- to: { dependencyTypes: ["npm-dev"], dependencyTypesNot: ["type-only"], pathNot: ["node_modules/@types/"] },
62
+ // a package that is also a peer dependency (the library declares it for its consumer, and keeps a copy to test with) is installed where it is used
63
+ to: { dependencyTypes: ["npm-dev"], dependencyTypesNot: ["type-only", "npm-peer"], pathNot: ["node_modules/@types/"] },
63
64
  },
64
65
  ],
65
66
  options: {
package/eslint/rules.js CHANGED
@@ -169,16 +169,7 @@ export function ownProductPrefix(cwd) {
169
169
  for (let dir = cwd; ; dir = dirname(dir)) {
170
170
  const file = join(dir, "package.json");
171
171
  if (existsSync(file)) {
172
- try {
173
- const name = String(JSON.parse(readFileSync(file, "utf8")).name ?? "");
174
- // A product names its packages `<product>-<part>` (acme-monorepo, @org/acme-admin); a one-word package
175
- // (@org/realtime, @org/ui) is a building block, not a product, and a context word is never one.
176
- const words = name.replace(/^@[^/]+\//, "").split(/[-_.]/).filter(Boolean);
177
- const word = words.length > 1 ? words[0].toUpperCase() : "";
178
- found = /^[A-Z][A-Z0-9]+$/.test(word) && !ENV_CONTEXT_WORDS.includes(word) ? word : null;
179
- } catch {
180
- found = null;
181
- }
172
+ found = productPrefixOf(file);
182
173
  break;
183
174
  }
184
175
  if (dirname(dir) === dir) break;
@@ -187,6 +178,24 @@ export function ownProductPrefix(cwd) {
187
178
  return found;
188
179
  }
189
180
 
181
+ /**
182
+ * The product word of one package.json (see `ownProductPrefix`); null when it cannot be read or is not a product.
183
+ * @param {string} file
184
+ * @returns {string | null}
185
+ */
186
+ function productPrefixOf(file) {
187
+ try {
188
+ const name = String(JSON.parse(readFileSync(file, "utf8")).name ?? "");
189
+ // A product names its packages `<product>-<part>` (acme-monorepo, @org/acme-admin); a one-word package
190
+ // (@org/realtime, @org/ui) is a building block, not a product, and a context word is never one.
191
+ const words = name.replace(/^@[^/]+\//, "").split(/[-_.]/).filter(Boolean);
192
+ const word = words.length > 1 ? words[0].toUpperCase() : "";
193
+ return /^[A-Z][A-Z0-9]+$/.test(word) && !ENV_CONTEXT_WORDS.includes(word) ? word : null;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+
190
199
  /** Vendors: a vendor is a value (`MEDIA_STORAGE_PROVIDER=bunny`, `MAIL_PROVIDER=mailgun`), never a name. */
191
200
  export const ENV_VENDOR_PREFIXES = Object.freeze([
192
201
  "BUNNY", "MAILGUN", "RESEND", "SENDGRID", "POSTMARK", "STRIPE", "CCBILL", "SEGPAY", "NOWPAYMENTS", "PAYPAL",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/config",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "The shared configuration of every Krizaka JavaScript repository: ESLint flat configs (base, Next.js, the four UI rules, the stack rules, no locale ternary), the « Krizaka first » covered-domains map and the dependency-cruiser preset generated from it (refused imports, cycles, unresolvable imports, dev dependencies), tsconfig bases, Prettier, EditorConfig, and the krizaka-ratchet and krizaka-known-violations debt counters.",
5
5
  "keywords": [
6
6
  "krizaka",
package/patterns.js CHANGED
@@ -102,20 +102,40 @@ export function balancedArguments(source, open) {
102
102
  let depth = 0;
103
103
  for (let i = open; i < source.length; i += 1) {
104
104
  const c = source[i];
105
- if (c === '"' || c === "'" || c === "`") {
106
- for (i += 1; i < source.length && source[i] !== c; i += 1) if (source[i] === "\\") i += 1;
107
- } else if (c === "/" && source[i + 1] === "/") {
108
- i = source.indexOf("\n", i);
109
- if (i < 0) break;
110
- } else if (c === "/" && source[i + 1] === "*") {
111
- i = source.indexOf("*/", i + 2) + 1;
112
- if (i <= 0) break;
113
- } else if (c === "(") depth += 1;
105
+ const skipped = skipLiteralOrComment(source, i);
106
+ if (skipped === null) break;
107
+ if (skipped > i) i = skipped;
108
+ else if (c === "(") depth += 1;
114
109
  else if (c === ")" && --depth === 0) return source.slice(open + 1, i);
115
110
  }
116
111
  return source.slice(open + 1);
117
112
  }
118
113
 
114
+ /**
115
+ * Where a string, template or comment that starts at `i` ends (the index of its last character), `i` itself when none starts
116
+ * there, null when a line comment or a block comment never ends.
117
+ * @param {string} source
118
+ * @param {number} i
119
+ * @returns {number | null}
120
+ */
121
+ function skipLiteralOrComment(source, i) {
122
+ const c = source[i];
123
+ if (c === '"' || c === "'" || c === "`") {
124
+ let end = i + 1;
125
+ while (end < source.length && source[end] !== c) end += source[end] === "\\" ? 2 : 1;
126
+ return end;
127
+ }
128
+ if (c === "/" && source[i + 1] === "/") {
129
+ const end = source.indexOf("\n", i);
130
+ return end < 0 ? null : end;
131
+ }
132
+ if (c === "/" && source[i + 1] === "*") {
133
+ const end = source.indexOf("*/", i + 2);
134
+ return end < 0 ? null : end + 1;
135
+ }
136
+ return i;
137
+ }
138
+
119
139
  // ── Selections live in the URL (Oussama, 2026-10-22: « changer les tabs doit changer les routes ») ──────────────────────
120
140
  // Read by the ESLint rule krizaka/route-state-selection and by the ratchet.
121
141
 
@@ -2,7 +2,7 @@
2
2
  // Same patterns as the ESLint rules (../patterns.js), applied to the source text: the counter sees every
3
3
  // occurrence (a class in a `cn()` argument or a constant too), so it can only be stricter than the rule.
4
4
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
- import { relative, resolve } from "node:path";
5
+ import { isAbsolute, relative, resolve } from "node:path";
6
6
 
7
7
  import { glob } from "tinyglobby";
8
8
 
@@ -190,6 +190,25 @@ export class RatchetError extends Error {}
190
190
  * @property {boolean} written the ratchet file was created, lowered or completed
191
191
  */
192
192
 
193
+ /**
194
+ * Each counter against its recorded value: the report, and the names that went up or down.
195
+ * @param {Counters} current
196
+ * @param {Counters} recorded
197
+ */
198
+ function compareCounters(current, recorded) {
199
+ /** @type {RatchetResult["counters"]} */
200
+ const report = /** @type {any} */ ({});
201
+ /** @type {CounterName[]} */ const increased = [];
202
+ /** @type {CounterName[]} */ const decreased = [];
203
+ for (const name of COUNTERS) {
204
+ const delta = current[name] - recorded[name];
205
+ report[name] = { recorded: recorded[name], current: current[name], delta };
206
+ if (delta > 0) increased.push(name);
207
+ if (delta < 0) decreased.push(name);
208
+ }
209
+ return { report, increased, decreased };
210
+ }
211
+
193
212
  /**
194
213
  * Runs the ratchet.
195
214
  * - `check` (default): fails when a counter is above its recorded value.
@@ -201,6 +220,11 @@ export class RatchetError extends Error {}
201
220
  */
202
221
  export async function runRatchet({ root, mode = "check", globs, file = RATCHET_FILE }) {
203
222
  const path = resolve(root, file);
223
+ // the ratchet file is read and written: it stays a .json file inside the repository root (no `../`, no absolute path elsewhere)
224
+ const inside = relative(resolve(root), path);
225
+ if (!path.endsWith(".json") || inside === "" || inside.startsWith("..") || isAbsolute(inside)) {
226
+ throw new RatchetError(`${file}: the ratchet file must be a .json file inside ${root}.`);
227
+ }
204
228
  const exists = existsSync(path);
205
229
  if (mode === "init" && exists) throw new RatchetError(`${file} already exists: use --update to lower it.`);
206
230
  if (mode !== "init" && !exists) throw new RatchetError(`${file} not found in ${root}: run krizaka-ratchet --init first.`);
@@ -222,16 +246,7 @@ export async function runRatchet({ root, mode = "check", globs, file = RATCHET_F
222
246
  /** @type {CounterName[]} */ const added = recordedFile ? COUNTERS.filter((n) => recordedFile.counters[n] === undefined) : [];
223
247
  const recorded = /** @type {Counters} */ ({ ...current, ...recordedFile?.counters });
224
248
 
225
- /** @type {RatchetResult["counters"]} */
226
- const report = /** @type {any} */ ({});
227
- /** @type {CounterName[]} */ const increased = [];
228
- /** @type {CounterName[]} */ const decreased = [];
229
- for (const name of COUNTERS) {
230
- const delta = current[name] - recorded[name];
231
- report[name] = { recorded: recorded[name], current: current[name], delta };
232
- if (delta > 0) increased.push(name);
233
- if (delta < 0) decreased.push(name);
234
- }
249
+ const { report, increased, decreased } = compareCounters(current, recorded);
235
250
 
236
251
  let written = false;
237
252
  if (mode === "init") {