millas 0.2.12-beta → 0.2.12-beta-2
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/admin/ActivityLog.js +153 -52
- package/src/admin/Admin.js +400 -167
- package/src/admin/AdminAuth.js +213 -98
- package/src/admin/FormGenerator.js +372 -0
- package/src/admin/HookRegistry.js +256 -0
- package/src/admin/QueryEngine.js +263 -0
- package/src/admin/ViewContext.js +309 -0
- package/src/admin/WidgetRegistry.js +406 -0
- package/src/admin/index.js +17 -0
- package/src/admin/resources/AdminResource.js +383 -97
- package/src/admin/static/admin.css +1341 -0
- package/src/admin/static/date-picker.css +157 -0
- package/src/admin/static/date-picker.js +316 -0
- package/src/admin/static/json-editor.css +649 -0
- package/src/admin/static/json-editor.js +1429 -0
- package/src/admin/static/ui.js +1044 -0
- package/src/admin/views/layouts/base.njk +65 -1013
- package/src/admin/views/pages/detail.njk +40 -16
- package/src/admin/views/pages/form.njk +47 -599
- package/src/admin/views/pages/list.njk +145 -62
- package/src/admin/views/partials/form-field.njk +53 -0
- package/src/admin/views/partials/form-footer.njk +28 -0
- package/src/admin/views/partials/form-readonly.njk +114 -0
- package/src/admin/views/partials/form-scripts.njk +476 -0
- package/src/admin/views/partials/form-widget.njk +296 -0
- package/src/admin/views/partials/json-dialog.njk +80 -0
- package/src/admin/views/partials/json-editor.njk +37 -0
- package/src/admin.zip +0 -0
- package/src/auth/Auth.js +31 -10
- package/src/auth/AuthController.js +3 -1
- package/src/auth/AuthUser.js +119 -0
- package/src/cli.js +4 -2
- package/src/commands/createsuperuser.js +254 -0
- package/src/commands/lang.js +589 -0
- package/src/commands/migrate.js +154 -81
- package/src/commands/serve.js +82 -110
- package/src/container/AppInitializer.js +215 -0
- package/src/container/Application.js +278 -253
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +29 -279
- package/src/container/MillasConfig.js +192 -0
- package/src/core/admin.js +5 -0
- package/src/core/auth.js +9 -0
- package/src/core/db.js +9 -0
- package/src/core/foundation.js +59 -0
- package/src/core/http.js +11 -0
- package/src/core/lang.js +1 -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/i18n/I18nServiceProvider.js +91 -0
- package/src/i18n/Translator.js +635 -0
- package/src/i18n/defaults.js +122 -0
- package/src/i18n/index.js +164 -0
- package/src/i18n/locales/en.js +55 -0
- package/src/i18n/locales/sw.js +48 -0
- package/src/index.js +5 -144
- package/src/logger/formatters/PrettyFormatter.js +103 -57
- package/src/logger/internal.js +2 -2
- package/src/logger/patchConsole.js +91 -81
- package/src/middleware/MiddlewareRegistry.js +62 -82
- package/src/migrations/system/0001_users.js +21 -0
- package/src/migrations/system/0002_admin_log.js +25 -0
- package/src/migrations/system/0003_sessions.js +23 -0
- package/src/orm/fields/index.js +210 -188
- package/src/orm/migration/DefaultValueParser.js +325 -0
- package/src/orm/migration/InteractiveResolver.js +191 -0
- package/src/orm/migration/Makemigrations.js +312 -0
- package/src/orm/migration/MigrationGraph.js +227 -0
- package/src/orm/migration/MigrationRunner.js +202 -108
- package/src/orm/migration/MigrationWriter.js +463 -0
- package/src/orm/migration/ModelInspector.js +412 -344
- package/src/orm/migration/ModelScanner.js +225 -0
- package/src/orm/migration/ProjectState.js +213 -0
- package/src/orm/migration/RenameDetector.js +175 -0
- package/src/orm/migration/SchemaBuilder.js +8 -81
- package/src/orm/migration/operations/base.js +57 -0
- package/src/orm/migration/operations/column.js +191 -0
- package/src/orm/migration/operations/fields.js +252 -0
- package/src/orm/migration/operations/index.js +55 -0
- package/src/orm/migration/operations/models.js +152 -0
- package/src/orm/migration/operations/registry.js +131 -0
- package/src/orm/migration/operations/special.js +51 -0
- package/src/orm/migration/utils.js +208 -0
- package/src/orm/model/Model.js +81 -13
- package/src/providers/AdminServiceProvider.js +66 -9
- package/src/providers/AuthServiceProvider.js +46 -7
- package/src/providers/CacheStorageServiceProvider.js +5 -3
- package/src/providers/DatabaseServiceProvider.js +3 -2
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +7 -3
- package/src/providers/MailServiceProvider.js +4 -3
- package/src/providers/QueueServiceProvider.js +4 -3
- package/src/router/Router.js +119 -152
- package/src/scaffold/templates.js +83 -26
- package/src/facades/Validation.js +0 -69
package/src/commands/migrate.js
CHANGED
|
@@ -9,40 +9,81 @@ module.exports = function (program) {
|
|
|
9
9
|
// ── makemigrations ──────────────────────────────────────────────────────────
|
|
10
10
|
program
|
|
11
11
|
.command('makemigrations')
|
|
12
|
-
.description('
|
|
13
|
-
.
|
|
12
|
+
.description('Detect model changes and generate migration files (never touches DB)')
|
|
13
|
+
.option('--dry-run', 'Show what would be generated without writing files')
|
|
14
|
+
.option('--noinput', 'Non-interactive mode — fails if dangerous fields need resolution')
|
|
15
|
+
.action(async (options) => {
|
|
14
16
|
try {
|
|
15
|
-
const ctx
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
);
|
|
22
|
-
const result = await inspector.makeMigrations();
|
|
17
|
+
const ctx = getProjectContext();
|
|
18
|
+
const Mk = require('../orm/migration/Makemigrations');
|
|
19
|
+
const mk = new Mk(ctx.modelsPath, ctx.appMigPath, ctx.systemMigPath, {
|
|
20
|
+
nonInteractive: options.noinput || false,
|
|
21
|
+
});
|
|
22
|
+
const result = await mk.run({ dryRun: options.dryRun });
|
|
23
23
|
|
|
24
24
|
if (result.files.length === 0) {
|
|
25
|
-
console.log(chalk.yellow(`\n
|
|
25
|
+
console.log(chalk.yellow(`\n No changes detected.\n`));
|
|
26
26
|
} else {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
if (options.dryRun) {
|
|
28
|
+
console.log(chalk.cyan('\n Would generate:'));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(chalk.green('\n Migrations generated:'));
|
|
31
|
+
}
|
|
32
|
+
result.files.forEach(f => console.log(chalk.cyan(` + ${f}`)));
|
|
33
|
+
result.ops.forEach(op => {
|
|
34
|
+
// Match Django's +/-/~ prefix style exactly
|
|
35
|
+
let prefix, label;
|
|
36
|
+
switch (op.type) {
|
|
37
|
+
case 'CreateModel':
|
|
38
|
+
prefix = chalk.green('+'); label = `Create model ${op.table}`; break;
|
|
39
|
+
case 'DeleteModel':
|
|
40
|
+
prefix = chalk.red('-'); label = `Delete model ${op.table}`; break;
|
|
41
|
+
case 'AddField':
|
|
42
|
+
prefix = chalk.green('+'); label = `Add field ${op.column} to ${op.table}`; break;
|
|
43
|
+
case 'RemoveField':
|
|
44
|
+
prefix = chalk.red('-'); label = `Remove field ${op.column} from ${op.table}`; break;
|
|
45
|
+
case 'AlterField':
|
|
46
|
+
prefix = chalk.yellow('~'); label = `Alter field ${op.column} on ${op.table}`; break;
|
|
47
|
+
case 'RenameField':
|
|
48
|
+
prefix = chalk.yellow('~'); label = `Rename field ${op.oldColumn} on ${op.table} to ${op.newColumn}`; break;
|
|
49
|
+
case 'RenameModel':
|
|
50
|
+
prefix = chalk.yellow('~'); label = `Rename model ${op.oldTable} to ${op.newTable}`; break;
|
|
51
|
+
default:
|
|
52
|
+
prefix = chalk.gray(' '); label = op.type;
|
|
53
|
+
}
|
|
54
|
+
console.log(chalk.gray(` ${prefix} ${label}`));
|
|
55
|
+
});
|
|
56
|
+
if (!options.dryRun) {
|
|
57
|
+
console.log(chalk.gray('\n Run: millas migrate to apply.\n'));
|
|
58
|
+
} else {
|
|
59
|
+
console.log();
|
|
60
|
+
}
|
|
30
61
|
}
|
|
31
62
|
} catch (err) {
|
|
32
63
|
bail('makemigrations', err);
|
|
33
64
|
}
|
|
34
|
-
// makemigrations doesn't open a DB connection so no closeDb() needed
|
|
35
65
|
});
|
|
36
66
|
|
|
37
67
|
// ── migrate ─────────────────────────────────────────────────────────────────
|
|
38
68
|
program
|
|
39
69
|
.command('migrate')
|
|
40
|
-
.description('
|
|
41
|
-
.
|
|
70
|
+
.description('Apply pending migrations in dependency order (never generates migrations)')
|
|
71
|
+
.option('--fake <name>', 'Mark a migration as applied without running it')
|
|
72
|
+
.action(async (options) => {
|
|
42
73
|
try {
|
|
43
74
|
const runner = await getRunner();
|
|
75
|
+
|
|
76
|
+
if (options.fake) {
|
|
77
|
+
const [source, name] = options.fake.includes(':')
|
|
78
|
+
? options.fake.split(':')
|
|
79
|
+
: ['app', options.fake];
|
|
80
|
+
const result = await runner.fake(source, name);
|
|
81
|
+
console.log(chalk.green(`\n Marked "${result.key}" as applied (fake).\n`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
44
85
|
const result = await runner.migrate();
|
|
45
|
-
|
|
86
|
+
printResult(result.ran, 'Applying');
|
|
46
87
|
} catch (err) {
|
|
47
88
|
bail('migrate', err);
|
|
48
89
|
} finally {
|
|
@@ -50,18 +91,69 @@ module.exports = function (program) {
|
|
|
50
91
|
}
|
|
51
92
|
});
|
|
52
93
|
|
|
53
|
-
// ── migrate:
|
|
94
|
+
// ── migrate:plan ─────────────────────────────────────────────────────────────
|
|
54
95
|
program
|
|
55
|
-
.command('migrate:
|
|
56
|
-
.description('
|
|
96
|
+
.command('migrate:plan')
|
|
97
|
+
.description('Preview which migrations would run without applying them')
|
|
98
|
+
.action(async () => {
|
|
99
|
+
try {
|
|
100
|
+
const runner = await getRunner();
|
|
101
|
+
const pending = await runner.plan();
|
|
102
|
+
|
|
103
|
+
if (pending.length === 0) {
|
|
104
|
+
console.log(chalk.yellow('\n Nothing to migrate.\n'));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(chalk.cyan('\n Migration plan:\n'));
|
|
109
|
+
pending.forEach(p => {
|
|
110
|
+
const color = p.source === 'system' ? chalk.gray : chalk.cyan;
|
|
111
|
+
console.log(color(` ${p.key}`));
|
|
112
|
+
});
|
|
113
|
+
console.log();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
bail('migrate:plan', err);
|
|
116
|
+
} finally {
|
|
117
|
+
await closeDb();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── migrate:status ───────────────────────────────────────────────────────────
|
|
122
|
+
program
|
|
123
|
+
.command('migrate:status')
|
|
124
|
+
.description('Show the status of all migrations')
|
|
57
125
|
.action(async () => {
|
|
58
126
|
try {
|
|
59
|
-
console.log(chalk.yellow('\n ⚠ Dropping all tables…\n'));
|
|
60
127
|
const runner = await getRunner();
|
|
61
|
-
const
|
|
62
|
-
|
|
128
|
+
const rows = await runner.status();
|
|
129
|
+
|
|
130
|
+
if (rows.length === 0) {
|
|
131
|
+
console.log(chalk.yellow('\n No migrations found.\n'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const colW = Math.max(...rows.map(r => r.key.length)) + 2;
|
|
136
|
+
console.log(`\n ${'Migration'.padEnd(colW)} ${'Status'.padEnd(10)} Batch`);
|
|
137
|
+
console.log(chalk.gray(' ' + '─'.repeat(colW + 20)));
|
|
138
|
+
|
|
139
|
+
let lastSource = null;
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
if (row.source !== lastSource) {
|
|
142
|
+
if (lastSource !== null) console.log();
|
|
143
|
+
lastSource = row.source;
|
|
144
|
+
}
|
|
145
|
+
const status = row.status === 'Applied'
|
|
146
|
+
? chalk.green(row.status.padEnd(10))
|
|
147
|
+
: chalk.yellow(row.status.padEnd(10));
|
|
148
|
+
const batch = row.batch ? chalk.gray(String(row.batch)) : chalk.gray('—');
|
|
149
|
+
const label = row.source === 'system'
|
|
150
|
+
? chalk.gray(row.key.padEnd(colW))
|
|
151
|
+
: chalk.cyan(row.key.padEnd(colW));
|
|
152
|
+
console.log(` ${label} ${status} ${batch}`);
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
63
155
|
} catch (err) {
|
|
64
|
-
bail('migrate:
|
|
156
|
+
bail('migrate:status', err);
|
|
65
157
|
} finally {
|
|
66
158
|
await closeDb();
|
|
67
159
|
}
|
|
@@ -76,7 +168,7 @@ module.exports = function (program) {
|
|
|
76
168
|
try {
|
|
77
169
|
const runner = await getRunner();
|
|
78
170
|
const result = await runner.rollback(Number(options.steps));
|
|
79
|
-
|
|
171
|
+
printResult(result.rolledBack, 'Reverting');
|
|
80
172
|
} catch (err) {
|
|
81
173
|
bail('migrate:rollback', err);
|
|
82
174
|
} finally {
|
|
@@ -84,66 +176,50 @@ module.exports = function (program) {
|
|
|
84
176
|
}
|
|
85
177
|
});
|
|
86
178
|
|
|
87
|
-
// ── migrate:
|
|
179
|
+
// ── migrate:fresh ────────────────────────────────────────────────────────────
|
|
88
180
|
program
|
|
89
|
-
.command('migrate:
|
|
90
|
-
.description('
|
|
181
|
+
.command('migrate:fresh')
|
|
182
|
+
.description('Drop all tables and re-run every migration from scratch')
|
|
91
183
|
.action(async () => {
|
|
92
184
|
try {
|
|
185
|
+
console.log(chalk.yellow('\n ⚠ Dropping all tables…\n'));
|
|
93
186
|
const runner = await getRunner();
|
|
94
|
-
const result = await runner.
|
|
95
|
-
|
|
187
|
+
const result = await runner.fresh();
|
|
188
|
+
printResult(result.ran, 'Applying');
|
|
96
189
|
} catch (err) {
|
|
97
|
-
bail('migrate:
|
|
190
|
+
bail('migrate:fresh', err);
|
|
98
191
|
} finally {
|
|
99
192
|
await closeDb();
|
|
100
193
|
}
|
|
101
194
|
});
|
|
102
195
|
|
|
103
|
-
// ── migrate:
|
|
196
|
+
// ── migrate:reset ────────────────────────────────────────────────────────────
|
|
104
197
|
program
|
|
105
|
-
.command('migrate:
|
|
106
|
-
.description('Rollback
|
|
198
|
+
.command('migrate:reset')
|
|
199
|
+
.description('Rollback ALL migrations')
|
|
107
200
|
.action(async () => {
|
|
108
201
|
try {
|
|
109
202
|
const runner = await getRunner();
|
|
110
|
-
const result = await runner.
|
|
111
|
-
|
|
203
|
+
const result = await runner.reset();
|
|
204
|
+
printResult(result.rolledBack, 'Reverting');
|
|
112
205
|
} catch (err) {
|
|
113
|
-
bail('migrate:
|
|
206
|
+
bail('migrate:reset', err);
|
|
114
207
|
} finally {
|
|
115
208
|
await closeDb();
|
|
116
209
|
}
|
|
117
210
|
});
|
|
118
211
|
|
|
119
|
-
// ── migrate:
|
|
212
|
+
// ── migrate:refresh ──────────────────────────────────────────────────────────
|
|
120
213
|
program
|
|
121
|
-
.command('migrate:
|
|
122
|
-
.description('
|
|
214
|
+
.command('migrate:refresh')
|
|
215
|
+
.description('Rollback all then re-run all migrations')
|
|
123
216
|
.action(async () => {
|
|
124
217
|
try {
|
|
125
218
|
const runner = await getRunner();
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
if (rows.length === 0) {
|
|
129
|
-
console.log(chalk.yellow('\n No migration files found.\n'));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const colW = Math.max(...rows.map(r => r.name.length)) + 2;
|
|
134
|
-
console.log(`\n ${'Migration'.padEnd(colW)} ${'Status'.padEnd(10)} Batch`);
|
|
135
|
-
console.log(chalk.gray(' ' + '─'.repeat(colW + 20)));
|
|
136
|
-
|
|
137
|
-
for (const row of rows) {
|
|
138
|
-
const status = row.status === 'Ran'
|
|
139
|
-
? chalk.green(row.status.padEnd(10))
|
|
140
|
-
: chalk.yellow(row.status.padEnd(10));
|
|
141
|
-
const batch = row.batch ? chalk.gray(String(row.batch)) : chalk.gray('—');
|
|
142
|
-
console.log(` ${chalk.cyan(row.name.padEnd(colW))} ${status} ${batch}`);
|
|
143
|
-
}
|
|
144
|
-
console.log();
|
|
219
|
+
const result = await runner.refresh();
|
|
220
|
+
printResult(result.ran, 'Applying');
|
|
145
221
|
} catch (err) {
|
|
146
|
-
bail('migrate:
|
|
222
|
+
bail('migrate:refresh', err);
|
|
147
223
|
} finally {
|
|
148
224
|
await closeDb();
|
|
149
225
|
}
|
|
@@ -193,10 +269,10 @@ module.exports = function (program) {
|
|
|
193
269
|
function getProjectContext() {
|
|
194
270
|
const cwd = process.cwd();
|
|
195
271
|
return {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
272
|
+
appMigPath: path.join(cwd, 'database/migrations'),
|
|
273
|
+
systemMigPath: path.join(__dirname, '../migrations/system'),
|
|
274
|
+
seedersPath: path.join(cwd, 'database/seeders'),
|
|
275
|
+
modelsPath: path.join(cwd, 'app/models'),
|
|
200
276
|
};
|
|
201
277
|
}
|
|
202
278
|
|
|
@@ -215,31 +291,28 @@ async function getRunner() {
|
|
|
215
291
|
const MigrationRunner = require('../orm/migration/MigrationRunner');
|
|
216
292
|
const ctx = getProjectContext();
|
|
217
293
|
const db = await getDbConnection();
|
|
218
|
-
return new MigrationRunner(db, ctx.
|
|
294
|
+
return new MigrationRunner(db, ctx.appMigPath, ctx.systemMigPath);
|
|
219
295
|
}
|
|
220
296
|
|
|
221
|
-
/**
|
|
222
|
-
* Destroy all open knex connection pools so the CLI process exits cleanly.
|
|
223
|
-
* Without this, knex keeps the event loop alive indefinitely after the
|
|
224
|
-
* command finishes, causing the terminal to appear to hang.
|
|
225
|
-
*/
|
|
226
297
|
async function closeDb() {
|
|
227
298
|
try {
|
|
228
299
|
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
229
300
|
await DatabaseManager.closeAll();
|
|
230
|
-
} catch {
|
|
301
|
+
} catch {}
|
|
231
302
|
}
|
|
232
303
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.log(chalk.yellow(`\n ${result.message}\n`));
|
|
304
|
+
function printResult(list, verb) {
|
|
305
|
+
if (!list || list.length === 0) {
|
|
306
|
+
console.log(chalk.yellow('\n Nothing to do.\n'));
|
|
237
307
|
return;
|
|
238
308
|
}
|
|
239
|
-
console.log(chalk.green(`\n
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
309
|
+
console.log(chalk.green(`\n Running migrations:`));
|
|
310
|
+
for (const entry of list) {
|
|
311
|
+
const label = typeof entry === 'object' ? entry.label || entry.key : entry;
|
|
312
|
+
const source = typeof entry === 'object' ? entry.source : null;
|
|
313
|
+
const color = source === 'system' ? chalk.gray : chalk.cyan;
|
|
314
|
+
console.log(color(` ${verb} ${label}... OK`));
|
|
315
|
+
}
|
|
243
316
|
console.log();
|
|
244
317
|
}
|
|
245
318
|
|
|
@@ -247,4 +320,4 @@ function bail(cmd, err) {
|
|
|
247
320
|
console.error(chalk.red(`\n ✖ ${cmd} failed: ${err.message}\n`));
|
|
248
321
|
if (process.env.DEBUG) console.error(err.stack);
|
|
249
322
|
closeDb().finally(() => process.exit(1));
|
|
250
|
-
}
|
|
323
|
+
}
|
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,75 @@ 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
|
+
DEBUG: process.env.APP_DEBUG,
|
|
89
|
+
},
|
|
90
|
+
stdio: 'inherit',
|
|
99
91
|
});
|
|
100
92
|
|
|
93
|
+
// Application.listen() sends { type:'ready' } via IPC once bound
|
|
94
|
+
this._child.on('message', msg => {
|
|
95
|
+
if (msg && msg.type === 'ready') {
|
|
96
|
+
this._starting = false;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
//
|
|
101
100
|
this._child.on('exit', (code, signal) => {
|
|
102
101
|
this._starting = false;
|
|
103
|
-
|
|
102
|
+
|
|
104
103
|
if (code !== 0 && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
|
|
105
|
-
|
|
106
|
-
'\n' + chalk.red(' ✖ App crashed') +
|
|
104
|
+
console.error(chalk.red('✖ App crashed') +
|
|
107
105
|
chalk.dim(` (exit ${code ?? signal})`) +
|
|
108
|
-
chalk.dim(' —
|
|
106
|
+
chalk.dim(' — fix the error, file watcher will reload…')
|
|
109
107
|
);
|
|
110
108
|
}
|
|
111
109
|
});
|
|
112
|
-
|
|
113
|
-
this._child.on('error',
|
|
110
|
+
//
|
|
111
|
+
this._child.on('error', err => {
|
|
114
112
|
this._starting = false;
|
|
115
|
-
|
|
113
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
116
114
|
});
|
|
117
115
|
}
|
|
118
116
|
|
|
119
117
|
_killChild(cb) {
|
|
120
|
-
if (!this._child || this._child.exitCode !== null)
|
|
121
|
-
cb();
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
118
|
+
if (!this._child || this._child.exitCode !== null) return cb();
|
|
124
119
|
this._child.once('exit', cb);
|
|
125
120
|
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
121
|
}
|
|
134
122
|
|
|
135
123
|
_restart(changedFile) {
|
|
136
|
-
const rel = changedFile ? path.relative(process.cwd(), changedFile) : '';
|
|
137
124
|
const link = changedFile
|
|
138
|
-
? `\x1b]8;;file://${changedFile}\x07${changedFile}\x1b]8;;\x07`
|
|
125
|
+
? `\x1b]8;;file://${changedFile}\x07${path.relative(process.cwd(), changedFile)}\x1b]8;;\x07`
|
|
139
126
|
: '';
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
'\n ' + chalk.yellow('↺') + ' ' +
|
|
127
|
+
console.warn(
|
|
128
|
+
chalk.yellow('↺') + ' ' +
|
|
143
129
|
chalk.white('Reloading') +
|
|
144
|
-
(link ? ' ' +
|
|
145
|
-
'\n'
|
|
130
|
+
(link ? chalk.blueBright(' ' + link) : '')
|
|
146
131
|
);
|
|
147
132
|
|
|
148
133
|
this._restarts++;
|
|
149
|
-
|
|
150
|
-
this._killChild(() => {
|
|
151
|
-
this._spawnChild();
|
|
152
|
-
});
|
|
134
|
+
this._killChild(() => this._spawnChild());
|
|
153
135
|
}
|
|
154
136
|
|
|
155
|
-
// ──
|
|
137
|
+
// ── Watcher ───────────────────────────────────────────────────────────────
|
|
156
138
|
|
|
157
139
|
_watch() {
|
|
140
|
+
console.log(chalk.green('✔') + ' ' +
|
|
141
|
+
chalk.dim('Watching for changes…')
|
|
142
|
+
);
|
|
158
143
|
const cwd = process.cwd();
|
|
159
144
|
const watchPaths = [
|
|
160
145
|
...WATCH_DIRS.map(d => path.join(cwd, d)),
|
|
@@ -166,6 +151,9 @@ class HotReloader {
|
|
|
166
151
|
persistent: true,
|
|
167
152
|
ignoreInitial: true,
|
|
168
153
|
ignored: /(^|[\/\\])\..(?!env)/,
|
|
154
|
+
// Wait for the file write to settle before reloading.
|
|
155
|
+
// Prevents double-restarts on editors that truncate then rewrite.
|
|
156
|
+
awaitWriteFinish: {stabilityThreshold: 80, pollInterval: 20},
|
|
169
157
|
});
|
|
170
158
|
|
|
171
159
|
watcher.on('all', (event, filePath) => {
|
|
@@ -179,9 +167,7 @@ class HotReloader {
|
|
|
179
167
|
|
|
180
168
|
_scheduleRestart(changedFile) {
|
|
181
169
|
clearTimeout(this._timer);
|
|
182
|
-
this._timer = setTimeout(() =>
|
|
183
|
-
this._restart(changedFile);
|
|
184
|
-
}, DEBOUNCE_MS);
|
|
170
|
+
this._timer = setTimeout(() => this._restart(changedFile), DEBOUNCE_MS);
|
|
185
171
|
}
|
|
186
172
|
|
|
187
173
|
_stopWatching() {
|
|
@@ -194,8 +180,7 @@ class HotReloader {
|
|
|
194
180
|
this._watchers = [];
|
|
195
181
|
}
|
|
196
182
|
|
|
197
|
-
|
|
198
|
-
// ── Signal handling ───────────────────────────────────────────────────────
|
|
183
|
+
// ── Signals ───────────────────────────────────────────────────────────────
|
|
199
184
|
|
|
200
185
|
_handleSignals() {
|
|
201
186
|
const cleanup = () => {
|
|
@@ -204,16 +189,14 @@ class HotReloader {
|
|
|
204
189
|
if (this._child) this._child.kill('SIGTERM');
|
|
205
190
|
process.exit(0);
|
|
206
191
|
};
|
|
207
|
-
|
|
208
|
-
process.
|
|
209
|
-
process.on('SIGTERM', cleanup);
|
|
192
|
+
process.once('SIGINT', cleanup);
|
|
193
|
+
process.once('SIGTERM', cleanup);
|
|
210
194
|
}
|
|
211
195
|
}
|
|
212
196
|
|
|
213
|
-
// ── Command
|
|
197
|
+
// ── Command ────────────────────────────────────────────────────────────────────
|
|
214
198
|
|
|
215
199
|
module.exports = function (program) {
|
|
216
|
-
|
|
217
200
|
program
|
|
218
201
|
.command('serve')
|
|
219
202
|
.description('Start the development server with hot reload')
|
|
@@ -221,6 +204,8 @@ module.exports = function (program) {
|
|
|
221
204
|
.option('-h, --host <host>', 'Host to bind to', 'localhost')
|
|
222
205
|
.option('--no-reload', 'Disable hot reload (run once, like production)')
|
|
223
206
|
.action((options) => {
|
|
207
|
+
|
|
208
|
+
const restoreAfterPatch = patchConsole(Logger,"SystemOut")
|
|
224
209
|
const appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
225
210
|
|
|
226
211
|
if (!fs.existsSync(appBootstrap)) {
|
|
@@ -229,51 +214,38 @@ module.exports = function (program) {
|
|
|
229
214
|
process.exit(1);
|
|
230
215
|
}
|
|
231
216
|
|
|
217
|
+
|
|
218
|
+
const publicPort = parseInt(options.port, 10) || 3000;
|
|
219
|
+
const publicHost = options.host || 'localhost';
|
|
220
|
+
|
|
232
221
|
const env = {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
222
|
+
NODE_ENV: process.env.APP_ENV || 'development',
|
|
223
|
+
MILLERS_NODE_ENV: true,
|
|
224
|
+
MILLAS_HOST: publicHost,
|
|
225
|
+
MILLAS_PORT: String(publicPort),
|
|
237
226
|
};
|
|
238
227
|
|
|
239
228
|
Object.assign(process.env, env);
|
|
229
|
+
printBanner(publicHost, publicPort);
|
|
240
230
|
|
|
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();
|
|
231
|
+
if (options.reload !== false) {
|
|
257
232
|
|
|
233
|
+
new HotReloader(appBootstrap, publicPort, publicHost).start();
|
|
258
234
|
} else {
|
|
259
|
-
// ── Single-run mode (--no-reload) ────────────────────────────────────
|
|
260
235
|
try {
|
|
261
236
|
require(appBootstrap);
|
|
262
|
-
} catch (
|
|
263
|
-
|
|
264
|
-
if (process.env.DEBUG) process.stderr.write(chalk.dim(err.stack) + '\n');
|
|
265
|
-
process.exit(1);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.log("Error starting server: ", +e)
|
|
266
239
|
}
|
|
267
240
|
}
|
|
268
241
|
});
|
|
269
242
|
|
|
270
243
|
};
|
|
271
244
|
|
|
272
|
-
// Exported so other commands (queue:work) can validate the project exists
|
|
273
245
|
module.exports.requireProject = function (command) {
|
|
274
246
|
const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
275
247
|
if (!fsnative.existsSync(bootstrapPath)) {
|
|
276
248
|
process.stderr.write(chalk.red(`\n ✖ Not inside a Millas project (${command}).\n\n`));
|
|
277
249
|
process.exit(1);
|
|
278
250
|
}
|
|
279
|
-
};
|
|
251
|
+
};
|