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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/template/.env.example +4 -0
  3. package/template/README.md +63 -8
  4. package/template/SECURITY.md +39 -0
  5. package/template/app/routes/api.route.js +1 -1
  6. package/template/app/routes/register.route.js +1 -1
  7. package/template/app/routes/web.route.js +1 -1
  8. package/template/app/socket/register.socket.js +0 -2
  9. package/template/config/express.config.js +8 -0
  10. package/template/core/common/string.js +1 -1
  11. package/template/core/cron.core.js +1 -0
  12. package/template/core/database.core.js +30 -17
  13. package/template/core/error.core.js +8 -4
  14. package/template/core/express.core.js +16 -4
  15. package/template/core/hooks.core.js +10 -7
  16. package/template/core/migrator.core.js +201 -0
  17. package/template/core/modules.core.js +167 -0
  18. package/template/core/queue.core.js +1 -0
  19. package/template/core/routing.core.d.ts +273 -0
  20. package/template/core/routing.core.js +666 -0
  21. package/template/core/seeder.core.js +105 -0
  22. package/template/core/socket.core.js +1 -0
  23. package/template/database/migrations/.gitkeep +0 -0
  24. package/template/database/seeders/.gitkeep +0 -0
  25. package/template/docs/Database.md +14 -8
  26. package/template/docs/Express.md +5 -2
  27. package/template/docs/Make.md +46 -0
  28. package/template/docs/Migration.md +56 -0
  29. package/template/docs/Modules.md +96 -0
  30. package/template/docs/README.md +5 -0
  31. package/template/docs/Routing.md +116 -0
  32. package/template/docs/Seeder.md +54 -0
  33. package/template/eslint.config.js +52 -0
  34. package/template/package-lock.json +1068 -70
  35. package/template/package.json +15 -8
  36. package/template/scripts/cli/args.js +39 -0
  37. package/template/scripts/cli/bootstrap.js +16 -0
  38. package/template/scripts/cli/db.js +79 -0
  39. package/template/scripts/cli/help.js +58 -0
  40. package/template/scripts/cli/keys.js +100 -0
  41. package/template/scripts/cli/log.js +58 -0
  42. package/template/scripts/cli/make.js +249 -0
  43. package/template/scripts/cli/names.js +51 -0
  44. package/template/scripts/cli/templates.js +358 -0
  45. package/template/scripts/cli.js +75 -234
  46. package/template/tests/http.test.js +99 -0
