@fragments-sdk/cli 0.14.3 → 0.15.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/README.md +0 -3
- package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +4745 -3817
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
- package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
- package/dist/chunk-32LIWN2P.js.map +1 -0
- package/dist/chunk-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-APTQIBS5.js → chunk-6SQPP47U.js} +153 -1342
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/chunk-7DZC4YEV.js +294 -0
- package/dist/chunk-7DZC4YEV.js.map +1 -0
- package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
- package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
- package/dist/{chunk-55KERLWL.js → chunk-HQ6A6DTV.js} +1587 -1073
- package/dist/chunk-HQ6A6DTV.js.map +1 -0
- package/dist/chunk-MHIBEEW4.js +511 -0
- package/dist/chunk-MHIBEEW4.js.map +1 -0
- package/dist/{chunk-5A6X2Y73.js → chunk-ONUP6Z4W.js} +25 -13
- package/dist/chunk-ONUP6Z4W.js.map +1 -0
- package/dist/chunk-QCN35LJU.js +630 -0
- package/dist/chunk-QCN35LJU.js.map +1 -0
- package/dist/chunk-T47OLCSF.js +36 -0
- package/dist/chunk-T47OLCSF.js.map +1 -0
- package/dist/codebase-scanner-MQHUZC2G.js +21 -0
- package/dist/converter-7XM3Y6NJ.js +33 -0
- package/dist/converter-7XM3Y6NJ.js.map +1 -0
- package/dist/core/index.js +43 -2
- package/dist/create-IH4R45GE.js +806 -0
- package/dist/create-IH4R45GE.js.map +1 -0
- package/dist/{generate-RYWIPDN2.js → generate-PVOLUAAC.js} +4 -6
- package/dist/{generate-RYWIPDN2.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/govern-scan-OYFZYOQW.js +413 -0
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +4 -23
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/{init-WRUSW7R5.js → init-SSGUSP7Z.js} +131 -129
- package/dist/init-SSGUSP7Z.js.map +1 -0
- package/dist/{init-cloud-REQ3XLHO.js → init-cloud-3DNKPWFB.js} +30 -5
- package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +5 -37
- package/dist/mcp-bin.js.map +1 -1
- package/dist/node-37AUE74M.js +65 -0
- package/dist/push-contracts-WY32TFP6.js +84 -0
- package/dist/push-contracts-WY32TFP6.js.map +1 -0
- package/dist/scan-PKSYSTRR.js +15 -0
- package/dist/{scan-generate-TFZVL3BT.js → scan-generate-VY27PIOX.js} +340 -52
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/scanner-4KZNOXAK.js +12 -0
- package/dist/{service-HKJ6B7P7.js → service-QJGWUIVL.js} +41 -30
- package/dist/{snapshot-C5DYIGIV.js → snapshot-WIJMEIFT.js} +2 -3
- package/dist/{snapshot-C5DYIGIV.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-DUVC4UIM.js → static-viewer-7QIBQZRC.js} +3 -4
- package/dist/static-viewer-7QIBQZRC.js.map +1 -0
- package/dist/{test-JW7JIDFG.js → test-64Z5BKBA.js} +4 -7
- package/dist/{test-JW7JIDFG.js.map → test-64Z5BKBA.js.map} +1 -1
- package/dist/token-normalizer-TEPOVBPV.js +312 -0
- package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
- package/dist/token-parser-32KOIOFN.js +22 -0
- package/dist/token-parser-32KOIOFN.js.map +1 -0
- package/dist/{tokens-KE73G5JC.js → tokens-NZWFQIAB.js} +10 -9
- package/dist/{tokens-KE73G5JC.js.map → tokens-NZWFQIAB.js.map} +1 -1
- package/dist/tokens-generate-5JQSJ27E.js +85 -0
- package/dist/tokens-generate-5JQSJ27E.js.map +1 -0
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +8 -6
- package/src/bin.ts +300 -48
- package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
- package/src/commands/__tests__/build-freshness.test.ts +231 -0
- package/src/commands/__tests__/create.test.ts +71 -0
- package/src/commands/__tests__/drift-sync.test.ts +1 -1
- package/src/commands/__tests__/govern.test.ts +258 -0
- package/src/commands/__tests__/init.test.ts +113 -0
- package/src/commands/__tests__/scan-generate.test.ts +189 -70
- package/src/commands/__tests__/verify.test.ts +91 -0
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +536 -0
- package/src/commands/discover.ts +151 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/enhance.ts +3 -1
- package/src/commands/govern-scan.ts +565 -0
- package/src/commands/govern.ts +67 -4
- package/src/commands/init-cloud.ts +32 -4
- package/src/commands/init.ts +152 -28
- package/src/commands/inspect.ts +290 -0
- package/src/commands/migrate-contract.ts +85 -0
- package/src/commands/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +439 -51
- package/src/commands/scan.ts +14 -0
- package/src/commands/setup.ts +27 -50
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-generate.ts +113 -0
- package/src/commands/tokens-push.ts +199 -0
- package/src/commands/verify.ts +195 -1
- package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
- package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
- package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
- package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
- package/src/core/__tests__/contract-parity.test.ts +316 -0
- package/src/core/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +40 -1
- package/src/core/config.ts +2 -1
- package/src/core/discovery.ts +13 -2
- package/src/core/drift-verifier.ts +123 -0
- package/src/core/extractor-adapter.ts +80 -0
- package/src/index.ts +3 -3
- package/src/mcp/__tests__/projectFields.test.ts +1 -1
- package/src/mcp/utils.ts +1 -50
- package/src/migrate/converter.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +253 -0
- package/src/migrate/report.ts +1 -1
- package/src/scripts/token-benchmark.ts +121 -0
- package/src/service/__tests__/props-extractor.test.ts +94 -0
- package/src/service/__tests__/token-normalizer.test.ts +690 -0
- package/src/service/ast-utils.ts +4 -23
- package/src/service/babel-config.ts +23 -0
- package/src/service/enhance/converter.ts +61 -0
- package/src/service/enhance/props-extractor.ts +25 -8
- package/src/service/enhance/scanner.ts +5 -24
- package/src/service/index.ts +8 -0
- package/src/service/snippet-validation.ts +9 -3
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-normalizer.ts +510 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/shared/index.ts +1 -0
- package/src/shared/project-fields.ts +46 -0
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +16 -1
- package/src/theme/schema.ts +8 -0
- package/src/theme/serializer.ts +13 -9
- package/src/theme/types.ts +8 -0
- package/src/validators.ts +1 -2
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
- package/src/viewer/style-utils.ts +27 -412
- package/src/viewer/vite-plugin.ts +2 -2
- package/dist/chunk-55KERLWL.js.map +0 -1
- package/dist/chunk-5A6X2Y73.js.map +0 -1
- package/dist/chunk-APTQIBS5.js.map +0 -1
- package/dist/chunk-EYXVAMEX.js +0 -626
- package/dist/chunk-EYXVAMEX.js.map +0 -1
- package/dist/chunk-I34BC3CU.js.map +0 -1
- package/dist/chunk-LOYS64QS.js +0 -2453
- package/dist/chunk-LOYS64QS.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- package/dist/chunk-ZKTFKHWN.js +0 -324
- package/dist/chunk-ZKTFKHWN.js.map +0 -1
- package/dist/discovery-VDANZAJ2.js +0 -28
- package/dist/init-WRUSW7R5.js.map +0 -1
- package/dist/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.js.map +0 -1
- package/dist/scan-YJHQIRKG.js +0 -14
- package/dist/scan-generate-TFZVL3BT.js.map +0 -1
- package/dist/viewer-2TZS3NDL.js +0 -2730
- package/dist/viewer-2TZS3NDL.js.map +0 -1
- package/src/build.ts +0 -612
- package/src/commands/dev.ts +0 -107
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1030
- package/src/core/token-resolver.ts +0 -155
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{discovery-VDANZAJ2.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-YJHQIRKG.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{service-HKJ6B7P7.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{static-viewer-DUVC4UIM.js.map → service-QJGWUIVL.js.map} +0 -0
package/src/build.ts
DELETED
|
@@ -1,612 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
-
import { resolve, join } from "node:path";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import type {
|
|
5
|
-
FragmentsConfig,
|
|
6
|
-
CompiledFragmentsFile,
|
|
7
|
-
CompiledFragment,
|
|
8
|
-
CompiledBlock,
|
|
9
|
-
CompiledTokenData,
|
|
10
|
-
} from "./core/index.js";
|
|
11
|
-
import { BRAND, compileBlock, parseTokenFile } from "./core/index.js";
|
|
12
|
-
import { resolveTokensWithSass } from "./core/token-resolver.js";
|
|
13
|
-
import type { BlockDefinition } from "./core/index.js";
|
|
14
|
-
import {
|
|
15
|
-
discoverFragmentFiles,
|
|
16
|
-
discoverBlockFiles,
|
|
17
|
-
discoverTokenFiles,
|
|
18
|
-
parseFragmentFile,
|
|
19
|
-
loadFragmentFile,
|
|
20
|
-
generateRegistry,
|
|
21
|
-
generateContextMd,
|
|
22
|
-
} from "./core/node.js";
|
|
23
|
-
import {
|
|
24
|
-
resolveComponentSourcePath,
|
|
25
|
-
} from "./core/auto-props.js";
|
|
26
|
-
import {
|
|
27
|
-
createComponentExtractor,
|
|
28
|
-
type PropMeta,
|
|
29
|
-
type ComponentMeta,
|
|
30
|
-
} from "./core/component-extractor.js";
|
|
31
|
-
import { buildComponentGraph } from "./core/graph-extractor.js";
|
|
32
|
-
import { serializeGraph } from "@fragments-sdk/context/graph";
|
|
33
|
-
import { resolvePerformanceConfig } from "./core/index.js";
|
|
34
|
-
import { measureBundleSizes, toPerformanceData } from "./core/bundle-measurer.js";
|
|
35
|
-
import type { PerformanceSummary } from "@fragments-sdk/context/types";
|
|
36
|
-
|
|
37
|
-
type CompiledProp = CompiledFragment["props"][string];
|
|
38
|
-
|
|
39
|
-
function normalizeParsedProps(
|
|
40
|
-
parsedProps: Record<string, Partial<CompiledProp>>
|
|
41
|
-
): Record<string, CompiledProp> {
|
|
42
|
-
return Object.fromEntries(
|
|
43
|
-
Object.entries(parsedProps).map(([name, prop]) => [
|
|
44
|
-
name,
|
|
45
|
-
{
|
|
46
|
-
type: prop.type ?? "custom",
|
|
47
|
-
description: prop.description ?? "",
|
|
48
|
-
default: prop.default,
|
|
49
|
-
required: prop.required,
|
|
50
|
-
values: prop.values,
|
|
51
|
-
constraints: prop.constraints,
|
|
52
|
-
},
|
|
53
|
-
])
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function mergeDocumentedAndAutoProps(
|
|
58
|
-
documentedProps: Record<string, CompiledProp>,
|
|
59
|
-
autoProps: Record<string, PropMeta>
|
|
60
|
-
): Record<string, CompiledProp> {
|
|
61
|
-
return Object.fromEntries(
|
|
62
|
-
Object.keys(autoProps)
|
|
63
|
-
// Strip inherited HTML/React props — they're identical across all components
|
|
64
|
-
// and bloat fragments.json. MCP consumers know these exist implicitly.
|
|
65
|
-
.filter((name) => autoProps[name].source === 'local' || name in documentedProps)
|
|
66
|
-
.map((name) => {
|
|
67
|
-
const documented = documentedProps[name];
|
|
68
|
-
const auto = autoProps[name];
|
|
69
|
-
|
|
70
|
-
return [
|
|
71
|
-
name,
|
|
72
|
-
{
|
|
73
|
-
type: auto.typeKind,
|
|
74
|
-
description: documented?.description ?? auto.description ?? "",
|
|
75
|
-
default: auto.default !== undefined ? auto.default : documented?.default,
|
|
76
|
-
required: auto.required,
|
|
77
|
-
values: auto.values ?? documented?.values,
|
|
78
|
-
constraints: documented?.constraints,
|
|
79
|
-
},
|
|
80
|
-
];
|
|
81
|
-
})
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Auto-compile a propsSummary for the contract from extracted props.
|
|
87
|
-
* Format: "variant: primary|secondary|ghost (required)"
|
|
88
|
-
*/
|
|
89
|
-
function compilePropsSummary(props: Record<string, PropMeta>): string[] {
|
|
90
|
-
return Object.entries(props)
|
|
91
|
-
.filter(([_, p]) => p.source === 'local')
|
|
92
|
-
.map(([name, prop]) => {
|
|
93
|
-
let summary = name + ': ';
|
|
94
|
-
if (prop.values && prop.values.length > 0) {
|
|
95
|
-
summary += prop.values.join('|');
|
|
96
|
-
} else {
|
|
97
|
-
summary += prop.typeKind;
|
|
98
|
-
}
|
|
99
|
-
if (prop.default !== undefined) {
|
|
100
|
-
summary += ` (default: ${prop.default})`;
|
|
101
|
-
}
|
|
102
|
-
if (prop.required) {
|
|
103
|
-
summary += ' (required)';
|
|
104
|
-
}
|
|
105
|
-
return summary;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export interface BuildResult {
|
|
110
|
-
success: boolean;
|
|
111
|
-
outputPath: string;
|
|
112
|
-
fragmentCount: number;
|
|
113
|
-
errors: Array<{ file: string; error: string }>;
|
|
114
|
-
warnings: Array<{ file: string; warning: string }>;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Build compiled fragments.json file for AI consumption.
|
|
119
|
-
*
|
|
120
|
-
* Uses AST parsing to extract metadata WITHOUT executing fragment files.
|
|
121
|
-
* This means the build works without any project dependencies installed.
|
|
122
|
-
*/
|
|
123
|
-
export async function buildFragments(
|
|
124
|
-
config: FragmentsConfig,
|
|
125
|
-
configDir: string
|
|
126
|
-
): Promise<BuildResult> {
|
|
127
|
-
const files = await discoverFragmentFiles(config, configDir);
|
|
128
|
-
const errors: Array<{ file: string; error: string }> = [];
|
|
129
|
-
const warnings: Array<{ file: string; warning: string }> = [];
|
|
130
|
-
const fragments: CompiledFragmentsFile["fragments"] = {};
|
|
131
|
-
|
|
132
|
-
// Create a persistent extractor — shared LanguageService across all fragments
|
|
133
|
-
// Try to find a tsconfig.json in the config directory
|
|
134
|
-
const tsconfigCandidates = [
|
|
135
|
-
resolve(configDir, 'tsconfig.json'),
|
|
136
|
-
resolve(configDir, '..', 'tsconfig.json'),
|
|
137
|
-
];
|
|
138
|
-
const tsconfigPath = tsconfigCandidates.find((p) => existsSync(p));
|
|
139
|
-
const extractor = createComponentExtractor(tsconfigPath);
|
|
140
|
-
|
|
141
|
-
for (const file of files) {
|
|
142
|
-
try {
|
|
143
|
-
// Read file content as text
|
|
144
|
-
const content = await readFile(file.absolutePath, "utf-8");
|
|
145
|
-
|
|
146
|
-
// Skip files that don't contain defineFragment() — e.g., story files
|
|
147
|
-
// that were accidentally included in the config
|
|
148
|
-
if (!content.includes("defineFragment")) {
|
|
149
|
-
warnings.push({
|
|
150
|
-
file: file.relativePath,
|
|
151
|
-
warning: "No defineFragment() call found",
|
|
152
|
-
});
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Parse using AST (no execution)
|
|
157
|
-
const parsed = parseFragmentFile(content, file.relativePath);
|
|
158
|
-
|
|
159
|
-
// Collect warnings
|
|
160
|
-
for (const warning of parsed.warnings) {
|
|
161
|
-
warnings.push({ file: file.relativePath, warning });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Check for required fields
|
|
165
|
-
if (!parsed.meta.name) {
|
|
166
|
-
warnings.push({
|
|
167
|
-
file: file.relativePath,
|
|
168
|
-
warning: "Missing meta.name in fragment definition — skipped",
|
|
169
|
-
});
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const documentedProps = normalizeParsedProps(parsed.props);
|
|
174
|
-
let mergedProps = documentedProps;
|
|
175
|
-
|
|
176
|
-
const componentExportName = parsed.componentName ?? parsed.meta.name;
|
|
177
|
-
const componentSourcePath = resolveComponentSourcePath(
|
|
178
|
-
file.absolutePath,
|
|
179
|
-
parsed.componentImport
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
// Extract full component metadata using persistent LanguageService
|
|
183
|
-
let extractedMeta: ComponentMeta | null = null;
|
|
184
|
-
if (componentExportName && componentSourcePath) {
|
|
185
|
-
try {
|
|
186
|
-
extractedMeta = extractor.extract(componentSourcePath, componentExportName);
|
|
187
|
-
} catch {
|
|
188
|
-
// Extraction failure is non-fatal — fall back to documented props
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (extractedMeta) {
|
|
192
|
-
const autoProps = extractedMeta.props;
|
|
193
|
-
const hasAutoProps = Object.keys(autoProps).length > 0;
|
|
194
|
-
|
|
195
|
-
if (hasAutoProps) {
|
|
196
|
-
const removedDocumentedProps = Object.keys(documentedProps).filter(
|
|
197
|
-
(propName) => !(propName in autoProps)
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
if (removedDocumentedProps.length > 0) {
|
|
201
|
-
warnings.push({
|
|
202
|
-
file: file.relativePath,
|
|
203
|
-
warning: `Removed ${removedDocumentedProps.length} documented props not present in source API: ${removedDocumentedProps.join(", ")}`,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
mergedProps = mergeDocumentedAndAutoProps(documentedProps, autoProps);
|
|
208
|
-
} else if (Object.keys(documentedProps).length > 0) {
|
|
209
|
-
warnings.push({
|
|
210
|
-
file: file.relativePath,
|
|
211
|
-
warning: "Auto-props extraction returned no props; falling back to documented props",
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
} else if (!componentExportName) {
|
|
216
|
-
warnings.push({
|
|
217
|
-
file: file.relativePath,
|
|
218
|
-
warning: "Unable to resolve component export name for auto-props extraction",
|
|
219
|
-
});
|
|
220
|
-
} else if (!componentSourcePath) {
|
|
221
|
-
warnings.push({
|
|
222
|
-
file: file.relativePath,
|
|
223
|
-
warning: `Unable to resolve component source path from import: ${parsed.componentImport ?? "unknown"}`,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Auto-compile contract if not manually authored
|
|
228
|
-
let contract = parsed.contract;
|
|
229
|
-
if (!contract?.propsSummary && extractedMeta) {
|
|
230
|
-
const summary = compilePropsSummary(extractedMeta.props);
|
|
231
|
-
if (summary.length > 0) {
|
|
232
|
-
contract = { ...contract, propsSummary: summary };
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Auto-enrich AI metadata from extractor's composition data
|
|
237
|
-
let ai = parsed.ai;
|
|
238
|
-
if (extractedMeta?.composition) {
|
|
239
|
-
const comp = extractedMeta.composition;
|
|
240
|
-
ai = {
|
|
241
|
-
compositionPattern: comp.pattern,
|
|
242
|
-
subComponents: comp.parts.map((p) => p.name),
|
|
243
|
-
...ai, // Manually authored ai fields take precedence
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Build compiled fragment from parsed metadata
|
|
248
|
-
const compiled: CompiledFragment = {
|
|
249
|
-
filePath: file.relativePath,
|
|
250
|
-
meta: {
|
|
251
|
-
name: parsed.meta.name,
|
|
252
|
-
description: parsed.meta.description ?? "",
|
|
253
|
-
category: parsed.meta.category ?? "Uncategorized",
|
|
254
|
-
status: parsed.meta.status,
|
|
255
|
-
tags: parsed.meta.tags,
|
|
256
|
-
since: parsed.meta.since,
|
|
257
|
-
...(parsed.meta.dependencies && { dependencies: parsed.meta.dependencies }),
|
|
258
|
-
figma: parsed.meta.figma,
|
|
259
|
-
},
|
|
260
|
-
usage: {
|
|
261
|
-
when: parsed.usage.when ?? [],
|
|
262
|
-
whenNot: parsed.usage.whenNot ?? [],
|
|
263
|
-
guidelines: parsed.usage.guidelines,
|
|
264
|
-
accessibility: parsed.usage.accessibility,
|
|
265
|
-
},
|
|
266
|
-
props: mergedProps,
|
|
267
|
-
relations: parsed.relations.map((rel) => ({
|
|
268
|
-
component: rel.component,
|
|
269
|
-
relationship: rel.relationship as
|
|
270
|
-
| "alternative"
|
|
271
|
-
| "sibling"
|
|
272
|
-
| "parent"
|
|
273
|
-
| "child"
|
|
274
|
-
| "composition",
|
|
275
|
-
note: rel.note,
|
|
276
|
-
})),
|
|
277
|
-
variants: parsed.variants.map((v) => ({
|
|
278
|
-
name: v.name,
|
|
279
|
-
description: v.description,
|
|
280
|
-
...(v.code && { code: v.code }),
|
|
281
|
-
...(v.figma && { figma: v.figma }),
|
|
282
|
-
...(v.args && { args: v.args }),
|
|
283
|
-
})),
|
|
284
|
-
// Include AI metadata (auto-enriched or manual)
|
|
285
|
-
...(ai && { ai }),
|
|
286
|
-
// Include contract metadata (auto-compiled or manual)
|
|
287
|
-
...(contract && { contract }),
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
fragments[parsed.meta.name] = compiled;
|
|
291
|
-
} catch (error) {
|
|
292
|
-
errors.push({
|
|
293
|
-
file: file.relativePath,
|
|
294
|
-
error: error instanceof Error ? error.message : String(error),
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
extractor.dispose();
|
|
300
|
-
|
|
301
|
-
// Discover and compile block files
|
|
302
|
-
const blocks: Record<string, CompiledBlock> = {};
|
|
303
|
-
try {
|
|
304
|
-
const blockFiles = await discoverBlockFiles(configDir, config.exclude);
|
|
305
|
-
for (const file of blockFiles) {
|
|
306
|
-
try {
|
|
307
|
-
// loadFragmentFile uses esbuild to bundle+evaluate, returns default export
|
|
308
|
-
// CJS/ESM interop may double-wrap the default export
|
|
309
|
-
let raw = await loadFragmentFile(file.absolutePath) as unknown as Record<string, unknown> | null;
|
|
310
|
-
// Unwrap double-default from CJS interop
|
|
311
|
-
if (raw && 'default' in raw && typeof raw.default === 'object') {
|
|
312
|
-
raw = raw.default as Record<string, unknown>;
|
|
313
|
-
}
|
|
314
|
-
const def = raw;
|
|
315
|
-
if (def && typeof def === 'object' && 'name' in def && 'code' in def && 'components' in def) {
|
|
316
|
-
const compiled = compileBlock(def as unknown as BlockDefinition, file.relativePath);
|
|
317
|
-
blocks[compiled.name] = compiled;
|
|
318
|
-
}
|
|
319
|
-
} catch (error) {
|
|
320
|
-
warnings.push({
|
|
321
|
-
file: file.relativePath,
|
|
322
|
-
warning: `Failed to load block: ${error instanceof Error ? error.message : String(error)}`,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
} catch {
|
|
327
|
-
// Block discovery failure is non-fatal
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Discover and extract design tokens from SCSS/CSS files
|
|
331
|
-
let tokens: CompiledTokenData | undefined;
|
|
332
|
-
try {
|
|
333
|
-
const tokenPatterns = config.tokens?.include;
|
|
334
|
-
const tokenFiles = await discoverTokenFiles(configDir, tokenPatterns, config.exclude);
|
|
335
|
-
if (tokenFiles.length > 0) {
|
|
336
|
-
// Merge tokens from all discovered files
|
|
337
|
-
const mergedCategories: Record<string, Array<{ name: string; value?: string; description?: string }>> = {};
|
|
338
|
-
let prefix = '--';
|
|
339
|
-
let total = 0;
|
|
340
|
-
|
|
341
|
-
// Read all file contents first for cross-file SCSS variable resolution
|
|
342
|
-
const fileContents: Array<{ content: string; path: string }> = [];
|
|
343
|
-
for (const file of tokenFiles) {
|
|
344
|
-
const content = await readFile(file.absolutePath, 'utf-8');
|
|
345
|
-
fileContents.push({ content, path: file.relativePath });
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Concatenate all contents so parseTokenFile can resolve SCSS vars across files
|
|
349
|
-
const allContent = fileContents.map((f) => f.content).join('\n');
|
|
350
|
-
|
|
351
|
-
for (const { content, path } of fileContents) {
|
|
352
|
-
// Parse with the combined content to enable cross-file SCSS var resolution
|
|
353
|
-
const parsed = parseTokenFile(allContent, path);
|
|
354
|
-
// But only use tokens from THIS file's content to avoid duplicates
|
|
355
|
-
const fileParsed = parseTokenFile(content, path);
|
|
356
|
-
prefix = fileParsed.prefix;
|
|
357
|
-
total += fileParsed.total;
|
|
358
|
-
for (const [cat, catTokens] of Object.entries(fileParsed.categories)) {
|
|
359
|
-
if (!mergedCategories[cat]) {
|
|
360
|
-
mergedCategories[cat] = [];
|
|
361
|
-
}
|
|
362
|
-
for (const t of catTokens) {
|
|
363
|
-
// Deduplicate by name
|
|
364
|
-
if (!mergedCategories[cat].some((e) => e.name === t.name)) {
|
|
365
|
-
// Use resolved value from the combined parse if available
|
|
366
|
-
const combinedToken = Object.values(parsed.categories)
|
|
367
|
-
.flat()
|
|
368
|
-
.find((ct) => ct.name === t.name);
|
|
369
|
-
const resolvedValue = combinedToken?.resolvedValue ?? t.resolvedValue;
|
|
370
|
-
|
|
371
|
-
mergedCategories[cat].push({
|
|
372
|
-
name: t.name,
|
|
373
|
-
...(resolvedValue
|
|
374
|
-
? { value: resolvedValue }
|
|
375
|
-
: t.value ? { value: t.value } : {}),
|
|
376
|
-
description: t.description,
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Sass compilation fallback: resolve tokens the regex parser couldn't handle
|
|
384
|
-
if (total > 0) {
|
|
385
|
-
const allTokens = Object.values(mergedCategories).flat();
|
|
386
|
-
const unresolved = allTokens.filter(
|
|
387
|
-
t => t.value && (t.value.includes('#{') || t.value.includes('$'))
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
if (unresolved.length > 0 && tokenFiles.length > 0) {
|
|
391
|
-
// Determine the tokens directory from the first discovered file
|
|
392
|
-
const tokensDir = resolve(configDir, tokenFiles[0].relativePath, '..');
|
|
393
|
-
const sassResolved = await resolveTokensWithSass(
|
|
394
|
-
unresolved as Array<{ name: string; value: string }>,
|
|
395
|
-
tokensDir,
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
// Merge sass-resolved values back into the categories
|
|
399
|
-
if (sassResolved.size > 0) {
|
|
400
|
-
for (const catTokens of Object.values(mergedCategories)) {
|
|
401
|
-
for (const token of catTokens) {
|
|
402
|
-
const resolved = sassResolved.get(token.name);
|
|
403
|
-
if (resolved && token.value && (token.value.includes('#{') || token.value.includes('$'))) {
|
|
404
|
-
token.value = resolved;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
tokens = { prefix, total, categories: mergedCategories };
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
} catch {
|
|
415
|
-
// Token extraction failure is non-fatal
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Read package name for import statements
|
|
419
|
-
let packageName: string | undefined;
|
|
420
|
-
const pkgJsonPath = resolve(configDir, "package.json");
|
|
421
|
-
if (existsSync(pkgJsonPath)) {
|
|
422
|
-
try {
|
|
423
|
-
const pkg = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
|
424
|
-
if (pkg.name) packageName = pkg.name;
|
|
425
|
-
} catch {
|
|
426
|
-
// Non-fatal
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Build component graph for AI structural queries
|
|
431
|
-
// Derive component directory from configDir (typically src/components/)
|
|
432
|
-
const componentDir = resolve(configDir, "src", "components");
|
|
433
|
-
let graphData: ReturnType<typeof serializeGraph> | undefined;
|
|
434
|
-
try {
|
|
435
|
-
const graphResult = await buildComponentGraph(fragments, blocks, componentDir);
|
|
436
|
-
|
|
437
|
-
// Auto-enrich fragments with detected metadata
|
|
438
|
-
for (const [name, fragment] of Object.entries(fragments)) {
|
|
439
|
-
const detected = graphResult.autoDetected.get(name);
|
|
440
|
-
if (!detected) continue;
|
|
441
|
-
|
|
442
|
-
if (!fragment.ai) fragment.ai = {};
|
|
443
|
-
if (!fragment.ai.subComponents && detected.subComponents) {
|
|
444
|
-
fragment.ai.subComponents = detected.subComponents;
|
|
445
|
-
}
|
|
446
|
-
if (!fragment.ai.compositionPattern && detected.compositionPattern) {
|
|
447
|
-
fragment.ai.compositionPattern = detected.compositionPattern;
|
|
448
|
-
}
|
|
449
|
-
if (!fragment.ai.commonPatterns && detected.commonPatterns) {
|
|
450
|
-
fragment.ai.commonPatterns = detected.commonPatterns;
|
|
451
|
-
}
|
|
452
|
-
if (!fragment.ai.requiredChildren && detected.requiredChildren) {
|
|
453
|
-
fragment.ai.requiredChildren = detected.requiredChildren;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Report drift warnings
|
|
458
|
-
for (const w of graphResult.warnings) {
|
|
459
|
-
warnings.push({ file: "graph", warning: w });
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
graphData = serializeGraph(graphResult.graph);
|
|
463
|
-
} catch (error) {
|
|
464
|
-
warnings.push({
|
|
465
|
-
file: "graph",
|
|
466
|
-
warning: `Graph extraction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Measure performance budgets if configured
|
|
471
|
-
let performanceSummary: PerformanceSummary | undefined;
|
|
472
|
-
if (config.performance) {
|
|
473
|
-
try {
|
|
474
|
-
const perfConfig = resolvePerformanceConfig(config.performance);
|
|
475
|
-
const perfResult = await measureBundleSizes(fragments, configDir, {
|
|
476
|
-
perfConfig,
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
const tiers: Record<string, number> = { lightweight: 0, moderate: 0, heavy: 0 };
|
|
480
|
-
let overBudgetCount = 0;
|
|
481
|
-
|
|
482
|
-
for (const [name, measurement] of perfResult.measurements) {
|
|
483
|
-
const fragment = fragments[name];
|
|
484
|
-
const contractBudget = fragment?.contract?.performanceBudget as number | undefined;
|
|
485
|
-
const perfData = toPerformanceData(measurement, perfConfig, contractBudget);
|
|
486
|
-
fragment.performance = perfData;
|
|
487
|
-
tiers[perfData.complexity]++;
|
|
488
|
-
if (perfData.overBudget) overBudgetCount++;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
performanceSummary = {
|
|
492
|
-
preset: perfConfig.preset,
|
|
493
|
-
budget: perfConfig.budgets.bundleSize,
|
|
494
|
-
total: perfResult.measurements.size,
|
|
495
|
-
overBudget: overBudgetCount,
|
|
496
|
-
tiers,
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
for (const err of perfResult.errors) {
|
|
500
|
-
warnings.push({
|
|
501
|
-
file: 'performance',
|
|
502
|
-
warning: `Could not measure ${err.name}: ${err.error}`,
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
} catch (error) {
|
|
506
|
-
warnings.push({
|
|
507
|
-
file: 'performance',
|
|
508
|
-
warning: `Performance measurement failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const output: CompiledFragmentsFile = {
|
|
514
|
-
version: "1.0.0",
|
|
515
|
-
generatedAt: new Date().toISOString(),
|
|
516
|
-
...(packageName && { packageName }),
|
|
517
|
-
fragments,
|
|
518
|
-
...(Object.keys(blocks).length > 0 && { blocks }),
|
|
519
|
-
...(tokens && { tokens }),
|
|
520
|
-
...(graphData && { graph: graphData }),
|
|
521
|
-
...(performanceSummary && { performanceSummary }),
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
|
|
525
|
-
await writeFile(outputPath, JSON.stringify(output));
|
|
526
|
-
|
|
527
|
-
return {
|
|
528
|
-
success: errors.length === 0,
|
|
529
|
-
outputPath,
|
|
530
|
-
fragmentCount: Object.keys(fragments).length,
|
|
531
|
-
errors,
|
|
532
|
-
warnings,
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Result of building the .fragments directory
|
|
538
|
-
*/
|
|
539
|
-
export interface FragmentsBuildResult {
|
|
540
|
-
success: boolean;
|
|
541
|
-
indexPath: string;
|
|
542
|
-
registryPath: string;
|
|
543
|
-
contextPath: string;
|
|
544
|
-
componentCount: number;
|
|
545
|
-
errors: Array<{ file: string; error: string }>;
|
|
546
|
-
warnings: Array<{ file: string; warning: string }>;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Build the .fragments/ directory with index.json, registry.json, and context.md
|
|
551
|
-
*
|
|
552
|
-
* This generates:
|
|
553
|
-
* - .fragments/index.json - Minimal name → path mapping (tiny, for quick lookups)
|
|
554
|
-
* - .fragments/registry.json - Component index with paths and enrichment references
|
|
555
|
-
* - .fragments/context.md - AI-ready consolidated context file
|
|
556
|
-
*/
|
|
557
|
-
export async function buildFragmentsDir(
|
|
558
|
-
config: FragmentsConfig,
|
|
559
|
-
configDir: string
|
|
560
|
-
): Promise<FragmentsBuildResult> {
|
|
561
|
-
const fragmentsDir = join(configDir, BRAND.dataDir);
|
|
562
|
-
const componentsDir = join(fragmentsDir, BRAND.componentsDir);
|
|
563
|
-
|
|
564
|
-
// Create directories
|
|
565
|
-
await mkdir(fragmentsDir, { recursive: true });
|
|
566
|
-
await mkdir(componentsDir, { recursive: true });
|
|
567
|
-
|
|
568
|
-
// Generate registry with config options
|
|
569
|
-
const registryResult = await generateRegistry({
|
|
570
|
-
projectRoot: configDir,
|
|
571
|
-
componentPatterns: config.components || ["src/**/*.tsx", "src/**/*.ts"],
|
|
572
|
-
storyPatterns: config.include || ["src/**/*.stories.tsx"],
|
|
573
|
-
fragmentsDir,
|
|
574
|
-
registryOptions: config.registry || {},
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
const errors = [...registryResult.errors];
|
|
578
|
-
const warnings = [...registryResult.warnings];
|
|
579
|
-
|
|
580
|
-
// Write index.json (minimal name → path)
|
|
581
|
-
const indexPath = join(fragmentsDir, "index.json");
|
|
582
|
-
await writeFile(indexPath, JSON.stringify(registryResult.index, null, 2));
|
|
583
|
-
|
|
584
|
-
// Write registry.json (full metadata)
|
|
585
|
-
const registryPath = join(fragmentsDir, BRAND.registryFile);
|
|
586
|
-
await writeFile(registryPath, JSON.stringify(registryResult.registry, null, 2));
|
|
587
|
-
|
|
588
|
-
// Generate context.md - focus on semantic knowledge, skip props (AI can read source)
|
|
589
|
-
const contextResult = generateContextMd(registryResult.registry, {
|
|
590
|
-
format: "markdown",
|
|
591
|
-
compact: false,
|
|
592
|
-
include: {
|
|
593
|
-
props: false, // AI can read TypeScript directly
|
|
594
|
-
relations: true,
|
|
595
|
-
code: false,
|
|
596
|
-
},
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
// Write context.md
|
|
600
|
-
const contextPath = join(fragmentsDir, BRAND.contextFile);
|
|
601
|
-
await writeFile(contextPath, contextResult.content);
|
|
602
|
-
|
|
603
|
-
return {
|
|
604
|
-
success: errors.length === 0,
|
|
605
|
-
indexPath,
|
|
606
|
-
registryPath,
|
|
607
|
-
contextPath,
|
|
608
|
-
componentCount: registryResult.registry.componentCount,
|
|
609
|
-
errors,
|
|
610
|
-
warnings,
|
|
611
|
-
};
|
|
612
|
-
}
|