@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
@@ -31,10 +31,100 @@ const child_process_1 = require("child_process");
31
31
  const fs_1 = require("fs");
32
32
  const os_1 = __importDefault(require("os"));
33
33
  const path_1 = __importStar(require("path"));
34
+ const chalk_1 = __importDefault(require("chalk"));
34
35
  const cli_ui_1 = require("../utils/cli-ui");
35
36
  const compiler_1 = __importDefault(require("../utils/compiler"));
37
+ const safe_spawn_1 = require("../utils/safe-spawn");
36
38
  /**
37
- * Helper function to load and extract outDir from tsconfig.build.json
39
+ * Resolve a tsconfig.json file with full `extends` chain support.
40
+ * Recursively loads the base config(s) and merges `compilerOptions`
41
+ * (child wins), producing the same flattened result that `tsc` sees.
42
+ */
43
+ function resolveTsConfig(configPath) {
44
+ if (!(0, fs_1.existsSync)(configPath)) {
45
+ return {};
46
+ }
47
+ let raw;
48
+ try {
49
+ raw = (0, fs_1.readFileSync)(configPath, "utf-8");
50
+ }
51
+ catch {
52
+ return {};
53
+ }
54
+ // tsconfig.json allows JS-style comments but JSON.parse does not.
55
+ // We strip them character-by-character so we never touch `//` or
56
+ // `/*` sequences that appear inside quoted strings (e.g. paths).
57
+ const stripped = stripJsonComments(raw);
58
+ let config;
59
+ try {
60
+ config = JSON.parse(stripped);
61
+ }
62
+ catch {
63
+ return {};
64
+ }
65
+ if (typeof config.extends === "string") {
66
+ const baseRelative = config.extends;
67
+ const basePath = path_1.default.resolve(path_1.default.dirname(configPath), baseRelative);
68
+ const baseConfig = resolveTsConfig(basePath);
69
+ const baseOpts = baseConfig.compilerOptions ?? {};
70
+ const childOpts = config.compilerOptions ?? {};
71
+ config.compilerOptions = { ...baseOpts, ...childOpts };
72
+ delete config.extends;
73
+ }
74
+ return config;
75
+ }
76
+ // Strip JS-style comments (single-line and block) from a JSON string
77
+ // without corrupting quoted content. Walks the input character by
78
+ // character, tracking whether we are inside a string literal.
79
+ function stripJsonComments(text) {
80
+ let result = "";
81
+ let i = 0;
82
+ const len = text.length;
83
+ while (i < len) {
84
+ const ch = text[i];
85
+ // String literal — copy verbatim until the closing quote.
86
+ if (ch === '"') {
87
+ let j = i + 1;
88
+ while (j < len) {
89
+ if (text[j] === "\\") {
90
+ j += 2; // skip escaped character
91
+ }
92
+ else if (text[j] === '"') {
93
+ j++;
94
+ break;
95
+ }
96
+ else {
97
+ j++;
98
+ }
99
+ }
100
+ result += text.slice(i, j);
101
+ i = j;
102
+ continue;
103
+ }
104
+ // Single-line comment
105
+ if (ch === "/" && text[i + 1] === "/") {
106
+ // Skip until end of line.
107
+ i += 2;
108
+ while (i < len && text[i] !== "\n")
109
+ i++;
110
+ continue;
111
+ }
112
+ // Multi-line comment
113
+ if (ch === "/" && text[i + 1] === "*") {
114
+ i += 2;
115
+ while (i < len && !(text[i] === "*" && text[i + 1] === "/"))
116
+ i++;
117
+ i += 2; // skip closing */
118
+ continue;
119
+ }
120
+ result += ch;
121
+ i++;
122
+ }
123
+ return result;
124
+ }
125
+ /**
126
+ * Helper function to load and extract outDir from tsconfig.build.json,
127
+ * resolving the `extends` chain so the value can live in the base config.
38
128
  */
