@expressots/cli 3.0.0-beta.4 → 4.0.0-preview.2

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