@hublo/sentinel 1.1.0-alpha.2 → 1.1.0-alpha.4
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 +148 -35
- package/dist/bin/sentinel.js +119 -48
- package/dist/chunk-FLLX5MN3.js +3155 -0
- package/dist/index.d.ts +59 -37
- package/dist/index.js +1 -1
- package/oxlint/nest.json +184 -187
- package/oxlint/node.json +27 -30
- package/oxlint/react.json +189 -204
- package/oxlint/svelte.json +215 -0
- package/package.json +6 -4
- package/dist/chunk-EZCRU6XE.js +0 -2062
|
@@ -0,0 +1,3155 @@
|
|
|
1
|
+
// src/core/registry.ts
|
|
2
|
+
var adapters = [];
|
|
3
|
+
var defaultRunner = {
|
|
4
|
+
// Filled in by tool branches, e.g. lint: 'eslint', typescript: 'tsc'.
|
|
5
|
+
};
|
|
6
|
+
function register(adapter) {
|
|
7
|
+
adapters.push(adapter);
|
|
8
|
+
}
|
|
9
|
+
function setDefaultRunner(target, runner) {
|
|
10
|
+
defaultRunner[target] = runner;
|
|
11
|
+
}
|
|
12
|
+
function all() {
|
|
13
|
+
return adapters;
|
|
14
|
+
}
|
|
15
|
+
function availableTargets() {
|
|
16
|
+
return [...new Set(adapters.map((a) => a.target))];
|
|
17
|
+
}
|
|
18
|
+
function resolve(target, preset, runner) {
|
|
19
|
+
const forTarget = adapters.filter((a) => a.target === target);
|
|
20
|
+
if (forTarget.length === 0) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`No adapter registered for target "${target}" yet (it ships in a later ticket).`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const candidates = preset ? forTarget.filter((a) => a.appliesTo(preset)) : forTarget;
|
|
26
|
+
if (candidates.length === 0) {
|
|
27
|
+
throw new Error(`No adapter for target "${target}" handles preset "${preset}".`);
|
|
28
|
+
}
|
|
29
|
+
const wanted = runner ?? defaultRunner[target];
|
|
30
|
+
const available = candidates.map((a) => a.runner).join(", ");
|
|
31
|
+
if (!wanted) {
|
|
32
|
+
const [first, ...rest] = candidates;
|
|
33
|
+
if (first && rest.length === 0) return first;
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Multiple runners for target "${target}" (${available}); pass --runner or set a default.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const matching = candidates.filter((a) => a.runner === wanted);
|
|
39
|
+
if (matching.length === 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`No runner "${wanted}" for target "${target}" (preset "${preset}"). Available: ${available}.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (matching.length > 1) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Ambiguous: ${matching.length} adapters claim target "${target}", runner "${wanted}", preset "${preset}".`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return matching[0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/core/base-adapter.ts
|
|
53
|
+
var BaseAdapter = class {
|
|
54
|
+
inspect(_ctx) {
|
|
55
|
+
throw new Error(`${this.runner}: --inspect not implemented yet`);
|
|
56
|
+
}
|
|
57
|
+
report(_ctx) {
|
|
58
|
+
throw new Error(`${this.runner}: --report not implemented yet`);
|
|
59
|
+
}
|
|
60
|
+
status(_ctx) {
|
|
61
|
+
throw new Error(`${this.runner}: --status not implemented yet`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/shared/color.ts
|
|
66
|
+
import { styleText } from "util";
|
|
67
|
+
function palette(stream) {
|
|
68
|
+
const paint = (format, text) => styleText(format, text, { stream });
|
|
69
|
+
return {
|
|
70
|
+
ok: (text) => paint(["green", "bold"], text),
|
|
71
|
+
fail: (text) => paint(["red", "bold"], text),
|
|
72
|
+
warn: (text) => paint("yellow", text),
|
|
73
|
+
strong: (text) => paint("bold", text),
|
|
74
|
+
dim: (text) => paint("dim", text),
|
|
75
|
+
accent: (text) => paint("cyan", text)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/shared/package-json.ts
|
|
80
|
+
import { existsSync, readFileSync } from "fs";
|
|
81
|
+
import { dirname, join } from "path";
|
|
82
|
+
import { fileURLToPath } from "url";
|
|
83
|
+
function readOwnPackage() {
|
|
84
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
85
|
+
for (; ; ) {
|
|
86
|
+
const pkgPath = join(dir, "package.json");
|
|
87
|
+
if (existsSync(pkgPath)) {
|
|
88
|
+
try {
|
|
89
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
90
|
+
if (typeof pkg.version === "string") {
|
|
91
|
+
return { name: pkg.name ?? "@hublo/sentinel", version: pkg.version };
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const parent = dirname(dir);
|
|
97
|
+
if (parent === dir) return { name: "@hublo/sentinel", version: "0.0.0" };
|
|
98
|
+
dir = parent;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function readOwnVersion() {
|
|
102
|
+
return readOwnPackage().version;
|
|
103
|
+
}
|
|
104
|
+
function readProjectPackageJson(dir) {
|
|
105
|
+
const path = join(dir, "package.json");
|
|
106
|
+
if (!existsSync(path)) return {};
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
109
|
+
} catch {
|
|
110
|
+
process.stderr.write(`sentinel: could not parse ${path}; ignoring for detection.
|
|
111
|
+
`);
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function readNxProjectName(dir) {
|
|
116
|
+
const path = join(dir, "project.json");
|
|
117
|
+
if (!existsSync(path)) return void 0;
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
120
|
+
return typeof parsed.name === "string" ? parsed.name : void 0;
|
|
121
|
+
} catch {
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/core/domain.ts
|
|
127
|
+
var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
|
|
128
|
+
var TARGETS = [
|
|
129
|
+
"lint",
|
|
130
|
+
"format",
|
|
131
|
+
"typescript",
|
|
132
|
+
"build",
|
|
133
|
+
"test",
|
|
134
|
+
"static-analysis",
|
|
135
|
+
"runtime-analysis",
|
|
136
|
+
"arch"
|
|
137
|
+
];
|
|
138
|
+
var PRESET_NAMES = ["react", "nest", "svelte", "node"];
|
|
139
|
+
|
|
140
|
+
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
141
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
142
|
+
import { basename, join as join8 } from "path";
|
|
143
|
+
import { spawnSync } from "child_process";
|
|
144
|
+
|
|
145
|
+
// src/core/config/deferred-rules.ts
|
|
146
|
+
function deferredRuleNames(rules) {
|
|
147
|
+
return rules.map((entry) => entry.rule);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/roles/lint/config-policy.ts
|
|
151
|
+
var LINT_CONFIG_FILE = ".oxlintrc.json";
|
|
152
|
+
var LINT_SCRIPT_NAME = "lint";
|
|
153
|
+
var PRESET_DIR = "./node_modules/@hublo/sentinel/oxlint";
|
|
154
|
+
function presetPath(preset) {
|
|
155
|
+
return `${PRESET_DIR}/${preset}.json`;
|
|
156
|
+
}
|
|
157
|
+
var SENTINEL_LINT_PRESET = /@hublo\/sentinel\/oxlint\/([a-z-]+)\.json$/;
|
|
158
|
+
function presetNameFromPath(preset) {
|
|
159
|
+
return preset ? SENTINEL_LINT_PRESET.exec(preset)?.[1] ?? void 0 : void 0;
|
|
160
|
+
}
|
|
161
|
+
var PERMITTED_LOCAL_KEYS = ["extends"];
|
|
162
|
+
var ESLINT_CONFIG_FILES = [
|
|
163
|
+
"eslint.config.js",
|
|
164
|
+
"eslint.config.mjs",
|
|
165
|
+
"eslint.config.cjs",
|
|
166
|
+
"eslint.config.ts"
|
|
167
|
+
];
|
|
168
|
+
function lintTarget() {
|
|
169
|
+
return {
|
|
170
|
+
executor: "nx:run-commands",
|
|
171
|
+
cache: true,
|
|
172
|
+
// The config is an input so a preset bump invalidates the cache. Without it, nx would
|
|
173
|
+
// replay a stale result after the rules changed underneath it.
|
|
174
|
+
inputs: ["default", `{projectRoot}/${LINT_CONFIG_FILE}`],
|
|
175
|
+
options: { cwd: "{projectRoot}", command: `pnpm run ${LINT_SCRIPT_NAME}` }
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/roles/lint/presets/base.json
|
|
180
|
+
var base_default = {
|
|
181
|
+
rules: {
|
|
182
|
+
"no-var": "error",
|
|
183
|
+
"prefer-const": [
|
|
184
|
+
"error",
|
|
185
|
+
{
|
|
186
|
+
destructuring: "any",
|
|
187
|
+
ignoreReadBeforeAssign: false
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
"prefer-rest-params": "error",
|
|
191
|
+
"prefer-spread": "error",
|
|
192
|
+
"typescript/ban-ts-comment": "error",
|
|
193
|
+
"typescript/no-array-constructor": "error",
|
|
194
|
+
"typescript/no-duplicate-enum-values": "error",
|
|
195
|
+
"typescript/no-empty-object-type": "error",
|
|
196
|
+
"typescript/no-extra-non-null-assertion": "error",
|
|
197
|
+
"typescript/no-misused-new": "error",
|
|
198
|
+
"typescript/no-namespace": "error",
|
|
199
|
+
"typescript/no-non-null-asserted-optional-chain": "error",
|
|
200
|
+
"typescript/no-this-alias": "error",
|
|
201
|
+
"typescript/no-unnecessary-type-constraint": "error",
|
|
202
|
+
"typescript/no-unsafe-declaration-merging": "error",
|
|
203
|
+
"typescript/no-unsafe-function-type": "error",
|
|
204
|
+
"typescript/no-unused-expressions": [
|
|
205
|
+
"error",
|
|
206
|
+
{
|
|
207
|
+
allowShortCircuit: false,
|
|
208
|
+
allowTaggedTemplates: false,
|
|
209
|
+
allowTernary: false
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
"typescript/no-wrapper-object-types": "error",
|
|
213
|
+
"typescript/prefer-as-const": "error",
|
|
214
|
+
"typescript/prefer-namespace-keyword": "error",
|
|
215
|
+
"typescript/triple-slash-reference": "error"
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/roles/lint/presets/nest.json
|
|
220
|
+
var nest_default = {
|
|
221
|
+
plugins: ["typescript", "jest", "import", "unicorn", "oxc", "node"],
|
|
222
|
+
jsPlugins: [
|
|
223
|
+
"eslint-plugin-no-barrel-files",
|
|
224
|
+
{
|
|
225
|
+
name: "nx",
|
|
226
|
+
specifier: "@nx/eslint-plugin"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "import-js",
|
|
230
|
+
specifier: "eslint-plugin-import"
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
settings: {
|
|
234
|
+
"import/extensions": [".ts", ".cts", ".mts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
235
|
+
"import/external-module-folders": ["node_modules", "node_modules/@types"],
|
|
236
|
+
"import/parsers": {
|
|
237
|
+
"@typescript-eslint/parser": [".ts", ".cts", ".mts", ".tsx"]
|
|
238
|
+
},
|
|
239
|
+
"import/resolver": {
|
|
240
|
+
node: {
|
|
241
|
+
extensions: [".ts", ".cts", ".mts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
rules: {
|
|
246
|
+
"array-callback-return": [
|
|
247
|
+
"error",
|
|
248
|
+
{
|
|
249
|
+
allowImplicit: false,
|
|
250
|
+
checkForEach: false,
|
|
251
|
+
allowVoid: false
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
complexity: ["error", 30],
|
|
255
|
+
curly: ["error", "all"],
|
|
256
|
+
"dot-notation": [
|
|
257
|
+
"error",
|
|
258
|
+
{
|
|
259
|
+
allowKeywords: true,
|
|
260
|
+
allowPattern: ""
|
|
261
|
+
}
|
|
262
|
+
],
|
|
263
|
+
eqeqeq: "error",
|
|
264
|
+
"for-direction": "error",
|
|
265
|
+
"func-style": [
|
|
266
|
+
"error",
|
|
267
|
+
"expression",
|
|
268
|
+
{
|
|
269
|
+
allowArrowFunctions: false,
|
|
270
|
+
allowTypeAnnotation: false,
|
|
271
|
+
overrides: {}
|
|
272
|
+
}
|
|
273
|
+
],
|
|
274
|
+
"import-js/no-duplicates": [
|
|
275
|
+
"error",
|
|
276
|
+
{
|
|
277
|
+
considerQueryString: true,
|
|
278
|
+
"prefer-inline": true
|
|
279
|
+
}
|
|
280
|
+
],
|
|
281
|
+
"import-js/order": {
|
|
282
|
+
severity: "warn",
|
|
283
|
+
reason: "was error in the module config, but the lint target only ever checked 2 files of 2888, so the 23 violations were never reported; held at warn so adoption does not turn a green module red, and restored once they are fixed",
|
|
284
|
+
options: [
|
|
285
|
+
{
|
|
286
|
+
"newlines-between": "always",
|
|
287
|
+
groups: ["builtin", "external", "internal", ["parent", "index"], "sibling"],
|
|
288
|
+
alphabetize: {
|
|
289
|
+
order: "asc",
|
|
290
|
+
caseInsensitive: false,
|
|
291
|
+
orderImportKind: "ignore"
|
|
292
|
+
},
|
|
293
|
+
pathGroups: [
|
|
294
|
+
{
|
|
295
|
+
pattern: "@nestjs/**",
|
|
296
|
+
group: "external"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
pattern: "{@admin/**,@authentication/**,@featureToggles/**,@institution/**,@hublo/**,@network/**,@mission/**,@notification/**,@shared/**,@talent/**,@worker/**,@contract/**,@config/**,@front/**}",
|
|
300
|
+
group: "internal",
|
|
301
|
+
position: "before"
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
pathGroupsExcludedImportTypes: ["builtin"],
|
|
305
|
+
distinctGroup: true,
|
|
306
|
+
sortTypesGroup: false,
|
|
307
|
+
named: false,
|
|
308
|
+
warnOnUnassignedImports: false
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
"jest/expect-expect": {
|
|
313
|
+
severity: "warn",
|
|
314
|
+
reason: "the modules already ran this at warn (confirmed in the bff-admin proof-of-concept config); adoption is like-for-like, so raising it to error is a separate decision"
|
|
315
|
+
},
|
|
316
|
+
"jest/no-alias-methods": "error",
|
|
317
|
+
"jest/no-commented-out-tests": {
|
|
318
|
+
severity: "warn",
|
|
319
|
+
reason: "the modules already ran this at warn (confirmed in the bff-admin proof-of-concept config); adoption is like-for-like, so raising it to error is a separate decision"
|
|
320
|
+
},
|
|
321
|
+
"jest/no-deprecated-functions": "error",
|
|
322
|
+
"jest/no-disabled-tests": {
|
|
323
|
+
severity: "warn",
|
|
324
|
+
reason: "the modules already ran this at warn (confirmed in the bff-admin proof-of-concept config); a skipped test is a deliberate act often paired with a ticket, so it reports rather than blocks"
|
|
325
|
+
},
|
|
326
|
+
"jest/no-done-callback": "error",
|
|
327
|
+
"jest/no-export": "error",
|
|
328
|
+
"jest/no-focused-tests": "error",
|
|
329
|
+
"jest/no-identical-title": "error",
|
|
330
|
+
"jest/no-interpolation-in-snapshots": "error",
|
|
331
|
+
"jest/no-jasmine-globals": "error",
|
|
332
|
+
"jest/no-mocks-import": "error",
|
|
333
|
+
"jest/no-standalone-expect": "error",
|
|
334
|
+
"jest/no-test-prefixes": "error",
|
|
335
|
+
"jest/prefer-to-be": "error",
|
|
336
|
+
"jest/prefer-to-contain": "error",
|
|
337
|
+
"jest/prefer-to-have-length": "error",
|
|
338
|
+
"jest/valid-describe-callback": "error",
|
|
339
|
+
"jest/valid-expect": "error",
|
|
340
|
+
"jest/valid-expect-in-promise": "error",
|
|
341
|
+
"jest/valid-title": "error",
|
|
342
|
+
"no-async-promise-executor": "error",
|
|
343
|
+
"no-barrel-files/no-barrel-files": {
|
|
344
|
+
severity: "off",
|
|
345
|
+
reason: "nest modules use barrel index.ts files as a deliberate structural pattern; the rule fired 127 times and every one was the architecture working as intended"
|
|
346
|
+
},
|
|
347
|
+
"no-case-declarations": "error",
|
|
348
|
+
"no-compare-neg-zero": "error",
|
|
349
|
+
"no-cond-assign": ["error", "except-parens"],
|
|
350
|
+
"no-console": [
|
|
351
|
+
"error",
|
|
352
|
+
{
|
|
353
|
+
allow: ["warn", "error"]
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
"no-constant-binary-expression": "error",
|
|
357
|
+
"no-constant-condition": [
|
|
358
|
+
"error",
|
|
359
|
+
{
|
|
360
|
+
checkLoops: "allExceptWhileTrue"
|
|
361
|
+
}
|
|
362
|
+
],
|
|
363
|
+
"no-control-regex": {
|
|
364
|
+
severity: "off",
|
|
365
|
+
reason: "the single occurrence is a regex whose whole purpose is stripping ASCII control characters before XML output, which the rule cannot distinguish from an accident"
|
|
366
|
+
},
|
|
367
|
+
"no-debugger": "error",
|
|
368
|
+
"no-delete-var": "error",
|
|
369
|
+
"no-dupe-else-if": "error",
|
|
370
|
+
"no-dupe-keys": "error",
|
|
371
|
+
"no-duplicate-case": "error",
|
|
372
|
+
"no-else-return": [
|
|
373
|
+
"error",
|
|
374
|
+
{
|
|
375
|
+
allowElseIf: true
|
|
376
|
+
}
|
|
377
|
+
],
|
|
378
|
+
"no-empty": [
|
|
379
|
+
"error",
|
|
380
|
+
{
|
|
381
|
+
allowEmptyCatch: false
|
|
382
|
+
}
|
|
383
|
+
],
|
|
384
|
+
"no-empty-character-class": "error",
|
|
385
|
+
"no-empty-pattern": [
|
|
386
|
+
"error",
|
|
387
|
+
{
|
|
388
|
+
allowObjectPatternsAsParameters: false
|
|
389
|
+
}
|
|
390
|
+
],
|
|
391
|
+
"no-empty-static-block": "error",
|
|
392
|
+
"no-ex-assign": "error",
|
|
393
|
+
"no-extra-boolean-cast": ["error", {}],
|
|
394
|
+
"no-fallthrough": [
|
|
395
|
+
"error",
|
|
396
|
+
{
|
|
397
|
+
allowEmptyCase: false,
|
|
398
|
+
reportUnusedFallthroughComment: false
|
|
399
|
+
}
|
|
400
|
+
],
|
|
401
|
+
"no-global-assign": [
|
|
402
|
+
"error",
|
|
403
|
+
{
|
|
404
|
+
exceptions: []
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
"no-implicit-coercion": [
|
|
408
|
+
"error",
|
|
409
|
+
{
|
|
410
|
+
allow: [],
|
|
411
|
+
boolean: true,
|
|
412
|
+
disallowTemplateShorthand: false,
|
|
413
|
+
number: true,
|
|
414
|
+
string: true
|
|
415
|
+
}
|
|
416
|
+
],
|
|
417
|
+
"no-invalid-regexp": ["error", {}],
|
|
418
|
+
"no-irregular-whitespace": [
|
|
419
|
+
"error",
|
|
420
|
+
{
|
|
421
|
+
skipComments: false,
|
|
422
|
+
skipJSXText: false,
|
|
423
|
+
skipRegExps: false,
|
|
424
|
+
skipStrings: true,
|
|
425
|
+
skipTemplates: false
|
|
426
|
+
}
|
|
427
|
+
],
|
|
428
|
+
"no-lonely-if": "error",
|
|
429
|
+
"no-loss-of-precision": "error",
|
|
430
|
+
"no-misleading-character-class": "error",
|
|
431
|
+
"no-nonoctal-decimal-escape": "error",
|
|
432
|
+
"no-prototype-builtins": "error",
|
|
433
|
+
"no-regex-spaces": "error",
|
|
434
|
+
"no-restricted-imports": [
|
|
435
|
+
"error",
|
|
436
|
+
{
|
|
437
|
+
paths: [
|
|
438
|
+
{
|
|
439
|
+
name: "@front/theme",
|
|
440
|
+
importNames: [
|
|
441
|
+
"hubloTheme",
|
|
442
|
+
"legacyHubloTheme",
|
|
443
|
+
"customCommonColors",
|
|
444
|
+
"customPalette",
|
|
445
|
+
"breakpointsOptions",
|
|
446
|
+
"breakpointsValues",
|
|
447
|
+
"spacingConstants"
|
|
448
|
+
],
|
|
449
|
+
message: "Use the MUI theme context or the public providers exposed by @front/theme."
|
|
450
|
+
}
|
|
451
|
+
],
|
|
452
|
+
patterns: [
|
|
453
|
+
{
|
|
454
|
+
group: ["@front/theme/*", "!@front/theme/testing"],
|
|
455
|
+
message: "Deep imports from @front/theme are forbidden outside the theme lib."
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
}
|
|
459
|
+
],
|
|
460
|
+
"no-self-assign": [
|
|
461
|
+
"error",
|
|
462
|
+
{
|
|
463
|
+
props: true
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
"no-shadow-restricted-names": [
|
|
467
|
+
"error",
|
|
468
|
+
{
|
|
469
|
+
reportGlobalThis: false
|
|
470
|
+
}
|
|
471
|
+
],
|
|
472
|
+
"no-sparse-arrays": "error",
|
|
473
|
+
"no-unneeded-ternary": [
|
|
474
|
+
"error",
|
|
475
|
+
{
|
|
476
|
+
defaultAssignment: true
|
|
477
|
+
}
|
|
478
|
+
],
|
|
479
|
+
"no-unsafe-finally": "error",
|
|
480
|
+
"no-unsafe-optional-chaining": [
|
|
481
|
+
"error",
|
|
482
|
+
{
|
|
483
|
+
disallowArithmeticOperators: false
|
|
484
|
+
}
|
|
485
|
+
],
|
|
486
|
+
"no-unused-labels": "error",
|
|
487
|
+
"no-unused-private-class-members": "error",
|
|
488
|
+
"no-useless-backreference": "error",
|
|
489
|
+
"no-useless-catch": "error",
|
|
490
|
+
"no-useless-escape": [
|
|
491
|
+
"error",
|
|
492
|
+
{
|
|
493
|
+
allowRegexCharacters: []
|
|
494
|
+
}
|
|
495
|
+
],
|
|
496
|
+
"no-useless-return": "error",
|
|
497
|
+
"nx/enforce-module-boundaries": [
|
|
498
|
+
"error",
|
|
499
|
+
{
|
|
500
|
+
enforceBuildableLibDependency: true,
|
|
501
|
+
allow: [],
|
|
502
|
+
depConstraints: [
|
|
503
|
+
{
|
|
504
|
+
sourceTag: "*",
|
|
505
|
+
onlyDependOnLibsWithTags: ["*"]
|
|
506
|
+
}
|
|
507
|
+
]
|
|
508
|
+
}
|
|
509
|
+
],
|
|
510
|
+
"prefer-arrow-callback": [
|
|
511
|
+
"error",
|
|
512
|
+
{
|
|
513
|
+
allowNamedFunctions: false,
|
|
514
|
+
allowUnboundThis: true
|
|
515
|
+
}
|
|
516
|
+
],
|
|
517
|
+
"require-yield": "error",
|
|
518
|
+
"typescript/await-thenable": "error",
|
|
519
|
+
"typescript/no-confusing-non-null-assertion": "error",
|
|
520
|
+
"typescript/no-empty-function": [
|
|
521
|
+
"error",
|
|
522
|
+
{
|
|
523
|
+
allow: []
|
|
524
|
+
}
|
|
525
|
+
],
|
|
526
|
+
"typescript/no-explicit-any": "error",
|
|
527
|
+
"typescript/no-floating-promises": [
|
|
528
|
+
"error",
|
|
529
|
+
{
|
|
530
|
+
ignoreVoid: true
|
|
531
|
+
}
|
|
532
|
+
],
|
|
533
|
+
"typescript/no-import-type-side-effects": "error",
|
|
534
|
+
"typescript/no-inferrable-types": "error",
|
|
535
|
+
"typescript/no-non-null-assertion": "error",
|
|
536
|
+
"typescript/no-unused-vars": [
|
|
537
|
+
"error",
|
|
538
|
+
{
|
|
539
|
+
argsIgnorePattern: "^_",
|
|
540
|
+
caughtErrors: "none"
|
|
541
|
+
}
|
|
542
|
+
],
|
|
543
|
+
"typescript/return-await": {
|
|
544
|
+
severity: "warn",
|
|
545
|
+
reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. The `never` option matches what no-return-await meant, so this is like-for-like, but it now runs over every file and finds real violations (9 on bff-admin, 5 on host-admin, measured with --type-aware). Held at warn so adoption does not turn a green module red, and raised to error once they are fixed. Switching to the stricter `in-try-catch`, which typescript-eslint recommends because `return await` inside try IS needed for correct error handling, is a separate decision with its own count.",
|
|
546
|
+
options: ["never"]
|
|
547
|
+
},
|
|
548
|
+
"typescript/switch-exhaustiveness-check": [
|
|
549
|
+
"error",
|
|
550
|
+
{
|
|
551
|
+
considerDefaultExhaustiveForUnions: true
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
"use-isnan": [
|
|
555
|
+
"error",
|
|
556
|
+
{
|
|
557
|
+
enforceForIndexOf: false,
|
|
558
|
+
enforceForSwitchCase: true
|
|
559
|
+
}
|
|
560
|
+
],
|
|
561
|
+
"valid-typeof": [
|
|
562
|
+
"error",
|
|
563
|
+
{
|
|
564
|
+
requireStringLiterals: false
|
|
565
|
+
}
|
|
566
|
+
]
|
|
567
|
+
},
|
|
568
|
+
uncovered: {
|
|
569
|
+
"array-bracket-newline": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
570
|
+
"array-bracket-spacing": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
571
|
+
"block-spacing": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
572
|
+
"computed-property-spacing": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
573
|
+
"dot-location": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
574
|
+
"eol-last": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
575
|
+
"func-call-spacing": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
576
|
+
"key-spacing": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
577
|
+
"keyword-spacing": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
578
|
+
"lines-between-class-members": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
579
|
+
"no-invalid-this": "no oxlint equivalent, and hosting does not help: the rule crashes on every file under oxlint's plugin API (sourceCode.getJSDocComment is not implemented). Its coverage moves to TypeScript's noImplicitThis, which the sentinel TypeScript role manages.",
|
|
580
|
+
"no-multi-spaces": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
581
|
+
"no-octal": "no oxlint equivalent",
|
|
582
|
+
"no-trailing-spaces": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
583
|
+
semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
584
|
+
"template-curly-spacing": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
|
|
585
|
+
"typescript/member-ordering": "oxlint has no native rule for it, and the preset deliberately does NOT host @typescript-eslint/eslint-plugin: hosting a whole plugin for one rule costs a dependency, a load step and a pass over every file. Accepted as a loss rather than paid for, the same call as no-octal."
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// src/roles/lint/presets/node.json
|
|
590
|
+
var node_default = {
|
|
591
|
+
plugins: ["typescript", "oxc"],
|
|
592
|
+
jsPlugins: [],
|
|
593
|
+
settings: null,
|
|
594
|
+
rules: {}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/roles/lint/presets/react.json
|
|
598
|
+
var react_default = {
|
|
599
|
+
plugins: ["typescript", "react", "jsx-a11y", "jest", "import", "unicorn", "oxc"],
|
|
600
|
+
jsPlugins: [
|
|
601
|
+
"eslint-plugin-no-barrel-files",
|
|
602
|
+
{
|
|
603
|
+
name: "nx",
|
|
604
|
+
specifier: "@nx/eslint-plugin"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "import-js",
|
|
608
|
+
specifier: "eslint-plugin-import"
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: "@tanstack/query",
|
|
612
|
+
specifier: "@tanstack/eslint-plugin-query"
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
name: "styled-components-a11y",
|
|
616
|
+
specifier: "eslint-plugin-styled-components-a11y"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "react-refresh",
|
|
620
|
+
specifier: "eslint-plugin-react-refresh"
|
|
621
|
+
},
|
|
622
|
+
"../plugins/hublo.js"
|
|
623
|
+
],
|
|
624
|
+
settings: {
|
|
625
|
+
"import/extensions": [".ts", ".cts", ".mts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
626
|
+
"import/external-module-folders": ["node_modules", "node_modules/@types"],
|
|
627
|
+
"import/parsers": {
|
|
628
|
+
"@typescript-eslint/parser": [".ts", ".cts", ".mts", ".tsx"]
|
|
629
|
+
},
|
|
630
|
+
"import/resolver": {
|
|
631
|
+
node: {
|
|
632
|
+
extensions: [".ts", ".cts", ".mts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
rules: {
|
|
637
|
+
"@tanstack/query/exhaustive-deps": {
|
|
638
|
+
severity: "off",
|
|
639
|
+
reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
|
|
640
|
+
},
|
|
641
|
+
"@tanstack/query/infinite-query-property-order": {
|
|
642
|
+
severity: "off",
|
|
643
|
+
reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
|
|
644
|
+
},
|
|
645
|
+
"@tanstack/query/mutation-property-order": {
|
|
646
|
+
severity: "off",
|
|
647
|
+
reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
|
|
648
|
+
},
|
|
649
|
+
"@tanstack/query/no-rest-destructuring": {
|
|
650
|
+
severity: "off",
|
|
651
|
+
reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
|
|
652
|
+
},
|
|
653
|
+
"@tanstack/query/no-unstable-deps": {
|
|
654
|
+
severity: "off",
|
|
655
|
+
reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
|
|
656
|
+
},
|
|
657
|
+
"@tanstack/query/no-void-query-fn": {
|
|
658
|
+
severity: "off",
|
|
659
|
+
reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
|
|
660
|
+
},
|
|
661
|
+
"@tanstack/query/stable-query-client": {
|
|
662
|
+
severity: "off",
|
|
663
|
+
reason: "the codebase predates these rules and has never satisfied them; enabling them is its own piece of work, not part of swapping linters"
|
|
664
|
+
},
|
|
665
|
+
curly: ["error", "all"],
|
|
666
|
+
"for-direction": "error",
|
|
667
|
+
"hublo/no-palette-common": {
|
|
668
|
+
severity: "off",
|
|
669
|
+
reason: "the house rule finds 3 real violations; they are fixed as their own change rather than inside a linter swap, so the rule ships visible-but-off and can be switched on once they are"
|
|
670
|
+
},
|
|
671
|
+
"import-js/order": [
|
|
672
|
+
"error",
|
|
673
|
+
{
|
|
674
|
+
"newlines-between": "always",
|
|
675
|
+
groups: ["builtin", "external", "internal", ["parent", "index"], "sibling"],
|
|
676
|
+
alphabetize: {
|
|
677
|
+
order: "asc"
|
|
678
|
+
},
|
|
679
|
+
pathGroups: [
|
|
680
|
+
{
|
|
681
|
+
pattern: "@nestjs/**",
|
|
682
|
+
group: "external"
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
pattern: "{@admin/**,@authentication/**,@featureToggles/**,@institution/**,@hublo/**,@network/**,@mission/**,@notification/**,@shared/**,@talent/**,@worker/**,@contract/**,@config/**,@front/**}",
|
|
686
|
+
group: "internal",
|
|
687
|
+
position: "before"
|
|
688
|
+
}
|
|
689
|
+
],
|
|
690
|
+
pathGroupsExcludedImportTypes: ["builtin"]
|
|
691
|
+
}
|
|
692
|
+
],
|
|
693
|
+
"jest/expect-expect": {
|
|
694
|
+
severity: "off",
|
|
695
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
696
|
+
},
|
|
697
|
+
"jest/no-alias-methods": {
|
|
698
|
+
severity: "off",
|
|
699
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
700
|
+
},
|
|
701
|
+
"jest/no-commented-out-tests": {
|
|
702
|
+
severity: "off",
|
|
703
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
704
|
+
},
|
|
705
|
+
"jest/no-conditional-expect": {
|
|
706
|
+
severity: "off",
|
|
707
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
708
|
+
},
|
|
709
|
+
"jest/no-deprecated-functions": {
|
|
710
|
+
severity: "off",
|
|
711
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
712
|
+
},
|
|
713
|
+
"jest/no-disabled-tests": {
|
|
714
|
+
severity: "off",
|
|
715
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
716
|
+
},
|
|
717
|
+
"jest/no-done-callback": {
|
|
718
|
+
severity: "off",
|
|
719
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
720
|
+
},
|
|
721
|
+
"jest/no-export": {
|
|
722
|
+
severity: "off",
|
|
723
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
724
|
+
},
|
|
725
|
+
"jest/no-focused-tests": {
|
|
726
|
+
severity: "off",
|
|
727
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
728
|
+
},
|
|
729
|
+
"jest/no-identical-title": {
|
|
730
|
+
severity: "off",
|
|
731
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
732
|
+
},
|
|
733
|
+
"jest/no-interpolation-in-snapshots": {
|
|
734
|
+
severity: "off",
|
|
735
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
736
|
+
},
|
|
737
|
+
"jest/no-jasmine-globals": {
|
|
738
|
+
severity: "off",
|
|
739
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
740
|
+
},
|
|
741
|
+
"jest/no-mocks-import": {
|
|
742
|
+
severity: "off",
|
|
743
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
744
|
+
},
|
|
745
|
+
"jest/no-standalone-expect": {
|
|
746
|
+
severity: "off",
|
|
747
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
748
|
+
},
|
|
749
|
+
"jest/no-test-prefixes": {
|
|
750
|
+
severity: "off",
|
|
751
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
752
|
+
},
|
|
753
|
+
"jest/prefer-to-be": {
|
|
754
|
+
severity: "off",
|
|
755
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
756
|
+
},
|
|
757
|
+
"jest/prefer-to-contain": {
|
|
758
|
+
severity: "off",
|
|
759
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
760
|
+
},
|
|
761
|
+
"jest/prefer-to-have-length": {
|
|
762
|
+
severity: "off",
|
|
763
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
764
|
+
},
|
|
765
|
+
"jest/valid-describe-callback": {
|
|
766
|
+
severity: "off",
|
|
767
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
768
|
+
},
|
|
769
|
+
"jest/valid-expect": {
|
|
770
|
+
severity: "off",
|
|
771
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
772
|
+
},
|
|
773
|
+
"jest/valid-expect-in-promise": {
|
|
774
|
+
severity: "off",
|
|
775
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
776
|
+
},
|
|
777
|
+
"jest/valid-title": {
|
|
778
|
+
severity: "off",
|
|
779
|
+
reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
|
|
780
|
+
},
|
|
781
|
+
"jsx-a11y/alt-text": "error",
|
|
782
|
+
"jsx-a11y/anchor-has-content": "error",
|
|
783
|
+
"jsx-a11y/anchor-is-valid": "error",
|
|
784
|
+
"jsx-a11y/aria-activedescendant-has-tabindex": "error",
|
|
785
|
+
"jsx-a11y/aria-props": "error",
|
|
786
|
+
"jsx-a11y/aria-proptypes": "error",
|
|
787
|
+
"jsx-a11y/aria-role": "error",
|
|
788
|
+
"jsx-a11y/aria-unsupported-elements": "error",
|
|
789
|
+
"jsx-a11y/autocomplete-valid": "error",
|
|
790
|
+
"jsx-a11y/click-events-have-key-events": "error",
|
|
791
|
+
"jsx-a11y/heading-has-content": "error",
|
|
792
|
+
"jsx-a11y/html-has-lang": "error",
|
|
793
|
+
"jsx-a11y/iframe-has-title": "error",
|
|
794
|
+
"jsx-a11y/img-redundant-alt": {
|
|
795
|
+
severity: "off",
|
|
796
|
+
reason: 'one real finding, a decorative image whose title is already rendered as a heading beside it, so the fix is alt="". A genuine accessibility issue rather than a false positive: parked only so swapping linters stays a lateral move, and worth switching back on as soon as it is fixed'
|
|
797
|
+
},
|
|
798
|
+
"jsx-a11y/interactive-supports-focus": [
|
|
799
|
+
"error",
|
|
800
|
+
{
|
|
801
|
+
tabbable: [
|
|
802
|
+
"button",
|
|
803
|
+
"checkbox",
|
|
804
|
+
"link",
|
|
805
|
+
"progressbar",
|
|
806
|
+
"searchbox",
|
|
807
|
+
"slider",
|
|
808
|
+
"spinbutton",
|
|
809
|
+
"switch",
|
|
810
|
+
"textbox"
|
|
811
|
+
]
|
|
812
|
+
}
|
|
813
|
+
],
|
|
814
|
+
"jsx-a11y/label-has-associated-control": "error",
|
|
815
|
+
"jsx-a11y/media-has-caption": "error",
|
|
816
|
+
"jsx-a11y/mouse-events-have-key-events": "error",
|
|
817
|
+
"jsx-a11y/no-access-key": "error",
|
|
818
|
+
"jsx-a11y/no-autofocus": "error",
|
|
819
|
+
"jsx-a11y/no-distracting-elements": "error",
|
|
820
|
+
"jsx-a11y/no-interactive-element-to-noninteractive-role": "error",
|
|
821
|
+
"jsx-a11y/no-noninteractive-element-interactions": [
|
|
822
|
+
"error",
|
|
823
|
+
{
|
|
824
|
+
body: ["onError", "onLoad"],
|
|
825
|
+
iframe: ["onError", "onLoad"],
|
|
826
|
+
img: ["onError", "onLoad"]
|
|
827
|
+
}
|
|
828
|
+
],
|
|
829
|
+
"jsx-a11y/no-noninteractive-element-to-interactive-role": "error",
|
|
830
|
+
"jsx-a11y/no-noninteractive-tabindex": "error",
|
|
831
|
+
"jsx-a11y/no-redundant-roles": "error",
|
|
832
|
+
"jsx-a11y/no-static-element-interactions": "error",
|
|
833
|
+
"jsx-a11y/role-has-required-aria-props": "error",
|
|
834
|
+
"jsx-a11y/role-supports-aria-props": "error",
|
|
835
|
+
"jsx-a11y/scope": "error",
|
|
836
|
+
"jsx-a11y/tabindex-no-positive": "error",
|
|
837
|
+
"no-async-promise-executor": "error",
|
|
838
|
+
"no-barrel-files/no-barrel-files": {
|
|
839
|
+
severity: "warn",
|
|
840
|
+
reason: "the modules already ran this at warn (confirmed in the host-admin proof-of-concept config); nest disables it outright as deliberate architecture, react keeps it visible"
|
|
841
|
+
},
|
|
842
|
+
"no-case-declarations": "error",
|
|
843
|
+
"no-compare-neg-zero": "error",
|
|
844
|
+
"no-cond-assign": ["error", "except-parens"],
|
|
845
|
+
"no-console": {
|
|
846
|
+
severity: "off",
|
|
847
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
848
|
+
},
|
|
849
|
+
"no-constant-binary-expression": "error",
|
|
850
|
+
"no-constant-condition": [
|
|
851
|
+
"error",
|
|
852
|
+
{
|
|
853
|
+
checkLoops: "allExceptWhileTrue"
|
|
854
|
+
}
|
|
855
|
+
],
|
|
856
|
+
"no-control-regex": "error",
|
|
857
|
+
"no-debugger": "error",
|
|
858
|
+
"no-delete-var": "error",
|
|
859
|
+
"no-dupe-else-if": "error",
|
|
860
|
+
"no-duplicate-case": "error",
|
|
861
|
+
"no-empty": [
|
|
862
|
+
"error",
|
|
863
|
+
{
|
|
864
|
+
allowEmptyCatch: false
|
|
865
|
+
}
|
|
866
|
+
],
|
|
867
|
+
"no-empty-character-class": "error",
|
|
868
|
+
"no-empty-pattern": [
|
|
869
|
+
"error",
|
|
870
|
+
{
|
|
871
|
+
allowObjectPatternsAsParameters: false
|
|
872
|
+
}
|
|
873
|
+
],
|
|
874
|
+
"no-empty-static-block": "error",
|
|
875
|
+
"no-ex-assign": "error",
|
|
876
|
+
"no-extra-boolean-cast": ["error", {}],
|
|
877
|
+
"no-fallthrough": [
|
|
878
|
+
"error",
|
|
879
|
+
{
|
|
880
|
+
allowEmptyCase: false,
|
|
881
|
+
reportUnusedFallthroughComment: false
|
|
882
|
+
}
|
|
883
|
+
],
|
|
884
|
+
"no-global-assign": [
|
|
885
|
+
"error",
|
|
886
|
+
{
|
|
887
|
+
exceptions: []
|
|
888
|
+
}
|
|
889
|
+
],
|
|
890
|
+
"no-invalid-regexp": ["error", {}],
|
|
891
|
+
"no-irregular-whitespace": [
|
|
892
|
+
"error",
|
|
893
|
+
{
|
|
894
|
+
skipComments: false,
|
|
895
|
+
skipJSXText: false,
|
|
896
|
+
skipRegExps: false,
|
|
897
|
+
skipStrings: true,
|
|
898
|
+
skipTemplates: false
|
|
899
|
+
}
|
|
900
|
+
],
|
|
901
|
+
"no-loss-of-precision": "error",
|
|
902
|
+
"no-misleading-character-class": "error",
|
|
903
|
+
"no-nonoctal-decimal-escape": "error",
|
|
904
|
+
"no-prototype-builtins": "error",
|
|
905
|
+
"no-regex-spaces": "error",
|
|
906
|
+
"no-restricted-imports": [
|
|
907
|
+
"error",
|
|
908
|
+
{
|
|
909
|
+
patterns: [
|
|
910
|
+
{
|
|
911
|
+
group: ["next/*"],
|
|
912
|
+
message: "Importing from next/* is not allowed in the admin project."
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
group: ["storybook/*"],
|
|
916
|
+
message: "Importing from storybook/* is not allowed in the admin project."
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
group: ["@nestjs/*"],
|
|
920
|
+
message: "Importing from @nestjs/* is not allowed in the admin project."
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
group: ["next-i18next"],
|
|
924
|
+
message: "Importing from next-i18next is not allowed in the admin project."
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
group: ["@hublo/style/colors", "@hublo/style/colors/*"],
|
|
928
|
+
message: "Importing runtime colors directly is not allowed. Use the MUI theme, theme-derived CSS variables, or an explicit product-specific mapping."
|
|
929
|
+
}
|
|
930
|
+
]
|
|
931
|
+
}
|
|
932
|
+
],
|
|
933
|
+
"no-self-assign": [
|
|
934
|
+
"error",
|
|
935
|
+
{
|
|
936
|
+
props: true
|
|
937
|
+
}
|
|
938
|
+
],
|
|
939
|
+
"no-shadow-restricted-names": [
|
|
940
|
+
"error",
|
|
941
|
+
{
|
|
942
|
+
reportGlobalThis: false
|
|
943
|
+
}
|
|
944
|
+
],
|
|
945
|
+
"no-sparse-arrays": "error",
|
|
946
|
+
"no-unexpected-multiline": "error",
|
|
947
|
+
"no-unsafe-finally": "error",
|
|
948
|
+
"no-unsafe-optional-chaining": {
|
|
949
|
+
severity: "off",
|
|
950
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
951
|
+
},
|
|
952
|
+
"no-unused-labels": "error",
|
|
953
|
+
"no-unused-private-class-members": "error",
|
|
954
|
+
"no-useless-backreference": "error",
|
|
955
|
+
"no-useless-catch": "error",
|
|
956
|
+
"no-useless-escape": [
|
|
957
|
+
"error",
|
|
958
|
+
{
|
|
959
|
+
allowRegexCharacters: []
|
|
960
|
+
}
|
|
961
|
+
],
|
|
962
|
+
"nx/enforce-module-boundaries": [
|
|
963
|
+
"error",
|
|
964
|
+
{
|
|
965
|
+
enforceBuildableLibDependency: true,
|
|
966
|
+
allow: [],
|
|
967
|
+
depConstraints: [
|
|
968
|
+
{
|
|
969
|
+
sourceTag: "*",
|
|
970
|
+
onlyDependOnLibsWithTags: ["*"]
|
|
971
|
+
}
|
|
972
|
+
]
|
|
973
|
+
}
|
|
974
|
+
],
|
|
975
|
+
"react-hooks/exhaustive-deps": {
|
|
976
|
+
severity: "warn",
|
|
977
|
+
reason: "the modules already ran this at warn (confirmed in the host-admin proof-of-concept config); its suggested fix can change behaviour, so it reports and a human decides"
|
|
978
|
+
},
|
|
979
|
+
"react-hooks/rules-of-hooks": "error",
|
|
980
|
+
"react-refresh/only-export-components": {
|
|
981
|
+
severity: "warn",
|
|
982
|
+
reason: "the modules already ran this at warn (confirmed in the host-admin proof-of-concept config); it guards fast-refresh ergonomics, not correctness",
|
|
983
|
+
options: [
|
|
984
|
+
{
|
|
985
|
+
allowConstantExport: true
|
|
986
|
+
}
|
|
987
|
+
]
|
|
988
|
+
},
|
|
989
|
+
"react/jsx-curly-brace-presence": {
|
|
990
|
+
severity: "off",
|
|
991
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
992
|
+
},
|
|
993
|
+
"require-yield": "error",
|
|
994
|
+
"styled-components-a11y/alt-text": "error",
|
|
995
|
+
"styled-components-a11y/anchor-has-content": "error",
|
|
996
|
+
"styled-components-a11y/anchor-is-valid": "error",
|
|
997
|
+
"styled-components-a11y/aria-activedescendant-has-tabindex": "error",
|
|
998
|
+
"styled-components-a11y/aria-props": "error",
|
|
999
|
+
"styled-components-a11y/aria-proptypes": "error",
|
|
1000
|
+
"styled-components-a11y/aria-role": "error",
|
|
1001
|
+
"styled-components-a11y/aria-unsupported-elements": "error",
|
|
1002
|
+
"styled-components-a11y/autocomplete-valid": "error",
|
|
1003
|
+
"styled-components-a11y/click-events-have-key-events": "error",
|
|
1004
|
+
"styled-components-a11y/control-has-associated-label": {
|
|
1005
|
+
severity: "off",
|
|
1006
|
+
reason: "off in the host-admin proof-of-concept config, with its element and role ignore lists; it fires on composed styled components where the label lives in a wrapper the rule cannot see",
|
|
1007
|
+
options: [
|
|
1008
|
+
{
|
|
1009
|
+
ignoreElements: ["audio", "canvas", "embed", "input", "textarea", "tr", "video"],
|
|
1010
|
+
ignoreRoles: [
|
|
1011
|
+
"grid",
|
|
1012
|
+
"listbox",
|
|
1013
|
+
"menu",
|
|
1014
|
+
"menubar",
|
|
1015
|
+
"radiogroup",
|
|
1016
|
+
"row",
|
|
1017
|
+
"tablist",
|
|
1018
|
+
"toolbar",
|
|
1019
|
+
"tree",
|
|
1020
|
+
"treegrid"
|
|
1021
|
+
],
|
|
1022
|
+
includeRoles: ["alert", "dialog"]
|
|
1023
|
+
}
|
|
1024
|
+
]
|
|
1025
|
+
},
|
|
1026
|
+
"styled-components-a11y/heading-has-content": "error",
|
|
1027
|
+
"styled-components-a11y/html-has-lang": "error",
|
|
1028
|
+
"styled-components-a11y/iframe-has-title": "error",
|
|
1029
|
+
"styled-components-a11y/img-redundant-alt": "error",
|
|
1030
|
+
"styled-components-a11y/interactive-supports-focus": [
|
|
1031
|
+
"error",
|
|
1032
|
+
{
|
|
1033
|
+
tabbable: [
|
|
1034
|
+
"button",
|
|
1035
|
+
"checkbox",
|
|
1036
|
+
"link",
|
|
1037
|
+
"progressbar",
|
|
1038
|
+
"searchbox",
|
|
1039
|
+
"slider",
|
|
1040
|
+
"spinbutton",
|
|
1041
|
+
"switch",
|
|
1042
|
+
"textbox"
|
|
1043
|
+
]
|
|
1044
|
+
}
|
|
1045
|
+
],
|
|
1046
|
+
"styled-components-a11y/label-has-associated-control": "error",
|
|
1047
|
+
"styled-components-a11y/label-has-for": {
|
|
1048
|
+
severity: "off",
|
|
1049
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1050
|
+
},
|
|
1051
|
+
"styled-components-a11y/media-has-caption": "error",
|
|
1052
|
+
"styled-components-a11y/mouse-events-have-key-events": "error",
|
|
1053
|
+
"styled-components-a11y/no-access-key": "error",
|
|
1054
|
+
"styled-components-a11y/no-autofocus": "error",
|
|
1055
|
+
"styled-components-a11y/no-distracting-elements": "error",
|
|
1056
|
+
"styled-components-a11y/no-interactive-element-to-noninteractive-role": "error",
|
|
1057
|
+
"styled-components-a11y/no-noninteractive-element-interactions": [
|
|
1058
|
+
"error",
|
|
1059
|
+
{
|
|
1060
|
+
body: ["onError", "onLoad"],
|
|
1061
|
+
iframe: ["onError", "onLoad"],
|
|
1062
|
+
img: ["onError", "onLoad"]
|
|
1063
|
+
}
|
|
1064
|
+
],
|
|
1065
|
+
"styled-components-a11y/no-noninteractive-element-to-interactive-role": "error",
|
|
1066
|
+
"styled-components-a11y/no-noninteractive-tabindex": "error",
|
|
1067
|
+
"styled-components-a11y/no-redundant-roles": "error",
|
|
1068
|
+
"styled-components-a11y/no-static-element-interactions": "error",
|
|
1069
|
+
"styled-components-a11y/role-has-required-aria-props": "error",
|
|
1070
|
+
"styled-components-a11y/role-supports-aria-props": "error",
|
|
1071
|
+
"styled-components-a11y/scope": "error",
|
|
1072
|
+
"styled-components-a11y/tabindex-no-positive": "error",
|
|
1073
|
+
"typescript/await-thenable": "error",
|
|
1074
|
+
"typescript/no-array-delete": "error",
|
|
1075
|
+
"typescript/no-base-to-string": "error",
|
|
1076
|
+
"typescript/no-duplicate-type-constituents": "error",
|
|
1077
|
+
"typescript/no-empty-function": {
|
|
1078
|
+
severity: "off",
|
|
1079
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1080
|
+
},
|
|
1081
|
+
"typescript/no-explicit-any": {
|
|
1082
|
+
severity: "off",
|
|
1083
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1084
|
+
},
|
|
1085
|
+
"typescript/no-floating-promises": [
|
|
1086
|
+
"error",
|
|
1087
|
+
{
|
|
1088
|
+
ignoreVoid: true
|
|
1089
|
+
}
|
|
1090
|
+
],
|
|
1091
|
+
"typescript/no-for-in-array": "error",
|
|
1092
|
+
"typescript/no-implied-eval": "error",
|
|
1093
|
+
"typescript/no-inferrable-types": "error",
|
|
1094
|
+
"typescript/no-misused-promises": [
|
|
1095
|
+
"error",
|
|
1096
|
+
{
|
|
1097
|
+
checksVoidReturn: {
|
|
1098
|
+
attributes: false
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
],
|
|
1102
|
+
"typescript/no-non-null-assertion": {
|
|
1103
|
+
severity: "warn",
|
|
1104
|
+
reason: "kept at the severity the proof of concept measured; no module config set it, so raising it to error would be a NEW enforcement decision rather than a like-for-like migration, and it needs its own count of violations first"
|
|
1105
|
+
},
|
|
1106
|
+
"typescript/no-redundant-type-constituents": "error",
|
|
1107
|
+
"typescript/no-require-imports": "error",
|
|
1108
|
+
"typescript/no-unnecessary-type-assertion": {
|
|
1109
|
+
severity: "off",
|
|
1110
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1111
|
+
},
|
|
1112
|
+
"typescript/no-unsafe-argument": {
|
|
1113
|
+
severity: "off",
|
|
1114
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1115
|
+
},
|
|
1116
|
+
"typescript/no-unsafe-assignment": {
|
|
1117
|
+
severity: "off",
|
|
1118
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1119
|
+
},
|
|
1120
|
+
"typescript/no-unsafe-call": {
|
|
1121
|
+
severity: "off",
|
|
1122
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1123
|
+
},
|
|
1124
|
+
"typescript/no-unsafe-enum-comparison": {
|
|
1125
|
+
severity: "off",
|
|
1126
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1127
|
+
},
|
|
1128
|
+
"typescript/no-unsafe-member-access": {
|
|
1129
|
+
severity: "off",
|
|
1130
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1131
|
+
},
|
|
1132
|
+
"typescript/no-unsafe-return": {
|
|
1133
|
+
severity: "off",
|
|
1134
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1135
|
+
},
|
|
1136
|
+
"typescript/no-unsafe-unary-minus": "error",
|
|
1137
|
+
"typescript/no-unused-vars": {
|
|
1138
|
+
severity: "off",
|
|
1139
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1140
|
+
},
|
|
1141
|
+
"typescript/only-throw-error": {
|
|
1142
|
+
severity: "off",
|
|
1143
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1144
|
+
},
|
|
1145
|
+
"typescript/prefer-promise-reject-errors": "error",
|
|
1146
|
+
"typescript/require-await": {
|
|
1147
|
+
severity: "off",
|
|
1148
|
+
reason: "pre-existing debt measured during the proof of concept; parked so adoption stays non-breaking"
|
|
1149
|
+
},
|
|
1150
|
+
"typescript/restrict-plus-operands": "error",
|
|
1151
|
+
"typescript/restrict-template-expressions": "error",
|
|
1152
|
+
"typescript/return-await": {
|
|
1153
|
+
severity: "warn",
|
|
1154
|
+
reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. The `never` option matches what no-return-await meant, so this is like-for-like, but it now runs over every file and finds real violations (9 on bff-admin, 5 on host-admin, measured with --type-aware). Held at warn so adoption does not turn a green module red, and raised to error once they are fixed. Switching to the stricter `in-try-catch`, which typescript-eslint recommends because `return await` inside try IS needed for correct error handling, is a separate decision with its own count.",
|
|
1155
|
+
options: ["never"]
|
|
1156
|
+
},
|
|
1157
|
+
"typescript/switch-exhaustiveness-check": {
|
|
1158
|
+
severity: "warn",
|
|
1159
|
+
reason: "kept at the severity the proof of concept measured; no module config set it, so raising it to error would be a NEW enforcement decision rather than a like-for-like migration, and it needs its own count of violations first"
|
|
1160
|
+
},
|
|
1161
|
+
"typescript/unbound-method": "error",
|
|
1162
|
+
"use-isnan": [
|
|
1163
|
+
"error",
|
|
1164
|
+
{
|
|
1165
|
+
enforceForIndexOf: false,
|
|
1166
|
+
enforceForSwitchCase: true
|
|
1167
|
+
}
|
|
1168
|
+
],
|
|
1169
|
+
"valid-typeof": [
|
|
1170
|
+
"error",
|
|
1171
|
+
{
|
|
1172
|
+
requireStringLiterals: false
|
|
1173
|
+
}
|
|
1174
|
+
]
|
|
1175
|
+
},
|
|
1176
|
+
uncovered: {
|
|
1177
|
+
"no-octal": "no oxlint equivalent",
|
|
1178
|
+
semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
// src/roles/lint/presets/svelte.json
|
|
1183
|
+
var svelte_default = {
|
|
1184
|
+
plugins: ["typescript", "oxc"],
|
|
1185
|
+
jsPlugins: [],
|
|
1186
|
+
settings: null,
|
|
1187
|
+
rules: {
|
|
1188
|
+
"constructor-super": "error",
|
|
1189
|
+
"for-direction": "error",
|
|
1190
|
+
"getter-return": [
|
|
1191
|
+
"error",
|
|
1192
|
+
{
|
|
1193
|
+
allowImplicit: false
|
|
1194
|
+
}
|
|
1195
|
+
],
|
|
1196
|
+
"no-async-promise-executor": "error",
|
|
1197
|
+
"no-case-declarations": "error",
|
|
1198
|
+
"no-class-assign": "error",
|
|
1199
|
+
"no-compare-neg-zero": "error",
|
|
1200
|
+
"no-cond-assign": ["error", "except-parens"],
|
|
1201
|
+
"no-const-assign": "error",
|
|
1202
|
+
"no-constant-binary-expression": "error",
|
|
1203
|
+
"no-constant-condition": [
|
|
1204
|
+
"error",
|
|
1205
|
+
{
|
|
1206
|
+
checkLoops: "allExceptWhileTrue"
|
|
1207
|
+
}
|
|
1208
|
+
],
|
|
1209
|
+
"no-control-regex": "error",
|
|
1210
|
+
"no-debugger": "error",
|
|
1211
|
+
"no-delete-var": "error",
|
|
1212
|
+
"no-dupe-class-members": "error",
|
|
1213
|
+
"no-dupe-else-if": "error",
|
|
1214
|
+
"no-dupe-keys": "error",
|
|
1215
|
+
"no-duplicate-case": "error",
|
|
1216
|
+
"no-empty": [
|
|
1217
|
+
"error",
|
|
1218
|
+
{
|
|
1219
|
+
allowEmptyCatch: false
|
|
1220
|
+
}
|
|
1221
|
+
],
|
|
1222
|
+
"no-empty-character-class": "error",
|
|
1223
|
+
"no-empty-pattern": [
|
|
1224
|
+
"error",
|
|
1225
|
+
{
|
|
1226
|
+
allowObjectPatternsAsParameters: false
|
|
1227
|
+
}
|
|
1228
|
+
],
|
|
1229
|
+
"no-empty-static-block": "error",
|
|
1230
|
+
"no-ex-assign": "error",
|
|
1231
|
+
"no-extra-boolean-cast": ["error", {}],
|
|
1232
|
+
"no-fallthrough": [
|
|
1233
|
+
"error",
|
|
1234
|
+
{
|
|
1235
|
+
allowEmptyCase: false,
|
|
1236
|
+
reportUnusedFallthroughComment: false
|
|
1237
|
+
}
|
|
1238
|
+
],
|
|
1239
|
+
"no-func-assign": "error",
|
|
1240
|
+
"no-global-assign": [
|
|
1241
|
+
"error",
|
|
1242
|
+
{
|
|
1243
|
+
exceptions: []
|
|
1244
|
+
}
|
|
1245
|
+
],
|
|
1246
|
+
"no-import-assign": "error",
|
|
1247
|
+
"no-invalid-regexp": ["error", {}],
|
|
1248
|
+
"no-irregular-whitespace": [
|
|
1249
|
+
"error",
|
|
1250
|
+
{
|
|
1251
|
+
skipComments: false,
|
|
1252
|
+
skipJSXText: false,
|
|
1253
|
+
skipRegExps: false,
|
|
1254
|
+
skipStrings: true,
|
|
1255
|
+
skipTemplates: false
|
|
1256
|
+
}
|
|
1257
|
+
],
|
|
1258
|
+
"no-loss-of-precision": "error",
|
|
1259
|
+
"no-misleading-character-class": "error",
|
|
1260
|
+
"no-new-native-nonconstructor": "error",
|
|
1261
|
+
"no-nonoctal-decimal-escape": "error",
|
|
1262
|
+
"no-obj-calls": "error",
|
|
1263
|
+
"no-prototype-builtins": "error",
|
|
1264
|
+
"no-redeclare": [
|
|
1265
|
+
"error",
|
|
1266
|
+
{
|
|
1267
|
+
builtinGlobals: true
|
|
1268
|
+
}
|
|
1269
|
+
],
|
|
1270
|
+
"no-regex-spaces": "error",
|
|
1271
|
+
"no-self-assign": [
|
|
1272
|
+
"error",
|
|
1273
|
+
{
|
|
1274
|
+
props: true
|
|
1275
|
+
}
|
|
1276
|
+
],
|
|
1277
|
+
"no-setter-return": "error",
|
|
1278
|
+
"no-shadow-restricted-names": [
|
|
1279
|
+
"error",
|
|
1280
|
+
{
|
|
1281
|
+
reportGlobalThis: false
|
|
1282
|
+
}
|
|
1283
|
+
],
|
|
1284
|
+
"no-sparse-arrays": "error",
|
|
1285
|
+
"no-this-before-super": "error",
|
|
1286
|
+
"no-undef": {
|
|
1287
|
+
severity: "off",
|
|
1288
|
+
reason: "unusable under oxlint for Svelte, and not for want of configuration. Svelte 5's runes ($state, $props, $derived, $effect) are compiler globals, and `$store` auto-subscription invents an identifier per store ($_, $page, $currentAdmin, \u2026), so the names cannot be enumerated. eslint-plugin-svelte's PARSER supplies both; oxlint has no Svelte parser, so it reported 1641 undefined identifiers on hr-management, none of them real. TypeScript and svelte-check already report genuinely undefined identifiers, which is why react and nest never enabled this rule either."
|
|
1289
|
+
},
|
|
1290
|
+
"no-unreachable": "error",
|
|
1291
|
+
"no-unsafe-finally": "error",
|
|
1292
|
+
"no-unsafe-negation": [
|
|
1293
|
+
"error",
|
|
1294
|
+
{
|
|
1295
|
+
enforceForOrderingRelations: false
|
|
1296
|
+
}
|
|
1297
|
+
],
|
|
1298
|
+
"no-unsafe-optional-chaining": [
|
|
1299
|
+
"error",
|
|
1300
|
+
{
|
|
1301
|
+
disallowArithmeticOperators: false
|
|
1302
|
+
}
|
|
1303
|
+
],
|
|
1304
|
+
"no-unused-labels": "error",
|
|
1305
|
+
"no-unused-private-class-members": "error",
|
|
1306
|
+
"no-useless-backreference": "error",
|
|
1307
|
+
"no-useless-catch": "error",
|
|
1308
|
+
"no-useless-escape": [
|
|
1309
|
+
"error",
|
|
1310
|
+
{
|
|
1311
|
+
allowRegexCharacters: []
|
|
1312
|
+
}
|
|
1313
|
+
],
|
|
1314
|
+
"no-with": "error",
|
|
1315
|
+
"prefer-rest-params": {
|
|
1316
|
+
severity: "warn",
|
|
1317
|
+
reason: "error in the module's eslint config, but never actually reported: `eslint src/lib/analytics/useAnalytics.js` exits clean today while oxlint finds 3 real violations in it. Held at warn so adoption does not turn a green module red, and restored to error once they are fixed. Not auto-fixable, verified with --fix."
|
|
1318
|
+
},
|
|
1319
|
+
"prefer-spread": {
|
|
1320
|
+
severity: "warn",
|
|
1321
|
+
reason: "error in the module's eslint config, but never actually reported: `eslint src/lib/analytics/useAnalytics.js` exits clean today while oxlint finds 3 real violations in it. Held at warn so adoption does not turn a green module red, and restored to error once they are fixed. Not auto-fixable, verified with --fix."
|
|
1322
|
+
},
|
|
1323
|
+
"require-yield": "error",
|
|
1324
|
+
"typescript/no-explicit-any": {
|
|
1325
|
+
severity: "warn",
|
|
1326
|
+
reason: "libs/front/ui enforces it and apps/front/hr-management turns it off; enforcing it would newly fail hr-management, so it reports for both instead of breaking one"
|
|
1327
|
+
},
|
|
1328
|
+
"typescript/no-require-imports": "error",
|
|
1329
|
+
"typescript/no-unused-vars": {
|
|
1330
|
+
severity: "warn",
|
|
1331
|
+
reason: "the two modules disagree: libs/front/ui runs it at error with no ignore patterns, apps/front/hr-management at warn with the ^_ patterns kept here (both read from eslint --print-config). Holding it at error would newly fail hr-management, so it reports for both and keeps the patterns hr-management already relies on",
|
|
1332
|
+
options: [
|
|
1333
|
+
{
|
|
1334
|
+
varsIgnorePattern: "^_",
|
|
1335
|
+
argsIgnorePattern: "^_",
|
|
1336
|
+
destructuredArrayIgnorePattern: "^_"
|
|
1337
|
+
}
|
|
1338
|
+
]
|
|
1339
|
+
},
|
|
1340
|
+
"use-isnan": [
|
|
1341
|
+
"error",
|
|
1342
|
+
{
|
|
1343
|
+
enforceForIndexOf: false,
|
|
1344
|
+
enforceForSwitchCase: true
|
|
1345
|
+
}
|
|
1346
|
+
],
|
|
1347
|
+
"valid-typeof": [
|
|
1348
|
+
"error",
|
|
1349
|
+
{
|
|
1350
|
+
requireStringLiterals: false
|
|
1351
|
+
}
|
|
1352
|
+
]
|
|
1353
|
+
},
|
|
1354
|
+
uncovered: {
|
|
1355
|
+
"svelte/comment-directive": "oxlint cannot parse Svelte templates, so the eslint-disable comments inside markup are neither read nor honoured",
|
|
1356
|
+
"svelte/system": "the plugin internal rule that reports Svelte parse errors; it needs the Svelte parser",
|
|
1357
|
+
"svelte/no-at-debug-tags": "reads {@debug} in the template",
|
|
1358
|
+
"svelte/no-at-html-tags": "reads {@html} in the template, the XSS guard; the highest-value loss in this list",
|
|
1359
|
+
"svelte/no-dupe-else-if-blocks": "reads {#if}/{:else if} chains in the template",
|
|
1360
|
+
"svelte/no-dupe-style-properties": "reads the style block",
|
|
1361
|
+
"svelte/no-dynamic-slot-name": "reads <slot> in the template",
|
|
1362
|
+
"svelte/no-inner-declarations": "the Svelte-aware variant; the core rule it shadows is not a substitute inside markup",
|
|
1363
|
+
"svelte/no-not-function-handler": "reads on:event handlers in the template",
|
|
1364
|
+
"svelte/no-object-in-text-mustaches": "reads {expression} interpolation in the template",
|
|
1365
|
+
"svelte/no-shorthand-style-property-overrides": "reads the style block",
|
|
1366
|
+
"svelte/no-unknown-style-directive-property": "reads style: directives in the template",
|
|
1367
|
+
"svelte/no-unused-svelte-ignore": "reads svelte-ignore comments in the template",
|
|
1368
|
+
"svelte/valid-compile": "runs the Svelte compiler and reports its warnings",
|
|
1369
|
+
"no-dupe-args": "no oxlint equivalent (TypeScript rejects duplicate parameter names anyway)",
|
|
1370
|
+
"no-octal": "no oxlint equivalent"
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
// src/roles/lint/preset-data.ts
|
|
1375
|
+
var BASE_RULES = base_default.rules;
|
|
1376
|
+
var PRESET_FILES = {
|
|
1377
|
+
nest: nest_default,
|
|
1378
|
+
node: node_default,
|
|
1379
|
+
react: react_default,
|
|
1380
|
+
svelte: svelte_default
|
|
1381
|
+
};
|
|
1382
|
+
var LINT_PRESETS = Object.keys(PRESET_FILES);
|
|
1383
|
+
function hasLintPreset(preset) {
|
|
1384
|
+
return preset in PRESET_FILES;
|
|
1385
|
+
}
|
|
1386
|
+
function presetFile(preset) {
|
|
1387
|
+
return PRESET_FILES[preset] ?? node_default;
|
|
1388
|
+
}
|
|
1389
|
+
function baseRules() {
|
|
1390
|
+
return BASE_RULES;
|
|
1391
|
+
}
|
|
1392
|
+
function presetRules(preset) {
|
|
1393
|
+
return presetFile(preset).rules;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/roles/lint/rule-data.ts
|
|
1397
|
+
var baseRules2 = baseRules();
|
|
1398
|
+
function isDecision(value) {
|
|
1399
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1400
|
+
}
|
|
1401
|
+
function severityOf(value) {
|
|
1402
|
+
if (isDecision(value)) return value.severity;
|
|
1403
|
+
if (Array.isArray(value)) return String(value[0]);
|
|
1404
|
+
return value;
|
|
1405
|
+
}
|
|
1406
|
+
function validate(preset, rules) {
|
|
1407
|
+
for (const [rule, value] of Object.entries(rules)) {
|
|
1408
|
+
const severity = severityOf(value);
|
|
1409
|
+
if (!["off", "warn", "error"].includes(severity)) {
|
|
1410
|
+
throw new Error(
|
|
1411
|
+
`${preset}: rule "${rule}" has severity "${severity}"; expected off, warn or error`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
if (severity === "error") continue;
|
|
1415
|
+
const reason = isDecision(value) ? value.reason.trim() : "";
|
|
1416
|
+
if (reason === "") {
|
|
1417
|
+
throw new Error(
|
|
1418
|
+
`${preset}: rule "${rule}" is set to "${severity}" without a reason. Every rule that is not enforced must say why, so --inspect can tell a developer what is not running. Use { "severity": "${severity}", "reason": "..." }.`
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function layered(preset) {
|
|
1424
|
+
const base = baseRules();
|
|
1425
|
+
const layer = presetRules(preset);
|
|
1426
|
+
validate("base", base);
|
|
1427
|
+
validate(preset, layer);
|
|
1428
|
+
return { ...base, ...layer };
|
|
1429
|
+
}
|
|
1430
|
+
function rulesFor(preset) {
|
|
1431
|
+
const entries = Object.entries(layered(preset)).map(([rule, value]) => {
|
|
1432
|
+
if (!isDecision(value)) return [rule, value];
|
|
1433
|
+
const options = value.options ?? [];
|
|
1434
|
+
return [rule, options.length > 0 ? [value.severity, ...options] : value.severity];
|
|
1435
|
+
});
|
|
1436
|
+
return Object.fromEntries(entries);
|
|
1437
|
+
}
|
|
1438
|
+
function disabledFor(preset) {
|
|
1439
|
+
return decisionsAt(preset, "off");
|
|
1440
|
+
}
|
|
1441
|
+
function downgradedFor(preset) {
|
|
1442
|
+
return decisionsAt(preset, "warn");
|
|
1443
|
+
}
|
|
1444
|
+
function decisionsAt(preset, severity) {
|
|
1445
|
+
return Object.entries(layered(preset)).filter(([, value]) => isDecision(value) && value.severity === severity).map(([rule, value]) => ({ rule, reason: value.reason })).sort((left, right) => left.rule.localeCompare(right.rule));
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/roles/lint/disabled-rules.ts
|
|
1449
|
+
function disabledRulesFor(preset) {
|
|
1450
|
+
return disabledFor(preset);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/roles/lint/downgraded-rules.ts
|
|
1454
|
+
function downgradedRulesFor(preset) {
|
|
1455
|
+
return downgradedFor(preset);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/roles/lint/presets.ts
|
|
1459
|
+
function lintPresetFor(preset) {
|
|
1460
|
+
const file = presetFile(preset);
|
|
1461
|
+
const resolved = LINT_PRESETS.includes(preset) ? preset : "node";
|
|
1462
|
+
return {
|
|
1463
|
+
plugins: file.plugins,
|
|
1464
|
+
jsPlugins: file.jsPlugins,
|
|
1465
|
+
...file.settings === null ? {} : { settings: file.settings },
|
|
1466
|
+
rules: rulesFor(resolved)
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/roles/lint/rename-suppressions.ts
|
|
1471
|
+
import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
1472
|
+
import { join as join2, relative } from "path";
|
|
1473
|
+
var SOURCE_EXTENSIONS = [
|
|
1474
|
+
".ts",
|
|
1475
|
+
".tsx",
|
|
1476
|
+
".js",
|
|
1477
|
+
".jsx",
|
|
1478
|
+
".mjs",
|
|
1479
|
+
".cjs",
|
|
1480
|
+
".mts",
|
|
1481
|
+
".cts",
|
|
1482
|
+
".svelte",
|
|
1483
|
+
".vue"
|
|
1484
|
+
];
|
|
1485
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage", ".git", ".nx"]);
|
|
1486
|
+
var SUPPRESSION = /(eslint-disable(?:-next-line|-line)?)\s+([^\n*]+?)(?:\s*--.*)?(?:\s*\*\/)?\s*$/;
|
|
1487
|
+
function renamedPrefixes(jsPlugins, declared = {}) {
|
|
1488
|
+
const renames = /* @__PURE__ */ new Map();
|
|
1489
|
+
for (const plugin of jsPlugins) {
|
|
1490
|
+
if (typeof plugin === "string") continue;
|
|
1491
|
+
const original = plugin.specifier.replace(/^eslint-plugin-/, "").replace(/\/eslint-plugin$/, "");
|
|
1492
|
+
if (original !== plugin.name) renames.set(`${original}/`, `${plugin.name}/`);
|
|
1493
|
+
}
|
|
1494
|
+
const slash = (value) => value.endsWith("/") ? value : `${value}/`;
|
|
1495
|
+
for (const [from, to] of Object.entries(declared)) renames.set(slash(from), slash(to));
|
|
1496
|
+
return collapseChains(renames);
|
|
1497
|
+
}
|
|
1498
|
+
function collapseChains(renames) {
|
|
1499
|
+
const collapsed = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const [from, to] of renames) {
|
|
1501
|
+
let target = to;
|
|
1502
|
+
for (let hop = 0; hop < renames.size; hop += 1) {
|
|
1503
|
+
const next = renames.get(target);
|
|
1504
|
+
if (next === void 0 || next === target) break;
|
|
1505
|
+
target = next;
|
|
1506
|
+
}
|
|
1507
|
+
collapsed.set(from, target);
|
|
1508
|
+
}
|
|
1509
|
+
return collapsed;
|
|
1510
|
+
}
|
|
1511
|
+
function* sourceFiles(dir) {
|
|
1512
|
+
let entries;
|
|
1513
|
+
try {
|
|
1514
|
+
entries = readdirSync(dir);
|
|
1515
|
+
} catch {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
for (const entry of entries) {
|
|
1519
|
+
const full = join2(dir, entry);
|
|
1520
|
+
let isDirectory;
|
|
1521
|
+
try {
|
|
1522
|
+
isDirectory = statSync(full).isDirectory();
|
|
1523
|
+
} catch {
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
if (isDirectory) {
|
|
1527
|
+
if (!SKIP_DIRECTORIES.has(entry)) yield* sourceFiles(full);
|
|
1528
|
+
} else if (SOURCE_EXTENSIONS.some((extension) => entry.endsWith(extension))) {
|
|
1529
|
+
yield full;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
function findSuppressionRenames(cwd, renames) {
|
|
1534
|
+
if (renames.size === 0) return [];
|
|
1535
|
+
const found = [];
|
|
1536
|
+
for (const file of sourceFiles(cwd)) {
|
|
1537
|
+
let content;
|
|
1538
|
+
try {
|
|
1539
|
+
content = readFileSync2(file, "utf8");
|
|
1540
|
+
} catch {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
if (!content.includes("eslint-disable")) continue;
|
|
1544
|
+
content.split("\n").forEach((line, index) => {
|
|
1545
|
+
const match = SUPPRESSION.exec(line);
|
|
1546
|
+
const rules = match?.[2];
|
|
1547
|
+
if (!rules) return;
|
|
1548
|
+
const names = rules.split(",").map((entry) => entry.trim());
|
|
1549
|
+
const renamed = [];
|
|
1550
|
+
const hits = [];
|
|
1551
|
+
for (const name of names) {
|
|
1552
|
+
const match2 = [...renames].find(([oldPrefix2]) => name.startsWith(oldPrefix2));
|
|
1553
|
+
if (!match2) {
|
|
1554
|
+
renamed.push(name);
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
const [oldPrefix, newPrefix] = match2;
|
|
1558
|
+
hits.push(name);
|
|
1559
|
+
renamed.push(newPrefix + name.slice(oldPrefix.length));
|
|
1560
|
+
}
|
|
1561
|
+
if (hits.length === 0) return;
|
|
1562
|
+
found.push({
|
|
1563
|
+
file: relative(cwd, file),
|
|
1564
|
+
line: index + 1,
|
|
1565
|
+
from: line,
|
|
1566
|
+
to: line.replace(rules, renamed.join(", ")),
|
|
1567
|
+
rule: hits.join(", ")
|
|
1568
|
+
});
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
return found;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// src/roles/lint/lint-script.ts
|
|
1575
|
+
var SENTINEL_LINT_COMMAND = "sentinel --run --lint";
|
|
1576
|
+
var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
|
|
1577
|
+
function isSeparatorAt(index) {
|
|
1578
|
+
return index % 2 === 1;
|
|
1579
|
+
}
|
|
1580
|
+
function isEslintSegment(segment) {
|
|
1581
|
+
return /(^|[\s/])eslint(\s|$)/.test(segment.trim());
|
|
1582
|
+
}
|
|
1583
|
+
function composeLintScript(existing) {
|
|
1584
|
+
if (!existing || existing.trim() === "") return SENTINEL_LINT_COMMAND;
|
|
1585
|
+
const parts = existing.split(SEGMENT_SEPARATOR);
|
|
1586
|
+
const commands = parts.filter((_, index) => !isSeparatorAt(index));
|
|
1587
|
+
if (!commands.some(isEslintSegment)) return existing;
|
|
1588
|
+
let replacedOnce = false;
|
|
1589
|
+
const rebuilt = parts.map((part, index) => {
|
|
1590
|
+
if (isSeparatorAt(index) || !isEslintSegment(part)) return part;
|
|
1591
|
+
if (replacedOnce) return null;
|
|
1592
|
+
replacedOnce = true;
|
|
1593
|
+
return SENTINEL_LINT_COMMAND;
|
|
1594
|
+
});
|
|
1595
|
+
const kept = [];
|
|
1596
|
+
for (let index = 0; index < rebuilt.length; index += 1) {
|
|
1597
|
+
const part = rebuilt[index];
|
|
1598
|
+
if (part === null) {
|
|
1599
|
+
if (kept.length > 0) kept.pop();
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
kept.push(part);
|
|
1603
|
+
}
|
|
1604
|
+
return kept.join("").trim();
|
|
1605
|
+
}
|
|
1606
|
+
function keepsOtherCommands(script) {
|
|
1607
|
+
return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== SENTINEL_LINT_COMMAND);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/roles/lint/eslint-ignores.ts
|
|
1611
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
1612
|
+
import { join as join3 } from "path";
|
|
1613
|
+
var IGNORE_BLOCKS = [/\bignores\s*:\s*\[([^\]]*)\]/g, /\bglobalIgnores\s*\(\s*\[([^\]]*)\]/g];
|
|
1614
|
+
var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
|
|
1615
|
+
var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
|
|
1616
|
+
function toOxlintPattern(pattern) {
|
|
1617
|
+
return pattern.replace(/^\.\//, "");
|
|
1618
|
+
}
|
|
1619
|
+
function readEslintIgnores(cwd) {
|
|
1620
|
+
const config = ESLINT_CONFIG_FILES.map((name) => join3(cwd, name)).find((path) => existsSync2(path));
|
|
1621
|
+
if (!config) return { patterns: [], unresolved: [] };
|
|
1622
|
+
let source;
|
|
1623
|
+
try {
|
|
1624
|
+
source = readFileSync3(config, "utf8");
|
|
1625
|
+
} catch {
|
|
1626
|
+
return { patterns: [], unresolved: [] };
|
|
1627
|
+
}
|
|
1628
|
+
const patterns = /* @__PURE__ */ new Set();
|
|
1629
|
+
for (const block of IGNORE_BLOCKS) {
|
|
1630
|
+
for (const [, body] of source.matchAll(block)) {
|
|
1631
|
+
for (const [, literal] of (body ?? "").matchAll(STRING_LITERAL)) {
|
|
1632
|
+
if (literal) patterns.add(toOxlintPattern(literal));
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
const unresolved = [...source.matchAll(OPAQUE_SOURCE)].map((match) => match[0]);
|
|
1637
|
+
return { patterns: [...patterns], unresolved: [...new Set(unresolved)] };
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// src/core/config/read-adoption.ts
|
|
1641
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
1642
|
+
import { join as join5 } from "path";
|
|
1643
|
+
|
|
1644
|
+
// src/shared/jsonc.ts
|
|
1645
|
+
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
1646
|
+
function parseJsonc(text, source = "config") {
|
|
1647
|
+
const errors = [];
|
|
1648
|
+
const value = parse(text, errors, { allowTrailingComma: true });
|
|
1649
|
+
if (errors.length > 0) {
|
|
1650
|
+
const details = errors.map((error) => printParseErrorCode(error.error)).join(", ");
|
|
1651
|
+
throw new Error(`${source}: malformed JSONC (${details}).`);
|
|
1652
|
+
}
|
|
1653
|
+
return value;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/core/config/owned-keys.ts
|
|
1657
|
+
function presetOwnedKeys(config, permitted) {
|
|
1658
|
+
if (!config) return [];
|
|
1659
|
+
return Object.keys(config).filter((key) => !permitted.includes(key));
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// src/core/config/resolve-config-target.ts
|
|
1663
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
1664
|
+
import { join as join4 } from "path";
|
|
1665
|
+
function readExtends(absolutePath) {
|
|
1666
|
+
let parsed;
|
|
1667
|
+
try {
|
|
1668
|
+
parsed = parseJsonc(readFileSync4(absolutePath, "utf8"), absolutePath);
|
|
1669
|
+
} catch {
|
|
1670
|
+
return [];
|
|
1671
|
+
}
|
|
1672
|
+
if (typeof parsed.extends === "string") return [parsed.extends];
|
|
1673
|
+
if (Array.isArray(parsed.extends)) {
|
|
1674
|
+
return parsed.extends.filter((entry) => typeof entry === "string");
|
|
1675
|
+
}
|
|
1676
|
+
return [];
|
|
1677
|
+
}
|
|
1678
|
+
function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
|
|
1679
|
+
let existing;
|
|
1680
|
+
let existingExtendsSomething = false;
|
|
1681
|
+
for (const candidate of candidates) {
|
|
1682
|
+
const absolutePath = join4(moduleDir, candidate);
|
|
1683
|
+
if (!existsSync3(absolutePath)) continue;
|
|
1684
|
+
const chain = readExtends(absolutePath);
|
|
1685
|
+
if (existing === void 0) {
|
|
1686
|
+
existing = candidate;
|
|
1687
|
+
existingExtendsSomething = chain.length > 0;
|
|
1688
|
+
}
|
|
1689
|
+
const extendsBase = chain.some((value) => markers.some((marker) => value.includes(marker)));
|
|
1690
|
+
if (extendsBase) return { path: candidate, reason: "extends-base" };
|
|
1691
|
+
}
|
|
1692
|
+
if (existing) {
|
|
1693
|
+
return { path: existing, reason: existingExtendsSomething ? "other-chain" : "no-extends" };
|
|
1694
|
+
}
|
|
1695
|
+
return { path: fallback, reason: "none" };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/core/config/read-adoption.ts
|
|
1699
|
+
function normaliseExtends(value) {
|
|
1700
|
+
if (typeof value === "string") return [value];
|
|
1701
|
+
if (Array.isArray(value)) {
|
|
1702
|
+
return value.filter((entry) => typeof entry === "string");
|
|
1703
|
+
}
|
|
1704
|
+
return [];
|
|
1705
|
+
}
|
|
1706
|
+
var NOT_ADOPTED = (configFile) => ({
|
|
1707
|
+
configFile,
|
|
1708
|
+
preset: null,
|
|
1709
|
+
adopted: false,
|
|
1710
|
+
conformant: false,
|
|
1711
|
+
drift: []
|
|
1712
|
+
});
|
|
1713
|
+
function readAdoption(cwd, options) {
|
|
1714
|
+
const target = resolveConfigTarget(cwd, options);
|
|
1715
|
+
if (target.reason === "none" || !existsSync4(join5(cwd, target.path))) {
|
|
1716
|
+
return NOT_ADOPTED(target.reason === "none" ? null : target.path);
|
|
1717
|
+
}
|
|
1718
|
+
let parsed;
|
|
1719
|
+
try {
|
|
1720
|
+
parsed = parseJsonc(readFileSync5(join5(cwd, target.path), "utf8"), target.path);
|
|
1721
|
+
} catch {
|
|
1722
|
+
return NOT_ADOPTED(target.path);
|
|
1723
|
+
}
|
|
1724
|
+
const preset = normaliseExtends(parsed.extends).find((entry) => options.presetPattern.test(entry)) ?? null;
|
|
1725
|
+
if (preset === null) return NOT_ADOPTED(target.path);
|
|
1726
|
+
const settings = options.settingsKey === null ? parsed : parsed[options.settingsKey];
|
|
1727
|
+
const drift = presetOwnedKeys(settings, options.permitted);
|
|
1728
|
+
return { configFile: target.path, preset, adopted: true, conformant: drift.length === 0, drift };
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/roles/lint/read-adoption.ts
|
|
1732
|
+
function readLintAdoption(cwd) {
|
|
1733
|
+
return readAdoption(cwd, {
|
|
1734
|
+
candidates: [LINT_CONFIG_FILE],
|
|
1735
|
+
markers: ["@hublo/sentinel/oxlint/"],
|
|
1736
|
+
fallback: LINT_CONFIG_FILE,
|
|
1737
|
+
presetPattern: SENTINEL_LINT_PRESET,
|
|
1738
|
+
// Drift for lint is any TOP-LEVEL key beyond `extends`, so the settings the shared
|
|
1739
|
+
// reader inspects are the config's own root rather than a nested options object.
|
|
1740
|
+
settingsKey: null,
|
|
1741
|
+
permitted: PERMITTED_LOCAL_KEYS
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/roles/lint/render-config.ts
|
|
1746
|
+
var DEFAULT_IGNORE_PATTERNS = [
|
|
1747
|
+
"**/dist/**",
|
|
1748
|
+
"**/node_modules/**",
|
|
1749
|
+
"**/coverage/**",
|
|
1750
|
+
"**/*.mock.ts"
|
|
1751
|
+
];
|
|
1752
|
+
|
|
1753
|
+
// src/roles/lint/resolve-oxlint.ts
|
|
1754
|
+
import { createRequire } from "module";
|
|
1755
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1756
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1757
|
+
import { delimiter, dirname as dirname3, join as join7 } from "path";
|
|
1758
|
+
|
|
1759
|
+
// src/shared/resolve-bin.ts
|
|
1760
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1761
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
1762
|
+
function resolveBin(fromDir, name) {
|
|
1763
|
+
let dir = fromDir;
|
|
1764
|
+
for (; ; ) {
|
|
1765
|
+
const candidate = join6(dir, "node_modules", ".bin", name);
|
|
1766
|
+
if (existsSync5(candidate)) return candidate;
|
|
1767
|
+
const parent = dirname2(dir);
|
|
1768
|
+
if (parent === dir) return void 0;
|
|
1769
|
+
dir = parent;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// src/roles/lint/resolve-oxlint.ts
|
|
1774
|
+
var PACKAGE_OF = {
|
|
1775
|
+
oxlint: "oxlint",
|
|
1776
|
+
tsgolint: "oxlint-tsgolint"
|
|
1777
|
+
};
|
|
1778
|
+
var require2 = createRequire(import.meta.url);
|
|
1779
|
+
function fromOwnInstall(name) {
|
|
1780
|
+
try {
|
|
1781
|
+
const packageName = PACKAGE_OF[name];
|
|
1782
|
+
const manifest = require2.resolve(`${packageName}/package.json`);
|
|
1783
|
+
const bin = require2(manifest).bin;
|
|
1784
|
+
const relative2 = typeof bin === "string" ? bin : bin?.[name];
|
|
1785
|
+
if (!relative2) return void 0;
|
|
1786
|
+
const executable = join7(dirname3(manifest), relative2);
|
|
1787
|
+
return existsSync6(executable) ? executable : void 0;
|
|
1788
|
+
} catch {
|
|
1789
|
+
return void 0;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function resolveOxlint(cwd, name = "oxlint") {
|
|
1793
|
+
return resolveBin(cwd, name) ?? fromOwnInstall(name);
|
|
1794
|
+
}
|
|
1795
|
+
function canRunTypeAware(cwd) {
|
|
1796
|
+
return tsgolintShim(cwd) !== void 0;
|
|
1797
|
+
}
|
|
1798
|
+
function oxlintPath(cwd, env) {
|
|
1799
|
+
const shim = tsgolintShim(cwd);
|
|
1800
|
+
if (!shim) return env.PATH;
|
|
1801
|
+
return [dirname3(shim), env.PATH].filter(Boolean).join(delimiter);
|
|
1802
|
+
}
|
|
1803
|
+
function tsgolintShim(cwd) {
|
|
1804
|
+
const fromModule = resolveBin(cwd, "tsgolint");
|
|
1805
|
+
if (fromModule) return fromModule;
|
|
1806
|
+
const candidates = [];
|
|
1807
|
+
for (const owner of ["oxlint-tsgolint", "oxlint"]) {
|
|
1808
|
+
try {
|
|
1809
|
+
const packageDir = dirname3(require2.resolve(`${owner}/package.json`));
|
|
1810
|
+
candidates.push(join7(packageDir, "node_modules", ".bin", "tsgolint"));
|
|
1811
|
+
candidates.push(join7(packageDir, "..", ".bin", "tsgolint"));
|
|
1812
|
+
} catch {
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
try {
|
|
1816
|
+
const ownRoot = dirname3(require2.resolve("@hublo/sentinel/package.json"));
|
|
1817
|
+
candidates.push(join7(ownRoot, "node_modules", ".bin", "tsgolint"));
|
|
1818
|
+
} catch {
|
|
1819
|
+
candidates.push(resolveBin(dirname3(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
|
|
1820
|
+
}
|
|
1821
|
+
return candidates.find((candidate) => candidate !== "" && existsSync6(candidate));
|
|
1822
|
+
}
|
|
1823
|
+
function oxlintSearchPath(cwd) {
|
|
1824
|
+
return [`${cwd}${delimiter}node_modules/.bin (and parents)`, "sentinel's own install"].join(
|
|
1825
|
+
" then "
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
1830
|
+
var MAX_FIX_PASSES = 3;
|
|
1831
|
+
function summariseByPlugin(rules) {
|
|
1832
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1833
|
+
for (const name of deferredRuleNames(rules)) {
|
|
1834
|
+
const plugin = name.includes("/") ? name.split("/")[0] ?? name : "core";
|
|
1835
|
+
counts.set(plugin, (counts.get(plugin) ?? 0) + 1);
|
|
1836
|
+
}
|
|
1837
|
+
return [...counts.entries()].sort((left, right) => right[1] - left[1]).map(([plugin, count]) => `${count} ${plugin}`).join(", ");
|
|
1838
|
+
}
|
|
1839
|
+
var OxlintAdapter = class extends BaseAdapter {
|
|
1840
|
+
target = "lint";
|
|
1841
|
+
runner = "oxlint";
|
|
1842
|
+
/**
|
|
1843
|
+
* Handles every preset, deliberately. Adapter selection happens BEFORE the engine can
|
|
1844
|
+
* ask `declaredPreset`, so filtering here would make an adopted module unreachable:
|
|
1845
|
+
* detection answers `node` for a hoisted-deps monorepo, no adapter would match, and the
|
|
1846
|
+
* module would report `unsupported` while its committed config says otherwise. Whether a
|
|
1847
|
+
* preset actually ships a preset is gated inside `plan`, where it can say so clearly.
|
|
1848
|
+
*/
|
|
1849
|
+
appliesTo(_flavour) {
|
|
1850
|
+
return true;
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* The preset this module committed to, read from the config sentinel wrote. Preferred
|
|
1854
|
+
* over detection because a monorepo hoists dependencies: a module declares no `react`,
|
|
1855
|
+
* so detection honestly answers `node` while the committed config says otherwise.
|
|
1856
|
+
*/
|
|
1857
|
+
declaredPreset(cwd) {
|
|
1858
|
+
return presetNameFromPath(readLintAdoption(cwd).preset);
|
|
1859
|
+
}
|
|
1860
|
+
plan(context) {
|
|
1861
|
+
if (!hasLintPreset(context.preset)) {
|
|
1862
|
+
return {
|
|
1863
|
+
operations: [],
|
|
1864
|
+
blocked: `sentinel does not ship a lint preset named "${context.preset}" yet (shipped: ${LINT_PRESETS.join(", ")}). Nothing was written; this module cannot adopt the lint preset until that preset ships.`
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
const current = presetNameFromPath(readLintAdoption(context.cwd).preset);
|
|
1868
|
+
if (current !== void 0 && current !== context.preset) {
|
|
1869
|
+
return {
|
|
1870
|
+
operations: [],
|
|
1871
|
+
blocked: `this module is already adopted as "${current}", and --preset says "${context.preset}". Nothing was written. If the change is deliberate, delete ${LINT_CONFIG_FILE} and re-run; otherwise re-run with --preset ${current}.`
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
const disabled = disabledRulesFor(context.preset);
|
|
1875
|
+
const own = readOwnPackage();
|
|
1876
|
+
const ignores = readEslintIgnores(context.cwd);
|
|
1877
|
+
const stub = {
|
|
1878
|
+
extends: [presetPath(context.preset)],
|
|
1879
|
+
...ignores.patterns.length > 0 ? { ignorePatterns: ignores.patterns } : {}
|
|
1880
|
+
};
|
|
1881
|
+
const operations = [
|
|
1882
|
+
{ kind: "write", path: LINT_CONFIG_FILE, contents: JSON.stringify(stub, null, 2) + "\n" },
|
|
1883
|
+
this.packageJsonOperation(context.cwd)
|
|
1884
|
+
];
|
|
1885
|
+
const removableDeps = this.moduleEslintDependencies(context.cwd);
|
|
1886
|
+
if (removableDeps.length > 0) {
|
|
1887
|
+
operations.push({
|
|
1888
|
+
kind: "remove-json-keys",
|
|
1889
|
+
path: "package.json",
|
|
1890
|
+
keys: removableDeps
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync7(join8(context.cwd, name)));
|
|
1894
|
+
for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
|
|
1895
|
+
if (existsSync7(join8(context.cwd, "project.json")) && !this.hasLintTarget(context.cwd)) {
|
|
1896
|
+
operations.push({
|
|
1897
|
+
kind: "remove-json-keys",
|
|
1898
|
+
path: "project.json",
|
|
1899
|
+
keys: [["targets", "lint"]]
|
|
1900
|
+
});
|
|
1901
|
+
operations.push({
|
|
1902
|
+
kind: "merge-json",
|
|
1903
|
+
path: "project.json",
|
|
1904
|
+
value: { targets: { lint: lintTarget() } }
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
const renames = findSuppressionRenames(
|
|
1908
|
+
context.cwd,
|
|
1909
|
+
renamedPrefixes(
|
|
1910
|
+
lintPresetFor(context.preset).jsPlugins,
|
|
1911
|
+
lintPresetFor(context.preset).renames
|
|
1912
|
+
)
|
|
1913
|
+
);
|
|
1914
|
+
const deleting = new Set(eslintConfigs);
|
|
1915
|
+
const rewritten = renames.filter((entry) => !deleting.has(entry.file));
|
|
1916
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1917
|
+
for (const entry of rewritten) {
|
|
1918
|
+
const list = byFile.get(entry.file) ?? [];
|
|
1919
|
+
list.push({ from: entry.from, to: entry.to });
|
|
1920
|
+
byFile.set(entry.file, list);
|
|
1921
|
+
}
|
|
1922
|
+
for (const [file, replacements] of byFile) {
|
|
1923
|
+
operations.push({ kind: "replace-lines", path: file, replacements });
|
|
1924
|
+
}
|
|
1925
|
+
const notes = [
|
|
1926
|
+
`wrote ${LINT_CONFIG_FILE} extending ${presetPath(context.preset)} (${own.name}@${own.version})`
|
|
1927
|
+
];
|
|
1928
|
+
if (disabled.length > 0) {
|
|
1929
|
+
notes.push(
|
|
1930
|
+
`${disabled.length} rule(s) not enforced by this preset (${summariseByPlugin(disabled)}). Run --inspect --lint for each rule and why.`
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
if (eslintConfigs.length > 0) {
|
|
1934
|
+
notes.push(`removed ${eslintConfigs.join(", ")} and pointed the nx lint target at oxlint`);
|
|
1935
|
+
}
|
|
1936
|
+
if (ignores.patterns.length > 0) {
|
|
1937
|
+
notes.push(
|
|
1938
|
+
`carried ${ignores.patterns.length} ignore pattern(s) over from the eslint config (${ignores.patterns.join(", ")}): they are this module's, not the preset's`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
if (ignores.unresolved.length > 0) {
|
|
1942
|
+
notes.push(
|
|
1943
|
+
`COULD NOT read ${ignores.unresolved.join(", ")} from the eslint config, so any ignores it contributed are NOT carried over. Check them and add what you need to \`ignorePatterns\` in ${LINT_CONFIG_FILE}`
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
if (removableDeps.length > 0) {
|
|
1947
|
+
notes.push(
|
|
1948
|
+
`removed ${removableDeps.length} eslint dependenc(ies) this module declared (${removableDeps.map(([, name]) => name).join(", ")}); the ROOT is untouched, it serves every module that has not migrated yet`
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
if (rewritten.length > 0) {
|
|
1952
|
+
notes.push(
|
|
1953
|
+
`renamed ${rewritten.length} suppression comment(s) whose plugin oxlint registers under a different name (${[...new Set(rewritten.map((entry) => entry.rule))].join(", ")}); left alone they would stop suppressing anything, silently`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
const script = composeLintScript(this.existingLintScript(context.cwd));
|
|
1957
|
+
if (keepsOtherCommands(script)) {
|
|
1958
|
+
notes.push(
|
|
1959
|
+
`kept the rest of the lint script (\`${script}\`): only the eslint part is sentinel's. Formatting stays with prettier until the formatter moves, as its own decision.`
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
notes.push("run `pnpm install`, then `pnpm run lint`");
|
|
1963
|
+
return { operations, notes };
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Write the `lint` script and the pinned `@hublo/sentinel` devDependency, creating a
|
|
1967
|
+
* `package.json` when the module has none. Real case: a BFF in the monorepo had no
|
|
1968
|
+
* `package.json` at all, so `pnpm exec` failed outright and there was nowhere to put a
|
|
1969
|
+
* script. `merge-json` preserves everything else and makes a re-run idempotent.
|
|
1970
|
+
*/
|
|
1971
|
+
packageJsonOperation(cwd) {
|
|
1972
|
+
const own = readOwnPackage();
|
|
1973
|
+
const value = {
|
|
1974
|
+
scripts: { [LINT_SCRIPT_NAME]: composeLintScript(this.existingLintScript(cwd)) },
|
|
1975
|
+
devDependencies: { [own.name]: own.version }
|
|
1976
|
+
};
|
|
1977
|
+
if (existsSync7(join8(cwd, "package.json"))) {
|
|
1978
|
+
return { kind: "merge-json", path: "package.json", value };
|
|
1979
|
+
}
|
|
1980
|
+
return {
|
|
1981
|
+
kind: "merge-json",
|
|
1982
|
+
path: "package.json",
|
|
1983
|
+
value: { name: readNxProjectName(cwd) ?? basename(cwd), private: true, ...value }
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* True when the nx lint target already says exactly what sentinel would write.
|
|
1988
|
+
*
|
|
1989
|
+
* Worth checking rather than rewriting: the remove-then-merge cycle moves the key's
|
|
1990
|
+
* position in the file, so an unchanged target still produces a diff, and `--init` would
|
|
1991
|
+
* report a change on every run forever.
|
|
1992
|
+
*/
|
|
1993
|
+
hasLintTarget(cwd) {
|
|
1994
|
+
try {
|
|
1995
|
+
const project = JSON.parse(readFileSync6(join8(cwd, "project.json"), "utf8"));
|
|
1996
|
+
return JSON.stringify(project.targets?.[LINT_SCRIPT_NAME]) === JSON.stringify(lintTarget());
|
|
1997
|
+
} catch {
|
|
1998
|
+
return false;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
/** The module's current `lint` script, so adoption replaces only the eslint part of it. */
|
|
2002
|
+
existingLintScript(cwd) {
|
|
2003
|
+
try {
|
|
2004
|
+
const pkg = JSON.parse(readFileSync6(join8(cwd, "package.json"), "utf8"));
|
|
2005
|
+
return pkg.scripts?.[LINT_SCRIPT_NAME];
|
|
2006
|
+
} catch {
|
|
2007
|
+
return void 0;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Lint the module. Announces the rules that are deliberately not enforced, so the
|
|
2012
|
+
* reduced coverage is never a silent gap, and refuses to report a pass when the linter
|
|
2013
|
+
* or its type-aware sidecar is missing.
|
|
2014
|
+
*/
|
|
2015
|
+
async run(ctx) {
|
|
2016
|
+
if (!existsSync7(join8(ctx.cwd, LINT_CONFIG_FILE))) {
|
|
2017
|
+
process.stderr.write(
|
|
2018
|
+
`sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
|
|
2019
|
+
`
|
|
2020
|
+
);
|
|
2021
|
+
return { ok: true, code: 0 };
|
|
2022
|
+
}
|
|
2023
|
+
const oxlint = resolveOxlint(ctx.cwd);
|
|
2024
|
+
if (!oxlint) {
|
|
2025
|
+
process.stderr.write(
|
|
2026
|
+
`sentinel lint(oxlint): could not find the oxlint binary (looked in ${oxlintSearchPath(ctx.cwd)}). Run \`pnpm install\` in the module.
|
|
2027
|
+
`
|
|
2028
|
+
);
|
|
2029
|
+
return { ok: false, code: 1 };
|
|
2030
|
+
}
|
|
2031
|
+
const missingPreset = this.unresolvedPreset(ctx.cwd);
|
|
2032
|
+
if (missingPreset) {
|
|
2033
|
+
process.stderr.write(
|
|
2034
|
+
`sentinel lint(oxlint): ${LINT_CONFIG_FILE} extends ${missingPreset}, which does not exist. That path assumes pnpm's isolated node_modules layout; if the workspace now uses a hoisted linker, or @hublo/sentinel is not installed in this module, run \`pnpm install\` here.
|
|
2035
|
+
`
|
|
2036
|
+
);
|
|
2037
|
+
return { ok: false, code: 1 };
|
|
2038
|
+
}
|
|
2039
|
+
this.announceDisabled(ctx.preset);
|
|
2040
|
+
const typeAware = canRunTypeAware(ctx.cwd);
|
|
2041
|
+
if (!typeAware) {
|
|
2042
|
+
process.stderr.write(
|
|
2043
|
+
`${palette(process.stderr).warn("sentinel lint(oxlint): oxlint-tsgolint is not installed, so type-aware rules will NOT run")}
|
|
2044
|
+
`
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
const env = { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) };
|
|
2048
|
+
const ignores = DEFAULT_IGNORE_PATTERNS.flatMap((pattern) => ["--ignore-pattern", pattern]);
|
|
2049
|
+
const lint = (extra) => {
|
|
2050
|
+
const passed = [...extra, ...ctx.toolArgs ?? []];
|
|
2051
|
+
const result = spawnSync(oxlint, ["-c", LINT_CONFIG_FILE, ...ignores, ...passed, "."], {
|
|
2052
|
+
cwd: ctx.cwd,
|
|
2053
|
+
stdio: "inherit",
|
|
2054
|
+
env
|
|
2055
|
+
});
|
|
2056
|
+
if (result.error) {
|
|
2057
|
+
process.stderr.write(
|
|
2058
|
+
`sentinel lint(oxlint): could not run oxlint (${result.error.message})
|
|
2059
|
+
`
|
|
2060
|
+
);
|
|
2061
|
+
return 1;
|
|
2062
|
+
}
|
|
2063
|
+
return result.status ?? 1;
|
|
2064
|
+
};
|
|
2065
|
+
if (ctx.fix) {
|
|
2066
|
+
for (let pass = 0; pass < MAX_FIX_PASSES; pass += 1) {
|
|
2067
|
+
if (lint(["--fix"]) === 0) break;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
const code = lint(typeAware ? ["--type-aware"] : []);
|
|
2071
|
+
return { ok: code === 0, code };
|
|
2072
|
+
}
|
|
2073
|
+
/** The module's resolved lint configuration, and what it deliberately does not enforce. */
|
|
2074
|
+
async inspect(ctx) {
|
|
2075
|
+
const adoption = readLintAdoption(ctx.cwd);
|
|
2076
|
+
const disabled = disabledRulesFor(ctx.preset);
|
|
2077
|
+
const rules = this.presetRuleCount(ctx.cwd);
|
|
2078
|
+
return {
|
|
2079
|
+
target: "lint",
|
|
2080
|
+
runner: "oxlint",
|
|
2081
|
+
adopted: adoption.adopted,
|
|
2082
|
+
preset: adoption.preset,
|
|
2083
|
+
configFile: adoption.configFile,
|
|
2084
|
+
rules,
|
|
2085
|
+
typeAware: canRunTypeAware(ctx.cwd),
|
|
2086
|
+
disabled: disabled.map((entry) => ({ rule: entry.rule, reason: entry.reason })),
|
|
2087
|
+
// Reported separately from `disabled`: these still run and still report, they just
|
|
2088
|
+
// cannot fail a build. Collapsing the two would hide the difference between "we do
|
|
2089
|
+
// not enforce this" and "we enforce this but will not block on it yet".
|
|
2090
|
+
downgraded: downgradedRulesFor(ctx.preset).map((entry) => ({
|
|
2091
|
+
rule: entry.rule,
|
|
2092
|
+
reason: entry.reason
|
|
2093
|
+
}))
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* The module's own eslint dependencies, as `package.json` key paths to remove.
|
|
2098
|
+
*
|
|
2099
|
+
* Deliberately narrow. Only packages that exist to serve eslint are listed, so nothing a
|
|
2100
|
+
* module might use for another purpose is taken away: `globals`, for instance, is a plain
|
|
2101
|
+
* data package that outlives its eslint config, and removing it on a guess would break a
|
|
2102
|
+
* build for a cleanup nobody asked for. Prettier is untouched too, since formatting stays
|
|
2103
|
+
* with prettier until the formatter migrates as its own decision.
|
|
2104
|
+
*/
|
|
2105
|
+
moduleEslintDependencies(cwd) {
|
|
2106
|
+
const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
|
|
2107
|
+
let manifest;
|
|
2108
|
+
try {
|
|
2109
|
+
manifest = JSON.parse(readFileSync6(join8(cwd, "package.json"), "utf8"));
|
|
2110
|
+
} catch {
|
|
2111
|
+
return [];
|
|
2112
|
+
}
|
|
2113
|
+
return ["dependencies", "devDependencies"].flatMap(
|
|
2114
|
+
(section) => Object.keys(manifest[section] ?? {}).filter(isEslintPackage).map((name) => [section, name])
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
/** How many rules the extended preset enforces, read from the preset on disk. */
|
|
2118
|
+
presetRuleCount(cwd) {
|
|
2119
|
+
const target = this.unresolvedPreset(cwd);
|
|
2120
|
+
if (target) return 0;
|
|
2121
|
+
let total = 0;
|
|
2122
|
+
try {
|
|
2123
|
+
const stub = JSON.parse(readFileSync6(join8(cwd, LINT_CONFIG_FILE), "utf8"));
|
|
2124
|
+
for (const entry of stub.extends ?? []) {
|
|
2125
|
+
const preset = JSON.parse(readFileSync6(join8(cwd, entry), "utf8"));
|
|
2126
|
+
total += Object.keys(preset.rules ?? {}).length;
|
|
2127
|
+
}
|
|
2128
|
+
} catch {
|
|
2129
|
+
return 0;
|
|
2130
|
+
}
|
|
2131
|
+
return total;
|
|
2132
|
+
}
|
|
2133
|
+
/** Lint health for `--report`. Deepened in a later step; today it is the run outcome. */
|
|
2134
|
+
async report(ctx) {
|
|
2135
|
+
const result = await this.run(ctx);
|
|
2136
|
+
return { ...result, metrics: { adopted: readLintAdoption(ctx.cwd).adopted } };
|
|
2137
|
+
}
|
|
2138
|
+
/** Adoption + conformity, read from the committed config with no tool run. */
|
|
2139
|
+
async status(ctx) {
|
|
2140
|
+
const adoption = readLintAdoption(ctx.cwd);
|
|
2141
|
+
return {
|
|
2142
|
+
adopted: adoption.adopted,
|
|
2143
|
+
preset: adoption.preset,
|
|
2144
|
+
conformant: adoption.conformant,
|
|
2145
|
+
drift: adoption.drift
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* The `extends` target this module points at, when it cannot be found on disk. Returns
|
|
2150
|
+
* undefined when everything resolves. Read from the committed config rather than
|
|
2151
|
+
* recomputed, so it checks what the module actually says.
|
|
2152
|
+
*/
|
|
2153
|
+
unresolvedPreset(cwd) {
|
|
2154
|
+
let parsed;
|
|
2155
|
+
try {
|
|
2156
|
+
parsed = JSON.parse(readFileSync6(join8(cwd, LINT_CONFIG_FILE), "utf8"));
|
|
2157
|
+
} catch {
|
|
2158
|
+
return void 0;
|
|
2159
|
+
}
|
|
2160
|
+
const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
|
|
2161
|
+
return targets.find((target) => !existsSync7(join8(cwd, target)));
|
|
2162
|
+
}
|
|
2163
|
+
/** Announce what is not enforced, so reduced coverage is never silent. */
|
|
2164
|
+
announceDisabled(preset) {
|
|
2165
|
+
const softened = downgradedRulesFor(preset);
|
|
2166
|
+
if (softened.length > 0) {
|
|
2167
|
+
process.stderr.write(
|
|
2168
|
+
`${palette(process.stderr).warn(
|
|
2169
|
+
`sentinel lint(oxlint): ${softened.length} rule(s) report but will not fail the build (${summariseByPlugin(softened)}); run --inspect --lint for each rule and why`
|
|
2170
|
+
)}
|
|
2171
|
+
`
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
const disabled = disabledRulesFor(preset);
|
|
2175
|
+
if (disabled.length === 0) return;
|
|
2176
|
+
process.stderr.write(
|
|
2177
|
+
`${palette(process.stderr).warn(
|
|
2178
|
+
`sentinel lint(oxlint): ${disabled.length} rule(s) not enforced by default (${summariseByPlugin(disabled)}); run --inspect --lint for each rule and why`
|
|
2179
|
+
)}
|
|
2180
|
+
`
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
// src/roles/lint/register.ts
|
|
2186
|
+
function registerLint() {
|
|
2187
|
+
register(new OxlintAdapter());
|
|
2188
|
+
setDefaultRunner("lint", "oxlint");
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
2192
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2193
|
+
import { createRequire as createRequire2 } from "module";
|
|
2194
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
2195
|
+
import { basename as basename2, join as join10 } from "path";
|
|
2196
|
+
|
|
2197
|
+
// src/core/config/preset-evidence.ts
|
|
2198
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
|
|
2199
|
+
import { join as join9 } from "path";
|
|
2200
|
+
var PATH_SIGNALS = [
|
|
2201
|
+
{
|
|
2202
|
+
preset: "nest",
|
|
2203
|
+
pattern: /(^|\/)apps\/nest\//,
|
|
2204
|
+
evidence: "it lives under apps/nest"
|
|
2205
|
+
}
|
|
2206
|
+
];
|
|
2207
|
+
var DEPENDENCY_SIGNALS = [
|
|
2208
|
+
{ preset: "react", pattern: /^(react|react-dom|@types\/react)$|^eslint-plugin-react/ },
|
|
2209
|
+
{ preset: "svelte", pattern: /^svelte$|^@sveltejs\// },
|
|
2210
|
+
{ preset: "nest", pattern: /^@nestjs\// }
|
|
2211
|
+
];
|
|
2212
|
+
function dependencyNames(cwd) {
|
|
2213
|
+
const path = join9(cwd, "package.json");
|
|
2214
|
+
if (!existsSync8(path)) return [];
|
|
2215
|
+
try {
|
|
2216
|
+
const manifest = parseJsonc(
|
|
2217
|
+
readFileSync7(path, "utf8"),
|
|
2218
|
+
path
|
|
2219
|
+
);
|
|
2220
|
+
return [
|
|
2221
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
2222
|
+
...Object.keys(manifest.devDependencies ?? {})
|
|
2223
|
+
];
|
|
2224
|
+
} catch {
|
|
2225
|
+
return [];
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
function declaresJsx(cwd) {
|
|
2229
|
+
let entries;
|
|
2230
|
+
try {
|
|
2231
|
+
entries = readdirSync2(cwd).filter(
|
|
2232
|
+
(name) => name.startsWith("tsconfig") && name.endsWith(".json")
|
|
2233
|
+
);
|
|
2234
|
+
} catch {
|
|
2235
|
+
return false;
|
|
2236
|
+
}
|
|
2237
|
+
for (const name of entries) {
|
|
2238
|
+
try {
|
|
2239
|
+
const config = parseJsonc(
|
|
2240
|
+
readFileSync7(join9(cwd, name), "utf8"),
|
|
2241
|
+
name
|
|
2242
|
+
);
|
|
2243
|
+
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
2244
|
+
} catch {
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
return false;
|
|
2248
|
+
}
|
|
2249
|
+
function presetSignals(cwd) {
|
|
2250
|
+
const signals = [];
|
|
2251
|
+
const dependencies = dependencyNames(cwd);
|
|
2252
|
+
for (const { preset, pattern } of DEPENDENCY_SIGNALS) {
|
|
2253
|
+
const hit = dependencies.find((name) => pattern.test(name));
|
|
2254
|
+
if (hit) signals.push({ preset, evidence: `it depends on "${hit}"` });
|
|
2255
|
+
}
|
|
2256
|
+
if (declaresJsx(cwd) && !signals.some((signal) => signal.preset === "react")) {
|
|
2257
|
+
signals.push({ preset: "react", evidence: 'its tsconfig sets "jsx"' });
|
|
2258
|
+
}
|
|
2259
|
+
const location = cwd.replaceAll("\\", "/");
|
|
2260
|
+
for (const { preset, pattern, evidence } of PATH_SIGNALS) {
|
|
2261
|
+
if (pattern.test(location) && !signals.some((signal) => signal.preset === preset)) {
|
|
2262
|
+
signals.push({ preset, evidence });
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
return signals;
|
|
2266
|
+
}
|
|
2267
|
+
function presetContradiction(declared, cwd) {
|
|
2268
|
+
const signals = presetSignals(cwd);
|
|
2269
|
+
if (signals.length === 0) return void 0;
|
|
2270
|
+
if (signals.some((signal) => signal.preset === declared)) return void 0;
|
|
2271
|
+
const [first] = signals;
|
|
2272
|
+
if (!first) return void 0;
|
|
2273
|
+
const evidence = signals.map((signal) => signal.evidence).join(", and ");
|
|
2274
|
+
return `--preset ${declared} does not match this module: ${evidence}, which makes it "${first.preset}". Nothing was written. Re-run with --preset ${first.preset}, or adopt from the module you actually meant.`;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
// src/roles/typescript/config-policy.ts
|
|
2278
|
+
var PERMITTED_COMPILER_OPTIONS = [
|
|
2279
|
+
"paths",
|
|
2280
|
+
"baseUrl",
|
|
2281
|
+
"rootDir",
|
|
2282
|
+
"outDir",
|
|
2283
|
+
"tsBuildInfoFile"
|
|
2284
|
+
];
|
|
2285
|
+
function presetOwnedKeys2(compilerOptions) {
|
|
2286
|
+
return presetOwnedKeys(compilerOptions, PERMITTED_COMPILER_OPTIONS);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/roles/typescript/presets/base.json
|
|
2290
|
+
var base_default2 = {
|
|
2291
|
+
$schema: "https://json.schemastore.org/tsconfig",
|
|
2292
|
+
compilerOptions: {
|
|
2293
|
+
strict: true,
|
|
2294
|
+
noFallthroughCasesInSwitch: true,
|
|
2295
|
+
forceConsistentCasingInFileNames: true,
|
|
2296
|
+
esModuleInterop: true,
|
|
2297
|
+
skipLibCheck: true
|
|
2298
|
+
},
|
|
2299
|
+
deferred: {
|
|
2300
|
+
noImplicitAny: {
|
|
2301
|
+
phase: 2,
|
|
2302
|
+
reason: "the implicit-any migration (TS70xx)"
|
|
2303
|
+
},
|
|
2304
|
+
noUnusedLocals: {
|
|
2305
|
+
phase: 2,
|
|
2306
|
+
reason: "unused-local cleanup (TS6133)"
|
|
2307
|
+
},
|
|
2308
|
+
noUnusedParameters: {
|
|
2309
|
+
phase: 2,
|
|
2310
|
+
reason: "unused-parameter cleanup (TS6133)"
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
|
|
2315
|
+
// src/roles/typescript/presets/nest.json
|
|
2316
|
+
var nest_default2 = {
|
|
2317
|
+
compilerOptions: {
|
|
2318
|
+
module: "esnext",
|
|
2319
|
+
moduleResolution: "bundler",
|
|
2320
|
+
types: ["node"],
|
|
2321
|
+
experimentalDecorators: true,
|
|
2322
|
+
emitDecoratorMetadata: true,
|
|
2323
|
+
declaration: true,
|
|
2324
|
+
sourceMap: true
|
|
2325
|
+
},
|
|
2326
|
+
notes: {
|
|
2327
|
+
module: "the backends are webpack-bundled and tsc only typechecks, so the module format is the bundler's concern; `bundler` resolution is what the repo already uses, and the only Node-friendly option TS7 still accepts since it removed classic `node`",
|
|
2328
|
+
target: "deliberately NOT set, here or in lib: those are environment the repo base owns. Overriding target flips useDefineForClassFields (TS2612) and narrowing lib drops DOM (GLfloat); both REPLACE the base rather than merge"
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
|
|
2332
|
+
// src/roles/typescript/presets/node.json
|
|
2333
|
+
var node_default2 = {
|
|
2334
|
+
compilerOptions: {
|
|
2335
|
+
module: "esnext",
|
|
2336
|
+
moduleResolution: "bundler",
|
|
2337
|
+
types: ["node"],
|
|
2338
|
+
declaration: true
|
|
2339
|
+
},
|
|
2340
|
+
notes: {
|
|
2341
|
+
module: "esnext/bundler like the react and nest presets, matching a monorepo whose libs are bundler-resolved in practice. NOT NodeNext: that would force its stricter ESM rules onto libs the repo resolves through a bundler"
|
|
2342
|
+
}
|
|
2343
|
+
};
|
|
2344
|
+
|
|
2345
|
+
// src/roles/typescript/presets/react.json
|
|
2346
|
+
var react_default2 = {
|
|
2347
|
+
compilerOptions: {
|
|
2348
|
+
target: "ES2023",
|
|
2349
|
+
lib: ["ES2023", "DOM", "DOM.Iterable"],
|
|
2350
|
+
module: "ESNext",
|
|
2351
|
+
moduleResolution: "bundler",
|
|
2352
|
+
jsx: "react-jsx",
|
|
2353
|
+
useDefineForClassFields: true,
|
|
2354
|
+
isolatedModules: true,
|
|
2355
|
+
moduleDetection: "force",
|
|
2356
|
+
noEmit: true,
|
|
2357
|
+
noUncheckedSideEffectImports: true
|
|
2358
|
+
},
|
|
2359
|
+
notes: {
|
|
2360
|
+
noEmit: "vite owns the build; tsc only typechecks here"
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
|
|
2364
|
+
// src/roles/typescript/preset-data.ts
|
|
2365
|
+
var BASE = base_default2;
|
|
2366
|
+
var LAYERS = {
|
|
2367
|
+
nest: nest_default2,
|
|
2368
|
+
node: node_default2,
|
|
2369
|
+
react: react_default2
|
|
2370
|
+
};
|
|
2371
|
+
var SHIPPED_PRESETS = Object.keys(LAYERS).sort();
|
|
2372
|
+
var DEFERRED_RULES = Object.entries(BASE.deferred ?? {}).map(([rule, entry]) => ({ rule, phase: entry.phase, reason: entry.reason })).sort((left, right) => left.rule.localeCompare(right.rule));
|
|
2373
|
+
function validate2() {
|
|
2374
|
+
for (const { rule, phase, reason } of DEFERRED_RULES) {
|
|
2375
|
+
if (!Number.isInteger(phase) || phase < 2) {
|
|
2376
|
+
throw new Error(
|
|
2377
|
+
`deferred rule "${rule}" has phase ${phase}; a deferred rule is scheduled for phase 2 or later`
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
if (reason.trim() === "") {
|
|
2381
|
+
throw new Error(
|
|
2382
|
+
`deferred rule "${rule}" has no reason; --inspect reports it and would say nothing`
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
if (rule in BASE.compilerOptions) {
|
|
2386
|
+
throw new Error(
|
|
2387
|
+
`"${rule}" is deferred and also set by the base preset; it would be announced as not enforced while being enforced`
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
for (const [preset, layer] of Object.entries(LAYERS)) {
|
|
2391
|
+
if (layer && rule in layer.compilerOptions) {
|
|
2392
|
+
throw new Error(
|
|
2393
|
+
`"${rule}" is deferred and also set by the ${preset} preset; it would be announced as not enforced while being enforced`
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
validate2();
|
|
2400
|
+
function hasShippedPreset(preset) {
|
|
2401
|
+
return preset in LAYERS;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// src/roles/typescript/phased-rules.ts
|
|
2405
|
+
function deferredRuleNames2() {
|
|
2406
|
+
return deferredRuleNames(DEFERRED_RULES);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
// src/roles/typescript/resolve-tsconfig-target.ts
|
|
2410
|
+
var TSCONFIG_CANDIDATES = ["tsconfig.app.json", "tsconfig.json"];
|
|
2411
|
+
var TSCONFIG_MARKERS = ["tsconfig.base.json", "@hublo/sentinel/tsconfig/"];
|
|
2412
|
+
var TSCONFIG_FALLBACK = "tsconfig.json";
|
|
2413
|
+
function resolveTsconfigTarget(moduleDir) {
|
|
2414
|
+
return resolveConfigTarget(moduleDir, {
|
|
2415
|
+
candidates: TSCONFIG_CANDIDATES,
|
|
2416
|
+
markers: TSCONFIG_MARKERS,
|
|
2417
|
+
fallback: TSCONFIG_FALLBACK
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// src/roles/typescript/read-adoption.ts
|
|
2422
|
+
var SENTINEL_PRESET = /^@hublo\/sentinel\/tsconfig\/[a-z-]+$/;
|
|
2423
|
+
function readTsconfigAdoption(cwd) {
|
|
2424
|
+
return readAdoption(cwd, {
|
|
2425
|
+
candidates: TSCONFIG_CANDIDATES,
|
|
2426
|
+
markers: TSCONFIG_MARKERS,
|
|
2427
|
+
fallback: TSCONFIG_FALLBACK,
|
|
2428
|
+
presetPattern: SENTINEL_PRESET,
|
|
2429
|
+
settingsKey: "compilerOptions",
|
|
2430
|
+
permitted: PERMITTED_COMPILER_OPTIONS
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
2435
|
+
var SENTINEL_PACKAGE = "@hublo/sentinel";
|
|
2436
|
+
var SENTINEL_PRESET_SCOPE = `${SENTINEL_PACKAGE}/tsconfig/`;
|
|
2437
|
+
var TYPECHECK_SCRIPT = { typecheck: "sentinel --run --typescript" };
|
|
2438
|
+
var INSTALL_NOTE = "run `pnpm install` to fetch @hublo/sentinel (added to the module devDependencies) so `extends` and the typecheck script resolve";
|
|
2439
|
+
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
2440
|
+
var DIAGNOSTIC_RE = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/;
|
|
2441
|
+
function parseDiagnostics(output) {
|
|
2442
|
+
const diagnostics = [];
|
|
2443
|
+
for (const raw of output.split("\n")) {
|
|
2444
|
+
const m = DIAGNOSTIC_RE.exec(raw.trim());
|
|
2445
|
+
if (m) {
|
|
2446
|
+
diagnostics.push({
|
|
2447
|
+
file: m[1],
|
|
2448
|
+
line: Number(m[2]),
|
|
2449
|
+
col: Number(m[3]),
|
|
2450
|
+
code: m[4],
|
|
2451
|
+
message: m[5]
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return diagnostics;
|
|
2456
|
+
}
|
|
2457
|
+
var PHASED_STRICTNESS_WARNING = `sentinel typescript: phase 1 (non-breaking) \u2014 deferred: ${deferredRuleNames2().join(", ")}. Enabled centrally in a later wave; run \`sentinel --inspect --typescript\` for the list, or \`sentinel --run --typescript -- --noImplicitAny\` to measure what one would cost today.`;
|
|
2458
|
+
function composeExtends(current, preset) {
|
|
2459
|
+
const chain = typeof current === "string" ? [current] : Array.isArray(current) ? current.filter((entry) => typeof entry === "string") : [];
|
|
2460
|
+
return chain.includes(preset) ? chain : [...chain, preset];
|
|
2461
|
+
}
|
|
2462
|
+
var TscAdapter = class extends BaseAdapter {
|
|
2463
|
+
target = "typescript";
|
|
2464
|
+
runner = "tsc";
|
|
2465
|
+
/**
|
|
2466
|
+
* The tsc adapter drives type-checking for any preset: `--run`/`--report`/
|
|
2467
|
+
* `--inspect` just execute tsc against the module's existing config, which is
|
|
2468
|
+
* meaningful regardless of preset. `--init` is the exception, it only WRITES a
|
|
2469
|
+
* preset for the names that ship one (gated inside `plan`), so svelte is not
|
|
2470
|
+
* clobbered with a non-existent preset.
|
|
2471
|
+
*/
|
|
2472
|
+
appliesTo(_flavour) {
|
|
2473
|
+
return true;
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* The preset read from the committed `extends` chain
|
|
2477
|
+
* (`@hublo/sentinel/tsconfig/nest` -> `nest`), or undefined when the module has not
|
|
2478
|
+
* adopted a preset, so the engine falls back to dependency detection.
|
|
2479
|
+
*
|
|
2480
|
+
* This is the same detection-free read `--status` uses, and it is why an adopted React app
|
|
2481
|
+
* reports `react` even in a monorepo that hoists `react` to the root.
|
|
2482
|
+
*/
|
|
2483
|
+
declaredPreset(cwd) {
|
|
2484
|
+
const { preset } = readTsconfigAdoption(cwd);
|
|
2485
|
+
if (!preset) return void 0;
|
|
2486
|
+
const name = preset.slice(preset.lastIndexOf("/") + 1);
|
|
2487
|
+
return PRESET_NAMES.includes(name) ? name : void 0;
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Plan `--init`: make the module extend the sentinel preset with a THIN,
|
|
2491
|
+
* conformant stub, route type-checking through the CLI, and pin the
|
|
2492
|
+
* `@hublo/sentinel` devDependency into the module. The engine applies the ops, so
|
|
2493
|
+
* adoption is `--init` then `pnpm install`, with nothing to add by hand.
|
|
2494
|
+
*
|
|
2495
|
+
* Per resolved case:
|
|
2496
|
+
* - extends-base: append the preset to the `extends` chain (keep the base for
|
|
2497
|
+
* the monorepo's paths/structure) + strip preset-owned `compilerOptions`
|
|
2498
|
+
* (drift), keeping the project's own paths/include (the allowlist).
|
|
2499
|
+
* - none: create a fresh thin `tsconfig.json`.
|
|
2500
|
+
* - other-chain (svelte): skip, its config extends a different base.
|
|
2501
|
+
*/
|
|
2502
|
+
plan(context) {
|
|
2503
|
+
if (!hasShippedPreset(context.preset)) {
|
|
2504
|
+
return {
|
|
2505
|
+
operations: [],
|
|
2506
|
+
blocked: `sentinel does not ship a TypeScript preset named "${context.preset}" yet (shipped: ${SHIPPED_PRESETS.join(", ")}). Nothing was written; this module cannot adopt the TypeScript preset until that preset ships.`
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
const current = this.declaredPreset(context.cwd);
|
|
2510
|
+
if (current !== void 0 && current !== context.preset) {
|
|
2511
|
+
return {
|
|
2512
|
+
operations: [],
|
|
2513
|
+
blocked: `this module is already adopted as "${current}", and --preset says "${context.preset}". Nothing was written. If the change is deliberate, remove the sentinel preset from the tsconfig \`extends\` chain and re-run; otherwise re-run with --preset ${current}.`
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
const contradiction = presetContradiction(context.preset, context.cwd);
|
|
2517
|
+
if (contradiction !== void 0) {
|
|
2518
|
+
return { operations: [], blocked: contradiction };
|
|
2519
|
+
}
|
|
2520
|
+
const target = resolveTsconfigTarget(context.cwd);
|
|
2521
|
+
const preset = `@hublo/sentinel/tsconfig/${context.preset}`;
|
|
2522
|
+
const addScript = this.packageJsonOperation(context.cwd);
|
|
2523
|
+
if (target.reason === "other-chain") {
|
|
2524
|
+
return {
|
|
2525
|
+
operations: [],
|
|
2526
|
+
blocked: `${target.path} extends a config chain sentinel does not handle, so nothing was written. Point it at the workspace base (or a plain config) and re-run.`
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
if (target.reason === "no-extends") {
|
|
2530
|
+
return {
|
|
2531
|
+
operations: [],
|
|
2532
|
+
blocked: `${target.path} extends nothing, so it defines its own complete set of compilerOptions and sentinel will not silently take ownership of them. Nothing was written. Make it extend the workspace base and re-run, and --init will then move the shared options into the preset.`
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
if (target.reason === "none") {
|
|
2536
|
+
const contents = JSON.stringify({ extends: preset, include: ["src"] }, null, 2) + "\n";
|
|
2537
|
+
return {
|
|
2538
|
+
operations: [{ kind: "write", path: target.path, contents }, addScript],
|
|
2539
|
+
notes: [`created ${target.path} (no tsconfig found)`, INSTALL_NOTE]
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
const existing = parseJsonc(
|
|
2543
|
+
readFileSync8(join10(context.cwd, target.path), "utf8"),
|
|
2544
|
+
target.path
|
|
2545
|
+
);
|
|
2546
|
+
const extendsChain = composeExtends(existing.extends, preset);
|
|
2547
|
+
const drift = presetOwnedKeys2(existing.compilerOptions);
|
|
2548
|
+
const operations = [
|
|
2549
|
+
{ kind: "merge-json", path: target.path, value: { extends: extendsChain } }
|
|
2550
|
+
];
|
|
2551
|
+
const notes = [];
|
|
2552
|
+
if (drift.length > 0) {
|
|
2553
|
+
const localKeys = Object.keys(existing.compilerOptions ?? {});
|
|
2554
|
+
const stripsAll = localKeys.length > 0 && localKeys.every((key) => drift.includes(key));
|
|
2555
|
+
operations.push({
|
|
2556
|
+
kind: "remove-json-keys",
|
|
2557
|
+
path: target.path,
|
|
2558
|
+
keys: stripsAll ? [["compilerOptions"]] : drift.map((key) => ["compilerOptions", key])
|
|
2559
|
+
});
|
|
2560
|
+
notes.push(`stripped preset-owned compilerOptions: ${drift.join(", ")}`);
|
|
2561
|
+
}
|
|
2562
|
+
operations.push(addScript);
|
|
2563
|
+
notes.push(INSTALL_NOTE);
|
|
2564
|
+
return { operations, notes };
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* The op that routes type-checking through the CLI. If the module already has a
|
|
2568
|
+
* `package.json`, merge the script in and leave the rest untouched. If it does
|
|
2569
|
+
* NOT (common for nx apps/services that carry only a `project.json`), scaffold a
|
|
2570
|
+
* minimal, workspace-valid one, its nx name + `private: true`, so pnpm accepts it
|
|
2571
|
+
* and it can carry the pinned `@hublo/sentinel` devDep this op also writes. The
|
|
2572
|
+
* exact version is written (never a range), so `pnpm install` resolves the same
|
|
2573
|
+
* build the stub's `extends` points at.
|
|
2574
|
+
*/
|
|
2575
|
+
packageJsonOperation(cwd) {
|
|
2576
|
+
const own = readOwnPackage();
|
|
2577
|
+
const devDependencies = { [own.name]: own.version };
|
|
2578
|
+
if (existsSync9(join10(cwd, "package.json"))) {
|
|
2579
|
+
return {
|
|
2580
|
+
kind: "merge-json",
|
|
2581
|
+
path: "package.json",
|
|
2582
|
+
value: { scripts: TYPECHECK_SCRIPT, devDependencies }
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
const name = readNxProjectName(cwd) ?? basename2(cwd);
|
|
2586
|
+
return {
|
|
2587
|
+
kind: "merge-json",
|
|
2588
|
+
path: "package.json",
|
|
2589
|
+
value: { name, private: true, scripts: TYPECHECK_SCRIPT, devDependencies }
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Type-check the module with `tsc -b` (build mode) on its solution config, the
|
|
2594
|
+
* way the monorepo itself does. Build mode walks the config's `references`, so a
|
|
2595
|
+
* references-only solution (Pattern A: app + spec) is actually checked instead of
|
|
2596
|
+
* passing vacuously; it also only caches SUCCESSFUL builds, so errors are always
|
|
2597
|
+
* re-reported. Uses the module's own tsc. Nothing to check is a pass.
|
|
2598
|
+
*/
|
|
2599
|
+
/**
|
|
2600
|
+
* Translate "the preset file is not there" into the one thing that fixes it.
|
|
2601
|
+
*
|
|
2602
|
+
* tsc reports a missing `extends` target as a bare TS6053, once per referenced project,
|
|
2603
|
+
* which reads as a broken preset rather than a missing install:
|
|
2604
|
+
*
|
|
2605
|
+
* tsconfig.json(2,46): error TS6053: File '@hublo/sentinel/tsconfig/node' not found.
|
|
2606
|
+
*
|
|
2607
|
+
* The usual cause is mundane and recurring: the module's `tsconfig` extends the package
|
|
2608
|
+
* from `node_modules`, and switching branches changes the lockfile without reinstalling,
|
|
2609
|
+
* so the package is simply absent. Twice while developing this, a green module looked like
|
|
2610
|
+
* a regression for exactly that reason.
|
|
2611
|
+
*
|
|
2612
|
+
* The lint role already says this; saying it here too means a developer gets the same
|
|
2613
|
+
* answer whichever tool they happen to run first.
|
|
2614
|
+
*/
|
|
2615
|
+
explainMissingPreset(output) {
|
|
2616
|
+
const missing = /error TS6053: File '(@hublo\/sentinel\/[^']+)' not found/.exec(output);
|
|
2617
|
+
if (!missing) return;
|
|
2618
|
+
process.stderr.write(
|
|
2619
|
+
`sentinel typescript(tsc): the tsconfig extends ${missing[1]}, which is not installed in this module. That usually means the lockfile changed without a reinstall (a branch switch is the common one). Run \`pnpm install\` and try again.
|
|
2620
|
+
`
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* The sentinel preset a tsconfig extends, when the package is not installed here.
|
|
2625
|
+
*
|
|
2626
|
+
* Checked BEFORE running, because `run` inherits tsc's stdio and never sees its output.
|
|
2627
|
+
* Without this the developer gets a bare TS6053 repeated once per referenced project:
|
|
2628
|
+
*
|
|
2629
|
+
* tsconfig.json(2,46): error TS6053: File '@hublo/sentinel/tsconfig/node' not found.
|
|
2630
|
+
*
|
|
2631
|
+
* which reads as a broken preset rather than the mundane and recurring cause: the module
|
|
2632
|
+
* extends the package from `node_modules`, and a branch switch changes the lockfile
|
|
2633
|
+
* without reinstalling. Twice while building this, a green module looked like a
|
|
2634
|
+
* regression for exactly that reason. The lint role already says it; both roles should.
|
|
2635
|
+
*/
|
|
2636
|
+
missingPresetPackage(cwd) {
|
|
2637
|
+
const target = resolveTsconfigTarget(cwd);
|
|
2638
|
+
if (target.reason === "none") return void 0;
|
|
2639
|
+
let chain;
|
|
2640
|
+
try {
|
|
2641
|
+
const parsed = parseJsonc(
|
|
2642
|
+
readFileSync8(join10(cwd, target.path), "utf8"),
|
|
2643
|
+
target.path
|
|
2644
|
+
);
|
|
2645
|
+
chain = parsed.extends;
|
|
2646
|
+
} catch {
|
|
2647
|
+
return void 0;
|
|
2648
|
+
}
|
|
2649
|
+
const entries = typeof chain === "string" ? [chain] : Array.isArray(chain) ? chain : [];
|
|
2650
|
+
const preset = entries.find(
|
|
2651
|
+
(entry) => typeof entry === "string" && entry.startsWith(SENTINEL_PRESET_SCOPE)
|
|
2652
|
+
);
|
|
2653
|
+
if (preset === void 0) return void 0;
|
|
2654
|
+
try {
|
|
2655
|
+
createRequire2(join10(cwd, "noop.js")).resolve(preset);
|
|
2656
|
+
return void 0;
|
|
2657
|
+
} catch {
|
|
2658
|
+
return preset;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
async run(ctx) {
|
|
2662
|
+
const config = this.typecheckTarget(ctx.cwd);
|
|
2663
|
+
if (!config) {
|
|
2664
|
+
process.stderr.write("sentinel typescript(tsc): no tsconfig to check\n");
|
|
2665
|
+
return { ok: true, code: 0 };
|
|
2666
|
+
}
|
|
2667
|
+
const missing = this.missingPresetPackage(ctx.cwd);
|
|
2668
|
+
if (missing) {
|
|
2669
|
+
process.stderr.write(
|
|
2670
|
+
`sentinel typescript(tsc): the tsconfig extends ${missing}, which is not installed in this module. That usually means the lockfile changed without a reinstall (a branch switch is the common one). Run \`pnpm install\` and try again.
|
|
2671
|
+
`
|
|
2672
|
+
);
|
|
2673
|
+
return { ok: false, code: 1 };
|
|
2674
|
+
}
|
|
2675
|
+
process.stderr.write(`${palette(process.stderr).warn(PHASED_STRICTNESS_WARNING)}
|
|
2676
|
+
`);
|
|
2677
|
+
const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
|
|
2678
|
+
if (ctx.toolArgs && ctx.toolArgs.length > 0) return this.runWithOptions(ctx, tsc, config);
|
|
2679
|
+
const result = spawnSync2(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
|
|
2680
|
+
if (result.error) {
|
|
2681
|
+
process.stderr.write(
|
|
2682
|
+
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
2683
|
+
`
|
|
2684
|
+
);
|
|
2685
|
+
return { ok: false, code: 1 };
|
|
2686
|
+
}
|
|
2687
|
+
const code = result.status ?? 1;
|
|
2688
|
+
return { ok: code === 0, code };
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* The same check, with the caller's own compiler options applied.
|
|
2692
|
+
*
|
|
2693
|
+
* It cannot reuse the `tsc -b` path above, and that is not a preference. Build mode
|
|
2694
|
+
* REFUSES most compiler options outright:
|
|
2695
|
+
*
|
|
2696
|
+
* $ tsc -b tsconfig.json --noImplicitAny
|
|
2697
|
+
* error TS5094: Compiler option '--noImplicitAny' may not be used with '--build'.
|
|
2698
|
+
*
|
|
2699
|
+
* So honouring them means project mode, which carries a trap of its own here: on this
|
|
2700
|
+
* repo's layout a module's `tsconfig.json` is a SOLUTION (`files: []`, `include: []`,
|
|
2701
|
+
* `references`), so `tsc -p` on it compiles NOTHING and exits 0. That would report a clean
|
|
2702
|
+
* answer for a check that never ran, and it is exactly why the normal path uses build
|
|
2703
|
+
* mode. So walk the references and check each project that actually holds files.
|
|
2704
|
+
*
|
|
2705
|
+
* `--noEmit` is forced: this asks a question about the code, it does not build. Letting a
|
|
2706
|
+
* run with non-standard options write output would poison the artifacts and the build info
|
|
2707
|
+
* the normal path depends on. `--composite false` rides with it, because a composite
|
|
2708
|
+
* project refuses `--noEmit`.
|
|
2709
|
+
*/
|
|
2710
|
+
runWithOptions(ctx, tsc, config) {
|
|
2711
|
+
let worst = 0;
|
|
2712
|
+
for (const project of this.referencedProjects(ctx.cwd, config)) {
|
|
2713
|
+
const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
|
|
2714
|
+
const result = spawnSync2(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
|
|
2715
|
+
if (result.error) {
|
|
2716
|
+
process.stderr.write(
|
|
2717
|
+
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
2718
|
+
`
|
|
2719
|
+
);
|
|
2720
|
+
return { ok: false, code: 1 };
|
|
2721
|
+
}
|
|
2722
|
+
worst = Math.max(worst, result.status ?? 1);
|
|
2723
|
+
}
|
|
2724
|
+
return { ok: worst === 0, code: worst };
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* The projects that actually hold files: a solution's `references`, else the config
|
|
2728
|
+
* itself. Returning the solution when it has references would check nothing, so the
|
|
2729
|
+
* fallback is only for a module whose config really is the compilation unit.
|
|
2730
|
+
*/
|
|
2731
|
+
referencedProjects(cwd, config) {
|
|
2732
|
+
try {
|
|
2733
|
+
const parsed = parseJsonc(
|
|
2734
|
+
readFileSync8(join10(cwd, config), "utf8"),
|
|
2735
|
+
config
|
|
2736
|
+
);
|
|
2737
|
+
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
2738
|
+
return referenced.length > 0 ? referenced : [config];
|
|
2739
|
+
} catch {
|
|
2740
|
+
return [config];
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
/**
|
|
2744
|
+
* The config to type-check with `tsc -b`. Prefer the module's root `tsconfig.json`
|
|
2745
|
+
* (the solution the monorepo builds; `tsc -b` follows its `references` to cover
|
|
2746
|
+
* app + spec), else the base-extending file, else null when there is nothing to
|
|
2747
|
+
* check.
|
|
2748
|
+
*/
|
|
2749
|
+
typecheckTarget(cwd) {
|
|
2750
|
+
if (existsSync9(join10(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
2751
|
+
const target = resolveTsconfigTarget(cwd);
|
|
2752
|
+
return target.reason === "none" ? null : target.path;
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* The module's resolved TypeScript config: which preset, which file, how, and the
|
|
2756
|
+
* phased-strictness state (`deferred` rules that are off in phase 1). This is the
|
|
2757
|
+
* "list what's deferred" query, `sentinel --inspect --typescript`.
|
|
2758
|
+
*/
|
|
2759
|
+
async inspect(ctx) {
|
|
2760
|
+
const target = resolveTsconfigTarget(ctx.cwd);
|
|
2761
|
+
const { preset, adopted } = readTsconfigAdoption(ctx.cwd);
|
|
2762
|
+
return {
|
|
2763
|
+
module: ctx.module,
|
|
2764
|
+
target: "typescript",
|
|
2765
|
+
// The machine-output key stays `flavour` for now. In this shape `preset` already
|
|
2766
|
+
// means the preset PATH, and the output is consumed (the migration status page reads
|
|
2767
|
+
// it), so it is renamed with the rest of the output when --report/--status change.
|
|
2768
|
+
flavour: ctx.preset,
|
|
2769
|
+
configFile: target.path,
|
|
2770
|
+
configState: target.reason,
|
|
2771
|
+
preset,
|
|
2772
|
+
adopted,
|
|
2773
|
+
phase: 1,
|
|
2774
|
+
deferred: DEFERRED_RULES
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Adoption + conformity from the committed tsconfig, for `--status`. No tsc run and
|
|
2779
|
+
* no preset guessing: reads the actual `extends` chain, so a workspace-wide scan is
|
|
2780
|
+
* a cheap coverage + drift dashboard (adopted? which preset? drifted?).
|
|
2781
|
+
*/
|
|
2782
|
+
async status(ctx) {
|
|
2783
|
+
const { adopted, preset, conformant, drift } = readTsconfigAdoption(ctx.cwd);
|
|
2784
|
+
return { adopted, preset, conformant, drift };
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* Report conformance for the module: `tsc -b` (build mode, so app + spec are
|
|
2788
|
+
* covered) and count total type errors plus the implicit-`any` family (TS70xx:
|
|
2789
|
+
* 7006/7031/7053/… ), the signal that drives the noImplicitAny migration. No
|
|
2790
|
+
* tsconfig is a clean, empty report.
|
|
2791
|
+
*/
|
|
2792
|
+
async report(ctx) {
|
|
2793
|
+
const config = this.typecheckTarget(ctx.cwd);
|
|
2794
|
+
if (!config) {
|
|
2795
|
+
return { ok: true, code: 0, metrics: { errors: 0, implicitAny: "deferred", diagnostics: [] } };
|
|
2796
|
+
}
|
|
2797
|
+
const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
|
|
2798
|
+
const result = spawnSync2(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
|
|
2799
|
+
if (result.error) {
|
|
2800
|
+
process.stderr.write(
|
|
2801
|
+
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
2802
|
+
`
|
|
2803
|
+
);
|
|
2804
|
+
return { ok: false, code: 1, metrics: { error: "tsc not available" } };
|
|
2805
|
+
}
|
|
2806
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
2807
|
+
this.explainMissingPreset(output);
|
|
2808
|
+
const errors = (output.match(/error TS\d+/g) ?? []).length;
|
|
2809
|
+
const parsed = parseDiagnostics(output);
|
|
2810
|
+
const cap = ctx.maxDiagnostics === 0 ? Infinity : ctx.maxDiagnostics ?? DEFAULT_MAX_DIAGNOSTICS;
|
|
2811
|
+
const diagnostics = Number.isFinite(cap) ? parsed.slice(0, cap) : parsed;
|
|
2812
|
+
return {
|
|
2813
|
+
ok: errors === 0,
|
|
2814
|
+
code: result.status ?? 0,
|
|
2815
|
+
metrics: {
|
|
2816
|
+
errors,
|
|
2817
|
+
implicitAny: this.implicitAnyMetric(ctx.cwd, tsc, config, output),
|
|
2818
|
+
diagnostics,
|
|
2819
|
+
diagnosticsTruncated: diagnostics.length < parsed.length
|
|
2820
|
+
}
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* The `implicitAny` report metric, honestly. Implicit-`any` violations (TS70xx) are
|
|
2825
|
+
* only *visible* to tsc when `noImplicitAny` is ON. In phase 1 the rule is DEFERRED
|
|
2826
|
+
* (off), so counting TS70xx from the committed-config run is structurally always 0 —
|
|
2827
|
+
* a misleading "no implicit-any" when the rule simply was not applied. So: report the
|
|
2828
|
+
* real count only when the rule is on, otherwise `'deferred'` (never a fake `0`). The
|
|
2829
|
+
* remaining debt while deferred is a separate, opt-in probe (a later `--migration`).
|
|
2830
|
+
*/
|
|
2831
|
+
implicitAnyMetric(cwd, tsc, config, mainOutput) {
|
|
2832
|
+
return this.noImplicitAnyEnabled(cwd, tsc, config, mainOutput) ? (mainOutput.match(/error TS70\d\d/g) ?? []).length : "deferred";
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Whether `noImplicitAny` is effectively ON in the module's resolved config. Read from
|
|
2836
|
+
* `tsc --showConfig` (an explicit value wins; otherwise `strict` implies it). If
|
|
2837
|
+
* `--showConfig` is unavailable, fall back to the run's own evidence: implicit-`any`
|
|
2838
|
+
* errors in the output mean the rule must be on.
|
|
2839
|
+
*/
|
|
2840
|
+
noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
|
|
2841
|
+
const shown = spawnSync2(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
|
|
2842
|
+
if (shown.status === 0 && shown.stdout) {
|
|
2843
|
+
try {
|
|
2844
|
+
const co = parseJsonc(
|
|
2845
|
+
shown.stdout,
|
|
2846
|
+
"tsconfig(--showConfig)"
|
|
2847
|
+
).compilerOptions;
|
|
2848
|
+
if (co) return co.noImplicitAny ?? co.strict === true;
|
|
2849
|
+
} catch {
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
return /error TS70\d\d/.test(mainOutput);
|
|
2853
|
+
}
|
|
2854
|
+
};
|
|
2855
|
+
|
|
2856
|
+
// src/roles/typescript/register.ts
|
|
2857
|
+
function registerTypescript() {
|
|
2858
|
+
register(new TscAdapter());
|
|
2859
|
+
setDefaultRunner("typescript", "tsc");
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// src/adapters.ts
|
|
2863
|
+
function registerAdapters() {
|
|
2864
|
+
registerTypescript();
|
|
2865
|
+
registerLint();
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/core/detect-framework.ts
|
|
2869
|
+
var FRAMEWORK_SIGNALS = [
|
|
2870
|
+
{ preset: "nest", dependency: "@nestjs/core" },
|
|
2871
|
+
{ preset: "react", dependency: "react" },
|
|
2872
|
+
{ preset: "svelte", dependency: "svelte" }
|
|
2873
|
+
];
|
|
2874
|
+
var BACKEND_HINTS = ["sails", "express", "koa", "fastify", "@hapi/hapi"];
|
|
2875
|
+
function describeFramework(packageJson) {
|
|
2876
|
+
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
2877
|
+
const matched = FRAMEWORK_SIGNALS.filter((s) => s.dependency in dependencies);
|
|
2878
|
+
const chosen = matched[0];
|
|
2879
|
+
const preset = chosen?.preset ?? "node";
|
|
2880
|
+
const source = chosen?.dependency ?? "(no framework dependency)";
|
|
2881
|
+
const otherFlavourSignals = matched.slice(1).map((s) => s.dependency);
|
|
2882
|
+
const backendHints = preset === "react" || preset === "svelte" ? BACKEND_HINTS.filter((hint) => hint in dependencies) : [];
|
|
2883
|
+
const conflicts = [...otherFlavourSignals, ...backendHints];
|
|
2884
|
+
return { preset, source, ambiguous: conflicts.length > 0, conflicts };
|
|
2885
|
+
}
|
|
2886
|
+
function detectFramework(packageJson) {
|
|
2887
|
+
return describeFramework(packageJson).preset;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// src/shared/text.ts
|
|
2891
|
+
function ensureLines(current, lines) {
|
|
2892
|
+
const present = new Set(current.split("\n").map((line) => line.trim()));
|
|
2893
|
+
const missing = lines.filter((line) => !present.has(line.trim()));
|
|
2894
|
+
if (missing.length === 0) return current;
|
|
2895
|
+
const prefix = current.length === 0 || current.endsWith("\n") ? current : current + "\n";
|
|
2896
|
+
return prefix + missing.join("\n") + "\n";
|
|
2897
|
+
}
|
|
2898
|
+
function toLines(text) {
|
|
2899
|
+
if (text.length === 0) return [];
|
|
2900
|
+
return text.replace(/\n$/, "").split("\n");
|
|
2901
|
+
}
|
|
2902
|
+
function diffLines(before, after) {
|
|
2903
|
+
const from = toLines(before);
|
|
2904
|
+
const to = toLines(after);
|
|
2905
|
+
const lcs = Array.from(
|
|
2906
|
+
{ length: from.length + 1 },
|
|
2907
|
+
() => new Array(to.length + 1).fill(0)
|
|
2908
|
+
);
|
|
2909
|
+
const cell = (i2, j2) => lcs[i2]?.[j2] ?? 0;
|
|
2910
|
+
for (let i2 = from.length - 1; i2 >= 0; i2--) {
|
|
2911
|
+
const row = lcs[i2];
|
|
2912
|
+
if (!row) continue;
|
|
2913
|
+
for (let j2 = to.length - 1; j2 >= 0; j2--) {
|
|
2914
|
+
row[j2] = from[i2] === to[j2] ? cell(i2 + 1, j2 + 1) + 1 : Math.max(cell(i2 + 1, j2), cell(i2, j2 + 1));
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
const out = [];
|
|
2918
|
+
let i = 0;
|
|
2919
|
+
let j = 0;
|
|
2920
|
+
while (i < from.length && j < to.length) {
|
|
2921
|
+
if (from[i] === to[j]) {
|
|
2922
|
+
out.push(` ${from[i] ?? ""}`);
|
|
2923
|
+
i++;
|
|
2924
|
+
j++;
|
|
2925
|
+
} else if (cell(i + 1, j) >= cell(i, j + 1)) {
|
|
2926
|
+
out.push(`- ${from[i] ?? ""}`);
|
|
2927
|
+
i++;
|
|
2928
|
+
} else {
|
|
2929
|
+
out.push(`+ ${to[j] ?? ""}`);
|
|
2930
|
+
j++;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
while (i < from.length) out.push(`- ${from[i++] ?? ""}`);
|
|
2934
|
+
while (j < to.length) out.push(`+ ${to[j++] ?? ""}`);
|
|
2935
|
+
return out;
|
|
2936
|
+
}
|
|
2937
|
+
function replaceLines(current, replacements) {
|
|
2938
|
+
if (replacements.length === 0) return current;
|
|
2939
|
+
const byLine = new Map(replacements.map((entry) => [entry.from, entry.to]));
|
|
2940
|
+
return current.split("\n").map((line) => byLine.get(line) ?? line).join("\n");
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
// src/core/apply-plan.ts
|
|
2944
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, renameSync, rmSync, writeFileSync } from "fs";
|
|
2945
|
+
import { resolve as resolve2, sep } from "path";
|
|
2946
|
+
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
2947
|
+
|
|
2948
|
+
// src/shared/deep-merge.ts
|
|
2949
|
+
function isPlainObject(value) {
|
|
2950
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
// src/core/apply-plan.ts
|
|
2954
|
+
function resolveWithinRoot(cwd, relativePath) {
|
|
2955
|
+
const root = resolve2(cwd);
|
|
2956
|
+
const absolutePath = resolve2(root, relativePath);
|
|
2957
|
+
if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
|
|
2958
|
+
throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
|
|
2959
|
+
}
|
|
2960
|
+
return absolutePath;
|
|
2961
|
+
}
|
|
2962
|
+
function readIfExists(absolutePath) {
|
|
2963
|
+
return existsSync10(absolutePath) ? readFileSync9(absolutePath, "utf8") : void 0;
|
|
2964
|
+
}
|
|
2965
|
+
function* leaves(value, prefix = []) {
|
|
2966
|
+
for (const [key, keyValue] of Object.entries(value)) {
|
|
2967
|
+
const path = [...prefix, key];
|
|
2968
|
+
if (isPlainObject(keyValue)) yield* leaves(keyValue, path);
|
|
2969
|
+
else yield [path, keyValue];
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
function mergeJsonc(current, value) {
|
|
2973
|
+
let text = current.trim().length > 0 ? current : "{}\n";
|
|
2974
|
+
for (const [path, leaf] of leaves(value)) {
|
|
2975
|
+
const edits = modify(text, path, leaf, {
|
|
2976
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
2977
|
+
});
|
|
2978
|
+
text = applyEdits(text, edits);
|
|
2979
|
+
}
|
|
2980
|
+
return text.endsWith("\n") ? text : text + "\n";
|
|
2981
|
+
}
|
|
2982
|
+
function removeJsoncKeys(current, keys) {
|
|
2983
|
+
let text = current.trim().length > 0 ? current : "{}\n";
|
|
2984
|
+
for (const path of keys) {
|
|
2985
|
+
const root = parseTree(text);
|
|
2986
|
+
if (!root || !findNodeAtLocation(root, path)) continue;
|
|
2987
|
+
const edits = modify(text, path, void 0, {
|
|
2988
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
2989
|
+
});
|
|
2990
|
+
text = applyEdits(text, edits);
|
|
2991
|
+
}
|
|
2992
|
+
return text.endsWith("\n") ? text : text + "\n";
|
|
2993
|
+
}
|
|
2994
|
+
function sameJson(left, right) {
|
|
2995
|
+
try {
|
|
2996
|
+
return JSON.stringify(parseJsonc(left, "a")) === JSON.stringify(parseJsonc(right, "b"));
|
|
2997
|
+
} catch {
|
|
2998
|
+
return false;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
function applyOperationTo(current, operation) {
|
|
3002
|
+
switch (operation.kind) {
|
|
3003
|
+
case "write":
|
|
3004
|
+
return sameJson(current, operation.contents) ? current : operation.contents;
|
|
3005
|
+
case "merge-json":
|
|
3006
|
+
return mergeJsonc(current, operation.value);
|
|
3007
|
+
case "ensure-lines":
|
|
3008
|
+
return ensureLines(current, operation.lines);
|
|
3009
|
+
case "remove-json-keys":
|
|
3010
|
+
return removeJsoncKeys(current, operation.keys);
|
|
3011
|
+
case "replace-lines":
|
|
3012
|
+
return replaceLines(current, operation.replacements);
|
|
3013
|
+
case "delete":
|
|
3014
|
+
return "";
|
|
3015
|
+
default: {
|
|
3016
|
+
const unreachable = operation;
|
|
3017
|
+
throw new Error(`Unknown file operation: ${JSON.stringify(unreachable)}`);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
function preparePlan(cwd, plan) {
|
|
3022
|
+
const prepared = /* @__PURE__ */ new Map();
|
|
3023
|
+
for (const operation of plan.operations) {
|
|
3024
|
+
const absolutePath = resolveWithinRoot(cwd, operation.path);
|
|
3025
|
+
const existing = prepared.get(operation.path);
|
|
3026
|
+
const before = existing?.before ?? readIfExists(absolutePath) ?? "";
|
|
3027
|
+
const current = existing?.after ?? before;
|
|
3028
|
+
prepared.set(operation.path, {
|
|
3029
|
+
path: operation.path,
|
|
3030
|
+
absolutePath,
|
|
3031
|
+
before,
|
|
3032
|
+
after: applyOperationTo(current, operation),
|
|
3033
|
+
// A later operation on the same path un-deletes it: the last word wins, the same
|
|
3034
|
+
// way content operations chain.
|
|
3035
|
+
deleted: operation.kind === "delete"
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
return [...prepared.values()];
|
|
3039
|
+
}
|
|
3040
|
+
function writeFileAtomic(absolutePath, contents) {
|
|
3041
|
+
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
3042
|
+
writeFileSync(tempPath, contents);
|
|
3043
|
+
renameSync(tempPath, absolutePath);
|
|
3044
|
+
}
|
|
3045
|
+
function applyPlan(cwd, plan) {
|
|
3046
|
+
const changed = preparePlan(cwd, plan).filter((file) => file.before !== file.after);
|
|
3047
|
+
for (const file of changed) {
|
|
3048
|
+
if (file.deleted) {
|
|
3049
|
+
rmSync(file.absolutePath, { force: true });
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
writeFileAtomic(file.absolutePath, file.after);
|
|
3053
|
+
}
|
|
3054
|
+
return changed.map((file) => file.path);
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/core/dispatch.ts
|
|
3058
|
+
function resolveFlavour(opts) {
|
|
3059
|
+
if (opts.preset) return opts.preset;
|
|
3060
|
+
const detection = describeFramework(readProjectPackageJson(opts.cwd));
|
|
3061
|
+
if (detection.ambiguous) {
|
|
3062
|
+
const warn = palette(process.stderr);
|
|
3063
|
+
process.stderr.write(
|
|
3064
|
+
warn.warn(
|
|
3065
|
+
`sentinel: preset is ambiguous, detected "${detection.preset}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --preset to write the intended preset.`
|
|
3066
|
+
) + "\n"
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
return detection.preset;
|
|
3070
|
+
}
|
|
3071
|
+
function previewPlan(opts, plan) {
|
|
3072
|
+
const changed = preparePlan(opts.cwd, plan).filter((file) => file.before !== file.after);
|
|
3073
|
+
if (opts.json) {
|
|
3074
|
+
process.stdout.write(
|
|
3075
|
+
JSON.stringify(
|
|
3076
|
+
{
|
|
3077
|
+
dryRun: true,
|
|
3078
|
+
notes: plan.notes ?? [],
|
|
3079
|
+
files: changed.map(({ path, before, after, deleted }) => ({
|
|
3080
|
+
path,
|
|
3081
|
+
action: deleted ? "delete" : before.length === 0 ? "create" : "update",
|
|
3082
|
+
before,
|
|
3083
|
+
after
|
|
3084
|
+
}))
|
|
3085
|
+
},
|
|
3086
|
+
null,
|
|
3087
|
+
2
|
|
3088
|
+
) + "\n"
|
|
3089
|
+
);
|
|
3090
|
+
return 0;
|
|
3091
|
+
}
|
|
3092
|
+
process.stderr.write(" dry run: no files written\n");
|
|
3093
|
+
for (const note of plan.notes ?? []) process.stderr.write(` ${note}
|
|
3094
|
+
`);
|
|
3095
|
+
if (changed.length === 0) {
|
|
3096
|
+
process.stderr.write(" nothing to change\n");
|
|
3097
|
+
return 0;
|
|
3098
|
+
}
|
|
3099
|
+
for (const { path, before, after, deleted } of changed) {
|
|
3100
|
+
const action = deleted ? "delete" : before.length === 0 ? "create" : "update";
|
|
3101
|
+
process.stdout.write(`
|
|
3102
|
+
${action} ${path}
|
|
3103
|
+
`);
|
|
3104
|
+
for (const line of diffLines(before, after)) process.stdout.write(` ${line}
|
|
3105
|
+
`);
|
|
3106
|
+
}
|
|
3107
|
+
return 0;
|
|
3108
|
+
}
|
|
3109
|
+
async function dispatch(opts) {
|
|
3110
|
+
if (opts.verb !== "init") {
|
|
3111
|
+
throw new Error(`dispatch handles --init only; --${opts.verb} routes through analyse()`);
|
|
3112
|
+
}
|
|
3113
|
+
const detected = resolveFlavour(opts);
|
|
3114
|
+
const adapter = resolve(opts.target, detected, opts.runner);
|
|
3115
|
+
const preset = opts.preset ?? adapter.declaredPreset?.(opts.cwd) ?? detected;
|
|
3116
|
+
const context = { cwd: opts.cwd, preset };
|
|
3117
|
+
const plan = await adapter.plan(context);
|
|
3118
|
+
if (plan.blocked) {
|
|
3119
|
+
process.stderr.write(`sentinel (${opts.target}): ${plan.blocked}
|
|
3120
|
+
`);
|
|
3121
|
+
return 1;
|
|
3122
|
+
}
|
|
3123
|
+
if (opts.dryRun) {
|
|
3124
|
+
return previewPlan(opts, plan);
|
|
3125
|
+
}
|
|
3126
|
+
const written = applyPlan(opts.cwd, plan);
|
|
3127
|
+
for (const path of written) process.stderr.write(` wrote ${path}
|
|
3128
|
+
`);
|
|
3129
|
+
for (const note of plan.notes ?? []) process.stderr.write(` ${note}
|
|
3130
|
+
`);
|
|
3131
|
+
return 0;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
export {
|
|
3135
|
+
register,
|
|
3136
|
+
setDefaultRunner,
|
|
3137
|
+
all,
|
|
3138
|
+
availableTargets,
|
|
3139
|
+
resolve,
|
|
3140
|
+
BaseAdapter,
|
|
3141
|
+
palette,
|
|
3142
|
+
readOwnPackage,
|
|
3143
|
+
readOwnVersion,
|
|
3144
|
+
readProjectPackageJson,
|
|
3145
|
+
readNxProjectName,
|
|
3146
|
+
resolveBin,
|
|
3147
|
+
VERBS,
|
|
3148
|
+
TARGETS,
|
|
3149
|
+
PRESET_NAMES,
|
|
3150
|
+
registerAdapters,
|
|
3151
|
+
describeFramework,
|
|
3152
|
+
detectFramework,
|
|
3153
|
+
dispatch
|
|
3154
|
+
};
|
|
3155
|
+
//# sourceMappingURL=chunk-FLLX5MN3.js.map
|