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.
Files changed (116) hide show
  1. package/package.json +3 -16
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +400 -167
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +309 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +383 -97
  12. package/src/admin/static/admin.css +1341 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +65 -1013
  19. package/src/admin/views/pages/detail.njk +40 -16
  20. package/src/admin/views/pages/form.njk +47 -599
  21. package/src/admin/views/pages/list.njk +145 -62
  22. package/src/admin/views/partials/form-field.njk +53 -0
  23. package/src/admin/views/partials/form-footer.njk +28 -0
  24. package/src/admin/views/partials/form-readonly.njk +114 -0
  25. package/src/admin/views/partials/form-scripts.njk +476 -0
  26. package/src/admin/views/partials/form-widget.njk +296 -0
  27. package/src/admin/views/partials/json-dialog.njk +80 -0
  28. package/src/admin/views/partials/json-editor.njk +37 -0
  29. package/src/admin.zip +0 -0
  30. package/src/auth/Auth.js +31 -10
  31. package/src/auth/AuthController.js +3 -1
  32. package/src/auth/AuthUser.js +119 -0
  33. package/src/cli.js +4 -2
  34. package/src/commands/createsuperuser.js +254 -0
  35. package/src/commands/lang.js +589 -0
  36. package/src/commands/migrate.js +154 -81
  37. package/src/commands/serve.js +82 -110
  38. package/src/container/AppInitializer.js +215 -0
  39. package/src/container/Application.js +278 -253
  40. package/src/container/HttpServer.js +156 -0
  41. package/src/container/MillasApp.js +29 -279
  42. package/src/container/MillasConfig.js +192 -0
  43. package/src/core/admin.js +5 -0
  44. package/src/core/auth.js +9 -0
  45. package/src/core/db.js +9 -0
  46. package/src/core/foundation.js +59 -0
  47. package/src/core/http.js +11 -0
  48. package/src/core/lang.js +1 -0
  49. package/src/core/mail.js +6 -0
  50. package/src/core/queue.js +7 -0
  51. package/src/core/validation.js +29 -0
  52. package/src/facades/Admin.js +1 -1
  53. package/src/facades/Auth.js +22 -39
  54. package/src/facades/Cache.js +21 -10
  55. package/src/facades/Database.js +1 -1
  56. package/src/facades/Events.js +18 -17
  57. package/src/facades/Facade.js +197 -0
  58. package/src/facades/Http.js +42 -45
  59. package/src/facades/Log.js +25 -49
  60. package/src/facades/Mail.js +27 -32
  61. package/src/facades/Queue.js +22 -15
  62. package/src/facades/Storage.js +18 -10
  63. package/src/facades/Url.js +53 -0
  64. package/src/http/HttpClient.js +673 -0
  65. package/src/http/ResponseDispatcher.js +18 -111
  66. package/src/http/UrlGenerator.js +375 -0
  67. package/src/http/WelcomePage.js +273 -0
  68. package/src/http/adapters/ExpressAdapter.js +315 -0
  69. package/src/http/adapters/HttpAdapter.js +168 -0
  70. package/src/http/adapters/index.js +9 -0
  71. package/src/i18n/I18nServiceProvider.js +91 -0
  72. package/src/i18n/Translator.js +635 -0
  73. package/src/i18n/defaults.js +122 -0
  74. package/src/i18n/index.js +164 -0
  75. package/src/i18n/locales/en.js +55 -0
  76. package/src/i18n/locales/sw.js +48 -0
  77. package/src/index.js +5 -144
  78. package/src/logger/formatters/PrettyFormatter.js +103 -57
  79. package/src/logger/internal.js +2 -2
  80. package/src/logger/patchConsole.js +91 -81
  81. package/src/middleware/MiddlewareRegistry.js +62 -82
  82. package/src/migrations/system/0001_users.js +21 -0
  83. package/src/migrations/system/0002_admin_log.js +25 -0
  84. package/src/migrations/system/0003_sessions.js +23 -0
  85. package/src/orm/fields/index.js +210 -188
  86. package/src/orm/migration/DefaultValueParser.js +325 -0
  87. package/src/orm/migration/InteractiveResolver.js +191 -0
  88. package/src/orm/migration/Makemigrations.js +312 -0
  89. package/src/orm/migration/MigrationGraph.js +227 -0
  90. package/src/orm/migration/MigrationRunner.js +202 -108
  91. package/src/orm/migration/MigrationWriter.js +463 -0
  92. package/src/orm/migration/ModelInspector.js +412 -344
  93. package/src/orm/migration/ModelScanner.js +225 -0
  94. package/src/orm/migration/ProjectState.js +213 -0
  95. package/src/orm/migration/RenameDetector.js +175 -0
  96. package/src/orm/migration/SchemaBuilder.js +8 -81
  97. package/src/orm/migration/operations/base.js +57 -0
  98. package/src/orm/migration/operations/column.js +191 -0
  99. package/src/orm/migration/operations/fields.js +252 -0
  100. package/src/orm/migration/operations/index.js +55 -0
  101. package/src/orm/migration/operations/models.js +152 -0
  102. package/src/orm/migration/operations/registry.js +131 -0
  103. package/src/orm/migration/operations/special.js +51 -0
  104. package/src/orm/migration/utils.js +208 -0
  105. package/src/orm/model/Model.js +81 -13
  106. package/src/providers/AdminServiceProvider.js +66 -9
  107. package/src/providers/AuthServiceProvider.js +46 -7
  108. package/src/providers/CacheStorageServiceProvider.js +5 -3
  109. package/src/providers/DatabaseServiceProvider.js +3 -2
  110. package/src/providers/EventServiceProvider.js +2 -1
  111. package/src/providers/LogServiceProvider.js +7 -3
  112. package/src/providers/MailServiceProvider.js +4 -3
  113. package/src/providers/QueueServiceProvider.js +4 -3
  114. package/src/router/Router.js +119 -152
  115. package/src/scaffold/templates.js +83 -26
  116. package/src/facades/Validation.js +0 -69
