@noctcore/lint-meta-rules 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/prisma.js ADDED
@@ -0,0 +1,356 @@
1
+ import {
2
+ globFiles
3
+ } from "./chunk-Z7TXSZR4.js";
4
+ import {
5
+ firstOptionOf,
6
+ resolveRules,
7
+ severityOf
8
+ } from "./chunk-VFCX3QKZ.js";
9
+
10
+ // src/prisma/prisma-method-surface.ts
11
+ import { PRISMA_READ_METHODS, PRISMA_WRITE_METHODS } from "@noctcore/eslint-plugin-prisma";
12
+ var DEFAULT_ID = "prisma-method-surface";
13
+ var DEFAULT_CLIENT_GLOBS = [
14
+ "generated/prisma/models/*.ts",
15
+ "node_modules/.prisma/client/index.d.ts"
16
+ ];
17
+ function blankComments(source) {
18
+ return source.replace(/\/\*[\s\S]*?\*\//gu, (comment) => comment.replace(/[^\n]/gu, " ")).replace(/(^|[^:])\/\/.*$/gmu, "$1");
19
+ }
20
+ function parseDelegateSurfaces(source, file = "") {
21
+ const lines = blankComments(source).split("\n");
22
+ const surfaces = [];
23
+ for (let i = 0; i < lines.length; i += 1) {
24
+ const open = /^(\s*)(?:export\s+)?interface\s+([A-Za-z_]\w*)Delegate\b.*\{\s*$/u.exec(
25
+ lines[i] ?? ""
26
+ );
27
+ if (open?.[2] === void 0) continue;
28
+ const indent = open[1] ?? "";
29
+ let memberIndent = null;
30
+ const methods = /* @__PURE__ */ new Set();
31
+ let j = i + 1;
32
+ for (; j < lines.length; j += 1) {
33
+ const line = lines[j] ?? "";
34
+ if (line.startsWith(`${indent}}`)) break;
35
+ if (line.trim() === "") continue;
36
+ const leading = /^\s*/u.exec(line)?.[0] ?? "";
37
+ memberIndent ??= leading;
38
+ if (leading !== memberIndent) continue;
39
+ const member = /^\s*([A-Za-z_]\w*)\s*</u.exec(line);
40
+ if (member?.[1] !== void 0) methods.add(member[1]);
41
+ }
42
+ surfaces.push({ file, model: open[2], methods: [...methods].sort() });
43
+ i = j;
44
+ }
45
+ return surfaces;
46
+ }
47
+ var sorted = (values) => [...values].sort();
48
+ function createPrismaMethodSurfaceRule(options = {}) {
49
+ const id = options.id ?? DEFAULT_ID;
50
+ const clientGlobs = options.clientGlobs ?? DEFAULT_CLIENT_GLOBS;
51
+ const writeMethods = options.writeMethods ?? PRISMA_WRITE_METHODS;
52
+ const readMethods = options.readMethods ?? PRISMA_READ_METHODS;
53
+ return {
54
+ id,
55
+ category: "config",
56
+ ciCritical: options.ciCritical ?? true,
57
+ description: "The Prisma reads and writes the rules police partition the generated client's <Model>Delegate method surface exactly, so a Prisma upgrade cannot add an unguarded method.",
58
+ run(ctx) {
59
+ const violations = [];
60
+ const report = (file, message) => {
61
+ violations.push({ file, rule: id, message });
62
+ };
63
+ const listFile = clientGlobs[0] ?? "<clientGlobs>";
64
+ const writes = new Set(writeMethods);
65
+ for (const method of readMethods) {
66
+ if (writes.has(method)) {
67
+ report(listFile, `"${method}" is configured as both a read and a write. Each delegate method belongs to exactly one.`);
68
+ }
69
+ }
70
+ const surfaces = globFiles((pattern) => ctx.glob(pattern), clientGlobs).flatMap(
71
+ (file) => parseDelegateSurfaces(ctx.read(file) ?? "", file)
72
+ ).filter((surface) => surface.methods.length > 0);
73
+ const [reference] = surfaces;
74
+ if (reference === void 0) {
75
+ report(
76
+ listFile,
77
+ `No generated Prisma client found under ${clientGlobs.map((glob) => `"${glob}"`).join(", ")}, so the method surface cannot be checked. Run \`prisma generate\` before lint-meta, or point \`clientGlobs\` at the generator's output.`
78
+ );
79
+ return violations;
80
+ }
81
+ const referenceKey = reference.methods.join(",");
82
+ for (const surface of surfaces) {
83
+ if (surface.methods.join(",") !== referenceKey) {
84
+ report(
85
+ surface.file,
86
+ `${surface.model}Delegate exposes [${surface.methods.join(", ")}], which differs from ${reference.model}Delegate's [${reference.methods.join(", ")}]. The method lists are model-agnostic, so they cannot describe both.`
87
+ );
88
+ }
89
+ }
90
+ const configured = /* @__PURE__ */ new Set([...writeMethods, ...readMethods]);
91
+ const actual = new Set(reference.methods);
92
+ const unguarded = sorted(reference.methods.filter((method) => !configured.has(method)));
93
+ const phantom = sorted([...configured].filter((method) => !actual.has(method)));
94
+ if (unguarded.length > 0) {
95
+ report(
96
+ reference.file,
97
+ `The generated client exposes ${unguarded.map((m) => `"${m}"`).join(", ")}, which is neither a configured read nor a configured write, so every rule guarding Prisma calls by method name misses it. Classify it in \`writeMethods\` or \`readMethods\` (and in the rules that read those lists).`
98
+ );
99
+ }
100
+ if (phantom.length > 0) {
101
+ report(
102
+ reference.file,
103
+ `${phantom.map((m) => `"${m}"`).join(", ")} is configured but the generated client no longer exposes it. Drop it from the lists, or check the client was regenerated for this Prisma version.`
104
+ );
105
+ }
106
+ return violations;
107
+ }
108
+ };
109
+ }
110
+
111
+ // src/prisma/tenant-model-registry-parity.ts
112
+ import {
113
+ parsePrismaSchema,
114
+ reconcileTenantRegistry
115
+ } from "@noctcore/eslint-plugin-prisma";
116
+ var DEFAULT_ID2 = "tenant-model-registry-parity";
117
+ var DEFAULT_SCHEMA_PATH = "prisma/schema.prisma";
118
+ var DEFAULT_TENANT_FIELDS = ["tenantId"];
119
+ var DEFAULT_PROBE = "src/__lint_meta_probe__.ts";
120
+ var DEFAULT_MODEL_RULES = [
121
+ "noctcore-prisma/no-cross-tenant-id-in-where",
122
+ "noctcore-prisma/tenant-scoped-tables-require-where",
123
+ "noctcore-prisma/tenant-write-must-carry-tenant-id"
124
+ ];
125
+ var DEFAULT_HAND_SCOPED_RULE = "noctcore-prisma/tenant-scoped-tables-require-where";
126
+ function blankCommentsAndStrings(source) {
127
+ const out = source.split("");
128
+ let i = 0;
129
+ while (i < source.length) {
130
+ const two = source.slice(i, i + 2);
131
+ if (two === "//") {
132
+ while (i < source.length && source[i] !== "\n") out[i++] = " ";
133
+ continue;
134
+ }
135
+ if (two === "/*") {
136
+ const end = source.indexOf("*/", i + 2);
137
+ const stop = end === -1 ? source.length : end + 2;
138
+ for (; i < stop; i += 1) if (source[i] !== "\n") out[i] = " ";
139
+ continue;
140
+ }
141
+ const quote = source[i];
142
+ if (quote === "'" || quote === '"' || quote === "`") {
143
+ i += 1;
144
+ while (i < source.length && source[i] !== quote) {
145
+ if (source[i] === "\\") out[i++] = " ";
146
+ if (i < source.length && source[i] !== "\n") out[i] = " ";
147
+ i += 1;
148
+ }
149
+ i += 1;
150
+ continue;
151
+ }
152
+ i += 1;
153
+ }
154
+ return out.join("");
155
+ }
156
+ var OPENERS = /* @__PURE__ */ new Set(["{", "[", "("]);
157
+ var CLOSERS = /* @__PURE__ */ new Set(["}", "]", ")"]);
158
+ function parseObjectLiteralKeys(source, exportName) {
159
+ const blanked = blankCommentsAndStrings(source);
160
+ const escaped = exportName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
161
+ const declaration = new RegExp(`\\b${escaped}\\s*(?::[^=]+)?=\\s*\\{`, "u").exec(blanked);
162
+ if (declaration === null) return null;
163
+ const open = declaration.index + declaration[0].length - 1;
164
+ const keys = [];
165
+ let depth = 0;
166
+ let token = "";
167
+ for (let i = open; i < blanked.length; i += 1) {
168
+ const char = blanked[i] ?? "";
169
+ if (OPENERS.has(char)) {
170
+ depth += 1;
171
+ token = "";
172
+ } else if (CLOSERS.has(char)) {
173
+ depth -= 1;
174
+ token = "";
175
+ if (depth === 0) return keys;
176
+ } else if (depth === 1 && char === ":") {
177
+ const key = token.trim();
178
+ if (/^[A-Za-z_$][\w$]*$/u.test(key)) keys.push(key);
179
+ token = "";
180
+ } else if (depth === 1 && char === ",") {
181
+ token = "";
182
+ } else if (depth === 1) {
183
+ token += char;
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+ function readSchema(ctx, schemaPath) {
189
+ const base = schemaPath.replace(/\/+$/u, "");
190
+ const files = ctx.glob(`${base}/**/*.prisma`).sort();
191
+ const texts = files.length > 0 ? files.map((file) => ctx.read(file) ?? "") : [ctx.read(base)];
192
+ if (texts.some((text) => text === null)) return null;
193
+ const parsed = parsePrismaSchema(texts.join("\n"));
194
+ return parsed.models.length > 0 ? parsed : null;
195
+ }
196
+ function fingerprint(models) {
197
+ return Object.keys(models).sort().map((model) => `${model}:${[...models[model] ?? []].sort().join("+")}`).join(",");
198
+ }
199
+ function handScopeMessages(registry) {
200
+ const unscoped = registry.unscopedByDesign;
201
+ const handScoped = registry.handScopedModels ?? {};
202
+ const pending = registry.handScopePending ?? {};
203
+ const messages = [];
204
+ for (const model of Object.keys(unscoped)) {
205
+ if (Object.hasOwn(handScoped, model) || Object.hasOwn(pending, model)) continue;
206
+ messages.push(
207
+ `\`${model}\` is exempt from the tenant-scope extension but has no \`handScopedModels\` entry, so "scoped by hand" is enforced by nothing. List the column(s) every query on it must filter by, or record why it cannot have one in \`handScopePending\`.`
208
+ );
209
+ }
210
+ for (const model of Object.keys(handScoped)) {
211
+ if (!Object.hasOwn(unscoped, model)) {
212
+ messages.push(
213
+ `\`${model}\` has a \`handScopedModels\` entry but is not exempt from the extension, so the static guard polices a model the runtime boundary already scopes. Drop the entry, or add the exemption it belongs to.`
214
+ );
215
+ }
216
+ }
217
+ for (const model of Object.keys(pending)) {
218
+ if (!Object.hasOwn(unscoped, model)) {
219
+ messages.push(
220
+ `\`${model}\` is recorded in \`handScopePending\` but is not exempt from the extension any more. The note is stale: delete it.`
221
+ );
222
+ }
223
+ }
224
+ return messages;
225
+ }
226
+ function createTenantModelRegistryParityRule(options = {}) {
227
+ const id = options.id ?? DEFAULT_ID2;
228
+ const schemaPath = options.schemaPath ?? DEFAULT_SCHEMA_PATH;
229
+ const tenantFields = options.tenantFields ?? DEFAULT_TENANT_FIELDS;
230
+ const registry = options.registry ?? { scopedModels: [], unscopedByDesign: {} };
231
+ const source = options.scopedModelsSource;
232
+ const registryFile = options.registryFile ?? source?.file ?? schemaPath;
233
+ const requireHandScope = options.requireHandScope ?? true;
234
+ const eslint = options.eslint === false ? null : options.eslint ?? {};
235
+ return {
236
+ id,
237
+ category: "config",
238
+ ciCritical: options.ciCritical ?? true,
239
+ description: "Every tenant-bearing Prisma model is scoped by the runtime extension or exempt with a reason, and the tenant lint rules resolve with exactly that registry.",
240
+ async runAsync(ctx) {
241
+ const violations = [];
242
+ const report = (file, message) => {
243
+ violations.push({ file, rule: id, message });
244
+ };
245
+ let scopedModels = registry.scopedModels;
246
+ if (source !== void 0) {
247
+ const text = ctx.read(source.file);
248
+ const keys = text === null ? null : parseObjectLiteralKeys(text, source.exportName);
249
+ if (keys === null || keys.length === 0) {
250
+ report(
251
+ source.file,
252
+ `Could not read the \`${source.exportName}\` object literal in ${source.file}, so the runtime scope map cannot be checked. The rule expects \`const ${source.exportName} = { model: ..., ... }\`; if the declaration moved or changed shape, update \`scopedModelsSource\` rather than dropping the check.`
253
+ );
254
+ return violations;
255
+ }
256
+ scopedModels = keys;
257
+ }
258
+ if (requireHandScope) {
259
+ for (const message of handScopeMessages(registry)) report(registryFile, message);
260
+ }
261
+ const parsed = readSchema(ctx, schemaPath);
262
+ if (parsed === null) {
263
+ report(
264
+ schemaPath,
265
+ `Could not read a Prisma schema with any model at "${schemaPath}", so tenant-bearing models cannot be derived. Point \`schemaPath\` at the schema file or folder rather than dropping the check.`
266
+ );
267
+ } else {
268
+ for (const message of reconcileTenantRegistry({
269
+ parsed,
270
+ tenantFields,
271
+ scopedModels,
272
+ unscopedByDesign: registry.unscopedByDesign
273
+ }).messages) {
274
+ report(source?.file ?? registryFile, message);
275
+ }
276
+ }
277
+ if (eslint === null) return violations;
278
+ const cwd = eslint.cwd ?? ".";
279
+ const configFile = eslint.configFile ?? cwd;
280
+ const modelsOption = eslint.modelsOption ?? "tenantModels";
281
+ const handScopedOption = eslint.handScopedOption ?? "handScopedModels";
282
+ const handScopedRule = eslint.handScopedRule === void 0 ? DEFAULT_HAND_SCOPED_RULE : eslint.handScopedRule;
283
+ const outcome = await resolveRules(ctx.root, cwd, [eslint.probe ?? DEFAULT_PROBE]);
284
+ if (!outcome.ok) {
285
+ report(
286
+ configFile,
287
+ `Could not resolve the effective ESLint config for "${cwd}", so the tenant rules' registry cannot be checked (if it imports a workspace package, build that first): ${outcome.error}`
288
+ );
289
+ return violations;
290
+ }
291
+ const rules = outcome.rules[0] ?? {};
292
+ const scopedSet = new Set(scopedModels);
293
+ for (const ruleId of eslint.modelRules ?? DEFAULT_MODEL_RULES) {
294
+ const entry = rules[ruleId];
295
+ const raw = firstOptionOf(entry)?.[modelsOption];
296
+ if (severityOf(entry) === 0) {
297
+ report(
298
+ configFile,
299
+ `"${ruleId}" is off (or not configured) for "${cwd}", so nothing statically polices the ${String(scopedModels.length)} tenant-scoped model(s). Enable it with \`${modelsOption}\` set to the scoped models.`
300
+ );
301
+ continue;
302
+ }
303
+ if (!Array.isArray(raw) || !raw.every((model) => typeof model === "string")) {
304
+ report(
305
+ configFile,
306
+ `"${ruleId}" resolves with no \`${modelsOption}\` option for "${cwd}", so it falls back to its default and does not police the ${String(scopedModels.length)} tenant-scoped model(s). Pass the scoped models to it.`
307
+ );
308
+ continue;
309
+ }
310
+ const configured = new Set(raw);
311
+ for (const model of scopedModels) {
312
+ if (!configured.has(model)) {
313
+ report(
314
+ configFile,
315
+ `\`${model}\` is tenant-scoped at runtime but is MISSING from the \`${modelsOption}\` option of "${ruleId}", so no static rule polices it. Add it to the list the config passes.`
316
+ );
317
+ }
318
+ }
319
+ for (const model of configured) {
320
+ if (!scopedSet.has(model)) {
321
+ report(
322
+ configFile,
323
+ `\`${model}\` is in the \`${modelsOption}\` option of "${ruleId}" but is not tenant-scoped at runtime, so lint guards a model the runtime boundary does not isolate. Scope it at runtime, or drop it from the list the config passes.`
324
+ );
325
+ }
326
+ }
327
+ }
328
+ const expectedHandScoped = registry.handScopedModels ?? {};
329
+ if (handScopedRule !== null && Object.keys(expectedHandScoped).length > 0) {
330
+ const raw = firstOptionOf(rules[handScopedRule])?.[handScopedOption];
331
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
332
+ report(
333
+ configFile,
334
+ `"${handScopedRule}" resolves with no \`${handScopedOption}\` option for "${cwd}", so the models the extension does NOT scope (${Object.keys(expectedHandScoped).sort().join(", ")}) are policed by nothing. Pass the registry's hand-scope map to it.`
335
+ );
336
+ } else {
337
+ const resolved = fingerprint(raw);
338
+ const expected = fingerprint(expectedHandScoped);
339
+ if (resolved !== expected) {
340
+ report(
341
+ configFile,
342
+ `The \`${handScopedOption}\` option "${handScopedRule}" resolves to does not match the registry (resolved: ${resolved}; expected: ${expected}). Pass the registry map itself rather than a hand-written copy.`
343
+ );
344
+ }
345
+ }
346
+ }
347
+ return violations;
348
+ }
349
+ };
350
+ }
351
+ export {
352
+ createPrismaMethodSurfaceRule,
353
+ createTenantModelRegistryParityRule,
354
+ parseDelegateSurfaces,
355
+ parseObjectLiteralKeys
356
+ };
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/resolved-config.ts
31
+ var resolved_config_exports = {};
32
+ __export(resolved_config_exports, {
33
+ createEslintConfigNoWarnRule: () => createEslintConfigNoWarnRule
34
+ });
35
+ module.exports = __toCommonJS(resolved_config_exports);
36
+
37
+ // src/resolved-config/resolve.ts
38
+ var import_node_fs = require("fs");
39
+ var import_node_path = __toESM(require("path"), 1);
40
+ var import_eslint = require("eslint");
41
+ function severityOf(entry) {
42
+ const raw = Array.isArray(entry) ? entry[0] : entry;
43
+ if (raw === 1 || raw === "warn") return 1;
44
+ if (raw === 2 || raw === "error") return 2;
45
+ return 0;
46
+ }
47
+ async function resolveRules(root, packageDir, probes) {
48
+ let cwd = import_node_path.default.resolve(root, packageDir);
49
+ try {
50
+ cwd = (0, import_node_fs.realpathSync)(cwd);
51
+ } catch (error) {
52
+ return { ok: false, error: String(error) };
53
+ }
54
+ const eslint = new import_eslint.ESLint({ cwd, errorOnUnmatchedPattern: false });
55
+ const rules = [];
56
+ const ignored = [];
57
+ for (const probe of probes) {
58
+ let config;
59
+ try {
60
+ config = await eslint.calculateConfigForFile(import_node_path.default.join(cwd, probe));
61
+ } catch (error) {
62
+ return { ok: false, error: `resolving "${probe}": ${String(error)}` };
63
+ }
64
+ if (config === void 0) ignored.push(probe);
65
+ else rules.push(config.rules ?? {});
66
+ }
67
+ if (rules.length > 0) return { ok: true, rules };
68
+ return {
69
+ ok: false,
70
+ error: probes.length === 0 ? "no probe files are configured" : `every probe (${ignored.map((probe) => `"${probe}"`).join(", ")}) is ignored by the config or matched by no block`
71
+ };
72
+ }
73
+
74
+ // src/resolved-config/eslint-config-no-warn.ts
75
+ var DEFAULT_ID = "eslint-config-no-warn";
76
+ var DEFAULT_PACKAGES = [".", "apps/*", "packages/*"];
77
+ var DEFAULT_CONFIG_FILES = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"];
78
+ var DEFAULT_PROBES = [
79
+ "src/__lint_meta_probe__.ts",
80
+ "src/__lint_meta_probe__.tsx",
81
+ "src/__lint_meta_probe__.test.ts",
82
+ "src/__lint_meta_probe__.test.tsx"
83
+ ];
84
+ function findConfigDirs(ctx, packages, configFiles) {
85
+ const byDir = /* @__PURE__ */ new Map();
86
+ for (const pattern of packages) {
87
+ const base = pattern.replace(/\/+$/u, "");
88
+ for (const name of configFiles) {
89
+ const matches = base === "." || base === "" ? ctx.exists(name) ? [name] : [] : ctx.glob(`${base}/${name}`);
90
+ for (const configFile of matches) {
91
+ const slash = configFile.lastIndexOf("/");
92
+ const dir = slash === -1 ? "." : configFile.slice(0, slash);
93
+ if (!byDir.has(dir)) byDir.set(dir, configFile);
94
+ }
95
+ }
96
+ }
97
+ return [...byDir].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([dir, configFile]) => ({ dir, configFile }));
98
+ }
99
+ function createEslintConfigNoWarnRule(options = {}) {
100
+ const id = options.id ?? DEFAULT_ID;
101
+ const packages = options.packages ?? DEFAULT_PACKAGES;
102
+ const configFiles = options.configFiles ?? DEFAULT_CONFIG_FILES;
103
+ const probes = options.probes ?? DEFAULT_PROBES;
104
+ return {
105
+ id,
106
+ category: "config",
107
+ ciCritical: options.ciCritical ?? true,
108
+ description: 'Every rule in the RESOLVED ESLint config is "error" or "off", never "warn", including severities a spread preset injects.',
109
+ async runAsync(ctx) {
110
+ const results = await Promise.all(
111
+ findConfigDirs(ctx, packages, configFiles).map(async ({ dir, configFile }) => {
112
+ const outcome = await resolveRules(ctx.root, dir, probes);
113
+ if (!outcome.ok) {
114
+ return [
115
+ {
116
+ file: configFile,
117
+ rule: id,
118
+ message: `Could not resolve the effective ESLint config for "${dir}", so its severities cannot be checked (if it imports a workspace package, build that first): ${outcome.error}`
119
+ }
120
+ ];
121
+ }
122
+ const warned = /* @__PURE__ */ new Set();
123
+ for (const rules of outcome.rules) {
124
+ for (const [ruleId, entry] of Object.entries(rules)) {
125
+ if (severityOf(entry) === 1) warned.add(ruleId);
126
+ }
127
+ }
128
+ return [...warned].sort().map(
129
+ (ruleId) => ({
130
+ file: configFile,
131
+ rule: id,
132
+ message: `Rule "${ruleId}" resolves to "warn" in "${dir}". ESLint severities must be "error" or "off", never "warn" (this is the RESOLVED severity, so it may come from a spread preset rather than a literal in the config file). Override it explicitly.`
133
+ })
134
+ );
135
+ })
136
+ );
137
+ return results.flat();
138
+ }
139
+ };
140
+ }
141
+ // Annotate the CommonJS export names for ESM import in node:
142
+ 0 && (module.exports = {
143
+ createEslintConfigNoWarnRule
144
+ });
@@ -0,0 +1,46 @@
1
+ import { IMetaRule } from '@noctcore/harness';
2
+
3
+ /**
4
+ * Options for {@link createEslintConfigNoWarnRule}.
5
+ *
6
+ * Every path is relative to the repo root the harness runs in.
7
+ */
8
+ interface EslintConfigNoWarnOptions {
9
+ /** Rule id, for running more than one instance. Default `eslint-config-no-warn`. */
10
+ readonly id?: string;
11
+ /**
12
+ * Directories whose ESLint config is resolved, as globs (`apps/*`) or plain
13
+ * paths (`.` for the repo root). A match is checked only when it holds one of
14
+ * `configFiles`. Default `['.', 'apps/*', 'packages/*']`.
15
+ */
16
+ readonly packages?: readonly string[];
17
+ /**
18
+ * The file names that mark a directory as owning a flat config. Default
19
+ * `eslint.config.js`, `eslint.config.mjs`, `eslint.config.cjs`.
20
+ */
21
+ readonly configFiles?: readonly string[];
22
+ /**
23
+ * Files, relative to each package, the config is resolved FOR. Resolution is
24
+ * glob matching, so they need not exist; pick one per file shape the config
25
+ * scopes blocks to. Default: a `.ts`, `.tsx`, `.test.ts` and `.test.tsx` file
26
+ * under `src/`.
27
+ */
28
+ readonly probes?: readonly string[];
29
+ /** Whether a violation fails CI. Default `true`. */
30
+ readonly ciCritical?: boolean;
31
+ }
32
+ /**
33
+ * ESLint severities must be `error` or `off`, never `warn`, as RESOLVED.
34
+ *
35
+ * Resolves each package's effective flat config through ESLint's
36
+ * `calculateConfigForFile` and reports every rule that ends up at `warn`. That is
37
+ * the point of the rule: a preset spread into the config (a `recommended` block
38
+ * that ships rules at `warn`) injects the severity without a `"warn"` literal in
39
+ * any file the project owns, so a text scan such as `no-warn-severity` passes it.
40
+ *
41
+ * Fails closed: a config that cannot be resolved at all is a violation, never a
42
+ * silent pass. Needs the optional `eslint` peer.
43
+ */
44
+ declare function createEslintConfigNoWarnRule(options?: EslintConfigNoWarnOptions): IMetaRule;
45
+
46
+ export { type EslintConfigNoWarnOptions, createEslintConfigNoWarnRule };
@@ -0,0 +1,46 @@
1
+ import { IMetaRule } from '@noctcore/harness';
2
+
3
+ /**
4
+ * Options for {@link createEslintConfigNoWarnRule}.
5
+ *
6
+ * Every path is relative to the repo root the harness runs in.
7
+ */
8
+ interface EslintConfigNoWarnOptions {
9
+ /** Rule id, for running more than one instance. Default `eslint-config-no-warn`. */
10
+ readonly id?: string;
11
+ /**
12
+ * Directories whose ESLint config is resolved, as globs (`apps/*`) or plain
13
+ * paths (`.` for the repo root). A match is checked only when it holds one of
14
+ * `configFiles`. Default `['.', 'apps/*', 'packages/*']`.
15
+ */
16
+ readonly packages?: readonly string[];
17
+ /**
18
+ * The file names that mark a directory as owning a flat config. Default
19
+ * `eslint.config.js`, `eslint.config.mjs`, `eslint.config.cjs`.
20
+ */
21
+ readonly configFiles?: readonly string[];
22
+ /**
23
+ * Files, relative to each package, the config is resolved FOR. Resolution is
24
+ * glob matching, so they need not exist; pick one per file shape the config
25
+ * scopes blocks to. Default: a `.ts`, `.tsx`, `.test.ts` and `.test.tsx` file
26
+ * under `src/`.
27
+ */
28
+ readonly probes?: readonly string[];
29
+ /** Whether a violation fails CI. Default `true`. */
30
+ readonly ciCritical?: boolean;
31
+ }
32
+ /**
33
+ * ESLint severities must be `error` or `off`, never `warn`, as RESOLVED.
34
+ *
35
+ * Resolves each package's effective flat config through ESLint's
36
+ * `calculateConfigForFile` and reports every rule that ends up at `warn`. That is
37
+ * the point of the rule: a preset spread into the config (a `recommended` block
38
+ * that ships rules at `warn`) injects the severity without a `"warn"` literal in
39
+ * any file the project owns, so a text scan such as `no-warn-severity` passes it.
40
+ *
41
+ * Fails closed: a config that cannot be resolved at all is a violation, never a
42
+ * silent pass. Needs the optional `eslint` peer.
43
+ */
44
+ declare function createEslintConfigNoWarnRule(options?: EslintConfigNoWarnOptions): IMetaRule;
45
+
46
+ export { type EslintConfigNoWarnOptions, createEslintConfigNoWarnRule };
@@ -0,0 +1,75 @@
1
+ import {
2
+ resolveRules,
3
+ severityOf
4
+ } from "./chunk-VFCX3QKZ.js";
5
+
6
+ // src/resolved-config/eslint-config-no-warn.ts
7
+ var DEFAULT_ID = "eslint-config-no-warn";
8
+ var DEFAULT_PACKAGES = [".", "apps/*", "packages/*"];
9
+ var DEFAULT_CONFIG_FILES = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"];
10
+ var DEFAULT_PROBES = [
11
+ "src/__lint_meta_probe__.ts",
12
+ "src/__lint_meta_probe__.tsx",
13
+ "src/__lint_meta_probe__.test.ts",
14
+ "src/__lint_meta_probe__.test.tsx"
15
+ ];
16
+ function findConfigDirs(ctx, packages, configFiles) {
17
+ const byDir = /* @__PURE__ */ new Map();
18
+ for (const pattern of packages) {
19
+ const base = pattern.replace(/\/+$/u, "");
20
+ for (const name of configFiles) {
21
+ const matches = base === "." || base === "" ? ctx.exists(name) ? [name] : [] : ctx.glob(`${base}/${name}`);
22
+ for (const configFile of matches) {
23
+ const slash = configFile.lastIndexOf("/");
24
+ const dir = slash === -1 ? "." : configFile.slice(0, slash);
25
+ if (!byDir.has(dir)) byDir.set(dir, configFile);
26
+ }
27
+ }
28
+ }
29
+ return [...byDir].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([dir, configFile]) => ({ dir, configFile }));
30
+ }
31
+ function createEslintConfigNoWarnRule(options = {}) {
32
+ const id = options.id ?? DEFAULT_ID;
33
+ const packages = options.packages ?? DEFAULT_PACKAGES;
34
+ const configFiles = options.configFiles ?? DEFAULT_CONFIG_FILES;
35
+ const probes = options.probes ?? DEFAULT_PROBES;
36
+ return {
37
+ id,
38
+ category: "config",
39
+ ciCritical: options.ciCritical ?? true,
40
+ description: 'Every rule in the RESOLVED ESLint config is "error" or "off", never "warn", including severities a spread preset injects.',
41
+ async runAsync(ctx) {
42
+ const results = await Promise.all(
43
+ findConfigDirs(ctx, packages, configFiles).map(async ({ dir, configFile }) => {
44
+ const outcome = await resolveRules(ctx.root, dir, probes);
45
+ if (!outcome.ok) {
46
+ return [
47
+ {
48
+ file: configFile,
49
+ rule: id,
50
+ message: `Could not resolve the effective ESLint config for "${dir}", so its severities cannot be checked (if it imports a workspace package, build that first): ${outcome.error}`
51
+ }
52
+ ];
53
+ }
54
+ const warned = /* @__PURE__ */ new Set();
55
+ for (const rules of outcome.rules) {
56
+ for (const [ruleId, entry] of Object.entries(rules)) {
57
+ if (severityOf(entry) === 1) warned.add(ruleId);
58
+ }
59
+ }
60
+ return [...warned].sort().map(
61
+ (ruleId) => ({
62
+ file: configFile,
63
+ rule: id,
64
+ message: `Rule "${ruleId}" resolves to "warn" in "${dir}". ESLint severities must be "error" or "off", never "warn" (this is the RESOLVED severity, so it may come from a spread preset rather than a literal in the config file). Override it explicitly.`
65
+ })
66
+ );
67
+ })
68
+ );
69
+ return results.flat();
70
+ }
71
+ };
72
+ }
73
+ export {
74
+ createEslintConfigNoWarnRule
75
+ };