aegisnode 0.0.5 → 0.1.1

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.
@@ -136,14 +136,29 @@ 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
- await startProject({ projectName, cwd: sandboxRoot });
141
+ await fs.mkdir(projectRoot, { recursive: true });
142
+ await startProject({ projectName, cwd: projectRoot });
141
143
  const generatedProjectEnv = await fs.readFile(path.join(projectRoot, '.env'), 'utf8');
142
144
  assert.match(generatedProjectEnv, /^APP_SECRET=.{16,}$/m);
143
145
  const generatedAppSecret = generatedProjectEnv.match(/^APP_SECRET=(.+)$/m)?.[1]?.trim();
144
146
  assert.ok(generatedAppSecret);
145
147
  const generatedSettings = await fs.readFile(path.join(projectRoot, 'settings.js'), 'utf8');
148
+ assert.ok(generatedSettings.includes("staticDir: 'public'"));
149
+ assert.ok(generatedSettings.includes("templates: {\n enabled: true,\n engine: 'ejs',\n dir: 'templates',\n base: 'base',\n }"));
146
150
  assert.ok(generatedSettings.includes(`appSecret: process.env.APP_SECRET || ${JSON.stringify(generatedAppSecret)}`));
151
+ const generatedConfig = await loadProjectConfig(projectRoot);
152
+ assert.equal(generatedConfig.security.ddos.maxRequests, 300);
153
+ await fs.access(path.join(projectRoot, 'public'));
154
+ await fs.access(path.join(projectRoot, 'templates'));
155
+ const nonEmptyProjectRoot = path.join(sandboxRoot, 'non-empty-project');
156
+ await fs.mkdir(nonEmptyProjectRoot, { recursive: true });
157
+ await fs.writeFile(path.join(nonEmptyProjectRoot, 'keep.txt'), 'occupied', 'utf8');
158
+ await assert.rejects(
159
+ () => startProject({ projectName: 'busy', cwd: nonEmptyProjectRoot }),
160
+ /Current directory is not empty/,
161
+ );
147
162
  await assert.rejects(
148
163
  () => runProject({
149
164
  rootDir: projectRoot,
@@ -155,9 +170,73 @@ async function main() {
155
170
  /started with "aegisnode runserver"/,
156
171
  );
157
172
 
173
+ const tsProjectName = 'forumts';
174
+ const tsProjectRoot = path.join(typescriptSandboxRoot, tsProjectName);
175
+ await fs.mkdir(tsProjectRoot, { recursive: true });
176
+ await startProject({ projectName: tsProjectName, cwd: tsProjectRoot, typescript: true });
177
+ await fs.access(path.join(tsProjectRoot, 'app.ts'));
178
+ await fs.access(path.join(tsProjectRoot, 'settings.ts'));
179
+ await fs.access(path.join(tsProjectRoot, 'routes.ts'));
180
+ await fs.access(path.join(tsProjectRoot, 'tsconfig.json'));
181
+ await fs.access(path.join(tsProjectRoot, 'public'));
182
+ await fs.access(path.join(tsProjectRoot, 'templates'));
183
+ const tsPackageJson = JSON.parse(await fs.readFile(path.join(tsProjectRoot, 'package.json'), 'utf8'));
184
+ assert.equal(tsPackageJson.scripts.test, 'node --import tsx/esm --test');
185
+ assert.equal(tsPackageJson.scripts.typecheck, 'tsc --noEmit');
186
+ assert.equal(tsPackageJson.devDependencies.tsx, '^4.21.0');
187
+ assert.equal(tsPackageJson.devDependencies.typescript, '^5.9.3');
188
+ const tsConfig = await loadProjectConfig(tsProjectRoot);
189
+ assert.equal(tsConfig.appName, 'forumts');
190
+ await createApp({
191
+ appName: 'users',
192
+ projectRoot: tsProjectRoot,
193
+ mount: '/users',
194
+ });
195
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'routes.ts'));
196
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'views.ts'));
197
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'models.ts'));
198
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'validators.ts'));
199
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'services.ts'));
200
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'utils.ts'));
201
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'subscribers.ts'));
202
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'models.test.ts'));
203
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'validators.test.ts'));
204
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'services.test.ts'));
205
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'tests', 'routes.test.ts'));
206
+ await generateArtifact({
207
+ type: 'view',
208
+ name: 'profile',
209
+ appName: 'users',
210
+ projectRoot: tsProjectRoot,
211
+ });
212
+ await fs.access(path.join(tsProjectRoot, 'apps', 'users', 'profile.view.ts'));
213
+ await generateArtifact({
214
+ type: 'route',
215
+ name: 'profile',
216
+ appName: 'users',
217
+ projectRoot: tsProjectRoot,
218
+ });
219
+ const tsUsersRoutesFile = await fs.readFile(path.join(tsProjectRoot, 'apps', 'users', 'routes.ts'), 'utf8');
220
+ assert.match(tsUsersRoutesFile, /import ProfileView from '\.\/profile\.view\.ts';/);
221
+ assert.match(tsUsersRoutesFile, /route\.get\('\/profile', ProfileView\.index\);/);
222
+ const tsDoctorReport = await runDoctor({
223
+ projectRoot: tsProjectRoot,
224
+ failOnError: true,
225
+ output: {
226
+ log() {},
227
+ },
228
+ });
229
+ assert.equal(tsDoctorReport.summary.errors, 0);
230
+ const tsServer = await runServer({
231
+ projectRoot: tsProjectRoot,
232
+ port: 0,
233
+ });
234
+ await tsServer.stop();
235
+
158
236
  const envProjectName = 'envdemo';
