@starci/eslint-canon-fe 9.3.0 → 9.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/CHANGELOG.md +9 -0
- package/README.md +5 -1
- package/dist/brand-values.d.ts +28 -0
- package/dist/brand-values.js +172 -0
- package/dist/button-intent.d.ts +11 -0
- package/dist/button-intent.js +361 -0
- package/dist/callback-shape.d.ts +7 -0
- package/dist/callback-shape.js +396 -0
- package/dist/class-names.d.ts +19 -0
- package/dist/class-names.js +170 -0
- package/dist/client-boundary.d.ts +42 -0
- package/dist/client-boundary.js +404 -0
- package/dist/comments.d.ts +47 -0
- package/dist/comments.js +156 -0
- package/dist/control-height.d.ts +7 -0
- package/dist/control-height.js +83 -0
- package/dist/drawing.d.ts +31 -0
- package/dist/drawing.js +376 -0
- package/dist/env-owner.d.ts +22 -0
- package/dist/env-owner.js +291 -0
- package/dist/fe-stack.d.ts +10 -0
- package/dist/fe-stack.js +73 -0
- package/dist/file-layout.d.ts +71 -0
- package/dist/file-layout.js +465 -0
- package/dist/formatting.d.ts +25 -0
- package/dist/formatting.js +104 -0
- package/dist/function-budget.d.ts +9 -0
- package/dist/function-budget.js +48 -0
- package/dist/grammar-shim.d.ts +5 -0
- package/dist/grammar-shim.js +126 -0
- package/dist/hooks-folder.d.ts +34 -0
- package/dist/hooks-folder.js +320 -0
- package/dist/hygiene.d.ts +39 -0
- package/dist/hygiene.js +540 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +310 -0
- package/{landmark.mjs → dist/landmark.d.ts} +4 -6
- package/dist/landmark.js +6 -0
- package/dist/lib/ast.d.ts +36 -0
- package/dist/lib/ast.js +94 -0
- package/dist/lib/bindings.d.ts +84 -0
- package/dist/lib/bindings.js +168 -0
- package/dist/lib/class-text.d.ts +3 -0
- package/dist/lib/class-text.js +30 -0
- package/dist/lib/config.d.ts +81 -0
- package/dist/lib/config.js +141 -0
- package/dist/lib/drawing-scope.d.ts +5 -0
- package/{lib/drawing-scope.mjs → dist/lib/drawing-scope.js} +6 -11
- package/dist/lib/eslint-boundary.d.ts +23 -0
- package/dist/lib/eslint-boundary.js +6 -0
- package/dist/lib/fe-stack.d.ts +49 -0
- package/dist/lib/fe-stack.js +98 -0
- package/dist/lib/hfs-view.d.ts +74 -0
- package/dist/lib/hfs-view.js +6 -0
- package/dist/lib/hfs.d.ts +34 -0
- package/dist/lib/hfs.js +72 -0
- package/dist/lib/next.d.ts +4 -0
- package/dist/lib/next.js +10 -0
- package/dist/lib/own-hook-origin.d.ts +17 -0
- package/dist/lib/own-hook-origin.js +52 -0
- package/dist/lib/package-root.d.ts +4 -0
- package/dist/lib/package-root.js +10 -0
- package/dist/lib/path.d.ts +2 -0
- package/{lib/path.mjs → dist/lib/path.js} +1 -1
- package/dist/lib/platform.d.ts +26 -0
- package/{lib/platform.mjs → dist/lib/platform.js} +38 -35
- package/dist/lib/program-for.d.ts +5 -0
- package/dist/lib/program-for.js +23 -0
- package/dist/lib/pure-owner-helper.d.ts +10 -0
- package/dist/lib/pure-owner-helper.js +156 -0
- package/dist/lib/react.d.ts +16 -0
- package/dist/lib/react.js +22 -0
- package/dist/lib/redux-hook-origin.d.ts +4 -0
- package/dist/lib/redux-hook-origin.js +50 -0
- package/dist/lib/rule.d.ts +28 -0
- package/dist/lib/rule.js +5 -0
- package/dist/lib/scope.d.ts +88 -0
- package/{lib/scope.mjs → dist/lib/scope.js} +83 -93
- package/dist/lib/typed-class-values.d.ts +13 -0
- package/dist/lib/typed-class-values.js +92 -0
- package/dist/lib/types.d.ts +44 -0
- package/dist/lib/types.js +64 -0
- package/dist/lib/why.d.ts +350 -0
- package/dist/lib/why.js +350 -0
- package/dist/lint-escape-hatch.d.ts +24 -0
- package/{lint-escape-hatch.mjs → dist/lint-escape-hatch.js} +38 -45
- package/dist/lists.d.ts +31 -0
- package/dist/lists.js +202 -0
- package/dist/loading.d.ts +34 -0
- package/dist/loading.js +157 -0
- package/dist/naming.d.ts +33 -0
- package/dist/naming.js +176 -0
- package/dist/native-controls.d.ts +24 -0
- package/dist/native-controls.js +105 -0
- package/dist/next-conventions.d.ts +48 -0
- package/dist/next-conventions.js +452 -0
- package/dist/project-graph.d.ts +5 -0
- package/{project-graph.mjs → dist/project-graph.js} +8 -12
- package/dist/props-and-slots.d.ts +19 -0
- package/dist/props-and-slots.js +320 -0
- package/dist/served-locale.d.ts +38 -0
- package/dist/served-locale.js +263 -0
- package/dist/shape-slot.d.ts +18 -0
- package/dist/shape-slot.js +324 -0
- package/dist/size-and-state-budget.d.ts +46 -0
- package/dist/size-and-state-budget.js +424 -0
- package/dist/size-growth.d.ts +24 -0
- package/dist/size-growth.js +64 -0
- package/dist/sonar-parity.d.ts +5 -0
- package/{sonar-parity.mjs → dist/sonar-parity.js} +11 -7
- package/dist/surface-rules.d.ts +18 -0
- package/dist/surface-rules.js +491 -0
- package/dist/swr-family.d.ts +15 -0
- package/dist/swr-family.js +419 -0
- package/dist/the-split.d.ts +27 -0
- package/dist/the-split.js +157 -0
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.js +35 -0
- package/dist/translation.d.ts +51 -0
- package/dist/translation.js +403 -0
- package/dist/transport.d.ts +73 -0
- package/dist/transport.js +885 -0
- package/dist/type-safety.d.ts +43 -0
- package/dist/type-safety.js +139 -0
- package/dist/ui-rulings.d.ts +11 -0
- package/dist/ui-rulings.js +121 -0
- package/package.json +31 -9
- package/runtime/knowledge/hfs/canon-pins.yaml +17 -10
- package/brand-values.mjs +0 -172
- package/button-intent.mjs +0 -320
- package/callback-shape.mjs +0 -332
- package/class-names.mjs +0 -149
- package/client-boundary.mjs +0 -400
- package/comments.mjs +0 -159
- package/control-height.mjs +0 -77
- package/drawing.mjs +0 -321
- package/env-owner.mjs +0 -244
- package/fe-stack.mjs +0 -86
- package/file-layout.mjs +0 -472
- package/formatting.mjs +0 -110
- package/function-budget.mjs +0 -49
- package/grammar-shim.mjs +0 -123
- package/hooks-folder.mjs +0 -300
- package/hygiene.mjs +0 -513
- package/index.mjs +0 -317
- package/lib/ast.mjs +0 -97
- package/lib/bindings.mjs +0 -159
- package/lib/class-text.mjs +0 -29
- package/lib/config.mjs +0 -142
- package/lib/fe-stack.mjs +0 -77
- package/lib/hfs.mjs +0 -49
- package/lib/next.mjs +0 -6
- package/lib/own-hook-origin.mjs +0 -53
- package/lib/program-for.mjs +0 -22
- package/lib/pure-owner-helper.mjs +0 -148
- package/lib/react.mjs +0 -28
- package/lib/redux-hook-origin.mjs +0 -44
- package/lib/typed-class-values.mjs +0 -81
- package/lib/types.mjs +0 -63
- package/lib/why.mjs +0 -352
- package/lists.mjs +0 -193
- package/loading.mjs +0 -156
- package/naming.mjs +0 -185
- package/native-controls.mjs +0 -108
- package/next-conventions.mjs +0 -447
- package/props-and-slots.mjs +0 -276
- package/served-locale.mjs +0 -243
- package/size-and-state-budget.mjs +0 -376
- package/size-growth.mjs +0 -62
- package/surface-rules.mjs +0 -443
- package/swr-family.mjs +0 -326
- package/the-split.mjs +0 -155
- package/tokens.mjs +0 -39
- package/translation.mjs +0 -392
- package/transport.mjs +0 -830
- package/type-safety.mjs +0 -147
- package/ui-rulings.mjs +0 -134
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `type-safety.md`.
|
|
3
|
+
*
|
|
4
|
+
* FOUR RULES, ONE IDEA: the compiler is told the truth or it is not told at all. The double cast, the
|
|
5
|
+
* plain assertion, the non-null assertion and `any` are the four spellings of "trust me". They live
|
|
6
|
+
* here rather than in a repository's own `eslint.config.mjs` because a repository carries no rule of
|
|
7
|
+
* its own: a rule that exists only in one repository's config is a rule the next repository forgets.
|
|
8
|
+
* The array spelling is a formatting question and stays with the formatter.
|
|
9
|
+
*
|
|
10
|
+
* THE EXEMPTION IS A SPEC, and it has to be. Proving a closed API refuses bad input means building
|
|
11
|
+
* bad input, and there is no way to construct a value the types forbid without telling the compiler
|
|
12
|
+
* to forget them. A judgement-based exemption would be argued at every call site; a spec is decided
|
|
13
|
+
* once, by its name.
|
|
14
|
+
*/
|
|
15
|
+
import { type Rules, type Recommended } from "./lib/rule.js";
|
|
16
|
+
/** A cast through `unknown` erases what the compiler knew, at the seam where it mattered. */
|
|
17
|
+
export declare const noDoubleCast: import("./lib/rule.js").Rule<"double", []>;
|
|
18
|
+
/**
|
|
19
|
+
* A type assertion is a claim the compiler cannot check, made at the place it was going to check.
|
|
20
|
+
*
|
|
21
|
+
* WHAT IS LEFT ALLOWED. `as const` (it narrows), `as unknown` (it widens, and everything is assignable
|
|
22
|
+
* to it), and `satisfies` (it checks). The cast through `unknown` to a real type is refused by
|
|
23
|
+
* `no-double-cast`, so this rule leaves the outer half of that pair to it and reports each once.
|
|
24
|
+
*/
|
|
25
|
+
export declare const noTypeAssertion: import("./lib/rule.js").Rule<"assertion", []>;
|
|
26
|
+
/** `x!` says a value is present without proving it. */
|
|
27
|
+
export declare const noNonNullAssertion: import("./lib/rule.js").Rule<"bang", []>;
|
|
28
|
+
/** `any` switches checking off for everything it touches. */
|
|
29
|
+
export declare const noExplicitAny: import("./lib/rule.js").Rule<"any", []>;
|
|
30
|
+
/** The rules this law contributes to the plugin. */
|
|
31
|
+
export declare const rules: Rules;
|
|
32
|
+
/**
|
|
33
|
+
* The level this law asks for, as the plugin's own opinion.
|
|
34
|
+
*
|
|
35
|
+
* Exact - it matches one syntactic shape - but a repository adopting it with history should expect
|
|
36
|
+
* each report to be real work: a double cast is usually load-bearing by the time anybody notices,
|
|
37
|
+
* and removing one means giving the value a shape it did not have.
|
|
38
|
+
*
|
|
39
|
+
* `any` and the array spelling are NOT published here. They belong to the TypeScript plugin's own
|
|
40
|
+
* rules, which a consuming repository already has: `@typescript-eslint/no-explicit-any` and
|
|
41
|
+
* `@typescript-eslint/array-type` with the generic default.
|
|
42
|
+
*/
|
|
43
|
+
export declare const recommended: Recommended;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `type-safety.md`.
|
|
3
|
+
*
|
|
4
|
+
* FOUR RULES, ONE IDEA: the compiler is told the truth or it is not told at all. The double cast, the
|
|
5
|
+
* plain assertion, the non-null assertion and `any` are the four spellings of "trust me". They live
|
|
6
|
+
* here rather than in a repository's own `eslint.config.mjs` because a repository carries no rule of
|
|
7
|
+
* its own: a rule that exists only in one repository's config is a rule the next repository forgets.
|
|
8
|
+
* The array spelling is a formatting question and stays with the formatter.
|
|
9
|
+
*
|
|
10
|
+
* THE EXEMPTION IS A SPEC, and it has to be. Proving a closed API refuses bad input means building
|
|
11
|
+
* bad input, and there is no way to construct a value the types forbid without telling the compiler
|
|
12
|
+
* to forget them. A judgement-based exemption would be argued at every call site; a spec is decided
|
|
13
|
+
* once, by its name.
|
|
14
|
+
*/
|
|
15
|
+
import { defineRule } from "./lib/rule.js";
|
|
16
|
+
import { isProductSource } from "./lib/scope.js";
|
|
17
|
+
/** True when a type node is the `unknown` keyword. */
|
|
18
|
+
const isUnknown = (node) => Boolean(node) && node?.type === "TSUnknownKeyword";
|
|
19
|
+
// -- TYPE-SAFETY-1 ---------------------------------------------------------------------------------
|
|
20
|
+
/** A cast through `unknown` erases what the compiler knew, at the seam where it mattered. */
|
|
21
|
+
export const noDoubleCast = defineRule({
|
|
22
|
+
meta: {
|
|
23
|
+
type: "problem",
|
|
24
|
+
docs: { description: "No cast through `unknown`; it turns checking off at the boundary." },
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
double: "Casting through `unknown` tells the compiler to forget everything it knew about this value, because the two types have nothing in common. That is not a narrowing - a narrowing is a claim the compiler can still partly check - it is an erasure, and the seam it erases is the one worth checking: where a value crosses from outside the program to inside it. Narrow from `unknown` with a check the compiler can follow, or fix the shape.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
if (!isProductSource(context))
|
|
32
|
+
return {};
|
|
33
|
+
return {
|
|
34
|
+
TSAsExpression(node) {
|
|
35
|
+
// The outer cast of a `x as unknown as T` pair: its operand is itself a cast to `unknown`.
|
|
36
|
+
const inner = node.expression;
|
|
37
|
+
if (!inner || inner.type !== "TSAsExpression")
|
|
38
|
+
return;
|
|
39
|
+
if (!isUnknown(inner.typeAnnotation))
|
|
40
|
+
return;
|
|
41
|
+
context.report({ node, messageId: "double" });
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
// -- TYPE-SAFETY-2 ---------------------------------------------------------------------------------
|
|
47
|
+
/** True for `as const`, the one assertion that narrows without claiming anything. */
|
|
48
|
+
const isConstAssertion = (type) => type.type === "TSTypeReference" && type.typeName.type === "Identifier" && type.typeName.name === "const";
|
|
49
|
+
/**
|
|
50
|
+
* A type assertion is a claim the compiler cannot check, made at the place it was going to check.
|
|
51
|
+
*
|
|
52
|
+
* WHAT IS LEFT ALLOWED. `as const` (it narrows), `as unknown` (it widens, and everything is assignable
|
|
53
|
+
* to it), and `satisfies` (it checks). The cast through `unknown` to a real type is refused by
|
|
54
|
+
* `no-double-cast`, so this rule leaves the outer half of that pair to it and reports each once.
|
|
55
|
+
*/
|
|
56
|
+
export const noTypeAssertion = defineRule({
|
|
57
|
+
meta: {
|
|
58
|
+
type: "problem",
|
|
59
|
+
docs: { description: "No `as T` or `<T>x`; narrow with a check, a guard or a parser." },
|
|
60
|
+
schema: [],
|
|
61
|
+
messages: {
|
|
62
|
+
assertion: "`as {{type}}` tells the compiler to believe something it could not verify. When the claim is wrong the type system keeps saying it is right and the failure moves to a reader. Narrow with a check the compiler can follow (a type guard, `in`, a discriminant), parse the value where it enters, or use `satisfies` when the goal is only to check a literal.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
create(context) {
|
|
66
|
+
if (!isProductSource(context))
|
|
67
|
+
return {};
|
|
68
|
+
const source = context.sourceCode ?? context.getSourceCode();
|
|
69
|
+
const report = (node) => context.report({ node, messageId: "assertion", data: { type: source.getText(node.typeAnnotation) } });
|
|
70
|
+
return {
|
|
71
|
+
TSAsExpression(node) {
|
|
72
|
+
if (isConstAssertion(node.typeAnnotation) || isUnknown(node.typeAnnotation))
|
|
73
|
+
return;
|
|
74
|
+
const inner = node.expression;
|
|
75
|
+
if (inner && inner.type === "TSAsExpression" && isUnknown(inner.typeAnnotation))
|
|
76
|
+
return;
|
|
77
|
+
report(node);
|
|
78
|
+
},
|
|
79
|
+
TSTypeAssertion(node) {
|
|
80
|
+
if (isConstAssertion(node.typeAnnotation))
|
|
81
|
+
return;
|
|
82
|
+
report(node);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
// -- TYPE-SAFETY-3 ---------------------------------------------------------------------------------
|
|
88
|
+
/** `x!` says a value is present without proving it. */
|
|
89
|
+
export const noNonNullAssertion = defineRule({
|
|
90
|
+
meta: {
|
|
91
|
+
type: "problem",
|
|
92
|
+
docs: { description: "No non-null assertion (`x!`); prove the value is there." },
|
|
93
|
+
schema: [],
|
|
94
|
+
messages: {
|
|
95
|
+
bang: "`!` asserts the value is not null or undefined and proves nothing: the day it is one, the failure is a TypeError in a reader's browser rather than a branch in the code. Handle the absent case (`if`, `??`, an early return) or make the type say it cannot be absent.",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
create(context) {
|
|
99
|
+
if (!isProductSource(context))
|
|
100
|
+
return {};
|
|
101
|
+
return { TSNonNullExpression: (node) => context.report({ node, messageId: "bang" }) };
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
// -- TYPE-SAFETY-4 ---------------------------------------------------------------------------------
|
|
105
|
+
/** `any` switches checking off for everything it touches. */
|
|
106
|
+
export const noExplicitAny = defineRule({
|
|
107
|
+
meta: {
|
|
108
|
+
type: "problem",
|
|
109
|
+
docs: { description: "No `any`; use a real type or `unknown` and narrow." },
|
|
110
|
+
schema: [],
|
|
111
|
+
messages: {
|
|
112
|
+
any: "`any` turns type checking off for this value and everything derived from it, silently. Use the real type, a generic, or `unknown` and narrow it where it is used.",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
create(context) {
|
|
116
|
+
if (!isProductSource(context))
|
|
117
|
+
return {};
|
|
118
|
+
return { TSAnyKeyword: (node) => context.report({ node, messageId: "any" }) };
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
/** The rules this law contributes to the plugin. */
|
|
122
|
+
export const rules = {
|
|
123
|
+
"no-double-cast": noDoubleCast,
|
|
124
|
+
"no-type-assertion": noTypeAssertion,
|
|
125
|
+
"no-non-null-assertion": noNonNullAssertion,
|
|
126
|
+
"no-explicit-any": noExplicitAny,
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* The level this law asks for, as the plugin's own opinion.
|
|
130
|
+
*
|
|
131
|
+
* Exact - it matches one syntactic shape - but a repository adopting it with history should expect
|
|
132
|
+
* each report to be real work: a double cast is usually load-bearing by the time anybody notices,
|
|
133
|
+
* and removing one means giving the value a shape it did not have.
|
|
134
|
+
*
|
|
135
|
+
* `any` and the array spelling are NOT published here. They belong to the TypeScript plugin's own
|
|
136
|
+
* rules, which a consuming repository already has: `@typescript-eslint/no-explicit-any` and
|
|
137
|
+
* `@typescript-eslint/array-type` with the generic default.
|
|
138
|
+
*/
|
|
139
|
+
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Recommended } from "./lib/rule.js";
|
|
2
|
+
/** Navigation-like single selection uses the row fill instead of a second check indicator. */
|
|
3
|
+
export declare const selectionFillNoCheck: import("./lib/rule.js").Rule<"indicator", []>;
|
|
4
|
+
/** Grammar internal selectors belong to Grammar, including selectors in colocated class declarations. */
|
|
5
|
+
export declare const noGrammarInternalSelector: import("./lib/rule.js").Rule<"internal", []>;
|
|
6
|
+
export declare const rules: {
|
|
7
|
+
"selection-fill-no-check": import("./lib/rule.js").Rule<"indicator", []>;
|
|
8
|
+
"no-grammar-internal-selector": import("./lib/rule.js").Rule<"internal", []>;
|
|
9
|
+
};
|
|
10
|
+
/** Both owner rulings are universal errors in the frontend canon. */
|
|
11
|
+
export declare const recommended: Recommended;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { attribute, attributeValue, staticText } from "./lib/ast.js";
|
|
2
|
+
import { importOf } from "./lib/bindings.js";
|
|
3
|
+
import { defineRule } from "./lib/rule.js";
|
|
4
|
+
import { isProductSource } from "./lib/scope.js";
|
|
5
|
+
import { typedClassStrings } from "./lib/typed-class-values.js";
|
|
6
|
+
/** Resolve a named or namespace import at its use, preserving lexical shadowing. */
|
|
7
|
+
const grammarComponent = (context, opening, expected) => {
|
|
8
|
+
const name = opening.name;
|
|
9
|
+
const root = name.type === "JSXMemberExpression" ? name.object : name;
|
|
10
|
+
if (root.type !== "JSXIdentifier")
|
|
11
|
+
return false;
|
|
12
|
+
const binding = importOf(context, root);
|
|
13
|
+
if (!binding ||
|
|
14
|
+
!/^@starci\/grammar(?:\/(?:common|core|offset-pop))?$/.test(binding.source))
|
|
15
|
+
return false;
|
|
16
|
+
return name.type === "JSXMemberExpression"
|
|
17
|
+
? binding.imported === null && name.property.name === expected
|
|
18
|
+
: binding.imported === expected;
|
|
19
|
+
};
|
|
20
|
+
/** Only a final explicit attribute is proof when an opaque spread could override a value. */
|
|
21
|
+
/** True when `text` is one of `values`; a missing text never is. */
|
|
22
|
+
const oneOf = (values, text) => text !== null && values.includes(text);
|
|
23
|
+
/** The plain tag name of a JSX element (`nav`), or null for a member or namespaced name. */
|
|
24
|
+
const plainTag = (name) => (name.type === "JSXIdentifier" ? name.name : null);
|
|
25
|
+
const propText = (opening, key) => {
|
|
26
|
+
let value = null;
|
|
27
|
+
for (const entry of opening.attributes) {
|
|
28
|
+
if (entry.type === "JSXSpreadAttribute")
|
|
29
|
+
value = null;
|
|
30
|
+
else if (entry.name.name === key)
|
|
31
|
+
value = staticText(entry.value);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
/** Navigation and rail/list placement are structurally identifiable without guessing from product names. */
|
|
36
|
+
const navigationList = (context, opening) => {
|
|
37
|
+
for (let node = opening.parent; node; node = node.parent) {
|
|
38
|
+
if (node.type !== "JSXElement")
|
|
39
|
+
continue;
|
|
40
|
+
const parent = node.openingElement;
|
|
41
|
+
if (oneOf(["nav", "ul", "ol"], plainTag(parent.name)) ||
|
|
42
|
+
oneOf(["navigation", "list"], propText(parent, "role")))
|
|
43
|
+
return true;
|
|
44
|
+
if (["Rail", "Sidebar"].some((role) => grammarComponent(context, parent, role)))
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
/** Navigation-like single selection uses the row fill instead of a second check indicator. */
|
|
50
|
+
export const selectionFillNoCheck = defineRule({
|
|
51
|
+
meta: {
|
|
52
|
+
type: "problem",
|
|
53
|
+
docs: {
|
|
54
|
+
description: "Navigation-like single-select ListBox rows use selectionIndicator=none.",
|
|
55
|
+
},
|
|
56
|
+
schema: [],
|
|
57
|
+
messages: {
|
|
58
|
+
indicator: 'Use selectionIndicator="none" on this navigation-like single-select ListBox; the row fill carries selection.',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
create(context) {
|
|
62
|
+
return {
|
|
63
|
+
JSXOpeningElement(node) {
|
|
64
|
+
if (!grammarComponent(context, node, "ListBox") ||
|
|
65
|
+
!navigationList(context, node))
|
|
66
|
+
return;
|
|
67
|
+
if (oneOf(["none", "multiple"], propText(node, "selectionMode")))
|
|
68
|
+
return;
|
|
69
|
+
if (propText(node, "selectionIndicator") !== "none")
|
|
70
|
+
context.report({ node, messageId: "indicator" });
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
/** Grammar internal selectors belong to Grammar, including selectors in colocated class declarations. */
|
|
76
|
+
export const noGrammarInternalSelector = defineRule({
|
|
77
|
+
meta: {
|
|
78
|
+
type: "problem",
|
|
79
|
+
docs: {
|
|
80
|
+
description: "Application styles cannot target starci-core internals.",
|
|
81
|
+
},
|
|
82
|
+
schema: [],
|
|
83
|
+
messages: {
|
|
84
|
+
internal: "Remove this starci-core- selector. Compose the Grammar public API; message actions belong in the ChatBubble floating toolbar.",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
create(context) {
|
|
88
|
+
if (!isProductSource(context))
|
|
89
|
+
return {};
|
|
90
|
+
const selector = /(?:\.|_|\[class[^\]]*)starci-core-/;
|
|
91
|
+
const report = (node, text) => {
|
|
92
|
+
if (text && selector.test(text))
|
|
93
|
+
context.report({ node, messageId: "internal" });
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
Literal(node) {
|
|
97
|
+
if (typeof node.value === "string")
|
|
98
|
+
report(node, node.value);
|
|
99
|
+
},
|
|
100
|
+
TemplateElement(node) {
|
|
101
|
+
report(node, node.value.cooked);
|
|
102
|
+
},
|
|
103
|
+
JSXAttribute(node) {
|
|
104
|
+
if (node.name.name !== "className")
|
|
105
|
+
return;
|
|
106
|
+
const value = attributeValue(attribute(node.parent, "className"));
|
|
107
|
+
if (value?.type !== "Identifier" && value?.type !== "MemberExpression")
|
|
108
|
+
return;
|
|
109
|
+
const strings = typedClassStrings(context, value, "classes");
|
|
110
|
+
if (strings.some((entry) => selector.test(entry.text)))
|
|
111
|
+
context.report({ node, messageId: "internal" });
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
export const rules = {
|
|
117
|
+
"selection-fill-no-check": selectionFillNoCheck,
|
|
118
|
+
"no-grammar-internal-selector": noGrammarInternalSelector,
|
|
119
|
+
};
|
|
120
|
+
/** Both owner rulings are universal errors in the frontend canon. */
|
|
121
|
+
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-fe",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.0",
|
|
4
4
|
"description": "Universal front-end lint laws for TypeScript and Next.js, independent of the selected Grammar family.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,18 +16,34 @@
|
|
|
16
16
|
},
|
|
17
17
|
"private": false,
|
|
18
18
|
"exports": {
|
|
19
|
-
".":
|
|
20
|
-
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./lib/*.mjs": {
|
|
24
|
+
"types": "./dist/lib/*.d.ts",
|
|
25
|
+
"default": "./dist/lib/*.js"
|
|
26
|
+
},
|
|
27
|
+
"./lib/*.js": {
|
|
28
|
+
"types": "./dist/lib/*.d.ts",
|
|
29
|
+
"default": "./dist/lib/*.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
21
32
|
},
|
|
22
|
-
"
|
|
33
|
+
"imports": {
|
|
34
|
+
"#runtime/*": {
|
|
35
|
+
"types": "./src/types/runtime/*",
|
|
36
|
+
"default": "./runtime/*"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"main": "./dist/index.js",
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
23
41
|
"files": [
|
|
24
42
|
"LICENSE",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
43
|
+
"dist/**/*.js",
|
|
44
|
+
"dist/**/*.d.ts",
|
|
27
45
|
"runtime/**",
|
|
28
46
|
"docs/*.md",
|
|
29
|
-
"*.ts",
|
|
30
|
-
"!*.spec.mjs",
|
|
31
47
|
"CHANGELOG.md",
|
|
32
48
|
"README.md"
|
|
33
49
|
],
|
|
@@ -43,7 +59,10 @@
|
|
|
43
59
|
"access": "public"
|
|
44
60
|
},
|
|
45
61
|
"scripts": {
|
|
46
|
-
"
|
|
62
|
+
"build": "node -e \"require('node:fs').rmSync('dist', {recursive: true, force: true})\" && tsc -p tsconfig.build.json && node -e \"const fs = require('node:fs'); for (const f of fs.readdirSync('dist', {recursive: true}).filter((n) => n.endsWith('.d.ts'))) fs.writeFileSync('dist/' + f, fs.readFileSync('dist/' + f, 'utf8').replace(/((?:from |import\\()['\\x22]\\.\\.?\\/[^'\\x22]+)\\.ts(['\\x22])/g, (all, head, quote) => head + '.js' + quote))\"",
|
|
63
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
64
|
+
"test": "node --test \"src/**/*.spec.ts\"",
|
|
65
|
+
"prepack": "npm run typecheck && npm run build && npm test"
|
|
47
66
|
},
|
|
48
67
|
"keywords": [
|
|
49
68
|
"eslint",
|
|
@@ -54,10 +73,13 @@
|
|
|
54
73
|
"dependencies": {
|
|
55
74
|
"@typescript-eslint/eslint-plugin": "^8.70.0",
|
|
56
75
|
"@typescript-eslint/parser": "^8.70.0",
|
|
76
|
+
"@typescript-eslint/utils": "^8.72.1",
|
|
57
77
|
"eslint-plugin-sonarjs": "4.2.1",
|
|
58
78
|
"eslint-plugin-unicorn": "65.0.0"
|
|
59
79
|
},
|
|
60
80
|
"devDependencies": {
|
|
81
|
+
"@starci/tsconfig": "file:../tsconfig",
|
|
82
|
+
"@types/node": "^24.19.2",
|
|
61
83
|
"eslint": "^9.18.0",
|
|
62
84
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
63
85
|
"typescript": "^6.0.3"
|
|
@@ -30,61 +30,61 @@ pins:
|
|
|
30
30
|
source: packages/event-stream/package.json
|
|
31
31
|
why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
|
|
32
32
|
'@starci/grammar':
|
|
33
|
-
version: 0.
|
|
33
|
+
version: 0.15.0
|
|
34
34
|
group: starci
|
|
35
35
|
install: registry
|
|
36
36
|
side: fe
|
|
37
37
|
source: packages/grammar/package.json
|
|
38
38
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
39
39
|
'@starci/eslint-canon-be':
|
|
40
|
-
version: 3.
|
|
40
|
+
version: 3.6.0
|
|
41
41
|
group: starci
|
|
42
42
|
install: registry
|
|
43
43
|
side: be
|
|
44
44
|
source: packages/eslint-canon-be/package.json
|
|
45
45
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
46
46
|
'@starci/eslint-canon-fe':
|
|
47
|
-
version: 9.
|
|
47
|
+
version: 9.4.0
|
|
48
48
|
group: starci
|
|
49
49
|
install: registry
|
|
50
50
|
side: fe
|
|
51
51
|
source: packages/eslint-canon-fe/package.json
|
|
52
52
|
why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
|
|
53
53
|
'@starci/eslint-grammar-core':
|
|
54
|
-
version: 0.
|
|
54
|
+
version: 0.15.0
|
|
55
55
|
group: starci
|
|
56
56
|
install: registry
|
|
57
57
|
side: fe
|
|
58
58
|
source: packages/eslint-grammar-core/package.json
|
|
59
59
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
60
60
|
'@starci/eslint-grammar-offset-pop':
|
|
61
|
-
version: 0.
|
|
61
|
+
version: 0.15.0
|
|
62
62
|
group: starci
|
|
63
63
|
install: registry
|
|
64
64
|
side: fe
|
|
65
65
|
source: packages/eslint-grammar-offset-pop/package.json
|
|
66
66
|
why: 'the Offset Pop Grammar component API lint, versioned with @starci/grammar; its shared component contract comes from the StarCi family lint without copying rules.'
|
|
67
67
|
'@starci/stylelint-canon':
|
|
68
|
-
version: 2.1
|
|
68
|
+
version: 2.2.1
|
|
69
69
|
group: starci
|
|
70
70
|
install: registry
|
|
71
71
|
side: fe
|
|
72
72
|
source: packages/stylelint-canon/package.json
|
|
73
73
|
why: 'the front-end stylelint canon: no-class-selector refuses a compound class selector (`div.card`); the brand layer accepts `--font-sans` and `--font-mono`; status-contrast measures the HeroUI soft pairs per theme; brand-layer-shape reads the shared :root,.light,.dark block and the info soft pair; appTokens come from loadAppTokens(import.meta.url) for the managed one-line stylelint.config.mjs.'
|
|
74
74
|
'@starci/tsconfig':
|
|
75
|
-
version: 2.0
|
|
75
|
+
version: 2.1.0
|
|
76
76
|
group: starci
|
|
77
77
|
install: registry
|
|
78
78
|
side: both
|
|
79
79
|
source: packages/tsconfig/package.json
|
|
80
80
|
'@starci/prettier-config':
|
|
81
|
-
version: 1.0
|
|
81
|
+
version: 1.1.0
|
|
82
82
|
group: starci
|
|
83
83
|
install: registry
|
|
84
84
|
side: both
|
|
85
85
|
source: packages/prettier-config/package.json
|
|
86
86
|
'@starci/jest-preset':
|
|
87
|
-
version: 2.
|
|
87
|
+
version: 2.4.1
|
|
88
88
|
group: starci
|
|
89
89
|
install: registry
|
|
90
90
|
side: be
|
|
@@ -97,6 +97,13 @@ pins:
|
|
|
97
97
|
side: be
|
|
98
98
|
source: packages/test-world/package.json
|
|
99
99
|
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
100
|
+
'@starci/ai-router':
|
|
101
|
+
version: 0.2.1
|
|
102
|
+
group: starci
|
|
103
|
+
install: registry
|
|
104
|
+
side: be
|
|
105
|
+
source: packages/ai-router/package.json
|
|
106
|
+
why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
100
107
|
'starci-harness':
|
|
101
108
|
version: 1.1.0-alpha.1
|
|
102
109
|
group: starci
|
|
@@ -105,7 +112,7 @@ pins:
|
|
|
105
112
|
source: cli/package.json
|
|
106
113
|
why: 'the one StarCi command line (bins starci and starci-harness): a product repository installs it for starci app, which it runs in-process through its exact @starci/hfs dependency, and its codegen loads starci-harness/input-bounds.'
|
|
107
114
|
'@starci/hfs':
|
|
108
|
-
version: 4.9.
|
|
115
|
+
version: 4.9.1
|
|
109
116
|
group: starci
|
|
110
117
|
install: registry
|
|
111
118
|
side: both
|
package/brand-values.mjs
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rule that holds `brand-values.md` (HFS R61 `FE_STYLE_TOKEN_ONLY`, the TypeScript half; the CSS
|
|
3
|
-
* half belongs to `@starci/stylelint-canon`).
|
|
4
|
-
*
|
|
5
|
-
* ONE PLACE FOR A COLOUR. Colour values exist in `modules/brand/brand.css`, set only on the tokens
|
|
6
|
-
* the grammar declares, with a light and a dark value, and checked against the brand record. A hex
|
|
7
|
-
* in a component is a colour that has no dark value, cannot be re-branded, and drifts from the
|
|
8
|
-
* record: one repository carried three different ways to override the brand, and another 4.3k lines
|
|
9
|
-
* of hand-made CSS with a private token namespace.
|
|
10
|
-
*
|
|
11
|
-
* LENGTHS FOLLOW. A pixel length in TypeScript (`"12px"`, `{ width: 12 }` in a `style` prop) is a
|
|
12
|
-
* spacing decision that skipped the grammar's scale, so it does not move with density, zoom or a
|
|
13
|
-
* breakpoint. Spacing and sizing come from the grammar's utility tokens.
|
|
14
|
-
*
|
|
15
|
-
* WHAT IT DELIBERATELY LEAVES ALONE: a fragment identifier (`href="#top"`, `id`, `htmlFor`), a
|
|
16
|
-
* `data:` URI, a component's semantic appearance prop (a named role rather than a raw colour), and
|
|
17
|
-
* Tailwind arbitrary values (`w-[12px]`), which `no-arbitrary-value` already reports - two rules
|
|
18
|
-
* for one span would be two findings for one fix.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { isContentFile } from "./comments.mjs"
|
|
22
|
-
|
|
23
|
-
/** A hex colour: 3, 4, 6 or 8 digits, not a longer word and not part of an identifier. */
|
|
24
|
-
const HEX_COLOR = /(?<![\w&])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\w-])/
|
|
25
|
-
|
|
26
|
-
/** A functional colour notation. */
|
|
27
|
-
const COLOR_FUNCTION = /\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color-mix)\(/i
|
|
28
|
-
|
|
29
|
-
/** A pixel length, not inside an arbitrary-value bracket and not part of a longer number or word. */
|
|
30
|
-
const PIXEL_LENGTH = /(?<![\w.#[-])-?(?:\d+\.?\d*|\.\d+)px\b/
|
|
31
|
-
|
|
32
|
-
/** Attributes whose value is a fragment or an id, where a `#abc` is not a colour. */
|
|
33
|
-
const NON_COLOR_ATTRS = new Set(["href", "id", "htmlFor", "to", "name", "aria-controls", "aria-labelledby", "aria-describedby"])
|
|
34
|
-
|
|
35
|
-
/** SVG paint attributes: a named colour here is a raw brand value. */
|
|
36
|
-
const PAINT_ATTRS = new Set(["fill", "stroke", "stopColor", "floodColor", "lightingColor"])
|
|
37
|
-
|
|
38
|
-
/** Style properties that take a colour. */
|
|
39
|
-
const COLOR_KEYS = new Set([
|
|
40
|
-
"color",
|
|
41
|
-
"background",
|
|
42
|
-
"backgroundColor",
|
|
43
|
-
"borderColor",
|
|
44
|
-
"outlineColor",
|
|
45
|
-
"fill",
|
|
46
|
-
"stroke",
|
|
47
|
-
"caretColor",
|
|
48
|
-
"accentColor",
|
|
49
|
-
"textDecorationColor",
|
|
50
|
-
])
|
|
51
|
-
|
|
52
|
-
/** Style properties that take a length. */
|
|
53
|
-
const LENGTH_KEYS = new Set([
|
|
54
|
-
"width",
|
|
55
|
-
"height",
|
|
56
|
-
"minWidth",
|
|
57
|
-
"maxWidth",
|
|
58
|
-
"minHeight",
|
|
59
|
-
"maxHeight",
|
|
60
|
-
"top",
|
|
61
|
-
"right",
|
|
62
|
-
"bottom",
|
|
63
|
-
"left",
|
|
64
|
-
"gap",
|
|
65
|
-
"margin",
|
|
66
|
-
"marginTop",
|
|
67
|
-
"marginRight",
|
|
68
|
-
"marginBottom",
|
|
69
|
-
"marginLeft",
|
|
70
|
-
"padding",
|
|
71
|
-
"paddingTop",
|
|
72
|
-
"paddingRight",
|
|
73
|
-
"paddingBottom",
|
|
74
|
-
"paddingLeft",
|
|
75
|
-
"fontSize",
|
|
76
|
-
"borderRadius",
|
|
77
|
-
"letterSpacing",
|
|
78
|
-
])
|
|
79
|
-
|
|
80
|
-
/** Values that carry no raw colour: a token reference or a keyword. */
|
|
81
|
-
const NEUTRAL_COLOR = /^(?:var\(|inherit$|initial$|unset$|revert$|currentcolor$|transparent$|none$)/i
|
|
82
|
-
|
|
83
|
-
/** Static string of a literal or an expression-free template, else null. */
|
|
84
|
-
const staticString = (node) => {
|
|
85
|
-
if (!node) return null
|
|
86
|
-
if (node.type === "Literal" && typeof node.value === "string") return node.value
|
|
87
|
-
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value.cooked ?? ""
|
|
88
|
-
return null
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** No raw colour, hex or pixel value outside `brand.css`. */
|
|
92
|
-
export const noRawBrandValue = {
|
|
93
|
-
meta: {
|
|
94
|
-
type: "problem",
|
|
95
|
-
docs: { description: "No hex, colour function, named paint colour or px length in TypeScript; tokens only." },
|
|
96
|
-
schema: [],
|
|
97
|
-
messages: {
|
|
98
|
-
color:
|
|
99
|
-
"A raw colour value (`{{value}}`). Colour values exist only in `modules/brand/brand.css`, on the tokens the grammar declares, with a light and a dark value. Here it has no dark value, cannot be re-branded and drifts from the brand record. Use a grammar token class or variable.",
|
|
100
|
-
length:
|
|
101
|
-
"A raw pixel length (`{{value}}`). Spacing and size come from the grammar's scale so they move with density, zoom and breakpoint. Use a grammar utility token.",
|
|
102
|
-
named:
|
|
103
|
-
"`{{key}}: {{value}}` puts a named colour in a style. Colour is a token, not a word: use a grammar token (`var(--...)` or a token class).",
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
create(context) {
|
|
107
|
-
if (isContentFile(context)) return {}
|
|
108
|
-
const source = context.sourceCode || context.getSourceCode()
|
|
109
|
-
/** Values already reported through an owner, so a string reports once. */
|
|
110
|
-
const claimed = new WeakSet()
|
|
111
|
-
|
|
112
|
-
/** True when a node sits inside a JSX `style={...}` attribute. */
|
|
113
|
-
const inStyleAttribute = (node) =>
|
|
114
|
-
source
|
|
115
|
-
.getAncestors(node)
|
|
116
|
-
.some((ancestor) => ancestor.type === "JSXAttribute" && ancestor.name.type === "JSXIdentifier" && ancestor.name.name === "style")
|
|
117
|
-
|
|
118
|
-
/** Reports the first raw value in a string, once. */
|
|
119
|
-
const scan = (node, text) => {
|
|
120
|
-
if (claimed.has(node) || typeof text !== "string" || text.startsWith("data:")) return
|
|
121
|
-
const parent = node.parent
|
|
122
|
-
if (parent && parent.type === "JSXAttribute" && NON_COLOR_ATTRS.has(parent.name.name)) return
|
|
123
|
-
if (parent && parent.type === "JSXExpressionContainer" && parent.parent.type === "JSXAttribute") {
|
|
124
|
-
if (NON_COLOR_ATTRS.has(parent.parent.name.name)) return
|
|
125
|
-
}
|
|
126
|
-
if (parent && (parent.type === "ImportDeclaration" || parent.type === "ExportAllDeclaration" || parent.type === "ExportNamedDeclaration")) return
|
|
127
|
-
const color = HEX_COLOR.exec(text) || COLOR_FUNCTION.exec(text)
|
|
128
|
-
if (color) return context.report({ node, messageId: "color", data: { value: color[0].replace(/\($/, "(...)") } })
|
|
129
|
-
const length = PIXEL_LENGTH.exec(text)
|
|
130
|
-
if (length) context.report({ node, messageId: "length", data: { value: length[0] } })
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
Literal(node) {
|
|
135
|
-
scan(node, node.value)
|
|
136
|
-
},
|
|
137
|
-
TemplateElement(node) {
|
|
138
|
-
scan(node, node.value && node.value.cooked)
|
|
139
|
-
},
|
|
140
|
-
JSXAttribute(node) {
|
|
141
|
-
if (node.name.type !== "JSXIdentifier" || !PAINT_ATTRS.has(node.name.name)) return
|
|
142
|
-
const value = node.value && node.value.type === "JSXExpressionContainer" ? node.value.expression : node.value
|
|
143
|
-
const text = staticString(value)
|
|
144
|
-
if (text === null || NEUTRAL_COLOR.test(text) || HEX_COLOR.test(text) || COLOR_FUNCTION.test(text)) return
|
|
145
|
-
claimed.add(value)
|
|
146
|
-
context.report({ node, messageId: "named", data: { key: node.name.name, value: text } })
|
|
147
|
-
},
|
|
148
|
-
Property(node) {
|
|
149
|
-
if (node.computed || node.key.type !== "Identifier" || !inStyleAttribute(node)) return
|
|
150
|
-
const key = node.key.name
|
|
151
|
-
if (COLOR_KEYS.has(key)) {
|
|
152
|
-
const text = staticString(node.value)
|
|
153
|
-
if (text === null || NEUTRAL_COLOR.test(text) || HEX_COLOR.test(text) || COLOR_FUNCTION.test(text)) return
|
|
154
|
-
claimed.add(node.value)
|
|
155
|
-
context.report({ node, messageId: "named", data: { key, value: text } })
|
|
156
|
-
} else if (LENGTH_KEYS.has(key) && node.value.type === "Literal" && typeof node.value.value === "number") {
|
|
157
|
-
if (node.value.value === 0) return
|
|
158
|
-
claimed.add(node.value)
|
|
159
|
-
context.report({ node: node.value, messageId: "length", data: { value: `${key}: ${node.value.value}` } })
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** The rules this law contributes to the plugin. */
|
|
167
|
-
export const rules = {
|
|
168
|
-
"no-raw-brand-value": noRawBrandValue,
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Every rule is an error. */
|
|
172
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|