aegisnode 0.0.5 → 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 +24 -9
- package/package.json +3 -2
- package/scripts/smoke-test.js +61 -0
- package/src/cli/commands/createapp.js +4 -3
- package/src/cli/commands/doctor.js +17 -11
- package/src/cli/commands/fixapp.js +9 -4
- 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 +12 -2
- package/src/cli/utils/apps.js +41 -34
- package/src/cli/utils/project.js +14 -6
- package/src/cli/utils/scaffolds.js +79 -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
|
@@ -68,9 +68,10 @@ Core features:
|
|
|
68
68
|
- Built-in runtime helpers (`money`, `number`, `dateTime`, `timeElapsed`, `toObjectId`) + `jlive` bridge
|
|
69
69
|
|
|
70
70
|
`startproject` creates `app.js`, `loader.cjs`, `.env`, `settings.js`, and `routes.js` without creating any default app.
|
|
71
|
-
|
|
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`.
|
|
72
73
|
|
|
73
|
-
Environment files are loaded automatically before `settings.js` is imported.
|
|
74
|
+
Environment files are loaded automatically before `settings.js` or `settings.ts` is imported.
|
|
74
75
|
Supported files:
|
|
75
76
|
- `.env`
|
|
76
77
|
- `.env.local`
|
|
@@ -87,6 +88,7 @@ Shell or hosting-panel environment variables win over values from `.env` files.
|
|
|
87
88
|
npm install -g aegisnode
|
|
88
89
|
|
|
89
90
|
aegisnode startproject blog
|
|
91
|
+
aegisnode startproject blog-ts --typescript
|
|
90
92
|
npm --prefix blog install
|
|
91
93
|
aegisnode runserver --project blog
|
|
92
94
|
|
|
@@ -102,6 +104,19 @@ aegisnode updatedeps --project blog
|
|
|
102
104
|
|
|
103
105
|
`cd blog` is optional. You can run commands from parent folder with `--project blog`.
|
|
104
106
|
|
|
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
|
+
|
|
105
120
|
`createapp`, `fix`, `generate`, `runserver`, `generateloader`, `doctor`, and `updatedeps` are project-level commands.
|
|
106
121
|
Run them from the project root; do not `cd` into `apps/<app>`.
|
|
107
122
|
Startup mode rules:
|
|
@@ -142,8 +157,8 @@ HTTPS note:
|
|
|
142
157
|
- Only enable `https` in `settings.js` when Node itself should serve TLS directly.
|
|
143
158
|
|
|
144
159
|
How it works:
|
|
145
|
-
- `loader.cjs` imports `app.js
|
|
146
|
-
- `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.
|
|
147
162
|
|
|
148
163
|
|
|
149
164
|
Generated routes are auto-wired into `apps/<app>/routes.js`.
|
|
@@ -162,10 +177,8 @@ By default, new app routes are API-ready:
|
|
|
162
177
|
|
|
163
178
|
Default flow is `route -> validator -> service -> model`.
|
|
164
179
|
Default app tests generated by `createapp`:
|
|
165
|
-
- `apps/<app>/tests/models.test.js`
|
|
166
|
-
- `apps/<app>/tests/validators.test.
|
|
167
|
-
- `apps/<app>/tests/services.test.js`
|
|
168
|
-
- `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`
|
|
169
182
|
|
|
170
183
|
Run all project tests:
|
|
171
184
|
|
|
@@ -278,7 +291,7 @@ export default {
|
|
|
278
291
|
|
|
279
292
|
Injected app layers also receive `env`, so views/services/models/validators/controllers/subscribers/loaders can use `env.MY_NAME` without importing `process.env`.
|
|
280
293
|
|
|
281
|
-
`settings.js` (generated shape):
|
|
294
|
+
`settings.js` (generated shape, or `settings.ts` in TypeScript mode):
|
|
282
295
|
|
|
283
296
|
```js
|
|
284
297
|
export default {
|
|
@@ -329,6 +342,8 @@ Each generated app usually contains:
|
|
|
329
342
|
- `apps/<app>/subscribers.js`
|
|
330
343
|
- `apps/<app>/routes.js`
|
|
331
344
|
|
|
345
|
+
If the project was created with `--typescript`, the same generated files use `.ts` instead of `.js`.
|
|
346
|
+
|
|
332
347
|
Usage by file:
|
|
333
348
|
- `views.js`: HTTP handlers (`req`, `res`, `next`). Default signature can be context-first: `handler({ service, validator, services, validators, ... }, req, res, next)`.
|
|
334
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`.
|
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
|
@@ -136,6 +136,7 @@ async function main() {
|
|
|
136
136
|
const dotenvSandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-dotenv-'));
|
|
137
137
|
const httpsSandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-https-'));
|
|
138
138
|
const proxySandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-proxy-'));
|
|
139
|
+
const typescriptSandboxRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aegisnode-ts-'));
|
|
139
140
|
|
|
140
141
|
await startProject({ projectName, cwd: sandboxRoot });
|
|
141
142
|
const generatedProjectEnv = await fs.readFile(path.join(projectRoot, '.env'), 'utf8');
|
|
@@ -155,6 +156,66 @@ async function main() {
|
|
|
155
156
|
/started with "aegisnode runserver"/,
|
|
156
157
|
);
|
|
157
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
|
+
|
|
158
219
|
const envProjectName = 'envdemo';
|
|
159
220
|
const envProjectRoot = path.join(envSandboxRoot, envProjectName);
|
|
160
221
|
await startProject({ projectName: envProjectName, cwd: envSandboxRoot });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ensureValidName, normalizeMountPath } from '../utils/fs.js';
|
|
2
|
-
import { resolveProjectRoot } from '../utils/project.js';
|
|
2
|
+
import { getProjectSourceExtension, resolveProjectRoot } from '../utils/project.js';
|
|
3
3
|
import {
|
|
4
4
|
detectSettingsMode,
|
|
5
5
|
ensureAppScaffold,
|
|
@@ -12,6 +12,7 @@ export async function createApp({ appName, projectRoot, mount }) {
|
|
|
12
12
|
ensureValidName(appName, 'app');
|
|
13
13
|
|
|
14
14
|
const resolvedRoot = await resolveProjectRoot(projectRoot);
|
|
15
|
+
const sourceExtension = getProjectSourceExtension(resolvedRoot);
|
|
15
16
|
const settingsMode = await detectSettingsMode(resolvedRoot);
|
|
16
17
|
const normalizedMount = normalizeMountPath(mount || `/${appName}`);
|
|
17
18
|
const existingApps = await readAppsConfig(settingsMode);
|
|
@@ -20,9 +21,9 @@ export async function createApp({ appName, projectRoot, mount }) {
|
|
|
20
21
|
throw new Error(`App "${appName}" already exists in project settings`);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
await ensureAppScaffold(resolvedRoot, appName);
|
|
24
|
+
await ensureAppScaffold(resolvedRoot, appName, { sourceExtension });
|
|
24
25
|
await updateAppRegistry(resolvedRoot, [...existingApps, { name: appName, mount: normalizedMount }], settingsMode);
|
|
25
|
-
await updateProjectRoutesFile(resolvedRoot, appName, normalizedMount);
|
|
26
|
+
await updateProjectRoutesFile(resolvedRoot, appName, normalizedMount, sourceExtension);
|
|
26
27
|
|
|
27
28
|
console.log(`App "${appName}" created at ${resolvedRoot}/apps/${appName}`);
|
|
28
29
|
}
|
|
@@ -2,8 +2,10 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { loadProjectConfig } from '../../runtime/config.js';
|
|
4
4
|
import { ensureValidName } from '../utils/fs.js';
|
|
5
|
-
import { resolveProjectRoot } from '../utils/project.js';
|
|
5
|
+
import { getProjectSourceExtension, resolveProjectRoot } from '../utils/project.js';
|
|
6
6
|
import { getAppScaffoldEntries, toImportName } from '../utils/apps.js';
|
|
7
|
+
import { withSourceExtension } from '../utils/scaffolds.js';
|
|
8
|
+
import { resolveSourceFile } from '../../utils/source-files.js';
|
|
7
9
|
|
|
8
10
|
function createCollector() {
|
|
9
11
|
const entries = [];
|
|
@@ -34,6 +36,9 @@ function escapeRegExp(value) {
|
|
|
34
36
|
|
|
35
37
|
async function runAppChecks(rootDir, config, collector, targetAppName = null) {
|
|
36
38
|
const apps = Array.isArray(config.apps) ? config.apps : [];
|
|
39
|
+
const sourceExtension = getProjectSourceExtension(rootDir);
|
|
40
|
+
const routesFile = resolveSourceFile(path.join(rootDir, 'routes'), [sourceExtension]) || resolveSourceFile(path.join(rootDir, 'routes'));
|
|
41
|
+
const routesLabel = routesFile ? path.basename(routesFile) : withSourceExtension('routes', sourceExtension);
|
|
37
42
|
const declaredApps = new Map();
|
|
38
43
|
|
|
39
44
|
for (const app of apps) {
|
|
@@ -52,7 +57,6 @@ async function runAppChecks(rootDir, config, collector, targetAppName = null) {
|
|
|
52
57
|
collector.ok(`Declared apps: ${apps.map((app) => app.name).join(', ')}`);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
const routesFile = path.join(rootDir, 'routes.js');
|
|
56
60
|
const routesFileExists = await fileExists(routesFile);
|
|
57
61
|
const routesContent = routesFileExists ? await fs.readFile(routesFile, 'utf8') : '';
|
|
58
62
|
const targetApps = targetAppName ? [targetAppName] : apps;
|
|
@@ -79,7 +83,7 @@ async function runAppChecks(rootDir, config, collector, targetAppName = null) {
|
|
|
79
83
|
continue;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
for (const entry of getAppScaffoldEntries(appName)) {
|
|
86
|
+
for (const entry of getAppScaffoldEntries(appName, sourceExtension)) {
|
|
83
87
|
const target = path.join(rootDir, entry.target);
|
|
84
88
|
if (!(await fileExists(target))) {
|
|
85
89
|
collector.warn(`App "${appName}" missing ${path.relative(appRoot, target)}.`);
|
|
@@ -96,37 +100,39 @@ async function runAppChecks(rootDir, config, collector, targetAppName = null) {
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
if (!routesFileExists) {
|
|
99
|
-
collector.warn(`Project
|
|
103
|
+
collector.warn(`Project ${routesLabel} is missing; app "${appName}" cannot be mounted centrally.`);
|
|
100
104
|
continue;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
const importPath = `./apps/${appName}/routes
|
|
107
|
+
const importPath = `./apps/${appName}/routes${sourceExtension}`;
|
|
104
108
|
const importName = toImportName(appName);
|
|
105
109
|
const routePattern = new RegExp(`route\\.use\\([^\\n]*,\\s*${escapeRegExp(importName)}\\s*\\);`);
|
|
106
110
|
|
|
107
111
|
if (!routesContent.includes(importPath)) {
|
|
108
|
-
collector.warn(`Project
|
|
112
|
+
collector.warn(`Project ${routesLabel} is missing import for app "${appName}".`);
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
if (!routePattern.test(routesContent)) {
|
|
112
|
-
collector.warn(`Project
|
|
116
|
+
collector.warn(`Project ${routesLabel} is missing route.use(...) mount for app "${appName}".`);
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
async function runStartupEntryChecks(rootDir, config, collector) {
|
|
118
122
|
const env = String(config.env || process.env.NODE_ENV || 'development').trim().toLowerCase();
|
|
119
|
-
const
|
|
123
|
+
const sourceExtension = getProjectSourceExtension(rootDir);
|
|
124
|
+
const appEntryPath = resolveSourceFile(path.join(rootDir, 'app'), [sourceExtension])
|
|
125
|
+
|| path.join(rootDir, withSourceExtension('app', sourceExtension));
|
|
120
126
|
const loaderEntryPath = path.join(rootDir, 'loader.cjs');
|
|
121
127
|
const appEntryExists = await fileExists(appEntryPath);
|
|
122
128
|
const loaderEntryExists = await fileExists(loaderEntryPath);
|
|
123
129
|
|
|
124
130
|
if (appEntryExists) {
|
|
125
|
-
collector.ok(
|
|
131
|
+
collector.ok(`${path.basename(appEntryPath)} exists.`);
|
|
126
132
|
} else if (env === 'production') {
|
|
127
|
-
collector.error(
|
|
133
|
+
collector.error(`${path.basename(appEntryPath)} is missing for production startup. Run "aegisnode generateloader" to restore startup entry files.`);
|
|
128
134
|
} else {
|
|
129
|
-
collector.warn(
|
|
135
|
+
collector.warn(`${path.basename(appEntryPath)} is missing. Run "aegisnode generateloader" to restore startup entry files.`);
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
if (loaderEntryExists) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { ensureValidName, normalizeMountPath } from '../utils/fs.js';
|
|
3
|
-
import { resolveProjectRoot } from '../utils/project.js';
|
|
3
|
+
import { getProjectSourceExtension, resolveProjectRoot } from '../utils/project.js';
|
|
4
|
+
import { withSourceExtension } from '../utils/scaffolds.js';
|
|
4
5
|
import {
|
|
5
6
|
detectSettingsMode,
|
|
6
7
|
ensureAppScaffold,
|
|
@@ -17,12 +18,16 @@ export async function runFixApp({ appName, projectRoot, mount } = {}) {
|
|
|
17
18
|
ensureValidName(appName, 'app');
|
|
18
19
|
|
|
19
20
|
const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
|
|
21
|
+
const sourceExtension = getProjectSourceExtension(resolvedRoot);
|
|
20
22
|
const settingsMode = await detectSettingsMode(resolvedRoot);
|
|
21
23
|
const existingApps = await readAppsConfig(settingsMode);
|
|
22
24
|
const existingApp = existingApps.find((entry) => entry.name === appName) || null;
|
|
23
25
|
const appMount = existingApp?.mount || normalizeMountPath(mount || `/${appName}`);
|
|
24
26
|
|
|
25
|
-
const scaffoldResult = await ensureAppScaffold(resolvedRoot, appName, {
|
|
27
|
+
const scaffoldResult = await ensureAppScaffold(resolvedRoot, appName, {
|
|
28
|
+
overwrite: false,
|
|
29
|
+
sourceExtension,
|
|
30
|
+
});
|
|
26
31
|
|
|
27
32
|
let registryUpdated = false;
|
|
28
33
|
if (!existingApp) {
|
|
@@ -34,7 +39,7 @@ export async function runFixApp({ appName, projectRoot, mount } = {}) {
|
|
|
34
39
|
registryUpdated = true;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
const routesResult = await updateProjectRoutesFile(resolvedRoot, appName, appMount);
|
|
42
|
+
const routesResult = await updateProjectRoutesFile(resolvedRoot, appName, appMount, sourceExtension);
|
|
38
43
|
const relativeWritten = scaffoldResult.written.map((target) => path.relative(resolvedRoot, target));
|
|
39
44
|
|
|
40
45
|
if (relativeWritten.length === 0 && !registryUpdated && !routesResult.updatedImport && !routesResult.updatedRoute) {
|
|
@@ -48,7 +53,7 @@ export async function runFixApp({ appName, projectRoot, mount } = {}) {
|
|
|
48
53
|
console.log(`Added app "${appName}" to settings.apps with mount ${appMount}`);
|
|
49
54
|
}
|
|
50
55
|
if (routesResult.updatedImport || routesResult.updatedRoute) {
|
|
51
|
-
console.log(`Updated
|
|
56
|
+
console.log(`Updated ${path.basename(routesResult.routesFile || withSourceExtension('routes', sourceExtension))} registration for app "${appName}"`);
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { ensureValidName, exists, writeFile } from '../utils/fs.js';
|
|
4
|
-
import { toPascalCase } from '../utils/scaffolds.js';
|
|
4
|
+
import { toPascalCase, withSourceExtension } from '../utils/scaffolds.js';
|
|
5
|
+
import { getProjectSourceExtension } from '../utils/project.js';
|
|
6
|
+
import { resolveSourceFile, resolveSourceIndexFile } from '../../utils/source-files.js';
|
|
5
7
|
|
|
6
8
|
const SUPPORTED_TYPES = new Set(['view', 'controller', 'model', 'validator', 'dto', 'service', 'subscriber', 'route']);
|
|
7
9
|
|
|
@@ -22,12 +24,14 @@ function assertType(type) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
async function assertProjectRoot(projectRoot) {
|
|
25
|
-
const hasSingleSettings =
|
|
26
|
-
const hasLegacySettings = (
|
|
27
|
-
|
|
27
|
+
const hasSingleSettings = Boolean(resolveSourceFile(path.join(projectRoot, 'settings')));
|
|
28
|
+
const hasLegacySettings = Boolean(
|
|
29
|
+
resolveSourceIndexFile(path.join(projectRoot, 'settings'))
|
|
30
|
+
|| resolveSourceFile(path.join(projectRoot, 'settings', 'apps')),
|
|
31
|
+
);
|
|
28
32
|
|
|
29
33
|
if (!hasSingleSettings && !hasLegacySettings) {
|
|
30
|
-
throw new Error('Not an AegisNode project root: missing settings.js');
|
|
34
|
+
throw new Error('Not an AegisNode project root: missing settings.js/settings.ts');
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -110,35 +114,35 @@ function renderSubscriber(name, appName) {
|
|
|
110
114
|
`;
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
async function getAppRoutesFile(appRoot) {
|
|
114
|
-
const primary = path.join(appRoot, 'routes.
|
|
115
|
-
if (await exists(primary)) {
|
|
117
|
+
async function getAppRoutesFile(appRoot, sourceExtension) {
|
|
118
|
+
const primary = resolveSourceFile(path.join(appRoot, 'routes'), [sourceExtension]) || resolveSourceFile(path.join(appRoot, 'routes'));
|
|
119
|
+
if (primary && await exists(primary)) {
|
|
116
120
|
return primary;
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
const legacy = path.join(appRoot, 'routes',
|
|
120
|
-
if (await exists(legacy)) {
|
|
123
|
+
const legacy = resolveSourceIndexFile(path.join(appRoot, 'routes'), [sourceExtension]) || resolveSourceIndexFile(path.join(appRoot, 'routes'));
|
|
124
|
+
if (legacy && await exists(legacy)) {
|
|
121
125
|
return legacy;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
throw new Error(`Missing app routes file in ${appRoot}. Expected routes
|
|
128
|
+
throw new Error(`Missing app routes file in ${appRoot}. Expected routes${sourceExtension} or routes/index${sourceExtension}`);
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
async function appendRouteToApp({ appRoot, routeName }) {
|
|
128
|
-
const routesFile = await getAppRoutesFile(appRoot);
|
|
129
|
-
const usesFlatRoutesFile = path.basename(routesFile) === 'routes
|
|
130
|
-
const flatViewPath = path.join(appRoot, `${routeName}.view
|
|
131
|
-
const nestedViewPath = path.join(appRoot, 'views', `${routeName}.view
|
|
131
|
+
async function appendRouteToApp({ appRoot, routeName, sourceExtension }) {
|
|
132
|
+
const routesFile = await getAppRoutesFile(appRoot, sourceExtension);
|
|
133
|
+
const usesFlatRoutesFile = path.basename(routesFile) === withSourceExtension('routes', sourceExtension);
|
|
134
|
+
const flatViewPath = path.join(appRoot, `${routeName}.view${sourceExtension}`);
|
|
135
|
+
const nestedViewPath = path.join(appRoot, 'views', `${routeName}.view${sourceExtension}`);
|
|
132
136
|
|
|
133
137
|
let importPath = null;
|
|
134
138
|
if (await exists(flatViewPath)) {
|
|
135
139
|
importPath = usesFlatRoutesFile
|
|
136
|
-
? `./${routeName}.view
|
|
137
|
-
: `../${routeName}.view
|
|
140
|
+
? `./${routeName}.view${sourceExtension}`
|
|
141
|
+
: `../${routeName}.view${sourceExtension}`;
|
|
138
142
|
} else if (await exists(nestedViewPath)) {
|
|
139
143
|
importPath = usesFlatRoutesFile
|
|
140
|
-
? `./views/${routeName}.view
|
|
141
|
-
: `../views/${routeName}.view
|
|
144
|
+
? `./views/${routeName}.view${sourceExtension}`
|
|
145
|
+
: `../views/${routeName}.view${sourceExtension}`;
|
|
142
146
|
} else {
|
|
143
147
|
throw new Error(`Missing view for route generation: ${flatViewPath} (or ${nestedViewPath}). Create it first with generate view ${routeName}.`);
|
|
144
148
|
}
|
|
@@ -193,18 +197,18 @@ async function appendRouteToApp({ appRoot, routeName }) {
|
|
|
193
197
|
return routesFile;
|
|
194
198
|
}
|
|
195
199
|
|
|
196
|
-
function resolveTarget(appRoot, type, name) {
|
|
200
|
+
function resolveTarget(appRoot, type, name, sourceExtension) {
|
|
197
201
|
switch (type) {
|
|
198
202
|
case 'view':
|
|
199
|
-
return path.join(appRoot, `${name}.view
|
|
203
|
+
return path.join(appRoot, `${name}.view${sourceExtension}`);
|
|
200
204
|
case 'model':
|
|
201
|
-
return path.join(appRoot, `${name}.model
|
|
205
|
+
return path.join(appRoot, `${name}.model${sourceExtension}`);
|
|
202
206
|
case 'service':
|
|
203
|
-
return path.join(appRoot, `${name}.service
|
|
207
|
+
return path.join(appRoot, `${name}.service${sourceExtension}`);
|
|
204
208
|
case 'validator':
|
|
205
|
-
return path.join(appRoot, `${name}.validator
|
|
209
|
+
return path.join(appRoot, `${name}.validator${sourceExtension}`);
|
|
206
210
|
case 'subscriber':
|
|
207
|
-
return path.join(appRoot, `${name}.subscriber
|
|
211
|
+
return path.join(appRoot, `${name}.subscriber${sourceExtension}`);
|
|
208
212
|
default:
|
|
209
213
|
throw new Error(`Unsupported type: ${type}`);
|
|
210
214
|
}
|
|
@@ -248,14 +252,15 @@ export async function generateArtifact({ type, name, appName, projectRoot }) {
|
|
|
248
252
|
const resolvedRoot = path.resolve(projectRoot);
|
|
249
253
|
await assertProjectRoot(resolvedRoot);
|
|
250
254
|
const appRoot = await assertAppRoot(resolvedRoot, appName);
|
|
255
|
+
const sourceExtension = getProjectSourceExtension(resolvedRoot);
|
|
251
256
|
|
|
252
257
|
if (normalizedType === 'route') {
|
|
253
|
-
const routesFile = await appendRouteToApp({ appRoot, routeName: name });
|
|
258
|
+
const routesFile = await appendRouteToApp({ appRoot, routeName: name, sourceExtension });
|
|
254
259
|
console.log(`Generated route /${name} in ${routesFile}`);
|
|
255
260
|
return;
|
|
256
261
|
}
|
|
257
262
|
|
|
258
|
-
const targetFile = resolveTarget(appRoot, normalizedType, name);
|
|
263
|
+
const targetFile = resolveTarget(appRoot, normalizedType, name, sourceExtension);
|
|
259
264
|
if (await exists(targetFile)) {
|
|
260
265
|
throw new Error(`File already exists: ${targetFile}`);
|
|
261
266
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { exists, writeFile } from '../utils/fs.js';
|
|
3
|
-
import { resolveProjectRoot } from '../utils/project.js';
|
|
4
|
-
import { renderProjectAppJs, renderProjectLoaderCjs } from '../utils/scaffolds.js';
|
|
3
|
+
import { getProjectSourceExtension, resolveProjectRoot } from '../utils/project.js';
|
|
4
|
+
import { renderProjectAppJs, renderProjectLoaderCjs, withSourceExtension } from '../utils/scaffolds.js';
|
|
5
5
|
|
|
6
6
|
async function ensureStartupFile(rootDir, fileName, content, output) {
|
|
7
7
|
const target = path.join(rootDir, fileName);
|
|
@@ -20,8 +20,14 @@ export async function runGenerateLoader({
|
|
|
20
20
|
output = console,
|
|
21
21
|
} = {}) {
|
|
22
22
|
const resolvedRoot = await resolveProjectRoot(projectRoot || process.cwd());
|
|
23
|
-
const
|
|
24
|
-
const
|
|
23
|
+
const sourceExtension = getProjectSourceExtension(resolvedRoot);
|
|
24
|
+
const createdApp = await ensureStartupFile(
|
|
25
|
+
resolvedRoot,
|
|
26
|
+
withSourceExtension('app', sourceExtension),
|
|
27
|
+
renderProjectAppJs(),
|
|
28
|
+
output,
|
|
29
|
+
);
|
|
30
|
+
const createdLoader = await ensureStartupFile(resolvedRoot, 'loader.cjs', renderProjectLoaderCjs(sourceExtension), output);
|
|
25
31
|
|
|
26
32
|
if (!createdApp && !createdLoader) {
|
|
27
33
|
output.log(`Startup entry files already exist in ${resolvedRoot}`);
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
renderProjectPackageJson,
|
|
11
11
|
renderProjectRoutes,
|
|
12
12
|
renderProjectSettings,
|
|
13
|
+
renderTsConfig,
|
|
14
|
+
withSourceExtension,
|
|
13
15
|
} from '../utils/scaffolds.js';
|
|
14
16
|
|
|
15
17
|
async function createSecret() {
|
|
@@ -37,32 +39,36 @@ async function assertCanCreateProject(projectDir) {
|
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
async function createBaseProjectFiles(projectRoot, projectName) {
|
|
42
|
+
async function createBaseProjectFiles(projectRoot, projectName, { typescript = false } = {}) {
|
|
41
43
|
const apps = [];
|
|
42
44
|
const appSecret = await createSecret();
|
|
45
|
+
const sourceExtension = typescript ? '.ts' : '.js';
|
|
43
46
|
|
|
44
47
|
await ensureDir(projectRoot);
|
|
45
48
|
await Promise.all([
|
|
46
49
|
ensureDir(path.join(projectRoot, 'apps')),
|
|
47
50
|
]);
|
|
48
51
|
|
|
49
|
-
await writeFile(path.join(projectRoot, 'app
|
|
50
|
-
await writeFile(path.join(projectRoot, 'loader.cjs'), renderProjectLoaderCjs());
|
|
51
|
-
await writeFile(path.join(projectRoot, 'package.json'), renderProjectPackageJson(projectName));
|
|
52
|
+
await writeFile(path.join(projectRoot, withSourceExtension('app', sourceExtension)), renderProjectAppJs());
|
|
53
|
+
await writeFile(path.join(projectRoot, 'loader.cjs'), renderProjectLoaderCjs(sourceExtension));
|
|
54
|
+
await writeFile(path.join(projectRoot, 'package.json'), renderProjectPackageJson(projectName, { typescript }));
|
|
52
55
|
await writeFile(path.join(projectRoot, '.gitignore'), renderProjectGitIgnore());
|
|
53
56
|
await writeFile(path.join(projectRoot, '.env'), renderProjectEnv(appSecret));
|
|
54
57
|
await writeFile(path.join(projectRoot, '.env.example'), renderEnvExample());
|
|
55
58
|
|
|
56
|
-
await writeFile(path.join(projectRoot, 'settings
|
|
57
|
-
await writeFile(path.join(projectRoot, 'routes
|
|
59
|
+
await writeFile(path.join(projectRoot, withSourceExtension('settings', sourceExtension)), renderProjectSettings(projectName, apps, appSecret));
|
|
60
|
+
await writeFile(path.join(projectRoot, withSourceExtension('routes', sourceExtension)), renderProjectRoutes());
|
|
61
|
+
if (typescript) {
|
|
62
|
+
await writeFile(path.join(projectRoot, 'tsconfig.json'), renderTsConfig());
|
|
63
|
+
}
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
export async function startProject({ projectName, cwd }) {
|
|
66
|
+
export async function startProject({ projectName, cwd, typescript = false }) {
|
|
61
67
|
ensureValidName(projectName, 'project');
|
|
62
68
|
|
|
63
69
|
const projectRoot = path.resolve(cwd, projectName);
|
|
64
70
|
await assertCanCreateProject(projectRoot);
|
|
65
|
-
await createBaseProjectFiles(projectRoot, projectName);
|
|
71
|
+
await createBaseProjectFiles(projectRoot, projectName, { typescript });
|
|
66
72
|
|
|
67
73
|
console.log(`AegisNode project created at ${projectRoot}`);
|
|
68
74
|
console.log('Next steps:');
|
package/src/cli/index.js
CHANGED
|
@@ -11,7 +11,7 @@ function printHelp() {
|
|
|
11
11
|
console.log(`AegisNode CLI
|
|
12
12
|
|
|
13
13
|
Usage:
|
|
14
|
-
aegisnode startproject <project-name>
|
|
14
|
+
aegisnode startproject <project-name> [--typescript]
|
|
15
15
|
aegisnode createapp <app-name> [--project <path>] [--mount </path>]
|
|
16
16
|
aegisnode fix [--app <app-name>] [--project <path>]
|
|
17
17
|
aegisnode generate <type> <name> --app <app-name> [--project <path>]
|
|
@@ -22,6 +22,7 @@ Usage:
|
|
|
22
22
|
|
|
23
23
|
Examples:
|
|
24
24
|
aegisnode startproject blog
|
|
25
|
+
aegisnode startproject blog --typescript
|
|
25
26
|
cd blog
|
|
26
27
|
npm install
|
|
27
28
|
aegisnode runserver
|
|
@@ -70,6 +71,11 @@ function parseFlags(tokens) {
|
|
|
70
71
|
continue;
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
if (token === '--typescript' || token === '--ts') {
|
|
75
|
+
flags.typescript = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
if (token === '-h' || token === '--help') {
|
|
74
80
|
flags.help = true;
|
|
75
81
|
continue;
|
|
@@ -101,7 +107,11 @@ export async function runCli(argv) {
|
|
|
101
107
|
if (!projectName) {
|
|
102
108
|
throw new Error('Missing project name. Usage: aegisnode startproject <project-name>');
|
|
103
109
|
}
|
|
104
|
-
await startProject({
|
|
110
|
+
await startProject({
|
|
111
|
+
projectName,
|
|
112
|
+
cwd: process.cwd(),
|
|
113
|
+
typescript: flags.typescript === true,
|
|
114
|
+
});
|
|
105
115
|
return;
|
|
106
116
|
}
|
|
107
117
|
|