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,666 @@
1
+ 'use strict'
2
+
3
+ // Laravel-style routing for Express, internalized into the core (previously the
4
+ // external package @refkinscallv/express-routing — authored by the same maintainer).
5
+ // The API is unchanged: define routes against this static class, then express.core
6
+ // calls Routes.apply(app, router) during boot.
7
+ //
8
+ // const Routes = require('@core/routing.core')
9
+ // Routes.group('api', () => {
10
+ // Routes.get('status', ({ res }) => res.json({ status: true }))
11
+ // })
12
+ //
13
+ // Every handler and handle()-based middleware receives the context { req, res, next,
14
+ // error }. The core never imports application code; route files are loaded through the
15
+ // register bridge, so the app -> core dependency arrow is preserved.
16
+ class Routes {
17
+ static routes = []
18
+ static prefix = ''
19
+ static groupMiddlewares = []
20
+ static globalMiddlewares = []
21
+ static _errorHandler = null
22
+ static _maintenanceMode = false
23
+ static _maintenanceHandler = null
24
+ static _fallbackHandler = null
25
+ static middlewareAliases = {}
26
+ static middlewareGroups = {}
27
+
28
+ // Register named middleware aliases (Laravel-style):
29
+ // Routes.registerMiddleware('auth', AuthMiddleware)
30
+ // Routes.registerMiddleware({ auth: AuthMiddleware, guest: GuestMiddleware })
31
+ static registerMiddleware(name, mw) {
32
+ if (name && typeof name === 'object') {
33
+ for (const key of Object.keys(name)) this.middlewareAliases[key] = name[key]
34
+ return this
35
+ }
36
+ this.middlewareAliases[name] = mw
37
+ return this
38
+ }
39
+
40
+ // Register a named middleware group — a string that expands to several middlewares.
41
+ static middlewareGroup(name, list) {
42
+ this.middlewareGroups[name] = Array.isArray(list) ? list : [list]
43
+ return this
44
+ }
45
+
46
+ // Expand a middleware list, resolving string entries to their registered alias
47
+ // (single) or group (many). Non-string entries pass through untouched.
48
+ static expandMiddleware(list) {
49
+ const out = []
50
+ for (const mw of list) {
51
+ if (typeof mw === 'string') {
52
+ if (Object.prototype.hasOwnProperty.call(this.middlewareGroups, mw)) {
53
+ out.push(...this.expandMiddleware(this.middlewareGroups[mw]))
54
+ } else if (Object.prototype.hasOwnProperty.call(this.middlewareAliases, mw)) {
55
+ out.push(this.middlewareAliases[mw])
56
+ } else {
57
+ throw new Error(`Unknown middleware "${mw}" — register it with Routes.registerMiddleware() or Routes.middlewareGroup()`)
58
+ }
59
+ } else {
60
+ out.push(mw)
61
+ }
62
+ }
63
+ return out
64
+ }
65
+
66
+ // Normalize a path: drop duplicate slashes and ensure a single leading slash.
67
+ static normalizePath(path) {
68
+ return '/' + path.split('/').filter(Boolean).join('/')
69
+ }
70
+
71
+ // Convert a method/function name to a kebab-case URL segment.
72
+ // SamplePath -> sample-path · samplePath -> sample-path · sample_path -> sample-path
73
+ static nameToPath(name) {
74
+ let result = name.replace(/_/g, '-')
75
+ result = result.replace(/([a-z])([A-Z][a-z])/g, '$1-$2')
76
+ return result.toLowerCase()
77
+ }
78
+
79
+ // Resolve a controller (static class, instance class, or plain object) + method
80
+ // name into a bound callable.
81
+ static resolveHandler(Controller, method) {
82
+ if (typeof Controller === 'function' && typeof Controller[method] === 'function') {
83
+ return Controller[method].bind(Controller)
84
+ }
85
+ if (typeof Controller === 'function') {
86
+ const instance = new Controller()
87
+ if (typeof instance[method] === 'function') {
88
+ return instance[method].bind(instance)
89
+ }
90
+ throw new Error(`Method "${method}" not found in controller instance "${Controller.name}"`)
91
+ }
92
+ if (typeof Controller === 'object' && Controller !== null && typeof Controller[method] === 'function') {
93
+ return Controller[method].bind(Controller)
94
+ }
95
+ throw new Error(`Cannot resolve handler for method "${method}"`)
96
+ }
97
+
98
+ // Resolve a middleware's handle({ req, res, next, error }) into a bound callable,
99
+ // or return null when the middleware is a plain Express function. Accepts a class
100
+ // with a static handle(), a class with an instance handle() (instantiated once), or
101
+ // a plain object with handle().
102
+ static resolveHandle(mw) {
103
+ if (typeof mw === 'function') {
104
+ if (typeof mw.handle === 'function') {
105
+ return mw.handle.bind(mw)
106
+ }
107
+ if (mw.prototype && typeof mw.prototype.handle === 'function') {
108
+ const instance = new mw()
109
+ return instance.handle.bind(instance)
110
+ }
111
+ return null
112
+ }
113
+ if (typeof mw === 'object' && mw !== null && typeof mw.handle === 'function') {
114
+ return mw.handle.bind(mw)
115
+ }
116
+ return null
117
+ }
118
+
119
+ // Wrap a resolved handle() function into Express-compatible middleware.
120
+ static wrapHandle(handleFn) {
121
+ return (req, res, next) => {
122
+ try {
123
+ const result = handleFn({ req, res, next, error: null })
124
+ Promise.resolve(result).catch(next)
125
+ } catch (err) {
126
+ next(err)
127
+ }
128
+ }
129
+ }
130
+
131
+ // Normalize a middleware entry to an Express-compatible function. A plain Express
132
+ // function passes through; a class/object with handle() is auto-wrapped.
133
+ static normalizeMiddleware(mw) {
134
+ const handleFn = this.resolveHandle(mw)
135
+ if (handleFn) {
136
+ return this.wrapHandle(handleFn)
137
+ }
138
+ if (typeof mw === 'function') {
139
+ return mw
140
+ }
141
+ throw new Error('Invalid middleware: must be a function or an object/class with a "handle({ req, res, next, error })" method')
142
+ }
143
+
144
+ // Strict normalization used in the chaining context — ONLY handle()-based
145
+ // middleware is allowed (plain Express functions are rejected).
146
+ static normalizeMiddlewareStrict(mw) {
147
+ const handleFn = this.resolveHandle(mw)
148
+ if (!handleFn) {
149
+ throw new Error('Chained middleware must implement a "handle({ req, res, next, error })" method. ' + 'Plain functions are not allowed in chaining syntax. Use the scoped callback form instead: ' + 'Routes.middleware([fn], () => { ... })')
150
+ }
151
+ return this.wrapHandle(handleFn)
152
+ }
153
+
154
+ // Add a route with the given HTTP method(s), path, handler, and middlewares.
155
+ static add(methods, path, handler, middlewares = []) {
156
+ const methodArray = Array.isArray(methods) ? methods : [methods]
157
+ const fullPath = this.normalizePath(`${this.prefix}/${path}`)
158
+ const route = {
159
+ methods: methodArray,
160
+ path: fullPath,
161
+ handler,
162
+ middlewares: [...this.globalMiddlewares, ...this.groupMiddlewares, ...middlewares],
163
+ name: null,
164
+ constraints: {},
165
+ }
166
+ this.routes.push(route)
167
+ return this.registration(route)
168
+ }
169
+
170
+ // Build a chainable registration handle for fluent configuration:
171
+ // Routes.get(...).name('users.show').whereNumber('id')
172
+ static registration(route) {
173
+ const handle = {
174
+ name(routeName) {
175
+ route.name = routeName
176
+ return handle
177
+ },
178
+ where(param, pattern) {
179
+ if (param && typeof param === 'object') {
180
+ Object.assign(route.constraints, param)
181
+ } else {
182
+ route.constraints[param] = pattern
183
+ }
184
+ return handle
185
+ },
186
+ whereNumber(param) {
187
+ route.constraints[param] = '[0-9]+'
188
+ return handle
189
+ },
190
+ whereAlpha(param) {
191
+ route.constraints[param] = '[A-Za-z]+'
192
+ return handle
193
+ },
194
+ whereAlphaNumeric(param) {
195
+ route.constraints[param] = '[A-Za-z0-9]+'
196
+ return handle
197
+ },
198
+ whereUuid(param) {
199
+ route.constraints[param] = '[0-9a-fA-F-]{36}'
200
+ return handle
201
+ },
202
+ }
203
+ return handle
204
+ }
205
+
206
+ static get(path, handler, middlewares = []) {
207
+ return this.add('get', path, handler, middlewares)
208
+ }
209
+ static post(path, handler, middlewares = []) {
210
+ return this.add('post', path, handler, middlewares)
211
+ }
212
+ static put(path, handler, middlewares = []) {
213
+ return this.add('put', path, handler, middlewares)
214
+ }
215
+ static delete(path, handler, middlewares = []) {
216
+ return this.add('delete', path, handler, middlewares)
217
+ }
218
+ static patch(path, handler, middlewares = []) {
219
+ return this.add('patch', path, handler, middlewares)
220
+ }
221
+ static options(path, handler, middlewares = []) {
222
+ return this.add('options', path, handler, middlewares)
223
+ }
224
+ static head(path, handler, middlewares = []) {
225
+ return this.add('head', path, handler, middlewares)
226
+ }
227
+
228
+ // Group routes under a common URL prefix with optional middleware.
229
+ static group(prefix, callback, middlewares = []) {
230
+ const previousPrefix = this.prefix
231
+ const previousMiddlewares = this.groupMiddlewares
232
+
233
+ const fullPrefix = [previousPrefix, prefix].filter(Boolean).join('/')
234
+ this.prefix = this.normalizePath(fullPrefix)
235
+ this.groupMiddlewares = [...previousMiddlewares, ...this.expandMiddleware(middlewares).map((mw) => this.normalizeMiddleware(mw))]
236
+
237
+ try {
238
+ callback()
239
+ } finally {
240
+ // Always restore — even if the callback throws — so a definition-time error
241
+ // does not corrupt the prefix/middlewares for subsequent routes.
242
+ this.prefix = previousPrefix
243
+ this.groupMiddlewares = previousMiddlewares
244
+ }
245
+ }
246
+
247
+ // Apply global middlewares.
248
+ //
249
+ // SCOPED (with callback) — accepts plain functions OR handle() classes:
250
+ // Routes.middleware([Mw, fn], () => { Routes.get(...) })
251
+ //
252
+ // CHAINING (without callback) — STRICT: only handle() classes/objects:
253
+ // Routes.middleware([Mw1, Mw2]).get(path, handler)
254
+ // Routes.middleware([Mw1, Mw2]).group(prefix, callback)
255
+ // Each chained call is terminal — globalMiddlewares are restored after.
256
+ static middleware(middlewares, callback) {
257
+ const prevMiddlewares = this.globalMiddlewares
258
+
259
+ if (typeof callback === 'function') {
260
+ const normalized = this.expandMiddleware(middlewares).map((mw) => this.normalizeMiddleware(mw))
261
+ this.globalMiddlewares = [...prevMiddlewares, ...normalized]
262
+ try {
263
+ callback()
264
+ } finally {
265
+ this.globalMiddlewares = prevMiddlewares
266
+ }
267
+ return this
268
+ }
269
+
270
+ // Chaining mode: STRICT. Validate eagerly so a bad middleware throws immediately,
271
+ // but DO NOT mutate globalMiddlewares here — that mutation is scoped to each
272
+ // terminal call below (otherwise a non-terminal Routes.middleware([Mw]) would leak
273
+ // the middleware into every later route).
274
+ const normalized = this.expandMiddleware(middlewares).map((mw) => this.normalizeMiddlewareStrict(mw))
275
+
276
+ const self = this
277
+
278
+ const withChained = (action) => {
279
+ const prev = self.globalMiddlewares
280
+ self.globalMiddlewares = [...prev, ...normalized]
281
+ try {
282
+ return action()
283
+ } finally {
284
+ self.globalMiddlewares = prev
285
+ }
286
+ }
287
+
288
+ return {
289
+ group(prefix, groupCallback, groupMiddlewares = []) {
290
+ return withChained(() => self.group(prefix, groupCallback, groupMiddlewares))
291
+ },
292
+ add(methods, path, handler, mws = []) {
293
+ return withChained(() => self.add(methods, path, handler, mws))
294
+ },
295
+ get(path, handler, mws = []) {
296
+ return withChained(() => self.add('get', path, handler, mws))
297
+ },
298
+ post(path, handler, mws = []) {
299
+ return withChained(() => self.add('post', path, handler, mws))
300
+ },
301
+ put(path, handler, mws = []) {
302
+ return withChained(() => self.add('put', path, handler, mws))
303
+ },
304
+ delete(path, handler, mws = []) {
305
+ return withChained(() => self.add('delete', path, handler, mws))
306
+ },
307
+ patch(path, handler, mws = []) {
308
+ return withChained(() => self.add('patch', path, handler, mws))
309
+ },
310
+ options(path, handler, mws = []) {
311
+ return withChained(() => self.add('options', path, handler, mws))
312
+ },
313
+ head(path, handler, mws = []) {
314
+ return withChained(() => self.add('head', path, handler, mws))
315
+ },
316
+ }
317
+ }
318
+
319
+ // Register a global error handler. Receives { req, res, next, error }.
320
+ static errorHandler(handler) {
321
+ if (Array.isArray(handler) && handler.length === 2) {
322
+ const [Controller, method] = handler
323
+ this._errorHandler = this.resolveHandler(Controller, method)
324
+ } else if (typeof handler === 'function') {
325
+ this._errorHandler = handler
326
+ } else {
327
+ throw new Error('Routes.errorHandler: invalid handler — must be a function or [Controller, "method"]')
328
+ }
329
+ }
330
+
331
+ // Enable/disable maintenance mode. When enabled, all requests get a 503 before any
332
+ // route runs. Handler receives { req, res, next, error }.
333
+ static maintenance(enabled, handler) {
334
+ this._maintenanceMode = Boolean(enabled)
335
+
336
+ if (handler) {
337
+ if (Array.isArray(handler) && handler.length === 2) {
338
+ const [Controller, method] = handler
339
+ this._maintenanceHandler = this.resolveHandler(Controller, method)
340
+ } else if (typeof handler === 'function') {
341
+ this._maintenanceHandler = handler
342
+ } else {
343
+ throw new Error('Routes.maintenance: invalid handler — must be a function or [Controller, "method"]')
344
+ }
345
+ }
346
+ }
347
+
348
+ // Auto-register every public method of a controller as a route.
349
+ // index -> base path · other methods -> kebab-case segment
350
+ // HTTP prefix detection: post_create -> POST /<base>/create
351
+ // Methods starting with "_" are private helpers and are never exposed.
352
+ static controller(basePath, Controller, methodMiddlewares = {}) {
353
+ const HTTP_PREFIXES = ['post', 'put', 'delete', 'patch', 'options', 'head']
354
+
355
+ let methods
356
+
357
+ // For class controllers, instance methods share a SINGLE instance so the
358
+ // constructor runs once and `this` state is shared across all routes.
359
+ let sharedInstance = null
360
+ const getInstance = () => {
361
+ if (!sharedInstance) sharedInstance = new Controller()
362
+ return sharedInstance
363
+ }
364
+
365
+ if (typeof Controller === 'function') {
366
+ const staticMethods = Object.getOwnPropertyNames(Controller).filter((n) => typeof Controller[n] === 'function' && !['length', 'name', 'prototype'].includes(n))
367
+ const proto = Controller.prototype
368
+ const instanceMethods = proto ? Object.getOwnPropertyNames(proto).filter((n) => n !== 'constructor' && typeof proto[n] === 'function') : []
369
+ methods = [...new Set([...staticMethods, ...instanceMethods])]
370
+ } else if (typeof Controller === 'object' && Controller !== null) {
371
+ methods = Object.getOwnPropertyNames(Controller).filter((n) => typeof Controller[n] === 'function')
372
+ } else {
373
+ throw new Error('Routes.controller: invalid Controller — must be a class, instance, or plain object')
374
+ }
375
+
376
+ methods = methods.filter((name) => !name.startsWith('_'))
377
+
378
+ for (const methodName of methods) {
379
+ let httpMethod = 'get'
380
+ let pathSegment = methodName
381
+
382
+ const matchedPrefix = HTTP_PREFIXES.find((p) => methodName.toLowerCase().startsWith(p + '_') || methodName.toLowerCase() === p)
383
+ if (matchedPrefix) {
384
+ httpMethod = matchedPrefix
385
+ pathSegment = methodName.slice(matchedPrefix.length).replace(/^_/, '')
386
+ }
387
+
388
+ let routePath
389
+ if (pathSegment === '' || pathSegment.toLowerCase() === 'index') {
390
+ routePath = this.normalizePath(basePath)
391
+ } else {
392
+ routePath = this.normalizePath(`${basePath}/${this.nameToPath(pathSegment)}`)
393
+ }
394
+
395
+ const specificMws = methodMiddlewares[methodName] ? (Array.isArray(methodMiddlewares[methodName]) ? methodMiddlewares[methodName] : [methodMiddlewares[methodName]]) : []
396
+
397
+ let handler
398
+ if (typeof Controller === 'function') {
399
+ if (typeof Controller[methodName] === 'function') {
400
+ handler = Controller[methodName].bind(Controller)
401
+ } else {
402
+ const instance = getInstance()
403
+ handler = instance[methodName].bind(instance)
404
+ }
405
+ } else {
406
+ handler = Controller[methodName].bind(Controller)
407
+ }
408
+
409
+ this.routes.push({
410
+ methods: [httpMethod],
411
+ path: routePath,
412
+ handler,
413
+ middlewares: [...this.globalMiddlewares, ...this.groupMiddlewares, ...specificMws],
414
+ _resolved: true,
415
+ name: null,
416
+ constraints: {},
417
+ })
418
+ }
419
+ }
420
+
421
+ // Register the seven RESTful resource routes (Laravel-style). Only actions the
422
+ // controller actually implements are registered.
423
+ static resource(name, Controller, options = {}) {
424
+ const param = options.parameter || 'id'
425
+ const base = this.normalizePath(`${this.prefix}/${name}`)
426
+ const nameBase = name.split('/').filter(Boolean).join('.')
427
+ const resolve = this.makeMethodResolver(Controller)
428
+
429
+ const specMws = options.middleware ? (Array.isArray(options.middleware) ? options.middleware : [options.middleware]) : []
430
+
431
+ const definitions = [
432
+ ['index', ['get'], base],
433
+ ['create', ['get'], `${base}/create`],
434
+ ['store', ['post'], base],
435
+ ['show', ['get'], `${base}/:${param}`],
436
+ ['edit', ['get'], `${base}/:${param}/edit`],
437
+ ['update', ['put', 'patch'], `${base}/:${param}`],
438
+ ['destroy', ['delete'], `${base}/:${param}`],
439
+ ]
440
+
441
+ let allowed = definitions.map((d) => d[0])
442
+ if (options.api) allowed = allowed.filter((a) => a !== 'create' && a !== 'edit')
443
+ if (Array.isArray(options.only)) allowed = allowed.filter((a) => options.only.includes(a))
444
+ if (Array.isArray(options.except)) allowed = allowed.filter((a) => !options.except.includes(a))
445
+
446
+ for (const [action, methods, routePath] of definitions) {
447
+ if (!allowed.includes(action)) continue
448
+ const handler = resolve(action)
449
+ if (!handler) continue // controller doesn't implement this action — skip
450
+ this.routes.push({
451
+ methods,
452
+ path: this.normalizePath(routePath),
453
+ handler,
454
+ middlewares: [...this.globalMiddlewares, ...this.groupMiddlewares, ...specMws],
455
+ _resolved: true,
456
+ name: `${nameBase}.${action}`,
457
+ constraints: {},
458
+ })
459
+ }
460
+ return this
461
+ }
462
+
463
+ // API resource — resource() without the HTML-form create/edit routes.
464
+ static apiResource(name, Controller, options = {}) {
465
+ return this.resource(name, Controller, { ...options, api: true })
466
+ }
467
+
468
+ // Build a resolver binding a controller method to the class (static), a single
469
+ // shared instance (instance method), or the object — null when it does not exist.
470
+ static makeMethodResolver(Controller) {
471
+ let instance = null
472
+ return (methodName) => {
473
+ if (typeof Controller === 'function') {
474
+ if (typeof Controller[methodName] === 'function') {
475
+ return Controller[methodName].bind(Controller)
476
+ }
477
+ const proto = Controller.prototype
478
+ if (proto && typeof proto[methodName] === 'function') {
479
+ if (!instance) instance = new Controller()
480
+ return instance[methodName].bind(instance)
481
+ }
482
+ return null
483
+ }
484
+ if (typeof Controller === 'object' && Controller !== null && typeof Controller[methodName] === 'function') {
485
+ return Controller[methodName].bind(Controller)
486
+ }
487
+ return null
488
+ }
489
+ }
490
+
491
+ // Register a redirect route (default status 302).
492
+ static redirect(from, to, status = 302) {
493
+ return this.add('get', from, ({ res }) => res.redirect(status, to))
494
+ }
495
+
496
+ // Register a route that renders a view via the Express view engine (res.render).
497
+ static view(path, view, data = {}) {
498
+ return this.add('get', path, ({ res }) => res.render(view, data))
499
+ }
500
+
501
+ // Register a fallback handler invoked when no other route matches (Laravel-style).
502
+ static fallback(handler) {
503
+ if (Array.isArray(handler) && handler.length === 2) {
504
+ const [Controller, method] = handler
505
+ this._fallbackHandler = this.resolveHandler(Controller, method)
506
+ } else if (typeof handler === 'function') {
507
+ this._fallbackHandler = handler
508
+ } else {
509
+ throw new Error('Routes.fallback: invalid handler — must be a function or [Controller, "method"]')
510
+ }
511
+ return this
512
+ }
513
+
514
+ // Generate a URL for a named route, substituting :param segments and appending any
515
+ // extra keys as a query string.
516
+ static url(name, params = {}) {
517
+ const route = this.routes.find((r) => r.name === name)
518
+ if (!route) {
519
+ throw new Error(`Route name "${name}" not found`)
520
+ }
521
+ const used = new Set()
522
+ let path = route.path.replace(/:([A-Za-z0-9_]+)(\?)?/g, (match, key, optional) => {
523
+ used.add(key)
524
+ if (params[key] === undefined || params[key] === null) {
525
+ if (optional) return ''
526
+ throw new Error(`Missing parameter "${key}" for route "${name}"`)
527
+ }
528
+ return encodeURIComponent(params[key])
529
+ })
530
+ path = this.normalizePath(path)
531
+ const query = Object.keys(params)
532
+ .filter((key) => !used.has(key) && params[key] !== undefined && params[key] !== null)
533
+ .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
534
+ return query.length ? `${path}?${query.join('&')}` : path
535
+ }
536
+
537
+ // Alias of Routes.url() — matches Laravel's route() helper.
538
+ static route(name, params = {}) {
539
+ return this.url(name, params)
540
+ }
541
+
542
+ // Info for all registered routes.
543
+ static allRoutes() {
544
+ return this.routes.map((route) => ({
545
+ methods: route.methods,
546
+ path: route.path,
547
+ name: route.name || null,
548
+ middlewareCount: route.middlewares.length,
549
+ handlerType: typeof route.handler === 'function' && !route._resolved ? 'function' : 'controller',
550
+ }))
551
+ }
552
+
553
+ // Apply all registered routes to Express.
554
+ // Routes.apply(app) — mount directly on app
555
+ // Routes.apply(app, router) — mount on router, then app.use(router)
556
+ static async apply(appOrRouter, router) {
557
+ const target = router || appOrRouter
558
+
559
+ // Maintenance mode — intercept all requests before routes.
560
+ if (this._maintenanceMode) {
561
+ const maintenanceFn =
562
+ this._maintenanceHandler ||
563
+ (({ res }) =>
564
+ res.status(503).json({
565
+ status: false,
566
+ code: 503,
567
+ message: 'Service Unavailable - Maintenance Mode',
568
+ }))
569
+
570
+ target.use((req, res, next) => {
571
+ try {
572
+ const result = maintenanceFn({ req, res, next, error: null })
573
+ Promise.resolve(result).catch(next)
574
+ } catch (err) {
575
+ next(err)
576
+ }
577
+ })
578
+
579
+ if (router) appOrRouter.use(router)
580
+ return
581
+ }
582
+
583
+ const ALLOWED_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']
584
+
585
+ for (const route of this.routes) {
586
+ let handlerFunction
587
+
588
+ if (route._resolved) {
589
+ handlerFunction = route.handler
590
+ } else if (typeof route.handler === 'function') {
591
+ handlerFunction = route.handler
592
+ } else if (Array.isArray(route.handler) && route.handler.length === 2) {
593
+ const [Controller, method] = route.handler
594
+ handlerFunction = this.resolveHandler(Controller, method)
595
+ } else {
596
+ throw new Error(`Invalid handler format for route: ${route.path}`)
597
+ }
598
+
599
+ if (!handlerFunction) continue
600
+
601
+ // Parameter constraints (Routes...where()) — a request whose param does not
602
+ // match is skipped with next('route') so a later route can still match.
603
+ const constraintKeys = Object.keys(route.constraints || {})
604
+ const constraintGuard = constraintKeys.length
605
+ ? (req, res, next) => {
606
+ for (const key of constraintKeys) {
607
+ const pattern = route.constraints[key]
608
+ const re = pattern instanceof RegExp ? pattern : new RegExp(`^(?:${pattern})$`)
609
+ const value = req.params[key] != null ? String(req.params[key]) : ''
610
+ if (!re.test(value)) return next('route')
611
+ }
612
+ next()
613
+ }
614
+ : null
615
+
616
+ for (const method of route.methods) {
617
+ if (!ALLOWED_METHODS.includes(method)) {
618
+ throw new Error(`Invalid HTTP method "${method}" for route: ${route.path}`)
619
+ }
620
+
621
+ const normalizedMiddlewares = this.expandMiddleware(route.middlewares).map((mw) => this.normalizeMiddleware(mw))
622
+ const chain = constraintGuard ? [constraintGuard, ...normalizedMiddlewares] : normalizedMiddlewares
623
+
624
+ target[method](route.path, ...chain, async (req, res, next) => {
625
+ try {
626
+ const result = handlerFunction({ req, res, next, error: null })
627
+ await Promise.resolve(result)
628
+ } catch (error) {
629
+ next(error)
630
+ }
631
+ })
632
+ }
633
+ }
634
+
635
+ // Fallback — runs when no route above matched (Laravel-style).
636
+ if (this._fallbackHandler) {
637
+ const fallbackFn = this._fallbackHandler
638
+ target.use((req, res, next) => {
639
+ try {
640
+ const result = fallbackFn({ req, res, next, error: null })
641
+ Promise.resolve(result).catch(next)
642
+ } catch (err) {
643
+ next(err)
644
+ }
645
+ })
646
+ }
647
+
648
+ // Global error handler.
649
+ if (this._errorHandler) {
650
+ const errFn = this._errorHandler
651
+ target.use((error, req, res, next) => {
652
+ try {
653
+ const result = errFn({ req, res, next, error })
654
+ Promise.resolve(result).catch((err) => next(err))
655
+ } catch (err) {
656
+ next(err)
657
+ }
658
+ })
659
+ }
660
+
661
+ if (router) appOrRouter.use(router)
662
+ }
663
+ }
664
+
665
+ module.exports = Routes
666
+ module.exports.default = Routes