webspresso 0.0.98 → 0.0.100
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/.agents/ADMIN.md +11 -1
- package/.agents/AGENTS.md +2 -2
- package/.agents/AUTH.md +415 -0
- package/.agents/CLI.md +47 -0
- package/.agents/ERRORS.md +59 -0
- package/.agents/ORM.md +178 -4
- package/.agents/PLUGINS.md +113 -8
- package/.agents/QUEUE.md +92 -0
- package/.agents/ROUTING.md +160 -7
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/webspresso.js +4 -0
- package/core/auth/index.js +123 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +477 -3
- package/core/auth/middleware.js +118 -5
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +507 -0
- package/core/auth/user-agent.js +161 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +10 -1
- package/core/orm/model.js +55 -1
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +327 -16
- package/core/orm/scopes.js +3 -2
- package/core/orm/search.js +213 -0
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +544 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +100 -1
- package/core/queue/scheduler.js +415 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +794 -6
- package/index.js +48 -0
- package/package.json +7 -5
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/cors.js +13 -2
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/file-manager/admin-component.js +69 -53
- package/plugins/file-manager/index.js +25 -8
- package/plugins/index.js +8 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/api/api-loader.js +1 -0
- package/src/bundle/index.js +2 -1
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +310 -104
- package/src/helpers.js +16 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/routing/cascading.js +490 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add Docker Command
|
|
3
|
+
* Generate production-ready multi-stage Dockerfile, .dockerignore, and docker-compose.yml
|
|
4
|
+
* @module bin/commands/add-docker
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalize Node image version string
|
|
14
|
+
* @param {string} [nodeVer='22-alpine']
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
function normalizeNodeVersion(nodeVer = '22-alpine') {
|
|
18
|
+
let v = String(nodeVer).trim();
|
|
19
|
+
if (v.startsWith('node:')) {
|
|
20
|
+
v = v.slice(5);
|
|
21
|
+
}
|
|
22
|
+
if (!v.includes('-')) {
|
|
23
|
+
v = `${v}-alpine`;
|
|
24
|
+
}
|
|
25
|
+
return v;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Auto-detect database from package.json or knexfile.js
|
|
30
|
+
* @param {string} cwd
|
|
31
|
+
* @param {Object} [pkg]
|
|
32
|
+
* @returns {'postgres'|'mysql'|'sqlite'|'none'}
|
|
33
|
+
*/
|
|
34
|
+
function detectDatabase(cwd, pkg = {}) {
|
|
35
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
36
|
+
|
|
37
|
+
if (deps['pg'] || deps['pg-hstore']) return 'postgres';
|
|
38
|
+
if (deps['mysql2'] || deps['mysql']) return 'mysql';
|
|
39
|
+
if (deps['better-sqlite3'] || deps['sqlite3']) return 'sqlite';
|
|
40
|
+
|
|
41
|
+
// Check knexfile.js or webspresso.db.js
|
|
42
|
+
const knexfilePath = path.join(cwd, 'knexfile.js');
|
|
43
|
+
if (fs.existsSync(knexfilePath)) {
|
|
44
|
+
try {
|
|
45
|
+
const content = fs.readFileSync(knexfilePath, 'utf8');
|
|
46
|
+
if (content.includes('postgresql') || content.includes('pg')) return 'postgres';
|
|
47
|
+
if (content.includes('mysql')) return 'mysql';
|
|
48
|
+
if (content.includes('sqlite')) return 'sqlite';
|
|
49
|
+
} catch (_) {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return 'sqlite';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate production-grade multi-stage Dockerfile
|
|
57
|
+
* @param {Object} options
|
|
58
|
+
* @param {string} [options.node='22-alpine']
|
|
59
|
+
* @param {number} [options.port=3000]
|
|
60
|
+
* @param {boolean} [options.hasBuildScript=false]
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function generateDockerfile(options = {}) {
|
|
64
|
+
const nodeImage = normalizeNodeVersion(options.node || '22-alpine');
|
|
65
|
+
const port = options.port || 3000;
|
|
66
|
+
const isAlpine = nodeImage.includes('alpine');
|
|
67
|
+
|
|
68
|
+
return `# ==========================================
|
|
69
|
+
# Webspresso Multi-Stage Production Dockerfile
|
|
70
|
+
# Security-hardened, non-root, minimal image size
|
|
71
|
+
# ==========================================
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------
|
|
74
|
+
# Stage 1: Dependencies (Native builds cache)
|
|
75
|
+
# ------------------------------------------
|
|
76
|
+
FROM node:${nodeImage} AS deps
|
|
77
|
+
${isAlpine ? 'RUN apk add --no-cache libc6-compat python3 make g++' : 'RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*'}
|
|
78
|
+
WORKDIR /app
|
|
79
|
+
|
|
80
|
+
# Copy dependency manifests
|
|
81
|
+
COPY package*.json ./
|
|
82
|
+
RUN npm ci --omit=dev && npm cache clean --force
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------
|
|
85
|
+
# Stage 2: Builder (Compile assets & app)
|
|
86
|
+
# ------------------------------------------
|
|
87
|
+
FROM node:${nodeImage} AS builder
|
|
88
|
+
WORKDIR /app
|
|
89
|
+
|
|
90
|
+
COPY package*.json ./
|
|
91
|
+
RUN npm ci
|
|
92
|
+
COPY . .
|
|
93
|
+
|
|
94
|
+
# Run build if a build script is defined in package.json
|
|
95
|
+
RUN if grep -q '"build":' package.json; then npm run build; fi
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------
|
|
98
|
+
# Stage 3: Runner (Production Image)
|
|
99
|
+
# ------------------------------------------
|
|
100
|
+
FROM node:${nodeImage} AS runner
|
|
101
|
+
WORKDIR /app
|
|
102
|
+
|
|
103
|
+
ENV NODE_ENV=production
|
|
104
|
+
ENV PORT=${port}
|
|
105
|
+
|
|
106
|
+
# Create unprivileged system group and user
|
|
107
|
+
${isAlpine
|
|
108
|
+
? 'RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 webspresso'
|
|
109
|
+
: 'RUN groupadd --system --gid 1001 nodejs && useradd --system --uid 1001 webspresso'}
|
|
110
|
+
|
|
111
|
+
# Copy dependencies and source code
|
|
112
|
+
COPY --from=deps --chown=webspresso:nodejs /app/node_modules ./node_modules
|
|
113
|
+
COPY --chown=webspresso:nodejs . .
|
|
114
|
+
|
|
115
|
+
# Copy compiled public assets from builder if present
|
|
116
|
+
COPY --from=builder --chown=webspresso:nodejs /app/public ./public
|
|
117
|
+
|
|
118
|
+
# Ensure upload and storage directories exist with proper write permissions
|
|
119
|
+
RUN mkdir -p storage/uploads public/uploads data && \\
|
|
120
|
+
chown -R webspresso:nodejs storage public data
|
|
121
|
+
|
|
122
|
+
# Run as non-root user
|
|
123
|
+
USER webspresso
|
|
124
|
+
|
|
125
|
+
EXPOSE ${port}
|
|
126
|
+
|
|
127
|
+
# Container Healthcheck (checks /health or /healthz endpoint)
|
|
128
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\
|
|
129
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:${port}/health || \\
|
|
130
|
+
wget --no-verbose --tries=1 --spider http://localhost:${port}/healthz || \\
|
|
131
|
+
exit 1
|
|
132
|
+
|
|
133
|
+
# Start the Webspresso application
|
|
134
|
+
CMD ["node", "server.js"]
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generate .dockerignore file content
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
function generateDockerignore() {
|
|
143
|
+
return `# Dependencies
|
|
144
|
+
node_modules
|
|
145
|
+
npm-debug.log*
|
|
146
|
+
yarn-debug.log*
|
|
147
|
+
yarn-error.log*
|
|
148
|
+
pnpm-debug.log*
|
|
149
|
+
|
|
150
|
+
# Git & IDE
|
|
151
|
+
.git
|
|
152
|
+
.gitignore
|
|
153
|
+
.gitattributes
|
|
154
|
+
.DS_Store
|
|
155
|
+
.idea
|
|
156
|
+
.vscode
|
|
157
|
+
*.suo
|
|
158
|
+
*.ntvs*
|
|
159
|
+
*.njsproj
|
|
160
|
+
|
|
161
|
+
# Environment & Secrets
|
|
162
|
+
.env
|
|
163
|
+
.env.*
|
|
164
|
+
!.env.example
|
|
165
|
+
|
|
166
|
+
# Local SQLite databases
|
|
167
|
+
*.sqlite
|
|
168
|
+
*.sqlite3
|
|
169
|
+
*.db
|
|
170
|
+
*.db-journal
|
|
171
|
+
*.db-wal
|
|
172
|
+
*.db-shm
|
|
173
|
+
|
|
174
|
+
# Tests & Coverage
|
|
175
|
+
tests
|
|
176
|
+
coverage
|
|
177
|
+
.nyc_output
|
|
178
|
+
playwright-report
|
|
179
|
+
test-results
|
|
180
|
+
|
|
181
|
+
# Temporary & Build Cache
|
|
182
|
+
dist/tmp
|
|
183
|
+
.turbo
|
|
184
|
+
.agents
|
|
185
|
+
docs
|
|
186
|
+
|
|
187
|
+
# Uploads directory (keep directory, exclude uploaded user media)
|
|
188
|
+
storage/uploads/*
|
|
189
|
+
!storage/uploads/.gitkeep
|
|
190
|
+
public/uploads/*
|
|
191
|
+
!public/uploads/.gitkeep
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate docker-compose.yml file content
|
|
197
|
+
* @param {Object} options
|
|
198
|
+
* @param {string} options.appName
|
|
199
|
+
* @param {'postgres'|'mysql'|'sqlite'|'none'} options.db
|
|
200
|
+
* @param {boolean} [options.redis=false]
|
|
201
|
+
* @param {number} [options.port=3000]
|
|
202
|
+
* @returns {string}
|
|
203
|
+
*/
|
|
204
|
+
function generateDockerCompose(options = {}) {
|
|
205
|
+
const {
|
|
206
|
+
appName = 'webspresso-app',
|
|
207
|
+
db = 'sqlite',
|
|
208
|
+
redis = false,
|
|
209
|
+
port = 3000,
|
|
210
|
+
} = options;
|
|
211
|
+
|
|
212
|
+
let dbService = '';
|
|
213
|
+
let dbVolume = '';
|
|
214
|
+
let dependsOn = [];
|
|
215
|
+
let appEnv = [
|
|
216
|
+
' - NODE_ENV=production',
|
|
217
|
+
` - PORT=${port}`,
|
|
218
|
+
' - SESSION_SECRET=change-me-to-a-random-32-char-string-in-production',
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
if (db === 'postgres') {
|
|
222
|
+
dependsOn.push(' postgres:\n condition: service_healthy');
|
|
223
|
+
appEnv.push(' - DATABASE_URL=postgres://webspresso:secretpassword@postgres:5432/webspresso');
|
|
224
|
+
dbService = `
|
|
225
|
+
postgres:
|
|
226
|
+
image: postgres:16-alpine
|
|
227
|
+
container_name: ${appName}-postgres
|
|
228
|
+
environment:
|
|
229
|
+
POSTGRES_DB: webspresso
|
|
230
|
+
POSTGRES_USER: webspresso
|
|
231
|
+
POSTGRES_PASSWORD: secretpassword
|
|
232
|
+
ports:
|
|
233
|
+
- "5432:5432"
|
|
234
|
+
volumes:
|
|
235
|
+
- postgres_data:/var/lib/postgresql/data
|
|
236
|
+
healthcheck:
|
|
237
|
+
test: ["CMD-SHELL", "pg_isready -U webspresso -d webspresso"]
|
|
238
|
+
interval: 10s
|
|
239
|
+
timeout: 5s
|
|
240
|
+
retries: 5
|
|
241
|
+
restart: unless-stopped
|
|
242
|
+
`;
|
|
243
|
+
dbVolume = ' postgres_data:\n';
|
|
244
|
+
} else if (db === 'mysql') {
|
|
245
|
+
dependsOn.push(' mysql:\n condition: service_healthy');
|
|
246
|
+
appEnv.push(' - DATABASE_URL=mysql://webspresso:secretpassword@mysql:3306/webspresso');
|
|
247
|
+
dbService = `
|
|
248
|
+
mysql:
|
|
249
|
+
image: mysql:8.0
|
|
250
|
+
container_name: ${appName}-mysql
|
|
251
|
+
environment:
|
|
252
|
+
MYSQL_DATABASE: webspresso
|
|
253
|
+
MYSQL_USER: webspresso
|
|
254
|
+
MYSQL_PASSWORD: secretpassword
|
|
255
|
+
MYSQL_ROOT_PASSWORD: rootsecretpassword
|
|
256
|
+
ports:
|
|
257
|
+
- "3306:3306"
|
|
258
|
+
volumes:
|
|
259
|
+
- mysql_data:/var/lib/mysql
|
|
260
|
+
healthcheck:
|
|
261
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
|
262
|
+
interval: 10s
|
|
263
|
+
timeout: 5s
|
|
264
|
+
retries: 5
|
|
265
|
+
restart: unless-stopped
|
|
266
|
+
`;
|
|
267
|
+
dbVolume = ' mysql_data:\n';
|
|
268
|
+
} else if (db === 'sqlite') {
|
|
269
|
+
appEnv.push(' - DATABASE_CLIENT=better-sqlite3');
|
|
270
|
+
appEnv.push(' - DATABASE_FILENAME=/app/data/app.sqlite');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let redisService = '';
|
|
274
|
+
let redisVolume = '';
|
|
275
|
+
if (redis) {
|
|
276
|
+
dependsOn.push(' redis:\n condition: service_healthy');
|
|
277
|
+
appEnv.push(' - REDIS_URL=redis://redis:6379');
|
|
278
|
+
redisService = `
|
|
279
|
+
redis:
|
|
280
|
+
image: redis:7-alpine
|
|
281
|
+
container_name: ${appName}-redis
|
|
282
|
+
ports:
|
|
283
|
+
- "6379:6379"
|
|
284
|
+
volumes:
|
|
285
|
+
- redis_data:/data
|
|
286
|
+
healthcheck:
|
|
287
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
288
|
+
interval: 10s
|
|
289
|
+
timeout: 5s
|
|
290
|
+
retries: 5
|
|
291
|
+
restart: unless-stopped
|
|
292
|
+
`;
|
|
293
|
+
redisVolume = ' redis_data:\n';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let dependsOnBlock = '';
|
|
297
|
+
if (dependsOn.length > 0) {
|
|
298
|
+
dependsOnBlock = ` depends_on:\n${dependsOn.join('\n')}\n`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const appVolumes = [' - ./storage/uploads:/app/storage/uploads'];
|
|
302
|
+
if (db === 'sqlite') {
|
|
303
|
+
appVolumes.push(' - ./data:/app/data');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let volumesSection = '';
|
|
307
|
+
if (dbVolume || redisVolume) {
|
|
308
|
+
volumesSection = `volumes:\n${dbVolume}${redisVolume}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return `services:
|
|
312
|
+
app:
|
|
313
|
+
build:
|
|
314
|
+
context: .
|
|
315
|
+
dockerfile: Dockerfile
|
|
316
|
+
container_name: ${appName}
|
|
317
|
+
ports:
|
|
318
|
+
- "${port}:${port}"
|
|
319
|
+
environment:
|
|
320
|
+
${appEnv.join('\n')}
|
|
321
|
+
volumes:
|
|
322
|
+
${appVolumes.join('\n')}
|
|
323
|
+
${dependsOnBlock} restart: unless-stopped
|
|
324
|
+
${dbService}${redisService}
|
|
325
|
+
${volumesSection}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Register command with Commander
|
|
330
|
+
* @param {import('commander').Command} program
|
|
331
|
+
*/
|
|
332
|
+
function registerCommand(program) {
|
|
333
|
+
program
|
|
334
|
+
.command('add:docker [docker]')
|
|
335
|
+
.alias('docker')
|
|
336
|
+
.alias('add-docker')
|
|
337
|
+
.description('Generate production-ready multi-stage Dockerfile, .dockerignore, and docker-compose.yml')
|
|
338
|
+
.option('-n, --node <version>', 'Node.js base image version (default: 22-alpine)', '22-alpine')
|
|
339
|
+
.option('-d, --db <type>', 'Database service to configure: postgres, mysql, sqlite, none (auto-detected by default)')
|
|
340
|
+
.option('-r, --redis', 'Include Redis service in docker-compose.yml', false)
|
|
341
|
+
.option('-p, --port <port>', 'Application container port (default: 3000)', '3000')
|
|
342
|
+
.option('-f, --force', 'Overwrite existing Dockerfile and docker-compose.yml without prompting', false)
|
|
343
|
+
.action(async (...actionArgs) => {
|
|
344
|
+
const cmd = actionArgs[actionArgs.length - 1];
|
|
345
|
+
const options = (cmd && typeof cmd.opts === 'function')
|
|
346
|
+
? cmd.opts()
|
|
347
|
+
: (actionArgs.find((a) => a && typeof a === 'object') || {});
|
|
348
|
+
|
|
349
|
+
const cwd = process.cwd();
|
|
350
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
351
|
+
|
|
352
|
+
if (!fs.existsSync(pkgPath)) {
|
|
353
|
+
console.error('❌ Not a Webspresso project! package.json was not found in current directory.');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let pkg = {};
|
|
358
|
+
try {
|
|
359
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error(`❌ Failed to parse package.json: ${err.message}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const appName = pkg.name ? pkg.name.replace(/[^a-zA-Z0-9_-]/g, '-') : 'webspresso-app';
|
|
366
|
+
const port = parseInt(options.port || '3000', 10) || 3000;
|
|
367
|
+
const dbType = options.db || detectDatabase(cwd, pkg);
|
|
368
|
+
const isRedis = Boolean(options.redis);
|
|
369
|
+
const nodeVersion = options.node || '22-alpine';
|
|
370
|
+
const force = Boolean(options.force);
|
|
371
|
+
|
|
372
|
+
console.log('\n🐳 Setting up Docker & Container configuration for Webspresso...\n');
|
|
373
|
+
|
|
374
|
+
const dockerfilePath = path.join(cwd, 'Dockerfile');
|
|
375
|
+
const dockerignorePath = path.join(cwd, '.dockerignore');
|
|
376
|
+
const composePath = path.join(cwd, 'docker-compose.yml');
|
|
377
|
+
|
|
378
|
+
// 1. Create .dockerignore
|
|
379
|
+
if (!fs.existsSync(dockerignorePath) || force) {
|
|
380
|
+
fs.writeFileSync(dockerignorePath, generateDockerignore(), 'utf8');
|
|
381
|
+
console.log(' ✅ Created .dockerignore');
|
|
382
|
+
} else {
|
|
383
|
+
console.log(' ℹ️ .dockerignore already exists (use --force to overwrite)');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 2. Create Dockerfile
|
|
387
|
+
if (!fs.existsSync(dockerfilePath) || force) {
|
|
388
|
+
const dockerfileContent = generateDockerfile({
|
|
389
|
+
node: nodeVersion,
|
|
390
|
+
port,
|
|
391
|
+
hasBuildScript: Boolean(pkg.scripts && pkg.scripts.build),
|
|
392
|
+
});
|
|
393
|
+
fs.writeFileSync(dockerfilePath, dockerfileContent, 'utf8');
|
|
394
|
+
console.log(` ✅ Created Dockerfile (multi-stage Node ${nodeVersion}, non-root webspresso user)`);
|
|
395
|
+
} else {
|
|
396
|
+
console.log(' ℹ️ Dockerfile already exists (use --force to overwrite)');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 3. Create docker-compose.yml
|
|
400
|
+
if (!fs.existsSync(composePath) || force) {
|
|
401
|
+
const composeContent = generateDockerCompose({
|
|
402
|
+
appName,
|
|
403
|
+
db: dbType,
|
|
404
|
+
redis: isRedis,
|
|
405
|
+
port,
|
|
406
|
+
});
|
|
407
|
+
fs.writeFileSync(composePath, composeContent, 'utf8');
|
|
408
|
+
console.log(` ✅ Created docker-compose.yml (app: port ${port}, db: ${dbType}${isRedis ? ', redis: true' : ''})`);
|
|
409
|
+
} else {
|
|
410
|
+
console.log(' ℹ️ docker-compose.yml already exists (use --force to overwrite)');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 4. Ensure local data & upload folders exist
|
|
414
|
+
if (dbType === 'sqlite') {
|
|
415
|
+
const dataDir = path.join(cwd, 'data');
|
|
416
|
+
if (!fs.existsSync(dataDir)) {
|
|
417
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
418
|
+
console.log(' ✅ Created data/ directory for persistent SQLite storage');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const storageDir = path.join(cwd, 'storage', 'uploads');
|
|
423
|
+
if (!fs.existsSync(storageDir)) {
|
|
424
|
+
fs.mkdirSync(storageDir, { recursive: true });
|
|
425
|
+
fs.writeFileSync(path.join(storageDir, '.gitkeep'), '', 'utf8');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 5. Update package.json scripts
|
|
429
|
+
pkg.scripts = pkg.scripts || {};
|
|
430
|
+
let pkgModified = false;
|
|
431
|
+
|
|
432
|
+
const dockerScripts = {
|
|
433
|
+
'docker:build': `docker build -t ${appName} .`,
|
|
434
|
+
'docker:run': `docker run -p ${port}:${port} --env-file .env ${appName}`,
|
|
435
|
+
'docker:up': 'docker compose up -d',
|
|
436
|
+
'docker:down': 'docker compose down',
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
for (const [key, cmdStr] of Object.entries(dockerScripts)) {
|
|
440
|
+
if (!pkg.scripts[key] || force) {
|
|
441
|
+
pkg.scripts[key] = cmdStr;
|
|
442
|
+
pkgModified = true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (pkgModified) {
|
|
447
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
448
|
+
console.log(' ✅ Added docker:* helper scripts to package.json');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log('\n🎉 Docker setup complete!\n');
|
|
452
|
+
console.log('Next steps:');
|
|
453
|
+
console.log(` 1. Build image: npm run docker:build`);
|
|
454
|
+
console.log(` 2. Run container: npm run docker:run`);
|
|
455
|
+
console.log(` 3. Start all stack: npm run docker:up\n`);
|
|
456
|
+
console.log(` Run preflight check: npx webspresso deploy:check\n`);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
module.exports = {
|
|
461
|
+
registerCommand,
|
|
462
|
+
generateDockerfile,
|
|
463
|
+
generateDockerignore,
|
|
464
|
+
generateDockerCompose,
|
|
465
|
+
detectDatabase,
|
|
466
|
+
normalizeNodeVersion,
|
|
467
|
+
};
|