@starci/eslint-canon-fe 9.2.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 +14 -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 +22 -14
- package/runtime/scripts/lib/package-at.mjs +5 -5
- 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 -315
- 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,19 +97,27 @@ 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/
|
|
101
|
-
version:
|
|
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.'
|
|
107
|
+
'starci-harness':
|
|
108
|
+
version: 1.1.0-alpha.1
|
|
102
109
|
group: starci
|
|
103
110
|
install: registry
|
|
104
111
|
side: both
|
|
105
|
-
|
|
112
|
+
source: cli/package.json
|
|
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.'
|
|
106
114
|
'@starci/hfs':
|
|
107
|
-
version: 4.
|
|
115
|
+
version: 4.9.1
|
|
108
116
|
group: starci
|
|
109
117
|
install: registry
|
|
110
118
|
side: both
|
|
111
119
|
source: packages/hfs/package.json
|
|
112
|
-
why: 'the
|
|
120
|
+
why: 'the app implementation starci-harness runs for starci app; it carries the generated runtime slice read by starci app commands.'
|
|
113
121
|
# --- tooling
|
|
114
122
|
typescript:
|
|
115
123
|
version: 5.9.3
|
|
@@ -13,9 +13,9 @@ export const packageAt = (dir, name) => {
|
|
|
13
13
|
/** This runtime's own `starci` bin (packages/cli is the only package that declares one). */
|
|
14
14
|
const RUNTIME_STARCI_BIN = path.join(skillRoot, 'packages', 'cli', 'bin', 'starci.mjs');
|
|
15
15
|
|
|
16
|
-
/** The `starci` bin of the
|
|
16
|
+
/** The `starci` bin of the starci-harness that `dir` installs (a product runs its own pinned CLI), else this runtime's own. */
|
|
17
17
|
function starciBin(dir) {
|
|
18
|
-
const packageFile = packageAt(dir, '
|
|
18
|
+
const packageFile = packageAt(dir, 'starci-harness');
|
|
19
19
|
if (packageFile) {
|
|
20
20
|
try {
|
|
21
21
|
const { bin } = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
@@ -50,13 +50,13 @@ export const loadTypescript = (...dirs) => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* The app's command line and the app implementation behind it: `bin` the `starci` bin of the app's installed
|
|
53
|
+
* The app's command line and the app implementation behind it: `bin` the `starci` bin of the app's installed starci-harness, else
|
|
54
54
|
* this runtime's packages/cli/bin/starci.mjs (starciBin); `dir` the @starci/hfs that answers
|
|
55
|
-
* `starci app` - the app's own install, else the one beside its
|
|
55
|
+
* `starci app` - the app's own install, else the one beside its starci-harness, else this runtime's packages/hfs - whose runtime
|
|
56
56
|
* slice the gate's base lint (scripts/gates/gate.mjs) and the READ digest (scripts/gates/read-digest.mjs) load in-process.
|
|
57
57
|
*/
|
|
58
58
|
export function hfsEntry(root) {
|
|
59
|
-
const cli = packageAt(root, '
|
|
59
|
+
const cli = packageAt(root, 'starci-harness');
|
|
60
60
|
const manifest = packageAt(root, '@starci/hfs') ?? (cli && packageAt(path.dirname(cli), '@starci/hfs'));
|
|
61
61
|
return { dir: manifest ? path.dirname(manifest) : path.join(skillRoot, 'packages', 'hfs'), bin: starciBin(root) };
|
|
62
62
|
}
|