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