wogiflow 1.0.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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Export Scanner
|
|
5
|
+
*
|
|
6
|
+
* Scans TypeScript/JavaScript files for exports to build an accurate
|
|
7
|
+
* import map for the local LLM. This ensures the LLM only uses imports
|
|
8
|
+
* that actually exist in the project.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node flow-export-scanner.js [project-root]
|
|
12
|
+
* node flow-export-scanner.js --cache # Use cached export map if fresh
|
|
13
|
+
*
|
|
14
|
+
* When used as a module, call setProjectRoot() before other functions.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { getProjectRoot: getProjectRootFromUtils, getConfig } = require('./flow-utils');
|
|
20
|
+
|
|
21
|
+
// Default to getProjectRoot from utils, can be overridden via setProjectRoot() or CLI arg
|
|
22
|
+
let PROJECT_ROOT = getProjectRootFromUtils();
|
|
23
|
+
let CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
|
|
24
|
+
let CACHE_PATH = path.join(PROJECT_ROOT, '.workflow/state/export-map.json');
|
|
25
|
+
const CACHE_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set the project root directory.
|
|
29
|
+
* Must be called before using any other functions when used as a module.
|
|
30
|
+
* @param {string} root - Absolute path to project root
|
|
31
|
+
*/
|
|
32
|
+
function setProjectRoot(root) {
|
|
33
|
+
PROJECT_ROOT = path.resolve(root);
|
|
34
|
+
CONFIG_PATH = path.join(PROJECT_ROOT, '.workflow/config.json');
|
|
35
|
+
CACHE_PATH = path.join(PROJECT_ROOT, '.workflow/state/export-map.json');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get current project root
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function getProjectRoot() {
|
|
43
|
+
return PROJECT_ROOT;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Alias getConfig as loadConfig for minimal code changes
|
|
47
|
+
const loadConfig = getConfig;
|
|
48
|
+
|
|
49
|
+
// ============================================================
|
|
50
|
+
// Export Extraction
|
|
51
|
+
// ============================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract exports from a TypeScript/JavaScript file
|
|
55
|
+
* @param {string} filePath - Path to the file
|
|
56
|
+
* @returns {{ namedExports: string[], defaultExport: string|null, types: string[], arrayExports: string[] }}
|
|
57
|
+
*/
|
|
58
|
+
function extractExports(filePath) {
|
|
59
|
+
const result = {
|
|
60
|
+
namedExports: [],
|
|
61
|
+
defaultExport: null,
|
|
62
|
+
types: [],
|
|
63
|
+
arrayExports: [] // Exports that are arrays (for variant detection)
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(filePath)) return result;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
70
|
+
|
|
71
|
+
// Match export { X, Y, Z } and export { X as Y }
|
|
72
|
+
const reExportMatches = content.matchAll(/export\s+\{\s*([^}]+)\s*\}/g);
|
|
73
|
+
for (const match of reExportMatches) {
|
|
74
|
+
const exports = match[1].split(',').map(e => {
|
|
75
|
+
const parts = e.trim().split(/\s+as\s+/);
|
|
76
|
+
return parts[parts.length - 1].trim(); // Use the aliased name if exists
|
|
77
|
+
}).filter(e => e && !e.startsWith('type '));
|
|
78
|
+
result.namedExports.push(...exports);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Match export type { X, Y }
|
|
82
|
+
const typeExportMatches = content.matchAll(/export\s+type\s+\{\s*([^}]+)\s*\}/g);
|
|
83
|
+
for (const match of typeExportMatches) {
|
|
84
|
+
const types = match[1].split(',').map(e => e.trim().split(/\s+as\s+/).pop().trim());
|
|
85
|
+
result.types.push(...types);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Match export const/function/class X and detect arrays
|
|
89
|
+
const namedExportMatches = content.matchAll(/export\s+(?:const|let|var)\s+(\w+)\s*(?::\s*\w+(?:\[\])?)?\s*=\s*(\[|\{|[^;]+)/g);
|
|
90
|
+
for (const match of namedExportMatches) {
|
|
91
|
+
const exportName = match[1];
|
|
92
|
+
const valueStart = match[2].trim();
|
|
93
|
+
|
|
94
|
+
if (!result.namedExports.includes(exportName)) {
|
|
95
|
+
result.namedExports.push(exportName);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Detect if this is an array export (common for variants)
|
|
99
|
+
if (valueStart === '[' ||
|
|
100
|
+
exportName.includes('Variants') ||
|
|
101
|
+
exportName.includes('Sizes') ||
|
|
102
|
+
exportName.includes('Statuses') ||
|
|
103
|
+
exportName.includes('Options')) {
|
|
104
|
+
result.arrayExports.push(exportName);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Match export function/class
|
|
109
|
+
const funcExportMatches = content.matchAll(/export\s+(?:function|class)\s+(\w+)/g);
|
|
110
|
+
for (const match of funcExportMatches) {
|
|
111
|
+
if (!result.namedExports.includes(match[1])) {
|
|
112
|
+
result.namedExports.push(match[1]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Match export type/interface X
|
|
117
|
+
const typeDefMatches = content.matchAll(/export\s+(?:type|interface)\s+(\w+)/g);
|
|
118
|
+
for (const match of typeDefMatches) {
|
|
119
|
+
if (!result.types.includes(match[1])) {
|
|
120
|
+
result.types.push(match[1]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Match export default X or export default function X
|
|
125
|
+
const defaultMatch = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
|
|
126
|
+
if (defaultMatch) {
|
|
127
|
+
result.defaultExport = defaultMatch[1];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Also check for "export default" at end of file (common pattern)
|
|
131
|
+
const defaultAtEnd = content.match(/export\s+default\s+(\w+)\s*;?\s*$/m);
|
|
132
|
+
if (defaultAtEnd && !result.defaultExport) {
|
|
133
|
+
result.defaultExport = defaultAtEnd[1];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
} catch (err) {
|
|
137
|
+
// Ignore read errors
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract props interface and type aliases from a component file
|
|
145
|
+
* @param {string} filePath - Path to the component file
|
|
146
|
+
* @returns {{ props: Object, typeAliases: Object, usageExample: Object|null, enums: Object, genericTypes: Object }}
|
|
147
|
+
*/
|
|
148
|
+
function extractComponentDetails(filePath) {
|
|
149
|
+
const result = {
|
|
150
|
+
props: {},
|
|
151
|
+
typeAliases: {},
|
|
152
|
+
usageExample: null,
|
|
153
|
+
enums: {},
|
|
154
|
+
genericTypes: {}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (!fs.existsSync(filePath)) return result;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
161
|
+
|
|
162
|
+
// Extract enums (e.g., enum Status { Active = 'active', Inactive = 'inactive' })
|
|
163
|
+
const enumMatches = content.matchAll(/enum\s+(\w+)\s*\{([^}]+)\}/g);
|
|
164
|
+
for (const match of enumMatches) {
|
|
165
|
+
const enumName = match[1];
|
|
166
|
+
const enumBody = match[2];
|
|
167
|
+
|
|
168
|
+
// Extract enum values
|
|
169
|
+
const valueMatches = enumBody.matchAll(/(\w+)\s*=\s*['"]([^'"]+)['"]/g);
|
|
170
|
+
const values = [];
|
|
171
|
+
for (const vm of valueMatches) {
|
|
172
|
+
values.push(vm[2]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Also handle simple enums without explicit values
|
|
176
|
+
if (values.length === 0) {
|
|
177
|
+
const simpleValues = enumBody.match(/\b(\w+)\b(?=\s*[,}])/g);
|
|
178
|
+
if (simpleValues) {
|
|
179
|
+
values.push(...simpleValues.filter(v => v !== 'const'));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (values.length > 0) {
|
|
184
|
+
result.enums[enumName] = values;
|
|
185
|
+
result.typeAliases[enumName] = values; // Also expose as type alias
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Extract type aliases - handle multiple patterns
|
|
190
|
+
// Pattern 1: type X = 'a' | 'b' | 'c' (string literal union)
|
|
191
|
+
const stringUnionMatches = content.matchAll(/type\s+(\w+)\s*=\s*(['"][^'"]+['"](?:\s*\|\s*['"][^'"]+['"])*)/g);
|
|
192
|
+
for (const match of stringUnionMatches) {
|
|
193
|
+
const typeName = match[1];
|
|
194
|
+
const typeValue = match[2];
|
|
195
|
+
const literalMatches = typeValue.match(/['"]([^'"]+)['"]/g);
|
|
196
|
+
if (literalMatches) {
|
|
197
|
+
result.typeAliases[typeName] = literalMatches.map(v => v.replace(/['"]/g, ''));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Pattern 2: type X = number | string | boolean (primitive union)
|
|
202
|
+
const primitiveUnionMatches = content.matchAll(/type\s+(\w+)\s*=\s*((?:string|number|boolean|null|undefined)(?:\s*\|\s*(?:string|number|boolean|null|undefined))*)/g);
|
|
203
|
+
for (const match of primitiveUnionMatches) {
|
|
204
|
+
result.typeAliases[match[1]] = [match[2]]; // Store as single value representing the union
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Pattern 3: type X = typeof Y[number] (indexed access types)
|
|
208
|
+
const indexedAccessMatches = content.matchAll(/type\s+(\w+)\s*=\s*typeof\s+(\w+)\[(?:number|'[^']+')?\]/g);
|
|
209
|
+
for (const match of indexedAccessMatches) {
|
|
210
|
+
const typeName = match[1];
|
|
211
|
+
const sourceArray = match[2];
|
|
212
|
+
// Link to the array type alias if we found it
|
|
213
|
+
if (result.typeAliases[`_array_${sourceArray}`]) {
|
|
214
|
+
result.typeAliases[typeName] = result.typeAliases[`_array_${sourceArray}`];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Pattern 4: type X<T> = ... (generic type definitions)
|
|
219
|
+
const genericTypeMatches = content.matchAll(/type\s+(\w+)<([^>]+)>\s*=\s*([^;\n]+)/g);
|
|
220
|
+
for (const match of genericTypeMatches) {
|
|
221
|
+
result.genericTypes[match[1]] = {
|
|
222
|
+
params: match[2].split(',').map(p => p.trim()),
|
|
223
|
+
definition: match[3].trim()
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Pattern 5: type Props = { ... } (object type alias - treat like interface)
|
|
228
|
+
const typeObjectMatches = content.matchAll(/type\s+(\w+Props)\s*=\s*\{/g);
|
|
229
|
+
for (const match of typeObjectMatches) {
|
|
230
|
+
const typeName = match[1];
|
|
231
|
+
const startIndex = match.index + match[0].length;
|
|
232
|
+
|
|
233
|
+
let braceCount = 1;
|
|
234
|
+
let endIndex = startIndex;
|
|
235
|
+
while (braceCount > 0 && endIndex < content.length) {
|
|
236
|
+
if (content[endIndex] === '{') braceCount++;
|
|
237
|
+
if (content[endIndex] === '}') braceCount--;
|
|
238
|
+
endIndex++;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const propsBody = content.slice(startIndex, endIndex - 1);
|
|
242
|
+
extractPropsFromBody(propsBody, result.props);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Also check for "as const" arrays that define variants
|
|
246
|
+
// e.g., const buttonVariants = ['primary', 'secondary'] as const
|
|
247
|
+
const constArrayMatches = content.matchAll(/(?:export\s+)?const\s+(\w+)\s*=\s*\[([^\]]+)\]\s*(?:as\s+const)?/g);
|
|
248
|
+
for (const match of constArrayMatches) {
|
|
249
|
+
const constName = match[1];
|
|
250
|
+
const arrayContent = match[2];
|
|
251
|
+
|
|
252
|
+
const literalMatches = arrayContent.match(/['"]([^'"]+)['"]/g);
|
|
253
|
+
if (literalMatches) {
|
|
254
|
+
const values = literalMatches.map(v => v.replace(/['"]/g, ''));
|
|
255
|
+
// Store as a pseudo-type for reference
|
|
256
|
+
result.typeAliases[`_array_${constName}`] = values;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Extract props interfaces - handle nested braces with balanced matching
|
|
261
|
+
// Match: interface XxxProps { ... } or interface XxxProps extends YYY { ... }
|
|
262
|
+
const propsInterfaceRegex = /interface\s+(\w+Props)\s*(?:<[^>]+>)?\s*(?:extends[^{]+)?\{/g;
|
|
263
|
+
let propsMatch;
|
|
264
|
+
while ((propsMatch = propsInterfaceRegex.exec(content)) !== null) {
|
|
265
|
+
const startIndex = propsMatch.index + propsMatch[0].length;
|
|
266
|
+
|
|
267
|
+
// Find matching closing brace with balanced brace counting
|
|
268
|
+
let braceCount = 1;
|
|
269
|
+
let endIndex = startIndex;
|
|
270
|
+
while (braceCount > 0 && endIndex < content.length) {
|
|
271
|
+
if (content[endIndex] === '{') braceCount++;
|
|
272
|
+
if (content[endIndex] === '}') braceCount--;
|
|
273
|
+
endIndex++;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const propsBody = content.slice(startIndex, endIndex - 1);
|
|
277
|
+
extractPropsFromBody(propsBody, result.props);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Extract React.FC<Props> or FC<Props> style component definitions
|
|
281
|
+
const fcPropsMatches = content.matchAll(/(?:React\.)?FC<(\w+)>/g);
|
|
282
|
+
for (const match of fcPropsMatches) {
|
|
283
|
+
const propsTypeName = match[1];
|
|
284
|
+
// Mark that this type is used as component props
|
|
285
|
+
result.typeAliases[`_fcProps_${propsTypeName}`] = propsTypeName;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
} catch (err) {
|
|
289
|
+
// Ignore read errors
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Extract props from an interface/type body
|
|
297
|
+
* @param {string} propsBody - The body content between braces
|
|
298
|
+
* @param {Object} propsTarget - Target object to store extracted props
|
|
299
|
+
*/
|
|
300
|
+
function extractPropsFromBody(propsBody, propsTarget) {
|
|
301
|
+
// Parse each prop line - handle multi-line types
|
|
302
|
+
const lines = propsBody.split('\n');
|
|
303
|
+
let currentProp = '';
|
|
304
|
+
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
const trimmed = line.trim();
|
|
307
|
+
|
|
308
|
+
// Skip comments
|
|
309
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;
|
|
310
|
+
|
|
311
|
+
currentProp += ' ' + trimmed;
|
|
312
|
+
|
|
313
|
+
// Check if we have a complete property (ends with ; or has balanced brackets)
|
|
314
|
+
const hasComplete = trimmed.endsWith(';') ||
|
|
315
|
+
(currentProp.split('{').length === currentProp.split('}').length &&
|
|
316
|
+
currentProp.split('<').length === currentProp.split('>').length);
|
|
317
|
+
|
|
318
|
+
if (hasComplete && currentProp.includes(':')) {
|
|
319
|
+
// Parse the accumulated property
|
|
320
|
+
const propMatch = currentProp.match(/^\s*(\w+)(\?)?:\s*(.+?)(?:;|$)/);
|
|
321
|
+
if (propMatch) {
|
|
322
|
+
const propName = propMatch[1];
|
|
323
|
+
const isOptional = !!propMatch[2];
|
|
324
|
+
let propType = propMatch[3].trim();
|
|
325
|
+
|
|
326
|
+
// Skip internal props (starting with $ or _)
|
|
327
|
+
if (!propName.startsWith('$') && !propName.startsWith('_')) {
|
|
328
|
+
// Clean up type (remove comments, trailing semicolons)
|
|
329
|
+
propType = propType.replace(/\/\*.*?\*\//g, '').replace(/;$/, '').trim();
|
|
330
|
+
|
|
331
|
+
propsTarget[propName] = {
|
|
332
|
+
type: propType,
|
|
333
|
+
optional: isOptional
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
currentProp = '';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Generate a usage example for a component
|
|
344
|
+
* @param {string} componentName - Name of the component
|
|
345
|
+
* @param {Object} props - Extracted props
|
|
346
|
+
* @param {Object} typeAliases - Type aliases for string literal unions
|
|
347
|
+
* @returns {{ jsx: string, propsInfo: string[] }}
|
|
348
|
+
*/
|
|
349
|
+
function generateUsageExample(componentName, props, typeAliases) {
|
|
350
|
+
let example = `<${componentName}`;
|
|
351
|
+
const propsInfo = [];
|
|
352
|
+
|
|
353
|
+
// Important props to show in examples
|
|
354
|
+
const importantProps = ['variant', 'size', 'type', 'status', 'color', 'kind'];
|
|
355
|
+
|
|
356
|
+
for (const propName of importantProps) {
|
|
357
|
+
if (props[propName]) {
|
|
358
|
+
const propType = props[propName].type;
|
|
359
|
+
|
|
360
|
+
// Look up the type in our aliases
|
|
361
|
+
let values = typeAliases[propType];
|
|
362
|
+
|
|
363
|
+
// Also check for array-based variants
|
|
364
|
+
if (!values) {
|
|
365
|
+
// Try to find matching array (e.g., variant -> buttonVariants)
|
|
366
|
+
for (const [aliasName, aliasValues] of Object.entries(typeAliases)) {
|
|
367
|
+
if (aliasName.startsWith('_array_') &&
|
|
368
|
+
aliasName.toLowerCase().includes(propName.toLowerCase())) {
|
|
369
|
+
values = aliasValues;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (values && values.length > 0) {
|
|
376
|
+
const defaultValue = values[0];
|
|
377
|
+
example += ` ${propName}="${defaultValue}"`;
|
|
378
|
+
propsInfo.push(`${propName}="${values.join('" | "')}"`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
example += `>{children}</${componentName}>`;
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
jsx: example,
|
|
387
|
+
propsInfo
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Scan a component/module directory for exports and resolve import path
|
|
393
|
+
* @param {string} dirPath - Full path to the component directory
|
|
394
|
+
* @param {string} baseImportPath - Base import path (e.g., '@/components')
|
|
395
|
+
* @param {boolean} includeDetails - Whether to extract props and usage examples
|
|
396
|
+
* @returns {{ exports: string[], types: string[], importPath: string, defaultExport: string|null, arrayExports: string[], props: Object, usageExample: Object|null }|null}
|
|
397
|
+
*/
|
|
398
|
+
function scanModuleExports(dirPath, baseImportPath, includeDetails = false) {
|
|
399
|
+
const dirName = path.basename(dirPath);
|
|
400
|
+
|
|
401
|
+
// Check for index file first
|
|
402
|
+
const indexFiles = ['index.ts', 'index.tsx', 'index.js', 'index.jsx'];
|
|
403
|
+
let mainFile = null;
|
|
404
|
+
let componentFile = null;
|
|
405
|
+
|
|
406
|
+
for (const indexFile of indexFiles) {
|
|
407
|
+
const indexPath = path.join(dirPath, indexFile);
|
|
408
|
+
if (fs.existsSync(indexPath)) {
|
|
409
|
+
mainFile = indexPath;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Also find the main component file for props extraction
|
|
415
|
+
const componentFiles = [`${dirName}.tsx`, `${dirName}.ts`, `${dirName}.jsx`, `${dirName}.js`];
|
|
416
|
+
for (const compFile of componentFiles) {
|
|
417
|
+
const compPath = path.join(dirPath, compFile);
|
|
418
|
+
if (fs.existsSync(compPath)) {
|
|
419
|
+
componentFile = compPath;
|
|
420
|
+
if (!mainFile) mainFile = compPath;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!mainFile) return null;
|
|
426
|
+
|
|
427
|
+
const result = extractExports(mainFile);
|
|
428
|
+
|
|
429
|
+
const moduleResult = {
|
|
430
|
+
exports: [...new Set(result.namedExports)],
|
|
431
|
+
types: [...new Set(result.types)],
|
|
432
|
+
defaultExport: result.defaultExport,
|
|
433
|
+
arrayExports: [...new Set(result.arrayExports)],
|
|
434
|
+
importPath: `${baseImportPath}/${dirName}`
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// Extract props and generate usage example if requested
|
|
438
|
+
if (includeDetails && componentFile) {
|
|
439
|
+
const details = extractComponentDetails(componentFile);
|
|
440
|
+
moduleResult.props = details.props;
|
|
441
|
+
moduleResult.typeAliases = details.typeAliases;
|
|
442
|
+
|
|
443
|
+
// Generate usage example
|
|
444
|
+
if (Object.keys(details.props).length > 0) {
|
|
445
|
+
moduleResult.usageExample = generateUsageExample(dirName, details.props, details.typeAliases);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return moduleResult;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Scan a single file (not a directory) for exports
|
|
454
|
+
* @param {string} filePath - Full path to the file
|
|
455
|
+
* @param {string} baseImportPath - Base import path
|
|
456
|
+
* @returns {{ exports: string[], types: string[], importPath: string, defaultExport: string|null }|null}
|
|
457
|
+
*/
|
|
458
|
+
function scanFileExports(filePath, baseImportPath) {
|
|
459
|
+
if (!fs.existsSync(filePath)) return null;
|
|
460
|
+
|
|
461
|
+
const fileName = path.basename(filePath);
|
|
462
|
+
const fileNameWithoutExt = fileName.replace(/\.(tsx?|jsx?)$/, '');
|
|
463
|
+
|
|
464
|
+
const result = extractExports(filePath);
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
exports: [...new Set(result.namedExports)],
|
|
468
|
+
types: [...new Set(result.types)],
|
|
469
|
+
defaultExport: result.defaultExport,
|
|
470
|
+
importPath: `${baseImportPath}/${fileNameWithoutExt}`
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ============================================================
|
|
475
|
+
// Export Map Building
|
|
476
|
+
// ============================================================
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Build export map for all configured directories
|
|
480
|
+
* @param {object} config - Project config
|
|
481
|
+
* @returns {object} Export map with components, hooks, types, etc.
|
|
482
|
+
*/
|
|
483
|
+
function buildExportMap(config) {
|
|
484
|
+
const projectContext = config.hybrid?.projectContext || {};
|
|
485
|
+
const exportMap = {
|
|
486
|
+
components: {},
|
|
487
|
+
hooks: {},
|
|
488
|
+
services: {},
|
|
489
|
+
types: {},
|
|
490
|
+
utils: {},
|
|
491
|
+
_meta: {
|
|
492
|
+
generatedAt: new Date().toISOString(),
|
|
493
|
+
projectRoot: PROJECT_ROOT
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Scan component directories (with details for usage examples)
|
|
498
|
+
const componentDirs = projectContext.componentDirs || ['src/components'];
|
|
499
|
+
for (const dir of componentDirs) {
|
|
500
|
+
const fullDir = path.join(PROJECT_ROOT, dir);
|
|
501
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
502
|
+
|
|
503
|
+
// Include details (props, usage examples) for components
|
|
504
|
+
scanDirectory(fullDir, '@/components', exportMap.components, true);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Scan hooks directory (use config if available, with glob support)
|
|
508
|
+
const hooksDirs = projectContext.hookDirs || ['src/hooks', 'hooks'];
|
|
509
|
+
for (const dir of hooksDirs) {
|
|
510
|
+
// Convert directory path to import path
|
|
511
|
+
// apps/web/src/features/auth/hooks -> @/features/auth/hooks
|
|
512
|
+
// src/hooks -> @/hooks
|
|
513
|
+
const importBase = dir
|
|
514
|
+
.replace(/^apps\/\w+\/src\//, '@/') // apps/web/src/ -> @/
|
|
515
|
+
.replace(/^src\//, '@/'); // src/ -> @/
|
|
516
|
+
|
|
517
|
+
// Handle glob patterns like src/hooks/*.ts
|
|
518
|
+
if (dir.includes('*')) {
|
|
519
|
+
const baseDir = dir.split('*')[0].replace(/\/$/, '');
|
|
520
|
+
const fullDir = path.join(PROJECT_ROOT, baseDir);
|
|
521
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
522
|
+
|
|
523
|
+
scanDirectoryFlat(fullDir, importBase.split('*')[0].replace(/\/$/, ''), exportMap.hooks);
|
|
524
|
+
} else {
|
|
525
|
+
const fullDir = path.join(PROJECT_ROOT, dir);
|
|
526
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
527
|
+
|
|
528
|
+
// Hooks can be individual files or directories
|
|
529
|
+
scanDirectoryFlat(fullDir, importBase, exportMap.hooks);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Scan services directory
|
|
534
|
+
const servicesDirs = ['src/services', 'services', 'src/lib'];
|
|
535
|
+
for (const dir of servicesDirs) {
|
|
536
|
+
const fullDir = path.join(PROJECT_ROOT, dir);
|
|
537
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
538
|
+
|
|
539
|
+
scanDirectoryFlat(fullDir, dir.startsWith('src/') ? `@/${dir.replace('src/', '')}` : `@/${dir}`, exportMap.services);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Scan type directories
|
|
543
|
+
const typeDirs = projectContext.typeDirs || ['src/types'];
|
|
544
|
+
for (const dir of typeDirs) {
|
|
545
|
+
// Handle glob patterns like src/types/*.ts
|
|
546
|
+
if (dir.includes('*')) {
|
|
547
|
+
const baseDir = dir.split('*')[0].replace(/\/$/, '');
|
|
548
|
+
const fullDir = path.join(PROJECT_ROOT, baseDir);
|
|
549
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
550
|
+
|
|
551
|
+
scanDirectoryFlat(fullDir, '@/types', exportMap.types, true);
|
|
552
|
+
} else {
|
|
553
|
+
const fullDir = path.join(PROJECT_ROOT, dir);
|
|
554
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
555
|
+
|
|
556
|
+
scanDirectoryFlat(fullDir, '@/types', exportMap.types, true);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Scan utils directory
|
|
561
|
+
const utilsDirs = ['src/utils', 'src/lib/utils', 'utils'];
|
|
562
|
+
for (const dir of utilsDirs) {
|
|
563
|
+
const fullDir = path.join(PROJECT_ROOT, dir);
|
|
564
|
+
if (!fs.existsSync(fullDir)) continue;
|
|
565
|
+
|
|
566
|
+
scanDirectoryFlat(fullDir, dir.startsWith('src/') ? `@/${dir.replace('src/', '')}` : `@/${dir}`, exportMap.utils);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return exportMap;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Scan a directory containing subdirectories (like src/components/)
|
|
574
|
+
* @param {boolean} includeDetails - Whether to extract props and usage examples
|
|
575
|
+
*/
|
|
576
|
+
function scanDirectory(dirPath, baseImportPath, target, includeDetails = false) {
|
|
577
|
+
try {
|
|
578
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
579
|
+
|
|
580
|
+
for (const entry of entries) {
|
|
581
|
+
if (!entry.isDirectory()) continue;
|
|
582
|
+
|
|
583
|
+
// Skip common excluded directories
|
|
584
|
+
if (['__tests__', '__mocks__', 'node_modules', '.git'].includes(entry.name)) continue;
|
|
585
|
+
|
|
586
|
+
const modulePath = path.join(dirPath, entry.name);
|
|
587
|
+
const result = scanModuleExports(modulePath, baseImportPath, includeDetails);
|
|
588
|
+
|
|
589
|
+
if (result && (result.exports.length > 0 || result.defaultExport)) {
|
|
590
|
+
target[entry.name] = result;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
} catch (err) {
|
|
594
|
+
// Ignore scan errors
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Scan a directory containing files (like src/hooks/)
|
|
600
|
+
*/
|
|
601
|
+
function scanDirectoryFlat(dirPath, baseImportPath, target, typesOnly = false) {
|
|
602
|
+
try {
|
|
603
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
604
|
+
|
|
605
|
+
for (const entry of entries) {
|
|
606
|
+
// Skip test files and common excludes
|
|
607
|
+
if (entry.name.includes('.test.') || entry.name.includes('.spec.')) continue;
|
|
608
|
+
if (entry.name.includes('.stories.')) continue;
|
|
609
|
+
if (entry.name === 'index.ts' || entry.name === 'index.js') continue;
|
|
610
|
+
|
|
611
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
612
|
+
|
|
613
|
+
if (entry.isDirectory()) {
|
|
614
|
+
// Recurse into subdirectory
|
|
615
|
+
const result = scanModuleExports(entryPath, baseImportPath);
|
|
616
|
+
if (result && (result.exports.length > 0 || result.defaultExport || result.types.length > 0)) {
|
|
617
|
+
target[entry.name] = result;
|
|
618
|
+
}
|
|
619
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
620
|
+
const result = scanFileExports(entryPath, baseImportPath);
|
|
621
|
+
if (result) {
|
|
622
|
+
const key = entry.name.replace(/\.(tsx?|jsx?)$/, '');
|
|
623
|
+
if (typesOnly) {
|
|
624
|
+
if (result.types.length > 0) {
|
|
625
|
+
target[key] = result;
|
|
626
|
+
}
|
|
627
|
+
} else if (result.exports.length > 0 || result.defaultExport) {
|
|
628
|
+
target[key] = result;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} catch (err) {
|
|
634
|
+
// Ignore scan errors
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ============================================================
|
|
639
|
+
// Caching
|
|
640
|
+
// ============================================================
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Load cached export map if it's fresh
|
|
644
|
+
* @returns {object|null} Cached export map or null
|
|
645
|
+
*/
|
|
646
|
+
function loadCachedExportMap() {
|
|
647
|
+
try {
|
|
648
|
+
if (!fs.existsSync(CACHE_PATH)) return null;
|
|
649
|
+
|
|
650
|
+
const stat = fs.statSync(CACHE_PATH);
|
|
651
|
+
const age = Date.now() - stat.mtimeMs;
|
|
652
|
+
|
|
653
|
+
if (age > CACHE_MAX_AGE_MS) return null;
|
|
654
|
+
|
|
655
|
+
return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf-8'));
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Save export map to cache
|
|
663
|
+
* @param {object} exportMap - Export map to cache
|
|
664
|
+
*/
|
|
665
|
+
function saveExportMapCache(exportMap) {
|
|
666
|
+
try {
|
|
667
|
+
const stateDir = path.dirname(CACHE_PATH);
|
|
668
|
+
if (!fs.existsSync(stateDir)) {
|
|
669
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
670
|
+
}
|
|
671
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify(exportMap, null, 2));
|
|
672
|
+
} catch (err) {
|
|
673
|
+
console.error(`Warning: Could not cache export map: ${err.message}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Clear the export map cache
|
|
679
|
+
*/
|
|
680
|
+
function clearCache() {
|
|
681
|
+
try {
|
|
682
|
+
if (fs.existsSync(CACHE_PATH)) {
|
|
683
|
+
fs.unlinkSync(CACHE_PATH);
|
|
684
|
+
console.log('✓ Cleared export map cache');
|
|
685
|
+
}
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.error(`Warning: Could not clear cache: ${err.message}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ============================================================
|
|
692
|
+
// Formatting for Templates
|
|
693
|
+
// ============================================================
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Format export map as markdown for templates
|
|
697
|
+
* @param {object} exportMap - Export map
|
|
698
|
+
* @returns {string} Markdown-formatted export list
|
|
699
|
+
*/
|
|
700
|
+
function formatExportMapForTemplate(exportMap) {
|
|
701
|
+
const lines = [];
|
|
702
|
+
|
|
703
|
+
// Components
|
|
704
|
+
if (Object.keys(exportMap.components).length > 0) {
|
|
705
|
+
lines.push('#### Components');
|
|
706
|
+
for (const [name, info] of Object.entries(exportMap.components)) {
|
|
707
|
+
const exports = info.exports.join(', ') || (info.defaultExport ? `default: ${info.defaultExport}` : '');
|
|
708
|
+
if (exports) {
|
|
709
|
+
lines.push(`- \`import { ${info.exports.join(', ')} } from '${info.importPath}'\``);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
lines.push('');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Hooks
|
|
716
|
+
if (Object.keys(exportMap.hooks).length > 0) {
|
|
717
|
+
lines.push('#### Hooks');
|
|
718
|
+
for (const [name, info] of Object.entries(exportMap.hooks)) {
|
|
719
|
+
const exports = info.exports.join(', ');
|
|
720
|
+
if (exports) {
|
|
721
|
+
lines.push(`- \`import { ${exports} } from '${info.importPath}'\``);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
lines.push('');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Services
|
|
728
|
+
if (Object.keys(exportMap.services).length > 0) {
|
|
729
|
+
lines.push('#### Services');
|
|
730
|
+
for (const [name, info] of Object.entries(exportMap.services)) {
|
|
731
|
+
const exports = info.exports.join(', ');
|
|
732
|
+
if (exports) {
|
|
733
|
+
lines.push(`- \`import { ${exports} } from '${info.importPath}'\``);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
lines.push('');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Types
|
|
740
|
+
if (Object.keys(exportMap.types).length > 0) {
|
|
741
|
+
lines.push('#### Types');
|
|
742
|
+
for (const [name, info] of Object.entries(exportMap.types)) {
|
|
743
|
+
const types = info.types.join(', ');
|
|
744
|
+
if (types) {
|
|
745
|
+
lines.push(`- \`import type { ${types} } from '${info.importPath}'\``);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
lines.push('');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Utils
|
|
752
|
+
if (Object.keys(exportMap.utils).length > 0) {
|
|
753
|
+
lines.push('#### Utilities');
|
|
754
|
+
for (const [name, info] of Object.entries(exportMap.utils)) {
|
|
755
|
+
const exports = info.exports.join(', ');
|
|
756
|
+
if (exports) {
|
|
757
|
+
lines.push(`- \`import { ${exports} } from '${info.importPath}'\``);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
lines.push('');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return lines.join('\n');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ============================================================
|
|
767
|
+
// Component Usage Validation
|
|
768
|
+
// ============================================================
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Validate component usage patterns in generated code
|
|
772
|
+
* @param {string} code - Generated code to validate
|
|
773
|
+
* @param {object} exportMap - Export map with array export info
|
|
774
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
775
|
+
*/
|
|
776
|
+
function validateComponentUsage(code, exportMap = null) {
|
|
777
|
+
const errors = [];
|
|
778
|
+
const warnings = [];
|
|
779
|
+
|
|
780
|
+
// Load export map if not provided
|
|
781
|
+
if (!exportMap) {
|
|
782
|
+
exportMap = loadCachedExportMap();
|
|
783
|
+
if (!exportMap) {
|
|
784
|
+
return { valid: true, errors: [], warnings: ['No export map available for validation'] };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Collect all array exports from components
|
|
789
|
+
const arrayExports = new Set();
|
|
790
|
+
for (const [name, info] of Object.entries(exportMap.components || {})) {
|
|
791
|
+
if (info.arrayExports) {
|
|
792
|
+
info.arrayExports.forEach(e => arrayExports.add(e));
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Check for array-as-object access patterns
|
|
797
|
+
// e.g., cardVariants.default, buttonVariants.primary
|
|
798
|
+
const arrayAccessPattern = /(\w+(?:Variants|Sizes|Statuses|Options))\.(\w+)/g;
|
|
799
|
+
const matches = code.matchAll(arrayAccessPattern);
|
|
800
|
+
|
|
801
|
+
for (const match of matches) {
|
|
802
|
+
const exportName = match[1];
|
|
803
|
+
const accessedProp = match[2];
|
|
804
|
+
|
|
805
|
+
// If this is a known array export, it's wrong to access it as an object
|
|
806
|
+
if (arrayExports.has(exportName)) {
|
|
807
|
+
errors.push(
|
|
808
|
+
`Invalid usage: "${match[0]}" - ${exportName} is an ARRAY, not an object. ` +
|
|
809
|
+
`Use string literal: "${accessedProp}" instead of ${exportName}.${accessedProp}`
|
|
810
|
+
);
|
|
811
|
+
} else {
|
|
812
|
+
// Even if not in our export map, warn about common patterns
|
|
813
|
+
warnings.push(
|
|
814
|
+
`Suspicious pattern: "${match[0]}" - ${exportName} is likely an array. ` +
|
|
815
|
+
`Consider using string literal: "${accessedProp}"`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Check for variant/size/type props using object access instead of string literals
|
|
821
|
+
// e.g., variant={buttonVariants.primary} instead of variant="primary"
|
|
822
|
+
const propObjectPattern = /(?:variant|size|type|status)=\{(\w+(?:Variants|Sizes|Types|Statuses))\.(\w+)\}/g;
|
|
823
|
+
const propMatches = code.matchAll(propObjectPattern);
|
|
824
|
+
|
|
825
|
+
for (const match of propMatches) {
|
|
826
|
+
const exportName = match[1];
|
|
827
|
+
const value = match[2];
|
|
828
|
+
errors.push(
|
|
829
|
+
`Invalid prop usage: "${match[0]}" - Use string literal instead: ` +
|
|
830
|
+
`variant="${value}" (NOT {${exportName}.${value}})`
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Check for hook file name vs export name mismatches
|
|
835
|
+
// Common pattern: use-auth-store.ts exports useAuthState, not useAuthStore
|
|
836
|
+
const hookPatterns = [
|
|
837
|
+
{ pattern: /useAuthStore\(\)/g, suggestion: 'useAuthState()' },
|
|
838
|
+
{ pattern: /useUserStore\(\)/g, suggestion: 'useUserState()' },
|
|
839
|
+
{ pattern: /useCartStore\(\)/g, suggestion: 'useCartState()' },
|
|
840
|
+
];
|
|
841
|
+
|
|
842
|
+
for (const { pattern, suggestion } of hookPatterns) {
|
|
843
|
+
if (pattern.test(code)) {
|
|
844
|
+
// Check if the actual export exists
|
|
845
|
+
const wrongName = pattern.source.replace(/\\/g, '').replace(/\(\)/g, '');
|
|
846
|
+
let found = false;
|
|
847
|
+
for (const [name, info] of Object.entries(exportMap.hooks || {})) {
|
|
848
|
+
if (info.exports?.includes(wrongName)) {
|
|
849
|
+
found = true;
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (!found) {
|
|
854
|
+
warnings.push(
|
|
855
|
+
`Possible hook name mistake: Check if "${wrongName}" is the correct export name. ` +
|
|
856
|
+
`File names often differ from export names (e.g., use-auth-store.ts might export ${suggestion.replace('()', '')})`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return {
|
|
863
|
+
valid: errors.length === 0,
|
|
864
|
+
errors,
|
|
865
|
+
warnings
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Format component with usage example for context
|
|
871
|
+
* @param {string} name - Component name
|
|
872
|
+
* @param {object} info - Component info from export map
|
|
873
|
+
* @returns {string} Formatted markdown
|
|
874
|
+
*/
|
|
875
|
+
function formatComponentWithUsage(name, info) {
|
|
876
|
+
let output = `#### ${name}\n\n`;
|
|
877
|
+
output += '```typescript\n';
|
|
878
|
+
if (info.exports.length > 0) {
|
|
879
|
+
output += `import { ${info.exports.join(', ')} } from '${info.importPath}';\n`;
|
|
880
|
+
} else if (info.defaultExport) {
|
|
881
|
+
output += `import ${info.defaultExport} from '${info.importPath}';\n`;
|
|
882
|
+
}
|
|
883
|
+
output += '```\n\n';
|
|
884
|
+
|
|
885
|
+
// Show props table if available
|
|
886
|
+
if (info.props && Object.keys(info.props).length > 0) {
|
|
887
|
+
output += '**Props:**\n';
|
|
888
|
+
|
|
889
|
+
// Important props to show first (styling/behavior related)
|
|
890
|
+
const importantProps = ['variant', 'size', 'padding', 'status', 'type', 'color', 'disabled', 'checked', 'onChange', 'onClick', 'children'];
|
|
891
|
+
const shownProps = new Set();
|
|
892
|
+
|
|
893
|
+
// Show important props first
|
|
894
|
+
for (const propName of importantProps) {
|
|
895
|
+
if (info.props[propName]) {
|
|
896
|
+
const propInfo = info.props[propName];
|
|
897
|
+
const optional = propInfo.optional ? '?' : '';
|
|
898
|
+
let typeDisplay = propInfo.type;
|
|
899
|
+
|
|
900
|
+
// Resolve type alias to actual values if available
|
|
901
|
+
if (info.typeAliases && info.typeAliases[propInfo.type]) {
|
|
902
|
+
typeDisplay = `"${info.typeAliases[propInfo.type].join('" | "')}"`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
output += `- \`${propName}${optional}\`: ${typeDisplay}\n`;
|
|
906
|
+
shownProps.add(propName);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Show remaining props (up to 3 more non-event, non-internal props)
|
|
911
|
+
let extraCount = 0;
|
|
912
|
+
for (const [propName, propInfo] of Object.entries(info.props)) {
|
|
913
|
+
if (shownProps.has(propName)) continue;
|
|
914
|
+
if (propName.startsWith('on') && propName !== 'onChange' && propName !== 'onClick') continue;
|
|
915
|
+
if (extraCount >= 3) break;
|
|
916
|
+
|
|
917
|
+
const optional = propInfo.optional ? '?' : '';
|
|
918
|
+
let typeDisplay = propInfo.type;
|
|
919
|
+
|
|
920
|
+
if (info.typeAliases && info.typeAliases[propInfo.type]) {
|
|
921
|
+
typeDisplay = `"${info.typeAliases[propInfo.type].join('" | "')}"`;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
output += `- \`${propName}${optional}\`: ${typeDisplay}\n`;
|
|
925
|
+
extraCount++;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
output += '\n';
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Add usage example
|
|
932
|
+
if (info.usageExample) {
|
|
933
|
+
output += '**Usage:**\n```tsx\n';
|
|
934
|
+
output += info.usageExample.jsx + '\n';
|
|
935
|
+
output += '```\n\n';
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Add warning about array exports
|
|
939
|
+
if (info.arrayExports && info.arrayExports.length > 0) {
|
|
940
|
+
output += `⚠️ \`${info.arrayExports.join('`, `')}\` are arrays for iteration, NOT objects.\n\n`;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return output;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ============================================================
|
|
947
|
+
// CLI
|
|
948
|
+
// ============================================================
|
|
949
|
+
|
|
950
|
+
function printUsage() {
|
|
951
|
+
console.log(`
|
|
952
|
+
Wogi Flow - Export Scanner
|
|
953
|
+
|
|
954
|
+
Scans your project for TypeScript/JavaScript exports to build an accurate
|
|
955
|
+
import map for the local LLM. This ensures generated code uses only valid imports.
|
|
956
|
+
|
|
957
|
+
Usage:
|
|
958
|
+
node flow-export-scanner.js [project-root]
|
|
959
|
+
node flow-export-scanner.js --cache # Use cached map if fresh (5 min)
|
|
960
|
+
node flow-export-scanner.js --clear # Clear the cache
|
|
961
|
+
node flow-export-scanner.js --format # Output formatted for templates
|
|
962
|
+
|
|
963
|
+
Output is saved to: .workflow/state/export-map.json
|
|
964
|
+
`);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Main - CLI execution
|
|
968
|
+
if (require.main === module) {
|
|
969
|
+
// Set project root from CLI arg (only when running directly as CLI)
|
|
970
|
+
const cliRoot = process.argv[2] && !process.argv[2].startsWith('--')
|
|
971
|
+
? path.resolve(process.argv[2])
|
|
972
|
+
: process.cwd();
|
|
973
|
+
setProjectRoot(cliRoot);
|
|
974
|
+
|
|
975
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
976
|
+
printUsage();
|
|
977
|
+
process.exit(0);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (process.argv.includes('--clear')) {
|
|
981
|
+
clearCache();
|
|
982
|
+
process.exit(0);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const useCache = process.argv.includes('--cache');
|
|
986
|
+
const formatOutput = process.argv.includes('--format');
|
|
987
|
+
|
|
988
|
+
let exportMap;
|
|
989
|
+
|
|
990
|
+
if (useCache) {
|
|
991
|
+
exportMap = loadCachedExportMap();
|
|
992
|
+
if (exportMap) {
|
|
993
|
+
console.log('Using cached export map');
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (!exportMap) {
|
|
998
|
+
console.log('Scanning project exports...\n');
|
|
999
|
+
const config = loadConfig();
|
|
1000
|
+
exportMap = buildExportMap(config);
|
|
1001
|
+
saveExportMapCache(exportMap);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Report findings
|
|
1005
|
+
const componentCount = Object.keys(exportMap.components).length;
|
|
1006
|
+
const hookCount = Object.keys(exportMap.hooks).length;
|
|
1007
|
+
const serviceCount = Object.keys(exportMap.services).length;
|
|
1008
|
+
const typeCount = Object.keys(exportMap.types).length;
|
|
1009
|
+
const utilCount = Object.keys(exportMap.utils).length;
|
|
1010
|
+
|
|
1011
|
+
console.log(`Found exports:`);
|
|
1012
|
+
console.log(` Components: ${componentCount}`);
|
|
1013
|
+
console.log(` Hooks: ${hookCount}`);
|
|
1014
|
+
console.log(` Services: ${serviceCount}`);
|
|
1015
|
+
console.log(` Types: ${typeCount}`);
|
|
1016
|
+
console.log(` Utils: ${utilCount}`);
|
|
1017
|
+
|
|
1018
|
+
if (formatOutput) {
|
|
1019
|
+
console.log('\n--- Template Format ---\n');
|
|
1020
|
+
console.log(formatExportMapForTemplate(exportMap));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
console.log(`\n✓ Export map saved to ${CACHE_PATH}`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
module.exports = {
|
|
1027
|
+
// Core scanning functions
|
|
1028
|
+
extractExports,
|
|
1029
|
+
extractComponentDetails,
|
|
1030
|
+
generateUsageExample,
|
|
1031
|
+
scanModuleExports,
|
|
1032
|
+
scanFileExports,
|
|
1033
|
+
buildExportMap,
|
|
1034
|
+
// Cache functions
|
|
1035
|
+
loadCachedExportMap,
|
|
1036
|
+
saveExportMapCache,
|
|
1037
|
+
clearCache,
|
|
1038
|
+
// Formatting functions
|
|
1039
|
+
formatExportMapForTemplate,
|
|
1040
|
+
validateComponentUsage,
|
|
1041
|
+
formatComponentWithUsage,
|
|
1042
|
+
// Configuration functions (for use as module)
|
|
1043
|
+
setProjectRoot,
|
|
1044
|
+
getProjectRoot,
|
|
1045
|
+
loadConfig
|
|
1046
|
+
};
|