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 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
- const envExample = `PORT=3000
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.10",
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
- "better-sqlite3": "^9.0.0",
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
  }