aegisnode 0.0.4 → 0.0.5
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/README.md +48 -3
- package/package.json +1 -1
- package/scripts/smoke-test.js +87 -0
- package/src/cli/commands/createapp.js +11 -174
- package/src/cli/commands/doctor.js +74 -20
- package/src/cli/commands/fixapp.js +65 -0
- package/src/cli/index.js +21 -1
- package/src/cli/utils/apps.js +253 -0
- package/src/cli/utils/scaffolds.js +13 -0
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ It keeps the development model simple, but adds enough structure and tooling to
|
|
|
43
43
|
Core features:
|
|
44
44
|
|
|
45
45
|
- CLI generators (`startproject`, `createapp`, `runserver`)
|
|
46
|
+
- App scaffold repair command (`fix`)
|
|
46
47
|
- Startup entry generator (`generateloader`)
|
|
47
48
|
- Project health checker (`doctor`)
|
|
48
49
|
- Dependency updater (`updatedeps`)
|
|
@@ -61,7 +62,7 @@ Core features:
|
|
|
61
62
|
- Root route file `routes.js` (not `routes/` folder)
|
|
62
63
|
- Automatic default confirmation page on `/` when no custom `/` route exists
|
|
63
64
|
- App folder uses `views.js` (not `controllers/` folder)
|
|
64
|
-
- `createapp` uses file modules: `views.js`, `models.js`, `validators.js`, `routes.js`, `subscribers.js`, `services.js`
|
|
65
|
+
- `createapp` uses file modules: `views.js`, `models.js`, `validators.js`, `routes.js`, `subscribers.js`, `services.js`, `utils.js`
|
|
65
66
|
- `createapp` also generates app tests in `apps/<app>/tests`
|
|
66
67
|
- EJS templates configurable in `settings.js` with Django-style base layout flow
|
|
67
68
|
- Built-in runtime helpers (`money`, `number`, `dateTime`, `timeElapsed`, `toObjectId`) + `jlive` bridge
|
|
@@ -90,16 +91,18 @@ npm --prefix blog install
|
|
|
90
91
|
aegisnode runserver --project blog
|
|
91
92
|
|
|
92
93
|
aegisnode createapp users --project blog
|
|
94
|
+
aegisnode fix --app users --project blog
|
|
93
95
|
aegisnode generate view profile --app users --project blog
|
|
94
96
|
aegisnode generate route profile --app users --project blog
|
|
95
97
|
aegisnode generateloader --project blog
|
|
96
98
|
aegisnode doctor --project blog
|
|
99
|
+
aegisnode doctor --app users --project blog
|
|
97
100
|
aegisnode updatedeps --project blog
|
|
98
101
|
```
|
|
99
102
|
|
|
100
103
|
`cd blog` is optional. You can run commands from parent folder with `--project blog`.
|
|
101
104
|
|
|
102
|
-
`createapp`, `generate`, `runserver`, `generateloader`, `doctor`, and `updatedeps` are project-level commands.
|
|
105
|
+
`createapp`, `fix`, `generate`, `runserver`, `generateloader`, `doctor`, and `updatedeps` are project-level commands.
|
|
103
106
|
Run them from the project root; do not `cd` into `apps/<app>`.
|
|
104
107
|
Startup mode rules:
|
|
105
108
|
- Development (`env === development`): start with `aegisnode runserver` only.
|
|
@@ -107,6 +110,20 @@ Startup mode rules:
|
|
|
107
110
|
- `node app.js` and `node loader.cjs` are rejected in development mode.
|
|
108
111
|
- `aegisnode runserver` is rejected outside development mode.
|
|
109
112
|
|
|
113
|
+
### Trust Proxy
|
|
114
|
+
|
|
115
|
+
If your app runs behind Nginx, Apache, Passenger, or another reverse proxy that terminates HTTPS before the Node process, set top-level `trustProxy` in `settings.js`:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
export default {
|
|
119
|
+
trustProxy: 1,
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This is the AegisNode equivalent of `app.set('trust proxy', 1)` in raw Express. It makes `req.secure`, `req.protocol`, client IP detection, secure cookies, and HTTPS-aware auth logic behave correctly behind the proxy.
|
|
124
|
+
|
|
125
|
+
Prefer an exact value such as `1`, `'loopback'`, or a subnet string instead of `true`.
|
|
126
|
+
|
|
110
127
|
### Deploy On Phusion Passenger
|
|
111
128
|
|
|
112
129
|
AegisNode supports Passenger-style startup using the generated `loader.cjs`.
|
|
@@ -170,6 +187,26 @@ aegisnode doctor
|
|
|
170
187
|
- Auth safety checks (JWT secret, OAuth2 `allowHttp` in production)
|
|
171
188
|
- Template directory availability
|
|
172
189
|
|
|
190
|
+
Run app-level scaffold checks for one app:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
aegisnode doctor --app users
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
App-level doctor focuses on the named app:
|
|
197
|
+
- Missing `views.js`, `models.js`, `services.js`, `validators.js`, `routes.js`, `subscribers.js`, `utils.js`
|
|
198
|
+
- Missing generated test files under `apps/<app>/tests`
|
|
199
|
+
- Missing `settings.apps` declaration for that app
|
|
200
|
+
- Missing central `routes.js` import/mount when `autoMountApps` is off
|
|
201
|
+
|
|
202
|
+
Repair a partially missing app scaffold:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
aegisnode fix --app users
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`fix` recreates missing default app files and tests without overwriting existing files. If the app is missing from `settings.apps` or central `routes.js`, it restores those registrations too.
|
|
209
|
+
|
|
173
210
|
Regenerate project startup entry files if needed:
|
|
174
211
|
|
|
175
212
|
```bash
|
|
@@ -288,16 +325,24 @@ Each generated app usually contains:
|
|
|
288
325
|
- `apps/<app>/views.js`
|
|
289
326
|
- `apps/<app>/models.js`
|
|
290
327
|
- `apps/<app>/services.js`
|
|
328
|
+
- `apps/<app>/utils.js`
|
|
291
329
|
- `apps/<app>/subscribers.js`
|
|
292
330
|
- `apps/<app>/routes.js`
|
|
293
331
|
|
|
294
332
|
Usage by file:
|
|
295
333
|
- `views.js`: HTTP handlers (`req`, `res`, `next`). Default signature can be context-first: `handler({ service, validator, services, validators, ... }, req, res, next)`.
|
|
334
|
+
Keep `views.js` thin: prefer only the view class and its imports. Avoid defining extra local helper/utility functions in the view file. Move reusable pure logic to `utils.js` and app workflows to `services.js`.
|
|
296
335
|
- `models.js`: data access layer only (SQL/NoSQL operations).
|
|
297
|
-
- `services.js`: business logic layer; orchestrates models.
|
|
336
|
+
- `services.js`: business logic layer; orchestrates models and uses injected runtime objects when needed.
|
|
337
|
+
- `utils.js`: app-local pure utility functions. Use this for small reusable helpers that belong only to the app. Do not put DB access, request validation, or business workflows here.
|
|
338
|
+
`utils.js` is a plain module, not an injected runtime layer. If a utility needs `jlive`, `helpers`, `i18n`, or another injected runtime object, inject that object into a view/service/model first and pass it into the utility function as an argument.
|
|
298
339
|
- `subscribers.js`: event listeners (for example `app.booted`, `ws.connection`, custom events).
|
|
299
340
|
- `routes.js`: route mapping only (`route.get(...)`, `route.post(...)`, `route.use(...)`) to view handlers.
|
|
300
341
|
|
|
342
|
+
Short rule for `utils.js` vs `services.js`:
|
|
343
|
+
- Use `utils.js` for pure app-local helpers such as string formatting, slug generation, payload shaping, or small mappers.
|
|
344
|
+
- Use `services.js` for application behavior: anything that coordinates models, injected runtime objects, or feature rules.
|
|
345
|
+
|
|
301
346
|
Route modules are mapping-only (`register(route)`).
|
|
302
347
|
Framework context is injected into handlers as first argument (when handler uses 4 args): `{ service, validator, services, models, validators, auth, mail, helpers, i18n, events, ... }`.
|
|
303
348
|
`req.aegis` is also available.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegisnode",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "A view-first Node.js framework for modular web apps and JSON APIs with CLI scaffolding, runtime injection, auth, uploads, i18n, mail, and WebSocket support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/scripts/smoke-test.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from 'path';
|
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
8
|
import { startProject } from '../src/cli/commands/startproject.js';
|
|
9
9
|
import { createApp } from '../src/cli/commands/createapp.js';
|
|
10
|
+
import { runCli } from '../src/cli/index.js';
|
|
10
11
|
import { generateArtifact } from '../src/cli/commands/generate.js';
|
|
11
12
|
import { createKernel } from '../src/runtime/kernel.js';
|
|
12
13
|
import { runServer } from '../src/cli/commands/runserver.js';
|
|
@@ -16,6 +17,7 @@ import { createAuthManager, normalizeAuthConfig } from '../src/runtime/auth.js';
|
|
|
16
17
|
import { loadProjectConfig } from '../src/runtime/config.js';
|
|
17
18
|
import { initializeDatabase, closeDatabase } from '../src/runtime/database.js';
|
|
18
19
|
import { runDoctor } from '../src/cli/commands/doctor.js';
|
|
20
|
+
import { runFixApp } from '../src/cli/commands/fixapp.js';
|
|
19
21
|
import { runUpdateDependencies } from '../src/cli/commands/updatedeps.js';
|
|
20
22
|
import { createHelpers } from '../src/runtime/helpers.js';
|
|
21
23
|
|
|
@@ -590,6 +592,7 @@ dkcqnJD4SGWVeG+KhA==
|
|
|
590
592
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'models.js'));
|
|
591
593
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'validators.js'));
|
|
592
594
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'services.js'));
|
|
595
|
+
await fs.access(path.join(projectRoot, 'apps', 'users', 'utils.js'));
|
|
593
596
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'subscribers.js'));
|
|
594
597
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'models.test.js'));
|
|
595
598
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'validators.test.js'));
|
|
@@ -605,6 +608,90 @@ dkcqnJD4SGWVeG+KhA==
|
|
|
605
608
|
assert.equal(doctorReport.summary.errors, 0);
|
|
606
609
|
const projectRoutesFile = await fs.readFile(path.join(projectRoot, 'routes.js'), 'utf8');
|
|
607
610
|
assert.match(projectRoutesFile, /route\.use\((['"])\/users\1, users\);/);
|
|
611
|
+
|
|
612
|
+
await fs.unlink(path.join(projectRoot, 'apps', 'users', 'utils.js'));
|
|
613
|
+
await fs.unlink(path.join(projectRoot, 'apps', 'users', 'tests', 'routes.test.js'));
|
|
614
|
+
await fs.writeFile(
|
|
615
|
+
path.join(projectRoot, 'routes.js'),
|
|
616
|
+
projectRoutesFile
|
|
617
|
+
.replace(/import users from '\.\/apps\/users\/routes\.js';\n/, '')
|
|
618
|
+
.replace(/\s*route\.use\((['"])\/users\1, users\);\n/, '\n'),
|
|
619
|
+
'utf8',
|
|
620
|
+
);
|
|
621
|
+
const appDoctorRouteReport = await runDoctor({
|
|
622
|
+
projectRoot,
|
|
623
|
+
appName: 'users',
|
|
624
|
+
failOnError: false,
|
|
625
|
+
output: {
|
|
626
|
+
log() {},
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
assert.equal(
|
|
630
|
+
appDoctorRouteReport.entries.some((entry) => entry.message.includes('Project routes.js is missing import for app "users".')),
|
|
631
|
+
true,
|
|
632
|
+
);
|
|
633
|
+
assert.equal(
|
|
634
|
+
appDoctorRouteReport.entries.some((entry) => entry.message.includes('Project routes.js is missing route.use(...) mount for app "users".')),
|
|
635
|
+
true,
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
const settingsBeforeFix = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
|
|
639
|
+
await fs.writeFile(
|
|
640
|
+
path.join(projectRoot, 'settings.js'),
|
|
641
|
+
settingsBeforeFix.replace(/\n\s*\{ name: "users", mount: "\/users" \},/, ''),
|
|
642
|
+
'utf8',
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
const appDoctorReport = await runDoctor({
|
|
646
|
+
projectRoot,
|
|
647
|
+
appName: 'users',
|
|
648
|
+
failOnError: false,
|
|
649
|
+
output: {
|
|
650
|
+
log() {},
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
assert.equal(appDoctorReport.summary.errors, 0);
|
|
654
|
+
assert.equal(
|
|
655
|
+
appDoctorReport.entries.some((entry) => entry.message.includes('App "users" is not declared in settings.apps.')),
|
|
656
|
+
true,
|
|
657
|
+
);
|
|
658
|
+
assert.equal(
|
|
659
|
+
appDoctorReport.entries.some((entry) => entry.message.includes('App "users" missing utils.js.')),
|
|
660
|
+
true,
|
|
661
|
+
);
|
|
662
|
+
assert.equal(
|
|
663
|
+
appDoctorReport.entries.some((entry) => entry.message.includes('App "users" missing tests/routes.test.js.')),
|
|
664
|
+
true,
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const fixAppResult = await runFixApp({
|
|
668
|
+
appName: 'users',
|
|
669
|
+
projectRoot,
|
|
670
|
+
});
|
|
671
|
+
assert.equal(fixAppResult.registryUpdated, true);
|
|
672
|
+
assert.equal(fixAppResult.routesUpdated, true);
|
|
673
|
+
await fs.access(path.join(projectRoot, 'apps', 'users', 'utils.js'));
|
|
674
|
+
await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'routes.test.js'));
|
|
675
|
+
const fixedSettingsFile = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
|
|
676
|
+
assert.match(fixedSettingsFile, /\{ name: "users", mount: "\/users" \},/);
|
|
677
|
+
const fixedRoutesFile = await fs.readFile(path.join(projectRoot, 'routes.js'), 'utf8');
|
|
678
|
+
assert.match(fixedRoutesFile, /import users from '\.\/apps\/users\/routes\.js';/);
|
|
679
|
+
assert.match(fixedRoutesFile, /route\.use\((['"])\/users\1, users\);/);
|
|
680
|
+
await assert.doesNotReject(
|
|
681
|
+
() => runCli(['doctor', '--app', 'users', '--project', projectRoot]),
|
|
682
|
+
);
|
|
683
|
+
await assert.rejects(
|
|
684
|
+
() => runCli(['doctor', 'users', '--project', projectRoot]),
|
|
685
|
+
/Doctor app target must use --app <app-name>/,
|
|
686
|
+
);
|
|
687
|
+
await assert.doesNotReject(
|
|
688
|
+
() => runCli(['fix', '--app', 'users', '--project', projectRoot]),
|
|
689
|
+
);
|
|
690
|
+
await assert.rejects(
|
|
691
|
+
() => runCli(['fix', 'users', '--project', projectRoot]),
|
|
692
|
+
/Fix app target must use --app <app-name>/,
|
|
693
|
+
);
|
|
694
|
+
|
|
608
695
|
await generateArtifact({
|
|
609
696
|
type: 'view',
|
|
610
697
|
name: 'profile',
|
|
@@ -1,191 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { pathToFileURL } from 'url';
|
|
4
|
-
import { ensureDir, ensureValidName, exists, normalizeMountPath, writeFile } from '../utils/fs.js';
|
|
1
|
+
import { ensureValidName, normalizeMountPath } from '../utils/fs.js';
|
|
5
2
|
import { resolveProjectRoot } from '../utils/project.js';
|
|
6
3
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
renderAppModelTest,
|
|
14
|
-
renderAppValidatorTest,
|
|
15
|
-
renderAppServiceTest,
|
|
16
|
-
renderAppRoutesTest,
|
|
17
|
-
renderSettingsApps,
|
|
18
|
-
} from '../utils/scaffolds.js';
|
|
19
|
-
|
|
20
|
-
const APPS_START = '// AEGIS_APPS_START';
|
|
21
|
-
const APPS_END = '// AEGIS_APPS_END';
|
|
22
|
-
|
|
23
|
-
function renderAppEntries(apps) {
|
|
24
|
-
return apps
|
|
25
|
-
.map((app) => ` { name: ${JSON.stringify(app.name)}, mount: ${JSON.stringify(app.mount)} },`)
|
|
26
|
-
.join('\n');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function toImportName(appName) {
|
|
30
|
-
const safe = appName
|
|
31
|
-
.split(/[-_\s]+/)
|
|
32
|
-
.filter(Boolean)
|
|
33
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
34
|
-
.join('');
|
|
35
|
-
|
|
36
|
-
if (!safe) {
|
|
37
|
-
return 'appRoutes';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return `${safe.charAt(0).toLowerCase()}${safe.slice(1)}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function readDefaultExport(filePath) {
|
|
44
|
-
const moduleUrl = `${pathToFileURL(filePath).href}?t=${Date.now()}`;
|
|
45
|
-
const loaded = await import(moduleUrl);
|
|
46
|
-
return loaded?.default;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function detectSettingsMode(projectRoot) {
|
|
50
|
-
const single = path.join(projectRoot, 'settings.js');
|
|
51
|
-
const split = path.join(projectRoot, 'settings', 'apps.js');
|
|
52
|
-
|
|
53
|
-
if (await exists(single)) {
|
|
54
|
-
return { mode: 'single', file: single };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (await exists(split)) {
|
|
58
|
-
return { mode: 'split', file: split };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
throw new Error(`Not an AegisNode project root: missing ${single} (or legacy ${split})`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function readAppsConfig(settingsMode) {
|
|
65
|
-
const normalizeApp = (entry) => {
|
|
66
|
-
if (!entry || typeof entry !== 'object' || typeof entry.name !== 'string') {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
ensureValidName(entry.name, 'app');
|
|
71
|
-
return {
|
|
72
|
-
name: entry.name,
|
|
73
|
-
mount: normalizeMountPath(entry.mount || `/${entry.name}`),
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
if (settingsMode.mode === 'single') {
|
|
78
|
-
const settings = await readDefaultExport(settingsMode.file);
|
|
79
|
-
const apps = settings?.apps;
|
|
80
|
-
|
|
81
|
-
if (!Array.isArray(apps)) {
|
|
82
|
-
throw new Error(`settings.js must export { apps: [] }. File: ${settingsMode.file}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return apps.map(normalizeApp).filter(Boolean);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const apps = await readDefaultExport(settingsMode.file);
|
|
89
|
-
if (!Array.isArray(apps)) {
|
|
90
|
-
throw new Error(`settings/apps.js must export an array. File: ${settingsMode.file}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return apps.map(normalizeApp).filter(Boolean);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function updateSingleSettingsApps(settingsFile, apps) {
|
|
97
|
-
const current = await fs.readFile(settingsFile, 'utf8');
|
|
98
|
-
|
|
99
|
-
if (!current.includes(APPS_START) || !current.includes(APPS_END)) {
|
|
100
|
-
throw new Error(`settings.js is missing ${APPS_START}/${APPS_END} markers: ${settingsFile}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const replacement = `${APPS_START}\n${renderAppEntries(apps)}\n ${APPS_END}`;
|
|
104
|
-
const updated = current.replace(/\/\/ AEGIS_APPS_START[\s\S]*?\/\/ AEGIS_APPS_END/m, replacement);
|
|
105
|
-
|
|
106
|
-
await writeFile(settingsFile, updated);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function updateAppRegistry(projectRoot, apps, settingsMode) {
|
|
110
|
-
if (settingsMode.mode === 'single') {
|
|
111
|
-
await updateSingleSettingsApps(settingsMode.file, apps);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
await writeFile(path.join(projectRoot, 'settings', 'apps.js'), renderSettingsApps(apps));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function updateProjectRoutesFile(projectRoot, appName, mountPath) {
|
|
119
|
-
const routesFile = path.join(projectRoot, 'routes.js');
|
|
120
|
-
if (!(await exists(routesFile))) {
|
|
121
|
-
// Keep backward compatibility for legacy projects that still use routes/index.js.
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const importName = toImportName(appName);
|
|
126
|
-
const importLine = `import ${importName} from './apps/${appName}/routes.js';`;
|
|
127
|
-
const routeLine = ` route.use(${JSON.stringify(mountPath)}, ${importName});`;
|
|
128
|
-
|
|
129
|
-
let content = await fs.readFile(routesFile, 'utf8');
|
|
130
|
-
|
|
131
|
-
if (!content.includes(importLine)) {
|
|
132
|
-
if (content.includes('// AEGIS_APP_IMPORTS_START') && content.includes('// AEGIS_APP_IMPORTS_END')) {
|
|
133
|
-
content = content.replace('// AEGIS_APP_IMPORTS_END', `${importLine}\n// AEGIS_APP_IMPORTS_END`);
|
|
134
|
-
} else {
|
|
135
|
-
content = `${importLine}\n${content}`;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (!content.includes(routeLine)) {
|
|
140
|
-
if (content.includes('// AEGIS_PROJECT_APP_ROUTES_START') && content.includes('// AEGIS_PROJECT_APP_ROUTES_END')) {
|
|
141
|
-
content = content.replace(' // AEGIS_PROJECT_APP_ROUTES_END', `${routeLine}\n // AEGIS_PROJECT_APP_ROUTES_END`);
|
|
142
|
-
} else {
|
|
143
|
-
const match = content.match(/register\s*\([^)]*\)\s*{[\s\S]*?\n\s*}/m);
|
|
144
|
-
if (match) {
|
|
145
|
-
content = content.replace(match[0], `${match[0]}\n${routeLine}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
await writeFile(routesFile, content);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function createScaffold(projectRoot, appName) {
|
|
154
|
-
const appRoot = path.join(projectRoot, 'apps', appName);
|
|
155
|
-
|
|
156
|
-
await ensureDir(appRoot);
|
|
157
|
-
await ensureDir(path.join(appRoot, 'tests'));
|
|
158
|
-
|
|
159
|
-
await writeFile(path.join(appRoot, 'views.js'), renderAppViewsFile(appName));
|
|
160
|
-
await writeFile(path.join(appRoot, 'models.js'), renderAppModelsFile(appName));
|
|
161
|
-
await writeFile(path.join(appRoot, 'validators.js'), renderAppValidatorsFile(appName));
|
|
162
|
-
await writeFile(path.join(appRoot, 'services.js'), renderAppServicesFile(appName));
|
|
163
|
-
await writeFile(path.join(appRoot, 'subscribers.js'), renderAppSubscribersFile(appName));
|
|
164
|
-
await writeFile(path.join(appRoot, 'routes.js'), renderAppRoutes(appName));
|
|
165
|
-
await writeFile(path.join(appRoot, 'tests', 'models.test.js'), renderAppModelTest(appName));
|
|
166
|
-
await writeFile(path.join(appRoot, 'tests', 'validators.test.js'), renderAppValidatorTest(appName));
|
|
167
|
-
await writeFile(path.join(appRoot, 'tests', 'services.test.js'), renderAppServiceTest(appName));
|
|
168
|
-
await writeFile(path.join(appRoot, 'tests', 'routes.test.js'), renderAppRoutesTest(appName));
|
|
169
|
-
}
|
|
4
|
+
detectSettingsMode,
|
|
5
|
+
ensureAppScaffold,
|
|
6
|
+
readAppsConfig,
|
|
7
|
+
updateAppRegistry,
|
|
8
|
+
updateProjectRoutesFile,
|
|
9
|
+
} from '../utils/apps.js';
|
|
170
10
|
|
|
171
11
|
export async function createApp({ appName, projectRoot, mount }) {
|
|
172
12
|
ensureValidName(appName, 'app');
|
|
173
13
|
|
|
174
14
|
const resolvedRoot = await resolveProjectRoot(projectRoot);
|
|
175
|
-
|
|
176
15
|
const settingsMode = await detectSettingsMode(resolvedRoot);
|
|
177
16
|
const normalizedMount = normalizeMountPath(mount || `/${appName}`);
|
|
178
17
|
const existingApps = await readAppsConfig(settingsMode);
|
|
179
18
|
|
|
180
19
|
if (existingApps.some((entry) => entry.name === appName)) {
|
|
181
|
-
throw new Error(`App
|
|
20
|
+
throw new Error(`App "${appName}" already exists in project settings`);
|
|
182
21
|
}
|
|
183
22
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
await createScaffold(resolvedRoot, appName);
|
|
187
|
-
await updateAppRegistry(resolvedRoot, updatedApps, settingsMode);
|
|
23
|
+
await ensureAppScaffold(resolvedRoot, appName);
|
|
24
|
+
await updateAppRegistry(resolvedRoot, [...existingApps, { name: appName, mount: normalizedMount }], settingsMode);
|
|
188
25
|
await updateProjectRoutesFile(resolvedRoot, appName, normalizedMount);
|
|
189
26
|
|
|
190
|
-
console.log(`App
|
|
27
|
+
console.log(`App "${appName}" created at ${resolvedRoot}/apps/${appName}`);
|
|
191
28
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { loadProjectConfig } from '../../runtime/config.js';
|
|
4
|
+
import { ensureValidName } from '../utils/fs.js';
|
|
4
5
|
import { resolveProjectRoot } from '../utils/project.js';
|
|
6
|
+
import { getAppScaffoldEntries, toImportName } from '../utils/apps.js';
|
|
5
7
|
|
|
6
8
|
function createCollector() {
|
|
7
9
|
const entries = [];
|
|
@@ -26,25 +28,47 @@ async function fileExists(filePath) {
|
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
function escapeRegExp(value) {
|
|
32
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runAppChecks(rootDir, config, collector, targetAppName = null) {
|
|
30
36
|
const apps = Array.isArray(config.apps) ? config.apps : [];
|
|
37
|
+
const declaredApps = new Map();
|
|
38
|
+
|
|
39
|
+
for (const app of apps) {
|
|
40
|
+
const appName = app?.name;
|
|
41
|
+
if (typeof appName === 'string' && appName.trim().length > 0) {
|
|
42
|
+
declaredApps.set(appName, app);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
31
45
|
|
|
32
|
-
if (apps.length === 0) {
|
|
46
|
+
if (!targetAppName && apps.length === 0) {
|
|
33
47
|
collector.warn('No apps declared in settings.apps.');
|
|
34
48
|
return;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
if (!targetAppName) {
|
|
52
|
+
collector.ok(`Declared apps: ${apps.map((app) => app.name).join(', ')}`);
|
|
53
|
+
}
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
const routesFile = path.join(rootDir, 'routes.js');
|
|
56
|
+
const routesFileExists = await fileExists(routesFile);
|
|
57
|
+
const routesContent = routesFileExists ? await fs.readFile(routesFile, 'utf8') : '';
|
|
58
|
+
const targetApps = targetAppName ? [targetAppName] : apps;
|
|
59
|
+
|
|
60
|
+
for (const appEntry of targetApps) {
|
|
61
|
+
const appName = targetAppName || appEntry?.name;
|
|
62
|
+
const app = targetAppName ? declaredApps.get(appName) || null : appEntry;
|
|
41
63
|
const mount = app?.mount;
|
|
42
64
|
if (typeof appName !== 'string' || appName.trim().length === 0) {
|
|
43
|
-
collector.error(`Invalid app entry: ${JSON.stringify(
|
|
65
|
+
collector.error(`Invalid app entry: ${JSON.stringify(appEntry)}`);
|
|
44
66
|
continue;
|
|
45
67
|
}
|
|
46
68
|
|
|
47
|
-
if (
|
|
69
|
+
if (!app) {
|
|
70
|
+
collector.warn(`App "${appName}" is not declared in settings.apps.`);
|
|
71
|
+
} else if (typeof mount !== 'string' || !mount.startsWith('/')) {
|
|
48
72
|
collector.error(`App "${appName}" has invalid mount "${String(mount)}" (must start with /).`);
|
|
49
73
|
}
|
|
50
74
|
|
|
@@ -55,17 +79,37 @@ async function runAppChecks(rootDir, config, collector) {
|
|
|
55
79
|
continue;
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const target = path.join(appRoot, fileName);
|
|
82
|
+
for (const entry of getAppScaffoldEntries(appName)) {
|
|
83
|
+
const target = path.join(rootDir, entry.target);
|
|
61
84
|
if (!(await fileExists(target))) {
|
|
62
|
-
collector.warn(`App "${appName}" missing ${
|
|
85
|
+
collector.warn(`App "${appName}" missing ${path.relative(appRoot, target)}.`);
|
|
63
86
|
}
|
|
64
87
|
}
|
|
65
88
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
if (!app) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (config.autoMountApps === true) {
|
|
94
|
+
collector.ok(`App "${appName}" will be mounted automatically from settings.apps.`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!routesFileExists) {
|
|
99
|
+
collector.warn(`Project routes.js is missing; app "${appName}" cannot be mounted centrally.`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const importPath = `./apps/${appName}/routes.js`;
|
|
104
|
+
const importName = toImportName(appName);
|
|
105
|
+
const routePattern = new RegExp(`route\\.use\\([^\\n]*,\\s*${escapeRegExp(importName)}\\s*\\);`);
|
|
106
|
+
|
|
107
|
+
if (!routesContent.includes(importPath)) {
|
|
108
|
+
collector.warn(`Project routes.js is missing import for app "${appName}".`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!routePattern.test(routesContent)) {
|
|
112
|
+
collector.warn(`Project routes.js is missing route.use(...) mount for app "${appName}".`);
|
|
69
113
|
}
|
|
70
114
|
}
|
|
71
115
|
}
|
|
@@ -193,7 +237,12 @@ export async function runDoctor({
|
|
|
193
237
|
projectRoot,
|
|
194
238
|
failOnError = true,
|
|
195
239
|
output = console,
|
|
240
|
+
appName = null,
|
|
196
241
|
} = {}) {
|
|
242
|
+
if (appName) {
|
|
243
|
+
ensureValidName(appName, 'app');
|
|
244
|
+
}
|
|
245
|
+
|
|
197
246
|
const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
|
|
198
247
|
const collector = createCollector();
|
|
199
248
|
|
|
@@ -202,12 +251,17 @@ export async function runDoctor({
|
|
|
202
251
|
const config = await loadProjectConfig(resolvedRoot);
|
|
203
252
|
collector.ok(`Environment: ${config.env || 'development'}`);
|
|
204
253
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
254
|
+
if (appName) {
|
|
255
|
+
collector.ok(`Doctor scope: app "${appName}"`);
|
|
256
|
+
await runAppChecks(resolvedRoot, config, collector, appName);
|
|
257
|
+
} else {
|
|
258
|
+
await runAppChecks(resolvedRoot, config, collector);
|
|
259
|
+
await runStartupEntryChecks(resolvedRoot, config, collector);
|
|
260
|
+
runSecurityChecks(config, collector);
|
|
261
|
+
runAuthChecks(config, collector);
|
|
262
|
+
runApiChecks(config, collector);
|
|
263
|
+
await runTemplateChecks(resolvedRoot, config, collector);
|
|
264
|
+
}
|
|
211
265
|
|
|
212
266
|
const summary = printSummary(collector.entries, output);
|
|
213
267
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { ensureValidName, normalizeMountPath } from '../utils/fs.js';
|
|
3
|
+
import { resolveProjectRoot } from '../utils/project.js';
|
|
4
|
+
import {
|
|
5
|
+
detectSettingsMode,
|
|
6
|
+
ensureAppScaffold,
|
|
7
|
+
readAppsConfig,
|
|
8
|
+
updateAppRegistry,
|
|
9
|
+
updateProjectRoutesFile,
|
|
10
|
+
} from '../utils/apps.js';
|
|
11
|
+
|
|
12
|
+
export async function runFixApp({ appName, projectRoot, mount } = {}) {
|
|
13
|
+
if (!appName) {
|
|
14
|
+
throw new Error('Missing app name. Usage: aegisnode fix --app <app-name>');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
ensureValidName(appName, 'app');
|
|
18
|
+
|
|
19
|
+
const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
|
|
20
|
+
const settingsMode = await detectSettingsMode(resolvedRoot);
|
|
21
|
+
const existingApps = await readAppsConfig(settingsMode);
|
|
22
|
+
const existingApp = existingApps.find((entry) => entry.name === appName) || null;
|
|
23
|
+
const appMount = existingApp?.mount || normalizeMountPath(mount || `/${appName}`);
|
|
24
|
+
|
|
25
|
+
const scaffoldResult = await ensureAppScaffold(resolvedRoot, appName, { overwrite: false });
|
|
26
|
+
|
|
27
|
+
let registryUpdated = false;
|
|
28
|
+
if (!existingApp) {
|
|
29
|
+
await updateAppRegistry(
|
|
30
|
+
resolvedRoot,
|
|
31
|
+
[...existingApps, { name: appName, mount: appMount }],
|
|
32
|
+
settingsMode,
|
|
33
|
+
);
|
|
34
|
+
registryUpdated = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const routesResult = await updateProjectRoutesFile(resolvedRoot, appName, appMount);
|
|
38
|
+
const relativeWritten = scaffoldResult.written.map((target) => path.relative(resolvedRoot, target));
|
|
39
|
+
|
|
40
|
+
if (relativeWritten.length === 0 && !registryUpdated && !routesResult.updatedImport && !routesResult.updatedRoute) {
|
|
41
|
+
console.log(`App "${appName}" is already complete.`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`App "${appName}" repaired at ${resolvedRoot}/apps/${appName}`);
|
|
44
|
+
if (relativeWritten.length > 0) {
|
|
45
|
+
console.log(`Created missing files: ${relativeWritten.join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
if (registryUpdated) {
|
|
48
|
+
console.log(`Added app "${appName}" to settings.apps with mount ${appMount}`);
|
|
49
|
+
}
|
|
50
|
+
if (routesResult.updatedImport || routesResult.updatedRoute) {
|
|
51
|
+
console.log(`Updated routes.js registration for app "${appName}"`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
rootDir: resolvedRoot,
|
|
57
|
+
appName,
|
|
58
|
+
mount: appMount,
|
|
59
|
+
createdFiles: scaffoldResult.written,
|
|
60
|
+
skippedFiles: scaffoldResult.skipped,
|
|
61
|
+
registryUpdated,
|
|
62
|
+
routesUpdated: routesResult.updatedImport || routesResult.updatedRoute,
|
|
63
|
+
routesFile: routesResult.routesFile,
|
|
64
|
+
};
|
|
65
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { generateArtifact } from './commands/generate.js';
|
|
|
5
5
|
import { runDoctor } from './commands/doctor.js';
|
|
6
6
|
import { runUpdateDependencies } from './commands/updatedeps.js';
|
|
7
7
|
import { runGenerateLoader } from './commands/generateloader.js';
|
|
8
|
+
import { runFixApp } from './commands/fixapp.js';
|
|
8
9
|
|
|
9
10
|
function printHelp() {
|
|
10
11
|
console.log(`AegisNode CLI
|
|
@@ -12,10 +13,11 @@ function printHelp() {
|
|
|
12
13
|
Usage:
|
|
13
14
|
aegisnode startproject <project-name>
|
|
14
15
|
aegisnode createapp <app-name> [--project <path>] [--mount </path>]
|
|
16
|
+
aegisnode fix [--app <app-name>] [--project <path>]
|
|
15
17
|
aegisnode generate <type> <name> --app <app-name> [--project <path>]
|
|
16
18
|
aegisnode runserver [--project <path>] [--port <number>]
|
|
17
19
|
aegisnode generateloader [--project <path>]
|
|
18
|
-
aegisnode doctor [--project <path>]
|
|
20
|
+
aegisnode doctor [--app <app-name>] [--project <path>]
|
|
19
21
|
aegisnode updatedeps [--project <path>]
|
|
20
22
|
|
|
21
23
|
Examples:
|
|
@@ -24,9 +26,11 @@ Examples:
|
|
|
24
26
|
npm install
|
|
25
27
|
aegisnode runserver
|
|
26
28
|
aegisnode createapp users
|
|
29
|
+
aegisnode fix --app users
|
|
27
30
|
aegisnode generate view user --app users
|
|
28
31
|
aegisnode generate validator user --app users
|
|
29
32
|
aegisnode generateloader --project blog
|
|
33
|
+
aegisnode doctor --app users --project blog
|
|
30
34
|
aegisnode updatedeps --project blog
|
|
31
35
|
`);
|
|
32
36
|
}
|
|
@@ -123,8 +127,24 @@ export async function runCli(argv) {
|
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
case 'doctor': {
|
|
130
|
+
if (positional.length > 0) {
|
|
131
|
+
throw new Error('Doctor app target must use --app <app-name>. Usage: aegisnode doctor [--app <app-name>] [--project <path>]');
|
|
132
|
+
}
|
|
126
133
|
await runDoctor({
|
|
127
134
|
projectRoot: flags.project ? String(flags.project) : process.cwd(),
|
|
135
|
+
appName: flags.app ? String(flags.app) : null,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case 'fix':
|
|
141
|
+
case 'fixapp': {
|
|
142
|
+
if (positional.length > 0) {
|
|
143
|
+
throw new Error('Fix app target must use --app <app-name>. Usage: aegisnode fix [--app <app-name>] [--project <path>]');
|
|
144
|
+
}
|
|
145
|
+
await runFixApp({
|
|
146
|
+
appName: flags.app ? String(flags.app) : undefined,
|
|
147
|
+
projectRoot: flags.project ? String(flags.project) : process.cwd(),
|
|
128
148
|
});
|
|
129
149
|
return;
|
|
130
150
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { ensureDir, ensureValidName, exists, normalizeMountPath, writeFile } from './fs.js';
|
|
5
|
+
import {
|
|
6
|
+
renderAppModelTest,
|
|
7
|
+
renderAppModelsFile,
|
|
8
|
+
renderAppRoutes,
|
|
9
|
+
renderAppRoutesTest,
|
|
10
|
+
renderAppServiceTest,
|
|
11
|
+
renderAppServicesFile,
|
|
12
|
+
renderAppSubscribersFile,
|
|
13
|
+
renderAppUtilsFile,
|
|
14
|
+
renderAppValidatorTest,
|
|
15
|
+
renderAppValidatorsFile,
|
|
16
|
+
renderAppViewsFile,
|
|
17
|
+
renderSettingsApps,
|
|
18
|
+
} from './scaffolds.js';
|
|
19
|
+
|
|
20
|
+
export const APPS_START = '// AEGIS_APPS_START';
|
|
21
|
+
export const APPS_END = '// AEGIS_APPS_END';
|
|
22
|
+
|
|
23
|
+
function renderAppEntries(apps) {
|
|
24
|
+
return apps
|
|
25
|
+
.map((app) => ` { name: ${JSON.stringify(app.name)}, mount: ${JSON.stringify(app.mount)} },`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function escapeRegExp(value) {
|
|
30
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function toImportName(appName) {
|
|
34
|
+
const safe = appName
|
|
35
|
+
.split(/[-_\s]+/)
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
38
|
+
.join('');
|
|
39
|
+
|
|
40
|
+
if (!safe) {
|
|
41
|
+
return 'appRoutes';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return `${safe.charAt(0).toLowerCase()}${safe.slice(1)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readDefaultExport(filePath) {
|
|
48
|
+
const moduleUrl = `${pathToFileURL(filePath).href}?t=${Date.now()}`;
|
|
49
|
+
const loaded = await import(moduleUrl);
|
|
50
|
+
return loaded?.default;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function detectSettingsMode(projectRoot) {
|
|
54
|
+
const single = path.join(projectRoot, 'settings.js');
|
|
55
|
+
const split = path.join(projectRoot, 'settings', 'apps.js');
|
|
56
|
+
|
|
57
|
+
if (await exists(single)) {
|
|
58
|
+
return { mode: 'single', file: single };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (await exists(split)) {
|
|
62
|
+
return { mode: 'split', file: split };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new Error(`Not an AegisNode project root: missing ${single} (or legacy ${split})`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function readAppsConfig(settingsMode) {
|
|
69
|
+
const normalizeApp = (entry) => {
|
|
70
|
+
if (!entry || typeof entry !== 'object' || typeof entry.name !== 'string') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ensureValidName(entry.name, 'app');
|
|
75
|
+
return {
|
|
76
|
+
name: entry.name,
|
|
77
|
+
mount: normalizeMountPath(entry.mount || `/${entry.name}`),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (settingsMode.mode === 'single') {
|
|
82
|
+
const settings = await readDefaultExport(settingsMode.file);
|
|
83
|
+
const apps = settings?.apps;
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(apps)) {
|
|
86
|
+
throw new Error(`settings.js must export { apps: [] }. File: ${settingsMode.file}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return apps.map(normalizeApp).filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const apps = await readDefaultExport(settingsMode.file);
|
|
93
|
+
if (!Array.isArray(apps)) {
|
|
94
|
+
throw new Error(`settings/apps.js must export an array. File: ${settingsMode.file}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return apps.map(normalizeApp).filter(Boolean);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function updateSingleSettingsApps(settingsFile, apps) {
|
|
101
|
+
const current = await fs.readFile(settingsFile, 'utf8');
|
|
102
|
+
|
|
103
|
+
if (!current.includes(APPS_START) || !current.includes(APPS_END)) {
|
|
104
|
+
throw new Error(`settings.js is missing ${APPS_START}/${APPS_END} markers: ${settingsFile}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const replacement = `${APPS_START}\n${renderAppEntries(apps)}\n ${APPS_END}`;
|
|
108
|
+
const updated = current.replace(/\/\/ AEGIS_APPS_START[\s\S]*?\/\/ AEGIS_APPS_END/m, replacement);
|
|
109
|
+
|
|
110
|
+
await writeFile(settingsFile, updated);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function updateAppRegistry(projectRoot, apps, settingsMode) {
|
|
114
|
+
if (settingsMode.mode === 'single') {
|
|
115
|
+
await updateSingleSettingsApps(settingsMode.file, apps);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await writeFile(path.join(projectRoot, 'settings', 'apps.js'), renderSettingsApps(apps));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getAppScaffoldEntries(appName) {
|
|
123
|
+
const appRoot = path.join('apps', appName);
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
target: path.join(appRoot, 'views.js'),
|
|
127
|
+
content: renderAppViewsFile(appName),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
target: path.join(appRoot, 'models.js'),
|
|
131
|
+
content: renderAppModelsFile(appName),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
target: path.join(appRoot, 'validators.js'),
|
|
135
|
+
content: renderAppValidatorsFile(appName),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
target: path.join(appRoot, 'services.js'),
|
|
139
|
+
content: renderAppServicesFile(appName),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
target: path.join(appRoot, 'utils.js'),
|
|
143
|
+
content: renderAppUtilsFile(),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
target: path.join(appRoot, 'subscribers.js'),
|
|
147
|
+
content: renderAppSubscribersFile(appName),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
target: path.join(appRoot, 'routes.js'),
|
|
151
|
+
content: renderAppRoutes(appName),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
target: path.join(appRoot, 'tests', 'models.test.js'),
|
|
155
|
+
content: renderAppModelTest(appName),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
target: path.join(appRoot, 'tests', 'validators.test.js'),
|
|
159
|
+
content: renderAppValidatorTest(appName),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
target: path.join(appRoot, 'tests', 'services.test.js'),
|
|
163
|
+
content: renderAppServiceTest(appName),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
target: path.join(appRoot, 'tests', 'routes.test.js'),
|
|
167
|
+
content: renderAppRoutesTest(appName),
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function ensureAppScaffold(projectRoot, appName, { overwrite = false } = {}) {
|
|
173
|
+
ensureValidName(appName, 'app');
|
|
174
|
+
|
|
175
|
+
const appRoot = path.join(projectRoot, 'apps', appName);
|
|
176
|
+
await ensureDir(appRoot);
|
|
177
|
+
await ensureDir(path.join(appRoot, 'tests'));
|
|
178
|
+
|
|
179
|
+
const written = [];
|
|
180
|
+
const skipped = [];
|
|
181
|
+
|
|
182
|
+
for (const entry of getAppScaffoldEntries(appName)) {
|
|
183
|
+
const target = path.join(projectRoot, entry.target);
|
|
184
|
+
if (!overwrite && await exists(target)) {
|
|
185
|
+
skipped.push(target);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await writeFile(target, entry.content);
|
|
190
|
+
written.push(target);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
appRoot,
|
|
195
|
+
written,
|
|
196
|
+
skipped,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function updateProjectRoutesFile(projectRoot, appName, mountPath) {
|
|
201
|
+
const routesFile = path.join(projectRoot, 'routes.js');
|
|
202
|
+
if (!(await exists(routesFile))) {
|
|
203
|
+
// Keep backward compatibility for legacy projects that still use routes/index.js.
|
|
204
|
+
return {
|
|
205
|
+
routesFile,
|
|
206
|
+
updatedImport: false,
|
|
207
|
+
updatedRoute: false,
|
|
208
|
+
skipped: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const importName = toImportName(appName);
|
|
213
|
+
const importLine = `import ${importName} from './apps/${appName}/routes.js';`;
|
|
214
|
+
const routeLine = ` route.use(${JSON.stringify(mountPath)}, ${importName});`;
|
|
215
|
+
const routePattern = new RegExp(`route\\.use\\([^\\n]*,\\s*${escapeRegExp(importName)}\\s*\\);`);
|
|
216
|
+
|
|
217
|
+
let content = await fs.readFile(routesFile, 'utf8');
|
|
218
|
+
let updatedImport = false;
|
|
219
|
+
let updatedRoute = false;
|
|
220
|
+
|
|
221
|
+
if (!content.includes(importLine)) {
|
|
222
|
+
if (content.includes('// AEGIS_APP_IMPORTS_START') && content.includes('// AEGIS_APP_IMPORTS_END')) {
|
|
223
|
+
content = content.replace('// AEGIS_APP_IMPORTS_END', `${importLine}\n// AEGIS_APP_IMPORTS_END`);
|
|
224
|
+
} else {
|
|
225
|
+
content = `${importLine}\n${content}`;
|
|
226
|
+
}
|
|
227
|
+
updatedImport = true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!routePattern.test(content)) {
|
|
231
|
+
if (content.includes('// AEGIS_PROJECT_APP_ROUTES_START') && content.includes('// AEGIS_PROJECT_APP_ROUTES_END')) {
|
|
232
|
+
content = content.replace(' // AEGIS_PROJECT_APP_ROUTES_END', `${routeLine}\n // AEGIS_PROJECT_APP_ROUTES_END`);
|
|
233
|
+
updatedRoute = true;
|
|
234
|
+
} else {
|
|
235
|
+
const match = content.match(/register\s*\([^)]*\)\s*{[\s\S]*?\n\s*}/m);
|
|
236
|
+
if (match) {
|
|
237
|
+
content = content.replace(match[0], `${match[0]}\n${routeLine}`);
|
|
238
|
+
updatedRoute = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (updatedImport || updatedRoute) {
|
|
244
|
+
await writeFile(routesFile, content);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
routesFile,
|
|
249
|
+
updatedImport,
|
|
250
|
+
updatedRoute,
|
|
251
|
+
skipped: false,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -403,6 +403,19 @@ export default {
|
|
|
403
403
|
`;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
export function renderAppUtilsFile() {
|
|
407
|
+
return `/**
|
|
408
|
+
* App-local pure utilities.
|
|
409
|
+
*
|
|
410
|
+
* Use this file for small reusable functions that belong only to this app.
|
|
411
|
+
* Keep database access in models, business workflows in services, and request
|
|
412
|
+
* validation/sanitization in validators.
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
export {};
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
406
419
|
export function renderAppSubscribersFile(appName) {
|
|
407
420
|
return `export default function register${toPascalCase(appName)}Subscribers({ events, logger }) {
|
|
408
421
|
events.subscribe('app.booted', ({ appName }) => {
|