@fragments-sdk/cli 0.10.1 → 0.12.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 (223) hide show
  1. package/dist/ai-client-I6MDWNYA.js +21 -0
  2. package/dist/bin.js +292 -367
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-PW7QTQA6.js → chunk-4OC7FTJB.js} +2 -2
  5. package/dist/{chunk-HRFUSSZI.js → chunk-AM4MRTMN.js} +2 -2
  6. package/dist/{chunk-5G3VZH43.js → chunk-GVDSFQ4E.js} +281 -351
  7. package/dist/chunk-GVDSFQ4E.js.map +1 -0
  8. package/dist/chunk-JJ2VRTBU.js +626 -0
  9. package/dist/chunk-JJ2VRTBU.js.map +1 -0
  10. package/dist/{chunk-D5PYOXEI.js → chunk-LVWFOLUZ.js} +148 -13
  11. package/dist/{chunk-D5PYOXEI.js.map → chunk-LVWFOLUZ.js.map} +1 -1
  12. package/dist/{chunk-WXSR2II7.js → chunk-OQKMEFOS.js} +58 -6
  13. package/dist/chunk-OQKMEFOS.js.map +1 -0
  14. package/dist/chunk-SXTKFDCR.js +104 -0
  15. package/dist/chunk-SXTKFDCR.js.map +1 -0
  16. package/dist/chunk-T5OMVL7E.js +443 -0
  17. package/dist/chunk-T5OMVL7E.js.map +1 -0
  18. package/dist/{chunk-ZM4ZQZWZ.js → chunk-TPWGL2XS.js} +39 -37
  19. package/dist/chunk-TPWGL2XS.js.map +1 -0
  20. package/dist/{chunk-OQO55NKV.js → chunk-WFS63PCW.js} +85 -11
  21. package/dist/chunk-WFS63PCW.js.map +1 -0
  22. package/dist/core/index.js +9 -1
  23. package/dist/{discovery-NEOY4MPN.js → discovery-ZJQSXF56.js} +3 -3
  24. package/dist/{generate-FBHSXR3D.js → generate-RJFS2JWA.js} +4 -4
  25. package/dist/index.js +7 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/init-ZSX3NRCZ.js +636 -0
  28. package/dist/init-ZSX3NRCZ.js.map +1 -0
  29. package/dist/mcp-bin.js +2 -2
  30. package/dist/{scan-CJF2DOQW.js → scan-3PMCJ4RB.js} +6 -6
  31. package/dist/scan-generate-SYU4PYZD.js +1115 -0
  32. package/dist/scan-generate-SYU4PYZD.js.map +1 -0
  33. package/dist/{service-TQYWY65E.js → service-VMGNJZ42.js} +3 -3
  34. package/dist/snapshot-XOISO2IS.js +139 -0
  35. package/dist/snapshot-XOISO2IS.js.map +1 -0
  36. package/dist/{static-viewer-NUBFPKWH.js → static-viewer-5GXH2MGE.js} +3 -3
  37. package/dist/static-viewer-5GXH2MGE.js.map +1 -0
  38. package/dist/{test-Z5LVO724.js → test-SI4NSHQX.js} +4 -4
  39. package/dist/{tokens-CE46OTMD.js → tokens-T6SIVUT5.js} +5 -5
  40. package/dist/{viewer-DNMNC5VS.js → viewer-7ZEAFBVN.js} +80 -58
  41. package/dist/viewer-7ZEAFBVN.js.map +1 -0
  42. package/package.json +6 -14
  43. package/src/ai-client.ts +156 -0
  44. package/src/bin.ts +74 -2
  45. package/src/build.ts +95 -33
  46. package/src/commands/__tests__/drift-sync.test.ts +252 -0
  47. package/src/commands/__tests__/scan-generate.test.ts +497 -45
  48. package/src/commands/enhance.ts +11 -35
  49. package/src/commands/init.ts +296 -193
  50. package/src/commands/scan-generate.ts +740 -139
  51. package/src/commands/scan.ts +37 -32
  52. package/src/commands/setup.ts +143 -52
  53. package/src/commands/snapshot.ts +197 -0
  54. package/src/commands/sync.ts +357 -0
  55. package/src/commands/validate.ts +43 -1
  56. package/src/core/component-extractor.test.ts +282 -0
  57. package/src/core/component-extractor.ts +1030 -0
  58. package/src/core/discovery.ts +93 -7
  59. package/src/service/enhance/props-extractor.ts +235 -13
  60. package/src/validators.ts +236 -0
  61. package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
  62. package/src/viewer/server.ts +37 -22
  63. package/src/viewer/vite-plugin.ts +25 -9
  64. package/dist/chunk-5G3VZH43.js.map +0 -1
  65. package/dist/chunk-OQO55NKV.js.map +0 -1
  66. package/dist/chunk-WXSR2II7.js.map +0 -1
  67. package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
  68. package/dist/init-NDQXUWDU.js +0 -796
  69. package/dist/init-NDQXUWDU.js.map +0 -1
  70. package/dist/scan-generate-SJAN5MVI.js +0 -691
  71. package/dist/scan-generate-SJAN5MVI.js.map +0 -1
  72. package/dist/viewer-DNMNC5VS.js.map +0 -1
  73. package/src/ai.ts +0 -266
  74. package/src/commands/init-framework.ts +0 -414
  75. package/src/mcp/bin.ts +0 -36
  76. package/src/migrate/bin.ts +0 -114
  77. package/src/theme/index.ts +0 -77
  78. package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
  79. package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
  80. package/src/viewer/__tests__/render-utils.test.ts +0 -232
  81. package/src/viewer/__tests__/style-utils.test.ts +0 -404
  82. package/src/viewer/assets/fragments-logo.ts +0 -4
  83. package/src/viewer/assets/fragments_logo.png +0 -0
  84. package/src/viewer/bin.ts +0 -86
  85. package/src/viewer/cli/health.ts +0 -256
  86. package/src/viewer/cli/index.ts +0 -33
  87. package/src/viewer/cli/scan.ts +0 -124
  88. package/src/viewer/cli/utils.ts +0 -174
  89. package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
  90. package/src/viewer/components/ActionCapture.tsx +0 -172
  91. package/src/viewer/components/ActionsPanel.tsx +0 -332
  92. package/src/viewer/components/AllVariantsPreview.tsx +0 -78
  93. package/src/viewer/components/App.tsx +0 -582
  94. package/src/viewer/components/BottomPanel.tsx +0 -288
  95. package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
  96. package/src/viewer/components/CodePanel.tsx +0 -118
  97. package/src/viewer/components/CommandPalette.tsx +0 -392
  98. package/src/viewer/components/ComponentDocView.tsx +0 -164
  99. package/src/viewer/components/ComponentGraph.tsx +0 -380
  100. package/src/viewer/components/ComponentHeader.tsx +0 -88
  101. package/src/viewer/components/ContractPanel.tsx +0 -241
  102. package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
  103. package/src/viewer/components/ErrorBoundary.tsx +0 -97
  104. package/src/viewer/components/FigmaEmbed.tsx +0 -238
  105. package/src/viewer/components/FragmentEditor.tsx +0 -525
  106. package/src/viewer/components/FragmentRenderer.tsx +0 -61
  107. package/src/viewer/components/HeaderSearch.tsx +0 -24
  108. package/src/viewer/components/HealthDashboard.tsx +0 -441
  109. package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
  110. package/src/viewer/components/Icons.tsx +0 -479
  111. package/src/viewer/components/InteractionsPanel.tsx +0 -757
  112. package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
  113. package/src/viewer/components/IsolatedRender.tsx +0 -113
  114. package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
  115. package/src/viewer/components/LandingPage.tsx +0 -421
  116. package/src/viewer/components/Layout.tsx +0 -27
  117. package/src/viewer/components/LeftSidebar.tsx +0 -472
  118. package/src/viewer/components/LoadErrorMessage.tsx +0 -102
  119. package/src/viewer/components/MultiViewportPreview.tsx +0 -522
  120. package/src/viewer/components/NoVariantsMessage.tsx +0 -59
  121. package/src/viewer/components/PanelShell.tsx +0 -161
  122. package/src/viewer/components/PerformancePanel.tsx +0 -304
  123. package/src/viewer/components/PreviewArea.tsx +0 -472
  124. package/src/viewer/components/PreviewAside.tsx +0 -168
  125. package/src/viewer/components/PreviewFrameHost.tsx +0 -303
  126. package/src/viewer/components/PreviewPane.tsx +0 -149
  127. package/src/viewer/components/PreviewToolbar.tsx +0 -80
  128. package/src/viewer/components/PropsEditor.tsx +0 -506
  129. package/src/viewer/components/PropsTable.tsx +0 -111
  130. package/src/viewer/components/RelationsSection.tsx +0 -88
  131. package/src/viewer/components/ResizablePanel.tsx +0 -271
  132. package/src/viewer/components/RightSidebar.tsx +0 -102
  133. package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
  134. package/src/viewer/components/ScreenshotButton.tsx +0 -90
  135. package/src/viewer/components/Sidebar.tsx +0 -169
  136. package/src/viewer/components/SkeletonLoader.tsx +0 -161
  137. package/src/viewer/components/ThemeProvider.tsx +0 -42
  138. package/src/viewer/components/Toast.tsx +0 -3
  139. package/src/viewer/components/TokenStylePanel.tsx +0 -699
  140. package/src/viewer/components/TopToolbar.tsx +0 -159
  141. package/src/viewer/components/UsageSection.tsx +0 -95
  142. package/src/viewer/components/VariantMatrix.tsx +0 -388
  143. package/src/viewer/components/VariantRenderer.tsx +0 -131
  144. package/src/viewer/components/VariantTabs.tsx +0 -40
  145. package/src/viewer/components/ViewerHeader.tsx +0 -69
  146. package/src/viewer/components/ViewerStateSync.tsx +0 -52
  147. package/src/viewer/components/ViewportSelector.tsx +0 -172
  148. package/src/viewer/components/WebMCPDevTools.tsx +0 -503
  149. package/src/viewer/components/WebMCPIntegration.tsx +0 -47
  150. package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
  151. package/src/viewer/components/_future/CreatePage.tsx +0 -836
  152. package/src/viewer/components/viewer-utils.ts +0 -16
  153. package/src/viewer/composition-renderer.ts +0 -381
  154. package/src/viewer/constants/index.ts +0 -1
  155. package/src/viewer/constants/ui.ts +0 -166
  156. package/src/viewer/entry.tsx +0 -335
  157. package/src/viewer/hooks/index.ts +0 -2
  158. package/src/viewer/hooks/useA11yCache.ts +0 -383
  159. package/src/viewer/hooks/useA11yService.ts +0 -364
  160. package/src/viewer/hooks/useActions.ts +0 -138
  161. package/src/viewer/hooks/useAppState.ts +0 -147
  162. package/src/viewer/hooks/useCompiledFragments.ts +0 -42
  163. package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
  164. package/src/viewer/hooks/useHmrStatus.ts +0 -109
  165. package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
  166. package/src/viewer/hooks/usePreviewBridge.ts +0 -347
  167. package/src/viewer/hooks/useScrollSpy.ts +0 -78
  168. package/src/viewer/hooks/useUrlState.ts +0 -318
  169. package/src/viewer/hooks/useViewSettings.ts +0 -111
  170. package/src/viewer/index.html +0 -28
  171. package/src/viewer/intelligence/healthReport.ts +0 -505
  172. package/src/viewer/intelligence/styleDrift.ts +0 -340
  173. package/src/viewer/intelligence/usageScanner.ts +0 -309
  174. package/src/viewer/jsx-parser.ts +0 -486
  175. package/src/viewer/preview-frame-entry.tsx +0 -25
  176. package/src/viewer/preview-frame.html +0 -125
  177. package/src/viewer/public/favicon.ico +0 -0
  178. package/src/viewer/render-template.html +0 -68
  179. package/src/viewer/styles/globals.css +0 -278
  180. package/src/viewer/types/a11y.ts +0 -197
  181. package/src/viewer/utils/a11y-fixes.ts +0 -509
  182. package/src/viewer/utils/actionExport.ts +0 -372
  183. package/src/viewer/utils/colorSchemes.ts +0 -201
  184. package/src/viewer/utils/detectRelationships.ts +0 -256
  185. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
  186. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
  187. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
  188. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
  189. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
  190. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
  191. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
  192. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
  193. package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
  194. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
  195. package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
  196. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
  197. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
  198. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -137
  199. package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
  200. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
  201. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
  202. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
  203. package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
  204. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
  205. package/src/viewer/vendor/shared/src/index.ts +0 -34
  206. package/src/viewer/vendor/shared/src/types.ts +0 -53
  207. package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
  208. package/src/viewer/webmcp/analytics.ts +0 -165
  209. package/src/viewer/webmcp/index.ts +0 -3
  210. package/src/viewer/webmcp/posthog-bridge.ts +0 -39
  211. package/src/viewer/webmcp/runtime-tools.ts +0 -152
  212. package/src/viewer/webmcp/scan-utils.ts +0 -135
  213. package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
  214. package/src/viewer/webmcp/viewer-state.ts +0 -45
  215. /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
  216. /package/dist/{chunk-PW7QTQA6.js.map → chunk-4OC7FTJB.js.map} +0 -0
  217. /package/dist/{chunk-HRFUSSZI.js.map → chunk-AM4MRTMN.js.map} +0 -0
  218. /package/dist/{scan-CJF2DOQW.js.map → discovery-ZJQSXF56.js.map} +0 -0
  219. /package/dist/{generate-FBHSXR3D.js.map → generate-RJFS2JWA.js.map} +0 -0
  220. /package/dist/{service-TQYWY65E.js.map → scan-3PMCJ4RB.js.map} +0 -0
  221. /package/dist/{static-viewer-NUBFPKWH.js.map → service-VMGNJZ42.js.map} +0 -0
  222. /package/dist/{test-Z5LVO724.js.map → test-SI4NSHQX.js.map} +0 -0
  223. /package/dist/{tokens-CE46OTMD.js.map → tokens-T6SIVUT5.js.map} +0 -0
