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 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
- It does not create `public/` or `logs/`; create your own folders and set them in `settings.js`.
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.js`
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.5",
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
  }
@@ -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 routes.js is missing; app "${appName}" cannot be mounted centrally.`);
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.js`;
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 routes.js is missing import for app "${appName}".`);
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 routes.js is missing route.use(...) mount for app "${appName}".`);
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 appEntryPath = path.join(rootDir, 'app.js');
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('app.js exists.');
131
+ collector.ok(`${path.basename(appEntryPath)} exists.`);
126
132
  } else if (env === 'production') {
127
- collector.error('app.js is missing for production startup. Run "aegisnode generateloader" to restore startup entry files.');
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('app.js is missing. Run "aegisnode generateloader" to restore startup entry files.');
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, { overwrite: false });
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 routes.js registration for app "${appName}"`);
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 = await exists(path.join(projectRoot, 'settings.js'));
26
- const hasLegacySettings = (await exists(path.join(projectRoot, 'settings', 'index.js')))
27
- || (await exists(path.join(projectRoot, 'settings', 'apps.js')));
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.js');
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', 'index.js');
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.js or routes/index.js`);
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.js';
130
- const flatViewPath = path.join(appRoot, `${routeName}.view.js`);
131
- const nestedViewPath = path.join(appRoot, 'views', `${routeName}.view.js`);
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.js`
137
- : `../${routeName}.view.js`;
140
+ ? `./${routeName}.view${sourceExtension}`
141
+ : `../${routeName}.view${sourceExtension}`;
138
142
  } else if (await exists(nestedViewPath)) {
139
143
  importPath = usesFlatRoutesFile
140
- ? `./views/${routeName}.view.js`
141
- : `../views/${routeName}.view.js`;
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.js`);
203
+ return path.join(appRoot, `${name}.view${sourceExtension}`);
200
204
  case 'model':
201
- return path.join(appRoot, `${name}.model.js`);
205
+ return path.join(appRoot, `${name}.model${sourceExtension}`);
202
206
  case 'service':
203
- return path.join(appRoot, `${name}.service.js`);
207
+ return path.join(appRoot, `${name}.service${sourceExtension}`);
204
208
  case 'validator':
205
- return path.join(appRoot, `${name}.validator.js`);
209
+ return path.join(appRoot, `${name}.validator${sourceExtension}`);
206
210
  case 'subscriber':
207
- return path.join(appRoot, `${name}.subscriber.js`);
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 createdApp = await ensureStartupFile(resolvedRoot, 'app.js', renderProjectAppJs(), output);
24
- const createdLoader = await ensureStartupFile(resolvedRoot, 'loader.cjs', renderProjectLoaderCjs(), output);
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.js'), renderProjectAppJs());
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.js'), renderProjectSettings(projectName, apps, appSecret));
57
- await writeFile(path.join(projectRoot, 'routes.js'), renderProjectRoutes());
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({ projectName, cwd: process.cwd() });
110
+ await startProject({
111
+ projectName,
112
+ cwd: process.cwd(),
113
+ typescript: flags.typescript === true,
114
+ });
105
115
  return;
106
116
  }
107
117