millas 0.2.11 → 0.2.12-beta
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 +17 -3
- package/src/auth/AuthController.js +42 -133
- package/src/auth/AuthMiddleware.js +12 -23
- package/src/auth/RoleMiddleware.js +7 -17
- package/src/commands/migrate.js +46 -31
- package/src/commands/serve.js +266 -37
- package/src/container/Application.js +88 -8
- package/src/controller/Controller.js +79 -300
- package/src/errors/ErrorRenderer.js +640 -0
- package/src/facades/Admin.js +49 -0
- package/src/facades/Auth.js +46 -0
- package/src/facades/Cache.js +17 -0
- package/src/facades/Database.js +43 -0
- package/src/facades/Events.js +24 -0
- package/src/facades/Http.js +54 -0
- package/src/facades/Log.js +56 -0
- package/src/facades/Mail.js +40 -0
- package/src/facades/Queue.js +23 -0
- package/src/facades/Storage.js +17 -0
- package/src/facades/Validation.js +69 -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 +144 -0
- package/src/http/helpers.js +164 -0
- package/src/http/index.js +13 -0
- package/src/index.js +55 -2
- package/src/logger/internal.js +76 -0
- package/src/logger/patchConsole.js +135 -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 +126 -0
- package/src/middleware/ThrottleMiddleware.js +22 -26
- package/src/orm/fields/index.js +124 -56
- package/src/orm/migration/ModelInspector.js +7 -3
- package/src/orm/model/Model.js +96 -6
- package/src/orm/query/QueryBuilder.js +141 -3
- package/src/providers/LogServiceProvider.js +88 -18
- package/src/providers/ProviderRegistry.js +14 -1
- package/src/providers/ServiceProvider.js +40 -8
- package/src/router/Router.js +155 -223
- package/src/scaffold/maker.js +24 -59
- package/src/scaffold/templates.js +13 -12
- package/src/validation/BaseValidator.js +193 -0
- package/src/validation/Validator.js +680 -0
package/src/commands/serve.js
CHANGED
|
@@ -3,48 +3,277 @@
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
|
+
const fsnative = require('fs');
|
|
7
|
+
const {fork} = require('child_process');
|
|
8
|
+
const chokidar = require('chokidar');
|
|
9
|
+
|
|
10
|
+
// ── ASCII banner ──────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const BANNER_LINES = [
|
|
13
|
+
' ███╗ ███╗██╗██╗ ██╗ █████╗ ███████╗',
|
|
14
|
+
' ████╗ ████║██║██║ ██║ ██╔══██╗██╔════╝',
|
|
15
|
+
' ██╔████╔██║██║██║ ██║ ███████║███████╗',
|
|
16
|
+
' ██║╚██╔╝██║██║██║ ██║ ██╔══██║╚════██║',
|
|
17
|
+
' ██║ ╚═╝ ██║██║███████╗███████╗██║ ██║███████║',
|
|
18
|
+
' ╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function printBanner(host, port) {
|
|
22
|
+
const env = process.env.NODE_ENV || 'development';
|
|
23
|
+
const ver = 'v' + (require('../../package.json').version || '0.1.2');
|
|
24
|
+
const url = `http://${host}:${port}`;
|
|
25
|
+
const hr = chalk.dim(' ' + '─'.repeat(54));
|
|
26
|
+
|
|
27
|
+
const envColour = env === 'production' ? chalk.red
|
|
28
|
+
: env === 'staging' ? chalk.yellow
|
|
29
|
+
: chalk.green;
|
|
30
|
+
|
|
31
|
+
process.stdout.write('\n');
|
|
32
|
+
for (const line of BANNER_LINES) {
|
|
33
|
+
process.stdout.write(chalk.bold.cyan(line) + '\n');
|
|
34
|
+
}
|
|
35
|
+
process.stdout.write('\n');
|
|
36
|
+
process.stdout.write(hr + '\n');
|
|
37
|
+
process.stdout.write(
|
|
38
|
+
' ' +
|
|
39
|
+
chalk.dim(ver.padEnd(8)) +
|
|
40
|
+
chalk.dim('│') + ' ' +
|
|
41
|
+
envColour('⬤ ' + env) + ' ' +
|
|
42
|
+
chalk.dim('│') + ' ' +
|
|
43
|
+
chalk.bold.white(url) +
|
|
44
|
+
'\n'
|
|
45
|
+
);
|
|
46
|
+
process.stdout.write(hr + '\n\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Hot reload watcher ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Directories watched for changes (relative to project root).
|
|
53
|
+
* Watching parent dirs is enough — fs.watch recursive covers all descendants.
|
|
54
|
+
*/
|
|
55
|
+
const WATCH_DIRS = [
|
|
56
|
+
'app',
|
|
57
|
+
'routes',
|
|
58
|
+
'config',
|
|
59
|
+
'bootstrap',
|
|
60
|
+
'providers',
|
|
61
|
+
'middleware',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/** File extensions that trigger a reload when changed. */
|
|
65
|
+
const WATCH_EXTS = new Set(['.js', '.mjs', '.cjs', '.json', '.njk', '.env']);
|
|
66
|
+
|
|
67
|
+
/** How long to wait after the last change before restarting (ms). */
|
|
68
|
+
const DEBOUNCE_MS = 300;
|
|
69
|
+
|
|
70
|
+
class HotReloader {
|
|
71
|
+
constructor(bootstrapPath, env) {
|
|
72
|
+
this._bootstrap = bootstrapPath;
|
|
73
|
+
this._cwd = path.dirname(bootstrapPath.replace(/bootstrap.+$/, '')) ||
|
|
74
|
+
process.cwd();
|
|
75
|
+
this._env = env;
|
|
76
|
+
this._child = null;
|
|
77
|
+
this._watchers = [];
|
|
78
|
+
this._timer = null;
|
|
79
|
+
this._starting = false;
|
|
80
|
+
this._restarts = 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
start() {
|
|
84
|
+
this._spawnChild();
|
|
85
|
+
this._watch();
|
|
86
|
+
this._handleSignals();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Child process management ──────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
_spawnChild() {
|
|
92
|
+
if (this._starting) return;
|
|
93
|
+
this._starting = true;
|
|
94
|
+
|
|
95
|
+
this._child = fork(this._bootstrap, [], {
|
|
96
|
+
env: {...process.env, ...this._env},
|
|
97
|
+
stdio: 'inherit', // child shares parent's stdout/stderr — output appears inline
|
|
98
|
+
// detached: false — child dies with parent
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this._child.on('exit', (code, signal) => {
|
|
102
|
+
this._starting = false;
|
|
103
|
+
// Abnormal exit (crash) — don't auto-restart, let the developer fix it
|
|
104
|
+
if (code !== 0 && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
'\n' + chalk.red(' ✖ App crashed') +
|
|
107
|
+
chalk.dim(` (exit ${code ?? signal})`) +
|
|
108
|
+
chalk.dim(' — waiting for changes…\n\n')
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this._child.on('error', (err) => {
|
|
114
|
+
this._starting = false;
|
|
115
|
+
process.stderr.write(chalk.red(`\n ✖ Child error: ${err.message}\n\n`));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_killChild(cb) {
|
|
120
|
+
if (!this._child || this._child.exitCode !== null) {
|
|
121
|
+
cb();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this._child.once('exit', cb);
|
|
125
|
+
this._child.kill('SIGTERM');
|
|
126
|
+
|
|
127
|
+
// Force kill if graceful shutdown takes too long
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
if (this._child && this._child.exitCode === null) {
|
|
130
|
+
this._child.kill('SIGKILL');
|
|
131
|
+
}
|
|
132
|
+
}, 3000).unref();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_restart(changedFile) {
|
|
136
|
+
const rel = changedFile ? path.relative(process.cwd(), changedFile) : '';
|
|
137
|
+
const link = changedFile
|
|
138
|
+
? `\x1b]8;;file://${changedFile}\x07${changedFile}\x1b]8;;\x07`
|
|
139
|
+
: '';
|
|
140
|
+
|
|
141
|
+
process.stdout.write(
|
|
142
|
+
'\n ' + chalk.yellow('↺') + ' ' +
|
|
143
|
+
chalk.white('Reloading') +
|
|
144
|
+
(link ? ' ' + chalk.cyan(link) : '') +
|
|
145
|
+
'\n'
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
this._restarts++;
|
|
149
|
+
|
|
150
|
+
this._killChild(() => {
|
|
151
|
+
this._spawnChild();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── File watching ─────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
_watch() {
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
const watchPaths = [
|
|
160
|
+
...WATCH_DIRS.map(d => path.join(cwd, d)),
|
|
161
|
+
path.join(cwd, '.env'),
|
|
162
|
+
path.join(cwd, '.env.local'),
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
166
|
+
persistent: true,
|
|
167
|
+
ignoreInitial: true,
|
|
168
|
+
ignored: /(^|[\/\\])\..(?!env)/,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
watcher.on('all', (event, filePath) => {
|
|
172
|
+
if (WATCH_EXTS.has(path.extname(filePath))) {
|
|
173
|
+
this._scheduleRestart(filePath);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
this._watchers.push(watcher);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_scheduleRestart(changedFile) {
|
|
181
|
+
clearTimeout(this._timer);
|
|
182
|
+
this._timer = setTimeout(() => {
|
|
183
|
+
this._restart(changedFile);
|
|
184
|
+
}, DEBOUNCE_MS);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_stopWatching() {
|
|
188
|
+
for (const w of this._watchers) {
|
|
189
|
+
try {
|
|
190
|
+
w.close();
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this._watchers = [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
// ── Signal handling ───────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
_handleSignals() {
|
|
201
|
+
const cleanup = () => {
|
|
202
|
+
clearTimeout(this._timer);
|
|
203
|
+
this._stopWatching();
|
|
204
|
+
if (this._child) this._child.kill('SIGTERM');
|
|
205
|
+
process.exit(0);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
process.on('SIGINT', cleanup);
|
|
209
|
+
process.on('SIGTERM', cleanup);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Command definition ────────────────────────────────────────────────────────
|
|
6
214
|
|
|
7
215
|
module.exports = function (program) {
|
|
8
|
-
program
|
|
9
|
-
.command('serve')
|
|
10
|
-
.description('Start the development server')
|
|
11
|
-
.option('-p, --port <port>', 'Port to listen on', '3000')
|
|
12
|
-
.option('-h, --host <host>', 'Host to bind to', 'localhost')
|
|
13
|
-
.action(async (options) => {
|
|
14
|
-
const appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
15
|
-
|
|
16
|
-
if (!fs.existsSync(appBootstrap)) {
|
|
17
|
-
console.error(chalk.red('\n ✖ No Millas project found in current directory.\n'));
|
|
18
|
-
console.log(` Make sure you are inside a Millas project and bootstrap/app.js exists.\n`);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
216
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
217
|
+
program
|
|
218
|
+
.command('serve')
|
|
219
|
+
.description('Start the development server with hot reload')
|
|
220
|
+
.option('-p, --port <port>', 'Port to listen on', '3000')
|
|
221
|
+
.option('-h, --host <host>', 'Host to bind to', 'localhost')
|
|
222
|
+
.option('--no-reload', 'Disable hot reload (run once, like production)')
|
|
223
|
+
.action((options) => {
|
|
224
|
+
const appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
25
225
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
226
|
+
if (!fs.existsSync(appBootstrap)) {
|
|
227
|
+
process.stderr.write(chalk.red('\n ✖ No Millas project found here.\n'));
|
|
228
|
+
process.stderr.write(chalk.dim(' Make sure bootstrap/app.js exists.\n\n'));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const env = {
|
|
233
|
+
MILLAS_PORT: options.port,
|
|
234
|
+
MILLAS_HOST: options.host,
|
|
235
|
+
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
236
|
+
MILLAS_RELOAD: options.reload ? '1' : '0',
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
Object.assign(process.env, env);
|
|
240
|
+
|
|
241
|
+
printBanner(options.host, options.port);
|
|
242
|
+
|
|
243
|
+
const hotReload = options.reload !== false;
|
|
244
|
+
|
|
245
|
+
if (hotReload) {
|
|
246
|
+
// ── Hot reload mode ──────────────────────────────────────────────────
|
|
247
|
+
// Parent process watches files. App runs in a child process.
|
|
248
|
+
// On change: kill child, clear require cache, refork.
|
|
249
|
+
process.stdout.write(
|
|
250
|
+
' ' + chalk.green('✔') + ' ' +
|
|
251
|
+
chalk.dim('Watching for file changes…') +
|
|
252
|
+
'\n'
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const reloader = new HotReloader(appBootstrap, env);
|
|
256
|
+
reloader.start();
|
|
257
|
+
|
|
258
|
+
} else {
|
|
259
|
+
// ── Single-run mode (--no-reload) ────────────────────────────────────
|
|
260
|
+
try {
|
|
261
|
+
require(appBootstrap);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
process.stderr.write(chalk.red(`\n ✖ Failed to start: ${err.message}\n\n`));
|
|
264
|
+
if (process.env.DEBUG) process.stderr.write(chalk.dim(err.stack) + '\n');
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
29
269
|
|
|
30
|
-
try {
|
|
31
|
-
require(appBootstrap);
|
|
32
|
-
} catch (err) {
|
|
33
|
-
console.error(chalk.red(`\n ✖ Failed to start server: ${err.message}\n`));
|
|
34
|
-
console.error(err.stack);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
270
|
};
|
|
39
271
|
|
|
40
|
-
// Exported so queue:work can
|
|
41
|
-
module.exports.requireProject = function(command) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.error(chalk.red(`\n ✖ Not inside a Millas project (${command}).\n`));
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
272
|
+
// Exported so other commands (queue:work) can validate the project exists
|
|
273
|
+
module.exports.requireProject = function (command) {
|
|
274
|
+
const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
275
|
+
if (!fsnative.existsSync(bootstrapPath)) {
|
|
276
|
+
process.stderr.write(chalk.red(`\n ✖ Not inside a Millas project (${command}).\n\n`));
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
50
279
|
};
|
|
@@ -82,12 +82,23 @@ class Application {
|
|
|
82
82
|
// ─── Lifecycle ───────────────────────────────────────────────────────────────
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* Run the full provider
|
|
85
|
+
* Run the full provider lifecycle:
|
|
86
|
+
* Phase 0 — beforeBoot (synchronous, global setup)
|
|
87
|
+
* Phase 1 — register (synchronous, container bindings)
|
|
88
|
+
* Phase 2 — boot (async, all bindings available)
|
|
89
|
+
*
|
|
90
|
+
* Emits: platform.booting → platform.booted
|
|
86
91
|
*/
|
|
87
92
|
async boot() {
|
|
88
93
|
if (this._booted) return this;
|
|
94
|
+
|
|
95
|
+
this._emitSync('platform.booting', { providers: this._providers.list() });
|
|
96
|
+
|
|
89
97
|
await this._providers.boot();
|
|
90
98
|
this._booted = true;
|
|
99
|
+
|
|
100
|
+
this._emitSync('platform.booted', { providers: this._providers.list() });
|
|
101
|
+
|
|
91
102
|
return this;
|
|
92
103
|
}
|
|
93
104
|
|
|
@@ -104,7 +115,7 @@ class Application {
|
|
|
104
115
|
* Or use app.mount() which does all three in one call.
|
|
105
116
|
*/
|
|
106
117
|
mountRoutes() {
|
|
107
|
-
this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
|
|
118
|
+
this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry, this._container);
|
|
108
119
|
this._router.mountRoutes();
|
|
109
120
|
return this;
|
|
110
121
|
}
|
|
@@ -114,7 +125,7 @@ class Application {
|
|
|
114
125
|
*/
|
|
115
126
|
mountFallbacks() {
|
|
116
127
|
if (!this._router) {
|
|
117
|
-
this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
|
|
128
|
+
this._router = new Router(this._express, this._route.getRegistry(), this._mwRegistry, this._container);
|
|
118
129
|
}
|
|
119
130
|
this._router.mountFallbacks();
|
|
120
131
|
return this;
|
|
@@ -126,30 +137,86 @@ class Application {
|
|
|
126
137
|
* is mounted before this call.
|
|
127
138
|
*/
|
|
128
139
|
mount() {
|
|
129
|
-
const router = new Router(this._express, this._route.getRegistry(), this._mwRegistry);
|
|
140
|
+
const router = new Router(this._express, this._route.getRegistry(), this._mwRegistry, this._container);
|
|
130
141
|
router.mount();
|
|
131
142
|
return this;
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
/**
|
|
135
146
|
* Start the HTTP server.
|
|
147
|
+
* Emits: platform.listening
|
|
136
148
|
*/
|
|
137
149
|
listen(port, host, callback) {
|
|
138
|
-
const _port = port || parseInt(process.env.APP_PORT,
|
|
150
|
+
const _port = port || parseInt(process.env.APP_PORT, 10) || 3000;
|
|
139
151
|
const _host = host || process.env.MILLAS_HOST || 'localhost';
|
|
140
152
|
|
|
141
|
-
this._express.listen(_port, _host, () => {
|
|
153
|
+
const server = this._express.listen(_port, _host, () => {
|
|
142
154
|
if (!process.env.MILLAS_ROUTE_LIST) {
|
|
155
|
+
const chalk = _tryChalk();
|
|
143
156
|
const routeCount = this._route.list().length;
|
|
144
|
-
|
|
145
|
-
|
|
157
|
+
|
|
158
|
+
// Use process.stdout.write directly so these lines always appear
|
|
159
|
+
// regardless of log level — even if console is patched and debug is off.
|
|
160
|
+
process.stdout.write(
|
|
161
|
+
' ' + chalk.dim('›') + ' ' +
|
|
162
|
+
chalk.white(routeCount + ' route' + (routeCount !== 1 ? 's' : '') + ' registered') +
|
|
163
|
+
'\n'
|
|
164
|
+
);
|
|
165
|
+
process.stdout.write(
|
|
166
|
+
' ' + chalk.dim('›') + ' ' +
|
|
167
|
+
chalk.dim('Press ') + chalk.bold('Ctrl+C') + chalk.dim(' to stop') +
|
|
168
|
+
'\n\n'
|
|
169
|
+
);
|
|
146
170
|
}
|
|
171
|
+
|
|
172
|
+
this._emitSync('platform.listening', { port: _port, host: _host });
|
|
173
|
+
|
|
147
174
|
if (typeof callback === 'function') callback(_port, _host);
|
|
148
175
|
});
|
|
149
176
|
|
|
177
|
+
this._server = server;
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Graceful shutdown — emits platform.shutting_down, then closes the server.
|
|
183
|
+
* Call this instead of process.exit() for clean teardown.
|
|
184
|
+
*
|
|
185
|
+
* process.on('SIGTERM', () => app.shutdown());
|
|
186
|
+
*/
|
|
187
|
+
async shutdown(code = 0) {
|
|
188
|
+
this._emitSync('platform.shutting_down', {});
|
|
189
|
+
if (this._server) {
|
|
190
|
+
await new Promise(resolve => this._server.close(resolve));
|
|
191
|
+
}
|
|
192
|
+
process.exit(code);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Platform event bus ───────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Listen to a platform lifecycle event.
|
|
199
|
+
*
|
|
200
|
+
* app.on('platform.booted', ({ providers }) => { ... });
|
|
201
|
+
* app.on('platform.listening', ({ port, host }) => { ... });
|
|
202
|
+
* app.on('platform.shutting_down', () => process.cleanup());
|
|
203
|
+
*/
|
|
204
|
+
on(event, fn) {
|
|
205
|
+
if (!this._platformListeners) this._platformListeners = new Map();
|
|
206
|
+
if (!this._platformListeners.has(event)) this._platformListeners.set(event, []);
|
|
207
|
+
this._platformListeners.get(event).push(fn);
|
|
150
208
|
return this;
|
|
151
209
|
}
|
|
152
210
|
|
|
211
|
+
/** Synchronous platform event dispatch (fire-and-forget, no await). */
|
|
212
|
+
_emitSync(event, data) {
|
|
213
|
+
if (!this._platformListeners) return;
|
|
214
|
+
const listeners = this._platformListeners.get(event) || [];
|
|
215
|
+
for (const fn of listeners) {
|
|
216
|
+
try { fn(data); } catch {}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
153
220
|
// ─── Container Proxy ─────────────────────────────────────────────────────────
|
|
154
221
|
|
|
155
222
|
/**
|
|
@@ -206,3 +273,16 @@ class Application {
|
|
|
206
273
|
}
|
|
207
274
|
|
|
208
275
|
module.exports = Application;
|
|
276
|
+
|
|
277
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
// Safely require chalk — it ships with Millas but we guard in case of oddities
|
|
280
|
+
function _tryChalk() {
|
|
281
|
+
try { return require('chalk'); } catch {
|
|
282
|
+
// Fallback: no-op chalk that returns strings unchanged
|
|
283
|
+
const id = s => s;
|
|
284
|
+
const p = new Proxy({}, { get: () => p, apply: (_, __, [s]) => String(s || '') });
|
|
285
|
+
p.dim = id; p.bold = p; p.white = id;
|
|
286
|
+
return p;
|
|
287
|
+
}
|
|
288
|
+
}
|