@@ -56,6 +56,8 @@ export interface ScanOptions {
56
56
  skipStorybook?: boolean;
57
57
  /** Verbose output */
58
58
  verbose?: boolean;
59
+ /** Suppress all console output (for use as a sub-step) */
60
+ quiet?: boolean;
59
61
  }
60
62
 
61
63
  export interface ScanResult {
@@ -78,6 +80,9 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
78
80
  const errors: Array<{ component: string; error: string }> = [];
79
81
  const warnings: Array<{ component: string; warning: string }> = [];
80
82
 
83
+ // In quiet mode, suppress all console output
84
+ const log = options.quiet ? (() => {}) : console.log.bind(console);
85
+
81
86
  // Load config or use defaults
82
87
  let configDir: string;
83
88
  let outputFile: string;
@@ -95,11 +100,11 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
95
100
  componentPatterns = options.componentPatterns;
96
101
  }
97
102
 
98
- console.log(pc.cyan(`\n${BRAND.name} Scan\n`));
99
- console.log(pc.dim("Zero-config fragments.json generation from source code\n"));
103
+ log(pc.cyan(`\n${BRAND.name} Scan\n`));
104
+ log(pc.dim("Zero-config fragments.json generation from source code\n"));
100
105
 
101
106
  // Phase 1: Discover components
