@starklab/stark-mcp 0.1.0

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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/package.json +31 -0
  4. package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Home.jsx +21 -0
  5. package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Menu.jsx +13 -0
  6. package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Profile.jsx +11 -0
  7. package/src/adopt/__fixtures__/dominion-fixture-app/src/theme.css +34 -0
  8. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/AppButton.jsx +8 -0
  9. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/BrandButton.jsx +9 -0
  10. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/CardBase.jsx +9 -0
  11. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/FeatureCard.jsx +7 -0
  12. package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/SectionCard.jsx +12 -0
  13. package/src/adopt/catalog.js +88 -0
  14. package/src/adopt/dominionFixture.test.js +165 -0
  15. package/src/adopt/moduleGraph.js +232 -0
  16. package/src/adopt/parseSource.js +25 -0
  17. package/src/adopt/propApiResolver.js +278 -0
  18. package/src/adopt/propApiResolver.test.js +229 -0
  19. package/src/adopt/referenceResolver.js +151 -0
  20. package/src/adopt/referenceResolver.test.js +213 -0
  21. package/src/adopt/rnTailwindResolver.js +347 -0
  22. package/src/adopt/rnTailwindResolver.test.js +263 -0
  23. package/src/adopt/rnTokenAliasResolver.js +474 -0
  24. package/src/adopt/rnTokenAliasResolver.test.js +260 -0
  25. package/src/adopt/tailwindResolver.js +512 -0
  26. package/src/adopt/tailwindResolver.test.js +178 -0
  27. package/src/adopt/targetDiscovery.js +237 -0
  28. package/src/adopt/targetDiscovery.test.js +227 -0
  29. package/src/adopt/tokenAliasResolver.js +513 -0
  30. package/src/adopt/tokenAliasResolver.test.js +319 -0
  31. package/src/adopt/wrapperResolver.js +874 -0
  32. package/src/adopt/wrapperResolver.test.js +324 -0
  33. package/src/cli.js +376 -0
  34. package/src/data.js +267 -0
  35. package/src/data.test.js +231 -0
  36. package/src/index.js +8 -0
  37. package/src/server.js +149 -0
