@fragments-sdk/cli 0.9.0 → 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.
Files changed (123) hide show
  1. package/dist/bin.js +83 -33
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
  4. package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
  5. package/dist/chunk-BW3ZATBW.js.map +1 -0
  6. package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
  7. package/dist/chunk-D7372LQX.js.map +1 -0
  8. package/dist/chunk-EZYXYWNF.js +131 -0
  9. package/dist/chunk-EZYXYWNF.js.map +1 -0
  10. package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
  11. package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
  12. package/dist/chunk-NVSPGSKB.js.map +1 -0
  13. package/dist/core/index.d.ts +105 -3
  14. package/dist/core/index.js +12 -2
  15. package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
  16. package/dist/generate-LQA2R7FN.js +461 -0
  17. package/dist/generate-LQA2R7FN.js.map +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +5 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/{init-KSAAS7X3.js → init-2GEGVIUQ.js} +13 -75
  22. package/dist/init-2GEGVIUQ.js.map +1 -0
  23. package/dist/mcp-bin.js +4 -3
  24. package/dist/mcp-bin.js.map +1 -1
  25. package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
  26. package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
  27. package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
  28. package/dist/storyFilters-3LUYAFZF.js +15 -0
  29. package/dist/storyFilters-3LUYAFZF.js.map +1 -0
  30. package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
  31. package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
  32. package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
  33. package/dist/{viewer-SBTJDMP7.js → viewer-RFA2KVBG.js} +243 -18
  34. package/dist/viewer-RFA2KVBG.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/build.ts +12 -2
  37. package/src/commands/build.ts +16 -2
  38. package/src/commands/generate.ts +383 -68
  39. package/src/commands/init.ts +9 -51
  40. package/src/core/config.ts +15 -2
  41. package/src/core/generators/typescript-extractor.ts +10 -0
  42. package/src/core/index.ts +15 -0
  43. package/src/core/schema.ts +10 -2
  44. package/src/core/storyFilters.test.ts +350 -0
  45. package/src/core/storyFilters.ts +253 -0
  46. package/src/core/types.ts +22 -0
  47. package/src/migrate/converter.ts +9 -1
  48. package/src/migrate/parser.ts +2 -0
  49. package/src/migrate/types.ts +2 -0
  50. package/src/setup.ts +69 -24
  51. package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
  52. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  53. package/src/viewer/components/ActionsPanel.tsx +31 -29
  54. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  55. package/src/viewer/components/App.tsx +187 -740
  56. package/src/viewer/components/BottomPanel.tsx +228 -132
  57. package/src/viewer/components/CodePanel.tsx +1 -1
  58. package/src/viewer/components/CommandPalette.tsx +7 -10
  59. package/src/viewer/components/ComponentDocView.tsx +164 -0
  60. package/src/viewer/components/ComponentGraph.tsx +111 -142
  61. package/src/viewer/components/ContractPanel.tsx +6 -6
  62. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  63. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  64. package/src/viewer/components/FragmentEditor.tsx +92 -115
  65. package/src/viewer/components/HeaderSearch.tsx +24 -0
  66. package/src/viewer/components/HealthDashboard.tsx +16 -2
  67. package/src/viewer/components/Icons.tsx +9 -0
  68. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  69. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  70. package/src/viewer/components/LandingPage.tsx +3 -3
  71. package/src/viewer/components/LeftSidebar.tsx +141 -63
  72. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  73. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  74. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  75. package/src/viewer/components/PanelShell.tsx +161 -0
  76. package/src/viewer/components/PerformancePanel.tsx +31 -28
  77. package/src/viewer/components/PreviewArea.tsx +1 -1
  78. package/src/viewer/components/PreviewAside.tsx +168 -0
  79. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  80. package/src/viewer/components/PropsEditor.tsx +70 -156
  81. package/src/viewer/components/ResizablePanel.tsx +103 -263
  82. package/src/viewer/components/RightSidebar.tsx +3 -9
  83. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  84. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  85. package/src/viewer/components/TopToolbar.tsx +159 -0
  86. package/src/viewer/components/VariantMatrix.tsx +42 -86
  87. package/src/viewer/components/VariantTabs.tsx +3 -3
  88. package/src/viewer/components/ViewerHeader.tsx +69 -0
  89. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  90. package/src/viewer/components/viewer-utils.ts +16 -0
  91. package/src/viewer/entry.tsx +5 -0
  92. package/src/viewer/hooks/useAppState.ts +27 -4
  93. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  94. package/src/viewer/preview-frame.html +6 -12
  95. package/src/viewer/server.ts +169 -2
  96. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  97. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  98. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  99. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +6 -18
  100. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  101. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +5 -16
  102. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  103. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  104. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  105. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
  106. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  107. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  108. package/src/viewer/vendor/shared/src/index.ts +8 -0
  109. package/src/viewer/vendor/shared/src/types.ts +12 -0
  110. package/src/viewer/vite-plugin.ts +109 -4
  111. package/dist/chunk-2JIKCJX3.js.map +0 -1
  112. package/dist/chunk-CJEGT3WD.js.map +0 -1
  113. package/dist/chunk-GOVI6COW.js.map +0 -1
  114. package/dist/generate-35OIMW4Y.js +0 -252
  115. package/dist/generate-35OIMW4Y.js.map +0 -1
  116. package/dist/init-KSAAS7X3.js.map +0 -1
  117. package/dist/viewer-SBTJDMP7.js.map +0 -1
  118. /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
  119. /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
  120. /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
  121. /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
  122. /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
  123. /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
  /**
@@ -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
- import { ${componentName} } from "${componentImport}";
562
+ ${componentImportStatement}
555
563
 
556
564
  export default defineFragment({
557
565
  component: ${componentName},
@@ -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
  }
@@ -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
- // Discover and convert stories
137
- const storyFiles = await discoverStorybookFiles(configDir, sbConfig.storyPatterns);
136
+ // Check if config.include already covers story files
137
+ const hasStoryPatterns = config.include.some((p: string) => p.includes('.stories.'));
138
138
 
139
- if (storyFiles.length > 0) {
140
- let converted = 0;
141
- for (const storyFile of storyFiles) {
142
- try {
143
- const parsed = await parseStoryFile(storyFile);
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
- // Create directory and write file
147
- await fs.mkdir(path.dirname(fragmentResult.outputFile), { recursive: true });
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
- result.fragmentFilesCreated = converted;
156
- log(pc.green(` Generated ${converted} fragment file(s)`));
170
+ result.fragmentFilesCreated = converted;
171
+ log(pc.green(` Generated ${converted} fragment file(s)`));
157
172
 
158
- // Refresh fragment files list
159
- fragmentFiles = await discoverFragmentFiles(config, configDir);
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(` Run ${pc.cyan(`${BRAND.cliCommand} add <ComponentName>`)} to create your first fragment`));
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
- log(pc.green(` Built ${buildResult.fragmentCount} fragment(s)`));
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