@fragments-sdk/cli 0.8.1 → 0.9.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/bin.js +517 -77
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
- package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
- package/dist/chunk-BW3ZATBW.js.map +1 -0
- package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
- package/dist/chunk-D7372LQX.js.map +1 -0
- package/dist/chunk-EZYXYWNF.js +131 -0
- package/dist/chunk-EZYXYWNF.js.map +1 -0
- package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
- package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
- package/dist/chunk-NVSPGSKB.js.map +1 -0
- package/dist/core/index.d.ts +105 -3
- package/dist/core/index.js +12 -2
- package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
- package/dist/generate-LQA2R7FN.js +461 -0
- package/dist/generate-LQA2R7FN.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{init-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
- package/dist/init-2GEGVIUQ.js.map +1 -0
- package/dist/mcp-bin.js +4 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
- package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
- package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
- package/dist/storyFilters-3LUYAFZF.js +15 -0
- package/dist/storyFilters-3LUYAFZF.js.map +1 -0
- package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
- package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
- package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
- package/dist/{viewer-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
- package/dist/viewer-RFA2KVBG.js.map +1 -0
- package/package.json +2 -2
- package/src/bin.ts +26 -0
- package/src/build.ts +12 -2
- package/src/commands/build.ts +16 -2
- package/src/commands/doctor.ts +498 -0
- package/src/commands/generate.ts +383 -68
- package/src/commands/init-framework.ts +1 -1
- package/src/commands/init.ts +9 -51
- package/src/core/config.ts +15 -2
- package/src/core/generators/typescript-extractor.ts +10 -0
- package/src/core/index.ts +15 -0
- package/src/core/schema.ts +10 -2
- package/src/core/storyFilters.test.ts +350 -0
- package/src/core/storyFilters.ts +253 -0
- package/src/core/types.ts +22 -0
- package/src/migrate/converter.ts +9 -1
- package/src/migrate/parser.ts +2 -0
- package/src/migrate/types.ts +2 -0
- package/src/setup.ts +69 -24
- package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
- package/src/viewer/components/AccessibilityPanel.tsx +305 -312
- package/src/viewer/components/ActionsPanel.tsx +31 -29
- package/src/viewer/components/AllVariantsPreview.tsx +78 -0
- package/src/viewer/components/App.tsx +187 -740
- package/src/viewer/components/BottomPanel.tsx +228 -132
- package/src/viewer/components/CodePanel.tsx +1 -1
- package/src/viewer/components/CommandPalette.tsx +7 -10
- package/src/viewer/components/ComponentDocView.tsx +164 -0
- package/src/viewer/components/ComponentGraph.tsx +111 -142
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
- package/src/viewer/components/FigmaEmbed.tsx +20 -18
- package/src/viewer/components/FragmentEditor.tsx +92 -115
- package/src/viewer/components/HeaderSearch.tsx +24 -0
- package/src/viewer/components/HealthDashboard.tsx +16 -2
- package/src/viewer/components/Icons.tsx +9 -0
- package/src/viewer/components/InteractionsPanel.tsx +101 -117
- package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
- package/src/viewer/components/LandingPage.tsx +3 -3
- package/src/viewer/components/LeftSidebar.tsx +141 -63
- package/src/viewer/components/LoadErrorMessage.tsx +102 -0
- package/src/viewer/components/MultiViewportPreview.tsx +61 -142
- package/src/viewer/components/NoVariantsMessage.tsx +59 -0
- package/src/viewer/components/PanelShell.tsx +161 -0
- package/src/viewer/components/PerformancePanel.tsx +31 -28
- package/src/viewer/components/PreviewArea.tsx +1 -1
- package/src/viewer/components/PreviewAside.tsx +168 -0
- package/src/viewer/components/PreviewFrameHost.tsx +3 -3
- package/src/viewer/components/PropsEditor.tsx +70 -156
- package/src/viewer/components/ResizablePanel.tsx +103 -263
- package/src/viewer/components/RightSidebar.tsx +3 -9
- package/src/viewer/components/SkeletonLoader.tsx +13 -13
- package/src/viewer/components/TokenStylePanel.tsx +182 -209
- package/src/viewer/components/TopToolbar.tsx +159 -0
- package/src/viewer/components/VariantMatrix.tsx +42 -86
- package/src/viewer/components/VariantTabs.tsx +3 -3
- package/src/viewer/components/ViewerHeader.tsx +69 -0
- package/src/viewer/components/WebMCPDevTools.tsx +17 -23
- package/src/viewer/components/viewer-utils.ts +16 -0
- package/src/viewer/entry.tsx +5 -0
- package/src/viewer/hooks/useAppState.ts +27 -4
- package/src/viewer/hooks/usePreviewBridge.ts +2 -2
- package/src/viewer/preview-frame.html +6 -12
- package/src/viewer/server.ts +184 -6
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
- package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
- package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
- package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
- package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
- package/src/viewer/vendor/shared/src/index.ts +8 -0
- package/src/viewer/vendor/shared/src/types.ts +12 -0
- package/src/viewer/vite-plugin.ts +109 -4
- package/dist/chunk-2JIKCJX3.js.map +0 -1
- package/dist/chunk-CJEGT3WD.js.map +0 -1
- package/dist/chunk-GOVI6COW.js.map +0 -1
- package/dist/generate-35OIMW4Y.js +0 -252
- package/dist/generate-35OIMW4Y.js.map +0 -1
- package/dist/init-KFYN37ZY.js.map +0 -1
- package/dist/viewer-HZK4BSDK.js.map +0 -1
- /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
- /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
- /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
- /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
- /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
- /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart filtering for Storybook adapter.
|
|
3
|
+
*
|
|
4
|
+
* Two layers:
|
|
5
|
+
* 1. Per-file heuristics — checkStoryExclusion() checks title, tags, component name, etc.
|
|
6
|
+
* 2. Cross-file sub-component detection — detectSubComponentPaths() uses directory structure.
|
|
7
|
+
*
|
|
8
|
+
* All functions are pure (no I/O, no side effects) for easy testing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { StorybookFilterConfig } from './types.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export type ExclusionReason =
|
|
18
|
+
| 'deprecated' // title contains "Deprecated"
|
|
19
|
+
| 'test-story' // title ends /tests? OR file matches *.test.stories.*
|
|
20
|
+
| 'svg-icon' // component name starts with Svg[A-Z]
|
|
21
|
+
| 'tag-excluded' // meta.tags includes hidden/internal/no-fragment
|
|
22
|
+
| 'empty-variants' // zero renderable story exports
|
|
23
|
+
| 'sub-component' // directory-based: file in another component's folder
|
|
24
|
+
| 'config-excluded'; // user explicit exclude pattern
|
|
25
|
+
|
|
26
|
+
export interface ExclusionResult {
|
|
27
|
+
excluded: boolean;
|
|
28
|
+
reason?: ExclusionReason;
|
|
29
|
+
detail?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Per-file heuristics
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const EXCLUDED_TAGS = new Set(['hidden', 'internal', 'no-fragment']);
|
|
37
|
+
|
|
38
|
+
const SVG_ICON_RE = /^Svg[A-Z]/;
|
|
39
|
+
const TEST_TITLE_RE = /\/tests?$/i;
|
|
40
|
+
const TEST_FILE_RE = /\.test\.stories\./;
|
|
41
|
+
const DEPRECATED_TITLE_RE = /\bDeprecated\b/i;
|
|
42
|
+
|
|
43
|
+
export interface CheckStoryExclusionOpts {
|
|
44
|
+
storybookTitle?: string;
|
|
45
|
+
componentName: string;
|
|
46
|
+
componentDisplayName?: string;
|
|
47
|
+
componentFunctionName?: string;
|
|
48
|
+
tags?: string[];
|
|
49
|
+
variantCount: number;
|
|
50
|
+
filePath: string;
|
|
51
|
+
config: StorybookFilterConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Per-file exclusion check. Returns `{ excluded: true, reason, detail }` when
|
|
56
|
+
* the fragment should be filtered out, or `{ excluded: false }` when it should
|
|
57
|
+
* be kept.
|
|
58
|
+
*
|
|
59
|
+
* Config `include` trumps everything — if a name matches `include`, it is
|
|
60
|
+
* never excluded by heuristics.
|
|
61
|
+
*/
|
|
62
|
+
export function checkStoryExclusion(opts: CheckStoryExclusionOpts): ExclusionResult {
|
|
63
|
+
const { config } = opts;
|
|
64
|
+
|
|
65
|
+
// Force-included names bypass all heuristic filters
|
|
66
|
+
if (isForceIncluded(opts.componentName, config)) {
|
|
67
|
+
return { excluded: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Config explicit exclude
|
|
71
|
+
if (isConfigExcluded(opts.componentName, config)) {
|
|
72
|
+
return {
|
|
73
|
+
excluded: true,
|
|
74
|
+
reason: 'config-excluded',
|
|
75
|
+
detail: `'${opts.componentName}' matches storybook.exclude pattern`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Deprecated
|
|
80
|
+
if (config.excludeDeprecated !== false && opts.storybookTitle && DEPRECATED_TITLE_RE.test(opts.storybookTitle)) {
|
|
81
|
+
return {
|
|
82
|
+
excluded: true,
|
|
83
|
+
reason: 'deprecated',
|
|
84
|
+
detail: `Title "${opts.storybookTitle}" contains "Deprecated"`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Test stories
|
|
89
|
+
if (config.excludeTests !== false) {
|
|
90
|
+
if (opts.storybookTitle && TEST_TITLE_RE.test(opts.storybookTitle)) {
|
|
91
|
+
return {
|
|
92
|
+
excluded: true,
|
|
93
|
+
reason: 'test-story',
|
|
94
|
+
detail: `Title "${opts.storybookTitle}" ends with /test(s)`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (TEST_FILE_RE.test(opts.filePath)) {
|
|
98
|
+
return {
|
|
99
|
+
excluded: true,
|
|
100
|
+
reason: 'test-story',
|
|
101
|
+
detail: `File path matches *.test.stories.*`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// SVG icons
|
|
107
|
+
if (config.excludeSvgIcons !== false) {
|
|
108
|
+
const names = [opts.componentName, opts.componentDisplayName, opts.componentFunctionName].filter(Boolean) as string[];
|
|
109
|
+
for (const name of names) {
|
|
110
|
+
if (SVG_ICON_RE.test(name)) {
|
|
111
|
+
return {
|
|
112
|
+
excluded: true,
|
|
113
|
+
reason: 'svg-icon',
|
|
114
|
+
detail: `Component name "${name}" matches Svg[A-Z] pattern`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Excluded tags
|
|
121
|
+
if (opts.tags?.length) {
|
|
122
|
+
const hit = opts.tags.find(t => EXCLUDED_TAGS.has(t));
|
|
123
|
+
if (hit) {
|
|
124
|
+
return {
|
|
125
|
+
excluded: true,
|
|
126
|
+
reason: 'tag-excluded',
|
|
127
|
+
detail: `Tag "${hit}" is in the exclusion set`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Empty variants
|
|
133
|
+
if (opts.variantCount === 0) {
|
|
134
|
+
return {
|
|
135
|
+
excluded: true,
|
|
136
|
+
reason: 'empty-variants',
|
|
137
|
+
detail: 'Zero renderable story exports',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { excluded: false };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Cross-file sub-component detection
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Given all story file relative paths, detect which ones are sub-components
|
|
150
|
+
* based on directory structure.
|
|
151
|
+
*
|
|
152
|
+
* Heuristic: within a directory, if one story file's base name matches the
|
|
153
|
+
* directory name, it is the "primary" component. All other story files in
|
|
154
|
+
* the same directory are considered sub-components.
|
|
155
|
+
*
|
|
156
|
+
* Example:
|
|
157
|
+
* src/components/Form/Form.stories.tsx → primary ("Form")
|
|
158
|
+
* src/components/Form/Checkbox.stories.tsx → sub-component of "Form"
|
|
159
|
+
* src/components/Form/RadioGroup.stories.tsx → sub-component of "Form"
|
|
160
|
+
*
|
|
161
|
+
* Returns a Map from relative path → parent component name.
|
|
162
|
+
* Paths NOT in the map are standalone components.
|
|
163
|
+
*/
|
|
164
|
+
export function detectSubComponentPaths(
|
|
165
|
+
storyFiles: Array<{ relativePath: string }>
|
|
166
|
+
): Map<string, string> {
|
|
167
|
+
// Group story files by their parent directory
|
|
168
|
+
const byDir = new Map<string, Array<{ relativePath: string; baseName: string }>>();
|
|
169
|
+
|
|
170
|
+
for (const file of storyFiles) {
|
|
171
|
+
const parts = file.relativePath.split('/');
|
|
172
|
+
if (parts.length < 2) continue; // skip root-level files
|
|
173
|
+
|
|
174
|
+
const fileName = parts[parts.length - 1];
|
|
175
|
+
// Extract base name: "Form.stories.tsx" → "Form"
|
|
176
|
+
const baseMatch = fileName.match(/^([^.]+)\.stories\./);
|
|
177
|
+
if (!baseMatch) continue;
|
|
178
|
+
|
|
179
|
+
const dir = parts.slice(0, -1).join('/');
|
|
180
|
+
const baseName = baseMatch[1];
|
|
181
|
+
|
|
182
|
+
if (!byDir.has(dir)) byDir.set(dir, []);
|
|
183
|
+
byDir.get(dir)!.push({ relativePath: file.relativePath, baseName });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const subComponentMap = new Map<string, string>();
|
|
187
|
+
|
|
188
|
+
for (const [dir, files] of byDir) {
|
|
189
|
+
if (files.length <= 1) continue; // single file in dir → always standalone
|
|
190
|
+
|
|
191
|
+
// Directory name is the last segment: "src/components/Form" → "Form"
|
|
192
|
+
const dirName = dir.split('/').pop()!;
|
|
193
|
+
|
|
194
|
+
// Find the primary: story whose base name matches the directory name
|
|
195
|
+
const primary = files.find(f => f.baseName === dirName);
|
|
196
|
+
if (!primary) continue; // no clear primary → keep all
|
|
197
|
+
|
|
198
|
+
// All others in this dir are sub-components
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
if (file.relativePath === primary.relativePath) continue;
|
|
201
|
+
subComponentMap.set(file.relativePath, primary.baseName);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return subComponentMap;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Config helpers
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if a component name matches the `storybook.include` patterns.
|
|
214
|
+
* Include is a force-include that bypasses all heuristic filters.
|
|
215
|
+
*/
|
|
216
|
+
export function isForceIncluded(name: string, config: StorybookFilterConfig): boolean {
|
|
217
|
+
if (!config.include?.length) return false;
|
|
218
|
+
return config.include.some(pattern => matchesPattern(name, pattern));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if a component name matches the `storybook.exclude` patterns.
|
|
223
|
+
*/
|
|
224
|
+
export function isConfigExcluded(name: string, config: StorybookFilterConfig): boolean {
|
|
225
|
+
if (!config.exclude?.length) return false;
|
|
226
|
+
return config.exclude.some(pattern => matchesPattern(name, pattern));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Simple pattern matching: exact match or glob-style prefix/suffix wildcards.
|
|
231
|
+
* "Button" → exact match
|
|
232
|
+
* "Svg*" → prefix match
|
|
233
|
+
* "*Icon" → suffix match
|
|
234
|
+
* "*Badge*" → contains match
|
|
235
|
+
*/
|
|
236
|
+
function matchesPattern(name: string, pattern: string): boolean {
|
|
237
|
+
if (!pattern.includes('*')) {
|
|
238
|
+
return name === pattern;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const parts = pattern.split('*');
|
|
242
|
+
if (parts.length === 2) {
|
|
243
|
+
const [prefix, suffix] = parts;
|
|
244
|
+
if (prefix && suffix) return name.startsWith(prefix) && name.endsWith(suffix);
|
|
245
|
+
if (prefix) return name.startsWith(prefix);
|
|
246
|
+
if (suffix) return name.endsWith(suffix);
|
|
247
|
+
return true; // pattern is just "*"
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Multi-wildcard: convert to regex
|
|
251
|
+
const escaped = parts.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*');
|
|
252
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
253
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -454,6 +454,25 @@ export interface SnippetPolicyConfig {
|
|
|
454
454
|
allowedExternalModules?: string[];
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Storybook adapter filtering configuration.
|
|
459
|
+
* Controls which Storybook stories are included when generating fragments.
|
|
460
|
+
*/
|
|
461
|
+
export interface StorybookFilterConfig {
|
|
462
|
+
/** Glob-style patterns for component names to explicitly exclude */
|
|
463
|
+
exclude?: string[];
|
|
464
|
+
/** Glob-style patterns for component names to force-include (bypasses all heuristic filters) */
|
|
465
|
+
include?: string[];
|
|
466
|
+
/** Exclude stories with "Deprecated" in the title (default: true) */
|
|
467
|
+
excludeDeprecated?: boolean;
|
|
468
|
+
/** Exclude test stories (title ending /test(s) or *.test.stories.* files) (default: true) */
|
|
469
|
+
excludeTests?: boolean;
|
|
470
|
+
/** Exclude SVG icon components (names matching Svg[A-Z]*) (default: true) */
|
|
471
|
+
excludeSvgIcons?: boolean;
|
|
472
|
+
/** Exclude sub-components detected by directory structure (default: true) */
|
|
473
|
+
excludeSubComponents?: boolean;
|
|
474
|
+
}
|
|
475
|
+
|
|
457
476
|
/**
|
|
458
477
|
* Config file structure
|
|
459
478
|
*/
|
|
@@ -499,6 +518,9 @@ export interface FragmentsConfig {
|
|
|
499
518
|
|
|
500
519
|
/** Performance budgets: preset name or custom config */
|
|
501
520
|
performance?: string | { preset?: string; budgets?: { bundleSize?: number } };
|
|
521
|
+
|
|
522
|
+
/** Storybook adapter filtering configuration */
|
|
523
|
+
storybook?: StorybookFilterConfig;
|
|
502
524
|
}
|
|
503
525
|
|
|
504
526
|
/**
|
package/src/migrate/converter.ts
CHANGED
|
@@ -87,6 +87,7 @@ export function convertToFragment(parsed: ParsedStoryFile): ConversionResult {
|
|
|
87
87
|
const code = generateFragmentCode({
|
|
88
88
|
componentName,
|
|
89
89
|
componentImport: parsed.meta.componentImport,
|
|
90
|
+
isDefaultImport: parsed.meta.isDefaultImport,
|
|
90
91
|
description: parsed.meta.description,
|
|
91
92
|
category,
|
|
92
93
|
tags: parsed.meta.tags,
|
|
@@ -484,6 +485,7 @@ interface GeneratedMetadata {
|
|
|
484
485
|
interface GenerateOptions {
|
|
485
486
|
componentName: string;
|
|
486
487
|
componentImport: string;
|
|
488
|
+
isDefaultImport?: boolean;
|
|
487
489
|
description?: string;
|
|
488
490
|
category: string;
|
|
489
491
|
tags?: string[];
|
|
@@ -504,6 +506,7 @@ function generateFragmentCode(options: GenerateOptions): string {
|
|
|
504
506
|
const {
|
|
505
507
|
componentName,
|
|
506
508
|
componentImport,
|
|
509
|
+
isDefaultImport,
|
|
507
510
|
description,
|
|
508
511
|
category,
|
|
509
512
|
tags,
|
|
@@ -550,8 +553,13 @@ ${generated.skippedVariants.map(sv => ` { name: "${escapeString(sv.name)}",
|
|
|
550
553
|
}
|
|
551
554
|
|
|
552
555
|
// Import the actual component - this makes the fragment immediately usable
|
|
556
|
+
// Use default import when the source component uses export default
|
|
557
|
+
const componentImportStatement = isDefaultImport
|
|
558
|
+
? `import ${componentName} from "${componentImport}";`
|
|
559
|
+
: `import { ${componentName} } from "${componentImport}";`;
|
|
560
|
+
|
|
553
561
|
return `import { defineFragment } from "@fragments-sdk/cli/core";
|
|
554
|
-
|
|
562
|
+
${componentImportStatement}
|
|
555
563
|
|
|
556
564
|
export default defineFragment({
|
|
557
565
|
component: ${componentName},
|
package/src/migrate/parser.ts
CHANGED
|
@@ -235,6 +235,7 @@ function parseMeta(
|
|
|
235
235
|
);
|
|
236
236
|
if (importMatch) {
|
|
237
237
|
result.componentImport = importMatch[1];
|
|
238
|
+
result.isDefaultImport = false;
|
|
238
239
|
} else {
|
|
239
240
|
// Try default import
|
|
240
241
|
const defaultImportMatch = content.match(
|
|
@@ -244,6 +245,7 @@ function parseMeta(
|
|
|
244
245
|
);
|
|
245
246
|
if (defaultImportMatch) {
|
|
246
247
|
result.componentImport = defaultImportMatch[1];
|
|
248
|
+
result.isDefaultImport = true;
|
|
247
249
|
}
|
|
248
250
|
}
|
|
249
251
|
}
|
package/src/migrate/types.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface ParsedMeta {
|
|
|
12
12
|
componentName: string;
|
|
13
13
|
/** Component import path */
|
|
14
14
|
componentImport?: string;
|
|
15
|
+
/** Whether the component uses a default import (export default) */
|
|
16
|
+
isDefaultImport?: boolean;
|
|
15
17
|
/** Tags from the story */
|
|
16
18
|
tags?: string[];
|
|
17
19
|
/** Description from parameters.docs */
|
package/src/setup.ts
CHANGED
|
@@ -2,6 +2,7 @@ import pc from 'picocolors';
|
|
|
2
2
|
import { BRAND } from './core/index.js';
|
|
3
3
|
import { loadConfig, discoverFragmentFiles } from './core/node.js';
|
|
4
4
|
import { buildFragments } from './build.js';
|
|
5
|
+
import { scan } from './commands/scan.js';
|
|
5
6
|
import {
|
|
6
7
|
detectStorybookConfig,
|
|
7
8
|
discoverStoryFiles as discoverStorybookFiles,
|
|
@@ -125,48 +126,78 @@ export async function runSetup(options: SetupOptions = {}): Promise<SetupResult>
|
|
|
125
126
|
let fragmentFiles = await discoverFragmentFiles(config, configDir);
|
|
126
127
|
|
|
127
128
|
if (fragmentFiles.length === 0 && !options.skipStorybook) {
|
|
128
|
-
// No fragment files - check for Storybook
|
|
129
|
+
// No fragment files - check for Storybook stories that the viewer can load directly
|
|
129
130
|
log(pc.yellow('\n No fragment files found'));
|
|
130
131
|
|
|
131
132
|
const sbConfig = await detectStorybookConfig(configDir);
|
|
132
133
|
if (sbConfig) {
|
|
133
134
|
log(pc.dim(` Found Storybook at ${sbConfig.configPath}`));
|
|
134
|
-
log(pc.dim(' Converting stories to fragments...\n'));
|
|
135
135
|
|
|
136
|
-
//
|
|
137
|
-
const
|
|
136
|
+
// Check if config.include already covers story files
|
|
137
|
+
const hasStoryPatterns = config.include.some((p: string) => p.includes('.stories.'));
|
|
138
138
|
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const fragmentResult = convertToFragment(parsed);
|
|
139
|
+
if (hasStoryPatterns) {
|
|
140
|
+
// Stories are in the include config — discover them directly
|
|
141
|
+
// The viewer handles .stories.tsx natively via storyModuleToFragment()
|
|
142
|
+
log(pc.dim(' Stories included in config — viewer will load them directly\n'));
|
|
143
|
+
fragmentFiles = await discoverFragmentFiles(config, configDir);
|
|
145
144
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
await fs.writeFile(fragmentResult.outputFile, fragmentResult.code);
|
|
149
|
-
converted++;
|
|
150
|
-
} catch {
|
|
151
|
-
// Skip files that can't be converted
|
|
152
|
-
}
|
|
145
|
+
if (fragmentFiles.length > 0) {
|
|
146
|
+
log(pc.green(` Found ${fragmentFiles.length} story/fragment file(s)`));
|
|
153
147
|
}
|
|
148
|
+
} else {
|
|
149
|
+
// Stories not in config — fall back to conversion
|
|
150
|
+
log(pc.dim(' Converting stories to fragments...\n'));
|
|
151
|
+
|
|
152
|
+
const storyFiles = await discoverStorybookFiles(configDir, sbConfig.storyPatterns);
|
|
153
|
+
|
|
154
|
+
if (storyFiles.length > 0) {
|
|
155
|
+
let converted = 0;
|
|
156
|
+
for (const storyFile of storyFiles) {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = await parseStoryFile(storyFile);
|
|
159
|
+
const fragmentResult = convertToFragment(parsed);
|
|
160
|
+
|
|
161
|
+
// Create directory and write file
|
|
162
|
+
await fs.mkdir(path.dirname(fragmentResult.outputFile), { recursive: true });
|
|
163
|
+
await fs.writeFile(fragmentResult.outputFile, fragmentResult.code);
|
|
164
|
+
converted++;
|
|
165
|
+
} catch {
|
|
166
|
+
// Skip files that can't be converted
|
|
167
|
+
}
|
|
168
|
+
}
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
result.fragmentFilesCreated = converted;
|
|
171
|
+
log(pc.green(` Generated ${converted} fragment file(s)`));
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
// Refresh fragment files list
|
|
174
|
+
fragmentFiles = await discoverFragmentFiles(config, configDir);
|
|
175
|
+
}
|
|
160
176
|
}
|
|
161
177
|
} else {
|
|
178
|
+
// No Storybook — auto-scan source code to generate fragments.json
|
|
162
179
|
log(pc.dim(' No Storybook config found'));
|
|
163
|
-
log(pc.dim(
|
|
180
|
+
log(pc.dim(' Auto-scanning source code...\n'));
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const scanResult = await scan({
|
|
184
|
+
config: options.configPath,
|
|
185
|
+
verbose: false,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (scanResult.componentCount > 0) {
|
|
189
|
+
result.fragmentsBuilt = scanResult.componentCount;
|
|
190
|
+
log(pc.green(` Scanned ${scanResult.componentCount} component(s) from source`));
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
log(pc.dim(` Run ${pc.cyan(`${BRAND.cliCommand} scan`)} to generate documentation from source`));
|
|
194
|
+
}
|
|
164
195
|
}
|
|
165
196
|
} else if (fragmentFiles.length > 0) {
|
|
166
197
|
log(pc.green(` Found ${fragmentFiles.length} fragment file(s)`));
|
|
167
198
|
}
|
|
168
199
|
|
|
169
|
-
// Step 2: Build fragments.json if needed
|
|
200
|
+
// Step 2: Build fragments.json if needed (only when fragment files exist)
|
|
170
201
|
if (fragmentFiles.length > 0 && !options.skipBuild) {
|
|
171
202
|
const outFile = config.outFile || BRAND.outFile;
|
|
172
203
|
const { stale, missing } = await isFragmentsJsonStale(configDir, outFile);
|
|
@@ -185,7 +216,21 @@ export async function runSetup(options: SetupOptions = {}): Promise<SetupResult>
|
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
218
|
|
|
188
|
-
|
|
219
|
+
if (buildResult.fragmentCount > 0) {
|
|
220
|
+
log(pc.green(` Built ${buildResult.fragmentCount} fragment(s)`));
|
|
221
|
+
} else {
|
|
222
|
+
// Build found 0 fragments — fallback to scan
|
|
223
|
+
log(pc.dim(' No compilable fragments found, falling back to source scan...'));
|
|
224
|
+
try {
|
|
225
|
+
const scanResult = await scan({ verbose: false });
|
|
226
|
+
if (scanResult.componentCount > 0) {
|
|
227
|
+
result.fragmentsBuilt = scanResult.componentCount;
|
|
228
|
+
log(pc.green(` Scanned ${scanResult.componentCount} component(s) from source`));
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// scan failed silently
|
|
232
|
+
}
|
|
233
|
+
}
|
|
189
234
|
} catch (error) {
|
|
190
235
|
result.errors.push(`Build failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
191
236
|
}
|
|
@@ -91,7 +91,7 @@ describe("virtual module @fragments-sdk/cli/core import", () => {
|
|
|
91
91
|
|
|
92
92
|
// The generated virtual module string should reference @fragments-sdk/cli/core
|
|
93
93
|
expect(content).toContain(
|
|
94
|
-
'import { storyModuleToFragment, setPreviewConfig } from "@fragments-sdk/cli/core"'
|
|
94
|
+
'import { storyModuleToFragment, setPreviewConfig, checkStoryExclusion, isForceIncluded, isConfigExcluded } from "@fragments-sdk/cli/core"'
|
|
95
95
|
);
|
|
96
96
|
});
|
|
97
97
|
|