@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
@@ -0,0 +1,657 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateDockerfiles = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const process_1 = require("process");
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const preset_registry_1 = require("../presets/preset-registry");
12
+ const template_loader_1 = require("./template-loader");
13
+ const cli_ui_1 = require("../../utils/cli-ui");
14
+ const bootstrap_analyzer_1 = require("../analyzers/bootstrap-analyzer");
15
+ /**
16
+ * Detects the entry point path by reading tsconfig.build.json
17
+ * Returns the path relative to /app in the container
18
+ */
19
+ function detectEntryPoint(cwd) {
20
+ // Try to read tsconfig.build.json first, then tsconfig.json
21
+ const tsconfigPaths = [
22
+ path_1.default.join(cwd, "tsconfig.build.json"),
23
+ path_1.default.join(cwd, "tsconfig.json"),
24
+ ];
25
+ for (const tsconfigPath of tsconfigPaths) {
26
+ if (fs_1.default.existsSync(tsconfigPath)) {
27
+ try {
28
+ // Read and parse tsconfig (handle comments by stripping them)
29
+ const content = fs_1.default.readFileSync(tsconfigPath, "utf-8");
30
+ // Simple JSON parse (tsconfig may have comments, so we strip them)
31
+ const cleanContent = content
32
+ .replace(/\/\*[\s\S]*?\*\//g, "") // Remove block comments
33
+ .replace(/\/\/.*/g, ""); // Remove line comments
34
+ const tsconfig = JSON.parse(cleanContent);
35
+ const outDir = tsconfig.compilerOptions?.outDir || "./dist";
36
+ const rootDir = tsconfig.compilerOptions?.rootDir || "./";
37
+ // Determine the entry point based on rootDir
38
+ // If rootDir is "." or "./" (project root), output will be dist/src/main.js
39
+ // If rootDir is "./src" or "src", output will be dist/main.js
40
+ const normalizedRootDir = rootDir
41
+ .replace(/^\.\//, "")
42
+ .replace(/\/$/, "");
43
+ const normalizedOutDir = outDir
44
+ .replace(/^\.\//, "")
45
+ .replace(/\/$/, "");
46
+ if (normalizedRootDir === "" || normalizedRootDir === ".") {
47
+ // rootDir is project root, so src/ folder is preserved
48
+ return `${normalizedOutDir}/src/main.js`;
49
+ }
50
+ else if (normalizedRootDir === "src") {
51
+ // rootDir is src/, so main.js is directly in outDir
52
+ return `${normalizedOutDir}/main.js`;
53
+ }
54
+ else {
55
+ // Custom rootDir, assume src structure is preserved
56
+ return `${normalizedOutDir}/src/main.js`;
57
+ }
58
+ }
59
+ catch (err) {
60
+ // Failed to parse, continue to fallback
61
+ }
62
+ }
63
+ }
64
+ // Default fallback for ExpressoTS projects
65
+ return "dist/src/main.js";
66
+ }
67
+ async function generateDockerfiles(options, analysis) {
68
+ const cwd = process.cwd();
69
+ const preset = (0, preset_registry_1.getPresetConfig)(options.preset);
70
+ const entryPoint = detectEntryPoint(cwd);
71
+ (0, cli_ui_1.printSection)(`📝 Generating Dockerfile${options.environment !== "all" ? `.${options.environment}` : "s"}`);
72
+ // Always generate production Dockerfile (as "Dockerfile")
73
+ // Plus environment-specific if requested
74
+ const environments = options.environment === "all"
75
+ ? ["development", "production"]
76
+ : options.environment === "development"
77
+ ? ["development", "production"] // Also generate production Dockerfile
78
+ : [options.environment];
79
+ for (const env of environments) {
80
+ // `staging` is a production-like environment (multi-stage
81
+ // build, prune dev deps, no source-mount expected) but with a
82
+ // distinct `NODE_ENV`, so it picks the production template.
83
+ // Anything not explicitly production-like falls through to dev.
84
+ const templateType = isProductionLikeEnv(env)
85
+ ? "production"
86
+ : "development";
87
+ const vars = (0, template_loader_1.buildDockerVars)(analysis, entryPoint);
88
+ const result = await (0, template_loader_1.loadDockerTemplate)(templateType, vars, () => generateDockerfileContent(env, preset, analysis, entryPoint));
89
+ (0, template_loader_1.logTemplateSource)(`Dockerfile.${env}`, result.source);
90
+ const filename = env === "production" ? "Dockerfile" : `Dockerfile.${env}`;
91
+ const filepath = path_1.default.join(cwd, filename);
92
+ fs_1.default.writeFileSync(filepath, result.content, "utf-8");
93
+ (0, cli_ui_1.printBullet)(chalk_1.default.green(`✓ Created ${filename}`));
94
+ }
95
+ // Generate .dockerignore
96
+ const dockerignore = generateDockerignoreContent(analysis);
97
+ fs_1.default.writeFileSync(path_1.default.join(cwd, ".dockerignore"), dockerignore, "utf-8");
98
+ (0, cli_ui_1.printBullet)(chalk_1.default.green(`✓ Created .dockerignore`));
99
+ // Generate helper script for local dependencies ONLY if needed
100
+ // This is a temporary solution for unpublished packages
101
+ if (analysis?.hasLocalDependencies) {
102
+ const setupScriptNode = generateDockerSetupScriptNode(analysis.localDependencyPaths);
103
+ fs_1.default.writeFileSync(path_1.default.join(cwd, "docker-setup.js"), setupScriptNode, "utf-8");
104
+ (0, cli_ui_1.printBullet)(chalk_1.default.green(`✓ Created docker-setup.js (for local dependencies)`));
105
+ // Also update package.json with docker:setup script using the
106
+ // detected package manager so the generated `docker:build`
107
+ // composite script works for pnpm/yarn/bun users too.
108
+ updatePackageJsonWithDockerScript(cwd, analysis?.packageManager ?? "npm");
109
+ (0, cli_ui_1.printBullet)(chalk_1.default.green(`✓ Updated package.json with docker:setup script`));
110
+ process_1.stdout.write("\n");
111
+ (0, cli_ui_1.printWarning)(".docker-deps/ and package.docker.json are temporary solutions for local file dependencies. Once packages are published to npm, you can remove these and use a simpler Dockerfile.", "containerize");
112
+ }
113
+ }
114
+ exports.generateDockerfiles = generateDockerfiles;
115
+ /**
116
+ * Production-like environments share the multi-stage / prune /
117
+ * baked-image Dockerfile shape. The only thing that differs is
118
+ * `NODE_ENV` (which apps may inspect for behavior switches).
119
+ */
120
+ function isProductionLikeEnv(env) {
121
+ return env === "production" || env === "staging";
122
+ }
123
+ function generateDockerfileContent(environment, preset, analysis, entryPoint) {
124
+ const nodeVersion = analysis?.nodeVersion || "22";
125
+ const packageManager = analysis?.packageManager || "npm";
126
+ const port = analysis?.port || 3000;
127
+ if (!isProductionLikeEnv(environment)) {
128
+ return generateDevelopmentDockerfile(nodeVersion, packageManager, port, preset, analysis);
129
+ }
130
+ return generateProductionDockerfile(nodeVersion, packageManager, port, preset, analysis, entryPoint, environment);
131
+ }
132
+ function generateDevelopmentDockerfile(nodeVersion, packageManager, port, preset, analysis) {
133
+ const baseImage = preset.baseImage || `node:${nodeVersion}-alpine`;
134
+ const hasLocalDeps = analysis?.hasLocalDependencies ?? false;
135
+ const localDepCopies = hasLocalDeps
136
+ ? generateLocalDependencyCopies(analysis.localDependencyPaths, packageManager)
137
+ : "";
138
+ // Bootstrap config analysis for env files
139
+ const bootstrapConfig = analysis?.bootstrapConfig;
140
+ const copyEnvFiles = bootstrapConfig && (0, bootstrap_analyzer_1.shouldCopyEnvFiles)(bootstrapConfig);
141
+ const envFileCopies = copyEnvFiles
142
+ ? generateEnvFileCopies(bootstrapConfig, "development")
143
+ : "";
144
+ const envFileNote = copyEnvFiles
145
+ ? "\n# Note: Environment files are copied based on bootstrap configuration"
146
+ : "";
147
+ // Package file handling - use package.docker.json for local deps
148
+ const packageCopySection = hasLocalDeps
149
+ ? `# Copy package files (use Docker-modified version for local dependencies)
150
+ COPY package.docker.json ./package.json`
151
+ : `# Copy package files
152
+ COPY package*.json ./`;
153
+ // Install command - lockfile-based installs can't resolve `file:`
154
+ // paths recorded by the host, so for local deps we drop down to the
155
+ // loose install for whichever PM is in use.
156
+ const installCommand = hasLocalDeps
157
+ ? `# Install dependencies (lockfile-free install for local file dependencies)
158
+ ${getLocalDepsInstallCommand(packageManager)}`
159
+ : getInstallCommand(packageManager, false);
160
+ return `# Development Dockerfile
161
+ # Generated by ExpressoTS CLI${hasLocalDeps ? "\n# Note: This project uses local file dependencies" : ""}${envFileNote}
162
+
163
+ FROM ${baseImage}
164
+
165
+ # Set working directory
166
+ WORKDIR /app
167
+
168
+ ${packageCopySection}
169
+ ${packageManager === "pnpm" ? "COPY pnpm-lock.yaml ./" : ""}
170
+ ${packageManager === "yarn" ? "COPY yarn.lock ./" : ""}
171
+ ${localDepCopies}
172
+
173
+ ${installCommand}
174
+
175
+ # Copy source code
176
+ COPY . .
177
+ ${envFileCopies}
178
+
179
+ # Expose port and debug port
180
+ EXPOSE ${port}
181
+ EXPOSE 9229
182
+
183
+ # Set environment
184
+ ENV NODE_ENV=development
185
+ ENV PORT=${port}
186
+
187
+ # Start with hot reload
188
+ ${getCmdScript(packageManager, "dev")}
189
+ `;
190
+ }
191
+ /**
192
+ * Generate COPY commands for environment files based on bootstrap config
193
+ */
194
+ function generateEnvFileCopies(bootstrapConfig, environment) {
195
+ const copies = [];
196
+ const envFile = (0, bootstrap_analyzer_1.getEnvFileForEnvironment)(bootstrapConfig, environment);
197
+ // Only copy files that exist
198
+ if (bootstrapConfig.existingEnvFiles.includes(envFile)) {
199
+ copies.push(`\n# Copy environment file for ${environment}`);
200
+ copies.push(`COPY ${envFile} ./${envFile}`);
201
+ }
202
+ // Also copy .env if it exists (base configuration)
203
+ if (bootstrapConfig.existingEnvFiles.includes(".env") &&
204
+ envFile !== ".env") {
205
+ copies.push(`COPY .env ./.env`);
206
+ }
207
+ return copies.length > 0 ? copies.join("\n") : "";
208
+ }
209
+ function generateProductionDockerfile(nodeVersion, packageManager, port, preset, analysis, entryPoint, environment = "production") {
210
+ const baseImage = preset.baseImage || `node:${nodeVersion}-alpine`;
211
+ const isMultiStage = preset.multiStage !== false;
212
+ const hasLocalDeps = analysis?.hasLocalDependencies ?? false;
213
+ if (!isMultiStage) {
214
+ return generateSingleStageDockerfile(baseImage, packageManager, port, preset, analysis, entryPoint, environment);
215
+ }
216
+ // Generate local dependency copy commands (only if using local file: deps)
217
+ const localDepCopies = hasLocalDeps
218
+ ? generateLocalDependencyCopies(analysis.localDependencyPaths, packageManager)
219
+ : "";
220
+ // Package file handling - only use package.docker.json for local deps.
221
+ // When local deps are present we deliberately do NOT copy the host
222
+ // lockfile: it still references the original `file:` paths from the
223
+ // host (e.g. `file:../expressots/...tgz`) which won't resolve inside
224
+ // the container. npm install will recreate the lockfile from the
225
+ // rewritten package.docker.json.
226
+ const packageCopySection = hasLocalDeps
227
+ ? `# Copy package files (use Docker-modified version for local dependencies)
228
+ # Lockfile is intentionally omitted: the host lockfile references file:../
229
+ # paths that don't exist inside the container.
230
+ COPY package.docker.json ./package.json`
231
+ : `# Copy package files
232
+ COPY package*.json ./
233
+ COPY package-lock.json* ./`;
234
+ // Skip prune for local dependencies as lockfile paths won't resolve.
235
+ // Otherwise use the package-manager-specific prune equivalent.
236
+ const pruneCommand = hasLocalDeps
237
+ ? `# Skip prune for local file dependencies (lockfile paths are from host)
238
+ # Once packages are published, you can re-enable the prune step.`
239
+ : `# Prune devDependencies after build
240
+ ${getPruneCommand(packageManager)}`;
241
+ // Multi-stage build (default for production)
242
+ return `# Production Dockerfile (Multi-stage)
243
+ # Generated by ExpressoTS CLI
244
+ # Preset: ${preset.name}${hasLocalDeps ? "\n# Note: This project uses local file dependencies (temporary until published)" : ""}
245
+
246
+ # ============================================
247
+ # Stage 1: Builder
248
+ # ============================================
249
+ FROM ${baseImage} AS builder
250
+
251
+ WORKDIR /app
252
+ ${localDepCopies}
253
+
254
+ ${packageCopySection}
255
+ ${packageManager === "pnpm" ? "COPY pnpm-lock.yaml ./" : ""}
256
+ ${packageManager === "yarn" ? "COPY yarn.lock ./" : ""}
257
+
258
+ # Install ALL dependencies (including devDependencies for build)
259
+ ${hasLocalDeps ? getLocalDepsInstallCommand(packageManager) : getInstallCommand(packageManager, false)}
260
+
261
+ # Copy source code
262
+ COPY . .
263
+
264
+ # Build application
265
+ ${getRunScriptCommand(packageManager, "build")}
266
+
267
+ ${pruneCommand}
268
+
269
+ ${preset.security?.enabled
270
+ ? `
271
+ # ============================================
272
+ # Stage 2: Production
273
+ # ============================================
274
+ FROM ${baseImage}
275
+
276
+ # Create non-root user for security
277
+ RUN addgroup -g 1001 -S nodejs && \\
278
+ adduser -S nodejs -u 1001
279
+
280
+ WORKDIR /app
281
+
282
+ # Copy necessary files from builder
283
+ COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
284
+ COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
285
+ COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
286
+
287
+ # Switch to non-root user
288
+ USER nodejs
289
+ `
290
+ : `
291
+ # ============================================
292
+ # Stage 2: Production
293
+ # ============================================
294
+ FROM ${baseImage}
295
+
296
+ WORKDIR /app
297
+
298
+ # Copy necessary files from builder
299
+ COPY --from=builder /app/dist ./dist
300
+ COPY --from=builder /app/node_modules ./node_modules
301
+ COPY --from=builder /app/package*.json ./
302
+ `}
303
+
304
+ # Expose port
305
+ EXPOSE ${port}
306
+
307
+ # Set environment variables
308
+ ENV NODE_ENV=${environment}
309
+ ENV PORT=${port}
310
+
311
+ ${analysis?.hasDatabase
312
+ ? `# Database connection will be provided via environment variables
313
+ # Example: DATABASE_URL=postgresql://user:pass@host:5432/db
314
+ `
315
+ : ""}
316
+ ${analysis?.hasRedis
317
+ ? `# Redis connection will be provided via environment variables
318
+ # Example: REDIS_URL=redis://host:6379
319
+ `
320
+ : ""}
321
+
322
+ ${preset.healthCheck?.enabled
323
+ ? `# Health check
324
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \\
325
+ CMD node -e "require('http').get('http://localhost:${port}/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
326
+ `
327
+ : ""}
328
+
329
+ # Start application
330
+ CMD ["node", "${entryPoint}"]
331
+ `;
332
+ }
333
+ function generateSingleStageDockerfile(baseImage, packageManager, port, preset, analysis, entryPoint, environment = "production") {
334
+ const hasLocalDeps = analysis?.hasLocalDependencies ?? false;
335
+ const localDepCopies = hasLocalDeps
336
+ ? generateLocalDependencyCopies(analysis.localDependencyPaths, packageManager)
337
+ : "";
338
+ // Package file handling
339
+ const packageCopySection = hasLocalDeps
340
+ ? `# Copy package files (use Docker-modified version for local dependencies)
341
+ COPY package.docker.json ./package.json`
342
+ : `# Copy package files
343
+ COPY package*.json ./`;
344
+ return `# Production Dockerfile (Single-stage)
345
+ # Generated by ExpressoTS CLI${hasLocalDeps ? "\n# Note: This project uses local file dependencies (temporary until published)" : ""}
346
+
347
+ FROM ${baseImage}
348
+
349
+ WORKDIR /app
350
+ ${localDepCopies}
351
+
352
+ ${packageCopySection}
353
+
354
+ # Install dependencies
355
+ ${hasLocalDeps ? getLocalDepsInstallCommand(packageManager) : getInstallCommand(packageManager, true)}
356
+
357
+ # Copy source code
358
+ COPY . .
359
+
360
+ # Build application
361
+ ${getRunScriptCommand(packageManager, "build")}
362
+
363
+ # Expose port
364
+ EXPOSE ${port}
365
+
366
+ # Set environment
367
+ ENV NODE_ENV=${environment}
368
+ ENV PORT=${port}
369
+
370
+ # Start application
371
+ CMD ["node", "${entryPoint}"]
372
+ `;
373
+ }
374
+ function getInstallCommand(packageManager, productionOnly) {
375
+ const prodFlag = productionOnly ? " --production" : "";
376
+ switch (packageManager) {
377
+ case "pnpm":
378
+ return `RUN pnpm install${productionOnly ? " --prod" : ""}`;
379
+ case "yarn":
380
+ return `RUN yarn install${productionOnly ? " --production" : ""}`;
381
+ case "bun":
382
+ return `RUN bun install${productionOnly ? " --production" : ""}`;
383
+ default:
384
+ return `RUN npm ci${prodFlag}`;
385
+ }
386
+ }
387
+ /**
388
+ * Install dependencies in dev mode for a project that uses local
389
+ * `file:` deps. We can't use the lockfile-based commands (`npm ci`,
390
+ * `pnpm install --frozen-lockfile`) because the lockfile pins paths
391
+ * from the host that don't exist inside the container, so we fall
392
+ * back to the looser install command for whichever PM is detected.
393
+ */
394
+ function getLocalDepsInstallCommand(packageManager) {
395
+ switch (packageManager) {
396
+ case "pnpm":
397
+ return `RUN pnpm install --no-frozen-lockfile`;
398
+ case "yarn":
399
+ return `RUN yarn install --no-immutable`;
400
+ case "bun":
401
+ return `RUN bun install --no-save`;
402
+ default:
403
+ return `RUN npm install`;
404
+ }
405
+ }
406
+ /**
407
+ * Returns the Dockerfile RUN command that runs an npm-style script
408
+ * (e.g. `build`) using the detected package manager.
409
+ */
410
+ function getRunScriptCommand(packageManager, scriptName) {
411
+ switch (packageManager) {
412
+ case "pnpm":
413
+ return `RUN pnpm run ${scriptName}`;
414
+ case "yarn":
415
+ return `RUN yarn ${scriptName}`;
416
+ case "bun":
417
+ return `RUN bun run ${scriptName}`;
418
+ default:
419
+ return `RUN npm run ${scriptName}`;
420
+ }
421
+ }
422
+ /**
423
+ * Returns the Dockerfile CMD instruction that runs an npm-style
424
+ * script (e.g. `dev`) using the detected package manager.
425
+ */
426
+ function getCmdScript(packageManager, scriptName) {
427
+ switch (packageManager) {
428
+ case "pnpm":
429
+ return `CMD ["pnpm", "run", "${scriptName}"]`;
430
+ case "yarn":
431
+ return `CMD ["yarn", "${scriptName}"]`;
432
+ case "bun":
433
+ return `CMD ["bun", "run", "${scriptName}"]`;
434
+ default:
435
+ return `CMD ["npm", "run", "${scriptName}"]`;
436
+ }
437
+ }
438
+ /**
439
+ * Returns the Dockerfile RUN command for pruning devDependencies in
440
+ * a multi-stage build. Only npm and yarn ship a built-in prune; for
441
+ * pnpm and bun we re-install with the production flag instead.
442
+ */
443
+ function getPruneCommand(packageManager) {
444
+ switch (packageManager) {
445
+ case "pnpm":
446
+ return `RUN pnpm install --prod --no-frozen-lockfile`;
447
+ case "yarn":
448
+ return `RUN yarn install --production --ignore-scripts --prefer-offline`;
449
+ case "bun":
450
+ return `RUN bun install --production`;
451
+ default:
452
+ return `RUN npm prune --production`;
453
+ }
454
+ }
455
+ function generateLocalDependencyCopies(localDependencyPaths, packageManager = "npm") {
456
+ if (!localDependencyPaths || localDependencyPaths.length === 0) {
457
+ return "";
458
+ }
459
+ const setupHint = getRunScriptShellInvocation(packageManager, "docker:setup");
460
+ return (`
461
+ # Copy local dependencies (these should be in the project directory)
462
+ # Run the setup script first: ${setupHint}` +
463
+ "\n" +
464
+ localDependencyPaths
465
+ .map((depPath) => {
466
+ const filename = path_1.default.basename(depPath);
467
+ return `COPY ./.docker-deps/${filename} ./.docker-deps/${filename}`;
468
+ })
469
+ .join("\n"));
470
+ }
471
+ /**
472
+ * Returns the shell invocation a developer would type to run an
473
+ * npm-style script (used in informational comments / generated
474
+ * package.json scripts, NOT inside Dockerfile RUN/CMD).
475
+ */
476
+ function getRunScriptShellInvocation(packageManager, scriptName) {
477
+ switch (packageManager) {
478
+ case "pnpm":
479
+ return `pnpm run ${scriptName}`;
480
+ case "yarn":
481
+ return `yarn ${scriptName}`;
482
+ case "bun":
483
+ return `bun run ${scriptName}`;
484
+ default:
485
+ return `npm run ${scriptName}`;
486
+ }
487
+ }
488
+ function generateDockerignoreContent(analysis) {
489
+ // Bootstrap config determines which env files should NOT be ignored
490
+ const bootstrapConfig = analysis?.bootstrapConfig;
491
+ const copyEnvFiles = bootstrapConfig && (0, bootstrap_analyzer_1.shouldCopyEnvFiles)(bootstrapConfig);
492
+ // Build env file exclusions based on bootstrap config
493
+ let envFileSection = `# Environment files
494
+ .env
495
+ .env.*
496
+ !.env.example`;
497
+ if (copyEnvFiles && bootstrapConfig) {
498
+ // Don't ignore env files that need to be copied
499
+ const envExclusions = bootstrapConfig.existingEnvFiles
500
+ .filter((f) => f !== ".env.example")
501
+ .map((f) => `!${f}`)
502
+ .join("\n");
503
+ if (envExclusions) {
504
+ envFileSection = `# Environment files (some included based on bootstrap config)
505
+ .env
506
+ .env.*
507
+ !.env.example
508
+ ${envExclusions}`;
509
+ }
510
+ }
511
+ return `# Generated by ExpressoTS CLI
512
+
513
+ # Dependencies
514
+ node_modules/
515
+ npm-debug.log
516
+ yarn-error.log
517
+ pnpm-debug.log
518
+ .pnpm-store/
519
+
520
+ # Build outputs
521
+ dist/
522
+ build/
523
+ *.tsbuildinfo
524
+
525
+ ${envFileSection}
526
+
527
+ # IDE
528
+ .vscode/
529
+ .idea/
530
+ *.swp
531
+ *.swo
532
+ *~
533
+
534
+ # OS
535
+ .DS_Store
536
+ Thumbs.db
537
+
538
+ # Testing
539
+ coverage/
540
+ .nyc_output/
541
+
542
+ # Git
543
+ .git/
544
+ .gitignore
545
+
546
+ # Docker
547
+ Dockerfile*
548
+ docker-compose*.yml
549
+ .dockerignore
550
+ docker-setup.js
551
+
552
+ # Documentation
553
+ README.md
554
+ docs/
555
+ *.md
556
+
557
+ # CI/CD
558
+ .github/
559
+ .gitlab-ci.yml
560
+ azure-pipelines.yml
561
+
562
+ # Misc
563
+ .editorconfig
564
+ .prettierrc
565
+ .eslintrc*
566
+ jest.config.*${analysis?.hasLocalDependencies ? "\n\n# Local dependencies (included via setup script)\n!.docker-deps/" : ""}
567
+ `;
568
+ }
569
+ function generateDockerSetupScriptNode(localDependencyPaths) {
570
+ return `#!/usr/bin/env node
571
+ // Docker setup script for local dependencies
572
+ // Generated by ExpressoTS CLI
573
+
574
+ const fs = require('fs');
575
+ const path = require('path');
576
+
577
+ console.log('📦 Setting up local dependencies for Docker build...');
578
+
579
+ // Create .docker-deps directory
580
+ const depsDir = '.docker-deps';
581
+ if (!fs.existsSync(depsDir)) {
582
+ fs.mkdirSync(depsDir, { recursive: true });
583
+ }
584
+
585
+ // Copy local dependency files
586
+ ${localDependencyPaths
587
+ .map((depPath) => {
588
+ const filename = path_1.default.basename(depPath);
589
+ return `console.log(' Copying ${filename}...');
590
+ try {
591
+ fs.copyFileSync('${depPath.replace(/\\/g, "/")}', path.join(depsDir, '${filename}'));
592
+ } catch (err) {
593
+ console.error(' ❌ Failed to copy ${filename}:', err.message);
594
+ process.exit(1);
595
+ }`;
596
+ })
597
+ .join("\n")}
598
+
599
+ console.log(' Creating Docker-compatible package.json...');
600
+
601
+ // Read the user's package.json
602
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
603
+
604
+ // Track all rewritten file: deps so we can force npm to use the
605
+ // flattened .docker-deps/ paths for any TRANSITIVE references too.
606
+ // This is critical: published tarballs of local packages bake in
607
+ // their own \`file:../...\` paths (e.g. core's package.json depends
608
+ // on shared via \`file:../shared/...tgz\`). Without overrides, npm
609
+ // would try to resolve those original host paths from inside
610
+ // node_modules and fail with ENOENT in the container.
611
+ const overrides = {};
612
+
613
+ const rewriteFileDeps = (depGroup) => {
614
+ if (!depGroup) return;
615
+ Object.keys(depGroup).forEach((key) => {
616
+ const value = depGroup[key];
617
+ if (typeof value !== 'string' || !value.startsWith('file:')) return;
618
+ const filename = value.split('/').pop();
619
+ // Only rewrite tarball references; directory file: refs are
620
+ // intentionally left as-is and will surface as a build failure
621
+ // the user can address (publish or replace with a tarball).
622
+ if (!filename.endsWith('.tgz')) return;
623
+ const newPath = 'file:.docker-deps/' + filename;
624
+ depGroup[key] = newPath;
625
+ overrides[key] = newPath;
626
+ });
627
+ };
628
+
629
+ rewriteFileDeps(pkg.dependencies);
630
+ rewriteFileDeps(pkg.devDependencies);
631
+
632
+ if (Object.keys(overrides).length > 0) {
633
+ pkg.overrides = Object.assign({}, pkg.overrides || {}, overrides);
634
+ console.log(' Added overrides for ' + Object.keys(overrides).length + ' transitive file: deps');
635
+ }
636
+
637
+ fs.writeFileSync('package.docker.json', JSON.stringify(pkg, null, 2) + '\\n', 'utf-8');
638
+
639
+ console.log('✅ Local dependencies setup complete!');
640
+ console.log(' You can now run: docker build -t myapp .');
641
+ `;
642
+ }
643
+ function updatePackageJsonWithDockerScript(cwd, packageManager = "npm") {
644
+ const packageJsonPath = path_1.default.join(cwd, "package.json");
645
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf-8"));
646
+ if (!packageJson.scripts) {
647
+ packageJson.scripts = {};
648
+ }
649
+ const imageName = packageJson.name?.replace(/[^a-z0-9-]/gi, "-").toLowerCase() ||
650
+ "expressots-app";
651
+ const setupInvocation = getRunScriptShellInvocation(packageManager, "docker:setup");
652
+ packageJson.scripts["docker:setup"] = "node docker-setup.js";
653
+ packageJson.scripts["docker:build"] =
654
+ `${setupInvocation} && docker build -t ${imageName} .`;
655
+ packageJson.scripts["docker:run"] = `docker run -p 3000:3000 ${imageName}`;
656
+ fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
657
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProjectAnalysis } from "../analyzers/project-analyzer";
2
+ type GeneratorOptions = {
3
+ environment: string;
4
+ preset: string;
5
+ [key: string]: any;
6
+ };
7
+ export declare function generateKubernetesConfigs(options: GeneratorOptions, analysis?: ProjectAnalysis): Promise<void>;
8
+ export {};