102
- console.log(pc.dim("Phase 1: Discovering components..."));
107
+ log(pc.dim("Phase 1: Discovering components..."));
103
108
  const components = await discoverAllComponents(configDir, {
104
109
  patterns: componentPatterns,
105
110
  exclude: ["**/*.test.*", "**/*.spec.*", "**/__tests__/**"],
@@ -107,7 +112,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
107
112
  });
108
113
 
109
114
  if (components.length === 0) {
110
- console.log(pc.yellow("No components found. Check your patterns or config."));
115
+ log(pc.yellow("No components found. Check your patterns or config."));
111
116
  return {
112
117
  success: false,
113
118
  outputPath: resolve(configDir, outputFile),
@@ -121,18 +126,18 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
121
126
  };
122
127
  }
123
128
 
124
- console.log(pc.green(` Found ${components.length} components`));
129
+ log(pc.green(` Found ${components.length} components`));
125
130
  if (options.verbose) {
126
131
  for (const comp of components.slice(0, 10)) {
127
- console.log(pc.dim(` - ${comp.name}: ${comp.relativePath}`));
132
+ log(pc.dim(` - ${comp.name}: ${comp.relativePath}`));
128
133
  }
129
134
  if (components.length > 10) {
130
- console.log(pc.dim(` ... and ${components.length - 10} more`));
135
+ log(pc.dim(` ... and ${components.length - 10} more`));
131
136
  }
132
137
  }
