@sarj/eslint-plugin 16.36.3 → 16.36.6
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/index.cjs +79 -15
- package/dist/index.js +79 -15
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1928,7 +1928,7 @@ var NO_TEST_COMMENTS_DOCUMENTATION = {
|
|
|
1928
1928
|
autofix: "safe",
|
|
1929
1929
|
limitations: [
|
|
1930
1930
|
"Checks actual line and block comment tokens in recognized maintained test files, including test fixtures; strings and template text remain unchanged. Generated/vendor files are excluded.",
|
|
1931
|
-
"Legal notices, shebangs, TypeScript triple-slash references, coverage/build annotations and explicit lint/type/test/formatter directives remain allowed.
|
|
1931
|
+
"Legal notices, shebangs, TypeScript triple-slash references, coverage/build annotations and explicit lint/type/test/formatter directives remain allowed. Executable detect-secrets 1.5.0 allowlist/whitelist pragmas retain its case-sensitive delimiter, spacing, physical-line and next-line grammar; arbitrary pragma prose remains ordinary commentary. Rationale on an executable directive remains the owning tool's responsibility.",
|
|
1932
1932
|
"The automatic fix removes comments and adjacent padding while retaining every line ending and necessary token boundary; it does not rename tests or remove executable statements. Bare integration/e2e directories and ambiguous hyphen/underscore test basenames require another concrete test-path signal or a runtime testing-framework import; ordinary production integration modules and rule implementations ending in -test are preserved."
|
|
1933
1933
|
],
|
|
1934
1934
|
examples: [
|
|
@@ -1939,6 +1939,52 @@ var NO_TEST_COMMENTS_DOCUMENTATION = {
|
|
|
1939
1939
|
var DIRECTIVE = /^(?:eslint(?:-\S+)?\b|(?:biome|oxlint|jshint|jslint|c8|v8|istanbul|vitest|prettier|dprint)\b|@(?:ts-(?:check|nocheck|expect-error|ignore)|vitest-environment|jest-environment|jsx\w*|license|preserve)\b|[#!]\s*__(?:PURE|NO_SIDE_EFFECTS)__|\/\s*<(?:reference|amd-module|amd-dependency)\b)/u;
|
|
1940
1940
|
var LEGAL = /(?:^|\n)\s*\*?\s*(?:Copyright\b|SPDX-License-Identifier:|Licensed under\b|@license\b|@preserve\b)/iu;
|
|
1941
1941
|
var TYPE_CONTRACT = /@(?:type|typedef|template|param|returns?|satisfies|callback|overload|implements|extends|augments|enum|this|import)\b/u;
|
|
1942
|
+
var DETECT_SECRETS_COMMENT_MARKERS = [
|
|
1943
|
+
{ start: "#", end: "" },
|
|
1944
|
+
{ start: "//", end: "" },
|
|
1945
|
+
{ start: "/\\*", end: " *\\*/" },
|
|
1946
|
+
{ start: "'", end: "" },
|
|
1947
|
+
{ start: "--", end: "" },
|
|
1948
|
+
{ start: "<!--[# \\t]*?", end: " *?-->" }
|
|
1949
|
+
];
|
|
1950
|
+
var DETECT_SECRETS_PRAGMAS = DETECT_SECRETS_COMMENT_MARKERS.flatMap(({ start, end }) => [
|
|
1951
|
+
{ nextLine: false, pattern: new RegExp(`[ \\t]*${start} *pragma: ?(?:allow|white)list[ -]secret[^\\n]*?${end}[ \\t]*(?![\\s\\S])`, "u") },
|
|
1952
|
+
{ nextLine: true, pattern: new RegExp(`^[ \\t]*${start} *pragma: ?allowlist[ -]nextline[ -]secret[^\\n]*?${end}[ \\t]*(?![\\s\\S])`, "u") }
|
|
1953
|
+
]);
|
|
1954
|
+
var PYTHON_WHITESPACE = new RegExp("\\p{White_Space}", "u");
|
|
1955
|
+
var PYTHON_EXTRA_WHITESPACE = /* @__PURE__ */ new Set(["", "", "", ""]);
|
|
1956
|
+
function hasDetectSecretsPragma(comment, text) {
|
|
1957
|
+
if (!comment.value.includes("pragma:")) return false;
|
|
1958
|
+
let lineStart = Math.max(text.lastIndexOf("\r", comment.range[0] - 1), text.lastIndexOf("\n", comment.range[0] - 1)) + 1;
|
|
1959
|
+
while (lineStart < comment.range[1]) {
|
|
1960
|
+
const nextCr = text.indexOf("\r", lineStart);
|
|
1961
|
+
const nextLf = text.indexOf("\n", lineStart);
|
|
1962
|
+
const lineEnd = Math.min(nextCr < 0 ? text.length : nextCr, nextLf < 0 ? text.length : nextLf);
|
|
1963
|
+
if (hasPragmaInReaderLine(comment, text.slice(lineStart, lineEnd), lineStart)) return true;
|
|
1964
|
+
lineStart = lineEnd + (text[lineEnd] === "\r" && text[lineEnd + 1] === "\n" ? 2 : 1);
|
|
1965
|
+
}
|
|
1966
|
+
return false;
|
|
1967
|
+
}
|
|
1968
|
+
function hasPragmaInReaderLine(comment, rawLine, lineStart) {
|
|
1969
|
+
const sameLine = stripPythonTrailingWhitespace(rawLine);
|
|
1970
|
+
for (const { nextLine, pattern } of DETECT_SECRETS_PRAGMAS) {
|
|
1971
|
+
const column = nextLine ? 0 : Math.max(0, comment.range[0] - lineStart);
|
|
1972
|
+
const match = pattern.exec((nextLine ? rawLine : sameLine).slice(column));
|
|
1973
|
+
if (match === null) continue;
|
|
1974
|
+
const pragmaStart = lineStart + column + match.index + match[0].indexOf("pragma:");
|
|
1975
|
+
if (pragmaStart >= comment.range[0] && pragmaStart < comment.range[1]) return true;
|
|
1976
|
+
}
|
|
1977
|
+
return false;
|
|
1978
|
+
}
|
|
1979
|
+
function stripPythonTrailingWhitespace(value) {
|
|
1980
|
+
let end = value.length;
|
|
1981
|
+
while (end > 0) {
|
|
1982
|
+
const character = value.charAt(end - 1);
|
|
1983
|
+
if (!PYTHON_WHITESPACE.test(character) && !PYTHON_EXTRA_WHITESPACE.has(character)) break;
|
|
1984
|
+
end--;
|
|
1985
|
+
}
|
|
1986
|
+
return value.slice(0, end);
|
|
1987
|
+
}
|
|
1942
1988
|
var no_test_comments_default = createRule({
|
|
1943
1989
|
name: "no-test-comments",
|
|
1944
1990
|
documentation: NO_TEST_COMMENTS_DOCUMENTATION,
|
|
@@ -1975,7 +2021,7 @@ var no_test_comments_default = createRule({
|
|
|
1975
2021
|
}
|
|
1976
2022
|
for (const [index, comment] of comments.entries()) {
|
|
1977
2023
|
const body3 = comment.value.trim().replace(/^\*\s*/u, "");
|
|
1978
|
-
if (DIRECTIVE.test(body3) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value)) continue;
|
|
2024
|
+
if (DIRECTIVE.test(body3) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value) || hasDetectSecretsPragma(comment, context.sourceCode.text)) continue;
|
|
1979
2025
|
context.report({
|
|
1980
2026
|
node: comment,
|
|
1981
2027
|
messageId: "testComment",
|
|
@@ -2268,8 +2314,13 @@ function commentSuppressesChecking(text, kind) {
|
|
|
2268
2314
|
|
|
2269
2315
|
// src/rules/_closed-object-contract.ts
|
|
2270
2316
|
var import_typescript2 = __toESM(require("typescript"), 1);
|
|
2271
|
-
function closedObjectContract(type, checker) {
|
|
2272
|
-
return visitContract(type, {
|
|
2317
|
+
function closedObjectContract(type, checker, singlePropertyOrigins = false) {
|
|
2318
|
+
return visitContract(type, {
|
|
2319
|
+
checker,
|
|
2320
|
+
active: /* @__PURE__ */ new Set(),
|
|
2321
|
+
completed: /* @__PURE__ */ new WeakMap(),
|
|
2322
|
+
origins: singlePropertyOrigins ? /* @__PURE__ */ new Map() : void 0
|
|
2323
|
+
});
|
|
2273
2324
|
}
|
|
2274
2325
|
function visitContract(type, proof) {
|
|
2275
2326
|
const known2 = proof.completed.get(type);
|
|
@@ -2283,10 +2334,20 @@ function visitContract(type, proof) {
|
|
|
2283
2334
|
}
|
|
2284
2335
|
function completeObject(type, proof) {
|
|
2285
2336
|
if (!ordinaryObject(type, proof.checker)) return false;
|
|
2337
|
+
if (proof.origins && !singleOrigins(type, proof.origins)) return false;
|
|
2286
2338
|
const declarations = type.getSymbol()?.getDeclarations()?.filter((declaration) => import_typescript2.default.isInterfaceDeclaration(declaration) || import_typescript2.default.isTypeLiteralNode(declaration));
|
|
2287
2339
|
if (!declarations?.length) return false;
|
|
2288
2340
|
return declarations.every((declaration) => completeDeclaration(declaration, proof));
|
|
2289
2341
|
}
|
|
2342
|
+
function singleOrigins(type, origins) {
|
|
2343
|
+
for (const property of type.getProperties()) {
|
|
2344
|
+
const declarations = property.getDeclarations();
|
|
2345
|
+
const declaration = declarations?.[0];
|
|
2346
|
+
if (!declaration || declarations?.length !== 1 || origins.has(property.name) && origins.get(property.name) !== declaration) return false;
|
|
2347
|
+
origins.set(property.name, declaration);
|
|
2348
|
+
}
|
|
2349
|
+
return true;
|
|
2350
|
+
}
|
|
2290
2351
|
function ordinaryObject(type, checker) {
|
|
2291
2352
|
if ((type.flags & import_typescript2.default.TypeFlags.Object) === 0 || type.isUnion() || checker.getIndexInfosOfType(type).length > 0) return false;
|
|
2292
2353
|
const object = type;
|
|
@@ -2605,6 +2666,9 @@ function directEval(node) {
|
|
|
2605
2666
|
function primitiveKeyword(node) {
|
|
2606
2667
|
return node !== void 0 && (node.kind === import_typescript6.default.SyntaxKind.StringKeyword || node.kind === import_typescript6.default.SyntaxKind.NumberKeyword || node.kind === import_typescript6.default.SyntaxKind.BooleanKeyword || node.kind === import_typescript6.default.SyntaxKind.BigIntKeyword);
|
|
2607
2668
|
}
|
|
2669
|
+
function nullishPrimitiveUnion(node) {
|
|
2670
|
+
return node !== void 0 && import_typescript6.default.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === import_typescript6.default.SyntaxKind.UndefinedKeyword || import_typescript6.default.isLiteralTypeNode(part) && part.literal.kind === import_typescript6.default.SyntaxKind.NullKeyword);
|
|
2671
|
+
}
|
|
2608
2672
|
function consumerAnnotation(node) {
|
|
2609
2673
|
if (node.params.length !== 1) return void 0;
|
|
2610
2674
|
const parameter = node.params[0];
|
|
@@ -2662,6 +2726,7 @@ function directCalls(binding3) {
|
|
|
2662
2726
|
// src/rules/no-redundant-consumer-union.ts
|
|
2663
2727
|
var CONFIG = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
|
|
2664
2728
|
var EXAMPLE = 'export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: "Ada", breed: "Collie"}; function label(value: Dog | Animal): string {return value.name;} label(dog);';
|
|
2729
|
+
var NULLABLE_EXAMPLE = EXAMPLE.replace("readonly name: string", "readonly name: string | null").replace("return value.name;", "return value.name ?? '';");
|
|
2665
2730
|
var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
|
|
2666
2731
|
defaultLevel: "warning",
|
|
2667
2732
|
summary: "Keep the covering object contract in a private scalar consumer union.",
|
|
@@ -2672,21 +2737,23 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
|
|
|
2672
2737
|
limitations: [
|
|
2673
2738
|
"Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Removed named types/imports must retain an independent top-level exported type-alias or direct type-export read. Other removable annotations/assertions, the covering annotation and own-definition/self references cannot keep one another live; otherwise simultaneous fixes can introduce unused-type errors. The exact authored covering type and terminator token sequence are retained; edits that would merge distinct CR and LF terminators remain manual; no producer, call or function body is rewritten.",
|
|
2674
2739
|
"Only an inline binary parameter union in a strict module's top-level private scalar consumer is supported. Exactly one parameter, an explicit broad primitive return annotation, a single pure returned scalar expression and direct identifier-argument callers are required. Type queries or type-only exports of the callable, public APIs, callbacks, contextual signatures, overloads, generics, decorators, async functions, direct eval and fresh-object callers prevent findings.",
|
|
2675
|
-
"Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, callbacks and recursive or nested object fields are unsupported.",
|
|
2740
|
+
"Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties, optionally paired directly with null or undefined in an exact two-part union. Required and optional nullable fields may be read under the same native coverage proof. Every field must have one actual declaration origin throughout each alternative's inheritance/intersection graph. Shared ancestors retain that origin; repeated merged fields, explicit overrides and independently declared overlapping fields abstain, including valid compatible declarations. This excludes recovered conflicting properties without claiming whole-source health. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, parenthesized or more complex property unions, callbacks and recursive or nested object fields are unsupported.",
|
|
2676
2741
|
"Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
|
|
2677
2742
|
"General union simplification can change Extract, conditional types, in-narrowing, fresh-literal checking and public presentation. Those contexts are excluded even when ordinary assignment succeeds. Flat destructuring counts every bound field as read; whole-object escape, mutation, helpers, closures, reflection, serialization, dynamic reads, implicit Object capabilities and JSX prevent findings. Imported Python protocol assignability is not approximated by this rule."
|
|
2678
2743
|
],
|
|
2679
2744
|
examples: [
|
|
2680
2745
|
{ id: "covered-alternative", title: "The private consumer needs the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE.replace("Dog | Animal", "Animal") }] },
|
|
2681
|
-
{ id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
2746
|
+
{ id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
|
|
2747
|
+
{ id: "nullable-covered-alternative", scenarioId: "nullable-readonly-contract", title: "A nullable name retains the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("Dog | Animal", "Animal") }] },
|
|
2748
|
+
{ id: "nullable-domain-contract", scenarioId: "nullable-readonly-contract", title: "A literal breed keeps the authored domain contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("readonly breed: string", "readonly breed: 'Collie'") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
2682
2749
|
]
|
|
2683
2750
|
};
|
|
2684
2751
|
function scalarFields(type, checker) {
|
|
2685
|
-
if (!closedObjectContract(type, checker)) return null;
|
|
2752
|
+
if (!closedObjectContract(type, checker, true)) return null;
|
|
2686
2753
|
const fields2 = /* @__PURE__ */ new Map();
|
|
2687
2754
|
for (const property of type.getProperties()) {
|
|
2688
2755
|
const declarations = property.getDeclarations();
|
|
2689
|
-
if (!declarations?.length || !declarations.every((member3) => import_typescript7.default.isPropertySignature(member3) && member3.name && (import_typescript7.default.isIdentifier(member3.name) || import_typescript7.default.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === import_typescript7.default.SyntaxKind.ReadonlyKeyword && primitiveKeyword(member3.type))) return null;
|
|
2756
|
+
if (!declarations?.length || !declarations.every((member3) => import_typescript7.default.isPropertySignature(member3) && member3.name && (import_typescript7.default.isIdentifier(member3.name) || import_typescript7.default.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === import_typescript7.default.SyntaxKind.ReadonlyKeyword && (primitiveKeyword(member3.type) || nullishPrimitiveUnion(member3.type)))) return null;
|
|
2690
2757
|
fields2.set(property.name, (property.flags & import_typescript7.default.SymbolFlags.Optional) === 0);
|
|
2691
2758
|
}
|
|
2692
2759
|
return fields2.size ? fields2 : null;
|
|
@@ -2773,7 +2840,7 @@ var import_typescript8 = __toESM(require("typescript"), 1);
|
|
|
2773
2840
|
var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
|
|
2774
2841
|
var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
|
|
2775
2842
|
var HELPER_EXAMPLE = 'export {}; const account = {name: "Ada", email: "a"}; function formatName(name: string): string {return name.toUpperCase();} function label(user: {email: string; name: string}): string {return formatName(user.name);} label(account);';
|
|
2776
|
-
var
|
|
2843
|
+
var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
|
|
2777
2844
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
2778
2845
|
defaultLevel: "warning",
|
|
2779
2846
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -2792,8 +2859,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
2792
2859
|
{ id: "used-fields", title: "Every required dependency is read", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE2.replace("return user.name;", "return user.name + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
|
|
2793
2860
|
{ id: "scalar-helper", scenarioId: "local-scalar-helper", title: "Only the name crosses a local scalar helper call", outcome: "match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("email: string;", "") }] },
|
|
2794
2861
|
{ id: "helper-and-other-field", scenarioId: "local-scalar-helper", title: "The consumer still requires a separately read field", outcome: "no-match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("return formatName(user.name);", "return formatName(user.name) + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
|
|
2795
|
-
{ id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source:
|
|
2796
|
-
{ id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source:
|
|
2862
|
+
{ id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2 }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("email: string | null;", "") }] },
|
|
2863
|
+
{ id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
2797
2864
|
]
|
|
2798
2865
|
};
|
|
2799
2866
|
function inlineFields(node, reads) {
|
|
@@ -2807,9 +2874,6 @@ function inlineFields(node, reads) {
|
|
|
2807
2874
|
}
|
|
2808
2875
|
return fields2;
|
|
2809
2876
|
}
|
|
2810
|
-
function nullishPrimitiveUnion(node) {
|
|
2811
|
-
return node !== void 0 && import_typescript8.default.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === import_typescript8.default.SyntaxKind.UndefinedKeyword || import_typescript8.default.isLiteralTypeNode(part) && part.literal.kind === import_typescript8.default.SyntaxKind.NullKeyword);
|
|
2812
|
-
}
|
|
2813
2877
|
var prefer_minimal_parameter_contract_default = createRule({
|
|
2814
2878
|
name: "prefer-minimal-parameter-contract",
|
|
2815
2879
|
documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
|
|
@@ -3961,7 +4025,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
3961
4025
|
// package.json
|
|
3962
4026
|
var package_default = {
|
|
3963
4027
|
name: "@sarj/eslint-plugin",
|
|
3964
|
-
version: "16.36.
|
|
4028
|
+
version: "16.36.6",
|
|
3965
4029
|
packageManager: "npm@12.2.1",
|
|
3966
4030
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
3967
4031
|
type: "module",
|
package/dist/index.js
CHANGED
|
@@ -1877,7 +1877,7 @@ var NO_TEST_COMMENTS_DOCUMENTATION = {
|
|
|
1877
1877
|
autofix: "safe",
|
|
1878
1878
|
limitations: [
|
|
1879
1879
|
"Checks actual line and block comment tokens in recognized maintained test files, including test fixtures; strings and template text remain unchanged. Generated/vendor files are excluded.",
|
|
1880
|
-
"Legal notices, shebangs, TypeScript triple-slash references, coverage/build annotations and explicit lint/type/test/formatter directives remain allowed.
|
|
1880
|
+
"Legal notices, shebangs, TypeScript triple-slash references, coverage/build annotations and explicit lint/type/test/formatter directives remain allowed. Executable detect-secrets 1.5.0 allowlist/whitelist pragmas retain its case-sensitive delimiter, spacing, physical-line and next-line grammar; arbitrary pragma prose remains ordinary commentary. Rationale on an executable directive remains the owning tool's responsibility.",
|
|
1881
1881
|
"The automatic fix removes comments and adjacent padding while retaining every line ending and necessary token boundary; it does not rename tests or remove executable statements. Bare integration/e2e directories and ambiguous hyphen/underscore test basenames require another concrete test-path signal or a runtime testing-framework import; ordinary production integration modules and rule implementations ending in -test are preserved."
|
|
1882
1882
|
],
|
|
1883
1883
|
examples: [
|
|
@@ -1888,6 +1888,52 @@ var NO_TEST_COMMENTS_DOCUMENTATION = {
|
|
|
1888
1888
|
var DIRECTIVE = /^(?:eslint(?:-\S+)?\b|(?:biome|oxlint|jshint|jslint|c8|v8|istanbul|vitest|prettier|dprint)\b|@(?:ts-(?:check|nocheck|expect-error|ignore)|vitest-environment|jest-environment|jsx\w*|license|preserve)\b|[#!]\s*__(?:PURE|NO_SIDE_EFFECTS)__|\/\s*<(?:reference|amd-module|amd-dependency)\b)/u;
|
|
1889
1889
|
var LEGAL = /(?:^|\n)\s*\*?\s*(?:Copyright\b|SPDX-License-Identifier:|Licensed under\b|@license\b|@preserve\b)/iu;
|
|
1890
1890
|
var TYPE_CONTRACT = /@(?:type|typedef|template|param|returns?|satisfies|callback|overload|implements|extends|augments|enum|this|import)\b/u;
|
|
1891
|
+
var DETECT_SECRETS_COMMENT_MARKERS = [
|
|
1892
|
+
{ start: "#", end: "" },
|
|
1893
|
+
{ start: "//", end: "" },
|
|
1894
|
+
{ start: "/\\*", end: " *\\*/" },
|
|
1895
|
+
{ start: "'", end: "" },
|
|
1896
|
+
{ start: "--", end: "" },
|
|
1897
|
+
{ start: "<!--[# \\t]*?", end: " *?-->" }
|
|
1898
|
+
];
|
|
1899
|
+
var DETECT_SECRETS_PRAGMAS = DETECT_SECRETS_COMMENT_MARKERS.flatMap(({ start, end }) => [
|
|
1900
|
+
{ nextLine: false, pattern: new RegExp(`[ \\t]*${start} *pragma: ?(?:allow|white)list[ -]secret[^\\n]*?${end}[ \\t]*(?![\\s\\S])`, "u") },
|
|
1901
|
+
{ nextLine: true, pattern: new RegExp(`^[ \\t]*${start} *pragma: ?allowlist[ -]nextline[ -]secret[^\\n]*?${end}[ \\t]*(?![\\s\\S])`, "u") }
|
|
1902
|
+
]);
|
|
1903
|
+
var PYTHON_WHITESPACE = new RegExp("\\p{White_Space}", "u");
|
|
1904
|
+
var PYTHON_EXTRA_WHITESPACE = /* @__PURE__ */ new Set(["", "", "", ""]);
|
|
1905
|
+
function hasDetectSecretsPragma(comment, text) {
|
|
1906
|
+
if (!comment.value.includes("pragma:")) return false;
|
|
1907
|
+
let lineStart = Math.max(text.lastIndexOf("\r", comment.range[0] - 1), text.lastIndexOf("\n", comment.range[0] - 1)) + 1;
|
|
1908
|
+
while (lineStart < comment.range[1]) {
|
|
1909
|
+
const nextCr = text.indexOf("\r", lineStart);
|
|
1910
|
+
const nextLf = text.indexOf("\n", lineStart);
|
|
1911
|
+
const lineEnd = Math.min(nextCr < 0 ? text.length : nextCr, nextLf < 0 ? text.length : nextLf);
|
|
1912
|
+
if (hasPragmaInReaderLine(comment, text.slice(lineStart, lineEnd), lineStart)) return true;
|
|
1913
|
+
lineStart = lineEnd + (text[lineEnd] === "\r" && text[lineEnd + 1] === "\n" ? 2 : 1);
|
|
1914
|
+
}
|
|
1915
|
+
return false;
|
|
1916
|
+
}
|
|
1917
|
+
function hasPragmaInReaderLine(comment, rawLine, lineStart) {
|
|
1918
|
+
const sameLine = stripPythonTrailingWhitespace(rawLine);
|
|
1919
|
+
for (const { nextLine, pattern } of DETECT_SECRETS_PRAGMAS) {
|
|
1920
|
+
const column = nextLine ? 0 : Math.max(0, comment.range[0] - lineStart);
|
|
1921
|
+
const match = pattern.exec((nextLine ? rawLine : sameLine).slice(column));
|
|
1922
|
+
if (match === null) continue;
|
|
1923
|
+
const pragmaStart = lineStart + column + match.index + match[0].indexOf("pragma:");
|
|
1924
|
+
if (pragmaStart >= comment.range[0] && pragmaStart < comment.range[1]) return true;
|
|
1925
|
+
}
|
|
1926
|
+
return false;
|
|
1927
|
+
}
|
|
1928
|
+
function stripPythonTrailingWhitespace(value) {
|
|
1929
|
+
let end = value.length;
|
|
1930
|
+
while (end > 0) {
|
|
1931
|
+
const character = value.charAt(end - 1);
|
|
1932
|
+
if (!PYTHON_WHITESPACE.test(character) && !PYTHON_EXTRA_WHITESPACE.has(character)) break;
|
|
1933
|
+
end--;
|
|
1934
|
+
}
|
|
1935
|
+
return value.slice(0, end);
|
|
1936
|
+
}
|
|
1891
1937
|
var no_test_comments_default = createRule({
|
|
1892
1938
|
name: "no-test-comments",
|
|
1893
1939
|
documentation: NO_TEST_COMMENTS_DOCUMENTATION,
|
|
@@ -1924,7 +1970,7 @@ var no_test_comments_default = createRule({
|
|
|
1924
1970
|
}
|
|
1925
1971
|
for (const [index, comment] of comments.entries()) {
|
|
1926
1972
|
const body3 = comment.value.trim().replace(/^\*\s*/u, "");
|
|
1927
|
-
if (DIRECTIVE.test(body3) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value)) continue;
|
|
1973
|
+
if (DIRECTIVE.test(body3) || LEGAL.test(comment.value) || legalLines.has(index) || TYPE_CONTRACT.test(comment.value) || hasDetectSecretsPragma(comment, context.sourceCode.text)) continue;
|
|
1928
1974
|
context.report({
|
|
1929
1975
|
node: comment,
|
|
1930
1976
|
messageId: "testComment",
|
|
@@ -2217,8 +2263,13 @@ function commentSuppressesChecking(text, kind) {
|
|
|
2217
2263
|
|
|
2218
2264
|
// src/rules/_closed-object-contract.ts
|
|
2219
2265
|
import ts2 from "typescript";
|
|
2220
|
-
function closedObjectContract(type, checker) {
|
|
2221
|
-
return visitContract(type, {
|
|
2266
|
+
function closedObjectContract(type, checker, singlePropertyOrigins = false) {
|
|
2267
|
+
return visitContract(type, {
|
|
2268
|
+
checker,
|
|
2269
|
+
active: /* @__PURE__ */ new Set(),
|
|
2270
|
+
completed: /* @__PURE__ */ new WeakMap(),
|
|
2271
|
+
origins: singlePropertyOrigins ? /* @__PURE__ */ new Map() : void 0
|
|
2272
|
+
});
|
|
2222
2273
|
}
|
|
2223
2274
|
function visitContract(type, proof) {
|
|
2224
2275
|
const known2 = proof.completed.get(type);
|
|
@@ -2232,10 +2283,20 @@ function visitContract(type, proof) {
|
|
|
2232
2283
|
}
|
|
2233
2284
|
function completeObject(type, proof) {
|
|
2234
2285
|
if (!ordinaryObject(type, proof.checker)) return false;
|
|
2286
|
+
if (proof.origins && !singleOrigins(type, proof.origins)) return false;
|
|
2235
2287
|
const declarations = type.getSymbol()?.getDeclarations()?.filter((declaration) => ts2.isInterfaceDeclaration(declaration) || ts2.isTypeLiteralNode(declaration));
|
|
2236
2288
|
if (!declarations?.length) return false;
|
|
2237
2289
|
return declarations.every((declaration) => completeDeclaration(declaration, proof));
|
|
2238
2290
|
}
|
|
2291
|
+
function singleOrigins(type, origins) {
|
|
2292
|
+
for (const property of type.getProperties()) {
|
|
2293
|
+
const declarations = property.getDeclarations();
|
|
2294
|
+
const declaration = declarations?.[0];
|
|
2295
|
+
if (!declaration || declarations?.length !== 1 || origins.has(property.name) && origins.get(property.name) !== declaration) return false;
|
|
2296
|
+
origins.set(property.name, declaration);
|
|
2297
|
+
}
|
|
2298
|
+
return true;
|
|
2299
|
+
}
|
|
2239
2300
|
function ordinaryObject(type, checker) {
|
|
2240
2301
|
if ((type.flags & ts2.TypeFlags.Object) === 0 || type.isUnion() || checker.getIndexInfosOfType(type).length > 0) return false;
|
|
2241
2302
|
const object = type;
|
|
@@ -2554,6 +2615,9 @@ function directEval(node) {
|
|
|
2554
2615
|
function primitiveKeyword(node) {
|
|
2555
2616
|
return node !== void 0 && (node.kind === ts6.SyntaxKind.StringKeyword || node.kind === ts6.SyntaxKind.NumberKeyword || node.kind === ts6.SyntaxKind.BooleanKeyword || node.kind === ts6.SyntaxKind.BigIntKeyword);
|
|
2556
2617
|
}
|
|
2618
|
+
function nullishPrimitiveUnion(node) {
|
|
2619
|
+
return node !== void 0 && ts6.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === ts6.SyntaxKind.UndefinedKeyword || ts6.isLiteralTypeNode(part) && part.literal.kind === ts6.SyntaxKind.NullKeyword);
|
|
2620
|
+
}
|
|
2557
2621
|
function consumerAnnotation(node) {
|
|
2558
2622
|
if (node.params.length !== 1) return void 0;
|
|
2559
2623
|
const parameter = node.params[0];
|
|
@@ -2611,6 +2675,7 @@ function directCalls(binding3) {
|
|
|
2611
2675
|
// src/rules/no-redundant-consumer-union.ts
|
|
2612
2676
|
var CONFIG = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
|
|
2613
2677
|
var EXAMPLE = 'export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: "Ada", breed: "Collie"}; function label(value: Dog | Animal): string {return value.name;} label(dog);';
|
|
2678
|
+
var NULLABLE_EXAMPLE = EXAMPLE.replace("readonly name: string", "readonly name: string | null").replace("return value.name;", "return value.name ?? '';");
|
|
2614
2679
|
var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
|
|
2615
2680
|
defaultLevel: "warning",
|
|
2616
2681
|
summary: "Keep the covering object contract in a private scalar consumer union.",
|
|
@@ -2621,21 +2686,23 @@ var NO_REDUNDANT_CONSUMER_UNION_DOCUMENTATION = {
|
|
|
2621
2686
|
limitations: [
|
|
2622
2687
|
"Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Removed named types/imports must retain an independent top-level exported type-alias or direct type-export read. Other removable annotations/assertions, the covering annotation and own-definition/self references cannot keep one another live; otherwise simultaneous fixes can introduce unused-type errors. The exact authored covering type and terminator token sequence are retained; edits that would merge distinct CR and LF terminators remain manual; no producer, call or function body is rewritten.",
|
|
2623
2688
|
"Only an inline binary parameter union in a strict module's top-level private scalar consumer is supported. Exactly one parameter, an explicit broad primitive return annotation, a single pure returned scalar expression and direct identifier-argument callers are required. Type queries or type-only exports of the callable, public APIs, callbacks, contextual signatures, overloads, generics, decorators, async functions, direct eval and fresh-object callers prevent findings.",
|
|
2624
|
-
"Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, callbacks and recursive or nested object fields are unsupported.",
|
|
2689
|
+
"Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties, optionally paired directly with null or undefined in an exact two-part union. Required and optional nullable fields may be read under the same native coverage proof. Every field must have one actual declaration origin throughout each alternative's inheritance/intersection graph. Shared ancestors retain that origin; repeated merged fields, explicit overrides and independently declared overlapping fields abstain, including valid compatible declarations. This excludes recovered conflicting properties without claiming whole-source health. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, parenthesized or more complex property unions, callbacks and recursive or nested object fields are unsupported.",
|
|
2625
2690
|
"Reports only proper native assignability: covered is assignable to covering and the reverse is false. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any also cannot witness coverage. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; mutually assignable but nonidentical structures are excluded.",
|
|
2626
2691
|
"General union simplification can change Extract, conditional types, in-narrowing, fresh-literal checking and public presentation. Those contexts are excluded even when ordinary assignment succeeds. Flat destructuring counts every bound field as read; whole-object escape, mutation, helpers, closures, reflection, serialization, dynamic reads, implicit Object capabilities and JSX prevent findings. Imported Python protocol assignability is not approximated by this rule."
|
|
2627
2692
|
],
|
|
2628
2693
|
examples: [
|
|
2629
2694
|
{ id: "covered-alternative", title: "The private consumer needs the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: EXAMPLE.replace("Dog | Animal", "Animal") }] },
|
|
2630
|
-
{ id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
2695
|
+
{ id: "public-contract", title: "A public union retains its observable alternatives", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE.replace("function label", "export function label") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true },
|
|
2696
|
+
{ id: "nullable-covered-alternative", scenarioId: "nullable-readonly-contract", title: "A nullable name retains the covering Animal contract", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE }, CONFIG], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("Dog | Animal", "Animal") }] },
|
|
2697
|
+
{ id: "nullable-domain-contract", scenarioId: "nullable-readonly-contract", title: "A literal breed keeps the authored domain contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE.replace("readonly breed: string", "readonly breed: 'Collie'") }, CONFIG], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
2631
2698
|
]
|
|
2632
2699
|
};
|
|
2633
2700
|
function scalarFields(type, checker) {
|
|
2634
|
-
if (!closedObjectContract(type, checker)) return null;
|
|
2701
|
+
if (!closedObjectContract(type, checker, true)) return null;
|
|
2635
2702
|
const fields2 = /* @__PURE__ */ new Map();
|
|
2636
2703
|
for (const property of type.getProperties()) {
|
|
2637
2704
|
const declarations = property.getDeclarations();
|
|
2638
|
-
if (!declarations?.length || !declarations.every((member3) => ts7.isPropertySignature(member3) && member3.name && (ts7.isIdentifier(member3.name) || ts7.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === ts7.SyntaxKind.ReadonlyKeyword && primitiveKeyword(member3.type))) return null;
|
|
2705
|
+
if (!declarations?.length || !declarations.every((member3) => ts7.isPropertySignature(member3) && member3.name && (ts7.isIdentifier(member3.name) || ts7.isStringLiteralLike(member3.name)) && member3.modifiers?.length === 1 && member3.modifiers[0]?.kind === ts7.SyntaxKind.ReadonlyKeyword && (primitiveKeyword(member3.type) || nullishPrimitiveUnion(member3.type)))) return null;
|
|
2639
2706
|
fields2.set(property.name, (property.flags & ts7.SymbolFlags.Optional) === 0);
|
|
2640
2707
|
}
|
|
2641
2708
|
return fields2.size ? fields2 : null;
|
|
@@ -2722,7 +2789,7 @@ import ts8 from "typescript";
|
|
|
2722
2789
|
var CONFIG2 = { path: "tsconfig.json", source: '{"compilerOptions":{"strict":true,"target":"ESNext","module":"ESNext","moduleResolution":"Bundler"}}' };
|
|
2723
2790
|
var EXAMPLE2 = 'export {}; const account = {name: "Ada", email: "a"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);';
|
|
2724
2791
|
var HELPER_EXAMPLE = 'export {}; const account = {name: "Ada", email: "a"}; function formatName(name: string): string {return name.toUpperCase();} function label(user: {email: string; name: string}): string {return formatName(user.name);} label(account);';
|
|
2725
|
-
var
|
|
2792
|
+
var NULLABLE_EXAMPLE2 = 'export {}; const account = {name: "Ada", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);';
|
|
2726
2793
|
var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
2727
2794
|
defaultLevel: "warning",
|
|
2728
2795
|
summary: "Prefer the proven read-only subset for a private consumer parameter.",
|
|
@@ -2741,8 +2808,8 @@ var PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION = {
|
|
|
2741
2808
|
{ id: "used-fields", title: "Every required dependency is read", outcome: "no-match", files: [{ path: "src/case.ts", source: EXAMPLE2.replace("return user.name;", "return user.name + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
|
|
2742
2809
|
{ id: "scalar-helper", scenarioId: "local-scalar-helper", title: "Only the name crosses a local scalar helper call", outcome: "match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("email: string;", "") }] },
|
|
2743
2810
|
{ id: "helper-and-other-field", scenarioId: "local-scalar-helper", title: "The consumer still requires a separately read field", outcome: "no-match", files: [{ path: "src/case.ts", source: HELPER_EXAMPLE.replace("return formatName(user.name);", "return formatName(user.name) + user.email;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true },
|
|
2744
|
-
{ id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source:
|
|
2745
|
-
{ id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source:
|
|
2811
|
+
{ id: "unread-nullable-field", scenarioId: "nullable-private-field", title: "A required nullable field is never inspected", outcome: "match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2 }, CONFIG2], focusPath: "src/case.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("email: string | null;", "") }] },
|
|
2812
|
+
{ id: "read-nullable-field", scenarioId: "nullable-private-field", title: "An inspected nullable field keeps its contract", outcome: "no-match", files: [{ path: "src/case.ts", source: NULLABLE_EXAMPLE2.replace("return user.name;", "return user.email ?? user.name;") }, CONFIG2], focusPath: "src/case.ts", expectedCount: 0, public: true }
|
|
2746
2813
|
]
|
|
2747
2814
|
};
|
|
2748
2815
|
function inlineFields(node, reads) {
|
|
@@ -2756,9 +2823,6 @@ function inlineFields(node, reads) {
|
|
|
2756
2823
|
}
|
|
2757
2824
|
return fields2;
|
|
2758
2825
|
}
|
|
2759
|
-
function nullishPrimitiveUnion(node) {
|
|
2760
|
-
return node !== void 0 && ts8.isUnionTypeNode(node) && node.types.length === 2 && node.types.some(primitiveKeyword) && node.types.some((part) => part.kind === ts8.SyntaxKind.UndefinedKeyword || ts8.isLiteralTypeNode(part) && part.literal.kind === ts8.SyntaxKind.NullKeyword);
|
|
2761
|
-
}
|
|
2762
2826
|
var prefer_minimal_parameter_contract_default = createRule({
|
|
2763
2827
|
name: "prefer-minimal-parameter-contract",
|
|
2764
2828
|
documentation: PREFER_MINIMAL_PARAMETER_CONTRACT_DOCUMENTATION,
|
|
@@ -3914,7 +3978,7 @@ function isSearchParamReceiver(call, services) {
|
|
|
3914
3978
|
// package.json
|
|
3915
3979
|
var package_default = {
|
|
3916
3980
|
name: "@sarj/eslint-plugin",
|
|
3917
|
-
version: "16.36.
|
|
3981
|
+
version: "16.36.6",
|
|
3918
3982
|
packageManager: "npm@12.2.1",
|
|
3919
3983
|
description: "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
3920
3984
|
type: "module",
|