@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.
Files changed (129) hide show
  1. package/dist/components/catalog.d.ts.map +1 -1
  2. package/dist/components/catalog.js +10 -4
  3. package/dist/components/catalog.js.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +11 -6
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/context/builder.d.ts.map +1 -1
  8. package/dist/context/builder.js.map +1 -1
  9. package/dist/context/filter.d.ts.map +1 -1
  10. package/dist/context/filter.js +5 -1
  11. package/dist/context/filter.js.map +1 -1
  12. package/dist/context/grounding.d.ts.map +1 -1
  13. package/dist/context/grounding.js +7 -3
  14. package/dist/context/grounding.js.map +1 -1
  15. package/dist/context/guards.d.ts.map +1 -1
  16. package/dist/context/guards.js +53 -0
  17. package/dist/context/guards.js.map +1 -1
  18. package/dist/context/repo-hints.js.map +1 -1
  19. package/dist/context/styling-detector.d.ts +24 -0
  20. package/dist/context/styling-detector.d.ts.map +1 -0
  21. package/dist/context/styling-detector.js +337 -0
  22. package/dist/context/styling-detector.js.map +1 -0
  23. package/dist/design/principles.js.map +1 -1
  24. package/dist/generation/gemini-client.d.ts.map +1 -1
  25. package/dist/generation/gemini-client.js.map +1 -1
  26. package/dist/generation/litellm-client.d.ts.map +1 -1
  27. package/dist/generation/litellm-client.js +14 -7
  28. package/dist/generation/litellm-client.js.map +1 -1
  29. package/dist/generation/remote-client.d.ts +10 -5
  30. package/dist/generation/remote-client.d.ts.map +1 -1
  31. package/dist/generation/remote-client.js +13 -2
  32. package/dist/generation/remote-client.js.map +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/output/file-writer.d.ts.map +1 -1
  35. package/dist/output/file-writer.js +4 -4
  36. package/dist/output/file-writer.js.map +1 -1
  37. package/dist/output/formatter.d.ts.map +1 -1
  38. package/dist/output/formatter.js +5 -2
  39. package/dist/output/formatter.js.map +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +2 -1
  42. package/dist/server.js.map +1 -1
  43. package/dist/stack/detect.d.ts.map +1 -1
  44. package/dist/stack/detect.js +42 -9
  45. package/dist/stack/detect.js.map +1 -1
  46. package/dist/tokens/sync.d.ts.map +1 -1
  47. package/dist/tokens/sync.js +22 -5
  48. package/dist/tokens/sync.js.map +1 -1
  49. package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
  50. package/dist/tools/analyze-screenshot-ui.js +5 -5
  51. package/dist/tools/analyze-screenshot-ui.js.map +1 -1
  52. package/dist/tools/analyze-tokens.d.ts.map +1 -1
  53. package/dist/tools/analyze-tokens.js +3 -1
  54. package/dist/tools/analyze-tokens.js.map +1 -1
  55. package/dist/tools/catalog-components.d.ts.map +1 -1
  56. package/dist/tools/catalog-components.js +1 -4
  57. package/dist/tools/catalog-components.js.map +1 -1
  58. package/dist/tools/create-ui.d.ts +3 -0
  59. package/dist/tools/create-ui.d.ts.map +1 -1
  60. package/dist/tools/create-ui.js +203 -75
  61. package/dist/tools/create-ui.js.map +1 -1
  62. package/dist/tools/detect-ui-stack.js.map +1 -1
  63. package/dist/tools/generate-component-variants.d.ts.map +1 -1
  64. package/dist/tools/generate-component-variants.js +15 -4
  65. package/dist/tools/generate-component-variants.js.map +1 -1
  66. package/dist/tools/generate-vibes.d.ts.map +1 -1
  67. package/dist/tools/generate-vibes.js +7 -3
  68. package/dist/tools/generate-vibes.js.map +1 -1
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/tools/modify-ui.d.ts.map +1 -1
  71. package/dist/tools/modify-ui.js +7 -2
  72. package/dist/tools/modify-ui.js.map +1 -1
  73. package/dist/tools/scaffold-project.d.ts.map +1 -1
  74. package/dist/tools/scaffold-project.js +3 -1
  75. package/dist/tools/scaffold-project.js.map +1 -1
  76. package/dist/tools/snippet-ui.d.ts +3 -1
  77. package/dist/tools/snippet-ui.d.ts.map +1 -1
  78. package/dist/tools/snippet-ui.js +219 -88
  79. package/dist/tools/snippet-ui.js.map +1 -1
  80. package/dist/tools/sync-design-tokens.d.ts.map +1 -1
  81. package/dist/tools/sync-design-tokens.js +26 -11
  82. package/dist/tools/sync-design-tokens.js.map +1 -1
  83. package/dist/utils/walk.d.ts.map +1 -1
  84. package/dist/utils/walk.js.map +1 -1
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.d.ts.map +1 -0
  87. package/dist/version.js +5 -0
  88. package/dist/version.js.map +1 -0
  89. package/package.json +55 -55
  90. package/src/__tests__/builder.test.ts +19 -19
  91. package/src/__tests__/config.test.ts +63 -31
  92. package/src/__tests__/filter.test.ts +98 -92
  93. package/src/__tests__/remote-client.test.ts +179 -0
  94. package/src/components/catalog.ts +170 -166
  95. package/src/config/index.ts +185 -177
  96. package/src/context/builder.ts +157 -157
  97. package/src/context/filter.ts +110 -104
  98. package/src/context/grounding.ts +143 -129
  99. package/src/context/guards.ts +97 -38
  100. package/src/context/repo-hints.ts +24 -24
  101. package/src/context/styling-detector.ts +460 -0
  102. package/src/design/principles.ts +14 -14
  103. package/src/generation/gemini-client.ts +53 -56
  104. package/src/generation/litellm-client.ts +102 -86
  105. package/src/generation/remote-client.ts +100 -77
  106. package/src/index.ts +16 -16
  107. package/src/output/file-writer.ts +123 -123
  108. package/src/output/formatter.ts +139 -132
  109. package/src/server.ts +12 -11
  110. package/src/stack/detect.ts +226 -175
  111. package/src/tokens/sync.ts +189 -155
  112. package/src/tools/analyze-screenshot-ui.ts +89 -88
  113. package/src/tools/analyze-tokens.ts +80 -78
  114. package/src/tools/catalog-components.ts +68 -68
  115. package/src/tools/create-ui.ts +295 -142
  116. package/src/tools/detect-ui-stack.ts +36 -36
  117. package/src/tools/generate-component-variants.ts +155 -135
  118. package/src/tools/generate-vibes.ts +121 -117
  119. package/src/tools/index.ts +14 -14
  120. package/src/tools/modify-ui.ts +170 -165
  121. package/src/tools/scaffold-project.ts +68 -66
  122. package/src/tools/snippet-ui.ts +323 -172
  123. package/src/tools/sync-design-tokens.ts +217 -195
  124. package/src/utils/walk.ts +47 -45
  125. package/src/version.ts +6 -0
  126. package/tsconfig.json +23 -33
  127. package/vitest.config.ts +10 -10
  128. package/.prettierrc +0 -9
  129. 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
