millas 0.2.12-beta → 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 +3 -16
- package/src/auth/Auth.js +13 -8
- package/src/auth/AuthController.js +3 -1
- package/src/auth/AuthUser.js +98 -0
- package/src/cli.js +1 -1
- package/src/commands/serve.js +81 -110
- package/src/container/AppInitializer.js +158 -0
- package/src/container/Application.js +278 -253
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +23 -280
- package/src/container/MillasConfig.js +163 -0
- 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/facades/Admin.js +1 -1
- package/src/facades/Auth.js +22 -39
- package/src/facades/Cache.js +21 -10
- package/src/facades/Database.js +1 -1
- package/src/facades/Events.js +18 -17
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +42 -45
- package/src/facades/Log.js +25 -49
- package/src/facades/Mail.js +27 -32
- package/src/facades/Queue.js +22 -15
- package/src/facades/Storage.js +18 -10
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/ResponseDispatcher.js +18 -111
- 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/index.js +5 -144
- package/src/logger/formatters/PrettyFormatter.js +15 -5
- package/src/logger/internal.js +2 -2
- package/src/logger/patchConsole.js +91 -81
- package/src/middleware/MiddlewareRegistry.js +62 -82
- package/src/orm/migration/ModelInspector.js +339 -340
- 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 +3 -2
- package/src/providers/MailServiceProvider.js +3 -2
- package/src/providers/QueueServiceProvider.js +3 -2
- package/src/router/Router.js +119 -152
- package/src/scaffold/templates.js +8 -7
- package/src/facades/Validation.js +0 -69
package/package.json
CHANGED
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "millas",
|
|
3
|
-
"version": "0.2.12-beta",
|
|
3
|
+
"version": "0.2.12-beta-1",
|
|
4
4
|
"description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
8
|
-
"./
|
|
9
|
-
"./
|
|
10
|
-
"./bin/*": "./bin/*.js",
|
|
11
|
-
"./validation": "./src/validation/Validator.js",
|
|
12
|
-
"./facades/Http": "./src/facades/Http.js",
|
|
13
|
-
"./facades/Validation": "./src/facades/Validation.js",
|
|
14
|
-
"./facades/Database": "./src/facades/Database.js",
|
|
15
|
-
"./facades/Auth": "./src/facades/Auth.js",
|
|
16
|
-
"./facades/Log": "./src/facades/Log.js",
|
|
17
|
-
"./facades/Cache": "./src/facades/Cache.js",
|
|
18
|
-
"./facades/Mail": "./src/facades/Mail.js",
|
|
19
|
-
"./facades/Queue": "./src/facades/Queue.js",
|
|
20
|
-
"./facades/Events": "./src/facades/Events.js",
|
|
21
|
-
"./facades/Storage": "./src/facades/Storage.js",
|
|
22
|
-
"./facades/Admin": "./src/facades/Admin.js"
|
|
8
|
+
"./core/*": "./src/core/*.js",
|
|
9
|
+
"./facades/*": "./src/facades/*.js"
|
|
23
10
|
},
|
|
24
11
|
"bin": {
|
|
25
12
|
"millas": "./bin/millas.js"
|
package/src/auth/Auth.js
CHANGED
|
@@ -230,13 +230,18 @@ class Auth {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
_buildTokenPayload(user) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
233
|
+
// If the model defines tokenPayload(), use it — allows custom JWT claims.
|
|
234
|
+
// Otherwise fall back to the standard shape.
|
|
235
|
+
const base = typeof user.tokenPayload === 'function'
|
|
236
|
+
? user.tokenPayload()
|
|
237
|
+
: {
|
|
238
|
+
id: user.id,
|
|
239
|
+
sub: user.id,
|
|
240
|
+
email: user.email,
|
|
241
|
+
role: user.role || null,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return { ...base, iat: Math.floor(Date.now() / 1000) };
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
_requireUserModel() {
|
|
@@ -251,4 +256,4 @@ class Auth {
|
|
|
251
256
|
|
|
252
257
|
// Singleton facade
|
|
253
258
|
module.exports = new Auth();
|
|
254
|
-
module.exports.Auth = Auth;
|
|
259
|
+
module.exports.Auth = Auth;
|
|
@@ -87,6 +87,8 @@ class AuthController extends Controller {
|
|
|
87
87
|
|
|
88
88
|
_safeUser(user) {
|
|
89
89
|
if (!user) return null;
|
|
90
|
+
// Use the model's toSafeObject() if defined (AuthUser and subclasses provide this)
|
|
91
|
+
if (typeof user.toSafeObject === 'function') return user.toSafeObject();
|
|
90
92
|
const data = user.toJSON ? user.toJSON() : { ...user };
|
|
91
93
|
delete data.password;
|
|
92
94
|
delete data.remember_token;
|
|
@@ -94,4 +96,4 @@ class AuthController extends Controller {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
module.exports = AuthController;
|
|
99
|
+
module.exports = AuthController;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Model = require('../orm/model/Model');
|
|
4
|
+
const fields = require('../orm/fields/index').fields;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AuthUser
|
|
8
|
+
*
|
|
9
|
+
* Base model for authentication. Ships with Millas.
|
|
10
|
+
* Covers the exact contract that Auth, AuthMiddleware, and AuthController expect:
|
|
11
|
+
* - email (unique, required for login)
|
|
12
|
+
* - password (hashed by Auth.register / Auth.hashPassword)
|
|
13
|
+
* - role (read by RoleMiddleware)
|
|
14
|
+
*
|
|
15
|
+
* ── Extending ────────────────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* Extend this instead of writing a User model from scratch.
|
|
18
|
+
* Add your own fields on top — all Auth behaviour is inherited.
|
|
19
|
+
*
|
|
20
|
+
* class User extends AuthUser {
|
|
21
|
+
* static table = 'users';
|
|
22
|
+
* static fields = {
|
|
23
|
+
* ...AuthUser.fields,
|
|
24
|
+
* phone: fields.string({ nullable: true }),
|
|
25
|
+
* avatar_url: fields.string({ nullable: true }),
|
|
26
|
+
* bio: fields.text({ nullable: true }),
|
|
27
|
+
* };
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* ── Customising the token payload ─────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* Override tokenPayload() to add custom claims to the JWT:
|
|
33
|
+
*
|
|
34
|
+
* class User extends AuthUser {
|
|
35
|
+
* tokenPayload() {
|
|
36
|
+
* return {
|
|
37
|
+
* ...super.tokenPayload(),
|
|
38
|
+
* plan: this.plan,
|
|
39
|
+
* orgId: this.org_id,
|
|
40
|
+
* };
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* ── Customising register / login hooks ────────────────────────────────────
|
|
45
|
+
*
|
|
46
|
+
* Override static hooks to run logic around auth operations:
|
|
47
|
+
*
|
|
48
|
+
* class User extends AuthUser {
|
|
49
|
+
* static async afterCreate(instance) {
|
|
50
|
+
* await emit('user.registered', { user: instance });
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
*/
|
|
54
|
+
class AuthUser extends Model {
|
|
55
|
+
static table = 'users';
|
|
56
|
+
|
|
57
|
+
static fields = {
|
|
58
|
+
id: fields.id(),
|
|
59
|
+
name: fields.string({ max: 100 }),
|
|
60
|
+
email: fields.string({ unique: true }),
|
|
61
|
+
password: fields.string(),
|
|
62
|
+
role: fields.enum(['admin', 'user'], { default: 'user' }),
|
|
63
|
+
created_at: fields.timestamp(),
|
|
64
|
+
updated_at: fields.timestamp(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ── Auth contract helpers ──────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The fields to include in the JWT payload.
|
|
71
|
+
* Override to add custom claims.
|
|
72
|
+
*
|
|
73
|
+
* @returns {object}
|
|
74
|
+
*/
|
|
75
|
+
tokenPayload() {
|
|
76
|
+
return {
|
|
77
|
+
id: this.id,
|
|
78
|
+
sub: this.id,
|
|
79
|
+
email: this.email,
|
|
80
|
+
role: this.role || null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Safe representation — strips sensitive fields.
|
|
86
|
+
* Used by AuthController._safeUser().
|
|
87
|
+
*
|
|
88
|
+
* @returns {object}
|
|
89
|
+
*/
|
|
90
|
+
toSafeObject() {
|
|
91
|
+
const data = { ...this };
|
|
92
|
+
delete data.password;
|
|
93
|
+
delete data.remember_token;
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = AuthUser;
|
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ const program = new Command();
|
|
|
7
7
|
program
|
|
8
8
|
.name('millas')
|
|
9
9
|
.description(chalk.cyan('⚡ Millas — A modern batteries-included Node.js framework'))
|
|
10
|
-
.version('0.1
|
|
10
|
+
.version('0.2.12-beta-1');
|
|
11
11
|
|
|
12
12
|
// Load all command modules
|
|
13
13
|
require('./commands/new')(program);
|
package/src/commands/serve.js
CHANGED
|
@@ -6,8 +6,9 @@ const fs = require('fs-extra');
|
|
|
6
6
|
const fsnative = require('fs');
|
|
7
7
|
const {fork} = require('child_process');
|
|
8
8
|
const chokidar = require('chokidar');
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const patchConsole = require("../logger/patchConsole");
|
|
10
|
+
const Logger = require("../logger/internal");
|
|
11
|
+
// ── ASCII banner ───────────────────────────────────────────────────────────────
|
|
11
12
|
|
|
12
13
|
const BANNER_LINES = [
|
|
13
14
|
' ███╗ ███╗██╗██╗ ██╗ █████╗ ███████╗',
|
|
@@ -20,10 +21,9 @@ const BANNER_LINES = [
|
|
|
20
21
|
|
|
21
22
|
function printBanner(host, port) {
|
|
22
23
|
const env = process.env.NODE_ENV || 'development';
|
|
23
|
-
const ver = 'v' + (require('../../package.json').version || '0.1.
|
|
24
|
+
const ver = 'v' + (require('../../package.json').version || '0.1.0');
|
|
24
25
|
const url = `http://${host}:${port}`;
|
|
25
26
|
const hr = chalk.dim(' ' + '─'.repeat(54));
|
|
26
|
-
|
|
27
27
|
const envColour = env === 'production' ? chalk.red
|
|
28
28
|
: env === 'staging' ? chalk.yellow
|
|
29
29
|
: chalk.green;
|
|
@@ -32,52 +32,37 @@ function printBanner(host, port) {
|
|
|
32
32
|
for (const line of BANNER_LINES) {
|
|
33
33
|
process.stdout.write(chalk.bold.cyan(line) + '\n');
|
|
34
34
|
}
|
|
35
|
-
process.stdout.write('\n');
|
|
36
|
-
process.stdout.write(hr + '\n');
|
|
35
|
+
process.stdout.write('\n' + hr + '\n');
|
|
37
36
|
process.stdout.write(
|
|
38
|
-
' ' +
|
|
39
|
-
chalk.dim(
|
|
40
|
-
chalk.dim('│') + ' ' +
|
|
41
|
-
envColour('⬤ ' + env) + ' ' +
|
|
42
|
-
chalk.dim('│') + ' ' +
|
|
43
|
-
chalk.bold.white(url) +
|
|
44
|
-
'\n'
|
|
37
|
+
' ' + chalk.dim(ver.padEnd(8)) +
|
|
38
|
+
chalk.dim('│') + ' ' + envColour('⬤ ' + env) + ' ' +
|
|
39
|
+
chalk.dim('│') + ' ' + chalk.bold.white(url) + '\n'
|
|
45
40
|
);
|
|
46
41
|
process.stdout.write(hr + '\n\n');
|
|
47
42
|
}
|
|
48
43
|
|
|
49
|
-
// ──
|
|
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
|
-
];
|
|
44
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
63
45
|
|
|
64
|
-
|
|
46
|
+
const WATCH_DIRS = ['app', 'routes', 'config', 'bootstrap', 'providers', 'middleware'];
|
|
65
47
|
const WATCH_EXTS = new Set(['.js', '.mjs', '.cjs', '.json', '.njk', '.env']);
|
|
48
|
+
const DEBOUNCE_MS = 250;
|
|
49
|
+
|
|
66
50
|
|
|
67
|
-
|
|
68
|
-
const DEBOUNCE_MS = 300;
|
|
51
|
+
// ── HotReloader ───────────────────────────────────────────────────────────────
|
|
69
52
|
|
|
70
53
|
class HotReloader {
|
|
71
|
-
constructor(bootstrapPath,
|
|
54
|
+
constructor(bootstrapPath, publicPort, publicHost) {
|
|
72
55
|
this._bootstrap = bootstrapPath;
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
this._env = env;
|
|
56
|
+
this._initialised = false
|
|
57
|
+
|
|
76
58
|
this._child = null;
|
|
77
|
-
this._watchers = [];
|
|
78
|
-
this._timer = null;
|
|
79
59
|
this._starting = false;
|
|
80
60
|
this._restarts = 0;
|
|
61
|
+
|
|
62
|
+
// Queued { req, res, timer } entries while child is restarting
|
|
63
|
+
this._queue = [];
|
|
64
|
+
this._watchers = [];
|
|
65
|
+
this._timer = null;
|
|
81
66
|
}
|
|
82
67
|
|
|
83
68
|
start() {
|
|
@@ -86,75 +71,74 @@ class HotReloader {
|
|
|
86
71
|
this._handleSignals();
|
|
87
72
|
}
|
|
88
73
|
|
|
89
|
-
// ── Child process management ──────────────────────────────────────────────
|
|
90
|
-
|
|
91
74
|
_spawnChild() {
|
|
92
|
-
if (this._starting)
|
|
75
|
+
if (this._starting) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
93
78
|
this._starting = true;
|
|
94
|
-
|
|
79
|
+
const extra = {}
|
|
80
|
+
if (!this._initialised) {
|
|
81
|
+
extra["MILLAS_START_UP"] = true
|
|
82
|
+
this._initialised = true
|
|
83
|
+
}
|
|
95
84
|
this._child = fork(this._bootstrap, [], {
|
|
96
|
-
env: {
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
env: {
|
|
86
|
+
...extra,
|
|
87
|
+
MILLAS_CHILD: '1',
|
|
88
|
+
},
|
|
89
|
+
stdio: 'inherit',
|
|
99
90
|
});
|
|
100
91
|
|
|
92
|
+
// Application.listen() sends { type:'ready' } via IPC once bound
|
|
93
|
+
this._child.on('message', msg => {
|
|
94
|
+
if (msg && msg.type === 'ready') {
|
|
95
|
+
this._starting = false;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
//
|
|
101
99
|
this._child.on('exit', (code, signal) => {
|
|
102
100
|
this._starting = false;
|
|
103
|
-
|
|
101
|
+
|
|
104
102
|
if (code !== 0 && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
|
|
105
|
-
|
|
106
|
-
'\n' + chalk.red(' ✖ App crashed') +
|
|
103
|
+
console.error(chalk.red('✖ App crashed') +
|
|
107
104
|
chalk.dim(` (exit ${code ?? signal})`) +
|
|
108
|
-
chalk.dim(' —
|
|
105
|
+
chalk.dim(' — fix the error, file watcher will reload…')
|
|
109
106
|
);
|
|
110
107
|
}
|
|
111
108
|
});
|
|
112
|
-
|
|
113
|
-
this._child.on('error',
|
|
109
|
+
//
|
|
110
|
+
this._child.on('error', err => {
|
|
114
111
|
this._starting = false;
|
|
115
|
-
|
|
112
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
116
113
|
});
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
_killChild(cb) {
|
|
120
|
-
if (!this._child || this._child.exitCode !== null)
|
|
121
|
-
cb();
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
117
|
+
if (!this._child || this._child.exitCode !== null) return cb();
|
|
124
118
|
this._child.once('exit', cb);
|
|
125
119
|
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
120
|
}
|
|
134
121
|
|
|
135
122
|
_restart(changedFile) {
|
|
136
|
-
const rel = changedFile ? path.relative(process.cwd(), changedFile) : '';
|
|
137
123
|
const link = changedFile
|
|
138
|
-
? `\x1b]8;;file://${changedFile}\x07${changedFile}\x1b]8;;\x07`
|
|
124
|
+
? `\x1b]8;;file://${changedFile}\x07${path.relative(process.cwd(), changedFile)}\x1b]8;;\x07`
|
|
139
125
|
: '';
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
'\n ' + chalk.yellow('↺') + ' ' +
|
|
126
|
+
console.warn(
|
|
127
|
+
chalk.yellow('↺') + ' ' +
|
|
143
128
|
chalk.white('Reloading') +
|
|
144
|
-
(link ? ' ' +
|
|
145
|
-
'\n'
|
|
129
|
+
(link ? chalk.blueBright(' ' + link) : '')
|
|
146
130
|
);
|
|
147
131
|
|
|
148
132
|
this._restarts++;
|
|
149
|
-
|
|
150
|
-
this._killChild(() => {
|
|
151
|
-
this._spawnChild();
|
|
152
|
-
});
|
|
133
|
+
this._killChild(() => this._spawnChild());
|
|
153
134
|
}
|
|
154
135
|
|
|
155
|
-
// ──
|
|
136
|
+
// ── Watcher ───────────────────────────────────────────────────────────────
|
|
156
137
|
|
|
157
138
|
_watch() {
|
|
139
|
+
console.log(chalk.green('✔') + ' ' +
|
|
140
|
+
chalk.dim('Watching for changes…')
|
|
141
|
+
);
|
|
158
142
|
const cwd = process.cwd();
|
|
159
143
|
const watchPaths = [
|
|
160
144
|
...WATCH_DIRS.map(d => path.join(cwd, d)),
|
|
@@ -166,6 +150,9 @@ class HotReloader {
|
|
|
166
150
|
persistent: true,
|
|
167
151
|
ignoreInitial: true,
|
|
168
152
|
ignored: /(^|[\/\\])\..(?!env)/,
|
|
153
|
+
// Wait for the file write to settle before reloading.
|
|
154
|
+
// Prevents double-restarts on editors that truncate then rewrite.
|
|
155
|
+
awaitWriteFinish: {stabilityThreshold: 80, pollInterval: 20},
|
|
169
156
|
});
|
|
170
157
|
|
|
171
158
|
watcher.on('all', (event, filePath) => {
|
|
@@ -179,9 +166,7 @@ class HotReloader {
|
|
|
179
166
|
|
|
180
167
|
_scheduleRestart(changedFile) {
|
|
181
168
|
clearTimeout(this._timer);
|
|
182
|
-
this._timer = setTimeout(() =>
|
|
183
|
-
this._restart(changedFile);
|
|
184
|
-
}, DEBOUNCE_MS);
|
|
169
|
+
this._timer = setTimeout(() => this._restart(changedFile), DEBOUNCE_MS);
|
|
185
170
|
}
|
|
186
171
|
|
|
187
172
|
_stopWatching() {
|
|
@@ -194,8 +179,7 @@ class HotReloader {
|
|
|
194
179
|
this._watchers = [];
|
|
195
180
|
}
|
|
196
181
|
|
|
197
|
-
|
|
198
|
-
// ── Signal handling ───────────────────────────────────────────────────────
|
|
182
|
+
// ── Signals ───────────────────────────────────────────────────────────────
|
|
199
183
|
|
|
200
184
|
_handleSignals() {
|
|
201
185
|
const cleanup = () => {
|
|
@@ -204,16 +188,14 @@ class HotReloader {
|
|
|
204
188
|
if (this._child) this._child.kill('SIGTERM');
|
|
205
189
|
process.exit(0);
|
|
206
190
|
};
|
|
207
|
-
|
|
208
|
-
process.
|
|
209
|
-
process.on('SIGTERM', cleanup);
|
|
191
|
+
process.once('SIGINT', cleanup);
|
|
192
|
+
process.once('SIGTERM', cleanup);
|
|
210
193
|
}
|
|
211
194
|
}
|
|
212
195
|
|
|
213
|
-
// ── Command
|
|
196
|
+
// ── Command ────────────────────────────────────────────────────────────────────
|
|
214
197
|
|
|
215
198
|
module.exports = function (program) {
|
|
216
|
-
|
|
217
199
|
program
|
|
218
200
|
.command('serve')
|
|
219
201
|
.description('Start the development server with hot reload')
|
|
@@ -221,6 +203,8 @@ module.exports = function (program) {
|
|
|
221
203
|
.option('-h, --host <host>', 'Host to bind to', 'localhost')
|
|
222
204
|
.option('--no-reload', 'Disable hot reload (run once, like production)')
|
|
223
205
|
.action((options) => {
|
|
206
|
+
|
|
207
|
+
const restoreAfterPatch = patchConsole(Logger,"SystemOut")
|
|
224
208
|
const appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
225
209
|
|
|
226
210
|
if (!fs.existsSync(appBootstrap)) {
|
|
@@ -229,51 +213,38 @@ module.exports = function (program) {
|
|
|
229
213
|
process.exit(1);
|
|
230
214
|
}
|
|
231
215
|
|
|
216
|
+
|
|
217
|
+
const publicPort = parseInt(options.port, 10) || 3000;
|
|
218
|
+
const publicHost = options.host || 'localhost';
|
|
219
|
+
|
|
232
220
|
const env = {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
221
|
+
NODE_ENV: process.env.APP_ENV || 'development',
|
|
222
|
+
MILLERS_NODE_ENV: true,
|
|
223
|
+
MILLAS_HOST: publicHost,
|
|
224
|
+
MILLAS_PORT: String(publicPort),
|
|
237
225
|
};
|
|
238
226
|
|
|
239
227
|
Object.assign(process.env, env);
|
|
228
|
+
printBanner(publicHost, publicPort);
|
|
240
229
|
|
|
241
|
-
|
|
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();
|
|
230
|
+
if (options.reload !== false) {
|
|
257
231
|
|
|
232
|
+
new HotReloader(appBootstrap, publicPort, publicHost).start();
|
|
258
233
|
} else {
|
|
259
|
-
// ── Single-run mode (--no-reload) ────────────────────────────────────
|
|
260
234
|
try {
|
|
261
235
|
require(appBootstrap);
|
|
262
|
-
} catch (
|
|
263
|
-
|
|
264
|
-
if (process.env.DEBUG) process.stderr.write(chalk.dim(err.stack) + '\n');
|
|
265
|
-
process.exit(1);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.log("Error starting server: ", +e)
|
|
266
238
|
}
|
|
267
239
|
}
|
|
268
240
|
});
|
|
269
241
|
|
|
270
242
|
};
|
|
271
243
|
|
|
272
|
-
// Exported so other commands (queue:work) can validate the project exists
|
|
273
244
|
module.exports.requireProject = function (command) {
|
|
274
245
|
const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
275
246
|
if (!fsnative.existsSync(bootstrapPath)) {
|
|
276
247
|
process.stderr.write(chalk.red(`\n ✖ Not inside a Millas project (${command}).\n\n`));
|
|
277
248
|
process.exit(1);
|
|
278
249
|
}
|
|
279
|
-
};
|
|
250
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const Application = require('./Application');
|
|
5
|
+
const HttpServer = require('./HttpServer');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AppInitialiser
|
|
9
|
+
*
|
|
10
|
+
* The internal engine that receives what bootstrap/app.js exports
|
|
11
|
+
* (a MillasInstance), extracts its config, and wires up everything:
|
|
12
|
+
*
|
|
13
|
+
* - ExpressAdapter
|
|
14
|
+
* - Application kernel (DI container, providers, routes, middleware)
|
|
15
|
+
* - Admin panel
|
|
16
|
+
* - HttpServer (port, signals, startup log, IPC ready signal)
|
|
17
|
+
*
|
|
18
|
+
* Developers never see or interact with this class.
|
|
19
|
+
* It is used exclusively by runner.js (millas serve) and the no-reload path.
|
|
20
|
+
*
|
|
21
|
+
* ── Flow ─────────────────────────────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* bootstrap/app.js → MillasInstance → AppInitialiser.boot()
|
|
24
|
+
* ├─ builds ExpressAdapter
|
|
25
|
+
* ├─ builds Application
|
|
26
|
+
* ├─ registers providers
|
|
27
|
+
* ├─ registers routes
|
|
28
|
+
* ├─ registers middleware aliases
|
|
29
|
+
* ├─ boots providers
|
|
30
|
+
* ├─ mounts routes
|
|
31
|
+
* ├─ mounts admin (if configured)
|
|
32
|
+
* ├─ mounts fallbacks
|
|
33
|
+
* └─ starts HttpServer
|
|
34
|
+
*/
|
|
35
|
+
class AppInitializer {
|
|
36
|
+
/**
|
|
37
|
+
* @param {Object} config — the sealed export of bootstrap/app.js
|
|
38
|
+
*/
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this._config = config;
|
|
41
|
+
this._kernel = null;
|
|
42
|
+
this._adapter = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Boot the full application and start the HTTP server.
|
|
47
|
+
* Returns a Promise that resolves once the server is listening.
|
|
48
|
+
*/
|
|
49
|
+
async boot() {
|
|
50
|
+
const cfg = this._config;
|
|
51
|
+
|
|
52
|
+
// ── Build the HTTP adapter ───────────────────────────────────────────────
|
|
53
|
+
const ExpressAdapter = require('../http/adapters/ExpressAdapter');
|
|
54
|
+
const expressApp = express();
|
|
55
|
+
this._adapter = new ExpressAdapter(expressApp);
|
|
56
|
+
this._adapter.applyBodyParsers();
|
|
57
|
+
|
|
58
|
+
// Raw adapter middleware (helmet, compression, etc.)
|
|
59
|
+
for (const mw of (cfg.adapterMiddleware || [])) {
|
|
60
|
+
this._adapter.applyMiddleware(mw);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Build the Application kernel ─────────────────────────────────────────
|
|
64
|
+
this._kernel = new Application(this._adapter);
|
|
65
|
+
|
|
66
|
+
// Core providers (auto-enabled unless disabled in config)
|
|
67
|
+
const coreProviders = this._buildCoreProviders(cfg);
|
|
68
|
+
this._kernel.providers([...coreProviders, ...cfg.providers]);
|
|
69
|
+
|
|
70
|
+
// Named middleware aliases
|
|
71
|
+
for (const {alias, handler} of (cfg.middleware || [])) {
|
|
72
|
+
this._kernel.middleware(alias, handler);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Route definitions
|
|
76
|
+
if (cfg.routes) {
|
|
77
|
+
this._kernel.routes(cfg.routes);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Boot providers ───────────────────────────────────────────────────────
|
|
81
|
+
await this._kernel.boot();
|
|
82
|
+
|
|
83
|
+
// ── Mount routes ─────────────────────────────────────────────────────────
|
|
84
|
+
if (!process.env.MILLAS_ROUTE_LIST) {
|
|
85
|
+
this._kernel.mountRoutes();
|
|
86
|
+
|
|
87
|
+
// Admin panel — mounted between routes and fallbacks
|
|
88
|
+
if (cfg.admin !== null) {
|
|
89
|
+
try {
|
|
90
|
+
const Admin = require('../admin/Admin');
|
|
91
|
+
if (cfg.admin && Object.keys(cfg.admin).length) {
|
|
92
|
+
Admin.configure(cfg.admin);
|
|
93
|
+
}
|
|
94
|
+
Admin.mount(expressApp);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
process.stderr.write(`[millas] Admin mount failed: ${err.message}\n`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._kernel.mountFallbacks();
|
|
101
|
+
|
|
102
|
+
// ── Start the HTTP server ──────────────────────────────────────────────
|
|
103
|
+
const server = new HttpServer(this._kernel, {
|
|
104
|
+
onStart: cfg.onStart || undefined,
|
|
105
|
+
onShutdown: cfg.onShutdown || undefined,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await server.start();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Internal ───────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
_buildCoreProviders(cfg) {
|
|
115
|
+
const providers = [];
|
|
116
|
+
const load = (p) => {
|
|
117
|
+
try {
|
|
118
|
+
return require(p);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (cfg.logging !== false) {
|
|
125
|
+
const p = load('../providers/LogServiceProvider');
|
|
126
|
+
if (p) providers.push(p);
|
|
127
|
+
}
|
|
128
|
+
if (cfg.database !== false) {
|
|
129
|
+
const p = load('../providers/DatabaseServiceProvider');
|
|
130
|
+
if (p) providers.push(p);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (cfg.cache !== false || cfg.storage !== false) {
|
|
134
|
+
const p = load('../providers/CacheStorageServiceProvider');
|
|
135
|
+
if (p) {
|
|
136
|
+
if (cfg.cache !== false && p.CacheServiceProvider) providers.push(p.CacheServiceProvider);
|
|
137
|
+
if (cfg.storage !== false && p.StorageServiceProvider) providers.push(p.StorageServiceProvider);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (cfg.mail !== false) {
|
|
142
|
+
const p = load('../providers/MailServiceProvider');
|
|
143
|
+
if (p) providers.push(p);
|
|
144
|
+
}
|
|
145
|
+
if (cfg.queue !== false) {
|
|
146
|
+
const p = load('../providers/QueueServiceProvider');
|
|
147
|
+
if (p) providers.push(p);
|
|
148
|
+
}
|
|
149
|
+
if (cfg.events !== false) {
|
|
150
|
+
const p = load('../providers/EventServiceProvider');
|
|
151
|
+
if (p) providers.push(p);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return providers;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = AppInitializer;
|