millas 0.2.11 → 0.2.12-beta-1
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 +6 -5
- package/src/auth/Auth.js +13 -8
- package/src/auth/AuthController.js +45 -134
- package/src/auth/AuthMiddleware.js +12 -23
- package/src/auth/AuthUser.js +98 -0
- package/src/auth/RoleMiddleware.js +7 -17
- package/src/cli.js +1 -1
- package/src/commands/migrate.js +46 -31
- package/src/commands/serve.js +238 -38
- package/src/container/AppInitializer.js +158 -0
- package/src/container/Application.js +288 -183
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +23 -280
- package/src/container/MillasConfig.js +163 -0
- package/src/controller/Controller.js +79 -300
- package/src/core/auth.js +9 -0
- package/src/core/db.js +8 -0
- package/src/core/foundation.js +67 -0
- package/src/core/http.js +11 -0
- package/src/core/mail.js +6 -0
- package/src/core/queue.js +7 -0
- package/src/core/validation.js +29 -0
- package/src/errors/ErrorRenderer.js +640 -0
- package/src/facades/Admin.js +49 -0
- package/src/facades/Auth.js +29 -0
- package/src/facades/Cache.js +28 -0
- package/src/facades/Database.js +43 -0
- package/src/facades/Events.js +25 -0
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +51 -0
- package/src/facades/Log.js +32 -0
- package/src/facades/Mail.js +35 -0
- package/src/facades/Queue.js +30 -0
- package/src/facades/Storage.js +25 -0
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/MillasRequest.js +253 -0
- package/src/http/MillasResponse.js +196 -0
- package/src/http/RequestContext.js +176 -0
- package/src/http/ResponseDispatcher.js +51 -0
- package/src/http/UrlGenerator.js +375 -0
- package/src/http/WelcomePage.js +273 -0
- package/src/http/adapters/ExpressAdapter.js +315 -0
- package/src/http/adapters/HttpAdapter.js +168 -0
- package/src/http/adapters/index.js +9 -0
- package/src/http/helpers.js +164 -0
- package/src/http/index.js +13 -0
- package/src/index.js +5 -91
- package/src/logger/formatters/PrettyFormatter.js +15 -5
- package/src/logger/internal.js +76 -0
- package/src/logger/patchConsole.js +145 -0
- package/src/middleware/CorsMiddleware.js +22 -30
- package/src/middleware/LogMiddleware.js +27 -59
- package/src/middleware/Middleware.js +24 -15
- package/src/middleware/MiddlewarePipeline.js +30 -67
- package/src/middleware/MiddlewareRegistry.js +106 -0
- package/src/middleware/ThrottleMiddleware.js +22 -26
- package/src/orm/fields/index.js +124 -56
- package/src/orm/migration/ModelInspector.js +339 -336
- package/src/orm/model/Model.js +96 -6
- package/src/orm/query/QueryBuilder.js +141 -3
- package/src/providers/AuthServiceProvider.js +9 -5
- package/src/providers/CacheStorageServiceProvider.js +3 -1
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +88 -17
- package/src/providers/MailServiceProvider.js +3 -2
- package/src/providers/ProviderRegistry.js +14 -1
- package/src/providers/QueueServiceProvider.js +3 -2
- package/src/providers/ServiceProvider.js +40 -8
- package/src/router/Router.js +121 -222
- package/src/scaffold/maker.js +24 -59
- package/src/scaffold/templates.js +21 -19
- package/src/validation/BaseValidator.js +193 -0
- package/src/validation/Validator.js +680 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const Container
|
|
3
|
+
const Container = require('./Container');
|
|
4
4
|
const ProviderRegistry = require('../providers/ProviderRegistry');
|
|
5
|
-
const {
|
|
6
|
-
const CorsMiddleware
|
|
5
|
+
const {Route, Router, MiddlewareRegistry} = require('../router');
|
|
6
|
+
const CorsMiddleware = require('../middleware/CorsMiddleware');
|
|
7
7
|
const ThrottleMiddleware = require('../middleware/ThrottleMiddleware');
|
|
8
|
-
const LogMiddleware
|
|
9
|
-
const AuthMiddleware
|
|
8
|
+
const LogMiddleware = require('../middleware/LogMiddleware');
|
|
9
|
+
const AuthMiddleware = require('../auth/AuthMiddleware');
|
|
10
|
+
const Facade = require("../facades/Facade");
|
|
11
|
+
const UrlGenerator = require("../http/UrlGenerator");
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Application
|
|
@@ -14,195 +16,298 @@ const AuthMiddleware = require('../auth/AuthMiddleware');
|
|
|
14
16
|
* The central Millas kernel. Owns:
|
|
15
17
|
* - The DI container
|
|
16
18
|
* - The provider registry
|
|
17
|
-
* - The Express
|
|
19
|
+
* - The HttpAdapter (NOT Express directly — any adapter works)
|
|
18
20
|
* - The route instance
|
|
19
21
|
* - The middleware registry
|
|
20
22
|
*
|
|
23
|
+
* The kernel is fully decoupled from Express. It talks to the HTTP layer
|
|
24
|
+
* exclusively through the HttpAdapter interface.
|
|
25
|
+
*
|
|
21
26
|
* Usage in bootstrap/app.js:
|
|
22
27
|
*
|
|
23
|
-
* const { Application }
|
|
24
|
-
* const
|
|
25
|
-
*
|
|
28
|
+
* const { Application } = require('millas/src/container');
|
|
29
|
+
* const { ExpressAdapter } = require('millas/src/http/adapters');
|
|
30
|
+
* const express = require('express');
|
|
31
|
+
*
|
|
32
|
+
* const adapter = new ExpressAdapter(express());
|
|
33
|
+
* const app = new Application(adapter);
|
|
34
|
+
* app.providers([AppServiceProvider]);
|
|
26
35
|
* await app.boot();
|
|
27
|
-
* app.routes(
|
|
28
|
-
* require('../routes/web')(route);
|
|
29
|
-
* require('../routes/api')(route);
|
|
30
|
-
* });
|
|
36
|
+
* app.routes(Route => require('../routes/api')(Route));
|
|
31
37
|
* app.mount();
|
|
32
38
|
* app.listen();
|
|
33
39
|
*/
|
|
34
40
|
class Application {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
41
|
+
/**
|
|
42
|
+
* @param {import('../http/adapters/HttpAdapter')} adapter
|
|
43
|
+
*/
|
|
44
|
+
constructor(adapter) {
|
|
45
|
+
this._adapter = adapter;
|
|
46
|
+
this._container = new Container();
|
|
47
|
+
this._providers = new ProviderRegistry(this._container, adapter.nativeApp || adapter);
|
|
48
|
+
this._mwRegistry = new MiddlewareRegistry();
|
|
49
|
+
this._route = new Route();
|
|
50
|
+
this._booted = false;
|
|
51
|
+
|
|
52
|
+
this._registerCoreBindings();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Configuration ──────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
providers(providers = []) {
|
|
58
|
+
this._providers.addMany(providers);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register a middleware alias.
|
|
64
|
+
*
|
|
65
|
+
* app.middleware('auth', AuthMiddleware)
|
|
66
|
+
* app.middleware('throttle', new ThrottleMiddleware({ max: 60 }))
|
|
67
|
+
*/
|
|
68
|
+
middleware(alias, handler) {
|
|
69
|
+
this._mwRegistry.register(alias, handler);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Define all routes via a callback.
|
|
75
|
+
*
|
|
76
|
+
* app.routes(Route => {
|
|
77
|
+
* require('../routes/api')(Route);
|
|
78
|
+
* });
|
|
79
|
+
*/
|
|
80
|
+
routes(callback) {
|
|
81
|
+
callback(this._route);
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Run the full provider lifecycle:
|
|
89
|
+
* Phase 0 — beforeBoot (sync, global setup)
|
|
90
|
+
* Phase 1 — register (sync, container bindings)
|
|
91
|
+
* Phase 2 — boot (async, all bindings available)
|
|
92
|
+
*/
|
|
93
|
+
async boot() {
|
|
94
|
+
if (this._booted) return this;
|
|
95
|
+
|
|
96
|
+
this._emitSync('platform.booting', {providers: this._providers.list()});
|
|
97
|
+
|
|
98
|
+
await this._providers.boot();
|
|
99
|
+
this._booted = true;
|
|
100
|
+
|
|
101
|
+
this._emitSync('platform.booted', {providers: this._providers.list()});
|
|
102
|
+
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Mount all registered routes onto the adapter.
|
|
108
|
+
* Does NOT add fallbacks — call mountFallbacks() separately after
|
|
109
|
+
* any extra middleware (e.g. Admin panel) is mounted.
|
|
110
|
+
*/
|
|
111
|
+
mountRoutes() {
|
|
112
|
+
this._router = new Router(
|
|
113
|
+
this._adapter,
|
|
114
|
+
this._route.getRegistry(),
|
|
115
|
+
this._mwRegistry,
|
|
116
|
+
this._container
|
|
117
|
+
);
|
|
118
|
+
this._router.mountRoutes();
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Mount 404 + error handler. Must be called LAST.
|
|
124
|
+
*/
|
|
125
|
+
mountFallbacks() {
|
|
126
|
+
if (!this._router) {
|
|
127
|
+
this._router = new Router(
|
|
128
|
+
this._adapter,
|
|
129
|
+
this._route.getRegistry(),
|
|
130
|
+
this._mwRegistry,
|
|
131
|
+
this._container
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
this._router.mountFallbacks();
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Mount routes + fallbacks in one call.
|
|
140
|
+
*/
|
|
141
|
+
mount() {
|
|
142
|
+
const router = new Router(
|
|
143
|
+
this._adapter,
|
|
144
|
+
this._route.getRegistry(),
|
|
145
|
+
this._mwRegistry,
|
|
146
|
+
this._container
|
|
147
|
+
);
|
|
148
|
+
router.mount();
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Start the HTTP server via the adapter.
|
|
154
|
+
* Emits: platform.listening
|
|
155
|
+
*/
|
|
156
|
+
async listen(port, host, callback) {
|
|
157
|
+
const _port = port
|
|
158
|
+
|| parseInt(process.env.MILLAS_INTERNAL_PORT, 10)
|
|
159
|
+
|| parseInt(process.env.APP_PORT, 10)
|
|
160
|
+
|| 3000;
|
|
161
|
+
const _host = host || process.env.MILLAS_HOST || 'localhost';
|
|
162
|
+
|
|
163
|
+
await this._adapter.listen(_port, _host);
|
|
164
|
+
|
|
165
|
+
if (process.env.APP_ENV === "development" && process.env.MILLAS_START_UP && !process.env.MILLERS_NODE_ENV) {
|
|
166
|
+
this._printStartupLog(_host, _port);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this._emitSync('platform.listening', {port: _port, host: _host});
|
|
170
|
+
|
|
171
|
+
if (typeof callback === 'function') callback(_port, _host);
|
|
172
|
+
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_printStartupLog(host, port) {
|
|
177
|
+
const chalk = _tryChalk();
|
|
143
178
|
const routeCount = this._route.list().length;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
179
|
+
const url = `http://${host}:${port}`;
|
|
180
|
+
|
|
181
|
+
// console.error(chalk.dim('›') + ' ' +
|
|
182
|
+
// chalk.dim('Listening on ') + chalk.bold.white(url)
|
|
183
|
+
// );
|
|
184
|
+
console.log(chalk.dim('›') + ' ' +
|
|
185
|
+
chalk.white(routeCount + ' route' + (routeCount !== 1 ? 's' : '') + ' registered')
|
|
186
|
+
);
|
|
187
|
+
console.error(chalk.dim('›') + ' ' +
|
|
188
|
+
chalk.dim('Press ') + chalk.bold('Ctrl+C') + chalk.dim(' to stop')
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Graceful shutdown.
|
|
194
|
+
*/
|
|
195
|
+
async shutdown(code = 0) {
|
|
196
|
+
this._emitSync('platform.shutting_down', {});
|
|
197
|
+
await this._adapter.close();
|
|
198
|
+
process.exit(code);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Platform event bus ─────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
on(event, fn) {
|
|
204
|
+
if (!this._platformListeners) this._platformListeners = new Map();
|
|
205
|
+
if (!this._platformListeners.has(event)) this._platformListeners.set(event, []);
|
|
206
|
+
this._platformListeners.get(event).push(fn);
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
_emitSync(event, data) {
|
|
211
|
+
if (!this._platformListeners) return;
|
|
212
|
+
const listeners = this._platformListeners.get(event) || [];
|
|
213
|
+
for (const fn of listeners) {
|
|
214
|
+
try {
|
|
215
|
+
fn(data);
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Container proxy ────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
bind(abstract, concrete) {
|
|
224
|
+
this._container.bind(abstract, concrete);
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
singleton(abstract, c) {
|
|
229
|
+
this._container.singleton(abstract, c);
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
instance(abstract, value) {
|
|
234
|
+
this._container.instance(abstract, value);
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
make(abstract, overrides) {
|
|
239
|
+
return this._container.make(abstract, overrides);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Accessors ──────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
get container() {
|
|
245
|
+
return this._container;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
get route() {
|
|
249
|
+
return this._route;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
get adapter() {
|
|
253
|
+
return this._adapter;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
get mwRegistry() {
|
|
257
|
+
return this._mwRegistry;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* express — backward-compatible escape hatch.
|
|
262
|
+
* Returns the native app from the adapter if available.
|
|
263
|
+
* Prefer adapter.nativeApp for new code.
|
|
264
|
+
*/
|
|
265
|
+
get express() {
|
|
266
|
+
return this._adapter.nativeApp || this._adapter;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Internal ───────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
_registerCoreBindings() {
|
|
272
|
+
this._container.instance('app', this);
|
|
273
|
+
this._container.instance('container', this._container);
|
|
274
|
+
|
|
275
|
+
const Facade = require('../facades/Facade');
|
|
276
|
+
Facade.setContainer(this._container);
|
|
277
|
+
// Http client — always available, no provider needed
|
|
278
|
+
const {HttpClient} = require('../http/HttpClient');
|
|
279
|
+
this._container.instance('HttpClient', HttpClient);
|
|
280
|
+
this._container.alias('http', "HttpClient");
|
|
281
|
+
|
|
282
|
+
const UrlGenerator = require('../http/UrlGenerator');
|
|
283
|
+
const urlGenerator = new UrlGenerator({
|
|
284
|
+
baseUrl: process.env.APP_URL || '',
|
|
285
|
+
appKey: process.env.APP_KEY || '',
|
|
286
|
+
routeRegistry: this._route?.getRegistry?.() || null,
|
|
287
|
+
});
|
|
288
|
+
this._container.instance('url', urlGenerator);
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
this._mwRegistry.register('cors', new CorsMiddleware());
|
|
292
|
+
this._mwRegistry.register('throttle', new ThrottleMiddleware({max: 60, window: 60}));
|
|
293
|
+
this._mwRegistry.register('log', new LogMiddleware());
|
|
294
|
+
this._mwRegistry.register('auth', AuthMiddleware);
|
|
295
|
+
}
|
|
206
296
|
}
|
|
207
297
|
|
|
208
298
|
module.exports = Application;
|
|
299
|
+
|
|
300
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
function _tryChalk() {
|
|
303
|
+
try {
|
|
304
|
+
return require('chalk');
|
|
305
|
+
} catch {
|
|
306
|
+
const id = s => s;
|
|
307
|
+
const p = new Proxy({}, {get: () => p, apply: (_, __, [s]) => String(s || '')});
|
|
308
|
+
p.dim = id;
|
|
309
|
+
p.bold = p;
|
|
310
|
+
p.white = id;
|
|
311
|
+
return p;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HttpServer
|
|
5
|
+
*
|
|
6
|
+
* Owns everything that belongs to the process/server layer — the things
|
|
7
|
+
* Express puts in bin/www that have nothing to do with your app logic:
|
|
8
|
+
*
|
|
9
|
+
* - Port normalisation and validation
|
|
10
|
+
* - EADDRINUSE / EACCES error handling with clear messages
|
|
11
|
+
* - Startup log (route count, URL, Ctrl+C hint)
|
|
12
|
+
* - IPC ready signal to the hot-reload proxy (millas serve)
|
|
13
|
+
* - Graceful shutdown on SIGTERM / SIGINT
|
|
14
|
+
*
|
|
15
|
+
* The Application kernel knows nothing about any of this.
|
|
16
|
+
*
|
|
17
|
+
* ── Usage in bootstrap/server.js ────────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* const app = require('./app'); // configured Millas app, no listen()
|
|
20
|
+
* const server = new HttpServer(app);
|
|
21
|
+
* server.start();
|
|
22
|
+
*
|
|
23
|
+
* ── Advanced ─────────────────────────────────────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* new HttpServer(app, {
|
|
26
|
+
* port: 4000,
|
|
27
|
+
* host: '0.0.0.0',
|
|
28
|
+
* onStart: (port, host) => console.log(`up on ${host}:${port}`),
|
|
29
|
+
* onShutdown: () => db.close(),
|
|
30
|
+
* }).start();
|
|
31
|
+
*/
|
|
32
|
+
class HttpServer {
|
|
33
|
+
/**
|
|
34
|
+
* @param {import('./Application')} app — booted Millas Application instance
|
|
35
|
+
* @param {object} options
|
|
36
|
+
* @param {number} [options.port]
|
|
37
|
+
* @param {string} [options.host]
|
|
38
|
+
* @param {Function}[options.onStart] — (port, host) => void, called after listen
|
|
39
|
+
* @param {Function}[options.onShutdown] — async () => void, called before process.exit
|
|
40
|
+
*/
|
|
41
|
+
constructor(app, options = {}) {
|
|
42
|
+
this._app = app;
|
|
43
|
+
this._options = options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve port, boot the server, wire signals.
|
|
48
|
+
* Returns a Promise that resolves once the server is listening.
|
|
49
|
+
*/
|
|
50
|
+
async start() {
|
|
51
|
+
const port = this._resolvePort();
|
|
52
|
+
const host = this._options.host
|
|
53
|
+
|| process.env.MILLAS_HOST
|
|
54
|
+
|| process.env.HOST
|
|
55
|
+
|| 'localhost';
|
|
56
|
+
|
|
57
|
+
// Boot providers if not already done
|
|
58
|
+
if (!this._app._booted) {
|
|
59
|
+
await this._app.boot();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Mount routes + fallbacks if not already mounted
|
|
63
|
+
if (!this._app._router) {
|
|
64
|
+
this._app.mount();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Start listening — may throw on EADDRINUSE / EACCES
|
|
68
|
+
try {
|
|
69
|
+
await this._app.listen(port, host);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this._handleListenError(err, port);
|
|
72
|
+
return; // _handleListenError always exits
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Notify the millas serve hot-reload proxy that we're ready
|
|
76
|
+
if (typeof process.send === 'function') {
|
|
77
|
+
process.send({type: 'ready', port});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// User callback
|
|
81
|
+
if (typeof this._options.onStart === 'function') {
|
|
82
|
+
this._options.onStart(port, host);
|
|
83
|
+
}
|
|
84
|
+
this._handleSignals();
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Private ────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
_resolvePort() {
|
|
91
|
+
const raw =
|
|
92
|
+
this._options.port ||
|
|
93
|
+
parseInt(process.env.MILLAS_INTERNAL_PORT, 10) ||
|
|
94
|
+
parseInt(process.env.APP_PORT, 10) ||
|
|
95
|
+
parseInt(process.env.PORT, 10) ||
|
|
96
|
+
3000;
|
|
97
|
+
|
|
98
|
+
return this._normalizePort(raw);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_normalizePort(val) {
|
|
102
|
+
const n = parseInt(val, 10);
|
|
103
|
+
if (isNaN(n)) return val; // named pipe — pass through
|
|
104
|
+
if (n >= 0) return n;
|
|
105
|
+
return 3000;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_handleListenError(err, port) {
|
|
109
|
+
const bind = typeof port === 'string' ? `pipe ${port}` : `port ${port}`;
|
|
110
|
+
|
|
111
|
+
switch (err.code) {
|
|
112
|
+
case 'EACCES':
|
|
113
|
+
console.error(
|
|
114
|
+
`✖ ${bind} requires elevated privileges.\n` +
|
|
115
|
+
` Try a port above 1024 or run with sudo.`
|
|
116
|
+
);
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
case 'EADDRINUSE':
|
|
120
|
+
console.error(
|
|
121
|
+
`✖ ${bind} is already in use.\n` +
|
|
122
|
+
` Another process is listening on that port.\n` +
|
|
123
|
+
` Try: APP_PORT=3001 millas serve`
|
|
124
|
+
);
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
default:
|
|
128
|
+
console.error(`✖ Listen error: ${err.message}`);
|
|
129
|
+
if (process.env.APP_DEBUG) console.error(err.stack + '\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_handleSignals() {
|
|
137
|
+
const shutdown = async (signal) => {
|
|
138
|
+
process.exit(0);
|
|
139
|
+
// process.stdout.write(`\n Shutting down (${signal})…\n`);
|
|
140
|
+
//
|
|
141
|
+
// if (typeof this._options.onShutdown === 'function') {
|
|
142
|
+
// try {
|
|
143
|
+
// await this._options.onShutdown();
|
|
144
|
+
// } catch {
|
|
145
|
+
// }
|
|
146
|
+
// }
|
|
147
|
+
//
|
|
148
|
+
// await this._app.shutdown(0);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
process.once('SIGTERM', () => shutdown("SIGTERM"));
|
|
152
|
+
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = HttpServer;
|