@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.
- package/dist/ai-client-I6MDWNYA.js +21 -0
- package/dist/bin.js +292 -367
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-PW7QTQA6.js → chunk-4OC7FTJB.js} +2 -2
- package/dist/{chunk-HRFUSSZI.js → chunk-AM4MRTMN.js} +2 -2
- package/dist/{chunk-5G3VZH43.js → chunk-GVDSFQ4E.js} +281 -351
- package/dist/chunk-GVDSFQ4E.js.map +1 -0
- package/dist/chunk-JJ2VRTBU.js +626 -0
- package/dist/chunk-JJ2VRTBU.js.map +1 -0
- package/dist/{chunk-D5PYOXEI.js → chunk-LVWFOLUZ.js} +148 -13
- package/dist/{chunk-D5PYOXEI.js.map → chunk-LVWFOLUZ.js.map} +1 -1
- package/dist/{chunk-WXSR2II7.js → chunk-OQKMEFOS.js} +58 -6
- package/dist/chunk-OQKMEFOS.js.map +1 -0
- package/dist/chunk-SXTKFDCR.js +104 -0
- package/dist/chunk-SXTKFDCR.js.map +1 -0
- package/dist/chunk-T5OMVL7E.js +443 -0
- package/dist/chunk-T5OMVL7E.js.map +1 -0
- package/dist/{chunk-ZM4ZQZWZ.js → chunk-TPWGL2XS.js} +39 -37
- package/dist/chunk-TPWGL2XS.js.map +1 -0
- package/dist/{chunk-OQO55NKV.js → chunk-WFS63PCW.js} +85 -11
- package/dist/chunk-WFS63PCW.js.map +1 -0
- package/dist/core/index.js +9 -1
- package/dist/{discovery-NEOY4MPN.js → discovery-ZJQSXF56.js} +3 -3
- package/dist/{generate-FBHSXR3D.js → generate-RJFS2JWA.js} +4 -4
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/init-ZSX3NRCZ.js +636 -0
- package/dist/init-ZSX3NRCZ.js.map +1 -0
- package/dist/mcp-bin.js +2 -2
- package/dist/{scan-CJF2DOQW.js → scan-3PMCJ4RB.js} +6 -6
- package/dist/scan-generate-SYU4PYZD.js +1115 -0
- package/dist/scan-generate-SYU4PYZD.js.map +1 -0
- package/dist/{service-TQYWY65E.js → service-VMGNJZ42.js} +3 -3
- package/dist/snapshot-XOISO2IS.js +139 -0
- package/dist/snapshot-XOISO2IS.js.map +1 -0
- package/dist/{static-viewer-NUBFPKWH.js → static-viewer-5GXH2MGE.js} +3 -3
- package/dist/static-viewer-5GXH2MGE.js.map +1 -0
- package/dist/{test-Z5LVO724.js → test-SI4NSHQX.js} +4 -4
- package/dist/{tokens-CE46OTMD.js → tokens-T6SIVUT5.js} +5 -5
- package/dist/{viewer-DNMNC5VS.js → viewer-7ZEAFBVN.js} +80 -58
- package/dist/viewer-7ZEAFBVN.js.map +1 -0
- package/package.json +6 -14
- package/src/ai-client.ts +156 -0
- package/src/bin.ts +74 -2
- package/src/build.ts +95 -33
- package/src/commands/__tests__/drift-sync.test.ts +252 -0
- package/src/commands/__tests__/scan-generate.test.ts +497 -45
- package/src/commands/enhance.ts +11 -35
- package/src/commands/init.ts +296 -193
- package/src/commands/scan-generate.ts +740 -139
- package/src/commands/scan.ts +37 -32
- package/src/commands/setup.ts +143 -52
- package/src/commands/snapshot.ts +197 -0
- package/src/commands/sync.ts +357 -0
- package/src/commands/validate.ts +43 -1
- package/src/core/component-extractor.test.ts +282 -0
- package/src/core/component-extractor.ts +1030 -0
- package/src/core/discovery.ts +93 -7
- package/src/service/enhance/props-extractor.ts +235 -13
- package/src/validators.ts +236 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
- package/src/viewer/server.ts +37 -22
- package/src/viewer/vite-plugin.ts +25 -9
- package/dist/chunk-5G3VZH43.js.map +0 -1
- package/dist/chunk-OQO55NKV.js.map +0 -1
- package/dist/chunk-WXSR2II7.js.map +0 -1
- package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
- package/dist/init-NDQXUWDU.js +0 -796
- package/dist/init-NDQXUWDU.js.map +0 -1
- package/dist/scan-generate-SJAN5MVI.js +0 -691
- package/dist/scan-generate-SJAN5MVI.js.map +0 -1
- package/dist/viewer-DNMNC5VS.js.map +0 -1
- package/src/ai.ts +0 -266
- package/src/commands/init-framework.ts +0 -414
- package/src/mcp/bin.ts +0 -36
- package/src/migrate/bin.ts +0 -114
- package/src/theme/index.ts +0 -77
- package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
- package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
- package/src/viewer/__tests__/render-utils.test.ts +0 -232
- package/src/viewer/__tests__/style-utils.test.ts +0 -404
- package/src/viewer/assets/fragments-logo.ts +0 -4
- package/src/viewer/assets/fragments_logo.png +0 -0
- package/src/viewer/bin.ts +0 -86
- package/src/viewer/cli/health.ts +0 -256
- package/src/viewer/cli/index.ts +0 -33
- package/src/viewer/cli/scan.ts +0 -124
- package/src/viewer/cli/utils.ts +0 -174
- package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
- package/src/viewer/components/ActionCapture.tsx +0 -172
- package/src/viewer/components/ActionsPanel.tsx +0 -332
- package/src/viewer/components/AllVariantsPreview.tsx +0 -78
- package/src/viewer/components/App.tsx +0 -582
- package/src/viewer/components/BottomPanel.tsx +0 -288
- package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
- package/src/viewer/components/CodePanel.tsx +0 -118
- package/src/viewer/components/CommandPalette.tsx +0 -392
- package/src/viewer/components/ComponentDocView.tsx +0 -164
- package/src/viewer/components/ComponentGraph.tsx +0 -380
- package/src/viewer/components/ComponentHeader.tsx +0 -88
- package/src/viewer/components/ContractPanel.tsx +0 -241
- package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
- package/src/viewer/components/ErrorBoundary.tsx +0 -97
- package/src/viewer/components/FigmaEmbed.tsx +0 -238
- package/src/viewer/components/FragmentEditor.tsx +0 -525
- package/src/viewer/components/FragmentRenderer.tsx +0 -61
- package/src/viewer/components/HeaderSearch.tsx +0 -24
- package/src/viewer/components/HealthDashboard.tsx +0 -441
- package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
- package/src/viewer/components/Icons.tsx +0 -479
- package/src/viewer/components/InteractionsPanel.tsx +0 -757
- package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
- package/src/viewer/components/IsolatedRender.tsx +0 -113
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
- package/src/viewer/components/LandingPage.tsx +0 -421
- package/src/viewer/components/Layout.tsx +0 -27
- package/src/viewer/components/LeftSidebar.tsx +0 -472
- package/src/viewer/components/LoadErrorMessage.tsx +0 -102
- package/src/viewer/components/MultiViewportPreview.tsx +0 -522
- package/src/viewer/components/NoVariantsMessage.tsx +0 -59
- package/src/viewer/components/PanelShell.tsx +0 -161
- package/src/viewer/components/PerformancePanel.tsx +0 -304
- package/src/viewer/components/PreviewArea.tsx +0 -472
- package/src/viewer/components/PreviewAside.tsx +0 -168
- package/src/viewer/components/PreviewFrameHost.tsx +0 -303
- package/src/viewer/components/PreviewPane.tsx +0 -149
- package/src/viewer/components/PreviewToolbar.tsx +0 -80
- package/src/viewer/components/PropsEditor.tsx +0 -506
- package/src/viewer/components/PropsTable.tsx +0 -111
- package/src/viewer/components/RelationsSection.tsx +0 -88
- package/src/viewer/components/ResizablePanel.tsx +0 -271
- package/src/viewer/components/RightSidebar.tsx +0 -102
- package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
- package/src/viewer/components/ScreenshotButton.tsx +0 -90
- package/src/viewer/components/Sidebar.tsx +0 -169
- package/src/viewer/components/SkeletonLoader.tsx +0 -161
- package/src/viewer/components/ThemeProvider.tsx +0 -42
- package/src/viewer/components/Toast.tsx +0 -3
- package/src/viewer/components/TokenStylePanel.tsx +0 -699
- package/src/viewer/components/TopToolbar.tsx +0 -159
- package/src/viewer/components/UsageSection.tsx +0 -95
- package/src/viewer/components/VariantMatrix.tsx +0 -388
- package/src/viewer/components/VariantRenderer.tsx +0 -131
- package/src/viewer/components/VariantTabs.tsx +0 -40
- package/src/viewer/components/ViewerHeader.tsx +0 -69
- package/src/viewer/components/ViewerStateSync.tsx +0 -52
- package/src/viewer/components/ViewportSelector.tsx +0 -172
- package/src/viewer/components/WebMCPDevTools.tsx +0 -503
- package/src/viewer/components/WebMCPIntegration.tsx +0 -47
- package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
- package/src/viewer/components/_future/CreatePage.tsx +0 -836
- package/src/viewer/components/viewer-utils.ts +0 -16
- package/src/viewer/composition-renderer.ts +0 -381
- package/src/viewer/constants/index.ts +0 -1
- package/src/viewer/constants/ui.ts +0 -166
- package/src/viewer/entry.tsx +0 -335
- package/src/viewer/hooks/index.ts +0 -2
- package/src/viewer/hooks/useA11yCache.ts +0 -383
- package/src/viewer/hooks/useA11yService.ts +0 -364
- package/src/viewer/hooks/useActions.ts +0 -138
- package/src/viewer/hooks/useAppState.ts +0 -147
- package/src/viewer/hooks/useCompiledFragments.ts +0 -42
- package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
- package/src/viewer/hooks/useHmrStatus.ts +0 -109
- package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
- package/src/viewer/hooks/usePreviewBridge.ts +0 -347
- package/src/viewer/hooks/useScrollSpy.ts +0 -78
- package/src/viewer/hooks/useUrlState.ts +0 -318
- package/src/viewer/hooks/useViewSettings.ts +0 -111
- package/src/viewer/index.html +0 -28
- package/src/viewer/intelligence/healthReport.ts +0 -505
- package/src/viewer/intelligence/styleDrift.ts +0 -340
- package/src/viewer/intelligence/usageScanner.ts +0 -309
- package/src/viewer/jsx-parser.ts +0 -486
- package/src/viewer/preview-frame-entry.tsx +0 -25
- package/src/viewer/preview-frame.html +0 -125
- package/src/viewer/public/favicon.ico +0 -0
- package/src/viewer/render-template.html +0 -68
- package/src/viewer/styles/globals.css +0 -278
- package/src/viewer/types/a11y.ts +0 -197
- package/src/viewer/utils/a11y-fixes.ts +0 -509
- package/src/viewer/utils/actionExport.ts +0 -372
- package/src/viewer/utils/colorSchemes.ts +0 -201
- package/src/viewer/utils/detectRelationships.ts +0 -256
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
- package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
- package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
- package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
- package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
- package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
- package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -137
- package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
- package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
- package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
- package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
- package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
- package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/index.ts +0 -34
- package/src/viewer/vendor/shared/src/types.ts +0 -53
- package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
- package/src/viewer/webmcp/analytics.ts +0 -165
- package/src/viewer/webmcp/index.ts +0 -3
- package/src/viewer/webmcp/posthog-bridge.ts +0 -39
- package/src/viewer/webmcp/runtime-tools.ts +0 -152
- package/src/viewer/webmcp/scan-utils.ts +0 -135
- package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
- package/src/viewer/webmcp/viewer-state.ts +0 -45
- /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
- /package/dist/{chunk-PW7QTQA6.js.map → chunk-4OC7FTJB.js.map} +0 -0
- /package/dist/{chunk-HRFUSSZI.js.map → chunk-AM4MRTMN.js.map} +0 -0
- /package/dist/{scan-CJF2DOQW.js.map → discovery-ZJQSXF56.js.map} +0 -0
- /package/dist/{generate-FBHSXR3D.js.map → generate-RJFS2JWA.js.map} +0 -0
- /package/dist/{service-TQYWY65E.js.map → scan-3PMCJ4RB.js.map} +0 -0
- /package/dist/{static-viewer-NUBFPKWH.js.map → service-VMGNJZ42.js.map} +0 -0
- /package/dist/{test-Z5LVO724.js.map → test-SI4NSHQX.js.map} +0 -0
- /package/dist/{tokens-CE46OTMD.js.map → tokens-T6SIVUT5.js.map} +0 -0
package/src/commands/scan.ts
CHANGED
|
@@ -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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
log(pc.green(` Found ${components.length} components`));
|
|
125
130
|
if (options.verbose) {
|
|
126
131
|
for (const comp of components.slice(0, 10)) {
|
|
127
|
-
|
|
132
|
+
log(pc.dim(` - ${comp.name}: ${comp.relativePath}`));
|
|
128
133
|
}
|
|
129
134
|
if (components.length > 10) {
|
|
130
|
-
|
|
135
|
+
log(pc.dim(` ... and ${components.length - 10} more`));
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
// Phase 2: Extract props from TypeScript
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
211
|
+
log(pc.yellow(` Usage scanning failed: ${e instanceof Error ? e.message : "unknown error"}`));
|
|
207
212
|
}
|
|
208
213
|
} else {
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
+
log(pc.yellow(` Storybook parsing failed: ${e instanceof Error ? e.message : "unknown error"}`));
|
|
236
241
|
}
|
|
237
242
|
} else {
|
|
238
|
-
|
|
243
|
+
log(pc.dim("\nPhase 4: Skipping Storybook parsing"));
|
|
239
244
|
}
|
|
240
245
|
|
|
241
246
|
// Phase 5: Generate fragments
|
|
242
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
295
|
+
log(pc.yellow(`\n ${warnings.length} warning(s)`));
|
|
291
296
|
if (options.verbose) {
|
|
292
297
|
for (const w of warnings) {
|
|
293
|
-
|
|
298
|
+
log(pc.dim(` ${w.component}: ${w.warning}`));
|
|
294
299
|
}
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
|
|
298
303
|
if (errors.length > 0) {
|
|
299
|
-
|
|
304
|
+
log(pc.red(`\n ${errors.length} error(s)`));
|
|
300
305
|
for (const e of errors) {
|
|
301
|
-
|
|
306
|
+
log(pc.dim(` ${e.component}: ${e.error}`));
|
|
302
307
|
}
|
|
303
308
|
}
|
|
304
309
|
|
|
305
|
-
|
|
310
|
+
log();
|
|
306
311
|
|
|
307
312
|
return {
|
|
308
313
|
success: errors.length === 0,
|
package/src/commands/setup.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
173
|
-
if (framework === '
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
233
|
+
// Add provider import after the last import line
|
|
234
|
+
const providerImport = "import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';";
|
|
178
235
|
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
'
|
|
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
|
+
}
|