@starklab/stark-mcp 0.1.0 → 0.2.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/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/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,319 +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
|
-
|
|
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
|
-
});
|
|
@@ -1,324 +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
|
-
|
|
7
|
-
import { resolveWrappers } from './wrapperResolver.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-wrap-'));
|
|
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 wrapper(result, exportedAs) {
|
|
28
|
-
const found = result.wrappers.find((w) => w.exportedAs === exportedAs);
|
|
29
|
-
if (!found) throw new Error(`No wrapper "${exportedAs}" in result. Found: ${result.wrappers.map((w) => w.exportedAs).join(', ')}`);
|
|
30
|
-
return found;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('resolveWrappers — faithfulness classification', () => {
|
|
34
|
-
it('classifies a pure passthrough wrapper as transparent', () => {
|
|
35
|
-
const root = fixture({
|
|
36
|
-
'src/MyButton.jsx': `
|
|
37
|
-
import { Button } from '@starklab/stk-components';
|
|
38
|
-
export function MyButton(props) {
|
|
39
|
-
return <Button {...props} />;
|
|
40
|
-
}
|
|
41
|
-
`,
|
|
42
|
-
});
|
|
43
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
44
|
-
const w = wrapper(result, 'MyButton');
|
|
45
|
-
expect(w.terminal).toBe('ds-component');
|
|
46
|
-
expect(w.component).toBe('Button');
|
|
47
|
-
expect(w.chain[0].faithfulness).toBe('transparent');
|
|
48
|
-
expect(w.depth).toBe(1);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('classifies a hardcoded prop as constraining, even alongside a spread', () => {
|
|
52
|
-
const root = fixture({
|
|
53
|
-
'src/MyButton.jsx': `
|
|
54
|
-
import { Button } from '@starklab/stk-components';
|
|
55
|
-
export function MyButton(props) {
|
|
56
|
-
return <Button {...props} variant="primary" />;
|
|
57
|
-
}
|
|
58
|
-
`,
|
|
59
|
-
});
|
|
60
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
61
|
-
expect(wrapper(result, 'MyButton').chain[0].faithfulness).toBe('constraining');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('classifies a className/style override as divergent, ranked above constraining', () => {
|
|
65
|
-
const root = fixture({
|
|
66
|
-
'src/MyButton.jsx': `
|
|
67
|
-
import { Button } from '@starklab/stk-components';
|
|
68
|
-
export function MyButton(props) {
|
|
69
|
-
return <Button {...props} variant="primary" className="custom-look" />;
|
|
70
|
-
}
|
|
71
|
-
`,
|
|
72
|
-
});
|
|
73
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
74
|
-
expect(wrapper(result, 'MyButton').chain[0].faithfulness).toBe('divergent');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('classifies a native wrapper around a single DS-bearing child as augmenting', () => {
|
|
78
|
-
const root = fixture({
|
|
79
|
-
'src/FormField.jsx': `
|
|
80
|
-
import { Button } from '@starklab/stk-components';
|
|
81
|
-
export function FormField() {
|
|
82
|
-
return (
|
|
83
|
-
<div className="field">
|
|
84
|
-
<Button variant="primary">Save</Button>
|
|
85
|
-
</div>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
`,
|
|
89
|
-
});
|
|
90
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
91
|
-
const w = wrapper(result, 'FormField');
|
|
92
|
-
expect(w.terminal).toBe('ds-component');
|
|
93
|
-
expect(w.component).toBe('Button');
|
|
94
|
-
expect(w.chain[0].faithfulness).toBe('augmenting');
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('resolveWrappers — terminal classification', () => {
|
|
99
|
-
it('does not report a native-only wrapper (no DS component reachable) as a wrapper', () => {
|
|
100
|
-
const root = fixture({
|
|
101
|
-
'src/Plain.jsx': `
|
|
102
|
-
export function Plain() {
|
|
103
|
-
return <div className="plain">hello</div>;
|
|
104
|
-
}
|
|
105
|
-
`,
|
|
106
|
-
});
|
|
107
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
108
|
-
expect(result.wrappers.find((w) => w.exportedAs === 'Plain')).toBeUndefined();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('does not report a third-party-only wrapper as a wrapper', () => {
|
|
112
|
-
const root = fixture({
|
|
113
|
-
'src/MyThing.jsx': `
|
|
114
|
-
import { Box } from 'some-other-lib';
|
|
115
|
-
export function MyThing() {
|
|
116
|
-
return <Box>hi</Box>;
|
|
117
|
-
}
|
|
118
|
-
`,
|
|
119
|
-
});
|
|
120
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
121
|
-
expect(result.wrappers.find((w) => w.exportedAs === 'MyThing')).toBeUndefined();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('never crashes or reports a wrapper for a circular local wrapper chain', () => {
|
|
125
|
-
const root = fixture({
|
|
126
|
-
'src/CycleA.jsx': `
|
|
127
|
-
import { CycleB } from './CycleB.jsx';
|
|
128
|
-
export function CycleA(props) { return <CycleB {...props} />; }
|
|
129
|
-
`,
|
|
130
|
-
'src/CycleB.jsx': `
|
|
131
|
-
import { CycleA } from './CycleA.jsx';
|
|
132
|
-
export function CycleB(props) { return <CycleA {...props} />; }
|
|
133
|
-
`,
|
|
134
|
-
});
|
|
135
|
-
expect(() => resolveWrappers(root, { platform: 'web' })).not.toThrow();
|
|
136
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
137
|
-
expect(result.wrappers.find((w) => w.exportedAs === 'CycleA')).toBeUndefined();
|
|
138
|
-
expect(result.wrappers.find((w) => w.exportedAs === 'CycleB')).toBeUndefined();
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('resolveWrappers — cross-file recursive chains', () => {
|
|
143
|
-
it('follows a wrapper-of-a-wrapper chain across files down to the DS terminal', () => {
|
|
144
|
-
const root = fixture({
|
|
145
|
-
'src/ButtonB.jsx': `
|
|
146
|
-
import { Button } from '@starklab/stk-components';
|
|
147
|
-
export function ButtonB(props) {
|
|
148
|
-
return <Button {...props} />;
|
|
149
|
-
}
|
|
150
|
-
`,
|
|
151
|
-
'src/ButtonA.jsx': `
|
|
152
|
-
import { ButtonB } from './ButtonB.jsx';
|
|
153
|
-
export function ButtonA(props) {
|
|
154
|
-
return <ButtonB {...props} />;
|
|
155
|
-
}
|
|
156
|
-
`,
|
|
157
|
-
});
|
|
158
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
159
|
-
const a = wrapper(result, 'ButtonA');
|
|
160
|
-
const b = wrapper(result, 'ButtonB');
|
|
161
|
-
expect(a.component).toBe('Button');
|
|
162
|
-
expect(a.depth).toBe(2);
|
|
163
|
-
expect(b.component).toBe('Button');
|
|
164
|
-
expect(b.depth).toBe(1);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('resolveWrappers — fanout counting', () => {
|
|
169
|
-
it('excludes a parent wrapper’s internal reference from the child wrapper’s own fanout', () => {
|
|
170
|
-
const root = fixture({
|
|
171
|
-
'src/ButtonB.jsx': `
|
|
172
|
-
import { Button } from '@starklab/stk-components';
|
|
173
|
-
export function ButtonB(props) {
|
|
174
|
-
return <Button {...props} />;
|
|
175
|
-
}
|
|
176
|
-
`,
|
|
177
|
-
'src/ButtonA.jsx': `
|
|
178
|
-
import { ButtonB } from './ButtonB.jsx';
|
|
179
|
-
export function ButtonA(props) {
|
|
180
|
-
return <ButtonB {...props} />;
|
|
181
|
-
}
|
|
182
|
-
`,
|
|
183
|
-
'src/App.jsx': `
|
|
184
|
-
import { ButtonA } from './ButtonA.jsx';
|
|
185
|
-
import { ButtonB } from './ButtonB.jsx';
|
|
186
|
-
export const App = () => (
|
|
187
|
-
<div>
|
|
188
|
-
<ButtonA />
|
|
189
|
-
<ButtonA />
|
|
190
|
-
<ButtonB />
|
|
191
|
-
</div>
|
|
192
|
-
);
|
|
193
|
-
`,
|
|
194
|
-
});
|
|
195
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
196
|
-
const a = wrapper(result, 'ButtonA');
|
|
197
|
-
const b = wrapper(result, 'ButtonB');
|
|
198
|
-
// App.jsx uses ButtonA twice and ButtonB once directly; ButtonA's own
|
|
199
|
-
// internal <ButtonB/> render must not inflate ButtonB's fanout.
|
|
200
|
-
expect(a.fanout.counts.jsx).toBe(2);
|
|
201
|
-
expect(b.fanout.counts.jsx).toBe(1);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe('resolveWrappers — dominion.config.json declared wrappers', () => {
|
|
206
|
-
it('fills in a wrapper auto-detection could not resolve', () => {
|
|
207
|
-
const root = fixture({
|
|
208
|
-
'src/DynamicThing.jsx': `
|
|
209
|
-
import React from 'react';
|
|
210
|
-
import { Button } from '@starklab/stk-components';
|
|
211
|
-
const Impl = Button;
|
|
212
|
-
export function DynamicThing(props) {
|
|
213
|
-
return React.createElement(Impl, props);
|
|
214
|
-
}
|
|
215
|
-
`,
|
|
216
|
-
'dominion.config.json': JSON.stringify({ wrappers: { './src/DynamicThing.jsx': 'Button' } }),
|
|
217
|
-
});
|
|
218
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
219
|
-
expect(result.dominionConfigFound).toBe(true);
|
|
220
|
-
const w = wrapper(result, 'DynamicThing');
|
|
221
|
-
expect(w.component).toBe('Button');
|
|
222
|
-
expect(w.provenance).toBe('declared');
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('keeps the auto-detected terminal and reports a contradiction when the declaration disagrees', () => {
|
|
226
|
-
const root = fixture({
|
|
227
|
-
'src/MyButton.jsx': `
|
|
228
|
-
import { Button } from '@starklab/stk-components';
|
|
229
|
-
export function MyButton(props) {
|
|
230
|
-
return <Button {...props} />;
|
|
231
|
-
}
|
|
232
|
-
`,
|
|
233
|
-
'dominion.config.json': JSON.stringify({ wrappers: { './src/MyButton.jsx': 'BadgeStatus' } }),
|
|
234
|
-
});
|
|
235
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
236
|
-
const w = wrapper(result, 'MyButton');
|
|
237
|
-
expect(w.component).toBe('Button'); // auto-detected wins, not overridden
|
|
238
|
-
expect(result.contradictions).toHaveLength(1);
|
|
239
|
-
expect(result.contradictions[0]).toMatchObject({ declaredAs: 'BadgeStatus', autoDetected: 'Button' });
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('reports an unresolved declaration when the configured file does not exist', () => {
|
|
243
|
-
const root = fixture({
|
|
244
|
-
'src/App.jsx': `export const App = () => null;`,
|
|
245
|
-
'dominion.config.json': JSON.stringify({ wrappers: { './src/Nope.jsx': 'Button' } }),
|
|
246
|
-
});
|
|
247
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
248
|
-
expect(result.unresolvedDeclarations).toHaveLength(1);
|
|
249
|
-
expect(result.unresolvedDeclarations[0].reason).toBe('file-not-found');
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
describe('resolveWrappers — cross-workspace resolution (ADOPTION_APP_PLAN.md §3e)', () => {
|
|
254
|
-
it('resolves a workspace-sibling import into its own source instead of stopping at third-party', () => {
|
|
255
|
-
const siblingDir = fixture({
|
|
256
|
-
'package.json': JSON.stringify({ name: '@acme/ui', main: 'src/index.js' }),
|
|
257
|
-
'src/index.js': `export { Wrapper } from './Wrapper.jsx';`,
|
|
258
|
-
'src/Wrapper.jsx': `
|
|
259
|
-
import { Button } from '@starklab/stk-components';
|
|
260
|
-
export function Wrapper(props) {
|
|
261
|
-
return <Button {...props} />;
|
|
262
|
-
}
|
|
263
|
-
`,
|
|
264
|
-
});
|
|
265
|
-
const root = fixture({
|
|
266
|
-
'src/App.jsx': `
|
|
267
|
-
import { Wrapper } from '@acme/ui';
|
|
268
|
-
export function AppWrapper(props) {
|
|
269
|
-
return <Wrapper {...props} />;
|
|
270
|
-
}
|
|
271
|
-
`,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const withoutWorkspace = resolveWrappers(root, { platform: 'web' });
|
|
275
|
-
expect(withoutWorkspace.wrappers.find((w) => w.exportedAs === 'AppWrapper')).toBeUndefined();
|
|
276
|
-
|
|
277
|
-
const workspacePackages = new Map([['@acme/ui', siblingDir]]);
|
|
278
|
-
const withWorkspace = resolveWrappers(root, { platform: 'web', workspacePackages });
|
|
279
|
-
const w = wrapper(withWorkspace, 'AppWrapper');
|
|
280
|
-
expect(w.terminal).toBe('ds-component');
|
|
281
|
-
expect(w.component).toBe('Button');
|
|
282
|
-
// AppWrapper (root) wraps Wrapper (sibling package) wraps Button — a
|
|
283
|
-
// genuine two-hop chain, same as the same-repo cross-file case above.
|
|
284
|
-
expect(w.depth).toBe(2);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('does not treat a package importing its own name as a crossing', () => {
|
|
288
|
-
const root = fixture({
|
|
289
|
-
'package.json': JSON.stringify({ name: '@acme/self', main: 'src/index.js' }),
|
|
290
|
-
'src/index.js': `export { Local } from './Local.jsx';`,
|
|
291
|
-
'src/Local.jsx': `
|
|
292
|
-
import { Button } from '@starklab/stk-components';
|
|
293
|
-
export function Local(props) {
|
|
294
|
-
return <Button {...props} />;
|
|
295
|
-
}
|
|
296
|
-
`,
|
|
297
|
-
});
|
|
298
|
-
const workspacePackages = new Map([['@acme/self', root]]);
|
|
299
|
-
expect(() => resolveWrappers(root, { platform: 'web', workspacePackages })).not.toThrow();
|
|
300
|
-
const result = resolveWrappers(root, { platform: 'web', workspacePackages });
|
|
301
|
-
const w = wrapper(result, 'Local');
|
|
302
|
-
expect(w.terminal).toBe('ds-component');
|
|
303
|
-
expect(w.component).toBe('Button');
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
describe('resolveWrappers — byComponent rollup', () => {
|
|
308
|
-
it('left-joins wrappers onto the full catalog', () => {
|
|
309
|
-
const root = fixture({
|
|
310
|
-
'src/MyButton.jsx': `
|
|
311
|
-
import { Button } from '@starklab/stk-components';
|
|
312
|
-
export function MyButton(props) {
|
|
313
|
-
return <Button {...props} />;
|
|
314
|
-
}
|
|
315
|
-
`,
|
|
316
|
-
});
|
|
317
|
-
const result = resolveWrappers(root, { platform: 'web' });
|
|
318
|
-
expect(result.byComponent.length).toBeGreaterThan(50);
|
|
319
|
-
const button = result.byComponent.find((c) => c.name === 'Button');
|
|
320
|
-
expect(button.viaWrappers).toHaveLength(1);
|
|
321
|
-
const accordion = result.byComponent.find((c) => c.name === 'Accordion');
|
|
322
|
-
expect(accordion.viaWrappers).toHaveLength(0);
|
|
323
|
-
});
|
|
324
|
-
});
|