aegisnode 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -12
- package/package.json +3 -2
- package/scripts/smoke-test.js +148 -0
- package/src/cli/commands/createapp.js +14 -176
- package/src/cli/commands/doctor.js +85 -25
- package/src/cli/commands/fixapp.js +70 -0
- package/src/cli/commands/generate.js +33 -28
- package/src/cli/commands/generateloader.js +10 -4
- package/src/cli/commands/startproject.js +14 -8
- package/src/cli/index.js +33 -3
- package/src/cli/utils/apps.js +260 -0
- package/src/cli/utils/project.js +14 -6
- package/src/cli/utils/scaffolds.js +92 -30
- package/src/index.js +1 -0
- package/src/runtime/config.js +9 -9
- package/src/runtime/kernel.js +49 -32
- package/src/runtime/typescript.js +21 -0
- package/src/utils/source-files.js +78 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ensureDir, ensureValidName, exists, normalizeMountPath, writeFile } from './fs.js';
|
|
4
|
+
import { importProjectModule } from '../../runtime/typescript.js';
|
|
5
|
+
import {
|
|
6
|
+
detectProjectSourceExtension,
|
|
7
|
+
resolveSourceFile,
|
|
8
|
+
} from '../../utils/source-files.js';
|
|
9
|
+
import {
|
|
10
|
+
renderAppModelTest,
|
|
11
|
+
renderAppModelsFile,
|
|
12
|
+
renderAppRoutes,
|
|
13
|
+
renderAppRoutesTest,
|
|
14
|
+
renderAppServiceTest,
|
|
15
|
+
renderAppServicesFile,
|
|
16
|
+
renderAppSubscribersFile,
|
|
17
|
+
renderAppUtilsFile,
|
|
18
|
+
renderAppValidatorTest,
|
|
19
|
+
renderAppValidatorsFile,
|
|
20
|
+
renderAppViewsFile,
|
|
21
|
+
renderSettingsApps,
|
|
22
|
+
withSourceExtension,
|
|
23
|
+
} from './scaffolds.js';
|
|
24
|
+
|
|
25
|
+
export const APPS_START = '// AEGIS_APPS_START';
|
|
26
|
+
export const APPS_END = '// AEGIS_APPS_END';
|
|
27
|
+
|
|
28
|
+
function renderAppEntries(apps) {
|
|
29
|
+
return apps
|
|
30
|
+
.map((app) => ` { name: ${JSON.stringify(app.name)}, mount: ${JSON.stringify(app.mount)} },`)
|
|
31
|
+
.join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function escapeRegExp(value) {
|
|
35
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function toImportName(appName) {
|
|
39
|
+
const safe = appName
|
|
40
|
+
.split(/[-_\s]+/)
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
43
|
+
.join('');
|
|
44
|
+
|
|
45
|
+
if (!safe) {
|
|
46
|
+
return 'appRoutes';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return `${safe.charAt(0).toLowerCase()}${safe.slice(1)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readDefaultExport(filePath) {
|
|
53
|
+
const loaded = await importProjectModule(filePath);
|
|
54
|
+
return loaded?.default;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function detectSettingsMode(projectRoot) {
|
|
58
|
+
const single = resolveSourceFile(path.join(projectRoot, 'settings'));
|
|
59
|
+
const split = resolveSourceFile(path.join(projectRoot, 'settings', 'apps'));
|
|
60
|
+
|
|
61
|
+
if (single && await exists(single)) {
|
|
62
|
+
return { mode: 'single', file: single };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (split && await exists(split)) {
|
|
66
|
+
return { mode: 'split', file: split };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new Error(`Not an AegisNode project root: missing settings.js/settings.ts (or legacy settings/apps.js/settings/apps.ts)`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function readAppsConfig(settingsMode) {
|
|
73
|
+
const normalizeApp = (entry) => {
|
|
74
|
+
if (!entry || typeof entry !== 'object' || typeof entry.name !== 'string') {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ensureValidName(entry.name, 'app');
|
|
79
|
+
return {
|
|
80
|
+
name: entry.name,
|
|
81
|
+
mount: normalizeMountPath(entry.mount || `/${entry.name}`),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (settingsMode.mode === 'single') {
|
|
86
|
+
const settings = await readDefaultExport(settingsMode.file);
|
|
87
|
+
const apps = settings?.apps;
|
|
88
|
+
|
|
89
|
+
if (!Array.isArray(apps)) {
|
|
90
|
+
throw new Error(`settings.js must export { apps: [] }. File: ${settingsMode.file}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return apps.map(normalizeApp).filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const apps = await readDefaultExport(settingsMode.file);
|
|
97
|
+
if (!Array.isArray(apps)) {
|
|
98
|
+
throw new Error(`settings/apps.js must export an array. File: ${settingsMode.file}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return apps.map(normalizeApp).filter(Boolean);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function updateSingleSettingsApps(settingsFile, apps) {
|
|
105
|
+
const current = await fs.readFile(settingsFile, 'utf8');
|
|
106
|
+
|
|
107
|
+
if (!current.includes(APPS_START) || !current.includes(APPS_END)) {
|
|
108
|
+
throw new Error(`settings.js is missing ${APPS_START}/${APPS_END} markers: ${settingsFile}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const replacement = `${APPS_START}\n${renderAppEntries(apps)}\n ${APPS_END}`;
|
|
112
|
+
const updated = current.replace(/\/\/ AEGIS_APPS_START[\s\S]*?\/\/ AEGIS_APPS_END/m, replacement);
|
|
113
|
+
|
|
114
|
+
await writeFile(settingsFile, updated);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function updateAppRegistry(projectRoot, apps, settingsMode) {
|
|
118
|
+
const sourceExtension = detectProjectSourceExtension(projectRoot);
|
|
119
|
+
|
|
120
|
+
if (settingsMode.mode === 'single') {
|
|
121
|
+
await updateSingleSettingsApps(settingsMode.file, apps);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await writeFile(path.join(projectRoot, 'settings', withSourceExtension('apps', sourceExtension)), renderSettingsApps(apps));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getAppScaffoldEntries(appName, sourceExtension = '.js') {
|
|
129
|
+
const appRoot = path.join('apps', appName);
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
target: path.join(appRoot, withSourceExtension('views', sourceExtension)),
|
|
133
|
+
content: renderAppViewsFile(appName, sourceExtension),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
target: path.join(appRoot, withSourceExtension('models', sourceExtension)),
|
|
137
|
+
content: renderAppModelsFile(appName),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
target: path.join(appRoot, withSourceExtension('validators', sourceExtension)),
|
|
141
|
+
content: renderAppValidatorsFile(appName),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
target: path.join(appRoot, withSourceExtension('services', sourceExtension)),
|
|
145
|
+
content: renderAppServicesFile(appName),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
target: path.join(appRoot, withSourceExtension('utils', sourceExtension)),
|
|
149
|
+
content: renderAppUtilsFile(),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
target: path.join(appRoot, withSourceExtension('subscribers', sourceExtension)),
|
|
153
|
+
content: renderAppSubscribersFile(appName),
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
target: path.join(appRoot, withSourceExtension('routes', sourceExtension)),
|
|
157
|
+
content: renderAppRoutes(appName, sourceExtension),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
target: path.join(appRoot, 'tests', withSourceExtension('models.test', sourceExtension)),
|
|
161
|
+
content: renderAppModelTest(appName, sourceExtension),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
target: path.join(appRoot, 'tests', withSourceExtension('validators.test', sourceExtension)),
|
|
165
|
+
content: renderAppValidatorTest(appName, sourceExtension),
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
target: path.join(appRoot, 'tests', withSourceExtension('services.test', sourceExtension)),
|
|
169
|
+
content: renderAppServiceTest(appName, sourceExtension),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
target: path.join(appRoot, 'tests', withSourceExtension('routes.test', sourceExtension)),
|
|
173
|
+
content: renderAppRoutesTest(appName, sourceExtension),
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function ensureAppScaffold(projectRoot, appName, { overwrite = false, sourceExtension = '.js' } = {}) {
|
|
179
|
+
ensureValidName(appName, 'app');
|
|
180
|
+
|
|
181
|
+
const appRoot = path.join(projectRoot, 'apps', appName);
|
|
182
|
+
await ensureDir(appRoot);
|
|
183
|
+
await ensureDir(path.join(appRoot, 'tests'));
|
|
184
|
+
|
|
185
|
+
const written = [];
|
|
186
|
+
const skipped = [];
|
|
187
|
+
|
|
188
|
+
for (const entry of getAppScaffoldEntries(appName, sourceExtension)) {
|
|
189
|
+
const target = path.join(projectRoot, entry.target);
|
|
190
|
+
if (!overwrite && await exists(target)) {
|
|
191
|
+
skipped.push(target);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await writeFile(target, entry.content);
|
|
196
|
+
written.push(target);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
appRoot,
|
|
201
|
+
written,
|
|
202
|
+
skipped,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function updateProjectRoutesFile(projectRoot, appName, mountPath, sourceExtension = null) {
|
|
207
|
+
const effectiveExtension = sourceExtension || detectProjectSourceExtension(projectRoot);
|
|
208
|
+
const routesFile = resolveSourceFile(path.join(projectRoot, 'routes'), [effectiveExtension]) || resolveSourceFile(path.join(projectRoot, 'routes'));
|
|
209
|
+
if (!routesFile || !(await exists(routesFile))) {
|
|
210
|
+
// Keep backward compatibility for legacy projects that still use routes/index.js.
|
|
211
|
+
return {
|
|
212
|
+
routesFile: routesFile || path.join(projectRoot, withSourceExtension('routes', effectiveExtension)),
|
|
213
|
+
updatedImport: false,
|
|
214
|
+
updatedRoute: false,
|
|
215
|
+
skipped: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const importName = toImportName(appName);
|
|
220
|
+
const importLine = `import ${importName} from './apps/${appName}/routes${effectiveExtension}';`;
|
|
221
|
+
const routeLine = ` route.use(${JSON.stringify(mountPath)}, ${importName});`;
|
|
222
|
+
const routePattern = new RegExp(`route\\.use\\([^\\n]*,\\s*${escapeRegExp(importName)}\\s*\\);`);
|
|
223
|
+
|
|
224
|
+
let content = await fs.readFile(routesFile, 'utf8');
|
|
225
|
+
let updatedImport = false;
|
|
226
|
+
let updatedRoute = false;
|
|
227
|
+
|
|
228
|
+
if (!content.includes(importLine)) {
|
|
229
|
+
if (content.includes('// AEGIS_APP_IMPORTS_START') && content.includes('// AEGIS_APP_IMPORTS_END')) {
|
|
230
|
+
content = content.replace('// AEGIS_APP_IMPORTS_END', `${importLine}\n// AEGIS_APP_IMPORTS_END`);
|
|
231
|
+
} else {
|
|
232
|
+
content = `${importLine}\n${content}`;
|
|
233
|
+
}
|
|
234
|
+
updatedImport = true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!routePattern.test(content)) {
|
|
238
|
+
if (content.includes('// AEGIS_PROJECT_APP_ROUTES_START') && content.includes('// AEGIS_PROJECT_APP_ROUTES_END')) {
|
|
239
|
+
content = content.replace(' // AEGIS_PROJECT_APP_ROUTES_END', `${routeLine}\n // AEGIS_PROJECT_APP_ROUTES_END`);
|
|
240
|
+
updatedRoute = true;
|
|
241
|
+
} else {
|
|
242
|
+
const match = content.match(/register\s*\([^)]*\)\s*{[\s\S]*?\n\s*}/m);
|
|
243
|
+
if (match) {
|
|
244
|
+
content = content.replace(match[0], `${match[0]}\n${routeLine}`);
|
|
245
|
+
updatedRoute = true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (updatedImport || updatedRoute) {
|
|
251
|
+
await writeFile(routesFile, content);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
routesFile,
|
|
256
|
+
updatedImport,
|
|
257
|
+
updatedRoute,
|
|
258
|
+
skipped: false,
|
|
259
|
+
};
|
|
260
|
+
}
|
package/src/cli/utils/project.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { detectProjectSourceExtension, resolveSourceFile, resolveSourceIndexFile } from '../../utils/source-files.js';
|
|
4
4
|
|
|
5
5
|
export async function hasRoutesFile(projectRoot) {
|
|
6
|
-
return (
|
|
7
|
-
|
|
6
|
+
return Boolean(
|
|
7
|
+
resolveSourceFile(path.join(projectRoot, 'routes'))
|
|
8
|
+
|| resolveSourceIndexFile(path.join(projectRoot, 'routes')),
|
|
9
|
+
);
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export async function hasSettingsFile(projectRoot) {
|
|
11
|
-
return (
|
|
12
|
-
|
|
13
|
-
|| (
|
|
13
|
+
return Boolean(
|
|
14
|
+
resolveSourceFile(path.join(projectRoot, 'settings'))
|
|
15
|
+
|| resolveSourceIndexFile(path.join(projectRoot, 'settings'))
|
|
16
|
+
|| resolveSourceFile(path.join(projectRoot, 'settings', 'apps')),
|
|
17
|
+
);
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export async function isProjectRoot(projectRoot) {
|
|
@@ -65,3 +69,7 @@ export async function resolveProjectRoot(projectRootHint) {
|
|
|
65
69
|
|
|
66
70
|
throw new Error(`Could not find an AegisNode project from ${startDir}. Run inside project or use --project <path>.`);
|
|
67
71
|
}
|
|
72
|
+
|
|
73
|
+
export function getProjectSourceExtension(projectRoot) {
|
|
74
|
+
return detectProjectSourceExtension(projectRoot);
|
|
75
|
+
}
|
|
@@ -6,31 +6,42 @@ export function toPascalCase(value) {
|
|
|
6
6
|
.join('');
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export function withSourceExtension(baseName, extension = '.js') {
|
|
10
|
+
return `${baseName}${extension}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
function renderAppEntries(apps, indent = ' ') {
|
|
10
14
|
return apps
|
|
11
15
|
.map((app) => `${indent}{ name: ${JSON.stringify(app.name)}, mount: ${JSON.stringify(app.mount)} },`)
|
|
12
16
|
.join('\n');
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
export function renderProjectPackageJson(projectName) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
test: 'node --test',
|
|
26
|
-
},
|
|
27
|
-
dependencies: {
|
|
28
|
-
aegisnode: '^0.1.0',
|
|
29
|
-
},
|
|
19
|
+
export function renderProjectPackageJson(projectName, { typescript = false } = {}) {
|
|
20
|
+
const packageJson = {
|
|
21
|
+
name: projectName,
|
|
22
|
+
version: '1.0.0',
|
|
23
|
+
private: true,
|
|
24
|
+
type: 'module',
|
|
25
|
+
scripts: {
|
|
26
|
+
dev: 'aegisnode runserver',
|
|
27
|
+
start: 'node loader.cjs',
|
|
28
|
+
test: typescript ? 'node --import tsx/esm --test' : 'node --test',
|
|
30
29
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
dependencies: {
|
|
31
|
+
aegisnode: '^0.1.0',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (typescript) {
|
|
36
|
+
packageJson.scripts.typecheck = 'tsc --noEmit';
|
|
37
|
+
packageJson.devDependencies = {
|
|
38
|
+
'@types/node': '^24.0.0',
|
|
39
|
+
tsx: '^4.21.0',
|
|
40
|
+
typescript: '^5.9.3',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return `${JSON.stringify(packageJson, null, 2)}\n`;
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
export function renderProjectAppJs() {
|
|
@@ -45,7 +56,18 @@ runProject({ rootDir: __dirname });
|
|
|
45
56
|
`;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
export function renderProjectLoaderCjs() {
|
|
59
|
+
export function renderProjectLoaderCjs(sourceExtension = '.js') {
|
|
60
|
+
if (sourceExtension === '.ts') {
|
|
61
|
+
return `import('aegisnode').then(async ({ registerTypeScriptRuntime }) => {
|
|
62
|
+
await registerTypeScriptRuntime();
|
|
63
|
+
return import('./app.ts');
|
|
64
|
+
}).catch((error) => {
|
|
65
|
+
console.error(error?.stack || error?.message || String(error));
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
});
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
return `import('./app.js').catch((error) => {
|
|
50
72
|
console.error(error?.stack || error?.message || String(error));
|
|
51
73
|
process.exitCode = 1;
|
|
@@ -53,6 +75,33 @@ export function renderProjectLoaderCjs() {
|
|
|
53
75
|
`;
|
|
54
76
|
}
|
|
55
77
|
|
|
78
|
+
export function renderTsConfig() {
|
|
79
|
+
return `${JSON.stringify(
|
|
80
|
+
{
|
|
81
|
+
compilerOptions: {
|
|
82
|
+
target: 'ES2022',
|
|
83
|
+
module: 'NodeNext',
|
|
84
|
+
moduleResolution: 'NodeNext',
|
|
85
|
+
strict: true,
|
|
86
|
+
esModuleInterop: true,
|
|
87
|
+
skipLibCheck: true,
|
|
88
|
+
forceConsistentCasingInFileNames: true,
|
|
89
|
+
noEmit: true,
|
|
90
|
+
allowImportingTsExtensions: true,
|
|
91
|
+
types: ['node'],
|
|
92
|
+
},
|
|
93
|
+
include: [
|
|
94
|
+
'app.ts',
|
|
95
|
+
'settings.ts',
|
|
96
|
+
'routes.ts',
|
|
97
|
+
'apps/**/*.ts',
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
null,
|
|
101
|
+
2,
|
|
102
|
+
)}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
export function renderProjectSettings(projectName, apps, appSecret = '') {
|
|
57
106
|
return `export default {
|
|
58
107
|
appName: '${projectName}',
|
|
@@ -177,7 +226,7 @@ export function renderController(appName) {
|
|
|
177
226
|
return renderView(appName);
|
|
178
227
|
}
|
|
179
228
|
|
|
180
|
-
export function renderAppViewsFile(appName) {
|
|
229
|
+
export function renderAppViewsFile(appName, sourceExtension = '.js') {
|
|
181
230
|
const className = `${toPascalCase(appName)}View`;
|
|
182
231
|
return `class ${className} {
|
|
183
232
|
static home(_context, req, res, next) {
|
|
@@ -403,6 +452,19 @@ export default {
|
|
|
403
452
|
`;
|
|
404
453
|
}
|
|
405
454
|
|
|
455
|
+
export function renderAppUtilsFile() {
|
|
456
|
+
return `/**
|
|
457
|
+
* App-local pure utilities.
|
|
458
|
+
*
|
|
459
|
+
* Use this file for small reusable functions that belong only to this app.
|
|
460
|
+
* Keep database access in models, business workflows in services, and request
|
|
461
|
+
* validation/sanitization in validators.
|
|
462
|
+
*/
|
|
463
|
+
|
|
464
|
+
export {};
|
|
465
|
+
`;
|
|
466
|
+
}
|
|
467
|
+
|
|
406
468
|
export function renderAppSubscribersFile(appName) {
|
|
407
469
|
return `export default function register${toPascalCase(appName)}Subscribers({ events, logger }) {
|
|
408
470
|
events.subscribe('app.booted', ({ appName }) => {
|
|
@@ -457,9 +519,9 @@ export default {
|
|
|
457
519
|
`;
|
|
458
520
|
}
|
|
459
521
|
|
|
460
|
-
export function renderAppRoutes(appName) {
|
|
522
|
+
export function renderAppRoutes(appName, sourceExtension = '.js') {
|
|
461
523
|
const viewClass = `${toPascalCase(appName)}View`;
|
|
462
|
-
return `import ${viewClass} from './views
|
|
524
|
+
return `import ${viewClass} from './views${sourceExtension}';
|
|
463
525
|
|
|
464
526
|
export default {
|
|
465
527
|
appName: ${JSON.stringify(appName)},
|
|
@@ -479,10 +541,10 @@ export default {
|
|
|
479
541
|
`;
|
|
480
542
|
}
|
|
481
543
|
|
|
482
|
-
export function renderAppModelTest(appName) {
|
|
544
|
+
export function renderAppModelTest(appName, sourceExtension = '.js') {
|
|
483
545
|
return `import test from 'node:test';
|
|
484
546
|
import assert from 'node:assert/strict';
|
|
485
|
-
import models from '../models
|
|
547
|
+
import models from '../models${sourceExtension}';
|
|
486
548
|
|
|
487
549
|
test('${appName} model exposes basic CRUD methods', async () => {
|
|
488
550
|
const Model = models[${JSON.stringify(appName)}];
|
|
@@ -504,10 +566,10 @@ test('${appName} model exposes basic CRUD methods', async () => {
|
|
|
504
566
|
`;
|
|
505
567
|
}
|
|
506
568
|
|
|
507
|
-
export function renderAppValidatorTest(appName) {
|
|
569
|
+
export function renderAppValidatorTest(appName, sourceExtension = '.js') {
|
|
508
570
|
return `import test from 'node:test';
|
|
509
571
|
import assert from 'node:assert/strict';
|
|
510
|
-
import validators from '../validators
|
|
572
|
+
import validators from '../validators${sourceExtension}';
|
|
511
573
|
|
|
512
574
|
test('${appName} validator validates id and payload', () => {
|
|
513
575
|
const Validator = validators[${JSON.stringify(appName)}];
|
|
@@ -524,10 +586,10 @@ test('${appName} validator validates id and payload', () => {
|
|
|
524
586
|
`;
|
|
525
587
|
}
|
|
526
588
|
|
|
527
|
-
export function renderAppServiceTest(appName) {
|
|
589
|
+
export function renderAppServiceTest(appName, sourceExtension = '.js') {
|
|
528
590
|
return `import test from 'node:test';
|
|
529
591
|
import assert from 'node:assert/strict';
|
|
530
|
-
import services from '../services
|
|
592
|
+
import services from '../services${sourceExtension}';
|
|
531
593
|
|
|
532
594
|
test('${appName} service delegates to model layer', async () => {
|
|
533
595
|
const Service = services[${JSON.stringify(appName)}];
|
|
@@ -567,10 +629,10 @@ test('${appName} service delegates to model layer', async () => {
|
|
|
567
629
|
`;
|
|
568
630
|
}
|
|
569
631
|
|
|
570
|
-
export function renderAppRoutesTest(appName) {
|
|
632
|
+
export function renderAppRoutesTest(appName, sourceExtension = '.js') {
|
|
571
633
|
return `import test from 'node:test';
|
|
572
634
|
import assert from 'node:assert/strict';
|
|
573
|
-
import routes from '../routes
|
|
635
|
+
import routes from '../routes${sourceExtension}';
|
|
574
636
|
|
|
575
637
|
test('${appName} routes register expected CRUD endpoints', () => {
|
|
576
638
|
const calls = [];
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { createLogger } from './runtime/logger.js';
|
|
|
5
5
|
export { deepMerge, normalizeApps } from './runtime/config.js';
|
|
6
6
|
export { createAuthManager, normalizeAuthConfig, createAuthGuard } from './runtime/auth.js';
|
|
7
7
|
export { createMailManager, normalizeMailConfig } from './runtime/mail.js';
|
|
8
|
+
export { registerTypeScriptRuntime } from './runtime/typescript.js';
|
|
8
9
|
export {
|
|
9
10
|
money,
|
|
10
11
|
number,
|
package/src/runtime/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveSourceFile, resolveSourceIndexFile } from '../utils/source-files.js';
|
|
4
|
+
import { importProjectModule } from './typescript.js';
|
|
4
5
|
|
|
5
6
|
const BASE_PROCESS_ENV = new Map(Object.entries(process.env));
|
|
6
7
|
const FRAMEWORK_LOADED_ENV_KEYS = new Set();
|
|
@@ -419,12 +420,11 @@ export function defaultConfig(rootDir) {
|
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
async function importDefaultIfExists(filePath) {
|
|
422
|
-
if (!fs.existsSync(filePath)) {
|
|
423
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
423
424
|
return null;
|
|
424
425
|
}
|
|
425
426
|
|
|
426
|
-
const
|
|
427
|
-
const loaded = await import(moduleUrl);
|
|
427
|
+
const loaded = await importProjectModule(filePath);
|
|
428
428
|
return loaded?.default ?? null;
|
|
429
429
|
}
|
|
430
430
|
|
|
@@ -432,12 +432,12 @@ export async function loadProjectConfig(rootDir, logger = null) {
|
|
|
432
432
|
loadEnvironmentFiles(rootDir, logger);
|
|
433
433
|
const config = defaultConfig(rootDir);
|
|
434
434
|
|
|
435
|
-
const settingsFile = path.join(rootDir, 'settings
|
|
435
|
+
const settingsFile = resolveSourceFile(path.join(rootDir, 'settings'));
|
|
436
436
|
const settingsDir = path.join(rootDir, 'settings');
|
|
437
|
-
const indexFile =
|
|
438
|
-
const dbFile = path.join(settingsDir, 'db
|
|
439
|
-
const cacheFile = path.join(settingsDir, 'cache
|
|
440
|
-
const appsFile = path.join(settingsDir, 'apps
|
|
437
|
+
const indexFile = resolveSourceIndexFile(settingsDir);
|
|
438
|
+
const dbFile = resolveSourceFile(path.join(settingsDir, 'db'));
|
|
439
|
+
const cacheFile = resolveSourceFile(path.join(settingsDir, 'cache'));
|
|
440
|
+
const appsFile = resolveSourceFile(path.join(settingsDir, 'apps'));
|
|
441
441
|
|
|
442
442
|
const [settingsConfig, indexConfig, dbConfig, cacheConfig, appsConfig] = await Promise.all([
|
|
443
443
|
importDefaultIfExists(settingsFile),
|