@compilr-dev/factory 0.1.10 → 0.1.12
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/list-toolkits-tool.js +2 -1
- package/dist/factory/registry.js +2 -0
- package/dist/factory/scaffold-tool.js +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/model/tools.js +6 -14
- package/dist/toolkits/react-go/api.d.ts +12 -0
- package/dist/toolkits/react-go/api.js +523 -0
- package/dist/toolkits/react-go/config.d.ts +9 -0
- package/dist/toolkits/react-go/config.js +129 -0
- package/dist/toolkits/react-go/helpers.d.ts +13 -0
- package/dist/toolkits/react-go/helpers.js +49 -0
- package/dist/toolkits/react-go/index.d.ts +11 -0
- package/dist/toolkits/react-go/index.js +55 -0
- package/dist/toolkits/react-go/seed.d.ts +12 -0
- package/dist/toolkits/react-go/seed.js +55 -0
- package/dist/toolkits/react-go/shared.d.ts +6 -0
- package/dist/toolkits/react-go/shared.js +6 -0
- package/dist/toolkits/react-go/static.d.ts +8 -0
- package/dist/toolkits/react-go/static.js +122 -0
- package/package.json +1 -1
|
@@ -7,7 +7,8 @@ import { defineTool, createSuccessResult, createErrorResult } from '@compilr-dev
|
|
|
7
7
|
export function createListToolkitsTool(registry) {
|
|
8
8
|
return defineTool({
|
|
9
9
|
name: 'factory_list_toolkits',
|
|
10
|
-
|
|
10
|
+
// Original: 'List all available factory toolkits. Each toolkit generates a different type of application (e.g. React+Node, Next.js, etc.).'
|
|
11
|
+
description: 'List all available factory toolkits.',
|
|
11
12
|
inputSchema: {
|
|
12
13
|
type: 'object',
|
|
13
14
|
properties: {},
|
package/dist/factory/registry.js
CHANGED
|
@@ -28,7 +28,9 @@ import { reactNodeToolkit } from '../toolkits/react-node/index.js';
|
|
|
28
28
|
import { nextPrismaToolkit } from '../toolkits/next-prisma/index.js';
|
|
29
29
|
import { staticLandingToolkit } from '../toolkits/static-landing/index.js';
|
|
30
30
|
import { reactFastapiToolkit } from '../toolkits/react-fastapi/index.js';
|
|
31
|
+
import { reactGoToolkit } from '../toolkits/react-go/index.js';
|
|
31
32
|
defaultRegistry.register(reactNodeToolkit);
|
|
32
33
|
defaultRegistry.register(nextPrismaToolkit);
|
|
33
34
|
defaultRegistry.register(staticLandingToolkit);
|
|
34
35
|
defaultRegistry.register(reactFastapiToolkit);
|
|
36
|
+
defaultRegistry.register(reactGoToolkit);
|
|
@@ -11,13 +11,15 @@ import { writeFactoryFiles } from './file-writer.js';
|
|
|
11
11
|
export function createScaffoldTool(config) {
|
|
12
12
|
return defineTool({
|
|
13
13
|
name: 'factory_scaffold',
|
|
14
|
-
|
|
14
|
+
// Original: 'Generate a full application from the Application Model. Reads the model, validates it, runs the toolkit generators, and writes files to disk. Use dry_run: true to preview without writing.'
|
|
15
|
+
description: 'Generate a full application from the Application Model and write files to disk.',
|
|
15
16
|
inputSchema: {
|
|
16
17
|
type: 'object',
|
|
17
18
|
properties: {
|
|
18
19
|
dry_run: {
|
|
19
20
|
type: 'boolean',
|
|
20
|
-
description: 'Preview generated files without writing to disk.
|
|
21
|
+
description: 'Preview generated files without writing to disk.',
|
|
22
|
+
default: false,
|
|
21
23
|
},
|
|
22
24
|
output_dir: {
|
|
23
25
|
type: 'string',
|
package/dist/index.d.ts
CHANGED
|
@@ -22,4 +22,5 @@ export { reactNodeToolkit } from './toolkits/react-node/index.js';
|
|
|
22
22
|
export { nextPrismaToolkit } from './toolkits/next-prisma/index.js';
|
|
23
23
|
export { staticLandingToolkit } from './toolkits/static-landing/index.js';
|
|
24
24
|
export { reactFastapiToolkit } from './toolkits/react-fastapi/index.js';
|
|
25
|
+
export { reactGoToolkit } from './toolkits/react-go/index.js';
|
|
25
26
|
export { factoryScaffoldSkill, factorySkills } from './factory/skill.js';
|
package/dist/index.js
CHANGED
|
@@ -27,5 +27,7 @@ export { nextPrismaToolkit } from './toolkits/next-prisma/index.js';
|
|
|
27
27
|
export { staticLandingToolkit } from './toolkits/static-landing/index.js';
|
|
28
28
|
// React + FastAPI Toolkit
|
|
29
29
|
export { reactFastapiToolkit } from './toolkits/react-fastapi/index.js';
|
|
30
|
+
// React + Go Toolkit
|
|
31
|
+
export { reactGoToolkit } from './toolkits/react-go/index.js';
|
|
30
32
|
// Factory skill (Phase 5)
|
|
31
33
|
export { factoryScaffoldSkill, factorySkills } from './factory/skill.js';
|
package/dist/model/tools.js
CHANGED
|
@@ -45,7 +45,8 @@ function createSummary(model) {
|
|
|
45
45
|
function createAppModelGetTool(config) {
|
|
46
46
|
return defineTool({
|
|
47
47
|
name: 'app_model_get',
|
|
48
|
-
|
|
48
|
+
// Original: 'Read the Application Model for the current project. Supports scoped reads: no params = full model, scope="summary" for overview, scope="identity"/"features"/"layout"/"theme"/"techStack" for sections, entity="Name" for a single entity.'
|
|
49
|
+
description: 'Read the Application Model. Supports scoped reads by section or entity name.',
|
|
49
50
|
inputSchema: {
|
|
50
51
|
type: 'object',
|
|
51
52
|
properties: {
|
|
@@ -112,18 +113,8 @@ function createAppModelGetTool(config) {
|
|
|
112
113
|
function createAppModelUpdateTool(config) {
|
|
113
114
|
return defineTool({
|
|
114
115
|
name: 'app_model_update',
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
EXAMPLES:
|
|
118
|
-
- updateIdentity: { "op": "updateIdentity", "updates": { "name": "My App", "description": "A task manager" } }
|
|
119
|
-
- updateTechStack: { "op": "updateTechStack", "updates": { "toolkit": "react-node" } }
|
|
120
|
-
- addEntity: { "op": "addEntity", "entity": { "name": "Task", "pluralName": "Tasks", "icon": "📋", "fields": [{ "name": "title", "label": "Title", "type": "string", "required": true }], "views": ["list", "detail", "card"], "relationships": [] } }
|
|
121
|
-
- addField: { "op": "addField", "entity": "Task", "field": { "name": "dueDate", "label": "Due Date", "type": "date", "required": false } }
|
|
122
|
-
- addRelationship: { "op": "addRelationship", "entity": "Task", "relationship": { "type": "belongsTo", "target": "Project" } }
|
|
123
|
-
- updateFeatures: { "op": "updateFeatures", "updates": { "dashboard": true, "darkMode": true } }
|
|
124
|
-
- updateTheme: { "op": "updateTheme", "updates": { "primaryColor": "#1976D2" } }
|
|
125
|
-
- updateLayout: { "op": "updateLayout", "updates": { "shell": "sidebar-header" } }
|
|
126
|
-
- removeEntity: { "op": "removeEntity", "entity": "Task", "force": true }`,
|
|
116
|
+
// Original: long description with 9 inline JSON examples (~1200 chars)
|
|
117
|
+
description: 'Apply a semantic operation to the Application Model. Auto-creates if none exists.',
|
|
127
118
|
inputSchema: {
|
|
128
119
|
type: 'object',
|
|
129
120
|
properties: {
|
|
@@ -402,7 +393,8 @@ function buildOperation(input) {
|
|
|
402
393
|
function createAppModelValidateTool(config) {
|
|
403
394
|
return defineTool({
|
|
404
395
|
name: 'app_model_validate',
|
|
405
|
-
|
|
396
|
+
// Original: 'Validate the Application Model for the current project. Returns validation errors or confirms the model is valid.'
|
|
397
|
+
description: 'Validate the Application Model. Returns errors or confirms validity.',
|
|
406
398
|
inputSchema: {
|
|
407
399
|
type: 'object',
|
|
408
400
|
properties: {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — API Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: server/main.go, server/models.go,
|
|
5
|
+
* server/data/{entity}.go, server/handlers/{entity}.go
|
|
6
|
+
*
|
|
7
|
+
* All files use `package main` so `go run ./server` compiles everything.
|
|
8
|
+
* Go 1.22+ net/http stdlib with method-based routing.
|
|
9
|
+
*/
|
|
10
|
+
import type { ApplicationModel } from '../../model/types.js';
|
|
11
|
+
import type { FactoryFile } from '../types.js';
|
|
12
|
+
export declare function generateApiFiles(model: ApplicationModel): FactoryFile[];
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — API Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: server/main.go, server/models.go,
|
|
5
|
+
* server/data/{entity}.go, server/handlers/{entity}.go
|
|
6
|
+
*
|
|
7
|
+
* All files use `package main` so `go run ./server` compiles everything.
|
|
8
|
+
* Go 1.22+ net/http stdlib with method-based routing.
|
|
9
|
+
*/
|
|
10
|
+
import { toCamelCase, toKebabCase, toPascalCase } from '../../model/naming.js';
|
|
11
|
+
import { goType, fkFieldName, belongsToRels, hasManyRels } from './helpers.js';
|
|
12
|
+
import { toSnakeCase } from './helpers.js';
|
|
13
|
+
import { generateSeedData } from './seed.js';
|
|
14
|
+
export function generateApiFiles(model) {
|
|
15
|
+
return [
|
|
16
|
+
generateServerMain(model),
|
|
17
|
+
generateModels(model),
|
|
18
|
+
...model.entities.flatMap((entity) => [
|
|
19
|
+
generateDataStore(model, entity),
|
|
20
|
+
generateHandlers(model, entity),
|
|
21
|
+
]),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Server Entry Point — server/main.go
|
|
26
|
+
// =============================================================================
|
|
27
|
+
function generateServerMain(model) {
|
|
28
|
+
const handlerRegistrations = model.entities
|
|
29
|
+
.map((e) => `\tRegister${e.name}Handlers(mux)`)
|
|
30
|
+
.join('\n');
|
|
31
|
+
return {
|
|
32
|
+
path: 'server/main.go',
|
|
33
|
+
content: `package main
|
|
34
|
+
|
|
35
|
+
import (
|
|
36
|
+
\t"fmt"
|
|
37
|
+
\t"log"
|
|
38
|
+
\t"net/http"
|
|
39
|
+
\t"os"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
func corsMiddleware(next http.Handler) http.Handler {
|
|
43
|
+
\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
44
|
+
\t\tw.Header().Set("Access-Control-Allow-Origin", "*")
|
|
45
|
+
\t\tw.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
46
|
+
\t\tw.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
47
|
+
\t\tif r.Method == http.MethodOptions {
|
|
48
|
+
\t\t\tw.WriteHeader(http.StatusNoContent)
|
|
49
|
+
\t\t\treturn
|
|
50
|
+
\t\t}
|
|
51
|
+
\t\tnext.ServeHTTP(w, r)
|
|
52
|
+
\t})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func main() {
|
|
56
|
+
\tmux := http.NewServeMux()
|
|
57
|
+
|
|
58
|
+
${handlerRegistrations}
|
|
59
|
+
|
|
60
|
+
\tport := os.Getenv("PORT")
|
|
61
|
+
\tif port == "" {
|
|
62
|
+
\t\tport = "8080"
|
|
63
|
+
\t}
|
|
64
|
+
|
|
65
|
+
\tfmt.Printf("Server listening on :%s\\n", port)
|
|
66
|
+
\tlog.Fatal(http.ListenAndServe(":"+port, corsMiddleware(mux)))
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Models — server/models.go
|
|
73
|
+
// =============================================================================
|
|
74
|
+
function generateModels(model) {
|
|
75
|
+
const modelBlocks = [];
|
|
76
|
+
for (const entity of model.entities) {
|
|
77
|
+
const btoRels = belongsToRels(entity);
|
|
78
|
+
// Main struct (with json tags)
|
|
79
|
+
const structFields = [];
|
|
80
|
+
structFields.push(`\tID int \`json:"id"\``);
|
|
81
|
+
for (const field of entity.fields) {
|
|
82
|
+
const goT = goType(field);
|
|
83
|
+
const jsonTag = field.name;
|
|
84
|
+
const goFieldName = toPascalCase(field.name);
|
|
85
|
+
structFields.push(`\t${goFieldName} ${goT} \`json:"${jsonTag}"\``);
|
|
86
|
+
}
|
|
87
|
+
for (const rel of btoRels) {
|
|
88
|
+
const fk = fkFieldName(rel);
|
|
89
|
+
const goFieldName = toPascalCase(fk);
|
|
90
|
+
structFields.push(`\t${goFieldName} int \`json:"${fk}"\``);
|
|
91
|
+
}
|
|
92
|
+
structFields.push(`\tCreatedAt string \`json:"createdAt"\``);
|
|
93
|
+
structFields.push(`\tUpdatedAt string \`json:"updatedAt"\``);
|
|
94
|
+
// Input struct (for create/update — pointer fields for optional update detection)
|
|
95
|
+
const inputFields = [];
|
|
96
|
+
for (const field of entity.fields) {
|
|
97
|
+
const goT = goType(field);
|
|
98
|
+
const jsonTag = field.name;
|
|
99
|
+
const goFieldName = toPascalCase(field.name);
|
|
100
|
+
inputFields.push(`\t${goFieldName} *${goT} \`json:"${jsonTag}"\``);
|
|
101
|
+
}
|
|
102
|
+
for (const rel of btoRels) {
|
|
103
|
+
const fk = fkFieldName(rel);
|
|
104
|
+
const goFieldName = toPascalCase(fk);
|
|
105
|
+
inputFields.push(`\t${goFieldName} *int \`json:"${fk}"\``);
|
|
106
|
+
}
|
|
107
|
+
modelBlocks.push(`type ${entity.name} struct {
|
|
108
|
+
${structFields.join('\n')}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type ${entity.name}Input struct {
|
|
112
|
+
${inputFields.join('\n')}
|
|
113
|
+
}`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
path: 'server/models.go',
|
|
117
|
+
content: `package main
|
|
118
|
+
${modelBlocks.length > 0 ? '\n' + modelBlocks.join('\n\n') + '\n' : ''}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// Data Store — server/data/{entity}.go
|
|
123
|
+
// =============================================================================
|
|
124
|
+
function generateDataStore(model, entity) {
|
|
125
|
+
const lowerName = toCamelCase(entity.pluralName);
|
|
126
|
+
const snakeName = toSnakeCase(entity.pluralName);
|
|
127
|
+
// Collect searchable fields (string + enum types)
|
|
128
|
+
const searchableFields = entity.fields
|
|
129
|
+
.filter((f) => f.type === 'string' || f.type === 'enum')
|
|
130
|
+
.map((f) => toPascalCase(f.name));
|
|
131
|
+
let searchLogic;
|
|
132
|
+
if (searchableFields.length > 0) {
|
|
133
|
+
const fieldChecks = searchableFields
|
|
134
|
+
.map((f) => `\t\t\t\tstrings.Contains(strings.ToLower(fmt.Sprintf("%v", item.${f})), q)`)
|
|
135
|
+
.join(' ||\n');
|
|
136
|
+
searchLogic = `\t\tq := strings.ToLower(search)
|
|
137
|
+
\t\tvar filtered []${entity.name}
|
|
138
|
+
\t\tfor _, item := range result {
|
|
139
|
+
\t\t\tif ${fieldChecks.trim()} {
|
|
140
|
+
\t\t\t\tfiltered = append(filtered, item)
|
|
141
|
+
\t\t\t}
|
|
142
|
+
\t\t}
|
|
143
|
+
\t\tresult = filtered`;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
searchLogic = `\t\tq := strings.ToLower(search)
|
|
147
|
+
\t\tvar filtered []${entity.name}
|
|
148
|
+
\t\tfor _, item := range result {
|
|
149
|
+
\t\t\tif strings.Contains(strings.ToLower(fmt.Sprintf("%v", item)), q) {
|
|
150
|
+
\t\t\t\tfiltered = append(filtered, item)
|
|
151
|
+
\t\t\t}
|
|
152
|
+
\t\t}
|
|
153
|
+
\t\tresult = filtered`;
|
|
154
|
+
}
|
|
155
|
+
// Filter logic — check field values via reflect-like approach (simple string comparison)
|
|
156
|
+
const filterableFields = entity.fields.map((f) => ({
|
|
157
|
+
jsonName: f.name,
|
|
158
|
+
goName: toPascalCase(f.name),
|
|
159
|
+
}));
|
|
160
|
+
const filterChecks = filterableFields
|
|
161
|
+
.map((f) => `\t\t\t\tcase "${f.jsonName}":\n\t\t\t\t\tif fmt.Sprintf("%v", item.${f.goName}) != value {\n\t\t\t\t\t\tmatch = false\n\t\t\t\t\t}`)
|
|
162
|
+
.join('\n');
|
|
163
|
+
const seedData = generateSeedData(model, entity);
|
|
164
|
+
return {
|
|
165
|
+
path: `server/data/${snakeName}.go`,
|
|
166
|
+
content: `package main
|
|
167
|
+
|
|
168
|
+
import (
|
|
169
|
+
\t"fmt"
|
|
170
|
+
\t"strings"
|
|
171
|
+
\t"sync"
|
|
172
|
+
\t"time"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
${seedData}
|
|
176
|
+
|
|
177
|
+
type ${entity.name}Store struct {
|
|
178
|
+
\tmu sync.Mutex
|
|
179
|
+
\titems []${entity.name}
|
|
180
|
+
\tnextID int
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var ${lowerName}Store = &${entity.name}Store{
|
|
184
|
+
\titems: make([]${entity.name}, len(seedData)),
|
|
185
|
+
\tnextID: len(seedData) + 1,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func init() {
|
|
189
|
+
\tcopy(${lowerName}Store.items, seedData)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func (s *${entity.name}Store) GetAll() []${entity.name} {
|
|
193
|
+
\ts.mu.Lock()
|
|
194
|
+
\tdefer s.mu.Unlock()
|
|
195
|
+
\tresult := make([]${entity.name}, len(s.items))
|
|
196
|
+
\tcopy(result, s.items)
|
|
197
|
+
\treturn result
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func (s *${entity.name}Store) GetByID(id int) (${entity.name}, bool) {
|
|
201
|
+
\ts.mu.Lock()
|
|
202
|
+
\tdefer s.mu.Unlock()
|
|
203
|
+
\tfor _, item := range s.items {
|
|
204
|
+
\t\tif item.ID == id {
|
|
205
|
+
\t\t\treturn item, true
|
|
206
|
+
\t\t}
|
|
207
|
+
\t}
|
|
208
|
+
\treturn ${entity.name}{}, false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func (s *${entity.name}Store) Query(search string, filters map[string]string) ([]${entity.name}, int) {
|
|
212
|
+
\ts.mu.Lock()
|
|
213
|
+
\tdefer s.mu.Unlock()
|
|
214
|
+
\tresult := make([]${entity.name}, len(s.items))
|
|
215
|
+
\tcopy(result, s.items)
|
|
216
|
+
|
|
217
|
+
\tif search != "" {
|
|
218
|
+
${searchLogic}
|
|
219
|
+
\t}
|
|
220
|
+
|
|
221
|
+
\tif len(filters) > 0 {
|
|
222
|
+
\t\tvar filtered []${entity.name}
|
|
223
|
+
\t\tfor _, item := range result {
|
|
224
|
+
\t\t\tmatch := true
|
|
225
|
+
\t\t\tfor field, value := range filters {
|
|
226
|
+
\t\t\t\tswitch field {
|
|
227
|
+
${filterChecks}
|
|
228
|
+
\t\t\t\t}
|
|
229
|
+
\t\t\t}
|
|
230
|
+
\t\t\tif match {
|
|
231
|
+
\t\t\t\tfiltered = append(filtered, item)
|
|
232
|
+
\t\t\t}
|
|
233
|
+
\t\t}
|
|
234
|
+
\t\tresult = filtered
|
|
235
|
+
\t}
|
|
236
|
+
|
|
237
|
+
\treturn result, len(result)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func (s *${entity.name}Store) Create(input ${entity.name}Input) ${entity.name} {
|
|
241
|
+
\ts.mu.Lock()
|
|
242
|
+
\tdefer s.mu.Unlock()
|
|
243
|
+
\tnow := time.Now().UTC().Format(time.RFC3339)
|
|
244
|
+
\titem := ${entity.name}{
|
|
245
|
+
\t\tID: s.nextID,
|
|
246
|
+
\t\tCreatedAt: now,
|
|
247
|
+
\t\tUpdatedAt: now,
|
|
248
|
+
\t}
|
|
249
|
+
\ts.nextID++
|
|
250
|
+
${entity.fields.map((f) => `\tif input.${toPascalCase(f.name)} != nil {\n\t\titem.${toPascalCase(f.name)} = *input.${toPascalCase(f.name)}\n\t}`).join('\n')}
|
|
251
|
+
${belongsToRels(entity)
|
|
252
|
+
.map((rel) => {
|
|
253
|
+
const fk = fkFieldName(rel);
|
|
254
|
+
const goField = toPascalCase(fk);
|
|
255
|
+
return `\tif input.${goField} != nil {\n\t\titem.${goField} = *input.${goField}\n\t}`;
|
|
256
|
+
})
|
|
257
|
+
.join('\n')}
|
|
258
|
+
\ts.items = append(s.items, item)
|
|
259
|
+
\treturn item
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
func (s *${entity.name}Store) Update(id int, input ${entity.name}Input) (${entity.name}, bool) {
|
|
263
|
+
\ts.mu.Lock()
|
|
264
|
+
\tdefer s.mu.Unlock()
|
|
265
|
+
\tfor i, item := range s.items {
|
|
266
|
+
\t\tif item.ID == id {
|
|
267
|
+
\t\t\tnow := time.Now().UTC().Format(time.RFC3339)
|
|
268
|
+
${entity.fields.map((f) => `\t\t\tif input.${toPascalCase(f.name)} != nil {\n\t\t\t\ts.items[i].${toPascalCase(f.name)} = *input.${toPascalCase(f.name)}\n\t\t\t}`).join('\n')}
|
|
269
|
+
${belongsToRels(entity)
|
|
270
|
+
.map((rel) => {
|
|
271
|
+
const fk = fkFieldName(rel);
|
|
272
|
+
const goField = toPascalCase(fk);
|
|
273
|
+
return `\t\t\tif input.${goField} != nil {\n\t\t\t\ts.items[i].${goField} = *input.${goField}\n\t\t\t}`;
|
|
274
|
+
})
|
|
275
|
+
.join('\n')}
|
|
276
|
+
\t\t\ts.items[i].UpdatedAt = now
|
|
277
|
+
\t\t\treturn s.items[i], true
|
|
278
|
+
\t\t}
|
|
279
|
+
\t}
|
|
280
|
+
\treturn ${entity.name}{}, false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func (s *${entity.name}Store) Remove(id int) bool {
|
|
284
|
+
\ts.mu.Lock()
|
|
285
|
+
\tdefer s.mu.Unlock()
|
|
286
|
+
\tfor i, item := range s.items {
|
|
287
|
+
\t\tif item.ID == id {
|
|
288
|
+
\t\t\ts.items = append(s.items[:i], s.items[i+1:]...)
|
|
289
|
+
\t\t\treturn true
|
|
290
|
+
\t\t}
|
|
291
|
+
\t}
|
|
292
|
+
\treturn false
|
|
293
|
+
}
|
|
294
|
+
`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// =============================================================================
|
|
298
|
+
// Handlers — server/handlers/{entity}.go
|
|
299
|
+
// =============================================================================
|
|
300
|
+
function generateHandlers(model, entity) {
|
|
301
|
+
const snakeName = toSnakeCase(entity.pluralName);
|
|
302
|
+
const lowerName = toCamelCase(entity.pluralName);
|
|
303
|
+
const apiRoute = '/api/' + toKebabCase(entity.pluralName).toLowerCase();
|
|
304
|
+
const btoRels = belongsToRels(entity);
|
|
305
|
+
const hmRels = hasManyRels(entity);
|
|
306
|
+
// Populate function for belongsTo
|
|
307
|
+
let populateType = '';
|
|
308
|
+
let populateFunc = '';
|
|
309
|
+
if (btoRels.length > 0) {
|
|
310
|
+
const populateFields = [];
|
|
311
|
+
// Original entity fields
|
|
312
|
+
populateFields.push(`\t${entity.name}`);
|
|
313
|
+
for (const rel of btoRels) {
|
|
314
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
315
|
+
if (!targetEntity)
|
|
316
|
+
continue;
|
|
317
|
+
const targetVar = rel.target.charAt(0).toLowerCase() + rel.target.slice(1);
|
|
318
|
+
populateFields.push(`\t${toPascalCase(targetVar)} *${rel.target} \`json:"${targetVar}"\``);
|
|
319
|
+
}
|
|
320
|
+
populateType = `\ntype ${entity.name}WithRels struct {
|
|
321
|
+
${populateFields.join('\n')}
|
|
322
|
+
}\n`;
|
|
323
|
+
const populateBody = btoRels
|
|
324
|
+
.map((rel) => {
|
|
325
|
+
const fk = fkFieldName(rel);
|
|
326
|
+
const goField = toPascalCase(fk);
|
|
327
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
328
|
+
if (!targetEntity)
|
|
329
|
+
return '';
|
|
330
|
+
const targetVar = rel.target.charAt(0).toLowerCase() + rel.target.slice(1);
|
|
331
|
+
const targetStoreName = toCamelCase(targetEntity.pluralName);
|
|
332
|
+
return `\tif item.${goField} != 0 {
|
|
333
|
+
\t\tif found, ok := ${targetStoreName}Store.GetByID(item.${goField}); ok {
|
|
334
|
+
\t\t\tresult.${toPascalCase(targetVar)} = &found
|
|
335
|
+
\t\t}
|
|
336
|
+
\t}`;
|
|
337
|
+
})
|
|
338
|
+
.filter(Boolean)
|
|
339
|
+
.join('\n');
|
|
340
|
+
populateFunc = `
|
|
341
|
+
func populate${entity.name}(item ${entity.name}) ${entity.name}WithRels {
|
|
342
|
+
\tresult := ${entity.name}WithRels{${entity.name}: item}
|
|
343
|
+
${populateBody}
|
|
344
|
+
\treturn result
|
|
345
|
+
}
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
// hasMany include logic
|
|
349
|
+
let includeLogic = '';
|
|
350
|
+
let includeType = '';
|
|
351
|
+
if (hmRels.length > 0) {
|
|
352
|
+
const includeFields = [];
|
|
353
|
+
if (btoRels.length > 0) {
|
|
354
|
+
includeFields.push(`\t${entity.name}WithRels`);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
includeFields.push(`\t${entity.name}`);
|
|
358
|
+
}
|
|
359
|
+
for (const rel of hmRels) {
|
|
360
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
361
|
+
if (!targetEntity)
|
|
362
|
+
continue;
|
|
363
|
+
const targetVar = toCamelCase(targetEntity.pluralName);
|
|
364
|
+
includeFields.push(`\t${toPascalCase(targetVar)} []${rel.target} \`json:"${targetVar},omitempty"\``);
|
|
365
|
+
}
|
|
366
|
+
includeType = `\ntype ${entity.name}WithIncludes struct {
|
|
367
|
+
${includeFields.join('\n')}
|
|
368
|
+
}\n`;
|
|
369
|
+
const includeChecks = hmRels
|
|
370
|
+
.map((rel) => {
|
|
371
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
372
|
+
if (!targetEntity)
|
|
373
|
+
return '';
|
|
374
|
+
const targetVar = toCamelCase(targetEntity.pluralName);
|
|
375
|
+
const targetStoreName = toCamelCase(targetEntity.pluralName);
|
|
376
|
+
const fkRel = targetEntity.relationships.find((r) => r.type === 'belongsTo' && r.target === entity.name) ?? { type: 'belongsTo', target: entity.name };
|
|
377
|
+
const fk = fkFieldName(fkRel);
|
|
378
|
+
const goFk = toPascalCase(fk);
|
|
379
|
+
return `\t\tfor _, inc := range includes {
|
|
380
|
+
\t\t\tif inc == "${targetVar}" {
|
|
381
|
+
\t\t\t\tallItems := ${targetStoreName}Store.GetAll()
|
|
382
|
+
\t\t\t\tvar related []${rel.target}
|
|
383
|
+
\t\t\t\tfor _, r := range allItems {
|
|
384
|
+
\t\t\t\t\tif r.${goFk} == item.ID {
|
|
385
|
+
\t\t\t\t\t\trelated = append(related, r)
|
|
386
|
+
\t\t\t\t\t}
|
|
387
|
+
\t\t\t\t}
|
|
388
|
+
\t\t\t\tresult.${toPascalCase(targetVar)} = related
|
|
389
|
+
\t\t\t}
|
|
390
|
+
\t\t}`;
|
|
391
|
+
})
|
|
392
|
+
.filter(Boolean)
|
|
393
|
+
.join('\n');
|
|
394
|
+
includeLogic = includeChecks;
|
|
395
|
+
}
|
|
396
|
+
// GET list handler
|
|
397
|
+
const listReturnExpr = btoRels.length > 0
|
|
398
|
+
? `\tpopulated := make([]${entity.name}WithRels, len(items))
|
|
399
|
+
\tfor i, item := range items {
|
|
400
|
+
\t\tpopulated[i] = populate${entity.name}(item)
|
|
401
|
+
\t}
|
|
402
|
+
\tjson.NewEncoder(w).Encode(map[string]any{"data": populated, "total": total})`
|
|
403
|
+
: `\tjson.NewEncoder(w).Encode(map[string]any{"data": items, "total": total})`;
|
|
404
|
+
// GET by ID handler
|
|
405
|
+
let getByIDBody;
|
|
406
|
+
if (hmRels.length > 0) {
|
|
407
|
+
const baseExpr = btoRels.length > 0 ? `populate${entity.name}(item)` : `item`;
|
|
408
|
+
const innerType = btoRels.length > 0 ? `${entity.name}WithRels` : entity.name;
|
|
409
|
+
getByIDBody = `\titem, ok := ${lowerName}Store.GetByID(id)
|
|
410
|
+
\tif !ok {
|
|
411
|
+
\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
412
|
+
\t\treturn
|
|
413
|
+
\t}
|
|
414
|
+
\tincludeParam := r.URL.Query().Get("include")
|
|
415
|
+
\tincludes := strings.Split(includeParam, ",")
|
|
416
|
+
\tresult := ${entity.name}WithIncludes{${innerType}: ${baseExpr}}
|
|
417
|
+
${includeLogic}
|
|
418
|
+
\tw.Header().Set("Content-Type", "application/json")
|
|
419
|
+
\tjson.NewEncoder(w).Encode(result)`;
|
|
420
|
+
}
|
|
421
|
+
else if (btoRels.length > 0) {
|
|
422
|
+
getByIDBody = `\titem, ok := ${lowerName}Store.GetByID(id)
|
|
423
|
+
\tif !ok {
|
|
424
|
+
\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
425
|
+
\t\treturn
|
|
426
|
+
\t}
|
|
427
|
+
\tw.Header().Set("Content-Type", "application/json")
|
|
428
|
+
\tjson.NewEncoder(w).Encode(populate${entity.name}(item))`;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
getByIDBody = `\titem, ok := ${lowerName}Store.GetByID(id)
|
|
432
|
+
\tif !ok {
|
|
433
|
+
\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
434
|
+
\t\treturn
|
|
435
|
+
\t}
|
|
436
|
+
\tw.Header().Set("Content-Type", "application/json")
|
|
437
|
+
\tjson.NewEncoder(w).Encode(item)`;
|
|
438
|
+
}
|
|
439
|
+
// Determine imports needed
|
|
440
|
+
const needsStrings = hmRels.length > 0;
|
|
441
|
+
return {
|
|
442
|
+
path: `server/handlers/${snakeName}.go`,
|
|
443
|
+
content: `package main
|
|
444
|
+
|
|
445
|
+
import (
|
|
446
|
+
\t"encoding/json"
|
|
447
|
+
\t"net/http"
|
|
448
|
+
\t"strconv"${needsStrings ? '\n\t"strings"' : ''}
|
|
449
|
+
)
|
|
450
|
+
${populateType}${includeType}${populateFunc}
|
|
451
|
+
func Register${entity.name}Handlers(mux *http.ServeMux) {
|
|
452
|
+
\tmux.HandleFunc("GET ${apiRoute}", func(w http.ResponseWriter, r *http.Request) {
|
|
453
|
+
\t\tsearch := r.URL.Query().Get("search")
|
|
454
|
+
\t\tfilters := make(map[string]string)
|
|
455
|
+
\t\tfor key, values := range r.URL.Query() {
|
|
456
|
+
\t\t\tif len(key) > 7 && key[:7] == "filter[" && key[len(key)-1] == ']' {
|
|
457
|
+
\t\t\t\tfield := key[7 : len(key)-1]
|
|
458
|
+
\t\t\t\tfilters[field] = values[0]
|
|
459
|
+
\t\t\t}
|
|
460
|
+
\t\t}
|
|
461
|
+
\t\titems, total := ${lowerName}Store.Query(search, filters)
|
|
462
|
+
\t\tw.Header().Set("Content-Type", "application/json")
|
|
463
|
+
${listReturnExpr}
|
|
464
|
+
\t})
|
|
465
|
+
|
|
466
|
+
\tmux.HandleFunc("GET ${apiRoute}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
467
|
+
\t\tid, err := strconv.Atoi(r.PathValue("id"))
|
|
468
|
+
\t\tif err != nil {
|
|
469
|
+
\t\t\thttp.Error(w, \`{"error":"invalid id"}\`, http.StatusBadRequest)
|
|
470
|
+
\t\t\treturn
|
|
471
|
+
\t\t}
|
|
472
|
+
${getByIDBody}
|
|
473
|
+
\t})
|
|
474
|
+
|
|
475
|
+
\tmux.HandleFunc("POST ${apiRoute}", func(w http.ResponseWriter, r *http.Request) {
|
|
476
|
+
\t\tvar input ${entity.name}Input
|
|
477
|
+
\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
478
|
+
\t\t\thttp.Error(w, \`{"error":"invalid json"}\`, http.StatusBadRequest)
|
|
479
|
+
\t\t\treturn
|
|
480
|
+
\t\t}
|
|
481
|
+
\t\titem := ${lowerName}Store.Create(input)
|
|
482
|
+
\t\tw.Header().Set("Content-Type", "application/json")
|
|
483
|
+
\t\tw.WriteHeader(http.StatusCreated)
|
|
484
|
+
\t\tjson.NewEncoder(w).Encode(item)
|
|
485
|
+
\t})
|
|
486
|
+
|
|
487
|
+
\tmux.HandleFunc("PUT ${apiRoute}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
488
|
+
\t\tid, err := strconv.Atoi(r.PathValue("id"))
|
|
489
|
+
\t\tif err != nil {
|
|
490
|
+
\t\t\thttp.Error(w, \`{"error":"invalid id"}\`, http.StatusBadRequest)
|
|
491
|
+
\t\t\treturn
|
|
492
|
+
\t\t}
|
|
493
|
+
\t\tvar input ${entity.name}Input
|
|
494
|
+
\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
495
|
+
\t\t\thttp.Error(w, \`{"error":"invalid json"}\`, http.StatusBadRequest)
|
|
496
|
+
\t\t\treturn
|
|
497
|
+
\t\t}
|
|
498
|
+
\t\titem, ok := ${lowerName}Store.Update(id, input)
|
|
499
|
+
\t\tif !ok {
|
|
500
|
+
\t\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
501
|
+
\t\t\treturn
|
|
502
|
+
\t\t}
|
|
503
|
+
\t\tw.Header().Set("Content-Type", "application/json")
|
|
504
|
+
\t\tjson.NewEncoder(w).Encode(item)
|
|
505
|
+
\t})
|
|
506
|
+
|
|
507
|
+
\tmux.HandleFunc("DELETE ${apiRoute}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
508
|
+
\t\tid, err := strconv.Atoi(r.PathValue("id"))
|
|
509
|
+
\t\tif err != nil {
|
|
510
|
+
\t\t\thttp.Error(w, \`{"error":"invalid id"}\`, http.StatusBadRequest)
|
|
511
|
+
\t\t\treturn
|
|
512
|
+
\t\t}
|
|
513
|
+
\t\tok := ${lowerName}Store.Remove(id)
|
|
514
|
+
\t\tif !ok {
|
|
515
|
+
\t\t\thttp.Error(w, \`{"error":"not found"}\`, http.StatusNotFound)
|
|
516
|
+
\t\t\treturn
|
|
517
|
+
\t\t}
|
|
518
|
+
\t\tw.WriteHeader(http.StatusNoContent)
|
|
519
|
+
\t})
|
|
520
|
+
}
|
|
521
|
+
`,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — Configuration Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: package.json, vite.config.ts, tailwind.config.js,
|
|
5
|
+
* tsconfig.json, postcss.config.js, go.mod
|
|
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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — Configuration Files Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: package.json, vite.config.ts, tailwind.config.js,
|
|
5
|
+
* tsconfig.json, postcss.config.js, go.mod
|
|
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
|
+
generateGoMod(appSlug),
|
|
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" "go run ./server"',
|
|
28
|
+
build: 'vite build',
|
|
29
|
+
preview: 'vite preview',
|
|
30
|
+
server: 'go run ./server',
|
|
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:8080',
|
|
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 generateGoMod(appSlug) {
|
|
122
|
+
return {
|
|
123
|
+
path: 'go.mod',
|
|
124
|
+
content: `module ${appSlug}
|
|
125
|
+
|
|
126
|
+
go 1.22
|
|
127
|
+
`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — Helpers
|
|
3
|
+
*
|
|
4
|
+
* Go-specific helpers plus re-exports from shared helpers.
|
|
5
|
+
*/
|
|
6
|
+
import type { Field } from '../../model/types.js';
|
|
7
|
+
/** Map a model FieldType to a Go type string. */
|
|
8
|
+
export declare function goType(field: Field): string;
|
|
9
|
+
/** Generate a Go zero value string for a field. */
|
|
10
|
+
export declare function goZeroValue(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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — Helpers
|
|
3
|
+
*
|
|
4
|
+
* Go-specific helpers plus re-exports from shared helpers.
|
|
5
|
+
*/
|
|
6
|
+
/** Map a model FieldType to a Go type string. */
|
|
7
|
+
export function goType(field) {
|
|
8
|
+
const lowerName = field.name.toLowerCase();
|
|
9
|
+
switch (field.type) {
|
|
10
|
+
case 'string':
|
|
11
|
+
case 'enum':
|
|
12
|
+
case 'date':
|
|
13
|
+
return 'string';
|
|
14
|
+
case 'number':
|
|
15
|
+
if (lowerName.includes('price') ||
|
|
16
|
+
lowerName.includes('cost') ||
|
|
17
|
+
lowerName.includes('amount')) {
|
|
18
|
+
return 'float64';
|
|
19
|
+
}
|
|
20
|
+
return 'int';
|
|
21
|
+
case 'boolean':
|
|
22
|
+
return 'bool';
|
|
23
|
+
default:
|
|
24
|
+
return 'string';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Generate a Go zero value string for a field. */
|
|
28
|
+
export function goZeroValue(field) {
|
|
29
|
+
switch (field.type) {
|
|
30
|
+
case 'string':
|
|
31
|
+
case 'enum':
|
|
32
|
+
case 'date':
|
|
33
|
+
return '""';
|
|
34
|
+
case 'number':
|
|
35
|
+
return '0';
|
|
36
|
+
case 'boolean':
|
|
37
|
+
return 'false';
|
|
38
|
+
default:
|
|
39
|
+
return '""';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Convert PascalCase or camelCase to snake_case. */
|
|
43
|
+
export function toSnakeCase(name) {
|
|
44
|
+
return name
|
|
45
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
46
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from '../shared/helpers.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit
|
|
3
|
+
*
|
|
4
|
+
* Generates a full-stack MVP: React 18 + Vite + Tailwind + Go (net/http).
|
|
5
|
+
* Deterministic: same ApplicationModel -> same output files.
|
|
6
|
+
*
|
|
7
|
+
* Frontend is identical to react-node (same React components, Vite config, Tailwind).
|
|
8
|
+
* Backend is Go net/http stdlib with in-memory data stores.
|
|
9
|
+
*/
|
|
10
|
+
import type { FactoryToolkit } from '../types.js';
|
|
11
|
+
export declare const reactGoToolkit: FactoryToolkit;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit
|
|
3
|
+
*
|
|
4
|
+
* Generates a full-stack MVP: React 18 + Vite + Tailwind + Go (net/http).
|
|
5
|
+
* Deterministic: same ApplicationModel -> same output files.
|
|
6
|
+
*
|
|
7
|
+
* Frontend is identical to react-node (same React components, Vite config, Tailwind).
|
|
8
|
+
* Backend is Go net/http stdlib 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 reactGoToolkit = {
|
|
20
|
+
id: 'react-go',
|
|
21
|
+
name: 'React + Go',
|
|
22
|
+
description: 'React 18 + Vite + Tailwind CSS + Go — full-stack MVP with Go net/http 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-go',
|
|
52
|
+
warnings,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — Seed Data Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates realistic mock data in Go struct literal format.
|
|
5
|
+
* Uses shared generateFieldValue for deterministic values,
|
|
6
|
+
* wraps them in Go slice-of-structs format for in-memory 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 (Go slice format). */
|
|
11
|
+
export declare function generateSeedData(model: ApplicationModel, entity: Entity): string;
|
|
12
|
+
export { SEED_COUNT };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go Toolkit — Seed Data Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates realistic mock data in Go struct literal format.
|
|
5
|
+
* Uses shared generateFieldValue for deterministic values,
|
|
6
|
+
* wraps them in Go slice-of-structs format for in-memory data stores.
|
|
7
|
+
*/
|
|
8
|
+
import { belongsToRels, fkFieldName } from './helpers.js';
|
|
9
|
+
import { toPascalCase } from '../../model/naming.js';
|
|
10
|
+
import { generateFieldValue, SEED_COUNT } from '../shared/seed-data.js';
|
|
11
|
+
/** Generate seed data items for a single entity (Go slice format). */
|
|
12
|
+
export function generateSeedData(model, entity) {
|
|
13
|
+
const rels = belongsToRels(entity);
|
|
14
|
+
const lines = [];
|
|
15
|
+
lines.push(`var seedData = []${entity.name}{`);
|
|
16
|
+
for (let i = 0; i < SEED_COUNT; i++) {
|
|
17
|
+
const fields = [];
|
|
18
|
+
fields.push(`ID: ${String(i + 1)}`);
|
|
19
|
+
for (const field of entity.fields) {
|
|
20
|
+
const jsValue = generateFieldValue(field, i, entity.name);
|
|
21
|
+
const goValue = jsToGoValue(jsValue, field.type);
|
|
22
|
+
fields.push(`${toPascalCase(field.name)}: ${goValue}`);
|
|
23
|
+
}
|
|
24
|
+
// FK fields from belongsTo relationships
|
|
25
|
+
for (const rel of rels) {
|
|
26
|
+
const fk = fkFieldName(rel);
|
|
27
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
28
|
+
const targetCount = targetEntity ? SEED_COUNT : SEED_COUNT;
|
|
29
|
+
fields.push(`${toPascalCase(fk)}: ${String((i % targetCount) + 1)}`);
|
|
30
|
+
}
|
|
31
|
+
// Implicit timestamps (deterministic)
|
|
32
|
+
const createdDate = new Date('2025-06-01T10:00:00Z');
|
|
33
|
+
createdDate.setDate(createdDate.getDate() + i * 2);
|
|
34
|
+
const updatedDate = new Date(createdDate);
|
|
35
|
+
updatedDate.setDate(updatedDate.getDate() + i);
|
|
36
|
+
fields.push(`CreatedAt: "${createdDate.toISOString()}"`);
|
|
37
|
+
fields.push(`UpdatedAt: "${updatedDate.toISOString()}"`);
|
|
38
|
+
lines.push(`\t{${fields.join(', ')}},`);
|
|
39
|
+
}
|
|
40
|
+
lines.push('}');
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
/** Convert a JS seed value string to Go format. */
|
|
44
|
+
function jsToGoValue(jsValue, fieldType) {
|
|
45
|
+
if (fieldType === 'boolean') {
|
|
46
|
+
return jsValue; // true/false same in Go
|
|
47
|
+
}
|
|
48
|
+
// String/enum/date values come wrapped in single quotes — convert to double quotes
|
|
49
|
+
if (jsValue.startsWith("'") && jsValue.endsWith("'")) {
|
|
50
|
+
return `"${jsValue.slice(1, -1)}"`;
|
|
51
|
+
}
|
|
52
|
+
// Numbers pass through unchanged
|
|
53
|
+
return jsValue;
|
|
54
|
+
}
|
|
55
|
+
export { SEED_COUNT };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go 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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React+Go 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
|
+
bin/
|
|
24
|
+
*.exe
|
|
25
|
+
`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function generateEnvExample() {
|
|
29
|
+
return {
|
|
30
|
+
path: '.env.example',
|
|
31
|
+
content: `PORT=8080
|
|
32
|
+
VITE_API_URL=http://localhost:8080
|
|
33
|
+
`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function generateReadme(model) {
|
|
37
|
+
let entitiesSection = '';
|
|
38
|
+
if (model.entities.length > 0) {
|
|
39
|
+
const entityLines = model.entities
|
|
40
|
+
.map((e) => {
|
|
41
|
+
const relCount = e.relationships.length;
|
|
42
|
+
const parts = [`${String(e.fields.length)} fields`];
|
|
43
|
+
if (relCount > 0) {
|
|
44
|
+
parts.push(`${String(relCount)} relationship${relCount > 1 ? 's' : ''}`);
|
|
45
|
+
}
|
|
46
|
+
return `- **${e.name}** (${e.icon}) — ${parts.join(', ')}`;
|
|
47
|
+
})
|
|
48
|
+
.join('\n');
|
|
49
|
+
entitiesSection = `\n## Entities\n\n${entityLines}\n`;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
path: 'README.md',
|
|
53
|
+
content: `# ${model.identity.name}
|
|
54
|
+
|
|
55
|
+
${model.identity.description}
|
|
56
|
+
${entitiesSection}
|
|
57
|
+
## Getting Started
|
|
58
|
+
|
|
59
|
+
\`\`\`bash
|
|
60
|
+
npm install
|
|
61
|
+
go run ./server
|
|
62
|
+
npm run dev
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
This starts both the Vite dev server and the Go server.
|
|
66
|
+
|
|
67
|
+
- **Frontend:** http://localhost:5173
|
|
68
|
+
- **API:** http://localhost:8080
|
|
69
|
+
|
|
70
|
+
## Tech Stack
|
|
71
|
+
|
|
72
|
+
- React 18 + TypeScript
|
|
73
|
+
- Vite
|
|
74
|
+
- Tailwind CSS
|
|
75
|
+
- Go (net/http)
|
|
76
|
+
- React Router v6
|
|
77
|
+
`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function generateIndexHtml(model) {
|
|
81
|
+
return {
|
|
82
|
+
path: 'index.html',
|
|
83
|
+
content: `<!doctype html>
|
|
84
|
+
<html lang="en">
|
|
85
|
+
<head>
|
|
86
|
+
<meta charset="UTF-8" />
|
|
87
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
88
|
+
<title>${model.identity.name}</title>
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<div id="root"></div>
|
|
92
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
95
|
+
`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function generateMainTsx() {
|
|
99
|
+
return {
|
|
100
|
+
path: 'src/main.tsx',
|
|
101
|
+
content: `import React from 'react';
|
|
102
|
+
import ReactDOM from 'react-dom/client';
|
|
103
|
+
import App from './App';
|
|
104
|
+
import './index.css';
|
|
105
|
+
|
|
106
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
107
|
+
<React.StrictMode>
|
|
108
|
+
<App />
|
|
109
|
+
</React.StrictMode>,
|
|
110
|
+
);
|
|
111
|
+
`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function generateIndexCss() {
|
|
115
|
+
return {
|
|
116
|
+
path: 'src/index.css',
|
|
117
|
+
content: `@tailwind base;
|
|
118
|
+
@tailwind components;
|
|
119
|
+
@tailwind utilities;
|
|
120
|
+
`,
|
|
121
|
+
};
|
|
122
|
+
}
|