@@ -0,0 +1,260 @@
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
+
7
+ import { resolveRnTokenAliases } from './rnTokenAliasResolver.js';
8
+
9
+ let tmpDirs = [];
10
+
11
+ afterEach(() => {
12
+ for (const dir of tmpDirs) rmSync(dir, { recursive: true, force: true });
13
+ tmpDirs = [];
14
+ });
15
+
16
+ function fixture(files) {
17
+ const root = mkdtempSync(path.join(os.tmpdir(), 'stark-adopt-rn-alias-'));
18
+ tmpDirs.push(root);
19
+ for (const [rel, content] of Object.entries(files)) {
20
+ const full = path.join(root, rel);
21
+ mkdirSync(path.dirname(full), { recursive: true });
22
+ writeFileSync(full, content);
23
+ }
24
+ return root;
25
+ }
26
+
27
+ function usage(result, stkToken) {
28
+ return result.usages.find((u) => u.stkToken === stkToken);
29
+ }
30
+
31
+ function property(result, name) {
32
+ const found = result.properties.find((p) => p.name === name);
33
+ if (!found) throw new Error(`No property "${name}" in result. Found: ${result.properties.map((p) => p.name).join(', ')}`);
34
+ return found;
35
+ }
36
+
37
+ function findingsFor(result, rule, propName) {
38
+ return result.findings.filter((f) => f.rule === rule && f.property === propName);
39
+ }
40
+
41
+ describe('resolveRnTokenAliases — detection', () => {
42
+ it('reports detected:false with no report when no file imports a Stark RN token package', () => {
43
+ const root = fixture({
44
+ 'src/App.js': `
45
+ import { StyleSheet } from 'react-native';
46
+ const styles = StyleSheet.create({ container: { backgroundColor: '#1956dd' } });
47
+ `,
48
+ });
49
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
50
+ expect(result.detected).toBe(false);
51
+ expect(result.report).toBeNull();
52
+ });
53
+
54
+ it('detects an import from @starklab/stk/rn even if unused elsewhere', () => {
55
+ const root = fixture({
56
+ 'src/App.js': `
57
+ import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
58
+ `,
59
+ });
60
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
61
+ expect(result.detected).toBe(true);
62
+ expect(result.confidence).toBe('uncertain');
63
+ });
64
+ });
65
+
66
+ describe('resolveRnTokenAliases — usage classification (Pass 2)', () => {
67
+ it('classifies a directly-imported token referenced with no indirection as direct', () => {
68
+ const root = fixture({
69
+ 'src/App.js': `
70
+ import { StyleSheet } from 'react-native';
71
+ import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
72
+ const styles = StyleSheet.create({ container: { backgroundColor: stkSurfaceBrand1Strong } });
73
+ `,
74
+ });
75
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
76
+ expect(usage(result, 'stkSurfaceBrand1Strong').classification).toBe('direct');
77
+ expect(result.report.direct).toBe(1);
78
+ });
79
+
80
+ it('classifies a reference through a scalar local const alias as aliased', () => {
81
+ const root = fixture({
82
+ 'src/App.js': `
83
+ import { StyleSheet } from 'react-native';
84
+ import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
85
+ const primary = stkSurfaceBrand1Strong;
86
+ const styles = StyleSheet.create({ container: { backgroundColor: primary } });
87
+ `,
88
+ });
89
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
90
+ const u = result.usages.find((x) => x.ref === 'primary');
91
+ expect(u.classification).toBe('aliased');
92
+ expect(result.report.aliased).toBe(1);
93
+ });
94
+
95
+ it('classifies a reference through an object-literal property alias as aliased', () => {
96
+ const root = fixture({
97
+ 'src/App.js': `
98
+ import { StyleSheet } from 'react-native';
99
+ import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
100
+ const theme = { primary: stkSurfaceBrand1Strong };
101
+ const styles = StyleSheet.create({ container: { backgroundColor: theme.primary } });
102
+ `,
103
+ });
104
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
105
+ const u = result.usages.find((x) => x.ref === 'theme.primary');
106
+ expect(u.classification).toBe('aliased');
107
+ });
108
+
109
+ it('classifies a reference through a raw-literal object alias as drift, not aliased', () => {
110
+ const root = fixture({
111
+ 'src/App.js': `
112
+ import { StyleSheet } from 'react-native';
113
+ import { stkSpacingMd } from '@starklab/stk/rn-spacing'; // unused, only to satisfy detection
114
+ const theme = { primary: '#1956dd' };
115
+ const styles = StyleSheet.create({ container: { backgroundColor: theme.primary } });
116
+ `,
117
+ });
118
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
119
+ const p = property(result, 'theme.primary');
120
+ expect(p.state).toBe('drift');
121
+ const u = result.usages.find((x) => x.ref === 'theme.primary');
122
+ expect(u.classification).toBe('drift');
123
+ expect(findingsFor(result, 'drift-behind-alias', 'theme.primary')).toHaveLength(1);
124
+ });
125
+
126
+ it('classifies a reference through an alias to a foreign (non-Stark) import as broken', () => {
127
+ const root = fixture({
128
+ 'src/App.js': `
129
+ import { StyleSheet } from 'react-native';
130
+ import { stkSpacingMd } from '@starklab/stk/rn-spacing'; // unused, only to satisfy detection
131
+ import { someColor } from 'some-other-lib';
132
+ const theme = { primary: someColor };
133
+ const styles = StyleSheet.create({ container: { backgroundColor: theme.primary } });
134
+ `,
135
+ });
136
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
137
+ const p = property(result, 'theme.primary');
138
+ expect(p.state).toBe('broken');
139
+ expect(findingsFor(result, 'broken-alias', 'theme.primary')).toHaveLength(1);
140
+ });
141
+
142
+ it('applies the name-only heuristic when the containing object has no static origin (e.g. a useTokens() hook)', () => {
143
+ const root = fixture({
144
+ 'src/App.js': `
145
+ import { StyleSheet } from 'react-native';
146
+ import { stkSpacingMd } from '@starklab/stk/rn-spacing'; // unused, only to satisfy detection
147
+ import { useTokens } from '../theme/ThemeContext';
148
+ function useStyles() {
149
+ const t = useTokens();
150
+ return StyleSheet.create({ container: { backgroundColor: t.stkSurfaceBrand1Strong } });
151
+ }
152
+ `,
153
+ });
154
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
155
+ const u = usage(result, 'stkSurfaceBrand1Strong');
156
+ expect(u.classification).toBe('direct');
157
+ expect(u.heuristic).toBe(true);
158
+ });
159
+
160
+ it('does not count an unrelated member/identifier access as a usage', () => {
161
+ const root = fixture({
162
+ 'src/App.js': `
163
+ import { StyleSheet } from 'react-native';
164
+ import { stkSpacingMd } from '@starklab/stk/rn-spacing'; // unused, only to satisfy detection
165
+ const styles = StyleSheet.create({ container: { flexDirection: 'row' } });
166
+ `,
167
+ });
168
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
169
+ expect(result.report.total).toBe(0);
170
+ });
171
+ });
172
+
173
+ describe('resolveRnTokenAliases — layer violations', () => {
174
+ it('flags a scalar alias resolving straight to a primitive RN token', () => {
175
+ const root = fixture({
176
+ 'src/App.js': `
177
+ import { StyleSheet } from 'react-native';
178
+ import { stkSpacingMd } from '@starklab/stk/rn-spacing';
179
+ const gap = stkSpacingMd;
180
+ const styles = StyleSheet.create({ container: { padding: gap } });
181
+ `,
182
+ });
183
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
184
+ const p = property(result, 'gap');
185
+ expect(p.state).toBe('conformant');
186
+ const findings = findingsFor(result, 'layer-violation', 'gap');
187
+ expect(findings).toHaveLength(1);
188
+ expect(findings[0].severity).toBe('critical');
189
+ });
190
+ });
191
+
192
+ describe('resolveRnTokenAliases — depth', () => {
193
+ it('warns past ~3 hops on a multi-hop conformant scalar chain', () => {
194
+ const root = fixture({
195
+ 'src/App.js': `
196
+ import { StyleSheet } from 'react-native';
197
+ import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
198
+ const hop4 = stkSurfaceBrand1Strong;
199
+ const hop3 = hop4;
200
+ const hop2 = hop3;
201
+ const hop1 = hop2;
202
+ const styles = StyleSheet.create({ container: { backgroundColor: hop1 } });
203
+ `,
204
+ });
205
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
206
+ const p = property(result, 'hop1');
207
+ expect(p.state).toBe('conformant');
208
+ expect(findingsFor(result, 'deep-alias-chain', 'hop1')).toHaveLength(1);
209
+ });
210
+
211
+ it('does not warn on a short chain within the depth budget', () => {
212
+ const root = fixture({
213
+ 'src/App.js': `
214
+ import { StyleSheet } from 'react-native';
215
+ import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
216
+ const hop2 = stkSurfaceBrand1Strong;
217
+ const hop1 = hop2;
218
+ const styles = StyleSheet.create({ container: { backgroundColor: hop1 } });
219
+ `,
220
+ });
221
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
222
+ expect(findingsFor(result, 'deep-alias-chain', 'hop1')).toHaveLength(0);
223
+ });
224
+ });
225
+
226
+ describe('resolveRnTokenAliases — alias count and report', () => {
227
+ it('counts every distinct alias-graph entry and reports the three-number summary', () => {
228
+ const root = fixture({
229
+ 'src/App.js': `
230
+ import { StyleSheet } from 'react-native';
231
+ import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
232
+ const primary = stkSurfaceBrand1Strong;
233
+ const secondary = '#1956dd';
234
+ const styles = StyleSheet.create({
235
+ a: { backgroundColor: stkSurfaceBrand1Strong },
236
+ b: { backgroundColor: primary },
237
+ c: { backgroundColor: secondary },
238
+ });
239
+ `,
240
+ });
241
+ const result = resolveRnTokenAliases(root, { platform: 'native' });
242
+ expect(result.aliasCount).toBe(2); // primary, secondary
243
+ expect(result.report.direct).toBe(1); // a
244
+ expect(result.report.aliased).toBe(1); // b
245
+ // c aliases a plain string local const — the alias graph tracked it (it's
246
+ // a real consumer-authored indirection), it just doesn't terminate at a
247
+ // Stark token, so it folds into "unresolved" same as CSS drift does on web.
248
+ expect(result.report.unresolved).toBe(1); // c
249
+ expect(result.report.total).toBe(3);
250
+ });
251
+ });
252
+
253
+ describe('resolveRnTokenAliases — platform scope', () => {
254
+ it('throws for a non-native platform since JS-object token indirection has no CSS/Tailwind equivalent', () => {
255
+ const root = fixture({
256
+ 'src/App.js': `import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';`,
257
+ });
258
+ expect(() => resolveRnTokenAliases(root, { platform: 'web' })).toThrow(/native/i);
259
+ });
260
+ });