@gemini-designer/mcp-server 0.1.2 → 0.1.29
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/dist/components/catalog.d.ts.map +1 -1
- package/dist/components/catalog.js +10 -4
- package/dist/components/catalog.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -6
- package/dist/config/index.js.map +1 -1
- package/dist/context/builder.d.ts.map +1 -1
- package/dist/context/builder.js.map +1 -1
- package/dist/context/filter.d.ts.map +1 -1
- package/dist/context/filter.js +5 -1
- package/dist/context/filter.js.map +1 -1
- package/dist/context/grounding.d.ts.map +1 -1
- package/dist/context/grounding.js +7 -3
- package/dist/context/grounding.js.map +1 -1
- package/dist/context/guards.d.ts.map +1 -1
- package/dist/context/guards.js +53 -0
- package/dist/context/guards.js.map +1 -1
- package/dist/context/repo-hints.js.map +1 -1
- package/dist/context/styling-detector.d.ts +24 -0
- package/dist/context/styling-detector.d.ts.map +1 -0
- package/dist/context/styling-detector.js +337 -0
- package/dist/context/styling-detector.js.map +1 -0
- package/dist/design/principles.js.map +1 -1
- package/dist/generation/gemini-client.d.ts.map +1 -1
- package/dist/generation/gemini-client.js.map +1 -1
- package/dist/generation/litellm-client.d.ts.map +1 -1
- package/dist/generation/litellm-client.js +14 -7
- package/dist/generation/litellm-client.js.map +1 -1
- package/dist/generation/remote-client.d.ts +10 -5
- package/dist/generation/remote-client.d.ts.map +1 -1
- package/dist/generation/remote-client.js +13 -2
- package/dist/generation/remote-client.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/output/file-writer.d.ts.map +1 -1
- package/dist/output/file-writer.js +4 -4
- package/dist/output/file-writer.js.map +1 -1
- package/dist/output/formatter.d.ts.map +1 -1
- package/dist/output/formatter.js +5 -2
- package/dist/output/formatter.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/stack/detect.d.ts.map +1 -1
- package/dist/stack/detect.js +42 -9
- package/dist/stack/detect.js.map +1 -1
- package/dist/tokens/sync.d.ts.map +1 -1
- package/dist/tokens/sync.js +22 -5
- package/dist/tokens/sync.js.map +1 -1
- package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
- package/dist/tools/analyze-screenshot-ui.js +5 -5
- package/dist/tools/analyze-screenshot-ui.js.map +1 -1
- package/dist/tools/analyze-tokens.d.ts.map +1 -1
- package/dist/tools/analyze-tokens.js +3 -1
- package/dist/tools/analyze-tokens.js.map +1 -1
- package/dist/tools/catalog-components.d.ts.map +1 -1
- package/dist/tools/catalog-components.js +1 -4
- package/dist/tools/catalog-components.js.map +1 -1
- package/dist/tools/create-ui.d.ts +3 -0
- package/dist/tools/create-ui.d.ts.map +1 -1
- package/dist/tools/create-ui.js +203 -75
- package/dist/tools/create-ui.js.map +1 -1
- package/dist/tools/detect-ui-stack.js.map +1 -1
- package/dist/tools/generate-component-variants.d.ts.map +1 -1
- package/dist/tools/generate-component-variants.js +15 -4
- package/dist/tools/generate-component-variants.js.map +1 -1
- package/dist/tools/generate-vibes.d.ts.map +1 -1
- package/dist/tools/generate-vibes.js +7 -3
- package/dist/tools/generate-vibes.js.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/modify-ui.d.ts.map +1 -1
- package/dist/tools/modify-ui.js +7 -2
- package/dist/tools/modify-ui.js.map +1 -1
- package/dist/tools/scaffold-project.d.ts.map +1 -1
- package/dist/tools/scaffold-project.js +3 -1
- package/dist/tools/scaffold-project.js.map +1 -1
- package/dist/tools/snippet-ui.d.ts +3 -1
- package/dist/tools/snippet-ui.d.ts.map +1 -1
- package/dist/tools/snippet-ui.js +219 -88
- package/dist/tools/snippet-ui.js.map +1 -1
- package/dist/tools/sync-design-tokens.d.ts.map +1 -1
- package/dist/tools/sync-design-tokens.js +26 -11
- package/dist/tools/sync-design-tokens.js.map +1 -1
- package/dist/utils/walk.d.ts.map +1 -1
- package/dist/utils/walk.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +55 -55
- package/src/__tests__/builder.test.ts +19 -19
- package/src/__tests__/config.test.ts +63 -31
- package/src/__tests__/filter.test.ts +98 -92
- package/src/__tests__/remote-client.test.ts +179 -0
- package/src/components/catalog.ts +170 -166
- package/src/config/index.ts +185 -177
- package/src/context/builder.ts +157 -157
- package/src/context/filter.ts +110 -104
- package/src/context/grounding.ts +143 -129
- package/src/context/guards.ts +97 -38
- package/src/context/repo-hints.ts +24 -24
- package/src/context/styling-detector.ts +460 -0
- package/src/design/principles.ts +14 -14
- package/src/generation/gemini-client.ts +53 -56
- package/src/generation/litellm-client.ts +102 -86
- package/src/generation/remote-client.ts +100 -77
- package/src/index.ts +16 -16
- package/src/output/file-writer.ts +123 -123
- package/src/output/formatter.ts +139 -132
- package/src/server.ts +12 -11
- package/src/stack/detect.ts +226 -175
- package/src/tokens/sync.ts +189 -155
- package/src/tools/analyze-screenshot-ui.ts +89 -88
- package/src/tools/analyze-tokens.ts +80 -78
- package/src/tools/catalog-components.ts +68 -68
- package/src/tools/create-ui.ts +295 -142
- package/src/tools/detect-ui-stack.ts +36 -36
- package/src/tools/generate-component-variants.ts +155 -135
- package/src/tools/generate-vibes.ts +121 -117
- package/src/tools/index.ts +14 -14
- package/src/tools/modify-ui.ts +170 -165
- package/src/tools/scaffold-project.ts +68 -66
- package/src/tools/snippet-ui.ts +323 -172
- package/src/tools/sync-design-tokens.ts +217 -195
- package/src/utils/walk.ts +47 -45
- package/src/version.ts +6 -0
- package/tsconfig.json +23 -33
- package/vitest.config.ts +10 -10
- package/.prettierrc +0 -9
- package/eslint.config.js +0 -37
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import type { Config } from '../config/index.js';
|
|
5
|
+
import { checkQuota, generateWithRemote } from '../generation/remote-client.js';
|
|
6
|
+
|
|
7
|
+
function getTestConfig(remoteEndpoint: string): Config {
|
|
8
|
+
return {
|
|
9
|
+
mode: 'remote',
|
|
10
|
+
localProvider: 'litellm',
|
|
11
|
+
apiKey: undefined,
|
|
12
|
+
remoteEndpoint,
|
|
13
|
+
remoteApiKey: 'test-api-key',
|
|
14
|
+
allowedPaths: [process.cwd()],
|
|
15
|
+
defaultFramework: 'react',
|
|
16
|
+
model: 'gemini-2.5-flash',
|
|
17
|
+
litellmEndpoint: undefined,
|
|
18
|
+
litellmApiKey: undefined,
|
|
19
|
+
litellmModel: undefined,
|
|
20
|
+
accessibility: 'wcag-aa',
|
|
21
|
+
breakpoints: {
|
|
22
|
+
sm: 640,
|
|
23
|
+
md: 768,
|
|
24
|
+
lg: 1024,
|
|
25
|
+
xl: 1280,
|
|
26
|
+
'2xl': 1536,
|
|
27
|
+
},
|
|
28
|
+
debug: false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function startServer(
|
|
33
|
+
handler: (req: http.IncomingMessage, res: http.ServerResponse) => void
|
|
34
|
+
): Promise<{ url: string; close: () => Promise<void> }> {
|
|
35
|
+
const server = http.createServer(handler);
|
|
36
|
+
|
|
37
|
+
await new Promise<void>((resolve) => {
|
|
38
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const address = server.address();
|
|
42
|
+
if (!address || typeof address === 'string') {
|
|
43
|
+
throw new Error('Failed to bind test server');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const url = `http://127.0.0.1:${address.port}`;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
url,
|
|
50
|
+
close: async () =>
|
|
51
|
+
await new Promise<void>((resolve, reject) => {
|
|
52
|
+
server.close((err) => {
|
|
53
|
+
if (err) reject(err);
|
|
54
|
+
else resolve();
|
|
55
|
+
});
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readJsonBody(req: http.IncomingMessage): Promise<unknown> {
|
|
61
|
+
const chunks: Buffer[] = [];
|
|
62
|
+
for await (const chunk of req) {
|
|
63
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
64
|
+
}
|
|
65
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
66
|
+
return raw.length ? JSON.parse(raw) : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('remote-client contract', () => {
|
|
70
|
+
it('POST /generate supports userPrompt (string)', async () => {
|
|
71
|
+
const server = await startServer(async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
expect(req.method).toBe('POST');
|
|
74
|
+
expect(req.url).toBe('/generate');
|
|
75
|
+
expect(req.headers.authorization).toBe('Bearer test-api-key');
|
|
76
|
+
|
|
77
|
+
const body = await readJsonBody(req);
|
|
78
|
+
expect(body).toEqual({
|
|
79
|
+
systemPrompt: 'sys',
|
|
80
|
+
toolName: 'my_tool',
|
|
81
|
+
userPrompt: 'hello',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
res.setHeader('content-type', 'application/json');
|
|
85
|
+
res.end(JSON.stringify({ content: 'ok', tokensUsed: 3 }));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
res.statusCode = 500;
|
|
88
|
+
res.end(String(err));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const cfg = getTestConfig(server.url);
|
|
94
|
+
const out = await generateWithRemote(cfg, 'sys', 'hello', 'my_tool');
|
|
95
|
+
expect(out).toBe('ok');
|
|
96
|
+
} finally {
|
|
97
|
+
await server.close();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('POST /generate supports userParts (multimodal)', async () => {
|
|
102
|
+
const server = await startServer(async (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
expect(req.method).toBe('POST');
|
|
105
|
+
expect(req.url).toBe('/generate');
|
|
106
|
+
expect(req.headers.authorization).toBe('Bearer test-api-key');
|
|
107
|
+
|
|
108
|
+
const body = await readJsonBody(req);
|
|
109
|
+
expect(body).toEqual({
|
|
110
|
+
systemPrompt: 'sys',
|
|
111
|
+
toolName: 'my_tool',
|
|
112
|
+
userParts: [
|
|
113
|
+
{ text: 'hello' },
|
|
114
|
+
{ inlineData: { mimeType: 'image/png', data: 'AA==' } },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
res.setHeader('content-type', 'application/json');
|
|
119
|
+
res.end(JSON.stringify({ content: 'ok', tokensUsed: 3 }));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
res.statusCode = 500;
|
|
122
|
+
res.end(String(err));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const cfg = getTestConfig(server.url);
|
|
128
|
+
const out = await generateWithRemote(
|
|
129
|
+
cfg,
|
|
130
|
+
'sys',
|
|
131
|
+
[{ text: 'hello' }, { inlineData: { mimeType: 'image/png', data: 'AA==' } }],
|
|
132
|
+
'my_tool'
|
|
133
|
+
);
|
|
134
|
+
expect(out).toBe('ok');
|
|
135
|
+
} finally {
|
|
136
|
+
await server.close();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('GET /quota returns budget fields { remainingBudgetUsd, maxBudgetUsd, ... }', async () => {
|
|
141
|
+
const server = await startServer(async (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
expect(req.method).toBe('GET');
|
|
144
|
+
expect(req.url).toBe('/quota');
|
|
145
|
+
expect(req.headers.authorization).toBe('Bearer test-api-key');
|
|
146
|
+
|
|
147
|
+
res.setHeader('content-type', 'application/json');
|
|
148
|
+
res.end(
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
remainingBudgetUsd: 9.87,
|
|
151
|
+
maxBudgetUsd: 10,
|
|
152
|
+
spendUsd: 0.13,
|
|
153
|
+
budgetDuration: '30d',
|
|
154
|
+
rpmLimit: 30,
|
|
155
|
+
tpmLimit: 200000,
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
res.statusCode = 500;
|
|
160
|
+
res.end(String(err));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const cfg = getTestConfig(server.url);
|
|
166
|
+
const quota = await checkQuota(cfg);
|
|
167
|
+
expect(quota).toEqual({
|
|
168
|
+
remainingBudgetUsd: 9.87,
|
|
169
|
+
maxBudgetUsd: 10,
|
|
170
|
+
spendUsd: 0.13,
|
|
171
|
+
budgetDuration: '30d',
|
|
172
|
+
rpmLimit: 30,
|
|
173
|
+
tpmLimit: 200000,
|
|
174
|
+
});
|
|
175
|
+
} finally {
|
|
176
|
+
await server.close();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -13,202 +13,206 @@ import * as path from 'node:path';
|
|
|
13
13
|
import { toPosixPath } from '../utils/walk.js';
|
|
14
14
|
|
|
15
15
|
export interface ComponentExport {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
name: string;
|
|
17
|
+
exportType: 'named' | 'default';
|
|
18
|
+
file: string; // relative to root
|
|
19
|
+
propsType?: string;
|
|
20
|
+
jsDoc?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface CatalogResult {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
root: string;
|
|
25
|
+
filesScanned: number;
|
|
26
|
+
components: ComponentExport[];
|
|
27
|
+
warnings: string[];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function readFileSafe(filePath: string): string | null {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
try {
|
|
32
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function rel(root: string, abs: string): string {
|
|
39
|
-
|
|
39
|
+
return toPosixPath(path.relative(root, abs));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function extractJSDocFromLeadingComment(text: string): string | undefined {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
// Simple: capture /** ... */ immediately preceding export
|
|
44
|
+
const m = text.match(/\/\*\*([\s\S]*?)\*\//);
|
|
45
|
+
if (!m) return undefined;
|
|
46
|
+
const cleaned = m[1]
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map((l) => l.replace(/^\s*\*\s?/, '').trim())
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(' ');
|
|
51
|
+
return cleaned || undefined;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async function scanWithTypeScript(root: string, files: string[]): Promise<CatalogResult> {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
const warnings: string[] = [];
|
|
56
|
+
const components: ComponentExport[] = [];
|
|
57
|
+
|
|
58
|
+
const ts = await import('typescript');
|
|
59
|
+
|
|
60
|
+
const getScriptKind = (filePath: string): import('typescript').ScriptKind => {
|
|
61
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
62
|
+
if (ext === '.tsx') return ts.ScriptKind.TSX;
|
|
63
|
+
if (ext === '.jsx') return ts.ScriptKind.JSX;
|
|
64
|
+
if (ext === '.ts') return ts.ScriptKind.TS;
|
|
65
|
+
if (ext === '.js') return ts.ScriptKind.JS;
|
|
66
|
+
return ts.ScriptKind.Unknown;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (const f of files) {
|
|
70
|
+
const content = readFileSafe(f);
|
|
71
|
+
if (content == null) continue;
|
|
72
|
+
|
|
73
|
+
const sf = ts.createSourceFile(f, content, ts.ScriptTarget.ES2022, true, getScriptKind(f));
|
|
74
|
+
|
|
75
|
+
const isExported = (node: import('typescript').Node): boolean => {
|
|
76
|
+
if (!ts.canHaveModifiers(node)) return false;
|
|
77
|
+
const mods = ts.getModifiers(node);
|
|
78
|
+
if (!mods) return false;
|
|
79
|
+
return mods.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
67
80
|
};
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const isExported = (node: any): boolean => {
|
|
76
|
-
const mods = node.modifiers;
|
|
77
|
-
if (!mods) return false;
|
|
78
|
-
return mods.some((m: any) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const isDefaultExport = (node: any): boolean => {
|
|
82
|
-
const mods = node.modifiers;
|
|
83
|
-
if (!mods) return false;
|
|
84
|
-
return mods.some((m: any) => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const textOfType = (typeNode: any): string | undefined => {
|
|
88
|
-
if (!typeNode) return undefined;
|
|
89
|
-
return content.slice(typeNode.pos, typeNode.end).trim();
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Collect import modules (optional future use)
|
|
93
|
-
// const imports = sf.statements.filter(ts.isImportDeclaration).map((i: any) => i.moduleSpecifier.text);
|
|
94
|
-
|
|
95
|
-
for (const stmt of sf.statements) {
|
|
96
|
-
if (ts.isFunctionDeclaration(stmt) && isExported(stmt)) {
|
|
97
|
-
const name = stmt.name?.text || (isDefaultExport(stmt) ? 'default' : 'anonymous');
|
|
98
|
-
const firstParam = stmt.parameters?.[0];
|
|
99
|
-
const propsType = textOfType(firstParam?.type);
|
|
100
|
-
components.push({
|
|
101
|
-
name: name === 'default' ? path.basename(f, path.extname(f)) : name,
|
|
102
|
-
exportType: isDefaultExport(stmt) ? 'default' : 'named',
|
|
103
|
-
file: rel(root, f),
|
|
104
|
-
propsType,
|
|
105
|
-
});
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
82
|
+
const isDefaultExport = (node: import('typescript').Node): boolean => {
|
|
83
|
+
if (!ts.canHaveModifiers(node)) return false;
|
|
84
|
+
const mods = ts.getModifiers(node);
|
|
85
|
+
if (!mods) return false;
|
|
86
|
+
return mods.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
87
|
+
};
|
|
108
88
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// Component-like initializers
|
|
115
|
-
const init = decl.initializer;
|
|
116
|
-
let propsType: string | undefined;
|
|
117
|
-
if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
|
|
118
|
-
const firstParam = init.parameters?.[0];
|
|
119
|
-
propsType = textOfType(firstParam?.type);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// React.forwardRef(...) pattern: export const X = React.forwardRef<...>((props, ref) => ...)
|
|
123
|
-
if (init && ts.isCallExpression(init)) {
|
|
124
|
-
const args = init.arguments;
|
|
125
|
-
const firstArg = args?.[0];
|
|
126
|
-
if (firstArg && (ts.isArrowFunction(firstArg) || ts.isFunctionExpression(firstArg))) {
|
|
127
|
-
const firstParam = firstArg.parameters?.[0];
|
|
128
|
-
propsType = textOfType(firstParam?.type);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
components.push({
|
|
133
|
-
name,
|
|
134
|
-
exportType: 'named',
|
|
135
|
-
file: rel(root, f),
|
|
136
|
-
propsType,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
89
|
+
const textOfType = (typeNode: import('typescript').Node | undefined): string | undefined => {
|
|
90
|
+
if (!typeNode) return undefined;
|
|
91
|
+
return content.slice(typeNode.pos, typeNode.end).trim();
|
|
92
|
+
};
|
|
141
93
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
94
|
+
// Collect import modules (optional future use)
|
|
95
|
+
// const imports = sf.statements.filter(ts.isImportDeclaration).map((i) => i.moduleSpecifier.text);
|
|
96
|
+
|
|
97
|
+
for (const stmt of sf.statements) {
|
|
98
|
+
if (ts.isFunctionDeclaration(stmt) && isExported(stmt)) {
|
|
99
|
+
const name = stmt.name?.text || (isDefaultExport(stmt) ? 'default' : 'anonymous');
|
|
100
|
+
const firstParam = stmt.parameters?.[0];
|
|
101
|
+
const propsType = textOfType(firstParam?.type);
|
|
102
|
+
components.push({
|
|
103
|
+
name: name === 'default' ? path.basename(f, path.extname(f)) : name,
|
|
104
|
+
exportType: isDefaultExport(stmt) ? 'default' : 'named',
|
|
105
|
+
file: rel(root, f),
|
|
106
|
+
propsType,
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (ts.isVariableStatement(stmt) && isExported(stmt)) {
|
|
112
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
113
|
+
const name = decl.name && ts.isIdentifier(decl.name) ? decl.name.text : undefined;
|
|
114
|
+
if (!name) continue;
|
|
115
|
+
|
|
116
|
+
// Component-like initializers
|
|
117
|
+
const init = decl.initializer;
|
|
118
|
+
let propsType: string | undefined;
|
|
119
|
+
if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
|
|
120
|
+
const firstParam = init.parameters?.[0];
|
|
121
|
+
propsType = textOfType(firstParam?.type);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// React.forwardRef(...) pattern: export const X = React.forwardRef<...>((props, ref) => ...)
|
|
125
|
+
if (init && ts.isCallExpression(init)) {
|
|
126
|
+
const args = init.arguments;
|
|
127
|
+
const firstArg = args?.[0];
|
|
128
|
+
if (firstArg && (ts.isArrowFunction(firstArg) || ts.isFunctionExpression(firstArg))) {
|
|
129
|
+
const firstParam = firstArg.parameters?.[0];
|
|
130
|
+
propsType = textOfType(firstParam?.type);
|
|
147
131
|
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
components.push({
|
|
135
|
+
name,
|
|
136
|
+
exportType: 'named',
|
|
137
|
+
file: rel(root, f),
|
|
138
|
+
propsType,
|
|
139
|
+
});
|
|
148
140
|
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (ts.isExportAssignment(stmt)) {
|
|
145
|
+
const expr = stmt.expression;
|
|
146
|
+
const name = ts.isIdentifier(expr) ? expr.text : path.basename(f, path.extname(f));
|
|
147
|
+
components.push({ name, exportType: 'default', file: rel(root, f) });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
root,
|
|
155
|
+
filesScanned: files.length,
|
|
156
|
+
components,
|
|
157
|
+
warnings,
|
|
158
|
+
};
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
function scanWithRegexFallback(root: string, files: string[]): CatalogResult {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
162
|
+
const components: ComponentExport[] = [];
|
|
163
|
+
const warnings: string[] = [
|
|
164
|
+
'TypeScript compiler API unavailable; using regex fallback (less accurate).',
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
for (const f of files) {
|
|
168
|
+
const content = readFileSafe(f);
|
|
169
|
+
if (content == null) continue;
|
|
170
|
+
|
|
171
|
+
// export function Foo(...)
|
|
172
|
+
const fnRe = /export\s+(default\s+)?function\s+([A-Za-z0-9_]+)/g;
|
|
173
|
+
let m: RegExpExecArray | null;
|
|
174
|
+
while ((m = fnRe.exec(content))) {
|
|
175
|
+
components.push({
|
|
176
|
+
name: m[2],
|
|
177
|
+
exportType: m[1] ? 'default' : 'named',
|
|
178
|
+
file: rel(root, f),
|
|
179
|
+
jsDoc: extractJSDocFromLeadingComment(content.slice(0, m.index)),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
178
182
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
// export const Foo = (...)
|
|
184
|
+
const constRe = /export\s+const\s+([A-Za-z0-9_]+)/g;
|
|
185
|
+
while ((m = constRe.exec(content))) {
|
|
186
|
+
components.push({
|
|
187
|
+
name: m[1],
|
|
188
|
+
exportType: 'named',
|
|
189
|
+
file: rel(root, f),
|
|
190
|
+
jsDoc: extractJSDocFromLeadingComment(content.slice(0, m.index)),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
189
193
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
194
|
+
// export default Identifier
|
|
195
|
+
const defRe = /export\s+default\s+([A-Za-z0-9_]+)/g;
|
|
196
|
+
while ((m = defRe.exec(content))) {
|
|
197
|
+
components.push({
|
|
198
|
+
name: m[1],
|
|
199
|
+
exportType: 'default',
|
|
200
|
+
file: rel(root, f),
|
|
201
|
+
jsDoc: extractJSDocFromLeadingComment(content.slice(0, m.index)),
|
|
202
|
+
});
|
|
200
203
|
}
|
|
204
|
+
}
|
|
201
205
|
|
|
202
|
-
|
|
206
|
+
return { root, filesScanned: files.length, components, warnings };
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
export async function buildComponentCatalog(root: string, files: string[]): Promise<CatalogResult> {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
try {
|
|
211
|
+
return await scanWithTypeScript(root, files);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const msg = error instanceof Error ? error.message : 'unknown error';
|
|
214
|
+
const fallback = scanWithRegexFallback(root, files);
|
|
215
|
+
fallback.warnings.push(`TypeScript scan failed: ${msg}`);
|
|
216
|
+
return fallback;
|
|
217
|
+
}
|
|
214
218
|
}
|