@fragments-sdk/cli 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-client-I6MDWNYA.js +21 -0
- package/dist/bin.js +419 -410
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-HRFUSSZI.js → chunk-3SOAPJDX.js} +2 -2
- package/dist/{chunk-D5PYOXEI.js → chunk-4K7EAQ5L.js} +148 -13
- package/dist/{chunk-D5PYOXEI.js.map → chunk-4K7EAQ5L.js.map} +1 -1
- package/dist/chunk-DXX6HADE.js +443 -0
- package/dist/chunk-DXX6HADE.js.map +1 -0
- package/dist/chunk-EYXVAMEX.js +626 -0
- package/dist/chunk-EYXVAMEX.js.map +1 -0
- package/dist/{chunk-ZM4ZQZWZ.js → chunk-FO6EBJWP.js} +39 -37
- package/dist/chunk-FO6EBJWP.js.map +1 -0
- package/dist/{chunk-OQO55NKV.js → chunk-QM7SVOGF.js} +120 -12
- package/dist/chunk-QM7SVOGF.js.map +1 -0
- package/dist/{chunk-5G3VZH43.js → chunk-RF3C6LGA.js} +281 -351
- package/dist/chunk-RF3C6LGA.js.map +1 -0
- package/dist/{chunk-WXSR2II7.js → chunk-SM674YAS.js} +58 -6
- package/dist/chunk-SM674YAS.js.map +1 -0
- package/dist/chunk-SXTKFDCR.js +104 -0
- package/dist/chunk-SXTKFDCR.js.map +1 -0
- package/dist/{chunk-PW7QTQA6.js → chunk-UV5JQV3R.js} +2 -2
- package/dist/core/index.js +13 -1
- package/dist/{discovery-NEOY4MPN.js → discovery-VSGC76JN.js} +3 -3
- package/dist/{generate-FBHSXR3D.js → generate-QZXOXYFW.js} +4 -4
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/init-XK6PRUE5.js +636 -0
- package/dist/init-XK6PRUE5.js.map +1 -0
- package/dist/mcp-bin.js +2 -2
- package/dist/{scan-CJF2DOQW.js → scan-CHQHXWVD.js} +6 -6
- package/dist/scan-generate-U3RFVDTX.js +1115 -0
- package/dist/scan-generate-U3RFVDTX.js.map +1 -0
- package/dist/{service-TQYWY65E.js → service-MMEKG4MZ.js} +3 -3
- package/dist/{snapshot-SV2JOFZH.js → snapshot-53TUR3HW.js} +2 -2
- package/dist/{static-viewer-NUBFPKWH.js → static-viewer-KKCR4KXR.js} +3 -3
- package/dist/static-viewer-KKCR4KXR.js.map +1 -0
- package/dist/{test-Z5LVO724.js → test-5UCKXYSC.js} +4 -4
- package/dist/{tokens-CE46OTMD.js → tokens-L46MK5AW.js} +5 -5
- package/dist/{viewer-DLLJIMCK.js → viewer-M2EQQSGE.js} +14 -14
- package/dist/viewer-M2EQQSGE.js.map +1 -0
- package/package.json +11 -9
- package/src/ai-client.ts +156 -0
- package/src/bin.ts +99 -2
- package/src/build.ts +95 -33
- package/src/commands/__tests__/drift-sync.test.ts +252 -0
- package/src/commands/__tests__/scan-generate.test.ts +497 -45
- package/src/commands/enhance.ts +11 -35
- package/src/commands/govern.ts +122 -0
- package/src/commands/init.ts +288 -260
- package/src/commands/scan-generate.ts +740 -139
- package/src/commands/scan.ts +37 -32
- package/src/commands/setup.ts +143 -52
- package/src/commands/sync.ts +357 -0
- package/src/commands/validate.ts +43 -1
- package/src/core/component-extractor.test.ts +282 -0
- package/src/core/component-extractor.ts +1030 -0
- package/src/core/discovery.ts +93 -7
- package/src/service/enhance/props-extractor.ts +235 -13
- package/src/validators.ts +236 -0
- package/src/viewer/vite-plugin.ts +1 -1
- package/dist/chunk-5G3VZH43.js.map +0 -1
- package/dist/chunk-OQO55NKV.js.map +0 -1
- package/dist/chunk-WXSR2II7.js.map +0 -1
- package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
- package/dist/init-UFGK5TCN.js +0 -867
- package/dist/init-UFGK5TCN.js.map +0 -1
- package/dist/scan-generate-SJAN5MVI.js +0 -691
- package/dist/scan-generate-SJAN5MVI.js.map +0 -1
- package/dist/viewer-DLLJIMCK.js.map +0 -1
- package/src/ai.ts +0 -266
- package/src/commands/init-framework.ts +0 -414
- package/src/mcp/bin.ts +0 -36
- package/src/migrate/bin.ts +0 -114
- package/src/theme/index.ts +0 -77
- package/src/viewer/bin.ts +0 -86
- package/src/viewer/cli/health.ts +0 -256
- package/src/viewer/cli/index.ts +0 -33
- package/src/viewer/cli/scan.ts +0 -124
- package/src/viewer/cli/utils.ts +0 -174
- /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
- /package/dist/{chunk-HRFUSSZI.js.map → chunk-3SOAPJDX.js.map} +0 -0
- /package/dist/{chunk-PW7QTQA6.js.map → chunk-UV5JQV3R.js.map} +0 -0
- /package/dist/{scan-CJF2DOQW.js.map → discovery-VSGC76JN.js.map} +0 -0
- /package/dist/{generate-FBHSXR3D.js.map → generate-QZXOXYFW.js.map} +0 -0
- /package/dist/{service-TQYWY65E.js.map → scan-CHQHXWVD.js.map} +0 -0
- /package/dist/{static-viewer-NUBFPKWH.js.map → service-MMEKG4MZ.js.map} +0 -0
- /package/dist/{snapshot-SV2JOFZH.js.map → snapshot-53TUR3HW.js.map} +0 -0
- /package/dist/{test-Z5LVO724.js.map → test-5UCKXYSC.js.map} +0 -0
- /package/dist/{tokens-CE46OTMD.js.map → tokens-L46MK5AW.js.map} +0 -0
package/src/ai-client.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AI client infrastructure for LLM-powered CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - `fragments enhance` (usage analysis + documentation generation)
|
|
6
|
+
* - `fragments init --scan --enrich` (knowledge field enrichment)
|
|
7
|
+
*
|
|
8
|
+
* Supports Anthropic (Claude) and OpenAI APIs via dynamic import.
|
|
9
|
+
* No new dependencies — uses existing @anthropic-ai/sdk and openai.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type AIProvider = 'anthropic' | 'openai' | 'none';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default lightweight models for enrichment (cheap, fast).
|
|
16
|
+
* Enhance uses its own heavier models locally.
|
|
17
|
+
*/
|
|
18
|
+
export const ENRICHMENT_MODELS: Record<AIProvider, string> = {
|
|
19
|
+
anthropic: 'claude-haiku-4-5-20251001',
|
|
20
|
+
openai: 'gpt-4o-mini',
|
|
21
|
+
none: '',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Detect which AI provider to use based on available API keys and options.
|
|
26
|
+
*/
|
|
27
|
+
export function detectProvider(opts?: {
|
|
28
|
+
provider?: AIProvider;
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
}): AIProvider {
|
|
31
|
+
if (opts?.provider) return opts.provider;
|
|
32
|
+
if (opts?.apiKey) {
|
|
33
|
+
if (opts.apiKey.startsWith('sk-ant-')) return 'anthropic';
|
|
34
|
+
if (opts.apiKey.startsWith('sk-')) return 'openai';
|
|
35
|
+
}
|
|
36
|
+
if (process.env.ANTHROPIC_API_KEY) return 'anthropic';
|
|
37
|
+
if (process.env.OPENAI_API_KEY) return 'openai';
|
|
38
|
+
return 'none';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the API key for a given provider.
|
|
43
|
+
*/
|
|
44
|
+
export function getApiKey(provider: AIProvider, explicitKey?: string): string | undefined {
|
|
45
|
+
if (explicitKey) return explicitKey;
|
|
46
|
+
if (provider === 'anthropic') return process.env.ANTHROPIC_API_KEY;
|
|
47
|
+
if (provider === 'openai') return process.env.OPENAI_API_KEY;
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create an AI client for the given provider via dynamic import.
|
|
53
|
+
*/
|
|
54
|
+
export async function createAIClient(provider: AIProvider, apiKey: string): Promise<unknown> {
|
|
55
|
+
if (provider === 'anthropic') {
|
|
56
|
+
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
|
57
|
+
return new Anthropic({ apiKey });
|
|
58
|
+
}
|
|
59
|
+
if (provider === 'openai') {
|
|
60
|
+
const OpenAI = (await import('openai')).default;
|
|
61
|
+
return new OpenAI({ apiKey });
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CompletionResult {
|
|
67
|
+
text: string;
|
|
68
|
+
inputTokens: number;
|
|
69
|
+
outputTokens: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a completion using the appropriate provider API.
|
|
74
|
+
*/
|
|
75
|
+
export async function generateCompletion(
|
|
76
|
+
client: unknown,
|
|
77
|
+
provider: AIProvider,
|
|
78
|
+
model: string,
|
|
79
|
+
system: string,
|
|
80
|
+
user: string,
|
|
81
|
+
maxTokens: number = 1024
|
|
82
|
+
): Promise<CompletionResult> {
|
|
83
|
+
if (provider === 'anthropic') {
|
|
84
|
+
const anthropic = client as import('@anthropic-ai/sdk').default;
|
|
85
|
+
const response = await anthropic.messages.create({
|
|
86
|
+
model,
|
|
87
|
+
max_tokens: maxTokens,
|
|
88
|
+
system,
|
|
89
|
+
messages: [{ role: 'user', content: user }],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const content = response.content[0];
|
|
93
|
+
if (content.type !== 'text') {
|
|
94
|
+
throw new Error('Unexpected response type');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
text: content.text,
|
|
99
|
+
inputTokens: response.usage?.input_tokens || 0,
|
|
100
|
+
outputTokens: response.usage?.output_tokens || 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (provider === 'openai') {
|
|
105
|
+
const openai = client as import('openai').default;
|
|
106
|
+
const response = await openai.chat.completions.create({
|
|
107
|
+
model,
|
|
108
|
+
max_tokens: maxTokens,
|
|
109
|
+
messages: [
|
|
110
|
+
{ role: 'system', content: system },
|
|
111
|
+
{ role: 'user', content: user },
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const content = response.choices[0]?.message?.content;
|
|
116
|
+
if (!content) {
|
|
117
|
+
throw new Error('No response from OpenAI');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
text: content,
|
|
122
|
+
inputTokens: response.usage?.prompt_tokens || 0,
|
|
123
|
+
outputTokens: response.usage?.completion_tokens || 0,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse a JSON response from an LLM, handling ```json fences and raw JSON.
|
|
132
|
+
*/
|
|
133
|
+
export function parseJSONResponse<T = unknown>(text: string): T {
|
|
134
|
+
const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) || text.match(/\{[\s\S]*\}/);
|
|
135
|
+
const jsonStr = jsonMatch ? (jsonMatch[1] || jsonMatch[0]) : text;
|
|
136
|
+
return JSON.parse(jsonStr);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Calculate estimated cost based on model and token usage.
|
|
141
|
+
*/
|
|
142
|
+
export function calculateCost(model: string, inputTokens: number, outputTokens: number): number {
|
|
143
|
+
// Approximate costs per 1M tokens (input/output)
|
|
144
|
+
const pricing: Record<string, { input: number; output: number }> = {
|
|
145
|
+
// Anthropic
|
|
146
|
+
'claude-haiku-4-5-20251001': { input: 0.80, output: 4.00 },
|
|
147
|
+
'claude-sonnet-4-20250514': { input: 3.00, output: 15.00 },
|
|
148
|
+
// OpenAI
|
|
149
|
+
'gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
150
|
+
'gpt-4o': { input: 2.50, output: 10.00 },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const modelPricing = pricing[model] || { input: 3.00, output: 15.00 };
|
|
154
|
+
return (inputTokens / 1_000_000) * modelPricing.input +
|
|
155
|
+
(outputTokens / 1_000_000) * modelPricing.output;
|
|
156
|
+
}
|
package/src/bin.ts
CHANGED
|
@@ -39,6 +39,8 @@ import { graph } from './commands/graph.js';
|
|
|
39
39
|
import { perf } from './commands/perf.js';
|
|
40
40
|
import { doctor } from './commands/doctor.js';
|
|
41
41
|
import { setup } from './commands/setup.js';
|
|
42
|
+
import { sync } from './commands/sync.js';
|
|
43
|
+
import { governCheck, governInit, governReport } from './commands/govern.js';
|
|
42
44
|
|
|
43
45
|
// Import existing commands that were already extracted
|
|
44
46
|
import { runScreenshotCommand } from './screenshot.js';
|
|
@@ -62,6 +64,8 @@ program
|
|
|
62
64
|
.option('--schema', 'Validate fragment schema only')
|
|
63
65
|
.option('--coverage', 'Validate coverage only')
|
|
64
66
|
.option('--snippets', 'Validate snippet/render policy only')
|
|
67
|
+
.option('--drift', 'Detect metadata drift between source and fragments')
|
|
68
|
+
.option('--tsconfig <path>', 'Path to tsconfig.json (for drift detection)')
|
|
65
69
|
.option('--snippet-mode <mode>', 'Override snippet policy mode (warn|error)')
|
|
66
70
|
.option('--component-start <name>', 'Start component name for alphabetical snippet batch validation')
|
|
67
71
|
.option('--component-limit <n>', 'Component count for alphabetical snippet batch validation', (value) => Number.parseInt(value, 10))
|
|
@@ -77,6 +81,33 @@ program
|
|
|
77
81
|
}
|
|
78
82
|
});
|
|
79
83
|
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// SYNC COMMAND
|
|
86
|
+
// ============================================================================
|
|
87
|
+
program
|
|
88
|
+
.command('sync')
|
|
89
|
+
.description('Auto-update fragment files from component source')
|
|
90
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
91
|
+
.option('--tsconfig <path>', 'Path to tsconfig.json')
|
|
92
|
+
.option('--dry-run', 'Preview changes without writing')
|
|
93
|
+
.option('--component <name>', 'Sync specific component only')
|
|
94
|
+
.action(async (options) => {
|
|
95
|
+
try {
|
|
96
|
+
const result = await sync({
|
|
97
|
+
config: options.config,
|
|
98
|
+
tsconfig: options.tsconfig,
|
|
99
|
+
dryRun: options.dryRun,
|
|
100
|
+
component: options.component,
|
|
101
|
+
});
|
|
102
|
+
if (!result.success) {
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
80
111
|
// ============================================================================
|
|
81
112
|
// BUILD COMMAND
|
|
82
113
|
// ============================================================================
|
|
@@ -794,10 +825,16 @@ program
|
|
|
794
825
|
// ============================================================================
|
|
795
826
|
program
|
|
796
827
|
.command('init')
|
|
797
|
-
.description('Initialize fragments in a project (
|
|
828
|
+
.description('Initialize fragments in a project (zero-config by default)')
|
|
798
829
|
.option('--force', 'Overwrite existing config')
|
|
799
|
-
.option('-y, --yes', 'Non-interactive mode
|
|
830
|
+
.option('-y, --yes', 'Non-interactive mode (now the default)')
|
|
831
|
+
.option('--configure', 'Interactive mode for theme seeds, snapshots, etc.')
|
|
800
832
|
.option('--scan <path>', 'Scan a TypeScript component directory and generate fragment files')
|
|
833
|
+
.option('--enrich', 'Use AI to fill knowledge fields during --scan (requires API key)')
|
|
834
|
+
.option('--dry-run', 'Show what --enrich would generate without calling API')
|
|
835
|
+
.option('--provider <provider>', 'AI provider for enrichment: anthropic or openai')
|
|
836
|
+
.option('--api-key <key>', 'API key for AI enrichment')
|
|
837
|
+
.option('--model <model>', 'Override AI model for enrichment')
|
|
801
838
|
.action(async (options) => {
|
|
802
839
|
try {
|
|
803
840
|
const { init } = await import('./commands/init.js');
|
|
@@ -806,6 +843,12 @@ program
|
|
|
806
843
|
force: options.force,
|
|
807
844
|
yes: options.scan ? true : options.yes,
|
|
808
845
|
scan: options.scan,
|
|
846
|
+
configure: options.configure,
|
|
847
|
+
enrich: options.enrich,
|
|
848
|
+
dryRun: options.dryRun,
|
|
849
|
+
provider: options.provider,
|
|
850
|
+
apiKey: options.apiKey,
|
|
851
|
+
model: options.model,
|
|
809
852
|
});
|
|
810
853
|
|
|
811
854
|
if (!result.success) {
|
|
@@ -1073,5 +1116,59 @@ program
|
|
|
1073
1116
|
}
|
|
1074
1117
|
});
|
|
1075
1118
|
|
|
1119
|
+
// ============================================================================
|
|
1120
|
+
// GOVERN COMMAND
|
|
1121
|
+
// ============================================================================
|
|
1122
|
+
const governCmd = program
|
|
1123
|
+
.command('govern')
|
|
1124
|
+
.description('AI UI governance checks');
|
|
1125
|
+
|
|
1126
|
+
governCmd
|
|
1127
|
+
.command('check')
|
|
1128
|
+
.description('Validate a UISpec against governance policies')
|
|
1129
|
+
.option('-i, --input <path>', 'Path to UISpec JSON file (or - for stdin)')
|
|
1130
|
+
.option('-c, --config <path>', 'Path to govern.config.ts')
|
|
1131
|
+
.option('-f, --format <format>', 'Output format: summary, json, sarif', 'summary')
|
|
1132
|
+
.option('-q, --quiet', 'Suppress non-error output')
|
|
1133
|
+
.action(async (options) => {
|
|
1134
|
+
try {
|
|
1135
|
+
const { exitCode } = await governCheck({
|
|
1136
|
+
input: options.input,
|
|
1137
|
+
config: options.config,
|
|
1138
|
+
format: options.format,
|
|
1139
|
+
quiet: options.quiet,
|
|
1140
|
+
});
|
|
1141
|
+
process.exit(exitCode);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
governCmd
|
|
1149
|
+
.command('init')
|
|
1150
|
+
.description('Generate a govern.config.ts template')
|
|
1151
|
+
.option('-o, --output <path>', 'Output path', 'govern.config.ts')
|
|
1152
|
+
.action(async (options) => {
|
|
1153
|
+
try {
|
|
1154
|
+
await governInit({ output: options.output });
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
governCmd
|
|
1162
|
+
.command('report')
|
|
1163
|
+
.description('Summarize governance audit log')
|
|
1164
|
+
.action(async () => {
|
|
1165
|
+
try {
|
|
1166
|
+
await governReport();
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1076
1173
|
// Parse command line arguments
|
|
1077
1174
|
program.parse();
|
package/src/build.ts
CHANGED
|
@@ -21,10 +21,13 @@ import {
|
|
|
21
21
|
generateContextMd,
|
|
22
22
|
} from "./core/node.js";
|
|
23
23
|
import {
|
|
24
|
-
extractCustomPropsFromComponentFile,
|
|
25
24
|
resolveComponentSourcePath,
|
|
26
|
-
type AutoDetectedPropDefinition,
|
|
27
25
|
} from "./core/auto-props.js";
|
|
26
|
+
import {
|
|
27
|
+
createComponentExtractor,
|
|
28
|
+
type PropMeta,
|
|
29
|
+
type ComponentMeta,
|
|
30
|
+
} from "./core/component-extractor.js";
|
|
28
31
|
import { buildComponentGraph } from "./core/graph-extractor.js";
|
|
29
32
|
import { serializeGraph } from "@fragments-sdk/context/graph";
|
|
30
33
|
import { resolvePerformanceConfig } from "./core/index.js";
|
|
@@ -53,17 +56,21 @@ function normalizeParsedProps(
|
|
|
53
56
|
|
|
54
57
|
function mergeDocumentedAndAutoProps(
|
|
55
58
|
documentedProps: Record<string, CompiledProp>,
|
|
56
|
-
autoProps: Record<string,
|
|
59
|
+
autoProps: Record<string, PropMeta>
|
|
57
60
|
): Record<string, CompiledProp> {
|
|
58
61
|
return Object.fromEntries(
|
|
59
|
-
Object.keys(autoProps)
|
|
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) => {
|
|
60
67
|
const documented = documentedProps[name];
|
|
61
68
|
const auto = autoProps[name];
|
|
62
69
|
|
|
63
70
|
return [
|
|
64
71
|
name,
|
|
65
72
|
{
|
|
66
|
-
type: auto.
|
|
73
|
+
type: auto.typeKind,
|
|
67
74
|
description: documented?.description ?? auto.description ?? "",
|
|
68
75
|
default: auto.default !== undefined ? auto.default : documented?.default,
|
|
69
76
|
required: auto.required,
|
|
@@ -75,6 +82,30 @@ function mergeDocumentedAndAutoProps(
|
|
|
75
82
|
);
|
|
76
83
|
}
|
|
77
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
|
+
|
|
78
109
|
export interface BuildResult {
|
|
79
110
|
success: boolean;
|
|
80
111
|
outputPath: string;
|
|
@@ -98,6 +129,15 @@ export async function buildFragments(
|
|
|
98
129
|
const warnings: Array<{ file: string; warning: string }> = [];
|
|
99
130
|
const fragments: CompiledFragmentsFile["fragments"] = {};
|
|
100
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
|
+
|
|
101
141
|
for (const file of files) {
|
|
102
142
|
try {
|
|
103
143
|
// Read file content as text
|
|
@@ -139,38 +179,38 @@ export async function buildFragments(
|
|
|
139
179
|
parsed.componentImport
|
|
140
180
|
);
|
|
141
181
|
|
|
182
|
+
// Extract full component metadata using persistent LanguageService
|
|
183
|
+
let extractedMeta: ComponentMeta | null = null;
|
|
142
184
|
if (componentExportName && componentSourcePath) {
|
|
143
|
-
|
|
144
|
-
componentSourcePath,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
for (const warning of autoPropsResult.warnings) {
|
|
149
|
-
warnings.push({ file: file.relativePath, warning });
|
|
185
|
+
try {
|
|
186
|
+
extractedMeta = extractor.extract(componentSourcePath, componentExportName);
|
|
187
|
+
} catch {
|
|
188
|
+
// Extraction failure is non-fatal — fall back to documented props
|
|
150
189
|
}
|
|
151
190
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
(propName) => !(propName in autoPropsResult.props)
|
|
156
|
-
);
|
|
191
|
+
if (extractedMeta) {
|
|
192
|
+
const autoProps = extractedMeta.props;
|
|
193
|
+
const hasAutoProps = Object.keys(autoProps).length > 0;
|
|
157
194
|
|
|
158
|
-
if (
|
|
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) {
|
|
159
209
|
warnings.push({
|
|
160
210
|
file: file.relativePath,
|
|
161
|
-
warning:
|
|
211
|
+
warning: "Auto-props extraction returned no props; falling back to documented props",
|
|
162
212
|
});
|
|
163
213
|
}
|
|
164
|
-
|
|
165
|
-
mergedProps = mergeDocumentedAndAutoProps(
|
|
166
|
-
documentedProps,
|
|
167
|
-
autoPropsResult.props
|
|
168
|
-
);
|
|
169
|
-
} else if (autoPropsResult.resolved && !hasAutoProps && Object.keys(documentedProps).length > 0) {
|
|
170
|
-
warnings.push({
|
|
171
|
-
file: file.relativePath,
|
|
172
|
-
warning: "Auto-props extraction returned no custom props; falling back to documented props",
|
|
173
|
-
});
|
|
174
214
|
}
|
|
175
215
|
} else if (!componentExportName) {
|
|
176
216
|
warnings.push({
|
|
@@ -184,6 +224,26 @@ export async function buildFragments(
|
|
|
184
224
|
});
|
|
185
225
|
}
|
|
186
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
|
+
|
|
187
247
|
// Build compiled fragment from parsed metadata
|
|
188
248
|
const compiled: CompiledFragment = {
|
|
189
249
|
filePath: file.relativePath,
|
|
@@ -221,10 +281,10 @@ export async function buildFragments(
|
|
|
221
281
|
...(v.figma && { figma: v.figma }),
|
|
222
282
|
...(v.args && { args: v.args }),
|
|
223
283
|
})),
|
|
224
|
-
// Include AI metadata
|
|
225
|
-
...(
|
|
226
|
-
// Include contract metadata
|
|
227
|
-
...(
|
|
284
|
+
// Include AI metadata (auto-enriched or manual)
|
|
285
|
+
...(ai && { ai }),
|
|
286
|
+
// Include contract metadata (auto-compiled or manual)
|
|
287
|
+
...(contract && { contract }),
|
|
228
288
|
};
|
|
229
289
|
|
|
230
290
|
fragments[parsed.meta.name] = compiled;
|
|
@@ -236,6 +296,8 @@ export async function buildFragments(
|
|
|
236
296
|
}
|
|
237
297
|
}
|
|
238
298
|
|
|
299
|
+
extractor.dispose();
|
|
300
|
+
|
|
239
301
|
// Discover and compile block files
|
|
240
302
|
const blocks: Record<string, CompiledBlock> = {};
|
|
241
303
|
try {
|