@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.
- package/README.md +41 -95
- package/bin/cicd/cli.d.ts +6 -0
- package/bin/cicd/cli.js +126 -0
- package/bin/cicd/form.d.ts +29 -0
- package/bin/cicd/form.js +345 -0
- package/bin/cicd/generators/azure-devops.d.ts +2 -0
- package/bin/cicd/generators/azure-devops.js +370 -0
- package/bin/cicd/generators/bitbucket.d.ts +2 -0
- package/bin/cicd/generators/bitbucket.js +217 -0
- package/bin/cicd/generators/circleci.d.ts +2 -0
- package/bin/cicd/generators/circleci.js +274 -0
- package/bin/cicd/generators/github-actions.d.ts +14 -0
- package/bin/cicd/generators/github-actions.js +426 -0
- package/bin/cicd/generators/gitlab-ci.d.ts +2 -0
- package/bin/cicd/generators/gitlab-ci.js +237 -0
- package/bin/cicd/generators/index.d.ts +6 -0
- package/bin/cicd/generators/index.js +15 -0
- package/bin/cicd/generators/jenkins.d.ts +2 -0
- package/bin/cicd/generators/jenkins.js +248 -0
- package/bin/cicd/generators/template-loader.d.ts +17 -0
- package/bin/cicd/generators/template-loader.js +128 -0
- package/bin/cicd/index.d.ts +1 -0
- package/bin/cicd/index.js +5 -0
- package/bin/cli.d.ts +1 -1
- package/bin/cli.js +18 -3
- package/bin/commands/project.commands.d.ts +19 -6
- package/bin/commands/project.commands.js +390 -61
- package/bin/config/index.d.ts +5 -0
- package/bin/config/index.js +10 -0
- package/bin/config/manager.d.ts +98 -0
- package/bin/config/manager.js +222 -0
- package/bin/containerize/analyzers/bootstrap-analyzer.d.ts +46 -0
- package/bin/containerize/analyzers/bootstrap-analyzer.js +187 -0
- package/bin/containerize/analyzers/project-analyzer.d.ts +20 -0
- package/bin/containerize/analyzers/project-analyzer.js +150 -0
- package/bin/containerize/cli.d.ts +4 -0
- package/bin/containerize/cli.js +113 -0
- package/bin/containerize/form.d.ts +15 -0
- package/bin/containerize/form.js +154 -0
- package/bin/containerize/generators/ci-generator.d.ts +31 -0
- package/bin/containerize/generators/ci-generator.js +936 -0
- package/bin/containerize/generators/docker-compose-generator.d.ts +8 -0
- package/bin/containerize/generators/docker-compose-generator.js +186 -0
- package/bin/containerize/generators/dockerfile-generator.d.ts +8 -0
- package/bin/containerize/generators/dockerfile-generator.js +635 -0
- package/bin/containerize/generators/kubernetes-generator.d.ts +8 -0
- package/bin/containerize/generators/kubernetes-generator.js +133 -0
- package/bin/containerize/generators/template-loader.d.ts +36 -0
- package/bin/containerize/generators/template-loader.js +129 -0
- package/bin/containerize/index.d.ts +4 -0
- package/bin/containerize/index.js +13 -0
- package/bin/containerize/presets/preset-registry.d.ts +20 -0
- package/bin/containerize/presets/preset-registry.js +102 -0
- package/bin/costs/cli.d.ts +5 -0
- package/bin/costs/cli.js +183 -0
- package/bin/costs/form.d.ts +44 -0
- package/bin/costs/form.js +412 -0
- package/bin/costs/index.d.ts +4 -0
- package/bin/costs/index.js +25 -0
- package/bin/costs/pricing-manager.d.ts +84 -0
- package/bin/costs/pricing-manager.js +342 -0
- package/bin/costs/providers/index.d.ts +32 -0
- package/bin/costs/providers/index.js +153 -0
- package/bin/costs/sources/api-source.d.ts +10 -0
- package/bin/costs/sources/api-source.js +32 -0
- package/bin/costs/sources/index.d.ts +6 -0
- package/bin/costs/sources/index.js +15 -0
- package/bin/costs/sources/local-json-source.d.ts +23 -0
- package/bin/costs/sources/local-json-source.js +59 -0
- package/bin/costs/sources/remote-json-source.d.ts +11 -0
- package/bin/costs/sources/remote-json-source.js +53 -0
- package/bin/costs/types.d.ts +53 -0
- package/bin/costs/types.js +5 -0
- package/bin/dev/cli.d.ts +4 -0
- package/bin/dev/cli.js +134 -0
- package/bin/dev/form.d.ts +36 -0
- package/bin/dev/form.js +254 -0
- package/bin/dev/index.d.ts +1 -0
- package/bin/dev/index.js +5 -0
- package/bin/generate/cli.js +29 -2
- package/bin/generate/form.d.ts +5 -1
- package/bin/generate/form.js +3 -3
- package/bin/generate/templates/nonopinionated/config.tpl +12 -0
- package/bin/generate/templates/nonopinionated/event.tpl +10 -0
- package/bin/generate/templates/nonopinionated/guard.tpl +18 -0
- package/bin/generate/templates/nonopinionated/handler.tpl +12 -0
- package/bin/generate/templates/nonopinionated/interceptor.tpl +27 -0
- package/bin/generate/templates/opinionated/config.tpl +47 -0
- package/bin/generate/templates/opinionated/entity.tpl +1 -8
- package/bin/generate/templates/opinionated/event.tpl +15 -0
- package/bin/generate/templates/opinionated/guard.tpl +41 -0
- package/bin/generate/templates/opinionated/handler.tpl +23 -0
- package/bin/generate/templates/opinionated/interceptor.tpl +50 -0
- package/bin/generate/utils/command-utils.d.ts +7 -3
- package/bin/generate/utils/command-utils.js +95 -31
- package/bin/generate/utils/nonopininated-cmd.d.ts +10 -1
- package/bin/generate/utils/nonopininated-cmd.js +100 -1
- package/bin/generate/utils/opinionated-cmd.d.ts +10 -1
- package/bin/generate/utils/opinionated-cmd.js +112 -7
- package/bin/generate/utils/string-utils.d.ts +6 -0
- package/bin/generate/utils/string-utils.js +13 -1
- package/bin/help/form.js +11 -3
- package/bin/migrate/analyzers/platform-detector.d.ts +14 -0
- package/bin/migrate/analyzers/platform-detector.js +116 -0
- package/bin/migrate/cli.d.ts +6 -0
- package/bin/migrate/cli.js +96 -0
- package/bin/migrate/form.d.ts +25 -0
- package/bin/migrate/form.js +347 -0
- package/bin/migrate/generators/compose-to-k8s.d.ts +2 -0
- package/bin/migrate/generators/compose-to-k8s.js +324 -0
- package/bin/migrate/generators/compose-to-railway.d.ts +2 -0
- package/bin/migrate/generators/compose-to-railway.js +138 -0
- package/bin/migrate/generators/compose-to-render.d.ts +2 -0
- package/bin/migrate/generators/compose-to-render.js +148 -0
- package/bin/migrate/generators/generic-migration.d.ts +9 -0
- package/bin/migrate/generators/generic-migration.js +221 -0
- package/bin/migrate/generators/heroku-to-fly.d.ts +2 -0
- package/bin/migrate/generators/heroku-to-fly.js +291 -0
- package/bin/migrate/generators/heroku-to-railway.d.ts +2 -0
- package/bin/migrate/generators/heroku-to-railway.js +283 -0
- package/bin/migrate/generators/heroku-to-render.d.ts +2 -0
- package/bin/migrate/generators/heroku-to-render.js +148 -0
- package/bin/migrate/generators/index.d.ts +7 -0
- package/bin/migrate/generators/index.js +17 -0
- package/bin/migrate/generators/template-loader.d.ts +21 -0
- package/bin/migrate/generators/template-loader.js +59 -0
- package/bin/migrate/index.d.ts +1 -0
- package/bin/migrate/index.js +5 -0
- package/bin/new/cli.js +21 -6
- package/bin/new/form.d.ts +25 -4
- package/bin/new/form.js +285 -70
- package/bin/profile/analyzers/dockerfile-analyzer.d.ts +27 -0
- package/bin/profile/analyzers/dockerfile-analyzer.js +122 -0
- package/bin/profile/analyzers/image-analyzer.d.ts +19 -0
- package/bin/profile/analyzers/image-analyzer.js +85 -0
- package/bin/profile/cli.d.ts +4 -0
- package/bin/profile/cli.js +92 -0
- package/bin/profile/form.d.ts +56 -0
- package/bin/profile/form.js +400 -0
- package/bin/profile/index.d.ts +1 -0
- package/bin/profile/index.js +5 -0
- package/bin/profile/optimizers/index.d.ts +19 -0
- package/bin/profile/optimizers/index.js +137 -0
- package/bin/providers/add/form.d.ts +1 -1
- package/bin/providers/add/form.js +27 -6
- package/bin/providers/create/form.js +2 -1
- package/bin/scripts/form.js +27 -5
- package/bin/studio/cli.d.ts +15 -0
- package/bin/studio/cli.js +166 -0
- package/bin/studio/index.d.ts +5 -0
- package/bin/studio/index.js +9 -0
- package/bin/templates/cache.d.ts +54 -0
- package/bin/templates/cache.js +180 -0
- package/bin/templates/cli.d.ts +8 -0
- package/bin/templates/cli.js +292 -0
- package/bin/templates/fetcher.d.ts +49 -0
- package/bin/templates/fetcher.js +208 -0
- package/bin/templates/index.d.ts +11 -0
- package/bin/templates/index.js +37 -0
- package/bin/templates/manager.d.ts +116 -0
- package/bin/templates/manager.js +323 -0
- package/bin/templates/renderer.d.ts +49 -0
- package/bin/templates/renderer.js +204 -0
- package/bin/templates/types.d.ts +51 -0
- package/bin/templates/types.js +5 -0
- package/bin/utils/add-module-to-container.d.ts +2 -2
- package/bin/utils/add-module-to-container.js +15 -5
- package/bin/utils/cli-ui.d.ts +30 -3
- package/bin/utils/cli-ui.js +95 -13
- package/bin/utils/index.d.ts +4 -0
- package/bin/utils/index.js +4 -0
- package/bin/utils/input-validation.d.ts +50 -0
- package/bin/utils/input-validation.js +143 -0
- package/bin/utils/package-manager-commands.d.ts +24 -0
- package/bin/utils/package-manager-commands.js +50 -0
- package/bin/utils/safe-spawn.d.ts +35 -0
- package/bin/utils/safe-spawn.js +51 -0
- package/bin/utils/update-tsconfig-paths.d.ts +35 -0
- package/bin/utils/update-tsconfig-paths.js +286 -0
- 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 {};
|