millas 0.2.28 → 0.2.30

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 (47) hide show
  1. package/bin/millas.js +12 -2
  2. package/package.json +2 -1
  3. package/src/cli.js +117 -20
  4. package/src/commands/call.js +1 -1
  5. package/src/commands/createsuperuser.js +137 -182
  6. package/src/commands/key.js +61 -83
  7. package/src/commands/lang.js +423 -515
  8. package/src/commands/make.js +88 -62
  9. package/src/commands/migrate.js +200 -279
  10. package/src/commands/new.js +55 -50
  11. package/src/commands/route.js +78 -80
  12. package/src/commands/schedule.js +52 -150
  13. package/src/commands/serve.js +158 -191
  14. package/src/console/AppCommand.js +106 -0
  15. package/src/console/BaseCommand.js +726 -0
  16. package/src/console/CommandContext.js +66 -0
  17. package/src/console/CommandRegistry.js +88 -0
  18. package/src/console/Style.js +123 -0
  19. package/src/console/index.js +12 -3
  20. package/src/container/AppInitializer.js +10 -0
  21. package/src/facades/DB.js +195 -0
  22. package/src/index.js +2 -1
  23. package/src/scaffold/maker.js +102 -42
  24. package/src/schematics/Collection.js +28 -0
  25. package/src/schematics/SchematicEngine.js +122 -0
  26. package/src/schematics/Template.js +99 -0
  27. package/src/schematics/index.js +7 -0
  28. package/src/templates/command/default.template.js +14 -0
  29. package/src/templates/command/schema.json +19 -0
  30. package/src/templates/controller/default.template.js +10 -0
  31. package/src/templates/controller/resource.template.js +59 -0
  32. package/src/templates/controller/schema.json +30 -0
  33. package/src/templates/job/default.template.js +11 -0
  34. package/src/templates/job/schema.json +19 -0
  35. package/src/templates/middleware/default.template.js +11 -0
  36. package/src/templates/middleware/schema.json +19 -0
  37. package/src/templates/migration/default.template.js +14 -0
  38. package/src/templates/migration/schema.json +19 -0
  39. package/src/templates/model/default.template.js +14 -0
  40. package/src/templates/model/migration.template.js +17 -0
  41. package/src/templates/model/schema.json +30 -0
  42. package/src/templates/service/default.template.js +12 -0
  43. package/src/templates/service/schema.json +19 -0
  44. package/src/templates/shape/default.template.js +11 -0
  45. package/src/templates/shape/schema.json +19 -0
  46. package/src/validation/BaseValidator.js +3 -0
  47. package/src/validation/types.js +3 -3
@@ -1,219 +1,186 @@
1
1
  'use strict';
2
2
 
3
- const chalk = require('chalk');
4
3
  const path = require('path');
5
4
  const fs = require('fs-extra');
6
- const fsnative = require('fs');
7
- const {fork} = require('child_process');
5
+ const { fork } = require('child_process');
8
6
  const chokidar = require('chokidar');
9
- const patchConsole = require("../logger/patchConsole");
10
- const Logger = require("../logger/internal");
7
+ const BaseCommand = require('../console/BaseCommand');
8
+ const patchConsole = require('../logger/patchConsole');
9
+ const Logger = require('../logger/internal');
11
10
 
12
11
  const WATCH_DIRS = ['app', 'routes', 'config', 'bootstrap', 'providers', 'middleware'];
13
12
  const WATCH_EXTS = new Set(['.js', '.mjs', '.cjs', '.json', '.njk', '.env']);
14
13
  const DEBOUNCE_MS = 250;
15
14
 
