stablekit.ts 0.3.0 → 0.3.1
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/eslint.cjs +68 -6
- package/dist/eslint.d.cts +16 -4
- package/dist/eslint.d.ts +16 -4
- package/dist/eslint.js +68 -6
- package/llms.txt +13 -0
- package/package.json +1 -1
package/dist/eslint.cjs
CHANGED
|
@@ -23,6 +23,65 @@ __export(eslint_exports, {
|
|
|
23
23
|
createArchitectureLint: () => createArchitectureLint
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(eslint_exports);
|
|
26
|
+
function findVariable(scope, name) {
|
|
27
|
+
let current = scope;
|
|
28
|
+
while (current) {
|
|
29
|
+
const variable = current.set.get(name);
|
|
30
|
+
if (variable) return variable;
|
|
31
|
+
current = current.upper;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
var noLoadingConflictRule = {
|
|
36
|
+
meta: {
|
|
37
|
+
type: "problem",
|
|
38
|
+
schema: [
|
|
39
|
+
{
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
passthrough: { type: "array", items: { type: "string" } }
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
messages: {
|
|
48
|
+
conflict: "Conditional variable as children of a component with a loading prop. The loading prop triggers an internal content swap \u2014 if children also change, both sides shrink simultaneously and pre-allocation is defeated. Use static children with the loading prop, or handle the entire swap explicitly with <StateSwap> in the caller."
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
create(context) {
|
|
52
|
+
const passthrough = context.options[0]?.passthrough ?? [];
|
|
53
|
+
return {
|
|
54
|
+
JSXElement(node) {
|
|
55
|
+
const opening = node.openingElement;
|
|
56
|
+
if (opening.name?.type !== "JSXIdentifier") return;
|
|
57
|
+
const name = opening.name.name;
|
|
58
|
+
if (!/^[A-Z]/.test(name)) return;
|
|
59
|
+
if (passthrough.includes(name)) return;
|
|
60
|
+
const hasLoading = opening.attributes.some(
|
|
61
|
+
(attr) => attr.type === "JSXAttribute" && attr.name?.name === "loading"
|
|
62
|
+
);
|
|
63
|
+
if (!hasLoading) return;
|
|
64
|
+
for (const child of node.children) {
|
|
65
|
+
if (child.type !== "JSXExpressionContainer") continue;
|
|
66
|
+
const expr = child.expression;
|
|
67
|
+
if (expr.type !== "Identifier") continue;
|
|
68
|
+
const scope = context.sourceCode ? context.sourceCode.getScope(node) : context.getScope();
|
|
69
|
+
const variable = findVariable(scope, expr.name);
|
|
70
|
+
if (!variable) continue;
|
|
71
|
+
for (const def of variable.defs) {
|
|
72
|
+
if (def.type === "Parameter") continue;
|
|
73
|
+
if (def.type === "Variable" && def.node.init) {
|
|
74
|
+
const initType = def.node.init.type;
|
|
75
|
+
if (initType === "ConditionalExpression" || initType === "LogicalExpression" || initType === "TemplateLiteral") {
|
|
76
|
+
context.report({ node: expr, messageId: "conflict" });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
26
85
|
function createArchitectureLint(options) {
|
|
27
86
|
const {
|
|
28
87
|
stateTokens,
|
|
@@ -160,13 +219,16 @@ function createArchitectureLint(options) {
|
|
|
160
219
|
selector: "JSXOpeningElement[name.name=/^[A-Z]/] > JSXAttribute[name.name='className']",
|
|
161
220
|
message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
|
|
162
221
|
}
|
|
163
|
-
]
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
222
|
+
]
|
|
223
|
+
],
|
|
224
|
+
"stablekit/no-loading-conflict": ["error", { passthrough: loadingPassthrough }]
|
|
225
|
+
},
|
|
226
|
+
plugins: {
|
|
227
|
+
stablekit: {
|
|
228
|
+
rules: {
|
|
229
|
+
"no-loading-conflict": noLoadingConflictRule
|
|
168
230
|
}
|
|
169
|
-
|
|
231
|
+
}
|
|
170
232
|
}
|
|
171
233
|
};
|
|
172
234
|
}
|
package/dist/eslint.d.cts
CHANGED
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
* component is a presentation leak across the Structure boundary.
|
|
31
31
|
* The component should own its own styling. Always on.
|
|
32
32
|
*
|
|
33
|
-
* 6. Dual-paradigm conflict
|
|
34
|
-
* internal content swapping (StateSwap). If
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* 6. Dual-paradigm conflict (custom rule with scope analysis) — a component
|
|
34
|
+
* with a `loading` prop does internal content swapping (StateSwap). If
|
|
35
|
+
* children are a variable derived from a conditional expression, both
|
|
36
|
+
* sides of the swap change simultaneously, defeating pre-allocation.
|
|
37
|
+
* Only flags locals initialized with ternaries/logical expressions.
|
|
38
|
+
* Props (function parameters) are allowed — they're static per mount.
|
|
37
39
|
*/
|
|
38
40
|
interface ArchitectureLintOptions {
|
|
39
41
|
/** State token names that should never appear in JS as Tailwind classes.
|
|
@@ -68,6 +70,16 @@ declare function createArchitectureLint(options: ArchitectureLintOptions): {
|
|
|
68
70
|
selector: string;
|
|
69
71
|
message: string;
|
|
70
72
|
})[];
|
|
73
|
+
"stablekit/no-loading-conflict": (string | {
|
|
74
|
+
passthrough: string[];
|
|
75
|
+
})[];
|
|
76
|
+
};
|
|
77
|
+
plugins: {
|
|
78
|
+
stablekit: {
|
|
79
|
+
rules: {
|
|
80
|
+
"no-loading-conflict": any;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
71
83
|
};
|
|
72
84
|
};
|
|
73
85
|
|
package/dist/eslint.d.ts
CHANGED
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
* component is a presentation leak across the Structure boundary.
|
|
31
31
|
* The component should own its own styling. Always on.
|
|
32
32
|
*
|
|
33
|
-
* 6. Dual-paradigm conflict
|
|
34
|
-
* internal content swapping (StateSwap). If
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* 6. Dual-paradigm conflict (custom rule with scope analysis) — a component
|
|
34
|
+
* with a `loading` prop does internal content swapping (StateSwap). If
|
|
35
|
+
* children are a variable derived from a conditional expression, both
|
|
36
|
+
* sides of the swap change simultaneously, defeating pre-allocation.
|
|
37
|
+
* Only flags locals initialized with ternaries/logical expressions.
|
|
38
|
+
* Props (function parameters) are allowed — they're static per mount.
|
|
37
39
|
*/
|
|
38
40
|
interface ArchitectureLintOptions {
|
|
39
41
|
/** State token names that should never appear in JS as Tailwind classes.
|
|
@@ -68,6 +70,16 @@ declare function createArchitectureLint(options: ArchitectureLintOptions): {
|
|
|
68
70
|
selector: string;
|
|
69
71
|
message: string;
|
|
70
72
|
})[];
|
|
73
|
+
"stablekit/no-loading-conflict": (string | {
|
|
74
|
+
passthrough: string[];
|
|
75
|
+
})[];
|
|
76
|
+
};
|
|
77
|
+
plugins: {
|
|
78
|
+
stablekit: {
|
|
79
|
+
rules: {
|
|
80
|
+
"no-loading-conflict": any;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
71
83
|
};
|
|
72
84
|
};
|
|
73
85
|
|
package/dist/eslint.js
CHANGED
|
@@ -1,4 +1,63 @@
|
|
|
1
1
|
// src/eslint.ts
|
|
2
|
+
function findVariable(scope, name) {
|
|
3
|
+
let current = scope;
|
|
4
|
+
while (current) {
|
|
5
|
+
const variable = current.set.get(name);
|
|
6
|
+
if (variable) return variable;
|
|
7
|
+
current = current.upper;
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
var noLoadingConflictRule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
schema: [
|
|
15
|
+
{
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
passthrough: { type: "array", items: { type: "string" } }
|
|
19
|
+
},
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
messages: {
|
|
24
|
+
conflict: "Conditional variable as children of a component with a loading prop. The loading prop triggers an internal content swap \u2014 if children also change, both sides shrink simultaneously and pre-allocation is defeated. Use static children with the loading prop, or handle the entire swap explicitly with <StateSwap> in the caller."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
const passthrough = context.options[0]?.passthrough ?? [];
|
|
29
|
+
return {
|
|
30
|
+
JSXElement(node) {
|
|
31
|
+
const opening = node.openingElement;
|
|
32
|
+
if (opening.name?.type !== "JSXIdentifier") return;
|
|
33
|
+
const name = opening.name.name;
|
|
34
|
+
if (!/^[A-Z]/.test(name)) return;
|
|
35
|
+
if (passthrough.includes(name)) return;
|
|
36
|
+
const hasLoading = opening.attributes.some(
|
|
37
|
+
(attr) => attr.type === "JSXAttribute" && attr.name?.name === "loading"
|
|
38
|
+
);
|
|
39
|
+
if (!hasLoading) return;
|
|
40
|
+
for (const child of node.children) {
|
|
41
|
+
if (child.type !== "JSXExpressionContainer") continue;
|
|
42
|
+
const expr = child.expression;
|
|
43
|
+
if (expr.type !== "Identifier") continue;
|
|
44
|
+
const scope = context.sourceCode ? context.sourceCode.getScope(node) : context.getScope();
|
|
45
|
+
const variable = findVariable(scope, expr.name);
|
|
46
|
+
if (!variable) continue;
|
|
47
|
+
for (const def of variable.defs) {
|
|
48
|
+
if (def.type === "Parameter") continue;
|
|
49
|
+
if (def.type === "Variable" && def.node.init) {
|
|
50
|
+
const initType = def.node.init.type;
|
|
51
|
+
if (initType === "ConditionalExpression" || initType === "LogicalExpression" || initType === "TemplateLiteral") {
|
|
52
|
+
context.report({ node: expr, messageId: "conflict" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
2
61
|
function createArchitectureLint(options) {
|
|
3
62
|
const {
|
|
4
63
|
stateTokens,
|
|
@@ -136,13 +195,16 @@ function createArchitectureLint(options) {
|
|
|
136
195
|
selector: "JSXOpeningElement[name.name=/^[A-Z]/] > JSXAttribute[name.name='className']",
|
|
137
196
|
message: "className on a custom component. Components own their own styling \u2014 use a data-attribute, variant prop, or CSS class internally instead of passing Tailwind utilities from the consumer."
|
|
138
197
|
}
|
|
139
|
-
]
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
198
|
+
]
|
|
199
|
+
],
|
|
200
|
+
"stablekit/no-loading-conflict": ["error", { passthrough: loadingPassthrough }]
|
|
201
|
+
},
|
|
202
|
+
plugins: {
|
|
203
|
+
stablekit: {
|
|
204
|
+
rules: {
|
|
205
|
+
"no-loading-conflict": noLoadingConflictRule
|
|
144
206
|
}
|
|
145
|
-
|
|
207
|
+
}
|
|
146
208
|
}
|
|
147
209
|
};
|
|
148
210
|
}
|
package/llms.txt
CHANGED
|
@@ -317,6 +317,19 @@ change to structure requires editing `.css`, a boundary has leaked.
|
|
|
317
317
|
or use a StableKit component (for state-driven swaps — StateSwap,
|
|
318
318
|
LayoutMap, LoadingBoundary, FadeTransition, StableField, StableCounter,
|
|
319
319
|
LayoutGroup). These rules are always on.
|
|
320
|
+
`className` on PascalCase components is banned — components own their own
|
|
321
|
+
styling. Use `classNamePassthrough` to exempt transparent wrappers (e.g.
|
|
322
|
+
`StableText`, `MediaSkeleton`, Lucide icons) that forward className to
|
|
323
|
+
their root element.
|
|
324
|
+
A custom rule (`stablekit/no-loading-conflict`) catches dual-paradigm
|
|
325
|
+
conflicts: if a component has a `loading` prop (which triggers an internal
|
|
326
|
+
content swap via StateSwap), children must not be variables derived from
|
|
327
|
+
conditional expressions. The `loading` prop controls geometry — if children
|
|
328
|
+
also change, both sides shrink simultaneously and pre-allocation is
|
|
329
|
+
defeated. Props (function parameters) are allowed because they are static
|
|
330
|
+
per mount. Use `loadingPassthrough` to exempt components where `loading`
|
|
331
|
+
does not trigger a content swap (e.g. `LoadingBoundary` controls opacity,
|
|
332
|
+
not geometry). Default passthrough: `["LoadingBoundary"]`.
|
|
320
333
|
4. **Stylelint** (`stablekit/stylelint`) — `createStyleLint({ functionalTokens })`
|
|
321
334
|
bans element selectors in CSS (`& svg`, `& span`), bans `!important`, and
|
|
322
335
|
bans functional color tokens inside `@utility` blocks. `functionalTokens`
|
package/package.json
CHANGED