@fragments-sdk/cli 0.4.0 → 0.4.2

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 (56) hide show
  1. package/dist/bin.js +11 -11
  2. package/dist/{chunk-OZQ7Z6C3.js → chunk-5CKYLCJH.js} +28 -1
  3. package/dist/{chunk-OZQ7Z6C3.js.map → chunk-5CKYLCJH.js.map} +1 -1
  4. package/dist/{chunk-5JNME72P.js → chunk-5ZYEOHYK.js} +11 -9
  5. package/dist/chunk-5ZYEOHYK.js.map +1 -0
  6. package/dist/{chunk-FYIYMXGA.js → chunk-G3M3MPQ6.js} +13 -2
  7. package/dist/chunk-G3M3MPQ6.js.map +1 -0
  8. package/dist/{chunk-LDKNZ55O.js → chunk-J4SI5RIH.js} +3 -3
  9. package/dist/{chunk-ODXQAQQX.js → chunk-ZFKGX3QK.js} +4 -4
  10. package/dist/{core-F3VT277E.js → core-LNXDLXDP.js} +2 -2
  11. package/dist/{generate-PNIUR75D.js → generate-OIXXHOWR.js} +3 -3
  12. package/dist/index.js +4 -4
  13. package/dist/{init-ON6WYG66.js → init-EVPXIDW4.js} +3 -3
  14. package/dist/mcp-bin.js +44 -43
  15. package/dist/mcp-bin.js.map +1 -1
  16. package/dist/scan-YVYD64GD.js +12 -0
  17. package/dist/{service-U7AR2PC2.js → service-K52ORLCJ.js} +3 -3
  18. package/dist/{static-viewer-QL2SCWYB.js → static-viewer-JNQIHA4B.js} +2 -2
  19. package/dist/{test-PBPKJ4WJ.js → test-USARUEFW.js} +8 -4
  20. package/dist/test-USARUEFW.js.map +1 -0
  21. package/dist/{tokens-4J4PRIGT.js → tokens-C6YHBOQE.js} +4 -4
  22. package/dist/{viewer-6VCZMA3T.js → viewer-H7TVFT4E.js} +14 -14
  23. package/dist/{viewer-6VCZMA3T.js.map → viewer-H7TVFT4E.js.map} +1 -1
  24. package/package.json +2 -1
  25. package/src/core/parser.ts +48 -0
  26. package/src/core/storyAdapter.ts +1 -1
  27. package/src/core/storybook-csf.ts +11 -0
  28. package/src/core/types.ts +1 -1
  29. package/src/mcp/__tests__/findFragmentsJson.test.ts +308 -0
  30. package/src/mcp/server.ts +59 -55
  31. package/src/service/enhance/doc-extractor.ts +2 -2
  32. package/src/service/enhance/props-extractor.ts +1 -1
  33. package/src/service/enhance/storybook-parser.ts +1 -1
  34. package/src/service/figma.ts +2 -2
  35. package/src/service/metrics-store.ts +2 -1
  36. package/src/service/patch-generator.ts +2 -1
  37. package/src/test/reporters/junit.ts +7 -3
  38. package/src/test/runner.ts +4 -4
  39. package/src/test/watch.ts +2 -2
  40. package/src/viewer/components/CodePanel.tsx +1 -1
  41. package/src/viewer/components/FigmaEmbed.tsx +1 -1
  42. package/src/viewer/jsx-parser.ts +2 -1
  43. package/src/viewer/utils/colorSchemes.ts +3 -3
  44. package/dist/chunk-5JNME72P.js.map +0 -1
  45. package/dist/chunk-FYIYMXGA.js.map +0 -1
  46. package/dist/scan-E6U644RS.js +0 -12
  47. package/dist/test-PBPKJ4WJ.js.map +0 -1
  48. /package/dist/{chunk-LDKNZ55O.js.map → chunk-J4SI5RIH.js.map} +0 -0
  49. /package/dist/{chunk-ODXQAQQX.js.map → chunk-ZFKGX3QK.js.map} +0 -0
  50. /package/dist/{core-F3VT277E.js.map → core-LNXDLXDP.js.map} +0 -0
  51. /package/dist/{generate-PNIUR75D.js.map → generate-OIXXHOWR.js.map} +0 -0
  52. /package/dist/{init-ON6WYG66.js.map → init-EVPXIDW4.js.map} +0 -0
  53. /package/dist/{scan-E6U644RS.js.map → scan-YVYD64GD.js.map} +0 -0
  54. /package/dist/{service-U7AR2PC2.js.map → service-K52ORLCJ.js.map} +0 -0
  55. /package/dist/{static-viewer-QL2SCWYB.js.map → static-viewer-JNQIHA4B.js.map} +0 -0
  56. /package/dist/{tokens-4J4PRIGT.js.map → tokens-C6YHBOQE.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragments-sdk/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "CLI, MCP server, and dev tools for Fragments design system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -100,6 +100,7 @@
100
100
  "scripts": {
101
101
  "build": "tsup",
102
102
  "dev": "tsup --watch",
103
+ "lint": "eslint src",
103
104
  "test": "vitest run",
104
105
  "typecheck": "tsc --noEmit",
105
106
  "clean": "rm -rf dist"
@@ -408,6 +408,51 @@ function extractVariants(
408
408
  return variants;
409
409
  }
410
410
 
411
+ /**
412
+ * Remove common leading whitespace from all lines (dedent).
413
+ * This handles template literals and JSX that have extra indentation from code formatting.
414
+ *
415
+ * Special handling: If the first line has no indentation (common after .trim()),
416
+ * we calculate minimum indent from subsequent lines only.
417
+ */
418
+ function dedent(str: string): string {
419
+ const lines = str.split('\n');
420
+
421
+ if (lines.length <= 1) {
422
+ return str;
423
+ }
424
+
425
+ // Check if first line has no indentation
426
+ const firstLineIndent = lines[0].match(/^(\s*)/)?.[1].length ?? 0;
427
+ const startIndex = firstLineIndent === 0 ? 1 : 0;
428
+
429
+ // Find the minimum indentation (ignoring empty lines)
430
+ let minIndent = Infinity;
431
+ for (let i = startIndex; i < lines.length; i++) {
432
+ const line = lines[i];
433
+ if (line.trim() === '') continue;
434
+ const match = line.match(/^(\s*)/);
435
+ if (match) {
436
+ minIndent = Math.min(minIndent, match[1].length);
437
+ }
438
+ }
439
+
440
+ // If no indentation found, return as-is
441
+ if (minIndent === Infinity || minIndent === 0) {
442
+ return str;
443
+ }
444
+
445
+ // Remove the common indentation from all lines (except first if it had no indent)
446
+ return lines
447
+ .map((line, index) => {
448
+ if (index === 0 && firstLineIndent === 0) {
449
+ return line; // Keep first line as-is
450
+ }
451
+ return line.slice(minIndent);
452
+ })
453
+ .join('\n');
454
+ }
455
+
411
456
  /**
412
457
  * Extract the code from a render function.
413
458
  */
@@ -428,6 +473,9 @@ function extractRenderCode(
428
473
  code = code.slice(1, -1).trim();
429
474
  }
430
475
 
476
+ // Dedent the code to remove common leading whitespace
477
+ code = dedent(code);
478
+
431
479
  return code;
432
480
  }
433
481
 
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { createElement, type ComponentType, type ReactNode } from "react";
12
- import { toId, storyNameFromExport, isExportStory } from "@storybook/csf";
12
+ import { toId, storyNameFromExport, isExportStory } from "./storybook-csf.js";
13
13
  import type {
14
14
  SegmentDefinition,
15
15
  SegmentMeta,
@@ -0,0 +1,11 @@
1
+ import {
2
+ toId as storybookToId,
3
+ storyNameFromExport as storybookStoryNameFromExport,
4
+ isExportStory as storybookIsExportStory,
5
+ } from "@storybook/csf";
6
+
7
+ export const toId: typeof storybookToId = (...args) => storybookToId(...args);
8
+ export const storyNameFromExport: typeof storybookStoryNameFromExport = (...args) =>
9
+ storybookStoryNameFromExport(...args);
10
+ export const isExportStory: typeof storybookIsExportStory = (...args) =>
11
+ storybookIsExportStory(...args);
package/src/core/types.ts CHANGED
@@ -5,7 +5,7 @@ import type { ComponentType, ReactNode, JSX } from "react";
5
5
  * This type is intentionally broad to support various React component patterns
6
6
  * including FC, forwardRef, memo, and class components across different React versions.
7
7
  */
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+
9
9
  export type SegmentComponent<TProps = any> =
10
10
  | ComponentType<TProps>
11
11
  | ((props: TProps) => ReactNode | JSX.Element | null);
@@ -0,0 +1,308 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, writeFileSync, rmSync, realpathSync, symlinkSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { findFragmentsJson } from '../server.js';
6
+
7
+ /**
8
+ * Creates a temporary directory tree for testing findFragmentsJson.
9
+ * Uses realpathSync to normalize macOS /var -> /private/var symlinks,
10
+ * ensuring path comparisons work reliably.
11
+ */
12
+ function createTmpDir(): string {
13
+ const raw = join(tmpdir(), `fragments-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
14
+ mkdirSync(raw, { recursive: true });
15
+ return realpathSync(raw);
16
+ }
17
+
18
+ function writeJson(path: string, data: unknown): void {
19
+ writeFileSync(path, JSON.stringify(data));
20
+ }
21
+
22
+ describe('findFragmentsJson', () => {
23
+ let root: string;
24
+
25
+ beforeEach(() => {
26
+ root = createTmpDir();
27
+ });
28
+
29
+ afterEach(() => {
30
+ rmSync(root, { recursive: true, force: true });
31
+ });
32
+
33
+ describe('Phase 1: walk upward (library author flow)', () => {
34
+ it('finds fragments.json in the start directory', () => {
35
+ writeFileSync(join(root, 'fragments.json'), '{}');
36
+
37
+ const result = findFragmentsJson(root);
38
+ expect(result).toEqual([join(root, 'fragments.json')]);
39
+ });
40
+
41
+ it('finds fragments.json in a parent directory', () => {
42
+ const sub = join(root, 'packages', 'app');
43
+ mkdirSync(sub, { recursive: true });
44
+ writeFileSync(join(root, 'fragments.json'), '{}');
45
+
46
+ const result = findFragmentsJson(sub);
47
+ expect(result).toEqual([join(root, 'fragments.json')]);
48
+ });
49
+
50
+ it('returns empty array when no fragments.json exists anywhere', () => {
51
+ const sub = join(root, 'some', 'deep', 'path');
52
+ mkdirSync(sub, { recursive: true });
53
+
54
+ const result = findFragmentsJson(sub);
55
+ expect(result).toEqual([]);
56
+ });
57
+ });
58
+
59
+ describe('Phase 2: resolve deps with "fragments" field', () => {
60
+ it('finds fragments.json via dependency with "fragments" field', () => {
61
+ const depDir = join(root, 'node_modules', 'my-ui-lib');
62
+ mkdirSync(depDir, { recursive: true });
63
+
64
+ writeJson(join(root, 'package.json'), {
65
+ dependencies: { 'my-ui-lib': '^1.0.0' },
66
+ });
67
+ writeJson(join(depDir, 'package.json'), {
68
+ name: 'my-ui-lib',
69
+ fragments: 'fragments.json',
70
+ });
71
+ writeFileSync(join(depDir, 'fragments.json'), '{}');
72
+
73
+ const result = findFragmentsJson(root);
74
+ expect(result.length).toBe(1);
75
+ expect(result[0]).toBe(join(depDir, 'fragments.json'));
76
+ });
77
+
78
+ it('resolves custom fragments path from dependency', () => {
79
+ const depDir = join(root, 'node_modules', 'my-ui-lib');
80
+ mkdirSync(join(depDir, 'dist'), { recursive: true });
81
+
82
+ writeJson(join(root, 'package.json'), {
83
+ dependencies: { 'my-ui-lib': '^1.0.0' },
84
+ });
85
+ writeJson(join(depDir, 'package.json'), {
86
+ name: 'my-ui-lib',
87
+ fragments: 'dist/fragments.json',
88
+ });
89
+ writeFileSync(join(depDir, 'dist', 'fragments.json'), '{}');
90
+
91
+ const result = findFragmentsJson(root);
92
+ expect(result.length).toBe(1);
93
+ expect(result[0]).toBe(join(depDir, 'dist', 'fragments.json'));
94
+ });
95
+
96
+ it('finds fragments.json from devDependencies', () => {
97
+ const depDir = join(root, 'node_modules', 'my-ui-lib');
98
+ mkdirSync(depDir, { recursive: true });
99
+
100
+ writeJson(join(root, 'package.json'), {
101
+ devDependencies: { 'my-ui-lib': '^1.0.0' },
102
+ });
103
+ writeJson(join(depDir, 'package.json'), {
104
+ name: 'my-ui-lib',
105
+ fragments: 'fragments.json',
106
+ });
107
+ writeFileSync(join(depDir, 'fragments.json'), '{}');
108
+
109
+ const result = findFragmentsJson(root);
110
+ expect(result.length).toBe(1);
111
+ expect(result[0]).toBe(join(depDir, 'fragments.json'));
112
+ });
113
+
114
+ it('skips dependencies without "fragments" field', () => {
115
+ const depDir = join(root, 'node_modules', 'react');
116
+ mkdirSync(depDir, { recursive: true });
117
+
118
+ writeJson(join(root, 'package.json'), {
119
+ dependencies: { react: '^18.0.0' },
120
+ });
121
+ writeJson(join(depDir, 'package.json'), {
122
+ name: 'react',
123
+ });
124
+
125
+ const result = findFragmentsJson(root);
126
+ expect(result).toEqual([]);
127
+ });
128
+
129
+ it('skips dependency when fragments file does not exist', () => {
130
+ const depDir = join(root, 'node_modules', 'my-ui-lib');
131
+ mkdirSync(depDir, { recursive: true });
132
+
133
+ writeJson(join(root, 'package.json'), {
134
+ dependencies: { 'my-ui-lib': '^1.0.0' },
135
+ });
136
+ writeJson(join(depDir, 'package.json'), {
137
+ name: 'my-ui-lib',
138
+ fragments: 'fragments.json',
139
+ });
140
+ // Note: fragments.json not created
141
+
142
+ const result = findFragmentsJson(root);
143
+ expect(result).toEqual([]);
144
+ });
145
+
146
+ it('skips unresolvable dependencies gracefully', () => {
147
+ writeJson(join(root, 'package.json'), {
148
+ dependencies: { 'nonexistent-package': '^1.0.0' },
149
+ });
150
+
151
+ const result = findFragmentsJson(root);
152
+ expect(result).toEqual([]);
153
+ });
154
+
155
+ it('handles project with no package.json', () => {
156
+ const result = findFragmentsJson(root);
157
+ expect(result).toEqual([]);
158
+ });
159
+ });
160
+
161
+ describe('symlink resolution (pnpm-style)', () => {
162
+ it('follows symlinks to find fragments.json', () => {
163
+ // Simulate pnpm layout: real package in .pnpm, symlink in node_modules
164
+ const realDir = join(root, '.pnpm', 'my-ui-lib@1.0.0', 'node_modules', 'my-ui-lib');
165
+ const symlinkDir = join(root, 'node_modules', 'my-ui-lib');
166
+
167
+ mkdirSync(realDir, { recursive: true });
168
+ mkdirSync(join(root, 'node_modules'), { recursive: true });
169
+
170
+ writeJson(join(realDir, 'package.json'), {
171
+ name: 'my-ui-lib',
172
+ fragments: 'fragments.json',
173
+ });
174
+ writeFileSync(join(realDir, 'fragments.json'), '{}');
175
+
176
+ // Create symlink: node_modules/my-ui-lib -> .pnpm/.../my-ui-lib
177
+ symlinkSync(realDir, symlinkDir, 'dir');
178
+
179
+ writeJson(join(root, 'package.json'), {
180
+ dependencies: { 'my-ui-lib': '^1.0.0' },
181
+ });
182
+
183
+ const result = findFragmentsJson(root);
184
+ expect(result.length).toBe(1);
185
+ // require.resolve follows the symlink to the real path
186
+ expect(result[0]).toBe(join(realDir, 'fragments.json'));
187
+ });
188
+ });
189
+
190
+ describe('hoisted dependencies', () => {
191
+ it('finds fragments.json from a hoisted parent node_modules', () => {
192
+ // Monorepo layout: deps hoisted to root, app is in packages/app
193
+ const appDir = join(root, 'packages', 'app');
194
+ const hoistedDep = join(root, 'node_modules', 'my-ui-lib');
195
+
196
+ mkdirSync(appDir, { recursive: true });
197
+ mkdirSync(hoistedDep, { recursive: true });
198
+
199
+ writeJson(join(appDir, 'package.json'), {
200
+ dependencies: { 'my-ui-lib': '^1.0.0' },
201
+ });
202
+ writeJson(join(hoistedDep, 'package.json'), {
203
+ name: 'my-ui-lib',
204
+ fragments: 'fragments.json',
205
+ });
206
+ writeFileSync(join(hoistedDep, 'fragments.json'), '{}');
207
+
208
+ // createRequire resolves up the directory tree, so this should work
209
+ const result = findFragmentsJson(appDir);
210
+ expect(result.length).toBe(1);
211
+ expect(result[0]).toBe(join(hoistedDep, 'fragments.json'));
212
+ });
213
+ });
214
+
215
+ describe('deduplication', () => {
216
+ it('returns both local and dep fragments when they are different files', () => {
217
+ // fragments.json in root (found by walk-up) AND dep has its own
218
+ writeFileSync(join(root, 'fragments.json'), '{}');
219
+
220
+ const depDir = join(root, 'node_modules', 'my-ui-lib');
221
+ mkdirSync(depDir, { recursive: true });
222
+ writeJson(join(root, 'package.json'), {
223
+ dependencies: { 'my-ui-lib': '^1.0.0' },
224
+ });
225
+ writeJson(join(depDir, 'package.json'), {
226
+ name: 'my-ui-lib',
227
+ fragments: 'fragments.json',
228
+ });
229
+ writeFileSync(join(depDir, 'fragments.json'), '{}');
230
+
231
+ const result = findFragmentsJson(root);
232
+ // Walk-up finds root/fragments.json, dep resolution finds dep/fragments.json
233
+ expect(result.length).toBe(2);
234
+ expect(result).toContain(join(root, 'fragments.json'));
235
+ expect(result).toContain(join(depDir, 'fragments.json'));
236
+ });
237
+ });
238
+
239
+ describe('multiple dependencies', () => {
240
+ it('finds fragments.json from multiple dependencies', () => {
241
+ const dep1 = join(root, 'node_modules', 'ui-lib-a');
242
+ const dep2 = join(root, 'node_modules', 'ui-lib-b');
243
+ mkdirSync(dep1, { recursive: true });
244
+ mkdirSync(dep2, { recursive: true });
245
+
246
+ writeJson(join(root, 'package.json'), {
247
+ dependencies: { 'ui-lib-a': '^1.0.0' },
248
+ devDependencies: { 'ui-lib-b': '^2.0.0' },
249
+ });
250
+ writeJson(join(dep1, 'package.json'), {
251
+ name: 'ui-lib-a',
252
+ fragments: 'fragments.json',
253
+ });
254
+ writeFileSync(join(dep1, 'fragments.json'), '{}');
255
+ writeJson(join(dep2, 'package.json'), {
256
+ name: 'ui-lib-b',
257
+ fragments: 'fragments.json',
258
+ });
259
+ writeFileSync(join(dep2, 'fragments.json'), '{}');
260
+
261
+ const result = findFragmentsJson(root);
262
+ expect(result.length).toBe(2);
263
+ expect(result).toContain(join(dep1, 'fragments.json'));
264
+ expect(result).toContain(join(dep2, 'fragments.json'));
265
+ });
266
+
267
+ it('only includes deps that have "fragments" field', () => {
268
+ const uiLib = join(root, 'node_modules', 'ui-lib');
269
+ const react = join(root, 'node_modules', 'react');
270
+ mkdirSync(uiLib, { recursive: true });
271
+ mkdirSync(react, { recursive: true });
272
+
273
+ writeJson(join(root, 'package.json'), {
274
+ dependencies: { react: '^18.0.0', 'ui-lib': '^1.0.0' },
275
+ });
276
+ writeJson(join(react, 'package.json'), { name: 'react' });
277
+ writeJson(join(uiLib, 'package.json'), {
278
+ name: 'ui-lib',
279
+ fragments: 'fragments.json',
280
+ });
281
+ writeFileSync(join(uiLib, 'fragments.json'), '{}');
282
+
283
+ const result = findFragmentsJson(root);
284
+ expect(result.length).toBe(1);
285
+ expect(result[0]).toBe(join(uiLib, 'fragments.json'));
286
+ });
287
+ });
288
+
289
+ describe('scoped packages', () => {
290
+ it('resolves scoped package names like @scope/ui', () => {
291
+ const depDir = join(root, 'node_modules', '@my-scope', 'ui');
292
+ mkdirSync(depDir, { recursive: true });
293
+
294
+ writeJson(join(root, 'package.json'), {
295
+ dependencies: { '@my-scope/ui': '^1.0.0' },
296
+ });
297
+ writeJson(join(depDir, 'package.json'), {
298
+ name: '@my-scope/ui',
299
+ fragments: 'fragments.json',
300
+ });
301
+ writeFileSync(join(depDir, 'fragments.json'), '{}');
302
+
303
+ const result = findFragmentsJson(root);
304
+ expect(result.length).toBe(1);
305
+ expect(result[0]).toBe(join(depDir, 'fragments.json'));
306
+ });
307
+ });
308
+ });
package/src/mcp/server.ts CHANGED
@@ -32,6 +32,7 @@ async function getService(): Promise<ServiceModule> {
32
32
  import { readFile } from 'node:fs/promises';
33
33
  import { existsSync, readFileSync } from 'node:fs';
34
34
  import { join, dirname, resolve } from 'node:path';
35
+ import { createRequire } from 'node:module';
35
36
  import { projectFields } from './utils.js';
36
37
 
37
38
  /**
@@ -65,6 +66,63 @@ function filterPlaceholders(items: string[] | undefined): string[] {
65
66
  );
66
67
  }
67
68
 
69
+ /**
70
+ * Find fragments.json files:
71
+ * 1. Walk up from startDir (for library authors with a local build)
72
+ * 2. Read package.json deps and resolve packages with a "fragments" field
73
+ *
74
+ * Uses Node.js module resolution (createRequire) to handle all package
75
+ * managers: pnpm symlinks, yarn PnP, monorepo hoisting, etc.
76
+ */
77
+ export function findFragmentsJson(startDir: string): string[] {
78
+ const found: string[] = [];
79
+ const resolvedStart = resolve(startDir);
80
+
81
+ // 1. Walk upward from startDir (library author flow)
82
+ let dir = resolvedStart;
83
+ while (true) {
84
+ const candidate = join(dir, BRAND.outFile);
85
+ if (existsSync(candidate)) {
86
+ found.push(candidate);
87
+ break;
88
+ }
89
+ const parent = dirname(dir);
90
+ if (parent === dir) break;
91
+ dir = parent;
92
+ }
93
+
94
+ // 2. Read package.json and resolve deps with "fragments" field
95
+ const pkgJsonPath = join(resolvedStart, 'package.json');
96
+ if (existsSync(pkgJsonPath)) {
97
+ try {
98
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
99
+ const allDeps = {
100
+ ...pkgJson.dependencies,
101
+ ...pkgJson.devDependencies,
102
+ };
103
+ const localRequire = createRequire(join(resolvedStart, 'noop.js'));
104
+ for (const depName of Object.keys(allDeps)) {
105
+ try {
106
+ const depPkgPath = localRequire.resolve(`${depName}/package.json`);
107
+ const depPkg = JSON.parse(readFileSync(depPkgPath, 'utf-8'));
108
+ if (depPkg.fragments) {
109
+ const fragmentsPath = join(dirname(depPkgPath), depPkg.fragments);
110
+ if (existsSync(fragmentsPath) && !found.includes(fragmentsPath)) {
111
+ found.push(fragmentsPath);
112
+ }
113
+ }
114
+ } catch {
115
+ // Package not resolvable or unreadable, skip
116
+ }
117
+ }
118
+ } catch {
119
+ // No package.json or unreadable
120
+ }
121
+ }
122
+
123
+ return found;
124
+ }
125
+
68
126
  /**
69
127
  * MCP Server configuration
70
128
  */
@@ -276,66 +334,12 @@ export function createMcpServer(config: McpServerConfig): Server {
276
334
  // Lazy-loaded resources
277
335
  let segmentsData: CompiledSegmentsFile | null = null;
278
336
  let packageName: string | null = null;
279
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- lazy-loaded from service
337
+
280
338
  let browserPool: any = null;
281
339
  let storageManager: any = null;
282
340
  let diffEngine: any = null;
283
341
  let isPoolWarming = false;
284
342
 
285
- /**
286
- * Find fragments.json files:
287
- * 1. Walk up from projectRoot (for library authors with a local build)
288
- * 2. Read package.json deps and resolve packages with a "fragments" field
289
- */
290
- function findFragmentsJson(startDir: string): string[] {
291
- const found: string[] = [];
292
- const resolvedStart = resolve(startDir);
293
-
294
- // 1. Walk upward from startDir (library author flow)
295
- let dir = resolvedStart;
296
- while (true) {
297
- const candidate = join(dir, BRAND.outFile);
298
- if (existsSync(candidate)) {
299
- found.push(candidate);
300
- break;
301
- }
302
- const parent = dirname(dir);
303
- if (parent === dir) break;
304
- dir = parent;
305
- }
306
-
307
- // 2. Read package.json and resolve deps with "fragments" field
308
- const pkgJsonPath = join(resolvedStart, 'package.json');
309
- if (existsSync(pkgJsonPath)) {
310
- try {
311
- const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
312
- const allDeps = {
313
- ...pkgJson.dependencies,
314
- ...pkgJson.devDependencies,
315
- };
316
- for (const depName of Object.keys(allDeps)) {
317
- const depPkgPath = join(resolvedStart, 'node_modules', depName, 'package.json');
318
- if (!existsSync(depPkgPath)) continue;
319
- try {
320
- const depPkg = JSON.parse(readFileSync(depPkgPath, 'utf-8'));
321
- if (depPkg.fragments) {
322
- const fragmentsPath = join(resolvedStart, 'node_modules', depName, depPkg.fragments);
323
- if (existsSync(fragmentsPath) && !found.includes(fragmentsPath)) {
324
- found.push(fragmentsPath);
325
- }
326
- }
327
- } catch {
328
- // Skip unreadable package
329
- }
330
- }
331
- } catch {
332
- // No package.json or unreadable
333
- }
334
- }
335
-
336
- return found;
337
- }
338
-
339
343
  async function loadSegments(): Promise<CompiledSegmentsFile> {
340
344
  if (segmentsData) {
341
345
  return segmentsData;
@@ -139,7 +139,7 @@ function extractJSDoc(
139
139
 
140
140
  // Parse JSDoc content
141
141
  const lines = content.split("\n");
142
- let currentDescription: string[] = [];
142
+ const currentDescription: string[] = [];
143
143
  let currentTag: string | null = null;
144
144
  let currentTagContent: string[] = [];
145
145
 
@@ -197,7 +197,7 @@ function processTag(
197
197
  if (propMatch) {
198
198
  const [, type, nameRaw, description] = propMatch;
199
199
  const isOptional = nameRaw.startsWith("[");
200
- const name = nameRaw.replace(/[\[\]]/g, "");
200
+ const name = nameRaw.replaceAll("[", "").replaceAll("]", "");
201
201
  docs.props?.push({
202
202
  name,
203
203
  type,
@@ -331,7 +331,7 @@ function parseJSDocContent(
331
331
  }
332
332
  ): void {
333
333
  const lines = content.split("\n");
334
- let currentDescription: string[] = [];
334
+ const currentDescription: string[] = [];
335
335
  let currentTag: string | null = null;
336
336
  let currentTagContent: string[] = [];
337
337
 
@@ -484,7 +484,7 @@ function camelToTitle(str: string): string {
484
484
  function inferComponentFromPath(filePath: string): string {
485
485
  const fileName = basename(filePath);
486
486
  // Remove .stories.tsx etc
487
- let name = fileName.replace(/\.stories\.(tsx?|jsx?|mdx?)$/, "");
487
+ const name = fileName.replace(/\.stories\.(tsx?|jsx?|mdx?)$/, "");
488
488
  // Handle patterns like Button.stories.tsx
489
489
  return name;
490
490
  }
@@ -313,7 +313,7 @@ export class FigmaClient {
313
313
  */
314
314
  parseUrl(url: string): FigmaUrlParts {
315
315
  // Match both /file/ and /design/ paths
316
- const urlPattern = /figma\.com\/(?:file|design)\/([^\/]+)\/[^?]*\?.*node-id=([^&]+)/i;
316
+ const urlPattern = /figma\.com\/(?:file|design)\/([^/]+)\/[^?]*\?.*node-id=([^&]+)/i;
317
317
  const match = url.match(urlPattern);
318
318
 
319
319
  if (!match) {
@@ -410,7 +410,7 @@ export class FigmaClient {
410
410
  */
411
411
  parseFileUrl(url: string): { fileKey: string; nodeId?: string } {
412
412
  // Match both /file/ and /design/ paths
413
- const urlPattern = /figma\.com\/(?:file|design)\/([^\/]+)/i;
413
+ const urlPattern = /figma\.com\/(?:file|design)\/([^/]+)/i;
414
414
  const match = url.match(urlPattern);
415
415
 
416
416
  if (!match) {
@@ -200,12 +200,13 @@ export class MetricsStore {
200
200
  let key: string;
201
201
 
202
202
  switch (groupBy) {
203
- case "week":
203
+ case "week": {
204
204
  // Get ISO week
205
205
  const weekStart = new Date(date);
206
206
  weekStart.setDate(date.getDate() - date.getDay());
207
207
  key = weekStart.toISOString().split("T")[0];
208
208
  break;
209
+ }
209
210
  case "month":
210
211
  key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
211
212
  break;
@@ -189,13 +189,14 @@ function generateHunk(
189
189
 
190
190
  // The hunk format depends on the style type
191
191
  switch (styleType) {
192
- case "inline":
192
+ case "inline": {
193
193
  // React inline style: { backgroundColor: '#0051c2' }
194
194
  const camelProp = toCamelCase(cssProperty);
195
195
  lines.push(`@@ -1,1 +1,1 @@ inline style`);
196
196
  lines.push(`- ${camelProp}: '${currentValue}',`);
197
197
  lines.push(`+ ${camelProp}: 'var(${suggestedFix.tokenName})',`);
198
198
  break;
199
+ }
199
200
 
200
201
  case "styled-components":
201
202
  case "emotion":
@@ -176,11 +176,15 @@ function generateTestCaseXml(result: TestResult): string {
176
176
  * Escape special XML characters
177
177
  */
178
178
  function escapeXml(str: string): string {
179
- return str
179
+ const cleaned = Array.from(str).filter((ch) => {
180
+ const code = ch.charCodeAt(0);
181
+ return code === 0x09 || code === 0x0A || code === 0x0D || code >= 0x20;
182
+ }).join('');
183
+
184
+ return cleaned
180
185
  .replace(/&/g, '&amp;')
181
186
  .replace(/</g, '&lt;')
182
187
  .replace(/>/g, '&gt;')
183
188
  .replace(/"/g, '&quot;')
184
- .replace(/'/g, '&apos;')
185
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); // Remove invalid XML characters
189
+ .replace(/'/g, '&apos;');
186
190
  }