@expressots/cli 3.0.0 → 4.0.0-preview.3

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 (194) hide show
  1. package/README.md +41 -95
  2. package/bin/cicd/cli.d.ts +6 -0
  3. package/bin/cicd/cli.js +128 -0
  4. package/bin/cicd/form.d.ts +29 -0
  5. package/bin/cicd/form.js +346 -0
  6. package/bin/cicd/generators/azure-devops.d.ts +2 -0
  7. package/bin/cicd/generators/azure-devops.js +370 -0
  8. package/bin/cicd/generators/bitbucket.d.ts +2 -0
  9. package/bin/cicd/generators/bitbucket.js +217 -0
  10. package/bin/cicd/generators/circleci.d.ts +2 -0
  11. package/bin/cicd/generators/circleci.js +274 -0
  12. package/bin/cicd/generators/github-actions.d.ts +14 -0
  13. package/bin/cicd/generators/github-actions.js +426 -0
  14. package/bin/cicd/generators/gitlab-ci.d.ts +2 -0
  15. package/bin/cicd/generators/gitlab-ci.js +237 -0
  16. package/bin/cicd/generators/index.d.ts +6 -0
  17. package/bin/cicd/generators/index.js +15 -0
  18. package/bin/cicd/generators/jenkins.d.ts +2 -0
  19. package/bin/cicd/generators/jenkins.js +248 -0
  20. package/bin/cicd/generators/template-loader.d.ts +17 -0
  21. package/bin/cicd/generators/template-loader.js +128 -0
  22. package/bin/cicd/index.d.ts +1 -0
  23. package/bin/cicd/index.js +5 -0
  24. package/bin/cli.d.ts +1 -5
  25. package/bin/cli.js +72 -7
  26. package/bin/commands/project.commands.d.ts +19 -6
  27. package/bin/commands/project.commands.js +602 -66
  28. package/bin/config/index.d.ts +5 -0
  29. package/bin/config/index.js +10 -0
  30. package/bin/config/manager.d.ts +98 -0
  31. package/bin/config/manager.js +222 -0
  32. package/bin/containerize/analyzers/bootstrap-analyzer.d.ts +46 -0
  33. package/bin/containerize/analyzers/bootstrap-analyzer.js +187 -0
  34. package/bin/containerize/analyzers/project-analyzer.d.ts +20 -0
  35. package/bin/containerize/analyzers/project-analyzer.js +150 -0
  36. package/bin/containerize/cli.d.ts +4 -0
  37. package/bin/containerize/cli.js +113 -0
  38. package/bin/containerize/form.d.ts +15 -0
  39. package/bin/containerize/form.js +152 -0
  40. package/bin/containerize/generators/ci-generator.d.ts +31 -0
  41. package/bin/containerize/generators/ci-generator.js +940 -0
  42. package/bin/containerize/generators/docker-compose-generator.d.ts +8 -0
  43. package/bin/containerize/generators/docker-compose-generator.js +187 -0
  44. package/bin/containerize/generators/dockerfile-generator.d.ts +8 -0
  45. package/bin/containerize/generators/dockerfile-generator.js +657 -0
  46. package/bin/containerize/generators/kubernetes-generator.d.ts +8 -0
  47. package/bin/containerize/generators/kubernetes-generator.js +134 -0
  48. package/bin/containerize/generators/template-loader.d.ts +36 -0
  49. package/bin/containerize/generators/template-loader.js +129 -0
  50. package/bin/containerize/index.d.ts +4 -0
  51. package/bin/containerize/index.js +13 -0
  52. package/bin/containerize/presets/preset-registry.d.ts +20 -0
  53. package/bin/containerize/presets/preset-registry.js +102 -0
  54. package/bin/costs/cli.d.ts +5 -0
  55. package/bin/costs/cli.js +185 -0
  56. package/bin/costs/form.d.ts +44 -0
  57. package/bin/costs/form.js +412 -0
  58. package/bin/costs/index.d.ts +4 -0
  59. package/bin/costs/index.js +25 -0
  60. package/bin/costs/pricing-manager.d.ts +84 -0
  61. package/bin/costs/pricing-manager.js +342 -0
  62. package/bin/costs/providers/index.d.ts +32 -0
  63. package/bin/costs/providers/index.js +153 -0
  64. package/bin/costs/sources/api-source.d.ts +10 -0
  65. package/bin/costs/sources/api-source.js +32 -0
  66. package/bin/costs/sources/index.d.ts +6 -0
  67. package/bin/costs/sources/index.js +15 -0
  68. package/bin/costs/sources/local-json-source.d.ts +23 -0
  69. package/bin/costs/sources/local-json-source.js +59 -0
  70. package/bin/costs/sources/remote-json-source.d.ts +11 -0
  71. package/bin/costs/sources/remote-json-source.js +53 -0
  72. package/bin/costs/types.d.ts +53 -0
  73. package/bin/costs/types.js +5 -0
  74. package/bin/dev/cli.d.ts +4 -0
  75. package/bin/dev/cli.js +136 -0
  76. package/bin/dev/form.d.ts +36 -0
  77. package/bin/dev/form.js +254 -0
  78. package/bin/dev/index.d.ts +1 -0
  79. package/bin/dev/index.js +5 -0
  80. package/bin/generate/cli.d.ts +1 -1
  81. package/bin/generate/cli.js +29 -2
  82. package/bin/generate/form.d.ts +5 -1
  83. package/bin/generate/form.js +3 -3
  84. package/bin/generate/templates/nonopinionated/config.tpl +12 -0
  85. package/bin/generate/templates/nonopinionated/event.tpl +10 -0
  86. package/bin/generate/templates/nonopinionated/guard.tpl +18 -0
  87. package/bin/generate/templates/nonopinionated/handler.tpl +12 -0
  88. package/bin/generate/templates/nonopinionated/interceptor.tpl +27 -0
  89. package/bin/generate/templates/opinionated/config.tpl +47 -0
  90. package/bin/generate/templates/opinionated/entity.tpl +1 -8
  91. package/bin/generate/templates/opinionated/event.tpl +15 -0
  92. package/bin/generate/templates/opinionated/guard.tpl +41 -0
  93. package/bin/generate/templates/opinionated/handler.tpl +23 -0
  94. package/bin/generate/templates/opinionated/interceptor.tpl +50 -0
  95. package/bin/generate/utils/command-utils.d.ts +20 -5
  96. package/bin/generate/utils/command-utils.js +145 -48
  97. package/bin/generate/utils/nonopininated-cmd.d.ts +10 -1
  98. package/bin/generate/utils/nonopininated-cmd.js +100 -1
  99. package/bin/generate/utils/opinionated-cmd.d.ts +10 -1
  100. package/bin/generate/utils/opinionated-cmd.js +128 -16
  101. package/bin/generate/utils/string-utils.d.ts +6 -0
  102. package/bin/generate/utils/string-utils.js +13 -1
  103. package/bin/help/cli.d.ts +1 -1
  104. package/bin/help/command-help-registry.d.ts +23 -0
  105. package/bin/help/command-help-registry.js +303 -0
  106. package/bin/help/command-help.d.ts +36 -0
  107. package/bin/help/command-help.js +56 -0
  108. package/bin/help/form.js +127 -22
  109. package/bin/help/main-help.d.ts +8 -0
  110. package/bin/help/main-help.js +126 -0
  111. package/bin/help/render.d.ts +32 -0
  112. package/bin/help/render.js +46 -0
  113. package/bin/info/cli.d.ts +1 -1
  114. package/bin/info/form.d.ts +1 -1
  115. package/bin/info/form.js +11 -11
  116. package/bin/migrate/analyzers/platform-detector.d.ts +14 -0
  117. package/bin/migrate/analyzers/platform-detector.js +116 -0
  118. package/bin/migrate/cli.d.ts +6 -0
  119. package/bin/migrate/cli.js +98 -0
  120. package/bin/migrate/form.d.ts +25 -0
  121. package/bin/migrate/form.js +348 -0
  122. package/bin/migrate/generators/compose-to-k8s.d.ts +2 -0
  123. package/bin/migrate/generators/compose-to-k8s.js +324 -0
  124. package/bin/migrate/generators/compose-to-railway.d.ts +2 -0
  125. package/bin/migrate/generators/compose-to-railway.js +138 -0
  126. package/bin/migrate/generators/compose-to-render.d.ts +2 -0
  127. package/bin/migrate/generators/compose-to-render.js +148 -0
  128. package/bin/migrate/generators/generic-migration.d.ts +9 -0
  129. package/bin/migrate/generators/generic-migration.js +221 -0
  130. package/bin/migrate/generators/heroku-to-fly.d.ts +2 -0
  131. package/bin/migrate/generators/heroku-to-fly.js +291 -0
  132. package/bin/migrate/generators/heroku-to-railway.d.ts +2 -0
  133. package/bin/migrate/generators/heroku-to-railway.js +283 -0
  134. package/bin/migrate/generators/heroku-to-render.d.ts +2 -0
  135. package/bin/migrate/generators/heroku-to-render.js +148 -0
  136. package/bin/migrate/generators/index.d.ts +7 -0
  137. package/bin/migrate/generators/index.js +17 -0
  138. package/bin/migrate/generators/template-loader.d.ts +21 -0
  139. package/bin/migrate/generators/template-loader.js +59 -0
  140. package/bin/migrate/index.d.ts +1 -0
  141. package/bin/migrate/index.js +5 -0
  142. package/bin/new/cli.d.ts +5 -1
  143. package/bin/new/cli.js +77 -14
  144. package/bin/new/form.d.ts +27 -4
  145. package/bin/new/form.js +605 -75
  146. package/bin/profile/analyzers/dockerfile-analyzer.d.ts +27 -0
  147. package/bin/profile/analyzers/dockerfile-analyzer.js +122 -0
  148. package/bin/profile/analyzers/image-analyzer.d.ts +19 -0
  149. package/bin/profile/analyzers/image-analyzer.js +85 -0
  150. package/bin/profile/cli.d.ts +4 -0
  151. package/bin/profile/cli.js +94 -0
  152. package/bin/profile/form.d.ts +56 -0
  153. package/bin/profile/form.js +401 -0
  154. package/bin/profile/index.d.ts +1 -0
  155. package/bin/profile/index.js +5 -0
  156. package/bin/profile/optimizers/index.d.ts +19 -0
  157. package/bin/profile/optimizers/index.js +137 -0
  158. package/bin/providers/add/form.d.ts +1 -1
  159. package/bin/providers/add/form.js +27 -6
  160. package/bin/providers/create/form.js +53 -3
  161. package/bin/scripts/form.js +27 -5
  162. package/bin/studio/cli.d.ts +15 -0
  163. package/bin/studio/cli.js +172 -0
  164. package/bin/studio/index.d.ts +5 -0
  165. package/bin/studio/index.js +9 -0
  166. package/bin/templates/cache.d.ts +54 -0
  167. package/bin/templates/cache.js +180 -0
  168. package/bin/templates/cli.d.ts +8 -0
  169. package/bin/templates/cli.js +294 -0
  170. package/bin/templates/fetcher.d.ts +49 -0
  171. package/bin/templates/fetcher.js +208 -0
  172. package/bin/templates/index.d.ts +11 -0
  173. package/bin/templates/index.js +37 -0
  174. package/bin/templates/manager.d.ts +116 -0
  175. package/bin/templates/manager.js +323 -0
  176. package/bin/templates/renderer.d.ts +49 -0
  177. package/bin/templates/renderer.js +204 -0
  178. package/bin/templates/types.d.ts +51 -0
  179. package/bin/templates/types.js +5 -0
  180. package/bin/utils/add-module-to-container.d.ts +14 -3
  181. package/bin/utils/add-module-to-container.js +327 -98
  182. package/bin/utils/cli-ui.d.ts +49 -3
  183. package/bin/utils/cli-ui.js +133 -13
  184. package/bin/utils/index.d.ts +4 -0
  185. package/bin/utils/index.js +4 -0
  186. package/bin/utils/input-validation.d.ts +50 -0
  187. package/bin/utils/input-validation.js +143 -0
  188. package/bin/utils/package-manager-commands.d.ts +24 -0
  189. package/bin/utils/package-manager-commands.js +50 -0
  190. package/bin/utils/safe-spawn.d.ts +35 -0
  191. package/bin/utils/safe-spawn.js +51 -0
  192. package/bin/utils/update-tsconfig-paths.d.ts +35 -0
  193. package/bin/utils/update-tsconfig-paths.js +326 -0
  194. package/package.json +165 -156
