@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,213 +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 { resolveReferences } from './referenceResolver.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-'));
|
|
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 component(result, name) {
|
|
28
|
-
const found = result.components.find((c) => c.name === name);
|
|
29
|
-
if (!found) throw new Error(`No component named "${name}" in result`);
|
|
30
|
-
return found;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('resolveReferences — reference kind classification', () => {
|
|
34
|
-
it('classifies a directly-imported JSX element as jsx, without double-counting the closing tag', () => {
|
|
35
|
-
const root = fixture({
|
|
36
|
-
'src/App.jsx': `
|
|
37
|
-
import { Button } from '@starklab/stk-components';
|
|
38
|
-
export const App = () => <Button variant="primary">Click</Button>;
|
|
39
|
-
`,
|
|
40
|
-
});
|
|
41
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
42
|
-
const button = component(result, 'Button');
|
|
43
|
-
expect(button.counts).toEqual({ jsx: 1, createElement: 0, hoc: 0, indirect: 0, reexport: 0 });
|
|
44
|
-
expect(button.sites).toHaveLength(1);
|
|
45
|
-
expect(button.sites[0].confidence).toBe('certain');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('classifies a compound JSX member expression (<Foo.Bar/>) as jsx, once per element', () => {
|
|
49
|
-
const root = fixture({
|
|
50
|
-
'src/App.jsx': `
|
|
51
|
-
import { Accordion } from '@starklab/stk-components';
|
|
52
|
-
export const App = () => <Accordion.Item>content</Accordion.Item>;
|
|
53
|
-
`,
|
|
54
|
-
});
|
|
55
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
56
|
-
const accordion = component(result, 'Accordion');
|
|
57
|
-
expect(accordion.counts.jsx).toBe(1);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('classifies React.createElement(Component, ...) and a bare invocation as createElement', () => {
|
|
61
|
-
const root = fixture({
|
|
62
|
-
'src/App.js': `
|
|
63
|
-
import React from 'react';
|
|
64
|
-
import { Button } from '@starklab/stk-components';
|
|
65
|
-
const a = React.createElement(Button, { variant: 'primary' });
|
|
66
|
-
const b = Button({ variant: 'ghost' });
|
|
67
|
-
`,
|
|
68
|
-
});
|
|
69
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
70
|
-
const button = component(result, 'Button');
|
|
71
|
-
expect(button.counts.createElement).toBe(2);
|
|
72
|
-
expect(button.counts.jsx).toBe(0);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('classifies a component passed as an argument to another function as hoc', () => {
|
|
76
|
-
const root = fixture({
|
|
77
|
-
'src/App.js': `
|
|
78
|
-
import styled from 'styled-components';
|
|
79
|
-
import { Button } from '@starklab/stk-components';
|
|
80
|
-
const StyledButton = styled(Button)\`color: red;\`;
|
|
81
|
-
`,
|
|
82
|
-
});
|
|
83
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
84
|
-
const button = component(result, 'Button');
|
|
85
|
-
expect(button.counts.hoc).toBe(1);
|
|
86
|
-
expect(button.sites[0].confidence).toBe('high');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('classifies a variable/ternary reference as indirect', () => {
|
|
90
|
-
const root = fixture({
|
|
91
|
-
'src/App.js': `
|
|
92
|
-
import { Button } from '@starklab/stk-components';
|
|
93
|
-
const chosen = true ? Button : null;
|
|
94
|
-
const obj = { Comp: Button };
|
|
95
|
-
`,
|
|
96
|
-
});
|
|
97
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
98
|
-
const button = component(result, 'Button');
|
|
99
|
-
expect(button.counts.indirect).toBe(2);
|
|
100
|
-
expect(button.sites.every((s) => s.confidence === 'needs-review')).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('classifies a forwarding export as reexport and excludes it from usage totals', () => {
|
|
104
|
-
const root = fixture({
|
|
105
|
-
'src/App.js': `
|
|
106
|
-
import { Button } from '@starklab/stk-components';
|
|
107
|
-
export { Button };
|
|
108
|
-
`,
|
|
109
|
-
});
|
|
110
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
111
|
-
const button = component(result, 'Button');
|
|
112
|
-
expect(button.counts.reexport).toBe(1);
|
|
113
|
-
expect(button.counts.jsx + button.counts.createElement + button.counts.hoc + button.counts.indirect).toBe(0);
|
|
114
|
-
// A reexport-only component still isn't "usage", so it shows up as zero-usage.
|
|
115
|
-
expect(result.zeroUsage).toContain('Button');
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('resolveReferences — left-join over the catalog (blind spot 1)', () => {
|
|
120
|
-
it('reports every catalog component, including ones with no references at all', () => {
|
|
121
|
-
const root = fixture({
|
|
122
|
-
'src/App.jsx': `
|
|
123
|
-
import { Button } from '@starklab/stk-components';
|
|
124
|
-
export const App = () => <Button>Click</Button>;
|
|
125
|
-
`,
|
|
126
|
-
});
|
|
127
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
128
|
-
// Total catalog size, not just what was referenced.
|
|
129
|
-
expect(result.components.length).toBeGreaterThan(50);
|
|
130
|
-
const accordion = component(result, 'Accordion');
|
|
131
|
-
expect(accordion.counts).toEqual({ jsx: 0, createElement: 0, hoc: 0, indirect: 0, reexport: 0 });
|
|
132
|
-
expect(result.zeroUsage).toContain('Accordion');
|
|
133
|
-
expect(result.zeroUsage).not.toContain('Button');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('resolveReferences — transitive barrel resolution (blind spot 2)', () => {
|
|
138
|
-
it('resolves a component re-exported through two hops of the consumer’s own barrels', () => {
|
|
139
|
-
const root = fixture({
|
|
140
|
-
'src/ui/index.js': `export { Button } from '../components/ButtonLocal.js';`,
|
|
141
|
-
'src/components/ButtonLocal.js': `export { Button } from '@starklab/stk-components';`,
|
|
142
|
-
'src/App.jsx': `
|
|
143
|
-
import { Button } from './ui/index.js';
|
|
144
|
-
export const App = () => <Button>Click</Button>;
|
|
145
|
-
`,
|
|
146
|
-
});
|
|
147
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
148
|
-
const button = component(result, 'Button');
|
|
149
|
-
expect(button.counts.jsx).toBe(1);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('does not resolve or crash on a circular local re-export chain', () => {
|
|
153
|
-
const root = fixture({
|
|
154
|
-
'src/a.js': `export { Button } from './b.js';`,
|
|
155
|
-
'src/b.js': `export { Button } from './a.js';`,
|
|
156
|
-
'src/App.jsx': `
|
|
157
|
-
import { Button } from './a.js';
|
|
158
|
-
export const App = () => <Button>Click</Button>;
|
|
159
|
-
`,
|
|
160
|
-
});
|
|
161
|
-
expect(() => resolveReferences(root, { platform: 'web' })).not.toThrow();
|
|
162
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
163
|
-
const button = component(result, 'Button');
|
|
164
|
-
// Never actually terminates at the real package, so it isn't counted.
|
|
165
|
-
expect(button.counts).toEqual({ jsx: 0, createElement: 0, hoc: 0, indirect: 0, reexport: 0 });
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('resolveReferences — platform separation', () => {
|
|
170
|
-
it('never counts a stk-react-native import while scanning the web platform', () => {
|
|
171
|
-
const root = fixture({
|
|
172
|
-
'src/App.jsx': `
|
|
173
|
-
import { Button } from '@starklab/stk-react-native';
|
|
174
|
-
export const App = () => <Button>Click</Button>;
|
|
175
|
-
`,
|
|
176
|
-
});
|
|
177
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
178
|
-
const button = component(result, 'Button');
|
|
179
|
-
expect(button.counts).toEqual({ jsx: 0, createElement: 0, hoc: 0, indirect: 0, reexport: 0 });
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('resolveReferences — resilience', () => {
|
|
184
|
-
it('records an unparseable file without failing the whole scan', () => {
|
|
185
|
-
const root = fixture({
|
|
186
|
-
'src/broken.js': `this is not { valid javascript at all +++`,
|
|
187
|
-
'src/App.jsx': `
|
|
188
|
-
import { Button } from '@starklab/stk-components';
|
|
189
|
-
export const App = () => <Button>Click</Button>;
|
|
190
|
-
`,
|
|
191
|
-
});
|
|
192
|
-
const result = resolveReferences(root, { platform: 'web' });
|
|
193
|
-
const button = component(result, 'Button');
|
|
194
|
-
expect(button.counts.jsx).toBe(1);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('excludes files matched by the ignore option from the scan', () => {
|
|
198
|
-
const root = fixture({
|
|
199
|
-
'src/App.jsx': `
|
|
200
|
-
import { Button } from '@starklab/stk-components';
|
|
201
|
-
export const App = () => <Button>Click</Button>;
|
|
202
|
-
`,
|
|
203
|
-
'src/generated/Skip.jsx': `
|
|
204
|
-
import { BadgeStatus } from '@starklab/stk-components';
|
|
205
|
-
export const Skip = () => <BadgeStatus>Skip</BadgeStatus>;
|
|
206
|
-
`,
|
|
207
|
-
});
|
|
208
|
-
const result = resolveReferences(root, { platform: 'web', ignore: ['**/generated/**'] });
|
|
209
|
-
expect(result.scannedFiles).toBe(1);
|
|
210
|
-
const button = component(result, 'Button');
|
|
211
|
-
expect(button.counts.jsx).toBe(1);
|
|
212
|
-
});
|
|
213
|
-
});
|
|
@@ -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
|
-
});
|