159
237
  const envProjectRoot = path.join(envSandboxRoot, envProjectName);
160
- await startProject({ projectName: envProjectName, cwd: envSandboxRoot });
238
+ await fs.mkdir(envProjectRoot, { recursive: true });
239
+ await startProject({ projectName: envProjectName, cwd: envProjectRoot });
161
240
  await fs.writeFile(
162
241
  path.join(envProjectRoot, 'settings.js'),
163
242
  `export default {
@@ -210,7 +289,8 @@ async function main() {
210
289
 
211
290
  const dotenvProjectName = 'dotenvdemo';
212
291
  const dotenvProjectRoot = path.join(dotenvSandboxRoot, dotenvProjectName);
213
- await startProject({ projectName: dotenvProjectName, cwd: dotenvSandboxRoot });
292
+ await fs.mkdir(dotenvProjectRoot, { recursive: true });
293
+ await startProject({ projectName: dotenvProjectName, cwd: dotenvProjectRoot });
214
294
  await fs.writeFile(
215
295
  path.join(dotenvProjectRoot, '.env'),
216
296
  `AEGIS_TEST_HOST=127.0.0.1
@@ -245,7 +325,8 @@ AEGIS_TEST_APP_SECRET=test-dotenv-secret
245
325
 
246
326
  const httpsProjectName = 'httpsdemo';
247
327
  const httpsProjectRoot = path.join(httpsSandboxRoot, httpsProjectName);
248
- await startProject({ projectName: httpsProjectName, cwd: httpsSandboxRoot });
328
+ await fs.mkdir(httpsProjectRoot, { recursive: true });
329
+ await startProject({ projectName: httpsProjectName, cwd: httpsProjectRoot });
249
330
  await fs.mkdir(path.join(httpsProjectRoot, 'certs'), { recursive: true });
250
331
  await fs.writeFile(
251
332
  path.join(httpsProjectRoot, 'certs', 'localhost-key.pem'),
@@ -328,7 +409,8 @@ dkcqnJD4SGWVeG+KhA==
328
409
 
329
410
  const proxyProjectName = 'proxydemo';
330
411
  const proxyProjectRoot = path.join(proxySandboxRoot, proxyProjectName);
331
- await startProject({ projectName: proxyProjectName, cwd: proxySandboxRoot });
412
+ await fs.mkdir(proxyProjectRoot, { recursive: true });
413
+ await startProject({ projectName: proxyProjectName, cwd: proxyProjectRoot });
332
414
  await fs.writeFile(
333
415
  path.join(proxyProjectRoot, 'routes.js'),
334
416
  `export default {\n register(route) {\n route.get('/secure-check', (req, res) => {\n res.json({ secure: req.secure, protocol: req.protocol });\n });\n },\n};\n`,
@@ -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() {
@@ -33,40 +35,45 @@ async function assertCanCreateProject(projectDir) {
33
35
 
34
36
  const empty = await isDirectoryEmpty(projectDir);
35
37
  if (!empty) {
36
- throw new Error(`Directory already exists and is not empty: ${projectDir}`);
38
+ throw new Error(`Current directory is not empty: ${projectDir}. Create and enter an empty project directory before running "aegisnode startproject".`);
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')),
50
+ ensureDir(path.join(projectRoot, 'public')),
51
+ ensureDir(path.join(projectRoot, 'templates')),
47
52
  ]);
48
53
 
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));
54
+ await writeFile(path.join(projectRoot, withSourceExtension('app', sourceExtension)), renderProjectAppJs());
55
+ await writeFile(path.join(projectRoot, 'loader.cjs'), renderProjectLoaderCjs(sourceExtension));
56
+ await writeFile(path.join(projectRoot, 'package.json'), renderProjectPackageJson(projectName, { typescript }));
52
57
  await writeFile(path.join(projectRoot, '.gitignore'), renderProjectGitIgnore());
53
58
  await writeFile(path.join(projectRoot, '.env'), renderProjectEnv(appSecret));
54
59
  await writeFile(path.join(projectRoot, '.env.example'), renderEnvExample());
55
60
 
56
- await writeFile(path.join(projectRoot, 'settings.js'), renderProjectSettings(projectName, apps, appSecret));
57
- await writeFile(path.join(projectRoot, 'routes.js'), renderProjectRoutes());
61
+ await writeFile(path.join(projectRoot, withSourceExtension('settings', sourceExtension)), renderProjectSettings(projectName, apps, appSecret));
62
+ await writeFile(path.join(projectRoot, withSourceExtension('routes', sourceExtension)), renderProjectRoutes());
63
+ if (typescript) {
64
+ await writeFile(path.join(projectRoot, 'tsconfig.json'), renderTsConfig());
65
+ }
58
66
  }
59
67
 
60
- export async function startProject({ projectName, cwd }) {
68
+ export async function startProject({ projectName, cwd, typescript = false }) {
61
69
  ensureValidName(projectName, 'project');
62
70
 
63
- const projectRoot = path.resolve(cwd, projectName);
71
+ const projectRoot = path.resolve(cwd);
64
72
  await assertCanCreateProject(projectRoot);
65
- await createBaseProjectFiles(projectRoot, projectName);
73
+ await createBaseProjectFiles(projectRoot, projectName, { typescript });
66
74
 
67
- console.log(`AegisNode project created at ${projectRoot}`);
75
+ console.log(`AegisNode project created in ${projectRoot}`);
68
76
  console.log('Next steps:');
69
- console.log(` cd ${projectName}`);
70
77
  console.log(' npm install');
71
78
  console.log(' aegisnode runserver');
72
79
  }
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>]
@@ -21,17 +21,18 @@ Usage:
21
21
  aegisnode updatedeps [--project <path>]
22
22
 
23
23
  Examples:
24
+ mkdir blog && cd blog
24
25
  aegisnode startproject blog
25
- cd blog
26
+ aegisnode startproject blog --typescript
26
27
  npm install
27
28
  aegisnode runserver
28
29
  aegisnode createapp users
29
30
  aegisnode fix --app users
30
31
  aegisnode generate view user --app users
31
32
  aegisnode generate validator user --app users
32
- aegisnode generateloader --project blog
33
- aegisnode doctor --app users --project blog
34
- aegisnode updatedeps --project blog
33
+ aegisnode generateloader
34
+ aegisnode doctor --app users
35
+ aegisnode updatedeps
35
36
  `);
36
37
  }
37
38
 
@@ -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