package/bin/new/form.js CHANGED
@@ -8,115 +8,513 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const cli_progress_1 = require("cli-progress");
9
9
  const degit_1 = __importDefault(require("degit"));
10
10
  const inquirer_1 = __importDefault(require("inquirer"));
11
- const node_child_process_1 = require("node:child_process");
12
11
  const node_fs_1 = __importDefault(require("node:fs"));
13
12
  const node_path_1 = __importDefault(require("node:path"));
14
13
  const cli_1 = require("../cli");
15
14
  const center_text_1 = require("../utils/center-text");
16
15
  const change_package_info_1 = require("../utils/change-package-info");
17
16
  const cli_ui_1 = require("../utils/cli-ui");
17
+ const input_validation_1 = require("../utils/input-validation");
18
+ const safe_spawn_1 = require("../utils/safe-spawn");
19
+ /**
20
+ * Install dependencies using the selected package manager
21
+ */
18
22
  async function packageManagerInstall({ packageManager, directory, progressBar, }) {
19
- const command = process.platform === "win32" ? `${packageManager}.cmd` : packageManager;
20
- const args = ["install", "--silent"];
23
+ if (!(0, input_validation_1.isValidPackageManager)(packageManager)) {
24
+ throw new Error(`Invalid package manager: ${packageManager}`);
25
+ }
26
+ // npm's `--silent` swallows errors too (loglevel=silent), which
27
+ // makes failures impossible to diagnose. `--loglevel=error` keeps
28
+ // the install quiet on the happy path but lets real failures
29
+ // stream to stderr so we can capture and surface them below.
30
+ const args = packageManager === "npm"
31
+ ? ["install", "--loglevel=error"]
32
+ : ["install", "--silent"];
21
33
  if (packageManager === "yarn") {
22
34
  args.push("--ignore-engines");
23
- args.splice(args.indexOf("--prefer-offline"), 1);
24
35
  }
25
36
  return new Promise((resolve, reject) => {
26
- const installProcess = (0, node_child_process_1.spawn)(command, args, {
37
+ // `safeSpawn` (cross-spawn) handles the Windows `.cmd` shim
38
+ // resolution and properly escapes argv even when the shell is
39
+ // involved on Windows. The `directory` value is only used as
40
+ // cwd; it is never interpolated into a command string.
41
+ const installProcess = (0, safe_spawn_1.safeSpawn)(packageManager, args, {
27
42
  cwd: directory,
28
- shell: true,
29
43
  timeout: 600000,
30
44
  });
31
- // Simulate incremental progress
32
- let progress = 0;
45
+ let progress = 50;
46
+ let lastProgressUpdate = Date.now();
33
47
  const interval = setInterval(() => {
34
- if (progress < 90) {
35
- progress += 5;
36
- progressBar.update(progress);
48
+ const now = Date.now();
49
+ if (progress < 88) {
50
+ const increment = progress < 70 ? 3 : 1;
51
+ progress = Math.min(progress + increment, 88);
52
+ progressBar.update(progress, {
53
+ doing: "Installing dependencies...",
54
+ });
55
+ }
56
+ else if (now - lastProgressUpdate > 3000) {
57
+ progressBar.update(progress, {
58
+ doing: "Installing dependencies...",
59
+ });
37
60
  }
38
61
  }, 1000);
39
- // Handle stdout for meaningful output or progress feedback
62
+ // Keep a rolling tail of stderr/stdout so we can surface a
63
+ // meaningful diagnostic when the install exits non-zero (npm's
64
+ // real error is otherwise hidden behind `--silent`).
65
+ const diagnosticBuffer = [];
66
+ const MAX_DIAGNOSTIC_LINES = 20;
67
+ const recordDiagnostic = (chunk) => {
68
+ for (const line of chunk.split(/\r?\n/)) {
69
+ const trimmed = line.trim();
70
+ if (!trimmed)
71
+ continue;
72
+ diagnosticBuffer.push(trimmed);
73
+ if (diagnosticBuffer.length > MAX_DIAGNOSTIC_LINES) {
74
+ diagnosticBuffer.shift();
75
+ }
76
+ }
77
+ };
40
78
  installProcess.stdout?.on("data", (data) => {
41
- const output = data.toString().trim();
42
- // Remove all data from || to the end of the line
43
- const cleanedOutput = output.replace(/\|\|.*$/g, "");
44
- // Match and handle npm-specific progress
79
+ const output = data.toString();
80
+ recordDiagnostic(output);
81
+ const cleanedOutput = output.trim().replace(/\|\|.*$/g, "");
45
82
  const npmProgressMatch = cleanedOutput.match(/\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/);
46
83
  if (npmProgressMatch) {
47
84
  const [, current, total, task] = npmProgressMatch;
48
- progress = Math.round((parseInt(current) / parseInt(total)) * 100);
85
+ const npmProgress = (parseInt(current) / parseInt(total)) * 100;
86
+ progress = Math.round(50 + npmProgress * 0.4);
87
+ lastProgressUpdate = Date.now();
49
88
  progressBar.update(progress, { doing: task });
50
89
  }
51
- else {
52
- // Update "task" without changing the progress
90
+ else if (cleanedOutput) {
91
+ lastProgressUpdate = Date.now();
53
92
  progressBar.update(progress, { doing: cleanedOutput });
54
93
  }
55
94
  });
56
- // Handle errors
95
+ installProcess.stderr?.on("data", (data) => {
96
+ const output = data.toString();
97
+ recordDiagnostic(output);
98
+ const cleanedOutput = output.trim().replace(/\|\|.*$/g, "");
99
+ const npmProgressMatch = cleanedOutput.match(/\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/);
100
+ if (npmProgressMatch) {
101
+ const [, current, total, task] = npmProgressMatch;
102
+ const npmProgress = (parseInt(current) / parseInt(total)) * 100;
103
+ progress = Math.round(50 + npmProgress * 0.4);
104
+ lastProgressUpdate = Date.now();
105
+ progressBar.update(progress, { doing: task });
106
+ }
107
+ });
57
108
  installProcess.on("error", (error) => {
58
- clearInterval(interval); // Stop interval on error
109
+ clearInterval(interval);
59
110
  progressBar.stop();
60
111
  reject(new Error(`Failed to start subprocess: ${error.message}`));
61
112
  });
62
- // Finalize progress on close
63
113
  installProcess.on("close", (code) => {
64
- clearInterval(interval); // Stop interval when the process ends
114
+ clearInterval(interval);
65
115
  if (code === 0) {
66
- progressBar.update(100, { doing: "Complete!" }); // Finalize progress
67
- progressBar.stop();
116
+ progressBar.update(90, { doing: "Dependencies installed" });
68
117
  resolve("Installation Done!");
69
118
  }
70
119
  else {
71
120
  progressBar.stop();
121
+ if (diagnosticBuffer.length > 0) {
122
+ console.log("\n");
123
+ console.log(chalk_1.default.bold.red(`${packageManager} install failed with exit code ${code}:`));
124
+ for (const line of diagnosticBuffer) {
125
+ console.log(chalk_1.default.gray(` ${line}`));
126
+ }
127
+ console.log("");
128
+ }
72
129
  reject(new Error(`${packageManager} install exited with code ${code}`));
73
130
  }
74
131
  });
75
132
  });
76
133
  }
134
+ /**
135
+ * Check if the package manager is installed
136
+ */
77
137
  async function checkIfPackageManagerExists(packageManager) {
78
- try {
79
- (0, node_child_process_1.execSync)(`${packageManager} --version`);
80
- return true;
138
+ if (!(0, input_validation_1.isValidPackageManager)(packageManager)) {
139
+ (0, cli_ui_1.printError)("Package manager not found!", packageManager);
140
+ process.exit(1);
81
141
  }
82
- catch (error) {
142
+ const result = (0, safe_spawn_1.safeSpawnSync)(packageManager, ["--version"], {
143
+ stdio: "ignore",
144
+ });
145
+ if (result.error || result.status !== 0) {
83
146
  (0, cli_ui_1.printError)("Package manager not found!", packageManager);
84
147
  process.exit(1);
85
148
  }
149
+ return true;
86
150
  }
87
- function renameEnvFile(directory) {
88
- try {
89
- const envExamplePath = node_path_1.default.join(directory, ".env.example");
90
- const envPath = node_path_1.default.join(directory, ".env");
91
- if (!node_fs_1.default.existsSync(envExamplePath)) {
92
- throw new Error(`File not found: ${envExamplePath}`);
93
- }
94
- node_fs_1.default.renameSync(envExamplePath, envPath);
151
+ /**
152
+ * Copy directory recursively (for local template testing)
153
+ */
154
+ function copyDirectorySync(src, dest) {
155
+ if (!node_fs_1.default.existsSync(dest)) {
156
+ node_fs_1.default.mkdirSync(dest, { recursive: true });
95
157
  }
96
- catch (error) {
97
- (0, cli_ui_1.printError)("Error renaming .env.example file", ".env.example to .env");
98
- process.exit(1);
158
+ const entries = node_fs_1.default.readdirSync(src, { withFileTypes: true });
159
+ for (const entry of entries) {
160
+ const srcPath = node_path_1.default.join(src, entry.name);
161
+ const destPath = node_path_1.default.join(dest, entry.name);
162
+ // Skip node_modules and dist directories
163
+ if (entry.name === "node_modules" || entry.name === "dist") {
164
+ continue;
165
+ }
166
+ if (entry.isDirectory()) {
167
+ copyDirectorySync(srcPath, destPath);
168
+ }
169
+ else {
170
+ node_fs_1.default.copyFileSync(srcPath, destPath);
171
+ }
99
172
  }
100
173
  }
174
+ /**
175
+ * Template definitions for v4.0
176
+ */
101
177
  var Template;
102
178
  (function (Template) {
103
- Template["nonopinionated"] = "Non-Opinionated :: Start with a clean slate and build your project from scratch.";
104
- Template["opinionated"] = "Opinionated :: Automatically scaffolds resources into a preset project structure. (Recommended)";
105
- Template["micro"] = "Micro :: A minimalistic template for building micro api's.";
179
+ Template["application"] = "Application :: Full-featured ExpressoTS application. (Recommended)";
180
+ Template["applicationWithEvents"] = "Application with Events :: Application template pre-wired with the type-safe Event Bus example.";
181
+ Template["micro"] = "Micro :: A minimalistic template for building micro APIs and serverless functions.";
106
182
  })(Template || (Template = {}));
183
+ /**
184
+ * Middleware presets for Application template
185
+ */
186
+ var MiddlewarePreset;
187
+ (function (MiddlewarePreset) {
188
+ MiddlewarePreset["api"] = "API :: REST API with security, compression, and auto-logging. (Recommended)";
189
+ MiddlewarePreset["web"] = "Web :: Full web app with cookies and session support.";
190
+ MiddlewarePreset["graphql"] = "GraphQL :: Optimized for GraphQL APIs.";
191
+ MiddlewarePreset["microservice"] = "Microservice :: Minimal setup for microservices.";
192
+ MiddlewarePreset["minimal"] = "Minimal :: Just request parsing, customize everything yourself.";
193
+ })(MiddlewarePreset || (MiddlewarePreset = {}));
194
+ /**
195
+ * Template folder mapping
196
+ */
197
+ const TEMPLATE_FOLDERS = {
198
+ Application: "application",
199
+ "Application with Events": "application-with-events",
200
+ Micro: "micro",
201
+ };
202
+ /**
203
+ * Middleware preset mapping to code
204
+ */
205
+ const PRESET_CODE = {
206
+ API: `this.Middleware.applyPreset("api");`,
207
+ Web: `this.Middleware.applyPreset("web");`,
208
+ GraphQL: [
209
+ `this.Middleware.applyPreset("graphql");`,
210
+ ``,
211
+ ` const apolloServer = new ApolloServer({ typeDefs, resolvers });`,
212
+ ` await apolloServer.start();`,
213
+ ` this.Middleware.add({`,
214
+ ` path: "/graphql",`,
215
+ ` middlewares: [expressMiddleware(apolloServer)],`,
216
+ ` });`,
217
+ ].join("\n"),
218
+ Microservice: `this.Middleware.applyPreset("microservice");`,
219
+ Minimal: `this.Middleware.parse();`,
220
+ };
221
+ /**
222
+ * Extra imports that specific presets need appended to app.ts.
223
+ */
224
+ const PRESET_IMPORTS = {
225
+ GraphQL: [
226
+ `import { ApolloServer } from "@apollo/server";`,
227
+ `import { expressMiddleware } from "@as-integrations/express5";`,
228
+ `import { typeDefs, resolvers } from "./graphql/schema";`,
229
+ ].join("\n"),
230
+ };
231
+ /**
232
+ * Per-preset runtime dependencies. The base application template ships only
233
+ * express + framework packages; each preset declares exactly which optional
234
+ * middleware packages it needs so scaffolded projects stay lean.
235
+ *
236
+ * Versions are pinned with `^` ranges matching the middleware-resolver
237
+ * registry expectations in `@expressots/core`.
238
+ */
239
+ const PRESET_DEPENDENCIES = {
240
+ API: {
241
+ dependencies: {
242
+ compression: "^1.8.1",
243
+ cors: "^2.8.6",
244
+ "express-rate-limit": "^8.5.1",
245
+ helmet: "^8.1.0",
246
+ },
247
+ devDependencies: {
248
+ "@types/compression": "^1.7.5",
249
+ "@types/cors": "^2.8.17",
250
+ },
251
+ },
252
+ Web: {
253
+ dependencies: {
254
+ compression: "^1.8.1",
255
+ "cookie-parser": "^1.4.7",
256
+ cors: "^2.8.6",
257
+ helmet: "^8.1.0",
258
+ },
259
+ devDependencies: {
260
+ "@types/compression": "^1.7.5",
261
+ "@types/cookie-parser": "^1.4.8",
262
+ "@types/cors": "^2.8.17",
263
+ },
264
+ },
265
+ GraphQL: {
266
+ dependencies: {
267
+ "@apollo/server": "^5.5.1",
268
+ "@as-integrations/express5": "^1.1.2",
269
+ compression: "^1.8.1",
270
+ cors: "^2.8.6",
271
+ graphql: "^16.14.0",
272
+ helmet: "^8.1.0",
273
+ },
274
+ devDependencies: {
275
+ "@types/compression": "^1.7.5",
276
+ "@types/cors": "^2.8.17",
277
+ },
278
+ },
279
+ Microservice: {
280
+ dependencies: {
281
+ compression: "^1.8.1",
282
+ },
283
+ devDependencies: {
284
+ "@types/compression": "^1.7.5",
285
+ },
286
+ },
287
+ Minimal: {},
288
+ };
289
+ /**
290
+ * Apply the selected middleware preset to the generated app.ts
291
+ */
292
+ function applyMiddlewarePreset(directory, preset) {
293
+ const appTsPath = node_path_1.default.join(directory, "src", "app.ts");
294
+ if (!node_fs_1.default.existsSync(appTsPath)) {
295
+ return;
296
+ }
297
+ // Extract preset name from selection (e.g., "API :: ..." -> "API")
298
+ const presetMatch = preset.match(/^(\w+) ::/);
299
+ const presetName = presetMatch ? presetMatch[1] : "API";
300
+ const presetCode = PRESET_CODE[presetName] || PRESET_CODE["API"];
301
+ let content = node_fs_1.default.readFileSync(appTsPath, "utf-8");
302
+ // Inject preset-specific imports after the existing import block.
303
+ // Match the first blank line (handles both LF and CRLF endings).
304
+ const extraImports = PRESET_IMPORTS[presetName];
305
+ if (extraImports) {
306
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
307
+ content = content.replace(new RegExp(`${eol}${eol}`), `${eol}${extraImports}${eol}${eol}`);
308
+ }
309
+ // Replace the placeholder with the preset code
310
+ content = content.replace(/\/\/ __MIDDLEWARE_PRESET_PLACEHOLDER__/, presetCode);
311
+ node_fs_1.default.writeFileSync(appTsPath, content, "utf-8");
312
+ }
313
+ /**
314
+ * GraphQL schema scaffold content. Provides sample typeDefs and resolvers
315
+ * so the generated project has a working `/graphql` endpoint out of the box.
316
+ */
317
+ const GRAPHQL_SCHEMA_CONTENT = `export const typeDefs = \`#graphql
318
+ type Query {
319
+ hello: String!
320
+ health: HealthStatus!
321
+ }
322
+
323
+ type Mutation {
324
+ echo(message: String!): EchoResponse!
325
+ }
326
+
327
+ type HealthStatus {
328
+ status: String!
329
+ timestamp: String!
330
+ uptime: Float!
331
+ }
332
+
333
+ type EchoResponse {
334
+ message: String!
335
+ receivedAt: String!
336
+ }
337
+ \`;
338
+
339
+ export const resolvers = {
340
+ Query: {
341
+ hello: () => "Hello from ExpressoTS GraphQL!",
342
+ health: () => ({
343
+ status: "ok",
344
+ timestamp: new Date().toISOString(),
345
+ uptime: process.uptime(),
346
+ }),
347
+ },
348
+ Mutation: {
349
+ echo: (_: unknown, { message }: { message: string }) => ({
350
+ message,
351
+ receivedAt: new Date().toISOString(),
352
+ }),
353
+ },
354
+ };
355
+ `;
356
+ /**
357
+ * Create additional source files required by specific presets.
358
+ * For example, the GraphQL preset ships a starter schema + resolvers.
359
+ */
360
+ function createPresetFiles(directory, preset) {
361
+ const presetMatch = preset.match(/^(\w+) ::/);
362
+ const presetName = presetMatch ? presetMatch[1] : "API";
363
+ if (presetName === "GraphQL") {
364
+ const graphqlDir = node_path_1.default.join(directory, "src", "graphql");
365
+ if (!node_fs_1.default.existsSync(graphqlDir)) {
366
+ node_fs_1.default.mkdirSync(graphqlDir, { recursive: true });
367
+ }
368
+ node_fs_1.default.writeFileSync(node_path_1.default.join(graphqlDir, "schema.ts"), GRAPHQL_SCHEMA_CONTENT, "utf-8");
369
+ }
370
+ }
371
+ /**
372
+ * Inject preset-specific dependencies into the scaffolded project's
373
+ * package.json **before** `npm install` runs, so everything resolves
374
+ * in a single install step.
375
+ */
376
+ function injectPresetDependencies(directory, preset) {
377
+ const pkgPath = node_path_1.default.join(directory, "package.json");
378
+ if (!node_fs_1.default.existsSync(pkgPath))
379
+ return;
380
+ const presetMatch = preset.match(/^(\w+) ::/);
381
+ const presetName = presetMatch ? presetMatch[1] : "API";
382
+ const presetDeps = PRESET_DEPENDENCIES[presetName];
383
+ if (!presetDeps)
384
+ return;
385
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf-8"));
386
+ if (presetDeps.dependencies) {
387
+ pkg.dependencies = { ...pkg.dependencies, ...presetDeps.dependencies };
388
+ }
389
+ if (presetDeps.devDependencies) {
390
+ pkg.devDependencies = {
391
+ ...pkg.devDependencies,
392
+ ...presetDeps.devDependencies,
393
+ };
394
+ }
395
+ node_fs_1.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n", "utf-8");
396
+ }
397
+ /**
398
+ * Enable local template mode for testing.
399
+ * Opt-in via `EXPRESSOTS_DEV=1` and `EXPRESSOTS_USE_LOCAL_TEMPLATES=1`.
400
+ * Both must be set so a stray env var alone cannot redirect a user's
401
+ * `expressots new` to local files.
402
+ */
403
+ const USE_LOCAL_TEMPLATES = process.env.EXPRESSOTS_DEV === "1" &&
404
+ process.env.EXPRESSOTS_USE_LOCAL_TEMPLATES === "1";
405
+ /**
406
+ * Skip the package-manager install step. Useful when iterating on
407
+ * templates that depend on unpublished package versions. Same dual
408
+ * env-var guard as `USE_LOCAL_TEMPLATES`.
409
+ */
410
+ const SKIP_INSTALL_FOR_TESTING = process.env.EXPRESSOTS_DEV === "1" &&
411
+ process.env.EXPRESSOTS_SKIP_INSTALL === "1";
412
+ /**
413
+ * Local templates path (relative to CLI installation)
414
+ * For development: points to the templates folder in the monorepo
415
+ * For production: this will be replaced with the actual path
416
+ */
417
+ const LOCAL_TEMPLATES_PATH = node_path_1.default.resolve(__dirname, "../../../templates");
418
+ /**
419
+ * Optional override for the templates ref/tag.
420
+ *
421
+ * Useful during the preview window before the matching `vX.Y.Z` tag exists
422
+ * on `expressots/templates`. Setting `EXPRESSOTS_TEMPLATE_REF=feature/v4.0`
423
+ * makes `expressots new` clone from that branch instead of the version tag.
424
+ *
425
+ * This is intentionally NOT gated by `EXPRESSOTS_DEV`: even an installed
426
+ * CLI consumer can opt into a custom ref to test pre-release scaffolds.
427
+ */
428
+ const TEMPLATE_REF_OVERRIDE = process.env.EXPRESSOTS_TEMPLATE_REF?.trim() || "";
429
+ /**
430
+ * Resolve the degit ref to use when fetching a template.
431
+ *
432
+ * Priority:
433
+ * 1. Explicit override via `EXPRESSOTS_TEMPLATE_REF`
434
+ * 2. The version-pinned tag matching this CLI build (`v${BUNDLE_VERSION}`)
435
+ */
436
+ function resolveTemplateRef() {
437
+ if (TEMPLATE_REF_OVERRIDE)
438
+ return TEMPLATE_REF_OVERRIDE;
439
+ return `v${cli_1.BUNDLE_VERSION}`;
440
+ }
441
+ /**
442
+ * Build the full degit URL for a given template folder.
443
+ */
444
+ function buildTemplateRepo(templateFolder, ref) {
445
+ return `expressots/templates/${templateFolder}#${ref}`;
446
+ }
447
+ /**
448
+ * Detect whether the running CLI is a preview build (e.g.
449
+ * `4.0.0-preview.3`). During the preview window the matching templates tag
450
+ * may not yet exist on GitHub, so we allow a soft fallback to the active
451
+ * release branch.
452
+ */
453
+ function isPreviewBuild() {
454
+ return /-(?:preview|alpha|beta|rc)\b/i.test(cli_1.BUNDLE_VERSION);
455
+ }
456
+ /**
457
+ * Fallback ref used when the primary ref is missing AND we are running a
458
+ * preview build. Matches the framework's working branch.
459
+ */
460
+ const PREVIEW_FALLBACK_REF = "feature/v4.0";
461
+ /**
462
+ * Clone a template from the public `expressots/templates` repo via degit.
463
+ *
464
+ * On `MISSING_REF` we attempt one graceful retry against the preview
465
+ * fallback branch — this keeps `npx @expressots/cli@next new` usable during
466
+ * the window between a CLI publish and the matching templates-tag push.
467
+ * For non-preview builds we let the error propagate so the user gets a
468
+ * loud, accurate diagnostic.
469
+ */
470
+ async function cloneFromGitHub({ templateFolder, targetDir, progressBar, }) {
471
+ const primaryRef = resolveTemplateRef();
472
+ const primaryRepo = buildTemplateRepo(templateFolder, primaryRef);
473
+ try {
474
+ await (0, degit_1.default)(primaryRepo, { force: false }).clone(targetDir);
475
+ progressBar.update(30, { doing: "Template ready" });
476
+ return;
477
+ }
478
+ catch (err) {
479
+ const isMissingRef = err?.code === "MISSING_REF";
480
+ const canFallback = isMissingRef && !TEMPLATE_REF_OVERRIDE && isPreviewBuild();
481
+ if (!canFallback)
482
+ throw err;
483
+ // Tag for this preview hasn't been pushed yet; transparently retry
484
+ // against the working branch and surface a one-line warning so the
485
+ // user knows what they actually got. Written to stdout while the bar
486
+ // renders on stderr, so the streams do not interfere.
487
+ console.log(chalk_1.default.yellow(`\n⚠ Templates tag "${primaryRef}" not found on GitHub yet — falling back to "${PREVIEW_FALLBACK_REF}". ` +
488
+ `Set EXPRESSOTS_TEMPLATE_REF=<branch-or-tag> to override.`));
489
+ const fallbackRepo = buildTemplateRepo(templateFolder, PREVIEW_FALLBACK_REF);
490
+ await (0, degit_1.default)(fallbackRepo, { force: false }).clone(targetDir);
491
+ progressBar.update(30, { doing: "Template ready (fallback ref)" });
492
+ }
493
+ }
494
+ /**
495
+ * Main project creation form
496
+ */
107
497
  const projectForm = async (projectName, args) => {
108
498
  let answer;
109
- const [packageManager, template, directory] = args;
499
+ const [packageManager, template, directory, preset, events] = args;
110
500
  if (packageManager && template) {
501
+ const resolvedPreset = preset ??
502
+ (template === "application"
503
+ ? "api"
504
+ : undefined);
111
505
  answer = {
112
506
  name: projectName,
113
507
  packageManager: packageManager,
114
508
  template: Template[template],
509
+ preset: resolvedPreset
510
+ ? MiddlewarePreset[resolvedPreset]
511
+ : undefined,
512
+ events: events,
115
513
  confirm: true,
116
514
  };
117
515
  }
118
516
  else {
119
- answer = await inquirer_1.default.prompt([
517
+ const baseAnswers = await inquirer_1.default.prompt([
120
518
  {
121
519
  type: "input",
122
520
  name: "name",
@@ -130,18 +528,56 @@ const projectForm = async (projectName, args) => {
130
528
  type: "list",
131
529
  name: "packageManager",
132
530
  message: "Package manager",
133
- choices: ["npm", "yarn", "pnpm", "bun"],
531
+ choices: [
532
+ "npm",
533
+ "yarn",
534
+ "pnpm",
535
+ ...(process.platform !== "win32" ? ["bun"] : []),
536
+ ],
134
537
  },
135
538
  {
136
539
  type: "list",
137
540
  name: "template",
138
541
  message: "Select a template",
139
542
  choices: [
140
- `Opinionated :: Automatically scaffolds resources into a preset project structure. (${chalk_1.default.yellow("Recommended")})`,
141
- "NonOpinionated :: Allows users to choose where to scaffold resources, offering flexible project organization.",
142
- "Micro :: A minimalistic template for building micro api's.",
543
+ `Application :: Full-featured ExpressoTS application. (${chalk_1.default.yellow("Recommended")})`,
544
+ "Micro :: A minimalistic template for building micro APIs and serverless functions.",
143
545
  ],
144
546
  },
547
+ ]);
548
+ // Only show preset selection for Application template
549
+ let presetAnswer = {};
550
+ let eventsAnswer = {};
551
+ if (baseAnswers.template.startsWith("Application")) {
552
+ presetAnswer = await inquirer_1.default.prompt([
553
+ {
554
+ type: "list",
555
+ name: "preset",
556
+ message: "Select a middleware preset",
557
+ choices: [
558
+ `API :: REST API with security, compression, and auto-logging. (${chalk_1.default.yellow("Recommended")})`,
559
+ "Web :: Full web app with cookies and session support.",
560
+ "GraphQL :: Optimized for GraphQL APIs.",
561
+ "Microservice :: Minimal setup for microservices.",
562
+ "Minimal :: Just request parsing, customize everything yourself.",
563
+ ],
564
+ },
565
+ ]);
566
+ // Opt-in to the type-safe Event Bus example. Defaults to No
567
+ // so the API/Web/GraphQL/etc. presets stay focused on what
568
+ // the user actually asked for. Picking Yes swaps the
569
+ // scaffold to `application-with-events` (extra event class
570
+ // + handler + `setupEventSystemForExpress` wiring).
571
+ eventsAnswer = await inquirer_1.default.prompt([
572
+ {
573
+ type: "confirm",
574
+ name: "events",
575
+ message: "Include the type-safe Event Bus example? (adds a sample event + handler)",
576
+ default: false,
577
+ },
578
+ ]);
579
+ }
580
+ const confirmAnswer = await inquirer_1.default.prompt([
145
581
  {
146
582
  type: "confirm",
147
583
  name: "confirm",
@@ -149,6 +585,12 @@ const projectForm = async (projectName, args) => {
149
585
  default: true,
150
586
  },
151
587
  ]);
588
+ answer = {
589
+ ...baseAnswers,
590
+ ...presetAnswer,
591
+ ...eventsAnswer,
592
+ ...confirmAnswer,
593
+ };
152
594
  }
153
595
  if (directory) {
154
596
  if (!node_fs_1.default.existsSync(node_path_1.default.join(directory, answer.name))) {
@@ -159,12 +601,6 @@ const projectForm = async (projectName, args) => {
159
601
  process.exit(1);
160
602
  }
161
603
  }
162
- // Hashmap of templates and their directories
163
- const templates = {
164
- NonOpinionated: "non_opinionated",
165
- Opinionated: "opinionated",
166
- Micro: "micro",
167
- };
168
604
  if (answer.confirm) {
169
605
  // Check if package manager is bun and OS is Windows
170
606
  if (answer.packageManager === "bun" && process.platform === "win32") {
@@ -172,36 +608,130 @@ const projectForm = async (projectName, args) => {
172
608
  process.exit(1);
173
609
  }
174
610
  await checkIfPackageManagerExists(answer.packageManager);
175
- console.log("\n");
611
+ process.stdout.write(`\n ${chalk_1.default.dim("Creating")} ${chalk_1.default.bold.green(answer.name)}\n\n`);
612
+ const termCols = typeof process.stdout.columns === "number" &&
613
+ process.stdout.columns > 0
614
+ ? process.stdout.columns
615
+ : 80;
616
+ const barsize = Math.max(20, Math.min(40, termCols - 22));
176
617
  const progressBar = new cli_progress_1.SingleBar({
177
- format: "Progress |" +
178
- chalk_1.default.green("{bar}") +
179
- "| {percentage}% || {doing}",
618
+ format: " {bar} " +
619
+ chalk_1.default.bold("{percentage}") +
620
+ chalk_1.default.dim("%") +
621
+ " " +
622
+ chalk_1.default.dim("{doing}"),
623
+ barCompleteChar: "\u2588",
624
+ barIncompleteChar: "\u2591",
625
+ formatBar: (progress, options) => {
626
+ const completeSize = Math.round(progress * (options.barsize ?? barsize));
627
+ const incompleteSize = (options.barsize ?? barsize) - completeSize;
628
+ return (chalk_1.default.green("\u2588".repeat(completeSize)) +
629
+ chalk_1.default.dim("\u2591".repeat(incompleteSize)));
630
+ },
631
+ barsize,
180
632
  hideCursor: true,
181
- }, cli_progress_1.Presets.rect);
633
+ clearOnComplete: false,
634
+ linewrap: false,
635
+ }, cli_progress_1.Presets.legacy);
182
636
  progressBar.start(100, 0, {
183
- doing: "Cloning project",
637
+ doing: "Fetching template",
184
638
  });
185
- const [_, template] = answer.template.match(/(.*) ::/);
186
- const repo = `expressots/templates/${templates[template]}#${cli_1.BUNDLE_VERSION}`;
639
+ // Extract template name from selection
640
+ const templateMatch = answer.template.match(/(.*) ::/);
641
+ if (!templateMatch || !templateMatch[1]) {
642
+ progressBar.stop();
643
+ (0, cli_ui_1.printError)(`Could not parse selected template: ${answer.template}`, "new");
644
+ process.exit(1);
645
+ }
646
+ const templateName = templateMatch[1];
647
+ // The "Application with Events" template is no longer a top-level
648
+ // choice. When the user opts into events on the Application track
649
+ // (or passes `--events`), swap the folder so we still pull from
650
+ // `application-with-events`. The folder split is preserved on disk
651
+ // for now so we keep two minimal sources of truth.
652
+ let templateFolder = TEMPLATE_FOLDERS[templateName];
653
+ if (templateName === "Application" && answer.events) {
654
+ templateFolder = TEMPLATE_FOLDERS["Application with Events"];
655
+ }
656
+ if (!templateFolder) {
657
+ progressBar.stop();
658
+ (0, cli_ui_1.printError)(`Unknown template: ${templateName}`, "new");
659
+ process.exit(1);
660
+ }
187
661
  try {
188
- const emitter = (0, degit_1.default)(repo);
189
- await emitter.clone(answer.name);
662
+ if (USE_LOCAL_TEMPLATES) {
663
+ // LOCAL TEMPLATE MODE (for testing)
664
+ const localTemplatePath = node_path_1.default.join(LOCAL_TEMPLATES_PATH, templateFolder);
665
+ if (!node_fs_1.default.existsSync(localTemplatePath)) {
666
+ progressBar.stop();
667
+ (0, cli_ui_1.printError)(`Local template not found at: ${localTemplatePath}`, "Please check your templates folder");
668
+ process.exit(1);
669
+ }
670
+ // Create target directory
671
+ node_fs_1.default.mkdirSync(answer.name, { recursive: true });
672
+ // Copy template files
673
+ copyDirectorySync(localTemplatePath, answer.name);
674
+ progressBar.update(30, { doing: "Template ready" });
675
+ }
676
+ else {
677
+ // GITHUB MODE (production)
678
+ // Pinned to the templates tag matching this CLI's published
679
+ // version (e.g. CLI 4.0.0-preview.3 -> templates/v4.0.0-preview.3).
680
+ // BUNDLE_VERSION reads from this package's package.json, so a
681
+ // CLI release and its templates tag move together.
682
+ await cloneFromGitHub({
683
+ templateFolder,
684
+ targetDir: answer.name,
685
+ progressBar,
686
+ });
687
+ }
190
688
  }
191
689
  catch (err) {
192
690
  console.log("\n");
193
- (0, cli_ui_1.printError)("Project already exists or Folder is not empty", answer.name);
691
+ // Surface the real failure cause so users can self-diagnose
692
+ // instead of guessing at "folder not empty" every time.
693
+ const msg = err?.message ? String(err.message) : String(err);
694
+ const code = err?.code ? ` [${err.code}]` : "";
695
+ if (err?.code === "DEST_NOT_EMPTY" ||
696
+ /already exists|not empty/i.test(msg)) {
697
+ (0, cli_ui_1.printError)(`Target folder "${answer.name}" already exists or is not empty`, answer.name);
698
+ }
699
+ else {
700
+ (0, cli_ui_1.printError)(`Failed to scaffold project${code}: ${msg}`, answer.name);
701
+ }
194
702
  process.exit(1);
195
703
  }
196
- progressBar.update(50, {
197
- doing: "Installing dependencies",
198
- });
199
- await packageManagerInstall({
200
- packageManager: answer.packageManager,
201
- directory: answer.name,
202
- progressBar,
203
- });
204
- progressBar.update(90);
704
+ // Apply preset files + placeholder substitution BEFORE install so
705
+ // that a failed/skipped install still leaves the user with a
706
+ // runnable scaffold (the middleware preset placeholder must not
707
+ // leak into src/app.ts as a literal comment).
708
+ if (answer.preset &&
709
+ (templateFolder === "application" ||
710
+ templateFolder === "application-with-events")) {
711
+ injectPresetDependencies(answer.name, answer.preset);
712
+ createPresetFiles(answer.name, answer.preset);
713
+ applyMiddlewarePreset(answer.name, answer.preset);
714
+ }
715
+ if (SKIP_INSTALL_FOR_TESTING) {
716
+ progressBar.update(90, {
717
+ doing: "Skipping install (testing mode)",
718
+ });
719
+ }
720
+ else {
721
+ progressBar.update(50, {
722
+ doing: "Installing dependencies",
723
+ });
724
+ await packageManagerInstall({
725
+ packageManager: answer.packageManager,
726
+ directory: answer.name,
727
+ progressBar,
728
+ });
729
+ }
730
+ // Progress should already be at 90% from packageManagerInstall
731
+ // Only update if we skipped installation
732
+ if (!SKIP_INSTALL_FOR_TESTING) {
733
+ progressBar.update(90, { doing: "Finalizing" });
734
+ }
205
735
  (0, change_package_info_1.changePackageName)({
206
736
  directory: answer.name,
207
737
  name: projectName,