133
138
 
134
139
  // Phase 2: Extract props from TypeScript
135
- console.log(pc.dim("\nPhase 2: Extracting props from TypeScript..."));
140
+ log(pc.dim("\nPhase 2: Extracting props from TypeScript..."));
136
141
  const propsMap = new Map<string, ReturnType<typeof convertToFragmentProps>>();
137
142
  const propsResults = new Map<string, PropsExtractionResult>();
138
143
  let propsExtracted = 0;
@@ -159,7 +164,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
159
164
  }
160
165
  }
161
166
 
162
- console.log(pc.green(` Extracted props for ${propsExtracted} components`));
167
+ log(pc.green(` Extracted props for ${propsExtracted} components`));
163
168
 
164
169
  // Phase 3: Scan for usage patterns
165
170
  let usageAnalysis: UsageAnalysis | undefined;
@@ -167,7 +172,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
167
172
  let allRelations = new Map<string, ComponentRelation[]>();
168
173
 
169
174
  if (!options.skipUsage) {
170
- console.log(pc.dim("\nPhase 3: Scanning for usage patterns..."));
175
+ log(pc.dim("\nPhase 3: Scanning for usage patterns..."));
171
176
  const usageDir = options.usageDir || configDir;
172
177
 
173
178
  try {
@@ -196,17 +201,17 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
196
201
 
197
202
  // Infer relations
198
203
  allRelations = inferAllRelations(usageAnalysis);
199
- console.log(pc.green(` Found ${usagesFound} usages across ${usageAnalysis.totalFiles} files`));
200
- console.log(pc.green(` Inferred relations for ${allRelations.size} components`));
204
+ log(pc.green(` Found ${usagesFound} usages across ${usageAnalysis.totalFiles} files`));
205
+ log(pc.green(` Inferred relations for ${allRelations.size} components`));
201
206
  } catch (e) {
202
207
  warnings.push({
203
208
  component: "*",
204
209
  warning: `Usage scanning failed: ${e instanceof Error ? e.message : String(e)}`,
205
210
  });
206
- console.log(pc.yellow(` Usage scanning failed: ${e instanceof Error ? e.message : "unknown error"}`));
211
+ log(pc.yellow(` Usage scanning failed: ${e instanceof Error ? e.message : "unknown error"}`));
207
212
  }
208
213
  } else {
209
- console.log(pc.dim("\nPhase 3: Skipping usage analysis"));
214
+ log(pc.dim("\nPhase 3: Skipping usage analysis"));
210
215
  }
211
216
 
212
217
  // Phase 4: Parse Storybook stories
@@ -214,7 +219,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
214
219
  let storiesParsed = 0;
215
220
 
216
221
  if (!options.skipStorybook) {
217
- console.log(pc.dim("\nPhase 4: Parsing Storybook stories..."));
222
+ log(pc.dim("\nPhase 4: Parsing Storybook stories..."));
218
223
 
219
224
  try {
220
225
  const allStories = await parseAllStories(configDir);
@@ -226,20 +231,20 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
226
231
  }
227
232
  }
228
233
 
229
- console.log(pc.green(` Parsed stories for ${storiesParsed} components`));
234
+ log(pc.green(` Parsed stories for ${storiesParsed} components`));
230
235
  } catch (e) {
231
236
  warnings.push({
232
237
  component: "*",
233
238
  warning: `Storybook parsing failed: ${e instanceof Error ? e.message : String(e)}`,
234
239
  });
235
- console.log(pc.yellow(` Storybook parsing failed: ${e instanceof Error ? e.message : "unknown error"}`));
240
+ log(pc.yellow(` Storybook parsing failed: ${e instanceof Error ? e.message : "unknown error"}`));
236
241
  }
237
242
  } else {
238
- console.log(pc.dim("\nPhase 4: Skipping Storybook parsing"));
243
+ log(pc.dim("\nPhase 4: Skipping Storybook parsing"));
239
244
  }
240
245
 
241
246
  // Phase 5: Generate fragments
242
- console.log(pc.dim("\nPhase 5: Generating fragments..."));
247
+ log(pc.dim("\nPhase 5: Generating fragments..."));
243
248
  const fragments: Record<string, CompiledFragment> = {};
244
249
 
