@starklab/stark-mcp 0.1.0 → 0.2.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/package.json +10 -4
- package/src/adopt/adoptScanReport.js +124 -0
- package/src/adopt/catalog.js +26 -6
- package/src/adopt/foreignDiscoveryResolver.js +276 -0
- package/src/adopt/foreignPropSchemaResolver.js +134 -0
- package/src/adopt/foreignScanReport.js +210 -0
- package/src/adopt/foreignScoringResolver.js +192 -0
- package/src/adopt/foreignSystemConfig.js +356 -0
- package/src/adopt/installedPackageDiscoveryResolver.js +602 -0
- package/src/adopt/installedPackagePropSchemaResolver.js +279 -0
- package/src/adopt/installedPackageScoringResolver.js +153 -0
- package/src/adopt/installedSystemAutoDetector.js +51 -0
- package/src/adopt/installedSystemScan.js +101 -0
- package/src/adopt/jsxOpportunityHelpers.js +99 -0
- package/src/adopt/moduleGraph.js +39 -8
- package/src/adopt/opportunityResolver.js +255 -0
- package/src/adopt/opportunitySignaturesNative.js +47 -0
- package/src/adopt/usageRulesResolver.js +298 -0
- package/src/adopt/vecnaMaterializer.js +165 -0
- package/src/adopt/vecnaVerifier.js +127 -0
- package/src/cli.js +407 -1
- package/src/server.js +38 -14
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Home.jsx +0 -21
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Menu.jsx +0 -13
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Profile.jsx +0 -11
- package/src/adopt/__fixtures__/dominion-fixture-app/src/theme.css +0 -34
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/AppButton.jsx +0 -8
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/BrandButton.jsx +0 -9
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/CardBase.jsx +0 -9
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/FeatureCard.jsx +0 -7
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/SectionCard.jsx +0 -12
- package/src/adopt/dominionFixture.test.js +0 -165
- package/src/adopt/propApiResolver.test.js +0 -229
- package/src/adopt/referenceResolver.test.js +0 -213
- package/src/adopt/rnTailwindResolver.test.js +0 -263
- package/src/adopt/rnTokenAliasResolver.test.js +0 -260
- package/src/adopt/tailwindResolver.test.js +0 -178
- package/src/adopt/targetDiscovery.test.js +0 -227
- package/src/adopt/tokenAliasResolver.test.js +0 -319
- package/src/adopt/wrapperResolver.test.js +0 -324
- package/src/data.test.js +0 -231
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
/* nested aliases — app-base -> app-mid -> app-top -> app-deep, terminal
|
|
3
|
-
depth 4 (deeper than tokenAliasResolver's WARN_DEPTH of 3), exercising
|
|
4
|
-
both transitive resolution and the deep-alias-chain finding */
|
|
5
|
-
--app-base: var(--stk-surface-brand-1-strong);
|
|
6
|
-
--app-mid: var(--app-base);
|
|
7
|
-
--app-top: var(--app-mid);
|
|
8
|
-
--app-deep: var(--app-top);
|
|
9
|
-
|
|
10
|
-
/* raw fallback — resolves conformant, still flagged info-level since the
|
|
11
|
-
fallback silently becomes load-bearing the day
|
|
12
|
-
--stk-surface-brand-1-strong is renamed */
|
|
13
|
-
--app-cta: var(--stk-surface-brand-1-strong, #1956dd);
|
|
14
|
-
|
|
15
|
-
/* primitive alias — layer violation: a consumer property pointing straight
|
|
16
|
-
at a base/primitive token instead of a semantic one */
|
|
17
|
-
--app-gap: var(--stk-spacing-md);
|
|
18
|
-
|
|
19
|
-
/* scoped-override target — conformant at :root */
|
|
20
|
-
--app-accent: var(--stk-surface-brand-1-strong);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/* scoped override — same property, different selector, resolving to a raw
|
|
24
|
-
hex under .theme-promo: :root and .theme-promo disagree, so the property
|
|
25
|
-
overall is partial-conformance rather than collapsed to one scope */
|
|
26
|
-
.theme-promo {
|
|
27
|
-
--app-accent: #1956dd;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/* Tailwind v4 theme entry — Home.jsx's className="dark:bg-primary" resolves
|
|
31
|
-
through this back to a Stark token */
|
|
32
|
-
@theme {
|
|
33
|
-
--color-primary: var(--stk-surface-brand-1-strong);
|
|
34
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Button } from '@starklab/stk-components';
|
|
3
|
-
|
|
4
|
-
// Divergent wrapper — a hardcoded className makes this a styling override,
|
|
5
|
-
// not a faithful passthrough. Used from several call sites (see
|
|
6
|
-
// pages/Home.jsx and pages/Profile.jsx) to exercise high fanout.
|
|
7
|
-
export function BrandButton(props) {
|
|
8
|
-
return <Button className="brand-button" {...props} />;
|
|
9
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Card } from '@starklab/stk-components';
|
|
3
|
-
|
|
4
|
-
// First hop of a two-level wrapper chain (see FeatureCard.jsx) — transparent
|
|
5
|
-
// passthrough. Only consumed internally by FeatureCard, so its own external
|
|
6
|
-
// fanout should be zero (the chain edge itself is excluded from fanout).
|
|
7
|
-
export function CardBase(props) {
|
|
8
|
-
return <Card {...props} />;
|
|
9
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Card } from '@starklab/stk-components';
|
|
3
|
-
|
|
4
|
-
// An "as"-polymorphic call site. ADOPTION_APP_PLAN.md §4 "Polymorphism" calls
|
|
5
|
-
// for a declared `polymorphicProp` field in prop-mapping/schema.json as
|
|
6
|
-
// future work — no dedicated `as`-detection exists in wrapperResolver.js yet,
|
|
7
|
-
// so a hardcoded `as` attribute is classified through the ordinary
|
|
8
|
-
// hardcodedProps -> "constraining" path, same as any other literal prop.
|
|
9
|
-
// This fixture documents that current (pre-declaration) behavior.
|
|
10
|
-
export function SectionCard(props) {
|
|
11
|
-
return <Card as="section" {...props} />;
|
|
12
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
import { resolveReferences } from './referenceResolver.js';
|
|
6
|
-
import { resolveWrappers } from './wrapperResolver.js';
|
|
7
|
-
import { resolveTokenAliases } from './tokenAliasResolver.js';
|
|
8
|
-
import { resolveTailwindTokens } from './tailwindResolver.js';
|
|
9
|
-
import { resolvePropApi } from './propApiResolver.js';
|
|
10
|
-
|
|
11
|
-
// This fixture app (__fixtures__/dominion-fixture-app/) is the shared
|
|
12
|
-
// consumer app ADOPTION_APP_PLAN.md's Phase 1 roadmap calls for — one app
|
|
13
|
-
// exercising every element the plan names (a theme file, nested aliases, a
|
|
14
|
-
// scoped override, a raw fallback, a primitive alias, a Tailwind variant, a
|
|
15
|
-
// transparent wrapper, a divergent high-fanout wrapper, a two-level wrapper
|
|
16
|
-
// chain, an "as"-polymorphic call site, a compound Foo.Bar usage, a direct
|
|
17
|
-
// invalid-enum-value call site, and a className passthrough) run through all
|
|
18
|
-
// five web resolvers together — treated as their test suite.
|
|
19
|
-
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '__fixtures__', 'dominion-fixture-app');
|
|
20
|
-
|
|
21
|
-
describe('dominion fixture app — resolveReferences', () => {
|
|
22
|
-
it('attributes direct and compound-tag usages to the right catalog components', () => {
|
|
23
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
24
|
-
const byName = new Map(result.components.map((c) => [c.name, c]));
|
|
25
|
-
|
|
26
|
-
expect(byName.get('Button').counts.jsx).toBe(3); // AppButton.jsx, BrandButton.jsx, Home.jsx
|
|
27
|
-
expect(byName.get('Card').counts.jsx).toBe(2); // CardBase.jsx, SectionCard.jsx
|
|
28
|
-
// outer <DropdownMenu> opening tag + <DropdownMenu.SubTrigger>'s object ref
|
|
29
|
-
expect(byName.get('DropdownMenu').counts.jsx).toBe(2);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('dominion fixture app — resolveWrappers', () => {
|
|
34
|
-
it('classifies faithfulness, chain depth, and fanout for every wrapper', () => {
|
|
35
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
36
|
-
const byExportedAs = new Map(result.wrappers.map((w) => [w.exportedAs, w]));
|
|
37
|
-
|
|
38
|
-
const appButton = byExportedAs.get('AppButton');
|
|
39
|
-
expect(appButton.component).toBe('Button');
|
|
40
|
-
expect(appButton.chain[0].faithfulness).toBe('transparent');
|
|
41
|
-
expect(appButton.depth).toBe(1);
|
|
42
|
-
|
|
43
|
-
const brandButton = byExportedAs.get('BrandButton');
|
|
44
|
-
expect(brandButton.component).toBe('Button');
|
|
45
|
-
expect(brandButton.chain[0].faithfulness).toBe('divergent');
|
|
46
|
-
expect(brandButton.fanout.counts.jsx).toBe(5); // 3x Home.jsx + 2x Profile.jsx
|
|
47
|
-
|
|
48
|
-
const cardBase = byExportedAs.get('CardBase');
|
|
49
|
-
expect(cardBase.component).toBe('Card');
|
|
50
|
-
expect(cardBase.chain[0].faithfulness).toBe('transparent');
|
|
51
|
-
// Only consumer is FeatureCard's own chain edge — excluded from fanout.
|
|
52
|
-
expect(cardBase.fanout.counts.jsx).toBe(0);
|
|
53
|
-
|
|
54
|
-
const featureCard = byExportedAs.get('FeatureCard');
|
|
55
|
-
expect(featureCard.component).toBe('Card');
|
|
56
|
-
expect(featureCard.depth).toBe(2);
|
|
57
|
-
expect(featureCard.warnDepth).toBe(false);
|
|
58
|
-
expect(featureCard.fanout.counts.jsx).toBe(1); // Home.jsx
|
|
59
|
-
|
|
60
|
-
const sectionCard = byExportedAs.get('SectionCard');
|
|
61
|
-
expect(sectionCard.component).toBe('Card');
|
|
62
|
-
// No dedicated `as`-polymorphism detection yet (ADOPTION_APP_PLAN.md §4)
|
|
63
|
-
// — a hardcoded `as` attribute takes the generic hardcodedProps path.
|
|
64
|
-
expect(sectionCard.chain[0].faithfulness).toBe('constraining');
|
|
65
|
-
|
|
66
|
-
const byComponent = new Map(result.byComponent.map((c) => [c.name, c]));
|
|
67
|
-
expect(byComponent.get('Card').viaWrappers.map((w) => w.exportedAs).sort()).toEqual([
|
|
68
|
-
'CardBase',
|
|
69
|
-
'FeatureCard',
|
|
70
|
-
'SectionCard',
|
|
71
|
-
]);
|
|
72
|
-
expect(byComponent.get('Button').viaWrappers.map((w) => w.exportedAs).sort()).toEqual([
|
|
73
|
-
'AppButton',
|
|
74
|
-
'BrandButton',
|
|
75
|
-
]);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('dominion fixture app — resolveTokenAliases', () => {
|
|
80
|
-
it('resolves nested aliases, flags deep chains, layer violations, raw fallbacks, and partial conformance', () => {
|
|
81
|
-
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
82
|
-
const byName = new Map(result.properties.map((p) => [p.name, p]));
|
|
83
|
-
|
|
84
|
-
expect(byName.get('--app-base').state).toBe('conformant');
|
|
85
|
-
expect(byName.get('--app-mid').state).toBe('conformant');
|
|
86
|
-
expect(byName.get('--app-top').state).toBe('conformant');
|
|
87
|
-
expect(byName.get('--app-deep').state).toBe('conformant');
|
|
88
|
-
|
|
89
|
-
const deepChain = result.findings.find((f) => f.rule === 'deep-alias-chain' && f.property === '--app-deep');
|
|
90
|
-
expect(deepChain).toBeDefined();
|
|
91
|
-
expect(deepChain.depth).toBe(4);
|
|
92
|
-
// Shallower hops in the same chain stay under WARN_DEPTH (3).
|
|
93
|
-
expect(result.findings.some((f) => f.rule === 'deep-alias-chain' && f.property === '--app-top')).toBe(false);
|
|
94
|
-
|
|
95
|
-
const rawFallback = result.findings.find((f) => f.rule === 'raw-fallback' && f.property === '--app-cta');
|
|
96
|
-
expect(rawFallback).toBeDefined();
|
|
97
|
-
|
|
98
|
-
const layerViolation = result.findings.find((f) => f.rule === 'layer-violation' && f.property === '--app-gap');
|
|
99
|
-
expect(layerViolation).toBeDefined();
|
|
100
|
-
expect(layerViolation.severity).toBe('critical');
|
|
101
|
-
|
|
102
|
-
expect(byName.get('--app-accent').state).toBe('partial-conformance');
|
|
103
|
-
expect(
|
|
104
|
-
result.findings.some(
|
|
105
|
-
(f) => f.rule === 'drift-behind-alias' && f.property === '--app-accent' && f.selector === '.theme-promo'
|
|
106
|
-
)
|
|
107
|
-
).toBe(true);
|
|
108
|
-
expect(result.findings.some((f) => f.rule === 'partial-conformance' && f.property === '--app-accent')).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('dominion fixture app — resolveTailwindTokens', () => {
|
|
113
|
-
it('resolves a v4 @theme entry through a variant-prefixed utility class back to a Stark token', () => {
|
|
114
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
115
|
-
|
|
116
|
-
expect(result.detected).toBe(true);
|
|
117
|
-
expect(result.source).toBe('v4');
|
|
118
|
-
|
|
119
|
-
const colorPrimary = result.properties.find((p) => p.name === '--color-primary');
|
|
120
|
-
expect(colorPrimary.state).toBe('conformant');
|
|
121
|
-
|
|
122
|
-
const usage = result.usages.find((u) => u.className === 'dark:bg-primary');
|
|
123
|
-
expect(usage).toBeDefined();
|
|
124
|
-
expect(usage.classification).toBe('aliased');
|
|
125
|
-
expect(result.report.aliased).toBeGreaterThanOrEqual(1);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('dominion fixture app — resolvePropApi', () => {
|
|
130
|
-
it('flags an invalid enum value and every className/style passthrough against real button/card mapping data', () => {
|
|
131
|
-
const result = resolvePropApi(root, { platform: 'web' });
|
|
132
|
-
|
|
133
|
-
// Only one literal enum value appears anywhere in the fixture app
|
|
134
|
-
// (Home.jsx's `variant="primry"`) — everything else is either a spread
|
|
135
|
-
// passthrough (AppButton.jsx, CardBase.jsx), an untracked attribute
|
|
136
|
-
// (`as` on SectionCard.jsx's Card, absent from both propMap and ignore),
|
|
137
|
-
// or has no attributes at all (Menu.jsx's DropdownMenu tags).
|
|
138
|
-
expect(result.report).toEqual({ total: 1, valid: 0, validPct: 0, invalid: 1, invalidPct: 100, unresolved: 0, unresolvedPct: 0 });
|
|
139
|
-
|
|
140
|
-
const enumFinding = result.findings.find((f) => f.rule === 'invalid-enum-value');
|
|
141
|
-
expect(enumFinding).toMatchObject({
|
|
142
|
-
component: 'Button',
|
|
143
|
-
prop: 'variant',
|
|
144
|
-
value: 'primry',
|
|
145
|
-
allowed: ['primary', 'ghost', 'outline', 'danger'],
|
|
146
|
-
severity: 'critical',
|
|
147
|
-
file: 'src/pages/Home.jsx',
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// className shows up on two real Button call sites — BrandButton.jsx's
|
|
151
|
-
// hardcoded wrapper prop and Home.jsx's own direct passthrough.
|
|
152
|
-
const styleFindings = result.findings.filter((f) => f.rule === 'style-escape-hatch');
|
|
153
|
-
expect(styleFindings.map((f) => f.file).sort()).toEqual([
|
|
154
|
-
'src/pages/Home.jsx',
|
|
155
|
-
'src/wrappers/BrandButton.jsx',
|
|
156
|
-
]);
|
|
157
|
-
expect(styleFindings.every((f) => f.component === 'Button' && f.prop === 'className' && f.severity === 'warning')).toBe(true);
|
|
158
|
-
|
|
159
|
-
// SectionCard.jsx's hardcoded `as="section"` is neither in Card's
|
|
160
|
-
// propMap nor its ignore list — absence of evidence, never a violation.
|
|
161
|
-
expect(result.findings.some((f) => f.file === 'src/wrappers/SectionCard.jsx')).toBe(false);
|
|
162
|
-
|
|
163
|
-
expect(result.findings).toHaveLength(3);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
6
|
-
import traverseModule from '@babel/traverse';
|
|
7
|
-
|
|
8
|
-
import { evaluateCallSite, resolvePropApi } from './propApiResolver.js';
|
|
9
|
-
import { parseSource } from './parseSource.js';
|
|
10
|
-
|
|
11
|
-
const traverse = traverseModule.default ?? traverseModule;
|
|
12
|
-
|
|
13
|
-
// Parses a single-element JSX snippet and returns its opening element's
|
|
14
|
-
// attribute nodes — the same shape resolvePropApi's own traversal passes
|
|
15
|
-
// into evaluateCallSite, so these unit tests exercise the exact input shape
|
|
16
|
-
// without needing a real mapping file or a scanned consumer app on disk.
|
|
17
|
-
function attrsFor(jsx) {
|
|
18
|
-
const ast = parseSource(`const El = () => (${jsx});`, 'snippet.jsx');
|
|
19
|
-
let attributes = null;
|
|
20
|
-
traverse(ast, {
|
|
21
|
-
JSXOpeningElement(p) {
|
|
22
|
-
if (attributes === null) attributes = p.node.attributes;
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
return attributes;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const ENUM_PROP_MAP = [
|
|
29
|
-
{ react: 'variant', type: 'enum', values: { primary: 'primary', ghost: 'ghost' } },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
let tmpDirs = [];
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
for (const dir of tmpDirs) rmSync(dir, { recursive: true, force: true });
|
|
36
|
-
tmpDirs = [];
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
function fixture(files) {
|
|
40
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'stark-adopt-propapi-'));
|
|
41
|
-
tmpDirs.push(root);
|
|
42
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
43
|
-
const full = path.join(root, rel);
|
|
44
|
-
mkdirSync(path.dirname(full), { recursive: true });
|
|
45
|
-
writeFileSync(full, content);
|
|
46
|
-
}
|
|
47
|
-
return root;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe('evaluateCallSite — enum values', () => {
|
|
51
|
-
it('classifies a literal value inside the declared enum as valid', () => {
|
|
52
|
-
const { checks, findings } = evaluateCallSite({
|
|
53
|
-
attributes: attrsFor('<Button variant="primary" />'),
|
|
54
|
-
propMap: ENUM_PROP_MAP,
|
|
55
|
-
component: 'Button',
|
|
56
|
-
});
|
|
57
|
-
expect(checks).toEqual([{ component: 'Button', prop: 'variant', rule: 'enum-value', classification: 'valid', value: 'primary', file: undefined, line: undefined }]);
|
|
58
|
-
expect(findings).toEqual([]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('classifies a literal value outside the declared enum as invalid, with an invalid-enum-value finding', () => {
|
|
62
|
-
const { checks, findings } = evaluateCallSite({
|
|
63
|
-
attributes: attrsFor('<Button variant="primry" />'),
|
|
64
|
-
propMap: ENUM_PROP_MAP,
|
|
65
|
-
component: 'Button',
|
|
66
|
-
});
|
|
67
|
-
expect(checks[0].classification).toBe('invalid');
|
|
68
|
-
expect(findings).toEqual([
|
|
69
|
-
{ rule: 'invalid-enum-value', severity: 'critical', component: 'Button', prop: 'variant', value: 'primry', allowed: ['primary', 'ghost'], file: undefined, line: undefined },
|
|
70
|
-
]);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('classifies a dynamic expression as unresolved, never valid or invalid', () => {
|
|
74
|
-
const { checks, findings } = evaluateCallSite({
|
|
75
|
-
attributes: attrsFor('<Button variant={someVar} />'),
|
|
76
|
-
propMap: ENUM_PROP_MAP,
|
|
77
|
-
component: 'Button',
|
|
78
|
-
});
|
|
79
|
-
expect(checks[0].classification).toBe('unresolved');
|
|
80
|
-
expect(findings).toEqual([]);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('also treats a template literal or ternary as unresolved', () => {
|
|
84
|
-
for (const jsx of ['<Button variant={`primary`} />', '<Button variant={cond ? "primary" : "ghost"} />']) {
|
|
85
|
-
const { checks } = evaluateCallSite({ attributes: attrsFor(jsx), propMap: ENUM_PROP_MAP, component: 'Button' });
|
|
86
|
-
expect(checks[0].classification).toBe('unresolved');
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('evaluateCallSite — required props', () => {
|
|
92
|
-
const REQUIRED_PROP_MAP = [{ react: 'label', type: 'text', required: true }];
|
|
93
|
-
|
|
94
|
-
it('flags a missing required prop as invalid, with a required-prop-missing finding', () => {
|
|
95
|
-
const { checks, findings } = evaluateCallSite({
|
|
96
|
-
attributes: attrsFor('<Field />'),
|
|
97
|
-
propMap: REQUIRED_PROP_MAP,
|
|
98
|
-
component: 'Field',
|
|
99
|
-
});
|
|
100
|
-
expect(checks).toEqual([{ component: 'Field', prop: 'label', rule: 'required-prop', classification: 'invalid', file: undefined, line: undefined }]);
|
|
101
|
-
expect(findings).toEqual([{ rule: 'required-prop-missing', severity: 'critical', component: 'Field', prop: 'label', file: undefined, line: undefined }]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('classifies a present required prop as valid', () => {
|
|
105
|
-
const { checks, findings } = evaluateCallSite({
|
|
106
|
-
attributes: attrsFor('<Field label="Name" />'),
|
|
107
|
-
propMap: REQUIRED_PROP_MAP,
|
|
108
|
-
component: 'Field',
|
|
109
|
-
});
|
|
110
|
-
expect(checks).toEqual([{ component: 'Field', prop: 'label', rule: 'required-prop', classification: 'valid', file: undefined, line: undefined }]);
|
|
111
|
-
expect(findings).toEqual([]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('leaves a missing required prop unresolved (not flagged) when the element spreads props', () => {
|
|
115
|
-
const { checks, findings } = evaluateCallSite({
|
|
116
|
-
attributes: attrsFor('<Field {...rest} />'),
|
|
117
|
-
propMap: REQUIRED_PROP_MAP,
|
|
118
|
-
component: 'Field',
|
|
119
|
-
});
|
|
120
|
-
expect(checks).toEqual([{ component: 'Field', prop: 'label', rule: 'required-prop', classification: 'unresolved', file: undefined, line: undefined }]);
|
|
121
|
-
expect(findings).toEqual([]);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('evaluateCallSite — deprecated props', () => {
|
|
126
|
-
it('flags a deprecated prop only when actually passed, with the reason carried through', () => {
|
|
127
|
-
const propMap = [{ react: 'legacy', type: 'text', deprecated: 'use "modern" instead' }];
|
|
128
|
-
|
|
129
|
-
const passed = evaluateCallSite({ attributes: attrsFor('<Widget legacy="x" />'), propMap, component: 'Widget' });
|
|
130
|
-
expect(passed.findings).toEqual([
|
|
131
|
-
{ rule: 'deprecated-prop-passed', severity: 'warning', component: 'Widget', prop: 'legacy', reason: 'use "modern" instead', file: undefined, line: undefined },
|
|
132
|
-
]);
|
|
133
|
-
// Presence-based, not value-based — doesn't feed the valid/invalid/unresolved ledger.
|
|
134
|
-
expect(passed.checks).toEqual([]);
|
|
135
|
-
|
|
136
|
-
const absent = evaluateCallSite({ attributes: attrsFor('<Widget />'), propMap, component: 'Widget' });
|
|
137
|
-
expect(absent.findings).toEqual([]);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('flags a plain deprecated: true prop with no reason field', () => {
|
|
141
|
-
const propMap = [{ react: 'legacy', type: 'text', deprecated: true }];
|
|
142
|
-
const { findings } = evaluateCallSite({ attributes: attrsFor('<Widget legacy="x" />'), propMap, component: 'Widget' });
|
|
143
|
-
expect(findings[0].reason).toBeUndefined();
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('evaluateCallSite — className/style escape hatch', () => {
|
|
148
|
-
it('flags className and style as style-escape-hatch findings, outside the ledger', () => {
|
|
149
|
-
const { checks, findings } = evaluateCallSite({
|
|
150
|
-
attributes: attrsFor('<Card className="foo" style={{ color: "red" }} />'),
|
|
151
|
-
propMap: [],
|
|
152
|
-
ignore: ['className', 'style'],
|
|
153
|
-
component: 'Card',
|
|
154
|
-
});
|
|
155
|
-
expect(checks).toEqual([]);
|
|
156
|
-
expect(findings).toEqual([
|
|
157
|
-
{ rule: 'style-escape-hatch', severity: 'warning', component: 'Card', prop: 'className', file: undefined, line: undefined },
|
|
158
|
-
{ rule: 'style-escape-hatch', severity: 'warning', component: 'Card', prop: 'style', file: undefined, line: undefined },
|
|
159
|
-
]);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe('evaluateCallSite — untracked props', () => {
|
|
164
|
-
it('never flags a prop in the mapping ignore list, or one absent from both propMap and ignore', () => {
|
|
165
|
-
const { checks, findings } = evaluateCallSite({
|
|
166
|
-
attributes: attrsFor('<Button aria-label="Close" onClick={fn} data-testid="x" />'),
|
|
167
|
-
propMap: ENUM_PROP_MAP,
|
|
168
|
-
ignore: ['aria-label', 'onClick', 'data-testid'],
|
|
169
|
-
component: 'Button',
|
|
170
|
-
});
|
|
171
|
-
expect(checks).toEqual([]);
|
|
172
|
-
expect(findings).toEqual([]);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('resolvePropApi — integration against the real Button mapping', () => {
|
|
177
|
-
it('throws for platform !== "web" — prop-mapping/ has no RN equivalent', () => {
|
|
178
|
-
const root = fixture({ 'src/App.jsx': `export const App = () => null;` });
|
|
179
|
-
expect(() => resolvePropApi(root, { platform: 'native' })).toThrow(/only supports platform "web"/);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('reports a valid check for a correct enum value at a real Button call site', () => {
|
|
183
|
-
const root = fixture({
|
|
184
|
-
'src/App.jsx': `
|
|
185
|
-
import { Button } from '@starklab/stk-components';
|
|
186
|
-
export const App = () => <Button variant="primary">Go</Button>;
|
|
187
|
-
`,
|
|
188
|
-
});
|
|
189
|
-
const result = resolvePropApi(root, { platform: 'web' });
|
|
190
|
-
expect(result.report).toEqual({ total: 1, valid: 1, validPct: 100, invalid: 0, invalidPct: 0, unresolved: 0, unresolvedPct: 0 });
|
|
191
|
-
expect(result.findings).toEqual([]);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('flags an invalid Button variant and a className passthrough at a real call site', () => {
|
|
195
|
-
const root = fixture({
|
|
196
|
-
'src/App.jsx': `
|
|
197
|
-
import { Button } from '@starklab/stk-components';
|
|
198
|
-
export const App = () => <Button variant="primry" className="hero-cta">Go</Button>;
|
|
199
|
-
`,
|
|
200
|
-
});
|
|
201
|
-
const result = resolvePropApi(root, { platform: 'web' });
|
|
202
|
-
expect(result.report.invalid).toBe(1);
|
|
203
|
-
expect(result.report.valid).toBe(0);
|
|
204
|
-
|
|
205
|
-
const enumFinding = result.findings.find((f) => f.rule === 'invalid-enum-value');
|
|
206
|
-
expect(enumFinding).toMatchObject({ component: 'Button', prop: 'variant', value: 'primry', severity: 'critical' });
|
|
207
|
-
|
|
208
|
-
const styleFinding = result.findings.find((f) => f.rule === 'style-escape-hatch');
|
|
209
|
-
expect(styleFinding).toMatchObject({ component: 'Button', prop: 'className', severity: 'warning' });
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('leaves a dynamic Button variant unresolved rather than scoring it', () => {
|
|
213
|
-
const root = fixture({
|
|
214
|
-
'src/App.jsx': `
|
|
215
|
-
import { Button } from '@starklab/stk-components';
|
|
216
|
-
export const App = ({ v }) => <Button variant={v}>Go</Button>;
|
|
217
|
-
`,
|
|
218
|
-
});
|
|
219
|
-
const result = resolvePropApi(root, { platform: 'web' });
|
|
220
|
-
expect(result.report).toEqual({ total: 1, valid: 0, validPct: 0, invalid: 0, invalidPct: 0, unresolved: 1, unresolvedPct: 100 });
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('reports an all-zero ledger when the consumer uses no catalog components', () => {
|
|
224
|
-
const root = fixture({ 'src/App.jsx': `export const App = () => <div />;` });
|
|
225
|
-
const result = resolvePropApi(root, { platform: 'web' });
|
|
226
|
-
expect(result.report).toEqual({ total: 0, valid: 0, validPct: 0, invalid: 0, invalidPct: 0, unresolved: 0, unresolvedPct: 0 });
|
|
227
|
-
expect(result.findings).toEqual([]);
|
|
228
|
-
});
|
|
229
|
-
});
|