@starklab/stark-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +31 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Home.jsx +21 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Menu.jsx +13 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/pages/Profile.jsx +11 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/theme.css +34 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/AppButton.jsx +8 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/BrandButton.jsx +9 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/CardBase.jsx +9 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/FeatureCard.jsx +7 -0
- package/src/adopt/__fixtures__/dominion-fixture-app/src/wrappers/SectionCard.jsx +12 -0
- package/src/adopt/catalog.js +88 -0
- package/src/adopt/dominionFixture.test.js +165 -0
- package/src/adopt/moduleGraph.js +232 -0
- package/src/adopt/parseSource.js +25 -0
- package/src/adopt/propApiResolver.js +278 -0
- package/src/adopt/propApiResolver.test.js +229 -0
- package/src/adopt/referenceResolver.js +151 -0
- package/src/adopt/referenceResolver.test.js +213 -0
- package/src/adopt/rnTailwindResolver.js +347 -0
- package/src/adopt/rnTailwindResolver.test.js +263 -0
- package/src/adopt/rnTokenAliasResolver.js +474 -0
- package/src/adopt/rnTokenAliasResolver.test.js +260 -0
- package/src/adopt/tailwindResolver.js +512 -0
- package/src/adopt/tailwindResolver.test.js +178 -0
- package/src/adopt/targetDiscovery.js +237 -0
- package/src/adopt/targetDiscovery.test.js +227 -0
- package/src/adopt/tokenAliasResolver.js +513 -0
- package/src/adopt/tokenAliasResolver.test.js +319 -0
- package/src/adopt/wrapperResolver.js +874 -0
- package/src/adopt/wrapperResolver.test.js +324 -0
- package/src/cli.js +376 -0
- package/src/data.js +267 -0
- package/src/data.test.js +231 -0
- package/src/index.js +8 -0
- package/src/server.js +149 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import fg from 'fast-glob';
|
|
5
|
+
import traverseModule from '@babel/traverse';
|
|
6
|
+
|
|
7
|
+
import { stkRoot } from '../data.js';
|
|
8
|
+
import { buildModuleGraph, resolveOriginDeep } from './moduleGraph.js';
|
|
9
|
+
|
|
10
|
+
const traverse = traverseModule.default ?? traverseModule;
|
|
11
|
+
|
|
12
|
+
// "5 is governance, 20 is safety" — see ADOPTION_APP_PLAN.md §4/§5a and the
|
|
13
|
+
// same pattern in moduleGraph.js's resolveOrigin/resolveOriginDeep and
|
|
14
|
+
// tokenAliasResolver.js's own resolveTerminal.
|
|
15
|
+
const WARN_DEPTH = 3;
|
|
16
|
+
const MAX_DEPTH = 20;
|
|
17
|
+
|
|
18
|
+
// The RN counterpart of CSS custom properties: no var(), so a Stark RN token
|
|
19
|
+
// constant is always reached through one of these package subpaths
|
|
20
|
+
// (packages/stk/package.json "exports"). Importing the bare web build
|
|
21
|
+
// (`@starklab/stk`) on RN is out of scope — it has no Platform.select
|
|
22
|
+
// values and isn't the documented RN entry point (see the port-to-rn skill).
|
|
23
|
+
const RN_TOKEN_PACKAGES = new Set([
|
|
24
|
+
'@starklab/stk/rn',
|
|
25
|
+
'@starklab/stk/rn-spacing',
|
|
26
|
+
'@starklab/stk/rn-typography',
|
|
27
|
+
'@starklab/stk/rn-easing',
|
|
28
|
+
'@starklab/stk/rn-shadows',
|
|
29
|
+
'@starklab/stk/rn-dark',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const RN_BUILD_FILES = [
|
|
33
|
+
'build/rn/tokens.rn.js',
|
|
34
|
+
'build/rn/tokens.dark.rn.js',
|
|
35
|
+
'build/rn/spacing.rn.js',
|
|
36
|
+
'build/rn/typography.rn.js',
|
|
37
|
+
'build/rn/easing.rn.js',
|
|
38
|
+
'build/rn/shadows.rn.js',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Inventory + layer classification
|
|
43
|
+
//
|
|
44
|
+
// Same DTCG source as tokenAliasResolver.js's loadTokenLayers, but flattened
|
|
45
|
+
// to RN token names (packages/stk's own strip-"--stk-"/split-"-"/camelCase/
|
|
46
|
+
// prefix-"stk" convention, documented in the port-to-rn skill) instead of CSS
|
|
47
|
+
// custom-property names, so a layer-violation finding (aliasing a primitive
|
|
48
|
+
// token) works the same way it does on web.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
function cssVarToRnToken(cssVarName) {
|
|
52
|
+
const rest = cssVarName.replace(/^--stk-/, '');
|
|
53
|
+
const camel = rest
|
|
54
|
+
.split('-')
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
57
|
+
.join('');
|
|
58
|
+
return `stk${camel}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function flattenDtcgRn(node, prefix, layer, out) {
|
|
62
|
+
if (node == null || typeof node !== 'object') return;
|
|
63
|
+
if ('$value' in node) {
|
|
64
|
+
out.set(cssVarToRnToken(`--stk-${prefix.join('-')}`), layer);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const [key, child] of Object.entries(node)) {
|
|
68
|
+
if (key.startsWith('$')) continue;
|
|
69
|
+
flattenDtcgRn(child, [...prefix, key], layer, out);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadRnTokenLayers(stkPkgRoot) {
|
|
74
|
+
const layers = new Map();
|
|
75
|
+
const dirs = [
|
|
76
|
+
['base', 'primitive'],
|
|
77
|
+
['semantic', 'semantic'],
|
|
78
|
+
['components', 'component'],
|
|
79
|
+
];
|
|
80
|
+
for (const [dir, layer] of dirs) {
|
|
81
|
+
const full = path.join(stkPkgRoot, 'tokens', dir);
|
|
82
|
+
if (!existsSync(full)) continue;
|
|
83
|
+
for (const file of fg.sync('**/*.json', { cwd: full, absolute: true })) {
|
|
84
|
+
let json;
|
|
85
|
+
try {
|
|
86
|
+
json = JSON.parse(readFileSync(file, 'utf-8'));
|
|
87
|
+
} catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
flattenDtcgRn(json, [], layer, layers);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return layers;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function loadRnTokenInventory(stkPkgRoot) {
|
|
97
|
+
const inventory = new Set();
|
|
98
|
+
for (const rel of RN_BUILD_FILES) {
|
|
99
|
+
const full = path.join(stkPkgRoot, rel);
|
|
100
|
+
if (!existsSync(full)) continue;
|
|
101
|
+
const src = readFileSync(full, 'utf-8');
|
|
102
|
+
for (const m of src.matchAll(/export const (stk[A-Za-z0-9]+)\s*=/g)) {
|
|
103
|
+
inventory.add(m[1]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (inventory.size === 0) {
|
|
107
|
+
throw new Error(`RN token build not found under ${stkPkgRoot}/build/rn — run "npm run build" in packages/stk first.`);
|
|
108
|
+
}
|
|
109
|
+
const layers = loadRnTokenLayers(stkPkgRoot);
|
|
110
|
+
return { inventory, layers };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Pass 1 — the consumer's own JS-object alias graph
|
|
115
|
+
//
|
|
116
|
+
// The RN equivalent of tokenAliasResolver.js's propGraph: instead of
|
|
117
|
+
// `--app-primary: var(--stk-…)`, the indirection is a local `const` whose
|
|
118
|
+
// initializer is either a scalar alias (`const primary = stkSurfaceBrand1Strong`)
|
|
119
|
+
// or an object literal whose properties alias tokens
|
|
120
|
+
// (`const theme = { primary: stkSurfaceBrand1Strong }`). Each alias is keyed
|
|
121
|
+
// per-file (JS module scope, unlike CSS custom properties which are
|
|
122
|
+
// effectively global) — `localName` for a scalar const, `localName.propName`
|
|
123
|
+
// for an object property.
|
|
124
|
+
//
|
|
125
|
+
// Known simplifications (documented, not oversights — this is the first RN
|
|
126
|
+
// resolver and ADOPTION_APP_PLAN.md §5a explicitly argues for building it
|
|
127
|
+
// only after the web one is validated, since there is no getComputedStyle()
|
|
128
|
+
// oracle to reconcile against on RN):
|
|
129
|
+
// - Declarations are captured file-wide (any scope depth), not just
|
|
130
|
+
// module-top-level; if the same name is `const`-declared twice in one
|
|
131
|
+
// file (e.g. two components each with their own local `theme`), the
|
|
132
|
+
// later declaration wins. Real component files define one theme/style
|
|
133
|
+
// object per file, so this rarely matters in practice.
|
|
134
|
+
// - Only single-hop member access (`obj.prop`) is resolved — `a.b.c` is out
|
|
135
|
+
// of scope, mirroring tailwindResolver.js's non-theme-utility carve-out
|
|
136
|
+
// for what static analysis doesn't reach.
|
|
137
|
+
// - A property whose value isn't one of {Identifier, `obj.prop` member
|
|
138
|
+
// access, plain literal} (a call, a ternary, a spread, a nested object…)
|
|
139
|
+
// is not captured as an alias at all — never scored, same "dynamic
|
|
140
|
+
// expressions are skipped" rule tailwindResolver.js applies to
|
|
141
|
+
// clsx()/ternary className values.
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
function isPlainLiteral(node) {
|
|
145
|
+
if (node.type === 'StringLiteral' || node.type === 'NumericLiteral' || node.type === 'BooleanLiteral') return true;
|
|
146
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) return true;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isAnalyzableValueNode(node) {
|
|
151
|
+
if (!node) return false;
|
|
152
|
+
if (node.type === 'Identifier') return true;
|
|
153
|
+
if (node.type === 'MemberExpression' && !node.computed && node.object.type === 'Identifier' && node.property.type === 'Identifier') return true;
|
|
154
|
+
return isPlainLiteral(node);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function setAlias(aliasGraph, file, key, node, line) {
|
|
158
|
+
let byFile = aliasGraph.get(file);
|
|
159
|
+
if (!byFile) {
|
|
160
|
+
byFile = new Map();
|
|
161
|
+
aliasGraph.set(file, byFile);
|
|
162
|
+
}
|
|
163
|
+
byFile.set(key, { node, line, file });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function buildAliasGraph(moduleGraph) {
|
|
167
|
+
const aliasGraph = new Map(); // file -> Map(key -> {node, line, file})
|
|
168
|
+
const definitionNodes = new Set(); // value nodes that are alias-graph edges, not Pass-2 usage sites
|
|
169
|
+
|
|
170
|
+
for (const file of moduleGraph.files) {
|
|
171
|
+
const entry = moduleGraph.graph.get(file);
|
|
172
|
+
if (!entry?.ast) continue;
|
|
173
|
+
|
|
174
|
+
traverse(entry.ast, {
|
|
175
|
+
VariableDeclarator(nodePath) {
|
|
176
|
+
const { id, init } = nodePath.node;
|
|
177
|
+
if (!init || id.type !== 'Identifier') return;
|
|
178
|
+
const localName = id.name;
|
|
179
|
+
|
|
180
|
+
if (init.type === 'ObjectExpression') {
|
|
181
|
+
for (const prop of init.properties) {
|
|
182
|
+
if (prop.type !== 'ObjectProperty' || prop.computed) continue;
|
|
183
|
+
const propName =
|
|
184
|
+
prop.key.type === 'Identifier' ? prop.key.name : prop.key.type === 'StringLiteral' ? prop.key.value : null;
|
|
185
|
+
if (!propName || !isAnalyzableValueNode(prop.value)) continue;
|
|
186
|
+
setAlias(aliasGraph, file, `${localName}.${propName}`, prop.value, prop.loc?.start?.line ?? null);
|
|
187
|
+
definitionNodes.add(prop.value);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!isAnalyzableValueNode(init)) return;
|
|
193
|
+
setAlias(aliasGraph, file, localName, init, nodePath.node.loc?.start?.line ?? null);
|
|
194
|
+
definitionNodes.add(init);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { aliasGraph, definitionNodes };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function classifyTerminalState(kind) {
|
|
203
|
+
if (kind === 'stk') return 'conformant';
|
|
204
|
+
if (kind === 'raw') return 'drift';
|
|
205
|
+
return 'broken'; // undefined | cycle | depth-limit | unsupported
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function classifyOrigin(origin, propName, ctx, visited, depth) {
|
|
209
|
+
if (!origin) return { kind: 'undefined', depth };
|
|
210
|
+
if (origin.cycle) return { kind: 'cycle', depth };
|
|
211
|
+
|
|
212
|
+
if (origin.type === 'package') {
|
|
213
|
+
if (!RN_TOKEN_PACKAGES.has(origin.pkg)) return { kind: 'undefined', depth };
|
|
214
|
+
const tokenName = origin.name === '*' ? propName : origin.name;
|
|
215
|
+
if (!tokenName || !ctx.inventory.has(tokenName)) return { kind: 'undefined', depth };
|
|
216
|
+
return { kind: 'stk', depth: depth + 1, stkToken: tokenName, layer: ctx.layers.get(tokenName) ?? 'unknown' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (origin.type === 'local') {
|
|
220
|
+
const key = propName ? `${origin.name}.${propName}` : origin.name;
|
|
221
|
+
if (ctx.aliasGraph.get(origin.file)?.has(key)) {
|
|
222
|
+
return resolveAliasKey(origin.file, key, ctx, visited, depth + 1);
|
|
223
|
+
}
|
|
224
|
+
return { kind: 'undefined', depth };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { kind: 'undefined', depth };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveAliasKey(file, key, ctx, visited, depth) {
|
|
231
|
+
const visitKey = `${file}::${key}`;
|
|
232
|
+
if (visited.has(visitKey)) return { kind: 'cycle', depth };
|
|
233
|
+
const entry = ctx.aliasGraph.get(file)?.get(key);
|
|
234
|
+
if (!entry) return { kind: 'undefined', depth };
|
|
235
|
+
const nextVisited = new Set(visited).add(visitKey);
|
|
236
|
+
return classifyNode(file, entry.node, ctx, nextVisited, depth + 1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function classifyIdentifierRef(file, name, ctx, visited, depth) {
|
|
240
|
+
if (ctx.aliasGraph.get(file)?.has(name)) {
|
|
241
|
+
return resolveAliasKey(file, name, ctx, visited, depth);
|
|
242
|
+
}
|
|
243
|
+
const origin = resolveOriginDeep(ctx.moduleGraph, file, name);
|
|
244
|
+
return classifyOrigin(origin, null, ctx, visited, depth);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function classifyMemberRef(file, objName, propName, ctx, visited, depth) {
|
|
248
|
+
const compoundKey = `${objName}.${propName}`;
|
|
249
|
+
if (ctx.aliasGraph.get(file)?.has(compoundKey)) {
|
|
250
|
+
return resolveAliasKey(file, compoundKey, ctx, visited, depth);
|
|
251
|
+
}
|
|
252
|
+
const origin = resolveOriginDeep(ctx.moduleGraph, file, objName);
|
|
253
|
+
return classifyOrigin(origin, propName, ctx, visited, depth);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function classifyNode(file, node, ctx, visited, depth) {
|
|
257
|
+
if (depth > MAX_DEPTH) return { kind: 'depth-limit', depth };
|
|
258
|
+
if (node.type === 'Identifier') return classifyIdentifierRef(file, node.name, ctx, visited, depth);
|
|
259
|
+
if (node.type === 'MemberExpression' && !node.computed && node.object.type === 'Identifier' && node.property.type === 'Identifier') {
|
|
260
|
+
return classifyMemberRef(file, node.object.name, node.property.name, ctx, visited, depth);
|
|
261
|
+
}
|
|
262
|
+
if (isPlainLiteral(node)) return { kind: 'raw', depth };
|
|
263
|
+
return { kind: 'unsupported', depth };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function classifyAliasGraph(aliasGraph, ctx) {
|
|
267
|
+
const properties = [];
|
|
268
|
+
const findings = [];
|
|
269
|
+
|
|
270
|
+
for (const [file, byKey] of aliasGraph) {
|
|
271
|
+
for (const [key, entry] of byKey) {
|
|
272
|
+
const terminal = classifyNode(file, entry.node, ctx, new Set(), 0);
|
|
273
|
+
const state = classifyTerminalState(terminal.kind);
|
|
274
|
+
|
|
275
|
+
if (terminal.kind === 'stk' && terminal.layer === 'primitive') {
|
|
276
|
+
findings.push({ rule: 'layer-violation', severity: 'critical', property: key, file, line: entry.line, stkToken: terminal.stkToken });
|
|
277
|
+
}
|
|
278
|
+
if (terminal.kind === 'stk' && terminal.depth > WARN_DEPTH) {
|
|
279
|
+
findings.push({ rule: 'deep-alias-chain', severity: 'warning', property: key, file, line: entry.line, depth: terminal.depth });
|
|
280
|
+
}
|
|
281
|
+
if (state === 'drift') {
|
|
282
|
+
findings.push({ rule: 'drift-behind-alias', severity: 'critical', property: key, file, line: entry.line });
|
|
283
|
+
}
|
|
284
|
+
if (state === 'broken') {
|
|
285
|
+
findings.push({ rule: 'broken-alias', severity: 'critical', property: key, file, line: entry.line, reason: terminal.kind });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
properties.push({ name: key, file, line: entry.line, state, terminal });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { properties, findings };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Pass 2 — usage classification
|
|
297
|
+
//
|
|
298
|
+
// A usage is `obj.prop` or a bare `name` reference outside of the Pass-1
|
|
299
|
+
// definition sites themselves. Three ways a usage resolves:
|
|
300
|
+
// 'direct' — the reference reaches a Stark RN token constant with no
|
|
301
|
+
// consumer indirection at all (a namespace-import property
|
|
302
|
+
// access, a directly-imported token identifier, or — the
|
|
303
|
+
// `useTokens()` case — an object whose origin isn't statically
|
|
304
|
+
// traceable but whose property name is an unambiguous "stk"-
|
|
305
|
+
// prefixed inventory token; this matches the reference's
|
|
306
|
+
// static *name*, never a resolved *value*, which is the
|
|
307
|
+
// distinction ADOPTION_APP_PLAN.md §5a's anti-pattern warning
|
|
308
|
+
// draws — "colour values collide constantly, a metric built on
|
|
309
|
+
// coincidence is worse than a missing metric").
|
|
310
|
+
// 'aliased' — the reference resolves through a Pass-1 alias-graph entry to
|
|
311
|
+
// a conformant stk terminal.
|
|
312
|
+
// (skipped) — anything else (an unrelated object/property pair, e.g.
|
|
313
|
+
// `props.variant` or `StyleSheet.create`) is out of scope, not
|
|
314
|
+
// a violation — same "not every property is ours to judge"
|
|
315
|
+
// rule tailwindResolver.js applies to Tailwind's own default
|
|
316
|
+
// palette classes.
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
function pushResolvedUsage(usages, { file, line, ref, terminal }) {
|
|
320
|
+
const state = classifyTerminalState(terminal.kind);
|
|
321
|
+
const classification = state === 'conformant' ? 'aliased' : state;
|
|
322
|
+
usages.push({ file, line, ref, classification, stkToken: terminal.stkToken, depth: terminal.depth });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function collectUsages(moduleGraph, definitionNodes, ctx) {
|
|
326
|
+
const usages = [];
|
|
327
|
+
|
|
328
|
+
for (const file of moduleGraph.files) {
|
|
329
|
+
const entry = moduleGraph.graph.get(file);
|
|
330
|
+
if (!entry?.ast) continue;
|
|
331
|
+
|
|
332
|
+
traverse(entry.ast, {
|
|
333
|
+
MemberExpression(nodePath) {
|
|
334
|
+
const node = nodePath.node;
|
|
335
|
+
if (definitionNodes.has(node)) return; // Pass-1 alias-graph edge, not a usage site
|
|
336
|
+
if (node.computed) return;
|
|
337
|
+
if (node.object.type !== 'Identifier' || node.property.type !== 'Identifier') return;
|
|
338
|
+
|
|
339
|
+
const objName = node.object.name;
|
|
340
|
+
const propName = node.property.name;
|
|
341
|
+
const line = node.loc?.start?.line ?? null;
|
|
342
|
+
const compoundKey = `${objName}.${propName}`;
|
|
343
|
+
|
|
344
|
+
if (ctx.aliasGraph.get(file)?.has(compoundKey)) {
|
|
345
|
+
pushResolvedUsage(usages, { file, line, ref: compoundKey, terminal: resolveAliasKey(file, compoundKey, ctx, new Set(), 0) });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const origin = resolveOriginDeep(ctx.moduleGraph, file, objName);
|
|
350
|
+
if (origin?.type === 'package' && origin.name === '*' && RN_TOKEN_PACKAGES.has(origin.pkg) && ctx.inventory.has(propName)) {
|
|
351
|
+
usages.push({ file, line, ref: compoundKey, classification: 'direct', stkToken: propName });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (origin?.type === 'local') {
|
|
355
|
+
const key = `${origin.name}.${propName}`;
|
|
356
|
+
if (ctx.aliasGraph.get(origin.file)?.has(key)) {
|
|
357
|
+
pushResolvedUsage(usages, { file, line, ref: compoundKey, terminal: resolveAliasKey(origin.file, key, ctx, new Set(), 0) });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (propName.startsWith('stk') && ctx.inventory.has(propName)) {
|
|
363
|
+
usages.push({ file, line, ref: compoundKey, classification: 'direct', stkToken: propName, heuristic: true });
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
Identifier(nodePath) {
|
|
368
|
+
if (!nodePath.isReferencedIdentifier()) return;
|
|
369
|
+
const node = nodePath.node;
|
|
370
|
+
if (definitionNodes.has(node)) return;
|
|
371
|
+
const name = node.name;
|
|
372
|
+
const line = node.loc?.start?.line ?? null;
|
|
373
|
+
|
|
374
|
+
if (ctx.aliasGraph.get(file)?.has(name)) {
|
|
375
|
+
pushResolvedUsage(usages, { file, line, ref: name, terminal: resolveAliasKey(file, name, ctx, new Set(), 0) });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const origin = resolveOriginDeep(ctx.moduleGraph, file, name);
|
|
380
|
+
if (origin?.type === 'package' && origin.name !== '*' && RN_TOKEN_PACKAGES.has(origin.pkg) && ctx.inventory.has(origin.name)) {
|
|
381
|
+
usages.push({ file, line, ref: name, classification: 'direct', stkToken: origin.name });
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return usages;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Same three-number report as tokenAliasResolver.js/tailwindResolver.js.
|
|
391
|
+
function summarizeReport(usages) {
|
|
392
|
+
const counts = { direct: 0, aliased: 0, unresolved: 0 };
|
|
393
|
+
for (const u of usages) {
|
|
394
|
+
if (u.classification === 'direct') counts.direct++;
|
|
395
|
+
else if (u.classification === 'aliased') counts.aliased++;
|
|
396
|
+
else counts.unresolved++;
|
|
397
|
+
}
|
|
398
|
+
const total = counts.direct + counts.aliased + counts.unresolved;
|
|
399
|
+
const pct = (n) => (total === 0 ? 0 : Math.round((n / total) * 1000) / 10);
|
|
400
|
+
return {
|
|
401
|
+
total,
|
|
402
|
+
direct: counts.direct,
|
|
403
|
+
directPct: pct(counts.direct),
|
|
404
|
+
aliased: counts.aliased,
|
|
405
|
+
aliasedPct: pct(counts.aliased),
|
|
406
|
+
unresolved: counts.unresolved,
|
|
407
|
+
unresolvedPct: pct(counts.unresolved),
|
|
408
|
+
conformancePct: pct(counts.direct + counts.aliased),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Resolves a consumer's own JS-object token indirection on React Native —
|
|
414
|
+
* the RN analog of tokenAliasResolver.js's CSS custom-property alias graph
|
|
415
|
+
* (ADOPTION_APP_PLAN.md §5a: "no CSS custom properties, so the indirection is
|
|
416
|
+
* a JS object… same graph, resolved over imports and property access instead
|
|
417
|
+
* of var()"). Native-only: CSS var()/Tailwind utility classes have no RN
|
|
418
|
+
* equivalent — those are the two web-only resolvers.
|
|
419
|
+
*
|
|
420
|
+
* Static-only by construction (§5a/§3d): there is no getComputedStyle()-style
|
|
421
|
+
* runtime oracle on RN to reconcile against, so `confidence: 'uncertain'`
|
|
422
|
+
* always accompanies a `detected: true` result — never treat this result as
|
|
423
|
+
* equal-confidence with the web resolver's CSS-based ground truth.
|
|
424
|
+
*/
|
|
425
|
+
export function resolveRnTokenAliases(root, { platform = 'native', ignore = [] } = {}) {
|
|
426
|
+
if (platform !== 'native') {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`resolveRnTokenAliases only supports platform "native" (got "${platform}") — CSS custom properties and Tailwind utility classes are the web analog, see tokenAliasResolver.js/tailwindResolver.js.`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const stkPkgRoot = stkRoot();
|
|
433
|
+
const { inventory, layers } = loadRnTokenInventory(stkPkgRoot);
|
|
434
|
+
const moduleGraph = buildModuleGraph(root, { ignore });
|
|
435
|
+
|
|
436
|
+
const anyStkImport = [...moduleGraph.graph.values()].some((entry) =>
|
|
437
|
+
[...entry.imports.values()].some((b) => RN_TOKEN_PACKAGES.has(b.source))
|
|
438
|
+
);
|
|
439
|
+
if (!anyStkImport) {
|
|
440
|
+
// Detection is always cheaper than resolution: no import from any Stark
|
|
441
|
+
// RN token package anywhere means there's nothing to measure, not a 0%
|
|
442
|
+
// score — same refusal tailwindResolver.js makes for "no config found".
|
|
443
|
+
return {
|
|
444
|
+
platform,
|
|
445
|
+
root,
|
|
446
|
+
detected: false,
|
|
447
|
+
reason:
|
|
448
|
+
'No import from a @starklab/stk RN token package (rn, rn-spacing, rn-typography, rn-easing, rn-shadows, rn-dark) found anywhere in the scanned files.',
|
|
449
|
+
report: null,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const { aliasGraph, definitionNodes } = buildAliasGraph(moduleGraph);
|
|
454
|
+
const ctx = { aliasGraph, moduleGraph, inventory, layers };
|
|
455
|
+
const { properties, findings } = classifyAliasGraph(aliasGraph, ctx);
|
|
456
|
+
const usages = collectUsages(moduleGraph, definitionNodes, ctx);
|
|
457
|
+
const report = summarizeReport(usages);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
platform,
|
|
461
|
+
root,
|
|
462
|
+
detected: true,
|
|
463
|
+
scannedFiles: moduleGraph.files.length,
|
|
464
|
+
tokenInventoryTotal: inventory.size,
|
|
465
|
+
aliasCount: [...aliasGraph.values()].reduce((n, byKey) => n + byKey.size, 0),
|
|
466
|
+
properties,
|
|
467
|
+
usages,
|
|
468
|
+
findings,
|
|
469
|
+
report,
|
|
470
|
+
confidence: 'uncertain',
|
|
471
|
+
confidenceReason:
|
|
472
|
+
'Static-only (ADOPTION_APP_PLAN.md §5a): no getComputedStyle()-equivalent runtime oracle exists on React Native to reconcile against, so this result has to be right by construction rather than cross-checked.',
|
|
473
|
+
};
|
|
474
|
+
}
|