@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 +1 -1
- package/dependency-cruiser.cjs +3 -2
- package/eslint/rules.js +19 -10
- package/package.json +1 -1
- package/patterns.js +29 -9
- package/ratchet/ratchet.js +26 -11
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)
|
package/dependency-cruiser.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
package/ratchet/ratchet.js
CHANGED
|
@@ -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
|
-
|
|
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") {
|