aegisnode 0.0.4 → 0.1.0
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 +72 -12
- package/package.json +3 -2
- package/scripts/smoke-test.js +148 -0
- package/src/cli/commands/createapp.js +14 -176
- package/src/cli/commands/doctor.js +85 -25
- package/src/cli/commands/fixapp.js +70 -0
- package/src/cli/commands/generate.js +33 -28
- package/src/cli/commands/generateloader.js +10 -4
- package/src/cli/commands/startproject.js +14 -8
- package/src/cli/index.js +33 -3
- package/src/cli/utils/apps.js +260 -0
- package/src/cli/utils/project.js +14 -6
- package/src/cli/utils/scaffolds.js +92 -30
- package/src/index.js +1 -0
- package/src/runtime/config.js +9 -9
- package/src/runtime/kernel.js +49 -32
- package/src/runtime/typescript.js +21 -0
- package/src/utils/source-files.js +78 -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,15 +62,16 @@ 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
|
|
68
69
|
|
|
69
70
|
`startproject` creates `app.js`, `loader.cjs`, `.env`, `settings.js`, and `routes.js` without creating any default app.
|
|
70
|
-
|
|
71
|
+
Use `startproject --typescript` to generate `app.ts`, `settings.ts`, `routes.ts`, app `*.ts` files, and `tsconfig.json` instead.
|
|
72
|
+
It does not create `public/` or `logs/`; create your own folders and set them in `settings.js` or `settings.ts`.
|
|
71
73
|
|
|
72
|
-
Environment files are loaded automatically before `settings.js` is imported.
|
|
74
|
+
Environment files are loaded automatically before `settings.js` or `settings.ts` is imported.
|
|
73
75
|
Supported files:
|
|
74
76
|
- `.env`
|
|
75
77
|
- `.env.local`
|
|
@@ -86,20 +88,36 @@ Shell or hosting-panel environment variables win over values from `.env` files.
|
|
|
86
88
|
npm install -g aegisnode
|
|
87
89
|
|
|
88
90
|
aegisnode startproject blog
|
|
91
|
+
aegisnode startproject blog-ts --typescript
|
|
89
92
|
npm --prefix blog install
|
|
90
93
|
aegisnode runserver --project blog
|
|
91
94
|
|
|
92
95
|
aegisnode createapp users --project blog
|
|
96
|
+
aegisnode fix --app users --project blog
|
|
93
97
|
aegisnode generate view profile --app users --project blog
|
|
94
98
|
aegisnode generate route profile --app users --project blog
|
|
95
99
|
aegisnode generateloader --project blog
|
|
96
100
|
aegisnode doctor --project blog
|
|
101
|
+
aegisnode doctor --app users --project blog
|
|
97
102
|
aegisnode updatedeps --project blog
|
|
98
103
|
```
|
|
99
104
|
|
|
100
105
|
`cd blog` is optional. You can run commands from parent folder with `--project blog`.
|
|
101
106
|
|
|
102
|
-
`
|
|
107
|
+
Use `--typescript` on `startproject` when you want a TypeScript scaffold. That generates `app.ts`, `settings.ts`, `routes.ts`, `tsconfig.json`, app files like `views.ts`/`services.ts`, and generated artifacts such as `profile.view.ts`.
|
|
108
|
+
|
|
109
|
+
### JavaScript vs TypeScript Projects
|
|
110
|
+
|
|
111
|
+
The project type is chosen once at `startproject` time:
|
|
112
|
+
- `aegisnode startproject blog` creates a JavaScript project
|
|
113
|
+
- `aegisnode startproject blog --typescript` creates a TypeScript project
|
|
114
|
+
|
|
115
|
+
After that, the rest of the CLI follows the project automatically:
|
|
116
|
+
- `createapp` generates `views.js` / `services.js` / `routes.js` in JavaScript projects, or `views.ts` / `services.ts` / `routes.ts` in TypeScript projects
|
|
117
|
+
- `generate` creates artifacts with the same extension as the project, for example `profile.view.js` or `profile.view.ts`
|
|
118
|
+
- `fix`, `doctor`, and `generateloader` also check and repair the matching project file type automatically
|
|
119
|
+
|
|
120
|
+
`createapp`, `fix`, `generate`, `runserver`, `generateloader`, `doctor`, and `updatedeps` are project-level commands.
|
|
103
121
|
Run them from the project root; do not `cd` into `apps/<app>`.
|
|
104
122
|
Startup mode rules:
|
|
105
123
|
- Development (`env === development`): start with `aegisnode runserver` only.
|
|
@@ -107,6 +125,20 @@ Startup mode rules:
|
|
|
107
125
|
- `node app.js` and `node loader.cjs` are rejected in development mode.
|
|
108
126
|
- `aegisnode runserver` is rejected outside development mode.
|
|
109
127
|
|
|
128
|
+
### Trust Proxy
|
|
129
|
+
|
|
130
|
+
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`:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
export default {
|
|
134
|
+
trustProxy: 1,
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
Prefer an exact value such as `1`, `'loopback'`, or a subnet string instead of `true`.
|
|
141
|
+
|
|
110
142
|
### Deploy On Phusion Passenger
|
|
111
143
|
|
|
112
144
|
AegisNode supports Passenger-style startup using the generated `loader.cjs`.
|
|
@@ -125,8 +157,8 @@ HTTPS note:
|
|
|
125
157
|
- Only enable `https` in `settings.js` when Node itself should serve TLS directly.
|
|
126
158
|
|
|
127
159
|
How it works:
|
|
128
|
-
- `loader.cjs` imports `app.js
|
|
129
|
-
- `app.js` starts AegisNode with project root resolved from its own file location, so it works correctly under Passenger.
|
|
160
|
+
- `loader.cjs` imports `app.js` in JavaScript projects or `app.ts` in TypeScript projects.
|
|
161
|
+
- `app.js` / `app.ts` starts AegisNode with project root resolved from its own file location, so it works correctly under Passenger.
|
|
130
162
|
|
|
131
163
|
|
|
132
164
|
Generated routes are auto-wired into `apps/<app>/routes.js`.
|
|
@@ -145,10 +177,8 @@ By default, new app routes are API-ready:
|
|
|
145
177
|
|
|
146
178
|
Default flow is `route -> validator -> service -> model`.
|
|
147
179
|
Default app tests generated by `createapp`:
|
|
148
|
-
- `apps/<app>/tests/models.test.js`
|
|
149
|
-
- `apps/<app>/tests/validators.test.
|
|
150
|
-
- `apps/<app>/tests/services.test.js`
|
|
151
|
-
- `apps/<app>/tests/routes.test.js`
|
|
180
|
+
- JavaScript projects: `apps/<app>/tests/models.test.js`, `validators.test.js`, `services.test.js`, `routes.test.js`
|
|
181
|
+
- TypeScript projects: `apps/<app>/tests/models.test.ts`, `validators.test.ts`, `services.test.ts`, `routes.test.ts`
|
|
152
182
|
|
|
153
183
|
Run all project tests:
|
|
154
184
|
|
|
@@ -170,6 +200,26 @@ aegisnode doctor
|
|
|
170
200
|
- Auth safety checks (JWT secret, OAuth2 `allowHttp` in production)
|
|
171
201
|
- Template directory availability
|
|
172
202
|
|
|
203
|
+
Run app-level scaffold checks for one app:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
aegisnode doctor --app users
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
App-level doctor focuses on the named app:
|
|
210
|
+
- Missing `views.js`, `models.js`, `services.js`, `validators.js`, `routes.js`, `subscribers.js`, `utils.js`
|
|
211
|
+
- Missing generated test files under `apps/<app>/tests`
|
|
212
|
+
- Missing `settings.apps` declaration for that app
|
|
213
|
+
- Missing central `routes.js` import/mount when `autoMountApps` is off
|
|
214
|
+
|
|
215
|
+
Repair a partially missing app scaffold:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
aegisnode fix --app users
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`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.
|
|
222
|
+
|
|
173
223
|
Regenerate project startup entry files if needed:
|
|
174
224
|
|
|
175
225
|
```bash
|
|
@@ -241,7 +291,7 @@ export default {
|
|
|
241
291
|
|
|
242
292
|
Injected app layers also receive `env`, so views/services/models/validators/controllers/subscribers/loaders can use `env.MY_NAME` without importing `process.env`.
|
|
243
293
|
|
|
244
|
-
`settings.js` (generated shape):
|
|
294
|
+
`settings.js` (generated shape, or `settings.ts` in TypeScript mode):
|
|
245
295
|
|
|
246
296
|
```js
|
|
247
297
|
export default {
|
|
@@ -288,16 +338,26 @@ Each generated app usually contains:
|
|
|
288
338
|
- `apps/<app>/views.js`
|
|
289
339
|
- `apps/<app>/models.js`
|
|
290
340
|
- `apps/<app>/services.js`
|
|
341
|
+
- `apps/<app>/utils.js`
|
|
291
342
|
- `apps/<app>/subscribers.js`
|
|
292
343
|
- `apps/<app>/routes.js`
|
|
293
344
|
|
|
345
|
+
If the project was created with `--typescript`, the same generated files use `.ts` instead of `.js`.
|
|
346
|
+
|
|
294
347
|
Usage by file:
|
|
295
348
|
- `views.js`: HTTP handlers (`req`, `res`, `next`). Default signature can be context-first: `handler({ service, validator, services, validators, ... }, req, res, next)`.
|
|
349
|
+
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
350
|
- `models.js`: data access layer only (SQL/NoSQL operations).
|
|
297
|
-
- `services.js`: business logic layer; orchestrates models.
|
|
351
|
+
- `services.js`: business logic layer; orchestrates models and uses injected runtime objects when needed.
|
|
352
|
+
- `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.
|
|
353
|
+
`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
354
|
- `subscribers.js`: event listeners (for example `app.booted`, `ws.connection`, custom events).
|
|
299
355
|
- `routes.js`: route mapping only (`route.get(...)`, `route.post(...)`, `route.use(...)`) to view handlers.
|
|
300
356
|
|
|
357
|
+
Short rule for `utils.js` vs `services.js`:
|
|
358
|
+
- Use `utils.js` for pure app-local helpers such as string formatting, slug generation, payload shaping, or small mappers.
|
|
359
|
+
- Use `services.js` for application behavior: anything that coordinates models, injected runtime objects, or feature rules.
|
|
360
|
+
|
|
301
361
|
Route modules are mapping-only (`register(route)`).
|
|
302
362
|
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
363
|
`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.1.0",
|
|
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",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"nodemailer": "^8.0.2",
|
|
56
56
|
"querymesh": "^0.0.7",
|
|
57
57
|
"socket.io": "^4.8.1",
|
|
58
|
-
"swagger-ui-express": "^5.0.1"
|
|
58
|
+
"swagger-ui-express": "^5.0.1",
|
|
59
|
+
"tsx": "^4.21.0"
|
|
59
60
|
}
|
|
60
61
|
}
|
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
|
|
|
@@ -134,6 +136,7 @@ async function main() {
|
|
|
134
136
|
const dotenvSandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-dotenv-'));
|
|
135
137
|
const httpsSandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-https-'));
|
|
136
138
|
const proxySandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-proxy-'));
|
|
139
|
+
const typescriptSandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-ts-'));
|
|
137
140
|
|
|
138
141
|
await startProject({ projectName, cwd: sandboxRoot });
|
|
139
142
|
const generatedProjectEnv = await fs.readFile(path.join(projectRoot, '.env'), 'utf8');
|
|
@@ -153,6 +156,66 @@ async function main() {
|
|
|
153
156
|
/started with "aegisnode runserver"/,
|
|
154
157
|
);
|
|
155
158
|
|
|
159
|
+
const tsProjectName = 'forumts';
|
|
160
|
+
const tsProjectRoot = path.join(typescriptSandboxRoot, tsProjectName);
|
|
161
|
+
await startProject({ projectName: tsProjectName, cwd: typescriptSandboxRoot, typescript: true });
|
|
162
|
+
await fs.access(path.join(tsProjectRoot, 'app.ts'));
|
|
163
|
+
await fs.access(path.join(tsProjectRoot, 'settings.ts'));
|
|
164
|
+
await fs.access(path.join(tsProjectRoot, 'routes.ts'));
|
|
165
|
+
await fs.access(path.join(tsProjectRoot, 'tsconfig.json'));
|
|
166
|
+
const tsPackageJson = JSON.parse(await fs.readFile(path.join(tsProjectRoot, 'package.json'), 'utf8'));
|
|
167
|
+
assert.equal(tsPackageJson.scripts.test, 'node --import tsx/esm --test');
|
|
168
|
+
assert.equal(tsPackageJson.scripts.typecheck, 'tsc --noEmit');
|
|
169
|
+
assert.equal(tsPackageJson.devDependencies.tsx, '^4.21.0');
|
|
170
|
+
assert.equal(tsPackageJson.devDependencies.typescript, '^5.9.3');
|
|
171
|
+
const tsConfig = await loadProjectConfig(tsProjectRoot);
|
|
172
|
+
assert.equal(tsConfig.appName, 'forumts');
|
|
173
|
+
await createApp({
|
|
174
|
+
appName: 'users',
|
|
175
|
+
projectRoot: tsProjectRoot,
|
|
176
|
+
mount: '/users',
|
|
177
|
+
});
|
|
178
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'routes.ts'));
|
|
179
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'views.ts'));
|
|
180
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'models.ts'));
|
|
181
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'validators.ts'));
|
|
182
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'services.ts'));
|
|
183
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'utils.ts'));
|
|
184
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'subscribers.ts'));
|
|
185
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'models.test.ts'));
|
|
186
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'validators.test.ts'));
|
|
187
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'services.test.ts'));
|
|
188
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'routes.test.ts'));
|
|
189
|
+
await generateArtifact({
|
|
190
|
+
type: 'view',
|
|
191
|
+
name: 'profile',
|
|
192
|
+
appName: 'users',
|
|
193
|
+
projectRoot: tsProjectRoot,
|
|
194
|
+
});
|
|
195
|
+
await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'profile.view.ts'));
|
|
196
|
+
await generateArtifact({
|
|
197
|
+
type: 'route',
|
|
198
|
+
name: 'profile',
|
|
199
|
+
appName: 'users',
|
|
200
|
+
projectRoot: tsProjectRoot,
|
|
201
|
+
});
|
|
202
|
+
const tsUsersRoutesFile = await fs.readFile(path.join(tsProjectRoot, 'apps', 'users', 'routes.ts'), 'utf8');
|
|
203
|
+
assert.match(tsUsersRoutesFile, /import ProfileView from '\.\/profile\.view\.ts';/);
|
|
204
|
+
assert.match(tsUsersRoutesFile, /route\.get\('\/profile', ProfileView\.index\);/);
|
|
205
|
+
const tsDoctorReport = await runDoctor({
|
|
206
|
+
projectRoot: tsProjectRoot,
|
|
207
|
+
failOnError: true,
|
|
208
|
+
output: {
|
|
209
|
+
log() {},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
assert.equal(tsDoctorReport.summary.errors, 0);
|
|
213
|
+
const tsServer = await runServer({
|
|
214
|
+
projectRoot: tsProjectRoot,
|
|
215
|
+
port: 0,
|
|
216
|
+
});
|
|
217
|
+
await tsServer.stop();
|
|
218
|
+
|
|
156
219
|
const envProjectName = 'envdemo';
|
|
157
220
|
const envProjectRoot = path.join(envSandboxRoot, envProjectName);
|
|
158
221
|
await startProject({ projectName: envProjectName, cwd: envSandboxRoot });
|
|
@@ -590,6 +653,7 @@ dkcqnJD4SGWVeG+KhA==
|
|
|
590
653
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'models.js'));
|
|
591
654
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'validators.js'));
|
|
592
655
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'services.js'));
|
|
656
|
+
await fs.access(path.join(projectRoot, 'apps', 'users', 'utils.js'));
|
|
593
657
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'subscribers.js'));
|
|
594
658
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'models.test.js'));
|
|
595
659
|
await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'validators.test.js'));
|
|
@@ -605,6 +669,90 @@ dkcqnJD4SGWVeG+KhA==
|
|
|
605
669
|
assert.equal(doctorReport.summary.errors, 0);
|
|
606
670
|
const projectRoutesFile = await fs.readFile(path.join(projectRoot, 'routes.js'), 'utf8');
|
|
607
671
|
assert.match(projectRoutesFile, /route\.use\((['"])\/users\1, users\);/);
|
|
672
|
+
|
|
673
|
+
await fs.unlink(path.join(projectRoot, 'apps', 'users', 'utils.js'));
|
|
674
|
+
await fs.unlink(path.join(projectRoot, 'apps', 'users', 'tests', 'routes.test.js'));
|
|
675
|
+
await fs.writeFile(
|
|
676
|
+
path.join(projectRoot, 'routes.js'),
|
|
677
|
+
projectRoutesFile
|
|
678
|
+
.replace(/import users from '\.\/apps\/users\/routes\.js';\n/, '')
|
|
679
|
+
.replace(/\s*route\.use\((['"])\/users\1, users\);\n/, '\n'),
|
|
680
|
+
'utf8',
|
|
681
|
+
);
|
|
682
|
+
const appDoctorRouteReport = await runDoctor({
|
|
683
|
+
projectRoot,
|
|
684
|
+
appName: 'users',
|
|
685
|
+
failOnError: false,
|
|
686
|
+
output: {
|
|
687
|
+
log() {},
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
assert.equal(
|
|
691
|
+
appDoctorRouteReport.entries.some((entry) => entry.message.includes('Project routes.js is missing import for app "users".')),
|
|
692
|
+
true,
|
|
693
|
+
);
|
|
694
|
+
assert.equal(
|
|
695
|
+
appDoctorRouteReport.entries.some((entry) => entry.message.includes('Project routes.js is missing route.use(...) mount for app "users".')),
|
|
696
|
+
true,
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const settingsBeforeFix = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
|
|
700
|
+
await fs.writeFile(
|
|
701
|
+
path.join(projectRoot, 'settings.js'),
|
|
702
|
+
settingsBeforeFix.replace(/\n\s*\{ name: "users", mount: "\/users" \},/, ''),
|
|
703
|
+
'utf8',
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
const appDoctorReport = await runDoctor({
|
|
707
|
+
projectRoot,
|
|
708
|
+
appName: 'users',
|
|
709
|
+
failOnError: false,
|
|
710
|
+
output: {
|
|
711
|
+
log() {},
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
assert.equal(appDoctorReport.summary.errors, 0);
|
|
715
|
+
assert.equal(
|
|
716
|
+
appDoctorReport.entries.some((entry) => entry.message.includes('App "users" is not declared in settings.apps.')),
|
|
717
|
+
true,
|
|
718
|
+
);
|
|
719
|
+
assert.equal(
|
|
720
|
+
appDoctorReport.entries.some((entry) => entry.message.includes('App "users" missing utils.js.')),
|
|
721
|
+
true,
|
|
722
|
+
);
|
|
723
|
+
assert.equal(
|
|
724
|
+
appDoctorReport.entries.some((entry) => entry.message.includes('App "users" missing tests/routes.test.js.')),
|
|
725
|
+
true,
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
const fixAppResult = await runFixApp({
|
|
729
|
+
appName: 'users',
|
|
730
|
+
projectRoot,
|
|
731
|
+
});
|
|
732
|
+
assert.equal(fixAppResult.registryUpdated, true);
|
|
733
|
+
assert.equal(fixAppResult.routesUpdated, true);
|
|
734
|
+
await fs.access(path.join(projectRoot, 'apps', 'users', 'utils.js'));
|
|
735
|
+
await fs.access(path.join(projectRoot, 'apps', 'users', 'tests', 'routes.test.js'));
|
|
736
|
+
const fixedSettingsFile = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
|
|
737
|
+
assert.match(fixedSettingsFile, /\{ name: "users", mount: "\/users" \},/);
|
|
738
|
+
const fixedRoutesFile = await fs.readFile(path.join(projectRoot, 'routes.js'), 'utf8');
|
|
739
|
+
assert.match(fixedRoutesFile, /import users from '\.\/apps\/users\/routes\.js';/);
|
|
740
|
+
assert.match(fixedRoutesFile, /route\.use\((['"])\/users\1, users\);/);
|
|
741
|
+
await assert.doesNotReject(
|
|
742
|
+
() => runCli(['doctor', '--app', 'users', '--project', projectRoot]),
|
|
743
|
+
);
|
|
744
|
+
await assert.rejects(
|
|
745
|
+
() => runCli(['doctor', 'users', '--project', projectRoot]),
|
|
746
|
+
/Doctor app target must use --app <app-name>/,
|
|
747
|
+
);
|
|
748
|
+
await assert.doesNotReject(
|
|
749
|
+
() => runCli(['fix', '--app', 'users', '--project', projectRoot]),
|
|
750
|
+
);
|
|
751
|
+
await assert.rejects(
|
|
752
|
+
() => runCli(['fix', 'users', '--project', projectRoot]),
|
|
753
|
+
/Fix app target must use --app <app-name>/,
|
|
754
|
+
);
|
|
755
|
+
|
|
608
756
|
await generateArtifact({
|
|
609
757
|
type: 'view',
|
|
610
758
|
name: 'profile',
|
|
@@ -1,191 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { pathToFileURL } from 'url';
|
|
4
|
-
import { ensureDir, ensureValidName, exists, normalizeMountPath, writeFile } from '../utils/fs.js';
|
|
5
|
-
import { resolveProjectRoot } from '../utils/project.js';
|
|
1
|
+
import { ensureValidName, normalizeMountPath } from '../utils/fs.js';
|
|
2
|
+
import { getProjectSourceExtension, 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
|
-
|
|
15
|
+
const sourceExtension = getProjectSourceExtension(resolvedRoot);
|
|
176
16
|
const settingsMode = await detectSettingsMode(resolvedRoot);
|
|
177
17
|
const normalizedMount = normalizeMountPath(mount || `/${appName}`);
|
|
178
18
|
const existingApps = await readAppsConfig(settingsMode);
|
|
179
19
|
|
|
180
20
|
if (existingApps.some((entry) => entry.name === appName)) {
|
|
181
|
-
throw new Error(`App
|
|
21
|
+
throw new Error(`App "${appName}" already exists in project settings`);
|
|
182
22
|
}
|
|
183
23
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
await
|
|
187
|
-
await updateAppRegistry(resolvedRoot, updatedApps, settingsMode);
|
|
188
|
-
await updateProjectRoutesFile(resolvedRoot, appName, normalizedMount);
|
|
24
|
+
await ensureAppScaffold(resolvedRoot, appName, { sourceExtension });
|
|
25
|
+
await updateAppRegistry(resolvedRoot, [...existingApps, { name: appName, mount: normalizedMount }], settingsMode);
|
|
26
|
+
await updateProjectRoutesFile(resolvedRoot, appName, normalizedMount, sourceExtension);
|
|
189
27
|
|
|
190
|
-
console.log(`App
|
|
28
|
+
console.log(`App "${appName}" created at ${resolvedRoot}/apps/${appName}`);
|
|
191
29
|
}
|