express-project-init 0.0.0-stage → 1.0.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.
Files changed (35) hide show
  1. package/README.md +108 -2
  2. package/dist/features.js +17 -0
  3. package/dist/features.js.map +1 -0
  4. package/dist/index.js +46 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/services/file.service.js +73 -0
  7. package/dist/services/file.service.js.map +1 -0
  8. package/dist/services/project.service.js +44 -0
  9. package/dist/services/project.service.js.map +1 -0
  10. package/dist/services/prompts.service.js +63 -0
  11. package/dist/services/prompts.service.js.map +1 -0
  12. package/dist/services/template.service.js +152 -0
  13. package/dist/services/template.service.js.map +1 -0
  14. package/dist/types.js +2 -0
  15. package/dist/types.js.map +1 -0
  16. package/package.json +57 -4
  17. package/templates/javascript/base/package.json +13 -0
  18. package/templates/javascript/base/src/app.js +22 -0
  19. package/templates/javascript/base/src/server.js +8 -0
  20. package/templates/javascript/base/src/utils/helpers.js +3 -0
  21. package/templates/javascript/base/src/utils/responseHelpers.js +19 -0
  22. package/templates/javascript/error-handler/src/app.js +28 -0
  23. package/templates/javascript/error-handler/src/middleware/error.middleware.js +36 -0
  24. package/templates/javascript/error-handler/src/middleware/notFound.middleware.js +5 -0
  25. package/templates/javascript/error-handler/src/utils/AppError.js +30 -0
  26. package/templates/typescript/base/package.json +20 -0
  27. package/templates/typescript/base/src/app.ts +22 -0
  28. package/templates/typescript/base/src/server.ts +8 -0
  29. package/templates/typescript/base/src/utils/helpers.ts +3 -0
  30. package/templates/typescript/base/src/utils/responseHelpers.ts +42 -0
  31. package/templates/typescript/base/tsconfig.json +30 -0
  32. package/templates/typescript/error-handler/src/app.ts +28 -0
  33. package/templates/typescript/error-handler/src/middleware/error.middleware.ts +42 -0
  34. package/templates/typescript/error-handler/src/middleware/notFound.middleware.ts +10 -0
  35. package/templates/typescript/error-handler/src/utils/AppError.ts +44 -0
