aegisnode 0.0.3 → 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 +1613 -1471
- package/package.json +1 -1
- package/scripts/smoke-test.js +186 -2
- package/src/cli/commands/createapp.js +11 -174
- package/src/cli/commands/doctor.js +98 -19
- package/src/cli/commands/fixapp.js +65 -0
- package/src/cli/commands/generateloader.js +37 -0
- package/src/cli/commands/startproject.js +1 -1
- package/src/cli/index.js +32 -1
- package/src/cli/utils/apps.js +253 -0
- package/src/cli/utils/scaffolds.js +17 -3
- package/src/runtime/kernel.js +10 -0
- package/src/runtime/upload.js +48 -0
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,14 +7,17 @@ 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';
|
|
14
|
+
import { runGenerateLoader } from '../src/cli/commands/generateloader.js';
|
|
13
15
|
import { runProject } from '../src/index.js';
|
|
14
16
|
import { createAuthManager, normalizeAuthConfig } from '../src/runtime/auth.js';
|
|
15
17
|
import { loadProjectConfig } from '../src/runtime/config.js';
|
|
16
18
|
import { initializeDatabase, closeDatabase } from '../src/runtime/database.js';
|
|
17
19
|
import { runDoctor } from '../src/cli/commands/doctor.js';
|
|
20
|
+
import { runFixApp } from '../src/cli/commands/fixapp.js';
|
|
18
21
|
import { runUpdateDependencies } from '../src/cli/commands/updatedeps.js';
|
|
19
22
|
import { createHelpers } from '../src/runtime/helpers.js';
|
|
20
23
|
|
|
@@ -137,8 +140,10 @@ async function main() {
|
|
|
137
140
|
await startProject({ projectName, cwd: sandboxRoot });
|
|
138
141
|
const generatedProjectEnv = await fs.readFile(path.join(projectRoot, '.env'), 'utf8');
|
|
139
142
|
assert.match(generatedProjectEnv, /^APP_SECRET=.{16,}$/m);
|
|
143
|
+
const generatedAppSecret = generatedProjectEnv.match(/^APP_SECRET=(.+)$/m)?.[1]?.trim();
|
|
144
|
+
assert.ok(generatedAppSecret);
|
|
140
145
|
const generatedSettings = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
|
|
141
|
-
assert.
|
|
146
|
+
assert.ok(generatedSettings.includes(`appSecret: process.env.APP_SECRET || ${JSON.stringify(generatedAppSecret)}`));
|
|
142
147
|
await assert.rejects(
|
|
143
148
|
() => runProject({
|
|
144
149
|
rootDir: projectRoot,
|
|
@@ -404,6 +409,74 @@ dkcqnJD4SGWVeG+KhA==
|
|
|
404
409
|
await fs.access(filePath);
|
|
405
410
|
}
|
|
406
411
|
|
|
412
|
+
await fs.unlink(path.join(projectRoot, 'app.js'));
|
|
413
|
+
await fs.unlink(path.join(projectRoot, 'loader.cjs'));
|
|
414
|
+
const restoredStartupEntries = await runGenerateLoader({
|
|
415
|
+
projectRoot,
|
|
416
|
+
output: {
|
|
417
|
+
log() {},
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
assert.equal(restoredStartupEntries.createdApp, true);
|
|
421
|
+
assert.equal(restoredStartupEntries.createdLoader, true);
|
|
422
|
+
await fs.access(path.join(projectRoot, 'app.js'));
|
|
423
|
+
await fs.access(path.join(projectRoot, 'loader.cjs'));
|
|
424
|
+
|
|
425
|
+
await fs.writeFile(
|
|
426
|
+
path.join(envProjectRoot, 'settings.js'),
|
|
427
|
+
`export default {
|
|
428
|
+
appName: 'envdemo',
|
|
429
|
+
env: 'production',
|
|
430
|
+
logging: {
|
|
431
|
+
level: 'info',
|
|
432
|
+
},
|
|
433
|
+
security: {
|
|
434
|
+
appSecret: process.env.APP_SECRET || '',
|
|
435
|
+
ddos: {
|
|
436
|
+
maxRequests: 120,
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
environments: {
|
|
440
|
+
default: {
|
|
441
|
+
security: {
|
|
442
|
+
ddos: {
|
|
443
|
+
windowMs: 45000,
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
production: {
|
|
448
|
+
logging: { level: 'warn' },
|
|
449
|
+
security: { ddos: { maxRequests: 80 } },
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
`,
|
|
454
|
+
'utf8',
|
|
455
|
+
);
|
|
456
|
+
await fs.unlink(path.join(envProjectRoot, 'loader.cjs'));
|
|
457
|
+
const missingLoaderDoctorReport = await runDoctor({
|
|
458
|
+
projectRoot: envProjectRoot,
|
|
459
|
+
failOnError: false,
|
|
460
|
+
output: {
|
|
461
|
+
log() {},
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
assert.equal(missingLoaderDoctorReport.summary.errors, 1);
|
|
465
|
+
assert.ok(
|
|
466
|
+
missingLoaderDoctorReport.entries.some((entry) => (
|
|
467
|
+
entry.level === 'ERROR'
|
|
468
|
+
&& /loader\.cjs is missing for production startup/.test(entry.message)
|
|
469
|
+
)),
|
|
470
|
+
);
|
|
471
|
+
const restoredProductionLoader = await runGenerateLoader({
|
|
472
|
+
projectRoot: envProjectRoot,
|
|
473
|
+
output: {
|
|
474
|
+
log() {},
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
assert.equal(restoredProductionLoader.createdLoader, true);
|
|
478
|
+
await fs.access(path.join(envProjectRoot, 'loader.cjs'));
|
|
479
|
+
|
|
407
480
|
const registryPackages = new Map([
|
|
408
481
|
['alpha', '2.0.0'],
|
|
409
482
|
['@scope/bravo', '3.4.0'],
|
|
@@ -519,6 +592,7 @@ dkcqnJD4SGWVeG+KhA==
|
|
|
519
592
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'models.js'));
|
|
520
593
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'validators.js'));
|
|
521
594
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'services.js'));
|
|
595
|
+
await fs.access(path.join(projectRoot, 'apps', 'users', 'utils.js'));
|
|
522
596
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'subscribers.js'));
|
|
523
597
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'models.test.js'));
|
|
524
598
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'validators.test.js'));
|
|
@@ -534,6 +608,90 @@ dkcqnJD4SGWVeG+KhA==
|
|
|
534
608
|
assert.equal(doctorReport.summary.errors, 0);
|
|
535
609
|
const projectRoutesFile = await fs.readFile(path.join(projectRoot, 'routes.js'), 'utf8');
|
|
536
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
|
+
|
|
537
695
|
await generateArtifact({
|
|
538
696
|
type: 'view',
|
|
539
697
|
name: 'profile',
|
|
@@ -1288,7 +1446,7 @@ export default {
|
|
|
1288
1446
|
);
|
|
1289
1447
|
await fs.writeFile(
|
|
1290
1448
|
path.join(projectRoot, 'routes.js'),
|
|
1291
|
-
`export default {\n register(route) {\n route.get('/csrf-token', (req, res) => {\n res.json({ token: req.csrfToken() });\n });\n\n route.get('/csrf-form', (req, res) => {\n return res.render('csrf-form', { layout: false });\n });\n\n route.post('/submit', (req, res) => {\n res.json({ ok: true, body: req.body || {} });\n });\n },\n};\n`,
|
|
1449
|
+
`export default {\n register(route) {\n route.get('/csrf-token', (req, res) => {\n res.json({ token: req.csrfToken() });\n });\n\n route.get('/csrf-form', (req, res) => {\n return res.render('csrf-form', { layout: false });\n });\n\n route.post('/submit', (req, res) => {\n res.json({ ok: true, body: req.body || {} });\n });\n\n route.post('/submit-upload', route.upload.none(), (req, res) => {\n res.json({ ok: true, body: req.body || {} });\n });\n },\n};\n`,
|
|
1292
1450
|
'utf8',
|
|
1293
1451
|
);
|
|
1294
1452
|
|
|
@@ -1361,6 +1519,32 @@ export default {
|
|
|
1361
1519
|
assert.equal(validJsonTokenJson.ok, true);
|
|
1362
1520
|
assert.equal(validJsonTokenJson.body.name, 'json-with-token');
|
|
1363
1521
|
|
|
1522
|
+
const missingMultipartTokenBody = new FormData();
|
|
1523
|
+
missingMultipartTokenBody.set('name', 'multipart-without-token');
|
|
1524
|
+
const missingMultipartTokenResponse = await fetch(`http://127.0.0.1:${csrfPort}/submit-upload`, {
|
|
1525
|
+
method: 'POST',
|
|
1526
|
+
headers: {
|
|
1527
|
+
cookie: csrfCookie,
|
|
1528
|
+
},
|
|
1529
|
+
body: missingMultipartTokenBody,
|
|
1530
|
+
});
|
|
1531
|
+
assert.equal(missingMultipartTokenResponse.status, 403);
|
|
1532
|
+
|
|
1533
|
+
const validMultipartTokenBody = new FormData();
|
|
1534
|
+
validMultipartTokenBody.set('_csrf', csrfTokenJson.token);
|
|
1535
|
+
validMultipartTokenBody.set('name', 'multipart-with-token');
|
|
1536
|
+
const validMultipartTokenResponse = await fetch(`http://127.0.0.1:${csrfPort}/submit-upload`, {
|
|
1537
|
+
method: 'POST',
|
|
1538
|
+
headers: {
|
|
1539
|
+
cookie: csrfCookie,
|
|
1540
|
+
},
|
|
1541
|
+
body: validMultipartTokenBody,
|
|
1542
|
+
});
|
|
1543
|
+
assert.equal(validMultipartTokenResponse.status, 200);
|
|
1544
|
+
const validMultipartTokenJson = await validMultipartTokenResponse.json();
|
|
1545
|
+
assert.equal(validMultipartTokenJson.ok, true);
|
|
1546
|
+
assert.equal(validMultipartTokenJson.body.name, 'multipart-with-token');
|
|
1547
|
+
|
|
1364
1548
|
const csrfFormResponse = await fetch(`http://127.0.0.1:${csrfPort}/csrf-form`, {
|
|
1365
1549
|
headers: {
|
|
1366
1550
|
cookie: csrfCookie,
|
|
@@ -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();
|
|
31
38
|
|
|
32
|
-
|
|
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
|
+
}
|
|
45
|
+
|
|
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,21 +79,65 @@ 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
|
}
|
|
72
116
|
|
|
117
|
+
async function runStartupEntryChecks(rootDir, config, collector) {
|
|
118
|
+
const env = String(config.env || process.env.NODE_ENV || 'development').trim().toLowerCase();
|
|
119
|
+
const appEntryPath = path.join(rootDir, 'app.js');
|
|
120
|
+
const loaderEntryPath = path.join(rootDir, 'loader.cjs');
|
|
121
|
+
const appEntryExists = await fileExists(appEntryPath);
|
|
122
|
+
const loaderEntryExists = await fileExists(loaderEntryPath);
|
|
123
|
+
|
|
124
|
+
if (appEntryExists) {
|
|
125
|
+
collector.ok('app.js exists.');
|
|
126
|
+
} else if (env === 'production') {
|
|
127
|
+
collector.error('app.js is missing for production startup. Run "aegisnode generateloader" to restore startup entry files.');
|
|
128
|
+
} else {
|
|
129
|
+
collector.warn('app.js is missing. Run "aegisnode generateloader" to restore startup entry files.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (loaderEntryExists) {
|
|
133
|
+
collector.ok('loader.cjs exists.');
|
|
134
|
+
} else if (env === 'production') {
|
|
135
|
+
collector.error('loader.cjs is missing for production startup. Run "aegisnode generateloader" to restore it.');
|
|
136
|
+
} else {
|
|
137
|
+
collector.warn('loader.cjs is missing. Generate it with "aegisnode generateloader" before non-development startup.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
73
141
|
function runSecurityChecks(config, collector) {
|
|
74
142
|
const env = String(config.env || process.env.NODE_ENV || 'development');
|
|
75
143
|
const security = config.security || {};
|
|
@@ -169,7 +237,12 @@ export async function runDoctor({
|
|
|
169
237
|
projectRoot,
|
|
170
238
|
failOnError = true,
|
|
171
239
|
output = console,
|
|
240
|
+
appName = null,
|
|
172
241
|
} = {}) {
|
|
242
|
+
if (appName) {
|
|
243
|
+
ensureValidName(appName, 'app');
|
|
244
|
+
}
|
|
245
|
+
|
|
173
246
|
const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
|
|
174
247
|
const collector = createCollector();
|
|
175
248
|
|
|
@@ -178,11 +251,17 @@ export async function runDoctor({
|
|
|
178
251
|
const config = await loadProjectConfig(resolvedRoot);
|
|
179
252
|
collector.ok(`Environment: ${config.env || 'development'}`);
|
|
180
253
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
186
265
|
|
|
187
266
|
const summary = printSummary(collector.entries, output);
|
|
188
267
|
|