@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.
Files changed (41) hide show
  1. package/package.json +10 -4
  2. package/src/adopt/adoptScanReport.js +124 -0
  3. package/src/adopt/catalog.js +26 -6
  4. package/src/adopt/foreignDiscoveryResolver.js +276 -0
  5. package/src/adopt/foreignPropSchemaResolver.js +134 -0
  6. package/src/adopt/foreignScanReport.js +210 -0
  7. package/src/adopt/foreignScoringResolver.js +192 -0
  8. package/src/adopt/foreignSystemConfig.js +356 -0
  9. package/src/adopt/installedPackageDiscoveryResolver.js +602 -0
  10. package/src/adopt/installedPackagePropSchemaResolver.js +279 -0
  11. package/src/adopt/installedPackageScoringResolver.js +153 -0
  12. package/src/adopt/installedSystemAutoDetector.js +51 -0
  13. package/src/adopt/installedSystemScan.js +101 -0
  14. package/src/adopt/jsxOpportunityHelpers.js +99 -0
  15. package/src/adopt/moduleGraph.js +39 -8
  16. package/src/adopt/opportunityResolver.js +255 -0
  17. package/src/adopt/opportunitySignaturesNative.js +47 -0
  18. package/src/adopt/usageRulesResolver.js +298 -0
  19. package/src/adopt/vecnaMaterializer.js +165 -0
  20. package/src/adopt/vecnaVerifier.js +127 -0
  21. package/src/cli.js +407 -1
  22. package/src/server.js +38 -14
  23. package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Home.jsx +0 -21
  24. package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Menu.jsx +0 -13
  25. package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Profile.jsx +0 -11
  26. package/src/adopt/__fixtures__/dominion-fixture-app/src/theme.css +0 -34
  27. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/AppButton.jsx +0 -8
  28. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/BrandButton.jsx +0 -9
  29. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/CardBase.jsx +0 -9
  30. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/FeatureCard.jsx +0 -7
  31. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/SectionCard.jsx +0 -12
  32. package/src/adopt/dominionFixture.test.js +0 -165
  33. package/src/adopt/propApiResolver.test.js +0 -229
  34. package/src/adopt/referenceResolver.test.js +0 -213
  35. package/src/adopt/rnTailwindResolver.test.js +0 -263
  36. package/src/adopt/rnTokenAliasResolver.test.js +0 -260
  37. package/src/adopt/tailwindResolver.test.js +0 -178
  38. package/src/adopt/targetDiscovery.test.js +0 -227
  39. package/src/adopt/tokenAliasResolver.test.js +0 -319
  40. package/src/adopt/wrapperResolver.test.js +0 -324
  41. 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,8 +0,0 @@
1
- import React from 'react';
2
- import { Button } from '@starklab/stk-components';
3
-
4
- // Transparent wrapper — spreads props through unmodified, no hardcoded or
5
- // style-only attributes.
6
- export function AppButton(props) {
7
- return <Button {...props} />;
8
- }
@@ -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,7 +0,0 @@
1
- import React from 'react';
2
- import { CardBase } from './CardBase';
3
-
4
- // Second hop of the chain — resolves through CardBase down to Card.
5
- export function FeatureCard(props) {
6
- return <CardBase {...props} />;
7
- }
@@ -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
- });