package/README.md CHANGED
@@ -1,3 +1,109 @@
1
- # Temporary Holding Version
1
+ # ExpressKit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Scaffold a clean Express API in seconds — JavaScript or TypeScript.**
4
+
5
+ [![Watch the ExpressKit demo](docs/demo.jpg)](docs/demo.mp4)
6
+
7
+ ExpressKit asks four questions, then writes a ready-to-run Express 5 project. Choose JavaScript or TypeScript, optionally add centralized error handling, and start building.
8
+
9
+ ## Quick start
10
+
11
+ Requires **Node.js 20+**.
12
+
13
+ ```bash
14
+ npm create expresskit@latest
15
+ ```
16
+
17
+ Or run it directly:
18
+
19
+ ```bash
20
+ npx create-expresskit@latest
21
+ ```
22
+
23
+ Answer the prompts:
24
+
25
+ ```text
26
+ ? Project name › my-api
27
+ ? Language › TypeScript
28
+ ? Create central error handling middleware? › Yes
29
+ ? Install dependencies? › Yes
30
+ ```
31
+
32
+ Then:
33
+
34
+ ```bash
35
+ cd my-api
36
+ npm run dev
37
+ ```
38
+
39
+ Open <http://localhost:3000/health>.
40
+
41
+ Use `.` at the project name prompt to scaffold in the current directory. ExpressKit will not overwrite existing files.
42
+
43
+ ## What you get
44
+
45
+ ```text
46
+ my-api/
47
+ ├── package.json
48
+ ├── tsconfig.json # TypeScript only
49
+ └── src/
50
+ ├── server.ts
51
+ ├── app.ts
52
+ ├── middleware/ # with the optional error handler
53
+ │ ├── error.middleware.ts
54
+ │ └── notFound.middleware.ts
55
+ └── utils/
56
+ ├── helpers.ts
57
+ ├── responseHelpers.ts
58
+ └── AppError.ts # with the optional error handler
59
+ ```
60
+
61
+ JavaScript projects use the same layout with `.js` files and no `tsconfig.json`.
62
+
63
+ ### Scripts
64
+
65
+ | Command | JavaScript | TypeScript |
66
+ | --- | --- | --- |
67
+ | `npm run dev` | `node --watch` | `tsx watch` |
68
+ | `npm start` | `node` | `tsx` |
69
+ | `npm run typecheck` | — | `tsc --noEmit` |
70
+
71
+ ## Error handling
72
+
73
+ With the error handler enabled, throw `BadRequestError`, `UnauthorizedError`, or `NotFoundError` from any route and one middleware formats the response:
74
+
75
+ ```json
76
+ {
77
+ "success": false,
78
+ "message": "Route not found",
79
+ "code": "NOT_FOUND"
80
+ }
81
+ ```
82
+
83
+ Successful responses use the same envelope with `success: true` and `data`.
84
+
85
+ ## How it works
86
+
87
+ Projects are assembled from a base template plus optional feature layers. Feature files replace base files at the same path, and feature `package.json` fragments are merged into the base manifest.
88
+
89
+ ## Local development
90
+
91
+ ```bash
92
+ npm install
93
+ npm run build
94
+ npm link
95
+ ```
96
+
97
+ Then, in a separate directory:
98
+
99
+ ```bash
100
+ mkdir -p /tmp/expresskit-test
101
+ cd /tmp/expresskit-test
102
+ create-expresskit
103
+ ```
104
+
105
+ Rebuild with `npm run build` after changing `src/`; the linked command picks up the new `dist/` automatically. Remove the global link with `npm unlink -g create-expresskit`.
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Every optional feature lives here once.
3
+ *
4
+ * To add a feature:
5
+ * 1. Add templates/<language>/<id>/ with the files it adds or overrides.
6
+ * 2. Add an entry to this array.
7
+ *
8
+ * The prompt, file layering, and package.json merging all derive from this list.
9
+ */
10
+ export const FEATURES = [
11
+ {
12
+ id: "error-handler",
13
+ prompt: "Create central error handling middleware?",
14
+ defaultValue: true,
15
+ },
16
+ ];
17
+ //# sourceMappingURL=features.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features.js","sourceRoot":"","sources":["../src/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB;QACE,EAAE,EAAE,eAAe;QACnB,MAAM,EAAE,2CAA2C;QACnD,YAAY,EAAE,IAAI;KACnB;CACO,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { spinner } from "@clack/prompts";
3
+ import { FileService } from "./services/file.service.js";
4
+ import { ProjectService } from "./services/project.service.js";
5
+ import { PromptService } from "./services/prompts.service.js";
6
+ import { TemplateService } from "./services/template.service.js";
7
+ async function main() {
8
+ const promptService = new PromptService();
9
+ const fileService = new FileService();
10
+ const templateService = new TemplateService();
11
+ // 1. Collect every answer first — the spinner only starts after all prompts are done
12
+ const projectName = await promptService.askProjectName();
13
+ const language = await promptService.askLanguage();
14
+ const availableFeatures = await templateService.getAvailableFeatures(language);
15
+ const features = await promptService.askFeatureAnswers(availableFeatures);
16
+ const installDependencies = await promptService.askShouldInstall();
17
+ const answers = {
18
+ projectName,
19
+ language,
20
+ features,
21
+ installDependencies,
22
+ };
23
+ // 2. Run the commands behind a spinner
24
+ const loading = spinner();
25
+ loading.start("Creating project");
26
+ try {
27
+ const projectPath = await fileService.createDirectory(projectName);
28
+ const projectService = new ProjectService(projectPath, answers);
29
+ await projectService.createProject();
30
+ if (installDependencies) {
31
+ loading.message("Installing dependencies");
32
+ await projectService.runInstallCommand();
33
+ }
34
+ loading.stop(`Created ${language} project at ${projectPath}`);
35
+ }
36
+ catch (error) {
37
+ loading.error("Project creation failed");
38
+ throw error;
39
+ }
40
+ }
41
+ main().catch((err) => {
42
+ const message = err instanceof Error ? err.message : "An unexpected error occurred";
43
+ console.error(`ExpressKit: ${message}`);
44
+ process.exitCode = 1;
45
+ });
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEzC,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,KAAK,UAAU,IAAI;IACjB,MAAM,aAAa,GAAG,IAAI,aAAa,EAAE,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;IACtC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;IAE9C,qFAAqF;IACrF,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC,cAAc,EAAE,CAAC;IACzD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC;IACnD,MAAM,iBAAiB,GACrB,MAAM,eAAe,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IAC1E,MAAM,mBAAmB,GAAG,MAAM,aAAa,CAAC,gBAAgB,EAAE,CAAC;IAEnE,MAAM,OAAO,GAAmB;QAC9B,WAAW;QACX,QAAQ;QACR,QAAQ;QACR,mBAAmB;KACpB,CAAC;IAEF,uCAAuC;IACvC,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC;IAC1B,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAElC,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,MAAM,WAAW,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;QACnE,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAChE,MAAM,cAAc,CAAC,aAAa,EAAE,CAAC;QAErC,IAAI,mBAAmB,EAAE,CAAC;YACxB,OAAO,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;YAC3C,MAAM,cAAc,CAAC,iBAAiB,EAAE,CAAC;QAC3C,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,WAAW,QAAQ,eAAe,WAAW,EAAE,CAAC,CAAC;IAChE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACzC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,MAAM,OAAO,GACX,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,8BAA8B,CAAC;IAEtE,OAAO,CAAC,KAAK,CAAC,eAAe,OAAO,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,73 @@
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export class FileService {
4
+ async createDirectory(projectName) {
5
+ const dirPath = path.resolve(process.cwd(), projectName);
6
+ try {
7
+ await mkdir(dirPath, { recursive: true });
8
+ return dirPath;
9
+ }
10
+ catch (error) {
11
+ const message = error instanceof Error ? error.message : "Unknown filesystem error";
12
+ throw new Error(`Failed to create project directory "${dirPath}": ${message}`, { cause: error });
13
+ }
14
+ }
15
+ /**
16
+ * Writes the complete, already-composed project in one pass.
17
+ * Every destination is checked before the first file is written.
18
+ */
19
+ async writeFiles(projectDirectory, files) {
20
+ const entries = Object.entries(files);
21
+ if (entries.length === 0) {
22
+ throw new Error("No files to create.");
23
+ }
24
+ const destinations = entries.map(([relativePath, contents]) => ({
25
+ relativePath,
26
+ contents,
27
+ absolutePath: this.resolveProjectPath(projectDirectory, relativePath),
28
+ }));
29
+ const existingFiles = await this.getExistingFiles(destinations);
30
+ if (existingFiles.length > 0) {
31
+ throw new Error(`Cannot create project: these files already exist in "${projectDirectory}": ${existingFiles.join(", ")}`);
32
+ }
33
+ const results = await Promise.allSettled(destinations.map(({ absolutePath, contents }) => this.writeFile(absolutePath, contents)));
34
+ const failures = results.filter((result) => result.status === "rejected");
35
+ if (failures.length > 0) {
36
+ throw new Error(`Failed to create ${failures.length} of ${entries.length} files.`, { cause: failures.map((failure) => failure.reason) });
37
+ }
38
+ }
39
+ resolveProjectPath(projectDirectory, relativePath) {
40
+ if (!relativePath.trim() || path.isAbsolute(relativePath)) {
41
+ throw new Error(`Generated file path must be a non-empty relative path: "${relativePath}".`);
42
+ }
43
+ const projectRoot = path.resolve(projectDirectory);
44
+ const destination = path.resolve(projectRoot, relativePath);
45
+ if (destination !== projectRoot &&
46
+ !destination.startsWith(`${projectRoot}${path.sep}`)) {
47
+ throw new Error(`Generated file path escapes the project directory: "${relativePath}".`);
48
+ }
49
+ return destination;
50
+ }
51
+ async getExistingFiles(files) {
52
+ const checks = files.map(async ({ relativePath, absolutePath }) => {
53
+ try {
54
+ await access(absolutePath);
55
+ return relativePath;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ });
61
+ const results = await Promise.all(checks);
62
+ return results.filter((file) => file !== null);
63
+ }
64
+ async writeFile(filePath, contents) {
65
+ await mkdir(path.dirname(filePath), { recursive: true });
66
+ // "wx" fails if the file exists, so we never overwrite user files.
67
+ await writeFile(filePath, contents, {
68
+ encoding: "utf-8",
69
+ flag: "wx",
70
+ });
71
+ }
72
+ }
73
+ //# sourceMappingURL=file.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.service.js","sourceRoot":"","sources":["../../src/services/file.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,MAAM,OAAO,WAAW;IACf,KAAK,CAAC,eAAe,CAAC,WAAmB;QAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,WAAW,CAAC,CAAC;QAEzD,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC1C,OAAO,OAAO,CAAC;QACjB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,OAAO,GACX,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,0BAA0B,CAAC;YAEtE,MAAM,IAAI,KAAK,CACb,uCAAuC,OAAO,MAAM,OAAO,EAAE,EAC7D,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;QACJ,CAAC;IACH,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,UAAU,CACrB,gBAAwB,EACxB,KAAqB;QAErB,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAEtC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9D,YAAY;YACZ,QAAQ;YACR,YAAY,EAAE,IAAI,CAAC,kBAAkB,CACnC,gBAAgB,EAChB,YAAY,CACb;SACF,CAAC,CAAC,CAAC;QAEJ,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC;QAEhE,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,wDAAwD,gBAAgB,MAAM,aAAa,CAAC,IAAI,CAC9F,IAAI,CACL,EAAE,CACJ,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,EAAE,CAC9C,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,QAAQ,CAAC,CACvC,CACF,CAAC;QAEF,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAC7B,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CACzC,CAAC;QAEF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACb,oBAAoB,QAAQ,CAAC,MAAM,OAAO,OAAO,CAAC,MAAM,SAAS,EACjE,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CACrD,CAAC;QACJ,CAAC;IACH,CAAC;IAEO,kBAAkB,CACxB,gBAAwB,EACxB,YAAoB;QAEpB,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC1D,MAAM,IAAI,KAAK,CACb,2DAA2D,YAAY,IAAI,CAC5E,CAAC;QACJ,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACnD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QAE5D,IACE,WAAW,KAAK,WAAW;YAC3B,CAAC,WAAW,CAAC,UAAU,CAAC,GAAG,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,EACpD,CAAC;YACD,MAAM,IAAI,KAAK,CACb,uDAAuD,YAAY,IAAI,CACxE,CAAC;QACJ,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAC5B,KAAuD;QAEvD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,EAAE;YAChE,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;gBAC3B,OAAO,YAAY,CAAC;YACtB,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAE1C,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACjE,CAAC;IAEO,KAAK,CAAC,SAAS,CACrB,QAAgB,EAChB,QAAgB;QAEhB,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEzD,mEAAmE;QACnE,MAAM,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE;YAClC,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,44 @@
1
+ import { spawn } from "node:child_process";
2
+ import { FileService } from "./file.service.js";
3
+ import { TemplateService } from "./template.service.js";
4
+ export class ProjectService {
5
+ projectPath;
6
+ answers;
7
+ fileService;
8
+ templateService;
9
+ constructor(projectPath, answers, fileService = new FileService(), templateService = new TemplateService()) {
10
+ this.projectPath = projectPath;
11
+ this.answers = answers;
12
+ this.fileService = fileService;
13
+ this.templateService = templateService;
14
+ }
15
+ async createProject() {
16
+ const files = await this.templateService.buildFilePlan(this.answers);
17
+ // Build the whole plan first, then write it in one pass.
18
+ await this.fileService.writeFiles(this.projectPath, files);
19
+ }
20
+ async runInstallCommand() {
21
+ return new Promise((resolve, reject) => {
22
+ const child = spawn("npm", ["install"], {
23
+ cwd: this.projectPath, // run inside the NEW project, not where the CLI was launched
24
+ stdio: ["ignore", "ignore", "pipe"], // hide npm output from the spinner; stderr kept for errors
25
+ shell: process.platform === "win32",
26
+ });
27
+ let stderr = "";
28
+ child.stderr?.on("data", (chunk) => {
29
+ stderr += chunk.toString();
30
+ });
31
+ child.on("error", reject);
32
+ child.on("close", (code) => {
33
+ if (code === 0) {
34
+ resolve();
35
+ }
36
+ else {
37
+ const details = stderr.trim();
38
+ reject(new Error(`npm install failed with exit code ${code}${details ? `\n${details}` : ""}`));
39
+ }
40
+ });
41
+ });
42
+ }
43
+ }
44
+ //# sourceMappingURL=project.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.service.js","sourceRoot":"","sources":["../../src/services/project.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,OAAO,cAAc;IACR,WAAW,CAAS;IACpB,OAAO,CAAiB;IACxB,WAAW,CAAc;IACzB,eAAe,CAAkB;IAElD,YACE,WAAmB,EACnB,OAAuB,EACvB,WAAW,GAAG,IAAI,WAAW,EAAE,EAC/B,eAAe,GAAG,IAAI,eAAe,EAAE;QAEvC,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IACzC,CAAC;IAEM,KAAK,CAAC,aAAa;QACxB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAErE,yDAAyD;QACzD,MAAM,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAC7D,CAAC;IAEM,KAAK,CAAC,iBAAiB;QAC5B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE;gBACtC,GAAG,EAAE,IAAI,CAAC,WAAW,EAAE,6DAA6D;gBACpF,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,2DAA2D;gBAChG,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;aACpC,CAAC,CAAC;YAEH,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;gBACzC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC7B,CAAC,CAAC,CAAC;YAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAE1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAY,EAAE,EAAE;gBACjC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;oBACf,OAAO,EAAE,CAAC;gBACZ,CAAC;qBAAM,CAAC;oBACN,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;oBAE9B,MAAM,CACJ,IAAI,KAAK,CACP,qCAAqC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAC5E,CACF,CAAC;gBACJ,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,63 @@
1
+ import { cancel, confirm, isCancel, select, text } from "@clack/prompts";
2
+ import { FEATURES } from "../features.js";
3
+ export class PromptService {
4
+ LANGUAGE_OPTIONS = [
5
+ { value: "javascript", label: "JavaScript" },
6
+ { value: "typescript", label: "TypeScript" },
7
+ ];
8
+ async askProjectName() {
9
+ const response = await text({
10
+ message: "What would you like to name your project?",
11
+ placeholder: "my-api",
12
+ defaultValue: ".",
13
+ validate(value) {
14
+ if (!value?.trim()) {
15
+ return "Please enter a project name or '.' to use the current directory.";
16
+ }
17
+ },
18
+ });
19
+ this.handleCancel(response);
20
+ return response.trim();
21
+ }
22
+ async askLanguage() {
23
+ const response = await select({
24
+ message: "Which language would you like to use?",
25
+ options: this.LANGUAGE_OPTIONS,
26
+ initialValue: "javascript",
27
+ });
28
+ this.handleCancel(response);
29
+ return response;
30
+ }
31
+ /**
32
+ * Asks one confirm per registered feature available for the chosen
33
+ * language. Unavailable features default to false.
34
+ */
35
+ async askFeatureAnswers(availableFeatures) {
36
+ const answers = Object.fromEntries(FEATURES.map((feature) => [feature.id, false]));
37
+ for (const feature of availableFeatures) {
38
+ const response = await confirm({
39
+ message: feature.prompt,
40
+ initialValue: feature.defaultValue,
41
+ });
42
+ this.handleCancel(response);
43
+ answers[feature.id] = response;
44
+ }
45
+ return answers;
46
+ }
47
+ async askShouldInstall() {
48
+ const response = await confirm({
49
+ message: "Install dependencies?",
50
+ initialValue: true,
51
+ });
52
+ this.handleCancel(response);
53
+ return response;
54
+ }
55
+ // Catches Ctrl+C / Esc so the CLI exits cleanly instead of crashing.
56
+ handleCancel(response) {
57
+ if (isCancel(response)) {
58
+ cancel("Project setup cancelled.");
59
+ process.exit(0);
60
+ }
61
+ }
62
+ }
63
+ //# sourceMappingURL=prompts.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.service.js","sourceRoot":"","sources":["../../src/services/prompts.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAgB,MAAM,gBAAgB,CAAC;AAGxD,MAAM,OAAO,aAAa;IACP,gBAAgB,GAG3B;QACJ,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE;QAC5C,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE;KAC7C,CAAC;IAEK,KAAK,CAAC,cAAc;QACzB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC;YAC1B,OAAO,EAAE,2CAA2C;YACpD,WAAW,EAAE,QAAQ;YACrB,YAAY,EAAE,GAAG;YACjB,QAAQ,CAAC,KAAK;gBACZ,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;oBACnB,OAAO,kEAAkE,CAAC;gBAC5E,CAAC;YACH,CAAC;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAS,QAAQ,CAAC,CAAC;QAEpC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAEM,KAAK,CAAC,WAAW;QACtB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC;YAC5B,OAAO,EAAE,uCAAuC;YAChD,OAAO,EAAE,IAAI,CAAC,gBAAgB;YAC9B,YAAY,EAAE,YAAY;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAW,QAAQ,CAAC,CAAC;QAEtC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,iBAAiB,CAC5B,iBAAqC;QAErC,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CAChC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAC7B,CAAC;QAEpB,KAAK,MAAM,OAAO,IAAI,iBAAiB,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC;gBAC7B,OAAO,EAAE,OAAO,CAAC,MAAM;gBACvB,YAAY,EAAE,OAAO,CAAC,YAAY;aACnC,CAAC,CAAC;YAEH,IAAI,CAAC,YAAY,CAAU,QAAQ,CAAC,CAAC;YAErC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC;QACjC,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAEM,KAAK,CAAC,gBAAgB;QAC3B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC;YAC7B,OAAO,EAAE,uBAAuB;YAChC,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,CAAU,QAAQ,CAAC,CAAC;QAErC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,qEAAqE;IAC7D,YAAY,CAAI,QAAiB;QACvC,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,0BAA0B,CAAC,CAAC;YACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,152 @@
1
+ import { access, readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { FEATURES } from "../features.js";
5
+ const PACKAGE_SECTIONS = [
6
+ "dependencies",
7
+ "devDependencies",
8
+ "scripts",
9
+ ];
10
+ export class TemplateService {
11
+ templateRoot;
12
+ constructor(templateRoot) {
13
+ if (templateRoot) {
14
+ this.templateRoot = path.resolve(templateRoot);
15
+ return;
16
+ }
17
+ const serviceDirectory = path.dirname(fileURLToPath(import.meta.url));
18
+ // templates/ sits two levels up from src/services (and dist/services).
19
+ this.templateRoot = path.resolve(serviceDirectory, "../../templates");
20
+ }
21
+ async getAvailableFeatures(language) {
22
+ const languageDirectory = path.join(this.templateRoot, language);
23
+ let entries;
24
+ try {
25
+ entries = await readdir(languageDirectory, { withFileTypes: true });
26
+ }
27
+ catch (error) {
28
+ throw new Error(`Unable to read templates for "${language}" at "${languageDirectory}".`, { cause: error });
29
+ }
30
+ const folders = new Set(entries
31
+ .filter((entry) => entry.isDirectory())
32
+ .map((entry) => entry.name));
33
+ return FEATURES.filter((feature) => folders.has(feature.id));
34
+ }
35
+ /**
36
+ * Builds the complete project in memory.
37
+ *
38
+ * Base files come first. Each enabled feature is then layered on top:
39
+ * feature files replace base files with the same relative path, while new
40
+ * paths are added. package.json is the exception — feature package.json
41
+ * files are fragments merged into the base package.json.
42
+ */
43
+ async buildFilePlan(answers) {
44
+ const baseFiles = await this.readLayer(answers.language, "base");
45
+ const basePackageSource = baseFiles["package.json"];
46
+ if (basePackageSource === undefined) {
47
+ throw new Error(`Base template for "${answers.language}" is missing package.json.`);
48
+ }
49
+ let packageJson = this.parseJson(basePackageSource, `${answers.language}/base/package.json`);
50
+ let files = { ...baseFiles };
51
+ for (const feature of FEATURES) {
52
+ if (!answers.features[feature.id]) {
53
+ continue;
54
+ }
55
+ const layer = await this.readLayer(answers.language, feature.id);
56
+ const fragmentSource = layer["package.json"];
57
+ if (fragmentSource !== undefined) {
58
+ const fragment = this.parsePackageFragment(fragmentSource, feature.id);
59
+ packageJson = this.mergePackageJson(packageJson, fragment, feature.id);
60
+ // The merged package.json is written last, not copied from the layer.
61
+ delete layer["package.json"];
62
+ }
63
+ files = { ...files, ...layer };
64
+ }
65
+ files["package.json"] = `${JSON.stringify(packageJson, null, 2)}\n`;
66
+ return files;
67
+ }
68
+ async readLayer(language, layerName) {
69
+ const layerDirectory = path.join(this.templateRoot, language, layerName);
70
+ try {
71
+ await access(layerDirectory);
72
+ }
73
+ catch (error) {
74
+ throw new Error(`Template folder not found: "${language}/${layerName}".`, { cause: error });
75
+ }
76
+ return this.readDirectory(layerDirectory);
77
+ }
78
+ async readDirectory(root, relativeDirectory = "") {
79
+ const directory = path.join(root, relativeDirectory);
80
+ const entries = await readdir(directory, { withFileTypes: true });
81
+ const files = {};
82
+ for (const entry of entries) {
83
+ const relativePath = relativeDirectory
84
+ ? path.posix.join(relativeDirectory, entry.name)
85
+ : entry.name;
86
+ if (entry.isDirectory()) {
87
+ Object.assign(files, await this.readDirectory(root, relativePath));
88
+ }
89
+ else if (entry.isFile()) {
90
+ files[relativePath] = await readFile(path.join(root, relativePath), "utf8");
91
+ }
92
+ }
93
+ return files;
94
+ }
95
+ parsePackageFragment(source, featureId) {
96
+ const parsed = this.parseJson(source, `${featureId}/package.json`);
97
+ const unknownKeys = Object.keys(parsed).filter((key) => !PACKAGE_SECTIONS.includes(key));
98
+ if (unknownKeys.length > 0) {
99
+ throw new Error(`Feature "${featureId}" has unsupported package.json keys: ${unknownKeys.join(", ")}. Only ${PACKAGE_SECTIONS.join(", ")} can be merged.`);
100
+ }
101
+ const fragment = {};
102
+ for (const section of PACKAGE_SECTIONS) {
103
+ const value = parsed[section];
104
+ if (value === undefined) {
105
+ continue;
106
+ }
107
+ if (!this.isStringRecord(value)) {
108
+ throw new Error(`"${section}" in feature "${featureId}" package.json must be an object of string values.`);
109
+ }
110
+ fragment[section] = value;
111
+ }
112
+ return fragment;
113
+ }
114
+ mergePackageJson(base, fragment, featureId) {
115
+ const merged = { ...base };
116
+ for (const section of PACKAGE_SECTIONS) {
117
+ const additions = fragment[section];
118
+ if (!additions) {
119
+ continue;
120
+ }
121
+ const target = merged[section]
122
+ ? { ...merged[section] }
123
+ : {};
124
+ for (const [name, value] of Object.entries(additions)) {
125
+ const existing = target[name];
126
+ if (existing !== undefined && existing !== value) {
127
+ throw new Error(`Feature "${featureId}" conflicts with package.json ${section} entry "${name}": "${existing}" (existing) vs "${value}" (feature).`);
128
+ }
129
+ target[name] = value;
130
+ }
131
+ merged[section] = target;
132
+ }
133
+ return merged;
134
+ }
135
+ isStringRecord(value) {
136
+ return (typeof value === "object" &&
137
+ value !== null &&
138
+ !Array.isArray(value) &&
139
+ Object.values(value).every((item) => typeof item === "string"));
140
+ }
141
+ parseJson(source, templatePath) {
142
+ try {
143
+ return JSON.parse(source);
144
+ }
145
+ catch (error) {
146
+ throw new Error(`Invalid JSON in template "${templatePath}".`, {
147
+ cause: error,
148
+ });
149
+ }
150
+ }
151
+ }
152
+ //# sourceMappingURL=template.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.service.js","sourceRoot":"","sources":["../../src/services/template.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAgB,MAAM,gBAAgB,CAAC;AAUxD,MAAM,gBAAgB,GAAG;IACvB,cAAc;IACd,iBAAiB;IACjB,SAAS;CACD,CAAC;AAIX,MAAM,OAAO,eAAe;IACT,YAAY,CAAS;IAEtC,YAAY,YAAqB;QAC/B,IAAI,YAAY,EAAE,CAAC;YACjB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YAC/C,OAAO;QACT,CAAC;QAED,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAEtE,uEAAuE;QACvE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,CAAC;IACxE,CAAC;IAEM,KAAK,CAAC,oBAAoB,CAC/B,QAAkB;QAElB,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;QAEjE,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,iBAAiB,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACtE,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACb,iCAAiC,QAAQ,SAAS,iBAAiB,IAAI,EACvE,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,OAAO;aACJ,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;aACtC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAC9B,CAAC;QAEF,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,aAAa,CACxB,OAAuB;QAEvB,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACjE,MAAM,iBAAiB,GAAG,SAAS,CAAC,cAAc,CAAC,CAAC;QAEpD,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CACb,sBAAsB,OAAO,CAAC,QAAQ,4BAA4B,CACnE,CAAC;QACJ,CAAC;QAED,IAAI,WAAW,GAAG,IAAI,CAAC,SAAS,CAC9B,iBAAiB,EACjB,GAAG,OAAO,CAAC,QAAQ,oBAAoB,CACxC,CAAC;QAEF,IAAI,KAAK,GAAmB,EAAE,GAAG,SAAS,EAAE,CAAC;QAE7C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;gBAClC,SAAS;YACX,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;YACjE,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC;YAE7C,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;gBACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CACxC,cAAc,EACd,OAAO,CAAC,EAAE,CACX,CAAC;gBAEF,WAAW,GAAG,IAAI,CAAC,gBAAgB,CACjC,WAAW,EACX,QAAQ,EACR,OAAO,CAAC,EAAE,CACX,CAAC;gBAEF,sEAAsE;gBACtE,OAAO,KAAK,CAAC,cAAc,CAAC,CAAC;YAC/B,CAAC;YAED,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,EAAE,CAAC;QACjC,CAAC;QAED,KAAK,CAAC,cAAc,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;QAEpE,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,SAAS,CACrB,QAAkB,EAClB,SAAiB;QAEjB,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAC9B,IAAI,CAAC,YAAY,EACjB,QAAQ,EACR,SAAS,CACV,CAAC;QAEF,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACb,+BAA+B,QAAQ,IAAI,SAAS,IAAI,EACxD,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;IAC5C,CAAC;IAEO,KAAK,CAAC,aAAa,CACzB,IAAY,EACZ,iBAAiB,GAAG,EAAE;QAEtB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAClE,MAAM,KAAK,GAAmB,EAAE,CAAC;QAEjC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,YAAY,GAAG,iBAAiB;gBACpC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,iBAAiB,EAAE,KAAK,CAAC,IAAI,CAAC;gBAChD,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;YAEf,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,MAAM,CAAC,MAAM,CACX,KAAK,EACL,MAAM,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,YAAY,CAAC,CAC7C,CAAC;YACJ,CAAC;iBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC1B,KAAK,CAAC,YAAY,CAAC,GAAG,MAAM,QAAQ,CAClC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,EAC7B,MAAM,CACP,CAAC;YACJ,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,oBAAoB,CAC1B,MAAc,EACd,SAAiB;QAEjB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAC3B,MAAM,EACN,GAAG,SAAS,eAAe,CAC5B,CAAC;QAEF,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAC5C,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,GAAqB,CAAC,CAC3D,CAAC;QAEF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CACb,YAAY,SAAS,wCAAwC,WAAW,CAAC,IAAI,CAC3E,IAAI,CACL,UAAU,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CACxD,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAoB,EAAE,CAAC;QAErC,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;YAE9B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,SAAS;YACX,CAAC;YAED,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;gBAChC,MAAM,IAAI,KAAK,CACb,IAAI,OAAO,iBAAiB,SAAS,oDAAoD,CAC1F,CAAC;YACJ,CAAC;YAED,QAAQ,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;QAC5B,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAEO,gBAAgB,CACtB,IAAiB,EACjB,QAAyB,EACzB,SAAiB;QAEjB,MAAM,MAAM,GAAgB,EAAE,GAAG,IAAI,EAAE,CAAC;QAExC,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;YACvC,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;YAEpC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,SAAS;YACX,CAAC;YAED,MAAM,MAAM,GAA2B,MAAM,CAAC,OAAO,CAAC;gBACpD,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,EAAE;gBACxB,CAAC,CAAC,EAAE,CAAC;YAEP,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;gBACtD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;gBAE9B,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;oBACjD,MAAM,IAAI,KAAK,CACb,YAAY,SAAS,iCAAiC,OAAO,WAAW,IAAI,OAAO,QAAQ,oBAAoB,KAAK,cAAc,CACnI,CAAC;gBACJ,CAAC;gBAED,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YACvB,CAAC;YAED,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC;QAC3B,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAEO,cAAc,CAAC,KAAc;QACnC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,KAAK,IAAI;YACd,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACrB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAC/D,CAAC;IACJ,CAAC;IAEO,SAAS,CAAI,MAAc,EAAE,YAAoB;QACvD,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAM,CAAC;QACjC,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,6BAA6B,YAAY,IAAI,EAAE;gBAC7D,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;IACH,CAAC;CACF"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "express-project-init",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Create production-ready Express.js API projects with JavaScript or TypeScript using clean, reusable starter templates.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "express-project-init": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "templates",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "scripts": {
20
+ "dev": "tsx src/index.ts",
21
+ "clean": "node -e \"const fs=require('node:fs'); for (const p of ['dist','tsconfig.tsbuildinfo']) fs.rmSync(p,{recursive:true,force:true})\"",
22
+ "build": "tsc",
23
+ "typecheck": "tsc --noEmit",
24
+ "prepack": "npm run clean && npm run build",
25
+ "start": "node dist/index.js"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/raunak-ray/ExpressKit.git"
30
+ },
31
+ "homepage": "https://github.com/raunak-ray/ExpressKit#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/raunak-ray/ExpressKit/issues"
34
+ },
35
+ "keywords": [
36
+ "express",
37
+ "expressjs",
38
+ "express-generator",
39
+ "express-cli",
40
+ "nodejs",
41
+ "api-generator",
42
+ "backend",
43
+ "project-scaffolding",
44
+ "boilerplate",
45
+ "starter-template",
46
+ "typescript",
47
+ "javascript"
48
+ ],
49
+ "author": "Raunak Ray",
50
+ "license": "MIT",
51
+ "dependencies": {
52
+ "@clack/prompts": "^1.8.1"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^26.6.5",
56
+ "tsx": "^4.23.15",
57
+ "typescript": "^7.0.2"
58
+ }
59
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "my-api",
3
+ "version": "1.0.0",
4
+ "description": "Express API generated by ExpressKit",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "node --watch src/server.js",
8
+ "start": "node src/server.js"
9
+ },
10
+ "dependencies": {
11
+ "express": "^5.1.0"
12
+ }
13
+ }
@@ -0,0 +1,22 @@
1
+ import express from "express";
2
+ import { getTimeStamp } from "./utils/helpers.js";
3
+ import { successResponse } from "./utils/responseHelpers.js";
4
+
5
+ const app = express();
6
+
7
+ app.use(express.json());
8
+
9
+ // Health check
10
+ app.get("/health", (_req, res) =>
11
+ successResponse(res, {
12
+ statusCode: 200,
13
+ message: "API is up and running",
14
+ data: {
15
+ status: "ok",
16
+ uptime: Math.round(process.uptime()),
17
+ timestamp: getTimeStamp(),
18
+ },
19
+ }),
20
+ );
21
+
22
+ export default app;
@@ -0,0 +1,8 @@
1
+ import app from "./app.js";
2
+
3
+ const PORT = process.env.PORT || 3000;
4
+
5
+ app.listen(PORT, () => {
6
+ console.log(`API running on http://localhost:${PORT}`);
7
+ console.log(`Health check: http://localhost:${PORT}/health`);
8
+ });
@@ -0,0 +1,3 @@
1
+ export function getTimeStamp() {
2
+ return new Date().toISOString();
3
+ }
@@ -0,0 +1,19 @@
1
+ export function successResponse(res, { statusCode = 200, message, data }) {
2
+ return res.status(statusCode).json({
3
+ success: true,
4
+ message,
5
+ data,
6
+ });
7
+ }
8
+
9
+ export function errorResponse(
10
+ res,
11
+ { statusCode = 500, message, code = "INTERNAL_ERROR", fields },
12
+ ) {
13
+ return res.status(statusCode).json({
14
+ success: false,
15
+ message,
16
+ code,
17
+ fields,
18
+ });
19
+ }
@@ -0,0 +1,28 @@
1
+ import express from "express";
2
+ import errorMiddleware from "./middleware/error.middleware.js";
3
+ import notFoundMiddleware from "./middleware/notFound.middleware.js";
4
+ import { getTimeStamp } from "./utils/helpers.js";
5
+ import { successResponse } from "./utils/responseHelpers.js";
6
+
7
+ const app = express();
8
+
9
+ app.use(express.json());
10
+
11
+ // Health check
12
+ app.get("/health", (_req, res) =>
13
+ successResponse(res, {
14
+ statusCode: 200,
15
+ message: "API is up and running",
16
+ data: {
17
+ status: "ok",
18
+ uptime: Math.round(process.uptime()),
19
+ timestamp: getTimeStamp(),
20
+ },
21
+ }),
22
+ );
23
+
24
+ // Unknown routes → 404, then every error gets formatted in one place.
25
+ app.use(notFoundMiddleware);
26
+ app.use(errorMiddleware);
27
+
28
+ export default app;
@@ -0,0 +1,36 @@
1
+ import { AppError, BadRequestError } from "../utils/AppError.js";
2
+ import { errorResponse } from "../utils/responseHelpers.js";
3
+ import { getTimeStamp } from "../utils/helpers.js";
4
+
5
+ // Express error handler — needs all 4 params, that's how Express recognizes it.
6
+ export default function errorMiddleware(error, req, res, next) {
7
+ const timeStamp = getTimeStamp();
8
+
9
+ // Expected errors — log them and send their details
10
+ if (error instanceof AppError) {
11
+ console.log(
12
+ `[${timeStamp}] [ERROR] ${req.method} ${req.originalUrl} - ${error.message}`,
13
+ );
14
+
15
+ return errorResponse(res, {
16
+ statusCode: error.statusCode,
17
+ message: error.message,
18
+ code: error.code,
19
+ fields: error instanceof BadRequestError ? error.fields : undefined, // fields only for bad requests
20
+ });
21
+ }
22
+
23
+ // Not an AppError — build a message if it's an Error, otherwise fall back
24
+ const message =
25
+ error instanceof Error ? error.message : "Internal server error";
26
+
27
+ console.error(
28
+ `[${timeStamp}] [ERROR] ${req.method} ${req.originalUrl} - ${message}`,
29
+ error,
30
+ );
31
+
32
+ return errorResponse(res, {
33
+ statusCode: 500,
34
+ message,
35
+ });
36
+ }
@@ -0,0 +1,5 @@
1
+ import { NotFoundError } from "../utils/AppError.js";
2
+
3
+ export default function notFoundMiddleware(req, res, next) {
4
+ next(new NotFoundError("Route not found"));
5
+ }
@@ -0,0 +1,30 @@
1
+ export class AppError extends Error {
2
+ constructor(message, { code = "INTERNAL_ERROR", statusCode = 500 } = {}) {
3
+ super(message);
4
+
5
+ this.name = this.constructor.name;
6
+ this.code = code; // machine-readable code, e.g. "BAD_REQUEST"
7
+ this.statusCode = statusCode; // HTTP status to send back
8
+ }
9
+ }
10
+
11
+ export class BadRequestError extends AppError {
12
+ // fields: validation errors, e.g. [{ field: "email", message: "required" }]
13
+ constructor(message = "Bad request", fields = []) {
14
+ super(message, { code: "BAD_REQUEST", statusCode: 400 });
15
+
16
+ this.fields = fields;
17
+ }
18
+ }
19
+
20
+ export class UnauthorizedError extends AppError {
21
+ constructor(message = "Unauthorized") {
22
+ super(message, { code: "UNAUTHORIZED", statusCode: 401 });
23
+ }
24
+ }
25
+
26
+ export class NotFoundError extends AppError {
27
+ constructor(message = "Not found") {
28
+ super(message, { code: "NOT_FOUND", statusCode: 404 });
29
+ }
30
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "my-api",
3
+ "version": "1.0.0",
4
+ "description": "Express API generated by ExpressKit",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts",
8
+ "start": "tsx src/server.ts",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "express": "^5.1.0"
13
+ },
14
+ "devDependencies": {
15
+ "@types/express": "^5.0.0",
16
+ "@types/node": "^26.6.5",
17
+ "tsx": "^4.23.15",
18
+ "typescript": "^5.9.0"
19
+ }
20
+ }
@@ -0,0 +1,22 @@
1
+ import express, { type Request, type Response } from "express";
2
+ import { getTimeStamp } from "./utils/helpers";
3
+ import { successResponse } from "./utils/responseHelpers";
4
+
5
+ const app = express();
6
+
7
+ app.use(express.json());
8
+
9
+ // Health check
10
+ app.get("/health", (_req: Request, res: Response) =>
11
+ successResponse(res, {
12
+ statusCode: 200,
13
+ message: "API is up and running",
14
+ data: {
15
+ status: "ok",
16
+ uptime: Math.round(process.uptime()),
17
+ timestamp: getTimeStamp(),
18
+ },
19
+ }),
20
+ );
21
+
22
+ export default app;
@@ -0,0 +1,8 @@
1
+ import app from "./app";
2
+
3
+ const PORT = Number(process.env.PORT) || 3000;
4
+
5
+ app.listen(PORT, () => {
6
+ console.log(`API running on http://localhost:${PORT}`);
7
+ console.log(`Health check: http://localhost:${PORT}/health`);
8
+ });
@@ -0,0 +1,3 @@
1
+ export function getTimeStamp(): string {
2
+ return new Date().toISOString();
3
+ }
@@ -0,0 +1,42 @@
1
+ import type { Response } from "express";
2
+
3
+ export type FieldError = {
4
+ field: string;
5
+ message: string;
6
+ };
7
+
8
+ type SuccessOptions<T> = {
9
+ statusCode?: number;
10
+ message: string;
11
+ data?: T;
12
+ };
13
+
14
+ export function successResponse<T>(
15
+ res: Response,
16
+ { statusCode = 200, message, data }: SuccessOptions<T>,
17
+ ): Response {
18
+ return res.status(statusCode).json({
19
+ success: true,
20
+ message,
21
+ data,
22
+ });
23
+ }
24
+
25
+ type ErrorOptions = {
26
+ statusCode?: number;
27
+ message: string;
28
+ code?: string;
29
+ fields?: FieldError[];
30
+ };
31
+
32
+ export function errorResponse(
33
+ res: Response,
34
+ { statusCode = 500, message, code = "INTERNAL_ERROR", fields }: ErrorOptions,
35
+ ): Response {
36
+ return res.status(statusCode).json({
37
+ success: false,
38
+ message,
39
+ code,
40
+ fields,
41
+ });
42
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Imports / modules
4
+ // "bundler" is THE setting that lets you import WITHOUT extensions:
5
+ // import app from "./app" (not "./app.ts")
6
+ "module": "esnext", // emit modern ES module syntax
7
+ "moduleResolution": "bundler", // resolve imports without ".ts" extensions
8
+ "moduleDetection": "force", // treat every file as an ES module (no CommonJS guessing)
9
+ "esModuleInterop": true, // allow normal default imports from CJS packages (express)
10
+ "verbatimModuleSyntax": true, // keep imports exactly as written; type-only imports must use "import type"
11
+
12
+ // Language level
13
+ "target": "esnext", // allow the newest JS features, no downleveling
14
+
15
+ // Type checking
16
+ "strict": true, // turn on all strict type checks
17
+ "noUnusedLocals": true, // error on declared-but-unused variables
18
+ "noUnusedParameters": true, // error on unused function parameters
19
+ "skipLibCheck": true, // don't type-check ".d.ts" files inside node_modules (faster)
20
+
21
+ // Runtime types
22
+ "types": ["node"], // auto-include ONLY Node types (no stray umbrella types)
23
+
24
+ // Output
25
+ "noEmit": true // tsx runs the code directly, so tsc only type-checks, never writes files
26
+ },
27
+ // Files in scope
28
+ "include": ["src"], // the compiler only looks at src/
29
+ "exclude": ["node_modules"] // never scan dependencies
30
+ }
@@ -0,0 +1,28 @@
1
+ import express, { type Request, type Response } from "express";
2
+ import errorMiddleware from "./middleware/error.middleware";
3
+ import notFoundMiddleware from "./middleware/notFound.middleware";
4
+ import { getTimeStamp } from "./utils/helpers";
5
+ import { successResponse } from "./utils/responseHelpers";
6
+
7
+ const app = express();
8
+
9
+ app.use(express.json());
10
+
11
+ // Health check
12
+ app.get("/health", (_req: Request, res: Response) =>
13
+ successResponse(res, {
14
+ statusCode: 200,
15
+ message: "API is up and running",
16
+ data: {
17
+ status: "ok",
18
+ uptime: Math.round(process.uptime()),
19
+ timestamp: getTimeStamp(),
20
+ },
21
+ }),
22
+ );
23
+
24
+ // Unknown routes → 404, then every error gets formatted in one place.
25
+ app.use(notFoundMiddleware);
26
+ app.use(errorMiddleware);
27
+
28
+ export default app;
@@ -0,0 +1,42 @@
1
+ import type { NextFunction, Request, Response } from "express";
2
+ import { AppError, BadRequestError } from "../utils/AppError";
3
+ import { errorResponse } from "../utils/responseHelpers";
4
+ import { getTimeStamp } from "../utils/helpers";
5
+
6
+ // Express error handler — needs all 4 params, that's how Express recognizes it.
7
+ export default function errorMiddleware(
8
+ error: unknown,
9
+ req: Request,
10
+ res: Response,
11
+ _next: NextFunction,
12
+ ): Response {
13
+ const timeStamp = getTimeStamp();
14
+
15
+ // Expected errors — log them and send their details
16
+ if (error instanceof AppError) {
17
+ console.log(
18
+ `[${timeStamp}] [ERROR] ${req.method} ${req.originalUrl} - ${error.message}`,
19
+ );
20
+
21
+ return errorResponse(res, {
22
+ statusCode: error.statusCode,
23
+ message: error.message,
24
+ code: error.code,
25
+ fields: error instanceof BadRequestError ? error.fields : undefined, // fields only for bad requests
26
+ });
27
+ }
28
+
29
+ // Not an AppError — build a message if it's an Error, otherwise fall back
30
+ const message =
31
+ error instanceof Error ? error.message : "Internal server error";
32
+
33
+ console.error(
34
+ `[${timeStamp}] [ERROR] ${req.method} ${req.originalUrl} - ${message}`,
35
+ error,
36
+ );
37
+
38
+ return errorResponse(res, {
39
+ statusCode: 500,
40
+ message,
41
+ });
42
+ }
@@ -0,0 +1,10 @@
1
+ import type { NextFunction, Request, Response } from "express";
2
+ import { NotFoundError } from "../utils/AppError";
3
+
4
+ export default function notFoundMiddleware(
5
+ _req: Request,
6
+ _res: Response,
7
+ next: NextFunction,
8
+ ): void {
9
+ next(new NotFoundError("Route not found"));
10
+ }
@@ -0,0 +1,44 @@
1
+ import type { FieldError } from "../utils/responseHelpers";
2
+
3
+ export type AppErrorOptions = {
4
+ code?: string;
5
+ statusCode?: number;
6
+ };
7
+
8
+ export class AppError extends Error {
9
+ public readonly code: string;
10
+ public readonly statusCode: number;
11
+
12
+ constructor(
13
+ message: string,
14
+ { code = "INTERNAL_ERROR", statusCode = 500 }: AppErrorOptions = {},
15
+ ) {
16
+ super(message);
17
+
18
+ this.name = this.constructor.name;
19
+ this.code = code; // machine-readable code, e.g. "BAD_REQUEST"
20
+ this.statusCode = statusCode; // HTTP status to send back
21
+ }
22
+ }
23
+
24
+ export class BadRequestError extends AppError {
25
+ public readonly fields: FieldError[]; // only this error carries fields
26
+
27
+ constructor(message = "Bad request", fields: FieldError[] = []) {
28
+ super(message, { code: "BAD_REQUEST", statusCode: 400 });
29
+
30
+ this.fields = fields;
31
+ }
32
+ }
33
+
34
+ export class UnauthorizedError extends AppError {
35
+ constructor(message = "Unauthorized") {
36
+ super(message, { code: "UNAUTHORIZED", statusCode: 401 });
37
+ }
38
+ }
39
+
40
+ export class NotFoundError extends AppError {
41
+ constructor(message = "Not found") {
42
+ super(message, { code: "NOT_FOUND", statusCode: 404 });
43
+ }
44
+ }