39
129
  function getOutDir() {
40
130
  const tsconfigBuildPath = (0, path_1.join)(process.cwd(), "tsconfig.build.json");
@@ -42,10 +132,11 @@ function getOutDir() {
42
132
  (0, cli_ui_1.printError)("Cannot find tsconfig.build.json. Please create one in the root directory", "tsconfig-build-path");
43
133
  process.exit(1);
44
134
  }
45
- const tsconfig = JSON.parse((0, fs_1.readFileSync)(tsconfigBuildPath, "utf-8"));
46
- const outDir = tsconfig.compilerOptions.outDir;
135
+ const tsconfig = resolveTsConfig(tsconfigBuildPath);
136
+ const opts = tsconfig.compilerOptions;
137
+ const outDir = opts?.outDir;
47
138
  if (!outDir) {
48
- (0, cli_ui_1.printError)("Cannot find outDir in tsconfig.build.json. Please provide an outDir.", "tsconfig-build-path");
139
+ (0, cli_ui_1.printError)("Cannot find outDir in tsconfig.build.json (or its extended config). Please provide an outDir.", "tsconfig-build-path");
49
140
  process.exit(1);
50
141
  }
51
142
  if (!(0, fs_1.existsSync)(outDir)) {
@@ -55,40 +146,63 @@ function getOutDir() {
55
146
  return outDir;
56
147
  }
57
148
  /**
58
- * Load the configuration from the compiler
59
- * @param compiler The compiler to load the configuration from
60
- * @returns The configuration
61
- */
62
- async function opinionatedConfig() {
63
- const { entryPoint } = await compiler_1.default.loadConfig();
64
- const config = [
65
- "--watch",
66
- "-r",
67
- "tsconfig-paths/register",
68
- `./src/${entryPoint}.ts`,
69
- ];
70
- return config;
71
- }
72
- /**
73
- * Load the configuration from the compiler
74
- * @param compiler The compiler to load the configuration from
75
- * @returns The configuration
149
+ * Build the tsx watch arguments for development mode.
150
+ * Uses tsx's built-in --watch flag for reliable cross-platform file watching
151
+ * (avoids nodemon + SIGTERM issues on Windows).
152
+ *
153
+ * @param opinionated - Whether to use opinionated configuration
154
+ * @returns The tsx arguments array
76
155
  */
77
- async function nonOpinionatedConfig() {
78
- const { entryPoint } = await compiler_1.default.loadConfig();
79
- const config = ["--watch", `./src/${entryPoint}.ts`];
80
- return config;
156
+ async function buildDevArgs(opinionated) {
157
+ const { entryPoint, sourceRoot } = await compiler_1.default.loadConfig();
158
+ const args = ["--watch"];
159
+ if (opinionated) {
160
+ args.push("-r", "tsconfig-paths/register");
161
+ }
162
+ // Honor `sourceRoot` from expressots.config.ts so projects whose
163
+ // source lives under a non-default folder (e.g. `api/` or
164
+ // `services/`) still resolve their entry point correctly.
165
+ args.push(`./${sourceRoot}/${entryPoint}.ts`);
166
+ return args;
81
167
  }
82
168
  /**
83
169
  * Dev command module
84
- * @type {CommandModule<object, object>}
170
+ * @type {CommandModule<object, DevCommandOptions>}
85
171
  * @returns The command module
86
172
  */
87
173
  exports.devCommand = {
88
174
  command: "dev",
89
175
  describe: "Start development server.",
90
- handler: async () => {
91
- await (0, exports.runCommand)({ command: "dev" });
176
+ builder: {
177
+ container: {
178
+ alias: "c",
179
+ type: "boolean",
180
+ default: false,
181
+ description: "Run development inside Docker container",
182
+ },
183
+ build: {
184
+ alias: "b",
185
+ type: "boolean",
186
+ default: false,
187
+ description: "Rebuild container before starting (with --container)",
188
+ },
189
+ detach: {
190
+ alias: "d",
191
+ type: "boolean",
192
+ default: false,
193
+ description: "Run container in background (with --container)",
194
+ },
195
+ },
196
+ handler: async (argv) => {
197
+ if (argv.container) {
198
+ await runContainerDev({
199
+ build: argv.build ?? false,
200
+ detach: argv.detach ?? false,
201
+ });
202
+ }
203
+ else {
204
+ await (0, exports.runCommand)({ command: "dev" });
205
+ }
92
206
  },
93
207
  };
94
208
  /**
@@ -115,6 +229,86 @@ exports.prodCommand = {
115
229
  await (0, exports.runCommand)({ command: "prod" });
116
230
  },
117
231
  };
232
+ /**
233
+ * Recursively collect the PIDs of every descendant of `rootPid`.
234
+ *
235
+ * `tsx --watch` exits immediately on SIGINT/SIGTERM and *abandons* the
236
+ * server process it spawned, so the server is still running its graceful
237
+ * shutdown after `tsx` is gone. To wait for it we snapshot the process
238
+ * tree (while it's still attached to `tsx`) and later poll those PIDs.
239
+ *
240
+ * Returns an empty list on Windows (no `ps`) or if the lookup fails, in
241
+ * which case the caller simply skips the wait.
242
+ */
243
+ function getDescendantPids(rootPid) {
244
+ if (process.platform === "win32" || !rootPid || rootPid < 0) {
245
+ return [];
246
+ }
247
+ try {
248
+ const res = (0, safe_spawn_1.safeSpawnSync)("ps", ["-A", "-o", "pid=,ppid="], {
249
+ encoding: "utf-8",
250
+ });
251
+ const out = res.stdout ? String(res.stdout) : "";
252
+ const childrenByParent = new Map();
253
+ for (const line of out.split("\n")) {
254
+ const match = line.trim().match(/^(\d+)\s+(\d+)$/);
255
+ if (!match)
256
+ continue;
257
+ const pid = Number(match[1]);
258
+ const ppid = Number(match[2]);
259
+ const siblings = childrenByParent.get(ppid) ?? [];
260
+ siblings.push(pid);
261
+ childrenByParent.set(ppid, siblings);
262
+ }
263
+ const descendants = [];
264
+ const stack = [rootPid];
265
+ while (stack.length > 0) {
266
+ const current = stack.pop();
267
+ for (const child of childrenByParent.get(current) ?? []) {
268
+ descendants.push(child);
269
+ stack.push(child);
270
+ }
271
+ }
272
+ return descendants;
273
+ }
274
+ catch {
275
+ return [];
276
+ }
277
+ }
278
+ /**
279
+ * Check whether a process is still alive. `process.kill(pid, 0)` sends no
280
+ * signal but performs the permission/existence check: ESRCH means gone,
281
+ * EPERM means alive but owned by another user.
282
+ */
283
+ function isPidAlive(pid) {
284
+ try {
285
+ process.kill(pid, 0);
286
+ return true;
287
+ }
288
+ catch (err) {
289
+ return err.code === "EPERM";
290
+ }
291
+ }
292
+ /**
293
+ * Resolve once every PID in `pids` has exited, or once `timeoutMs` elapses.
294
+ */
295
+ function waitForPidsToExit(pids, timeoutMs) {
296
+ return new Promise((resolve) => {
297
+ if (pids.length === 0) {
298
+ resolve();
299
+ return;
300
+ }
301
+ const deadline = Date.now() + timeoutMs;
302
+ const poll = () => {
303
+ if (Date.now() >= deadline || !pids.some(isPidAlive)) {
304
+ resolve();
305
+ return;
306
+ }
307
+ setTimeout(poll, 50);
308
+ };
309
+ poll();
310
+ });
311
+ }
118
312
  /**
119
313
  * Helper function to execute a command
120
314
  * @param command The command to execute
@@ -124,16 +318,68 @@ exports.prodCommand = {
124
318
  */
125
319
  function execCmd(command, args, cwd = process.cwd()) {
126
320
  return new Promise((resolve, reject) => {
127
- const proc = (0, child_process_1.spawn)(command, args, {
321
+ // `safeSpawn` (cross-spawn) resolves Windows `.cmd` shims (npx,
322
+ // tsx, tsc, etc.) via PATHEXT and applies cmd.exe-aware escaping
323
+ // for every argv entry, while falling through to plain `spawn`
324
+ // with `shell: false` on Unix.
325
+ const proc = (0, safe_spawn_1.safeSpawn)(command, args, {
128
326
  stdio: "inherit",
129
- shell: true,
130
327
  cwd,
131
328
  });
132
- proc.on("close", (code) => {
133
- if (code === 0) {
134
- resolve();
329
+ // On Ctrl+C the SIGINT hits the whole foreground process group, so
330
+ // the spawned process (and the server it runs) already receive it.
331
+ // We keep *this* (parent) process alive — instead of dying from the
332
+ // default signal behavior — so the shell doesn't redraw its prompt
333
+ // until the server has finished shutting down. We also snapshot the
334
+ // child process tree on the first signal: `tsx --watch` exits right
335
+ // away and abandons the server, so we remember the server PID(s) to
336
+ // wait on them after `tsx` is gone.
337
+ let descendantPids = [];
338
+ let signalled = false;
339
+ const onSignal = () => {
340
+ if (signalled)
341
+ return;
342
+ signalled = true;
343
+ descendantPids = getDescendantPids(proc.pid ?? -1);
344
+ };
345
+ process.on("SIGINT", onSignal);
346
+ process.on("SIGTERM", onSignal);
347
+ const cleanup = () => {
348
+ process.removeListener("SIGINT", onSignal);
349
+ process.removeListener("SIGTERM", onSignal);
350
+ };
351
+ proc.on("error", (err) => {
352
+ cleanup();
353
+ reject(err);
354
+ });
355
+ proc.on("close", (code, signal) => {
356
+ // Exit codes 130 (SIGINT) and 143 (SIGTERM) mean the user or
357
+ // orchestrator intentionally stopped the process — not a failure.
358
+ const isSignalExit = signalled ||
359
+ code === 130 ||
360
+ code === 143 ||
361
+ signal === "SIGINT" ||
362
+ signal === "SIGTERM";
363
+ if (code === 0 || isSignalExit) {
364
+ if (isSignalExit) {
365
+ // The server (a `tsx --watch` grandchild) may still be
366
+ // finishing its graceful shutdown after `tsx` itself has
367
+ // exited. Wait for it to fully terminate so its final
368
+ // "Graceful shutdown completed" log lands before the shell
369
+ // prompt returns. The 9s cap matches the framework's own
370
+ // shutdown watchdog (default timeout + buffer).
371
+ void waitForPidsToExit(descendantPids, 9000).then(() => {
372
+ cleanup();
373
+ resolve();
374
+ });
375
+ }
376
+ else {
377
+ cleanup();
378
+ resolve();
379
+ }
135
380
  }
136
381
  else {
382
+ cleanup();
137
383
  reject(new Error(`Command failed with code ${code}`));
138
384
  }
139
385
  });
@@ -153,47 +399,340 @@ const compileTypescript = async () => {
153
399
  await execCmd("npx", ["tsc", "-p", "tsconfig.build.json"]);
154
400
  (0, cli_ui_1.printSuccess)("Built successfully", "compile-typescript");
155
401
  };
402
+ /**
403
+ * Transform path aliases to relative paths in compiled JavaScript files.
404
+ * This runs after TypeScript compilation to ensure production builds work
405
+ * without runtime path resolution.
406
+ *
407
+ * @param outDir - The output directory (e.g., "./dist")
408
+ */
409
+ const transformPathAliases = async (outDir) => {
410
+ const tsconfigPath = (0, path_1.join)(process.cwd(), "tsconfig.build.json");
411
+ if (!(0, fs_1.existsSync)(tsconfigPath)) {
412
+ return; // No tsconfig.build.json, skip transformation
413
+ }
414
+ const tsconfig = resolveTsConfig(tsconfigPath);
415
+ const opts = tsconfig.compilerOptions;
416
+ const paths = opts?.paths;
417
+ // `baseUrl` is deprecated in TypeScript 7. When it's omitted the path
418
+ // targets are resolved relative to the tsconfig file itself, which is
419
+ // the project root in our generated templates — so default to ".".
420
+ const baseUrl = opts?.baseUrl ?? ".";
421
+ if (!paths) {
422
+ return; // No path aliases defined, skip
423
+ }
424
+ // Build regex patterns for each alias
425
+ const aliasPatterns = [];
426
+ for (const [alias, targets] of Object.entries(paths)) {
427
+ if (!Array.isArray(targets) || targets.length === 0)
428
+ continue;
429
+ // Convert @alias/* to regex pattern
430
+ // Matches: require("@alias/something") or require('@alias/something')
431
+ const aliasBase = alias.replace("/*", "");
432
+ const targetBase = targets[0].replace("/*", "");
433
+ // Pattern to match require("@alias/...") or require('@alias/...')
434
+ const pattern = new RegExp(`require\\(["']${aliasBase.replace("@", "\\@")}/([^"']+)["']\\)`, "g");
435
+ aliasPatterns.push({
436
+ pattern,
437
+ alias: aliasBase,
438
+ target: targetBase,
439
+ });
440
+ }
441
+ if (aliasPatterns.length === 0) {
442
+ return;
443
+ }
444
+ // Recursively find all .js files in outDir
445
+ const findJsFiles = async (dir) => {
446
+ const files = [];
447
+ const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
448
+ for (const entry of entries) {
449
+ const fullPath = (0, path_1.join)(dir, entry.name);
450
+ if (entry.isDirectory()) {
451
+ files.push(...(await findJsFiles(fullPath)));
452
+ }
453
+ else if (entry.name.endsWith(".js")) {
454
+ files.push(fullPath);
455
+ }
456
+ }
457
+ return files;
458
+ };
459
+ const jsFiles = await findJsFiles(outDir);
460
+ let transformedCount = 0;
461
+ for (const file of jsFiles) {
462
+ let content = await fs_1.promises.readFile(file, "utf-8");
463
+ let modified = false;
464
+ // Get the directory of the current file relative to outDir
465
+ const fileDir = path_1.default.dirname(file);
466
+ for (const { pattern, alias, target } of aliasPatterns) {
467
+ // Calculate the relative path from this file to the target
468
+ const targetDir = (0, path_1.join)(outDir, baseUrl.replace("./", ""), target);
469
+ let relativePath = path_1.default.relative(fileDir, targetDir);
470
+ // Ensure it starts with ./ or ../
471
+ if (!relativePath.startsWith(".")) {
472
+ relativePath = "./" + relativePath;
473
+ }
474
+ // Replace Windows backslashes with forward slashes
475
+ relativePath = relativePath.replace(/\\/g, "/");
476
+ // Replace the alias with the relative path
477
+ const newContent = content.replace(pattern, (match, subPath) => {
478
+ modified = true;
479
+ return `require("${relativePath}/${subPath}")`;
480
+ });
481
+ if (newContent !== content) {
482
+ content = newContent;
483
+ }
484
+ }
485
+ if (modified) {
486
+ await fs_1.promises.writeFile(file, content, "utf-8");
487
+ transformedCount++;
488
+ }
489
+ }
490
+ if (transformedCount > 0) {
491
+ (0, cli_ui_1.printSuccess)(`Path aliases resolved in ${transformedCount} files`, "transform-paths");
492
+ }
493
+ };
156
494
  /**
157
495
  * Helper function to copy files to the dist directory
158
496
  */
159
497
  const copyFiles = async (outDir) => {
160
- const { opinionated } = await compiler_1.default.loadConfig();
161
- let filesToCopy = [];
162
- if (opinionated) {
163
- filesToCopy = [
164
- "./register-path.js",
165
- "tsconfig.build.json",
166
- "package.json",
167
- ];
168
- }
169
- else {
170
- filesToCopy = ["tsconfig.json", "package.json"];
498
+ // Only copy package.json - path aliases are resolved at build time
499
+ // No need for tsconfig files or register-path.js in production
500
+ const filesToCopy = ["package.json"];
501
+ for (const file of filesToCopy) {
502
+ if ((0, fs_1.existsSync)(file)) {
503
+ await fs_1.promises.copyFile(file, (0, path_1.join)(outDir, path_1.default.basename(file)));
504
+ }
171
505
  }
172
- filesToCopy.forEach((file) => {
173
- fs_1.promises.copyFile(file, (0, path_1.join)(outDir, path_1.default.basename(file)));
174
- });
175
506
  };
176
507
  /**
177
508
  * Helper function to clear the screen
178
509
  */
179
510
  const clearScreen = () => {
511
+ // `cls` and `clear` are built-ins / well-known executables.
512
+ // Invoking them via `shell: true` is safe here because there are no
513
+ // user-controlled args, but we keep `windowsHide: true` to suppress
514
+ // the Windows console flash.
180
515
  const platform = os_1.default.platform();
181
516
  const command = platform === "win32" ? "cls" : "clear";
182
- (0, child_process_1.spawn)(command, { stdio: "inherit", shell: true });
517
+ (0, child_process_1.spawn)(command, [], { stdio: "inherit", shell: true, windowsHide: true });
183
518
  };
519
+ /**
520
+ * Run development in Docker container with auto-setup
521
+ * This is the seamless "just works" experience
522
+ */
523
+ async function runContainerDev(options) {
524
+ console.log(chalk_1.default.cyan("\n🐳 ExpressoTS Container Development\n"));
525
+ const cwd = process.cwd();
526
+ const composeDevFile = (0, path_1.join)(cwd, "docker-compose.development.yml");
527
+ const dockerfileDevFile = (0, path_1.join)(cwd, "Dockerfile.development");
528
+ const dockerSetupFile = (0, path_1.join)(cwd, "docker-setup.js");
529
+ const dockerDepsDir = (0, path_1.join)(cwd, ".docker-deps");
530
+ const packageDockerJson = (0, path_1.join)(cwd, "package.docker.json");
531
+ // Check if Docker is running
532
+ if (!isDockerRunning()) {
533
+ console.log(chalk_1.default.red("āŒ Docker is not running."));
534
+ console.log(chalk_1.default.gray(" Please start Docker Desktop or Docker daemon."));
535
+ return;
536
+ }
537
+ // Step 1: Auto-generate Docker files if missing
538
+ if (!(0, fs_1.existsSync)(dockerfileDevFile) || !(0, fs_1.existsSync)(composeDevFile)) {
539
+ console.log(chalk_1.default.yellow("šŸ“ Docker files not found. Generating..."));
540
+ try {
541
+ // Import and run containerize
542
+ const { containerizeProject } = await Promise.resolve().then(() => __importStar(require("../containerize/form")));
543
+ await containerizeProject({
544
+ target: "docker",
545
+ environment: "development",
546
+ preset: "standard",
547
+ analyze: true,
548
+ skipCompose: false,
549
+ includeCi: false,
550
+ });
551
+ console.log();
552
+ }
553
+ catch (error) {
554
+ console.log(chalk_1.default.red("āŒ Failed to generate Docker files."));
555
+ console.log(chalk_1.default.gray(" Run manually: expressots containerize docker --env development"));
556
+ return;
557
+ }
558
+ }
559
+ // Step 1.5: Check bootstrap config and create missing env files if needed
560
+ try {
561
+ const { analyzeBootstrapConfig, shouldCopyEnvFiles, getEnvFileForEnvironment, } = await Promise.resolve().then(() => __importStar(require("../containerize/analyzers/bootstrap-analyzer")));
562
+ const bootstrapConfig = await analyzeBootstrapConfig();
563
+ if (bootstrapConfig.hasEnvFileConfig &&
564
+ shouldCopyEnvFiles(bootstrapConfig)) {
565
+ const devEnvFile = getEnvFileForEnvironment(bootstrapConfig, "development");
566
+ // Check if required env file is missing
567
+ if (bootstrapConfig.missingEnvFiles.includes(devEnvFile)) {
568
+ console.log(chalk_1.default.yellow(`āš ļø Required env file missing: ${devEnvFile}`));
569
+ // Auto-create template if configured or prompt user
570
+ if (bootstrapConfig.autoCreateTemplate) {
571
+ console.log(chalk_1.default.gray(` Creating template ${devEnvFile}...`));
572
+ await createEnvTemplate(cwd, devEnvFile, "development", bootstrapConfig.requiredVariables);
573
+ console.log(chalk_1.default.green(` āœ“ Created ${devEnvFile}`));
574
+ }
575
+ else {
576
+ // Provide helpful instructions
577
+ console.log(chalk_1.default.cyan("\nšŸ’” To fix this, either:"));
578
+ console.log(chalk_1.default.gray(` 1. Create ${devEnvFile} with your environment variables`));
579
+ console.log(chalk_1.default.gray(` 2. Add autoCreateTemplate: true to envFileConfig in bootstrap`));
580
+ console.log(chalk_1.default.gray(` 3. Use skipFileLoading: true for container deployments`));
581
+ console.log();
582
+ // Still continue - the container might work if env vars are set in docker-compose
583
+ console.log(chalk_1.default.yellow(` āš ļø Container may fail if ${devEnvFile} is required`));
584
+ console.log();
585
+ }
586
+ }
587
+ // Show required variables that need to be set
588
+ if (bootstrapConfig.requiredVariables.length > 0) {
589
+ console.log(chalk_1.default.cyan("šŸ“‹ Required environment variables:"));
590
+ bootstrapConfig.requiredVariables.forEach((varName) => {
591
+ console.log(chalk_1.default.gray(` • ${varName}`));
592
+ });
593
+ console.log(chalk_1.default.gray(` Set these in ${devEnvFile} or docker-compose.development.yml`));
594
+ console.log();
595
+ }
596
+ }
597
+ }
598
+ catch (error) {
599
+ // Non-fatal - continue with container startup
600
+ console.log(chalk_1.default.gray(" (Bootstrap analysis skipped)"));
601
+ }
602
+ // Step 2: Auto-run docker:setup if local dependencies exist
603
+ if ((0, fs_1.existsSync)(packageDockerJson) && (0, fs_1.existsSync)(dockerSetupFile)) {
604
+ // Check if .docker-deps needs to be updated
605
+ const needsSetup = !(0, fs_1.existsSync)(dockerDepsDir) || isDirEmpty(dockerDepsDir);
606
+ if (needsSetup) {
607
+ console.log(chalk_1.default.yellow("šŸ“¦ Setting up local dependencies..."));
608
+ try {
609
+ const setupResult = (0, safe_spawn_1.safeSpawnSync)(process.execPath, ["docker-setup.js"], {
610
+ cwd,
611
+ stdio: "inherit",
612
+ encoding: "utf-8",
613
+ });
614
+ if (setupResult.error)
615
+ throw setupResult.error;
616
+ if (typeof setupResult.status === "number" &&
617
+ setupResult.status !== 0) {
618
+ throw new Error(`exited with code ${setupResult.status}`);
619
+ }
620
+ console.log();
621
+ }
622
+ catch (error) {
623
+ console.log(chalk_1.default.red("āŒ Failed to setup local dependencies."));
624
+ console.log(chalk_1.default.gray(" Run manually: npm run docker:setup"));
625
+ return;
626
+ }
627
+ }
628
+ }
629
+ // Step 3: Start the containers
630
+ console.log(chalk_1.default.yellow(`šŸ“„ Using docker-compose.development.yml`));
631
+ const args = ["-f", composeDevFile, "up"];
632
+ if (options.build) {
633
+ console.log(chalk_1.default.yellow("šŸ”Ø Rebuilding containers..."));
634
+ args.splice(2, 0, "--build");
635
+ }
636
+ if (options.detach) {
637
+ args.push("-d");
638
+ }
639
+ console.log(chalk_1.default.yellow("šŸš€ Starting development containers...\n"));
640
+ // Print dev info
641
+ console.log(chalk_1.default.bold("Development Environment:"));
642
+ console.log(` 🌐 App: http://localhost:3000`);
643
+ console.log(` šŸ” Debug: localhost:9229`);
644
+ console.log();
645
+ console.log(chalk_1.default.bold("Commands:"));
646
+ console.log(` ${chalk_1.default.gray("expressots dev -c")} Start containers`);
647
+ console.log(` ${chalk_1.default.gray("expressots dev -c -b")} Rebuild & start`);
648
+ console.log(` ${chalk_1.default.gray("expressots dev -c -d")} Start in background`);
649
+ console.log(` ${chalk_1.default.gray("docker-compose -f docker-compose.development.yml down")} Stop`);
650
+ console.log();
651
+ console.log(chalk_1.default.green("šŸ”„ Hot reload enabled - edit files to see changes"));
652
+ if (!options.detach) {
653
+ console.log(chalk_1.default.gray("Press Ctrl+C to stop\n"));
654
+ }
655
+ // Run docker-compose
656
+ runDockerComposeCommand(args, cwd, options.detach);
657
+ if (options.detach) {
658
+ console.log(chalk_1.default.green("\nāœ… Containers started in background."));
659
+ console.log(chalk_1.default.gray(" View logs: docker-compose -f docker-compose.development.yml logs -f"));
660
+ }
661
+ }
662
+ /**
663
+ * Check if Docker is running
664
+ */
665
+ function isDockerRunning() {
666
+ const result = (0, safe_spawn_1.safeSpawnSync)("docker", ["info"], {
667
+ stdio: ["pipe", "pipe", "pipe"],
668
+ });
669
+ return !result.error && result.status === 0;
670
+ }
671
+ /**
672
+ * Check if directory is empty
673
+ */
674
+ function isDirEmpty(dir) {
675
+ try {
676
+ const files = (0, fs_1.readdirSync)(dir);
677
+ return files.length === 0;
678
+ }
679
+ catch {
680
+ return true;
681
+ }
682
+ }
683
+ /**
684
+ * Create an environment template file
685
+ */
686
+ async function createEnvTemplate(cwd, fileName, environment, requiredVariables) {
687
+ const filePath = (0, path_1.join)(cwd, fileName);
688
+ const commonVars = [
689
+ "PORT=3000",
690
+ `NODE_ENV=${environment}`,
691
+ "# Add your environment variables below",
692
+ ];
693
+ const requiredVars = requiredVariables.map((key) => `${key}=`);
694
+ const template = [...commonVars, ...requiredVars].join("\n");
695
+ await fs_1.promises.writeFile(filePath, template, "utf-8");
696
+ }
697
+ /**
698
+ * Run docker-compose command
699
+ */
700
+ function runDockerComposeCommand(args, cwd, detach) {
701
+ // Try docker compose (v2) first, fall back to docker-compose (v1).
702
+ // `safeSpawnSync` (cross-spawn) handles platform-specific binary
703
+ // resolution and cmd.exe-aware argv escaping, so compose file paths
704
+ // and service names are forwarded as discrete arguments rather than
705
+ // re-interpreted by the OS shell.
706
+ const v2 = (0, safe_spawn_1.safeSpawnSync)("docker", ["compose", ...args], {
707
+ cwd,
708
+ stdio: "inherit",
709
+ });
710
+ if (v2.error || (typeof v2.status === "number" && v2.status !== 0)) {
711
+ const v1 = (0, safe_spawn_1.safeSpawnSync)("docker-compose", args, {
712
+ cwd,
713
+ stdio: "inherit",
714
+ });
715
+ if (v1.error || (typeof v1.status === "number" && v1.status !== 0)) {
716
+ console.log(chalk_1.default.red("Error running docker-compose"));
717
+ }
718
+ }
719
+ // `detach` is honored implicitly by docker compose itself when "-d"
720
+ // is present in `args`. The previous branch tried to switch between
721
+ // execSync and spawnSync for that case, which served no real
722
+ // purpose and made shell injection harder to reason about.
723
+ void detach;
724
+ }
184
725
  /**
185
726
  * Helper function to run a command
186
- * @param command The command to run
727
+ * @param options The command options
187
728
  */
188
729
  const runCommand = async ({ command, }) => {
189
- const { opinionated, entryPoint } = await compiler_1.default.loadConfig();
730
+ const { opinionated, entryPoint, sourceRoot } = await compiler_1.default.loadConfig();
190
731
  const outDir = getOutDir();
191
732
  try {
192
733
  switch (command) {
193
734
  case "dev":
194
- execCmd("tsx", opinionated
195
- ? await opinionatedConfig()
196
- : await nonOpinionatedConfig());
735
+ await execCmd("tsx", await buildDevArgs(opinionated));
197
736
  break;
198
737
  case "build":
199
738
  if (!outDir) {
@@ -202,6 +741,10 @@ const runCommand = async ({ command, }) => {
202
741
  }
203
742
  await cleanDist(outDir);
204
743
  await compileTypescript();
744
+ // Transform path aliases to relative paths for production
745
+ if (opinionated) {
746
+ await transformPathAliases(outDir);
747
+ }
205
748
  await copyFiles(outDir);
206
749
  break;
207
750
  case "prod": {
@@ -209,19 +752,12 @@ const runCommand = async ({ command, }) => {
209
752
  (0, cli_ui_1.printError)("Cannot run in prod mode. Please provide an outDir in tsconfig.build.json", "prod-command");
210
753
  process.exit(1);
211
754
  }
212
- let config = [];
213
- if (opinionated) {
214
- config = [
215
- "-r",
216
- `./${outDir}/register-path.js`,
217
- `./${outDir}/src/${entryPoint}.js`,
218
- ];
219
- }
220
- else {
221
- config = [`./${outDir}/${entryPoint}.js`];
222
- }
755
+ // Honor `sourceRoot` so the compiled entry-point path
756
+ // matches whatever folder the user configured (the TS
757
+ // compiler preserves the source tree under `outDir`).
758
+ const config = [`./${outDir}/${sourceRoot}/${entryPoint}.js`];
223
759
  clearScreen();
224
- execCmd("node", config);
760
+ await execCmd("node", config);
225
761
  break;
226
762
  }
227
763
  default: