@fragments-sdk/cli 0.3.3 → 0.4.1

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 (82) hide show
  1. package/dist/bin.js +18 -13
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-PMGI7ATF.js → chunk-5CKYLCJH.js} +58 -2
  4. package/dist/chunk-5CKYLCJH.js.map +1 -0
  5. package/dist/{chunk-MUZ6CM66.js → chunk-5ZYEOHYK.js} +13 -11
  6. package/dist/chunk-5ZYEOHYK.js.map +1 -0
  7. package/dist/{chunk-XHNKNI6J.js → chunk-AW7MWOUH.js} +9 -1
  8. package/dist/chunk-AW7MWOUH.js.map +1 -0
  9. package/dist/{chunk-LY2CFFPY.js → chunk-G3M3MPQ6.js} +14 -3
  10. package/dist/chunk-G3M3MPQ6.js.map +1 -0
  11. package/dist/{chunk-3OTEW66K.js → chunk-J4SI5RIH.js} +4 -4
  12. package/dist/{chunk-BSCG3IP7.js → chunk-NOTYONHY.js} +2 -2
  13. package/dist/{chunk-D6VXWI45.js → chunk-ZFKGX3QK.js} +8 -6
  14. package/dist/chunk-ZFKGX3QK.js.map +1 -0
  15. package/dist/{core-DWKLGY4N.js → core-LNXDLXDP.js} +5 -3
  16. package/dist/{generate-3LBZANQ3.js → generate-OIXXHOWR.js} +4 -4
  17. package/dist/index.d.ts +16 -0
  18. package/dist/index.js +6 -6
  19. package/dist/{init-NKIUCYTG.js → init-EVPXIDW4.js} +4 -4
  20. package/dist/mcp-bin.js +3 -3
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-YVYD64GD.js +12 -0
  23. package/dist/{service-QSZMZJBJ.js → service-K52ORLCJ.js} +4 -4
  24. package/dist/{static-viewer-MIPGZ4Z7.js → static-viewer-JNQIHA4B.js} +4 -4
  25. package/dist/{test-ZCTR4LBB.js → test-USARUEFW.js} +9 -5
  26. package/dist/test-USARUEFW.js.map +1 -0
  27. package/dist/{tokens-5JQ5IOR2.js → tokens-C6YHBOQE.js} +5 -5
  28. package/dist/{viewer-D7QC4GM2.js → viewer-H7TVFT4E.js} +15 -15
  29. package/dist/{viewer-D7QC4GM2.js.map → viewer-H7TVFT4E.js.map} +1 -1
  30. package/package.json +2 -1
  31. package/src/bin.ts +7 -1
  32. package/src/build.ts +2 -0
  33. package/src/core/index.ts +4 -0
  34. package/src/core/parser.ts +102 -1
  35. package/src/core/schema.ts +11 -0
  36. package/src/core/storyAdapter.ts +1 -1
  37. package/src/core/storybook-csf.ts +11 -0
  38. package/src/core/types.ts +25 -1
  39. package/src/mcp/server.ts +1 -1
  40. package/src/migrate/bin.ts +7 -1
  41. package/src/migrate/report.ts +1 -1
  42. package/src/service/enhance/doc-extractor.ts +2 -2
  43. package/src/service/enhance/props-extractor.ts +1 -1
  44. package/src/service/enhance/storybook-parser.ts +1 -1
  45. package/src/service/figma.ts +2 -2
  46. package/src/service/metrics-store.ts +2 -1
  47. package/src/service/patch-generator.ts +2 -1
  48. package/src/service/report.ts +1 -1
  49. package/src/test/reporters/junit.ts +7 -3
  50. package/src/test/runner.ts +4 -4
  51. package/src/test/watch.ts +2 -2
  52. package/src/theme/__tests__/generator.test.ts +412 -0
  53. package/src/theme/__tests__/presets.test.ts +169 -0
  54. package/src/theme/__tests__/schema.test.ts +463 -0
  55. package/src/theme/__tests__/serializer.test.ts +326 -0
  56. package/src/theme/generator.ts +355 -0
  57. package/src/theme/index.ts +61 -0
  58. package/src/theme/presets.ts +189 -0
  59. package/src/theme/schema.ts +193 -0
  60. package/src/theme/serializer.ts +123 -0
  61. package/src/theme/types.ts +210 -0
  62. package/src/viewer/components/CodePanel.tsx +1 -1
  63. package/src/viewer/components/FigmaEmbed.tsx +1 -1
  64. package/src/viewer/jsx-parser.ts +2 -1
  65. package/src/viewer/styles/globals.css +1 -1
  66. package/src/viewer/utils/colorSchemes.ts +3 -3
  67. package/dist/chunk-D6VXWI45.js.map +0 -1
  68. package/dist/chunk-LY2CFFPY.js.map +0 -1
  69. package/dist/chunk-MUZ6CM66.js.map +0 -1
  70. package/dist/chunk-PMGI7ATF.js.map +0 -1
  71. package/dist/chunk-XHNKNI6J.js.map +0 -1
  72. package/dist/scan-3ZAOVO4U.js +0 -12
  73. package/dist/test-ZCTR4LBB.js.map +0 -1
  74. /package/dist/{chunk-3OTEW66K.js.map → chunk-J4SI5RIH.js.map} +0 -0
  75. /package/dist/{chunk-BSCG3IP7.js.map → chunk-NOTYONHY.js.map} +0 -0
  76. /package/dist/{core-DWKLGY4N.js.map → core-LNXDLXDP.js.map} +0 -0
  77. /package/dist/{generate-3LBZANQ3.js.map → generate-OIXXHOWR.js.map} +0 -0
  78. /package/dist/{init-NKIUCYTG.js.map → init-EVPXIDW4.js.map} +0 -0
  79. /package/dist/{scan-3ZAOVO4U.js.map → scan-YVYD64GD.js.map} +0 -0
  80. /package/dist/{service-QSZMZJBJ.js.map → service-K52ORLCJ.js.map} +0 -0
  81. /package/dist/{static-viewer-MIPGZ4Z7.js.map → static-viewer-JNQIHA4B.js.map} +0 -0
  82. /package/dist/{tokens-5JQ5IOR2.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.3.3",
3
+ "version": "0.4.1",
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"
package/src/bin.ts CHANGED
@@ -8,9 +8,15 @@
8
8
 
9
9
  import { Command } from 'commander';
10
10
  import pc from 'picocolors';
11
+ import { readFileSync } from 'node:fs';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { dirname, join } from 'node:path';
11
14
  import { BRAND } from './core/index.js';
12
15
  import { loadConfig } from './core/node.js';
13
16
 
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')) as { version: string };
19
+
14
20
  // Import command implementations
15
21
  import { validate } from './commands/validate.js';
16
22
  import { build } from './commands/build.js';
@@ -40,7 +46,7 @@ const program = new Command();
40
46
  program
41
47
  .name(BRAND.cliCommand)
42
48
  .description(`${BRAND.name} - Design system documentation and compliance tool`)
43
- .version('0.1.0');
49
+ .version(pkg.version);
44
50
 
45
51
  // ============================================================================
46
52
  // VALIDATE COMMAND
package/src/build.ts CHANGED
@@ -111,6 +111,8 @@ export async function buildSegments(
111
111
  ...(v.figma && { figma: v.figma }),
112
112
  ...(v.args && { args: v.args }),
113
113
  })),
