@vira-ui/cli 1.0.1 → 1.0.2
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 +965 -0
- package/dist/index.js +1221 -34
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -91,8 +91,79 @@ const program = new commander_1.Command();
|
|
|
91
91
|
program
|
|
92
92
|
.name("vira")
|
|
93
93
|
.description("ViraJS CLI - Create projects and generate code")
|
|
94
|
-
.version("0.
|
|
94
|
+
.version("1.0.1");
|
|
95
95
|
const SUPPORTED_TEMPLATES = ["frontend", "fullstack", "kanban"];
|
|
96
|
+
/**
|
|
97
|
+
* Инициализация проекта в текущей директории
|
|
98
|
+
*/
|
|
99
|
+
program
|
|
100
|
+
.command("init")
|
|
101
|
+
.description("Initialize a Vira project in the current directory")
|
|
102
|
+
.option("-t, --template <template>", "Template type (frontend|fullstack|kanban). If not specified, interactive selection will be shown.")
|
|
103
|
+
.action(async (options) => {
|
|
104
|
+
const projectPath = process.cwd();
|
|
105
|
+
const projectName = path.basename(projectPath);
|
|
106
|
+
// Проверяем, не пуста ли директория
|
|
107
|
+
const files = await fs.readdir(projectPath);
|
|
108
|
+
const ignoreFiles = ['.git', '.gitignore', 'node_modules', '.DS_Store'];
|
|
109
|
+
const visibleFiles = files.filter(f => !ignoreFiles.includes(f) && !f.startsWith('.'));
|
|
110
|
+
if (visibleFiles.length > 0) {
|
|
111
|
+
const { proceed } = await inquirer_1.default.prompt([
|
|
112
|
+
{
|
|
113
|
+
type: "confirm",
|
|
114
|
+
name: "proceed",
|
|
115
|
+
message: `Directory is not empty. Continue anyway?`,
|
|
116
|
+
default: false,
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
if (!proceed) {
|
|
120
|
+
console.log(chalk_1.default.yellow("Init cancelled."));
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log(chalk_1.default.blue(`\nInitializing Vira project in: ${projectPath}\n`));
|
|
125
|
+
// Интерактивный выбор шаблона, если не указан
|
|
126
|
+
let template;
|
|
127
|
+
if (options.template) {
|
|
128
|
+
template = options.template;
|
|
129
|
+
if (!SUPPORTED_TEMPLATES.includes(template)) {
|
|
130
|
+
console.error(chalk_1.default.red(`Unknown template: ${template}. Use one of: ${SUPPORTED_TEMPLATES.join(", ")}`));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const { selectedTemplate } = await inquirer_1.default.prompt([
|
|
136
|
+
{
|
|
137
|
+
type: "list",
|
|
138
|
+
name: "selectedTemplate",
|
|
139
|
+
message: "Select a template:",
|
|
140
|
+
choices: [
|
|
141
|
+
{
|
|
142
|
+
name: "Frontend (React + Vite + Vira UI)",
|
|
143
|
+
value: "frontend",
|
|
144
|
+
short: "frontend",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "Fullstack (Frontend + Go Backend + Docker)",
|
|
148
|
+
value: "fullstack",
|
|
149
|
+
short: "fullstack",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "Kanban (Reference app with VRP)",
|
|
153
|
+
value: "kanban",
|
|
154
|
+
short: "kanban",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
default: "frontend",
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
template = selectedTemplate;
|
|
161
|
+
}
|
|
162
|
+
// Создаём структуру проекта
|
|
163
|
+
await createProjectStructure(projectPath, template);
|
|
164
|
+
console.log(chalk_1.default.green(`\n✓ Project initialized successfully!\n`));
|
|
165
|
+
printNextSteps(projectName, template, true);
|
|
166
|
+
});
|
|
96
167
|
/**
|
|
97
168
|
* Создание проекта
|
|
98
169
|
*/
|
|
@@ -159,18 +230,27 @@ program
|
|
|
159
230
|
.command("generate")
|
|
160
231
|
.alias("g")
|
|
161
232
|
.description("Generate code")
|
|
162
|
-
.argument("<type>", "Type: component, service, page, model, route")
|
|
233
|
+
.argument("<type>", "Type: component, service, page, model, route, test")
|
|
163
234
|
.argument("<name>", "Name")
|
|
164
235
|
.option("-d, --dir <directory>", "Output directory", "src")
|
|
236
|
+
.option("-i, --interactive", "Interactive mode (for components and services)", false)
|
|
237
|
+
.option("--vrp", "Use Vira Reactive Protocol (VRP)", false)
|
|
238
|
+
.option("--no-vrp", "Do not use VRP (explicit)", false)
|
|
165
239
|
.action(async (type, name, options) => {
|
|
166
240
|
console.log(chalk_1.default.blue(`Generating ${type}: ${name}`));
|
|
167
241
|
switch (type) {
|
|
168
242
|
case "component":
|
|
169
243
|
case "comp":
|
|
170
|
-
await generateComponent(name, options.dir
|
|
244
|
+
await generateComponent(name, options.dir, {
|
|
245
|
+
interactive: options.interactive ?? false,
|
|
246
|
+
useVRP: options.vrp ? true : options.noVrp ? false : undefined,
|
|
247
|
+
});
|
|
171
248
|
break;
|
|
172
249
|
case "service":
|
|
173
|
-
await generateService(name, options.dir
|
|
250
|
+
await generateService(name, options.dir, {
|
|
251
|
+
interactive: options.interactive ?? false,
|
|
252
|
+
useVRP: options.vrp ? true : options.noVrp ? false : undefined,
|
|
253
|
+
});
|
|
174
254
|
break;
|
|
175
255
|
case "page":
|
|
176
256
|
await generatePage(name, options.dir);
|
|
@@ -181,6 +261,9 @@ program
|
|
|
181
261
|
case "route":
|
|
182
262
|
await generateRoute(name, options.dir);
|
|
183
263
|
break;
|
|
264
|
+
case "test":
|
|
265
|
+
await generateTest(name, options.dir);
|
|
266
|
+
break;
|
|
184
267
|
default:
|
|
185
268
|
console.error(chalk_1.default.red(`Unknown type: ${type}`));
|
|
186
269
|
process.exit(1);
|
|
@@ -227,22 +310,105 @@ make
|
|
|
227
310
|
await generateEventHandler(name, options.dir);
|
|
228
311
|
console.log(chalk_1.default.green(`✓ event handler ${name} created in ${options.dir}`));
|
|
229
312
|
});
|
|
313
|
+
make
|
|
314
|
+
.command("crud")
|
|
315
|
+
.description("Create CRUD handlers for a resource")
|
|
316
|
+
.argument("<name>", "Resource name (e.g. user)")
|
|
317
|
+
.option("-d, --dir <directory>", "Target directory", path.join("backend", "internal", "handlers"))
|
|
318
|
+
.option("-m, --model <model>", "Model name (defaults to capitalized resource name)")
|
|
319
|
+
.action(async (name, options) => {
|
|
320
|
+
await generateCRUDHandler(name, options.dir, options.model);
|
|
321
|
+
console.log(chalk_1.default.green(`✓ CRUD handlers for ${name} created in ${options.dir}`));
|
|
322
|
+
});
|
|
230
323
|
const protoCommand = program
|
|
231
324
|
.command("proto")
|
|
232
|
-
.description("VRP
|
|
325
|
+
.description("VRP (Vira Reactive Protocol) utilities");
|
|
233
326
|
protoCommand
|
|
234
327
|
.command("validate")
|
|
235
|
-
.description("Validate VRP protocol schema")
|
|
236
|
-
.
|
|
237
|
-
|
|
238
|
-
|
|
328
|
+
.description("Validate VRP protocol schema and types")
|
|
329
|
+
.option("--file <path>", "Path to types file", path.join("backend", "internal", "types", "types.go"))
|
|
330
|
+
.action(async (options) => {
|
|
331
|
+
const typesPath = path.resolve(process.cwd(), options.file || path.join("backend", "internal", "types", "types.go"));
|
|
332
|
+
try {
|
|
333
|
+
if (await fs.pathExists(typesPath)) {
|
|
334
|
+
const content = await fs.readFile(typesPath, "utf8");
|
|
335
|
+
const structs = parseGoStructs(content);
|
|
336
|
+
if (structs.length === 0) {
|
|
337
|
+
console.log(chalk_1.default.yellow("⚠ No Go structs found in types file"));
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
console.log(chalk_1.default.green(`✓ VRP v0.1 protocol schema validated`));
|
|
341
|
+
console.log(chalk_1.default.gray(` Found ${structs.length} type(s): ${structs.map(s => s.name).join(", ")}`));
|
|
342
|
+
console.log(chalk_1.default.gray(" See VIRA_PROTOCOL.md for full specification"));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
console.log(chalk_1.default.yellow(`⚠ Types file not found: ${typesPath}`));
|
|
347
|
+
console.log(chalk_1.default.gray(" VRP protocol schema structure is valid"));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
console.log(chalk_1.default.yellow("⚠ Could not validate types file"));
|
|
352
|
+
console.log(chalk_1.default.gray(" VRP protocol schema structure is valid"));
|
|
353
|
+
}
|
|
239
354
|
});
|
|
240
355
|
protoCommand
|
|
241
356
|
.command("generate")
|
|
242
|
-
.description("Generate protocol documentation
|
|
243
|
-
.
|
|
244
|
-
|
|
245
|
-
|
|
357
|
+
.description("Generate protocol documentation and channel helpers")
|
|
358
|
+
.option("--file <path>", "Path to types file", path.join("backend", "internal", "types", "types.go"))
|
|
359
|
+
.option("--output <path>", "Output directory", "docs")
|
|
360
|
+
.action(async (options) => {
|
|
361
|
+
const typesPath = path.resolve(process.cwd(), options.file || path.join("backend", "internal", "types", "types.go"));
|
|
362
|
+
const outputDir = path.resolve(process.cwd(), options.output || "docs");
|
|
363
|
+
try {
|
|
364
|
+
if (await fs.pathExists(typesPath)) {
|
|
365
|
+
const content = await fs.readFile(typesPath, "utf8");
|
|
366
|
+
const structs = parseGoStructs(content);
|
|
367
|
+
await fs.ensureDir(outputDir);
|
|
368
|
+
// Генерируем документацию по каналам
|
|
369
|
+
const channelDocs = generateChannelDocumentation(structs);
|
|
370
|
+
await fs.writeFile(path.join(outputDir, "VRP_CHANNELS.md"), channelDocs);
|
|
371
|
+
console.log(chalk_1.default.green(`✓ VRP protocol docs generated`));
|
|
372
|
+
console.log(chalk_1.default.gray(` Channels: ${outputDir}/VRP_CHANNELS.md`));
|
|
373
|
+
console.log(chalk_1.default.gray(` Protocol version: v0.1`));
|
|
374
|
+
console.log(chalk_1.default.gray(` Types: ${structs.length}`));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
console.log(chalk_1.default.yellow(`⚠ Types file not found: ${typesPath}`));
|
|
378
|
+
console.log(chalk_1.default.gray(" Generating basic protocol documentation"));
|
|
379
|
+
await fs.ensureDir(outputDir);
|
|
380
|
+
const basicDocs = `# Vira Reactive Protocol (VRP)
|
|
381
|
+
|
|
382
|
+
Protocol version: v0.1
|
|
383
|
+
|
|
384
|
+
## Overview
|
|
385
|
+
|
|
386
|
+
VRP is a WebSocket-based protocol for real-time state synchronization.
|
|
387
|
+
|
|
388
|
+
## Message Types
|
|
389
|
+
|
|
390
|
+
- \`handshake\` - Initial connection
|
|
391
|
+
- \`ack\` - Acknowledgment
|
|
392
|
+
- \`sub\` - Subscribe to channel
|
|
393
|
+
- \`unsub\` - Unsubscribe from channel
|
|
394
|
+
- \`update\` - Full state update
|
|
395
|
+
- \`event\` - Event notification
|
|
396
|
+
- \`diff\` - Partial state update
|
|
397
|
+
- \`ping\` / \`pong\` - Keep-alive
|
|
398
|
+
- \`error\` - Error message
|
|
399
|
+
|
|
400
|
+
See VIRA_PROTOCOL.md for full specification.
|
|
401
|
+
`;
|
|
402
|
+
await fs.writeFile(path.join(outputDir, "VRP_CHANNELS.md"), basicDocs);
|
|
403
|
+
console.log(chalk_1.default.green(`✓ Basic VRP protocol docs: ${outputDir}/VRP_CHANNELS.md`));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
console.error(chalk_1.default.red("✗ Error generating documentation"));
|
|
408
|
+
if (error instanceof Error) {
|
|
409
|
+
console.error(chalk_1.default.red(` ${error.message}`));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
246
412
|
});
|
|
247
413
|
program
|
|
248
414
|
.command("doc")
|
|
@@ -268,14 +434,27 @@ program
|
|
|
268
434
|
.option("--backend <path>", "Path to Go types file", path.join("backend", "internal", "types", "types.go"))
|
|
269
435
|
.option("--frontend <path>", "Output TS types path (frontend)", path.join("frontend", "src", "vira-types.ts"))
|
|
270
436
|
.option("--ui <path>", "Output TS types path (ui)", path.join("ui", "src", "vira-types.ts"))
|
|
437
|
+
.option("-w, --watch", "Watch mode: automatically sync on file changes", false)
|
|
271
438
|
.action(async (options) => {
|
|
272
439
|
if (options.types) {
|
|
273
|
-
|
|
440
|
+
if (options.watch) {
|
|
441
|
+
console.log(chalk_1.default.yellow("Watch mode is not yet implemented. Running once..."));
|
|
442
|
+
await syncTypes(options);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
await syncTypes(options);
|
|
446
|
+
}
|
|
274
447
|
}
|
|
275
448
|
else {
|
|
276
449
|
console.log(chalk_1.default.yellow("Nothing to sync. Use --types to sync TypeScript types."));
|
|
277
450
|
}
|
|
278
451
|
});
|
|
452
|
+
program
|
|
453
|
+
.command("validate")
|
|
454
|
+
.description("Validate project structure and configuration")
|
|
455
|
+
.action(async () => {
|
|
456
|
+
await validateProject();
|
|
457
|
+
});
|
|
279
458
|
program.parse(process.argv);
|
|
280
459
|
/**
|
|
281
460
|
* Создание структуры проекта
|
|
@@ -313,6 +492,16 @@ async function createFullstackProject(projectPath) {
|
|
|
313
492
|
await createBackendStub(backendPath);
|
|
314
493
|
await createDeployScaffold(deployPath);
|
|
315
494
|
await createWorkspaceReadme(projectPath);
|
|
495
|
+
// 🎯 6️⃣ Добавляем корневой package.json с start:dev скриптом
|
|
496
|
+
const rootPackageJson = {
|
|
497
|
+
name: path.basename(projectPath),
|
|
498
|
+
version: "0.1.0",
|
|
499
|
+
private: true,
|
|
500
|
+
scripts: {
|
|
501
|
+
"start:dev": "cd deploy && docker compose -f docker-compose.dev.yml up -d && cd ../frontend && npm install && npm run dev",
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
await fs.writeJSON(path.join(projectPath, "package.json"), rootPackageJson, { spaces: 2 });
|
|
316
505
|
}
|
|
317
506
|
/**
|
|
318
507
|
* Create Kanban reference app (VRP-only, no direct useState/fetch)
|
|
@@ -361,7 +550,10 @@ async function createFrontendProject(projectPath) {
|
|
|
361
550
|
dependencies: {
|
|
362
551
|
"@vira-ui/core": "^1.0.0",
|
|
363
552
|
"@vira-ui/ui": "^1.0.0",
|
|
553
|
+
"@vira-ui/react": "^1.0.0",
|
|
364
554
|
"lucide-react": "^0.400.0",
|
|
555
|
+
"uuid": "^9.0.1",
|
|
556
|
+
// lodash не нужен - используем debounceServiceMethod из @vira-ui/core!
|
|
365
557
|
},
|
|
366
558
|
devDependencies: {
|
|
367
559
|
"@vira-ui/babel-plugin": "^1.0.0",
|
|
@@ -369,6 +561,7 @@ async function createFrontendProject(projectPath) {
|
|
|
369
561
|
"@types/node": "^20.10.0",
|
|
370
562
|
"@types/react": "^18.2.0",
|
|
371
563
|
"@types/react-dom": "^18.2.0",
|
|
564
|
+
"@types/uuid": "^9.0.7",
|
|
372
565
|
"react": "^18.2.0",
|
|
373
566
|
"react-dom": "^18.2.0",
|
|
374
567
|
"typescript": "^5.3.0",
|
|
@@ -440,10 +633,15 @@ async function createDeployScaffold(deployPath) {
|
|
|
440
633
|
async function createWorkspaceReadme(projectPath) {
|
|
441
634
|
await fs.writeFile(path.join(projectPath, "README.md"), readme_1.readme);
|
|
442
635
|
}
|
|
443
|
-
function printNextSteps(projectName, template) {
|
|
636
|
+
function printNextSteps(projectName, template, isInit = false) {
|
|
444
637
|
console.log(chalk_1.default.yellow(`\nNext steps:`));
|
|
445
638
|
if (template === "fullstack") {
|
|
446
|
-
|
|
639
|
+
if (!isInit) {
|
|
640
|
+
console.log(` cd ${projectName}/frontend`);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
console.log(` cd frontend`);
|
|
644
|
+
}
|
|
447
645
|
console.log(` npm install`);
|
|
448
646
|
console.log(` npm run dev`);
|
|
449
647
|
console.log(`\nUI package:`);
|
|
@@ -455,41 +653,435 @@ function printNextSteps(projectName, template) {
|
|
|
455
653
|
console.log(` go mod tidy`);
|
|
456
654
|
console.log(` go run ./cmd/api`);
|
|
457
655
|
console.log(`\nDev stack (DB/Redis/Kafka):`);
|
|
458
|
-
|
|
656
|
+
if (!isInit) {
|
|
657
|
+
console.log(` cd ../${projectName}/deploy && docker compose -f docker-compose.dev.yml up`);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
console.log(` cd ../deploy && docker compose -f docker-compose.dev.yml up`);
|
|
661
|
+
}
|
|
459
662
|
return;
|
|
460
663
|
}
|
|
461
|
-
|
|
664
|
+
if (!isInit) {
|
|
665
|
+
console.log(` cd ${projectName}`);
|
|
666
|
+
}
|
|
462
667
|
console.log(` npm install`);
|
|
463
668
|
console.log(` npm run dev`);
|
|
464
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* Сбор VRP конфигурации
|
|
672
|
+
*/
|
|
673
|
+
async function collectVRPConfig(name, useVRP, interactive) {
|
|
674
|
+
if (useVRP === false) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
if (!interactive && useVRP !== true) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
// Если явно указано --vrp, используем значения по умолчанию
|
|
681
|
+
if (useVRP === true && !interactive) {
|
|
682
|
+
return {
|
|
683
|
+
channel: name.toLowerCase(),
|
|
684
|
+
stateType: `${name}State`,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
// Интерактивный режим: спрашиваем
|
|
688
|
+
if (interactive && useVRP === undefined) {
|
|
689
|
+
const answer = await inquirer_1.default.prompt([
|
|
690
|
+
{
|
|
691
|
+
type: "confirm",
|
|
692
|
+
name: "useVRP",
|
|
693
|
+
message: "Use Vira Reactive Protocol (VRP) for state management?",
|
|
694
|
+
default: false,
|
|
695
|
+
},
|
|
696
|
+
]);
|
|
697
|
+
if (!answer.useVRP) {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Спрашиваем детали VRP
|
|
702
|
+
const config = await inquirer_1.default.prompt([
|
|
703
|
+
{
|
|
704
|
+
type: "input",
|
|
705
|
+
name: "channel",
|
|
706
|
+
message: "VRP channel name (e.g., 'user', 'task:123', 'demo'):",
|
|
707
|
+
default: name.toLowerCase(),
|
|
708
|
+
validate: (input) => input.length > 0 || "Channel name is required",
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
type: "input",
|
|
712
|
+
name: "stateType",
|
|
713
|
+
message: "State type name (interface name):",
|
|
714
|
+
default: `${name}State`,
|
|
715
|
+
},
|
|
716
|
+
]);
|
|
717
|
+
return {
|
|
718
|
+
channel: config.channel,
|
|
719
|
+
stateType: config.stateType,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Сбор props компонента
|
|
724
|
+
*/
|
|
725
|
+
async function collectProps(name, hasVRP, interactive) {
|
|
726
|
+
if (!interactive) {
|
|
727
|
+
return { props: [], hasProps: true };
|
|
728
|
+
}
|
|
729
|
+
const answer = await inquirer_1.default.prompt([
|
|
730
|
+
{
|
|
731
|
+
type: "confirm",
|
|
732
|
+
name: "hasProps",
|
|
733
|
+
message: "Does this component need props?",
|
|
734
|
+
default: !hasVRP,
|
|
735
|
+
},
|
|
736
|
+
]);
|
|
737
|
+
if (!answer.hasProps) {
|
|
738
|
+
return { props: [], hasProps: false };
|
|
739
|
+
}
|
|
740
|
+
const props = [];
|
|
741
|
+
let addMore = true;
|
|
742
|
+
while (addMore) {
|
|
743
|
+
const prop = await inquirer_1.default.prompt([
|
|
744
|
+
{
|
|
745
|
+
type: "input",
|
|
746
|
+
name: "name",
|
|
747
|
+
message: "Prop name:",
|
|
748
|
+
validate: (input) => input.length > 0 || "Prop name is required",
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
type: "list",
|
|
752
|
+
name: "type",
|
|
753
|
+
message: "Prop type:",
|
|
754
|
+
choices: [
|
|
755
|
+
"string",
|
|
756
|
+
"number",
|
|
757
|
+
"boolean",
|
|
758
|
+
"ReactNode",
|
|
759
|
+
"() => void",
|
|
760
|
+
"(e: Event) => void",
|
|
761
|
+
"string[]",
|
|
762
|
+
"Custom",
|
|
763
|
+
],
|
|
764
|
+
default: "string",
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
type: "input",
|
|
768
|
+
name: "customType",
|
|
769
|
+
message: "Custom type:",
|
|
770
|
+
when: (answers) => answers.type === "Custom",
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
type: "confirm",
|
|
774
|
+
name: "required",
|
|
775
|
+
message: "Required prop?",
|
|
776
|
+
default: true,
|
|
777
|
+
},
|
|
778
|
+
]);
|
|
779
|
+
const propType = prop.type === "Custom" ? prop.customType : prop.type;
|
|
780
|
+
props.push({
|
|
781
|
+
name: prop.name,
|
|
782
|
+
type: propType,
|
|
783
|
+
required: prop.required,
|
|
784
|
+
});
|
|
785
|
+
const { continueAdding } = await inquirer_1.default.prompt([
|
|
786
|
+
{
|
|
787
|
+
type: "confirm",
|
|
788
|
+
name: "continueAdding",
|
|
789
|
+
message: "Add another prop?",
|
|
790
|
+
default: true,
|
|
791
|
+
},
|
|
792
|
+
]);
|
|
793
|
+
addMore = continueAdding;
|
|
794
|
+
}
|
|
795
|
+
return { props, hasProps: true };
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Построение импортов
|
|
799
|
+
*/
|
|
800
|
+
function buildImports(vrpConfig, useViraUI) {
|
|
801
|
+
let imports = `import { createElement } from '@vira-ui/core';
|
|
802
|
+
import type { ViraComponentProps } from '@vira-ui/core';`;
|
|
803
|
+
if (vrpConfig) {
|
|
804
|
+
imports += `\nimport { useViraState } from '@vira-ui/react';`;
|
|
805
|
+
}
|
|
806
|
+
if (useViraUI) {
|
|
807
|
+
imports += `\nimport { Container, Stack } from '@vira-ui/ui';`;
|
|
808
|
+
}
|
|
809
|
+
return imports;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Построение интерфейса props
|
|
813
|
+
*/
|
|
814
|
+
function buildPropsInterface(name, props) {
|
|
815
|
+
if (props.length === 0) {
|
|
816
|
+
return `export interface ${name}Props extends ViraComponentProps {
|
|
817
|
+
// Add your props here
|
|
818
|
+
}`;
|
|
819
|
+
}
|
|
820
|
+
return `export interface ${name}Props extends ViraComponentProps {
|
|
821
|
+
${props.map((p) => ` ${p.name}${p.required ? "" : "?"}: ${p.type};`).join("\n")}
|
|
822
|
+
}`;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Построение тела компонента
|
|
826
|
+
*/
|
|
827
|
+
function buildComponentBody(name, vrpConfig, props, hasProps, useViraUI) {
|
|
828
|
+
const propsUsage = props.map((p) => ` ${p.name}={props.${p.name}}`).join("\n");
|
|
829
|
+
if (vrpConfig) {
|
|
830
|
+
return ` const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${vrpConfig.stateType}>('${vrpConfig.channel}', null);
|
|
831
|
+
|
|
832
|
+
// Use data from VRP state
|
|
833
|
+
// Example: const value = data?.field ?? defaultValue;
|
|
834
|
+
|
|
835
|
+
// 🎯 4️⃣ Для inline-редактирования с авто-save используйте встроенный watch() с debounce:
|
|
836
|
+
// import { watch, signal } from '@vira-ui/core';
|
|
837
|
+
// const [editValue, setEditValue] = signal('');
|
|
838
|
+
// watch(() => editValue(), (newValue) => {
|
|
839
|
+
// sendDiff({ [field]: newValue, updated_at: new Date().toISOString() });
|
|
840
|
+
// }, { debounce: 500 });
|
|
841
|
+
|
|
842
|
+
return createElement('div', { className: '${name.toLowerCase()}' },
|
|
843
|
+
${propsUsage ? propsUsage + ",\n" : ""} // Add your content here
|
|
844
|
+
);`;
|
|
845
|
+
}
|
|
846
|
+
if (useViraUI) {
|
|
847
|
+
return ` return (
|
|
848
|
+
<Container>
|
|
849
|
+
<Stack>
|
|
850
|
+
${propsUsage ? propsUsage.split('\n').map(p => ` ${p.replace(' ', '')}`).join('\n') + "\n" : ""} {/* Add your content here */}
|
|
851
|
+
</Stack>
|
|
852
|
+
</Container>
|
|
853
|
+
);`;
|
|
854
|
+
}
|
|
855
|
+
return ` return createElement('div', { className: '${name.toLowerCase()}' },
|
|
856
|
+
${propsUsage ? propsUsage + ",\n" : ""} // Add your content here
|
|
857
|
+
);`;
|
|
858
|
+
}
|
|
465
859
|
/**
|
|
466
860
|
* Генерация компонента
|
|
467
861
|
*/
|
|
468
|
-
async function generateComponent(name, dir) {
|
|
862
|
+
async function generateComponent(name, dir, config) {
|
|
469
863
|
const componentPath = path.join(process.cwd(), dir, "components", `${name}.tsx`);
|
|
470
864
|
await fs.ensureDir(path.dirname(componentPath));
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
865
|
+
// Сбор конфигурации
|
|
866
|
+
const vrpConfig = await collectVRPConfig(name, config.useVRP, config.interactive);
|
|
867
|
+
const { props, hasProps } = await collectProps(name, !!vrpConfig, config.interactive);
|
|
868
|
+
// Спрашиваем про Vira UI, если интерактивный режим
|
|
869
|
+
let useViraUI = false;
|
|
870
|
+
if (config.interactive) {
|
|
871
|
+
const uiAnswer = await inquirer_1.default.prompt([
|
|
872
|
+
{
|
|
873
|
+
type: "confirm",
|
|
874
|
+
name: "useViraUI",
|
|
875
|
+
message: "Use Vira UI components (@vira-ui/ui)?",
|
|
876
|
+
default: true,
|
|
877
|
+
},
|
|
878
|
+
]);
|
|
879
|
+
useViraUI = uiAnswer.useViraUI;
|
|
880
|
+
}
|
|
881
|
+
// Построение кода
|
|
882
|
+
const imports = buildImports(vrpConfig, useViraUI);
|
|
883
|
+
const propsInterface = buildPropsInterface(name, props);
|
|
884
|
+
const componentBody = buildComponentBody(name, vrpConfig, props, hasProps, useViraUI);
|
|
885
|
+
// Сборка финального кода
|
|
886
|
+
let componentCode = `${imports}
|
|
887
|
+
`;
|
|
888
|
+
// Интерфейс состояния VRP
|
|
889
|
+
if (vrpConfig) {
|
|
890
|
+
componentCode += `export interface ${vrpConfig.stateType} {
|
|
891
|
+
// Add your state fields here
|
|
892
|
+
id?: string;
|
|
476
893
|
}
|
|
477
894
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
//
|
|
481
|
-
|
|
895
|
+
`;
|
|
896
|
+
}
|
|
897
|
+
// Интерфейс props
|
|
898
|
+
if (hasProps || !vrpConfig) {
|
|
899
|
+
componentCode += `${propsInterface}
|
|
900
|
+
|
|
901
|
+
`;
|
|
902
|
+
}
|
|
903
|
+
// Сигнатура функции
|
|
904
|
+
const functionSignature = hasProps || !vrpConfig
|
|
905
|
+
? `export function ${name}(props: ${name}Props) {`
|
|
906
|
+
: `export function ${name}() {`;
|
|
907
|
+
componentCode += `${functionSignature}
|
|
908
|
+
${componentBody}
|
|
482
909
|
}
|
|
483
910
|
`;
|
|
484
911
|
await fs.writeFile(componentPath, componentCode);
|
|
485
912
|
}
|
|
486
913
|
/**
|
|
487
|
-
*
|
|
914
|
+
* Сбор VRP конфигурации для сервиса
|
|
488
915
|
*/
|
|
489
|
-
async function
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
916
|
+
async function collectServiceVRPConfig(name, useVRP, interactive) {
|
|
917
|
+
if (useVRP === false) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
if (!interactive && useVRP !== true) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
// Если явно указано --vrp, используем значения по умолчанию
|
|
924
|
+
if (useVRP === true && !interactive) {
|
|
925
|
+
return {
|
|
926
|
+
channel: name.toLowerCase(),
|
|
927
|
+
stateType: `${name}State`,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
// Интерактивный режим: спрашиваем
|
|
931
|
+
if (interactive && useVRP === undefined) {
|
|
932
|
+
const answer = await inquirer_1.default.prompt([
|
|
933
|
+
{
|
|
934
|
+
type: "confirm",
|
|
935
|
+
name: "useVRP",
|
|
936
|
+
message: "Use Vira Reactive Protocol (VRP) for this service?",
|
|
937
|
+
default: true,
|
|
938
|
+
},
|
|
939
|
+
]);
|
|
940
|
+
if (!answer.useVRP) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// Спрашиваем детали VRP
|
|
945
|
+
const config = await inquirer_1.default.prompt([
|
|
946
|
+
{
|
|
947
|
+
type: "input",
|
|
948
|
+
name: "channel",
|
|
949
|
+
message: "VRP channel name:",
|
|
950
|
+
default: name.toLowerCase(),
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
type: "input",
|
|
954
|
+
name: "stateType",
|
|
955
|
+
message: "State type name:",
|
|
956
|
+
default: `${name}State`,
|
|
957
|
+
},
|
|
958
|
+
]);
|
|
959
|
+
return {
|
|
960
|
+
channel: config.channel,
|
|
961
|
+
stateType: config.stateType,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Построение VRP-based сервиса
|
|
966
|
+
*/
|
|
967
|
+
function buildVRPService(name, vrpConfig) {
|
|
968
|
+
const lowerName = name.toLowerCase();
|
|
969
|
+
return `// ${name} service using Vira Core DI container + VRP
|
|
970
|
+
import { createService, useService } from '@vira-ui/core';
|
|
971
|
+
import { useViraState } from '@vira-ui/react';
|
|
972
|
+
import { v4 as uuid } from 'uuid';
|
|
973
|
+
|
|
974
|
+
export interface ${vrpConfig.stateType} {
|
|
975
|
+
// Add your state fields here
|
|
976
|
+
id?: string;
|
|
977
|
+
created_at?: string;
|
|
978
|
+
updated_at?: string;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// 🎯 3️⃣ Универсальный VRP hook для списков (переиспользуемый для любых сущностей)
|
|
982
|
+
export function useVrpList<T>(channel: string) {
|
|
983
|
+
const { data, sendEvent, sendDiff } = useViraState<T[] | T>(channel, []);
|
|
984
|
+
const list = Array.isArray(data) ? data : Object.values(data || {});
|
|
985
|
+
return { data: list, sendEvent, sendDiff };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Create ${lowerName} service (singleton via DI container)
|
|
989
|
+
// Service holds pure business logic helpers
|
|
990
|
+
createService('${lowerName}', () => ({
|
|
991
|
+
// Add your business logic methods here
|
|
992
|
+
processData(data: ${vrpConfig.stateType} | null): any {
|
|
993
|
+
if (!data) return null;
|
|
994
|
+
// Add processing logic
|
|
995
|
+
return data;
|
|
996
|
+
},
|
|
997
|
+
}));
|
|
998
|
+
|
|
999
|
+
// Hook for ${name} operations (combines service + VRP state)
|
|
1000
|
+
export function use${name}(id?: string) {
|
|
1001
|
+
const channel = id ? \`${vrpConfig.channel}:\${id}\` : '${vrpConfig.channel}';
|
|
1002
|
+
const { data, sendEvent, sendUpdate, sendDiff } = id
|
|
1003
|
+
? useViraState<${vrpConfig.stateType}>(channel, null)
|
|
1004
|
+
: useVrpList<${vrpConfig.stateType}>(channel);
|
|
1005
|
+
const ${lowerName}Service = useService<{ processData: (data: ${vrpConfig.stateType} | null) => any }>('${lowerName}');
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
1008
|
+
data,
|
|
1009
|
+
// 🎯 1️⃣ Создание с авто-генерацией UUID на фронте (VRP сразу знает id, не надо ждать бэка)
|
|
1010
|
+
create(item: Omit<${vrpConfig.stateType}, 'id' | 'created_at' | 'updated_at'>) {
|
|
1011
|
+
const id = uuid();
|
|
1012
|
+
const newItem: ${vrpConfig.stateType} = {
|
|
1013
|
+
...item,
|
|
1014
|
+
id,
|
|
1015
|
+
created_at: new Date().toISOString(),
|
|
1016
|
+
updated_at: new Date().toISOString(),
|
|
1017
|
+
};
|
|
1018
|
+
sendEvent('${lowerName}.created', {
|
|
1019
|
+
...newItem,
|
|
1020
|
+
timestamp: new Date().toISOString()
|
|
1021
|
+
});
|
|
1022
|
+
},
|
|
1023
|
+
// Update operations
|
|
1024
|
+
update(updates: Partial<${vrpConfig.stateType}>) {
|
|
1025
|
+
sendDiff(updates);
|
|
1026
|
+
sendEvent('${lowerName}.updated', {
|
|
1027
|
+
...updates,
|
|
1028
|
+
updated_at: new Date().toISOString(),
|
|
1029
|
+
timestamp: new Date().toISOString()
|
|
1030
|
+
});
|
|
1031
|
+
},
|
|
1032
|
+
// Delete operation
|
|
1033
|
+
delete(itemId: string) {
|
|
1034
|
+
sendEvent('${lowerName}.deleted', {
|
|
1035
|
+
id: itemId,
|
|
1036
|
+
timestamp: new Date().toISOString()
|
|
1037
|
+
});
|
|
1038
|
+
},
|
|
1039
|
+
sendEvent,
|
|
1040
|
+
sendDiff,
|
|
1041
|
+
// Service methods
|
|
1042
|
+
processData() {
|
|
1043
|
+
return ${lowerName}Service.processData(data);
|
|
1044
|
+
},
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// 🎯 2️⃣ Сервис для bulk actions с VRP (переиспользуемый для любых сущностей)
|
|
1049
|
+
// Используем batch() из @vira-ui/core для оптимизации множественных обновлений
|
|
1050
|
+
import { batch } from '@vira-ui/core';
|
|
1051
|
+
|
|
1052
|
+
createService('${lowerName}Bulk', () => ({
|
|
1053
|
+
bulkUpdate(ids: string[], payload: Partial<${vrpConfig.stateType}>, sendEvent: Function) {
|
|
1054
|
+
// batch() группирует все обновления в один цикл - компоненты обновятся только один раз!
|
|
1055
|
+
batch(() => {
|
|
1056
|
+
ids.forEach(id => {
|
|
1057
|
+
sendEvent('${lowerName}.updated', {
|
|
1058
|
+
id,
|
|
1059
|
+
...payload,
|
|
1060
|
+
updated_at: new Date().toISOString(),
|
|
1061
|
+
timestamp: new Date().toISOString()
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
},
|
|
1066
|
+
bulkDelete(ids: string[], sendEvent: Function) {
|
|
1067
|
+
// batch() для оптимизации массового удаления
|
|
1068
|
+
batch(() => {
|
|
1069
|
+
ids.forEach(id => {
|
|
1070
|
+
sendEvent('${lowerName}.deleted', {
|
|
1071
|
+
id,
|
|
1072
|
+
timestamp: new Date().toISOString()
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
},
|
|
1077
|
+
}));
|
|
1078
|
+
`;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Построение стандартного сервиса с signals
|
|
1082
|
+
*/
|
|
1083
|
+
function buildStandardService(name) {
|
|
1084
|
+
return `import { createViraService, signal } from '@vira-ui/core';
|
|
493
1085
|
|
|
494
1086
|
export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
|
|
495
1087
|
const data = signal([]);
|
|
@@ -517,6 +1109,19 @@ export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
|
|
|
517
1109
|
};
|
|
518
1110
|
});
|
|
519
1111
|
`;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Генерация сервиса
|
|
1115
|
+
*/
|
|
1116
|
+
async function generateService(name, dir, config) {
|
|
1117
|
+
const servicePath = path.join(process.cwd(), dir, "services", `${name}Service.ts`);
|
|
1118
|
+
await fs.ensureDir(path.dirname(servicePath));
|
|
1119
|
+
// Сбор VRP конфигурации
|
|
1120
|
+
const vrpConfig = await collectServiceVRPConfig(name, config.useVRP, config.interactive);
|
|
1121
|
+
// Построение кода
|
|
1122
|
+
const serviceCode = vrpConfig
|
|
1123
|
+
? buildVRPService(name, vrpConfig)
|
|
1124
|
+
: buildStandardService(name);
|
|
520
1125
|
await fs.writeFile(servicePath, serviceCode);
|
|
521
1126
|
}
|
|
522
1127
|
/**
|
|
@@ -525,9 +1130,49 @@ export const ${name}Service = createViraService('${name.toLowerCase()}', () => {
|
|
|
525
1130
|
async function generatePage(name, dir) {
|
|
526
1131
|
const pagePath = path.join(process.cwd(), dir, "pages", `${name}Page.tsx`);
|
|
527
1132
|
await fs.ensureDir(path.dirname(pagePath));
|
|
528
|
-
const pageCode = `import { createElement } from '@vira-ui/core';
|
|
1133
|
+
const pageCode = `import { createService, useService, createElement } from '@vira-ui/core';
|
|
1134
|
+
import { useViraState } from '@vira-ui/react';
|
|
1135
|
+
|
|
1136
|
+
// 🎯 5️⃣ VRP notifications: очередь с максимумом (предотвращает перегрузку фронта)
|
|
1137
|
+
const MAX_NOTIFICATIONS = 5;
|
|
1138
|
+
|
|
1139
|
+
// Создаём сервис для управления состоянием страницы
|
|
1140
|
+
createService('${name.toLowerCase()}Page', () => ({
|
|
1141
|
+
searchQuery: '',
|
|
1142
|
+
selectedIds: new Set<string>(),
|
|
1143
|
+
|
|
1144
|
+
setSearchQuery(value: string) {
|
|
1145
|
+
this.searchQuery = value;
|
|
1146
|
+
},
|
|
1147
|
+
|
|
1148
|
+
toggleSelect(id: string) {
|
|
1149
|
+
if (this.selectedIds.has(id)) {
|
|
1150
|
+
this.selectedIds.delete(id);
|
|
1151
|
+
} else {
|
|
1152
|
+
this.selectedIds.add(id);
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
|
|
1156
|
+
clearSelection() {
|
|
1157
|
+
this.selectedIds.clear();
|
|
1158
|
+
},
|
|
1159
|
+
}));
|
|
529
1160
|
|
|
530
1161
|
export function ${name}Page() {
|
|
1162
|
+
// VRP для списка
|
|
1163
|
+
// const { data, create, sendEvent } = use${name}();
|
|
1164
|
+
|
|
1165
|
+
// VRP для real-time уведомлений с лимитом
|
|
1166
|
+
const notificationsState = useViraState<any[]>('notifications:${name.toLowerCase()}', []);
|
|
1167
|
+
|
|
1168
|
+
// Используем сервис страницы
|
|
1169
|
+
const pageService = useService('${name.toLowerCase()}Page');
|
|
1170
|
+
|
|
1171
|
+
// Обработка уведомлений с лимитом очереди
|
|
1172
|
+
if (notificationsState.data && notificationsState.data.length > MAX_NOTIFICATIONS) {
|
|
1173
|
+
notificationsState.data = notificationsState.data.slice(-MAX_NOTIFICATIONS);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
531
1176
|
return createElement('div', { className: '${name.toLowerCase()}-page' },
|
|
532
1177
|
createElement('h1', null, '${name}'),
|
|
533
1178
|
// Add your content here
|
|
@@ -570,6 +1215,78 @@ export const ${name}Route = reactiveRoute({
|
|
|
570
1215
|
`;
|
|
571
1216
|
await fs.writeFile(routePath, routeCode);
|
|
572
1217
|
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Генерация теста
|
|
1220
|
+
*/
|
|
1221
|
+
async function generateTest(name, dir) {
|
|
1222
|
+
// Определяем тип файла (component, service, page)
|
|
1223
|
+
const componentPath = path.join(process.cwd(), dir, "components", `${name}.tsx`);
|
|
1224
|
+
const servicePath = path.join(process.cwd(), dir, "services", `${name}Service.ts`);
|
|
1225
|
+
const pagePath = path.join(process.cwd(), dir, "pages", `${name}Page.tsx`);
|
|
1226
|
+
let testPath;
|
|
1227
|
+
let testCode;
|
|
1228
|
+
if (await fs.pathExists(componentPath)) {
|
|
1229
|
+
// Тест для компонента
|
|
1230
|
+
testPath = path.join(process.cwd(), dir, "components", `${name}.test.tsx`);
|
|
1231
|
+
testCode = `import React from 'react';
|
|
1232
|
+
import { render } from '@testing-library/react';
|
|
1233
|
+
import { ${name} } from './${name}';
|
|
1234
|
+
import type { ${name}Props } from './${name}';
|
|
1235
|
+
|
|
1236
|
+
describe('${name}', () => {
|
|
1237
|
+
it('renders correctly', () => {
|
|
1238
|
+
const props: ${name}Props = {
|
|
1239
|
+
// Add test props here
|
|
1240
|
+
};
|
|
1241
|
+
const { container } = render(React.createElement(${name}, props));
|
|
1242
|
+
expect(container).toBeTruthy();
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
`;
|
|
1246
|
+
}
|
|
1247
|
+
else if (await fs.pathExists(servicePath)) {
|
|
1248
|
+
// Тест для сервиса
|
|
1249
|
+
testPath = path.join(process.cwd(), dir, "services", `${name}Service.test.ts`);
|
|
1250
|
+
testCode = `import { ${name}Service } from './${name}Service';
|
|
1251
|
+
|
|
1252
|
+
describe('${name}Service', () => {
|
|
1253
|
+
it('should be defined', () => {
|
|
1254
|
+
expect(${name}Service).toBeDefined();
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
// Add more tests here
|
|
1258
|
+
});
|
|
1259
|
+
`;
|
|
1260
|
+
}
|
|
1261
|
+
else if (await fs.pathExists(pagePath)) {
|
|
1262
|
+
// Тест для страницы
|
|
1263
|
+
testPath = path.join(process.cwd(), dir, "pages", `${name}Page.test.tsx`);
|
|
1264
|
+
testCode = `import React from 'react';
|
|
1265
|
+
import { render } from '@testing-library/react';
|
|
1266
|
+
import { ${name}Page } from './${name}Page';
|
|
1267
|
+
|
|
1268
|
+
describe('${name}Page', () => {
|
|
1269
|
+
it('renders correctly', () => {
|
|
1270
|
+
const { container } = render(React.createElement(${name}Page));
|
|
1271
|
+
expect(container).toBeTruthy();
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
`;
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
// Общий тест, если файл не найден
|
|
1278
|
+
testPath = path.join(process.cwd(), dir, "__tests__", `${name}.test.ts`);
|
|
1279
|
+
await fs.ensureDir(path.dirname(testPath));
|
|
1280
|
+
testCode = `describe('${name}', () => {
|
|
1281
|
+
it('should work', () => {
|
|
1282
|
+
expect(true).toBe(true);
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
`;
|
|
1286
|
+
}
|
|
1287
|
+
await fs.ensureDir(path.dirname(testPath));
|
|
1288
|
+
await fs.writeFile(testPath, testCode);
|
|
1289
|
+
}
|
|
573
1290
|
/**
|
|
574
1291
|
* Sync TypeScript types from Go structs (scaffold-level parser)
|
|
575
1292
|
*/
|
|
@@ -728,6 +1445,50 @@ function toCamel(name) {
|
|
|
728
1445
|
return name;
|
|
729
1446
|
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
730
1447
|
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Генерация документации по VRP каналам
|
|
1450
|
+
*/
|
|
1451
|
+
function generateChannelDocumentation(structs) {
|
|
1452
|
+
const lines = [];
|
|
1453
|
+
lines.push("# Vira Reactive Protocol (VRP) Channels");
|
|
1454
|
+
lines.push("");
|
|
1455
|
+
lines.push("Protocol version: v0.1");
|
|
1456
|
+
lines.push("");
|
|
1457
|
+
lines.push("## Available Channels");
|
|
1458
|
+
lines.push("");
|
|
1459
|
+
lines.push("The following channels are available based on your Go types:");
|
|
1460
|
+
lines.push("");
|
|
1461
|
+
for (const struct of structs) {
|
|
1462
|
+
const channelName = struct.name.toLowerCase();
|
|
1463
|
+
lines.push(`### \`${channelName}\` / \`${channelName}:{id}\``);
|
|
1464
|
+
lines.push("");
|
|
1465
|
+
lines.push(`Type: \`${struct.name}\``);
|
|
1466
|
+
lines.push("");
|
|
1467
|
+
if (struct.fields.length > 0) {
|
|
1468
|
+
lines.push("Fields:");
|
|
1469
|
+
lines.push("");
|
|
1470
|
+
for (const field of struct.fields) {
|
|
1471
|
+
const jsonName = field.json || toCamel(field.name);
|
|
1472
|
+
const tsType = goTypeToTs(field.type);
|
|
1473
|
+
lines.push(`- \`${jsonName}\`: \`${tsType}\``);
|
|
1474
|
+
}
|
|
1475
|
+
lines.push("");
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
lines.push("## Usage");
|
|
1479
|
+
lines.push("");
|
|
1480
|
+
lines.push("```typescript");
|
|
1481
|
+
lines.push("import { useViraState } from '@vira-ui/react';");
|
|
1482
|
+
lines.push("");
|
|
1483
|
+
if (structs.length > 0) {
|
|
1484
|
+
const firstStruct = structs[0];
|
|
1485
|
+
lines.push(`const { data, sendEvent, sendUpdate, sendDiff } = useViraState<${firstStruct.name}>('${firstStruct.name.toLowerCase()}', null);`);
|
|
1486
|
+
}
|
|
1487
|
+
lines.push("```");
|
|
1488
|
+
lines.push("");
|
|
1489
|
+
lines.push("See VIRA_PROTOCOL.md for full specification.");
|
|
1490
|
+
return lines.join("\n");
|
|
1491
|
+
}
|
|
731
1492
|
/**
|
|
732
1493
|
* Go HTTP handler scaffold
|
|
733
1494
|
*/
|
|
@@ -829,3 +1590,429 @@ function toPascal(value) {
|
|
|
829
1590
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
830
1591
|
.join("");
|
|
831
1592
|
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Генерация CRUD handlers для ресурса
|
|
1595
|
+
*/
|
|
1596
|
+
async function generateCRUDHandler(name, dir, modelName) {
|
|
1597
|
+
const safeName = name.toLowerCase();
|
|
1598
|
+
const handlerName = capitalize(name);
|
|
1599
|
+
const model = modelName || capitalize(name);
|
|
1600
|
+
const targetDir = path.join(process.cwd(), dir);
|
|
1601
|
+
await fs.ensureDir(targetDir);
|
|
1602
|
+
const handlerCode = `package handlers
|
|
1603
|
+
|
|
1604
|
+
import (
|
|
1605
|
+
"encoding/json"
|
|
1606
|
+
"net/http"
|
|
1607
|
+
"strconv"
|
|
1608
|
+
"time"
|
|
1609
|
+
"github.com/gorilla/mux"
|
|
1610
|
+
"github.com/go-playground/validator/v10"
|
|
1611
|
+
"github.com/google/uuid"
|
|
1612
|
+
)
|
|
1613
|
+
|
|
1614
|
+
var validate = validator.New()
|
|
1615
|
+
|
|
1616
|
+
// 🎯 Production-ready: Pagination support
|
|
1617
|
+
type PaginationParams struct {
|
|
1618
|
+
Limit int \`json:"limit"\`
|
|
1619
|
+
Offset int \`json:"offset"\`
|
|
1620
|
+
Cursor string \`json:"cursor,omitempty"\`
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// 🎯 Production-ready: List response with pagination
|
|
1624
|
+
type ${handlerName}ListResponse struct {
|
|
1625
|
+
Items []${model} \`json:"items"\`
|
|
1626
|
+
Total int \`json:"total"\`
|
|
1627
|
+
Limit int \`json:"limit"\`
|
|
1628
|
+
Offset int \`json:"offset"\`
|
|
1629
|
+
HasMore bool \`json:"has_more"\`
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// 🎯 Production-ready: Event logging structure
|
|
1633
|
+
type ${handlerName}Event struct {
|
|
1634
|
+
ID string \`json:"id"\`
|
|
1635
|
+
Type string \`json:"type"\` // created, updated, deleted
|
|
1636
|
+
EntityID string \`json:"entity_id"\`
|
|
1637
|
+
UserID string \`json:"user_id,omitempty"\`
|
|
1638
|
+
OldValue *${model} \`json:"old_value,omitempty"\`
|
|
1639
|
+
NewValue *${model} \`json:"new_value,omitempty"\`
|
|
1640
|
+
Timestamp time.Time \`json:"timestamp"\`
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// 🎯 Production-ready: Batch request/response
|
|
1644
|
+
type BatchUpdateRequest struct {
|
|
1645
|
+
IDs []string \`json:"ids" validate:"required,min=1"\`
|
|
1646
|
+
Payload map[string]interface{} \`json:"payload" validate:"required"\`
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
type BatchDeleteRequest struct {
|
|
1650
|
+
IDs []string \`json:"ids" validate:"required,min=1"\`
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// List${handlerName} handles GET /${safeName} with pagination
|
|
1654
|
+
func List${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1655
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1656
|
+
|
|
1657
|
+
// 🎯 Production-ready: Parse pagination params (limit, offset, cursor)
|
|
1658
|
+
limit := 50 // default
|
|
1659
|
+
offset := 0
|
|
1660
|
+
|
|
1661
|
+
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
|
1662
|
+
if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 && parsed <= 100 {
|
|
1663
|
+
limit = parsed
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
|
1668
|
+
if parsed, err := strconv.Atoi(offsetStr); err == nil && parsed >= 0 {
|
|
1669
|
+
offset = parsed
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// TODO: Implement actual DB query with limit/offset
|
|
1674
|
+
// items, total, err := db.List${handlerName}(limit, offset)
|
|
1675
|
+
// if err != nil {
|
|
1676
|
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
1677
|
+
// return
|
|
1678
|
+
// }
|
|
1679
|
+
|
|
1680
|
+
// 🎯 Production-ready: Redis caching (commented for now - implement with your cache layer)
|
|
1681
|
+
// cacheKey := fmt.Sprintf("${safeName}:list:limit=%d:offset=%d", limit, offset)
|
|
1682
|
+
// if cached := redis.Get(cacheKey); cached != nil {
|
|
1683
|
+
// json.NewEncoder(w).Encode(cached)
|
|
1684
|
+
// return
|
|
1685
|
+
// }
|
|
1686
|
+
|
|
1687
|
+
response := ${handlerName}ListResponse{
|
|
1688
|
+
Items: []${model}{},
|
|
1689
|
+
Total: 0,
|
|
1690
|
+
Limit: limit,
|
|
1691
|
+
Offset: offset,
|
|
1692
|
+
HasMore: false,
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// 🎯 Production-ready: Cache response (TTL 30s for lists)
|
|
1696
|
+
// redis.Set(cacheKey, response, 30*time.Second)
|
|
1697
|
+
|
|
1698
|
+
json.NewEncoder(w).Encode(response)
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// Get${handlerName} handles GET /${safeName}/{id}
|
|
1702
|
+
func Get${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1703
|
+
vars := mux.Vars(r)
|
|
1704
|
+
id := vars["id"]
|
|
1705
|
+
|
|
1706
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1707
|
+
|
|
1708
|
+
// 🎯 Production-ready: Redis caching for detail views
|
|
1709
|
+
// cacheKey := fmt.Sprintf("${safeName}:detail:%s", id)
|
|
1710
|
+
// if cached := redis.Get(cacheKey); cached != nil {
|
|
1711
|
+
// json.NewEncoder(w).Encode(cached)
|
|
1712
|
+
// return
|
|
1713
|
+
// }
|
|
1714
|
+
|
|
1715
|
+
// TODO: Implement actual DB query
|
|
1716
|
+
// item, err := db.Get${handlerName}(id)
|
|
1717
|
+
// if err != nil {
|
|
1718
|
+
// http.Error(w, err.Error(), http.StatusNotFound)
|
|
1719
|
+
// return
|
|
1720
|
+
// }
|
|
1721
|
+
|
|
1722
|
+
item := ${model}{ID: id}
|
|
1723
|
+
|
|
1724
|
+
// 🎯 Production-ready: Cache detail (TTL 5min)
|
|
1725
|
+
// redis.Set(cacheKey, item, 5*time.Minute)
|
|
1726
|
+
|
|
1727
|
+
json.NewEncoder(w).Encode(item)
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// Create${handlerName} handles POST /${safeName}
|
|
1731
|
+
func Create${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1732
|
+
var input ${model}
|
|
1733
|
+
|
|
1734
|
+
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
1735
|
+
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
1736
|
+
return
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// 🎯 Production-ready: Validation (email, phone, unique fields)
|
|
1740
|
+
if err := validate.Struct(input); err != nil {
|
|
1741
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1742
|
+
w.WriteHeader(http.StatusBadRequest)
|
|
1743
|
+
json.NewEncoder(w).Encode(map[string]string{
|
|
1744
|
+
"error": "Validation failed",
|
|
1745
|
+
"details": err.Error(),
|
|
1746
|
+
})
|
|
1747
|
+
return
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// Generate UUID if not provided
|
|
1751
|
+
if input.ID == "" {
|
|
1752
|
+
input.ID = uuid.New().String()
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
input.CreatedAt = time.Now()
|
|
1756
|
+
input.UpdatedAt = time.Now()
|
|
1757
|
+
|
|
1758
|
+
// TODO: Implement actual DB insert
|
|
1759
|
+
// if err := db.Create${handlerName}(&input); err != nil {
|
|
1760
|
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
1761
|
+
// return
|
|
1762
|
+
// }
|
|
1763
|
+
|
|
1764
|
+
// 🎯 Production-ready: Log event for audit trail
|
|
1765
|
+
event := ${handlerName}Event{
|
|
1766
|
+
ID: uuid.New().String(),
|
|
1767
|
+
Type: "created",
|
|
1768
|
+
EntityID: input.ID,
|
|
1769
|
+
NewValue: &input,
|
|
1770
|
+
Timestamp: time.Now(),
|
|
1771
|
+
}
|
|
1772
|
+
// logEvent(event) // Implement event logging to client_events table
|
|
1773
|
+
|
|
1774
|
+
// 🎯 Production-ready: Invalidate cache
|
|
1775
|
+
// redis.Del("${safeName}:list:*")
|
|
1776
|
+
|
|
1777
|
+
// 🎯 Production-ready: Emit VRP event (batchEmit for large lists)
|
|
1778
|
+
// vrp.BatchEmit("${safeName}", "created", input)
|
|
1779
|
+
|
|
1780
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1781
|
+
w.WriteHeader(http.StatusCreated)
|
|
1782
|
+
json.NewEncoder(w).Encode(input)
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// Update${handlerName} handles PUT /${safeName}/{id}
|
|
1786
|
+
func Update${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1787
|
+
vars := mux.Vars(r)
|
|
1788
|
+
id := vars["id"]
|
|
1789
|
+
|
|
1790
|
+
var input ${model}
|
|
1791
|
+
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
1792
|
+
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
1793
|
+
return
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// 🎯 Production-ready: Get old value for event logging
|
|
1797
|
+
// oldValue, _ := db.Get${handlerName}(id)
|
|
1798
|
+
|
|
1799
|
+
// 🎯 Production-ready: Validation
|
|
1800
|
+
if err := validate.Struct(input); err != nil {
|
|
1801
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1802
|
+
w.WriteHeader(http.StatusBadRequest)
|
|
1803
|
+
json.NewEncoder(w).Encode(map[string]string{
|
|
1804
|
+
"error": "Validation failed",
|
|
1805
|
+
"details": err.Error(),
|
|
1806
|
+
})
|
|
1807
|
+
return
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
input.ID = id
|
|
1811
|
+
input.UpdatedAt = time.Now()
|
|
1812
|
+
|
|
1813
|
+
// TODO: Implement actual DB update
|
|
1814
|
+
// if err := db.Update${handlerName}(&input); err != nil {
|
|
1815
|
+
// http.Error(w, err.Error(), http.StatusNotFound)
|
|
1816
|
+
// return
|
|
1817
|
+
// }
|
|
1818
|
+
|
|
1819
|
+
// 🎯 Production-ready: Log event with old/new values
|
|
1820
|
+
// event := ${handlerName}Event{
|
|
1821
|
+
// ID: uuid.New().String(),
|
|
1822
|
+
// Type: "updated",
|
|
1823
|
+
// EntityID: id,
|
|
1824
|
+
// OldValue: oldValue,
|
|
1825
|
+
// NewValue: &input,
|
|
1826
|
+
// Timestamp: time.Now(),
|
|
1827
|
+
// }
|
|
1828
|
+
// logEvent(event)
|
|
1829
|
+
|
|
1830
|
+
// 🎯 Production-ready: Invalidate cache
|
|
1831
|
+
// redis.Del(fmt.Sprintf("${safeName}:detail:%s", id), "${safeName}:list:*")
|
|
1832
|
+
|
|
1833
|
+
// 🎯 Production-ready: Emit VRP diff for real-time updates
|
|
1834
|
+
// vrp.SendDiff("${safeName}:" + id, input)
|
|
1835
|
+
|
|
1836
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1837
|
+
json.NewEncoder(w).Encode(input)
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// Delete${handlerName} handles DELETE /${safeName}/{id}
|
|
1841
|
+
func Delete${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1842
|
+
vars := mux.Vars(r)
|
|
1843
|
+
id := vars["id"]
|
|
1844
|
+
|
|
1845
|
+
// 🎯 Production-ready: Get value for event logging
|
|
1846
|
+
// oldValue, _ := db.Get${handlerName}(id)
|
|
1847
|
+
|
|
1848
|
+
// TODO: Implement actual DB delete
|
|
1849
|
+
// if err := db.Delete${handlerName}(id); err != nil {
|
|
1850
|
+
// http.Error(w, err.Error(), http.StatusNotFound)
|
|
1851
|
+
// return
|
|
1852
|
+
// }
|
|
1853
|
+
|
|
1854
|
+
// 🎯 Production-ready: Log deletion event
|
|
1855
|
+
// event := ${handlerName}Event{
|
|
1856
|
+
// ID: uuid.New().String(),
|
|
1857
|
+
// Type: "deleted",
|
|
1858
|
+
// EntityID: id,
|
|
1859
|
+
// OldValue: oldValue,
|
|
1860
|
+
// Timestamp: time.Now(),
|
|
1861
|
+
// }
|
|
1862
|
+
// logEvent(event)
|
|
1863
|
+
|
|
1864
|
+
// 🎯 Production-ready: Invalidate cache
|
|
1865
|
+
// redis.Del(fmt.Sprintf("${safeName}:detail:%s", id), "${safeName}:list:*")
|
|
1866
|
+
|
|
1867
|
+
// 🎯 Production-ready: Emit VRP event
|
|
1868
|
+
// vrp.SendEvent("${safeName}", "deleted", map[string]string{"id": id})
|
|
1869
|
+
|
|
1870
|
+
w.WriteHeader(http.StatusNoContent)
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// 🎯 Production-ready: BatchUpdate handles POST /${safeName}/batch/update
|
|
1874
|
+
func BatchUpdate${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1875
|
+
var req BatchUpdateRequest
|
|
1876
|
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
1877
|
+
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
1878
|
+
return
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
if err := validate.Struct(req); err != nil {
|
|
1882
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1883
|
+
w.WriteHeader(http.StatusBadRequest)
|
|
1884
|
+
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
|
1885
|
+
return
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// TODO: Implement batch update in transaction
|
|
1889
|
+
// tx := db.Begin()
|
|
1890
|
+
// for _, id := range req.IDs {
|
|
1891
|
+
// if err := tx.Update${handlerName}(id, req.Payload); err != nil {
|
|
1892
|
+
// tx.Rollback()
|
|
1893
|
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
1894
|
+
// return
|
|
1895
|
+
// }
|
|
1896
|
+
// }
|
|
1897
|
+
// tx.Commit()
|
|
1898
|
+
|
|
1899
|
+
// 🎯 Production-ready: Batch emit VRP events
|
|
1900
|
+
// vrp.BatchEmit("${safeName}", "updated", req)
|
|
1901
|
+
|
|
1902
|
+
// Invalidate cache
|
|
1903
|
+
// redis.Del("${safeName}:*")
|
|
1904
|
+
|
|
1905
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1906
|
+
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
1907
|
+
"updated": len(req.IDs),
|
|
1908
|
+
"ids": req.IDs,
|
|
1909
|
+
})
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// 🎯 Production-ready: BatchDelete handles POST /${safeName}/batch/delete
|
|
1913
|
+
func BatchDelete${handlerName}(w http.ResponseWriter, r *http.Request) {
|
|
1914
|
+
var req BatchDeleteRequest
|
|
1915
|
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
1916
|
+
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
1917
|
+
return
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if err := validate.Struct(req); err != nil {
|
|
1921
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1922
|
+
w.WriteHeader(http.StatusBadRequest)
|
|
1923
|
+
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
|
1924
|
+
return
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// TODO: Implement batch delete in transaction
|
|
1928
|
+
// tx := db.Begin()
|
|
1929
|
+
// for _, id := range req.IDs {
|
|
1930
|
+
// if err := tx.Delete${handlerName}(id); err != nil {
|
|
1931
|
+
// tx.Rollback()
|
|
1932
|
+
// http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
1933
|
+
// return
|
|
1934
|
+
// }
|
|
1935
|
+
// }
|
|
1936
|
+
// tx.Commit()
|
|
1937
|
+
|
|
1938
|
+
// 🎯 Production-ready: Batch emit VRP events
|
|
1939
|
+
// vrp.BatchEmit("${safeName}", "deleted", req)
|
|
1940
|
+
|
|
1941
|
+
// Invalidate cache
|
|
1942
|
+
// redis.Del("${safeName}:*")
|
|
1943
|
+
|
|
1944
|
+
w.WriteHeader(http.StatusNoContent)
|
|
1945
|
+
}
|
|
1946
|
+
`;
|
|
1947
|
+
await fs.writeFile(path.join(targetDir, `${safeName}_crud.go`), handlerCode);
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Валидация проекта
|
|
1951
|
+
*/
|
|
1952
|
+
async function validateProject() {
|
|
1953
|
+
const cwd = process.cwd();
|
|
1954
|
+
const errors = [];
|
|
1955
|
+
const warnings = [];
|
|
1956
|
+
console.log(chalk_1.default.blue("\nValidating Vira project...\n"));
|
|
1957
|
+
// Проверка структуры frontend проекта
|
|
1958
|
+
const frontendPath = path.join(cwd, "frontend");
|
|
1959
|
+
const frontendExists = await fs.pathExists(frontendPath);
|
|
1960
|
+
if (frontendExists) {
|
|
1961
|
+
const requiredFiles = [
|
|
1962
|
+
"package.json",
|
|
1963
|
+
"vite.config.ts",
|
|
1964
|
+
"tsconfig.json",
|
|
1965
|
+
"src/main.tsx",
|
|
1966
|
+
];
|
|
1967
|
+
for (const file of requiredFiles) {
|
|
1968
|
+
const filePath = path.join(frontendPath, file);
|
|
1969
|
+
if (!(await fs.pathExists(filePath))) {
|
|
1970
|
+
errors.push(`Missing frontend file: ${file}`);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
// Проверка структуры директорий
|
|
1974
|
+
const requiredDirs = ["src/components", "src/services", "src/pages"];
|
|
1975
|
+
for (const dir of requiredDirs) {
|
|
1976
|
+
const dirPath = path.join(frontendPath, dir);
|
|
1977
|
+
if (!(await fs.pathExists(dirPath))) {
|
|
1978
|
+
warnings.push(`Missing frontend directory: ${dir}`);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
else {
|
|
1983
|
+
// Проверка standalone frontend
|
|
1984
|
+
if (!(await fs.pathExists(path.join(cwd, "package.json")))) {
|
|
1985
|
+
errors.push("Missing package.json");
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
// Проверка структуры backend проекта
|
|
1989
|
+
const backendPath = path.join(cwd, "backend");
|
|
1990
|
+
if (await fs.pathExists(backendPath)) {
|
|
1991
|
+
const requiredFiles = ["go.mod", "cmd/api/main.go"];
|
|
1992
|
+
for (const file of requiredFiles) {
|
|
1993
|
+
const filePath = path.join(backendPath, file);
|
|
1994
|
+
if (!(await fs.pathExists(filePath))) {
|
|
1995
|
+
errors.push(`Missing backend file: ${file}`);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
// Вывод результатов
|
|
2000
|
+
if (errors.length > 0) {
|
|
2001
|
+
console.log(chalk_1.default.red("✗ Errors found:"));
|
|
2002
|
+
errors.forEach((err) => console.log(chalk_1.default.red(` - ${err}`)));
|
|
2003
|
+
}
|
|
2004
|
+
if (warnings.length > 0) {
|
|
2005
|
+
console.log(chalk_1.default.yellow("\n⚠ Warnings:"));
|
|
2006
|
+
warnings.forEach((warn) => console.log(chalk_1.default.yellow(` - ${warn}`)));
|
|
2007
|
+
}
|
|
2008
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
2009
|
+
console.log(chalk_1.default.green("✓ Project structure is valid!"));
|
|
2010
|
+
}
|
|
2011
|
+
else if (errors.length === 0) {
|
|
2012
|
+
console.log(chalk_1.default.green("\n✓ Project structure is valid (with warnings)"));
|
|
2013
|
+
}
|
|
2014
|
+
else {
|
|
2015
|
+
console.log(chalk_1.default.red("\n✗ Project validation failed"));
|
|
2016
|
+
process.exit(1);
|
|
2017
|
+
}
|
|
2018
|
+
}
|