@@ -0,0 +1,167 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ const logger = require('@core/logger.core')
7
+
8
+ const MODULES_DIR = path.resolve(process.cwd(), 'modules')
9
+
10
+ // Optional modular layer. A "module" groups one feature's pieces (models, routes,
11
+ // migrations, seeders, jobs, queue, socket) under modules/<name>/ behind a single
12
+ // entry file. Discovery is additive: the flat layout (database/models, app/routes…)
13
+ // keeps working untouched, and a project with no modules/ directory pays nothing.
14
+ //
15
+ // Entry file — modules/<name>/<name>.module.js (also accepts index.js / module.js):
16
+ //
17
+ // module.exports = {
18
+ // name: 'blog', // optional, defaults to the folder name
19
+ // models: './models', // dir of (sequelize, DataTypes) factories
20
+ // migrations: './migrations', // dir of up/down migration files
21
+ // seeders: './seeders', // dir of seeder files
22
+ // routes: './routes', // file or dir required for its routing side-effect
23
+ // middlewares: (app) => {}, // global Express middleware
24
+ // jobs: (Cron) => {}, // cron definitions
25
+ // queue: (Queue) => {}, // queue workers
26
+ // socket: (io, Socket) => {}, // socket handlers
27
+ // }
28
+ //
29
+ // The core layer never imports module code statically; modules are app-land and are
30
+ // discovered here at runtime, keeping the arrow app -> core one-directional.
31
+ class Modules {
32
+ static _modules = null
33
+
34
+ // Entry-file candidates, tried in order, for a folder named <name>.
35
+ static _entryCandidates(name) {
36
+ return [`${name}.module.js`, 'module.js', 'index.js']
37
+ }
38
+
39
+ // Discover and cache every module once. Safe to call repeatedly.
40
+ static discover() {
41
+ if (Modules._modules) return Modules._modules
42
+ Modules._modules = []
43
+
44
+ if (!fs.existsSync(MODULES_DIR)) return Modules._modules
45
+
46
+ for (const entry of fs.readdirSync(MODULES_DIR, { withFileTypes: true })) {
47
+ if (!entry.isDirectory() || entry.name.startsWith('_')) continue
48
+
49
+ const dir = path.join(MODULES_DIR, entry.name)
50
+ const entryFile = Modules._entryCandidates(entry.name)
51
+ .map((f) => path.join(dir, f))
52
+ .find((f) => fs.existsSync(f))
53
+
54
+ if (!entryFile) {
55
+ logger.warn(`Module "${entry.name}" has no entry file (expected one of: ${Modules._entryCandidates(entry.name).join(', ')}); skipping`)
56
+ continue
57
+ }
58
+
59
+ try {
60
+ const raw = require(entryFile)
61
+ const def = raw && raw.default !== undefined ? raw.default : raw
62
+ if (!def || typeof def !== 'object') {
63
+ logger.warn(`Module "${entry.name}" entry must export an object; skipping`)
64
+ continue
65
+ }
66
+ Modules._modules.push({ name: def.name || entry.name, dir, def })
67
+ } catch (err) {
68
+ logger.error(`Failed to load module "${entry.name}"`, err)
69
+ }
70
+ }
71
+
72
+ const names = Modules._modules.map((m) => m.name)
73
+ if (names.length) logger.info(`Loaded ${names.length} module(s): ${names.join(', ')}`)
74
+ return Modules._modules
75
+ }
76
+
77
+ // Absolute, existing directories declared by modules for a given kind
78
+ // ('models' | 'migrations' | 'seeders'). Relative paths resolve against the
79
+ // module folder; missing directories are silently dropped.
80
+ static dirs(kind) {
81
+ const out = []
82
+ for (const mod of Modules.discover()) {
83
+ const rel = mod.def[kind]
84
+ if (!rel) continue
85
+ const abs = path.resolve(mod.dir, rel)
86
+ if (fs.existsSync(abs)) out.push(abs)
87
+ else logger.warn(`Module "${mod.name}" ${kind} directory not found: ${abs}`)
88
+ }
89
+ return out
90
+ }
91
+
92
+ // Require each module's routes for their routing side-effect. When `routes` points
93
+ // to a directory, every *.js file in it is loaded (sorted; "_"-prefixed skipped) so
94
+ // dropping in a new <name>.route.js is enough — no central wiring needed. When it
95
+ // points to a single file, just that file is required.
96
+ static requireRoutes() {
97
+ for (const mod of Modules.discover()) {
98
+ const rel = mod.def.routes
99
+ if (!rel) continue
100
+ const abs = path.resolve(mod.dir, rel)
101
+ if (!fs.existsSync(abs)) {
102
+ logger.warn(`Module "${mod.name}" routes not found: ${abs}`)
103
+ continue
104
+ }
105
+
106
+ const files = fs.statSync(abs).isDirectory()
107
+ ? fs
108
+ .readdirSync(abs)
109
+ .filter((f) => f.endsWith('.js') && !f.startsWith('_'))
110
+ .sort()
111
+ .map((f) => path.join(abs, f))
112
+ : [abs]
113
+
114
+ for (const file of files) {
115
+ try {
116
+ require(file)
117
+ } catch (err) {
118
+ logger.error(`Failed to load routes for module "${mod.name}" (${path.basename(file)})`, err)
119
+ throw err
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ // Invoke each module's callable for a given kind ('middlewares' | 'jobs' |
126
+ // 'queue' | 'socket'), forwarding args (the core facade). Failures are isolated
127
+ // per module so one bad module cannot abort boot of the rest.
128
+ static async invoke(kind, args = []) {
129
+ for (const mod of Modules.discover()) {
130
+ const fn = mod.def[kind]
131
+ if (typeof fn !== 'function') continue
132
+ try {
133
+ await fn(...args)
134
+ } catch (err) {
135
+ logger.error(`Module "${mod.name}" ${kind} register failed`, err)
136
+ throw err
137
+ }
138
+ }
139
+ }
140
+
141
+ // Run each module's lifecycle hook for a stage ('before' | 'after' | 'shutdown'),
142
+ // from its `hooks: { before, after, shutdown }` export. Errors propagate so a
143
+ // failing before-hook aborts boot, matching the flat hook contract.
144
+ static async runHooks(stage, ctx = {}) {
145
+ for (const mod of Modules.discover()) {
146
+ const fn = mod.def.hooks && mod.def.hooks[stage]
147
+ if (typeof fn !== 'function') continue
148
+ try {
149
+ await fn(ctx)
150
+ } catch (err) {
151
+ logger.error(`Module "${mod.name}" hook "${stage}" failed`, err)
152
+ throw err
153
+ }
154
+ }
155
+ }
156
+
157
+ static list() {
158
+ return Modules.discover().map((m) => m.name)
159
+ }
160
+
161
+ // Reset the discovery cache (used by tooling/tests that mutate modules/).
162
+ static reset() {
163
+ Modules._modules = null
164
+ }
165
+ }
166
+
167
+ module.exports = Modules
@@ -99,6 +99,7 @@ class Queue {
99
99
  }
100
100
 
101
101
  await Register.invoke(REGISTER_FILE, [Queue], { label: 'app/queue/register.queue.js' })
102
+ await require('@core/modules.core').invoke('queue', [Queue])
102
103
 
103
104
  if (config.queue.persist && config.db.enabled) {
104
105
  try {
@@ -0,0 +1,273 @@
1
+ import { Application, Router, Request, Response, NextFunction } from 'express'
2
+
3
+ /**
4
+ * HTTP context passed to ALL handlers and handle() middleware — always { req, res, next, error }
5
+ */
6
+ export interface HttpContext {
7
+ req: Request
8
+ res: Response
9
+ next: NextFunction
10
+ /** Populated in errorHandler, null in normal route/middleware handlers */
11
+ error: Error | null
12
+ }
13
+
14
+ /**
15
+ * Route handler function — receives the full HttpContext.
16
+ * The return value is ignored, so handlers may freely `return res.json(...)`
17
+ * or be `async`. (Typed as `any` to allow the common `({ res }) => res.json(...)` form.)
18
+ */
19
+ export type RouteHandler = (ctx: HttpContext) => any
20
+
21
+ /** Controller method reference tuple */
22
+ export type ControllerHandler = [any, string]
23
+
24
+ /** Handler can be an inline function or a controller binding */
25
+ export type Handler = RouteHandler | ControllerHandler
26
+
27
+ /**
28
+ * Middleware — plain Express function, object/class with handle(), OR a registered
29
+ * alias/group name (string, see Routes.registerMiddleware / Routes.middlewareGroup).
30
+ *
31
+ * PLAIN FUNCTION — allowed in scoped Routes.middleware([fn], () => { ... }) and per-route:
32
+ * (req, res, next) => void
33
+ *
34
+ * HANDLE CLASS — required in chaining Routes.middleware([Mw]).get(...):
35
+ * class Mw { static handle({ req, res, next, error }: HttpContext): void }
36
+ * class Mw { handle({ req, res, next, error }: HttpContext): void }
37
+ * const obj = { handle({ req, res, next, error }: HttpContext): void }
38
+ *
39
+ * STRING — a registered middleware alias or group: 'auth', 'web', ...
40
+ */
41
+ export type MiddlewareFn = (req: Request, res: Response, next: NextFunction) => void | Promise<void>
42
+
43
+ export interface MiddlewareClass {
44
+ handle(ctx: HttpContext): void | Promise<void>
45
+ }
46
+
47
+ export type Middleware = MiddlewareFn | MiddlewareClass | (new () => MiddlewareClass) | string
48
+
49
+ /** HTTP methods supported by the router */
50
+ export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head'
51
+
52
+ /** Parameter constraint — a regex source string or a RegExp. */
53
+ export type RouteConstraint = string | RegExp
54
+
55
+ /** Route information object returned by allRoutes() */
56
+ export interface RouteInfo {
57
+ methods: HttpMethod[]
58
+ path: string
59
+ /** Route name (Routes...name()), or null if unnamed */
60
+ name: string | null
61
+ middlewareCount: number
62
+ handlerType: 'function' | 'controller'
63
+ }
64
+
65
+ /** The seven RESTful resource actions. */
66
+ export type ResourceAction = 'index' | 'create' | 'store' | 'show' | 'edit' | 'update' | 'destroy'
67
+
68
+ /** Options for Routes.resource() / Routes.apiResource(). */
69
+ export interface ResourceOptions {
70
+ /** Only register these actions. */
71
+ only?: ResourceAction[]
72
+ /** Register all actions except these. */
73
+ except?: ResourceAction[]
74
+ /** Path parameter name for show/update/destroy/edit (default: 'id'). */
75
+ parameter?: string
76
+ /** When true, omit the HTML-form `create` and `edit` routes. */
77
+ api?: boolean
78
+ /** Middleware applied to every generated resource route. */
79
+ middleware?: Middleware | Middleware[]
80
+ }
81
+
82
+ /**
83
+ * Chainable handle returned by route-definition methods, enabling Laravel-style
84
+ * fluent configuration:
85
+ * Routes.get('/users/:id', h).name('users.show').whereNumber('id')
86
+ */
87
+ export interface RouteRegistration {
88
+ /** Assign a route name for URL generation via Routes.url() / Routes.route(). */
89
+ name(name: string): RouteRegistration
90
+ /** Constrain a path parameter to a pattern (regex string or RegExp). */
91
+ where(param: string, pattern: RouteConstraint): RouteRegistration
92
+ /** Constrain several path parameters at once. */
93
+ where(constraints: Record<string, RouteConstraint>): RouteRegistration
94
+ whereNumber(param: string): RouteRegistration
95
+ whereAlpha(param: string): RouteRegistration
96
+ whereAlphaNumeric(param: string): RouteRegistration
97
+ whereUuid(param: string): RouteRegistration
98
+ }
99
+
100
+ /**
101
+ * Per-method middleware map for Routes.controller().
102
+ * Keys are controller method names; values are a single middleware or array.
103
+ * Only handle()-based middleware classes/objects are recommended here.
104
+ */
105
+ export type MethodMiddlewareMap = Record<string, Middleware | Middleware[]>
106
+
107
+ /**
108
+ * Proxy returned by Routes.middleware() when called WITHOUT a callback (chaining mode).
109
+ *
110
+ * STRICT: only handle()-based middleware classes/objects (or aliases that resolve to one)
111
+ * are allowed. Each method call is terminal — globalMiddlewares are restored after.
112
+ *
113
+ * Usage:
114
+ * Routes.middleware([Mw]).get(path, handler).name('...')
115
+ * Routes.middleware([Mw]).group(prefix, callback)
116
+ */
117
+ export interface MiddlewareChain {
118
+ group(prefix: string, callback: () => void, middlewares?: Middleware[]): void
119
+ add(methods: HttpMethod | HttpMethod[], path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
120
+ get(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
121
+ post(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
122
+ put(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
123
+ delete(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
124
+ patch(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
125
+ options(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
126
+ head(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
127
+ }
128
+
129
+ export default class Routes {
130
+ static routes: Array<{
131
+ methods: HttpMethod[]
132
+ path: string
133
+ handler: Handler
134
+ middlewares: Middleware[]
135
+ name?: string | null
136
+ constraints?: Record<string, RouteConstraint>
137
+ _resolved?: boolean
138
+ }>
139
+ static prefix: string
140
+ static groupMiddlewares: Middleware[]
141
+ static globalMiddlewares: Middleware[]
142
+ static middlewareAliases: Record<string, Middleware>
143
+ static middlewareGroups: Record<string, Middleware[]>
144
+ static _errorHandler: RouteHandler | null
145
+ static _maintenanceMode: boolean
146
+ static _maintenanceHandler: RouteHandler | null
147
+ static _fallbackHandler: RouteHandler | null
148
+
149
+ static normalizePath(path: string): string
150
+
151
+ /**
152
+ * Convert method/function name to kebab-case URL segment.
153
+ * samplePath → sample-path
154
+ * SamplePath → sample-path
155
+ * sample_path → sample-path
156
+ * Samplepath → samplepath
157
+ */
158
+ static nameToPath(name: string): string
159
+
160
+ static resolveHandler(Controller: any, method: string): RouteHandler
161
+
162
+ /**
163
+ * Normalize middleware — accepts plain function OR handle() class/object.
164
+ * Used for scoped middleware and per-route middleware.
165
+ */
166
+ static normalizeMiddleware(mw: Middleware): MiddlewareFn
167
+
168
+ /**
169
+ * Strict normalization — ONLY handle() classes/objects allowed.
170
+ * Used internally by chaining syntax. Throws if a plain function is passed.
171
+ */
172
+ static normalizeMiddlewareStrict(mw: Middleware): MiddlewareFn
173
+
174
+ /** Register named middleware aliases (Laravel-style). */
175
+ static registerMiddleware(name: string, mw: Middleware): typeof Routes
176
+ static registerMiddleware(map: Record<string, Middleware>): typeof Routes
177
+
178
+ /** Register a named middleware group — a string that expands to several middlewares. */
179
+ static middlewareGroup(name: string, list: Middleware[]): typeof Routes
180
+
181
+ /** Expand a middleware list, resolving string aliases/groups to actual middleware. */
182
+ static expandMiddleware(list: Middleware[]): Middleware[]
183
+
184
+ static add(methods: HttpMethod | HttpMethod[], path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
185
+ static get(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
186
+ static post(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
187
+ static put(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
188
+ static delete(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
189
+ static patch(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
190
+ static options(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
191
+ static head(path: string, handler: Handler, middlewares?: Middleware[]): RouteRegistration
192
+ static group(prefix: string, callback: () => void, middlewares?: Middleware[]): void
193
+
194
+ /**
195
+ * Apply global middlewares.
196
+ *
197
+ * SCOPED (with callback) — accepts plain functions OR handle() classes:
198
+ * Routes.middleware([Mw, fn], () => { Routes.get(...) })
199
+ * Returns `typeof Routes` for fluency.
200
+ *
201
+ * CHAINING (without callback) — STRICT: only handle() classes/objects:
202
+ * Routes.middleware([Mw]).get(path, handler)
203
+ * Routes.middleware([Mw]).group(prefix, callback)
204
+ * Returns MiddlewareChain — each call is terminal.
205
+ */
206
+ static middleware(middlewares: Middleware[], callback: () => void): typeof Routes
207
+ static middleware(middlewares: Middleware[]): MiddlewareChain
208
+
209
+ static errorHandler(handler: Handler): void
210
+ static maintenance(enabled: boolean, handler?: Handler): void
211
+
212
+ /**
213
+ * Auto-register all methods of a controller as routes.
214
+ *
215
+ * Methods whose name starts with `_` (e.g. `_helper`) are treated as private
216
+ * helpers and are NEVER registered as routes.
217
+ *
218
+ * @param basePath Base URL for the controller
219
+ * @param Controller Class (static or instance) or plain object
220
+ * @param methodMiddlewares Optional per-method middleware map:
221
+ * {
222
+ * 'index': AuthMiddleware,
223
+ * 'myProfile': [AuthMiddleware, LogMiddleware],
224
+ * }
225
+ */
226
+ static controller(basePath: string, Controller: any, methodMiddlewares?: MethodMiddlewareMap): void
227
+
228
+ /**
229
+ * Register the seven RESTful resource routes for a controller (Laravel-style):
230
+ * GET index · GET create · POST store · GET show · GET edit · PUT|PATCH update · DELETE destroy
231
+ * Each is named `<name>.<action>`. Only actions the controller implements are registered.
232
+ */
233
+ static resource(name: string, Controller: any, options?: ResourceOptions): typeof Routes
234
+
235
+ /** API resource — resource() without the HTML-form create/edit routes. */
236
+ static apiResource(name: string, Controller: any, options?: ResourceOptions): typeof Routes
237
+
238
+ /**
239
+ * Build a resolver binding a controller method to the class, a single shared
240
+ * instance, or the object — returning null when the method does not exist.
241
+ */
242
+ static makeMethodResolver(Controller: any): (method: string) => RouteHandler | null
243
+
244
+ /** Register a redirect route (default status 302). */
245
+ static redirect(from: string, to: string, status?: number): RouteRegistration
246
+
247
+ /** Register a route that renders a view via the Express view engine (res.render). */
248
+ static view(path: string, view: string, data?: Record<string, any>): RouteRegistration
249
+
250
+ /** Register a fallback handler invoked when no other route matches. */
251
+ static fallback(handler: Handler): typeof Routes
252
+
253
+ /**
254
+ * Generate a URL for a named route, substituting `:param` segments and appending
255
+ * any extra keys as a query string.
256
+ * Routes.url('users.show', { id: 5 }) // → /users/5
257
+ */
258
+ static url(name: string, params?: Record<string, any>): string
259
+
260
+ /** Alias of Routes.url() — matches Laravel's `route()` helper. */
261
+ static route(name: string, params?: Record<string, any>): string
262
+
263
+ static allRoutes(): RouteInfo[]
264
+
265
+ /**
266
+ * Apply routes to Express.
267
+ * Routes.apply(app) — direct mount
268
+ * Routes.apply(app, router) — auto app.use(router)
269
+ */
270
+ static apply(appOrRouter: Application | Router, router?: Router): Promise<void>
271
+ }
272
+
273
+ export { Routes }