webspresso 0.0.13 → 0.0.14
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 +4 -7
- package/bin/commands/add-tailwind.js +151 -0
- package/bin/commands/api.js +70 -0
- package/bin/commands/db-make.js +76 -0
- package/bin/commands/db-migrate.js +43 -0
- package/bin/commands/db-rollback.js +48 -0
- package/bin/commands/db-status.js +53 -0
- package/bin/commands/dev.js +73 -0
- package/bin/commands/new.js +634 -0
- package/bin/commands/page.js +134 -0
- package/bin/commands/seed.js +154 -0
- package/bin/commands/start.js +30 -0
- package/bin/utils/db.js +54 -0
- package/bin/utils/migration.js +36 -0
- package/bin/utils/project.js +97 -0
- package/bin/utils/seed.js +112 -0
- package/bin/webspresso.js +24 -1696
- package/core/orm/index.js +14 -1
- package/core/orm/migrations/scaffold.js +5 -0
- package/core/orm/model.js +8 -0
- package/core/orm/schema-helpers.js +39 -1
- package/core/orm/seeder.js +56 -3
- package/core/orm/types.js +28 -1
- package/index.js +2 -1
- package/package.json +1 -1
- package/plugins/admin-panel/admin-user-model.js +42 -0
- package/plugins/admin-panel/api.js +436 -0
- package/plugins/admin-panel/app.js +68 -0
- package/plugins/admin-panel/auth.js +157 -0
- package/plugins/admin-panel/components.js +359 -0
- package/plugins/admin-panel/field-renderers/array.js +57 -0
- package/plugins/admin-panel/field-renderers/basic.js +205 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +124 -0
- package/plugins/admin-panel/field-renderers/index.js +93 -0
- package/plugins/admin-panel/field-renderers/json.js +52 -0
- package/plugins/admin-panel/field-renderers/relations.js +96 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +83 -0
- package/plugins/admin-panel/index.js +187 -0
- package/plugins/admin-panel/migration-template.js +39 -0
- package/plugins/admin-panel/styles.js +9 -0
- package/plugins/index.js +2 -0
package/bin/webspresso.js
CHANGED
|
@@ -6,1708 +6,36 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { program } = require('commander');
|
|
9
|
-
const inquirer = require('inquirer');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const { spawn, execSync } = require('child_process');
|
|
13
9
|
|
|
14
10
|
program
|
|
15
11
|
.name('webspresso')
|
|
16
12
|
.description('Webspresso CLI - Minimal file-based SSR framework')
|
|
17
13
|
.version(require('../package.json').version);
|
|
18
14
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
name: 'useCurrent',
|
|
44
|
-
message: `Install in current directory (${currentDirName})?`,
|
|
45
|
-
default: true
|
|
46
|
-
}
|
|
47
|
-
]);
|
|
48
|
-
|
|
49
|
-
if (useCurrent) {
|
|
50
|
-
useCurrentDir = true;
|
|
51
|
-
projectPath = process.cwd();
|
|
52
|
-
|
|
53
|
-
// Check for existing Webspresso files
|
|
54
|
-
if (fs.existsSync(path.join(projectPath, 'server.js')) ||
|
|
55
|
-
fs.existsSync(path.join(projectPath, 'pages'))) {
|
|
56
|
-
console.error('❌ Current directory already contains a Webspresso project!');
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Warn if there are existing files
|
|
61
|
-
if (hasExistingFiles) {
|
|
62
|
-
const { continueAnyway } = await inquirer.prompt([
|
|
63
|
-
{
|
|
64
|
-
type: 'confirm',
|
|
65
|
-
name: 'continueAnyway',
|
|
66
|
-
message: '⚠️ Current directory is not empty. Continue anyway?',
|
|
67
|
-
default: false
|
|
68
|
-
}
|
|
69
|
-
]);
|
|
70
|
-
|
|
71
|
-
if (!continueAnyway) {
|
|
72
|
-
console.log('Cancelled.');
|
|
73
|
-
process.exit(0);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Ask for project name (for package.json)
|
|
78
|
-
const { name } = await inquirer.prompt([
|
|
79
|
-
{
|
|
80
|
-
type: 'input',
|
|
81
|
-
name: 'name',
|
|
82
|
-
message: 'Project name:',
|
|
83
|
-
default: currentDirName,
|
|
84
|
-
validate: (input) => {
|
|
85
|
-
if (!input.trim()) return 'Project name is required';
|
|
86
|
-
if (!/^[a-z0-9-_]+$/i.test(input)) return 'Project name can only contain letters, numbers, hyphens, and underscores';
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
]);
|
|
91
|
-
|
|
92
|
-
projectName = name;
|
|
93
|
-
} else {
|
|
94
|
-
// Ask for directory name
|
|
95
|
-
const { dirName } = await inquirer.prompt([
|
|
96
|
-
{
|
|
97
|
-
type: 'input',
|
|
98
|
-
name: 'dirName',
|
|
99
|
-
message: 'Project directory name:',
|
|
100
|
-
validate: (input) => {
|
|
101
|
-
if (!input.trim()) return 'Directory name is required';
|
|
102
|
-
if (!/^[a-z0-9-_]+$/i.test(input)) return 'Directory name can only contain letters, numbers, hyphens, and underscores';
|
|
103
|
-
if (fs.existsSync(path.resolve(input))) return `Directory ${input} already exists!`;
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
projectName = dirName;
|
|
110
|
-
projectPath = path.resolve(dirName);
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
projectName = projectNameArg;
|
|
114
|
-
projectPath = path.resolve(projectNameArg);
|
|
115
|
-
|
|
116
|
-
if (fs.existsSync(projectPath)) {
|
|
117
|
-
console.error(`❌ Directory ${projectName} already exists!`);
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
console.log(`\n🚀 Creating new Webspresso project: ${projectName}\n`);
|
|
123
|
-
|
|
124
|
-
// Ask about database
|
|
125
|
-
const { useDatabase, databaseType } = await inquirer.prompt([
|
|
126
|
-
{
|
|
127
|
-
type: 'confirm',
|
|
128
|
-
name: 'useDatabase',
|
|
129
|
-
message: 'Will you use a database?',
|
|
130
|
-
default: false
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
type: 'list',
|
|
134
|
-
name: 'databaseType',
|
|
135
|
-
message: 'Which database?',
|
|
136
|
-
choices: [
|
|
137
|
-
{ name: 'SQLite (better-sqlite3)', value: 'better-sqlite3' },
|
|
138
|
-
{ name: 'PostgreSQL (pg)', value: 'pg' },
|
|
139
|
-
{ name: 'MySQL (mysql2)', value: 'mysql2' },
|
|
140
|
-
{ name: 'None', value: null }
|
|
141
|
-
],
|
|
142
|
-
default: 'better-sqlite3',
|
|
143
|
-
when: (answers) => answers.useDatabase
|
|
144
|
-
}
|
|
145
|
-
]);
|
|
146
|
-
|
|
147
|
-
// Ask about seed data if database is selected
|
|
148
|
-
let useSeed = false;
|
|
149
|
-
if (useDatabase && databaseType) {
|
|
150
|
-
const { generateSeed } = await inquirer.prompt([
|
|
151
|
-
{
|
|
152
|
-
type: 'confirm',
|
|
153
|
-
name: 'generateSeed',
|
|
154
|
-
message: 'Generate seed data based on existing models?',
|
|
155
|
-
default: false
|
|
156
|
-
}
|
|
157
|
-
]);
|
|
158
|
-
useSeed = generateSeed;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Create directory structure (skip root if using current dir)
|
|
162
|
-
if (!useCurrentDir) {
|
|
163
|
-
fs.mkdirSync(projectPath, { recursive: true });
|
|
164
|
-
}
|
|
165
|
-
fs.mkdirSync(path.join(projectPath, 'pages'), { recursive: true });
|
|
166
|
-
fs.mkdirSync(path.join(projectPath, 'pages', 'locales'), { recursive: true });
|
|
167
|
-
fs.mkdirSync(path.join(projectPath, 'views'), { recursive: true });
|
|
168
|
-
fs.mkdirSync(path.join(projectPath, 'public'), { recursive: true });
|
|
169
|
-
|
|
170
|
-
// Create models directory if database is selected
|
|
171
|
-
if (useDatabase && databaseType) {
|
|
172
|
-
fs.mkdirSync(path.join(projectPath, 'models'), { recursive: true });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Create package.json
|
|
176
|
-
const packageJson = {
|
|
177
|
-
name: projectName,
|
|
178
|
-
version: '1.0.0',
|
|
179
|
-
description: 'Webspresso project',
|
|
180
|
-
main: 'server.js',
|
|
181
|
-
scripts: {
|
|
182
|
-
dev: 'node --watch server.js',
|
|
183
|
-
start: 'NODE_ENV=production node server.js'
|
|
184
|
-
},
|
|
185
|
-
dependencies: {
|
|
186
|
-
webspresso: '*',
|
|
187
|
-
dotenv: '^16.3.1'
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// Add database driver if selected
|
|
192
|
-
if (useDatabase && databaseType) {
|
|
193
|
-
const dbDrivers = {
|
|
194
|
-
'better-sqlite3': '^9.0.0',
|
|
195
|
-
'pg': '^8.0.0',
|
|
196
|
-
'mysql2': '^3.0.0'
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
packageJson.dependencies[databaseType] = dbDrivers[databaseType];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Add faker if seed is selected
|
|
203
|
-
if (useSeed) {
|
|
204
|
-
packageJson.dependencies['@faker-js/faker'] = '^8.0.0';
|
|
205
|
-
packageJson.scripts.seed = 'node seeds/index.js';
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
fs.writeFileSync(
|
|
209
|
-
path.join(projectPath, 'package.json'),
|
|
210
|
-
JSON.stringify(packageJson, null, 2) + '\n'
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
// Create server.js
|
|
214
|
-
const serverJs = `require('dotenv').config();
|
|
215
|
-
const { createApp } = require('webspresso');
|
|
216
|
-
const path = require('path');
|
|
217
|
-
|
|
218
|
-
const { app } = createApp({
|
|
219
|
-
pagesDir: path.join(__dirname, 'pages'),
|
|
220
|
-
viewsDir: path.join(__dirname, 'views'),
|
|
221
|
-
publicDir: path.join(__dirname, 'public')
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const PORT = process.env.PORT || 3000;
|
|
225
|
-
|
|
226
|
-
app.listen(PORT, () => {
|
|
227
|
-
console.log(\`🚀 Server running at http://localhost:\${PORT}\`);
|
|
228
|
-
});
|
|
229
|
-
`;
|
|
230
|
-
|
|
231
|
-
fs.writeFileSync(path.join(projectPath, 'server.js'), serverJs);
|
|
232
|
-
|
|
233
|
-
// Create .env.example
|
|
234
|
-
let envExample = `PORT=3000
|
|
235
|
-
NODE_ENV=development
|
|
236
|
-
DEFAULT_LOCALE=en
|
|
237
|
-
SUPPORTED_LOCALES=en,tr
|
|
238
|
-
BASE_URL=http://localhost:3000
|
|
239
|
-
`;
|
|
240
|
-
|
|
241
|
-
if (useDatabase && databaseType) {
|
|
242
|
-
if (databaseType === 'better-sqlite3') {
|
|
243
|
-
envExample += `DATABASE_URL=sqlite:./database.sqlite
|
|
244
|
-
`;
|
|
245
|
-
} else if (databaseType === 'pg') {
|
|
246
|
-
envExample += `DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
247
|
-
`;
|
|
248
|
-
} else if (databaseType === 'mysql2') {
|
|
249
|
-
envExample += `DATABASE_URL=mysql://user:password@localhost:3306/dbname
|
|
250
|
-
`;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
fs.writeFileSync(path.join(projectPath, '.env.example'), envExample);
|
|
255
|
-
|
|
256
|
-
// Create database config if database is selected
|
|
257
|
-
if (useDatabase && databaseType) {
|
|
258
|
-
let dbConfig = '';
|
|
259
|
-
|
|
260
|
-
if (databaseType === 'better-sqlite3') {
|
|
261
|
-
dbConfig = `module.exports = {
|
|
262
|
-
client: 'better-sqlite3',
|
|
263
|
-
connection: {
|
|
264
|
-
filename: process.env.DATABASE_URL?.replace('sqlite:', '') || './database.sqlite'
|
|
265
|
-
},
|
|
266
|
-
migrations: {
|
|
267
|
-
directory: './migrations',
|
|
268
|
-
tableName: 'knex_migrations',
|
|
269
|
-
},
|
|
270
|
-
useNullAsDefault: true
|
|
271
|
-
};
|
|
272
|
-
`;
|
|
273
|
-
} else if (databaseType === 'pg') {
|
|
274
|
-
dbConfig = `module.exports = {
|
|
275
|
-
client: 'pg',
|
|
276
|
-
connection: process.env.DATABASE_URL,
|
|
277
|
-
migrations: {
|
|
278
|
-
directory: './migrations',
|
|
279
|
-
tableName: 'knex_migrations',
|
|
280
|
-
},
|
|
281
|
-
pool: {
|
|
282
|
-
min: 2,
|
|
283
|
-
max: 10
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
`;
|
|
287
|
-
} else if (databaseType === 'mysql2') {
|
|
288
|
-
dbConfig = `module.exports = {
|
|
289
|
-
client: 'mysql2',
|
|
290
|
-
connection: process.env.DATABASE_URL,
|
|
291
|
-
migrations: {
|
|
292
|
-
directory: './migrations',
|
|
293
|
-
tableName: 'knex_migrations',
|
|
294
|
-
},
|
|
295
|
-
pool: {
|
|
296
|
-
min: 2,
|
|
297
|
-
max: 10
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
`;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
fs.writeFileSync(path.join(projectPath, 'webspresso.db.js'), dbConfig);
|
|
304
|
-
|
|
305
|
-
// Create migrations directory
|
|
306
|
-
fs.mkdirSync(path.join(projectPath, 'migrations'), { recursive: true });
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Create seed files if seed is selected
|
|
310
|
-
if (useSeed) {
|
|
311
|
-
fs.mkdirSync(path.join(projectPath, 'seeds'), { recursive: true });
|
|
312
|
-
|
|
313
|
-
const seedIndex = `require('dotenv').config();
|
|
314
|
-
const { faker } = require('@faker-js/faker');
|
|
315
|
-
const path = require('path');
|
|
316
|
-
const fs = require('fs');
|
|
317
|
-
const { createDatabase, getAllModels } = require('webspresso/orm');
|
|
318
|
-
const dbConfig = require('./webspresso.db.js');
|
|
319
|
-
|
|
320
|
-
const db = createDatabase(dbConfig);
|
|
321
|
-
const seeder = db.seeder(faker);
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Load all models from models/ directory
|
|
325
|
-
*/
|
|
326
|
-
function loadModels() {
|
|
327
|
-
const modelsDir = path.join(__dirname, 'models');
|
|
328
|
-
|
|
329
|
-
if (!fs.existsSync(modelsDir)) {
|
|
330
|
-
return [];
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const modelFiles = fs.readdirSync(modelsDir)
|
|
334
|
-
.filter(file => file.endsWith('.js') && !file.startsWith('_'));
|
|
335
|
-
|
|
336
|
-
const loadedModels = [];
|
|
337
|
-
for (const file of modelFiles) {
|
|
338
|
-
try {
|
|
339
|
-
require(path.join(modelsDir, file));
|
|
340
|
-
loadedModels.push(file);
|
|
341
|
-
} catch (error) {
|
|
342
|
-
console.warn(\`⚠️ Failed to load model from \${file}:\`, error.message);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return loadedModels;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Seed database with fake data
|
|
351
|
-
* This script automatically detects models in the models/ directory
|
|
352
|
-
* and generates seed data based on their schemas.
|
|
353
|
-
*/
|
|
354
|
-
async function runSeeds() {
|
|
355
|
-
try {
|
|
356
|
-
console.log('🌱 Starting seed process...');
|
|
357
|
-
|
|
358
|
-
// Load all models
|
|
359
|
-
const loadedFiles = loadModels();
|
|
360
|
-
|
|
361
|
-
if (loadedFiles.length === 0) {
|
|
362
|
-
console.log('⚠️ No model files found in models/ directory.');
|
|
363
|
-
console.log(' Create models first, then run: npm run seed');
|
|
364
|
-
await db.knex.destroy();
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Get all registered models
|
|
369
|
-
const models = getAllModels();
|
|
370
|
-
|
|
371
|
-
if (models.size === 0) {
|
|
372
|
-
console.log('⚠️ No models registered. Make sure your model files export models using defineModel().');
|
|
373
|
-
await db.knex.destroy();
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
console.log(\`📦 Found \${models.size} model(s):\`);
|
|
378
|
-
for (const [name] of models) {
|
|
379
|
-
console.log(\` - \${name}\`);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Seed each model
|
|
383
|
-
const results = {};
|
|
384
|
-
for (const [modelName, model] of models) {
|
|
385
|
-
console.log(\`\\n🌱 Seeding \${modelName}...\`);
|
|
386
|
-
|
|
387
|
-
// Default count: 10 records per model
|
|
388
|
-
const count = 10;
|
|
389
|
-
const records = await seeder.seed(modelName, count);
|
|
390
|
-
results[modelName] = records.length;
|
|
391
|
-
|
|
392
|
-
console.log(\`✅ Created \${records.length} \${modelName} record(s)\`);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
console.log(\`\\n✨ Seed completed! Created:\`);
|
|
396
|
-
for (const [modelName, count] of Object.entries(results)) {
|
|
397
|
-
console.log(\` - \${count} \${modelName} record(s)\`);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
await db.knex.destroy();
|
|
401
|
-
} catch (error) {
|
|
402
|
-
console.error('❌ Seed failed:', error);
|
|
403
|
-
await db.knex.destroy().catch(() => {});
|
|
404
|
-
process.exit(1);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
runSeeds();
|
|
409
|
-
`;
|
|
410
|
-
|
|
411
|
-
fs.writeFileSync(path.join(projectPath, 'seeds', 'index.js'), seedIndex);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Create .gitignore
|
|
415
|
-
const gitignore = `node_modules/
|
|
416
|
-
.env
|
|
417
|
-
.env.local
|
|
418
|
-
.DS_Store
|
|
419
|
-
coverage/
|
|
420
|
-
*.log
|
|
421
|
-
`;
|
|
422
|
-
|
|
423
|
-
fs.writeFileSync(path.join(projectPath, '.gitignore'), gitignore);
|
|
424
|
-
|
|
425
|
-
// Create default layout
|
|
426
|
-
let layoutNjk;
|
|
427
|
-
|
|
428
|
-
if (useTailwind) {
|
|
429
|
-
layoutNjk = `<!DOCTYPE html>
|
|
430
|
-
<html lang="{{ locale or 'en' }}">
|
|
431
|
-
<head>
|
|
432
|
-
<meta charset="UTF-8">
|
|
433
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
434
|
-
<title>{{ meta.title or 'Webspresso' }}</title>
|
|
435
|
-
{% if meta.description %}
|
|
436
|
-
<meta name="description" content="{{ meta.description }}">
|
|
437
|
-
{% endif %}
|
|
438
|
-
{% if meta.canonical %}
|
|
439
|
-
<link rel="canonical" href="{{ meta.canonical }}">
|
|
440
|
-
{% else %}
|
|
441
|
-
<link rel="canonical" href="{{ fsy.canonical() }}">
|
|
442
|
-
{% endif %}
|
|
443
|
-
<link rel="stylesheet" href="/css/style.css">
|
|
444
|
-
</head>
|
|
445
|
-
<body class="min-h-screen flex flex-col bg-gray-50">
|
|
446
|
-
<nav class="bg-white shadow-sm border-b border-gray-200">
|
|
447
|
-
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
448
|
-
<div class="flex justify-between h-16">
|
|
449
|
-
<div class="flex items-center">
|
|
450
|
-
<a href="/" class="text-xl font-bold text-gray-900 hover:text-gray-700">
|
|
451
|
-
{{ t('site.name') or 'Webspresso' }}
|
|
452
|
-
</a>
|
|
453
|
-
</div>
|
|
454
|
-
<div class="flex items-center space-x-4">
|
|
455
|
-
<a href="/" class="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium {% if fsy.isPath('/') %}text-blue-600 bg-blue-50{% endif %}">
|
|
456
|
-
{{ t('nav.home') or 'Home' }}
|
|
457
|
-
</a>
|
|
458
|
-
</div>
|
|
459
|
-
</div>
|
|
460
|
-
</div>
|
|
461
|
-
</nav>
|
|
462
|
-
|
|
463
|
-
<main class="flex-1 max-w-7xl mx-auto py-6 sm:px-6 lg:px-8 w-full">
|
|
464
|
-
{% block content %}{% endblock %}
|
|
465
|
-
</main>
|
|
466
|
-
|
|
467
|
-
<footer class="bg-white border-t border-gray-200">
|
|
468
|
-
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
|
469
|
-
<p class="text-center text-sm text-gray-500">
|
|
470
|
-
{{ t('footer.copyright') or '© 2025 Webspresso. All rights reserved.' }}
|
|
471
|
-
</p>
|
|
472
|
-
</div>
|
|
473
|
-
</footer>
|
|
474
|
-
</body>
|
|
475
|
-
</html>
|
|
476
|
-
`;
|
|
477
|
-
} else {
|
|
478
|
-
layoutNjk = `<!DOCTYPE html>
|
|
479
|
-
<html lang="{{ locale or 'en' }}">
|
|
480
|
-
<head>
|
|
481
|
-
<meta charset="UTF-8">
|
|
482
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
483
|
-
<title>{{ meta.title or 'Webspresso' }}</title>
|
|
484
|
-
{% if meta.description %}
|
|
485
|
-
<meta name="description" content="{{ meta.description }}">
|
|
486
|
-
{% endif %}
|
|
487
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
488
|
-
</head>
|
|
489
|
-
<body>
|
|
490
|
-
<main>
|
|
491
|
-
{% block content %}{% endblock %}
|
|
492
|
-
</main>
|
|
493
|
-
</body>
|
|
494
|
-
</html>
|
|
495
|
-
`;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
fs.writeFileSync(path.join(projectPath, 'views', 'layout.njk'), layoutNjk);
|
|
499
|
-
|
|
500
|
-
// Create index page
|
|
501
|
-
let indexNjk;
|
|
502
|
-
|
|
503
|
-
if (useTailwind) {
|
|
504
|
-
indexNjk = `{% extends "layout.njk" %}
|
|
505
|
-
|
|
506
|
-
{% block content %}
|
|
507
|
-
<div class="px-4 py-16 sm:px-6 lg:px-8">
|
|
508
|
-
<div class="text-center">
|
|
509
|
-
<h1 class="text-4xl font-bold text-gray-900 sm:text-5xl md:text-6xl">
|
|
510
|
-
{{ t('welcome') or 'Welcome to Webspresso' }}
|
|
511
|
-
</h1>
|
|
512
|
-
<p class="mt-3 max-w-md mx-auto text-base text-gray-500 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl">
|
|
513
|
-
{{ t('description') or 'Start building your SSR app!' }}
|
|
514
|
-
</p>
|
|
515
|
-
<div class="mt-5 max-w-md mx-auto sm:flex sm:justify-center md:mt-8">
|
|
516
|
-
<div class="rounded-md shadow">
|
|
517
|
-
<a href="/" class="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 md:py-4 md:text-lg md:px-10">
|
|
518
|
-
Get started
|
|
519
|
-
</a>
|
|
520
|
-
</div>
|
|
521
|
-
</div>
|
|
522
|
-
</div>
|
|
523
|
-
</div>
|
|
524
|
-
{% endblock %}
|
|
525
|
-
`;
|
|
526
|
-
} else {
|
|
527
|
-
indexNjk = `{% extends "layout.njk" %}
|
|
528
|
-
|
|
529
|
-
{% block content %}
|
|
530
|
-
<div>
|
|
531
|
-
<h1>{{ t('welcome') or 'Welcome to Webspresso' }}</h1>
|
|
532
|
-
<p>{{ t('description') or 'Start building your SSR app!' }}</p>
|
|
533
|
-
</div>
|
|
534
|
-
{% endblock %}
|
|
535
|
-
`;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
fs.writeFileSync(path.join(projectPath, 'pages', 'index.njk'), indexNjk);
|
|
539
|
-
|
|
540
|
-
// Create locales
|
|
541
|
-
const enJson = {
|
|
542
|
-
site: {
|
|
543
|
-
name: 'Webspresso'
|
|
544
|
-
},
|
|
545
|
-
nav: {
|
|
546
|
-
home: 'Home'
|
|
547
|
-
},
|
|
548
|
-
footer: {
|
|
549
|
-
copyright: '© 2025 Webspresso. All rights reserved.'
|
|
550
|
-
},
|
|
551
|
-
welcome: 'Welcome to Webspresso',
|
|
552
|
-
description: 'Start building your SSR app!'
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
fs.writeFileSync(
|
|
556
|
-
path.join(projectPath, 'pages', 'locales', 'en.json'),
|
|
557
|
-
JSON.stringify(enJson, null, 2) + '\n'
|
|
558
|
-
);
|
|
559
|
-
|
|
560
|
-
const trJson = {
|
|
561
|
-
site: {
|
|
562
|
-
name: 'Webspresso'
|
|
563
|
-
},
|
|
564
|
-
nav: {
|
|
565
|
-
home: 'Ana Sayfa'
|
|
566
|
-
},
|
|
567
|
-
footer: {
|
|
568
|
-
copyright: '© 2025 Webspresso. Tüm hakları saklıdır.'
|
|
569
|
-
},
|
|
570
|
-
welcome: 'Webspresso\'ya Hoş Geldiniz',
|
|
571
|
-
description: 'SSR uygulamanızı oluşturmaya başlayın!'
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
fs.writeFileSync(
|
|
575
|
-
path.join(projectPath, 'pages', 'locales', 'tr.json'),
|
|
576
|
-
JSON.stringify(trJson, null, 2) + '\n'
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
// Create README
|
|
580
|
-
const readme = `# ${projectName}
|
|
581
|
-
|
|
582
|
-
Webspresso project
|
|
583
|
-
|
|
584
|
-
## Getting Started
|
|
585
|
-
|
|
586
|
-
\`\`\`bash
|
|
587
|
-
npm install
|
|
588
|
-
npm run dev
|
|
589
|
-
\`\`\`
|
|
590
|
-
|
|
591
|
-
Visit http://localhost:3000
|
|
592
|
-
`;
|
|
593
|
-
|
|
594
|
-
fs.writeFileSync(path.join(projectPath, 'README.md'), readme);
|
|
595
|
-
|
|
596
|
-
// Add Tailwind if requested (default: true)
|
|
597
|
-
if (useTailwind) {
|
|
598
|
-
console.log('\n🎨 Setting up Tailwind CSS...\n');
|
|
599
|
-
|
|
600
|
-
// Create src directory
|
|
601
|
-
fs.mkdirSync(path.join(projectPath, 'src'), { recursive: true });
|
|
602
|
-
|
|
603
|
-
// Create input.css
|
|
604
|
-
const inputCss = `@tailwind base;
|
|
605
|
-
@tailwind components;
|
|
606
|
-
@tailwind utilities;
|
|
607
|
-
`;
|
|
608
|
-
fs.writeFileSync(path.join(projectPath, 'src', 'input.css'), inputCss);
|
|
609
|
-
|
|
610
|
-
// Create tailwind.config.js
|
|
611
|
-
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
612
|
-
module.exports = {
|
|
613
|
-
content: [
|
|
614
|
-
'./pages/**/*.{njk,js}',
|
|
615
|
-
'./views/**/*.njk',
|
|
616
|
-
'./src/**/*.js'
|
|
617
|
-
],
|
|
618
|
-
theme: {
|
|
619
|
-
extend: {},
|
|
620
|
-
},
|
|
621
|
-
plugins: [],
|
|
622
|
-
}
|
|
623
|
-
`;
|
|
624
|
-
fs.writeFileSync(path.join(projectPath, 'tailwind.config.js'), tailwindConfig);
|
|
625
|
-
|
|
626
|
-
// Create postcss.config.js
|
|
627
|
-
const postcssConfig = `module.exports = {
|
|
628
|
-
plugins: {
|
|
629
|
-
tailwindcss: {},
|
|
630
|
-
autoprefixer: {},
|
|
631
|
-
},
|
|
632
|
-
}
|
|
633
|
-
`;
|
|
634
|
-
fs.writeFileSync(path.join(projectPath, 'postcss.config.js'), postcssConfig);
|
|
635
|
-
|
|
636
|
-
// Create public/css directory
|
|
637
|
-
fs.mkdirSync(path.join(projectPath, 'public', 'css'), { recursive: true });
|
|
638
|
-
|
|
639
|
-
// Create placeholder CSS (will be replaced by build)
|
|
640
|
-
fs.writeFileSync(path.join(projectPath, 'public', 'css', 'style.css'), '/* Run npm run build:css */\n');
|
|
641
|
-
|
|
642
|
-
// Update package.json
|
|
643
|
-
const updatedPackageJson = JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf-8'));
|
|
644
|
-
updatedPackageJson.devDependencies = updatedPackageJson.devDependencies || {};
|
|
645
|
-
updatedPackageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
646
|
-
updatedPackageJson.devDependencies['postcss'] = '^8.4.35';
|
|
647
|
-
updatedPackageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
648
|
-
|
|
649
|
-
updatedPackageJson.scripts['build:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --minify';
|
|
650
|
-
updatedPackageJson.scripts['watch:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --watch';
|
|
651
|
-
updatedPackageJson.scripts.dev = 'npm run watch:css & node --watch server.js';
|
|
652
|
-
updatedPackageJson.scripts.start = 'npm run build:css && NODE_ENV=production node server.js';
|
|
653
|
-
|
|
654
|
-
fs.writeFileSync(
|
|
655
|
-
path.join(projectPath, 'package.json'),
|
|
656
|
-
JSON.stringify(updatedPackageJson, null, 2) + '\n'
|
|
657
|
-
);
|
|
658
|
-
|
|
659
|
-
console.log('✅ Tailwind CSS setup complete!');
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Helper function to run installation
|
|
663
|
-
async function runInstallation(projectPath, useTailwind) {
|
|
664
|
-
console.log('\n📦 Installing dependencies...\n');
|
|
665
|
-
const { execSync } = require('child_process');
|
|
666
|
-
try {
|
|
667
|
-
execSync('npm install', {
|
|
668
|
-
stdio: 'inherit',
|
|
669
|
-
cwd: projectPath
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
if (useTailwind) {
|
|
673
|
-
console.log('\n🎨 Building Tailwind CSS...\n');
|
|
674
|
-
execSync('npm run build:css', {
|
|
675
|
-
stdio: 'inherit',
|
|
676
|
-
cwd: projectPath
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
console.log('\n✅ Installation complete!\n');
|
|
681
|
-
} catch (err) {
|
|
682
|
-
console.error('❌ Installation failed:', err.message);
|
|
683
|
-
process.exit(1);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// Helper function to start dev server
|
|
688
|
-
function startDevServer(projectPath, useTailwind) {
|
|
689
|
-
console.log('\n🚀 Starting development server...\n');
|
|
690
|
-
console.log('Press Ctrl+C to stop\n');
|
|
691
|
-
|
|
692
|
-
const { spawn } = require('child_process');
|
|
693
|
-
|
|
694
|
-
if (useTailwind) {
|
|
695
|
-
// Start CSS watch and server together
|
|
696
|
-
const cssWatch = spawn('npm', ['run', 'watch:css'], {
|
|
697
|
-
stdio: 'inherit',
|
|
698
|
-
shell: true,
|
|
699
|
-
cwd: projectPath
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
const server = spawn('node', ['--watch', 'server.js'], {
|
|
703
|
-
stdio: 'inherit',
|
|
704
|
-
shell: true,
|
|
705
|
-
cwd: projectPath,
|
|
706
|
-
env: { ...process.env, PORT: process.env.PORT || '3000', NODE_ENV: 'development' }
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
// Handle exit
|
|
710
|
-
const cleanup = () => {
|
|
711
|
-
cssWatch.kill();
|
|
712
|
-
server.kill();
|
|
713
|
-
process.exit(0);
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
process.on('SIGINT', cleanup);
|
|
717
|
-
process.on('SIGTERM', cleanup);
|
|
718
|
-
|
|
719
|
-
cssWatch.on('exit', cleanup);
|
|
720
|
-
server.on('exit', cleanup);
|
|
721
|
-
} else {
|
|
722
|
-
// Just start server
|
|
723
|
-
const server = spawn('node', ['--watch', 'server.js'], {
|
|
724
|
-
stdio: 'inherit',
|
|
725
|
-
shell: true,
|
|
726
|
-
cwd: projectPath,
|
|
727
|
-
env: { ...process.env, PORT: process.env.PORT || '3000', NODE_ENV: 'development' }
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
server.on('exit', (code) => {
|
|
731
|
-
process.exit(code || 0);
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
process.on('SIGINT', () => {
|
|
735
|
-
server.kill();
|
|
736
|
-
process.exit(0);
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Auto install if requested or ask interactively
|
|
742
|
-
if (autoInstall) {
|
|
743
|
-
await runInstallation(projectPath, useTailwind);
|
|
744
|
-
|
|
745
|
-
// Ask if user wants to start dev server
|
|
746
|
-
const { shouldStartDev } = await inquirer.prompt([
|
|
747
|
-
{
|
|
748
|
-
type: 'confirm',
|
|
749
|
-
name: 'shouldStartDev',
|
|
750
|
-
message: 'Start development server?',
|
|
751
|
-
default: true
|
|
752
|
-
}
|
|
753
|
-
]);
|
|
754
|
-
|
|
755
|
-
if (shouldStartDev) {
|
|
756
|
-
startDevServer(projectPath, useTailwind);
|
|
757
|
-
} else {
|
|
758
|
-
console.log('✅ Project ready!\n');
|
|
759
|
-
console.log('Start developing:');
|
|
760
|
-
if (!useCurrentDir) {
|
|
761
|
-
console.log(` cd ${projectName}`);
|
|
762
|
-
}
|
|
763
|
-
console.log(' npm run dev\n');
|
|
764
|
-
}
|
|
765
|
-
} else {
|
|
766
|
-
// Ask if user wants to install dependencies
|
|
767
|
-
const { shouldInstall } = await inquirer.prompt([
|
|
768
|
-
{
|
|
769
|
-
type: 'confirm',
|
|
770
|
-
name: 'shouldInstall',
|
|
771
|
-
message: 'Install dependencies and build CSS now?',
|
|
772
|
-
default: true
|
|
773
|
-
}
|
|
774
|
-
]);
|
|
775
|
-
|
|
776
|
-
if (shouldInstall) {
|
|
777
|
-
await runInstallation(projectPath, useTailwind);
|
|
778
|
-
|
|
779
|
-
// Ask if user wants to start dev server
|
|
780
|
-
const { shouldStartDev } = await inquirer.prompt([
|
|
781
|
-
{
|
|
782
|
-
type: 'confirm',
|
|
783
|
-
name: 'shouldStartDev',
|
|
784
|
-
message: 'Start development server?',
|
|
785
|
-
default: true
|
|
786
|
-
}
|
|
787
|
-
]);
|
|
788
|
-
|
|
789
|
-
if (shouldStartDev) {
|
|
790
|
-
startDevServer(projectPath, useTailwind);
|
|
791
|
-
} else {
|
|
792
|
-
console.log('\n✅ Project ready!\n');
|
|
793
|
-
console.log('Start developing:');
|
|
794
|
-
if (!useCurrentDir) {
|
|
795
|
-
console.log(` cd ${projectName}`);
|
|
796
|
-
}
|
|
797
|
-
console.log(' npm run dev\n');
|
|
798
|
-
}
|
|
799
|
-
} else {
|
|
800
|
-
console.log('\n✅ Project created successfully!\n');
|
|
801
|
-
console.log('Next steps:');
|
|
802
|
-
if (!useCurrentDir) {
|
|
803
|
-
console.log(` cd ${projectName}`);
|
|
804
|
-
}
|
|
805
|
-
console.log(' npm install');
|
|
806
|
-
if (useTailwind) {
|
|
807
|
-
console.log(' npm run build:css');
|
|
808
|
-
}
|
|
809
|
-
console.log(' npm run dev\n');
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
// Add page command
|
|
815
|
-
program
|
|
816
|
-
.command('page')
|
|
817
|
-
.description('Add a new page to the current project')
|
|
818
|
-
.action(async () => {
|
|
819
|
-
if (!fs.existsSync('pages')) {
|
|
820
|
-
console.error('❌ Not a Webspresso project! Run this command in your project directory.');
|
|
821
|
-
process.exit(1);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
const answers = await inquirer.prompt([
|
|
825
|
-
{
|
|
826
|
-
type: 'input',
|
|
827
|
-
name: 'route',
|
|
828
|
-
message: 'Route path (e.g., /about or /blog/post):',
|
|
829
|
-
validate: (input) => {
|
|
830
|
-
if (!input.startsWith('/')) {
|
|
831
|
-
return 'Route must start with /';
|
|
832
|
-
}
|
|
833
|
-
return true;
|
|
834
|
-
}
|
|
835
|
-
},
|
|
836
|
-
{
|
|
837
|
-
type: 'confirm',
|
|
838
|
-
name: 'hasConfig',
|
|
839
|
-
message: 'Add route config file (.js)?',
|
|
840
|
-
default: false
|
|
841
|
-
},
|
|
842
|
-
{
|
|
843
|
-
type: 'confirm',
|
|
844
|
-
name: 'hasLocales',
|
|
845
|
-
message: 'Add locale files?',
|
|
846
|
-
default: false
|
|
847
|
-
}
|
|
848
|
-
]);
|
|
849
|
-
|
|
850
|
-
const route = answers.route.replace(/^\//, '');
|
|
851
|
-
const routePath = path.join('pages', route);
|
|
852
|
-
const dirPath = path.dirname(routePath);
|
|
853
|
-
const fileName = path.basename(routePath);
|
|
854
|
-
|
|
855
|
-
// Create directory
|
|
856
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
857
|
-
|
|
858
|
-
// Create .njk file
|
|
859
|
-
const templateName = fileName === 'index' ? 'index' : fileName;
|
|
860
|
-
const njkFile = path.join(dirPath, `${templateName}.njk`);
|
|
861
|
-
|
|
862
|
-
const njkContent = `{% extends "layout.njk" %}
|
|
863
|
-
|
|
864
|
-
{% block content %}
|
|
865
|
-
<div>
|
|
866
|
-
<h1>{{ t('title') or '${route}' }}</h1>
|
|
867
|
-
<p>{{ t('description') or 'Page content' }}</p>
|
|
868
|
-
</div>
|
|
869
|
-
{% endblock %}
|
|
870
|
-
`;
|
|
871
|
-
|
|
872
|
-
fs.writeFileSync(njkFile, njkContent);
|
|
873
|
-
console.log(`✅ Created ${njkFile}`);
|
|
874
|
-
|
|
875
|
-
// Create config file if requested
|
|
876
|
-
if (answers.hasConfig) {
|
|
877
|
-
const jsFile = path.join(dirPath, `${templateName}.js`);
|
|
878
|
-
const jsContent = `module.exports = {
|
|
879
|
-
async load(req, ctx) {
|
|
880
|
-
return {
|
|
881
|
-
// Add your data here
|
|
882
|
-
};
|
|
883
|
-
},
|
|
884
|
-
|
|
885
|
-
meta(req, ctx) {
|
|
886
|
-
return {
|
|
887
|
-
title: ctx.t('meta.title') || '${route}',
|
|
888
|
-
description: ctx.t('meta.description') || ''
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
};
|
|
892
|
-
`;
|
|
893
|
-
|
|
894
|
-
fs.writeFileSync(jsFile, jsContent);
|
|
895
|
-
console.log(`✅ Created ${jsFile}`);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// Create locales if requested
|
|
899
|
-
if (answers.hasLocales) {
|
|
900
|
-
const localesDir = path.join(dirPath, 'locales');
|
|
901
|
-
fs.mkdirSync(localesDir, { recursive: true });
|
|
902
|
-
|
|
903
|
-
const enContent = {
|
|
904
|
-
title: route,
|
|
905
|
-
description: 'Page description',
|
|
906
|
-
meta: {
|
|
907
|
-
title: `${route} - Webspresso`,
|
|
908
|
-
description: 'Page description'
|
|
909
|
-
}
|
|
910
|
-
};
|
|
911
|
-
|
|
912
|
-
fs.writeFileSync(
|
|
913
|
-
path.join(localesDir, 'en.json'),
|
|
914
|
-
JSON.stringify(enContent, null, 2) + '\n'
|
|
915
|
-
);
|
|
916
|
-
|
|
917
|
-
const trContent = {
|
|
918
|
-
title: route,
|
|
919
|
-
description: 'Sayfa açıklaması',
|
|
920
|
-
meta: {
|
|
921
|
-
title: `${route} - Webspresso`,
|
|
922
|
-
description: 'Sayfa açıklaması'
|
|
923
|
-
}
|
|
924
|
-
};
|
|
925
|
-
|
|
926
|
-
fs.writeFileSync(
|
|
927
|
-
path.join(localesDir, 'tr.json'),
|
|
928
|
-
JSON.stringify(trContent, null, 2) + '\n'
|
|
929
|
-
);
|
|
930
|
-
|
|
931
|
-
console.log(`✅ Created locale files in ${localesDir}`);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
console.log(`\n✅ Page created at ${route}\n`);
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
// Add API command
|
|
938
|
-
program
|
|
939
|
-
.command('api')
|
|
940
|
-
.description('Add a new API endpoint to the current project')
|
|
941
|
-
.action(async () => {
|
|
942
|
-
if (!fs.existsSync('pages')) {
|
|
943
|
-
console.error('❌ Not a Webspresso project! Run this command in your project directory.');
|
|
944
|
-
process.exit(1);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
const answers = await inquirer.prompt([
|
|
948
|
-
{
|
|
949
|
-
type: 'input',
|
|
950
|
-
name: 'route',
|
|
951
|
-
message: 'API route path (e.g., /api/users or /api/users/[id]):',
|
|
952
|
-
validate: (input) => {
|
|
953
|
-
if (!input.startsWith('/api/')) {
|
|
954
|
-
return 'API route must start with /api/';
|
|
955
|
-
}
|
|
956
|
-
return true;
|
|
957
|
-
}
|
|
958
|
-
},
|
|
959
|
-
{
|
|
960
|
-
type: 'list',
|
|
961
|
-
name: 'method',
|
|
962
|
-
message: 'HTTP method:',
|
|
963
|
-
choices: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
964
|
-
default: 'GET'
|
|
965
|
-
}
|
|
966
|
-
]);
|
|
967
|
-
|
|
968
|
-
const route = answers.route.replace(/^\/api\//, '');
|
|
969
|
-
const routePath = path.join('pages', 'api', route);
|
|
970
|
-
const dirPath = path.dirname(routePath);
|
|
971
|
-
const fileName = path.basename(routePath);
|
|
972
|
-
|
|
973
|
-
// Create directory
|
|
974
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
975
|
-
|
|
976
|
-
// Create API file
|
|
977
|
-
const apiFile = path.join(dirPath, `${fileName}.${answers.method.toLowerCase()}.js`);
|
|
978
|
-
|
|
979
|
-
const apiContent = `/**
|
|
980
|
-
* ${answers.method} ${answers.route}
|
|
981
|
-
*/
|
|
982
|
-
|
|
983
|
-
module.exports = async function handler(req, res) {
|
|
984
|
-
res.json({
|
|
985
|
-
message: 'Hello from ${answers.route}',
|
|
986
|
-
method: '${answers.method}',
|
|
987
|
-
timestamp: new Date().toISOString()
|
|
988
|
-
});
|
|
989
|
-
};
|
|
990
|
-
`;
|
|
991
|
-
|
|
992
|
-
fs.writeFileSync(apiFile, apiContent);
|
|
993
|
-
console.log(`\n✅ Created ${apiFile}\n`);
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
// Dev command
|
|
997
|
-
program
|
|
998
|
-
.command('dev')
|
|
999
|
-
.description('Start development server')
|
|
1000
|
-
.option('-p, --port <port>', 'Port number', '3000')
|
|
1001
|
-
.option('--no-css', 'Skip CSS watch (if Tailwind is set up)')
|
|
1002
|
-
.action((options) => {
|
|
1003
|
-
if (!fs.existsSync('server.js')) {
|
|
1004
|
-
console.error('❌ server.js not found! Make sure you are in a Webspresso project.');
|
|
1005
|
-
process.exit(1);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
process.env.PORT = options.port;
|
|
1009
|
-
process.env.NODE_ENV = 'development';
|
|
1010
|
-
|
|
1011
|
-
const hasTailwind = fs.existsSync('tailwind.config.js') && fs.existsSync('src/input.css');
|
|
1012
|
-
const shouldWatchCss = hasTailwind && options.css !== false;
|
|
1013
|
-
|
|
1014
|
-
if (shouldWatchCss) {
|
|
1015
|
-
console.log(`\n🚀 Starting development server on port ${options.port}...`);
|
|
1016
|
-
console.log(' Watching CSS and server files...\n');
|
|
1017
|
-
|
|
1018
|
-
// Start CSS watch
|
|
1019
|
-
const cssWatch = spawn('npm', ['run', 'watch:css'], {
|
|
1020
|
-
stdio: 'inherit',
|
|
1021
|
-
shell: true
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
|
-
// Start server
|
|
1025
|
-
const server = spawn('node', ['--watch', 'server.js'], {
|
|
1026
|
-
stdio: 'inherit',
|
|
1027
|
-
shell: true,
|
|
1028
|
-
env: { ...process.env, PORT: options.port, NODE_ENV: 'development' }
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
// Handle exit
|
|
1032
|
-
const cleanup = () => {
|
|
1033
|
-
cssWatch.kill();
|
|
1034
|
-
server.kill();
|
|
1035
|
-
process.exit(0);
|
|
1036
|
-
};
|
|
1037
|
-
|
|
1038
|
-
process.on('SIGINT', cleanup);
|
|
1039
|
-
process.on('SIGTERM', cleanup);
|
|
1040
|
-
|
|
1041
|
-
cssWatch.on('exit', cleanup);
|
|
1042
|
-
server.on('exit', cleanup);
|
|
1043
|
-
} else {
|
|
1044
|
-
console.log(`\n🚀 Starting development server on port ${options.port}...\n`);
|
|
1045
|
-
|
|
1046
|
-
const { spawn } = require('child_process');
|
|
1047
|
-
const child = spawn('node', ['--watch', 'server.js'], {
|
|
1048
|
-
stdio: 'inherit',
|
|
1049
|
-
shell: true,
|
|
1050
|
-
env: { ...process.env, PORT: options.port, NODE_ENV: 'development' }
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
child.on('exit', (code) => {
|
|
1054
|
-
process.exit(code || 0);
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
// Start command
|
|
1060
|
-
program
|
|
1061
|
-
.command('start')
|
|
1062
|
-
.description('Start production server')
|
|
1063
|
-
.option('-p, --port <port>', 'Port number', '3000')
|
|
1064
|
-
.action((options) => {
|
|
1065
|
-
if (!fs.existsSync('server.js')) {
|
|
1066
|
-
console.error('❌ server.js not found! Make sure you are in a Webspresso project.');
|
|
1067
|
-
process.exit(1);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
process.env.PORT = options.port;
|
|
1071
|
-
process.env.NODE_ENV = 'production';
|
|
1072
|
-
|
|
1073
|
-
console.log(`\n🚀 Starting production server on port ${options.port}...\n`);
|
|
1074
|
-
|
|
1075
|
-
const serverPath = path.resolve(process.cwd(), 'server.js');
|
|
1076
|
-
require(serverPath);
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
// Add Tailwind command
|
|
1080
|
-
program
|
|
1081
|
-
.command('add tailwind')
|
|
1082
|
-
.description('Add Tailwind CSS to the project with build process')
|
|
1083
|
-
.action(async () => {
|
|
1084
|
-
if (!fs.existsSync('package.json')) {
|
|
1085
|
-
console.error('❌ Not a Webspresso project! Run this command in your project directory.');
|
|
1086
|
-
process.exit(1);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
console.log('\n🎨 Adding Tailwind CSS to your project...\n');
|
|
1090
|
-
|
|
1091
|
-
// Read package.json
|
|
1092
|
-
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
|
1093
|
-
|
|
1094
|
-
// Add dev dependencies
|
|
1095
|
-
if (!packageJson.devDependencies) {
|
|
1096
|
-
packageJson.devDependencies = {};
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
packageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
1100
|
-
packageJson.devDependencies['postcss'] = '^8.4.35';
|
|
1101
|
-
packageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
1102
|
-
|
|
1103
|
-
// Add build scripts
|
|
1104
|
-
if (!packageJson.scripts) {
|
|
1105
|
-
packageJson.scripts = {};
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
packageJson.scripts['build:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --minify';
|
|
1109
|
-
packageJson.scripts['watch:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --watch';
|
|
1110
|
-
|
|
1111
|
-
// Update dev script to include CSS watch
|
|
1112
|
-
if (packageJson.scripts.dev) {
|
|
1113
|
-
packageJson.scripts.dev = 'npm run watch:css & node --watch server.js';
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// Update start script to build CSS
|
|
1117
|
-
if (packageJson.scripts.start) {
|
|
1118
|
-
packageJson.scripts.start = 'npm run build:css && NODE_ENV=production node server.js';
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
|
|
1122
|
-
console.log('✅ Updated package.json');
|
|
1123
|
-
|
|
1124
|
-
// Create src directory if it doesn't exist
|
|
1125
|
-
if (!fs.existsSync('src')) {
|
|
1126
|
-
fs.mkdirSync('src', { recursive: true });
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Create input.css
|
|
1130
|
-
const inputCss = `@tailwind base;
|
|
1131
|
-
@tailwind components;
|
|
1132
|
-
@tailwind utilities;
|
|
1133
|
-
`;
|
|
1134
|
-
|
|
1135
|
-
fs.writeFileSync('src/input.css', inputCss);
|
|
1136
|
-
console.log('✅ Created src/input.css');
|
|
1137
|
-
|
|
1138
|
-
// Create tailwind.config.js
|
|
1139
|
-
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
1140
|
-
module.exports = {
|
|
1141
|
-
content: [
|
|
1142
|
-
'./pages/**/*.{njk,js}',
|
|
1143
|
-
'./views/**/*.njk',
|
|
1144
|
-
'./src/**/*.js'
|
|
1145
|
-
],
|
|
1146
|
-
theme: {
|
|
1147
|
-
extend: {},
|
|
1148
|
-
},
|
|
1149
|
-
plugins: [],
|
|
1150
|
-
}
|
|
1151
|
-
`;
|
|
1152
|
-
|
|
1153
|
-
fs.writeFileSync('tailwind.config.js', tailwindConfig);
|
|
1154
|
-
console.log('✅ Created tailwind.config.js');
|
|
1155
|
-
|
|
1156
|
-
// Create postcss.config.js
|
|
1157
|
-
const postcssConfig = `module.exports = {
|
|
1158
|
-
plugins: {
|
|
1159
|
-
tailwindcss: {},
|
|
1160
|
-
autoprefixer: {},
|
|
1161
|
-
},
|
|
1162
|
-
}
|
|
1163
|
-
`;
|
|
1164
|
-
|
|
1165
|
-
fs.writeFileSync('postcss.config.js', postcssConfig);
|
|
1166
|
-
console.log('✅ Created postcss.config.js');
|
|
1167
|
-
|
|
1168
|
-
// Check if layout.njk exists and update it (before creating CSS)
|
|
1169
|
-
const layoutPath = 'views/layout.njk';
|
|
1170
|
-
if (fs.existsSync(layoutPath)) {
|
|
1171
|
-
let layoutContent = fs.readFileSync(layoutPath, 'utf-8');
|
|
1172
|
-
|
|
1173
|
-
// Remove CDN script if exists
|
|
1174
|
-
layoutContent = layoutContent.replace(
|
|
1175
|
-
/<script src="https:\/\/cdn\.tailwindcss\.com"><\/script>/g,
|
|
1176
|
-
''
|
|
1177
|
-
);
|
|
1178
|
-
|
|
1179
|
-
// Add local CSS link if not exists
|
|
1180
|
-
if (!layoutContent.includes('/css/style.css')) {
|
|
1181
|
-
layoutContent = layoutContent.replace(
|
|
1182
|
-
/(<\/head>)/,
|
|
1183
|
-
' <link rel="stylesheet" href="/css/style.css">\n$1'
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
fs.writeFileSync(layoutPath, layoutContent);
|
|
1188
|
-
console.log('✅ Updated views/layout.njk');
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// Create public/css directory
|
|
1192
|
-
if (!fs.existsSync('public/css')) {
|
|
1193
|
-
fs.mkdirSync('public/css', { recursive: true });
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
// Create placeholder CSS
|
|
1197
|
-
fs.writeFileSync('public/css/style.css', '/* Run npm run build:css */\n');
|
|
1198
|
-
console.log('✅ Created public/css/style.css');
|
|
1199
|
-
|
|
1200
|
-
// Try to build CSS if tailwindcss is already installed
|
|
1201
|
-
const tailwindBin = path.join(process.cwd(), 'node_modules', '.bin', 'tailwindcss');
|
|
1202
|
-
if (fs.existsSync(tailwindBin)) {
|
|
1203
|
-
try {
|
|
1204
|
-
const { execSync } = require('child_process');
|
|
1205
|
-
console.log('\n🎨 Building Tailwind CSS from your templates...');
|
|
1206
|
-
execSync('npm run build:css', { stdio: 'inherit', cwd: process.cwd() });
|
|
1207
|
-
console.log('✅ Tailwind CSS built successfully!\n');
|
|
1208
|
-
} catch (err) {
|
|
1209
|
-
console.log('\n⚠️ CSS build failed. Run "npm run build:css" manually.\n');
|
|
1210
|
-
}
|
|
1211
|
-
} else {
|
|
1212
|
-
console.log('\n✅ Tailwind CSS added successfully!\n');
|
|
1213
|
-
console.log('Next steps:');
|
|
1214
|
-
console.log(' npm install');
|
|
1215
|
-
console.log(' npm run build:css');
|
|
1216
|
-
console.log(' npm run dev\n');
|
|
1217
|
-
}
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
// ============================================================================
|
|
1221
|
-
// Database Commands
|
|
1222
|
-
// ============================================================================
|
|
1223
|
-
|
|
1224
|
-
/**
|
|
1225
|
-
* Load database configuration
|
|
1226
|
-
* @param {string} [configPath] - Custom config path
|
|
1227
|
-
* @returns {Object} Database config
|
|
1228
|
-
*/
|
|
1229
|
-
function loadDbConfig(configPath) {
|
|
1230
|
-
const defaultPaths = ['webspresso.db.js', 'knexfile.js'];
|
|
1231
|
-
const paths = configPath ? [configPath, ...defaultPaths] : defaultPaths;
|
|
1232
|
-
|
|
1233
|
-
for (const p of paths) {
|
|
1234
|
-
const fullPath = path.resolve(process.cwd(), p);
|
|
1235
|
-
if (fs.existsSync(fullPath)) {
|
|
1236
|
-
return { config: require(fullPath), path: fullPath };
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
console.error('❌ Database config not found. Create webspresso.db.js or knexfile.js');
|
|
1241
|
-
process.exit(1);
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* Create database instance from config
|
|
1246
|
-
* @param {Object} config - Database config
|
|
1247
|
-
* @param {string} [env] - Environment name
|
|
1248
|
-
* @returns {Promise<Object>} Database instance
|
|
1249
|
-
*/
|
|
1250
|
-
async function createDbInstance(config, env) {
|
|
1251
|
-
const environment = env || process.env.NODE_ENV || 'development';
|
|
1252
|
-
const dbConfig = config[environment] || config;
|
|
1253
|
-
|
|
1254
|
-
// Dynamic import knex
|
|
1255
|
-
let knex;
|
|
1256
|
-
try {
|
|
1257
|
-
knex = require('knex');
|
|
1258
|
-
} catch {
|
|
1259
|
-
console.error('❌ Knex not installed. Run: npm install knex');
|
|
1260
|
-
process.exit(1);
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
return knex(dbConfig);
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// db:migrate command
|
|
1267
|
-
program
|
|
1268
|
-
.command('db:migrate')
|
|
1269
|
-
.description('Run pending database migrations')
|
|
1270
|
-
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
1271
|
-
.option('-c, --config <path>', 'Path to database config file')
|
|
1272
|
-
.action(async (options) => {
|
|
1273
|
-
const { config, path: configPath } = loadDbConfig(options.config);
|
|
1274
|
-
console.log(`\n📦 Using config: ${configPath}`);
|
|
1275
|
-
console.log(` Environment: ${options.env}\n`);
|
|
1276
|
-
|
|
1277
|
-
const knex = await createDbInstance(config, options.env);
|
|
1278
|
-
|
|
1279
|
-
try {
|
|
1280
|
-
const migrationConfig = config.migrations || {};
|
|
1281
|
-
const [batch, migrations] = await knex.migrate.latest(migrationConfig);
|
|
1282
|
-
|
|
1283
|
-
if (migrations.length === 0) {
|
|
1284
|
-
console.log('✅ Already up to date.\n');
|
|
1285
|
-
} else {
|
|
1286
|
-
console.log(`Running migrations (batch ${batch}):`);
|
|
1287
|
-
for (const m of migrations) {
|
|
1288
|
-
console.log(` → ${m}`);
|
|
1289
|
-
}
|
|
1290
|
-
console.log(`\n✅ Done. ${migrations.length} migration(s) completed.\n`);
|
|
1291
|
-
}
|
|
1292
|
-
} catch (err) {
|
|
1293
|
-
console.error('❌ Migration failed:', err.message);
|
|
1294
|
-
process.exit(1);
|
|
1295
|
-
} finally {
|
|
1296
|
-
await knex.destroy();
|
|
1297
|
-
}
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
// db:rollback command
|
|
1301
|
-
program
|
|
1302
|
-
.command('db:rollback')
|
|
1303
|
-
.description('Rollback the last batch of migrations')
|
|
1304
|
-
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
1305
|
-
.option('-c, --config <path>', 'Path to database config file')
|
|
1306
|
-
.option('-a, --all', 'Rollback all migrations')
|
|
1307
|
-
.action(async (options) => {
|
|
1308
|
-
const { config, path: configPath } = loadDbConfig(options.config);
|
|
1309
|
-
console.log(`\n📦 Using config: ${configPath}`);
|
|
1310
|
-
console.log(` Environment: ${options.env}\n`);
|
|
1311
|
-
|
|
1312
|
-
const knex = await createDbInstance(config, options.env);
|
|
1313
|
-
|
|
1314
|
-
try {
|
|
1315
|
-
const migrationConfig = {
|
|
1316
|
-
...(config.migrations || {}),
|
|
1317
|
-
...(options.all ? { all: true } : {}),
|
|
1318
|
-
};
|
|
1319
|
-
|
|
1320
|
-
const [batch, migrations] = await knex.migrate.rollback(migrationConfig);
|
|
1321
|
-
|
|
1322
|
-
if (migrations.length === 0) {
|
|
1323
|
-
console.log('✅ Nothing to rollback.\n');
|
|
1324
|
-
} else {
|
|
1325
|
-
console.log(`Rolling back${options.all ? ' all' : ''} migrations:`);
|
|
1326
|
-
for (const m of migrations) {
|
|
1327
|
-
console.log(` ← ${m}`);
|
|
1328
|
-
}
|
|
1329
|
-
console.log(`\n✅ Done. ${migrations.length} migration(s) rolled back.\n`);
|
|
1330
|
-
}
|
|
1331
|
-
} catch (err) {
|
|
1332
|
-
console.error('❌ Rollback failed:', err.message);
|
|
1333
|
-
process.exit(1);
|
|
1334
|
-
} finally {
|
|
1335
|
-
await knex.destroy();
|
|
1336
|
-
}
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
// db:status command
|
|
1340
|
-
program
|
|
1341
|
-
.command('db:status')
|
|
1342
|
-
.description('Show migration status')
|
|
1343
|
-
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
1344
|
-
.option('-c, --config <path>', 'Path to database config file')
|
|
1345
|
-
.action(async (options) => {
|
|
1346
|
-
const { config, path: configPath } = loadDbConfig(options.config);
|
|
1347
|
-
console.log(`\n📦 Using config: ${configPath}`);
|
|
1348
|
-
console.log(` Environment: ${options.env}\n`);
|
|
1349
|
-
|
|
1350
|
-
const knex = await createDbInstance(config, options.env);
|
|
1351
|
-
|
|
1352
|
-
try {
|
|
1353
|
-
const migrationConfig = config.migrations || {};
|
|
1354
|
-
const [completed, pending] = await knex.migrate.list(migrationConfig);
|
|
1355
|
-
|
|
1356
|
-
console.log('Migration Status');
|
|
1357
|
-
console.log('================\n');
|
|
1358
|
-
|
|
1359
|
-
// Sort all migrations by name
|
|
1360
|
-
const all = [
|
|
1361
|
-
...completed.map(m => ({ name: m.name || m, completed: true })),
|
|
1362
|
-
...pending.map(m => ({ name: m.name || m, completed: false })),
|
|
1363
|
-
].sort((a, b) => a.name.localeCompare(b.name));
|
|
1364
|
-
|
|
1365
|
-
if (all.length === 0) {
|
|
1366
|
-
console.log(' No migrations found.\n');
|
|
1367
|
-
} else {
|
|
1368
|
-
for (const m of all) {
|
|
1369
|
-
const status = m.completed ? '✓' : '○';
|
|
1370
|
-
const suffix = m.completed ? '' : ' (pending)';
|
|
1371
|
-
console.log(` ${status} ${m.name}${suffix}`);
|
|
1372
|
-
}
|
|
1373
|
-
console.log(`\n Total: ${all.length} (${completed.length} completed, ${pending.length} pending)\n`);
|
|
1374
|
-
}
|
|
1375
|
-
} catch (err) {
|
|
1376
|
-
console.error('❌ Failed to get status:', err.message);
|
|
1377
|
-
process.exit(1);
|
|
1378
|
-
} finally {
|
|
1379
|
-
await knex.destroy();
|
|
1380
|
-
}
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
|
-
// db:make command
|
|
1384
|
-
program
|
|
1385
|
-
.command('db:make <name>')
|
|
1386
|
-
.description('Create a new migration file')
|
|
1387
|
-
.option('-c, --config <path>', 'Path to database config file')
|
|
1388
|
-
.option('-m, --model <model>', 'Generate migration from model (requires models directory)')
|
|
1389
|
-
.action(async (name, options) => {
|
|
1390
|
-
const { config, path: configPath } = loadDbConfig(options.config);
|
|
1391
|
-
console.log(`\n📦 Using config: ${configPath}\n`);
|
|
1392
|
-
|
|
1393
|
-
const migrationDir = config.migrations?.directory || './migrations';
|
|
1394
|
-
|
|
1395
|
-
// Ensure migrations directory exists
|
|
1396
|
-
if (!fs.existsSync(migrationDir)) {
|
|
1397
|
-
fs.mkdirSync(migrationDir, { recursive: true });
|
|
1398
|
-
console.log(`Created directory: ${migrationDir}`);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// Generate filename with timestamp
|
|
1402
|
-
const now = new Date();
|
|
1403
|
-
const timestamp = [
|
|
1404
|
-
now.getFullYear(),
|
|
1405
|
-
String(now.getMonth() + 1).padStart(2, '0'),
|
|
1406
|
-
String(now.getDate()).padStart(2, '0'),
|
|
1407
|
-
'_',
|
|
1408
|
-
String(now.getHours()).padStart(2, '0'),
|
|
1409
|
-
String(now.getMinutes()).padStart(2, '0'),
|
|
1410
|
-
String(now.getSeconds()).padStart(2, '0'),
|
|
1411
|
-
].join('');
|
|
1412
|
-
|
|
1413
|
-
const filename = `${timestamp}_${name}.js`;
|
|
1414
|
-
const filepath = path.join(migrationDir, filename);
|
|
1415
|
-
|
|
1416
|
-
let content;
|
|
1417
|
-
|
|
1418
|
-
if (options.model) {
|
|
1419
|
-
// Try to load model and generate migration from schema
|
|
1420
|
-
const modelsDir = config.models || './models';
|
|
1421
|
-
const modelPath = path.resolve(process.cwd(), modelsDir, `${options.model}.js`);
|
|
1422
|
-
|
|
1423
|
-
if (fs.existsSync(modelPath)) {
|
|
1424
|
-
try {
|
|
1425
|
-
const model = require(modelPath);
|
|
1426
|
-
const { scaffoldMigration } = require('../core/orm/migrations/scaffold');
|
|
1427
|
-
content = scaffoldMigration(model);
|
|
1428
|
-
console.log(`Generated migration from model: ${options.model}`);
|
|
1429
|
-
} catch (err) {
|
|
1430
|
-
console.warn(`⚠️ Could not generate from model: ${err.message}`);
|
|
1431
|
-
console.log(' Creating empty migration instead.\n');
|
|
1432
|
-
content = getDefaultMigrationContent(name);
|
|
1433
|
-
}
|
|
1434
|
-
} else {
|
|
1435
|
-
console.warn(`⚠️ Model not found: ${modelPath}`);
|
|
1436
|
-
console.log(' Creating empty migration instead.\n');
|
|
1437
|
-
content = getDefaultMigrationContent(name);
|
|
1438
|
-
}
|
|
1439
|
-
} else {
|
|
1440
|
-
content = getDefaultMigrationContent(name);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
fs.writeFileSync(filepath, content);
|
|
1444
|
-
console.log(`✅ Created: ${filepath}\n`);
|
|
1445
|
-
});
|
|
1446
|
-
|
|
1447
|
-
/**
|
|
1448
|
-
* Get default migration content
|
|
1449
|
-
* @param {string} name - Migration name
|
|
1450
|
-
* @returns {string}
|
|
1451
|
-
*/
|
|
1452
|
-
function getDefaultMigrationContent(name) {
|
|
1453
|
-
// Parse table name from migration name (e.g., create_users_table -> users)
|
|
1454
|
-
const match = name.match(/^create_(\w+)_table$/);
|
|
1455
|
-
const tableName = match ? match[1] : 'table_name';
|
|
1456
|
-
|
|
1457
|
-
return `/**
|
|
1458
|
-
* Migration: ${name}
|
|
1459
|
-
*/
|
|
1460
|
-
|
|
1461
|
-
exports.up = function(knex) {
|
|
1462
|
-
return knex.schema.createTable('${tableName}', (table) => {
|
|
1463
|
-
table.bigIncrements('id').primary();
|
|
1464
|
-
// Add your columns here
|
|
1465
|
-
table.timestamps(true, true);
|
|
1466
|
-
});
|
|
1467
|
-
};
|
|
1468
|
-
|
|
1469
|
-
exports.down = function(knex) {
|
|
1470
|
-
return knex.schema.dropTableIfExists('${tableName}');
|
|
1471
|
-
};
|
|
1472
|
-
`;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// seed command
|
|
1476
|
-
program
|
|
1477
|
-
.command('seed')
|
|
1478
|
-
.description('Run database seeders to populate database with fake data')
|
|
1479
|
-
.option('-c, --config <path>', 'Path to database config file')
|
|
1480
|
-
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
1481
|
-
.option('--setup', 'Setup seed files if they don\'t exist')
|
|
1482
|
-
.action(async (options) => {
|
|
1483
|
-
// Check if it's a Webspresso project
|
|
1484
|
-
if (!fs.existsSync(path.join(process.cwd(), 'server.js')) &&
|
|
1485
|
-
!fs.existsSync(path.join(process.cwd(), 'pages'))) {
|
|
1486
|
-
console.error('❌ Not a Webspresso project! Run this command in your project directory.');
|
|
1487
|
-
process.exit(1);
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
// Check for faker in the project's node_modules
|
|
1491
|
-
// Use require.resolve with paths to ensure we load from the project directory
|
|
1492
|
-
let faker;
|
|
1493
|
-
try {
|
|
1494
|
-
// Try to resolve from project's node_modules first
|
|
1495
|
-
const resolvedPath = require.resolve('@faker-js/faker', {
|
|
1496
|
-
paths: [process.cwd(), path.join(process.cwd(), 'node_modules')]
|
|
1497
|
-
});
|
|
1498
|
-
faker = require(resolvedPath);
|
|
1499
|
-
} catch {
|
|
1500
|
-
console.error('❌ @faker-js/faker not installed.');
|
|
1501
|
-
|
|
1502
|
-
// Check if it's in package.json
|
|
1503
|
-
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
1504
|
-
let shouldInstall = false;
|
|
1505
|
-
|
|
1506
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
1507
|
-
try {
|
|
1508
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
1509
|
-
const hasFaker = packageJson.dependencies?.['@faker-js/faker'] ||
|
|
1510
|
-
packageJson.devDependencies?.['@faker-js/faker'];
|
|
1511
|
-
|
|
1512
|
-
if (hasFaker) {
|
|
1513
|
-
console.log(' @faker-js/faker is in package.json but not installed.');
|
|
1514
|
-
console.log(' Run: npm install');
|
|
1515
|
-
process.exit(1);
|
|
1516
|
-
} else {
|
|
1517
|
-
// Ask if user wants to install it
|
|
1518
|
-
const { install } = await inquirer.prompt([
|
|
1519
|
-
{
|
|
1520
|
-
type: 'confirm',
|
|
1521
|
-
name: 'install',
|
|
1522
|
-
message: 'Install @faker-js/faker now?',
|
|
1523
|
-
default: true
|
|
1524
|
-
}
|
|
1525
|
-
]);
|
|
1526
|
-
|
|
1527
|
-
if (install) {
|
|
1528
|
-
shouldInstall = true;
|
|
1529
|
-
} else {
|
|
1530
|
-
console.log(' Install it manually with: npm install @faker-js/faker');
|
|
1531
|
-
process.exit(1);
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
} catch (err) {
|
|
1535
|
-
console.log(' Install it with: npm install @faker-js/faker');
|
|
1536
|
-
process.exit(1);
|
|
1537
|
-
}
|
|
1538
|
-
} else {
|
|
1539
|
-
console.log(' Install it with: npm install @faker-js/faker');
|
|
1540
|
-
process.exit(1);
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
// Install faker if user confirmed
|
|
1544
|
-
if (shouldInstall) {
|
|
1545
|
-
console.log('\n📦 Installing @faker-js/faker...\n');
|
|
1546
|
-
try {
|
|
1547
|
-
execSync('npm install @faker-js/faker', {
|
|
1548
|
-
stdio: 'inherit',
|
|
1549
|
-
cwd: process.cwd()
|
|
1550
|
-
});
|
|
1551
|
-
console.log('\n✅ @faker-js/faker installed successfully!\n');
|
|
1552
|
-
|
|
1553
|
-
// Try to require again from project's node_modules
|
|
1554
|
-
const resolvedPath = require.resolve('@faker-js/faker', {
|
|
1555
|
-
paths: [process.cwd(), path.join(process.cwd(), 'node_modules')]
|
|
1556
|
-
});
|
|
1557
|
-
// Clear cache to ensure fresh load
|
|
1558
|
-
delete require.cache[resolvedPath];
|
|
1559
|
-
faker = require(resolvedPath);
|
|
1560
|
-
} catch (err) {
|
|
1561
|
-
console.error('\n❌ Failed to install @faker-js/faker:', err.message);
|
|
1562
|
-
console.log(' Install it manually with: npm install @faker-js/faker');
|
|
1563
|
-
process.exit(1);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
// Load database config
|
|
1569
|
-
const { config, path: configPath } = loadDbConfig(options.config);
|
|
1570
|
-
console.log(`\n📦 Using config: ${configPath}`);
|
|
1571
|
-
console.log(` Environment: ${options.env}\n`);
|
|
1572
|
-
|
|
1573
|
-
// Check if models directory exists
|
|
1574
|
-
const modelsDir = path.join(process.cwd(), 'models');
|
|
1575
|
-
if (!fs.existsSync(modelsDir)) {
|
|
1576
|
-
console.error('❌ models/ directory not found.');
|
|
1577
|
-
console.log(' Create models first, then run: webspresso seed');
|
|
1578
|
-
process.exit(1);
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// Check if seeds directory exists
|
|
1582
|
-
const seedsDir = path.join(process.cwd(), 'seeds');
|
|
1583
|
-
const seedIndexPath = path.join(seedsDir, 'index.js');
|
|
1584
|
-
|
|
1585
|
-
if (!fs.existsSync(seedsDir) || !fs.existsSync(seedIndexPath)) {
|
|
1586
|
-
if (options.setup) {
|
|
1587
|
-
console.log('📝 Setting up seed files...\n');
|
|
1588
|
-
|
|
1589
|
-
// Create seeds directory
|
|
1590
|
-
if (!fs.existsSync(seedsDir)) {
|
|
1591
|
-
fs.mkdirSync(seedsDir, { recursive: true });
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
// Create seed index file
|
|
1595
|
-
const seedIndex = `require('dotenv').config();
|
|
1596
|
-
const { faker } = require('@faker-js/faker');
|
|
1597
|
-
const path = require('path');
|
|
1598
|
-
const fs = require('fs');
|
|
1599
|
-
const { createDatabase, getAllModels } = require('webspresso/orm');
|
|
1600
|
-
const dbConfig = require('../webspresso.db.js');
|
|
1601
|
-
|
|
1602
|
-
const db = createDatabase(dbConfig);
|
|
1603
|
-
const seeder = db.seeder(faker);
|
|
1604
|
-
|
|
1605
|
-
/**
|
|
1606
|
-
* Load all models from models/ directory
|
|
1607
|
-
*/
|
|
1608
|
-
function loadModels() {
|
|
1609
|
-
const modelsDir = path.join(__dirname, '..', 'models');
|
|
1610
|
-
|
|
1611
|
-
if (!fs.existsSync(modelsDir)) {
|
|
1612
|
-
return [];
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
const modelFiles = fs.readdirSync(modelsDir)
|
|
1616
|
-
.filter(file => file.endsWith('.js') && !file.startsWith('_'));
|
|
1617
|
-
|
|
1618
|
-
const loadedModels = [];
|
|
1619
|
-
for (const file of modelFiles) {
|
|
1620
|
-
try {
|
|
1621
|
-
require(path.join(modelsDir, file));
|
|
1622
|
-
loadedModels.push(file);
|
|
1623
|
-
} catch (error) {
|
|
1624
|
-
console.warn(\`⚠️ Failed to load model from \${file}:\`, error.message);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
return loadedModels;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
/**
|
|
1632
|
-
* Seed database with fake data
|
|
1633
|
-
* This script automatically detects models in the models/ directory
|
|
1634
|
-
* and generates seed data based on their schemas.
|
|
1635
|
-
*/
|
|
1636
|
-
async function runSeeds() {
|
|
1637
|
-
try {
|
|
1638
|
-
console.log('🌱 Starting seed process...');
|
|
1639
|
-
|
|
1640
|
-
// Load all models
|
|
1641
|
-
const loadedFiles = loadModels();
|
|
1642
|
-
|
|
1643
|
-
if (loadedFiles.length === 0) {
|
|
1644
|
-
console.log('⚠️ No model files found in models/ directory.');
|
|
1645
|
-
console.log(' Create models first, then run: webspresso seed');
|
|
1646
|
-
await db.knex.destroy();
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
// Get all registered models
|
|
1651
|
-
const models = getAllModels();
|
|
1652
|
-
|
|
1653
|
-
if (models.size === 0) {
|
|
1654
|
-
console.log('⚠️ No models registered. Make sure your model files export models using defineModel().');
|
|
1655
|
-
await db.knex.destroy();
|
|
1656
|
-
return;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
console.log(\`📦 Found \${models.size} model(s):\`);
|
|
1660
|
-
for (const [name] of models) {
|
|
1661
|
-
console.log(\` - \${name}\`);
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
// Seed each model
|
|
1665
|
-
const results = {};
|
|
1666
|
-
for (const [modelName, model] of models) {
|
|
1667
|
-
console.log(\`\\n🌱 Seeding \${modelName}...\`);
|
|
1668
|
-
|
|
1669
|
-
// Default count: 10 records per model
|
|
1670
|
-
const count = 10;
|
|
1671
|
-
const records = await seeder.seed(modelName, count);
|
|
1672
|
-
results[modelName] = records.length;
|
|
1673
|
-
|
|
1674
|
-
console.log(\`✅ Created \${records.length} \${modelName} record(s)\`);
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
console.log(\`\\n✨ Seed completed! Created:\`);
|
|
1678
|
-
for (const [modelName, count] of Object.entries(results)) {
|
|
1679
|
-
console.log(\` - \${count} \${modelName} record(s)\`);
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
await db.knex.destroy();
|
|
1683
|
-
} catch (error) {
|
|
1684
|
-
console.error('❌ Seed failed:', error);
|
|
1685
|
-
await db.knex.destroy().catch(() => {});
|
|
1686
|
-
process.exit(1);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
runSeeds();
|
|
1691
|
-
`;
|
|
1692
|
-
|
|
1693
|
-
fs.writeFileSync(seedIndexPath, seedIndex);
|
|
1694
|
-
console.log('✅ Seed files created!\n');
|
|
1695
|
-
} else {
|
|
1696
|
-
console.error('❌ seeds/index.js not found.');
|
|
1697
|
-
console.log(' Run with --setup flag to create seed files: webspresso seed --setup');
|
|
1698
|
-
process.exit(1);
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
// Run the seed script
|
|
1703
|
-
try {
|
|
1704
|
-
require(seedIndexPath);
|
|
1705
|
-
} catch (error) {
|
|
1706
|
-
console.error('❌ Failed to run seed:', error.message);
|
|
1707
|
-
process.exit(1);
|
|
1708
|
-
}
|
|
1709
|
-
});
|
|
15
|
+
// Register commands
|
|
16
|
+
const { registerCommand: registerNew } = require('./commands/new');
|
|
17
|
+
const { registerCommand: registerPage } = require('./commands/page');
|
|
18
|
+
const { registerCommand: registerApi } = require('./commands/api');
|
|
19
|
+
const { registerCommand: registerDev } = require('./commands/dev');
|
|
20
|
+
const { registerCommand: registerStart } = require('./commands/start');
|
|
21
|
+
const { registerCommand: registerAddTailwind } = require('./commands/add-tailwind');
|
|
22
|
+
const { registerCommand: registerDbMigrate } = require('./commands/db-migrate');
|
|
23
|
+
const { registerCommand: registerDbRollback } = require('./commands/db-rollback');
|
|
24
|
+
const { registerCommand: registerDbStatus } = require('./commands/db-status');
|
|
25
|
+
const { registerCommand: registerDbMake } = require('./commands/db-make');
|
|
26
|
+
const { registerCommand: registerSeed } = require('./commands/seed');
|
|
27
|
+
|
|
28
|
+
registerNew(program);
|
|
29
|
+
registerPage(program);
|
|
30
|
+
registerApi(program);
|
|
31
|
+
registerDev(program);
|
|
32
|
+
registerStart(program);
|
|
33
|
+
registerAddTailwind(program);
|
|
34
|
+
registerDbMigrate(program);
|
|
35
|
+
registerDbRollback(program);
|
|
36
|
+
registerDbStatus(program);
|
|
37
|
+
registerDbMake(program);
|
|
38
|
+
registerSeed(program);
|
|
1710
39
|
|
|
1711
40
|
// Parse arguments
|
|
1712
41
|
program.parse();
|
|
1713
|
-
|