16
-
17
- // ── HotReloader ───────────────────────────────────────────────────────────────
18
-
19
15
  class HotReloader {
20
- constructor(bootstrapPath, publicPort, publicHost) {
21
- this._bootstrap = bootstrapPath;
22
- this._initialised = false
16
+ constructor(bootstrapPath) {
17
+ this._bootstrap = bootstrapPath;
18
+ this._initialised = false;
19
+ this._child = null;
20
+ this._starting = false;
21
+ this._restarts = 0;
22
+ this._queue = [];
23
+ this._watchers = [];
24
+ this._timer = null;
25
+ }
26
+
27
+ start() {
28
+ this._spawnChild();
29
+ this._watch();
30
+ this._handleSignals();
31
+ }
32
+
33
+ _spawnChild() {
34
+ if (this._starting) return;
35
+
36
+ this._starting = true;
37
+ const extra = {};
38
+
39
+ if (!this._initialised) {
40
+ extra['MILLAS_START_UP'] = true;
41
+ this._initialised = true;
42
+ }
23
43
 
24
- this._child = null;
44
+ this._child = fork(this._bootstrap, [], {
45
+ env: {
46
+ ...process.env,
47
+ ...extra,
48
+ MILLAS_CHILD: '1',
49
+ },
50
+ stdio: 'inherit',
51
+ });
52
+
53
+ this._child.on('message', msg => {
54
+ if (msg && msg.type === 'ready') {
25
55
  this._starting = false;
26
- this._restarts = 0;
56
+ }
57
+ });
27
58
 
28
- // Queued { req, res, timer } entries while child is restarting
29
- this._queue = [];
30
- this._watchers = [];
31
- this._timer = null;
32
- }
59
+ this._child.on('exit', (code, signal) => {
60
+ this._starting = false;
33
61
 
34
- start() {
35
- this._spawnChild();
36
- this._watch();
37
- this._handleSignals();
62
+ if (code !== 0 && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
63
+ console.error(
64
+ this.style?.danger('✖ App crashed') ||
65
+ `✖ App crashed (exit ${code ?? signal}) — fix the error, file watcher will reload…`
66
+ );
67
+ }
68
+ });
69
+
70
+ this._child.on('error', err => {
71
+ this._starting = false;
72
+ console.error(`✖ ${err.message}`);
73
+ });
74
+ }
75
+
76
+ _killChild(cb) {
77
+ if (!this._child || this._child.exitCode !== null) return cb();
78
+ this._child.once('exit', cb);
79
+ this._child.kill('SIGTERM');
80
+ }
81
+
82
+ _restart(changedFile) {
83
+ console.warn(`↺ Reloading${changedFile ? ' ' + changedFile : ''}`);
84
+ this._restarts++;
85
+ this._killChild(() => this._spawnChild());
86
+ }
87
+
88
+ _watch() {
89
+ console.log('✔ Watching for changes…');
90
+ const cwd = process.cwd();
91
+ const watchPaths = [
92
+ ...WATCH_DIRS.map(d => path.join(cwd, d)),
93
+ path.join(cwd, '.env'),
94
+ path.join(cwd, '.env.local'),
95
+ ];
96
+
97
+ const watcher = chokidar.watch(watchPaths, {
98
+ persistent: true,
99
+ ignoreInitial: true,
100
+ ignored: /(^|[\/\\])\..(?!env)/,
101
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 },
102
+ });
103
+
104
+ watcher.on('all', (event, filePath) => {
105
+ if (WATCH_EXTS.has(path.extname(filePath))) {
106
+ this._scheduleRestart(filePath);
107
+ }
108
+ });
109
+
110
+ this._watchers.push(watcher);
111
+ }
112
+
113
+ _scheduleRestart(changedFile) {
114
+ clearTimeout(this._timer);
115
+ this._timer = setTimeout(() => this._restart(changedFile), DEBOUNCE_MS);
116
+ }
117
+
118
+ _stopWatching() {
119
+ for (const w of this._watchers) {
120
+ try {
121
+ w.close();
122
+ } catch {}
38
123
  }
124
+ this._watchers = [];
125
+ }
126
+
127
+ _handleSignals() {
128
+ const cleanup = () => {
129
+ clearTimeout(this._timer);
130
+ this._stopWatching();
131
+ if (this._child) this._child.kill('SIGTERM');
132
+ process.exit(0);
133
+ };
134
+ process.once('SIGINT', cleanup);
135
+ process.once('SIGTERM', cleanup);
136
+ }
137
+ }
39
138
 