114
+ // Include AI metadata if present
115
+ ...(parsed.ai && { ai: parsed.ai }),
114
116
  };
115
117
 
116
118
  segments[parsed.meta.name] = compiled;
package/src/core/index.ts CHANGED
@@ -32,6 +32,8 @@ export type {
32
32
  // Contract and provenance types
33
33
  SegmentContract,
34
34
  SegmentGenerated,
35
+ // AI metadata type
36
+ AIMetadata,
35
37
  // Screenshot types
36
38
  ScreenshotConfig,
37
39
  ServiceConfig,
@@ -87,6 +89,8 @@ export {
87
89
  segmentGeneratedSchema,
88
90
  segmentBanSchema,
89
91
  recipeDefinitionSchema,
92
+ // AI metadata schema
93
+ aiMetadataSchema,
90
94
  } from "./schema.js";
91
95
 
92
96
  // Main API
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import ts from "typescript";
11
- import type { SegmentMeta, SegmentUsage, PropDefinition } from "./types.js";
11
+ import type { SegmentMeta, SegmentUsage, PropDefinition, AIMetadata } from "./types.js";
12
12
 
13
13
  /**
14
14
  * Parsed segment metadata (extracted statically from AST)
@@ -45,6 +45,9 @@ export interface ParsedSegmentMetadata {
45
45
  note: string;
46
46
  }>;
47
47
 
48
+ /** AI-specific metadata for playground context generation */
49
+ ai?: AIMetadata;
50
+
48
51
  /** Parse warnings */
49
52
  warnings: string[];
50
53
  }