- name: string;
17
- exportType: 'named' | 'default';
18
- file: string; // relative to root
19
- propsType?: string;
20
- jsDoc?: string;
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
- root: string;
25
- filesScanned: number;
26
- components: ComponentExport[];
27
- warnings: string[];
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
- try {
32
- return fs.readFileSync(filePath, 'utf-8');
33
- } catch {
34
- return null;
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
- return toPosixPath(path.relative(root, abs));
39
+ return toPosixPath(path.relative(root, abs));
40
40
  }
41
41
 
42
42
  function extractJSDocFromLeadingComment(text: string): string | undefined {
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;
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
- const warnings: string[] = [];
56
- const components: ComponentExport[] = [];
57
-
58
- const ts = await import('typescript');
59
-
60
- const getScriptKind = (filePath: string): any => {
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;
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
- 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: 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
- if (ts.isVariableStatement(stmt) && isExported(stmt)) {
110
- for (const decl of stmt.declarationList.declarations) {
111
- const name = decl.name && ts.isIdentifier(decl.name) ? decl.name.text : undefined;
112
- if (!name) continue;
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
- if (ts.isExportAssignment(stmt)) {
143
- const expr = stmt.expression;
144
- const name = ts.isIdentifier(expr) ? expr.text : path.basename(f, path.extname(f));
145
- components.push({ name, exportType: 'default', file: rel(root, f) });
146
- continue;
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
- return {
152
- root,
153
- filesScanned: files.length,
154
- components,
155
- warnings,
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
- const components: ComponentExport[] = [];
161
- const warnings: string[] = ['TypeScript compiler API unavailable; using regex fallback (less accurate).'];
162
-
163
- for (const f of files) {
164
- const content = readFileSafe(f);
165
- if (content == null) continue;
166
-
167
- // export function Foo(...)
168
- const fnRe = /export\s+(default\s+)?function\s+([A-Za-z0-9_]+)/g;
169
- let m: RegExpExecArray | null;
170
- while ((m = fnRe.exec(content))) {
171
- components.push({
172
- name: m[2],
173
- exportType: m[1] ? 'default' : 'named',
174
- file: rel(root, f),
175
- jsDoc: extractJSDocFromLeadingComment(content.slice(0, m.index)),
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
- // export const Foo = (...)
180
- const constRe = /export\s+const\s+([A-Za-z0-9_]+)/g;
181
- while ((m = constRe.exec(content))) {
182
- components.push({
183
- name: m[1],
184
- exportType: 'named',
185
- file: rel(root, f),
186
- jsDoc: extractJSDocFromLeadingComment(content.slice(0, m.index)),
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
- // export default Identifier
191
- const defRe = /export\s+default\s+([A-Za-z0-9_]+)/g;
192
- while ((m = defRe.exec(content))) {
193
- components.push({
194
- name: m[1],
195
- exportType: 'default',
196
- file: rel(root, f),
197
- jsDoc: extractJSDocFromLeadingComment(content.slice(0, m.index)),
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
- return { root, filesScanned: files.length, components, warnings };
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
- try {
207
- return await scanWithTypeScript(root, files);
208
- } catch (error) {
209
- const msg = error instanceof Error ? error.message : 'unknown error';
210
- const fallback = scanWithRegexFallback(root, files);
211
- fallback.warnings.push(`TypeScript scan failed: ${msg}`);
212
- return fallback;
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
  }