40
- _spawnChild() {
41
- if (this._starting) {
42
- return;
43
- }
44
- this._starting = true;
45
- const extra = {}
46
- if (!this._initialised) {
47
- extra["MILLAS_START_UP"] = true
48
- this._initialised = true
49
- }
139
+ class ServeCommand extends BaseCommand {
140
+ static description = 'Start the development server with hot reload';
50
141
 
51
- this._child = fork(this._bootstrap, [], {
52
- env: {
53
- ...extra,
54
- MILLAS_CHILD: '1',
55
- DEBUG: process.env.APP_DEBUG,
56
- MILLAS_HOST: process.env.MILLAS_HOST,
57
- MILLAS_PORT: process.env.MILLAS_PORT,
58
- },
59
- stdio: 'inherit',
60
- });
61
-
62
- // Application.listen() sends { type:'ready' } via IPC once bound
63
- this._child.on('message', msg => {
64
- if (msg && msg.type === 'ready') {
65
- this._starting = false;
66
- }
67
- });
68
- //
69
- this._child.on('exit', (code, signal) => {
70
- this._starting = false;
71
-
72
- if (code !== 0 && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
73
- console.error(chalk.red('✖ App crashed') +
74
- chalk.dim(` (exit ${code ?? signal})`) +
75
- chalk.dim(' — fix the error, file watcher will reload…')
76
- );
77
- }
78
- });
79
- //
80
- this._child.on('error', err => {
81
- this._starting = false;
82
- console.error(chalk.red(`✖ ${err.message}`));
83
- });
84
- }
142
+ async onInit(register) {
143
+ register
144
+ .command(async (port, host, reload) => {
145
+ const restoreAfterPatch = patchConsole(Logger, 'SystemOut');
146
+ const appBootstrap = path.resolve(this.cwd, 'bootstrap/app.js');
85
147
 
86
- _killChild(cb) {
87
- if (!this._child || this._child.exitCode !== null) return cb();
88
- this._child.once('exit', cb);
89
- this._child.kill('SIGTERM');
90
- }
148
+ if (!fs.existsSync(appBootstrap)) {
149
+ throw new Error('No Millas project found here. Make sure bootstrap/app.js exists.');
150
+ }
91
151
 
92
- _restart(changedFile) {
152
+ const publicPort = parseInt(port || process.env.APP_PORT || 3000, 10);
153
+ const publicHost = host || process.env.APP_HOST || 'localhost';
93
154
 
94
- console.warn(
95
- chalk.yellow('↺') + ' ' +
96
- chalk.white('Reloading') +
97
- (changedFile ? chalk.blueBright(' ' + changedFile) : '')
155
+ const env = Object.fromEntries(
156
+ Object.entries({
157
+ NODE_ENV: process.env.APP_ENV,
158
+ MILLERS_NODE_ENV: true,
159
+ MILLAS_HOST: publicHost,
160
+ MILLAS_PORT: String(publicPort),
161
+ }).filter(([, v]) => v !== undefined)
98
162
  );
99
163
 
100
- this._restarts++;
101
- this._killChild(() => this._spawnChild());
102
- }
103
-
104
- // ── Watcher ───────────────────────────────────────────────────────────────
105
-
106
- _watch() {
107
- console.log(chalk.green('✔') + ' ' +
108
- chalk.dim('Watching for changes…')
109
- );
110
- const cwd = process.cwd();
111
- const watchPaths = [
112
- ...WATCH_DIRS.map(d => path.join(cwd, d)),
113
- path.join(cwd, '.env'),
114
- path.join(cwd, '.env.local'),
115
- ];
116
-
117
- const watcher = chokidar.watch(watchPaths, {
118
- persistent: true,
119
- ignoreInitial: true,
120
- ignored: /(^|[\/\\])\..(?!env)/,
121
- // Wait for the file write to settle before reloading.
122
- // Prevents double-restarts on editors that truncate then rewrite.
123
- awaitWriteFinish: {stabilityThreshold: 80, pollInterval: 20},
124
- });
125
-
126
- watcher.on('all', (event, filePath) => {
127
- if (WATCH_EXTS.has(path.extname(filePath))) {
128
- this._scheduleRestart(filePath);
129
- }
130
- });
131
-
132
- this._watchers.push(watcher);
133
- }
164
+ Object.assign(process.env, env);
134
165
 
135
- _scheduleRestart(changedFile) {
136
- clearTimeout(this._timer);
137
- this._timer = setTimeout(() => this._restart(changedFile), DEBOUNCE_MS);
138
- }
166
+ const enableReload = reload !== false;
139
167
 
140
- _stopWatching() {
141
- for (const w of this._watchers) {
142
- try {
143
- w.close();
144
- } catch {
145
- }
168
+ if (enableReload) {
169
+ new HotReloader(appBootstrap).start();
170
+ } else {
171
+ try {
172
+ await require(appBootstrap);
173
+ } catch (e) {
174
+ throw new Error(`Error starting server: ${e.message}`);
175
+ }
146
176
  }
147
- this._watchers = [];
148
- }
149
-
150
- // ── Signals ───────────────────────────────────────────────────────────────
151
-
152
- _handleSignals() {
153
- const cleanup = () => {
154
- clearTimeout(this._timer);
155
- this._stopWatching();
156
- if (this._child) this._child.kill('SIGTERM');
157
- process.exit(0);
158
- };
159
- process.once('SIGINT', cleanup);
160
- process.once('SIGTERM', cleanup);
161
- }
177
+ })
178
+ .name('serve')
179
+ .num('port', v => v.optional().min(1).max(65535), 'Port to listen on')
180
+ .str('host', v => v.optional(), 'Host to bind to')
181
+ .bool('reload', v => v.default(true), 'Enable hot reload')
182
+ .description('Start the development server with hot reload');
183
+ }
162
184
  }
163
185
 
164
- // ── Command ────────────────────────────────────────────────────────────────────
165
-
166
- module.exports = function (program) {
167
- program
168
- .command('serve')
169
- .description('Start the development server with hot reload')
170
- .option('-p, --port <port>', 'Port to listen on')
171
- .option('-h, --host <host>', 'Host to bind to')
172
- .option('--no-reload', 'Disable hot reload (run once, like production)')
173
- .action((options) => {
174
-
175
- require('dotenv').config({ path: path.resolve(process.cwd(), '.env') });
176
-
177
- const restoreAfterPatch = patchConsole(Logger,"SystemOut")
178
- let appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
179
-
180
- if (!fs.existsSync(appBootstrap)) {
181
- process.stderr.write(chalk.red('\n ✖ No Millas project found here.\n'));
182
- process.stderr.write(chalk.dim(' Make sure bootstrap/app.js exists.\n\n'));
183
- process.exit(1);
184
- }
185
-
186
- const publicPort = parseInt(options.port ||process.env.APP_PORT, 10);
187
- const publicHost = options.host;
188
-
189
- const env = Object.fromEntries(
190
- Object.entries({
191
- NODE_ENV: process.env.APP_ENV,
192
- MILLERS_NODE_ENV: true,
193
- MILLAS_HOST: publicHost,
194
- MILLAS_PORT: String(publicPort),
195
- }).filter(([, v]) => v !== undefined)
196
- );
197
-
198
- Object.assign(process.env, env)
199
-
200
- if (options.reload !== false) {
201
- new HotReloader(appBootstrap, publicPort, publicHost).start();
202
- } else {
203
- try {
204
- require(appBootstrap);
205
- } catch (e) {
206
- console.log("Error starting server: ", +e)
207
- }
208
- }
209
- });
210
-
211
- };
212
-
213
- module.exports.requireProject = function (command) {
214
- const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
215
- if (!fsnative.existsSync(bootstrapPath)) {
216
- process.stderr.write(chalk.red(`\n ✖ Not inside a Millas project (${command}).\n\n`));
217
- process.exit(1);
218
- }
219
- };
186
+ module.exports = ServeCommand;
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+
6
+ /**
7
+ * AppCommand - Base class for commands that need app bootstrapping
8
+ *
9
+ * Provides app context loading for commands that need access to:
10
+ * - Routes
11
+ * - Models
12
+ * - Services
13
+ * - Configuration
14
+ */
15
+ class AppCommand {
16
+
17
+ /**
18
+ *
19
+ * @param context
20
+ */
21
+ constructor(context) {
22
+ this.context = context;
23
+ this.program = context.program;
24
+ this.container = context.container;
25
+ this.logger = context.logger;
26
+ this.cwd = context.cwd;
27
+ this._app = null;
28
+ this._appBootstrapped = false;
29
+ }
30
+
31
+ /**
32
+ * Get the app bootstrap path
33
+ * Override this to customize the bootstrap location
34
+ */
35
+ getAppBootstrapPath() {
36
+ return path.resolve(this.cwd, 'bootstrap/app.js');
37
+ }
38
+
39
+ /**
40
+ * Check if app bootstrap exists
41
+ */
42
+ hasAppBootstrap() {
43
+ return fs.existsSync(this.getAppBootstrapPath());
44
+ }
45
+
46
+ /**
47
+ * Bootstrap the application
48
+ * Override this method to customize bootstrapping behavior
49
+ *
50
+ * @returns {Object} The bootstrapped app
51
+ */
52
+ async #appBoot() {
53
+ if (this._appBootstrapped) {
54
+ return this._app;
55
+ }
56
+
57
+ const bootstrapPath = this.getAppBootstrapPath();
58
+
59
+ if (!fs.existsSync(bootstrapPath)) {
60
+ throw new Error('Not inside a Millas project. bootstrap/app.js not found.');
61
+ }
62
+
63
+ try {
64
+ this._app = await require(bootstrapPath);
65
+ this._appBootstrapped = true;
66
+ return this._app;
67
+ } catch (err) {
68
+ throw new Error(`Failed to bootstrap app: ${err.message}`);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get the bootstrapped app (lazy loads if needed)
74
+ * @returns Application
75
+ */
76
+ async getApp() {
77
+ if (!this._appBootstrapped) {
78
+ await this.#appBoot();
79
+ }
80
+ return this._app;
81
+ }
82
+
83
+ /**
84
+ * Get a specific export from the bootstrapped app
85
+ *
86
+ * @param {string} key - The export key (e.g., 'route', 'app', 'db')
87
+ * @returns {*} The exported value
88
+ */
89
+ async getAppExport(key) {
90
+ const app = await this.getApp();
91
+ return app[key];
92
+ }
93
+
94
+ /**
95
+ * Require app bootstrap (throws if not found)
96
+ * Use this in commands that MUST have an app context
97
+ */
98
+ async requireApp() {
99
+ if (!this.hasAppBootstrap()) {
100
+ throw new Error('This command requires a Millas project. Run inside a project directory.');
101
+ }
102
+ return await this.getApp();
103
+ }
104
+ }
105
+
106
+ module.exports = AppCommand;