@@ -129,6 +132,9 @@ export function parseSegmentFile(
129
132
  // Extract relations
130
133
  const relations = extractRelations(arg, warnings);
131
134
 
135
+ // Extract AI metadata
136
+ const ai = extractAIMetadata(arg, warnings);
137
+
132
138
  return {
133
139
  componentImport,
134
140
  componentName,
@@ -137,6 +143,7 @@ export function parseSegmentFile(
137
143
  props,
138
144
  variants,
139
145
  relations,
146
+ ai,
140
147
  warnings,
141
148
  };
142
149
  }
@@ -401,6 +408,51 @@ function extractVariants(
401
408
  return variants;
402
409
  }
403
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
+
404
456
  /**
405
457
  * Extract the code from a render function.
406
458
  */
@@ -421,6 +473,9 @@ function extractRenderCode(
421
473
  code = code.slice(1, -1).trim();
422
474
  }
423
475
 
476
+ // Dedent the code to remove common leading whitespace
477
+ code = dedent(code);
478
+
424
479
  return code;
425
480
  }
426
481
 
@@ -460,6 +515,52 @@ function extractRelations(
460
515
  return relations;
461
516
  }
462
517
 
518
+ /**
519
+ * Extract AI metadata from defineSegment call.
520
+ */
521
+ function extractAIMetadata(
522
+ arg: ts.ObjectLiteralExpression,
523
+ warnings: string[]
524
+ ): AIMetadata | undefined {
525
+ const aiProp = findProperty(arg, "ai");
526
+ if (!aiProp || !ts.isObjectLiteralExpression(aiProp)) {
527
+ return undefined;
528
+ }
529
+
530
+ const ai: AIMetadata = {};
531
+
532
+ // Extract compositionPattern
533
+ const compositionPattern = extractStringProperty(aiProp, "compositionPattern");
534
+ if (compositionPattern && ['compound', 'simple', 'controlled'].includes(compositionPattern)) {
535
+ ai.compositionPattern = compositionPattern as AIMetadata['compositionPattern'];
536
+ }
537
+
538
+ // Extract subComponents array
539
+ const subComponents = extractStringArray(aiProp, "subComponents");
540
+ if (subComponents.length > 0) {
541
+ ai.subComponents = subComponents;
542
+ }
543
+
544
+ // Extract requiredChildren array
545
+ const requiredChildren = extractStringArray(aiProp, "requiredChildren");
546
+ if (requiredChildren.length > 0) {
547
+ ai.requiredChildren = requiredChildren;
548
+ }
549
+
550
+ // Extract commonPatterns array
551
+ const commonPatterns = extractStringArray(aiProp, "commonPatterns");
552
+ if (commonPatterns.length > 0) {
553
+ ai.commonPatterns = commonPatterns;
554
+ }
555
+
556
+ // Only return if we have any fields
557
+ if (Object.keys(ai).length > 0) {
558
+ return ai;
559
+ }
560
+
561
+ return undefined;
562
+ }
563
+
463
564
  /**
464
565
  * Extract a string property from an object literal.
465
566
  */
@@ -138,6 +138,16 @@ export const segmentGeneratedSchema = z.object({
138
138
  timestamp: z.string().datetime().optional(),
139
139
  });
140
140
 
141
+ /**
142
+ * Schema for AI-specific metadata for playground context generation
143
+ */
144
+ export const aiMetadataSchema = z.object({
145
+ compositionPattern: z.enum(['compound', 'simple', 'controlled']).optional(),
146
+ subComponents: z.array(z.string()).optional(),
147
+ requiredChildren: z.array(z.string()).optional(),
148
+ commonPatterns: z.array(z.string()).optional(),
149
+ });
150
+
141
151
  /**
142
152
  * Schema for recipe definitions
143
153
  */
@@ -158,6 +168,7 @@ export const segmentDefinitionSchema = z.object({
158
168
  relations: z.array(componentRelationSchema).optional(),
159
169
  variants: z.array(segmentVariantSchema), // Allow empty variants array
160
170
  contract: segmentContractSchema.optional(),
171
+ ai: aiMetadataSchema.optional(),
161
172
  _generated: segmentGeneratedSchema.optional(),
162
173
  });
163
174
 
@@ -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);
@@ -314,6 +314,24 @@ export interface SegmentGenerated {
314
314
  timestamp?: string;
315
315
  }
316
316
 
317
+ /**
318
+ * AI-specific metadata for playground context generation
319
+ * Provides hints for AI code generation about component composition
320
+ */
321
+ export interface AIMetadata {
322
+ /** How this component is composed with others */
323
+ compositionPattern?: "compound" | "simple" | "controlled";
324
+
325
+ /** Sub-component names (without parent prefix, e.g., "Header" not "Card.Header") */
326
+ subComponents?: string[];
327
+
328
+ /** Sub-components that must be present for valid composition */
329
+ requiredChildren?: string[];
330
+
331
+ /** Common usage patterns as JSX strings for AI reference */
332
+ commonPatterns?: string[];
333
+ }
334
+
317
335
  /**
318
336
  * Complete segment definition
319
337
  */
