wmx-os-generators 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -0
- package/src/deployGenerator.ts +127 -0
- package/src/docsGenerator.ts +326 -0
- package/src/index.ts +3 -0
- package/src/scaffold.ts +106 -0
- package/templates/nextjs-postgres/.env.example +1 -0
- package/templates/nextjs-postgres/README.md +13 -0
- package/templates/nextjs-postgres/drizzle.config.ts +10 -0
- package/templates/nextjs-postgres/package.json +26 -0
- package/templates/nextjs-postgres/src/app/layout.tsx +14 -0
- package/templates/nextjs-postgres/src/app/page.tsx +8 -0
- package/templates/react-express-mongo/README.md +23 -0
- package/templates/react-express-mongo/backend/.env.example +3 -0
- package/templates/react-express-mongo/backend/package.json +23 -0
- package/templates/react-express-mongo/backend/src/db.ts +16 -0
- package/templates/react-express-mongo/backend/src/index.ts +22 -0
- package/templates/react-express-mongo/frontend/index.html +12 -0
- package/templates/react-express-mongo/frontend/package.json +21 -0
- package/templates/react-express-mongo/frontend/src/App.tsx +12 -0
- package/templates/react-express-mongo/frontend/src/main.tsx +9 -0
- package/templates/react-express-mongo/frontend/vite.config.ts +12 -0
- package/tsconfig.json +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wmx-os-generators",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc --project tsconfig.json",
|
|
9
|
+
"dev": "tsc --project tsconfig.json --watch"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"wmx-os-core": "workspace:*",
|
|
13
|
+
"wmx-os-scanners": "workspace:*",
|
|
14
|
+
"execa": "^8.0.0",
|
|
15
|
+
"fs-extra": "^11.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/fs-extra": "^11.0.0",
|
|
19
|
+
"@types/node": "^20.0.0",
|
|
20
|
+
"typescript": "^5.4.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
interface WmxConfig {
|
|
5
|
+
projectName?: string
|
|
6
|
+
framework?: string
|
|
7
|
+
packageManager?: string
|
|
8
|
+
frontend?: { framework?: string; dir?: string }
|
|
9
|
+
backend?: { framework?: string; dir?: string }
|
|
10
|
+
database?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const BACKEND_FRAMEWORKS = ['express', 'nestjs', 'fastify', 'koa']
|
|
14
|
+
|
|
15
|
+
function hasBackend(config: WmxConfig): boolean {
|
|
16
|
+
return !!(config.backend && BACKEND_FRAMEWORKS.includes(config.backend.framework ?? ''))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function generateVercelConfig(config: WmxConfig): object {
|
|
20
|
+
const frontendDir = config.frontend?.dir ?? 'frontend'
|
|
21
|
+
const backendDir = config.backend?.dir ?? 'backend'
|
|
22
|
+
|
|
23
|
+
if (hasBackend(config)) {
|
|
24
|
+
return {
|
|
25
|
+
version: 2,
|
|
26
|
+
builds: [
|
|
27
|
+
{
|
|
28
|
+
src: `${frontendDir}/package.json`,
|
|
29
|
+
use: '@vercel/static-build',
|
|
30
|
+
config: { distDir: 'dist' },
|
|
31
|
+
},
|
|
32
|
+
{ src: `${backendDir}/package.json`, use: '@vercel/node' },
|
|
33
|
+
],
|
|
34
|
+
routes: [
|
|
35
|
+
{ src: '/api/(.*)', dest: `/${backendDir}` },
|
|
36
|
+
{ src: '/(.*)', dest: `/${frontendDir}` },
|
|
37
|
+
],
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
version: 2,
|
|
43
|
+
builds: [
|
|
44
|
+
{
|
|
45
|
+
src: `${frontendDir}/package.json`,
|
|
46
|
+
use: '@vercel/static-build',
|
|
47
|
+
config: { distDir: 'dist' },
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function generateRenderConfig(config: WmxConfig): string {
|
|
54
|
+
const frontendDir = config.frontend?.dir ?? 'frontend'
|
|
55
|
+
const backendDir = config.backend?.dir ?? 'backend'
|
|
56
|
+
const projectName = config.projectName ?? 'my-app'
|
|
57
|
+
|
|
58
|
+
const envExamplePath = path.join(process.cwd(), '.env.example')
|
|
59
|
+
let envKeys: string[] = []
|
|
60
|
+
if (fse.pathExistsSync(envExamplePath)) {
|
|
61
|
+
const content = fse.readFileSync(envExamplePath, 'utf-8')
|
|
62
|
+
envKeys = content
|
|
63
|
+
.split('\n')
|
|
64
|
+
.filter(line => line.includes('=') && !line.trimStart().startsWith('#'))
|
|
65
|
+
.map(line => line.split('=')[0].trim())
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (hasBackend(config)) {
|
|
70
|
+
const envSection = envKeys.length > 0
|
|
71
|
+
? `\n envVars:\n${envKeys.map(k => ` - key: ${k}\n value: placeholder`).join('\n')}`
|
|
72
|
+
: ''
|
|
73
|
+
|
|
74
|
+
return `services:
|
|
75
|
+
- type: web
|
|
76
|
+
name: ${projectName}-backend
|
|
77
|
+
runtime: node
|
|
78
|
+
rootDir: ${backendDir}
|
|
79
|
+
buildCommand: npm install && npm run build
|
|
80
|
+
startCommand: npm start${envSection}
|
|
81
|
+
|
|
82
|
+
- type: static
|
|
83
|
+
name: ${projectName}-frontend
|
|
84
|
+
rootDir: ${frontendDir}
|
|
85
|
+
buildCommand: npm install && npm run build
|
|
86
|
+
staticPublishPath: dist
|
|
87
|
+
`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `services:
|
|
91
|
+
- type: static
|
|
92
|
+
name: ${projectName}-frontend
|
|
93
|
+
rootDir: ${frontendDir}
|
|
94
|
+
buildCommand: npm install && npm run build
|
|
95
|
+
staticPublishPath: dist
|
|
96
|
+
`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function generateNetlifyConfig(config: WmxConfig): string {
|
|
100
|
+
const frontendDir = config.frontend?.dir ?? 'frontend'
|
|
101
|
+
const framework = config.frontend?.framework ?? ''
|
|
102
|
+
|
|
103
|
+
let buildCommand: string
|
|
104
|
+
let publish: string
|
|
105
|
+
|
|
106
|
+
if (framework === 'next') {
|
|
107
|
+
buildCommand = 'npm run build'
|
|
108
|
+
publish = '.next'
|
|
109
|
+
} else if (framework === 'nuxt') {
|
|
110
|
+
buildCommand = 'npm run generate'
|
|
111
|
+
publish = 'dist'
|
|
112
|
+
} else {
|
|
113
|
+
buildCommand = 'npm run build'
|
|
114
|
+
publish = 'dist'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return `[build]
|
|
118
|
+
base = "${frontendDir}"
|
|
119
|
+
command = "${buildCommand}"
|
|
120
|
+
publish = "${publish}"
|
|
121
|
+
|
|
122
|
+
[[redirects]]
|
|
123
|
+
from = "/*"
|
|
124
|
+
to = "/index.html"
|
|
125
|
+
status = 200
|
|
126
|
+
`
|
|
127
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import type { AnalysisResult, ImportEntry, RouteEntry } from 'wmx-os-scanners'
|
|
4
|
+
|
|
5
|
+
// ── Existing helpers ──────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function topImports(imports: ImportEntry[], n = 10): Array<{ module: string; count: number }> {
|
|
8
|
+
const counts = new Map<string, number>()
|
|
9
|
+
for (const imp of imports) {
|
|
10
|
+
counts.set(imp.to, (counts.get(imp.to) ?? 0) + 1)
|
|
11
|
+
}
|
|
12
|
+
return [...counts.entries()]
|
|
13
|
+
.sort((a, b) => b[1] - a[1])
|
|
14
|
+
.slice(0, n)
|
|
15
|
+
.map(([module, count]) => ({ module, count }))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function generateAnalysis(result: AnalysisResult, outputDir: string): Promise<void> {
|
|
19
|
+
await fs.ensureDir(outputDir)
|
|
20
|
+
|
|
21
|
+
const top = topImports(result.imports)
|
|
22
|
+
|
|
23
|
+
const routeTable = result.routes.length > 0
|
|
24
|
+
? [
|
|
25
|
+
'| Method | Path | File | Line |',
|
|
26
|
+
'|--------|------|------|------|',
|
|
27
|
+
...result.routes.map(r =>
|
|
28
|
+
`| \`${r.method}\` | \`${r.path}\` | ${r.file} | ${r.line} |`
|
|
29
|
+
)
|
|
30
|
+
].join('\n')
|
|
31
|
+
: '_No Express or Next.js routes detected._'
|
|
32
|
+
|
|
33
|
+
const importList = top.length > 0
|
|
34
|
+
? top.map((t, i) => `${i + 1}. \`${t.module}\` — imported **${t.count}** time${t.count === 1 ? '' : 's'}`).join('\n')
|
|
35
|
+
: '_No imports detected._'
|
|
36
|
+
|
|
37
|
+
const md = `# Project Architecture
|
|
38
|
+
|
|
39
|
+
_Generated by wmx analyze on ${new Date().toISOString()}_
|
|
40
|
+
|
|
41
|
+
## Folder Structure
|
|
42
|
+
|
|
43
|
+
\`\`\`
|
|
44
|
+
${result.folderTree}
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
## API Routes
|
|
48
|
+
|
|
49
|
+
${routeTable}
|
|
50
|
+
|
|
51
|
+
## Top Imports
|
|
52
|
+
|
|
53
|
+
${importList}
|
|
54
|
+
|
|
55
|
+
## Exports Summary
|
|
56
|
+
|
|
57
|
+
Total exported symbols: **${result.exports.length}**
|
|
58
|
+
|
|
59
|
+
| Type | Count |
|
|
60
|
+
|------|-------|
|
|
61
|
+
| Functions | ${result.exports.filter(e => e.type === 'function').length} |
|
|
62
|
+
| Classes | ${result.exports.filter(e => e.type === 'class').length} |
|
|
63
|
+
| Constants | ${result.exports.filter(e => e.type === 'const').length} |
|
|
64
|
+
| Types | ${result.exports.filter(e => e.type === 'type').length} |
|
|
65
|
+
`
|
|
66
|
+
|
|
67
|
+
await fs.writeFile(path.join(outputDir, 'analysis.md'), md, 'utf8')
|
|
68
|
+
|
|
69
|
+
await fs.writeFile(
|
|
70
|
+
path.join(outputDir, 'analysis.json'),
|
|
71
|
+
JSON.stringify(result, null, 2),
|
|
72
|
+
'utf8'
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Project structure tree helper (for generateReadme) ────────────────────────
|
|
77
|
+
|
|
78
|
+
const PROJ_IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', '.wmx'])
|
|
79
|
+
const PROJ_IGNORE_FILES = new Set(['.env', '.env.example'])
|
|
80
|
+
|
|
81
|
+
async function buildProjectStructure(dir: string, maxDepth: number, depth = 0): Promise<string> {
|
|
82
|
+
if (depth >= maxDepth) return ''
|
|
83
|
+
|
|
84
|
+
let entries: string[]
|
|
85
|
+
try {
|
|
86
|
+
entries = await fs.readdir(dir)
|
|
87
|
+
} catch {
|
|
88
|
+
return ''
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const filtered = entries.filter(e => {
|
|
92
|
+
if (PROJ_IGNORE_DIRS.has(e)) return false
|
|
93
|
+
if (PROJ_IGNORE_FILES.has(e)) return false
|
|
94
|
+
if (/\.lock$/.test(e)) return false
|
|
95
|
+
return true
|
|
96
|
+
})
|
|
97
|
+
filtered.sort()
|
|
98
|
+
|
|
99
|
+
const indent = ' '.repeat(depth)
|
|
100
|
+
const lines: string[] = []
|
|
101
|
+
|
|
102
|
+
for (const entry of filtered) {
|
|
103
|
+
const fullPath = path.join(dir, entry)
|
|
104
|
+
let isDir = false
|
|
105
|
+
try {
|
|
106
|
+
const stat = await fs.stat(fullPath)
|
|
107
|
+
isDir = stat.isDirectory()
|
|
108
|
+
} catch {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isDir) {
|
|
113
|
+
lines.push(`${indent}${entry}/`)
|
|
114
|
+
const sub = await buildProjectStructure(fullPath, maxDepth, depth + 1)
|
|
115
|
+
if (sub) lines.push(sub)
|
|
116
|
+
} else {
|
|
117
|
+
lines.push(`${indent}${entry}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return lines.join('\n')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── generateReadme ────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export async function generateReadme(cwd: string): Promise<string> {
|
|
127
|
+
// Read package.json
|
|
128
|
+
let pkg: Record<string, unknown> = {}
|
|
129
|
+
try {
|
|
130
|
+
const raw = await fs.readFile(path.join(cwd, 'package.json'), 'utf8')
|
|
131
|
+
pkg = JSON.parse(raw) as Record<string, unknown>
|
|
132
|
+
} catch { /* missing or invalid — use defaults */ }
|
|
133
|
+
|
|
134
|
+
// Read .wmxrc.json
|
|
135
|
+
let wmxrc: Record<string, unknown> = {}
|
|
136
|
+
try {
|
|
137
|
+
const raw = await fs.readFile(path.join(cwd, '.wmxrc.json'), 'utf8')
|
|
138
|
+
wmxrc = JSON.parse(raw) as Record<string, unknown>
|
|
139
|
+
} catch { /* missing — use defaults */ }
|
|
140
|
+
|
|
141
|
+
// Read .env.example
|
|
142
|
+
let envExampleRaw = ''
|
|
143
|
+
try {
|
|
144
|
+
envExampleRaw = await fs.readFile(path.join(cwd, '.env.example'), 'utf8')
|
|
145
|
+
} catch { /* missing — handled below */ }
|
|
146
|
+
|
|
147
|
+
// 1. Title
|
|
148
|
+
const title = (pkg.name as string | undefined) ?? 'My Project'
|
|
149
|
+
|
|
150
|
+
// 2. Description
|
|
151
|
+
const description = (pkg.description as string | undefined) ?? 'A project built with wmx'
|
|
152
|
+
|
|
153
|
+
// 3. Tech Stack
|
|
154
|
+
const deps = (pkg.dependencies as Record<string, string> | undefined) ?? {}
|
|
155
|
+
const devDeps = (pkg.devDependencies as Record<string, string> | undefined) ?? {}
|
|
156
|
+
const allDeps = { ...deps, ...devDeps }
|
|
157
|
+
|
|
158
|
+
const techMap: Array<[string, string]> = [
|
|
159
|
+
['react', 'React'],
|
|
160
|
+
['express', 'Express'],
|
|
161
|
+
['mongoose', 'MongoDB'],
|
|
162
|
+
['next', 'Next.js'],
|
|
163
|
+
['pg', 'PostgreSQL'],
|
|
164
|
+
['vue', 'Vue'],
|
|
165
|
+
]
|
|
166
|
+
const detectedTech = techMap
|
|
167
|
+
.filter(([key]) => key in allDeps)
|
|
168
|
+
.map(([, label]) => `- ${label}`)
|
|
169
|
+
const techStackSection = detectedTech.length > 0
|
|
170
|
+
? detectedTech.join('\n')
|
|
171
|
+
: '_No tech stack detected._'
|
|
172
|
+
|
|
173
|
+
// 4. Prerequisites
|
|
174
|
+
const packageManager = wmxrc.packageManager as string | undefined
|
|
175
|
+
const prereqItems = ['- Node.js >= 18', ...(packageManager ? [`- ${packageManager}`] : [])]
|
|
176
|
+
const prerequisitesSection = prereqItems.join('\n')
|
|
177
|
+
|
|
178
|
+
// 5. Installation
|
|
179
|
+
const repoObj = pkg.repository as string | { url?: string } | undefined
|
|
180
|
+
const repoUrl = typeof repoObj === 'string'
|
|
181
|
+
? repoObj
|
|
182
|
+
: (repoObj?.url ?? 'https://github.com/your-org/your-repo')
|
|
183
|
+
const projectName = (pkg.name as string | undefined) ?? 'your-project'
|
|
184
|
+
const installCmd = packageManager === 'pnpm' ? 'pnpm install'
|
|
185
|
+
: packageManager === 'bun' ? 'bun install'
|
|
186
|
+
: packageManager === 'yarn' ? 'yarn install'
|
|
187
|
+
: 'npm install'
|
|
188
|
+
|
|
189
|
+
// 6. Environment Variables
|
|
190
|
+
const envKeys = envExampleRaw
|
|
191
|
+
.split('\n')
|
|
192
|
+
.filter(line => line.includes('=') && !line.trimStart().startsWith('#'))
|
|
193
|
+
.map(line => line.split('=')[0].trim())
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
const envSection = envKeys.length > 0
|
|
196
|
+
? '```bash\n' + envKeys.map(k => `${k}=`).join('\n') + '\n```'
|
|
197
|
+
: '_No environment variables required._'
|
|
198
|
+
|
|
199
|
+
// 7. Available Scripts
|
|
200
|
+
const scripts = (pkg.scripts as Record<string, string> | undefined) ?? {}
|
|
201
|
+
const scriptEntries = Object.entries(scripts)
|
|
202
|
+
const scriptsSection = scriptEntries.length > 0
|
|
203
|
+
? scriptEntries.map(([key, val]) => `- \`npm run ${key}\` — ${val}`).join('\n')
|
|
204
|
+
: '_No scripts defined._'
|
|
205
|
+
|
|
206
|
+
// 8. Project Structure (2 levels deep)
|
|
207
|
+
const structure = await buildProjectStructure(cwd, 2)
|
|
208
|
+
|
|
209
|
+
// 9. License
|
|
210
|
+
const license = (pkg.license as string | undefined) ?? 'MIT'
|
|
211
|
+
|
|
212
|
+
return `# ${title}
|
|
213
|
+
|
|
214
|
+
${description}
|
|
215
|
+
|
|
216
|
+
## Tech Stack
|
|
217
|
+
|
|
218
|
+
${techStackSection}
|
|
219
|
+
|
|
220
|
+
## Prerequisites
|
|
221
|
+
|
|
222
|
+
${prerequisitesSection}
|
|
223
|
+
|
|
224
|
+
## Installation
|
|
225
|
+
|
|
226
|
+
\`\`\`bash
|
|
227
|
+
1. git clone ${repoUrl}
|
|
228
|
+
2. cd ${projectName}
|
|
229
|
+
3. ${installCmd}
|
|
230
|
+
\`\`\`
|
|
231
|
+
|
|
232
|
+
## Environment Variables
|
|
233
|
+
|
|
234
|
+
${envSection}
|
|
235
|
+
|
|
236
|
+
## Available Scripts
|
|
237
|
+
|
|
238
|
+
${scriptsSection}
|
|
239
|
+
|
|
240
|
+
## Project Structure
|
|
241
|
+
|
|
242
|
+
\`\`\`
|
|
243
|
+
${structure}
|
|
244
|
+
\`\`\`
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
This project is licensed under the ${license} license.
|
|
249
|
+
`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── generateArchitectureDocs ──────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
export function generateArchitectureDocs(analysis: AnalysisResult): string {
|
|
255
|
+
// Derive a unique source-file count from the data available in AnalysisResult
|
|
256
|
+
const uniqueFiles = new Set([
|
|
257
|
+
...analysis.imports.map(i => i.from),
|
|
258
|
+
...analysis.exports.map(e => e.file),
|
|
259
|
+
...analysis.routes.map(r => r.file),
|
|
260
|
+
]).size
|
|
261
|
+
|
|
262
|
+
// API Routes table
|
|
263
|
+
const routeTable = analysis.routes.length > 0
|
|
264
|
+
? [
|
|
265
|
+
'| Method | Endpoint | File | Description |',
|
|
266
|
+
'|--------|----------|------|-------------|',
|
|
267
|
+
...analysis.routes.map(r =>
|
|
268
|
+
`| \`${r.method}\` | \`${r.path}\` | ${r.file} | |`
|
|
269
|
+
)
|
|
270
|
+
].join('\n')
|
|
271
|
+
: '_No API routes detected._'
|
|
272
|
+
|
|
273
|
+
// Key Dependencies — external (non-relative) import targets, deduped
|
|
274
|
+
const externalDeps = [...new Set(
|
|
275
|
+
analysis.imports
|
|
276
|
+
.map(i => i.to)
|
|
277
|
+
.filter(m => !m.startsWith('.') && !m.startsWith('/'))
|
|
278
|
+
)].sort()
|
|
279
|
+
const depsSection = externalDeps.length > 0
|
|
280
|
+
? externalDeps.map(d => `- \`${d}\``).join('\n')
|
|
281
|
+
: '_No external dependencies detected._'
|
|
282
|
+
|
|
283
|
+
return `# Project Architecture
|
|
284
|
+
|
|
285
|
+
## Overview
|
|
286
|
+
|
|
287
|
+
This document describes the architecture of the project.
|
|
288
|
+
|
|
289
|
+
Analyzed **${uniqueFiles}** source files.
|
|
290
|
+
|
|
291
|
+
## Folder Structure
|
|
292
|
+
|
|
293
|
+
\`\`\`
|
|
294
|
+
${analysis.folderTree}
|
|
295
|
+
\`\`\`
|
|
296
|
+
|
|
297
|
+
## API Routes
|
|
298
|
+
|
|
299
|
+
${routeTable}
|
|
300
|
+
|
|
301
|
+
## Key Dependencies
|
|
302
|
+
|
|
303
|
+
${depsSection}
|
|
304
|
+
`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── generateApiDocs ───────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
export function generateApiDocs(routes: RouteEntry[]): string {
|
|
310
|
+
const routeTable = routes.length > 0
|
|
311
|
+
? [
|
|
312
|
+
'| Method | Endpoint | File | Description |',
|
|
313
|
+
'|--------|----------|------|-------------|',
|
|
314
|
+
...routes.map(r =>
|
|
315
|
+
`| \`${r.method}\` | \`${r.path}\` | ${r.file} | |`
|
|
316
|
+
)
|
|
317
|
+
].join('\n')
|
|
318
|
+
: '_No routes detected._'
|
|
319
|
+
|
|
320
|
+
return `# API Reference
|
|
321
|
+
|
|
322
|
+
${routeTable}
|
|
323
|
+
|
|
324
|
+
_Update the Description column with endpoint-specific documentation._
|
|
325
|
+
`
|
|
326
|
+
}
|
package/src/index.ts
ADDED
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import { execa } from 'execa'
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = path.dirname(__filename)
|
|
8
|
+
|
|
9
|
+
interface ScaffoldAnswers {
|
|
10
|
+
projectName: string
|
|
11
|
+
framework: string
|
|
12
|
+
backend: string
|
|
13
|
+
database: string
|
|
14
|
+
packageManager: string
|
|
15
|
+
features?: string[]
|
|
16
|
+
[key: string]: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ScaffoldGenerator {
|
|
20
|
+
static async generate(answers: ScaffoldAnswers): Promise<void> {
|
|
21
|
+
const templateName = ScaffoldGenerator.resolveTemplate(answers)
|
|
22
|
+
const templatesDir = path.join(__dirname, '..', 'templates')
|
|
23
|
+
const templatePath = path.join(templatesDir, templateName)
|
|
24
|
+
const targetPath = path.join(process.cwd(), answers.projectName)
|
|
25
|
+
|
|
26
|
+
if (!(await fs.pathExists(templatePath))) {
|
|
27
|
+
throw new Error(`Template not found: ${templateName} (looked in ${templatePath})`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (await fs.pathExists(targetPath)) {
|
|
31
|
+
throw new Error(`Directory already exists: ${targetPath}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`\nScaffolding "${answers.projectName}" from template "${templateName}"...`)
|
|
35
|
+
|
|
36
|
+
await fs.copy(templatePath, targetPath)
|
|
37
|
+
|
|
38
|
+
await ScaffoldGenerator.replaceInAllFiles(targetPath, '__PROJECT_NAME__', answers.projectName)
|
|
39
|
+
|
|
40
|
+
const wmxrc = {
|
|
41
|
+
projectName: answers.projectName,
|
|
42
|
+
framework: answers.framework,
|
|
43
|
+
backend: answers.backend,
|
|
44
|
+
database: answers.database,
|
|
45
|
+
packageManager: answers.packageManager,
|
|
46
|
+
features: answers.features ?? [],
|
|
47
|
+
scaffoldedAt: new Date().toISOString(),
|
|
48
|
+
template: templateName
|
|
49
|
+
}
|
|
50
|
+
await fs.writeJson(path.join(targetPath, '.wmxrc.json'), wmxrc, { spaces: 2 })
|
|
51
|
+
|
|
52
|
+
console.log(`\nRunning ${answers.packageManager} install...`)
|
|
53
|
+
try {
|
|
54
|
+
if (templateName === 'react-express-mongo') {
|
|
55
|
+
await execa(answers.packageManager, ['install'], {
|
|
56
|
+
cwd: path.join(targetPath, 'frontend'),
|
|
57
|
+
stdio: 'inherit'
|
|
58
|
+
})
|
|
59
|
+
await execa(answers.packageManager, ['install'], {
|
|
60
|
+
cwd: path.join(targetPath, 'backend'),
|
|
61
|
+
stdio: 'inherit'
|
|
62
|
+
})
|
|
63
|
+
} else {
|
|
64
|
+
await execa(answers.packageManager, ['install'], {
|
|
65
|
+
cwd: targetPath,
|
|
66
|
+
stdio: 'inherit'
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.warn(`\nPackage install failed (you can run it manually): ${(err as Error).message}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`\n✅ Project "${answers.projectName}" created at ${targetPath}`)
|
|
74
|
+
console.log(` cd ${answers.projectName} and follow the README to get started.\n`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static resolveTemplate(answers: ScaffoldAnswers): string {
|
|
78
|
+
const fw = answers.framework?.toLowerCase() ?? ''
|
|
79
|
+
const be = answers.backend?.toLowerCase() ?? ''
|
|
80
|
+
const db = answers.database?.toLowerCase() ?? ''
|
|
81
|
+
|
|
82
|
+
if (fw.includes('next')) return 'nextjs-postgres'
|
|
83
|
+
if (fw.includes('react') && be.includes('express') && db.includes('mongo')) return 'react-express-mongo'
|
|
84
|
+
return 'react-express-mongo'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private static async replaceInAllFiles(dir: string, search: string, replacement: string): Promise<void> {
|
|
88
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
const fullPath = path.join(dir, entry.name)
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
await ScaffoldGenerator.replaceInAllFiles(fullPath, search, replacement)
|
|
93
|
+
} else {
|
|
94
|
+
try {
|
|
95
|
+
const content = await fs.readFile(fullPath, 'utf8')
|
|
96
|
+
if (content.includes(search)) {
|
|
97
|
+
const updated = content.split(search).join(replacement)
|
|
98
|
+
await fs.writeFile(fullPath, updated, 'utf8')
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Skip binary files silently
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DATABASE_URL=
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
A Next.js 14 + PostgreSQL + Drizzle ORM starter, scaffolded by WMX CLI.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp .env.example .env
|
|
9
|
+
# Set DATABASE_URL in .env (e.g. postgresql://user:pass@localhost:5432/dbname)
|
|
10
|
+
npm install
|
|
11
|
+
npm run db:push # push schema to your database
|
|
12
|
+
npm run dev # start dev server on http://localhost:3000
|
|
13
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "next dev",
|
|
6
|
+
"build": "next build",
|
|
7
|
+
"start": "next start",
|
|
8
|
+
"db:generate": "drizzle-kit generate",
|
|
9
|
+
"db:push": "drizzle-kit push"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"next": "^14.0.0",
|
|
13
|
+
"react": "^18.2.0",
|
|
14
|
+
"react-dom": "^18.2.0",
|
|
15
|
+
"pg": "^8.11.0",
|
|
16
|
+
"drizzle-orm": "^0.29.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.0.0",
|
|
20
|
+
"@types/pg": "^8.10.0",
|
|
21
|
+
"@types/react": "^18.2.0",
|
|
22
|
+
"@types/react-dom": "^18.2.0",
|
|
23
|
+
"drizzle-kit": "^0.20.0",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: '__PROJECT_NAME__',
|
|
5
|
+
description: 'Built with Next.js 14 and PostgreSQL'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<body>{children}</body>
|
|
12
|
+
</html>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export default function Home() {
|
|
2
|
+
return (
|
|
3
|
+
<main style={{ fontFamily: 'sans-serif', textAlign: 'center', marginTop: '4rem' }}>
|
|
4
|
+
<h1>Welcome to __PROJECT_NAME__</h1>
|
|
5
|
+
<p>Next.js 14 + PostgreSQL + Drizzle ORM starter — edit <code>src/app/page.tsx</code> to begin.</p>
|
|
6
|
+
</main>
|
|
7
|
+
)
|
|
8
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
A full-stack starter built with React 18 + Express + MongoDB, scaffolded by WMX CLI.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
### Backend
|
|
8
|
+
```bash
|
|
9
|
+
cd backend
|
|
10
|
+
cp .env.example .env
|
|
11
|
+
# Fill in MONGODB_URI and JWT_SECRET in .env
|
|
12
|
+
npm install
|
|
13
|
+
npm run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Frontend
|
|
17
|
+
```bash
|
|
18
|
+
cd frontend
|
|
19
|
+
npm install
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The frontend runs on http://localhost:5173 and proxies `/api` requests to the backend on port 5000.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "ts-node-dev --respawn src/index.ts",
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"start": "node dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"cors": "^2.8.5",
|
|
12
|
+
"dotenv": "^16.0.0",
|
|
13
|
+
"express": "^4.18.0",
|
|
14
|
+
"mongoose": "^8.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/cors": "^2.8.13",
|
|
18
|
+
"@types/express": "^4.17.21",
|
|
19
|
+
"@types/node": "^20.0.0",
|
|
20
|
+
"ts-node-dev": "^2.0.0",
|
|
21
|
+
"typescript": "^5.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import mongoose from 'mongoose'
|
|
2
|
+
|
|
3
|
+
export async function connectDB(): Promise<void> {
|
|
4
|
+
const uri = process.env.MONGODB_URI
|
|
5
|
+
if (!uri) {
|
|
6
|
+
console.warn('MONGODB_URI not set — skipping DB connection')
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
await mongoose.connect(uri)
|
|
11
|
+
console.log('MongoDB connected')
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error('MongoDB connection error:', err)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import cors from 'cors'
|
|
3
|
+
import dotenv from 'dotenv'
|
|
4
|
+
import { connectDB } from './db'
|
|
5
|
+
|
|
6
|
+
dotenv.config()
|
|
7
|
+
|
|
8
|
+
const app = express()
|
|
9
|
+
const PORT = process.env.PORT || 5000
|
|
10
|
+
|
|
11
|
+
app.use(cors())
|
|
12
|
+
app.use(express.json())
|
|
13
|
+
|
|
14
|
+
app.get('/api/health', (_req, res) => {
|
|
15
|
+
res.json({ status: 'ok', project: '__PROJECT_NAME__' })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
connectDB().then(() => {
|
|
19
|
+
app.listen(PORT, () => {
|
|
20
|
+
console.log(`__PROJECT_NAME__ backend running on http://localhost:${PORT}`)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__PROJECT_NAME__</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "tsc && vite build",
|
|
8
|
+
"preview": "vite preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"react": "^18.2.0",
|
|
12
|
+
"react-dom": "^18.2.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/react": "^18.2.0",
|
|
16
|
+
"@types/react-dom": "^18.2.0",
|
|
17
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
18
|
+
"typescript": "^5.0.0",
|
|
19
|
+
"vite": "^5.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
function App() {
|
|
4
|
+
return (
|
|
5
|
+
<div style={{ fontFamily: 'sans-serif', textAlign: 'center', marginTop: '4rem' }}>
|
|
6
|
+
<h1>Welcome to __PROJECT_NAME__</h1>
|
|
7
|
+
<p>React + Express + MongoDB starter — edit <code>src/App.tsx</code> to get started.</p>
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default App
|