245
250
  for (const comp of components) {
@@ -277,32 +282,32 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
277
282
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
278
283
 
279
284
  // Summary
280
- console.log(pc.dim("\n────────────────────────────────────────"));
281
- console.log(pc.green(`\n✓ Generated fragments.json in ${elapsed}s`));
282
- console.log(pc.dim(` Output: ${relative(process.cwd(), outputPath)}`));
283
- console.log(pc.dim(` Components: ${Object.keys(fragments).length}`));
284
- console.log(pc.dim(` Props extracted: ${propsExtracted}`));
285
- console.log(pc.dim(` Usages found: ${usagesFound}`));
286
- console.log(pc.dim(` Relations inferred: ${allRelations.size}`));
287
- console.log(pc.dim(` Stories parsed: ${storiesParsed}`));
285
+ log(pc.dim("\n────────────────────────────────────────"));
286
+ log(pc.green(`\n✓ Generated fragments.json in ${elapsed}s`));
287
+ log(pc.dim(` Output: ${relative(process.cwd(), outputPath)}`));
288
+ log(pc.dim(` Components: ${Object.keys(fragments).length}`));
289
+ log(pc.dim(` Props extracted: ${propsExtracted}`));
290
+ log(pc.dim(` Usages found: ${usagesFound}`));
291
+ log(pc.dim(` Relations inferred: ${allRelations.size}`));
292
+ log(pc.dim(` Stories parsed: ${storiesParsed}`));
288
293
 
289
294
  if (warnings.length > 0) {
290
- console.log(pc.yellow(`\n ${warnings.length} warning(s)`));
295
+ log(pc.yellow(`\n ${warnings.length} warning(s)`));
291
296
  if (options.verbose) {
292
297
  for (const w of warnings) {
293
- console.log(pc.dim(` ${w.component}: ${w.warning}`));
298
+ log(pc.dim(` ${w.component}: ${w.warning}`));
294
299
  }
295
300
  }
296
301
  }
297
302
 
298
303
  if (errors.length > 0) {
299
- console.log(pc.red(`\n ${errors.length} error(s)`));
304
+ log(pc.red(`\n ${errors.length} error(s)`));
300
305
  for (const e of errors) {
301
- console.log(pc.dim(` ${e.component}: ${e.error}`));
306
+ log(pc.dim(` ${e.component}: ${e.error}`));
302
307
  }
303
308
  }
304
309
 
305
- console.log();
310
+ log();
306
311
 
307
312
  return {
308
313
  success: errors.length === 0,
@@ -36,13 +36,13 @@ export interface SetupResult {
36
36
  errors: string[];
37
37
  }
38
38
 
39
- type Framework = 'nextjs-app' | 'nextjs-pages' | 'vite' | 'unknown';
39
+ export type Framework = 'nextjs-app' | 'nextjs-pages' | 'vite' | 'remix' | 'astro' | 'unknown';
40
40
 
41
41
  // ============================================
42
42
  // Detection
43
43
  // ============================================
44
44
 
45
- async function fileExists(path: string): Promise<boolean> {
45
+ export async function fileExists(path: string): Promise<boolean> {
46
46
  try {
47
47
  await access(path);
48
48
  return true;
@@ -51,7 +51,7 @@ async function fileExists(path: string): Promise<boolean> {
51
51
  }
52
52
  }
53
53
 
54
- async function detectFramework(root: string): Promise<Framework> {
54
+ export async function detectSetupFramework(root: string): Promise<Framework> {
55
55
  // Next.js App Router
56
56
  if (
57
57
  await fileExists(join(root, 'app/layout.tsx')) ||
@@ -77,7 +77,23 @@ async function detectFramework(root: string): Promise<Framework> {
77
77
  return 'nextjs-app';
78
78
  }
79
79
 
80
- // Vite
80
+ // Remix
81
+ if (
82
+ await fileExists(join(root, 'app/root.tsx')) ||
83
+ await fileExists(join(root, 'app/root.ts'))
84
+ ) {
85
+ return 'remix';
86
+ }
87
+
88
+ // Astro
89
+ if (
90
+ await fileExists(join(root, 'astro.config.mjs')) ||
91
+ await fileExists(join(root, 'astro.config.ts'))
92
+ ) {
93
+ return 'astro';
94
+ }
95
+
96
+ // Vite (check after Remix/Astro since they also use Vite under the hood)
81
97
  if (
82
98
  await fileExists(join(root, 'vite.config.ts')) ||
83
99
  await fileExists(join(root, 'vite.config.js'))
@@ -85,10 +101,25 @@ async function detectFramework(root: string): Promise<Framework> {
85
101
  return 'vite';
86
102
  }
87
103
 
104
+ // Fallback: check package.json for framework deps
105
+ try {
106
+ const pkgPath = join(root, 'package.json');
107
+ const pkgContent = await readFile(pkgPath, 'utf-8');
108
+ const pkg = JSON.parse(pkgContent);
109
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
110
+
111
+ if (allDeps['next']) return 'nextjs-app';
112
+ if (allDeps['@remix-run/react']) return 'remix';
113
+ if (allDeps['astro']) return 'astro';
114
+ if (allDeps['vite']) return 'vite';
115
+ } catch {
116
+ // No package.json or parse error
117
+ }
118
+
88
119
  return 'unknown';
89
120
  }
90
121
 
91
- async function findEntryFile(root: string, framework: Framework): Promise<string | null> {
122
+ export async function findEntryFile(root: string, framework: Framework): Promise<string | null> {
92
123
  const candidates: string[] = [];
93
124
 
94
125
  switch (framework) {
@@ -101,6 +132,16 @@ async function findEntryFile(root: string, framework: Framework): Promise<string
101
132
  case 'nextjs-pages':
102
133
  candidates.push('pages/_app.tsx', 'pages/_app.ts');
103
134
  break;
135
+ case 'remix':
136
+ candidates.push('app/root.tsx', 'app/root.ts');
137
+ break;
138
+ case 'astro':
139
+ candidates.push(
140
+ 'src/layouts/Layout.astro',
141
+ 'src/layouts/BaseLayout.astro',
142
+ 'src/pages/index.astro'
143
+ );
144
+ break;
104
145
  case 'vite':
105
146
  candidates.push(
106
147
  'src/main.tsx', 'src/main.ts',
@@ -138,7 +179,7 @@ async function findNextConfig(root: string): Promise<string | null> {
138
179
  // Actions
139
180
  // ============================================
140
181
 
141
- async function addStylesImport(root: string, entryFile: string): Promise<{ modified: boolean; message: string }> {
182
+ export async function addStylesImport(root: string, entryFile: string): Promise<{ modified: boolean; message: string }> {
142
183
  const fullPath = join(root, entryFile);
143
184
  const content = await readFile(fullPath, 'utf-8');
144
185
 
@@ -146,22 +187,37 @@ async function addStylesImport(root: string, entryFile: string): Promise<{ modif
146
187
  return { modified: false, message: `Styles already imported in ${entryFile}` };
147
188
  }
148
189
 
149
- // Add import at the top of the file, after any 'use client' directive
150
190
  const stylesImport = "import '@fragments-sdk/ui/styles';";
151
191
  let newContent: string;
152
192
 
153
- if (content.startsWith("'use client'") || content.startsWith('"use client"')) {
154
- const directiveEnd = content.indexOf('\n') + 1;
155
- newContent = content.slice(0, directiveEnd) + stylesImport + '\n' + content.slice(directiveEnd);
193
+ // Astro files: insert inside frontmatter block (between --- fences)
194
+ if (entryFile.endsWith('.astro')) {
195
+ const fenceStart = content.indexOf('---');
196
+ if (fenceStart !== -1) {
197
+ const insertPos = fenceStart + 4; // after "---\n"
198
+ newContent = content.slice(0, insertPos) + stylesImport + '\n' + content.slice(insertPos);
199
+ } else {
200
+ // No frontmatter — add one
201
+ newContent = `---\n${stylesImport}\n---\n${content}`;
202
+ }
156
203
  } else {
157
- newContent = stylesImport + '\n' + content;
204
+ const useClientMatch = content.match(/^(?:\uFEFF)?[ \t]*['"]use client['"]\s*;?[ \t]*$/m);
205
+
206
+ if (useClientMatch && useClientMatch.index != null) {
207
+ const directiveLineEnd = content.indexOf('\n', useClientMatch.index);
208
+ const directiveEnd = directiveLineEnd === -1 ? content.length : directiveLineEnd + 1;
209
+ const separator = directiveLineEnd === -1 ? '\n' : '';
210
+ newContent = content.slice(0, directiveEnd) + separator + stylesImport + '\n' + content.slice(directiveEnd);
211
+ } else {
212
+ newContent = stylesImport + '\n' + content;
213
+ }
158
214
  }
159
215
 
160
216
  await writeFile(fullPath, newContent, 'utf-8');
161
217
  return { modified: true, message: `Added styles import to ${entryFile}` };
162
218
  }
163
219
 
164
- async function addThemeProvider(root: string, entryFile: string, framework: Framework): Promise<{ modified: boolean; message: string }> {
220
+ export async function addThemeProvider(root: string, entryFile: string, framework: Framework): Promise<{ modified: boolean; message: string }> {
165
221
  const fullPath = join(root, entryFile);
166
222
  const content = await readFile(fullPath, 'utf-8');
167
223
 
@@ -169,49 +225,81 @@ async function addThemeProvider(root: string, entryFile: string, framework: Fram
169
225
  return { modified: false, message: `ThemeProvider already present in ${entryFile}` };
170
226
  }
171
227
 
172
- // For Next.js App Router, add import and wrap children
173
- if (framework === 'nextjs-app') {
174
- // Add import
175
- const providerImport = "import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';";
228
+ // Astro uses .astro files can't inject React imports
229
+ if (framework === 'astro') {
230
+ return { modified: false, message: 'Add ThemeProvider in your React island — see https://usefragments.com/getting-started#astro' };
231
+ }
176
232
 
177
- let newContent = content;
233
+ // Add provider import after the last import line
234
+ const providerImport = "import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';";
178
235
 
179
- // Find the last import line to add our import after it
180
- const importLines = content.split('\n');
181
- let lastImportIdx = -1;
182
- for (let i = 0; i < importLines.length; i++) {
183
- if (importLines[i].startsWith('import ') || importLines[i].startsWith("import '") || importLines[i].startsWith('import "')) {
184
- lastImportIdx = i;
185
- }
186
- }
236
+ let newContent = content;
237
+ const lines = content.split('\n');
187
238
 
188
- if (lastImportIdx >= 0) {
189
- importLines.splice(lastImportIdx + 1, 0, providerImport);
190
- newContent = importLines.join('\n');
191
- } else {
192
- newContent = providerImport + '\n' + content;
239
+ // Prefer placing right after the @fragments-sdk/ui/styles import if it exists
240
+ let insertIdx = -1;
241
+ for (let i = 0; i < lines.length; i++) {
242
+ if (lines[i].includes('@fragments-sdk/ui/styles')) {
243
+ insertIdx = i;
244
+ break;
193
245
  }
246
+ }
194
247
 
195
- // Add suppressHydrationWarning hint and provider wrapping in a comment
196
- // We can't safely auto-wrap JSX, so we add a guide comment instead
197
- if (!content.includes('suppressHydrationWarning')) {
198
- await writeFile(fullPath, newContent, 'utf-8');
199
- return {
200
- modified: true,
201
- message: `Added provider imports to ${entryFile}. Wrap your {children} with:\n` +
202
- ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>{children}</ToastProvider></TooltipProvider></ThemeProvider>\n` +
203
- ` Add suppressHydrationWarning to your <html> tag`,
204
- };
248
+ // Otherwise, place after the last import line
249
+ if (insertIdx === -1) {
250
+ for (let i = 0; i < lines.length; i++) {
251
+ if (lines[i].startsWith('import ') || lines[i].startsWith("import '") || lines[i].startsWith('import "')) {
252
+ insertIdx = i;
253
+ }
205
254
  }
255
+ }
206
256
 
207
- await writeFile(fullPath, newContent, 'utf-8');
208
- return { modified: true, message: `Added provider imports to ${entryFile}. Wrap {children} with ThemeProvider.` };
257
+ if (insertIdx >= 0) {
258
+ lines.splice(insertIdx + 1, 0, providerImport);
259
+ newContent = lines.join('\n');
260
+ } else {
261
+ newContent = providerImport + '\n' + content;
209
262
  }
210
263
 
211
- return { modified: false, message: 'Manual ThemeProvider setup needed — see https://usefragments.com/getting-started#provider-setup' };
264
+ await writeFile(fullPath, newContent, 'utf-8');
265
+
266
+ // Framework-specific wrap instructions
267
+ if (framework === 'nextjs-app') {
268
+ const hint = !content.includes('suppressHydrationWarning')
269
+ ? `\n Add suppressHydrationWarning to your <html> tag`
270
+ : '';
271
+ return {
272
+ modified: true,
273
+ message: `Added provider imports to ${entryFile}. Wrap {children} with:\n` +
274
+ ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>{children}</ToastProvider></TooltipProvider></ThemeProvider>${hint}`,
275
+ };
276
+ }
277
+
278
+ if (framework === 'nextjs-pages') {
279
+ return {
280
+ modified: true,
281
+ message: `Added provider imports to ${entryFile}. Wrap <Component {...pageProps} /> with:\n` +
282
+ ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>...</ToastProvider></TooltipProvider></ThemeProvider>`,
283
+ };
284
+ }
285
+
286
+ if (framework === 'remix') {
287
+ return {
288
+ modified: true,
289
+ message: `Added provider imports to ${entryFile}. Wrap <Outlet /> with:\n` +
290
+ ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>...</ToastProvider></TooltipProvider></ThemeProvider>`,
291
+ };
292
+ }
293
+
294
+ // Vite and unknown — generic instruction
295
+ return {
296
+ modified: true,
297
+ message: `Added provider imports to ${entryFile}. Wrap your app root with:\n` +
298
+ ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>...</ToastProvider></TooltipProvider></ThemeProvider>`,
299
+ };
212
300
  }
213
301
 
214
- async function addTranspilePackages(root: string): Promise<{ modified: boolean; message: string }> {
302
+ export async function addTranspilePackages(root: string): Promise<{ modified: boolean; message: string }> {
215
303
  const configFile = await findNextConfig(root);
216
304
  if (!configFile) {
217
305
  return { modified: false, message: 'No next.config found' };
@@ -235,8 +323,8 @@ async function addTranspilePackages(root: string): Promise<{ modified: boolean;
235
323
  // Add transpilePackages to the config
236
324
  // Try to find the config object and add the property
237
325
  const patterns = [
238
- // const nextConfig = { ... }
239
- { search: /const\s+\w+\s*=\s*\{/, replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],` },
326
+ // const nextConfig: NextConfig = { ... } (with optional type annotation)
327
+ { search: /const\s+\w+\s*(?::\s*\w+)?\s*=\s*\{/, replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],` },
240
328
  // module.exports = { ... }
241
329
  { search: /module\.exports\s*=\s*\{/, replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],` },
242
330
  // export default { ... }
@@ -380,12 +468,15 @@ export async function setup(options: SetupOptions = {}): Promise<SetupResult> {
380
468
  console.log(pc.cyan(`\n${BRAND.name} Setup\n`));
381
469
 
382
470
  // 1. Detect framework
383
- const framework = await detectFramework(root);
384
- const frameworkLabel =
385
- framework === 'nextjs-app' ? 'Next.js (App Router)' :
386
- framework === 'nextjs-pages' ? 'Next.js (Pages Router)' :
387
- framework === 'vite' ? 'Vite' :
388
- 'Unknown';
471
+ const framework = await detectSetupFramework(root);
472
+ const frameworkLabels: Record<string, string> = {
473
+ 'nextjs-app': 'Next.js (App Router)',
474
+ 'nextjs-pages': 'Next.js (Pages Router)',
475
+ 'vite': 'Vite',
476
+ 'remix': 'Remix',
477
+ 'astro': 'Astro',
478
+ };
479
+ const frameworkLabel = frameworkLabels[framework] || 'Unknown';
389
480
 
390
481
  console.log(` ${pc.dim('Framework:')} ${frameworkLabel}`);
391
482
 
@@ -0,0 +1,197 @@
1
+ /**
2
+ * fragments snapshot - Run visual snapshot tests per component variant
3
+ *
4
+ * Starts the dev server (if not already running), then runs Playwright
5
+ * snapshot tests against all component variants discovered in fragments.json.
6
+ */
7
+
8
+ import { resolve } from "node:path";
9
+ import { execSync, spawn } from "node:child_process";
10
+ import { existsSync } from "node:fs";
11
+ import pc from "picocolors";
12
+ import { BRAND } from "../core/index.js";
13
+
14
+ export interface SnapshotOptions {
15
+ /** Port of a running dev server (skips starting one) */
16
+ port?: number | string;
17
+ /** Update existing snapshots instead of comparing */
18
+ update?: boolean;
19
+ /** Filter to a specific component name */
20
+ component?: string;
21
+ /** Path to Playwright config (auto-detected if omitted) */
22
+ config?: string;
23
+ /** Path to the snapshot spec file */
24
+ spec?: string;
25
+ /** CI mode — non-interactive, exit 1 on mismatch */
26
+ ci?: boolean;
27
+ }
28
+
29
+ export interface SnapshotResult {
30
+ success: boolean;
31
+ totalTests: number;
32
+ passed: number;
33
+ failed: number;
34
+ }
35
+
36
+ /**
37
+ * Find the snapshot spec file.
38
+ * Checks project root e2e/ first, then falls back to the CLI's bundled spec.
39
+ */
40
+ function findSnapshotSpec(projectRoot: string, explicitPath?: string): string | null {
41
+ if (explicitPath) {
42
+ const resolved = resolve(projectRoot, explicitPath);
43
+ return existsSync(resolved) ? resolved : null;
44
+ }
45
+
46
+ // Check common locations
47
+ const candidates = [
48
+ resolve(projectRoot, "e2e/component-visual-snapshots.spec.ts"),
49
+ resolve(projectRoot, "tests/visual-snapshots.spec.ts"),
50
+ resolve(projectRoot, "test/visual-snapshots.spec.ts"),
51
+ ];
52
+
53
+ for (const candidate of candidates) {
54
+ if (existsSync(candidate)) {
55
+ return candidate;
56
+ }
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Run visual snapshot tests.
64
+ */
65
+ export async function snapshot(options: SnapshotOptions = {}): Promise<SnapshotResult> {
66
+ const projectRoot = process.cwd();
67
+ const {
68
+ port,
69
+ update = false,
70
+ component,
71
+ ci = false,
72
+ } = options;
73
+
74
+ console.log(pc.cyan(`\n${BRAND.name} Visual Snapshots\n`));
75
+
76
+ // Check that fragments.json exists
77
+ const fragmentsJson = resolve(projectRoot, BRAND.outFile);
78
+ if (!existsSync(fragmentsJson)) {
79
+ console.error(
80
+ pc.red(`${BRAND.outFile} not found. Run ${pc.bold(`${BRAND.cliCommand} build`)} first.`)
81
+ );
82
+ return { success: false, totalTests: 0, passed: 0, failed: 0 };
83
+ }
84
+
85
+ // Find the snapshot spec
86
+ const specPath = findSnapshotSpec(projectRoot, options.spec);
87
+ if (!specPath) {
88
+ console.error(
89
+ pc.red("No snapshot spec found.") + "\n" +
90
+ pc.dim("Expected: e2e/component-visual-snapshots.spec.ts\n") +
91
+ pc.dim(`Create one or specify with --spec <path>`)
92
+ );
93
+ return { success: false, totalTests: 0, passed: 0, failed: 0 };
94
+ }
95
+
96
+ console.log(pc.dim(`Spec: ${specPath}`));
97
+
98
+ // Check for Playwright
99
+ try {
100
+ execSync("npx playwright --version", { stdio: "pipe" });
101
+ } catch {
102
+ console.error(
103
+ pc.red("Playwright not found.") + "\n" +
104
+ pc.dim("Install it: pnpm add -D @playwright/test && npx playwright install chromium")
105
+ );
106
+ return { success: false, totalTests: 0, passed: 0, failed: 0 };
107
+ }
108
+
109
+ // Build Playwright args
110
+ const args = ["playwright", "test", specPath];
111
+
112
+ if (update) {
113
+ args.push("--update-snapshots");
114
+ console.log(pc.yellow("Updating snapshots (baselines will be overwritten)"));
115
+ }
116
+
117
+ if (component) {
118
+ args.push("--grep", component);
119
+ console.log(pc.dim(`Filtering: ${component}`));
120
+ }
121
+
122
+ // If a port is specified, set the BASE_URL env var so the spec
123
+ // can connect to an already-running dev server
124
+ const env: Record<string, string> = { ...process.env as Record<string, string> };
125
+ if (port) {
126
+ env.FRAGMENTS_DEV_PORT = String(port);
127
+ console.log(pc.dim(`Using running dev server on port ${port}`));
128
+ }
129
+
130
+ console.log(pc.dim("\nRunning snapshot tests...\n"));
131
+
132
+ // Run Playwright
133
+ return new Promise<SnapshotResult>((resolveResult) => {
134
+ const child = spawn("npx", args, {
135
+ cwd: projectRoot,
136
+ stdio: ci ? "pipe" : "inherit",
137
+ env,
138
+ });
139
+
140
+ let stdout = "";
141
+
142
+ if (ci && child.stdout) {
143
+ child.stdout.on("data", (data) => {
144
+ stdout += data.toString();
145
+ });
146
+ }
147
+ if (ci && child.stderr) {
148
+ child.stderr.on("data", (data) => {
149
+ stdout += data.toString();
150
+ });
151
+ }
152
+
153
+ child.on("close", (code) => {
154
+ const success = code === 0;
155
+
156
+ if (ci) {
157
+ // Parse Playwright output for test counts
158
+ const passedMatch = stdout.match(/(\d+) passed/);
159
+ const failedMatch = stdout.match(/(\d+) failed/);
160
+ const passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
161
+ const failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
162
+
163
+ if (!success) {
164
+ process.stdout.write(stdout);
165
+ }
166
+
167
+ resolveResult({
168
+ success,
169
+ totalTests: passed + failed,
170
+ passed,
171
+ failed,
172
+ });
173
+ } else {
174
+ if (success) {
175
+ console.log(pc.green("\n✓ All snapshots match\n"));
176
+ } else if (update) {
177
+ console.log(pc.green("\n✓ Snapshots updated\n"));
178
+ } else {
179
+ console.log(pc.red("\n✗ Snapshot mismatches detected"));
180
+ console.log(pc.dim(`Run ${pc.bold(`${BRAND.cliCommand} snapshot --update`)} to accept changes\n`));
181
+ }
182
+
183
+ resolveResult({
184
+ success: success || update,
185
+ totalTests: 0,
186
+ passed: 0,
187
+ failed: 0,
188
+ });
189
+ }
190
+ });
191
+
192
+ child.on("error", (err) => {
193
+ console.error(pc.red(`Failed to run Playwright: ${err.message}`));
194
+ resolveResult({ success: false, totalTests: 0, passed: 0, failed: 0 });
195
+ });
196
+ });
197
+ }