@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
package/bin/new/form.js
CHANGED
|
@@ -8,115 +8,259 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
8
8
|
const cli_progress_1 = require("cli-progress");
|
|
9
9
|
const degit_1 = __importDefault(require("degit"));
|
|
10
10
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
-
const node_child_process_1 = require("node:child_process");
|
|
12
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
-
const cli_1 = require("../cli");
|
|
15
13
|
const center_text_1 = require("../utils/center-text");
|
|
16
14
|
const change_package_info_1 = require("../utils/change-package-info");
|
|
17
15
|
const cli_ui_1 = require("../utils/cli-ui");
|
|
16
|
+
const input_validation_1 = require("../utils/input-validation");
|
|
17
|
+
const safe_spawn_1 = require("../utils/safe-spawn");
|
|
18
|
+
/**
|
|
19
|
+
* Install dependencies using the selected package manager
|
|
20
|
+
*/
|
|
18
21
|
async function packageManagerInstall({ packageManager, directory, progressBar, }) {
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
if (!(0, input_validation_1.isValidPackageManager)(packageManager)) {
|
|
23
|
+
throw new Error(`Invalid package manager: ${packageManager}`);
|
|
24
|
+
}
|
|
25
|
+
// npm's `--silent` swallows errors too (loglevel=silent), which
|
|
26
|
+
// makes failures impossible to diagnose. `--loglevel=error` keeps
|
|
27
|
+
// the install quiet on the happy path but lets real failures
|
|
28
|
+
// stream to stderr so we can capture and surface them below.
|
|
29
|
+
const args = packageManager === "npm"
|
|
30
|
+
? ["install", "--loglevel=error"]
|
|
31
|
+
: ["install", "--silent"];
|
|
21
32
|
if (packageManager === "yarn") {
|
|
22
33
|
args.push("--ignore-engines");
|
|
23
|
-
args.splice(args.indexOf("--prefer-offline"), 1);
|
|
24
34
|
}
|
|
25
35
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
36
|
+
// `safeSpawn` (cross-spawn) handles the Windows `.cmd` shim
|
|
37
|
+
// resolution and properly escapes argv even when the shell is
|
|
38
|
+
// involved on Windows. The `directory` value is only used as
|
|
39
|
+
// cwd; it is never interpolated into a command string.
|
|
40
|
+
const installProcess = (0, safe_spawn_1.safeSpawn)(packageManager, args, {
|
|
27
41
|
cwd: directory,
|
|
28
|
-
shell: true,
|
|
29
42
|
timeout: 600000,
|
|
30
43
|
});
|
|
31
|
-
|
|
32
|
-
let
|
|
44
|
+
let progress = 50;
|
|
45
|
+
let lastProgressUpdate = Date.now();
|
|
33
46
|
const interval = setInterval(() => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (progress < 88) {
|
|
49
|
+
const increment = progress < 70 ? 3 : 1;
|
|
50
|
+
progress = Math.min(progress + increment, 88);
|
|
51
|
+
progressBar.update(progress, {
|
|
52
|
+
doing: "Installing dependencies...",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else if (now - lastProgressUpdate > 3000) {
|
|
56
|
+
progressBar.update(progress, {
|
|
57
|
+
doing: "Installing dependencies...",
|
|
58
|
+
});
|
|
37
59
|
}
|
|
38
60
|
}, 1000);
|
|
39
|
-
//
|
|
61
|
+
// Keep a rolling tail of stderr/stdout so we can surface a
|
|
62
|
+
// meaningful diagnostic when the install exits non-zero (npm's
|
|
63
|
+
// real error is otherwise hidden behind `--silent`).
|
|
64
|
+
const diagnosticBuffer = [];
|
|
65
|
+
const MAX_DIAGNOSTIC_LINES = 20;
|
|
66
|
+
const recordDiagnostic = (chunk) => {
|
|
67
|
+
for (const line of chunk.split(/\r?\n/)) {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
if (!trimmed)
|
|
70
|
+
continue;
|
|
71
|
+
diagnosticBuffer.push(trimmed);
|
|
72
|
+
if (diagnosticBuffer.length > MAX_DIAGNOSTIC_LINES) {
|
|
73
|
+
diagnosticBuffer.shift();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
40
77
|
installProcess.stdout?.on("data", (data) => {
|
|
41
|
-
const output = data.toString()
|
|
42
|
-
|
|
43
|
-
const cleanedOutput = output.replace(/\|\|.*$/g, "");
|
|
44
|
-
// Match and handle npm-specific progress
|
|
78
|
+
const output = data.toString();
|
|
79
|
+
recordDiagnostic(output);
|
|
80
|
+
const cleanedOutput = output.trim().replace(/\|\|.*$/g, "");
|
|
45
81
|
const npmProgressMatch = cleanedOutput.match(/\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/);
|
|
46
82
|
if (npmProgressMatch) {
|
|
47
83
|
const [, current, total, task] = npmProgressMatch;
|
|
48
|
-
|
|
84
|
+
const npmProgress = (parseInt(current) / parseInt(total)) * 100;
|
|
85
|
+
progress = Math.round(50 + npmProgress * 0.4);
|
|
86
|
+
lastProgressUpdate = Date.now();
|
|
49
87
|
progressBar.update(progress, { doing: task });
|
|
50
88
|
}
|
|
51
|
-
else {
|
|
52
|
-
|
|
89
|
+
else if (cleanedOutput) {
|
|
90
|
+
lastProgressUpdate = Date.now();
|
|
53
91
|
progressBar.update(progress, { doing: cleanedOutput });
|
|
54
92
|
}
|
|
55
93
|
});
|
|
56
|
-
|
|
94
|
+
installProcess.stderr?.on("data", (data) => {
|
|
95
|
+
const output = data.toString();
|
|
96
|
+
recordDiagnostic(output);
|
|
97
|
+
const cleanedOutput = output.trim().replace(/\|\|.*$/g, "");
|
|
98
|
+
const npmProgressMatch = cleanedOutput.match(/\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/);
|
|
99
|
+
if (npmProgressMatch) {
|
|
100
|
+
const [, current, total, task] = npmProgressMatch;
|
|
101
|
+
const npmProgress = (parseInt(current) / parseInt(total)) * 100;
|
|
102
|
+
progress = Math.round(50 + npmProgress * 0.4);
|
|
103
|
+
lastProgressUpdate = Date.now();
|
|
104
|
+
progressBar.update(progress, { doing: task });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
57
107
|
installProcess.on("error", (error) => {
|
|
58
|
-
clearInterval(interval);
|
|
108
|
+
clearInterval(interval);
|
|
59
109
|
progressBar.stop();
|
|
60
110
|
reject(new Error(`Failed to start subprocess: ${error.message}`));
|
|
61
111
|
});
|
|
62
|
-
// Finalize progress on close
|
|
63
112
|
installProcess.on("close", (code) => {
|
|
64
|
-
clearInterval(interval);
|
|
113
|
+
clearInterval(interval);
|
|
65
114
|
if (code === 0) {
|
|
66
|
-
progressBar.update(
|
|
67
|
-
progressBar.stop();
|
|
115
|
+
progressBar.update(90, { doing: "Dependencies installed" });
|
|
68
116
|
resolve("Installation Done!");
|
|
69
117
|
}
|
|
70
118
|
else {
|
|
71
119
|
progressBar.stop();
|
|
120
|
+
if (diagnosticBuffer.length > 0) {
|
|
121
|
+
console.log("\n");
|
|
122
|
+
console.log(chalk_1.default.bold.red(`${packageManager} install failed with exit code ${code}:`));
|
|
123
|
+
for (const line of diagnosticBuffer) {
|
|
124
|
+
console.log(chalk_1.default.gray(` ${line}`));
|
|
125
|
+
}
|
|
126
|
+
console.log("");
|
|
127
|
+
}
|
|
72
128
|
reject(new Error(`${packageManager} install exited with code ${code}`));
|
|
73
129
|
}
|
|
74
130
|
});
|
|
75
131
|
});
|
|
76
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Check if the package manager is installed
|
|
135
|
+
*/
|
|
77
136
|
async function checkIfPackageManagerExists(packageManager) {
|
|
78
|
-
|
|
79
|
-
(0,
|
|
80
|
-
|
|
137
|
+
if (!(0, input_validation_1.isValidPackageManager)(packageManager)) {
|
|
138
|
+
(0, cli_ui_1.printError)("Package manager not found!", packageManager);
|
|
139
|
+
process.exit(1);
|
|
81
140
|
}
|
|
82
|
-
|
|
141
|
+
const result = (0, safe_spawn_1.safeSpawnSync)(packageManager, ["--version"], {
|
|
142
|
+
stdio: "ignore",
|
|
143
|
+
});
|
|
144
|
+
if (result.error || result.status !== 0) {
|
|
83
145
|
(0, cli_ui_1.printError)("Package manager not found!", packageManager);
|
|
84
146
|
process.exit(1);
|
|
85
147
|
}
|
|
148
|
+
return true;
|
|
86
149
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
node_fs_1.default.renameSync(envExamplePath, envPath);
|
|
150
|
+
/**
|
|
151
|
+
* Copy directory recursively (for local template testing)
|
|
152
|
+
*/
|
|
153
|
+
function copyDirectorySync(src, dest) {
|
|
154
|
+
if (!node_fs_1.default.existsSync(dest)) {
|
|
155
|
+
node_fs_1.default.mkdirSync(dest, { recursive: true });
|
|
95
156
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
157
|
+
const entries = node_fs_1.default.readdirSync(src, { withFileTypes: true });
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const srcPath = node_path_1.default.join(src, entry.name);
|
|
160
|
+
const destPath = node_path_1.default.join(dest, entry.name);
|
|
161
|
+
// Skip node_modules and dist directories
|
|
162
|
+
if (entry.name === "node_modules" || entry.name === "dist") {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
copyDirectorySync(srcPath, destPath);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
node_fs_1.default.copyFileSync(srcPath, destPath);
|
|
170
|
+
}
|
|
99
171
|
}
|
|
100
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Template definitions for v4.0
|
|
175
|
+
*/
|
|
101
176
|
var Template;
|
|
102
177
|
(function (Template) {
|
|
103
|
-
Template["
|
|
104
|
-
Template["
|
|
105
|
-
Template["micro"] = "Micro :: A minimalistic template for building micro api's.";
|
|
178
|
+
Template["application"] = "Application :: Full-featured ExpressoTS application. (Recommended)";
|
|
179
|
+
Template["micro"] = "Micro :: A minimalistic template for building micro APIs and serverless functions.";
|
|
106
180
|
})(Template || (Template = {}));
|
|
181
|
+
/**
|
|
182
|
+
* Middleware presets for Application template
|
|
183
|
+
*/
|
|
184
|
+
var MiddlewarePreset;
|
|
185
|
+
(function (MiddlewarePreset) {
|
|
186
|
+
MiddlewarePreset["api"] = "API :: REST API with security, compression, and auto-logging. (Recommended)";
|
|
187
|
+
MiddlewarePreset["web"] = "Web :: Full web app with cookies and session support.";
|
|
188
|
+
MiddlewarePreset["graphql"] = "GraphQL :: Optimized for GraphQL APIs.";
|
|
189
|
+
MiddlewarePreset["microservice"] = "Microservice :: Minimal setup for microservices.";
|
|
190
|
+
MiddlewarePreset["minimal"] = "Minimal :: Just request parsing, customize everything yourself.";
|
|
191
|
+
})(MiddlewarePreset || (MiddlewarePreset = {}));
|
|
192
|
+
/**
|
|
193
|
+
* Template folder mapping
|
|
194
|
+
*/
|
|
195
|
+
const TEMPLATE_FOLDERS = {
|
|
196
|
+
Application: "application",
|
|
197
|
+
Micro: "micro",
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Middleware preset mapping to code
|
|
201
|
+
*/
|
|
202
|
+
const PRESET_CODE = {
|
|
203
|
+
API: `this.Middleware.applyPreset("api");`,
|
|
204
|
+
Web: `this.Middleware.applyPreset("web");`,
|
|
205
|
+
GraphQL: `this.Middleware.applyPreset("graphql");`,
|
|
206
|
+
Microservice: `this.Middleware.applyPreset("microservice");`,
|
|
207
|
+
Minimal: `this.Middleware.parse();`,
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Apply the selected middleware preset to the generated app.ts
|
|
211
|
+
*/
|
|
212
|
+
function applyMiddlewarePreset(directory, preset) {
|
|
213
|
+
const appTsPath = node_path_1.default.join(directory, "src", "app.ts");
|
|
214
|
+
if (!node_fs_1.default.existsSync(appTsPath)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// Extract preset name from selection (e.g., "API :: ..." -> "API")
|
|
218
|
+
const presetMatch = preset.match(/^(\w+) ::/);
|
|
219
|
+
const presetName = presetMatch ? presetMatch[1] : "API";
|
|
220
|
+
const presetCode = PRESET_CODE[presetName] || PRESET_CODE["API"];
|
|
221
|
+
let content = node_fs_1.default.readFileSync(appTsPath, "utf-8");
|
|
222
|
+
// Replace the placeholder with the preset code
|
|
223
|
+
content = content.replace(/\/\/ __MIDDLEWARE_PRESET_PLACEHOLDER__/, presetCode);
|
|
224
|
+
node_fs_1.default.writeFileSync(appTsPath, content, "utf-8");
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Enable local template mode for testing.
|
|
228
|
+
* Opt-in via `EXPRESSOTS_DEV=1` and `EXPRESSOTS_USE_LOCAL_TEMPLATES=1`.
|
|
229
|
+
* Both must be set so a stray env var alone cannot redirect a user's
|
|
230
|
+
* `expressots new` to local files.
|
|
231
|
+
*/
|
|
232
|
+
const USE_LOCAL_TEMPLATES = process.env.EXPRESSOTS_DEV === "1" &&
|
|
233
|
+
process.env.EXPRESSOTS_USE_LOCAL_TEMPLATES === "1";
|
|
234
|
+
/**
|
|
235
|
+
* Skip the package-manager install step. Useful when iterating on
|
|
236
|
+
* templates that depend on unpublished package versions. Same dual
|
|
237
|
+
* env-var guard as `USE_LOCAL_TEMPLATES`.
|
|
238
|
+
*/
|
|
239
|
+
const SKIP_INSTALL_FOR_TESTING = process.env.EXPRESSOTS_DEV === "1" &&
|
|
240
|
+
process.env.EXPRESSOTS_SKIP_INSTALL === "1";
|
|
241
|
+
/**
|
|
242
|
+
* Local templates path (relative to CLI installation)
|
|
243
|
+
* For development: points to the templates folder in the monorepo
|
|
244
|
+
* For production: this will be replaced with the actual path
|
|
245
|
+
*/
|
|
246
|
+
const LOCAL_TEMPLATES_PATH = node_path_1.default.resolve(__dirname, "../../../templates");
|
|
247
|
+
/**
|
|
248
|
+
* Main project creation form
|
|
249
|
+
*/
|
|
107
250
|
const projectForm = async (projectName, args) => {
|
|
108
251
|
let answer;
|
|
109
|
-
const [packageManager, template, directory] = args;
|
|
252
|
+
const [packageManager, template, directory, preset] = args;
|
|
110
253
|
if (packageManager && template) {
|
|
111
254
|
answer = {
|
|
112
255
|
name: projectName,
|
|
113
256
|
packageManager: packageManager,
|
|
114
257
|
template: Template[template],
|
|
258
|
+
preset: preset ? MiddlewarePreset[preset] : undefined,
|
|
115
259
|
confirm: true,
|
|
116
260
|
};
|
|
117
261
|
}
|
|
118
262
|
else {
|
|
119
|
-
|
|
263
|
+
const baseAnswers = await inquirer_1.default.prompt([
|
|
120
264
|
{
|
|
121
265
|
type: "input",
|
|
122
266
|
name: "name",
|
|
@@ -130,18 +274,42 @@ const projectForm = async (projectName, args) => {
|
|
|
130
274
|
type: "list",
|
|
131
275
|
name: "packageManager",
|
|
132
276
|
message: "Package manager",
|
|
133
|
-
choices: [
|
|
277
|
+
choices: [
|
|
278
|
+
"npm",
|
|
279
|
+
"yarn",
|
|
280
|
+
"pnpm",
|
|
281
|
+
...(process.platform !== "win32" ? ["bun"] : []),
|
|
282
|
+
],
|
|
134
283
|
},
|
|
135
284
|
{
|
|
136
285
|
type: "list",
|
|
137
286
|
name: "template",
|
|
138
287
|
message: "Select a template",
|
|
139
288
|
choices: [
|
|
140
|
-
`
|
|
141
|
-
"
|
|
142
|
-
"Micro :: A minimalistic template for building micro api's.",
|
|
289
|
+
`Application :: Full-featured ExpressoTS application. (${chalk_1.default.yellow("Recommended")})`,
|
|
290
|
+
"Micro :: A minimalistic template for building micro APIs and serverless functions.",
|
|
143
291
|
],
|
|
144
292
|
},
|
|
293
|
+
]);
|
|
294
|
+
// Only show preset selection for Application template
|
|
295
|
+
let presetAnswer = {};
|
|
296
|
+
if (baseAnswers.template.startsWith("Application")) {
|
|
297
|
+
presetAnswer = await inquirer_1.default.prompt([
|
|
298
|
+
{
|
|
299
|
+
type: "list",
|
|
300
|
+
name: "preset",
|
|
301
|
+
message: "Select a middleware preset",
|
|
302
|
+
choices: [
|
|
303
|
+
`API :: REST API with security, compression, and auto-logging. (${chalk_1.default.yellow("Recommended")})`,
|
|
304
|
+
"Web :: Full web app with cookies and session support.",
|
|
305
|
+
"GraphQL :: Optimized for GraphQL APIs.",
|
|
306
|
+
"Microservice :: Minimal setup for microservices.",
|
|
307
|
+
"Minimal :: Just request parsing, customize everything yourself.",
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
]);
|
|
311
|
+
}
|
|
312
|
+
const confirmAnswer = await inquirer_1.default.prompt([
|
|
145
313
|
{
|
|
146
314
|
type: "confirm",
|
|
147
315
|
name: "confirm",
|
|
@@ -149,6 +317,11 @@ const projectForm = async (projectName, args) => {
|
|
|
149
317
|
default: true,
|
|
150
318
|
},
|
|
151
319
|
]);
|
|
320
|
+
answer = {
|
|
321
|
+
...baseAnswers,
|
|
322
|
+
...presetAnswer,
|
|
323
|
+
...confirmAnswer,
|
|
324
|
+
};
|
|
152
325
|
}
|
|
153
326
|
if (directory) {
|
|
154
327
|
if (!node_fs_1.default.existsSync(node_path_1.default.join(directory, answer.name))) {
|
|
@@ -159,12 +332,6 @@ const projectForm = async (projectName, args) => {
|
|
|
159
332
|
process.exit(1);
|
|
160
333
|
}
|
|
161
334
|
}
|
|
162
|
-
// Hashmap of templates and their directories
|
|
163
|
-
const templates = {
|
|
164
|
-
NonOpinionated: "non_opinionated",
|
|
165
|
-
Opinionated: "opinionated",
|
|
166
|
-
Micro: "micro",
|
|
167
|
-
};
|
|
168
335
|
if (answer.confirm) {
|
|
169
336
|
// Check if package manager is bun and OS is Windows
|
|
170
337
|
if (answer.packageManager === "bun" && process.platform === "win32") {
|
|
@@ -180,28 +347,76 @@ const projectForm = async (projectName, args) => {
|
|
|
180
347
|
hideCursor: true,
|
|
181
348
|
}, cli_progress_1.Presets.rect);
|
|
182
349
|
progressBar.start(100, 0, {
|
|
183
|
-
doing: "
|
|
350
|
+
doing: "Creating project",
|
|
184
351
|
});
|
|
185
|
-
|
|
186
|
-
const
|
|
352
|
+
// Extract template name from selection
|
|
353
|
+
const templateMatch = answer.template.match(/(.*) ::/);
|
|
354
|
+
if (!templateMatch || !templateMatch[1]) {
|
|
355
|
+
progressBar.stop();
|
|
356
|
+
(0, cli_ui_1.printError)(`Could not parse selected template: ${answer.template}`, "new");
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
const templateName = templateMatch[1];
|
|
360
|
+
const templateFolder = TEMPLATE_FOLDERS[templateName];
|
|
361
|
+
if (!templateFolder) {
|
|
362
|
+
progressBar.stop();
|
|
363
|
+
(0, cli_ui_1.printError)(`Unknown template: ${templateName}`, "new");
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
187
366
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
367
|
+
if (USE_LOCAL_TEMPLATES) {
|
|
368
|
+
// LOCAL TEMPLATE MODE (for testing)
|
|
369
|
+
const localTemplatePath = node_path_1.default.join(LOCAL_TEMPLATES_PATH, templateFolder);
|
|
370
|
+
if (!node_fs_1.default.existsSync(localTemplatePath)) {
|
|
371
|
+
progressBar.stop();
|
|
372
|
+
(0, cli_ui_1.printError)(`Local template not found at: ${localTemplatePath}`, "Please check your templates folder");
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
// Create target directory
|
|
376
|
+
node_fs_1.default.mkdirSync(answer.name, { recursive: true });
|
|
377
|
+
// Copy template files
|
|
378
|
+
copyDirectorySync(localTemplatePath, answer.name);
|
|
379
|
+
progressBar.update(30, { doing: "Template copied" });
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// GITHUB MODE (production)
|
|
383
|
+
// Pinned to the v4.0.0 GA tag of the templates repo so a
|
|
384
|
+
// CLI shipped at v4.0.0 keeps working even if `main` moves.
|
|
385
|
+
const repo = `expressots/templates/${templateFolder}#v4.0.0-preview.1`;
|
|
386
|
+
const emitter = (0, degit_1.default)(repo);
|
|
387
|
+
await emitter.clone(answer.name);
|
|
388
|
+
progressBar.update(30, { doing: "Template cloned" });
|
|
389
|
+
}
|
|
190
390
|
}
|
|
191
391
|
catch (err) {
|
|
192
392
|
console.log("\n");
|
|
193
393
|
(0, cli_ui_1.printError)("Project already exists or Folder is not empty", answer.name);
|
|
194
394
|
process.exit(1);
|
|
195
395
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
progressBar,
|
|
203
|
-
|
|
204
|
-
|
|
396
|
+
if (SKIP_INSTALL_FOR_TESTING) {
|
|
397
|
+
progressBar.update(90, {
|
|
398
|
+
doing: "Skipping install (testing mode)",
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
progressBar.update(50, {
|
|
403
|
+
doing: "Installing dependencies",
|
|
404
|
+
});
|
|
405
|
+
await packageManagerInstall({
|
|
406
|
+
packageManager: answer.packageManager,
|
|
407
|
+
directory: answer.name,
|
|
408
|
+
progressBar,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
// Progress should already be at 90% from packageManagerInstall
|
|
412
|
+
// Only update if we skipped installation
|
|
413
|
+
if (!SKIP_INSTALL_FOR_TESTING) {
|
|
414
|
+
progressBar.update(90, { doing: "Finalizing project" });
|
|
415
|
+
}
|
|
416
|
+
// Apply middleware preset for Application template
|
|
417
|
+
if (answer.preset && templateFolder === "application") {
|
|
418
|
+
applyMiddlewarePreset(answer.name, answer.preset);
|
|
419
|
+
}
|
|
205
420
|
(0, change_package_info_1.changePackageName)({
|
|
206
421
|
directory: answer.name,
|
|
207
422
|
name: projectName,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface DockerfileAnalysis {
|
|
2
|
+
baseImage?: string;
|
|
3
|
+
nodeVersion?: string;
|
|
4
|
+
layers: number;
|
|
5
|
+
hasMultiStage: boolean;
|
|
6
|
+
hasHealthCheck: boolean;
|
|
7
|
+
hasNonRootUser: boolean;
|
|
8
|
+
hasDockerignore: boolean;
|
|
9
|
+
hasNpmInstallWithoutCi: boolean;
|
|
10
|
+
hasCurlOrWgetWithoutCleanup: boolean;
|
|
11
|
+
instructions: DockerInstruction[];
|
|
12
|
+
stages: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface DockerInstruction {
|
|
15
|
+
line: number;
|
|
16
|
+
instruction: string;
|
|
17
|
+
arguments: string;
|
|
18
|
+
raw: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Analyze a Dockerfile for issues and metrics
|
|
22
|
+
*/
|
|
23
|
+
export declare function analyzeDockerfile(dockerfilePath: string): Promise<DockerfileAnalysis>;
|
|
24
|
+
/**
|
|
25
|
+
* Parse a specific Dockerfile instruction
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseInstruction(line: string): DockerInstruction | null;
|
|
@@ -0,0 +1,122 @@
|
|
|
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.parseInstruction = exports.analyzeDockerfile = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/**
|
|
10
|
+
* Analyze a Dockerfile for issues and metrics
|
|
11
|
+
*/
|
|
12
|
+
async function analyzeDockerfile(dockerfilePath) {
|
|
13
|
+
const content = fs_1.default.readFileSync(dockerfilePath, "utf-8");
|
|
14
|
+
const lines = content.split("\n");
|
|
15
|
+
const instructions = [];
|
|
16
|
+
const stages = [];
|
|
17
|
+
let hasMultiStage = false;
|
|
18
|
+
let hasHealthCheck = false;
|
|
19
|
+
let hasNonRootUser = false;
|
|
20
|
+
let hasNpmInstallWithoutCi = false;
|
|
21
|
+
let hasCurlOrWgetWithoutCleanup = false;
|
|
22
|
+
let baseImage;
|
|
23
|
+
let nodeVersion;
|
|
24
|
+
let runLayerCount = 0;
|
|
25
|
+
// Parse Dockerfile
|
|
26
|
+
let currentLine = 0;
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
currentLine++;
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
// Skip comments and empty lines
|
|
31
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Parse instruction
|
|
35
|
+
const match = trimmed.match(/^(\w+)\s*(.*)/);
|
|
36
|
+
if (!match)
|
|
37
|
+
continue;
|
|
38
|
+
const instruction = match[1].toUpperCase();
|
|
39
|
+
const args = match[2];
|
|
40
|
+
instructions.push({
|
|
41
|
+
line: currentLine,
|
|
42
|
+
instruction,
|
|
43
|
+
arguments: args,
|
|
44
|
+
raw: trimmed,
|
|
45
|
+
});
|
|
46
|
+
// Analyze instruction
|
|
47
|
+
switch (instruction) {
|
|
48
|
+
case "FROM":
|
|
49
|
+
if (stages.length > 0) {
|
|
50
|
+
hasMultiStage = true;
|
|
51
|
+
}
|
|
52
|
+
stages.push(args);
|
|
53
|
+
// Extract base image info
|
|
54
|
+
if (!baseImage) {
|
|
55
|
+
baseImage = args.split(/\s+/)[0];
|
|
56
|
+
// Try to extract Node version
|
|
57
|
+
const nodeMatch = baseImage.match(/node:(\d+)/);
|
|
58
|
+
if (nodeMatch) {
|
|
59
|
+
nodeVersion = nodeMatch[1];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
case "HEALTHCHECK":
|
|
64
|
+
hasHealthCheck = true;
|
|
65
|
+
break;
|
|
66
|
+
case "USER":
|
|
67
|
+
// Check if non-root user
|
|
68
|
+
if (args && args !== "root" && args !== "0") {
|
|
69
|
+
hasNonRootUser = true;
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case "RUN":
|
|
73
|
+
runLayerCount++;
|
|
74
|
+
// Check for npm install vs npm ci
|
|
75
|
+
if (args.includes("npm install") && !args.includes("npm ci")) {
|
|
76
|
+
hasNpmInstallWithoutCi = true;
|
|
77
|
+
}
|
|
78
|
+
// Check for curl/wget without cleanup
|
|
79
|
+
if ((args.includes("curl") || args.includes("wget")) &&
|
|
80
|
+
!args.includes("rm ")) {
|
|
81
|
+
hasCurlOrWgetWithoutCleanup = true;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Check for .dockerignore
|
|
87
|
+
const dockerignorePath = path_1.default.join(path_1.default.dirname(dockerfilePath), ".dockerignore");
|
|
88
|
+
const hasDockerignore = fs_1.default.existsSync(dockerignorePath);
|
|
89
|
+
return {
|
|
90
|
+
baseImage,
|
|
91
|
+
nodeVersion,
|
|
92
|
+
layers: runLayerCount,
|
|
93
|
+
hasMultiStage,
|
|
94
|
+
hasHealthCheck,
|
|
95
|
+
hasNonRootUser,
|
|
96
|
+
hasDockerignore,
|
|
97
|
+
hasNpmInstallWithoutCi,
|
|
98
|
+
hasCurlOrWgetWithoutCleanup,
|
|
99
|
+
instructions,
|
|
100
|
+
stages,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
exports.analyzeDockerfile = analyzeDockerfile;
|
|
104
|
+
/**
|
|
105
|
+
* Parse a specific Dockerfile instruction
|
|
106
|
+
*/
|
|
107
|
+
function parseInstruction(line) {
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const match = trimmed.match(/^(\w+)\s*(.*)/);
|
|
113
|
+
if (!match)
|
|
114
|
+
return null;
|
|
115
|
+
return {
|
|
116
|
+
line: 0,
|
|
117
|
+
instruction: match[1].toUpperCase(),
|
|
118
|
+
arguments: match[2],
|
|
119
|
+
raw: trimmed,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
exports.parseInstruction = parseInstruction;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ImageAnalysis {
|
|
2
|
+
size: string;
|
|
3
|
+
layers: number;
|
|
4
|
+
created: string;
|
|
5
|
+
os: string;
|
|
6
|
+
architecture: string;
|
|
7
|
+
vulnerabilities: Vulnerability[];
|
|
8
|
+
}
|
|
9
|
+
export interface Vulnerability {
|
|
10
|
+
id: string;
|
|
11
|
+
severity: "low" | "medium" | "high" | "critical";
|
|
12
|
+
description: string;
|
|
13
|
+
package?: string;
|
|
14
|
+
fixedVersion?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Analyze a Docker image for size, layers, and vulnerabilities
|
|
18
|
+
*/
|
|
19
|
+
export declare function analyzeImage(imageName: string): Promise<ImageAnalysis>;
|