@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,263 +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 { resolveRnTailwindTokens } from './rnTailwindResolver.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-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
|
-
const NATIVEWIND_PKG = `{ "name": "app", "dependencies": { "nativewind": "^4.0.0" } }`;
|
|
28
|
-
|
|
29
|
-
function usage(result, className) {
|
|
30
|
-
return result.usages.find((u) => u.className === className);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('resolveRnTailwindTokens — NativeWind detection', () => {
|
|
34
|
-
it('reports detected:false with no report when there is no NativeWind signal at all', () => {
|
|
35
|
-
const root = fixture({
|
|
36
|
-
'package.json': `{ "name": "app", "dependencies": {} }`,
|
|
37
|
-
'src/App.js': `import { View } from 'react-native'; export const App = () => <View className="flex" />;`,
|
|
38
|
-
});
|
|
39
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
40
|
-
expect(result.detected).toBe(false);
|
|
41
|
-
expect(result.report).toBeNull();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('does not treat a bare tailwind.config.js as a NativeWind signal (could belong to an unrelated web app in the same monorepo)', () => {
|
|
45
|
-
const root = fixture({
|
|
46
|
-
'package.json': `{ "name": "app", "dependencies": {} }`,
|
|
47
|
-
'tailwind.config.js': `
|
|
48
|
-
import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
|
|
49
|
-
module.exports = { theme: { extend: { colors: { primary: stkSurfaceBrand1Strong } } } };
|
|
50
|
-
`,
|
|
51
|
-
});
|
|
52
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
53
|
-
expect(result.detected).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('detects via a "nativewind" package.json dependency', () => {
|
|
57
|
-
const root = fixture({ 'package.json': NATIVEWIND_PKG });
|
|
58
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
59
|
-
expect(result.detected).toBe(true);
|
|
60
|
-
expect(result.nativewindSignal).toBe('package.json');
|
|
61
|
-
expect(result.reason).toMatch(/no tailwind\.config/i);
|
|
62
|
-
expect(result.report).toBeNull();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('detects via a "nativewind/babel" preset in babel.config.js', () => {
|
|
66
|
-
const root = fixture({
|
|
67
|
-
'package.json': `{ "name": "app", "dependencies": {} }`,
|
|
68
|
-
'babel.config.js': `module.exports = { presets: ['babel-preset-expo', 'nativewind/babel'] };`,
|
|
69
|
-
});
|
|
70
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
71
|
-
expect(result.detected).toBe(true);
|
|
72
|
-
expect(result.nativewindSignal).toBe('babel.config');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('detects via a "withNativeWind" wrapper in metro.config.js', () => {
|
|
76
|
-
const root = fixture({
|
|
77
|
-
'package.json': `{ "name": "app", "dependencies": {} }`,
|
|
78
|
-
'metro.config.js': `
|
|
79
|
-
const { withNativeWind } = require('nativewind/metro');
|
|
80
|
-
module.exports = withNativeWind(config, { input: './global.css' });
|
|
81
|
-
`,
|
|
82
|
-
});
|
|
83
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
84
|
-
expect(result.detected).toBe(true);
|
|
85
|
-
expect(result.nativewindSignal).toBe('metro.config');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('reports themeParseIncomplete instead of a zero score for a dynamic/function-based v3 config', () => {
|
|
89
|
-
const root = fixture({
|
|
90
|
-
'package.json': NATIVEWIND_PKG,
|
|
91
|
-
'tailwind.config.js': `module.exports = { theme: (helpers) => ({ colors: { primary: helpers.colors.blue } }) };`,
|
|
92
|
-
});
|
|
93
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
94
|
-
expect(result.detected).toBe(true);
|
|
95
|
-
expect(result.themeParseIncomplete).toBe(true);
|
|
96
|
-
expect(result.report).toBeNull();
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe('resolveRnTailwindTokens — theme.colors classification', () => {
|
|
101
|
-
it('classifies a theme entry importing a Stark RN token identifier directly as conformant', () => {
|
|
102
|
-
const root = fixture({
|
|
103
|
-
'package.json': NATIVEWIND_PKG,
|
|
104
|
-
'tailwind.config.js': `
|
|
105
|
-
import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
|
|
106
|
-
module.exports = { theme: { extend: { colors: { primary: stkSurfaceBrand1Strong } } } };
|
|
107
|
-
`,
|
|
108
|
-
});
|
|
109
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
110
|
-
const prop = result.properties.find((p) => p.name === '--color-primary');
|
|
111
|
-
expect(prop.state).toBe('conformant');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('classifies a nested DEFAULT/shade color entry', () => {
|
|
115
|
-
const root = fixture({
|
|
116
|
-
'package.json': NATIVEWIND_PKG,
|
|
117
|
-
'tailwind.config.js': `
|
|
118
|
-
import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
|
|
119
|
-
module.exports = { theme: { extend: { colors: { brand: { DEFAULT: stkSurfaceBrand1Strong, 500: '#1956dd' } } } } };
|
|
120
|
-
`,
|
|
121
|
-
});
|
|
122
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
123
|
-
expect(result.properties.find((p) => p.name === '--color-brand').state).toBe('conformant');
|
|
124
|
-
expect(result.properties.find((p) => p.name === '--color-brand-500').state).toBe('drift');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('classifies a theme entry aliasing a raw literal as drift, not aliased', () => {
|
|
128
|
-
const root = fixture({
|
|
129
|
-
'package.json': NATIVEWIND_PKG,
|
|
130
|
-
'tailwind.config.js': `module.exports = { theme: { extend: { colors: { primary: '#1956dd' } } } };`,
|
|
131
|
-
});
|
|
132
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
133
|
-
const prop = result.properties.find((p) => p.name === '--color-primary');
|
|
134
|
-
expect(prop.state).toBe('drift');
|
|
135
|
-
expect(result.findings.some((f) => f.rule === 'drift-behind-alias' && f.property === '--color-primary')).toBe(true);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('classifies a theme entry aliasing a foreign (non-Stark) import as broken', () => {
|
|
139
|
-
const root = fixture({
|
|
140
|
-
'package.json': NATIVEWIND_PKG,
|
|
141
|
-
'tailwind.config.js': `
|
|
142
|
-
import { someColor } from 'some-other-lib';
|
|
143
|
-
module.exports = { theme: { extend: { colors: { primary: someColor } } } };
|
|
144
|
-
`,
|
|
145
|
-
});
|
|
146
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
147
|
-
const prop = result.properties.find((p) => p.name === '--color-primary');
|
|
148
|
-
expect(prop.state).toBe('broken');
|
|
149
|
-
expect(result.findings.some((f) => f.rule === 'broken-alias' && f.property === '--color-primary')).toBe(true);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('flags a theme entry resolving straight to a primitive RN token as a layer violation', () => {
|
|
153
|
-
const root = fixture({
|
|
154
|
-
'package.json': NATIVEWIND_PKG,
|
|
155
|
-
'tailwind.config.js': `
|
|
156
|
-
import { stkSpacingMd } from '@starklab/stk/rn-spacing';
|
|
157
|
-
module.exports = { theme: { extend: { colors: { primary: stkSpacingMd } } } };
|
|
158
|
-
`,
|
|
159
|
-
});
|
|
160
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
161
|
-
const prop = result.properties.find((p) => p.name === '--color-primary');
|
|
162
|
-
expect(prop.state).toBe('conformant');
|
|
163
|
-
const findings = result.findings.filter((f) => f.rule === 'layer-violation' && f.property === '--color-primary');
|
|
164
|
-
expect(findings).toHaveLength(1);
|
|
165
|
-
expect(findings[0].severity).toBe('critical');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('warns past ~3 hops on a multi-hop conformant chain declared in the config file itself', () => {
|
|
169
|
-
const root = fixture({
|
|
170
|
-
'package.json': NATIVEWIND_PKG,
|
|
171
|
-
'tailwind.config.js': `
|
|
172
|
-
import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
|
|
173
|
-
const hop4 = stkSurfaceBrand1Strong;
|
|
174
|
-
const hop3 = hop4;
|
|
175
|
-
const hop2 = hop3;
|
|
176
|
-
const hop1 = hop2;
|
|
177
|
-
module.exports = { theme: { extend: { colors: { primary: hop1 } } } };
|
|
178
|
-
`,
|
|
179
|
-
});
|
|
180
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
181
|
-
const prop = result.properties.find((p) => p.name === '--color-primary');
|
|
182
|
-
expect(prop.state).toBe('conformant');
|
|
183
|
-
expect(result.findings.some((f) => f.rule === 'deep-alias-chain' && f.property === '--color-primary')).toBe(true);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe('resolveRnTailwindTokens — className usage scanning', () => {
|
|
188
|
-
it('classifies a className resolving to a conformant theme entry as aliased', () => {
|
|
189
|
-
const root = fixture({
|
|
190
|
-
'package.json': NATIVEWIND_PKG,
|
|
191
|
-
'tailwind.config.js': `
|
|
192
|
-
import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
|
|
193
|
-
module.exports = { theme: { extend: { colors: { primary: stkSurfaceBrand1Strong } } } };
|
|
194
|
-
`,
|
|
195
|
-
'src/App.js': `import { View } from 'react-native'; export const App = () => <View className="bg-primary" />;`,
|
|
196
|
-
});
|
|
197
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
198
|
-
expect(usage(result, 'bg-primary').classification).toBe('aliased');
|
|
199
|
-
expect(result.report.aliased).toBe(1);
|
|
200
|
-
expect(result.report.direct).toBe(0);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('classifies a className resolving to a drifted theme entry as drift, not aliased', () => {
|
|
204
|
-
const root = fixture({
|
|
205
|
-
'package.json': NATIVEWIND_PKG,
|
|
206
|
-
'tailwind.config.js': `module.exports = { theme: { extend: { colors: { primary: '#1956dd' } } } };`,
|
|
207
|
-
'src/App.js': `import { View } from 'react-native'; export const App = () => <View className="bg-primary" />;`,
|
|
208
|
-
});
|
|
209
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
210
|
-
expect(usage(result, 'bg-primary').classification).toBe('drift');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('strips a variant prefix before matching the base utility', () => {
|
|
214
|
-
const root = fixture({
|
|
215
|
-
'package.json': NATIVEWIND_PKG,
|
|
216
|
-
'tailwind.config.js': `
|
|
217
|
-
import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
|
|
218
|
-
module.exports = { theme: { extend: { colors: { primary: stkSurfaceBrand1Strong } } } };
|
|
219
|
-
`,
|
|
220
|
-
'src/App.js': `import { View } from 'react-native'; export const App = () => <View className="dark:bg-primary" />;`,
|
|
221
|
-
});
|
|
222
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
223
|
-
expect(usage(result, 'dark:bg-primary').classification).toBe('aliased');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('does not count an unrelated utility class or Tailwind default-palette color as a usage', () => {
|
|
227
|
-
const root = fixture({
|
|
228
|
-
'package.json': NATIVEWIND_PKG,
|
|
229
|
-
'tailwind.config.js': `
|
|
230
|
-
import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
|
|
231
|
-
module.exports = { theme: { extend: { colors: { primary: stkSurfaceBrand1Strong } } } };
|
|
232
|
-
`,
|
|
233
|
-
'src/App.js': `import { View } from 'react-native'; export const App = () => <View className="flex bg-red-500" />;`,
|
|
234
|
-
});
|
|
235
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
236
|
-
expect(usage(result, 'flex')).toBeUndefined();
|
|
237
|
-
expect(usage(result, 'bg-red-500')).toBeUndefined();
|
|
238
|
-
expect(result.report.total).toBe(0);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('does not count a dynamic className expression (ternary) since it is not statically resolvable', () => {
|
|
242
|
-
const root = fixture({
|
|
243
|
-
'package.json': NATIVEWIND_PKG,
|
|
244
|
-
'tailwind.config.js': `
|
|
245
|
-
import { stkSurfaceBrand1Strong } from '@starklab/stk/rn';
|
|
246
|
-
module.exports = { theme: { extend: { colors: { primary: stkSurfaceBrand1Strong } } } };
|
|
247
|
-
`,
|
|
248
|
-
'src/App.js': `
|
|
249
|
-
import { View } from 'react-native';
|
|
250
|
-
export const App = ({ active }) => <View className={active ? 'bg-primary' : 'bg-primary'} />;
|
|
251
|
-
`,
|
|
252
|
-
});
|
|
253
|
-
const result = resolveRnTailwindTokens(root, { platform: 'native' });
|
|
254
|
-
expect(result.report.total).toBe(0);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe('resolveRnTailwindTokens — platform scope', () => {
|
|
259
|
-
it('throws for a non-native platform since NativeWind is the RN analog of web Tailwind', () => {
|
|
260
|
-
const root = fixture({ 'package.json': NATIVEWIND_PKG });
|
|
261
|
-
expect(() => resolveRnTailwindTokens(root, { platform: 'web' })).toThrow(/native/i);
|
|
262
|
-
});
|
|
263
|
-
});
|
|
@@ -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
|
-
});
|