create-rebe 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.env.example +4 -0
- package/template/README.md +63 -8
- package/template/SECURITY.md +39 -0
- package/template/app/routes/api.route.js +1 -1
- package/template/app/routes/register.route.js +1 -1
- package/template/app/routes/web.route.js +1 -1
- package/template/app/socket/register.socket.js +0 -2
- package/template/config/express.config.js +8 -0
- package/template/core/common/string.js +1 -1
- package/template/core/cron.core.js +1 -0
- package/template/core/database.core.js +30 -17
- package/template/core/error.core.js +8 -4
- package/template/core/express.core.js +16 -4
- package/template/core/hooks.core.js +10 -7
- package/template/core/migrator.core.js +201 -0
- package/template/core/modules.core.js +167 -0
- package/template/core/queue.core.js +1 -0
- package/template/core/routing.core.d.ts +273 -0
- package/template/core/routing.core.js +666 -0
- package/template/core/seeder.core.js +105 -0
- package/template/core/socket.core.js +1 -0
- package/template/database/migrations/.gitkeep +0 -0
- package/template/database/seeders/.gitkeep +0 -0
- package/template/docs/Database.md +14 -8
- package/template/docs/Express.md +5 -2
- package/template/docs/Make.md +46 -0
- package/template/docs/Migration.md +56 -0
- package/template/docs/Modules.md +96 -0
- package/template/docs/README.md +5 -0
- package/template/docs/Routing.md +116 -0
- package/template/docs/Seeder.md +54 -0
- package/template/eslint.config.js +52 -0
- package/template/package-lock.json +1068 -70
- package/template/package.json +15 -8
- package/template/scripts/cli/args.js +39 -0
- package/template/scripts/cli/bootstrap.js +16 -0
- package/template/scripts/cli/db.js +79 -0
- package/template/scripts/cli/help.js +58 -0
- package/template/scripts/cli/keys.js +100 -0
- package/template/scripts/cli/log.js +58 -0
- package/template/scripts/cli/make.js +249 -0
- package/template/scripts/cli/names.js +51 -0
- package/template/scripts/cli/templates.js +358 -0
- package/template/scripts/cli.js +75 -234
- package/template/tests/http.test.js +99 -0
package/template/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rebe",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Ready-to-use Node.js backend framework. One process serves a JSON API, Socket.IO, cron jobs, and an in-process queue on a layered config/core/app architecture, with JWT auth, Sequelize, Winston logging, and centralized configuration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Refkinscallv <refkinscallv@gmail.com>",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"cli": "node ./scripts/cli.js",
|
|
37
37
|
"start": "node .",
|
|
38
38
|
"dev": "nodemon",
|
|
39
|
-
"format": "prettier --write ."
|
|
39
|
+
"format": "prettier --write .",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"audit:prod": "npm audit --omit=dev --audit-level=high"
|
|
40
42
|
},
|
|
41
43
|
"_moduleAliases": {
|
|
42
44
|
"@config": "config",
|
|
@@ -52,13 +54,18 @@
|
|
|
52
54
|
"@storage": "storage"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
57
|
+
"@eslint/js": "^10.0.1",
|
|
58
|
+
"eslint": "^10.5.0",
|
|
59
|
+
"eslint-plugin-n": "^18.1.0",
|
|
60
|
+
"eslint-plugin-security": "^4.0.1",
|
|
61
|
+
"globals": "^17.6.0",
|
|
55
62
|
"jest": "^30.4.2",
|
|
56
63
|
"nodemon": "^3.1.14",
|
|
57
|
-
"prettier": "^3.8.4"
|
|
64
|
+
"prettier": "^3.8.4",
|
|
65
|
+
"supertest": "^7.2.2"
|
|
58
66
|
},
|
|
59
67
|
"dependencies": {
|
|
60
|
-
"
|
|
61
|
-
"axios": "^1.17.0",
|
|
68
|
+
"axios": "^1.18.0",
|
|
62
69
|
"bcrypt": "^6.0.0",
|
|
63
70
|
"compression": "^1.8.1",
|
|
64
71
|
"cors": "^2.8.6",
|
|
@@ -69,10 +76,10 @@
|
|
|
69
76
|
"ioredis": "^5.11.1",
|
|
70
77
|
"jsonwebtoken": "^9.0.3",
|
|
71
78
|
"module-alias": "^2.3.4",
|
|
72
|
-
"multer": "^2.
|
|
79
|
+
"multer": "^2.2.0",
|
|
73
80
|
"mysql2": "^3.22.5",
|
|
74
|
-
"node-cron": "^4.
|
|
75
|
-
"nodemailer": "^
|
|
81
|
+
"node-cron": "^4.4.1",
|
|
82
|
+
"nodemailer": "^9.0.1",
|
|
76
83
|
"sequelize": "^6.37.8",
|
|
77
84
|
"socket.io": "^4.8.3",
|
|
78
85
|
"winston": "^3.19.0",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Tiny argv parser for CLI commands. Splits a raw args array into positionals and
|
|
4
|
+
// flags/options:
|
|
5
|
+
// --seed -> options.seed = true
|
|
6
|
+
// --step=2 -> options.step = '2'
|
|
7
|
+
// --module blog -> options.module = 'blog'
|
|
8
|
+
// --class=UserSeed -> options.class = 'UserSeed'
|
|
9
|
+
function parseArgs(argv = []) {
|
|
10
|
+
const positionals = []
|
|
11
|
+
const options = {}
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const token = argv[i]
|
|
15
|
+
if (!token.startsWith('--')) {
|
|
16
|
+
positionals.push(token)
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const body = token.slice(2)
|
|
21
|
+
const eq = body.indexOf('=')
|
|
22
|
+
if (eq !== -1) {
|
|
23
|
+
options[body.slice(0, eq)] = body.slice(eq + 1)
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const next = argv[i + 1]
|
|
28
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
29
|
+
options[body] = next
|
|
30
|
+
i++
|
|
31
|
+
} else {
|
|
32
|
+
options[body] = true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { positionals, options }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { parseArgs }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Lazily prepare the runtime for commands that touch the application cores
|
|
4
|
+
// (migrations, seeders). Registers module-alias so @core/@config resolve exactly as
|
|
5
|
+
// they do at app boot, then loads .env. Kept out of the top-level dispatcher so
|
|
6
|
+
// pure-scaffolding and key commands stay dependency-free and fast.
|
|
7
|
+
let prepared = false
|
|
8
|
+
|
|
9
|
+
function prepareRuntime() {
|
|
10
|
+
if (prepared) return
|
|
11
|
+
require('module-alias/register')
|
|
12
|
+
require('dotenv').config()
|
|
13
|
+
prepared = true
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { prepareRuntime }
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Log = require('./log')
|
|
4
|
+
const { parseArgs } = require('./args')
|
|
5
|
+
|
|
6
|
+
// Database commands. The dispatcher calls prepareRuntime() and closes the connection
|
|
7
|
+
// for these, so handlers can require the cores directly and stay focused on intent.
|
|
8
|
+
function migrator() {
|
|
9
|
+
return require('@core/migrator.core')
|
|
10
|
+
}
|
|
11
|
+
function seeder() {
|
|
12
|
+
return require('@core/seeder.core')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function maybeSeed(options) {
|
|
16
|
+
if (options.seed) {
|
|
17
|
+
Log.info('Seeding…')
|
|
18
|
+
await seeder().seed(typeof options.seed === 'string' ? options.seed : undefined)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function migrate() {
|
|
23
|
+
await migrator().up()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function rollback(argv) {
|
|
27
|
+
const { options } = parseArgs(argv)
|
|
28
|
+
const step = Math.max(1, Number(options.step) || 1)
|
|
29
|
+
await migrator().down({ step })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// reset / fresh: drop every table, then migrate from scratch (optionally seed).
|
|
33
|
+
async function reset(argv) {
|
|
34
|
+
const { options } = parseArgs(argv)
|
|
35
|
+
await migrator().dropAllTables()
|
|
36
|
+
await migrator().up()
|
|
37
|
+
await maybeSeed(options)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// refresh: roll every migration back via down(), then migrate up again (optionally seed).
|
|
41
|
+
async function refresh(argv) {
|
|
42
|
+
const { options } = parseArgs(argv)
|
|
43
|
+
await migrator().rollbackAll()
|
|
44
|
+
await migrator().up()
|
|
45
|
+
await maybeSeed(options)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function wipe() {
|
|
49
|
+
await migrator().dropAllTables()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function status() {
|
|
53
|
+
const rows = await migrator().status()
|
|
54
|
+
Log.banner()
|
|
55
|
+
console.log(Log.color('yellow', 'Migrations'))
|
|
56
|
+
Log.line()
|
|
57
|
+
if (!rows.length) {
|
|
58
|
+
console.log(Log.color('gray', ' (none found)'))
|
|
59
|
+
} else {
|
|
60
|
+
for (const r of rows) {
|
|
61
|
+
const tag = r.applied ? Log.color('green', `[applied · batch ${r.batch}]`) : Log.color('gray', '[pending]')
|
|
62
|
+
console.log(` ${tag} ${r.name}`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// db:seed [--all] [--class=Name]
|
|
69
|
+
async function seed(argv) {
|
|
70
|
+
const { options } = parseArgs(argv)
|
|
71
|
+
if (options.all) return seeder().seedAll()
|
|
72
|
+
return seeder().seed(options.class)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function seedAll() {
|
|
76
|
+
await seeder().seedAll()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { migrate, rollback, reset, fresh: reset, refresh, wipe, status, seed, seedAll }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Log = require('./log')
|
|
4
|
+
|
|
5
|
+
function row(name, desc) {
|
|
6
|
+
console.log(` ${name}`)
|
|
7
|
+
console.log(Log.color('gray', ` ${desc}`))
|
|
8
|
+
console.log()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function help() {
|
|
12
|
+
Log.banner()
|
|
13
|
+
|
|
14
|
+
console.log(Log.color('yellow', 'Usage'))
|
|
15
|
+
Log.line()
|
|
16
|
+
console.log(' npm run cli -- <command> [options]')
|
|
17
|
+
console.log()
|
|
18
|
+
|
|
19
|
+
console.log(Log.color('yellow', 'Project'))
|
|
20
|
+
Log.line()
|
|
21
|
+
row('setup [--upgrade]', 'Create .env, install deps, generate keys (--upgrade runs npm-check-updates)')
|
|
22
|
+
row('help', 'Show this help')
|
|
23
|
+
|
|
24
|
+
console.log(Log.color('yellow', 'Keys'))
|
|
25
|
+
Log.line()
|
|
26
|
+
row('key:generate', 'Generate APP_KEY')
|
|
27
|
+
row('key:jwt [--refresh]', 'Generate JWT_SECRET (or JWT_REFRESH_SECRET with --refresh)')
|
|
28
|
+
|
|
29
|
+
console.log(Log.color('yellow', 'Scaffolding') + Log.color('gray', ' (all support --module=<name>)'))
|
|
30
|
+
Log.line()
|
|
31
|
+
row('make:model <Name> [--table=t] [--no-migration]', 'Model + matching create-table migration')
|
|
32
|
+
row('make:migration <name>', 'Blank up/down migration')
|
|
33
|
+
row('make:seed <Name>', 'Seeder')
|
|
34
|
+
row('make:controller <Name> [--resource] [--api]', 'HTTP controller (plain, or RESTful resource)')
|
|
35
|
+
row('make:middleware <Name>', 'Route middleware (handle() class)')
|
|
36
|
+
row('make:validator <Name>', 'Zod validator schemas')
|
|
37
|
+
row('make:route <name>', 'Route group file')
|
|
38
|
+
row('make:job <Name>', 'Cron job module')
|
|
39
|
+
row('make:queue <Name>', 'Queue worker module')
|
|
40
|
+
row('make:socket <Name>', 'Socket handler module')
|
|
41
|
+
row('make:hook [name]', 'Lifecycle hooks ({ before, after, shutdown })')
|
|
42
|
+
row('make:resource <Name> [--api]', 'Vertical slice: controller + model + migration + validator + route')
|
|
43
|
+
row('make:module <name>', 'Full mini-app module (http/, routes, jobs, queue, socket, hooks, models, migrations, seeders)')
|
|
44
|
+
|
|
45
|
+
console.log(Log.color('yellow', 'Database'))
|
|
46
|
+
Log.line()
|
|
47
|
+
row('db:migrate (alias: migrate)', 'Run all pending migrations')
|
|
48
|
+
row('db:rollback [--step=N] (alias: rollback)', 'Roll back the last batch (or last N batches)')
|
|
49
|
+
row('db:reset [--seed] (alias: reset)', 'Drop all tables, then migrate (optionally seed)')
|
|
50
|
+
row('db:refresh [--seed]', 'Roll back everything via down(), then migrate (optionally seed)')
|
|
51
|
+
row('db:fresh [--seed]', 'Alias of db:reset')
|
|
52
|
+
row('db:status', 'List applied and pending migrations')
|
|
53
|
+
row('db:wipe', 'Drop all tables (no migrate)')
|
|
54
|
+
row('db:seed [--all] [--class=Name] (alias: seed)', 'Run the default/named seeder, or all with --all')
|
|
55
|
+
row('db:seed-all (alias: seed-all)', 'Run every seeder')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { help }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const crypto = require('crypto')
|
|
6
|
+
const { execSync } = require('child_process')
|
|
7
|
+
|
|
8
|
+
const Log = require('./log')
|
|
9
|
+
const { parseArgs } = require('./args')
|
|
10
|
+
|
|
11
|
+
function generateKey(bytes = 32) {
|
|
12
|
+
return crypto.randomBytes(bytes).toString('hex')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getEnvPath() {
|
|
16
|
+
return path.join(process.cwd(), '.env')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function updateEnv(key, value) {
|
|
20
|
+
const envPath = getEnvPath()
|
|
21
|
+
if (!fs.existsSync(envPath)) {
|
|
22
|
+
Log.error('.env file not found')
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let content = fs.readFileSync(envPath, 'utf8')
|
|
27
|
+
const regex = new RegExp(`^${key}=.*$`, 'm')
|
|
28
|
+
content = regex.test(content) ? content.replace(regex, `${key}=${value}`) : `${content}\n${key}=${value}`
|
|
29
|
+
fs.writeFileSync(envPath, content)
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printKey(label, key) {
|
|
34
|
+
Log.success(`${label} generated`)
|
|
35
|
+
console.log()
|
|
36
|
+
console.log(Log.color('cyan', key))
|
|
37
|
+
console.log()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generateAppKey() {
|
|
41
|
+
printKey('APP_KEY', updateEnv('APP_KEY', generateKey(32)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function generateJwtSecret() {
|
|
45
|
+
printKey('JWT_SECRET', updateEnv('JWT_SECRET', generateKey(64)))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function generateJwtRefreshSecret() {
|
|
49
|
+
printKey('JWT_REFRESH_SECRET', updateEnv('JWT_REFRESH_SECRET', generateKey(64)))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function keyJwt(argv) {
|
|
53
|
+
const { options } = parseArgs(argv)
|
|
54
|
+
if (options.refresh) generateJwtRefreshSecret()
|
|
55
|
+
else generateJwtSecret()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// setup: copy .env, install deps, generate keys. Dependency upgrades are opt-in via
|
|
59
|
+
// --upgrade: blindly running `npm-check-updates -u` on every setup is what pulls in
|
|
60
|
+
// fresh, sometimes-vulnerable versions, so the default installs the pinned versions.
|
|
61
|
+
function setup(argv) {
|
|
62
|
+
Log.banner()
|
|
63
|
+
const { options } = parseArgs(argv)
|
|
64
|
+
|
|
65
|
+
const examplePath = path.join(process.cwd(), '.env.example')
|
|
66
|
+
const envPath = path.join(process.cwd(), '.env')
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(examplePath)) {
|
|
69
|
+
Log.error('.env.example not found')
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!fs.existsSync(envPath)) {
|
|
74
|
+
fs.copyFileSync(examplePath, envPath)
|
|
75
|
+
Log.success('.env created')
|
|
76
|
+
} else {
|
|
77
|
+
Log.warn('.env already exists')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if (options.upgrade) {
|
|
82
|
+
Log.info('Upgrading package.json dependencies (--upgrade)...')
|
|
83
|
+
execSync('npx npm-check-updates -u', { stdio: 'inherit' })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Log.info('Installing dependencies...')
|
|
87
|
+
execSync('npm install', { stdio: 'inherit' })
|
|
88
|
+
|
|
89
|
+
generateAppKey()
|
|
90
|
+
generateJwtSecret()
|
|
91
|
+
generateJwtRefreshSecret()
|
|
92
|
+
|
|
93
|
+
Log.success('Setup completed')
|
|
94
|
+
} catch (error) {
|
|
95
|
+
Log.error(error.message)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { setup, generateAppKey, keyJwt }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Shared CLI logger. Display name is driven by APP_NAME (no hardcoded brand); falls
|
|
4
|
+
// back to 'rebe' when .env is not present yet (e.g. before `setup`).
|
|
5
|
+
const APP_NAME = (process.env.APP_NAME || 'rebe').toUpperCase()
|
|
6
|
+
|
|
7
|
+
class Log {
|
|
8
|
+
static colors = {
|
|
9
|
+
reset: '\x1b[0m',
|
|
10
|
+
red: '\x1b[31m',
|
|
11
|
+
green: '\x1b[32m',
|
|
12
|
+
yellow: '\x1b[33m',
|
|
13
|
+
blue: '\x1b[34m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
white: '\x1b[37m',
|
|
16
|
+
gray: '\x1b[90m',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static color(color, message) {
|
|
20
|
+
return `${this.colors[color] || ''}${message}${this.colors.reset}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static info(message) {
|
|
24
|
+
console.log(this.color('blue', `[INFO] ${message}`))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static success(message) {
|
|
28
|
+
console.log(this.color('green', `[ OK ] ${message}`))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static warn(message) {
|
|
32
|
+
console.log(this.color('yellow', `[WARN] ${message}`))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static error(message) {
|
|
36
|
+
console.log(this.color('red', `[FAIL] ${message}`))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static line() {
|
|
40
|
+
console.log(this.color('gray', '────────────────────────────────────────'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static banner() {
|
|
44
|
+
const width = 38
|
|
45
|
+
const title = `${APP_NAME} CLI`
|
|
46
|
+
const padTotal = Math.max(0, width - title.length)
|
|
47
|
+
const left = Math.floor(padTotal / 2)
|
|
48
|
+
const centered = ' '.repeat(left) + title + ' '.repeat(padTotal - left)
|
|
49
|
+
|
|
50
|
+
console.log()
|
|
51
|
+
console.log(this.color('cyan', `╔${'═'.repeat(width)}╗`))
|
|
52
|
+
console.log(this.color('cyan', `║${centered}║`))
|
|
53
|
+
console.log(this.color('cyan', `╚${'═'.repeat(width)}╝`))
|
|
54
|
+
console.log()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = Log
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
const Log = require('./log')
|
|
7
|
+
const { parseArgs } = require('./args')
|
|
8
|
+
const { toKebab, toSnake, tableName, timestamp } = require('./names')
|
|
9
|
+
const tpl = require('./templates')
|
|
10
|
+
|
|
11
|
+
const ROOT = process.cwd()
|
|
12
|
+
|
|
13
|
+
// Directory layout per artifact kind, for the flat app and for a module subfolder.
|
|
14
|
+
const FLAT_DIRS = {
|
|
15
|
+
models: ['database', 'models'],
|
|
16
|
+
migrations: ['database', 'migrations'],
|
|
17
|
+
seeders: ['database', 'seeders'],
|
|
18
|
+
controllers: ['app', 'http', 'controllers'],
|
|
19
|
+
middlewares: ['app', 'http', 'middlewares'],
|
|
20
|
+
validators: ['app', 'http', 'validators'],
|
|
21
|
+
routes: ['app', 'routes'],
|
|
22
|
+
jobs: ['app', 'jobs'],
|
|
23
|
+
queue: ['app', 'queue'],
|
|
24
|
+
socket: ['app', 'socket'],
|
|
25
|
+
hooks: ['app', 'hooks'],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MODULE_DIRS = {
|
|
29
|
+
models: ['models'],
|
|
30
|
+
migrations: ['migrations'],
|
|
31
|
+
seeders: ['seeders'],
|
|
32
|
+
controllers: ['http', 'controllers'],
|
|
33
|
+
middlewares: ['http', 'middlewares'],
|
|
34
|
+
validators: ['http', 'validators'],
|
|
35
|
+
routes: ['routes'],
|
|
36
|
+
jobs: ['jobs'],
|
|
37
|
+
queue: ['queue'],
|
|
38
|
+
socket: ['socket'],
|
|
39
|
+
hooks: ['hooks'],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolve a base directory for generated files: the flat layout by default, or a
|
|
43
|
+
// module subfolder when --module=<name> is given.
|
|
44
|
+
function baseDir(kind, moduleName) {
|
|
45
|
+
if (moduleName) return path.join(ROOT, 'modules', toKebab(moduleName), ...(MODULE_DIRS[kind] || [kind]))
|
|
46
|
+
return path.join(ROOT, ...(FLAT_DIRS[kind] || [kind]))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Write a file, creating parent dirs. Refuses to overwrite unless --force.
|
|
50
|
+
function writeFile(target, content, force) {
|
|
51
|
+
if (fs.existsSync(target) && !force) {
|
|
52
|
+
Log.error(`Already exists: ${path.relative(ROOT, target)} (use --force to overwrite)`)
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
56
|
+
fs.writeFileSync(target, content)
|
|
57
|
+
Log.success(`Created ${path.relative(ROOT, target)}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function gitkeep(dir) {
|
|
61
|
+
const keep = path.join(dir, '.gitkeep')
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
63
|
+
if (!fs.existsSync(keep)) fs.writeFileSync(keep, '')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function requireName(positionals, what) {
|
|
67
|
+
const name = positionals[0]
|
|
68
|
+
if (!name) {
|
|
69
|
+
Log.error(`Missing name. Usage: npm run cli -- ${what} <Name>`)
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
return name
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Print a "now wire it up" hint for layers loaded via a single register file.
|
|
76
|
+
function wireHint(options, flatMsg) {
|
|
77
|
+
if (options.module) {
|
|
78
|
+
Log.info(`Wire it into modules/${toKebab(options.module)}/${toKebab(options.module)}.module.js`)
|
|
79
|
+
} else {
|
|
80
|
+
Log.info(flatMsg)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Route files in a module's routes/ dir are auto-loaded; flat route files must be
|
|
85
|
+
// required from app/routes/register.route.js.
|
|
86
|
+
function routeWireHint(options, slug) {
|
|
87
|
+
if (options.module) {
|
|
88
|
+
Log.info(`Auto-loaded from modules/${toKebab(options.module)}/routes/`)
|
|
89
|
+
} else {
|
|
90
|
+
Log.info(`Add require('@routes/${slug}.route') to app/routes/register.route.js`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Data layer ───────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
// make:model <Name> [--module=m] [--table=t] [--no-migration]
|
|
97
|
+
function makeModel(argv) {
|
|
98
|
+
const { positionals, options } = parseArgs(argv)
|
|
99
|
+
const name = requireName(positionals, 'make:model')
|
|
100
|
+
const table = options.table || tableName(name)
|
|
101
|
+
|
|
102
|
+
writeFile(path.join(baseDir('models', options.module), `${toKebab(name)}.model.js`), tpl.modelTemplate(name, table), options.force)
|
|
103
|
+
|
|
104
|
+
if (options.migration !== false && !options['no-migration']) {
|
|
105
|
+
writeFile(path.join(baseDir('migrations', options.module), `${timestamp()}_create_${table}.js`), tpl.createMigrationTemplate(table), options.force)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// make:migration <name> [--module=m]
|
|
110
|
+
function makeMigration(argv) {
|
|
111
|
+
const { positionals, options } = parseArgs(argv)
|
|
112
|
+
const name = requireName(positionals, 'make:migration')
|
|
113
|
+
writeFile(path.join(baseDir('migrations', options.module), `${timestamp()}_${toSnake(name)}.js`), tpl.blankMigrationTemplate(toSnake(name)), options.force)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// make:seed <Name> [--module=m]
|
|
117
|
+
function makeSeed(argv) {
|
|
118
|
+
const { positionals, options } = parseArgs(argv)
|
|
119
|
+
const name = requireName(positionals, 'make:seed')
|
|
120
|
+
writeFile(path.join(baseDir('seeders', options.module), `${toKebab(name)}.js`), tpl.seederTemplate(name), options.force)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── HTTP layer ───────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
// make:controller <Name> [--resource] [--api] [--module=m]
|
|
126
|
+
function makeController(argv) {
|
|
127
|
+
const { positionals, options } = parseArgs(argv)
|
|
128
|
+
const name = requireName(positionals, 'make:controller')
|
|
129
|
+
const resource = Boolean(options.resource || options.api)
|
|
130
|
+
writeFile(path.join(baseDir('controllers', options.module), `${toKebab(name)}.controller.js`), tpl.controllerTemplate(name, { resource, api: Boolean(options.api) }), options.force)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// make:middleware <Name> [--module=m]
|
|
134
|
+
function makeMiddleware(argv) {
|
|
135
|
+
const { positionals, options } = parseArgs(argv)
|
|
136
|
+
const name = requireName(positionals, 'make:middleware')
|
|
137
|
+
writeFile(path.join(baseDir('middlewares', options.module), `${toKebab(name)}.middleware.js`), tpl.middlewareTemplate(name), options.force)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// make:validator <Name> [--module=m]
|
|
141
|
+
function makeValidator(argv) {
|
|
142
|
+
const { positionals, options } = parseArgs(argv)
|
|
143
|
+
const name = requireName(positionals, 'make:validator')
|
|
144
|
+
writeFile(path.join(baseDir('validators', options.module), `${toKebab(name)}.validator.js`), tpl.validatorTemplate(name), options.force)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// make:route <name> [--module=m]
|
|
148
|
+
function makeRoute(argv) {
|
|
149
|
+
const { positionals, options } = parseArgs(argv)
|
|
150
|
+
const name = requireName(positionals, 'make:route')
|
|
151
|
+
writeFile(path.join(baseDir('routes', options.module), `${toKebab(name)}.route.js`), tpl.routeTemplate(name), options.force)
|
|
152
|
+
routeWireHint(options, toKebab(name))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Background / lifecycle ─────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
// make:job <Name> [--module=m]
|
|
158
|
+
function makeJob(argv) {
|
|
159
|
+
const { positionals, options } = parseArgs(argv)
|
|
160
|
+
const name = requireName(positionals, 'make:job')
|
|
161
|
+
writeFile(path.join(baseDir('jobs', options.module), `${toKebab(name)}.job.js`), tpl.jobTemplate(name), options.force)
|
|
162
|
+
wireHint(options, `Require + invoke it from app/jobs/register.job.js: require('./${toKebab(name)}.job')(Cron)`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// make:queue <Name> [--module=m]
|
|
166
|
+
function makeQueue(argv) {
|
|
167
|
+
const { positionals, options } = parseArgs(argv)
|
|
168
|
+
const name = requireName(positionals, 'make:queue')
|
|
169
|
+
writeFile(path.join(baseDir('queue', options.module), `${toKebab(name)}.queue.js`), tpl.queueTemplate(name), options.force)
|
|
170
|
+
wireHint(options, `Require + invoke it from app/queue/register.queue.js: require('./${toKebab(name)}.queue')(Queue)`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// make:socket <Name> [--module=m]
|
|
174
|
+
function makeSocket(argv) {
|
|
175
|
+
const { positionals, options } = parseArgs(argv)
|
|
176
|
+
const name = requireName(positionals, 'make:socket')
|
|
177
|
+
writeFile(path.join(baseDir('socket', options.module), `${toKebab(name)}.socket.js`), tpl.socketTemplate(name), options.force)
|
|
178
|
+
wireHint(options, `Require + invoke it from app/socket/register.socket.js: require('./${toKebab(name)}.socket')(io, Socket)`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// make:hook [name] [--module=m]
|
|
182
|
+
function makeHook(argv) {
|
|
183
|
+
const { positionals, options } = parseArgs(argv)
|
|
184
|
+
const fileName = positionals[0] ? `${toKebab(positionals[0])}.hook.js` : 'register.hook.js'
|
|
185
|
+
writeFile(path.join(baseDir('hooks', options.module), fileName), tpl.hookTemplate(), options.force)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Bundles ────────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
// make:resource <Name> [--api] [--module=m] — a wired vertical slice.
|
|
191
|
+
function makeResource(argv) {
|
|
192
|
+
const { positionals, options } = parseArgs(argv)
|
|
193
|
+
const name = requireName(positionals, 'make:resource')
|
|
194
|
+
const api = Boolean(options.api)
|
|
195
|
+
const table = options.table || tableName(name)
|
|
196
|
+
const force = options.force
|
|
197
|
+
|
|
198
|
+
writeFile(path.join(baseDir('controllers', options.module), `${toKebab(name)}.controller.js`), tpl.controllerTemplate(name, { resource: true, api }), force)
|
|
199
|
+
writeFile(path.join(baseDir('models', options.module), `${toKebab(name)}.model.js`), tpl.modelTemplate(name, table), force)
|
|
200
|
+
writeFile(path.join(baseDir('migrations', options.module), `${timestamp()}_create_${table}.js`), tpl.createMigrationTemplate(table), force)
|
|
201
|
+
writeFile(path.join(baseDir('validators', options.module), `${toKebab(name)}.validator.js`), tpl.validatorTemplate(name), force)
|
|
202
|
+
|
|
203
|
+
// The route requires the controller: alias path for flat, relative for a module.
|
|
204
|
+
const controllerRequire = options.module ? `../http/controllers/${toKebab(name)}.controller` : `@http/controllers/${toKebab(name)}.controller`
|
|
205
|
+
writeFile(path.join(baseDir('routes', options.module), `${toKebab(name)}.route.js`), tpl.resourceRouteTemplate(name, { api, controllerRequire }), force)
|
|
206
|
+
|
|
207
|
+
routeWireHint(options, toKebab(name))
|
|
208
|
+
Log.info('Then run: npm run cli -- db:migrate')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Module skeleton ──────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
// make:module <name> — a self-contained mini-app mirroring the full app layout.
|
|
214
|
+
function makeModule(argv) {
|
|
215
|
+
const { positionals, options } = parseArgs(argv)
|
|
216
|
+
const name = requireName(positionals, 'make:module')
|
|
217
|
+
const slug = toKebab(name)
|
|
218
|
+
const dir = path.join(ROOT, 'modules', slug)
|
|
219
|
+
|
|
220
|
+
writeFile(path.join(dir, `${slug}.module.js`), tpl.moduleEntryTemplate(slug), options.force)
|
|
221
|
+
writeFile(path.join(dir, 'routes', 'index.js'), tpl.moduleRouteTemplate(slug), options.force)
|
|
222
|
+
writeFile(path.join(dir, 'http', 'middlewares', 'register.middleware.js'), tpl.appMiddlewareTemplate(slug), options.force)
|
|
223
|
+
writeFile(path.join(dir, 'jobs', 'register.job.js'), tpl.jobTemplate(slug), options.force)
|
|
224
|
+
writeFile(path.join(dir, 'queue', 'register.queue.js'), tpl.queueTemplate(slug), options.force)
|
|
225
|
+
writeFile(path.join(dir, 'socket', 'register.socket.js'), tpl.socketTemplate(slug), options.force)
|
|
226
|
+
writeFile(path.join(dir, 'hooks', 'register.hook.js'), tpl.hookTemplate(), options.force)
|
|
227
|
+
|
|
228
|
+
for (const sub of [['http', 'controllers'], ['http', 'validators'], ['models'], ['migrations'], ['seeders']]) {
|
|
229
|
+
gitkeep(path.join(dir, ...sub))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Log.success(`Module "${slug}" scaffolded at ${path.relative(ROOT, dir)} (full mini-app layout)`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
makeModel,
|
|
237
|
+
makeMigration,
|
|
238
|
+
makeSeed,
|
|
239
|
+
makeController,
|
|
240
|
+
makeMiddleware,
|
|
241
|
+
makeValidator,
|
|
242
|
+
makeRoute,
|
|
243
|
+
makeJob,
|
|
244
|
+
makeQueue,
|
|
245
|
+
makeSocket,
|
|
246
|
+
makeHook,
|
|
247
|
+
makeResource,
|
|
248
|
+
makeModule,
|
|
249
|
+
}
|