@@ -9,40 +9,81 @@ module.exports = function (program) {
9
9
  // ── makemigrations ──────────────────────────────────────────────────────────
10
10
  program
11
11
  .command('makemigrations')
12
- .description('Scan model files, detect schema changes, generate migration files')
13
- .action(async () => {
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 = getProjectContext();
16
- const ModelInspector = require('../orm/migration/ModelInspector');
17
- const inspector = new ModelInspector(
18
- ctx.modelsPath,
19
- ctx.migrationsPath,
20
- ctx.snapshotPath,
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 ${result.message}\n`));
25
+ console.log(chalk.yellow(`\n No changes detected.\n`));
26
26
  } else {
27
- console.log(chalk.green(`\n ✔ ${result.message}`));
28
- result.files.forEach(f => console.log(chalk.cyan(` + ${f}`)));
29
- console.log(chalk.gray('\n Run: millas migrate to apply these migrations.\n'));
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('Run all pending migrations')
41
- .action(async () => {
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
- printMigrationResult(result, 'Ran');
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:fresh ────────────────────────────────────────────────────────────
94
+ // ── migrate:plan ─────────────────────────────────────────────────────────────
54
95
  program
55
- .command('migrate:fresh')
56
- .description('Drop ALL tables then re-run every migration from scratch')
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 result = await runner.fresh();
62
- printMigrationResult(result, 'Ran');
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:fresh', err);
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
- printMigrationResult(result, 'Rolled back');
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:reset ────────────────────────────────────────────────────────────
179
+ // ── migrate:fresh ────────────────────────────────────────────────────────────
88
180
  program
89
- .command('migrate:reset')
90
- .description('Rollback ALL migrations')
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.reset();
95
- printMigrationResult(result, 'Rolled back');
187
+ const result = await runner.fresh();
188
+ printResult(result.ran, 'Applying');
96
189
  } catch (err) {
97
- bail('migrate:reset', err);
190
+ bail('migrate:fresh', err);
98
191
  } finally {
99
192
  await closeDb();
100
193
  }
101
194
  });
102
195
 
103
- // ── migrate:refresh ──────────────────────────────────────────────────────────
196
+ // ── migrate:reset ────────────────────────────────────────────────────────────
104
197
  program
105
- .command('migrate:refresh')
106
- .description('Rollback all then re-run all migrations')
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.refresh();
111
- printMigrationResult(result, 'Ran');
203
+ const result = await runner.reset();
204
+ printResult(result.rolledBack, 'Reverting');
112
205
  } catch (err) {
113
- bail('migrate:refresh', err);
206
+ bail('migrate:reset', err);
114
207
  } finally {
115
208
  await closeDb();
116
209
  }
117
210
  });
118
211
 
119
- // ── migrate:status ───────────────────────────────────────────────────────────
212
+ // ── migrate:refresh ──────────────────────────────────────────────────────────
120
213
  program
121
- .command('migrate:status')
122
- .description('Show the status of all migration files')
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 rows = await runner.status();
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:status', err);
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
- migrationsPath: path.join(cwd, 'database/migrations'),
197
- seedersPath: path.join(cwd, 'database/seeders'),
198
- modelsPath: path.join(cwd, 'app/models'),
199
- snapshotPath: path.join(cwd, '.millas/schema.json'),
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.migrationsPath);
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 { /* already closed or never opened — safe to ignore */ }
301
+ } catch {}
231
302
  }
232
303
 
233
- function printMigrationResult(result, verb) {
234
- const list = result.ran || result.rolledBack || [];
235
- if (list.length === 0) {
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 ✔ ${result.message}`));
240
- list.forEach(f =>
241
- console.log(chalk.cyan(` ${verb === 'Ran' ? '+' : '-'} ${f}`))
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
+ }
@@ -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
- // ── ASCII banner ──────────────────────────────────────────────────────────────
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.2');
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(ver.padEnd(8)) +
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
- // ── 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
- ];
44
+ // ── Constants ─────────────────────────────────────────────────────────────────
63
45
 
64
- /** File extensions that trigger a reload when changed. */
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
- /** How long to wait after the last change before restarting (ms). */
68
- const DEBOUNCE_MS = 300;
51
+ // ── HotReloader ───────────────────────────────────────────────────────────────
69
52
 
70
53
  class HotReloader {
71
- constructor(bootstrapPath, env) {
54
+ constructor(bootstrapPath, publicPort, publicHost) {
72
55
  this._bootstrap = bootstrapPath;
73
- this._cwd = path.dirname(bootstrapPath.replace(/bootstrap.+$/, '')) ||
74
- process.cwd();
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) return;
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: {...process.env, ...this._env},
97
- stdio: 'inherit', // child shares parent's stdout/stderr — output appears inline
98
- // detached: false — child dies with parent
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
- // Abnormal exit (crash) — don't auto-restart, let the developer fix it
102
+
104
103
  if (code !== 0 && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
105
- process.stdout.write(
106
- '\n' + chalk.red(' ✖ App crashed') +
104
+ console.error(chalk.red('✖ App crashed') +
107
105
  chalk.dim(` (exit ${code ?? signal})`) +
108
- chalk.dim(' — waiting for changes…\n\n')
106
+ chalk.dim(' — fix the error, file watcher will reload…')
109
107
  );
110
108
  }
111
109
  });
112
-
113
- this._child.on('error', (err) => {
110
+ //
111
+ this._child.on('error', err => {
114
112
  this._starting = false;
115
- process.stderr.write(chalk.red(`\n ✖ Child error: ${err.message}\n\n`));
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
- process.stdout.write(
142
- '\n ' + chalk.yellow('↺') + ' ' +
127
+ console.warn(
128
+ chalk.yellow('↺') + ' ' +
143
129
  chalk.white('Reloading') +
144
- (link ? ' ' + chalk.cyan(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
- // ── File watching ─────────────────────────────────────────────────────────
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.on('SIGINT', cleanup);
209
- process.on('SIGTERM', cleanup);
192
+ process.once('SIGINT', cleanup);
193
+ process.once('SIGTERM', cleanup);
210
194
  }
211
195
  }
212
196
 
213
- // ── Command definition ────────────────────────────────────────────────────────
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
- MILLAS_PORT: options.port,
234
- MILLAS_HOST: options.host,
235
- NODE_ENV: process.env.NODE_ENV || 'development',
236
- MILLAS_RELOAD: options.reload ? '1' : '0',
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
- 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();
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 (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);
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
+ };