@compilr-dev/factory 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/factory/registry.js +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/toolkits/react-fastapi/api.d.ts +9 -0
- package/dist/toolkits/react-fastapi/api.js +378 -0
- package/dist/toolkits/react-fastapi/config.d.ts +9 -0
- package/dist/toolkits/react-fastapi/config.js +128 -0
- package/dist/toolkits/react-fastapi/helpers.d.ts +13 -0
- package/dist/toolkits/react-fastapi/helpers.js +54 -0
- package/dist/toolkits/react-fastapi/index.d.ts +11 -0
- package/dist/toolkits/react-fastapi/index.js +55 -0
- package/dist/toolkits/react-fastapi/seed.d.ts +12 -0
- package/dist/toolkits/react-fastapi/seed.js +54 -0
- package/dist/toolkits/react-fastapi/shared.d.ts +6 -0
- package/dist/toolkits/react-fastapi/shared.js +6 -0
- package/dist/toolkits/react-fastapi/static.d.ts +8 -0
- package/dist/toolkits/react-fastapi/static.js +123 -0
- package/dist/toolkits/static-landing/config.d.ts +8 -0
- package/dist/toolkits/static-landing/config.js +63 -0
- package/dist/toolkits/static-landing/index.d.ts +8 -0
- package/dist/toolkits/static-landing/index.js +35 -0
- package/dist/toolkits/static-landing/pages.d.ts +8 -0
- package/dist/toolkits/static-landing/pages.js +191 -0
- package/dist/toolkits/static-landing/static.d.ts +8 -0
- package/dist/toolkits/static-landing/static.js +65 -0
- package/package.json +1 -1
package/dist/factory/registry.js
CHANGED
|
@@ -26,5 +26,9 @@ export const defaultRegistry = new ToolkitRegistry();
|
|
|
26
26
|
// Register built-in toolkits
|
|
27
27
|
import { reactNodeToolkit } from '../toolkits/react-node/index.js';
|
|
28
28
|
import { nextPrismaToolkit } from '../toolkits/next-prisma/index.js';
|
|
29
|
+
import { staticLandingToolkit } from '../toolkits/static-landing/index.js';
|
|
30
|
+
import { reactFastapiToolkit } from '../toolkits/react-fastapi/index.js';
|
|
29
31
|
defaultRegistry.register(reactNodeToolkit);
|
|
30
32
|
defaultRegistry.register(nextPrismaToolkit);
|
|
33
|
+
defaultRegistry.register(staticLandingToolkit);
|
|
34
|
+
defaultRegistry.register(reactFastapiToolkit);
|
package/dist/index.d.ts
CHANGED
|
@@ -20,4 +20,6 @@ export { writeFactoryFiles } from './factory/file-writer.js';
|
|
|
20
20
|
export type { WriteResult } from './factory/file-writer.js';
|
|
21
21
|
export { reactNodeToolkit } from './toolkits/react-node/index.js';
|
|
22
22
|
export { nextPrismaToolkit } from './toolkits/next-prisma/index.js';
|
|
23
|
+
export { staticLandingToolkit } from './toolkits/static-landing/index.js';
|
|
24
|
+
export { reactFastapiToolkit } from './toolkits/react-fastapi/index.js';
|
|
23
25
|
export { factoryScaffoldSkill, factorySkills } from './factory/skill.js';
|
package/dist/index.js
CHANGED
|
@@ -23,5 +23,9 @@ export { writeFactoryFiles } from './factory/file-writer.js';
|
|
|
23
23
|
export { reactNodeToolkit } from './toolkits/react-node/index.js';
|
|
24
24
|
// Next.js + Prisma Toolkit
|
|
25
25
|
export { nextPrismaToolkit } from './toolkits/next-prisma/index.js';
|
|
26
|
+
// Static Landing Page Toolkit
|
|
27
|
+
export { staticLandingToolkit } from './toolkits/static-landing/index.js';
|
|
28
|
+
// React + FastAPI Toolkit
|
|
29
|
+
export { reactFastapiToolkit } from './toolkits/react-fastapi/index.js';
|
|
26
30
|
// Factory skill (Phase 5)
|
|
27
31
|
export { factoryScaffoldSkill, factorySkills } from './factory/skill.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit — API Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: server/main.py, server/models.py,
|
|
5
|
+
* server/database/{entity}.py, server/routers/{entity}.py
|
|
6
|
+
*/
|
|
7
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
8
|
+
import type { FactoryFile } from '../types.js';
|
|
9
|
+
export declare function generateApiFiles(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit — API Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: server/main.py, server/models.py,
|
|
5
|
+
* server/database/{entity}.py, server/routers/{entity}.py
|
|
6
|
+
*/
|
|
7
|
+
import { toCamelCase, toKebabCase } from '../../model/naming.js';
|
|
8
|
+
import { pyType, fkFieldName, belongsToRels, hasManyRels } from './helpers.js';
|
|
9
|
+
import { toSnakeCase } from './helpers.js';
|
|
10
|
+
import { generateSeedData } from './seed.js';
|
|
11
|
+
export function generateApiFiles(model) {
|
|
12
|
+
return [
|
|
13
|
+
generateServerMain(model),
|
|
14
|
+
generateModels(model),
|
|
15
|
+
...model.entities.flatMap((entity) => [
|
|
16
|
+
generateDatabase(model, entity),
|
|
17
|
+
generateRouter(model, entity),
|
|
18
|
+
]),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Server Entry Point
|
|
23
|
+
// =============================================================================
|
|
24
|
+
function generateServerMain(model) {
|
|
25
|
+
const routerImports = model.entities
|
|
26
|
+
.map((e) => {
|
|
27
|
+
const snakeName = toSnakeCase(e.pluralName);
|
|
28
|
+
return `from server.routers.${snakeName} import router as ${snakeName}_router`;
|
|
29
|
+
})
|
|
30
|
+
.join('\n');
|
|
31
|
+
const routerIncludes = model.entities
|
|
32
|
+
.map((e) => {
|
|
33
|
+
const snakeName = toSnakeCase(e.pluralName);
|
|
34
|
+
const apiRoute = '/api/' + toKebabCase(e.pluralName).toLowerCase();
|
|
35
|
+
return `app.include_router(${snakeName}_router, prefix="${apiRoute}")`;
|
|
36
|
+
})
|
|
37
|
+
.join('\n');
|
|
38
|
+
return {
|
|
39
|
+
path: 'server/main.py',
|
|
40
|
+
content: `from fastapi import FastAPI
|
|
41
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
42
|
+
${routerImports}
|
|
43
|
+
|
|
44
|
+
app = FastAPI(title="${model.identity.name}")
|
|
45
|
+
|
|
46
|
+
app.add_middleware(
|
|
47
|
+
CORSMiddleware,
|
|
48
|
+
allow_origins=["*"],
|
|
49
|
+
allow_credentials=True,
|
|
50
|
+
allow_methods=["*"],
|
|
51
|
+
allow_headers=["*"],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
${routerIncludes}
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
import uvicorn
|
|
58
|
+
uvicorn.run("server.main:app", host="0.0.0.0", port=8000, reload=True)
|
|
59
|
+
`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Pydantic Models
|
|
64
|
+
// =============================================================================
|
|
65
|
+
function generateModels(model) {
|
|
66
|
+
const modelBlocks = [];
|
|
67
|
+
for (const entity of model.entities) {
|
|
68
|
+
const btoRels = belongsToRels(entity);
|
|
69
|
+
// Create model (no id/timestamps)
|
|
70
|
+
const createFields = [];
|
|
71
|
+
for (const field of entity.fields) {
|
|
72
|
+
const pyT = pyType(field);
|
|
73
|
+
if (field.required) {
|
|
74
|
+
createFields.push(` ${field.name}: ${pyT}`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
createFields.push(` ${field.name}: Optional[${pyT}] = None`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const rel of btoRels) {
|
|
81
|
+
const fk = fkFieldName(rel);
|
|
82
|
+
createFields.push(` ${fk}: int`);
|
|
83
|
+
}
|
|
84
|
+
// Update model (all Optional)
|
|
85
|
+
const updateFields = [];
|
|
86
|
+
for (const field of entity.fields) {
|
|
87
|
+
const pyT = pyType(field);
|
|
88
|
+
updateFields.push(` ${field.name}: Optional[${pyT}] = None`);
|
|
89
|
+
}
|
|
90
|
+
for (const rel of btoRels) {
|
|
91
|
+
const fk = fkFieldName(rel);
|
|
92
|
+
updateFields.push(` ${fk}: Optional[int] = None`);
|
|
93
|
+
}
|
|
94
|
+
// Response model (id + all fields + timestamps)
|
|
95
|
+
const responseFields = [' id: int'];
|
|
96
|
+
for (const field of entity.fields) {
|
|
97
|
+
const pyT = pyType(field);
|
|
98
|
+
if (field.required) {
|
|
99
|
+
responseFields.push(` ${field.name}: ${pyT}`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
responseFields.push(` ${field.name}: Optional[${pyT}] = None`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const rel of btoRels) {
|
|
106
|
+
const fk = fkFieldName(rel);
|
|
107
|
+
responseFields.push(` ${fk}: int`);
|
|
108
|
+
}
|
|
109
|
+
responseFields.push(' createdAt: str');
|
|
110
|
+
responseFields.push(' updatedAt: str');
|
|
111
|
+
modelBlocks.push(`
|
|
112
|
+
class ${entity.name}Create(BaseModel):
|
|
113
|
+
${createFields.join('\n')}
|
|
114
|
+
|
|
115
|
+
class ${entity.name}Update(BaseModel):
|
|
116
|
+
${updateFields.join('\n')}
|
|
117
|
+
|
|
118
|
+
class ${entity.name}Response(BaseModel):
|
|
119
|
+
${responseFields.join('\n')}
|
|
120
|
+
model_config = ConfigDict(populate_by_name=True)`);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
path: 'server/models.py',
|
|
124
|
+
content: `from typing import Optional
|
|
125
|
+
from pydantic import BaseModel, ConfigDict
|
|
126
|
+
${modelBlocks.join('\n')}
|
|
127
|
+
`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// Database (Data Store)
|
|
132
|
+
// =============================================================================
|
|
133
|
+
function generateDatabase(model, entity) {
|
|
134
|
+
const snakeName = toSnakeCase(entity.pluralName);
|
|
135
|
+
// Collect searchable fields (string + enum types)
|
|
136
|
+
const searchableFields = entity.fields
|
|
137
|
+
.filter((f) => f.type === 'string' || f.type === 'enum')
|
|
138
|
+
.map((f) => `item.get("${f.name}", "")`);
|
|
139
|
+
const searchLogic = searchableFields.length > 0
|
|
140
|
+
? ` q = search.lower()
|
|
141
|
+
result = [
|
|
142
|
+
item for item in result
|
|
143
|
+
if any(
|
|
144
|
+
q in str(v).lower()
|
|
145
|
+
for v in [${searchableFields.join(', ')}]
|
|
146
|
+
if v is not None
|
|
147
|
+
)
|
|
148
|
+
]`
|
|
149
|
+
: ` q = search.lower()
|
|
150
|
+
result = [
|
|
151
|
+
item for item in result
|
|
152
|
+
if any(
|
|
153
|
+
q in str(v).lower()
|
|
154
|
+
for v in item.values()
|
|
155
|
+
if v is not None
|
|
156
|
+
)
|
|
157
|
+
]`;
|
|
158
|
+
const seedData = generateSeedData(model, entity);
|
|
159
|
+
return {
|
|
160
|
+
path: `server/database/${snakeName}.py`,
|
|
161
|
+
content: `"""In-memory data store for ${entity.pluralName}."""
|
|
162
|
+
|
|
163
|
+
from datetime import datetime, timezone
|
|
164
|
+
from typing import Optional
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
${seedData}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class DataStore:
|
|
171
|
+
def __init__(self):
|
|
172
|
+
self.items: list[dict] = [dict(item) for item in SEED_DATA]
|
|
173
|
+
self.next_id: int = len(self.items) + 1
|
|
174
|
+
|
|
175
|
+
def get_all(self) -> list[dict]:
|
|
176
|
+
return self.items
|
|
177
|
+
|
|
178
|
+
def get_by_id(self, item_id: int) -> Optional[dict]:
|
|
179
|
+
for item in self.items:
|
|
180
|
+
if item["id"] == item_id:
|
|
181
|
+
return item
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def query(
|
|
185
|
+
self,
|
|
186
|
+
search: Optional[str] = None,
|
|
187
|
+
filters: Optional[dict[str, str]] = None,
|
|
188
|
+
) -> dict:
|
|
189
|
+
result = list(self.items)
|
|
190
|
+
if search:
|
|
191
|
+
${searchLogic}
|
|
192
|
+
if filters:
|
|
193
|
+
for field, value in filters.items():
|
|
194
|
+
result = [
|
|
195
|
+
item for item in result
|
|
196
|
+
if str(item.get(field, "")) == value
|
|
197
|
+
]
|
|
198
|
+
return {"data": result, "total": len(result)}
|
|
199
|
+
|
|
200
|
+
def create(self, data: dict) -> dict:
|
|
201
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
202
|
+
item = {
|
|
203
|
+
**data,
|
|
204
|
+
"id": self.next_id,
|
|
205
|
+
"createdAt": now,
|
|
206
|
+
"updatedAt": now,
|
|
207
|
+
}
|
|
208
|
+
self.next_id += 1
|
|
209
|
+
self.items.append(item)
|
|
210
|
+
return item
|
|
211
|
+
|
|
212
|
+
def update(self, item_id: int, data: dict) -> Optional[dict]:
|
|
213
|
+
for i, item in enumerate(self.items):
|
|
214
|
+
if item["id"] == item_id:
|
|
215
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
216
|
+
self.items[i] = {**item, **data, "id": item_id, "updatedAt": now}
|
|
217
|
+
return self.items[i]
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
def remove(self, item_id: int) -> bool:
|
|
221
|
+
for i, item in enumerate(self.items):
|
|
222
|
+
if item["id"] == item_id:
|
|
223
|
+
self.items.pop(i)
|
|
224
|
+
return True
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
store = DataStore()
|
|
229
|
+
`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// =============================================================================
|
|
233
|
+
// Router
|
|
234
|
+
// =============================================================================
|
|
235
|
+
function generateRouter(model, entity) {
|
|
236
|
+
const snakeName = toSnakeCase(entity.pluralName);
|
|
237
|
+
const btoRels = belongsToRels(entity);
|
|
238
|
+
const hmRels = hasManyRels(entity);
|
|
239
|
+
// Imports for belongsTo populate
|
|
240
|
+
const populateImports = btoRels
|
|
241
|
+
.map((rel) => {
|
|
242
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
243
|
+
if (!targetEntity)
|
|
244
|
+
return '';
|
|
245
|
+
const targetSnake = toSnakeCase(targetEntity.pluralName);
|
|
246
|
+
return `from server.database.${targetSnake} import store as ${targetSnake}_store`;
|
|
247
|
+
})
|
|
248
|
+
.filter(Boolean)
|
|
249
|
+
.join('\n');
|
|
250
|
+
// Imports for hasMany includes
|
|
251
|
+
const hasManyImports = hmRels
|
|
252
|
+
.map((rel) => {
|
|
253
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
254
|
+
if (!targetEntity)
|
|
255
|
+
return '';
|
|
256
|
+
const targetSnake = toSnakeCase(targetEntity.pluralName);
|
|
257
|
+
return `from server.database.${targetSnake} import store as ${targetSnake}_store`;
|
|
258
|
+
})
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
.join('\n');
|
|
261
|
+
// Deduplicate imports (if same entity appears in both belongsTo and hasMany targets)
|
|
262
|
+
const allImports = [
|
|
263
|
+
...new Set([...populateImports.split('\n'), ...hasManyImports.split('\n')].filter(Boolean)),
|
|
264
|
+
].join('\n');
|
|
265
|
+
// Populate function for belongsTo
|
|
266
|
+
const populateLogic = btoRels.length > 0
|
|
267
|
+
? `
|
|
268
|
+
|
|
269
|
+
def populate(item: dict) -> dict:
|
|
270
|
+
result = dict(item)
|
|
271
|
+
${btoRels
|
|
272
|
+
.map((rel) => {
|
|
273
|
+
const fk = fkFieldName(rel);
|
|
274
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
275
|
+
if (!targetEntity)
|
|
276
|
+
return '';
|
|
277
|
+
const targetSnake = toSnakeCase(targetEntity.pluralName);
|
|
278
|
+
const targetVar = rel.target.charAt(0).toLowerCase() + rel.target.slice(1);
|
|
279
|
+
return ` if result.get("${fk}"):
|
|
280
|
+
result["${targetVar}"] = ${targetSnake}_store.get_by_id(result["${fk}"])`;
|
|
281
|
+
})
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
.join('\n')}
|
|
284
|
+
return result
|
|
285
|
+
`
|
|
286
|
+
: '';
|
|
287
|
+
// GET list with optional populate
|
|
288
|
+
const getListReturn = btoRels.length > 0
|
|
289
|
+
? ' return {"data": [populate(item) for item in result["data"]], "total": result["total"]}'
|
|
290
|
+
: ' return {"data": result["data"], "total": result["total"]}';
|
|
291
|
+
// GET by ID with ?include= for hasMany
|
|
292
|
+
let getByIdBody;
|
|
293
|
+
if (hmRels.length > 0) {
|
|
294
|
+
const includeChecks = hmRels
|
|
295
|
+
.map((rel) => {
|
|
296
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
297
|
+
if (!targetEntity)
|
|
298
|
+
return '';
|
|
299
|
+
const targetVar = toCamelCase(targetEntity.pluralName);
|
|
300
|
+
const targetSnake = toSnakeCase(targetEntity.pluralName);
|
|
301
|
+
const fkRel = targetEntity.relationships.find((r) => r.type === 'belongsTo' && r.target === entity.name) ?? { type: 'belongsTo', target: entity.name };
|
|
302
|
+
const fk = fkFieldName(fkRel);
|
|
303
|
+
return ` if "${targetVar}" in include:
|
|
304
|
+
result["${targetVar}"] = [
|
|
305
|
+
r for r in ${targetSnake}_store.get_all()
|
|
306
|
+
if r.get("${fk}") == item["id"]
|
|
307
|
+
]`;
|
|
308
|
+
})
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
.join('\n');
|
|
311
|
+
const populateExpr = btoRels.length > 0 ? 'populate(item)' : 'dict(item)';
|
|
312
|
+
getByIdBody = ` item = store.get_by_id(item_id)
|
|
313
|
+
if not item:
|
|
314
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
315
|
+
include_param = request.query_params.get("include", "")
|
|
316
|
+
include = [s.strip() for s in include_param.split(",") if s.strip()]
|
|
317
|
+
result = ${populateExpr}
|
|
318
|
+
${includeChecks}
|
|
319
|
+
return result`;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
const getByIdReturn = btoRels.length > 0 ? ' return populate(item)' : ' return item';
|
|
323
|
+
getByIdBody = ` item = store.get_by_id(item_id)
|
|
324
|
+
if not item:
|
|
325
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
326
|
+
${getByIdReturn}`;
|
|
327
|
+
}
|
|
328
|
+
// Determine if we need Request import
|
|
329
|
+
const needsRequest = hmRels.length > 0;
|
|
330
|
+
return {
|
|
331
|
+
path: `server/routers/${snakeName}.py`,
|
|
332
|
+
content: `import re
|
|
333
|
+
from fastapi import APIRouter, HTTPException${needsRequest ? ', Request' : ''}
|
|
334
|
+
from server.models import ${entity.name}Create, ${entity.name}Update
|
|
335
|
+
from server.database.${snakeName} import store
|
|
336
|
+
${allImports ? allImports + '\n' : ''}
|
|
337
|
+
router = APIRouter()
|
|
338
|
+
${populateLogic}
|
|
339
|
+
|
|
340
|
+
@router.get("/")
|
|
341
|
+
def list_items(request: Request):
|
|
342
|
+
search = request.query_params.get("search")
|
|
343
|
+
filters: dict[str, str] = {}
|
|
344
|
+
for key, value in request.query_params.items():
|
|
345
|
+
match = re.match(r"^filter\\[(.+)\\]$", key)
|
|
346
|
+
if match:
|
|
347
|
+
filters[match.group(1)] = value
|
|
348
|
+
result = store.query(search=search, filters=filters if filters else None)
|
|
349
|
+
${getListReturn}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@router.get("/{item_id}")
|
|
353
|
+
def get_item(item_id: int${needsRequest ? ', request: Request' : ''}):
|
|
354
|
+
${getByIdBody}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@router.post("/", status_code=201)
|
|
358
|
+
def create_item(body: ${entity.name}Create):
|
|
359
|
+
return store.create(body.model_dump())
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@router.put("/{item_id}")
|
|
363
|
+
def update_item(item_id: int, body: ${entity.name}Update):
|
|
364
|
+
item = store.update(item_id, {k: v for k, v in body.model_dump().items() if v is not None})
|
|
365
|
+
if not item:
|
|
366
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
367
|
+
return item
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@router.delete("/{item_id}", status_code=204)
|
|
371
|
+
def delete_item(item_id: int):
|
|
372
|
+
success = store.remove(item_id)
|
|
373
|
+
if not success:
|
|
374
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
375
|
+
return None
|
|
376
|
+
`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit — Configuration Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: package.json, vite.config.ts, tailwind.config.js,
|
|
5
|
+
* tsconfig.json, postcss.config.js, requirements.txt
|
|
6
|
+
*/
|
|
7
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
8
|
+
import type { FactoryFile } from '../types.js';
|
|
9
|
+
export declare function generateConfigFiles(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit — Configuration Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: package.json, vite.config.ts, tailwind.config.js,
|
|
5
|
+
* tsconfig.json, postcss.config.js, requirements.txt
|
|
6
|
+
*/
|
|
7
|
+
import { toKebabCase } from '../../model/naming.js';
|
|
8
|
+
import { generateColorShades } from '../shared/color-utils.js';
|
|
9
|
+
export function generateConfigFiles(model) {
|
|
10
|
+
const appSlug = toKebabCase(model.identity.name);
|
|
11
|
+
return [
|
|
12
|
+
generatePackageJson(appSlug),
|
|
13
|
+
generateViteConfig(),
|
|
14
|
+
generateTailwindConfig(model),
|
|
15
|
+
generateTsConfig(),
|
|
16
|
+
generatePostCssConfig(),
|
|
17
|
+
generateRequirementsTxt(),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
function generatePackageJson(appSlug) {
|
|
21
|
+
const pkg = {
|
|
22
|
+
name: appSlug,
|
|
23
|
+
version: '0.1.0',
|
|
24
|
+
private: true,
|
|
25
|
+
type: 'module',
|
|
26
|
+
scripts: {
|
|
27
|
+
dev: 'concurrently "vite" "uvicorn server.main:app --reload --port 8000"',
|
|
28
|
+
build: 'vite build',
|
|
29
|
+
preview: 'vite preview',
|
|
30
|
+
server: 'uvicorn server.main:app --reload --port 8000',
|
|
31
|
+
},
|
|
32
|
+
dependencies: {
|
|
33
|
+
react: '^18.3.1',
|
|
34
|
+
'react-dom': '^18.3.1',
|
|
35
|
+
'react-router-dom': '^6.28.0',
|
|
36
|
+
},
|
|
37
|
+
devDependencies: {
|
|
38
|
+
'@types/react': '^18.3.12',
|
|
39
|
+
'@types/react-dom': '^18.3.1',
|
|
40
|
+
'@vitejs/plugin-react': '^4.3.4',
|
|
41
|
+
autoprefixer: '^10.4.20',
|
|
42
|
+
concurrently: '^9.1.0',
|
|
43
|
+
postcss: '^8.4.49',
|
|
44
|
+
'@tailwindcss/forms': '^0.5.9',
|
|
45
|
+
tailwindcss: '^3.4.15',
|
|
46
|
+
typescript: '^5.6.3',
|
|
47
|
+
vite: '^6.0.0',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return { path: 'package.json', content: JSON.stringify(pkg, null, 2) + '\n' };
|
|
51
|
+
}
|
|
52
|
+
function generateViteConfig() {
|
|
53
|
+
return {
|
|
54
|
+
path: 'vite.config.ts',
|
|
55
|
+
content: `import { defineConfig } from 'vite';
|
|
56
|
+
import react from '@vitejs/plugin-react';
|
|
57
|
+
|
|
58
|
+
export default defineConfig({
|
|
59
|
+
plugins: [react()],
|
|
60
|
+
server: {
|
|
61
|
+
proxy: {
|
|
62
|
+
'/api': 'http://localhost:8000',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function generateTailwindConfig(model) {
|
|
70
|
+
return {
|
|
71
|
+
path: 'tailwind.config.js',
|
|
72
|
+
content: `import forms from '@tailwindcss/forms';
|
|
73
|
+
|
|
74
|
+
/** @type {import('tailwindcss').Config} */
|
|
75
|
+
export default {
|
|
76
|
+
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
|
77
|
+
darkMode: 'class',
|
|
78
|
+
theme: {
|
|
79
|
+
extend: {
|
|
80
|
+
colors: {
|
|
81
|
+
primary: ${generateColorShades(model.theme.primaryColor)},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
plugins: [forms],
|
|
86
|
+
};
|
|
87
|
+
`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function generateTsConfig() {
|
|
91
|
+
const config = {
|
|
92
|
+
compilerOptions: {
|
|
93
|
+
target: 'ES2022',
|
|
94
|
+
module: 'ESNext',
|
|
95
|
+
moduleResolution: 'bundler',
|
|
96
|
+
jsx: 'react-jsx',
|
|
97
|
+
strict: true,
|
|
98
|
+
esModuleInterop: true,
|
|
99
|
+
skipLibCheck: true,
|
|
100
|
+
forceConsistentCasingInFileNames: true,
|
|
101
|
+
resolveJsonModule: true,
|
|
102
|
+
isolatedModules: true,
|
|
103
|
+
noEmit: true,
|
|
104
|
+
},
|
|
105
|
+
include: ['src'],
|
|
106
|
+
};
|
|
107
|
+
return { path: 'tsconfig.json', content: JSON.stringify(config, null, 2) + '\n' };
|
|
108
|
+
}
|
|
109
|
+
function generatePostCssConfig() {
|
|
110
|
+
return {
|
|
111
|
+
path: 'postcss.config.js',
|
|
112
|
+
content: `export default {
|
|
113
|
+
plugins: {
|
|
114
|
+
tailwindcss: {},
|
|
115
|
+
autoprefixer: {},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function generateRequirementsTxt() {
|
|
122
|
+
return {
|
|
123
|
+
path: 'requirements.txt',
|
|
124
|
+
content: `fastapi>=0.115.0
|
|
125
|
+
uvicorn[standard]>=0.32.0
|
|
126
|
+
`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit — Helpers
|
|
3
|
+
*
|
|
4
|
+
* Python-specific helpers plus re-exports from shared helpers.
|
|
5
|
+
*/
|
|
6
|
+
import type { Field } from '../../model/types.js';
|
|
7
|
+
/** Map a model FieldType to a Python type string. */
|
|
8
|
+
export declare function pyType(field: Field): string;
|
|
9
|
+
/** Generate a Python default value string for a field. */
|
|
10
|
+
export declare function pyDefault(field: Field): string;
|
|
11
|
+
/** Convert PascalCase or camelCase to snake_case. */
|
|
12
|
+
export declare function toSnakeCase(name: string): string;
|
|
13
|
+
export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from '../shared/helpers.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit — Helpers
|
|
3
|
+
*
|
|
4
|
+
* Python-specific helpers plus re-exports from shared helpers.
|
|
5
|
+
*/
|
|
6
|
+
/** Map a model FieldType to a Python type string. */
|
|
7
|
+
export function pyType(field) {
|
|
8
|
+
const lowerName = field.name.toLowerCase();
|
|
9
|
+
switch (field.type) {
|
|
10
|
+
case 'string':
|
|
11
|
+
case 'enum':
|
|
12
|
+
case 'date':
|
|
13
|
+
return 'str';
|
|
14
|
+
case 'number':
|
|
15
|
+
if (lowerName.includes('price') ||
|
|
16
|
+
lowerName.includes('cost') ||
|
|
17
|
+
lowerName.includes('amount')) {
|
|
18
|
+
return 'float';
|
|
19
|
+
}
|
|
20
|
+
return 'int';
|
|
21
|
+
case 'boolean':
|
|
22
|
+
return 'bool';
|
|
23
|
+
default:
|
|
24
|
+
return 'str';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Generate a Python default value string for a field. */
|
|
28
|
+
export function pyDefault(field) {
|
|
29
|
+
if (!field.required)
|
|
30
|
+
return 'None';
|
|
31
|
+
switch (field.type) {
|
|
32
|
+
case 'string':
|
|
33
|
+
case 'date':
|
|
34
|
+
return "''";
|
|
35
|
+
case 'enum': {
|
|
36
|
+
const values = field.enumValues ?? [];
|
|
37
|
+
return values.length > 0 ? `'${values[0]}'` : "''";
|
|
38
|
+
}
|
|
39
|
+
case 'number':
|
|
40
|
+
return '0';
|
|
41
|
+
case 'boolean':
|
|
42
|
+
return 'False';
|
|
43
|
+
default:
|
|
44
|
+
return "''";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Convert PascalCase or camelCase to snake_case. */
|
|
48
|
+
export function toSnakeCase(name) {
|
|
49
|
+
return name
|
|
50
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
51
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
|
|
52
|
+
.toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from '../shared/helpers.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit
|
|
3
|
+
*
|
|
4
|
+
* Generates a full-stack MVP: React 18 + Vite + Tailwind + FastAPI (Python).
|
|
5
|
+
* Deterministic: same ApplicationModel -> same output files.
|
|
6
|
+
*
|
|
7
|
+
* Frontend is identical to react-node (same React components, Vite config, Tailwind).
|
|
8
|
+
* Backend is Python FastAPI with in-memory data stores.
|
|
9
|
+
*/
|
|
10
|
+
import type { FactoryToolkit } from '../types.js';
|
|
11
|
+
export declare const reactFastapiToolkit: FactoryToolkit;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit
|
|
3
|
+
*
|
|
4
|
+
* Generates a full-stack MVP: React 18 + Vite + Tailwind + FastAPI (Python).
|
|
5
|
+
* Deterministic: same ApplicationModel -> same output files.
|
|
6
|
+
*
|
|
7
|
+
* Frontend is identical to react-node (same React components, Vite config, Tailwind).
|
|
8
|
+
* Backend is Python FastAPI with in-memory data stores.
|
|
9
|
+
*/
|
|
10
|
+
import { generateConfigFiles } from './config.js';
|
|
11
|
+
import { generateStaticFiles } from './static.js';
|
|
12
|
+
import { generateShellFiles } from '../react-node/shell.js';
|
|
13
|
+
import { generateSharedComponents } from './shared.js';
|
|
14
|
+
import { generateDashboard } from '../react-node/dashboard.js';
|
|
15
|
+
import { generateEntityFiles } from '../react-node/entity.js';
|
|
16
|
+
import { generateRouter } from '../react-node/router.js';
|
|
17
|
+
import { generateApiFiles } from './api.js';
|
|
18
|
+
import { generateTypesFile } from '../react-node/types-gen.js';
|
|
19
|
+
export const reactFastapiToolkit = {
|
|
20
|
+
id: 'react-fastapi',
|
|
21
|
+
name: 'React + FastAPI',
|
|
22
|
+
description: 'React 18 + Vite + Tailwind CSS + FastAPI — full-stack MVP with Python backend',
|
|
23
|
+
requiredSections: ['identity', 'entities', 'layout', 'features', 'theme'],
|
|
24
|
+
generate(model) {
|
|
25
|
+
const warnings = [];
|
|
26
|
+
const allFiles = [];
|
|
27
|
+
// Collect files from all generators
|
|
28
|
+
const generators = [
|
|
29
|
+
generateConfigFiles(model),
|
|
30
|
+
generateStaticFiles(model),
|
|
31
|
+
generateTypesFile(model),
|
|
32
|
+
generateShellFiles(model),
|
|
33
|
+
generateSharedComponents(),
|
|
34
|
+
generateDashboard(model),
|
|
35
|
+
generateEntityFiles(model),
|
|
36
|
+
generateRouter(model),
|
|
37
|
+
generateApiFiles(model),
|
|
38
|
+
];
|
|
39
|
+
for (const files of generators) {
|
|
40
|
+
allFiles.push(...files);
|
|
41
|
+
}
|
|
42
|
+
// Check for potential issues
|
|
43
|
+
if (model.entities.length === 0) {
|
|
44
|
+
warnings.push('No entities defined — generated app will have minimal functionality.');
|
|
45
|
+
}
|
|
46
|
+
if (model.entities.length > 10) {
|
|
47
|
+
warnings.push('Large number of entities — generated sidebar may be crowded.');
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
files: allFiles,
|
|
51
|
+
toolkit: 'react-fastapi',
|
|
52
|
+
warnings,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit — Seed Data Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates realistic mock data in Python dict format.
|
|
5
|
+
* Uses shared generateFieldValue for deterministic values,
|
|
6
|
+
* wraps them in Python list-of-dicts format for FastAPI data stores.
|
|
7
|
+
*/
|
|
8
|
+
import type { ApplicationModel, Entity } from '../../model/types.js';
|
|
9
|
+
import { SEED_COUNT } from '../shared/seed-data.js';
|
|
10
|
+
/** Generate seed data items for a single entity (Python list format). */
|
|
11
|
+
export declare function generateSeedData(model: ApplicationModel, entity: Entity): string;
|
|
12
|
+
export { SEED_COUNT };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit — Seed Data Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates realistic mock data in Python dict format.
|
|
5
|
+
* Uses shared generateFieldValue for deterministic values,
|
|
6
|
+
* wraps them in Python list-of-dicts format for FastAPI data stores.
|
|
7
|
+
*/
|
|
8
|
+
import { belongsToRels, fkFieldName } from './helpers.js';
|
|
9
|
+
import { generateFieldValue, SEED_COUNT } from '../shared/seed-data.js';
|
|
10
|
+
/** Generate seed data items for a single entity (Python list format). */
|
|
11
|
+
export function generateSeedData(model, entity) {
|
|
12
|
+
const rels = belongsToRels(entity);
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push('SEED_DATA = [');
|
|
15
|
+
for (let i = 0; i < SEED_COUNT; i++) {
|
|
16
|
+
lines.push(' {');
|
|
17
|
+
lines.push(` "id": ${String(i + 1)},`);
|
|
18
|
+
for (const field of entity.fields) {
|
|
19
|
+
const jsValue = generateFieldValue(field, i, entity.name);
|
|
20
|
+
const pyValue = jsToPyValue(jsValue, field.type);
|
|
21
|
+
lines.push(` "${field.name}": ${pyValue},`);
|
|
22
|
+
}
|
|
23
|
+
// FK fields from belongsTo relationships
|
|
24
|
+
for (const rel of rels) {
|
|
25
|
+
const fk = fkFieldName(rel);
|
|
26
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
27
|
+
const targetCount = targetEntity ? SEED_COUNT : SEED_COUNT;
|
|
28
|
+
lines.push(` "${fk}": ${String((i % targetCount) + 1)},`);
|
|
29
|
+
}
|
|
30
|
+
// Implicit timestamps (deterministic)
|
|
31
|
+
const createdDate = new Date('2025-06-01T10:00:00Z');
|
|
32
|
+
createdDate.setDate(createdDate.getDate() + i * 2);
|
|
33
|
+
const updatedDate = new Date(createdDate);
|
|
34
|
+
updatedDate.setDate(updatedDate.getDate() + i);
|
|
35
|
+
lines.push(` "createdAt": "${createdDate.toISOString()}",`);
|
|
36
|
+
lines.push(` "updatedAt": "${updatedDate.toISOString()}",`);
|
|
37
|
+
lines.push(' },');
|
|
38
|
+
}
|
|
39
|
+
lines.push(']');
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
}
|
|
42
|
+
/** Convert a JS seed value string to Python format. */
|
|
43
|
+
function jsToPyValue(jsValue, fieldType) {
|
|
44
|
+
if (fieldType === 'boolean') {
|
|
45
|
+
return jsValue === 'true' ? 'True' : 'False';
|
|
46
|
+
}
|
|
47
|
+
// String/enum/date values come wrapped in single quotes — convert to double quotes
|
|
48
|
+
if (jsValue.startsWith("'") && jsValue.endsWith("'")) {
|
|
49
|
+
return `"${jsValue.slice(1, -1)}"`;
|
|
50
|
+
}
|
|
51
|
+
// Numbers pass through unchanged
|
|
52
|
+
return jsValue;
|
|
53
|
+
}
|
|
54
|
+
export { SEED_COUNT };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI 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[];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+FastAPI Toolkit — Static Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: .gitignore, .env.example, README.md, index.html, src/main.tsx, src/index.css
|
|
5
|
+
*/
|
|
6
|
+
export function generateStaticFiles(model) {
|
|
7
|
+
return [
|
|
8
|
+
generateGitignore(),
|
|
9
|
+
generateEnvExample(),
|
|
10
|
+
generateReadme(model),
|
|
11
|
+
generateIndexHtml(model),
|
|
12
|
+
generateMainTsx(),
|
|
13
|
+
generateIndexCss(),
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
function generateGitignore() {
|
|
17
|
+
return {
|
|
18
|
+
path: '.gitignore',
|
|
19
|
+
content: `node_modules
|
|
20
|
+
dist
|
|
21
|
+
.env
|
|
22
|
+
*.local
|
|
23
|
+
__pycache__
|
|
24
|
+
.venv
|
|
25
|
+
*.pyc
|
|
26
|
+
`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function generateEnvExample() {
|
|
30
|
+
return {
|
|
31
|
+
path: '.env.example',
|
|
32
|
+
content: `PORT=8000
|
|
33
|
+
VITE_API_URL=http://localhost:8000
|
|
34
|
+
`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function generateReadme(model) {
|
|
38
|
+
let entitiesSection = '';
|
|
39
|
+
if (model.entities.length > 0) {
|
|
40
|
+
const entityLines = model.entities
|
|
41
|
+
.map((e) => {
|
|
42
|
+
const relCount = e.relationships.length;
|
|
43
|
+
const parts = [`${String(e.fields.length)} fields`];
|
|
44
|
+
if (relCount > 0) {
|
|
45
|
+
parts.push(`${String(relCount)} relationship${relCount > 1 ? 's' : ''}`);
|
|
46
|
+
}
|
|
47
|
+
return `- **${e.name}** (${e.icon}) — ${parts.join(', ')}`;
|
|
48
|
+
})
|
|
49
|
+
.join('\n');
|
|
50
|
+
entitiesSection = `\n## Entities\n\n${entityLines}\n`;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
path: 'README.md',
|
|
54
|
+
content: `# ${model.identity.name}
|
|
55
|
+
|
|
56
|
+
${model.identity.description}
|
|
57
|
+
${entitiesSection}
|
|
58
|
+
## Getting Started
|
|
59
|
+
|
|
60
|
+
\`\`\`bash
|
|
61
|
+
npm install
|
|
62
|
+
pip install -r requirements.txt
|
|
63
|
+
npm run dev
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
This starts both the Vite dev server and the FastAPI server.
|
|
67
|
+
|
|
68
|
+
- **Frontend:** http://localhost:5173
|
|
69
|
+
- **API:** http://localhost:8000
|
|
70
|
+
|
|
71
|
+
## Tech Stack
|
|
72
|
+
|
|
73
|
+
- React 18 + TypeScript
|
|
74
|
+
- Vite
|
|
75
|
+
- Tailwind CSS
|
|
76
|
+
- FastAPI (Python)
|
|
77
|
+
- React Router v6
|
|
78
|
+
`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function generateIndexHtml(model) {
|
|
82
|
+
return {
|
|
83
|
+
path: 'index.html',
|
|
84
|
+
content: `<!doctype html>
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<head>
|
|
87
|
+
<meta charset="UTF-8" />
|
|
88
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
89
|
+
<title>${model.identity.name}</title>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div id="root"></div>
|
|
93
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function generateMainTsx() {
|
|
100
|
+
return {
|
|
101
|
+
path: 'src/main.tsx',
|
|
102
|
+
content: `import React from 'react';
|
|
103
|
+
import ReactDOM from 'react-dom/client';
|
|
104
|
+
import App from './App';
|
|
105
|
+
import './index.css';
|
|
106
|
+
|
|
107
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
108
|
+
<React.StrictMode>
|
|
109
|
+
<App />
|
|
110
|
+
</React.StrictMode>,
|
|
111
|
+
);
|
|
112
|
+
`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function generateIndexCss() {
|
|
116
|
+
return {
|
|
117
|
+
path: 'src/index.css',
|
|
118
|
+
content: `@tailwind base;
|
|
119
|
+
@tailwind components;
|
|
120
|
+
@tailwind utilities;
|
|
121
|
+
`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Landing Page Toolkit — Configuration Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: package.json, tailwind.config.js, postcss.config.js
|
|
5
|
+
*/
|
|
6
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
7
|
+
import type { FactoryFile } from '../types.js';
|
|
8
|
+
export declare function generateConfigFiles(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Landing Page Toolkit — Configuration Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: package.json, tailwind.config.js, postcss.config.js
|
|
5
|
+
*/
|
|
6
|
+
import { toKebabCase } from '../../model/naming.js';
|
|
7
|
+
import { generateColorShades } from '../shared/color-utils.js';
|
|
8
|
+
export function generateConfigFiles(model) {
|
|
9
|
+
const appSlug = toKebabCase(model.identity.name);
|
|
10
|
+
return [generatePackageJson(appSlug), generateTailwindConfig(model), generatePostCssConfig()];
|
|
11
|
+
}
|
|
12
|
+
function generatePackageJson(appSlug) {
|
|
13
|
+
const pkg = {
|
|
14
|
+
name: appSlug,
|
|
15
|
+
version: '0.1.0',
|
|
16
|
+
private: true,
|
|
17
|
+
scripts: {
|
|
18
|
+
dev: 'concurrently "npx tailwindcss -i ./src/input.css -o ./dist/style.css --watch" "npx serve . -l 3000"',
|
|
19
|
+
build: 'npx tailwindcss -i ./src/input.css -o ./dist/style.css --minify',
|
|
20
|
+
preview: 'npx serve . -l 3000',
|
|
21
|
+
},
|
|
22
|
+
devDependencies: {
|
|
23
|
+
autoprefixer: '^10.4.20',
|
|
24
|
+
concurrently: '^9.1.0',
|
|
25
|
+
postcss: '^8.4.49',
|
|
26
|
+
serve: '^14.2.4',
|
|
27
|
+
tailwindcss: '^3.4.15',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
return { path: 'package.json', content: JSON.stringify(pkg, null, 2) + '\n' };
|
|
31
|
+
}
|
|
32
|
+
function generateTailwindConfig(model) {
|
|
33
|
+
const darkLine = model.features.darkMode ? ` darkMode: 'class',\n` : '';
|
|
34
|
+
const shades = generateColorShades(model.theme.primaryColor);
|
|
35
|
+
return {
|
|
36
|
+
path: 'tailwind.config.js',
|
|
37
|
+
content: `/** @type {import('tailwindcss').Config} */
|
|
38
|
+
export default {
|
|
39
|
+
${darkLine} content: ['./*.html'],
|
|
40
|
+
theme: {
|
|
41
|
+
extend: {
|
|
42
|
+
colors: {
|
|
43
|
+
primary: ${shades},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
plugins: [],
|
|
48
|
+
};
|
|
49
|
+
`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function generatePostCssConfig() {
|
|
53
|
+
return {
|
|
54
|
+
path: 'postcss.config.js',
|
|
55
|
+
content: `export default {
|
|
56
|
+
plugins: {
|
|
57
|
+
tailwindcss: {},
|
|
58
|
+
autoprefixer: {},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Landing Page Toolkit
|
|
3
|
+
*
|
|
4
|
+
* Generates a responsive landing page: HTML + Tailwind CSS + minimal JavaScript.
|
|
5
|
+
* Deterministic: same ApplicationModel → same output files.
|
|
6
|
+
*/
|
|
7
|
+
import type { FactoryToolkit } from '../types.js';
|
|
8
|
+
export declare const staticLandingToolkit: FactoryToolkit;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Landing Page Toolkit
|
|
3
|
+
*
|
|
4
|
+
* Generates a responsive landing page: HTML + Tailwind CSS + minimal JavaScript.
|
|
5
|
+
* Deterministic: same ApplicationModel → same output files.
|
|
6
|
+
*/
|
|
7
|
+
import { generateConfigFiles } from './config.js';
|
|
8
|
+
import { generateStaticFiles } from './static.js';
|
|
9
|
+
import { generatePages } from './pages.js';
|
|
10
|
+
export const staticLandingToolkit = {
|
|
11
|
+
id: 'static-landing',
|
|
12
|
+
name: 'Static Landing Page',
|
|
13
|
+
description: 'HTML + Tailwind CSS — responsive marketing/landing page with minimal JavaScript',
|
|
14
|
+
requiredSections: ['identity', 'theme'],
|
|
15
|
+
generate(model) {
|
|
16
|
+
const warnings = [];
|
|
17
|
+
const allFiles = [];
|
|
18
|
+
const generators = [
|
|
19
|
+
generateConfigFiles(model),
|
|
20
|
+
generateStaticFiles(model),
|
|
21
|
+
generatePages(model),
|
|
22
|
+
];
|
|
23
|
+
for (const files of generators) {
|
|
24
|
+
allFiles.push(...files);
|
|
25
|
+
}
|
|
26
|
+
if (model.entities.length === 0) {
|
|
27
|
+
warnings.push('No entities defined — features section will use placeholder content.');
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
files: allFiles,
|
|
31
|
+
toolkit: 'static-landing',
|
|
32
|
+
warnings,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Landing Page Toolkit — Page Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: index.html, 404.html
|
|
5
|
+
*/
|
|
6
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
7
|
+
import type { FactoryFile } from '../types.js';
|
|
8
|
+
export declare function generatePages(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Landing Page Toolkit — Page Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: index.html, 404.html
|
|
5
|
+
*/
|
|
6
|
+
export function generatePages(model) {
|
|
7
|
+
return [generateIndexHtml(model), generate404Html(model)];
|
|
8
|
+
}
|
|
9
|
+
function generateFeatureCards(model) {
|
|
10
|
+
if (model.entities.length > 0) {
|
|
11
|
+
return model.entities
|
|
12
|
+
.map((entity) => ` <div class="bg-white p-6 shadow-sm dark:bg-gray-800">
|
|
13
|
+
<div class="mb-4 text-4xl">${entity.icon}</div>
|
|
14
|
+
<h3 class="mb-2 text-xl font-bold text-gray-900 dark:text-white">${entity.name}</h3>
|
|
15
|
+
<p class="text-gray-600 dark:text-gray-300">${entity.description ?? `Manage your ${entity.pluralName.toLowerCase()} efficiently.`}</p>
|
|
16
|
+
</div>`)
|
|
17
|
+
.join('\n');
|
|
18
|
+
}
|
|
19
|
+
const placeholders = [
|
|
20
|
+
{
|
|
21
|
+
icon: '⚡',
|
|
22
|
+
title: 'Fast & Reliable',
|
|
23
|
+
desc: 'Built for speed with optimized performance out of the box.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
icon: '🔒',
|
|
27
|
+
title: 'Secure by Default',
|
|
28
|
+
desc: 'Security best practices baked in from the start.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
icon: '🎯',
|
|
32
|
+
title: 'Easy to Use',
|
|
33
|
+
desc: 'Intuitive interface designed for the best user experience.',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
return placeholders
|
|
37
|
+
.map((p) => ` <div class="bg-white p-6 shadow-sm dark:bg-gray-800">
|
|
38
|
+
<div class="mb-4 text-4xl">${p.icon}</div>
|
|
39
|
+
<h3 class="mb-2 text-xl font-bold text-gray-900 dark:text-white">${p.title}</h3>
|
|
40
|
+
<p class="text-gray-600 dark:text-gray-300">${p.desc}</p>
|
|
41
|
+
</div>`)
|
|
42
|
+
.join('\n');
|
|
43
|
+
}
|
|
44
|
+
function generateDarkModeToggle() {
|
|
45
|
+
return `
|
|
46
|
+
<button id="dark-toggle" class="text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white">
|
|
47
|
+
<span id="dark-icon">🌙</span>
|
|
48
|
+
</button>`;
|
|
49
|
+
}
|
|
50
|
+
function generateDarkModeScript() {
|
|
51
|
+
return `
|
|
52
|
+
// Dark mode toggle
|
|
53
|
+
const toggle = document.getElementById('dark-toggle');
|
|
54
|
+
const icon = document.getElementById('dark-icon');
|
|
55
|
+
if (localStorage.getItem('theme') === 'dark') {
|
|
56
|
+
document.documentElement.classList.add('dark');
|
|
57
|
+
icon.textContent = '☀️';
|
|
58
|
+
}
|
|
59
|
+
toggle.addEventListener('click', () => {
|
|
60
|
+
document.documentElement.classList.toggle('dark');
|
|
61
|
+
const isDark = document.documentElement.classList.contains('dark');
|
|
62
|
+
icon.textContent = isDark ? '☀️' : '🌙';
|
|
63
|
+
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
64
|
+
});`;
|
|
65
|
+
}
|
|
66
|
+
function generateCtaSection(model) {
|
|
67
|
+
return `
|
|
68
|
+
<!-- CTA -->
|
|
69
|
+
<section id="cta" class="bg-primary px-6 py-20 text-center text-white">
|
|
70
|
+
<div class="mx-auto max-w-2xl">
|
|
71
|
+
<h2 class="mb-4 text-3xl font-bold">Ready to get started?</h2>
|
|
72
|
+
<p class="mb-8 text-lg opacity-90">Join us and experience ${model.identity.name} today.</p>
|
|
73
|
+
<form class="mx-auto flex max-w-md gap-2">
|
|
74
|
+
<input
|
|
75
|
+
type="email"
|
|
76
|
+
placeholder="Enter your email"
|
|
77
|
+
class="flex-1 px-4 py-3 text-gray-900 placeholder-gray-500 focus:outline-none"
|
|
78
|
+
/>
|
|
79
|
+
<button type="submit" class="bg-gray-900 px-6 py-3 font-semibold text-white transition-colors hover:bg-gray-800">
|
|
80
|
+
Sign Up
|
|
81
|
+
</button>
|
|
82
|
+
</form>
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
function generateIndexHtml(model) {
|
|
88
|
+
const { name, description } = model.identity;
|
|
89
|
+
const hasDarkMode = model.features.darkMode;
|
|
90
|
+
const hasAuth = model.features.auth;
|
|
91
|
+
const darkBg = hasDarkMode ? ' dark:bg-gray-900' : '';
|
|
92
|
+
const darkText = hasDarkMode ? ' dark:text-white' : '';
|
|
93
|
+
const darkNavBg = hasDarkMode ? ' dark:bg-gray-800' : '';
|
|
94
|
+
const navSignUp = hasAuth
|
|
95
|
+
? `\n <a href="#cta" class="bg-primary px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-primary-700">Sign Up</a>`
|
|
96
|
+
: '';
|
|
97
|
+
const darkToggle = hasDarkMode ? generateDarkModeToggle() : '';
|
|
98
|
+
const ctaSection = hasAuth ? generateCtaSection(model) : '';
|
|
99
|
+
const featureCards = generateFeatureCards(model);
|
|
100
|
+
const scriptParts = [];
|
|
101
|
+
if (hasDarkMode) {
|
|
102
|
+
scriptParts.push(generateDarkModeScript());
|
|
103
|
+
}
|
|
104
|
+
scriptParts.push(`
|
|
105
|
+
// Smooth scroll for anchor links
|
|
106
|
+
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
107
|
+
anchor.addEventListener('click', (e) => {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
const target = document.querySelector(anchor.getAttribute('href'));
|
|
110
|
+
if (target) target.scrollIntoView({ behavior: 'smooth' });
|
|
111
|
+
});
|
|
112
|
+
});`);
|
|
113
|
+
const scriptTag = `\n <script>${scriptParts.join('\n')}\n </script>`;
|
|
114
|
+
return {
|
|
115
|
+
path: 'index.html',
|
|
116
|
+
content: `<!doctype html>
|
|
117
|
+
<html lang="en">
|
|
118
|
+
<head>
|
|
119
|
+
<meta charset="UTF-8" />
|
|
120
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
121
|
+
<meta name="description" content="${description}" />
|
|
122
|
+
<title>${name}</title>
|
|
123
|
+
<link rel="stylesheet" href="./dist/style.css" />
|
|
124
|
+
</head>
|
|
125
|
+
<body class="bg-white text-gray-900${darkBg}${darkText}">
|
|
126
|
+
<!-- Nav -->
|
|
127
|
+
<nav class="sticky top-0 z-50 bg-white px-6 py-4 shadow-sm${darkNavBg}">
|
|
128
|
+
<div class="mx-auto flex max-w-6xl items-center justify-between">
|
|
129
|
+
<a href="#" class="text-xl font-bold text-primary">${name}</a>
|
|
130
|
+
<div class="flex items-center gap-4">
|
|
131
|
+
<a href="#features" class="text-sm text-gray-600 hover:text-gray-900${hasDarkMode ? ' dark:text-gray-300 dark:hover:text-white' : ''}"
|
|
132
|
+
>Features</a
|
|
133
|
+
>${darkToggle}${navSignUp}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</nav>
|
|
137
|
+
|
|
138
|
+
<!-- Hero -->
|
|
139
|
+
<section id="hero" class="px-6 py-24 text-center">
|
|
140
|
+
<div class="mx-auto max-w-3xl">
|
|
141
|
+
<h1 class="mb-6 text-5xl font-bold tracking-tight">${name}</h1>
|
|
142
|
+
<p class="mb-8 text-xl text-gray-600${hasDarkMode ? ' dark:text-gray-300' : ''}">${description}</p>
|
|
143
|
+
<a href="#features" class="btn-primary">Get Started</a>
|
|
144
|
+
</div>
|
|
145
|
+
</section>
|
|
146
|
+
|
|
147
|
+
<!-- Features -->
|
|
148
|
+
<section id="features" class="bg-gray-50 px-6 py-20${hasDarkMode ? ' dark:bg-gray-800/50' : ''}">
|
|
149
|
+
<div class="mx-auto max-w-6xl">
|
|
150
|
+
<h2 class="mb-12 text-center text-3xl font-bold">Features</h2>
|
|
151
|
+
<div class="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
152
|
+
${featureCards}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</section>
|
|
156
|
+
${ctaSection}
|
|
157
|
+
<!-- Footer -->
|
|
158
|
+
<footer class="border-t border-gray-200 px-6 py-8${hasDarkMode ? ' dark:border-gray-700' : ''}">
|
|
159
|
+
<div class="mx-auto max-w-6xl text-center text-sm text-gray-500${hasDarkMode ? ' dark:text-gray-400' : ''}">
|
|
160
|
+
© ${String(new Date().getFullYear())} ${name}. All rights reserved.
|
|
161
|
+
</div>
|
|
162
|
+
</footer>
|
|
163
|
+
${scriptTag}
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
|
+
`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function generate404Html(model) {
|
|
170
|
+
const { name } = model.identity;
|
|
171
|
+
return {
|
|
172
|
+
path: '404.html',
|
|
173
|
+
content: `<!doctype html>
|
|
174
|
+
<html lang="en">
|
|
175
|
+
<head>
|
|
176
|
+
<meta charset="UTF-8" />
|
|
177
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
178
|
+
<title>Page Not Found — ${name}</title>
|
|
179
|
+
<link rel="stylesheet" href="./dist/style.css" />
|
|
180
|
+
</head>
|
|
181
|
+
<body class="flex min-h-screen items-center justify-center bg-white text-gray-900">
|
|
182
|
+
<div class="text-center">
|
|
183
|
+
<h1 class="mb-4 text-6xl font-bold text-primary">404</h1>
|
|
184
|
+
<p class="mb-8 text-xl text-gray-600">Page not found</p>
|
|
185
|
+
<a href="/" class="btn-primary">Go back home</a>
|
|
186
|
+
</div>
|
|
187
|
+
</body>
|
|
188
|
+
</html>
|
|
189
|
+
`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Landing Page Toolkit — Static Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: .gitignore, README.md, src/input.css, dist/.gitkeep
|
|
5
|
+
*/
|
|
6
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
7
|
+
import type { FactoryFile } from '../types.js';
|
|
8
|
+
export declare function generateStaticFiles(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Landing Page Toolkit — Static Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: .gitignore, README.md, src/input.css, dist/.gitkeep
|
|
5
|
+
*/
|
|
6
|
+
export function generateStaticFiles(model) {
|
|
7
|
+
return [generateGitignore(), generateReadme(model), generateInputCss(), generateDistGitkeep()];
|
|
8
|
+
}
|
|
9
|
+
function generateGitignore() {
|
|
10
|
+
return {
|
|
11
|
+
path: '.gitignore',
|
|
12
|
+
content: `node_modules
|
|
13
|
+
dist/style.css
|
|
14
|
+
`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function generateReadme(model) {
|
|
18
|
+
return {
|
|
19
|
+
path: 'README.md',
|
|
20
|
+
content: `# ${model.identity.name}
|
|
21
|
+
|
|
22
|
+
${model.identity.description}
|
|
23
|
+
|
|
24
|
+
## Getting Started
|
|
25
|
+
|
|
26
|
+
\`\`\`bash
|
|
27
|
+
npm install
|
|
28
|
+
npm run dev
|
|
29
|
+
\`\`\`
|
|
30
|
+
|
|
31
|
+
## Build
|
|
32
|
+
|
|
33
|
+
\`\`\`bash
|
|
34
|
+
npm run build
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
## Tech Stack
|
|
38
|
+
|
|
39
|
+
- HTML
|
|
40
|
+
- Tailwind CSS
|
|
41
|
+
- PostCSS + Autoprefixer
|
|
42
|
+
`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function generateInputCss() {
|
|
46
|
+
return {
|
|
47
|
+
path: 'src/input.css',
|
|
48
|
+
content: `@tailwind base;
|
|
49
|
+
@tailwind components;
|
|
50
|
+
@tailwind utilities;
|
|
51
|
+
|
|
52
|
+
@layer components {
|
|
53
|
+
.btn-primary {
|
|
54
|
+
@apply inline-block rounded-none bg-primary px-6 py-3 font-semibold text-white transition-colors hover:bg-primary-700;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function generateDistGitkeep() {
|
|
61
|
+
return {
|
|
62
|
+
path: 'dist/.gitkeep',
|
|
63
|
+
content: '',
|
|
64
|
+
};
|
|
65
|
+
}
|