webspresso 0.0.10 ā 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/bin/webspresso.js +458 -2
- package/package.json +5 -4
- package/src/helpers.js +162 -0
package/README.md
CHANGED
|
@@ -73,6 +73,8 @@ webspresso new my-app --no-tailwind
|
|
|
73
73
|
- Asks if you want to install in the current directory
|
|
74
74
|
- If current directory is not empty, shows a warning
|
|
75
75
|
- Prompts for project name (defaults to current folder name)
|
|
76
|
+
- Asks if you will use a database (SQLite, PostgreSQL, or MySQL)
|
|
77
|
+
- If yes, adds the appropriate driver to `package.json` and creates `webspresso.db.js` config
|
|
76
78
|
- After project creation, asks if you want to install dependencies
|
|
77
79
|
- If yes, runs `npm install` and `npm run build:css`
|
|
78
80
|
- Then asks if you want to start the development server
|
|
@@ -96,6 +98,43 @@ webspresso new my-app
|
|
|
96
98
|
|
|
97
99
|
**Note:** When dev server starts with Tailwind CSS, it automatically runs `watch:css` in the background to watch for CSS changes.
|
|
98
100
|
|
|
101
|
+
**Database Selection:**
|
|
102
|
+
During project creation, you'll be asked if you want to use a database:
|
|
103
|
+
- **SQLite** (better-sqlite3) - Recommended for development and small projects
|
|
104
|
+
- **PostgreSQL** (pg) - For production applications
|
|
105
|
+
- **MySQL** (mysql2) - Alternative SQL database
|
|
106
|
+
|
|
107
|
+
If you select a database:
|
|
108
|
+
- The appropriate driver is added to `package.json` dependencies
|
|
109
|
+
- `webspresso.db.js` config file is created with proper settings
|
|
110
|
+
- `migrations/` directory is created
|
|
111
|
+
- `models/` directory is created
|
|
112
|
+
- `DATABASE_URL` is added to `.env.example` with a template
|
|
113
|
+
|
|
114
|
+
**Seed Data Generation:**
|
|
115
|
+
After selecting a database, you'll be asked if you want to generate seed data:
|
|
116
|
+
- If yes, `@faker-js/faker` is added to dependencies
|
|
117
|
+
- `seeds/` directory is created with `seeds/index.js`
|
|
118
|
+
- `npm run seed` script is added to `package.json`
|
|
119
|
+
- The seed script automatically detects models in `models/` directory and generates fake data based on their schemas
|
|
120
|
+
|
|
121
|
+
To run seeds after creating models:
|
|
122
|
+
```bash
|
|
123
|
+
npm run seed
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The seed script will:
|
|
127
|
+
- Load all models from `models/` directory
|
|
128
|
+
- Generate 10 fake records per model (by default)
|
|
129
|
+
- Use smart field detection based on column names (email, name, title, etc.)
|
|
130
|
+
|
|
131
|
+
You can always add database support later by:
|
|
132
|
+
1. Installing the driver: `npm install better-sqlite3` (or `pg`, `mysql2`)
|
|
133
|
+
2. Creating `webspresso.db.js` config file
|
|
134
|
+
3. Adding `DATABASE_URL` to your `.env` file
|
|
135
|
+
4. Creating `models/` directory and defining your models
|
|
136
|
+
5. Optionally adding seed support: `npm install @faker-js/faker` and creating `seeds/index.js`
|
|
137
|
+
|
|
99
138
|
Options:
|
|
100
139
|
- `-i, --install` - Auto run `npm install` and `npm run build:css` (non-interactive)
|
|
101
140
|
- `--no-tailwind` - Skip Tailwind CSS setup
|
|
@@ -676,6 +715,17 @@ The `fsy` object is available in all templates:
|
|
|
676
715
|
{{ fsy.prettyBytes(1024) }}
|
|
677
716
|
{{ fsy.prettyMs(5000) }}
|
|
678
717
|
|
|
718
|
+
{# Date/Time helpers (dayjs) #}
|
|
719
|
+
{{ fsy.dateFormat(post.created_at, 'YYYY-MM-DD HH:mm') }}
|
|
720
|
+
{{ fsy.dateFromNow(post.created_at) }} {# "2 hours ago" #}
|
|
721
|
+
{{ fsy.dateAgo(post.created_at) }} {# "2 hours ago" #}
|
|
722
|
+
{{ fsy.dateUntil(event.date) }} {# "in 2 hours" #}
|
|
723
|
+
{{ fsy.date(post.created_at).format('MMMM D, YYYY') }} {# Full dayjs API #}
|
|
724
|
+
{% if fsy.dateIsBefore(post.created_at, fsy.date()) %}Published{% endif %}
|
|
725
|
+
{{ fsy.dateDiff(post.created_at, fsy.date(), 'day') }} days ago
|
|
726
|
+
{{ fsy.dateAdd(post.created_at, 7, 'day').format('YYYY-MM-DD') }}
|
|
727
|
+
{{ fsy.dateStartOf(post.created_at, 'month').format('YYYY-MM-DD') }}
|
|
728
|
+
|
|
679
729
|
{# Environment #}
|
|
680
730
|
{% if fsy.isDev() %}Dev mode{% endif %}
|
|
681
731
|
|
|
@@ -1040,6 +1090,32 @@ npm install better-sqlite3
|
|
|
1040
1090
|
|
|
1041
1091
|
### Database Seeding
|
|
1042
1092
|
|
|
1093
|
+
**CLI Command:**
|
|
1094
|
+
|
|
1095
|
+
The easiest way to seed your database is using the CLI command:
|
|
1096
|
+
|
|
1097
|
+
```bash
|
|
1098
|
+
# Run seeds (requires seeds/index.js)
|
|
1099
|
+
webspresso seed
|
|
1100
|
+
|
|
1101
|
+
# Setup seed files if they don't exist
|
|
1102
|
+
webspresso seed --setup
|
|
1103
|
+
|
|
1104
|
+
# Use custom database config
|
|
1105
|
+
webspresso seed --config ./custom-db-config.js
|
|
1106
|
+
|
|
1107
|
+
# Use different environment
|
|
1108
|
+
webspresso seed --env production
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
The `webspresso seed` command:
|
|
1112
|
+
- Automatically loads all models from `models/` directory
|
|
1113
|
+
- Generates fake data based on model schemas
|
|
1114
|
+
- Creates 10 records per model by default
|
|
1115
|
+
- Uses smart field detection for appropriate fake data
|
|
1116
|
+
|
|
1117
|
+
**Manual Setup:**
|
|
1118
|
+
|
|
1043
1119
|
Generate fake data for testing and development using `@faker-js/faker`:
|
|
1044
1120
|
|
|
1045
1121
|
```bash
|
package/bin/webspresso.js
CHANGED
|
@@ -9,7 +9,7 @@ const { program } = require('commander');
|
|
|
9
9
|
const inquirer = require('inquirer');
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
-
const { spawn } = require('child_process');
|
|
12
|
+
const { spawn, execSync } = require('child_process');
|
|
13
13
|
|
|
14
14
|
program
|
|
15
15
|
.name('webspresso')
|
|
@@ -121,6 +121,43 @@ program
|
|
|
121
121
|
|
|
122
122
|
console.log(`\nš Creating new Webspresso project: ${projectName}\n`);
|
|
123
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
|
+
|
|
124
161
|
// Create directory structure (skip root if using current dir)
|
|
125
162
|
if (!useCurrentDir) {
|
|
126
163
|
fs.mkdirSync(projectPath, { recursive: true });
|
|
@@ -130,6 +167,11 @@ program
|
|
|
130
167
|
fs.mkdirSync(path.join(projectPath, 'views'), { recursive: true });
|
|
131
168
|
fs.mkdirSync(path.join(projectPath, 'public'), { recursive: true });
|
|
132
169
|
|
|
170
|
+
// Create models directory if database is selected
|
|
171
|
+
if (useDatabase && databaseType) {
|
|
172
|
+
fs.mkdirSync(path.join(projectPath, 'models'), { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
133
175
|
// Create package.json
|
|
134
176
|
const packageJson = {
|
|
135
177
|
name: projectName,
|
|
@@ -146,6 +188,23 @@ program
|
|
|
146
188
|
}
|
|
147
189
|
};
|
|
148
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
|
+
|
|
149
208
|
fs.writeFileSync(
|
|
150
209
|
path.join(projectPath, 'package.json'),
|
|
151
210
|
JSON.stringify(packageJson, null, 2) + '\n'
|
|
@@ -172,15 +231,186 @@ app.listen(PORT, () => {
|
|
|
172
231
|
fs.writeFileSync(path.join(projectPath, 'server.js'), serverJs);
|
|
173
232
|
|
|
174
233
|
// Create .env.example
|
|
175
|
-
|
|
234
|
+
let envExample = `PORT=3000
|
|
176
235
|
NODE_ENV=development
|
|
177
236
|
DEFAULT_LOCALE=en
|
|
178
237
|
SUPPORTED_LOCALES=en,tr
|
|
179
238
|
BASE_URL=http://localhost:3000
|
|
180
239
|
`;
|
|
181
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
|
+
|
|
182
254
|
fs.writeFileSync(path.join(projectPath, '.env.example'), envExample);
|
|
183
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
|
+
|
|
184
414
|
// Create .gitignore
|
|
185
415
|
const gitignore = `node_modules/
|
|
186
416
|
.env
|
|
@@ -1242,6 +1472,232 @@ exports.down = function(knex) {
|
|
|
1242
1472
|
`;
|
|
1243
1473
|
}
|
|
1244
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
|
|
1491
|
+
let faker;
|
|
1492
|
+
try {
|
|
1493
|
+
faker = require('@faker-js/faker');
|
|
1494
|
+
} catch {
|
|
1495
|
+
console.error('ā @faker-js/faker not installed.');
|
|
1496
|
+
|
|
1497
|
+
// Check if it's in package.json
|
|
1498
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
1499
|
+
let shouldInstall = false;
|
|
1500
|
+
|
|
1501
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
1502
|
+
try {
|
|
1503
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
1504
|
+
const hasFaker = packageJson.dependencies?.['@faker-js/faker'] ||
|
|
1505
|
+
packageJson.devDependencies?.['@faker-js/faker'];
|
|
1506
|
+
|
|
1507
|
+
if (hasFaker) {
|
|
1508
|
+
console.log(' @faker-js/faker is in package.json but not installed.');
|
|
1509
|
+
console.log(' Run: npm install');
|
|
1510
|
+
process.exit(1);
|
|
1511
|
+
} else {
|
|
1512
|
+
// Ask if user wants to install it
|
|
1513
|
+
const { install } = await inquirer.prompt([
|
|
1514
|
+
{
|
|
1515
|
+
type: 'confirm',
|
|
1516
|
+
name: 'install',
|
|
1517
|
+
message: 'Install @faker-js/faker now?',
|
|
1518
|
+
default: true
|
|
1519
|
+
}
|
|
1520
|
+
]);
|
|
1521
|
+
|
|
1522
|
+
if (install) {
|
|
1523
|
+
shouldInstall = true;
|
|
1524
|
+
} else {
|
|
1525
|
+
console.log(' Install it manually with: npm install @faker-js/faker');
|
|
1526
|
+
process.exit(1);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
console.log(' Install it with: npm install @faker-js/faker');
|
|
1531
|
+
process.exit(1);
|
|
1532
|
+
}
|
|
1533
|
+
} else {
|
|
1534
|
+
console.log(' Install it with: npm install @faker-js/faker');
|
|
1535
|
+
process.exit(1);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Install faker if user confirmed
|
|
1539
|
+
if (shouldInstall) {
|
|
1540
|
+
console.log('\nš¦ Installing @faker-js/faker...\n');
|
|
1541
|
+
try {
|
|
1542
|
+
execSync('npm install @faker-js/faker', {
|
|
1543
|
+
stdio: 'inherit',
|
|
1544
|
+
cwd: process.cwd()
|
|
1545
|
+
});
|
|
1546
|
+
console.log('\nā
@faker-js/faker installed successfully!\n');
|
|
1547
|
+
|
|
1548
|
+
// Try to require again
|
|
1549
|
+
faker = require('@faker-js/faker');
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
console.error('\nā Failed to install @faker-js/faker:', err.message);
|
|
1552
|
+
console.log(' Install it manually with: npm install @faker-js/faker');
|
|
1553
|
+
process.exit(1);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Load database config
|
|
1559
|
+
const { config, path: configPath } = loadDbConfig(options.config);
|
|
1560
|
+
console.log(`\nš¦ Using config: ${configPath}`);
|
|
1561
|
+
console.log(` Environment: ${options.env}\n`);
|
|
1562
|
+
|
|
1563
|
+
// Check if models directory exists
|
|
1564
|
+
const modelsDir = path.join(process.cwd(), 'models');
|
|
1565
|
+
if (!fs.existsSync(modelsDir)) {
|
|
1566
|
+
console.error('ā models/ directory not found.');
|
|
1567
|
+
console.log(' Create models first, then run: webspresso seed');
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Check if seeds directory exists
|
|
1572
|
+
const seedsDir = path.join(process.cwd(), 'seeds');
|
|
1573
|
+
const seedIndexPath = path.join(seedsDir, 'index.js');
|
|
1574
|
+
|
|
1575
|
+
if (!fs.existsSync(seedsDir) || !fs.existsSync(seedIndexPath)) {
|
|
1576
|
+
if (options.setup) {
|
|
1577
|
+
console.log('š Setting up seed files...\n');
|
|
1578
|
+
|
|
1579
|
+
// Create seeds directory
|
|
1580
|
+
if (!fs.existsSync(seedsDir)) {
|
|
1581
|
+
fs.mkdirSync(seedsDir, { recursive: true });
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Create seed index file
|
|
1585
|
+
const seedIndex = `require('dotenv').config();
|
|
1586
|
+
const { faker } = require('@faker-js/faker');
|
|
1587
|
+
const path = require('path');
|
|
1588
|
+
const fs = require('fs');
|
|
1589
|
+
const { createDatabase, getAllModels } = require('webspresso/orm');
|
|
1590
|
+
const dbConfig = require('../webspresso.db.js');
|
|
1591
|
+
|
|
1592
|
+
const db = createDatabase(dbConfig);
|
|
1593
|
+
const seeder = db.seeder(faker);
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* Load all models from models/ directory
|
|
1597
|
+
*/
|
|
1598
|
+
function loadModels() {
|
|
1599
|
+
const modelsDir = path.join(__dirname, '..', 'models');
|
|
1600
|
+
|
|
1601
|
+
if (!fs.existsSync(modelsDir)) {
|
|
1602
|
+
return [];
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const modelFiles = fs.readdirSync(modelsDir)
|
|
1606
|
+
.filter(file => file.endsWith('.js') && !file.startsWith('_'));
|
|
1607
|
+
|
|
1608
|
+
const loadedModels = [];
|
|
1609
|
+
for (const file of modelFiles) {
|
|
1610
|
+
try {
|
|
1611
|
+
require(path.join(modelsDir, file));
|
|
1612
|
+
loadedModels.push(file);
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
console.warn(\`ā ļø Failed to load model from \${file}:\`, error.message);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
return loadedModels;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Seed database with fake data
|
|
1623
|
+
* This script automatically detects models in the models/ directory
|
|
1624
|
+
* and generates seed data based on their schemas.
|
|
1625
|
+
*/
|
|
1626
|
+
async function runSeeds() {
|
|
1627
|
+
try {
|
|
1628
|
+
console.log('š± Starting seed process...');
|
|
1629
|
+
|
|
1630
|
+
// Load all models
|
|
1631
|
+
const loadedFiles = loadModels();
|
|
1632
|
+
|
|
1633
|
+
if (loadedFiles.length === 0) {
|
|
1634
|
+
console.log('ā ļø No model files found in models/ directory.');
|
|
1635
|
+
console.log(' Create models first, then run: webspresso seed');
|
|
1636
|
+
await db.knex.destroy();
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Get all registered models
|
|
1641
|
+
const models = getAllModels();
|
|
1642
|
+
|
|
1643
|
+
if (models.size === 0) {
|
|
1644
|
+
console.log('ā ļø No models registered. Make sure your model files export models using defineModel().');
|
|
1645
|
+
await db.knex.destroy();
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
console.log(\`š¦ Found \${models.size} model(s):\`);
|
|
1650
|
+
for (const [name] of models) {
|
|
1651
|
+
console.log(\` - \${name}\`);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Seed each model
|
|
1655
|
+
const results = {};
|
|
1656
|
+
for (const [modelName, model] of models) {
|
|
1657
|
+
console.log(\`\\nš± Seeding \${modelName}...\`);
|
|
1658
|
+
|
|
1659
|
+
// Default count: 10 records per model
|
|
1660
|
+
const count = 10;
|
|
1661
|
+
const records = await seeder.seed(modelName, count);
|
|
1662
|
+
results[modelName] = records.length;
|
|
1663
|
+
|
|
1664
|
+
console.log(\`ā
Created \${records.length} \${modelName} record(s)\`);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
console.log(\`\\n⨠Seed completed! Created:\`);
|
|
1668
|
+
for (const [modelName, count] of Object.entries(results)) {
|
|
1669
|
+
console.log(\` - \${count} \${modelName} record(s)\`);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
await db.knex.destroy();
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
console.error('ā Seed failed:', error);
|
|
1675
|
+
await db.knex.destroy().catch(() => {});
|
|
1676
|
+
process.exit(1);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
runSeeds();
|
|
1681
|
+
`;
|
|
1682
|
+
|
|
1683
|
+
fs.writeFileSync(seedIndexPath, seedIndex);
|
|
1684
|
+
console.log('ā
Seed files created!\n');
|
|
1685
|
+
} else {
|
|
1686
|
+
console.error('ā seeds/index.js not found.');
|
|
1687
|
+
console.log(' Run with --setup flag to create seed files: webspresso seed --setup');
|
|
1688
|
+
process.exit(1);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// Run the seed script
|
|
1693
|
+
try {
|
|
1694
|
+
require(seedIndexPath);
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
console.error('ā Failed to run seed:', error.message);
|
|
1697
|
+
process.exit(1);
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1245
1701
|
// Parse arguments
|
|
1246
1702
|
program.parse();
|
|
1247
1703
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"commander": "^11.1.0",
|
|
41
|
+
"dayjs": "^1.11.19",
|
|
41
42
|
"express": "^4.18.2",
|
|
42
43
|
"helmet": "^7.2.0",
|
|
43
44
|
"inquirer": "^8.2.6",
|
|
@@ -46,11 +47,11 @@
|
|
|
46
47
|
"zod": "^3.23.0"
|
|
47
48
|
},
|
|
48
49
|
"peerDependencies": {
|
|
50
|
+
"@faker-js/faker": "^9.0.0",
|
|
51
|
+
"better-sqlite3": "^9.0.0",
|
|
49
52
|
"dotenv": "^16.0.0",
|
|
50
|
-
"pg": "^8.0.0",
|
|
51
53
|
"mysql2": "^3.0.0",
|
|
52
|
-
"
|
|
53
|
-
"@faker-js/faker": "^9.0.0"
|
|
54
|
+
"pg": "^8.0.0"
|
|
54
55
|
},
|
|
55
56
|
"peerDependenciesMeta": {
|
|
56
57
|
"dotenv": {
|
package/src/helpers.js
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
const querystring = require('querystring');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const dayjs = require('dayjs');
|
|
10
|
+
const relativeTime = require('dayjs/plugin/relativeTime');
|
|
11
|
+
const utc = require('dayjs/plugin/utc');
|
|
12
|
+
const timezone = require('dayjs/plugin/timezone');
|
|
13
|
+
const customParseFormat = require('dayjs/plugin/customParseFormat');
|
|
14
|
+
|
|
15
|
+
// Extend dayjs with plugins
|
|
16
|
+
dayjs.extend(relativeTime);
|
|
17
|
+
dayjs.extend(utc);
|
|
18
|
+
dayjs.extend(timezone);
|
|
19
|
+
dayjs.extend(customParseFormat);
|
|
9
20
|
|
|
10
21
|
/**
|
|
11
22
|
* Asset Manager - handles asset paths, versioning, and manifest
|
|
@@ -419,6 +430,157 @@ function createHelpers(ctx) {
|
|
|
419
430
|
*/
|
|
420
431
|
img(src, alt = '', attrs = {}) {
|
|
421
432
|
return getAssetManager().img(src, alt, attrs);
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Date/time helpers using dayjs
|
|
437
|
+
*/
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Create a dayjs instance from a date
|
|
441
|
+
* @param {string|Date|number} date - Date to parse
|
|
442
|
+
* @param {string} format - Optional format string
|
|
443
|
+
* @returns {Object} dayjs instance
|
|
444
|
+
*/
|
|
445
|
+
date(date, format) {
|
|
446
|
+
if (!date) return dayjs();
|
|
447
|
+
if (format) {
|
|
448
|
+
return dayjs(date, format);
|
|
449
|
+
}
|
|
450
|
+
return dayjs(date);
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Format a date
|
|
455
|
+
* @param {string|Date|number} date - Date to format
|
|
456
|
+
* @param {string} format - Format string (default: 'YYYY-MM-DD')
|
|
457
|
+
* @returns {string}
|
|
458
|
+
*/
|
|
459
|
+
dateFormat(date, format = 'YYYY-MM-DD') {
|
|
460
|
+
if (!date) return '';
|
|
461
|
+
return dayjs(date).format(format);
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get relative time (e.g., "2 hours ago")
|
|
466
|
+
* @param {string|Date|number} date - Date to format
|
|
467
|
+
* @returns {string}
|
|
468
|
+
*/
|
|
469
|
+
dateFromNow(date) {
|
|
470
|
+
if (!date) return '';
|
|
471
|
+
return dayjs(date).fromNow();
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get time ago (e.g., "2 hours ago")
|
|
476
|
+
* @param {string|Date|number} date - Date to format
|
|
477
|
+
* @returns {string}
|
|
478
|
+
*/
|
|
479
|
+
dateAgo(date) {
|
|
480
|
+
if (!date) return '';
|
|
481
|
+
return dayjs(date).fromNow();
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get time until (e.g., "in 2 hours")
|
|
486
|
+
* @param {string|Date|number} date - Date to format
|
|
487
|
+
* @returns {string}
|
|
488
|
+
*/
|
|
489
|
+
dateUntil(date) {
|
|
490
|
+
if (!date) return '';
|
|
491
|
+
return dayjs(date).toNow();
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Check if date is before another date
|
|
496
|
+
* @param {string|Date|number} date1 - First date
|
|
497
|
+
* @param {string|Date|number} date2 - Second date
|
|
498
|
+
* @returns {boolean}
|
|
499
|
+
*/
|
|
500
|
+
dateIsBefore(date1, date2) {
|
|
501
|
+
if (!date1 || !date2) return false;
|
|
502
|
+
return dayjs(date1).isBefore(date2);
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Check if date is after another date
|
|
507
|
+
* @param {string|Date|number} date1 - First date
|
|
508
|
+
* @param {string|Date|number} date2 - Second date
|
|
509
|
+
* @returns {boolean}
|
|
510
|
+
*/
|
|
511
|
+
dateIsAfter(date1, date2) {
|
|
512
|
+
if (!date1 || !date2) return false;
|
|
513
|
+
return dayjs(date1).isAfter(date2);
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Check if date is same as another date
|
|
518
|
+
* @param {string|Date|number} date1 - First date
|
|
519
|
+
* @param {string|Date|number} date2 - Second date
|
|
520
|
+
* @param {string} unit - Unit to compare (day, month, year, etc.)
|
|
521
|
+
* @returns {boolean}
|
|
522
|
+
*/
|
|
523
|
+
dateIsSame(date1, date2, unit = 'day') {
|
|
524
|
+
if (!date1 || !date2) return false;
|
|
525
|
+
return dayjs(date1).isSame(date2, unit);
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get difference between two dates
|
|
530
|
+
* @param {string|Date|number} date1 - First date
|
|
531
|
+
* @param {string|Date|number} date2 - Second date
|
|
532
|
+
* @param {string} unit - Unit (day, month, year, hour, minute, second)
|
|
533
|
+
* @returns {number}
|
|
534
|
+
*/
|
|
535
|
+
dateDiff(date1, date2, unit = 'day') {
|
|
536
|
+
if (!date1 || !date2) return 0;
|
|
537
|
+
return dayjs(date1).diff(date2, unit);
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Add time to a date
|
|
542
|
+
* @param {string|Date|number} date - Date to add to
|
|
543
|
+
* @param {number} amount - Amount to add
|
|
544
|
+
* @param {string} unit - Unit (day, month, year, hour, minute, second)
|
|
545
|
+
* @returns {Object} dayjs instance
|
|
546
|
+
*/
|
|
547
|
+
dateAdd(date, amount, unit = 'day') {
|
|
548
|
+
if (!date) return dayjs();
|
|
549
|
+
return dayjs(date).add(amount, unit);
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Subtract time from a date
|
|
554
|
+
* @param {string|Date|number} date - Date to subtract from
|
|
555
|
+
* @param {number} amount - Amount to subtract
|
|
556
|
+
* @param {string} unit - Unit (day, month, year, hour, minute, second)
|
|
557
|
+
* @returns {Object} dayjs instance
|
|
558
|
+
*/
|
|
559
|
+
dateSubtract(date, amount, unit = 'day') {
|
|
560
|
+
if (!date) return dayjs();
|
|
561
|
+
return dayjs(date).subtract(amount, unit);
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Get start of a time period
|
|
566
|
+
* @param {string|Date|number} date - Date
|
|
567
|
+
* @param {string} unit - Unit (day, month, year, week)
|
|
568
|
+
* @returns {Object} dayjs instance
|
|
569
|
+
*/
|
|
570
|
+
dateStartOf(date, unit = 'day') {
|
|
571
|
+
if (!date) return dayjs();
|
|
572
|
+
return dayjs(date).startOf(unit);
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Get end of a time period
|
|
577
|
+
* @param {string|Date|number} date - Date
|
|
578
|
+
* @param {string} unit - Unit (day, month, year, week)
|
|
579
|
+
* @returns {Object} dayjs instance
|
|
580
|
+
*/
|
|
581
|
+
dateEndOf(date, unit = 'day') {
|
|
582
|
+
if (!date) return dayjs();
|
|
583
|
+
return dayjs(date).endOf(unit);
|
|
422
584
|
}
|
|
423
585
|
};
|
|
424
586
|
}
|