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.
- package/README.md +190 -218
- package/package.json +12 -11
- package/scripts/smoke-test.js +87 -5
- 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 +19 -12
- package/src/cli/index.js +16 -6
- package/src/cli/utils/apps.js +41 -34
- package/src/cli/utils/project.js +14 -6
- package/src/cli/utils/scaffolds.js +87 -31
- package/src/index.js +1 -0
- package/src/runtime/config.js +10 -10
- package/src/runtime/kernel.js +49 -32
- package/src/runtime/typescript.js +21 -0
- package/src/utils/source-files.js +78 -0
package/scripts/smoke-test.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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() {
|
|
@@ -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(`
|
|
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
|
|
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
|
|
57
|
-
await writeFile(path.join(projectRoot, 'routes
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
33
|
-
aegisnode doctor --app users
|
|
34
|
-
aegisnode updatedeps
|
|
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({
|
|
110
|
+
await startProject({
|
|
111
|
+
projectName,
|
|
112
|
+
cwd: process.cwd(),
|
|
113
|
+
typescript: flags.typescript === true,
|
|
114
|
+
});
|
|
105
115
|
return;
|
|
106
116
|
}
|
|
107
117
|
|