boilercraft 1.0.0 → 1.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/README.md +70 -5
- package/bin/boilercraft.js +34 -4
- package/cli/app.js +144 -27
- package/cli/ui.js +67 -1
- package/package.json +17 -5
- package/public/index.html +25 -1
- package/server.js +3 -1
- package/services/engine/extras.js +354 -0
- package/services/engine/postCreate.js +30 -0
- package/services/engine/scaffoldEngine.js +34 -8
- package/services/engine/versionCatalog.js +16 -0
- package/services/generate.js +2 -2
- package/services/presets.js +87 -0
- package/services/updateNotifier.js +50 -0
- package/stacks/django/overlays/common/templates/home.html +20 -0
- package/stacks/django/snippets/settings.py +27 -0
- package/stacks/django/stack.json +42 -0
- package/stacks/dotnet-core/stack.json +6 -0
- package/stacks/fastapi/overlays/common/app/__init__.py +0 -0
- package/stacks/fastapi/overlays/common/app/main.py +22 -0
- package/stacks/fastapi/overlays/common/app/static/index.html +20 -0
- package/stacks/fastapi/overlays/db/app/db.py +30 -0
- package/stacks/fastapi/stack.json +40 -0
- package/stacks/go-gin/overlays/common/main.go +32 -0
- package/stacks/go-gin/overlays/common/public/index.html +20 -0
- package/stacks/go-gin/stack.json +28 -0
- package/stacks/laravel/stack.json +12 -0
- package/stacks/nestjs/stack.json +46 -0
- package/stacks/nextjs/stack.json +12 -0
- package/stacks/node-express/stack.json +11 -0
- package/stacks/raw-php/stack.json +25 -0
- package/stacks/vite/stack.json +61 -0
- package/templates/raw-php-mysql/config/Database.php +1 -0
- package/docs/STACK_ENGINE.md +0 -93
- package/docs/assets/cli-create.svg +0 -52
- package/docs/assets/cli-menu.svg +0 -47
- package/docs/stacks/dotnet-core.md +0 -69
- package/docs/stacks/laravel-vue.md +0 -46
- package/docs/stacks/nextjs.md +0 -58
- package/docs/stacks/node-express.md +0 -58
- package/docs/stacks/raw-php.md +0 -130
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "boilercraft",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Create Laravel, Next.js, Express and .NET projects with the official tooling — any version, auth, themes. Run: npx boilercraft",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Create Laravel, Next.js, NestJS, Vite, Django, FastAPI, Go, Express and .NET projects with the official tooling — any version, auth, themes, Docker, CI. Run: npx boilercraft",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node server.js",
|
|
8
8
|
"desktop": "electron .",
|
|
9
9
|
"dev": "node server.js",
|
|
10
10
|
"build:exe": "electron-builder --win portable",
|
|
11
|
-
"cli": "node bin/boilercraft.js"
|
|
11
|
+
"cli": "node bin/boilercraft.js",
|
|
12
|
+
"test": "node --test test/engine.test.js test/extras.test.js test/cli-services.test.js"
|
|
12
13
|
},
|
|
13
14
|
"build": {
|
|
14
15
|
"appId": "com.argrabbi.boilercraft",
|
|
@@ -41,7 +42,19 @@
|
|
|
41
42
|
"cli",
|
|
42
43
|
"scaffold",
|
|
43
44
|
"create-app",
|
|
44
|
-
"npx"
|
|
45
|
+
"npx",
|
|
46
|
+
"nestjs",
|
|
47
|
+
"vite",
|
|
48
|
+
"react",
|
|
49
|
+
"vue",
|
|
50
|
+
"svelte",
|
|
51
|
+
"django",
|
|
52
|
+
"fastapi",
|
|
53
|
+
"python",
|
|
54
|
+
"go",
|
|
55
|
+
"gin",
|
|
56
|
+
"docker",
|
|
57
|
+
"prisma"
|
|
45
58
|
],
|
|
46
59
|
"author": "ARG RABBI",
|
|
47
60
|
"license": "MIT",
|
|
@@ -73,7 +86,6 @@
|
|
|
73
86
|
"stacks/",
|
|
74
87
|
"templates/",
|
|
75
88
|
"public/",
|
|
76
|
-
"docs/",
|
|
77
89
|
"server.js",
|
|
78
90
|
"README.md",
|
|
79
91
|
"LICENSE"
|
package/public/index.html
CHANGED
|
@@ -530,6 +530,7 @@
|
|
|
530
530
|
const { stacks } = await res.json();
|
|
531
531
|
for (const stack of stacks) {
|
|
532
532
|
stackCatalog[stack.id] = stack;
|
|
533
|
+
addBackendCard(stack);
|
|
533
534
|
const select = document.getElementById(VERSION_SELECTS[stack.id]);
|
|
534
535
|
if (!select) continue;
|
|
535
536
|
select.innerHTML = '';
|
|
@@ -552,6 +553,26 @@
|
|
|
552
553
|
updateSummary();
|
|
553
554
|
}
|
|
554
555
|
|
|
556
|
+
// Stacks without a hand-made card above (NestJS, Vite, Django, FastAPI, Go …)
|
|
557
|
+
// get one from their manifest, so a new stack shows up here with no HTML change.
|
|
558
|
+
function addBackendCard(stack) {
|
|
559
|
+
const grid = document.getElementById('backendOptions');
|
|
560
|
+
if (grid.querySelector(`[data-stack="${stack.id}"]`)) return;
|
|
561
|
+
if ([...grid.querySelectorAll('.choice-card')].some(c => (c.getAttribute('onclick') || '').includes(`'${stack.id}'`))) return;
|
|
562
|
+
const card = document.createElement('div');
|
|
563
|
+
card.className = 'choice-card';
|
|
564
|
+
card.dataset.stack = stack.id;
|
|
565
|
+
card.onclick = () => selectBackend(stack.id, card);
|
|
566
|
+
const title = document.createElement('div');
|
|
567
|
+
title.className = 'choice-title';
|
|
568
|
+
title.textContent = stack.name;
|
|
569
|
+
const desc = document.createElement('div');
|
|
570
|
+
desc.className = 'choice-desc';
|
|
571
|
+
desc.textContent = stack.description || '';
|
|
572
|
+
card.append(title, desc);
|
|
573
|
+
grid.appendChild(card);
|
|
574
|
+
}
|
|
575
|
+
|
|
555
576
|
function renderEngineNote() {
|
|
556
577
|
const note = document.getElementById('engineNote');
|
|
557
578
|
const stack = stackCatalog[state.backend];
|
|
@@ -645,7 +666,10 @@
|
|
|
645
666
|
port: parseInt(document.getElementById('serverPort').value),
|
|
646
667
|
backend: state.backend,
|
|
647
668
|
stack: state.backend,
|
|
648
|
-
database
|
|
669
|
+
// Stacks like Vite or Go have their own database list (e.g. only 'none').
|
|
670
|
+
database: stackCatalog[state.backend] && !stackCatalog[state.backend].databases.includes(state.database)
|
|
671
|
+
? stackCatalog[state.backend].databases[0]
|
|
672
|
+
: state.database,
|
|
649
673
|
styling: state.styling,
|
|
650
674
|
// Keyed by stack id; values are major versions ("12", "16", …).
|
|
651
675
|
versions: {
|
package/server.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const cors = require('cors');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
|
|
4
|
+
// Every stack now goes through services/generate.js (manifest engine, with the
|
|
5
|
+
// template generator only as an offline fallback), so this direct import is unused.
|
|
6
|
+
// const { generateProject } = require('./services/projectGenerator');
|
|
5
7
|
const { getLatestNpmVersion, getLatestPackagistVersion } = require('./services/versionResolver');
|
|
6
8
|
const { describeStacks, RequirementError } = require('./services/engine/scaffoldEngine');
|
|
7
9
|
// generate() (engine + legacy fallback) lives in services/generate.js, shared with the CLI.
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optional add-ons that work across stacks: Docker, GitHub Actions CI,
|
|
6
|
+
* ESLint + Prettier and Prisma. A manifest opts in with
|
|
7
|
+
*
|
|
8
|
+
* "runtime": "node" | "php" | "dotnet" | "python" | "go",
|
|
9
|
+
* "extras": ["docker", "ci", "lint", "prisma"],
|
|
10
|
+
* "docker": { "build": "npm run build", "start": "npm start" },
|
|
11
|
+
* "ci": { "test": "php artisan test" }
|
|
12
|
+
*
|
|
13
|
+
* Files are generated from the runtime, so a new stack gets all of this by
|
|
14
|
+
* declaring three fields instead of copying templates around.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const EXTRAS = {
|
|
18
|
+
docker: { label: 'Docker', hint: 'Dockerfile + docker-compose with your database' },
|
|
19
|
+
ci: { label: 'GitHub Actions CI', hint: 'build + test on every push' },
|
|
20
|
+
lint: { label: 'ESLint + Prettier', hint: 'lint and format scripts' },
|
|
21
|
+
prisma: { label: 'Prisma ORM', hint: 'schema, client and migrations' }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function supportedExtras(stack) {
|
|
25
|
+
return (stack.extras || []).filter(x => EXTRAS[x]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const write = (dir, rel, content) => {
|
|
29
|
+
const file = path.join(dir, rel);
|
|
30
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
31
|
+
fs.writeFileSync(file, content.replace(/^\n/, ''));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Docker
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
function dockerfile(stack, ctx) {
|
|
39
|
+
const port = ctx.vars.PORT;
|
|
40
|
+
const d = stack.docker || {};
|
|
41
|
+
const cmd = JSON.stringify((d.start || '').split(' ').filter(Boolean));
|
|
42
|
+
switch (stack.runtime) {
|
|
43
|
+
case 'node':
|
|
44
|
+
return `
|
|
45
|
+
FROM node:22-alpine
|
|
46
|
+
WORKDIR /app
|
|
47
|
+
COPY package*.json ./
|
|
48
|
+
RUN npm ci
|
|
49
|
+
COPY . .
|
|
50
|
+
${d.build ? `RUN ${d.build}\n` : ''}ENV PORT=${port}
|
|
51
|
+
EXPOSE ${port}
|
|
52
|
+
CMD ${cmd}
|
|
53
|
+
`;
|
|
54
|
+
case 'php':
|
|
55
|
+
return `
|
|
56
|
+
FROM php:8.3-cli
|
|
57
|
+
RUN apt-get update && apt-get install -y git unzip libzip-dev \\
|
|
58
|
+
&& docker-php-ext-install pdo pdo_mysql zip && rm -rf /var/lib/apt/lists/*
|
|
59
|
+
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
|
60
|
+
WORKDIR /app
|
|
61
|
+
COPY . .
|
|
62
|
+
${fs.existsSync(path.join(ctx.projectDir, 'composer.json')) ? 'RUN composer install --no-dev --optimize-autoloader --no-interaction\n' : ''}EXPOSE ${port}
|
|
63
|
+
CMD ${cmd}
|
|
64
|
+
`;
|
|
65
|
+
case 'dotnet':
|
|
66
|
+
return `
|
|
67
|
+
FROM mcr.microsoft.com/dotnet/sdk:${ctx.major}.0 AS build
|
|
68
|
+
WORKDIR /src
|
|
69
|
+
COPY . .
|
|
70
|
+
RUN dotnet publish -c Release -o /out
|
|
71
|
+
|
|
72
|
+
FROM mcr.microsoft.com/dotnet/aspnet:${ctx.major}.0
|
|
73
|
+
WORKDIR /app
|
|
74
|
+
COPY --from=build /out .
|
|
75
|
+
ENV ASPNETCORE_URLS=http://+:${port}
|
|
76
|
+
EXPOSE ${port}
|
|
77
|
+
ENTRYPOINT ["dotnet", "${ctx.name}.dll"]
|
|
78
|
+
`;
|
|
79
|
+
case 'python':
|
|
80
|
+
return `
|
|
81
|
+
FROM python:3.12-slim
|
|
82
|
+
WORKDIR /app
|
|
83
|
+
COPY requirements.txt .
|
|
84
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
85
|
+
COPY . .
|
|
86
|
+
EXPOSE ${port}
|
|
87
|
+
CMD ${cmd}
|
|
88
|
+
`;
|
|
89
|
+
case 'go':
|
|
90
|
+
return `
|
|
91
|
+
FROM golang:1-alpine AS build
|
|
92
|
+
WORKDIR /src
|
|
93
|
+
COPY go.* ./
|
|
94
|
+
RUN go mod download
|
|
95
|
+
COPY . .
|
|
96
|
+
RUN CGO_ENABLED=0 go build -o /app .
|
|
97
|
+
|
|
98
|
+
FROM alpine:3
|
|
99
|
+
COPY --from=build /app /app
|
|
100
|
+
COPY --from=build /src/public /public
|
|
101
|
+
WORKDIR /
|
|
102
|
+
ENV PORT=${port}
|
|
103
|
+
EXPOSE ${port}
|
|
104
|
+
ENTRYPOINT ["/app"]
|
|
105
|
+
`;
|
|
106
|
+
default:
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const DB_SERVICES = {
|
|
112
|
+
mysql: name => `
|
|
113
|
+
db:
|
|
114
|
+
image: mysql:8
|
|
115
|
+
environment:
|
|
116
|
+
MYSQL_DATABASE: ${name}
|
|
117
|
+
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
|
118
|
+
ports: ["3306:3306"]
|
|
119
|
+
volumes: [db-data:/var/lib/mysql]
|
|
120
|
+
`,
|
|
121
|
+
postgresql: name => `
|
|
122
|
+
db:
|
|
123
|
+
image: postgres:16
|
|
124
|
+
environment:
|
|
125
|
+
POSTGRES_DB: ${name}
|
|
126
|
+
POSTGRES_PASSWORD: postgres
|
|
127
|
+
ports: ["5432:5432"]
|
|
128
|
+
volumes: [db-data:/var/lib/postgresql/data]
|
|
129
|
+
`,
|
|
130
|
+
mongodb: () => `
|
|
131
|
+
db:
|
|
132
|
+
image: mongo:7
|
|
133
|
+
ports: ["27017:27017"]
|
|
134
|
+
volumes: [db-data:/data/db]
|
|
135
|
+
`,
|
|
136
|
+
mssql: () => `
|
|
137
|
+
db:
|
|
138
|
+
image: mcr.microsoft.com/mssql/server:2022-latest
|
|
139
|
+
environment:
|
|
140
|
+
ACCEPT_EULA: "Y"
|
|
141
|
+
MSSQL_SA_PASSWORD: \${DB_PASSWORD:-Change_me_123}
|
|
142
|
+
ports: ["1433:1433"]
|
|
143
|
+
volumes: [db-data:/var/opt/mssql]
|
|
144
|
+
`
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function applyDocker(stack, ctx) {
|
|
148
|
+
const file = dockerfile(stack, ctx);
|
|
149
|
+
if (!file) return ctx.warn(`Docker: no Dockerfile recipe for runtime '${stack.runtime}'`);
|
|
150
|
+
const { projectDir, vars, database } = ctx;
|
|
151
|
+
write(projectDir, 'Dockerfile', file);
|
|
152
|
+
write(projectDir, '.dockerignore', `
|
|
153
|
+
node_modules
|
|
154
|
+
vendor
|
|
155
|
+
bin
|
|
156
|
+
obj
|
|
157
|
+
.venv
|
|
158
|
+
__pycache__
|
|
159
|
+
.git
|
|
160
|
+
.env
|
|
161
|
+
`);
|
|
162
|
+
const db = DB_SERVICES[database];
|
|
163
|
+
write(projectDir, 'docker-compose.yml', `
|
|
164
|
+
services:
|
|
165
|
+
app:
|
|
166
|
+
build: .
|
|
167
|
+
ports: ["${vars.PORT}:${vars.PORT}"]
|
|
168
|
+
env_file:
|
|
169
|
+
- path: .env
|
|
170
|
+
required: false
|
|
171
|
+
${db ? ' environment:\n DB_HOST: db\n depends_on: [db]\n' : ''}${db ? db(vars.DB_NAME) : ''}${db ? '\nvolumes:\n db-data:\n' : ''}`);
|
|
172
|
+
ctx.log('Added Dockerfile and docker-compose.yml');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// GitHub Actions
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
function ciSteps(stack, ctx) {
|
|
180
|
+
const test = stack.ci?.test;
|
|
181
|
+
switch (stack.runtime) {
|
|
182
|
+
case 'node':
|
|
183
|
+
return `
|
|
184
|
+
- uses: actions/setup-node@v4
|
|
185
|
+
with:
|
|
186
|
+
node-version: 22
|
|
187
|
+
cache: npm
|
|
188
|
+
- run: npm ci
|
|
189
|
+
- run: npm run lint --if-present
|
|
190
|
+
- run: npm run build --if-present
|
|
191
|
+
- run: ${test || 'npm test --if-present'}`;
|
|
192
|
+
case 'php':
|
|
193
|
+
return `
|
|
194
|
+
- uses: shivammathur/setup-php@v2
|
|
195
|
+
with:
|
|
196
|
+
php-version: '8.3'
|
|
197
|
+
${fs.existsSync(path.join(ctx.projectDir, 'composer.json')) ? ' - run: composer install --no-interaction --prefer-dist\n' : ''} - run: ${test || 'find . -name "*.php" -not -path "./vendor/*" -print0 | xargs -0 -n1 php -l'}`;
|
|
198
|
+
case 'dotnet':
|
|
199
|
+
return `
|
|
200
|
+
- uses: actions/setup-dotnet@v4
|
|
201
|
+
with:
|
|
202
|
+
dotnet-version: '${ctx.major}.0.x'
|
|
203
|
+
- run: dotnet build
|
|
204
|
+
- run: ${test || 'dotnet test --no-build'}`;
|
|
205
|
+
case 'python':
|
|
206
|
+
return `
|
|
207
|
+
- uses: actions/setup-python@v5
|
|
208
|
+
with:
|
|
209
|
+
python-version: '3.12'
|
|
210
|
+
- run: pip install -r requirements.txt
|
|
211
|
+
- run: ${test || 'python -m compileall -q .'}`;
|
|
212
|
+
case 'go':
|
|
213
|
+
return `
|
|
214
|
+
- uses: actions/setup-go@v5
|
|
215
|
+
with:
|
|
216
|
+
go-version: stable
|
|
217
|
+
- run: go vet ./...
|
|
218
|
+
- run: go build ./...
|
|
219
|
+
- run: ${test || 'go test ./...'}`;
|
|
220
|
+
default:
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function applyCi(stack, ctx) {
|
|
226
|
+
const steps = ciSteps(stack, ctx);
|
|
227
|
+
if (!steps) return ctx.warn(`CI: no workflow recipe for runtime '${stack.runtime}'`);
|
|
228
|
+
write(ctx.projectDir, '.github/workflows/ci.yml', `
|
|
229
|
+
name: CI
|
|
230
|
+
|
|
231
|
+
on:
|
|
232
|
+
push:
|
|
233
|
+
pull_request:
|
|
234
|
+
|
|
235
|
+
jobs:
|
|
236
|
+
build:
|
|
237
|
+
runs-on: ubuntu-latest
|
|
238
|
+
steps:
|
|
239
|
+
- uses: actions/checkout@v4${steps}
|
|
240
|
+
`);
|
|
241
|
+
ctx.log('Added GitHub Actions workflow');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// ESLint + Prettier (Node stacks)
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
function readPkg(dir) {
|
|
249
|
+
const file = path.join(dir, 'package.json');
|
|
250
|
+
return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function mergeScripts(dir, scripts) {
|
|
254
|
+
const pkg = readPkg(dir);
|
|
255
|
+
if (!pkg) return;
|
|
256
|
+
pkg.scripts = { ...scripts, ...pkg.scripts }; // never clobber the framework's own scripts
|
|
257
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function applyLint(stack, ctx, { runCommand }) {
|
|
261
|
+
const { projectDir } = ctx;
|
|
262
|
+
const pkg = readPkg(projectDir);
|
|
263
|
+
if (!pkg) return ctx.warn('ESLint + Prettier: no package.json, skipped');
|
|
264
|
+
const hasEslint = !!(pkg.devDependencies?.eslint || pkg.dependencies?.eslint);
|
|
265
|
+
|
|
266
|
+
ctx.log('Adding ESLint + Prettier');
|
|
267
|
+
const deps = ['prettier', 'eslint-config-prettier'];
|
|
268
|
+
if (!hasEslint) deps.push('eslint', '@eslint/js', 'globals');
|
|
269
|
+
await runCommand('npm', ['install', '-D', ...deps], projectDir, ctx.log);
|
|
270
|
+
|
|
271
|
+
if (!hasEslint && pkg.type === 'module') {
|
|
272
|
+
write(projectDir, 'eslint.config.js', `
|
|
273
|
+
import js from '@eslint/js';
|
|
274
|
+
import globals from 'globals';
|
|
275
|
+
import prettier from 'eslint-config-prettier';
|
|
276
|
+
|
|
277
|
+
export default [
|
|
278
|
+
{ ignores: ['node_modules/', 'dist/'] },
|
|
279
|
+
js.configs.recommended,
|
|
280
|
+
{ languageOptions: { ecmaVersion: 'latest', sourceType: 'module', globals: { ...globals.browser, ...globals.node } } },
|
|
281
|
+
prettier
|
|
282
|
+
];
|
|
283
|
+
`);
|
|
284
|
+
} else if (!hasEslint) {
|
|
285
|
+
write(projectDir, 'eslint.config.js', `
|
|
286
|
+
const js = require('@eslint/js');
|
|
287
|
+
const globals = require('globals');
|
|
288
|
+
const prettier = require('eslint-config-prettier');
|
|
289
|
+
|
|
290
|
+
module.exports = [
|
|
291
|
+
{ ignores: ['node_modules/', 'dist/', 'public/'] },
|
|
292
|
+
js.configs.recommended,
|
|
293
|
+
{ languageOptions: { ecmaVersion: 'latest', sourceType: 'commonjs', globals: globals.node } },
|
|
294
|
+
prettier
|
|
295
|
+
];
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
write(projectDir, '.prettierrc', '{\n "singleQuote": true,\n "semi": true,\n "printWidth": 100\n}\n');
|
|
299
|
+
write(projectDir, '.prettierignore', 'node_modules\ndist\n.next\nbuild\n');
|
|
300
|
+
mergeScripts(projectDir, { lint: 'eslint .', format: 'prettier --write .' });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Prisma (Node stacks)
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
const PRISMA_PROVIDERS = { mysql: 'mysql', postgresql: 'postgresql', mssql: 'sqlserver', mongodb: 'mongodb', sqlite: 'sqlite' };
|
|
308
|
+
|
|
309
|
+
function prismaUrl(database, db) {
|
|
310
|
+
return {
|
|
311
|
+
mysql: `mysql://root:@127.0.0.1:3306/${db}`,
|
|
312
|
+
postgresql: `postgresql://postgres:postgres@127.0.0.1:5432/${db}`,
|
|
313
|
+
mssql: `sqlserver://127.0.0.1:1433;database=${db};user=sa;password=change-me;trustServerCertificate=true`,
|
|
314
|
+
mongodb: `mongodb://127.0.0.1:27017/${db}`,
|
|
315
|
+
sqlite: 'file:./dev.db'
|
|
316
|
+
}[database];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function applyPrisma(stack, ctx, { runCommand }) {
|
|
320
|
+
const provider = PRISMA_PROVIDERS[ctx.database];
|
|
321
|
+
if (!provider) return ctx.warn(`Prisma: database '${ctx.database}' is not supported, skipped`);
|
|
322
|
+
const { projectDir } = ctx;
|
|
323
|
+
ctx.log('Adding Prisma ORM');
|
|
324
|
+
await runCommand('npm', ['install', '-D', 'prisma@6'], projectDir, ctx.log);
|
|
325
|
+
await runCommand('npm', ['install', '@prisma/client@6'], projectDir, ctx.log);
|
|
326
|
+
await runCommand('npx', ['prisma', 'init', '--datasource-provider', provider], projectDir, ctx.log);
|
|
327
|
+
|
|
328
|
+
const url = `DATABASE_URL="${prismaUrl(ctx.database, ctx.vars.DB_NAME)}"`;
|
|
329
|
+
for (const f of ['.env', '.env.local']) {
|
|
330
|
+
const file = path.join(projectDir, f);
|
|
331
|
+
if (!fs.existsSync(file)) continue;
|
|
332
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
333
|
+
fs.writeFileSync(file, /^DATABASE_URL=.*$/m.test(content) ? content.replace(/^DATABASE_URL=.*$/m, url) : content.replace(/\s*$/, `\n${url}\n`));
|
|
334
|
+
}
|
|
335
|
+
mergeScripts(projectDir, { 'db:migrate': 'prisma migrate dev', 'db:studio': 'prisma studio' });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
async function applyExtras(ctx, helpers) {
|
|
341
|
+
const { stack } = ctx;
|
|
342
|
+
for (const extra of ctx.extras || []) {
|
|
343
|
+
try {
|
|
344
|
+
if (extra === 'docker') applyDocker(stack, ctx);
|
|
345
|
+
else if (extra === 'ci') applyCi(stack, ctx);
|
|
346
|
+
else if (extra === 'lint') await applyLint(stack, ctx, helpers);
|
|
347
|
+
else if (extra === 'prisma') await applyPrisma(stack, ctx, helpers);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
ctx.warn(`${EXTRAS[extra].label} failed: ${err.message.split('\n')[0]}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
module.exports = { EXTRAS, supportedExtras, applyExtras, dockerfile };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Leaves the project as a git repository with one clean commit. Some official
|
|
6
|
+
* creators (create-next-app, nest new) already run `git init` and commit their
|
|
7
|
+
* own files; then only BoilerCraft's additions are committed on top.
|
|
8
|
+
* Never fails the scaffold — a missing git or identity just becomes a warning.
|
|
9
|
+
*/
|
|
10
|
+
async function initGit(ctx, capture) {
|
|
11
|
+
const { projectDir, log, warn } = ctx;
|
|
12
|
+
const git = (...args) => capture('git', args, projectDir);
|
|
13
|
+
|
|
14
|
+
if (!(await git('--version')).ok) return warn('git not found — skipped repository setup');
|
|
15
|
+
|
|
16
|
+
const fresh = !fs.existsSync(path.join(projectDir, '.git'));
|
|
17
|
+
if (fresh) {
|
|
18
|
+
const init = await git('init', '-b', 'main');
|
|
19
|
+
if (!init.ok && !(await git('init')).ok) return warn('git init failed — skipped repository setup');
|
|
20
|
+
}
|
|
21
|
+
await git('add', '-A');
|
|
22
|
+
if (!(await git('status', '--porcelain')).out.trim()) return log('Git repository ready');
|
|
23
|
+
|
|
24
|
+
const message = fresh ? 'Initial commit from BoilerCraft' : 'Add BoilerCraft auth, database and theme';
|
|
25
|
+
const commit = await git('commit', '-q', '-m', message);
|
|
26
|
+
if (commit.ok) return log(fresh ? 'Initialized git repository' : 'Committed BoilerCraft files');
|
|
27
|
+
warn('git commit failed (set user.name / user.email), files are staged — commit them yourself');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { initGit };
|
|
@@ -4,6 +4,8 @@ const crypto = require('crypto');
|
|
|
4
4
|
const { spawn } = require('child_process');
|
|
5
5
|
const { listVersions, majorOf } = require('./versionCatalog');
|
|
6
6
|
const { toolEnv } = require('./toolInstaller');
|
|
7
|
+
const { applyExtras, supportedExtras } = require('./extras');
|
|
8
|
+
const { initGit } = require('./postCreate');
|
|
7
9
|
const { generateThemeCSS, getThemeSwitcherJS } = require('../themeGenerator');
|
|
8
10
|
const { generateDynamicStackReadme } = require('../readmeGenerator');
|
|
9
11
|
const {
|
|
@@ -31,7 +33,8 @@ const {
|
|
|
31
33
|
* needs one more overlay folder + a `when: { version: ">=N" }` step.
|
|
32
34
|
*/
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
// BOILERCRAFT_STACKS_DIR lets tests point the engine at fixture stacks.
|
|
37
|
+
const STACKS_DIR = process.env.BOILERCRAFT_STACKS_DIR || path.join(__dirname, '..', '..', 'stacks');
|
|
35
38
|
|
|
36
39
|
class RequirementError extends Error {
|
|
37
40
|
constructor(missing) {
|
|
@@ -52,7 +55,8 @@ function loadStacks() {
|
|
|
52
55
|
.map(dir => {
|
|
53
56
|
const manifest = JSON.parse(fs.readFileSync(path.join(STACKS_DIR, dir, 'stack.json'), 'utf8'));
|
|
54
57
|
return { ...manifest, id: manifest.id || dir, dir: path.join(STACKS_DIR, dir) };
|
|
55
|
-
})
|
|
58
|
+
})
|
|
59
|
+
.sort((a, b) => (a.order ?? 99) - (b.order ?? 99));
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function getStack(id) {
|
|
@@ -96,6 +100,7 @@ function matches(when, ctx) {
|
|
|
96
100
|
if (when.auth !== undefined && when.auth !== ctx.auth) return false;
|
|
97
101
|
if (when.roles !== undefined && when.roles !== ctx.roles) return false;
|
|
98
102
|
if (when.platform !== undefined && !oneOf(when.platform, process.platform)) return false;
|
|
103
|
+
if (when.extra !== undefined && !(ctx.extras || []).includes(when.extra)) return false;
|
|
99
104
|
// Any other key refers to a stack-specific option (e.g. Next.js "router").
|
|
100
105
|
for (const [key, expected] of Object.entries(when)) {
|
|
101
106
|
if (BUILTIN_WHEN_KEYS.has(key)) continue;
|
|
@@ -104,7 +109,7 @@ function matches(when, ctx) {
|
|
|
104
109
|
return true;
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
const BUILTIN_WHEN_KEYS = new Set(['version', 'database', 'styling', 'auth', 'roles', 'platform']);
|
|
112
|
+
const BUILTIN_WHEN_KEYS = new Set(['version', 'database', 'styling', 'auth', 'roles', 'platform', 'extra']);
|
|
108
113
|
|
|
109
114
|
// Stack-specific options (manifest "options"), validated, with defaults filled in.
|
|
110
115
|
function resolveOptions(stack, given = {}) {
|
|
@@ -190,7 +195,9 @@ function buildVars(config, stack, ctx) {
|
|
|
190
195
|
const dbEnv = password => ({
|
|
191
196
|
mongodb: `MONGODB_URI=mongodb://127.0.0.1:27017/${vars.DB_NAME}`,
|
|
192
197
|
mysql: `DB_HOST=127.0.0.1\nDB_PORT=3306\nDB_DATABASE=${vars.DB_NAME}\nDB_USERNAME=root\nDB_PASSWORD=`,
|
|
193
|
-
mssql: `DB_HOST=127.0.0.1\nDB_PORT=1433\nDB_DATABASE=${vars.DB_NAME}\nDB_USERNAME=sa\nDB_PASSWORD=${password}
|
|
198
|
+
mssql: `DB_HOST=127.0.0.1\nDB_PORT=1433\nDB_DATABASE=${vars.DB_NAME}\nDB_USERNAME=sa\nDB_PASSWORD=${password}`,
|
|
199
|
+
postgresql: `DB_HOST=127.0.0.1\nDB_PORT=5432\nDB_DATABASE=${vars.DB_NAME}\nDB_USERNAME=postgres\nDB_PASSWORD=${password}`,
|
|
200
|
+
sqlite: 'DB_PATH=database.sqlite'
|
|
194
201
|
}[ctx.database] || '');
|
|
195
202
|
// Stack options are available as {{OPTION_<ID>}}, e.g. {{OPTION_FRONTEND}}.
|
|
196
203
|
for (const [id, value] of Object.entries(ctx.options || {})) vars[`OPTION_${id.toUpperCase()}`] = String(value);
|
|
@@ -253,7 +260,8 @@ function runCommand(cmd, args, cwd, log, extraEnv = {}) {
|
|
|
253
260
|
const child = spawn(line, {
|
|
254
261
|
cwd,
|
|
255
262
|
shell: true,
|
|
256
|
-
|
|
263
|
+
stdio: ['ignore', 'pipe', 'pipe'], // no stdin: a tool that prompts gets EOF instead of hanging
|
|
264
|
+
env: { ...toolEnv(), CI: '1', COMPOSER_NO_INTERACTION: '1', DOTNET_CLI_TELEMETRY_OPTOUT: '1', DOTNET_NOLOGO: '1', CHECKPOINT_DISABLE: '1', ...extraEnv }
|
|
257
265
|
});
|
|
258
266
|
let tail = '';
|
|
259
267
|
const onData = chunk => {
|
|
@@ -272,8 +280,12 @@ function runCommand(cmd, args, cwd, log, extraEnv = {}) {
|
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
function captureCommand(cmd, args) {
|
|
283
|
+
return captureIn(cmd, args);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function captureIn(cmd, args, cwd) {
|
|
275
287
|
return new Promise(resolve => {
|
|
276
|
-
const child = spawn([cmd, ...args].map(quoteArg).join(' '), { shell: true, env: toolEnv() });
|
|
288
|
+
const child = spawn([cmd, ...args].map(quoteArg).join(' '), { shell: true, cwd, env: toolEnv() });
|
|
277
289
|
let out = '';
|
|
278
290
|
child.stdout.on('data', c => (out += c));
|
|
279
291
|
child.stderr.on('data', c => (out += c));
|
|
@@ -325,6 +337,7 @@ async function executeStep(step, ctx) {
|
|
|
325
337
|
copyDir(path.join(stack.dir, step.copy), step.to ? resolvePath(step.to) : projectDir, vars, log);
|
|
326
338
|
} else if (step.write) {
|
|
327
339
|
const file = resolvePath(step.write);
|
|
340
|
+
if (step.ifMissing && fs.existsSync(file)) return;
|
|
328
341
|
ensureDir(path.dirname(file));
|
|
329
342
|
fs.writeFileSync(file, interpolate(readSource(step), vars));
|
|
330
343
|
} else if (step.prepend) {
|
|
@@ -334,6 +347,7 @@ async function executeStep(step, ctx) {
|
|
|
334
347
|
} else if (step.append) {
|
|
335
348
|
const file = resolvePath(step.append);
|
|
336
349
|
if (!fs.existsSync(file)) return warn(`append: ${step.append} not found, skipped`);
|
|
350
|
+
if (step.skipIfContains && fs.readFileSync(file, 'utf8').includes(step.skipIfContains)) return;
|
|
337
351
|
fs.appendFileSync(file, interpolate(readSource(step), vars));
|
|
338
352
|
} else if (step.replace) {
|
|
339
353
|
const file = resolvePath(step.replace);
|
|
@@ -376,6 +390,7 @@ async function executeStep(step, ctx) {
|
|
|
376
390
|
async function checkRequirements(stack, major) {
|
|
377
391
|
const missing = [];
|
|
378
392
|
for (const req of stack.requires || []) {
|
|
393
|
+
if (req.platform && !oneOf(req.platform, process.platform)) continue;
|
|
379
394
|
const { ok, out } = await captureCommand(req.cmd, req.args || ['--version']);
|
|
380
395
|
if (!ok) {
|
|
381
396
|
missing.push({ tool: req.tool || req.cmd, name: req.name || req.cmd, install: req.install, major });
|
|
@@ -413,6 +428,10 @@ async function describeStacks() {
|
|
|
413
428
|
databases: stack.databases,
|
|
414
429
|
options: stack.options || [],
|
|
415
430
|
stylings: stack.stylings,
|
|
431
|
+
extras: supportedExtras(stack),
|
|
432
|
+
auth: stack.auth !== false,
|
|
433
|
+
description: stack.description || '',
|
|
434
|
+
nextSteps: stack.nextSteps || [],
|
|
416
435
|
versions,
|
|
417
436
|
requirements: { ok: missing.length === 0, missing }
|
|
418
437
|
};
|
|
@@ -467,9 +486,10 @@ async function scaffold(config, outputBasePath = null, onLog = () => {}) {
|
|
|
467
486
|
major,
|
|
468
487
|
database,
|
|
469
488
|
styling,
|
|
470
|
-
auth: config.auth ? config.auth.enabled !== false : true,
|
|
489
|
+
auth: stack.auth === false ? false : config.auth ? config.auth.enabled !== false : true,
|
|
471
490
|
roles: !!config.auth?.roles,
|
|
472
491
|
options: resolveOptions(stack, config.options),
|
|
492
|
+
extras: (config.extras || []).filter(x => supportedExtras(stack).includes(x)),
|
|
473
493
|
parentDir,
|
|
474
494
|
projectDir,
|
|
475
495
|
log,
|
|
@@ -491,6 +511,9 @@ async function scaffold(config, outputBasePath = null, onLog = () => {}) {
|
|
|
491
511
|
);
|
|
492
512
|
}
|
|
493
513
|
|
|
514
|
+
await applyExtras(ctx, { runCommand, interpolate });
|
|
515
|
+
if (config.git !== false) await initGit(ctx, captureIn);
|
|
516
|
+
|
|
494
517
|
log('Done.');
|
|
495
518
|
return {
|
|
496
519
|
success: true,
|
|
@@ -498,7 +521,7 @@ async function scaffold(config, outputBasePath = null, onLog = () => {}) {
|
|
|
498
521
|
projectName: name,
|
|
499
522
|
path: projectDir,
|
|
500
523
|
version: major,
|
|
501
|
-
combination: [`${stack.id}@${major}`, ...Object.values(ctx.options), database, styling].join(' + '),
|
|
524
|
+
combination: [`${stack.id}@${major}`, ...Object.values(ctx.options), database, styling, ...ctx.extras].join(' + '),
|
|
502
525
|
nextSteps: (stack.nextSteps || []).map(s => interpolate(s, ctx.vars)),
|
|
503
526
|
warnings,
|
|
504
527
|
logs
|
|
@@ -513,5 +536,8 @@ module.exports = {
|
|
|
513
536
|
loadStacks,
|
|
514
537
|
parseMajor,
|
|
515
538
|
versionMatches,
|
|
539
|
+
matches,
|
|
540
|
+
interpolate,
|
|
541
|
+
resolveOptions,
|
|
516
542
|
RequirementError
|
|
517
543
|
};
|
|
@@ -55,6 +55,22 @@ const sources = {
|
|
|
55
55
|
return newestPerMajor(releases.map(r => r.version), minMajor);
|
|
56
56
|
},
|
|
57
57
|
|
|
58
|
+
async pypi({ package: pkg, minMajor = 0 }) {
|
|
59
|
+
const res = await axios.get(`https://pypi.org/pypi/${encodeURIComponent(pkg)}/json`, { timeout: 5000 });
|
|
60
|
+
return newestPerMajor(Object.keys(res.data.releases || {}), minMajor);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Go modules: proxy.golang.org lists every tagged version, one per line.
|
|
64
|
+
async goproxy({ package: mod, minMajor = 0 }) {
|
|
65
|
+
const res = await axios.get(`https://proxy.golang.org/${mod.toLowerCase()}/@v/list`, { timeout: 5000, responseType: 'text' });
|
|
66
|
+
return newestPerMajor(String(res.data).split('\n').filter(Boolean), minMajor);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// Stacks with no upstream framework release (e.g. raw PHP) pin their list.
|
|
70
|
+
async static({ offline = [] }) {
|
|
71
|
+
return offline.map(major => ({ major, latest: `${major}.x` }));
|
|
72
|
+
},
|
|
73
|
+
|
|
58
74
|
async dotnet({ minMajor = 0 }) {
|
|
59
75
|
const res = await axios.get(
|
|
60
76
|
'https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json',
|
package/services/generate.js
CHANGED
|
@@ -21,8 +21,8 @@ async function runLegacy(config, warning) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Every stack has a manifest in stacks/ and is scaffolded by the engine;
|
|
25
|
+
* the template generator is only an offline fallback. If the
|
|
26
26
|
* required toolchain is missing, fall back to templates unless the caller
|
|
27
27
|
* opted out with allowLegacyFallback: false.
|
|
28
28
|
*/
|