@@ -339,6 +357,9 @@ export interface SegmentDefinition<TProps = unknown> {
339
357
  /** Agent-optimized contract metadata */
340
358
  contract?: SegmentContract;
341
359
 
360
+ /** AI-specific metadata for playground context generation */
361
+ ai?: AIMetadata;
362
+
342
363
  /** Provenance tracking (for generated segments) */
343
364
  _generated?: SegmentGenerated;
344
365
  }
@@ -706,6 +727,9 @@ export interface CompiledSegment {
706
727
  /** Agent-optimized contract metadata */
707
728
  contract?: SegmentContract;
708
729
 
730
+ /** AI-specific metadata for playground context generation */
731
+ ai?: AIMetadata;
732
+
709
733
  /** Provenance tracking (for generated segments) */
710
734
  _generated?: SegmentGenerated;
711
735
  }
package/src/mcp/server.ts CHANGED
@@ -276,7 +276,7 @@ export function createMcpServer(config: McpServerConfig): Server {
276
276
  // Lazy-loaded resources
277
277
  let segmentsData: CompiledSegmentsFile | null = null;
278
278
  let packageName: string | null = null;
279
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- lazy-loaded from service
279
+
280
280
  let browserPool: any = null;
281
281
  let storageManager: any = null;
282
282
  let diffEngine: any = null;
@@ -7,15 +7,21 @@
7
7
 
8
8
  import { Command } from "commander";
9
9
  import pc from "picocolors";
10
+ import { readFileSync } from "node:fs";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, join } from "node:path";
10
13
  import { BRAND } from "../core/index.js";
11
14
  import { migrate } from "./migrate.js";
12
15
 
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8")) as { version: string };
18
+
13
19
  const program = new Command();
14
20
 
15
21
  program
16
22
  .name("segments-migrate")
17
23
  .description(`${BRAND.name} Storybook Migration Tool`)
18
- .version("0.0.1");
24
+ .version(pkg.version);
19
25
 
20
26
  program
21
27
  .command("migrate")
@@ -49,7 +49,7 @@ function getStyles(): string {
49
49
  --bg-warning: rgba(234, 179, 8, 0.1);
50
50
  --bg-error: rgba(239, 68, 68, 0.1);
51
51
  --border: #262626;
52
- --text: #fafafa;
52
+ --text: #f2f2f2;
53
53
  --text-secondary: #a1a1aa;
54
54
  --text-muted: #71717a;
55
55
  --accent: #8b5cf6;
@@ -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":
@@ -61,7 +61,7 @@ function getStyles(): string {
61
61
  --bg-card: #141414;
62
62
  --bg-hover: #1a1a1a;
63
63
  --border: #262626;
64
- --text: #fafafa;
64
+ --text: #f2f2f2;
65
65
  --text-secondary: #a1a1aa;
66
66
  --text-muted: #71717a;
67
67
  --accent: #10b981;
@@ -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
  }
@@ -20,11 +20,11 @@ import type {
20
20
  import { groupTestsByComponent } from './discovery.js';
21
21
 
22
22
  // Dynamic playwright types (since it's optional)
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+
24
24
  type Browser = any;
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+
26
26
  type BrowserContext = any;
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+
28
28
  type Page = any;
29
29
 
30
30
  /**
@@ -59,7 +59,7 @@ export async function runTests(
59
59
  }
60
60
 
61
61
  // Import Playwright dynamically (optional dependency)
62
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+
63
63
  let playwright: any;
64
64
  try {
65
65
  playwright = await import('playwright');
package/src/test/watch.ts CHANGED
@@ -34,7 +34,7 @@ export async function startWatchMode(
34
34
 
35
35
  let debounceTimer: NodeJS.Timeout | null = null;
36
36
  let isRunning = false;
37
- let pendingFiles = new Set<string>();
37
+ const pendingFiles = new Set<string>();
38
38
 
39
39
  // Get files to watch
40
40
  const segmentFiles = await discoverSegmentFiles(config, configDir);
@@ -190,7 +190,7 @@ export async function startInteractiveWatchMode(
190
190
  runnerOptions: RunnerOptions,
191
191
  reporters: TestReporter[]
192
192
  ): Promise<void> {
193
- let lastFailedTests: TestCase[] = [];
193
+ const lastFailedTests: TestCase[] = [];
194
194
 
195
195
  // Start basic watch mode
196
196
  await startWatchMode(config, configDir, runnerOptions, reporters, {