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