@kerfjs/ui 5.0.0-beta.69 → 5.0.0-beta.70
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/ai/component-catalog.json +11 -1
- package/ai/component-catalog.schema.json +1 -1
- package/ai/component-composition.json +1 -0
- package/ai/component-composition.schema.json +1 -1
- package/ai/components/lucide-icon.md +12 -0
- package/ai/public-api-signatures-v1.md +7 -3
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/analyzer/component-ownership.d.mts +16 -0
- package/analyzer/component-ownership.mjs +87 -4
- package/analyzer/index.d.mts +2 -0
- package/analyzer/index.mjs +490 -9
- package/dist/catalog.js +3 -3
- package/dist/{chunk-2ZOLX3IE.js → chunk-DB5ADJKF.js} +2 -2
- package/dist/{chunk-2ZOLX3IE.js.map → chunk-DB5ADJKF.js.map} +1 -1
- package/dist/{chunk-X3X4KRDK.js → chunk-DC6CN2DS.js} +2 -2
- package/dist/{chunk-X3X4KRDK.js.map → chunk-DC6CN2DS.js.map} +1 -1
- package/dist/{chunk-AUVG54KC.js → chunk-G4S2QU6V.js} +2 -2
- package/dist/{chunk-AUVG54KC.js.map → chunk-G4S2QU6V.js.map} +1 -1
- package/dist/{chunk-CRVDF4A6.js → chunk-K67Y2JNF.js} +2 -2
- package/dist/{chunk-CRVDF4A6.js.map → chunk-K67Y2JNF.js.map} +1 -1
- package/dist/{chunk-H7IODXGY.js → chunk-KCCEH7RF.js} +3 -3
- package/dist/{chunk-H7IODXGY.js.map → chunk-KCCEH7RF.js.map} +1 -1
- package/dist/{chunk-3BCJGM36.js → chunk-M6DEEBEF.js} +2 -2
- package/dist/{chunk-3BCJGM36.js.map → chunk-M6DEEBEF.js.map} +1 -1
- package/dist/{chunk-X3TPECWG.js → chunk-OXO2QJWV.js} +3 -1
- package/dist/chunk-OXO2QJWV.js.map +1 -0
- package/dist/{chunk-B3XWUX3A.js → chunk-RNQNEENY.js} +2 -2
- package/dist/{chunk-B3XWUX3A.js.map → chunk-RNQNEENY.js.map} +1 -1
- package/dist/{chunk-PQZPVBP4.js → chunk-RVERAFFY.js} +2 -2
- package/dist/{chunk-PQZPVBP4.js.map → chunk-RVERAFFY.js.map} +1 -1
- package/dist/{chunk-UA6JW52D.js → chunk-T2E5WBCZ.js} +110 -5
- package/dist/chunk-T2E5WBCZ.js.map +1 -0
- package/dist/{chunk-MOSIE4Z2.js → chunk-WTSNV5RG.js} +3 -2
- package/dist/chunk-WTSNV5RG.js.map +1 -0
- package/dist/collapsible-panel.js +3 -3
- package/dist/disclosure-arrow.js +1 -1
- package/dist/index.js +8 -8
- package/dist/list-header.js +1 -1
- package/dist/lucide-icon.d.ts +3 -1
- package/dist/lucide-icon.js +1 -1
- package/dist/nav-stack.js +1 -1
- package/dist/select.js +1 -1
- package/dist/split-view.js +1 -1
- package/dist/styles/app-tab.css +4 -0
- package/dist/styles/tab-bar.css +8 -0
- package/dist/styles/toolbar-control-group.css +1 -1
- package/dist/tab-bar.d.ts +3 -1
- package/dist/tab-bar.js +1 -1
- package/dist/token-search-field.js +1 -1
- package/dist/wire-tab-bars.js +1 -1
- package/dist/wire-token-search-fields.js +1 -1
- package/dist/workbench.js +1 -1
- package/docs/component-contract.md +16 -3
- package/docs/component-selection.md +8 -1
- package/docs/ui-analyzer.md +30 -9
- package/docs/ui-doctor.md +15 -6
- package/doctor/config.schema.json +2 -0
- package/doctor/index.d.mts +2 -0
- package/doctor/index.mjs +21 -0
- package/package.json +1 -1
- package/dist/chunk-MOSIE4Z2.js.map +0 -1
- package/dist/chunk-UA6JW52D.js.map +0 -1
- package/dist/chunk-X3TPECWG.js.map +0 -1
package/analyzer/index.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
componentLabel,
|
|
19
19
|
componentName,
|
|
20
20
|
componentOwnershipFacts,
|
|
21
|
+
contextualComponents,
|
|
21
22
|
configurationFor,
|
|
22
23
|
privateVariableOwner,
|
|
23
24
|
reportGap,
|
|
@@ -80,6 +81,10 @@ export const UI_ANALYSIS_RULES = Object.freeze({
|
|
|
80
81
|
severity: 'error',
|
|
81
82
|
title: "Hook class restyles a cataloged component's root",
|
|
82
83
|
},
|
|
84
|
+
'KUI-L023': {
|
|
85
|
+
severity: 'error',
|
|
86
|
+
title: "Another component's public class is rendered as markup",
|
|
87
|
+
},
|
|
83
88
|
});
|
|
84
89
|
|
|
85
90
|
const ownershipAction =
|
|
@@ -92,6 +97,7 @@ export const UI_ANALYSIS_ACTIONS = Object.freeze({
|
|
|
92
97
|
'KUI-L021':
|
|
93
98
|
'Set the typed prop the diagnostic names on the component instead of overriding its token.',
|
|
94
99
|
'KUI-L022': ownershipAction,
|
|
100
|
+
'KUI-L023': ownershipAction,
|
|
95
101
|
});
|
|
96
102
|
|
|
97
103
|
const adoptionRules = new Set([
|
|
@@ -104,6 +110,7 @@ const adoptionRules = new Set([
|
|
|
104
110
|
'KUI-L020',
|
|
105
111
|
'KUI-L021',
|
|
106
112
|
'KUI-L022',
|
|
113
|
+
'KUI-L023',
|
|
107
114
|
]);
|
|
108
115
|
|
|
109
116
|
const sourceExtensions = new Set(['.js', '.jsx', '.mjs', '.ts', '.tsx']);
|
|
@@ -243,7 +250,7 @@ function reachableStyleFiles(file, imports, styleFacts) {
|
|
|
243
250
|
return visited;
|
|
244
251
|
}
|
|
245
252
|
|
|
246
|
-
async function loadCatalogs(profileResult) {
|
|
253
|
+
async function loadCatalogs(profileResult, ownership) {
|
|
247
254
|
const entries = [];
|
|
248
255
|
for (const catalog of profileResult.profile?.catalogs ?? []) {
|
|
249
256
|
const owner = profileResult.provenance[`$catalogs.${catalog.package}`];
|
|
@@ -256,8 +263,10 @@ async function loadCatalogs(profileResult) {
|
|
|
256
263
|
),
|
|
257
264
|
);
|
|
258
265
|
const selection = await loadSelectionFacts(owner, catalog);
|
|
266
|
+
const joined = new Set();
|
|
259
267
|
for (const entry of artifact.entries ?? []) {
|
|
260
268
|
const selected = selection.get(entry.key);
|
|
269
|
+
joined.add(entry.key);
|
|
261
270
|
const componentSource =
|
|
262
271
|
selected?.componentSource ??
|
|
263
272
|
(/\.[cm]?[jt]sx?$/.test(entry.source) ? entry.source : undefined);
|
|
@@ -269,10 +278,35 @@ async function loadCatalogs(profileResult) {
|
|
|
269
278
|
moduleImports: selected.moduleImports,
|
|
270
279
|
componentSource,
|
|
271
280
|
styleSources: entry.styleSources ?? selected.styleSources,
|
|
281
|
+
catalogDirectory: dirname(owner),
|
|
272
282
|
}
|
|
273
|
-
: { ...entry, componentSource },
|
|
283
|
+
: { ...entry, componentSource, catalogDirectory: dirname(owner) },
|
|
274
284
|
);
|
|
275
285
|
}
|
|
286
|
+
for (const [key, selected] of selection) {
|
|
287
|
+
if (
|
|
288
|
+
ownership !== 'component' ||
|
|
289
|
+
joined.has(key) ||
|
|
290
|
+
!selected.componentSource
|
|
291
|
+
)
|
|
292
|
+
continue;
|
|
293
|
+
entries.push({
|
|
294
|
+
key,
|
|
295
|
+
package: catalog.package,
|
|
296
|
+
id: selected.id,
|
|
297
|
+
name: selected.name,
|
|
298
|
+
kind: selected.kind,
|
|
299
|
+
componentSource: selected.componentSource,
|
|
300
|
+
styleSources: selected.styleSources,
|
|
301
|
+
moduleImports: selected.moduleImports,
|
|
302
|
+
publicExports: selected.publicExports,
|
|
303
|
+
boundaries: {
|
|
304
|
+
publicClasses: selected.publicClasses ?? [],
|
|
305
|
+
rootElement: selected.rootElement,
|
|
306
|
+
},
|
|
307
|
+
catalogDirectory: dirname(owner),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
276
310
|
} catch {
|
|
277
311
|
// Profile validation reports the precise catalog load failure.
|
|
278
312
|
}
|
|
@@ -299,7 +333,12 @@ async function loadSelectionFacts(owner, catalog) {
|
|
|
299
333
|
for (const entry of artifact.entries ?? []) {
|
|
300
334
|
const delivery = entry.delivery ?? {};
|
|
301
335
|
facts.set(`${packageName}:${entry.id}`, {
|
|
336
|
+
id: entry.id,
|
|
337
|
+
name: entry.name,
|
|
338
|
+
kind: entry.kind,
|
|
302
339
|
publicExports: entry.publicExports,
|
|
340
|
+
publicClasses: entry.publicClasses,
|
|
341
|
+
rootElement: entry.rootElement,
|
|
303
342
|
componentSource: entry.source,
|
|
304
343
|
styleSources: entry.styleSources,
|
|
305
344
|
moduleImports: [delivery.browserImport, delivery.moduleImport].filter(
|
|
@@ -429,6 +468,7 @@ function catalogFacts(entries) {
|
|
|
429
468
|
}
|
|
430
469
|
}
|
|
431
470
|
return {
|
|
471
|
+
entries,
|
|
432
472
|
publicClasses,
|
|
433
473
|
publicTokens,
|
|
434
474
|
publicParts,
|
|
@@ -438,6 +478,129 @@ function catalogFacts(entries) {
|
|
|
438
478
|
};
|
|
439
479
|
}
|
|
440
480
|
|
|
481
|
+
const bemBlock = (name) => name.split(/__|--/, 1)[0];
|
|
482
|
+
|
|
483
|
+
function renderedLiteralClasses(file, source) {
|
|
484
|
+
const names = new Set();
|
|
485
|
+
const parsed = ts.createSourceFile(
|
|
486
|
+
file,
|
|
487
|
+
source,
|
|
488
|
+
ts.ScriptTarget.Latest,
|
|
489
|
+
true,
|
|
490
|
+
file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.JSX,
|
|
491
|
+
);
|
|
492
|
+
const visit = (node) => {
|
|
493
|
+
if (
|
|
494
|
+
ts.isJsxAttribute(node) &&
|
|
495
|
+
['class', 'className'].includes(node.name.text)
|
|
496
|
+
) {
|
|
497
|
+
const value =
|
|
498
|
+
node.initializer && ts.isStringLiteral(node.initializer)
|
|
499
|
+
? node.initializer.text
|
|
500
|
+
: node.initializer &&
|
|
501
|
+
ts.isJsxExpression(node.initializer) &&
|
|
502
|
+
node.initializer.expression &&
|
|
503
|
+
ts.isStringLiteral(node.initializer.expression)
|
|
504
|
+
? node.initializer.expression.text
|
|
505
|
+
: undefined;
|
|
506
|
+
for (const name of value?.split(/\s+/) ?? []) if (name) names.add(name);
|
|
507
|
+
}
|
|
508
|
+
ts.forEachChild(node, visit);
|
|
509
|
+
};
|
|
510
|
+
visit(parsed);
|
|
511
|
+
return names;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function inferOwnedClasses(entries, contents, imports) {
|
|
515
|
+
return entries.map((entry) => {
|
|
516
|
+
if (!entry.componentSource || !entry.catalogDirectory) return entry;
|
|
517
|
+
const sourceFile = resolve(entry.catalogDirectory, entry.componentSource);
|
|
518
|
+
const source = contents.get(sourceFile);
|
|
519
|
+
if (!source) return entry;
|
|
520
|
+
const styleFiles = entry.styleSources?.length
|
|
521
|
+
? entry.styleSources.map((path) => resolve(entry.catalogDirectory, path))
|
|
522
|
+
: [...(imports.get(sourceFile) ?? [])];
|
|
523
|
+
const styleSources = styleFiles.map((file) =>
|
|
524
|
+
relative(entry.catalogDirectory, file).replaceAll('\\', '/'),
|
|
525
|
+
);
|
|
526
|
+
const styledBlocks = new Set();
|
|
527
|
+
for (const style of styleFiles) {
|
|
528
|
+
const css = contents.get(style);
|
|
529
|
+
if (!css) continue;
|
|
530
|
+
try {
|
|
531
|
+
postcss.parse(css, { from: style }).walkRules((rule) => {
|
|
532
|
+
for (const name of subjectClasses(rule.selector))
|
|
533
|
+
styledBlocks.add(bemBlock(name));
|
|
534
|
+
});
|
|
535
|
+
} catch {
|
|
536
|
+
// The stylesheet parser reports malformed CSS in the normal pass.
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const inferred = [...renderedLiteralClasses(sourceFile, source)].filter(
|
|
540
|
+
(name) => styledBlocks.has(bemBlock(name)),
|
|
541
|
+
);
|
|
542
|
+
if (!inferred.length) return { ...entry, styleSources };
|
|
543
|
+
return {
|
|
544
|
+
...entry,
|
|
545
|
+
styleSources,
|
|
546
|
+
boundaries: {
|
|
547
|
+
...entry.boundaries,
|
|
548
|
+
publicClasses: [
|
|
549
|
+
...new Set([...(entry.boundaries?.publicClasses ?? []), ...inferred]),
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function implicitOwnershipEntries(files, imports, root) {
|
|
557
|
+
const found = [];
|
|
558
|
+
const packages = new Map();
|
|
559
|
+
const packageAt = async (directory) => {
|
|
560
|
+
if (packages.has(directory)) return packages.get(directory);
|
|
561
|
+
let result;
|
|
562
|
+
try {
|
|
563
|
+
const manifest = JSON.parse(
|
|
564
|
+
await readFile(resolve(directory, 'package.json'), 'utf8'),
|
|
565
|
+
);
|
|
566
|
+
if (typeof manifest.name === 'string')
|
|
567
|
+
result = { name: manifest.name, directory };
|
|
568
|
+
} catch {
|
|
569
|
+
// Search the containing directory for a package manifest.
|
|
570
|
+
}
|
|
571
|
+
if (!result && directory !== root) {
|
|
572
|
+
const parent = dirname(directory);
|
|
573
|
+
if (!relative(root, parent).startsWith('..'))
|
|
574
|
+
result = await packageAt(parent);
|
|
575
|
+
}
|
|
576
|
+
packages.set(directory, result);
|
|
577
|
+
return result;
|
|
578
|
+
};
|
|
579
|
+
for (const file of files) {
|
|
580
|
+
if (!sourceExtensions.has(extname(file))) continue;
|
|
581
|
+
const styles = (imports.get(file) ?? []).filter((path) =>
|
|
582
|
+
files.includes(path),
|
|
583
|
+
);
|
|
584
|
+
if (!styles.length) continue;
|
|
585
|
+
const owner = await packageAt(dirname(file));
|
|
586
|
+
if (!owner) continue;
|
|
587
|
+
const source = relative(owner.directory, file).replaceAll('\\', '/');
|
|
588
|
+
const id = `module:${source}`;
|
|
589
|
+
found.push({
|
|
590
|
+
key: `${owner.name}:${id}`,
|
|
591
|
+
package: owner.name,
|
|
592
|
+
id,
|
|
593
|
+
name: basename(file).replace(/\.[^.]+$/, ''),
|
|
594
|
+
componentSource: source,
|
|
595
|
+
catalogDirectory: owner.directory,
|
|
596
|
+
styleSources: styles.map((style) =>
|
|
597
|
+
relative(owner.directory, style).replaceAll('\\', '/'),
|
|
598
|
+
),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return found;
|
|
602
|
+
}
|
|
603
|
+
|
|
441
604
|
function spacingValues(value) {
|
|
442
605
|
const results = [];
|
|
443
606
|
for (const match of value.matchAll(/(-?\d*\.?\d+)(px|rem)\b/g)) {
|
|
@@ -480,16 +643,20 @@ function inspectComponentOwnership(
|
|
|
480
643
|
diagnostics,
|
|
481
644
|
adoption,
|
|
482
645
|
isForeign,
|
|
646
|
+
ownershipEvidence,
|
|
647
|
+
ownershipContext,
|
|
483
648
|
) {
|
|
484
649
|
if (inKeyframes(rule)) return;
|
|
485
650
|
const restyling = restylingDeclarations(rule);
|
|
486
651
|
if (restyling.length === 0) return;
|
|
487
652
|
const properties = [...new Set(restyling.map((decl) => decl.prop))];
|
|
488
|
-
|
|
653
|
+
const restyled = restyledComponents(
|
|
489
654
|
rule.selector,
|
|
490
655
|
facts.ownership,
|
|
491
656
|
isForeign,
|
|
492
|
-
|
|
657
|
+
ownershipEvidence,
|
|
658
|
+
);
|
|
659
|
+
for (const { entry, via, name } of restyled)
|
|
493
660
|
diagnostics.push(
|
|
494
661
|
diagnostic(
|
|
495
662
|
'KUI-L019',
|
|
@@ -508,6 +675,38 @@ function inspectComponentOwnership(
|
|
|
508
675
|
adoption,
|
|
509
676
|
),
|
|
510
677
|
);
|
|
678
|
+
if (ownershipContext !== 'any' || !ownershipEvidence?.ownPackage) return;
|
|
679
|
+
for (const { entry, name, position } of contextualComponents(
|
|
680
|
+
rule.selector,
|
|
681
|
+
facts.ownership,
|
|
682
|
+
isForeign,
|
|
683
|
+
ownershipEvidence.ownPackage,
|
|
684
|
+
)) {
|
|
685
|
+
if (position === 'subject') continue;
|
|
686
|
+
if (
|
|
687
|
+
restyled.some(
|
|
688
|
+
(item) => item.entry.key === entry.key && item.name === name,
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
continue;
|
|
692
|
+
diagnostics.push(
|
|
693
|
+
diagnostic(
|
|
694
|
+
'KUI-L019',
|
|
695
|
+
location(file, rule),
|
|
696
|
+
`\`${name}\` uses ${componentLabel(entry)} as ${position} selector context (${properties.join(', ')}); in strict component ownership, sibling classes cannot key this stylesheet. Configure the component through ${configurationFor(entry)}. ${reportGap(entry)}`,
|
|
697
|
+
{
|
|
698
|
+
selector: rule.selector,
|
|
699
|
+
component: entry.key,
|
|
700
|
+
via: 'context',
|
|
701
|
+
position,
|
|
702
|
+
target: name,
|
|
703
|
+
properties,
|
|
704
|
+
},
|
|
705
|
+
undefined,
|
|
706
|
+
adoption,
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
511
710
|
}
|
|
512
711
|
|
|
513
712
|
function inspectVariableOwnership(
|
|
@@ -577,6 +776,8 @@ async function inspectCss(
|
|
|
577
776
|
adoption = false,
|
|
578
777
|
siblingOnLoud = [],
|
|
579
778
|
isForeign = () => true,
|
|
779
|
+
ownershipEvidence,
|
|
780
|
+
ownershipContext = 'subject',
|
|
580
781
|
) {
|
|
581
782
|
let root;
|
|
582
783
|
try {
|
|
@@ -609,6 +810,8 @@ async function inspectCss(
|
|
|
609
810
|
diagnostics,
|
|
610
811
|
adoption,
|
|
611
812
|
isForeign,
|
|
813
|
+
ownershipEvidence,
|
|
814
|
+
ownershipContext,
|
|
612
815
|
);
|
|
613
816
|
if (!inKeyframes(rule) && restylingDeclarations(rule).length > 0)
|
|
614
817
|
for (const className of subjects) {
|
|
@@ -775,6 +978,164 @@ function literalClasses(attribute) {
|
|
|
775
978
|
return { values: [], dynamic: true };
|
|
776
979
|
}
|
|
777
980
|
|
|
981
|
+
function literalClassValues(expression) {
|
|
982
|
+
if (!expression) return [];
|
|
983
|
+
if (ts.isStringLiteralLike(expression))
|
|
984
|
+
return expression.text.split(/\s+/).filter(Boolean);
|
|
985
|
+
if (ts.isParenthesizedExpression(expression))
|
|
986
|
+
return literalClassValues(expression.expression);
|
|
987
|
+
if (ts.isConditionalExpression(expression))
|
|
988
|
+
return [
|
|
989
|
+
...literalClassValues(expression.whenTrue),
|
|
990
|
+
...literalClassValues(expression.whenFalse),
|
|
991
|
+
];
|
|
992
|
+
return [];
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function jsxOwnershipEvidence(style, facts, contents, root, compilerCache) {
|
|
996
|
+
const hooks = new Map();
|
|
997
|
+
const composedChildren = new Map();
|
|
998
|
+
const ambiguousHooks = new Set();
|
|
999
|
+
const ambiguousChildren = new Set();
|
|
1000
|
+
const record = (map, ambiguous, key, entry) => {
|
|
1001
|
+
if (ambiguous.has(key)) return;
|
|
1002
|
+
if (map.has(key) && map.get(key).key !== entry.key) {
|
|
1003
|
+
map.delete(key);
|
|
1004
|
+
ambiguous.add(key);
|
|
1005
|
+
} else map.set(key, entry);
|
|
1006
|
+
};
|
|
1007
|
+
for (const owner of facts.entries) {
|
|
1008
|
+
if (!owner.componentSource || !owner.catalogDirectory) continue;
|
|
1009
|
+
const relativeStyle = relative(owner.catalogDirectory, style).replaceAll(
|
|
1010
|
+
'\\',
|
|
1011
|
+
'/',
|
|
1012
|
+
);
|
|
1013
|
+
if (!owner.styleSources?.includes(relativeStyle)) continue;
|
|
1014
|
+
const file = resolve(owner.catalogDirectory, owner.componentSource);
|
|
1015
|
+
const sourceText = contents.get(file);
|
|
1016
|
+
if (!sourceText) continue;
|
|
1017
|
+
const source = ts.createSourceFile(
|
|
1018
|
+
file,
|
|
1019
|
+
sourceText,
|
|
1020
|
+
ts.ScriptTarget.Latest,
|
|
1021
|
+
true,
|
|
1022
|
+
ts.ScriptKind.TSX,
|
|
1023
|
+
);
|
|
1024
|
+
const imports = new Map();
|
|
1025
|
+
const namespaces = new Map();
|
|
1026
|
+
const compilerOptions = compilerOptionsFor(file, root, compilerCache);
|
|
1027
|
+
const aliasSource = (module) =>
|
|
1028
|
+
module && !module.startsWith('.') && compilerOptions?.paths
|
|
1029
|
+
? ts.resolveModuleName(module, file, compilerOptions, ts.sys)
|
|
1030
|
+
.resolvedModule?.resolvedFileName
|
|
1031
|
+
: undefined;
|
|
1032
|
+
for (const statement of source.statements) {
|
|
1033
|
+
if (
|
|
1034
|
+
!ts.isImportDeclaration(statement) ||
|
|
1035
|
+
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
1036
|
+
)
|
|
1037
|
+
continue;
|
|
1038
|
+
const module = statement.moduleSpecifier.text;
|
|
1039
|
+
const bindings = statement.importClause?.namedBindings;
|
|
1040
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
1041
|
+
namespaces.set(bindings.name.text, module);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
1045
|
+
for (const item of bindings.elements) {
|
|
1046
|
+
const exported = item.propertyName?.text ?? item.name.text;
|
|
1047
|
+
const entry = resolveExportEntry(
|
|
1048
|
+
facts.exportEntries.get(exported),
|
|
1049
|
+
exported,
|
|
1050
|
+
module,
|
|
1051
|
+
file,
|
|
1052
|
+
owner.catalogDirectory,
|
|
1053
|
+
aliasSource(module),
|
|
1054
|
+
);
|
|
1055
|
+
if (entry) imports.set(item.name.text, entry);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const openingOf = (node) =>
|
|
1059
|
+
ts.isJsxElement(node) ? node.openingElement : node;
|
|
1060
|
+
const entryFor = (opening) => {
|
|
1061
|
+
const tag = opening.tagName.getText(source);
|
|
1062
|
+
if (imports.has(tag)) return imports.get(tag);
|
|
1063
|
+
if (
|
|
1064
|
+
ts.isPropertyAccessExpression(opening.tagName) &&
|
|
1065
|
+
ts.isIdentifier(opening.tagName.expression)
|
|
1066
|
+
) {
|
|
1067
|
+
const module = namespaces.get(opening.tagName.expression.text);
|
|
1068
|
+
return resolveExportEntry(
|
|
1069
|
+
facts.exportEntries.get(opening.tagName.name.text),
|
|
1070
|
+
opening.tagName.name.text,
|
|
1071
|
+
module,
|
|
1072
|
+
file,
|
|
1073
|
+
owner.catalogDirectory,
|
|
1074
|
+
aliasSource(module),
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
return undefined;
|
|
1078
|
+
};
|
|
1079
|
+
const classesOf = (opening) =>
|
|
1080
|
+
literalClasses(
|
|
1081
|
+
opening.attributes.properties.find(
|
|
1082
|
+
(item) =>
|
|
1083
|
+
ts.isJsxAttribute(item) &&
|
|
1084
|
+
['class', 'className'].includes(item.name.getText(source)),
|
|
1085
|
+
),
|
|
1086
|
+
).values;
|
|
1087
|
+
const visit = (node) => {
|
|
1088
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1089
|
+
const opening = openingOf(node);
|
|
1090
|
+
const entry = entryFor(opening);
|
|
1091
|
+
if (entry)
|
|
1092
|
+
for (const name of classesOf(opening))
|
|
1093
|
+
record(hooks, ambiguousHooks, name, entry);
|
|
1094
|
+
if (ts.isJsxElement(node)) {
|
|
1095
|
+
const parentClasses = classesOf(opening);
|
|
1096
|
+
const possible = new Map();
|
|
1097
|
+
let dynamic = false;
|
|
1098
|
+
for (const child of node.children) {
|
|
1099
|
+
if (ts.isJsxExpression(child) && child.expression) dynamic = true;
|
|
1100
|
+
if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
|
|
1101
|
+
continue;
|
|
1102
|
+
const childOpening = openingOf(child);
|
|
1103
|
+
const childEntry = entryFor(childOpening);
|
|
1104
|
+
const type =
|
|
1105
|
+
childEntry?.boundaries?.rootElement ??
|
|
1106
|
+
(childEntry
|
|
1107
|
+
? undefined
|
|
1108
|
+
: childOpening.tagName.getText(source).toLowerCase());
|
|
1109
|
+
if (!type) continue;
|
|
1110
|
+
const candidates = possible.get(type) ?? [];
|
|
1111
|
+
candidates.push(childEntry);
|
|
1112
|
+
possible.set(type, candidates);
|
|
1113
|
+
}
|
|
1114
|
+
if (!dynamic)
|
|
1115
|
+
for (const [type, candidates] of possible)
|
|
1116
|
+
if (
|
|
1117
|
+
candidates.length > 0 &&
|
|
1118
|
+
candidates.every(
|
|
1119
|
+
(candidate) => candidate?.key === candidates[0]?.key,
|
|
1120
|
+
) &&
|
|
1121
|
+
candidates[0]
|
|
1122
|
+
)
|
|
1123
|
+
for (const name of parentClasses)
|
|
1124
|
+
record(
|
|
1125
|
+
composedChildren,
|
|
1126
|
+
ambiguousChildren,
|
|
1127
|
+
`${name}|${type}`,
|
|
1128
|
+
candidates[0],
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
ts.forEachChild(node, visit);
|
|
1133
|
+
};
|
|
1134
|
+
visit(source);
|
|
1135
|
+
}
|
|
1136
|
+
return { componentMode: true, hooks, composedChildren };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
778
1139
|
function cssPreferred(contract) {
|
|
779
1140
|
return (
|
|
780
1141
|
[
|
|
@@ -1004,6 +1365,28 @@ function inspectTsx(
|
|
|
1004
1365
|
}
|
|
1005
1366
|
}
|
|
1006
1367
|
const stack = [];
|
|
1368
|
+
const borrowed = (className, node, via) => {
|
|
1369
|
+
for (const owner of facts.classEntries.get(className) ?? []) {
|
|
1370
|
+
if (
|
|
1371
|
+
!isForeign(owner) ||
|
|
1372
|
+
owner.boundaries?.placeableClasses?.includes(className)
|
|
1373
|
+
)
|
|
1374
|
+
continue;
|
|
1375
|
+
diagnostics.push(
|
|
1376
|
+
diagnostic(
|
|
1377
|
+
'KUI-L023',
|
|
1378
|
+
location(file, node, source),
|
|
1379
|
+
`\`.${className}\` is rendered in this module's ${via} but belongs to ${componentLabel(owner)}. Render that component or configure it through ${configurationFor(owner)}. ${reportGap(owner)}`,
|
|
1380
|
+
{ className, component: owner.key, via },
|
|
1381
|
+
undefined,
|
|
1382
|
+
adoption,
|
|
1383
|
+
),
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
const borrowedValues = (values, node, via) => {
|
|
1388
|
+
for (const className of new Set(values)) borrowed(className, node, via);
|
|
1389
|
+
};
|
|
1007
1390
|
const visit = (node) => {
|
|
1008
1391
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1009
1392
|
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
@@ -1028,6 +1411,15 @@ function inspectTsx(
|
|
|
1028
1411
|
);
|
|
1029
1412
|
const classes = literalClasses(classAttribute);
|
|
1030
1413
|
const at = location(file, opening, source);
|
|
1414
|
+
if (!entry && /^[a-z]/.test(tag))
|
|
1415
|
+
borrowedValues(
|
|
1416
|
+
classAttribute?.initializer &&
|
|
1417
|
+
ts.isJsxExpression(classAttribute.initializer)
|
|
1418
|
+
? literalClassValues(classAttribute.initializer.expression)
|
|
1419
|
+
: classes.values,
|
|
1420
|
+
classAttribute ?? opening,
|
|
1421
|
+
'JSX markup',
|
|
1422
|
+
);
|
|
1031
1423
|
if (entry)
|
|
1032
1424
|
inspectCssValues(
|
|
1033
1425
|
file,
|
|
@@ -1126,6 +1518,36 @@ function inspectTsx(
|
|
|
1126
1518
|
return;
|
|
1127
1519
|
}
|
|
1128
1520
|
if (ts.isCallExpression(node)) {
|
|
1521
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
1522
|
+
const method = node.expression.name.text;
|
|
1523
|
+
const receiver = node.expression.expression;
|
|
1524
|
+
if (
|
|
1525
|
+
ts.isPropertyAccessExpression(receiver) &&
|
|
1526
|
+
receiver.name.text === 'classList'
|
|
1527
|
+
) {
|
|
1528
|
+
const written =
|
|
1529
|
+
method === 'add'
|
|
1530
|
+
? node.arguments
|
|
1531
|
+
: method === 'toggle'
|
|
1532
|
+
? node.arguments.slice(0, 1)
|
|
1533
|
+
: method === 'replace'
|
|
1534
|
+
? node.arguments.slice(1, 2)
|
|
1535
|
+
: [];
|
|
1536
|
+
for (const value of written)
|
|
1537
|
+
borrowedValues(literalClassValues(value), value, 'classList write');
|
|
1538
|
+
}
|
|
1539
|
+
if (
|
|
1540
|
+
method === 'setAttribute' &&
|
|
1541
|
+
node.arguments[0] &&
|
|
1542
|
+
ts.isStringLiteralLike(node.arguments[0]) &&
|
|
1543
|
+
node.arguments[0].text === 'class'
|
|
1544
|
+
)
|
|
1545
|
+
borrowedValues(
|
|
1546
|
+
literalClassValues(node.arguments[1]),
|
|
1547
|
+
node.arguments[1] ?? node,
|
|
1548
|
+
'setAttribute write',
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1129
1551
|
const entry = ts.isIdentifier(node.expression)
|
|
1130
1552
|
? imports.get(node.expression.text)
|
|
1131
1553
|
: ts.isPropertyAccessExpression(node.expression) &&
|
|
@@ -1150,6 +1572,25 @@ function inspectTsx(
|
|
|
1150
1572
|
diagnostics,
|
|
1151
1573
|
);
|
|
1152
1574
|
}
|
|
1575
|
+
if (
|
|
1576
|
+
ts.isBinaryExpression(node) &&
|
|
1577
|
+
[ts.SyntaxKind.EqualsToken, ts.SyntaxKind.PlusEqualsToken].includes(
|
|
1578
|
+
node.operatorToken.kind,
|
|
1579
|
+
) &&
|
|
1580
|
+
ts.isPropertyAccessExpression(node.left) &&
|
|
1581
|
+
node.left.name.text === 'className'
|
|
1582
|
+
)
|
|
1583
|
+
borrowedValues(
|
|
1584
|
+
literalClassValues(node.right),
|
|
1585
|
+
node.right,
|
|
1586
|
+
'className assignment',
|
|
1587
|
+
);
|
|
1588
|
+
if (ts.isStringLiteralLike(node) && node.text.includes('<')) {
|
|
1589
|
+
for (const match of node.text.matchAll(
|
|
1590
|
+
/<[a-z][^>]*\sclass\s*=\s*["']([^"']+)["']/gi,
|
|
1591
|
+
))
|
|
1592
|
+
borrowedValues(match[1].split(/\s+/).filter(Boolean), node, 'raw HTML');
|
|
1593
|
+
}
|
|
1153
1594
|
ts.forEachChild(node, visit);
|
|
1154
1595
|
};
|
|
1155
1596
|
visit(source);
|
|
@@ -1161,6 +1602,8 @@ export async function analyzeUiProject({
|
|
|
1161
1602
|
knownRules = [],
|
|
1162
1603
|
adoption = false,
|
|
1163
1604
|
ownership = 'package',
|
|
1605
|
+
ownershipContext = 'subject',
|
|
1606
|
+
implicitComponentOwnership = false,
|
|
1164
1607
|
profile: packageProfile = resolve(
|
|
1165
1608
|
import.meta.dirname,
|
|
1166
1609
|
'../ai/application-ui-profile.defaults.json',
|
|
@@ -1196,6 +1639,13 @@ export async function analyzeUiProject({
|
|
|
1196
1639
|
}
|
|
1197
1640
|
}
|
|
1198
1641
|
files.sort();
|
|
1642
|
+
const imports = new Map(
|
|
1643
|
+
files.map((file) => [file, relativeStyleImports(file, contents.get(file))]),
|
|
1644
|
+
);
|
|
1645
|
+
const implicitEntries =
|
|
1646
|
+
ownership === 'component' && implicitComponentOwnership
|
|
1647
|
+
? await implicitOwnershipEntries(files, imports, root)
|
|
1648
|
+
: [];
|
|
1199
1649
|
const profileContexts = new Map();
|
|
1200
1650
|
const loadContext = async (file) => {
|
|
1201
1651
|
const startDirectory = file === root ? root : dirname(file);
|
|
@@ -1209,10 +1659,30 @@ export async function analyzeUiProject({
|
|
|
1209
1659
|
packageProfile,
|
|
1210
1660
|
knownRules: [...Object.keys(UI_ANALYSIS_RULES), ...knownRules],
|
|
1211
1661
|
});
|
|
1212
|
-
const entries = await loadCatalogs(profileResult);
|
|
1662
|
+
const entries = await loadCatalogs(profileResult, ownership);
|
|
1663
|
+
const catalogedSources = new Set(
|
|
1664
|
+
entries
|
|
1665
|
+
.filter((entry) => entry.componentSource)
|
|
1666
|
+
.map((entry) =>
|
|
1667
|
+
resolve(entry.catalogDirectory, entry.componentSource),
|
|
1668
|
+
),
|
|
1669
|
+
);
|
|
1670
|
+
const ownershipEntries = [
|
|
1671
|
+
...entries,
|
|
1672
|
+
...implicitEntries.filter(
|
|
1673
|
+
(entry) =>
|
|
1674
|
+
!catalogedSources.has(
|
|
1675
|
+
resolve(entry.catalogDirectory, entry.componentSource),
|
|
1676
|
+
),
|
|
1677
|
+
),
|
|
1678
|
+
];
|
|
1213
1679
|
return {
|
|
1214
1680
|
profileResult,
|
|
1215
|
-
facts: catalogFacts(
|
|
1681
|
+
facts: catalogFacts(
|
|
1682
|
+
ownership === 'component'
|
|
1683
|
+
? inferOwnedClasses(ownershipEntries, contents, imports)
|
|
1684
|
+
: entries,
|
|
1685
|
+
),
|
|
1216
1686
|
startDirectory,
|
|
1217
1687
|
};
|
|
1218
1688
|
})(),
|
|
@@ -1225,9 +1695,6 @@ export async function analyzeUiProject({
|
|
|
1225
1695
|
),
|
|
1226
1696
|
);
|
|
1227
1697
|
if (!files.length) await loadContext(root);
|
|
1228
|
-
const imports = new Map(
|
|
1229
|
-
files.map((file) => [file, relativeStyleImports(file, contents.get(file))]),
|
|
1230
|
-
);
|
|
1231
1698
|
// A stylesheet or source file belongs to the package whose manifest is
|
|
1232
1699
|
// nearest it; entries of every other catalog package are foreign to it.
|
|
1233
1700
|
const packages = new Map();
|
|
@@ -1375,6 +1842,7 @@ export async function analyzeUiProject({
|
|
|
1375
1842
|
: new Set([contexts.get(file)]);
|
|
1376
1843
|
for (const context of applicableContexts) {
|
|
1377
1844
|
const fileDiagnostics = [];
|
|
1845
|
+
const own = await packageOf(dirname(file));
|
|
1378
1846
|
await inspectCss(
|
|
1379
1847
|
file,
|
|
1380
1848
|
contents.get(file),
|
|
@@ -1384,6 +1852,19 @@ export async function analyzeUiProject({
|
|
|
1384
1852
|
adoption,
|
|
1385
1853
|
siblingOnLoud(file),
|
|
1386
1854
|
await foreignTo(file),
|
|
1855
|
+
ownership === 'component'
|
|
1856
|
+
? {
|
|
1857
|
+
...jsxOwnershipEvidence(
|
|
1858
|
+
file,
|
|
1859
|
+
context.facts,
|
|
1860
|
+
contents,
|
|
1861
|
+
root,
|
|
1862
|
+
compilerOptions,
|
|
1863
|
+
),
|
|
1864
|
+
ownPackage: own?.name,
|
|
1865
|
+
}
|
|
1866
|
+
: undefined,
|
|
1867
|
+
ownershipContext,
|
|
1387
1868
|
);
|
|
1388
1869
|
recordDiagnostics(fileDiagnostics, context);
|
|
1389
1870
|
}
|
package/dist/catalog.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Workbench } from './chunk-
|
|
1
|
+
import { Workbench } from './chunk-KCCEH7RF.js';
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
import { PopupMenu } from './chunk-J3KGJ6P6.js';
|
|
5
5
|
|
|
6
6
|
import { Row } from './chunk-VZOFZPSO.js';
|
|
7
|
-
import { ListHeader } from './chunk-
|
|
7
|
+
import { ListHeader } from './chunk-M6DEEBEF.js';
|
|
8
8
|
import { Text } from './chunk-YDGNU2OD.js';
|
|
9
9
|
import { ListInsetText } from './chunk-DG6G5EPF.js';
|
|
10
10
|
import { ListItem } from './chunk-NC4CFOID.js';
|
|
@@ -20,7 +20,7 @@ import { List } from './chunk-W2AC6Q2A.js';
|
|
|
20
20
|
|
|
21
21
|
import { ToolbarControlGroup, ToolbarActionLink } from './chunk-QJ5VFCW3.js';
|
|
22
22
|
import { Toolbar } from './chunk-RGS4WX2M.js';
|
|
23
|
-
import { LucideIcon } from './chunk-
|
|
23
|
+
import { LucideIcon } from './chunk-WTSNV5RG.js';
|
|
24
24
|
|
|
25
25
|
import { filterDataAttributes } from './chunk-SRSJO5QE.js';
|
|
26
26
|
|