@compilr-dev/factory 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/dist/factory/file-writer.d.ts +11 -0
- package/dist/factory/file-writer.js +21 -0
- package/dist/factory/list-toolkits-tool.d.ts +12 -0
- package/dist/factory/list-toolkits-tool.js +35 -0
- package/dist/factory/registry.d.ts +15 -0
- package/dist/factory/registry.js +28 -0
- package/dist/factory/scaffold-tool.d.ts +21 -0
- package/dist/factory/scaffold-tool.js +95 -0
- package/dist/factory/skill.d.ts +14 -0
- package/dist/factory/skill.js +146 -0
- package/dist/factory/tools.d.ts +14 -0
- package/dist/factory/tools.js +17 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +25 -0
- package/dist/model/defaults.d.ts +10 -0
- package/dist/model/defaults.js +68 -0
- package/dist/model/naming.d.ts +12 -0
- package/dist/model/naming.js +80 -0
- package/dist/model/operations-entity.d.ts +35 -0
- package/dist/model/operations-entity.js +110 -0
- package/dist/model/operations-field.d.ts +33 -0
- package/dist/model/operations-field.js +104 -0
- package/dist/model/operations-relationship.d.ts +19 -0
- package/dist/model/operations-relationship.js +90 -0
- package/dist/model/operations-section.d.ts +32 -0
- package/dist/model/operations-section.js +35 -0
- package/dist/model/operations.d.ts +12 -0
- package/dist/model/operations.js +63 -0
- package/dist/model/persistence.d.ts +19 -0
- package/dist/model/persistence.js +40 -0
- package/dist/model/schema.d.ts +15 -0
- package/dist/model/schema.js +269 -0
- package/dist/model/tools.d.ts +14 -0
- package/dist/model/tools.js +380 -0
- package/dist/model/types.d.ts +70 -0
- package/dist/model/types.js +8 -0
- package/dist/toolkits/react-node/api.d.ts +8 -0
- package/dist/toolkits/react-node/api.js +198 -0
- package/dist/toolkits/react-node/config.d.ts +9 -0
- package/dist/toolkits/react-node/config.js +120 -0
- package/dist/toolkits/react-node/dashboard.d.ts +8 -0
- package/dist/toolkits/react-node/dashboard.js +60 -0
- package/dist/toolkits/react-node/entity.d.ts +8 -0
- package/dist/toolkits/react-node/entity.js +469 -0
- package/dist/toolkits/react-node/helpers.d.ts +27 -0
- package/dist/toolkits/react-node/helpers.js +71 -0
- package/dist/toolkits/react-node/index.d.ts +8 -0
- package/dist/toolkits/react-node/index.js +52 -0
- package/dist/toolkits/react-node/router.d.ts +8 -0
- package/dist/toolkits/react-node/router.js +49 -0
- package/dist/toolkits/react-node/seed.d.ts +11 -0
- package/dist/toolkits/react-node/seed.js +144 -0
- package/dist/toolkits/react-node/shared.d.ts +7 -0
- package/dist/toolkits/react-node/shared.js +119 -0
- package/dist/toolkits/react-node/shell.d.ts +8 -0
- package/dist/toolkits/react-node/shell.js +152 -0
- package/dist/toolkits/react-node/static.d.ts +8 -0
- package/dist/toolkits/react-node/static.js +105 -0
- package/dist/toolkits/react-node/types-gen.d.ts +8 -0
- package/dist/toolkits/react-node/types-gen.js +31 -0
- package/dist/toolkits/types.d.ts +22 -0
- package/dist/toolkits/types.js +6 -0
- package/package.json +51 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Shared Helpers
|
|
3
|
+
*
|
|
4
|
+
* Utility functions used across generators.
|
|
5
|
+
*/
|
|
6
|
+
import type { Entity, Field, Relationship } from '../../model/types.js';
|
|
7
|
+
/** Indent every line of a multi-line string. */
|
|
8
|
+
export declare function indent(text: string, spaces: number): string;
|
|
9
|
+
/** Map a model FieldType to a TypeScript type string. */
|
|
10
|
+
export declare function tsType(field: Field): string;
|
|
11
|
+
/** Get the FK field name for a belongsTo relationship. */
|
|
12
|
+
export declare function fkFieldName(rel: Relationship): string;
|
|
13
|
+
/** Get belongsTo relationships for an entity. */
|
|
14
|
+
export declare function belongsToRels(entity: Entity): readonly Relationship[];
|
|
15
|
+
/** Get hasMany relationships for an entity. */
|
|
16
|
+
export declare function hasManyRels(entity: Entity): readonly Relationship[];
|
|
17
|
+
/** Generate a route path from entity plural name. */
|
|
18
|
+
export declare function routePath(entity: Entity): string;
|
|
19
|
+
/** Generate an API path from entity plural name. */
|
|
20
|
+
export declare function apiPath(entity: Entity): string;
|
|
21
|
+
/** Map a model FieldType to an HTML input type. */
|
|
22
|
+
export declare function inputType(field: Field): string;
|
|
23
|
+
/** Generate import statement lines. */
|
|
24
|
+
export declare function generateImports(imports: Array<{
|
|
25
|
+
from: string;
|
|
26
|
+
names: string[];
|
|
27
|
+
}>): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Shared Helpers
|
|
3
|
+
*
|
|
4
|
+
* Utility functions used across generators.
|
|
5
|
+
*/
|
|
6
|
+
import { toCamelCase, toKebabCase } from '../../model/naming.js';
|
|
7
|
+
/** Indent every line of a multi-line string. */
|
|
8
|
+
export function indent(text, spaces) {
|
|
9
|
+
const pad = ' '.repeat(spaces);
|
|
10
|
+
return text
|
|
11
|
+
.split('\n')
|
|
12
|
+
.map((line) => (line.trim() === '' ? '' : pad + line))
|
|
13
|
+
.join('\n');
|
|
14
|
+
}
|
|
15
|
+
/** Map a model FieldType to a TypeScript type string. */
|
|
16
|
+
export function tsType(field) {
|
|
17
|
+
switch (field.type) {
|
|
18
|
+
case 'string':
|
|
19
|
+
case 'enum':
|
|
20
|
+
return 'string';
|
|
21
|
+
case 'number':
|
|
22
|
+
return 'number';
|
|
23
|
+
case 'boolean':
|
|
24
|
+
return 'boolean';
|
|
25
|
+
case 'date':
|
|
26
|
+
return 'string'; // ISO date string
|
|
27
|
+
default:
|
|
28
|
+
return 'string';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Get the FK field name for a belongsTo relationship. */
|
|
32
|
+
export function fkFieldName(rel) {
|
|
33
|
+
return rel.fieldName ?? toCamelCase(rel.target) + 'Id';
|
|
34
|
+
}
|
|
35
|
+
/** Get belongsTo relationships for an entity. */
|
|
36
|
+
export function belongsToRels(entity) {
|
|
37
|
+
return entity.relationships.filter((r) => r.type === 'belongsTo');
|
|
38
|
+
}
|
|
39
|
+
/** Get hasMany relationships for an entity. */
|
|
40
|
+
export function hasManyRels(entity) {
|
|
41
|
+
return entity.relationships.filter((r) => r.type === 'hasMany');
|
|
42
|
+
}
|
|
43
|
+
/** Generate a route path from entity plural name. */
|
|
44
|
+
export function routePath(entity) {
|
|
45
|
+
return '/' + toKebabCase(entity.pluralName).toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
/** Generate an API path from entity plural name. */
|
|
48
|
+
export function apiPath(entity) {
|
|
49
|
+
return '/api/' + toKebabCase(entity.pluralName).toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
/** Map a model FieldType to an HTML input type. */
|
|
52
|
+
export function inputType(field) {
|
|
53
|
+
switch (field.type) {
|
|
54
|
+
case 'string':
|
|
55
|
+
return 'text';
|
|
56
|
+
case 'number':
|
|
57
|
+
return 'number';
|
|
58
|
+
case 'boolean':
|
|
59
|
+
return 'checkbox';
|
|
60
|
+
case 'date':
|
|
61
|
+
return 'date';
|
|
62
|
+
case 'enum':
|
|
63
|
+
return 'select';
|
|
64
|
+
default:
|
|
65
|
+
return 'text';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Generate import statement lines. */
|
|
69
|
+
export function generateImports(imports) {
|
|
70
|
+
return imports.map((i) => `import { ${i.names.join(', ')} } from '${i.from}';`).join('\n');
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit
|
|
3
|
+
*
|
|
4
|
+
* Generates a full-stack MVP: React 18 + Vite + Tailwind + Express.
|
|
5
|
+
* Deterministic: same ApplicationModel → same output files.
|
|
6
|
+
*/
|
|
7
|
+
import type { FactoryToolkit } from '../types.js';
|
|
8
|
+
export declare const reactNodeToolkit: FactoryToolkit;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit
|
|
3
|
+
*
|
|
4
|
+
* Generates a full-stack MVP: React 18 + Vite + Tailwind + Express.
|
|
5
|
+
* Deterministic: same ApplicationModel → same output files.
|
|
6
|
+
*/
|
|
7
|
+
import { generateConfigFiles } from './config.js';
|
|
8
|
+
import { generateStaticFiles } from './static.js';
|
|
9
|
+
import { generateShellFiles } from './shell.js';
|
|
10
|
+
import { generateSharedComponents } from './shared.js';
|
|
11
|
+
import { generateDashboard } from './dashboard.js';
|
|
12
|
+
import { generateEntityFiles } from './entity.js';
|
|
13
|
+
import { generateRouter } from './router.js';
|
|
14
|
+
import { generateApiFiles } from './api.js';
|
|
15
|
+
import { generateTypesFile } from './types-gen.js';
|
|
16
|
+
export const reactNodeToolkit = {
|
|
17
|
+
id: 'react-node',
|
|
18
|
+
name: 'React + Node',
|
|
19
|
+
description: 'React 18 + Vite + Tailwind CSS + Express — full-stack MVP scaffold',
|
|
20
|
+
requiredSections: ['identity', 'entities', 'layout', 'features', 'theme'],
|
|
21
|
+
generate(model) {
|
|
22
|
+
const warnings = [];
|
|
23
|
+
const allFiles = [];
|
|
24
|
+
// Collect files from all generators
|
|
25
|
+
const generators = [
|
|
26
|
+
generateConfigFiles(model),
|
|
27
|
+
generateStaticFiles(model),
|
|
28
|
+
generateTypesFile(model),
|
|
29
|
+
generateShellFiles(model),
|
|
30
|
+
generateSharedComponents(),
|
|
31
|
+
generateDashboard(model),
|
|
32
|
+
generateEntityFiles(model),
|
|
33
|
+
generateRouter(model),
|
|
34
|
+
generateApiFiles(model),
|
|
35
|
+
];
|
|
36
|
+
for (const files of generators) {
|
|
37
|
+
allFiles.push(...files);
|
|
38
|
+
}
|
|
39
|
+
// Check for potential issues
|
|
40
|
+
if (model.entities.length === 0) {
|
|
41
|
+
warnings.push('No entities defined — generated app will have minimal functionality.');
|
|
42
|
+
}
|
|
43
|
+
if (model.entities.length > 10) {
|
|
44
|
+
warnings.push('Large number of entities — generated sidebar may be crowded.');
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
files: allFiles,
|
|
48
|
+
toolkit: 'react-node',
|
|
49
|
+
warnings,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Router Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates src/router.tsx with React Router v6 routes.
|
|
5
|
+
*/
|
|
6
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
7
|
+
import type { FactoryFile } from '../types.js';
|
|
8
|
+
export declare function generateRouter(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Router Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates src/router.tsx with React Router v6 routes.
|
|
5
|
+
*/
|
|
6
|
+
import { toKebabCase } from '../../model/naming.js';
|
|
7
|
+
export function generateRouter(model) {
|
|
8
|
+
const imports = [];
|
|
9
|
+
const routes = [];
|
|
10
|
+
// Layout import
|
|
11
|
+
imports.push("import Layout from './components/layout/Layout';");
|
|
12
|
+
// Dashboard
|
|
13
|
+
if (model.features.dashboard) {
|
|
14
|
+
imports.push("import Dashboard from './pages/Dashboard';");
|
|
15
|
+
routes.push(' { index: true, element: <Dashboard /> },');
|
|
16
|
+
}
|
|
17
|
+
// Entity routes
|
|
18
|
+
for (const entity of model.entities) {
|
|
19
|
+
const routeBase = toKebabCase(entity.pluralName).toLowerCase();
|
|
20
|
+
imports.push(`import ${entity.name}List from './pages/${entity.name}List';`);
|
|
21
|
+
imports.push(`import ${entity.name}Detail from './pages/${entity.name}Detail';`);
|
|
22
|
+
imports.push(`import ${entity.name}Form from './components/${routeBase}/${entity.name}Form';`);
|
|
23
|
+
if (!model.features.dashboard && entity === model.entities[0]) {
|
|
24
|
+
routes.push(` { index: true, element: <${entity.name}List /> },`);
|
|
25
|
+
}
|
|
26
|
+
routes.push(` { path: '${routeBase}', element: <${entity.name}List /> },`);
|
|
27
|
+
routes.push(` { path: '${routeBase}/new', element: <${entity.name}Form /> },`);
|
|
28
|
+
routes.push(` { path: '${routeBase}/:id', element: <${entity.name}Detail /> },`);
|
|
29
|
+
routes.push(` { path: '${routeBase}/:id/edit', element: <${entity.name}Form /> },`);
|
|
30
|
+
}
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
path: 'src/router.tsx',
|
|
34
|
+
content: `import { createBrowserRouter } from 'react-router-dom';
|
|
35
|
+
${imports.join('\n')}
|
|
36
|
+
|
|
37
|
+
export const router = createBrowserRouter([
|
|
38
|
+
{
|
|
39
|
+
path: '/',
|
|
40
|
+
element: <Layout />,
|
|
41
|
+
children: [
|
|
42
|
+
${routes.join('\n')}
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
`,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Seed Data Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates realistic mock data based on field semantics.
|
|
5
|
+
* Deterministic: same input → same output (uses index-based logic, not Math.random).
|
|
6
|
+
*/
|
|
7
|
+
import type { ApplicationModel, Entity } from '../../model/types.js';
|
|
8
|
+
declare const SEED_COUNT = 8;
|
|
9
|
+
/** Generate seed data items for a single entity. */
|
|
10
|
+
export declare function generateSeedData(model: ApplicationModel, entity: Entity): string;
|
|
11
|
+
export { SEED_COUNT };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Seed Data Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates realistic mock data based on field semantics.
|
|
5
|
+
* Deterministic: same input → same output (uses index-based logic, not Math.random).
|
|
6
|
+
*/
|
|
7
|
+
import { toCamelCase } from '../../model/naming.js';
|
|
8
|
+
import { belongsToRels, fkFieldName } from './helpers.js';
|
|
9
|
+
const PERSON_NAMES = [
|
|
10
|
+
'Alice Johnson',
|
|
11
|
+
'Bob Smith',
|
|
12
|
+
'Carol Davis',
|
|
13
|
+
'David Lee',
|
|
14
|
+
'Eva Martinez',
|
|
15
|
+
'Frank Wilson',
|
|
16
|
+
'Grace Chen',
|
|
17
|
+
'Henry Brown',
|
|
18
|
+
'Iris Kim',
|
|
19
|
+
'Jack Taylor',
|
|
20
|
+
];
|
|
21
|
+
const COMPANY_NAMES = [
|
|
22
|
+
'Acme Corp',
|
|
23
|
+
'TechVista Inc',
|
|
24
|
+
'BlueSky Solutions',
|
|
25
|
+
'Greenfield LLC',
|
|
26
|
+
'Summit Dynamics',
|
|
27
|
+
'Cascade Systems',
|
|
28
|
+
'Meridian Labs',
|
|
29
|
+
'Atlas Ventures',
|
|
30
|
+
];
|
|
31
|
+
const PRODUCT_NAMES = [
|
|
32
|
+
'Premium Widget',
|
|
33
|
+
'Standard Package',
|
|
34
|
+
'Enterprise Suite',
|
|
35
|
+
'Starter Kit',
|
|
36
|
+
'Pro Bundle',
|
|
37
|
+
'Deluxe Edition',
|
|
38
|
+
'Basic Plan',
|
|
39
|
+
'Advanced Toolkit',
|
|
40
|
+
];
|
|
41
|
+
const DESCRIPTIONS = [
|
|
42
|
+
'A comprehensive solution for modern needs.',
|
|
43
|
+
'Designed for efficiency and reliability.',
|
|
44
|
+
'Built with quality and care.',
|
|
45
|
+
'Perfect for teams of any size.',
|
|
46
|
+
'Streamlined for maximum productivity.',
|
|
47
|
+
'Industry-leading performance.',
|
|
48
|
+
'Trusted by thousands of users.',
|
|
49
|
+
'Next-generation technology.',
|
|
50
|
+
];
|
|
51
|
+
const EMAILS = [
|
|
52
|
+
'alice@example.com',
|
|
53
|
+
'bob@example.com',
|
|
54
|
+
'carol@example.com',
|
|
55
|
+
'david@example.com',
|
|
56
|
+
'eva@example.com',
|
|
57
|
+
'frank@example.com',
|
|
58
|
+
'grace@example.com',
|
|
59
|
+
'henry@example.com',
|
|
60
|
+
];
|
|
61
|
+
const SEED_COUNT = 8;
|
|
62
|
+
function generateFieldValue(field, index, entityName) {
|
|
63
|
+
const lowerName = field.name.toLowerCase();
|
|
64
|
+
const lowerEntity = entityName.toLowerCase();
|
|
65
|
+
switch (field.type) {
|
|
66
|
+
case 'string':
|
|
67
|
+
if (lowerName.includes('email'))
|
|
68
|
+
return `'${EMAILS[index % EMAILS.length]}'`;
|
|
69
|
+
if (lowerName.includes('name') || lowerName === 'title') {
|
|
70
|
+
if (lowerEntity.includes('customer') ||
|
|
71
|
+
lowerEntity.includes('user') ||
|
|
72
|
+
lowerEntity.includes('person') ||
|
|
73
|
+
lowerEntity.includes('employee')) {
|
|
74
|
+
return `'${PERSON_NAMES[index % PERSON_NAMES.length]}'`;
|
|
75
|
+
}
|
|
76
|
+
if (lowerEntity.includes('company') || lowerEntity.includes('organization')) {
|
|
77
|
+
return `'${COMPANY_NAMES[index % COMPANY_NAMES.length]}'`;
|
|
78
|
+
}
|
|
79
|
+
return `'${PRODUCT_NAMES[index % PRODUCT_NAMES.length]}'`;
|
|
80
|
+
}
|
|
81
|
+
if (lowerName.includes('description') ||
|
|
82
|
+
lowerName.includes('note') ||
|
|
83
|
+
lowerName.includes('comment')) {
|
|
84
|
+
return `'${DESCRIPTIONS[index % DESCRIPTIONS.length]}'`;
|
|
85
|
+
}
|
|
86
|
+
if (lowerName.includes('phone'))
|
|
87
|
+
return `'555-010${String(index)}'`;
|
|
88
|
+
if (lowerName.includes('address'))
|
|
89
|
+
return `'${String(100 + index * 10)} Main St'`;
|
|
90
|
+
return `'${field.label} ${String(index + 1)}'`;
|
|
91
|
+
case 'number':
|
|
92
|
+
if (lowerName.includes('price') || lowerName.includes('cost') || lowerName.includes('amount'))
|
|
93
|
+
return String(10 + index * 15 + 0.99);
|
|
94
|
+
if (lowerName.includes('quantity') || lowerName.includes('count'))
|
|
95
|
+
return String(1 + (index % 10));
|
|
96
|
+
if (lowerName.includes('age'))
|
|
97
|
+
return String(22 + index * 5);
|
|
98
|
+
if (lowerName.includes('size') || lowerName.includes('capacity'))
|
|
99
|
+
return String(2 + (index % 8));
|
|
100
|
+
return String(index + 1);
|
|
101
|
+
case 'boolean':
|
|
102
|
+
return index % 2 === 0 ? 'true' : 'false';
|
|
103
|
+
case 'date': {
|
|
104
|
+
const base = new Date('2025-06-15');
|
|
105
|
+
base.setDate(base.getDate() + (index * 3 - 10));
|
|
106
|
+
return `'${base.toISOString().split('T')[0]}'`;
|
|
107
|
+
}
|
|
108
|
+
case 'enum': {
|
|
109
|
+
const values = field.enumValues ?? [];
|
|
110
|
+
if (values.length === 0)
|
|
111
|
+
return "''";
|
|
112
|
+
return `'${values[index % values.length]}'`;
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
return `'${field.label} ${String(index + 1)}'`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Generate seed data items for a single entity. */
|
|
119
|
+
export function generateSeedData(model, entity) {
|
|
120
|
+
const varName = toCamelCase(entity.pluralName);
|
|
121
|
+
const rels = belongsToRels(entity);
|
|
122
|
+
const lines = [];
|
|
123
|
+
lines.push(`export const ${varName}SeedData = [`);
|
|
124
|
+
for (let i = 0; i < SEED_COUNT; i++) {
|
|
125
|
+
lines.push(' {');
|
|
126
|
+
lines.push(` id: ${String(i + 1)},`);
|
|
127
|
+
for (const field of entity.fields) {
|
|
128
|
+
const value = generateFieldValue(field, i, entity.name);
|
|
129
|
+
lines.push(` ${field.name}: ${value},`);
|
|
130
|
+
}
|
|
131
|
+
// FK fields from belongsTo relationships
|
|
132
|
+
for (const rel of rels) {
|
|
133
|
+
const fk = fkFieldName(rel);
|
|
134
|
+
// Assign FK cycling through target's seed data
|
|
135
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
136
|
+
const targetCount = targetEntity ? SEED_COUNT : SEED_COUNT;
|
|
137
|
+
lines.push(` ${fk}: ${String((i % targetCount) + 1)},`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(' },');
|
|
140
|
+
}
|
|
141
|
+
lines.push('];');
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
export { SEED_COUNT };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Shared Components Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: ViewToggle.tsx, SearchBar.tsx, FilterBar.tsx
|
|
5
|
+
*/
|
|
6
|
+
export function generateSharedComponents() {
|
|
7
|
+
return [generateViewToggle(), generateSearchBar(), generateFilterBar()];
|
|
8
|
+
}
|
|
9
|
+
function generateViewToggle() {
|
|
10
|
+
return {
|
|
11
|
+
path: 'src/components/shared/ViewToggle.tsx',
|
|
12
|
+
content: `interface ViewToggleProps {
|
|
13
|
+
view: 'card' | 'list';
|
|
14
|
+
onChange: (view: 'card' | 'list') => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ViewToggle({ view, onChange }: ViewToggleProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex rounded-lg border border-gray-300 dark:border-gray-600">
|
|
20
|
+
<button
|
|
21
|
+
onClick={() => onChange('card')}
|
|
22
|
+
className={\`px-3 py-1 text-sm \${view === 'card' ? 'bg-primary text-white' : 'text-gray-600 dark:text-gray-300'}\`}
|
|
23
|
+
>
|
|
24
|
+
Grid
|
|
25
|
+
</button>
|
|
26
|
+
<button
|
|
27
|
+
onClick={() => onChange('list')}
|
|
28
|
+
className={\`px-3 py-1 text-sm \${view === 'list' ? 'bg-primary text-white' : 'text-gray-600 dark:text-gray-300'}\`}
|
|
29
|
+
>
|
|
30
|
+
List
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function generateSearchBar() {
|
|
39
|
+
return {
|
|
40
|
+
path: 'src/components/shared/SearchBar.tsx',
|
|
41
|
+
content: `import { useState, useEffect } from 'react';
|
|
42
|
+
|
|
43
|
+
interface SearchBarProps {
|
|
44
|
+
onSearch: (query: string) => void;
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function SearchBar({ onSearch, placeholder = 'Search...' }: SearchBarProps) {
|
|
49
|
+
const [value, setValue] = useState('');
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const timer = setTimeout(() => onSearch(value), 300);
|
|
53
|
+
return () => clearTimeout(timer);
|
|
54
|
+
}, [value, onSearch]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="relative">
|
|
58
|
+
<input
|
|
59
|
+
type="text"
|
|
60
|
+
value={value}
|
|
61
|
+
onChange={(e) => setValue(e.target.value)}
|
|
62
|
+
placeholder={placeholder}
|
|
63
|
+
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-primary focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
64
|
+
/>
|
|
65
|
+
{value && (
|
|
66
|
+
<button
|
|
67
|
+
onClick={() => setValue('')}
|
|
68
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
69
|
+
>
|
|
70
|
+
×
|
|
71
|
+
</button>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function generateFilterBar() {
|
|
80
|
+
return {
|
|
81
|
+
path: 'src/components/shared/FilterBar.tsx',
|
|
82
|
+
content: `interface FilterOption {
|
|
83
|
+
label: string;
|
|
84
|
+
field: string;
|
|
85
|
+
values: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface FilterBarProps {
|
|
89
|
+
filters: FilterOption[];
|
|
90
|
+
active: Record<string, string>;
|
|
91
|
+
onChange: (field: string, value: string) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default function FilterBar({ filters, active, onChange }: FilterBarProps) {
|
|
95
|
+
if (filters.length === 0) return null;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="flex flex-wrap gap-2">
|
|
99
|
+
{filters.map((filter) => (
|
|
100
|
+
<select
|
|
101
|
+
key={filter.field}
|
|
102
|
+
value={active[filter.field] ?? ''}
|
|
103
|
+
onChange={(e) => onChange(filter.field, e.target.value)}
|
|
104
|
+
className="rounded-lg border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
105
|
+
>
|
|
106
|
+
<option value="">{filter.label}: All</option>
|
|
107
|
+
{filter.values.map((v) => (
|
|
108
|
+
<option key={v} value={v}>
|
|
109
|
+
{v}
|
|
110
|
+
</option>
|
|
111
|
+
))}
|
|
112
|
+
</select>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Shell Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: App.tsx, Layout.tsx, Sidebar.tsx, Header.tsx
|
|
5
|
+
*/
|
|
6
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
7
|
+
import type { FactoryFile } from '../types.js';
|
|
8
|
+
export declare function generateShellFiles(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Shell Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: App.tsx, Layout.tsx, Sidebar.tsx, Header.tsx
|
|
5
|
+
*/
|
|
6
|
+
import { toKebabCase } from '../../model/naming.js';
|
|
7
|
+
export function generateShellFiles(model) {
|
|
8
|
+
return [generateApp(model), generateLayout(model), generateSidebar(model), generateHeader(model)];
|
|
9
|
+
}
|
|
10
|
+
function generateApp(_model) {
|
|
11
|
+
return {
|
|
12
|
+
path: 'src/App.tsx',
|
|
13
|
+
content: `import { RouterProvider } from 'react-router-dom';
|
|
14
|
+
import { router } from './router';
|
|
15
|
+
|
|
16
|
+
export default function App() {
|
|
17
|
+
return <RouterProvider router={router} />;
|
|
18
|
+
}
|
|
19
|
+
`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function generateLayout(model) {
|
|
23
|
+
const hasSidebar = model.layout.shell === 'sidebar-header';
|
|
24
|
+
if (hasSidebar) {
|
|
25
|
+
return {
|
|
26
|
+
path: 'src/components/layout/Layout.tsx',
|
|
27
|
+
content: `import { Outlet } from 'react-router-dom';
|
|
28
|
+
import Sidebar from './Sidebar';
|
|
29
|
+
import Header from './Header';
|
|
30
|
+
|
|
31
|
+
export default function Layout() {
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
|
|
34
|
+
<Sidebar />
|
|
35
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
36
|
+
<Header />
|
|
37
|
+
<main className="flex-1 overflow-y-auto p-6">
|
|
38
|
+
<Outlet />
|
|
39
|
+
</main>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
path: 'src/components/layout/Layout.tsx',
|
|
49
|
+
content: `import { Outlet } from 'react-router-dom';
|
|
50
|
+
import Header from './Header';
|
|
51
|
+
|
|
52
|
+
export default function Layout() {
|
|
53
|
+
return (
|
|
54
|
+
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
55
|
+
<Header />
|
|
56
|
+
<main className="mx-auto max-w-7xl p-6">
|
|
57
|
+
<Outlet />
|
|
58
|
+
</main>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function generateSidebar(model) {
|
|
66
|
+
const navItems = model.entities
|
|
67
|
+
.map((e) => {
|
|
68
|
+
const path = '/' + toKebabCase(e.pluralName).toLowerCase();
|
|
69
|
+
return ` { label: '${e.icon} ${e.pluralName}', path: '${path}' },`;
|
|
70
|
+
})
|
|
71
|
+
.join('\n');
|
|
72
|
+
return {
|
|
73
|
+
path: 'src/components/layout/Sidebar.tsx',
|
|
74
|
+
content: `import { useState } from 'react';
|
|
75
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
76
|
+
|
|
77
|
+
const navItems = [
|
|
78
|
+
${navItems}
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
export default function Sidebar() {
|
|
82
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
83
|
+
const location = useLocation();
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<aside
|
|
87
|
+
className={\`\${collapsed ? 'w-16' : 'w-64'} flex flex-col border-r border-gray-200 bg-white transition-all dark:border-gray-700 dark:bg-gray-800\`}
|
|
88
|
+
>
|
|
89
|
+
<div className="flex h-14 items-center justify-between px-4">
|
|
90
|
+
{!collapsed && (
|
|
91
|
+
<span className="text-lg font-bold text-gray-900 dark:text-white">
|
|
92
|
+
${model.identity.name}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
<button
|
|
96
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
97
|
+
className="rounded p-1 text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
98
|
+
>
|
|
99
|
+
{collapsed ? '→' : '←'}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
<nav className="flex-1 space-y-1 px-2 py-4">
|
|
103
|
+
{navItems.map((item) => (
|
|
104
|
+
<Link
|
|
105
|
+
key={item.path}
|
|
106
|
+
to={item.path}
|
|
107
|
+
className={\`flex items-center rounded-lg px-3 py-2 text-sm font-medium \${
|
|
108
|
+
location.pathname.startsWith(item.path)
|
|
109
|
+
? 'bg-primary/10 text-primary'
|
|
110
|
+
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'
|
|
111
|
+
}\`}
|
|
112
|
+
>
|
|
113
|
+
{collapsed ? item.label.slice(0, 2) : item.label}
|
|
114
|
+
</Link>
|
|
115
|
+
))}
|
|
116
|
+
</nav>
|
|
117
|
+
</aside>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function generateHeader(model) {
|
|
124
|
+
const darkModeToggle = model.features.darkMode
|
|
125
|
+
? `
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => document.documentElement.classList.toggle('dark')}
|
|
128
|
+
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
129
|
+
>
|
|
130
|
+
🌓
|
|
131
|
+
</button>`
|
|
132
|
+
: '';
|
|
133
|
+
return {
|
|
134
|
+
path: 'src/components/layout/Header.tsx',
|
|
135
|
+
content: `import { useLocation } from 'react-router-dom';
|
|
136
|
+
|
|
137
|
+
export default function Header() {
|
|
138
|
+
const location = useLocation();
|
|
139
|
+
const path = location.pathname.split('/').filter(Boolean);
|
|
140
|
+
const breadcrumb = path.length > 0 ? path.join(' / ') : 'Home';
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<header className="flex h-14 items-center justify-between border-b border-gray-200 bg-white px-6 dark:border-gray-700 dark:bg-gray-800">
|
|
144
|
+
<span className="text-sm text-gray-500 dark:text-gray-400">{breadcrumb}</span>
|
|
145
|
+
<div className="flex items-center gap-2">${darkModeToggle}
|
|
146
|
+
</div>
|
|
147
|
+
</header>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Node Toolkit — Static Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: .gitignore, .env.example, README.md, index.html, src/main.tsx, src/index.css
|
|
5
|
+
*/
|
|
6
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
7
|
+
import type { FactoryFile } from '../types.js';
|
|
8
|
+
export declare function generateStaticFiles(model: ApplicationModel): FactoryFile[];
|