@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,260 +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 { 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
|
-
});
|
|
@@ -1,178 +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 { resolveTailwindTokens } from './tailwindResolver.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-tailwind-'));
|
|
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, className) {
|
|
28
|
-
return result.usages.find((u) => u.className === className);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('resolveTailwindTokens — detection', () => {
|
|
32
|
-
it('reports detected:false with no report when neither a config nor an @theme block exists', () => {
|
|
33
|
-
const root = fixture({
|
|
34
|
-
'src/app.css': `.btn { color: red; }`,
|
|
35
|
-
'src/App.jsx': `export const App = () => <div className="flex p-4" />;`,
|
|
36
|
-
});
|
|
37
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
38
|
-
expect(result.detected).toBe(false);
|
|
39
|
-
expect(result.report).toBeNull();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('detects a v4 @theme block even with no config file', () => {
|
|
43
|
-
const root = fixture({
|
|
44
|
-
'src/app.css': `@theme { --color-primary: var(--stk-surface-brand-1-strong); }`,
|
|
45
|
-
});
|
|
46
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
47
|
-
expect(result.detected).toBe(true);
|
|
48
|
-
expect(result.source).toBe('v4');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('detects a v3 tailwind.config.js even with no @theme block', () => {
|
|
52
|
-
const root = fixture({
|
|
53
|
-
'tailwind.config.js': `module.exports = { theme: { extend: { colors: { primary: 'var(--stk-surface-brand-1-strong)' } } } };`,
|
|
54
|
-
});
|
|
55
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
56
|
-
expect(result.detected).toBe(true);
|
|
57
|
-
expect(result.source).toBe('v3');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('reports themeParseIncomplete instead of a zero score for a dynamic/function-based v3 config', () => {
|
|
61
|
-
const root = fixture({
|
|
62
|
-
'tailwind.config.js': `module.exports = { theme: (helpers) => ({ colors: { primary: helpers.colors.blue } }) };`,
|
|
63
|
-
});
|
|
64
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
65
|
-
expect(result.detected).toBe(true);
|
|
66
|
-
expect(result.themeParseIncomplete).toBe(true);
|
|
67
|
-
expect(result.report).toBeNull();
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('resolveTailwindTokens — v4 @theme classification', () => {
|
|
72
|
-
it('classifies a @theme entry aliasing an inventory stk token as conformant', () => {
|
|
73
|
-
const root = fixture({
|
|
74
|
-
'src/app.css': `@theme { --color-primary: var(--stk-surface-brand-1-strong); }`,
|
|
75
|
-
});
|
|
76
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
77
|
-
const prop = result.properties.find((p) => p.name === '--color-primary');
|
|
78
|
-
expect(prop.state).toBe('conformant');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('classifies a @theme entry with a raw value as drift', () => {
|
|
82
|
-
const root = fixture({
|
|
83
|
-
'src/app.css': `@theme { --color-primary: #1956dd; }`,
|
|
84
|
-
});
|
|
85
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
86
|
-
const prop = result.properties.find((p) => p.name === '--color-primary');
|
|
87
|
-
expect(prop.state).toBe('drift');
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('resolveTailwindTokens — v3 tailwind.config theme.extend.colors classification', () => {
|
|
92
|
-
it('classifies a flat color entry aliasing an inventory stk token as conformant', () => {
|
|
93
|
-
const root = fixture({
|
|
94
|
-
'tailwind.config.js': `module.exports = { theme: { extend: { colors: { primary: 'var(--stk-surface-brand-1-strong)' } } } };`,
|
|
95
|
-
});
|
|
96
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
97
|
-
const prop = result.properties.find((p) => p.name === '--color-primary');
|
|
98
|
-
expect(prop.state).toBe('conformant');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('classifies a nested DEFAULT/shade color entry', () => {
|
|
102
|
-
const root = fixture({
|
|
103
|
-
'tailwind.config.js': `module.exports = { theme: { extend: { colors: { brand: { DEFAULT: 'var(--stk-surface-brand-1-strong)', 500: '#1956dd' } } } } };`,
|
|
104
|
-
});
|
|
105
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
106
|
-
expect(result.properties.find((p) => p.name === '--color-brand').state).toBe('conformant');
|
|
107
|
-
expect(result.properties.find((p) => p.name === '--color-brand-500').state).toBe('drift');
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('resolveTailwindTokens — JSX className usage scanning', () => {
|
|
112
|
-
it('classifies a utility class resolving to a conformant theme entry as aliased', () => {
|
|
113
|
-
const root = fixture({
|
|
114
|
-
'src/app.css': `@theme { --color-primary: var(--stk-surface-brand-1-strong); }`,
|
|
115
|
-
'src/App.jsx': `export const App = () => <div className="bg-primary" />;`,
|
|
116
|
-
});
|
|
117
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
118
|
-
expect(usage(result, 'bg-primary').classification).toBe('aliased');
|
|
119
|
-
expect(result.report.aliased).toBe(1);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('classifies a utility class resolving to a drifted theme entry as drift, not aliased', () => {
|
|
123
|
-
const root = fixture({
|
|
124
|
-
'src/app.css': `@theme { --color-primary: #1956dd; }`,
|
|
125
|
-
'src/App.jsx': `export const App = () => <div className="bg-primary" />;`,
|
|
126
|
-
});
|
|
127
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
128
|
-
expect(usage(result, 'bg-primary').classification).toBe('drift');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('strips a variant prefix before matching the base utility', () => {
|
|
132
|
-
const root = fixture({
|
|
133
|
-
'src/app.css': `@theme { --color-primary: var(--stk-surface-brand-1-strong); }`,
|
|
134
|
-
'src/App.jsx': `export const App = () => <div className="hover:bg-primary dark:[&:hover]:bg-primary" />;`,
|
|
135
|
-
});
|
|
136
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
137
|
-
expect(usage(result, 'hover:bg-primary').classification).toBe('aliased');
|
|
138
|
-
expect(usage(result, 'dark:[&:hover]:bg-primary').classification).toBe('aliased');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('classifies an arbitrary-value var() utility as direct', () => {
|
|
142
|
-
const root = fixture({
|
|
143
|
-
'src/app.css': `@theme { --color-primary: var(--stk-surface-brand-1-strong); }`,
|
|
144
|
-
'src/App.jsx': `export const App = () => <div className="bg-[var(--stk-surface-brand-1-strong)]" />;`,
|
|
145
|
-
});
|
|
146
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
147
|
-
expect(usage(result, 'bg-[var(--stk-surface-brand-1-strong)]').classification).toBe('direct');
|
|
148
|
-
expect(result.report.direct).toBe(1);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('does not count an unrelated utility class or Tailwind default-palette color as a usage', () => {
|
|
152
|
-
const root = fixture({
|
|
153
|
-
'src/app.css': `@theme { --color-primary: var(--stk-surface-brand-1-strong); }`,
|
|
154
|
-
'src/App.jsx': `export const App = () => <div className="flex bg-red-500 p-4" />;`,
|
|
155
|
-
});
|
|
156
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
157
|
-
expect(usage(result, 'flex')).toBeUndefined();
|
|
158
|
-
expect(usage(result, 'bg-red-500')).toBeUndefined();
|
|
159
|
-
expect(usage(result, 'p-4')).toBeUndefined();
|
|
160
|
-
expect(result.report.total).toBe(0);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('does not count a dynamic className expression (clsx/ternary) since it is not statically resolvable', () => {
|
|
164
|
-
const root = fixture({
|
|
165
|
-
'src/app.css': `@theme { --color-primary: var(--stk-surface-brand-1-strong); }`,
|
|
166
|
-
'src/App.jsx': `export const App = ({ active }) => <div className={active ? 'bg-primary' : 'bg-primary'} />;`,
|
|
167
|
-
});
|
|
168
|
-
const result = resolveTailwindTokens(root, { platform: 'web' });
|
|
169
|
-
expect(result.report.total).toBe(0);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('resolveTailwindTokens — platform scope', () => {
|
|
174
|
-
it('throws for a non-web platform since Tailwind utility classes have no RN equivalent', () => {
|
|
175
|
-
const root = fixture({ 'tailwind.config.js': `module.exports = {};` });
|
|
176
|
-
expect(() => resolveTailwindTokens(root, { platform: 'native' })).toThrow(/web/i);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
@@ -1,227 +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 { discoverTargets, workspacePackageMap } from './targetDiscovery.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-targets-'));
|
|
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 target(discovery, dir) {
|
|
28
|
-
const found = discovery.targets.find((t) => t.dir === dir);
|
|
29
|
-
if (!found) throw new Error(`No target "${dir}". Found: ${discovery.targets.map((t) => t.dir).join(', ')}`);
|
|
30
|
-
return found;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('discoverTargets — manifest source resolution', () => {
|
|
34
|
-
it('reads npm/yarn array-form package.json#workspaces', () => {
|
|
35
|
-
const root = fixture({
|
|
36
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
37
|
-
'packages/a/package.json': JSON.stringify({ name: 'a' }),
|
|
38
|
-
'packages/b/package.json': JSON.stringify({ name: 'b' }),
|
|
39
|
-
});
|
|
40
|
-
const d = discoverTargets(root);
|
|
41
|
-
expect(d.manifestSource).toBe('package.json#workspaces');
|
|
42
|
-
expect(d.targets.map((t) => t.dir).sort()).toEqual(['packages/a', 'packages/b']);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('reads yarn object-form package.json#workspaces.packages', () => {
|
|
46
|
-
const root = fixture({
|
|
47
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: { packages: ['apps/*'] } }),
|
|
48
|
-
'apps/one/package.json': JSON.stringify({ name: 'one' }),
|
|
49
|
-
});
|
|
50
|
-
const d = discoverTargets(root);
|
|
51
|
-
expect(d.manifestSource).toBe('package.json#workspaces');
|
|
52
|
-
expect(d.targets.map((t) => t.dir)).toEqual(['apps/one']);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('falls back to pnpm-workspace.yaml when package.json has no workspaces field', () => {
|
|
56
|
-
const root = fixture({
|
|
57
|
-
'package.json': JSON.stringify({ name: 'root' }),
|
|
58
|
-
'pnpm-workspace.yaml': "packages:\n - 'packages/*'\n - 'apps/*'\n",
|
|
59
|
-
'packages/x/package.json': JSON.stringify({ name: 'x' }),
|
|
60
|
-
'apps/y/package.json': JSON.stringify({ name: 'y' }),
|
|
61
|
-
});
|
|
62
|
-
const d = discoverTargets(root);
|
|
63
|
-
expect(d.manifestSource).toBe('pnpm-workspace.yaml');
|
|
64
|
-
expect(d.targets.map((t) => t.dir).sort()).toEqual(['apps/y', 'packages/x']);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('falls back to a whole-repo scan when no manifest declares globs', () => {
|
|
68
|
-
const root = fixture({
|
|
69
|
-
'package.json': JSON.stringify({ name: 'root' }),
|
|
70
|
-
'nested/thing/package.json': JSON.stringify({ name: 'thing' }),
|
|
71
|
-
});
|
|
72
|
-
const d = discoverTargets(root);
|
|
73
|
-
expect(d.manifestSource).toBeNull();
|
|
74
|
-
expect(d.targets.map((t) => t.dir)).toEqual(['nested/thing']);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('treats the root itself as the sole target when nothing nested exists either', () => {
|
|
78
|
-
const root = fixture({
|
|
79
|
-
'package.json': JSON.stringify({ name: 'root' }),
|
|
80
|
-
});
|
|
81
|
-
const d = discoverTargets(root);
|
|
82
|
-
expect(d.targets.map((t) => t.dir)).toEqual(['.']);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('records turbo.json/nx.json presence as informational monorepoTooling flags', () => {
|
|
86
|
-
const root = fixture({
|
|
87
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
88
|
-
'turbo.json': '{}',
|
|
89
|
-
'packages/a/package.json': JSON.stringify({ name: 'a' }),
|
|
90
|
-
});
|
|
91
|
-
const d = discoverTargets(root);
|
|
92
|
-
expect(d.monorepoTooling).toEqual({ turbo: true, nx: false });
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('discoverTargets — scope classification', () => {
|
|
97
|
-
it('marks a direct @starklab/stk* dependent in scope', () => {
|
|
98
|
-
const root = fixture({
|
|
99
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
100
|
-
'packages/consumer/package.json': JSON.stringify({
|
|
101
|
-
name: 'consumer',
|
|
102
|
-
dependencies: { '@starklab/stk-components': '^1.0.0' },
|
|
103
|
-
}),
|
|
104
|
-
});
|
|
105
|
-
const d = discoverTargets(root);
|
|
106
|
-
const t = target(d, 'packages/consumer');
|
|
107
|
-
expect(t.inScope).toBe(true);
|
|
108
|
-
expect(t.scopeReason).toMatch(/depends on @starklab\/stk\*/);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('marks a target with no stk* dependency, direct or transitive, out of scope — not 0%', () => {
|
|
112
|
-
const root = fixture({
|
|
113
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
114
|
-
'packages/unrelated/package.json': JSON.stringify({ name: 'unrelated', dependencies: { lodash: '^4.0.0' } }),
|
|
115
|
-
});
|
|
116
|
-
const d = discoverTargets(root);
|
|
117
|
-
const t = target(d, 'packages/unrelated');
|
|
118
|
-
expect(t.inScope).toBe(false);
|
|
119
|
-
expect(t.scopeReason).toBe('no @starklab/stk* dependency, direct or transitive');
|
|
120
|
-
expect(d.excluded.map((x) => x.dir)).toContain('packages/unrelated');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('marks a transitive dependent (via a workspace sibling) in scope', () => {
|
|
124
|
-
const root = fixture({
|
|
125
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
126
|
-
'packages/core/package.json': JSON.stringify({
|
|
127
|
-
name: 'core',
|
|
128
|
-
dependencies: { '@starklab/stk': '^1.0.0' },
|
|
129
|
-
}),
|
|
130
|
-
'packages/app/package.json': JSON.stringify({ name: 'app', dependencies: { core: '1.0.0' } }),
|
|
131
|
-
});
|
|
132
|
-
const d = discoverTargets(root);
|
|
133
|
-
const t = target(d, 'packages/app');
|
|
134
|
-
expect(t.inScope).toBe(true);
|
|
135
|
-
expect(t.scopeReason).toMatch(/via a workspace sibling/);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('flags an excluded target with a UI framework dependency as an opportunity', () => {
|
|
139
|
-
const root = fixture({
|
|
140
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
141
|
-
'packages/ui-no-stk/package.json': JSON.stringify({ name: 'ui-no-stk', dependencies: { react: '^18.0.0' } }),
|
|
142
|
-
});
|
|
143
|
-
const d = discoverTargets(root);
|
|
144
|
-
const t = target(d, 'packages/ui-no-stk');
|
|
145
|
-
expect(t.inScope).toBe(false);
|
|
146
|
-
expect(t.rendersUi).toBe(true);
|
|
147
|
-
expect(d.opportunities.map((x) => x.dir)).toContain('packages/ui-no-stk');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('does not list an excluded, non-UI target as an opportunity', () => {
|
|
151
|
-
const root = fixture({
|
|
152
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
153
|
-
'packages/plain/package.json': JSON.stringify({ name: 'plain', dependencies: { lodash: '^4.0.0' } }),
|
|
154
|
-
});
|
|
155
|
-
const d = discoverTargets(root);
|
|
156
|
-
expect(d.opportunities.map((x) => x.dir)).not.toContain('packages/plain');
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe('discoverTargets — dominion.config.json overrides', () => {
|
|
161
|
-
it('force-includes a target with no stk* dependency, unrestricted', () => {
|
|
162
|
-
const root = fixture({
|
|
163
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
164
|
-
'packages/early/package.json': JSON.stringify({ name: 'early', dependencies: { lodash: '^4.0.0' } }),
|
|
165
|
-
'dominion.config.json': JSON.stringify({ targets: { forceInclude: ['packages/early'] } }),
|
|
166
|
-
});
|
|
167
|
-
const d = discoverTargets(root);
|
|
168
|
-
const t = target(d, 'packages/early');
|
|
169
|
-
expect(t.inScope).toBe(true);
|
|
170
|
-
expect(t.forceIncluded).toBe(true);
|
|
171
|
-
expect(t.scopeReason).toBe('force-included');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('force-excludes a target that would otherwise be in scope, and requires a reason', () => {
|
|
175
|
-
const root = fixture({
|
|
176
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
177
|
-
'packages/gaming/package.json': JSON.stringify({
|
|
178
|
-
name: 'gaming',
|
|
179
|
-
dependencies: { '@starklab/stk-components': '^1.0.0' },
|
|
180
|
-
}),
|
|
181
|
-
'dominion.config.json': JSON.stringify({
|
|
182
|
-
targets: { forceExclude: { 'packages/gaming': 'prototype, not tracked yet' } },
|
|
183
|
-
}),
|
|
184
|
-
});
|
|
185
|
-
const d = discoverTargets(root);
|
|
186
|
-
const t = target(d, 'packages/gaming');
|
|
187
|
-
expect(t.inScope).toBe(false);
|
|
188
|
-
expect(t.forceExcluded).toBe(true);
|
|
189
|
-
expect(t.scopeReason).toBe('force-excluded: prototype, not tracked yet');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('gives force-exclude priority over force-include when both name the same target', () => {
|
|
193
|
-
const root = fixture({
|
|
194
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
195
|
-
'packages/contested/package.json': JSON.stringify({ name: 'contested' }),
|
|
196
|
-
'dominion.config.json': JSON.stringify({
|
|
197
|
-
targets: {
|
|
198
|
-
forceInclude: ['packages/contested'],
|
|
199
|
-
forceExclude: { 'packages/contested': 'explicitly out' },
|
|
200
|
-
},
|
|
201
|
-
}),
|
|
202
|
-
});
|
|
203
|
-
const d = discoverTargets(root);
|
|
204
|
-
const t = target(d, 'packages/contested');
|
|
205
|
-
expect(t.inScope).toBe(false);
|
|
206
|
-
expect(t.scopeReason).toBe('force-excluded: explicitly out');
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe('workspacePackageMap', () => {
|
|
211
|
-
it('maps every target with a package.json#name to its absolute dir, in and out of scope alike', () => {
|
|
212
|
-
const root = fixture({
|
|
213
|
-
'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/*'] }),
|
|
214
|
-
'packages/consumer/package.json': JSON.stringify({
|
|
215
|
-
name: 'consumer',
|
|
216
|
-
dependencies: { '@starklab/stk-components': '^1.0.0' },
|
|
217
|
-
}),
|
|
218
|
-
'packages/unrelated/package.json': JSON.stringify({ name: 'unrelated' }),
|
|
219
|
-
});
|
|
220
|
-
const d = discoverTargets(root);
|
|
221
|
-
const map = workspacePackageMap(d);
|
|
222
|
-
expect(map.get('consumer')).toBe(path.join(root, 'packages/consumer'));
|
|
223
|
-
// Out-of-scope targets are still mapped — a cross-workspace wrapper
|
|
224
|
-
// chain can legitimately pass through one on its way to a DS component.
|
|
225
|
-
expect(map.get('unrelated')).toBe(path.join(root, 'packages/unrelated'));
|
|
226
|
-
});
|
|
227
|
-
});
|