eslint-plugin-kerfjs 5.0.0-beta.60 → 5.0.0-beta.62
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/docs/rules/ui-composition.md +1 -0
- package/index.js +1 -1
- package/lib/rules/ui-composition.js +37 -13
- package/lib/ui-contract.js +13 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Enforces composition facts published by the versioned UI catalog. `KUI-L201` reports a catalog component under a known, disallowed direct parent. `KUI-L202` reports a statically visible zone child—including an intrinsic or unknown JSX element—that is not one of the zone's cataloged `accepts` entries. `KUI-L203` reports a statically provable zone cardinality violation. A zone participates only when it declares an explicit `jsx.prop` binding, including `children`; the rule never guesses that a zone id is a JSX prop. Toolbar, Pane, TabBar, AppTab, feedback, search, and layout validation is therefore catalog-driven rather than duplicated in rule-local component cases.
|
|
4
4
|
|
|
5
5
|
The parent check is intentionally conservative about the child: it runs only for a child that resolves to a cataloged entry (or a declared wrapper's roots) whose parents are `listed`. Its direct JSX parent may be any element the rule can see: a cataloged component, a declared wrapper (which counts as each of its roots), an intrinsic element such as `<div>`, or an unresolved component, and any parent that is not a listed parent reports `KUI-L201`. Bound zone checks understand aliases, namespace imports, fragments, arrays, and conditional/logical branches, but expressions whose content cannot be resolved statically remain runtime and accessibility work. Accepted concepts that do not identify a catalog entry remain machine-readable guidance while the rule enforces their statically provable cardinality only. No autofix is offered because wrapping or moving UI changes structure and behavior.
|
|
6
|
+
An additional JSX export from an entry can declare its own `jsxExports.<name>.parents` contract. It uses that placement instead of the entry's root placement and is opaque to zone matching, so an action link does not impersonate its enclosing control group.
|
|
6
7
|
|
|
7
8
|
## Application and third-party wrappers
|
|
8
9
|
|
package/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import uiPublicBoundaries from './lib/rules/ui-public-boundaries.js';
|
|
|
14
14
|
import uiWiring from './lib/rules/ui-wiring.js';
|
|
15
15
|
|
|
16
16
|
const plugin = {
|
|
17
|
-
meta: { name: 'eslint-plugin-kerfjs', version: '5.0.0-beta.
|
|
17
|
+
meta: { name: 'eslint-plugin-kerfjs', version: '5.0.0-beta.62' },
|
|
18
18
|
rules: {
|
|
19
19
|
'no-inline-jsx-event-handlers': noInlineJsxEventHandlers,
|
|
20
20
|
'no-raw-with-dynamic-arg': noRawWithDynamicArg,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
importRegistry,
|
|
3
3
|
isExcepted,
|
|
4
|
+
jsxExportName,
|
|
4
5
|
jsxKey,
|
|
5
6
|
loadUiContract,
|
|
6
7
|
UI_CONTRACT_LOAD_CODE,
|
|
@@ -35,7 +36,17 @@ function zoneShape(node, registry, contract) {
|
|
|
35
36
|
if (!node || node.type === 'JSXEmptyExpression') return emptyShape();
|
|
36
37
|
if (node.type === 'JSXElement') {
|
|
37
38
|
const key = jsxKey(node.openingElement.name, registry, contract);
|
|
38
|
-
const
|
|
39
|
+
const entry = contract.entries.get(key);
|
|
40
|
+
const exportName = jsxExportName(node.openingElement.name, registry);
|
|
41
|
+
// A second JSX export from one package entry may have its own element
|
|
42
|
+
// shape. It must not impersonate the entry's root in a parent's zone.
|
|
43
|
+
if (entry?.jsxExports?.[exportName])
|
|
44
|
+
return {
|
|
45
|
+
min: 1,
|
|
46
|
+
max: 1,
|
|
47
|
+
items: [{ label: `${key}:${exportName}` }],
|
|
48
|
+
};
|
|
49
|
+
const roots = entry?.rendersAs;
|
|
39
50
|
// A declared wrapper renders one of its roots, or nothing.
|
|
40
51
|
if (roots?.length)
|
|
41
52
|
return {
|
|
@@ -166,23 +177,36 @@ export default {
|
|
|
166
177
|
const key = jsxKey(node.name, registry, contract);
|
|
167
178
|
const entry = contract.entries.get(key);
|
|
168
179
|
if (!entry) return;
|
|
180
|
+
const exportName = jsxExportName(node.name, registry);
|
|
181
|
+
const exportContract = entry.jsxExports?.[exportName];
|
|
169
182
|
// A wrapper answers to its own parent contract and to each root's.
|
|
170
183
|
// Report one violated requirement per placement, with roots first.
|
|
171
|
-
const placed =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
const placed = exportContract
|
|
185
|
+
? [
|
|
186
|
+
{
|
|
187
|
+
entry: { ...entry, parents: exportContract.parents },
|
|
188
|
+
child: `${key}:${exportName}`,
|
|
189
|
+
},
|
|
190
|
+
]
|
|
191
|
+
: [
|
|
192
|
+
...(entry.rendersAs ?? []).flatMap((root) => {
|
|
193
|
+
const rootEntry = contract.entries.get(root);
|
|
194
|
+
return rootEntry
|
|
195
|
+
? [{ entry: rootEntry, child: `${key} (renders ${root})` }]
|
|
196
|
+
: [];
|
|
197
|
+
}),
|
|
198
|
+
{ entry, child: key },
|
|
199
|
+
];
|
|
200
|
+
const constrained = placed.filter(
|
|
180
201
|
({ entry: placedEntry }) => placedEntry.parents?.mode === 'listed',
|
|
181
202
|
);
|
|
182
|
-
if (
|
|
203
|
+
if (
|
|
204
|
+
constrained.length &&
|
|
205
|
+
!isExcepted(contract, PARENT_CODE, filename)
|
|
206
|
+
) {
|
|
183
207
|
const parent = directParentKey(node, registry, contract);
|
|
184
208
|
if (parent) {
|
|
185
|
-
const violation =
|
|
209
|
+
const violation = constrained.find(
|
|
186
210
|
({ entry: placedEntry }) =>
|
|
187
211
|
!rootsOf(parent.key, contract).every((root) =>
|
|
188
212
|
placedEntry.parents.entries.includes(root),
|
|
@@ -199,7 +223,7 @@ export default {
|
|
|
199
223
|
});
|
|
200
224
|
}
|
|
201
225
|
}
|
|
202
|
-
for (const zone of entry.zones ?? []) {
|
|
226
|
+
for (const zone of exportContract ? [] : (entry.zones ?? [])) {
|
|
203
227
|
const bound = boundZone(node, zone, registry, contract);
|
|
204
228
|
if (!bound) continue;
|
|
205
229
|
const { node: reportNode, shape } = bound;
|
package/lib/ui-contract.js
CHANGED
|
@@ -481,6 +481,19 @@ export function jsxKey(name, registry, contract) {
|
|
|
481
481
|
return undefined;
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
+
/** Imported export name behind a JSX tag, including aliases and namespaces. */
|
|
485
|
+
export function jsxExportName(name, registry) {
|
|
486
|
+
if (name.type === 'JSXIdentifier')
|
|
487
|
+
return registry.helpers.get(name.name)?.imported;
|
|
488
|
+
if (
|
|
489
|
+
name.type === 'JSXMemberExpression' &&
|
|
490
|
+
name.object.type === 'JSXIdentifier' &&
|
|
491
|
+
name.property.type === 'JSXIdentifier'
|
|
492
|
+
)
|
|
493
|
+
return name.property.name;
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
|
|
484
497
|
// A custom element authored directly as a lowercase JSX tag resolves to its
|
|
485
498
|
// selection-catalog entry. Kept separate from `jsxKey` so composition rules
|
|
486
499
|
// keep treating raw elements as application-owned markup.
|