@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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +31 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Home.jsx +21 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Menu.jsx +13 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Profile.jsx +11 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/theme.css +34 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/AppButton.jsx +8 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/BrandButton.jsx +9 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/CardBase.jsx +9 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/FeatureCard.jsx +7 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/SectionCard.jsx +12 -0
- package/src/adopt/catalog.js +88 -0
- package/src/adopt/dominionFixture.test.js +165 -0
- package/src/adopt/moduleGraph.js +232 -0
- package/src/adopt/parseSource.js +25 -0
- package/src/adopt/propApiResolver.js +278 -0
- package/src/adopt/propApiResolver.test.js +229 -0
- package/src/adopt/referenceResolver.js +151 -0
- package/src/adopt/referenceResolver.test.js +213 -0
- package/src/adopt/rnTailwindResolver.js +347 -0
- package/src/adopt/rnTailwindResolver.test.js +263 -0
- package/src/adopt/rnTokenAliasResolver.js +474 -0
- package/src/adopt/rnTokenAliasResolver.test.js +260 -0
- package/src/adopt/tailwindResolver.js +512 -0
- package/src/adopt/tailwindResolver.test.js +178 -0
- package/src/adopt/targetDiscovery.js +237 -0
- package/src/adopt/targetDiscovery.test.js +227 -0
- package/src/adopt/tokenAliasResolver.js +513 -0
- package/src/adopt/tokenAliasResolver.test.js +319 -0
- package/src/adopt/wrapperResolver.js +874 -0
- package/src/adopt/wrapperResolver.test.js +324 -0
- package/src/cli.js +376 -0
- package/src/data.js +267 -0
- package/src/data.test.js +231 -0
- package/src/index.js +8 -0
- package/src/server.js +149 -0
|
@@ -0,0 +1,319 @@
|
|
|
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 { resolveTokenAliases } from './tokenAliasResolver.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-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 property(result, name) {
|
|
28
|
+
const found = result.properties.find((p) => p.name === name);
|
|
29
|
+
if (!found) throw new Error(`No property "${name}" in result. Found: ${result.properties.map((p) => p.name).join(', ')}`);
|
|
30
|
+
return found;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findingsFor(result, rule, propName) {
|
|
34
|
+
return result.findings.filter((f) => f.rule === rule && f.property === propName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('resolveTokenAliases — terminal classification', () => {
|
|
38
|
+
it('classifies a pure alias to an inventory stk token as conformant', () => {
|
|
39
|
+
const root = fixture({
|
|
40
|
+
'src/app.css': `:root { --app-primary: var(--stk-surface-brand-1-default); }`,
|
|
41
|
+
});
|
|
42
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
43
|
+
const p = property(result, '--app-primary');
|
|
44
|
+
expect(p.state).toBe('conformant');
|
|
45
|
+
expect(p.scopes[0].terminal.stkToken).toBe('--stk-surface-brand-1-default');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('classifies a raw-value alias as drift-behind-alias', () => {
|
|
49
|
+
const root = fixture({
|
|
50
|
+
'src/app.css': `:root { --app-primary: #1956dd; }`,
|
|
51
|
+
});
|
|
52
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
53
|
+
const p = property(result, '--app-primary');
|
|
54
|
+
expect(p.state).toBe('drift');
|
|
55
|
+
expect(findingsFor(result, 'drift-behind-alias', '--app-primary')).toHaveLength(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('classifies an alias to an undefined property as broken', () => {
|
|
59
|
+
const root = fixture({
|
|
60
|
+
'src/app.css': `:root { --app-primary: var(--app-does-not-exist); }`,
|
|
61
|
+
});
|
|
62
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
63
|
+
const p = property(result, '--app-primary');
|
|
64
|
+
expect(p.state).toBe('broken');
|
|
65
|
+
expect(findingsFor(result, 'broken-alias', '--app-primary')[0].reason).toBe('undefined');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('classifies a cyclic alias chain as broken without throwing', () => {
|
|
69
|
+
const root = fixture({
|
|
70
|
+
'src/app.css': `:root { --a: var(--b); --b: var(--a); }`,
|
|
71
|
+
});
|
|
72
|
+
expect(() => resolveTokenAliases(root, { platform: 'web' })).not.toThrow();
|
|
73
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
74
|
+
expect(property(result, '--a').state).toBe('broken');
|
|
75
|
+
expect(findingsFor(result, 'broken-alias', '--a')[0].reason).toBe('cycle');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('resolveTokenAliases — depth', () => {
|
|
80
|
+
it('warns past ~3 hops on a multi-hop conformant chain', () => {
|
|
81
|
+
const root = fixture({
|
|
82
|
+
'src/app.css': `
|
|
83
|
+
:root {
|
|
84
|
+
--hop1: var(--hop2);
|
|
85
|
+
--hop2: var(--hop3);
|
|
86
|
+
--hop3: var(--hop4);
|
|
87
|
+
--hop4: var(--stk-spacing-md);
|
|
88
|
+
}
|
|
89
|
+
`,
|
|
90
|
+
});
|
|
91
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
92
|
+
const p = property(result, '--hop1');
|
|
93
|
+
expect(p.state).toBe('conformant');
|
|
94
|
+
expect(findingsFor(result, 'deep-alias-chain', '--hop1')).toHaveLength(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not warn on a short chain within the depth budget', () => {
|
|
98
|
+
const root = fixture({
|
|
99
|
+
'src/app.css': `
|
|
100
|
+
:root {
|
|
101
|
+
--hop1: var(--hop2);
|
|
102
|
+
--hop2: var(--stk-spacing-md);
|
|
103
|
+
}
|
|
104
|
+
`,
|
|
105
|
+
});
|
|
106
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
107
|
+
expect(findingsFor(result, 'deep-alias-chain', '--hop1')).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('resolveTokenAliases — fallbacks', () => {
|
|
112
|
+
it('flags a raw-value fallback as info, never critical', () => {
|
|
113
|
+
const root = fixture({
|
|
114
|
+
'src/app.css': `:root { --app-primary: var(--stk-surface-brand-1-default, #1956dd); }`,
|
|
115
|
+
});
|
|
116
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
117
|
+
const findings = findingsFor(result, 'raw-fallback', '--app-primary');
|
|
118
|
+
expect(findings).toHaveLength(1);
|
|
119
|
+
expect(findings[0].severity).toBe('info');
|
|
120
|
+
// the primary resolves fine, so the property itself is still conformant
|
|
121
|
+
expect(property(result, '--app-primary').state).toBe('conformant');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does not flag a fully nested-var fallback', () => {
|
|
125
|
+
const root = fixture({
|
|
126
|
+
'src/app.css': `:root { --app-primary: var(--stk-surface-brand-1-default, var(--stk-spacing-md)); }`,
|
|
127
|
+
});
|
|
128
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
129
|
+
expect(findingsFor(result, 'raw-fallback', '--app-primary')).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('resolves through a raw fallback when the primary is undefined', () => {
|
|
133
|
+
const root = fixture({
|
|
134
|
+
'src/app.css': `:root { --app-primary: var(--app-missing, #1956dd); }`,
|
|
135
|
+
});
|
|
136
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
137
|
+
const p = property(result, '--app-primary');
|
|
138
|
+
expect(p.state).toBe('drift');
|
|
139
|
+
expect(p.scopes[0].terminal.viaFallback).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('resolveTokenAliases — layer violations', () => {
|
|
144
|
+
it('flags an alias resolving straight to a primitive token', () => {
|
|
145
|
+
const root = fixture({
|
|
146
|
+
'src/app.css': `:root { --app-primary: var(--stk-color-shade-palette-1-1); }`,
|
|
147
|
+
});
|
|
148
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
149
|
+
const p = property(result, '--app-primary');
|
|
150
|
+
// simultaneously conformant (resolves to an inventory stk token) AND a layer violation
|
|
151
|
+
expect(p.state).toBe('conformant');
|
|
152
|
+
const findings = findingsFor(result, 'layer-violation', '--app-primary');
|
|
153
|
+
expect(findings).toHaveLength(1);
|
|
154
|
+
expect(findings[0].severity).toBe('critical');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not flag an alias resolving to a semantic or component token', () => {
|
|
158
|
+
const root = fixture({
|
|
159
|
+
'src/app.css': `:root { --app-primary: var(--stk-surface-brand-1-default); }`,
|
|
160
|
+
});
|
|
161
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
162
|
+
expect(findingsFor(result, 'layer-violation', '--app-primary')).toHaveLength(0);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('resolveTokenAliases — partial-conformance across scopes', () => {
|
|
167
|
+
it('reports a property redefined differently under two selectors as partial-conformance, never picking a dominant scope', () => {
|
|
168
|
+
const root = fixture({
|
|
169
|
+
'src/app.css': `
|
|
170
|
+
:root { --app-primary: var(--stk-surface-brand-1-default); }
|
|
171
|
+
.theme-promo { --app-primary: #ff00ff; }
|
|
172
|
+
`,
|
|
173
|
+
});
|
|
174
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
175
|
+
const p = property(result, '--app-primary');
|
|
176
|
+
expect(p.state).toBe('partial-conformance');
|
|
177
|
+
expect(p.scopes).toHaveLength(2);
|
|
178
|
+
|
|
179
|
+
const finding = result.findings.find((f) => f.rule === 'partial-conformance' && f.property === '--app-primary');
|
|
180
|
+
expect(finding).toBeDefined();
|
|
181
|
+
expect(finding.severity).toBeNull();
|
|
182
|
+
expect(finding.scopes).toEqual(
|
|
183
|
+
expect.arrayContaining([
|
|
184
|
+
{ selector: ':root', state: 'conformant' },
|
|
185
|
+
{ selector: '.theme-promo', state: 'drift' },
|
|
186
|
+
])
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('does not report partial-conformance when two selectors agree on the same state', () => {
|
|
191
|
+
const root = fixture({
|
|
192
|
+
'src/app.css': `
|
|
193
|
+
:root { --app-primary: var(--stk-surface-brand-1-default); }
|
|
194
|
+
.card { --app-primary: var(--stk-surface-brand-1-strong); }
|
|
195
|
+
`,
|
|
196
|
+
});
|
|
197
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
198
|
+
expect(property(result, '--app-primary').state).toBe('conformant');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('resolveTokenAliases — usage classification (Pass 2)', () => {
|
|
203
|
+
it('classifies a direct --stk-* call site as direct', () => {
|
|
204
|
+
const root = fixture({
|
|
205
|
+
'src/app.css': `.btn { background: var(--stk-surface-brand-1-default); }`,
|
|
206
|
+
});
|
|
207
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
208
|
+
const usage = result.usages.find((u) => u.property === 'background');
|
|
209
|
+
expect(usage.classification).toBe('direct');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('classifies a call site through a conformant consumer alias as aliased', () => {
|
|
213
|
+
const root = fixture({
|
|
214
|
+
'src/app.css': `
|
|
215
|
+
:root { --app-primary: var(--stk-surface-brand-1-default); }
|
|
216
|
+
.btn { background: var(--app-primary); }
|
|
217
|
+
`,
|
|
218
|
+
});
|
|
219
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
220
|
+
const usage = result.usages.find((u) => u.property === 'background');
|
|
221
|
+
expect(usage.classification).toBe('aliased');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('classifies a call site through a drifted consumer alias as drift, not aliased', () => {
|
|
225
|
+
const root = fixture({
|
|
226
|
+
'src/app.css': `
|
|
227
|
+
:root { --app-primary: #1956dd; }
|
|
228
|
+
.btn { background: var(--app-primary); }
|
|
229
|
+
`,
|
|
230
|
+
});
|
|
231
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
232
|
+
const usage = result.usages.find((u) => u.property === 'background');
|
|
233
|
+
expect(usage.classification).toBe('drift');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('classifies a call site whose scope cannot be bound as unresolved', () => {
|
|
237
|
+
const root = fixture({
|
|
238
|
+
'src/app.css': `
|
|
239
|
+
:root { --app-primary: var(--stk-surface-brand-1-default); }
|
|
240
|
+
.theme-promo { --app-primary: #ff00ff; }
|
|
241
|
+
.btn { background: var(--app-primary); }
|
|
242
|
+
`,
|
|
243
|
+
});
|
|
244
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
245
|
+
const usage = result.usages.find((u) => u.property === 'background');
|
|
246
|
+
expect(usage.classification).toBe('unresolved');
|
|
247
|
+
expect(usage.reason).toBe('ambiguous-scope');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('reports the three-number summary with direct + aliased as conformancePct', () => {
|
|
251
|
+
const root = fixture({
|
|
252
|
+
'src/app.css': `
|
|
253
|
+
:root { --app-primary: var(--stk-surface-brand-1-default); }
|
|
254
|
+
.a { background: var(--stk-surface-brand-1-default); }
|
|
255
|
+
.b { background: var(--app-primary); }
|
|
256
|
+
.c { background: #1956dd; }
|
|
257
|
+
`,
|
|
258
|
+
});
|
|
259
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
260
|
+
expect(result.report.direct).toBe(1);
|
|
261
|
+
expect(result.report.aliased).toBe(1);
|
|
262
|
+
expect(result.report.total).toBe(2); // the raw literal in .c has no var() at all — not a usage site
|
|
263
|
+
expect(result.report.conformancePct).toBe(100);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('resolveTokenAliases — --stk-* declarations are never treated as consumer aliases', () => {
|
|
268
|
+
it('does not flag a synced copy of the design system\'s own tokens.css as layer violations or drift', () => {
|
|
269
|
+
const root = fixture({
|
|
270
|
+
// Mirrors apps/storybook/src/tokens/ — a checked-in copy of Stark's own
|
|
271
|
+
// compiled CSS, which legitimately aliases semantic --stk-* names
|
|
272
|
+
// straight to primitive --stk-* names. This is Stark's own internal
|
|
273
|
+
// aliasing, not something a consumer wrote, and must not appear in the
|
|
274
|
+
// consumer alias graph at all.
|
|
275
|
+
'src/tokens/tokens.css': `
|
|
276
|
+
:root {
|
|
277
|
+
--stk-surface-brand-1-subtlest: var(--stk-color-shade-palette-1-1);
|
|
278
|
+
--stk-spacing-md: 16px;
|
|
279
|
+
}
|
|
280
|
+
`,
|
|
281
|
+
});
|
|
282
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
283
|
+
expect(result.properties.find((p) => p.name === '--stk-surface-brand-1-subtlest')).toBeUndefined();
|
|
284
|
+
expect(result.aliasCount).toBe(0);
|
|
285
|
+
expect(result.findings).toHaveLength(0);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('still counts a genuine consumer alias declared alongside a synced tokens.css copy', () => {
|
|
289
|
+
const root = fixture({
|
|
290
|
+
'src/tokens/tokens.css': `:root { --stk-surface-brand-1-subtlest: var(--stk-color-shade-palette-1-1); }`,
|
|
291
|
+
'src/app.css': `:root { --app-primary: var(--stk-surface-brand-1-default); }`,
|
|
292
|
+
});
|
|
293
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
294
|
+
expect(result.aliasCount).toBe(1);
|
|
295
|
+
expect(property(result, '--app-primary').state).toBe('conformant');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('resolveTokenAliases — alias count', () => {
|
|
300
|
+
it('counts every distinct consumer-declared custom property, conformant or not', () => {
|
|
301
|
+
const root = fixture({
|
|
302
|
+
'src/app.css': `
|
|
303
|
+
:root {
|
|
304
|
+
--app-primary: var(--stk-surface-brand-1-default);
|
|
305
|
+
--app-secondary: #1956dd;
|
|
306
|
+
}
|
|
307
|
+
`,
|
|
308
|
+
});
|
|
309
|
+
const result = resolveTokenAliases(root, { platform: 'web' });
|
|
310
|
+
expect(result.aliasCount).toBe(2);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('resolveTokenAliases — platform scope', () => {
|
|
315
|
+
it('throws for a non-web platform since CSS var() has no RN equivalent', () => {
|
|
316
|
+
const root = fixture({ 'src/app.css': `:root {}` });
|
|
317
|
+
expect(() => resolveTokenAliases(root, { platform: 'native' })).toThrow(/web/i);
|
|
318
|
+
});
|
|
319
|
+
});
|