@sculptor/cli 0.1.14 → 0.2.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 +28 -19
- package/dist/cli.js +91 -26
- package/dist/cli.js.map +1 -1
- package/dist/config-commands.d.ts +6 -0
- package/dist/config-commands.js +106 -0
- package/dist/config-commands.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins.d.ts +24 -0
- package/dist/plugins.js +26 -0
- package/dist/plugins.js.map +1 -0
- package/dist/scaffold.d.ts +1 -29
- package/dist/scaffold.js +1 -820
- package/dist/scaffold.js.map +1 -1
- package/dist/template-engine.d.ts +26 -0
- package/dist/template-engine.js +82 -0
- package/dist/template-engine.js.map +1 -0
- package/package.json +6 -4
package/dist/scaffold.js
CHANGED
|
@@ -1,821 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { ensureDir, writeTextFile } from "./fs.js";
|
|
4
|
-
const toPascalCase = (value) => value
|
|
5
|
-
.split(/[^a-zA-Z0-9]+/)
|
|
6
|
-
.filter(Boolean)
|
|
7
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
8
|
-
.join("");
|
|
9
|
-
const toCamelCase = (value) => {
|
|
10
|
-
const pascal = toPascalCase(value);
|
|
11
|
-
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
12
|
-
};
|
|
13
|
-
const toKebabCase = (value) => value
|
|
14
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
15
|
-
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
16
|
-
.replace(/^-+|-+$/g, "")
|
|
17
|
-
.toLowerCase();
|
|
18
|
-
const normalizeRelativePath = (value) => value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
19
|
-
const resolveFileStem = (name, outputDir) => {
|
|
20
|
-
if (name) {
|
|
21
|
-
return name;
|
|
22
|
-
}
|
|
23
|
-
const trimmedDir = normalizeRelativePath(outputDir);
|
|
24
|
-
const parts = trimmedDir.split("/").filter(Boolean);
|
|
25
|
-
const inferred = parts[parts.length - 1];
|
|
26
|
-
return inferred ?? "index";
|
|
27
|
-
};
|
|
28
|
-
const controllerFileName = (name) => `${name}.controller.ts`;
|
|
29
|
-
const serviceFileName = (name) => `${name}.service.ts`;
|
|
30
|
-
const moduleFileName = (name) => `${name}.module.ts`;
|
|
31
|
-
const middlewareFileName = (name) => `${name}.middleware.ts`;
|
|
32
|
-
const routeFileName = (name) => `${name}.routes.ts`;
|
|
33
|
-
const typeFileName = (name, variant) => {
|
|
34
|
-
const suffix = variant === "type" ? "type" : variant;
|
|
35
|
-
return `${name}.${suffix}.ts`;
|
|
36
|
-
};
|
|
37
|
-
const specFileName = (name, suffix) => `${name}.${suffix}.spec.ts`;
|
|
38
|
-
const specImportPath = (sourcePath) => normalizeRelativePath(path.posix.relative("src/tests", sourcePath)).replace(/\.ts$/, ".js");
|
|
39
|
-
const resolveGeneratorOutputDir = (kind, outputDir) => {
|
|
40
|
-
if (outputDir) {
|
|
41
|
-
return normalizeRelativePath(outputDir);
|
|
42
|
-
}
|
|
43
|
-
switch (kind) {
|
|
44
|
-
case "controller":
|
|
45
|
-
return "src/app/controllers";
|
|
46
|
-
case "service":
|
|
47
|
-
return "src/app/services";
|
|
48
|
-
case "module":
|
|
49
|
-
return "src/app/modules";
|
|
50
|
-
case "middleware":
|
|
51
|
-
return "src/app/middlewares";
|
|
52
|
-
case "type":
|
|
53
|
-
return "src/app/types";
|
|
54
|
-
case "route":
|
|
55
|
-
return "src/app/routes";
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
const devScriptFor = (devServer) => devServer === "tsx"
|
|
59
|
-
? "tsx src/main.ts"
|
|
60
|
-
: "nodemon --watch src --ext ts --exec tsx src/main.ts";
|
|
61
|
-
const devDependenciesFor = (devServer) => devServer === "tsx"
|
|
62
|
-
? ""
|
|
63
|
-
: ` "nodemon": "^3.1.9",\n`;
|
|
64
|
-
const rootPackageJsonTemplate = (appName, version, devServer) => `{
|
|
65
|
-
"name": "${toKebabCase(appName)}",
|
|
66
|
-
"version": "${version}",
|
|
67
|
-
"private": true,
|
|
68
|
-
"type": "module",
|
|
69
|
-
"scripts": {
|
|
70
|
-
"start": "tsx src/main.ts",
|
|
71
|
-
"dev": "${devScriptFor(devServer)}",
|
|
72
|
-
"build": "tsc -p tsconfig.json",
|
|
73
|
-
"lint": "eslint . --ext .ts",
|
|
74
|
-
"test": "sc test"
|
|
75
|
-
},
|
|
76
|
-
"dependencies": {
|
|
77
|
-
"express": "^4.21.2",
|
|
78
|
-
"reflect-metadata": "^0.2.2"
|
|
79
|
-
},
|
|
80
|
-
"devDependencies": {
|
|
81
|
-
"@types/express": "^4.17.23",
|
|
82
|
-
"@types/node": "^22.15.3",
|
|
83
|
-
"@types/supertest": "^6.0.3",
|
|
84
|
-
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
|
85
|
-
"@typescript-eslint/parser": "^8.30.1",
|
|
86
|
-
"eslint": "^9.20.0",
|
|
87
|
-
${devDependenciesFor(devServer)}
|
|
88
|
-
"supertest": "^7.1.0",
|
|
89
|
-
"tsx": "^4.19.4",
|
|
90
|
-
"typescript": "^5.8.3",
|
|
91
|
-
"vitest": "^2.1.8"
|
|
92
|
-
}
|
|
93
|
-
}`;
|
|
94
|
-
const rootReadmeTemplate = (appName) => `# ${appName}
|
|
95
|
-
|
|
96
|
-
## Scripts
|
|
97
|
-
|
|
98
|
-
- \`npm run start\` - start the app
|
|
99
|
-
- \`npm run dev\` - start the app in development mode
|
|
100
|
-
- \`npm run build\` - compile the app
|
|
101
|
-
- \`npm run lint\` - lint the source
|
|
102
|
-
- \`npm run test\` - run the test suite through \`sc test\`
|
|
103
|
-
|
|
104
|
-
## Logging
|
|
105
|
-
|
|
106
|
-
The scaffold includes \`@sculptor/paws\` and turns logging on by default.
|
|
107
|
-
|
|
108
|
-
- \`logging.enabled\` controls whether anything prints
|
|
109
|
-
- \`logging.dogMode\` toggles dog personalities vs standard labels
|
|
110
|
-
|
|
111
|
-
The default scaffold boots in dog mode so the logger is visible immediately.
|
|
112
|
-
|
|
113
|
-
## Test Harness
|
|
114
|
-
|
|
115
|
-
The scaffold generates a Vitest registry under \`src/tests\`:
|
|
116
|
-
|
|
117
|
-
- \`src/tests/registry.ts\` collects generated spec entrypoints
|
|
118
|
-
- \`src/tests/runner.ts\` imports the registry entries
|
|
119
|
-
- \`src/tests/runner.spec.ts\` is the Vitest entrypoint used by \`sc test\`
|
|
120
|
-
|
|
121
|
-
When you add new specs under \`src/tests\`, rerun \`sc test\` and the harness will pick them up.
|
|
122
|
-
`;
|
|
123
|
-
const rootTsconfigTemplate = `{
|
|
124
|
-
"compilerOptions": {
|
|
125
|
-
"target": "ES2020",
|
|
126
|
-
"module": "NodeNext",
|
|
127
|
-
"moduleResolution": "NodeNext",
|
|
128
|
-
"lib": ["ES2020"],
|
|
129
|
-
"strict": true,
|
|
130
|
-
"declaration": true,
|
|
131
|
-
"sourceMap": true,
|
|
132
|
-
"rootDir": "src",
|
|
133
|
-
"outDir": "dist",
|
|
134
|
-
"esModuleInterop": true,
|
|
135
|
-
"allowSyntheticDefaultImports": true,
|
|
136
|
-
"experimentalDecorators": true,
|
|
137
|
-
"emitDecoratorMetadata": true,
|
|
138
|
-
"useDefineForClassFields": false,
|
|
139
|
-
"skipLibCheck": true,
|
|
140
|
-
"types": ["node"]
|
|
141
|
-
},
|
|
142
|
-
"include": ["src/**/*.ts"],
|
|
143
|
-
"exclude": ["dist", "node_modules"]
|
|
144
|
-
}`;
|
|
145
|
-
const sculptorTemplate = (mode, devServer, frameworkLock, testing) => `{
|
|
146
|
-
"project": {
|
|
147
|
-
"srcRoot": "src",
|
|
148
|
-
"entryFile": "main.ts",
|
|
149
|
-
"devServer": "${devServer}"
|
|
150
|
-
},
|
|
151
|
-
"logging": {
|
|
152
|
-
"enabled": true,
|
|
153
|
-
"dogMode": true
|
|
154
|
-
},
|
|
155
|
-
"routing": {
|
|
156
|
-
"style": "${mode}"
|
|
157
|
-
},
|
|
158
|
-
"testing": {
|
|
159
|
-
"generate": ${testing.generate ? "true" : "false"},
|
|
160
|
-
"framework": "${testing.framework}"
|
|
161
|
-
},
|
|
162
|
-
"frameworkLock": ${frameworkLock ? "true" : "false"}
|
|
163
|
-
}`;
|
|
164
|
-
const eslintConfigTemplate = `import parser from "@typescript-eslint/parser";
|
|
165
|
-
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
|
166
|
-
|
|
167
|
-
export default [
|
|
168
|
-
{
|
|
169
|
-
files: ["**/*.ts"],
|
|
170
|
-
languageOptions: {
|
|
171
|
-
parser,
|
|
172
|
-
parserOptions: {
|
|
173
|
-
ecmaVersion: "latest",
|
|
174
|
-
sourceType: "module"
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
plugins: {
|
|
178
|
-
"@typescript-eslint": tsPlugin
|
|
179
|
-
},
|
|
180
|
-
rules: {
|
|
181
|
-
"no-unused-vars": "off",
|
|
182
|
-
"@typescript-eslint/no-unused-vars": [
|
|
183
|
-
"error",
|
|
184
|
-
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" }
|
|
185
|
-
],
|
|
186
|
-
"@typescript-eslint/no-explicit-any": "off"
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
ignores: ["dist/**", "node_modules/**"]
|
|
191
|
-
}
|
|
192
|
-
];
|
|
193
|
-
`;
|
|
194
|
-
const vitestConfigTemplate = `import { defineConfig } from "vitest/config";
|
|
195
|
-
|
|
196
|
-
export default defineConfig({
|
|
197
|
-
test: {
|
|
198
|
-
environment: "node",
|
|
199
|
-
globals: true
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
`;
|
|
203
|
-
const mainSpecTemplate = `import type { Server } from "node:http";
|
|
204
|
-
import { fileURLToPath } from "node:url";
|
|
205
|
-
|
|
206
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
207
|
-
|
|
208
|
-
import { startApp } from "@sculptor/core";
|
|
209
|
-
import { registry } from "../registry.js";
|
|
210
|
-
|
|
211
|
-
describe("app bootstrap", () => {
|
|
212
|
-
let server: Server | undefined;
|
|
213
|
-
|
|
214
|
-
afterEach(async () => {
|
|
215
|
-
vi.restoreAllMocks();
|
|
216
|
-
|
|
217
|
-
if (!server) {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
await new Promise<void>((resolve) => {
|
|
222
|
-
server?.close(() => resolve());
|
|
223
|
-
});
|
|
224
|
-
server = undefined;
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("starts the application", async () => {
|
|
228
|
-
const logs: string[] = [];
|
|
229
|
-
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
|
|
230
|
-
logs.push(args.map(String).join(" "));
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
server = await startApp({
|
|
234
|
-
registry,
|
|
235
|
-
rootDir: fileURLToPath(new URL("../..", import.meta.url)),
|
|
236
|
-
port: 0
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
expect(logs.join("\\n")).toContain("Local: http://localhost:");
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
`;
|
|
243
|
-
const appTsconfigTemplate = `{
|
|
244
|
-
"compilerOptions": {
|
|
245
|
-
"target": "ES2020",
|
|
246
|
-
"module": "NodeNext",
|
|
247
|
-
"moduleResolution": "NodeNext",
|
|
248
|
-
"lib": ["ES2020"],
|
|
249
|
-
"strict": true,
|
|
250
|
-
"declaration": true,
|
|
251
|
-
"sourceMap": true,
|
|
252
|
-
"rootDir": "src",
|
|
253
|
-
"outDir": "dist",
|
|
254
|
-
"esModuleInterop": true,
|
|
255
|
-
"allowSyntheticDefaultImports": true,
|
|
256
|
-
"experimentalDecorators": true,
|
|
257
|
-
"emitDecoratorMetadata": true,
|
|
258
|
-
"useDefineForClassFields": false,
|
|
259
|
-
"skipLibCheck": true,
|
|
260
|
-
"types": ["node"]
|
|
261
|
-
},
|
|
262
|
-
"include": ["src/**/*.ts"],
|
|
263
|
-
"exclude": ["dist", "node_modules"]
|
|
264
|
-
}`;
|
|
265
|
-
const propsTemplate = `{
|
|
266
|
-
"app": {
|
|
267
|
-
"port": 3000,
|
|
268
|
-
"prefix": "/api"
|
|
269
|
-
}
|
|
270
|
-
}`;
|
|
271
|
-
const mainTemplate = `import { paws } from "@sculptor/paws";
|
|
272
|
-
import { startApp } from "@sculptor/core";
|
|
273
|
-
import { fileURLToPath } from "node:url";
|
|
274
|
-
import { registry } from "./registry.js";
|
|
275
|
-
|
|
276
|
-
const appRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
277
|
-
|
|
278
|
-
process.env.SCULPTOR_ROOT_DIR = appRoot;
|
|
279
|
-
paws.boot();
|
|
280
|
-
|
|
281
|
-
void startApp({ registry, rootDir: appRoot });
|
|
282
|
-
`;
|
|
283
|
-
const registryTemplate = (mode) => {
|
|
284
|
-
if (mode === "decorator") {
|
|
285
|
-
return `import { HealthController } from "./app/controllers/health.controller.js";
|
|
286
|
-
|
|
287
|
-
export const registry = {
|
|
288
|
-
controllers: [HealthController],
|
|
289
|
-
routes: [],
|
|
290
|
-
services: []
|
|
291
|
-
};
|
|
292
|
-
`;
|
|
293
|
-
}
|
|
294
|
-
if (mode === "functional") {
|
|
295
|
-
return `import { healthRoutes } from "./app/routes/health.routes.js";
|
|
296
|
-
|
|
297
|
-
export const registry = {
|
|
298
|
-
controllers: [],
|
|
299
|
-
routes: [healthRoutes],
|
|
300
|
-
services: []
|
|
301
|
-
};
|
|
302
|
-
`;
|
|
303
|
-
}
|
|
304
|
-
return `import { HealthController } from "./app/controllers/health.controller.js";
|
|
305
|
-
import { healthRoutes } from "./app/routes/health.routes.js";
|
|
306
|
-
|
|
307
|
-
export const registry = {
|
|
308
|
-
controllers: [HealthController],
|
|
309
|
-
routes: [healthRoutes],
|
|
310
|
-
services: []
|
|
311
|
-
};
|
|
312
|
-
`;
|
|
313
|
-
};
|
|
314
|
-
const healthControllerTemplate = `import { Controller, Get } from "@sculptor/core";
|
|
315
|
-
|
|
316
|
-
@Controller("/health")
|
|
317
|
-
export class HealthController {
|
|
318
|
-
@Get("/")
|
|
319
|
-
health() {
|
|
320
|
-
return { status: "ok" };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
@Get("/ping")
|
|
324
|
-
ping() {
|
|
325
|
-
return { message: "pong" };
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
`;
|
|
329
|
-
const healthRoutesTemplate = `import { Router } from "express";
|
|
330
|
-
|
|
331
|
-
export const healthRoutes = Router();
|
|
332
|
-
|
|
333
|
-
healthRoutes.get("/health", (_req, res) => {
|
|
334
|
-
res.json({ status: "ok" });
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
healthRoutes.get("/ping", (_req, res) => {
|
|
338
|
-
res.json({ message: "pong" });
|
|
339
|
-
});
|
|
340
|
-
`;
|
|
341
|
-
const healthServiceTemplate = `export class HealthService {
|
|
342
|
-
status() {
|
|
343
|
-
return { status: "ok" };
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
`;
|
|
347
|
-
const healthModuleTemplate = `export class HealthModule {}
|
|
348
|
-
`;
|
|
349
|
-
const healthControllerSpecTemplate = `import { describe, expect, it } from "vitest";
|
|
350
|
-
|
|
351
|
-
import { HealthController } from "../app/controllers/health.controller.js";
|
|
352
|
-
|
|
353
|
-
describe("HealthController", () => {
|
|
354
|
-
it("returns the expected health payload", () => {
|
|
355
|
-
const controller = new HealthController();
|
|
356
|
-
|
|
357
|
-
expect(controller.health()).toEqual({ status: "ok" });
|
|
358
|
-
expect(controller.ping()).toEqual({ message: "pong" });
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
`;
|
|
362
|
-
const healthRoutesSpecTemplate = `import express from "express";
|
|
363
|
-
import request from "supertest";
|
|
364
|
-
import { describe, expect, it } from "vitest";
|
|
365
|
-
|
|
366
|
-
import { healthRoutes } from "../app/routes/health.routes.js";
|
|
367
|
-
|
|
368
|
-
describe("healthRoutes", () => {
|
|
369
|
-
it("serves the health endpoint", async () => {
|
|
370
|
-
const app = express();
|
|
371
|
-
app.use(healthRoutes);
|
|
372
|
-
|
|
373
|
-
await request(app).get("/health").expect(200).expect({ status: "ok" });
|
|
374
|
-
await request(app).get("/ping").expect(200).expect({ message: "pong" });
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
`;
|
|
378
|
-
const testRegistryTemplate = (specs) => `export const testRegistry = [
|
|
379
|
-
${specs.map((spec) => ` "${spec}"`).join(",\n")}
|
|
380
|
-
] as const;
|
|
381
|
-
`;
|
|
382
|
-
const testRunnerTemplate = () => `import { testRegistry } from "./registry.js";
|
|
383
|
-
|
|
384
|
-
for (const spec of testRegistry) {
|
|
385
|
-
await import(spec);
|
|
386
|
-
}
|
|
387
|
-
`;
|
|
388
|
-
const testHarnessSpecTemplate = () => `import "./runner.js";
|
|
389
|
-
`;
|
|
390
|
-
const collectSpecPaths = (dir, rootDir = dir) => {
|
|
391
|
-
if (!fs.existsSync(dir)) {
|
|
392
|
-
return [];
|
|
393
|
-
}
|
|
394
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
395
|
-
const specs = [];
|
|
396
|
-
for (const entry of entries) {
|
|
397
|
-
const fullPath = path.join(dir, entry.name);
|
|
398
|
-
if (entry.isDirectory()) {
|
|
399
|
-
specs.push(...collectSpecPaths(fullPath, rootDir));
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
if (!entry.isFile()) {
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
if (!entry.name.endsWith(".spec.ts")) {
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
const relativePath = normalizeRelativePath(path.relative(rootDir, fullPath));
|
|
409
|
-
if (relativePath === "registry.ts" ||
|
|
410
|
-
relativePath === "runner.ts" ||
|
|
411
|
-
relativePath === "runner.spec.ts") {
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
specs.push(`./${relativePath.replace(/\.ts$/, ".js")}`);
|
|
415
|
-
}
|
|
416
|
-
return specs.sort();
|
|
417
|
-
};
|
|
418
|
-
export const syncTestHarness = (targetDir) => {
|
|
419
|
-
const testsDir = path.join(targetDir, "src", "tests");
|
|
420
|
-
ensureDir(testsDir);
|
|
421
|
-
const specs = collectSpecPaths(testsDir);
|
|
422
|
-
writeTextFile(path.join(testsDir, "registry.ts"), testRegistryTemplate(specs));
|
|
423
|
-
writeTextFile(path.join(testsDir, "runner.ts"), testRunnerTemplate());
|
|
424
|
-
writeTextFile(path.join(testsDir, "runner.spec.ts"), testHarnessSpecTemplate());
|
|
425
|
-
};
|
|
426
|
-
const controllerSpecTemplate = (name, sourcePath) => {
|
|
427
|
-
const importPath = specImportPath(sourcePath);
|
|
428
|
-
return `import { describe, expect, it } from "vitest";
|
|
429
|
-
|
|
430
|
-
import { ${toPascalCase(name)}Controller } from "${importPath}";
|
|
431
|
-
|
|
432
|
-
describe("${toPascalCase(name)}Controller", () => {
|
|
433
|
-
it("returns the expected payload", () => {
|
|
434
|
-
const controller = new ${toPascalCase(name)}Controller();
|
|
435
|
-
|
|
436
|
-
expect(controller.findAll()).toEqual({ resource: "${name}" });
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
`;
|
|
440
|
-
};
|
|
441
|
-
const serviceSpecTemplate = (name, sourcePath) => {
|
|
442
|
-
const importPath = specImportPath(sourcePath);
|
|
443
|
-
return `import { describe, expect, it } from "vitest";
|
|
444
|
-
|
|
445
|
-
import { ${toPascalCase(name)}Service } from "${importPath}";
|
|
446
|
-
|
|
447
|
-
describe("${toPascalCase(name)}Service", () => {
|
|
448
|
-
it("can be instantiated", () => {
|
|
449
|
-
expect(new ${toPascalCase(name)}Service()).toBeInstanceOf(${toPascalCase(name)}Service);
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
`;
|
|
453
|
-
};
|
|
454
|
-
const routeSpecTemplate = (name, sourcePath) => {
|
|
455
|
-
const importPath = specImportPath(sourcePath);
|
|
456
|
-
return `import express from "express";
|
|
457
|
-
import request from "supertest";
|
|
458
|
-
import { describe, expect, it } from "vitest";
|
|
459
|
-
|
|
460
|
-
import { ${toCamelCase(name)}Routes } from "${importPath}";
|
|
461
|
-
|
|
462
|
-
describe("${toCamelCase(name)}Routes", () => {
|
|
463
|
-
it("serves the resource endpoint", async () => {
|
|
464
|
-
const app = express();
|
|
465
|
-
app.use(${toCamelCase(name)}Routes);
|
|
466
|
-
|
|
467
|
-
await request(app).get("/${name}").expect(200).expect({ resource: "${name}" });
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
`;
|
|
471
|
-
};
|
|
472
|
-
const middlewareSpecTemplate = (name, sourcePath) => {
|
|
473
|
-
const importPath = specImportPath(sourcePath);
|
|
474
|
-
return `import { describe, expect, it, vi } from "vitest";
|
|
475
|
-
|
|
476
|
-
import { ${toCamelCase(name)}Middleware } from "${importPath}";
|
|
477
|
-
|
|
478
|
-
describe("${toCamelCase(name)}Middleware", () => {
|
|
479
|
-
it("calls next", () => {
|
|
480
|
-
const next = vi.fn();
|
|
481
|
-
|
|
482
|
-
${toCamelCase(name)}Middleware({} as never, {} as never, next);
|
|
483
|
-
|
|
484
|
-
expect(next).toHaveBeenCalledTimes(1);
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
`;
|
|
488
|
-
};
|
|
489
|
-
const createDecoratorController = (name, outputDir = "src/app/controllers") => ({
|
|
490
|
-
[`${normalizeRelativePath(outputDir)}/${controllerFileName(name)}`]: `import { Controller, Get } from "@sculptor/core";
|
|
491
|
-
|
|
492
|
-
@Controller("/${name}")
|
|
493
|
-
export class ${toPascalCase(name)}Controller {
|
|
494
|
-
@Get("/")
|
|
495
|
-
findAll() {
|
|
496
|
-
return { resource: "${name}" };
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
`
|
|
500
|
-
});
|
|
501
|
-
const createServiceResource = (name) => ({
|
|
502
|
-
[`src/app/services/${serviceFileName(name)}`]: `export class ${toPascalCase(name)}Service {}
|
|
503
|
-
`
|
|
504
|
-
});
|
|
505
|
-
const createModuleResource = (name) => ({
|
|
506
|
-
[`src/app/modules/${moduleFileName(name)}`]: `export class ${toPascalCase(name)}Module {}
|
|
507
|
-
`
|
|
508
|
-
});
|
|
509
|
-
const createMiddlewareResource = (name) => ({
|
|
510
|
-
[`src/app/middlewares/${middlewareFileName(name)}`]: `import type { NextFunction, Request, Response } from "express";
|
|
511
|
-
|
|
512
|
-
export const ${toCamelCase(name)}Middleware = (
|
|
513
|
-
_req: Request,
|
|
514
|
-
_res: Response,
|
|
515
|
-
next: NextFunction
|
|
516
|
-
) => {
|
|
517
|
-
next();
|
|
518
|
-
};
|
|
519
|
-
`
|
|
520
|
-
});
|
|
521
|
-
const createRouteResource = (name) => ({
|
|
522
|
-
[`src/app/routes/${routeFileName(name)}`]: `import { Router } from "express";
|
|
523
|
-
|
|
524
|
-
export const ${toCamelCase(name)}Routes = Router();
|
|
525
|
-
|
|
526
|
-
${toCamelCase(name)}Routes.get("/${name}", (_req, res) => {
|
|
527
|
-
res.json({ resource: "${name}" });
|
|
528
|
-
});
|
|
529
|
-
`
|
|
530
|
-
});
|
|
531
|
-
const createHandlerResource = (name) => ({
|
|
532
|
-
[`src/app/handlers/${name}.handler.ts`]: `export const ${toPascalCase(name)}Handler = (
|
|
533
|
-
_req: unknown,
|
|
534
|
-
res: { json: (value: unknown) => void }
|
|
535
|
-
) => {
|
|
536
|
-
res.json({ resource: "${name}" });
|
|
537
|
-
};
|
|
538
|
-
`
|
|
539
|
-
});
|
|
540
|
-
const createTypeResource = (name, variant, outputDir) => {
|
|
541
|
-
const fileName = typeFileName(name, variant);
|
|
542
|
-
const targetDir = normalizeRelativePath(outputDir);
|
|
543
|
-
if (variant === "interface") {
|
|
544
|
-
return {
|
|
545
|
-
[`${targetDir}/${fileName}`]: `export interface ${toPascalCase(name)} {
|
|
546
|
-
id?: string;
|
|
547
|
-
}
|
|
548
|
-
`
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
if (variant === "class") {
|
|
552
|
-
return {
|
|
553
|
-
[`${targetDir}/${fileName}`]: `export class ${toPascalCase(name)} {}
|
|
554
|
-
`
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
if (variant === "enum") {
|
|
558
|
-
return {
|
|
559
|
-
[`${targetDir}/${fileName}`]: `export enum ${toPascalCase(name)} {
|
|
560
|
-
Default = "default"
|
|
561
|
-
}
|
|
562
|
-
`
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
return {
|
|
566
|
-
[`${targetDir}/${fileName}`]: `export type ${toPascalCase(name)} = Record<string, unknown>;
|
|
567
|
-
`
|
|
568
|
-
};
|
|
569
|
-
};
|
|
570
|
-
const appShellFiles = (metadata) => ({
|
|
571
|
-
"package.json": rootPackageJsonTemplate(metadata.appName, metadata.version, metadata.devServer),
|
|
572
|
-
"README.md": rootReadmeTemplate(metadata.appName),
|
|
573
|
-
"tsconfig.json": rootTsconfigTemplate,
|
|
574
|
-
"sculptor.json": sculptorTemplate(metadata.mode, metadata.devServer, metadata.frameworkLock, metadata.testing),
|
|
575
|
-
"props.json": propsTemplate,
|
|
576
|
-
"src/main.ts": mainTemplate,
|
|
577
|
-
"src/registry.ts": registryTemplate(metadata.mode)
|
|
578
|
-
});
|
|
579
|
-
export const scaffoldProject = (metadata, targetDir) => {
|
|
580
|
-
ensureDir(targetDir);
|
|
581
|
-
const rootFiles = {
|
|
582
|
-
...appShellFiles(metadata),
|
|
583
|
-
"eslint.config.js": eslintConfigTemplate,
|
|
584
|
-
"vitest.config.ts": vitestConfigTemplate,
|
|
585
|
-
"src/app/services/health.service.ts": healthServiceTemplate,
|
|
586
|
-
"src/app/modules/health.module.ts": healthModuleTemplate
|
|
587
|
-
};
|
|
588
|
-
if (metadata.mode === "decorator" || metadata.mode === "hybrid") {
|
|
589
|
-
rootFiles["src/app/controllers/health.controller.ts"] =
|
|
590
|
-
healthControllerTemplate;
|
|
591
|
-
}
|
|
592
|
-
if (metadata.mode === "functional" || metadata.mode === "hybrid") {
|
|
593
|
-
rootFiles["src/app/routes/health.routes.ts"] = healthRoutesTemplate;
|
|
594
|
-
}
|
|
595
|
-
if (metadata.testing.generate) {
|
|
596
|
-
rootFiles["src/tests/main.spec.ts"] = mainSpecTemplate;
|
|
597
|
-
if (metadata.mode === "decorator" || metadata.mode === "hybrid") {
|
|
598
|
-
rootFiles["src/tests/health.controller.spec.ts"] = healthControllerSpecTemplate;
|
|
599
|
-
}
|
|
600
|
-
if (metadata.mode === "functional" || metadata.mode === "hybrid") {
|
|
601
|
-
rootFiles["src/tests/health.routes.spec.ts"] = healthRoutesSpecTemplate;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
for (const [relativePath, content] of Object.entries(rootFiles)) {
|
|
605
|
-
writeTextFile(path.join(targetDir, relativePath), content);
|
|
606
|
-
}
|
|
607
|
-
for (const dir of [
|
|
608
|
-
"src/app/controllers",
|
|
609
|
-
"src/app/routes",
|
|
610
|
-
"src/app/services",
|
|
611
|
-
"src/app/modules",
|
|
612
|
-
"src/app/middlewares",
|
|
613
|
-
"src/app/handlers",
|
|
614
|
-
"src/tests"
|
|
615
|
-
]) {
|
|
616
|
-
ensureDir(path.join(targetDir, dir));
|
|
617
|
-
}
|
|
618
|
-
if (metadata.testing.generate) {
|
|
619
|
-
syncTestHarness(targetDir);
|
|
620
|
-
}
|
|
621
|
-
};
|
|
622
|
-
export const generateResourceFiles = (kind, name, mode, devServer = "tsx", outputDir, typeVariant = "type", testingGenerate = false) => {
|
|
623
|
-
const resolvedOutputDir = resolveGeneratorOutputDir(kind, outputDir);
|
|
624
|
-
const resolvedName = resolveFileStem(name, resolvedOutputDir);
|
|
625
|
-
const functionalRouteDir = normalizeRelativePath(outputDir ?? "src/app/routes");
|
|
626
|
-
const functionalHandlerDir = normalizeRelativePath(outputDir ?? "src/app/handlers");
|
|
627
|
-
const sourceFiles = {};
|
|
628
|
-
switch (kind) {
|
|
629
|
-
case "controller":
|
|
630
|
-
if (mode === "functional") {
|
|
631
|
-
Object.assign(sourceFiles, {
|
|
632
|
-
[`${functionalRouteDir}/${routeFileName(resolvedName)}`]: createRouteResource(resolvedName)[`src/app/routes/${routeFileName(resolvedName)}`],
|
|
633
|
-
[`${functionalHandlerDir}/${resolvedName}.handler.ts`]: createHandlerResource(resolvedName)[`src/app/handlers/${resolvedName}.handler.ts`]
|
|
634
|
-
});
|
|
635
|
-
break;
|
|
636
|
-
}
|
|
637
|
-
if (mode === "hybrid") {
|
|
638
|
-
Object.assign(sourceFiles, {
|
|
639
|
-
...createDecoratorController(resolvedName, resolvedOutputDir),
|
|
640
|
-
[`${functionalRouteDir}/${routeFileName(resolvedName)}`]: createRouteResource(resolvedName)[`src/app/routes/${routeFileName(resolvedName)}`],
|
|
641
|
-
[`${functionalHandlerDir}/${resolvedName}.handler.ts`]: createHandlerResource(resolvedName)[`src/app/handlers/${resolvedName}.handler.ts`]
|
|
642
|
-
});
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
Object.assign(sourceFiles, createDecoratorController(resolvedName, resolvedOutputDir));
|
|
646
|
-
break;
|
|
647
|
-
case "service":
|
|
648
|
-
Object.assign(sourceFiles, {
|
|
649
|
-
[`${resolvedOutputDir}/${serviceFileName(resolvedName)}`]: createServiceResource(resolvedName)[`src/app/services/${serviceFileName(resolvedName)}`]
|
|
650
|
-
});
|
|
651
|
-
break;
|
|
652
|
-
case "module":
|
|
653
|
-
Object.assign(sourceFiles, {
|
|
654
|
-
[`${resolvedOutputDir}/${moduleFileName(resolvedName)}`]: createModuleResource(resolvedName)[`src/app/modules/${moduleFileName(resolvedName)}`]
|
|
655
|
-
});
|
|
656
|
-
break;
|
|
657
|
-
case "middleware":
|
|
658
|
-
Object.assign(sourceFiles, {
|
|
659
|
-
[`${resolvedOutputDir}/${middlewareFileName(resolvedName)}`]: createMiddlewareResource(resolvedName)[`src/app/middlewares/${middlewareFileName(resolvedName)}`]
|
|
660
|
-
});
|
|
661
|
-
break;
|
|
662
|
-
case "type":
|
|
663
|
-
Object.assign(sourceFiles, createTypeResource(resolvedName, typeVariant, resolvedOutputDir));
|
|
664
|
-
break;
|
|
665
|
-
case "route":
|
|
666
|
-
if (mode === "functional" || mode === "hybrid") {
|
|
667
|
-
Object.assign(sourceFiles, {
|
|
668
|
-
[`${resolvedOutputDir}/${routeFileName(resolvedName)}`]: createRouteResource(resolvedName)[`src/app/routes/${routeFileName(resolvedName)}`]
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
break;
|
|
672
|
-
}
|
|
673
|
-
if (!testingGenerate) {
|
|
674
|
-
return sourceFiles;
|
|
675
|
-
}
|
|
676
|
-
const testFiles = {};
|
|
677
|
-
for (const [filePath] of Object.entries(sourceFiles)) {
|
|
678
|
-
const normalizedPath = normalizeRelativePath(filePath);
|
|
679
|
-
const baseName = path.posix.basename(normalizedPath, ".ts");
|
|
680
|
-
if (normalizedPath.endsWith(".controller.ts")) {
|
|
681
|
-
const resourceName = baseName.replace(/\.controller$/, "");
|
|
682
|
-
testFiles[`src/tests/${specFileName(resourceName, "controller")}`] =
|
|
683
|
-
controllerSpecTemplate(resourceName, normalizedPath);
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
if (normalizedPath.endsWith(".service.ts")) {
|
|
687
|
-
const resourceName = baseName.replace(/\.service$/, "");
|
|
688
|
-
testFiles[`src/tests/${specFileName(resourceName, "service")}`] =
|
|
689
|
-
serviceSpecTemplate(resourceName, normalizedPath);
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
if (normalizedPath.endsWith(".routes.ts")) {
|
|
693
|
-
const resourceName = baseName.replace(/\.routes$/, "");
|
|
694
|
-
testFiles[`src/tests/${specFileName(resourceName, "routes")}`] =
|
|
695
|
-
routeSpecTemplate(resourceName, normalizedPath);
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
if (normalizedPath.endsWith(".middleware.ts")) {
|
|
699
|
-
const resourceName = baseName.replace(/\.middleware$/, "");
|
|
700
|
-
testFiles[`src/tests/${specFileName(resourceName, "middleware")}`] =
|
|
701
|
-
middlewareSpecTemplate(resourceName, normalizedPath);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
return {
|
|
705
|
-
...sourceFiles,
|
|
706
|
-
...testFiles
|
|
707
|
-
};
|
|
708
|
-
};
|
|
709
|
-
export const writeGeneratedFiles = (targetDir, files) => {
|
|
710
|
-
for (const [relativePath, content] of Object.entries(files)) {
|
|
711
|
-
writeTextFile(path.join(targetDir, relativePath), content);
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
export const readModeFromFlags = (flags, fallback) => {
|
|
715
|
-
if (flags.includes("--functional")) {
|
|
716
|
-
return "functional";
|
|
717
|
-
}
|
|
718
|
-
if (flags.includes("--decorator")) {
|
|
719
|
-
return "decorator";
|
|
720
|
-
}
|
|
721
|
-
if (flags.includes("--hybrid")) {
|
|
722
|
-
return "hybrid";
|
|
723
|
-
}
|
|
724
|
-
return fallback;
|
|
725
|
-
};
|
|
726
|
-
export const parseGenerateMode = (mode, fallback) => mode === "functional" || mode === "decorator" || mode === "hybrid" ? mode : fallback;
|
|
727
|
-
export const controllerHelp = `# Controller Generator
|
|
728
|
-
|
|
729
|
-
## Usage
|
|
730
|
-
|
|
731
|
-
\`\`\`bash
|
|
732
|
-
sc generate controller user
|
|
733
|
-
sc g c user
|
|
734
|
-
\nsc generate controller user in src/app/controllers
|
|
735
|
-
\nsc g c user --functional
|
|
736
|
-
\`\`\`
|
|
737
|
-
|
|
738
|
-
## Output
|
|
739
|
-
|
|
740
|
-
- \`controllers/*.controller.ts\`
|
|
741
|
-
- in functional mode, generates \`routes/*.routes.ts\` and \`handlers/*.handler.ts\`
|
|
742
|
-
`;
|
|
743
|
-
export const moduleHelp = `# Module Generator
|
|
744
|
-
|
|
745
|
-
## Usage
|
|
746
|
-
|
|
747
|
-
\`\`\`bash
|
|
748
|
-
sc generate module user
|
|
749
|
-
sc g mo user
|
|
750
|
-
\`\`\`
|
|
751
|
-
`;
|
|
752
|
-
export const middlewareHelp = `# Middleware Generator
|
|
753
|
-
|
|
754
|
-
## Usage
|
|
755
|
-
|
|
756
|
-
\`\`\`bash
|
|
757
|
-
sc generate middleware auth
|
|
758
|
-
sc g mw auth
|
|
759
|
-
\`\`\`
|
|
760
|
-
`;
|
|
761
|
-
export const typeHelp = `# Type Generator
|
|
762
|
-
|
|
763
|
-
## Usage
|
|
764
|
-
|
|
765
|
-
\`\`\`bash
|
|
766
|
-
sc generate type user
|
|
767
|
-
sc g t user
|
|
768
|
-
sc g t user -i
|
|
769
|
-
sc g t user -c
|
|
770
|
-
sc g t user -e
|
|
771
|
-
\`\`\`
|
|
772
|
-
|
|
773
|
-
## Flags
|
|
774
|
-
|
|
775
|
-
- \`-i\`, \`-interface\` => \`*.interface.ts\`
|
|
776
|
-
- \`-c\`, \`-class\` => \`*.class.ts\`
|
|
777
|
-
- \`-e\`, \`-enum\` => \`*.enum.ts\`
|
|
778
|
-
- default => \`*.type.ts\`
|
|
779
|
-
`;
|
|
780
|
-
export const routeHelp = `# Route Generator
|
|
781
|
-
|
|
782
|
-
## Usage
|
|
783
|
-
|
|
784
|
-
\`\`\`bash
|
|
785
|
-
sc generate route user
|
|
786
|
-
sc g r user
|
|
787
|
-
\`\`\`
|
|
788
|
-
|
|
789
|
-
## Mode Guard
|
|
790
|
-
|
|
791
|
-
- routers can only be scaffolded in functional or hybrid mode
|
|
792
|
-
`;
|
|
793
|
-
export const generateHelp = `# Generate
|
|
794
|
-
|
|
795
|
-
## Usage
|
|
796
|
-
|
|
797
|
-
\`\`\`bash
|
|
798
|
-
sc generate controller user
|
|
799
|
-
sc generate service user
|
|
800
|
-
sc generate module user
|
|
801
|
-
sc generate middleware auth
|
|
802
|
-
sc generate type user
|
|
803
|
-
sc generate route user
|
|
804
|
-
\`\`\`
|
|
805
|
-
|
|
806
|
-
## Aliases
|
|
807
|
-
|
|
808
|
-
- \`sc g\`
|
|
809
|
-
- \`controller\` -> \`c\`
|
|
810
|
-
- \`service\` -> \`s\`
|
|
811
|
-
- \`module\` -> \`m\` / \`mo\`
|
|
812
|
-
- \`middleware\` -> \`mw\`
|
|
813
|
-
- \`type\` -> \`t\`
|
|
814
|
-
- \`route\` -> \`r\`
|
|
815
|
-
|
|
816
|
-
## Path
|
|
817
|
-
|
|
818
|
-
- append \`in <path>\` to write into a custom directory
|
|
819
|
-
- default output for types is \`src/app/types\`
|
|
820
|
-
`;
|
|
1
|
+
export { controllerHelp, generateHelp, generateResourceFiles, middlewareHelp, moduleHelp, parseGenerateMode, readModeFromFlags, routeHelp, scaffoldProject, syncTestHarness, typeHelp, writeGeneratedFiles } from "../../template-registry/dist/index.js";
|
|
821
2
|
//# sourceMappingURL=scaffold.js.map
|