@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
package/src/commands/init.ts
CHANGED
|
@@ -462,9 +462,9 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
462
462
|
|
|
463
463
|
// Step 3: Gather configuration (interactive unless --yes)
|
|
464
464
|
let componentPath = detection.suggestedComponentPath;
|
|
465
|
-
let runScan = scenario === "components";
|
|
465
|
+
let runScan = scenario === "components" || scenario === "stories";
|
|
466
466
|
let createExample = scenario === "fresh";
|
|
467
|
-
let startServer =
|
|
467
|
+
let startServer = false;
|
|
468
468
|
|
|
469
469
|
if (!options.yes) {
|
|
470
470
|
// Ask about component location
|
|
@@ -473,13 +473,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
473
473
|
default: detection.suggestedComponentPath,
|
|
474
474
|
});
|
|
475
475
|
|
|
476
|
-
if (scenario === "
|
|
477
|
-
// For component-only projects, ask about scanning
|
|
478
|
-
runScan = await confirm({
|
|
479
|
-
message: "Auto-generate documentation from TypeScript?",
|
|
480
|
-
default: true,
|
|
481
|
-
});
|
|
482
|
-
} else {
|
|
476
|
+
if (scenario === "fresh") {
|
|
483
477
|
// Fresh project - ask about example
|
|
484
478
|
createExample = await confirm({
|
|
485
479
|
message: "Create an example Button component to get started?",
|
|
@@ -502,7 +496,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
502
496
|
`${componentPath}/**/*.fragment.tsx`,
|
|
503
497
|
];
|
|
504
498
|
|
|
505
|
-
//
|
|
499
|
+
// If Storybook stories detected, also include them for direct rendering
|
|
506
500
|
if (scenario === 'stories') {
|
|
507
501
|
includePaths.push(`${componentPath}/**/*.stories.tsx`);
|
|
508
502
|
includePaths.push(`${componentPath}/**/*.stories.ts`);
|
|
@@ -557,39 +551,9 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
557
551
|
}
|
|
558
552
|
}
|
|
559
553
|
|
|
560
|
-
if (
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
for (const compFile of detection.componentFiles) {
|
|
564
|
-
const absPath = join(projectRoot, compFile);
|
|
565
|
-
const dir = dirname(absPath);
|
|
566
|
-
const fileName = basename(compFile, ".tsx");
|
|
567
|
-
const componentName = toPascalCase(fileName);
|
|
568
|
-
const fragmentPath = join(dir, `${fileName}.fragment.tsx`);
|
|
569
|
-
|
|
570
|
-
// Skip if fragment already exists
|
|
571
|
-
try {
|
|
572
|
-
await access(fragmentPath);
|
|
573
|
-
continue; // already exists
|
|
574
|
-
} catch {
|
|
575
|
-
// doesn't exist, create it
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
try {
|
|
579
|
-
const stub = generateFragmentStub(componentName, `./${fileName}`);
|
|
580
|
-
await writeFile(fragmentPath, stub, "utf-8");
|
|
581
|
-
stubsCreated++;
|
|
582
|
-
} catch {
|
|
583
|
-
// skip files we can't write to
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (stubsCreated > 0) {
|
|
588
|
-
console.log(pc.green(`✓ Generated ${stubsCreated} fragment stub(s)`));
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Run scan to generate fragments.json
|
|
592
|
-
console.log(pc.dim("\nGenerating documentation from source code...\n"));
|
|
554
|
+
if (runScan) {
|
|
555
|
+
// Run scan to generate fragments.json from source code
|
|
556
|
+
console.log(pc.dim("\nScanning source code for documentation...\n"));
|
|
593
557
|
try {
|
|
594
558
|
const { scan } = await import("./scan.js");
|
|
595
559
|
await scan({
|
|
@@ -641,14 +605,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
641
605
|
console.log(pc.green("\n✓ Setup complete!\n"));
|
|
642
606
|
|
|
643
607
|
if (startServer) {
|
|
644
|
-
|
|
645
|
-
scenario === "stories"
|
|
646
|
-
? `Your ${detection.storyFiles.length} stories are loading...`
|
|
647
|
-
: scenario === "components"
|
|
648
|
-
? `Your ${detection.componentFiles.length} components are being documented...`
|
|
649
|
-
: `Your first component is ready!`;
|
|
650
|
-
|
|
651
|
-
console.log(pc.cyan(serverMessage));
|
|
608
|
+
console.log(pc.cyan("Starting viewer...\n"));
|
|
652
609
|
startDevServer(projectRoot);
|
|
653
610
|
} else {
|
|
654
611
|
console.log(pc.cyan("Next steps:"));
|
|
@@ -656,6 +613,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
656
613
|
if (scenario === "fresh") {
|
|
657
614
|
console.log(` 2. Edit ${pc.bold(`${componentPath}/Button/Button.fragment.tsx`)}`);
|
|
658
615
|
}
|
|
616
|
+
console.log(` 3. Run ${pc.bold(`${BRAND.cliCommand} generate`)} to create fragment files for your components`);
|
|
659
617
|
console.log();
|
|
660
618
|
}
|
|
661
619
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -2,9 +2,16 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { resolve, dirname } from 'node:path';
|
|
3
3
|
import { createJiti } from 'jiti';
|
|
4
4
|
import { BRAND } from './constants.js';
|
|
5
|
-
import type { FragmentsConfig } from './types.js';
|
|
5
|
+
import type { FragmentsConfig, StorybookFilterConfig } from './types.js';
|
|
6
6
|
import { fragmentsConfigSchema } from './schema.js';
|
|
7
7
|
|
|
8
|
+
const STORYBOOK_FILTER_DEFAULTS: StorybookFilterConfig = {
|
|
9
|
+
excludeDeprecated: true,
|
|
10
|
+
excludeTests: true,
|
|
11
|
+
excludeSvgIcons: true,
|
|
12
|
+
excludeSubComponents: true,
|
|
13
|
+
};
|
|
14
|
+
|
|
8
15
|
const DEFAULT_CONFIG: FragmentsConfig = {
|
|
9
16
|
include: [
|
|
10
17
|
`src/**/*${BRAND.fileExtension}`, // *.fragment.tsx files
|
|
@@ -13,6 +20,7 @@ const DEFAULT_CONFIG: FragmentsConfig = {
|
|
|
13
20
|
exclude: ['**/node_modules/**'],
|
|
14
21
|
components: ['src/**/index.tsx', 'src/**/*.tsx'],
|
|
15
22
|
framework: 'react',
|
|
23
|
+
storybook: STORYBOOK_FILTER_DEFAULTS,
|
|
16
24
|
snippets: {
|
|
17
25
|
mode: 'warn',
|
|
18
26
|
scope: 'snippet+render',
|
|
@@ -77,8 +85,13 @@ export async function loadConfig(configPath?: string): Promise<{
|
|
|
77
85
|
throw new Error(`Invalid config in ${resolvedPath}:\n${errors}`);
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
const merged: FragmentsConfig = { ...DEFAULT_CONFIG, ...result.data };
|
|
89
|
+
|
|
90
|
+
// Deep-merge storybook filter defaults so consumers only override what they need
|
|
91
|
+
merged.storybook = { ...STORYBOOK_FILTER_DEFAULTS, ...result.data?.storybook };
|
|
92
|
+
|
|
80
93
|
return {
|
|
81
|
-
config:
|
|
94
|
+
config: merged,
|
|
82
95
|
configDir: dirname(resolvedPath),
|
|
83
96
|
};
|
|
84
97
|
} catch (error) {
|
|
@@ -13,6 +13,8 @@ import type { RegistryPropEntry } from "../fragment-types.js";
|
|
|
13
13
|
export interface ExtractedProps {
|
|
14
14
|
/** Component name */
|
|
15
15
|
componentName: string;
|
|
16
|
+
/** Whether the component uses export default */
|
|
17
|
+
isDefaultExport?: boolean;
|
|
16
18
|
/** Props interface name (e.g., "ButtonProps") */
|
|
17
19
|
propsInterfaceName?: string;
|
|
18
20
|
/** Extracted props */
|
|
@@ -56,6 +58,7 @@ export function extractPropsFromSource(
|
|
|
56
58
|
// Find all exports and props interfaces
|
|
57
59
|
const propsInterfaces = new Map<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>();
|
|
58
60
|
const componentExports: string[] = [];
|
|
61
|
+
const defaultExports = new Set<string>();
|
|
59
62
|
const importedModules: string[] = [];
|
|
60
63
|
|
|
61
64
|
ts.forEachChild(sourceFile, (node) => {
|
|
@@ -88,8 +91,14 @@ export function extractPropsFromSource(
|
|
|
88
91
|
const hasExport = node.modifiers?.some(
|
|
89
92
|
(m) => m.kind === ts.SyntaxKind.ExportKeyword
|
|
90
93
|
);
|
|
94
|
+
const hasDefault = node.modifiers?.some(
|
|
95
|
+
(m) => m.kind === ts.SyntaxKind.DefaultKeyword
|
|
96
|
+
);
|
|
91
97
|
if (hasExport) {
|
|
92
98
|
componentExports.push(node.name.text);
|
|
99
|
+
if (hasDefault) {
|
|
100
|
+
defaultExports.add(node.name.text);
|
|
101
|
+
}
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -130,6 +139,7 @@ export function extractPropsFromSource(
|
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
result.componentName = mainComponent;
|
|
142
|
+
result.isDefaultExport = defaultExports.has(mainComponent);
|
|
133
143
|
|
|
134
144
|
// Find matching props interface
|
|
135
145
|
const propsInterfaceName = `${mainComponent}Props`;
|
package/src/core/index.ts
CHANGED
|
@@ -57,6 +57,8 @@ export type {
|
|
|
57
57
|
FigmaInstanceMapping,
|
|
58
58
|
FigmaChildrenMapping,
|
|
59
59
|
FigmaTextContentMapping,
|
|
60
|
+
// Storybook filter configuration
|
|
61
|
+
StorybookFilterConfig,
|
|
60
62
|
// Token configuration
|
|
61
63
|
TokenConfig,
|
|
62
64
|
// Compiled token types
|
|
@@ -141,6 +143,19 @@ export type {
|
|
|
141
143
|
CSF2Story,
|
|
142
144
|
} from "./storyAdapter.js";
|
|
143
145
|
|
|
146
|
+
// Storybook adapter filtering
|
|
147
|
+
export {
|
|
148
|
+
checkStoryExclusion,
|
|
149
|
+
detectSubComponentPaths,
|
|
150
|
+
isForceIncluded,
|
|
151
|
+
isConfigExcluded,
|
|
152
|
+
} from "./storyFilters.js";
|
|
153
|
+
export type {
|
|
154
|
+
ExclusionReason,
|
|
155
|
+
ExclusionResult,
|
|
156
|
+
CheckStoryExclusionOpts,
|
|
157
|
+
} from "./storyFilters.js";
|
|
158
|
+
|
|
144
159
|
// Context generation for AI agents
|
|
145
160
|
export { generateContext } from "./context.js";
|
|
146
161
|
export type { ContextOptions, ContextResult } from "./context.js";
|
package/src/core/schema.ts
CHANGED
|
@@ -63,8 +63,8 @@ export const fragmentMetaSchema = z.object({
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
export const fragmentUsageSchema = z.object({
|
|
66
|
-
when: z.array(z.string())
|
|
67
|
-
whenNot: z.array(z.string())
|
|
66
|
+
when: z.array(z.string()),
|
|
67
|
+
whenNot: z.array(z.string()),
|
|
68
68
|
guidelines: z.array(z.string()).optional(),
|
|
69
69
|
accessibility: z.array(z.string()).optional(),
|
|
70
70
|
});
|
|
@@ -213,6 +213,14 @@ export const fragmentsConfigSchema = z.object({
|
|
|
213
213
|
}).optional(),
|
|
214
214
|
}),
|
|
215
215
|
]).optional(),
|
|
216
|
+
storybook: z.object({
|
|
217
|
+
exclude: z.array(z.string()).optional(),
|
|
218
|
+
include: z.array(z.string()).optional(),
|
|
219
|
+
excludeDeprecated: z.boolean().optional(),
|
|
220
|
+
excludeTests: z.boolean().optional(),
|
|
221
|
+
excludeSvgIcons: z.boolean().optional(),
|
|
222
|
+
excludeSubComponents: z.boolean().optional(),
|
|
223
|
+
}).optional(),
|
|
216
224
|
});
|
|
217
225
|
|
|
218
226
|
/**
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Storybook adapter filtering.
|
|
3
|
+
* Tests per-file heuristics, cross-file sub-component detection, and config precedence.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
checkStoryExclusion,
|
|
9
|
+
detectSubComponentPaths,
|
|
10
|
+
isForceIncluded,
|
|
11
|
+
isConfigExcluded,
|
|
12
|
+
type ExclusionResult,
|
|
13
|
+
} from './storyFilters.js';
|
|
14
|
+
import type { StorybookFilterConfig } from './types.js';
|
|
15
|
+
|
|
16
|
+
const DEFAULTS: StorybookFilterConfig = {
|
|
17
|
+
excludeDeprecated: true,
|
|
18
|
+
excludeTests: true,
|
|
19
|
+
excludeSvgIcons: true,
|
|
20
|
+
excludeSubComponents: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// detectSubComponentPaths
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
describe('detectSubComponentPaths', () => {
|
|
28
|
+
it('identifies sub-components when one story matches the directory name', () => {
|
|
29
|
+
const files = [
|
|
30
|
+
{ relativePath: 'src/components/Form/Form.stories.tsx' },
|
|
31
|
+
{ relativePath: 'src/components/Form/Checkbox.stories.tsx' },
|
|
32
|
+
{ relativePath: 'src/components/Form/RadioGroup.stories.tsx' },
|
|
33
|
+
];
|
|
34
|
+
const result = detectSubComponentPaths(files);
|
|
35
|
+
|
|
36
|
+
expect(result.size).toBe(2);
|
|
37
|
+
expect(result.get('src/components/Form/Checkbox.stories.tsx')).toBe('Form');
|
|
38
|
+
expect(result.get('src/components/Form/RadioGroup.stories.tsx')).toBe('Form');
|
|
39
|
+
expect(result.has('src/components/Form/Form.stories.tsx')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('treats story in its own sub-directory as standalone', () => {
|
|
43
|
+
const files = [
|
|
44
|
+
{ relativePath: 'src/components/Form/Form.stories.tsx' },
|
|
45
|
+
{ relativePath: 'src/components/Button/Button.stories.tsx' },
|
|
46
|
+
];
|
|
47
|
+
const result = detectSubComponentPaths(files);
|
|
48
|
+
expect(result.size).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps single story in a directory as standalone', () => {
|
|
52
|
+
const files = [
|
|
53
|
+
{ relativePath: 'src/components/Button/Button.stories.tsx' },
|
|
54
|
+
];
|
|
55
|
+
const result = detectSubComponentPaths(files);
|
|
56
|
+
expect(result.size).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('keeps all stories when no story matches the directory name', () => {
|
|
60
|
+
const files = [
|
|
61
|
+
{ relativePath: 'src/components/Inputs/Checkbox.stories.tsx' },
|
|
62
|
+
{ relativePath: 'src/components/Inputs/RadioGroup.stories.tsx' },
|
|
63
|
+
{ relativePath: 'src/components/Inputs/Toggle.stories.tsx' },
|
|
64
|
+
];
|
|
65
|
+
const result = detectSubComponentPaths(files);
|
|
66
|
+
expect(result.size).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('detects three stories with one primary → two sub-components', () => {
|
|
70
|
+
const files = [
|
|
71
|
+
{ relativePath: 'src/components/Table/Table.stories.tsx' },
|
|
72
|
+
{ relativePath: 'src/components/Table/TableHeader.stories.tsx' },
|
|
73
|
+
{ relativePath: 'src/components/Table/TableRow.stories.tsx' },
|
|
74
|
+
];
|
|
75
|
+
const result = detectSubComponentPaths(files);
|
|
76
|
+
|
|
77
|
+
expect(result.size).toBe(2);
|
|
78
|
+
expect(result.get('src/components/Table/TableHeader.stories.tsx')).toBe('Table');
|
|
79
|
+
expect(result.get('src/components/Table/TableRow.stories.tsx')).toBe('Table');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('ignores non-story file patterns', () => {
|
|
83
|
+
const files = [
|
|
84
|
+
{ relativePath: 'src/components/Form/Form.fragment.tsx' },
|
|
85
|
+
{ relativePath: 'src/components/Form/Checkbox.fragment.tsx' },
|
|
86
|
+
];
|
|
87
|
+
const result = detectSubComponentPaths(files);
|
|
88
|
+
expect(result.size).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// checkStoryExclusion
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
describe('checkStoryExclusion', () => {
|
|
97
|
+
const base = {
|
|
98
|
+
componentName: 'Button',
|
|
99
|
+
variantCount: 3,
|
|
100
|
+
filePath: 'src/components/Button/Button.stories.tsx',
|
|
101
|
+
config: DEFAULTS,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
it('keeps a normal component', () => {
|
|
105
|
+
const result = checkStoryExclusion({
|
|
106
|
+
...base,
|
|
107
|
+
storybookTitle: 'Components/Actions/Button',
|
|
108
|
+
});
|
|
109
|
+
expect(result.excluded).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('excludes deprecated titles', () => {
|
|
113
|
+
const result = checkStoryExclusion({
|
|
114
|
+
...base,
|
|
115
|
+
storybookTitle: 'Deprecated/OldButton',
|
|
116
|
+
});
|
|
117
|
+
expect(result).toEqual<ExclusionResult>({
|
|
118
|
+
excluded: true,
|
|
119
|
+
reason: 'deprecated',
|
|
120
|
+
detail: expect.stringContaining('Deprecated'),
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('excludes titles ending with /test', () => {
|
|
125
|
+
const result = checkStoryExclusion({
|
|
126
|
+
...base,
|
|
127
|
+
storybookTitle: 'Components/Button/Test',
|
|
128
|
+
});
|
|
129
|
+
expect(result.excluded).toBe(true);
|
|
130
|
+
expect(result.reason).toBe('test-story');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('excludes titles ending with /tests', () => {
|
|
134
|
+
const result = checkStoryExclusion({
|
|
135
|
+
...base,
|
|
136
|
+
storybookTitle: 'Components/Button/Tests',
|
|
137
|
+
});
|
|
138
|
+
expect(result.excluded).toBe(true);
|
|
139
|
+
expect(result.reason).toBe('test-story');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('excludes *.test.stories.* file paths', () => {
|
|
143
|
+
const result = checkStoryExclusion({
|
|
144
|
+
...base,
|
|
145
|
+
filePath: 'src/components/Button/Button.test.stories.tsx',
|
|
146
|
+
});
|
|
147
|
+
expect(result.excluded).toBe(true);
|
|
148
|
+
expect(result.reason).toBe('test-story');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('excludes SVG icons by displayName', () => {
|
|
152
|
+
const result = checkStoryExclusion({
|
|
153
|
+
...base,
|
|
154
|
+
componentName: 'HomeLarge',
|
|
155
|
+
componentDisplayName: 'SvgHomeLarge',
|
|
156
|
+
});
|
|
157
|
+
expect(result.excluded).toBe(true);
|
|
158
|
+
expect(result.reason).toBe('svg-icon');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('excludes SVG icons by function name', () => {
|
|
162
|
+
const result = checkStoryExclusion({
|
|
163
|
+
...base,
|
|
164
|
+
componentName: 'HomeLarge',
|
|
165
|
+
componentFunctionName: 'SvgHomeLarge',
|
|
166
|
+
});
|
|
167
|
+
expect(result.excluded).toBe(true);
|
|
168
|
+
expect(result.reason).toBe('svg-icon');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('excludes SVG icons by component name', () => {
|
|
172
|
+
const result = checkStoryExclusion({
|
|
173
|
+
...base,
|
|
174
|
+
componentName: 'SvgArrowRight',
|
|
175
|
+
});
|
|
176
|
+
expect(result.excluded).toBe(true);
|
|
177
|
+
expect(result.reason).toBe('svg-icon');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('excludes hidden-tagged stories', () => {
|
|
181
|
+
const result = checkStoryExclusion({
|
|
182
|
+
...base,
|
|
183
|
+
tags: ['hidden'],
|
|
184
|
+
});
|
|
185
|
+
expect(result.excluded).toBe(true);
|
|
186
|
+
expect(result.reason).toBe('tag-excluded');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('excludes internal-tagged stories', () => {
|
|
190
|
+
const result = checkStoryExclusion({
|
|
191
|
+
...base,
|
|
192
|
+
tags: ['internal'],
|
|
193
|
+
});
|
|
194
|
+
expect(result.excluded).toBe(true);
|
|
195
|
+
expect(result.reason).toBe('tag-excluded');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('excludes no-fragment-tagged stories', () => {
|
|
199
|
+
const result = checkStoryExclusion({
|
|
200
|
+
...base,
|
|
201
|
+
tags: ['no-fragment'],
|
|
202
|
+
});
|
|
203
|
+
expect(result.excluded).toBe(true);
|
|
204
|
+
expect(result.reason).toBe('tag-excluded');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('excludes stories with zero variants', () => {
|
|
208
|
+
const result = checkStoryExclusion({
|
|
209
|
+
...base,
|
|
210
|
+
variantCount: 0,
|
|
211
|
+
});
|
|
212
|
+
expect(result.excluded).toBe(true);
|
|
213
|
+
expect(result.reason).toBe('empty-variants');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Config precedence
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
describe('config precedence', () => {
|
|
222
|
+
it('force-includes a component despite SVG icon rule', () => {
|
|
223
|
+
const result = checkStoryExclusion({
|
|
224
|
+
componentName: 'SvgHomeLarge',
|
|
225
|
+
variantCount: 1,
|
|
226
|
+
filePath: 'src/icons/SvgHomeLarge.stories.tsx',
|
|
227
|
+
config: { ...DEFAULTS, include: ['SvgHomeLarge'] },
|
|
228
|
+
});
|
|
229
|
+
expect(result.excluded).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('force-includes a component despite deprecated rule', () => {
|
|
233
|
+
const result = checkStoryExclusion({
|
|
234
|
+
componentName: 'OldButton',
|
|
235
|
+
storybookTitle: 'Deprecated/OldButton',
|
|
236
|
+
variantCount: 1,
|
|
237
|
+
filePath: 'src/components/OldButton.stories.tsx',
|
|
238
|
+
config: { ...DEFAULTS, include: ['OldButton'] },
|
|
239
|
+
});
|
|
240
|
+
expect(result.excluded).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('keeps deprecated when excludeDeprecated is false', () => {
|
|
244
|
+
const result = checkStoryExclusion({
|
|
245
|
+
componentName: 'OldButton',
|
|
246
|
+
storybookTitle: 'Deprecated/OldButton',
|
|
247
|
+
variantCount: 1,
|
|
248
|
+
filePath: 'src/components/OldButton.stories.tsx',
|
|
249
|
+
config: { ...DEFAULTS, excludeDeprecated: false },
|
|
250
|
+
});
|
|
251
|
+
expect(result.excluded).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('keeps test stories when excludeTests is false', () => {
|
|
255
|
+
const result = checkStoryExclusion({
|
|
256
|
+
componentName: 'Button',
|
|
257
|
+
storybookTitle: 'Components/Button/Tests',
|
|
258
|
+
variantCount: 1,
|
|
259
|
+
filePath: 'src/components/Button.stories.tsx',
|
|
260
|
+
config: { ...DEFAULTS, excludeTests: false },
|
|
261
|
+
});
|
|
262
|
+
expect(result.excluded).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('keeps SVG icons when excludeSvgIcons is false', () => {
|
|
266
|
+
const result = checkStoryExclusion({
|
|
267
|
+
componentName: 'SvgArrowRight',
|
|
268
|
+
variantCount: 1,
|
|
269
|
+
filePath: 'src/icons/SvgArrowRight.stories.tsx',
|
|
270
|
+
config: { ...DEFAULTS, excludeSvgIcons: false },
|
|
271
|
+
});
|
|
272
|
+
expect(result.excluded).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('config-excludes a component by name', () => {
|
|
276
|
+
const result = checkStoryExclusion({
|
|
277
|
+
componentName: 'JCB',
|
|
278
|
+
variantCount: 1,
|
|
279
|
+
filePath: 'src/components/JCB.stories.tsx',
|
|
280
|
+
config: { ...DEFAULTS, exclude: ['JCB'] },
|
|
281
|
+
});
|
|
282
|
+
expect(result.excluded).toBe(true);
|
|
283
|
+
expect(result.reason).toBe('config-excluded');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('config-excludes with wildcard pattern', () => {
|
|
287
|
+
const result = checkStoryExclusion({
|
|
288
|
+
componentName: 'SvgHomeLarge',
|
|
289
|
+
variantCount: 1,
|
|
290
|
+
filePath: 'src/icons/SvgHomeLarge.stories.tsx',
|
|
291
|
+
config: { ...DEFAULTS, exclude: ['Svg*'] },
|
|
292
|
+
});
|
|
293
|
+
expect(result.excluded).toBe(true);
|
|
294
|
+
expect(result.reason).toBe('config-excluded');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('include wins over exclude', () => {
|
|
298
|
+
const result = checkStoryExclusion({
|
|
299
|
+
componentName: 'SvgHomeLarge',
|
|
300
|
+
variantCount: 1,
|
|
301
|
+
filePath: 'src/icons/SvgHomeLarge.stories.tsx',
|
|
302
|
+
config: { ...DEFAULTS, include: ['SvgHomeLarge'], exclude: ['Svg*'] },
|
|
303
|
+
});
|
|
304
|
+
expect(result.excluded).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// isForceIncluded / isConfigExcluded helpers
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
describe('isForceIncluded', () => {
|
|
313
|
+
it('returns false with no include patterns', () => {
|
|
314
|
+
expect(isForceIncluded('Button', {})).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('matches exact name', () => {
|
|
318
|
+
expect(isForceIncluded('Button', { include: ['Button'] })).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('matches wildcard prefix', () => {
|
|
322
|
+
expect(isForceIncluded('SvgArrowRight', { include: ['Svg*'] })).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('does not match non-matching pattern', () => {
|
|
326
|
+
expect(isForceIncluded('Button', { include: ['Svg*'] })).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('matches suffix wildcard', () => {
|
|
330
|
+
expect(isForceIncluded('BigButton', { include: ['*Button'] })).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('matches contains wildcard', () => {
|
|
334
|
+
expect(isForceIncluded('MyButtonGroup', { include: ['*Button*'] })).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('isConfigExcluded', () => {
|
|
339
|
+
it('returns false with no exclude patterns', () => {
|
|
340
|
+
expect(isConfigExcluded('Button', {})).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('matches exact name', () => {
|
|
344
|
+
expect(isConfigExcluded('JCB', { exclude: ['JCB'] })).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('matches wildcard', () => {
|
|
348
|
+
expect(isConfigExcluded('SvgHomeLarge', { exclude: ['Svg*'] })).toBe(true);
|
|
349
|
+
});
|
|
350
|
+
});
|