@expressots/cli 3.0.0 → 4.0.0-preview.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -95
- package/bin/cicd/cli.d.ts +6 -0
- package/bin/cicd/cli.js +128 -0
- package/bin/cicd/form.d.ts +29 -0
- package/bin/cicd/form.js +346 -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 -5
- package/bin/cli.js +72 -7
- package/bin/commands/project.commands.d.ts +19 -6
- package/bin/commands/project.commands.js +602 -66
- 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 +152 -0
- package/bin/containerize/generators/ci-generator.d.ts +31 -0
- package/bin/containerize/generators/ci-generator.js +940 -0
- package/bin/containerize/generators/docker-compose-generator.d.ts +8 -0
- package/bin/containerize/generators/docker-compose-generator.js +187 -0
- package/bin/containerize/generators/dockerfile-generator.d.ts +8 -0
- package/bin/containerize/generators/dockerfile-generator.js +657 -0
- package/bin/containerize/generators/kubernetes-generator.d.ts +8 -0
- package/bin/containerize/generators/kubernetes-generator.js +134 -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 +185 -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 +136 -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.d.ts +1 -1
- 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 +20 -5
- package/bin/generate/utils/command-utils.js +145 -48
- 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 +128 -16
- package/bin/generate/utils/string-utils.d.ts +6 -0
- package/bin/generate/utils/string-utils.js +13 -1
- package/bin/help/cli.d.ts +1 -1
- package/bin/help/command-help-registry.d.ts +23 -0
- package/bin/help/command-help-registry.js +303 -0
- package/bin/help/command-help.d.ts +36 -0
- package/bin/help/command-help.js +56 -0
- package/bin/help/form.js +127 -22
- package/bin/help/main-help.d.ts +8 -0
- package/bin/help/main-help.js +126 -0
- package/bin/help/render.d.ts +32 -0
- package/bin/help/render.js +46 -0
- package/bin/info/cli.d.ts +1 -1
- package/bin/info/form.d.ts +1 -1
- package/bin/info/form.js +11 -11
- 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 +98 -0
- package/bin/migrate/form.d.ts +25 -0
- package/bin/migrate/form.js +348 -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.d.ts +5 -1
- package/bin/new/cli.js +77 -14
- package/bin/new/form.d.ts +27 -4
- package/bin/new/form.js +605 -75
- 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 +94 -0
- package/bin/profile/form.d.ts +56 -0
- package/bin/profile/form.js +401 -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 +53 -3
- package/bin/scripts/form.js +27 -5
- package/bin/studio/cli.d.ts +15 -0
- package/bin/studio/cli.js +172 -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 +294 -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 +14 -3
- package/bin/utils/add-module-to-container.js +327 -98
- package/bin/utils/cli-ui.d.ts +49 -3
- package/bin/utils/cli-ui.js +133 -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 +326 -0
- package/package.json +165 -156
package/bin/new/form.js
CHANGED
|
@@ -8,115 +8,513 @@ 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
13
|
const cli_1 = require("../cli");
|
|
15
14
|
const center_text_1 = require("../utils/center-text");
|
|
16
15
|
const change_package_info_1 = require("../utils/change-package-info");
|
|
17
16
|
const cli_ui_1 = require("../utils/cli-ui");
|
|
17
|
+
const input_validation_1 = require("../utils/input-validation");
|
|
18
|
+
const safe_spawn_1 = require("../utils/safe-spawn");
|
|
19
|
+
/**
|
|
20
|
+
* Install dependencies using the selected package manager
|
|
21
|
+
*/
|
|
18
22
|
async function packageManagerInstall({ packageManager, directory, progressBar, }) {
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
if (!(0, input_validation_1.isValidPackageManager)(packageManager)) {
|
|
24
|
+
throw new Error(`Invalid package manager: ${packageManager}`);
|
|
25
|
+
}
|
|
26
|
+
// npm's `--silent` swallows errors too (loglevel=silent), which
|
|
27
|
+
// makes failures impossible to diagnose. `--loglevel=error` keeps
|
|
28
|
+
// the install quiet on the happy path but lets real failures
|
|
29
|
+
// stream to stderr so we can capture and surface them below.
|
|
30
|
+
const args = packageManager === "npm"
|
|
31
|
+
? ["install", "--loglevel=error"]
|
|
32
|
+
: ["install", "--silent"];
|
|
21
33
|
if (packageManager === "yarn") {
|
|
22
34
|
args.push("--ignore-engines");
|
|
23
|
-
args.splice(args.indexOf("--prefer-offline"), 1);
|
|
24
35
|
}
|
|
25
36
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
37
|
+
// `safeSpawn` (cross-spawn) handles the Windows `.cmd` shim
|
|
38
|
+
// resolution and properly escapes argv even when the shell is
|
|
39
|
+
// involved on Windows. The `directory` value is only used as
|
|
40
|
+
// cwd; it is never interpolated into a command string.
|
|
41
|
+
const installProcess = (0, safe_spawn_1.safeSpawn)(packageManager, args, {
|
|
27
42
|
cwd: directory,
|
|
28
|
-
shell: true,
|
|
29
43
|
timeout: 600000,
|
|
30
44
|
});
|
|
31
|
-
|
|
32
|
-
let
|
|
45
|
+
let progress = 50;
|
|
46
|
+
let lastProgressUpdate = Date.now();
|
|
33
47
|
const interval = setInterval(() => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
if (progress < 88) {
|
|
50
|
+
const increment = progress < 70 ? 3 : 1;
|
|
51
|
+
progress = Math.min(progress + increment, 88);
|
|
52
|
+
progressBar.update(progress, {
|
|
53
|
+
doing: "Installing dependencies...",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else if (now - lastProgressUpdate > 3000) {
|
|
57
|
+
progressBar.update(progress, {
|
|
58
|
+
doing: "Installing dependencies...",
|
|
59
|
+
});
|
|
37
60
|
}
|
|
38
61
|
}, 1000);
|
|
39
|
-
//
|
|
62
|
+
// Keep a rolling tail of stderr/stdout so we can surface a
|
|
63
|
+
// meaningful diagnostic when the install exits non-zero (npm's
|
|
64
|
+
// real error is otherwise hidden behind `--silent`).
|
|
65
|
+
const diagnosticBuffer = [];
|
|
66
|
+
const MAX_DIAGNOSTIC_LINES = 20;
|
|
67
|
+
const recordDiagnostic = (chunk) => {
|
|
68
|
+
for (const line of chunk.split(/\r?\n/)) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (!trimmed)
|
|
71
|
+
continue;
|
|
72
|
+
diagnosticBuffer.push(trimmed);
|
|
73
|
+
if (diagnosticBuffer.length > MAX_DIAGNOSTIC_LINES) {
|
|
74
|
+
diagnosticBuffer.shift();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
40
78
|
installProcess.stdout?.on("data", (data) => {
|
|
41
|
-
const output = data.toString()
|
|
42
|
-
|
|
43
|
-
const cleanedOutput = output.replace(/\|\|.*$/g, "");
|
|
44
|
-
// Match and handle npm-specific progress
|
|
79
|
+
const output = data.toString();
|
|
80
|
+
recordDiagnostic(output);
|
|
81
|
+
const cleanedOutput = output.trim().replace(/\|\|.*$/g, "");
|
|
45
82
|
const npmProgressMatch = cleanedOutput.match(/\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/);
|
|
46
83
|
if (npmProgressMatch) {
|
|
47
84
|
const [, current, total, task] = npmProgressMatch;
|
|
48
|
-
|
|
85
|
+
const npmProgress = (parseInt(current) / parseInt(total)) * 100;
|
|
86
|
+
progress = Math.round(50 + npmProgress * 0.4);
|
|
87
|
+
lastProgressUpdate = Date.now();
|
|
49
88
|
progressBar.update(progress, { doing: task });
|
|
50
89
|
}
|
|
51
|
-
else {
|
|
52
|
-
|
|
90
|
+
else if (cleanedOutput) {
|
|
91
|
+
lastProgressUpdate = Date.now();
|
|
53
92
|
progressBar.update(progress, { doing: cleanedOutput });
|
|
54
93
|
}
|
|
55
94
|
});
|
|
56
|
-
|
|
95
|
+
installProcess.stderr?.on("data", (data) => {
|
|
96
|
+
const output = data.toString();
|
|
97
|
+
recordDiagnostic(output);
|
|
98
|
+
const cleanedOutput = output.trim().replace(/\|\|.*$/g, "");
|
|
99
|
+
const npmProgressMatch = cleanedOutput.match(/\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/);
|
|
100
|
+
if (npmProgressMatch) {
|
|
101
|
+
const [, current, total, task] = npmProgressMatch;
|
|
102
|
+
const npmProgress = (parseInt(current) / parseInt(total)) * 100;
|
|
103
|
+
progress = Math.round(50 + npmProgress * 0.4);
|
|
104
|
+
lastProgressUpdate = Date.now();
|
|
105
|
+
progressBar.update(progress, { doing: task });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
57
108
|
installProcess.on("error", (error) => {
|
|
58
|
-
clearInterval(interval);
|
|
109
|
+
clearInterval(interval);
|
|
59
110
|
progressBar.stop();
|
|
60
111
|
reject(new Error(`Failed to start subprocess: ${error.message}`));
|
|
61
112
|
});
|
|
62
|
-
// Finalize progress on close
|
|
63
113
|
installProcess.on("close", (code) => {
|
|
64
|
-
clearInterval(interval);
|
|
114
|
+
clearInterval(interval);
|
|
65
115
|
if (code === 0) {
|
|
66
|
-
progressBar.update(
|
|
67
|
-
progressBar.stop();
|
|
116
|
+
progressBar.update(90, { doing: "Dependencies installed" });
|
|
68
117
|
resolve("Installation Done!");
|
|
69
118
|
}
|
|
70
119
|
else {
|
|
71
120
|
progressBar.stop();
|
|
121
|
+
if (diagnosticBuffer.length > 0) {
|
|
122
|
+
console.log("\n");
|
|
123
|
+
console.log(chalk_1.default.bold.red(`${packageManager} install failed with exit code ${code}:`));
|
|
124
|
+
for (const line of diagnosticBuffer) {
|
|
125
|
+
console.log(chalk_1.default.gray(` ${line}`));
|
|
126
|
+
}
|
|
127
|
+
console.log("");
|
|
128
|
+
}
|
|
72
129
|
reject(new Error(`${packageManager} install exited with code ${code}`));
|
|
73
130
|
}
|
|
74
131
|
});
|
|
75
132
|
});
|
|
76
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Check if the package manager is installed
|
|
136
|
+
*/
|
|
77
137
|
async function checkIfPackageManagerExists(packageManager) {
|
|
78
|
-
|
|
79
|
-
(0,
|
|
80
|
-
|
|
138
|
+
if (!(0, input_validation_1.isValidPackageManager)(packageManager)) {
|
|
139
|
+
(0, cli_ui_1.printError)("Package manager not found!", packageManager);
|
|
140
|
+
process.exit(1);
|
|
81
141
|
}
|
|
82
|
-
|
|
142
|
+
const result = (0, safe_spawn_1.safeSpawnSync)(packageManager, ["--version"], {
|
|
143
|
+
stdio: "ignore",
|
|
144
|
+
});
|
|
145
|
+
if (result.error || result.status !== 0) {
|
|
83
146
|
(0, cli_ui_1.printError)("Package manager not found!", packageManager);
|
|
84
147
|
process.exit(1);
|
|
85
148
|
}
|
|
149
|
+
return true;
|
|
86
150
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
node_fs_1.default.renameSync(envExamplePath, envPath);
|
|
151
|
+
/**
|
|
152
|
+
* Copy directory recursively (for local template testing)
|
|
153
|
+
*/
|
|
154
|
+
function copyDirectorySync(src, dest) {
|
|
155
|
+
if (!node_fs_1.default.existsSync(dest)) {
|
|
156
|
+
node_fs_1.default.mkdirSync(dest, { recursive: true });
|
|
95
157
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
158
|
+
const entries = node_fs_1.default.readdirSync(src, { withFileTypes: true });
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const srcPath = node_path_1.default.join(src, entry.name);
|
|
161
|
+
const destPath = node_path_1.default.join(dest, entry.name);
|
|
162
|
+
// Skip node_modules and dist directories
|
|
163
|
+
if (entry.name === "node_modules" || entry.name === "dist") {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (entry.isDirectory()) {
|
|
167
|
+
copyDirectorySync(srcPath, destPath);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
node_fs_1.default.copyFileSync(srcPath, destPath);
|
|
171
|
+
}
|
|
99
172
|
}
|
|
100
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Template definitions for v4.0
|
|
176
|
+
*/
|
|
101
177
|
var Template;
|
|
102
178
|
(function (Template) {
|
|
103
|
-
Template["
|
|
104
|
-
Template["
|
|
105
|
-
Template["micro"] = "Micro :: A minimalistic template for building micro
|
|
179
|
+
Template["application"] = "Application :: Full-featured ExpressoTS application. (Recommended)";
|
|
180
|
+
Template["applicationWithEvents"] = "Application with Events :: Application template pre-wired with the type-safe Event Bus example.";
|
|
181
|
+
Template["micro"] = "Micro :: A minimalistic template for building micro APIs and serverless functions.";
|
|
106
182
|
})(Template || (Template = {}));
|
|
183
|
+
/**
|
|
184
|
+
* Middleware presets for Application template
|
|
185
|
+
*/
|
|
186
|
+
var MiddlewarePreset;
|
|
187
|
+
(function (MiddlewarePreset) {
|
|
188
|
+
MiddlewarePreset["api"] = "API :: REST API with security, compression, and auto-logging. (Recommended)";
|
|
189
|
+
MiddlewarePreset["web"] = "Web :: Full web app with cookies and session support.";
|
|
190
|
+
MiddlewarePreset["graphql"] = "GraphQL :: Optimized for GraphQL APIs.";
|
|
191
|
+
MiddlewarePreset["microservice"] = "Microservice :: Minimal setup for microservices.";
|
|
192
|
+
MiddlewarePreset["minimal"] = "Minimal :: Just request parsing, customize everything yourself.";
|
|
193
|
+
})(MiddlewarePreset || (MiddlewarePreset = {}));
|
|
194
|
+
/**
|
|
195
|
+
* Template folder mapping
|
|
196
|
+
*/
|
|
197
|
+
const TEMPLATE_FOLDERS = {
|
|
198
|
+
Application: "application",
|
|
199
|
+
"Application with Events": "application-with-events",
|
|
200
|
+
Micro: "micro",
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Middleware preset mapping to code
|
|
204
|
+
*/
|
|
205
|
+
const PRESET_CODE = {
|
|
206
|
+
API: `this.Middleware.applyPreset("api");`,
|
|
207
|
+
Web: `this.Middleware.applyPreset("web");`,
|
|
208
|
+
GraphQL: [
|
|
209
|
+
`this.Middleware.applyPreset("graphql");`,
|
|
210
|
+
``,
|
|
211
|
+
` const apolloServer = new ApolloServer({ typeDefs, resolvers });`,
|
|
212
|
+
` await apolloServer.start();`,
|
|
213
|
+
` this.Middleware.add({`,
|
|
214
|
+
` path: "/graphql",`,
|
|
215
|
+
` middlewares: [expressMiddleware(apolloServer)],`,
|
|
216
|
+
` });`,
|
|
217
|
+
].join("\n"),
|
|
218
|
+
Microservice: `this.Middleware.applyPreset("microservice");`,
|
|
219
|
+
Minimal: `this.Middleware.parse();`,
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Extra imports that specific presets need appended to app.ts.
|
|
223
|
+
*/
|
|
224
|
+
const PRESET_IMPORTS = {
|
|
225
|
+
GraphQL: [
|
|
226
|
+
`import { ApolloServer } from "@apollo/server";`,
|
|
227
|
+
`import { expressMiddleware } from "@as-integrations/express5";`,
|
|
228
|
+
`import { typeDefs, resolvers } from "./graphql/schema";`,
|
|
229
|
+
].join("\n"),
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Per-preset runtime dependencies. The base application template ships only
|
|
233
|
+
* express + framework packages; each preset declares exactly which optional
|
|
234
|
+
* middleware packages it needs so scaffolded projects stay lean.
|
|
235
|
+
*
|
|
236
|
+
* Versions are pinned with `^` ranges matching the middleware-resolver
|
|
237
|
+
* registry expectations in `@expressots/core`.
|
|
238
|
+
*/
|
|
239
|
+
const PRESET_DEPENDENCIES = {
|
|
240
|
+
API: {
|
|
241
|
+
dependencies: {
|
|
242
|
+
compression: "^1.8.1",
|
|
243
|
+
cors: "^2.8.6",
|
|
244
|
+
"express-rate-limit": "^8.5.1",
|
|
245
|
+
helmet: "^8.1.0",
|
|
246
|
+
},
|
|
247
|
+
devDependencies: {
|
|
248
|
+
"@types/compression": "^1.7.5",
|
|
249
|
+
"@types/cors": "^2.8.17",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
Web: {
|
|
253
|
+
dependencies: {
|
|
254
|
+
compression: "^1.8.1",
|
|
255
|
+
"cookie-parser": "^1.4.7",
|
|
256
|
+
cors: "^2.8.6",
|
|
257
|
+
helmet: "^8.1.0",
|
|
258
|
+
},
|
|
259
|
+
devDependencies: {
|
|
260
|
+
"@types/compression": "^1.7.5",
|
|
261
|
+
"@types/cookie-parser": "^1.4.8",
|
|
262
|
+
"@types/cors": "^2.8.17",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
GraphQL: {
|
|
266
|
+
dependencies: {
|
|
267
|
+
"@apollo/server": "^5.5.1",
|
|
268
|
+
"@as-integrations/express5": "^1.1.2",
|
|
269
|
+
compression: "^1.8.1",
|
|
270
|
+
cors: "^2.8.6",
|
|
271
|
+
graphql: "^16.14.0",
|
|
272
|
+
helmet: "^8.1.0",
|
|
273
|
+
},
|
|
274
|
+
devDependencies: {
|
|
275
|
+
"@types/compression": "^1.7.5",
|
|
276
|
+
"@types/cors": "^2.8.17",
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
Microservice: {
|
|
280
|
+
dependencies: {
|
|
281
|
+
compression: "^1.8.1",
|
|
282
|
+
},
|
|
283
|
+
devDependencies: {
|
|
284
|
+
"@types/compression": "^1.7.5",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
Minimal: {},
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Apply the selected middleware preset to the generated app.ts
|
|
291
|
+
*/
|
|
292
|
+
function applyMiddlewarePreset(directory, preset) {
|
|
293
|
+
const appTsPath = node_path_1.default.join(directory, "src", "app.ts");
|
|
294
|
+
if (!node_fs_1.default.existsSync(appTsPath)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// Extract preset name from selection (e.g., "API :: ..." -> "API")
|
|
298
|
+
const presetMatch = preset.match(/^(\w+) ::/);
|
|
299
|
+
const presetName = presetMatch ? presetMatch[1] : "API";
|
|
300
|
+
const presetCode = PRESET_CODE[presetName] || PRESET_CODE["API"];
|
|
301
|
+
let content = node_fs_1.default.readFileSync(appTsPath, "utf-8");
|
|
302
|
+
// Inject preset-specific imports after the existing import block.
|
|
303
|
+
// Match the first blank line (handles both LF and CRLF endings).
|
|
304
|
+
const extraImports = PRESET_IMPORTS[presetName];
|
|
305
|
+
if (extraImports) {
|
|
306
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
307
|
+
content = content.replace(new RegExp(`${eol}${eol}`), `${eol}${extraImports}${eol}${eol}`);
|
|
308
|
+
}
|
|
309
|
+
// Replace the placeholder with the preset code
|
|
310
|
+
content = content.replace(/\/\/ __MIDDLEWARE_PRESET_PLACEHOLDER__/, presetCode);
|
|
311
|
+
node_fs_1.default.writeFileSync(appTsPath, content, "utf-8");
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* GraphQL schema scaffold content. Provides sample typeDefs and resolvers
|
|
315
|
+
* so the generated project has a working `/graphql` endpoint out of the box.
|
|
316
|
+
*/
|
|
317
|
+
const GRAPHQL_SCHEMA_CONTENT = `export const typeDefs = \`#graphql
|
|
318
|
+
type Query {
|
|
319
|
+
hello: String!
|
|
320
|
+
health: HealthStatus!
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
type Mutation {
|
|
324
|
+
echo(message: String!): EchoResponse!
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
type HealthStatus {
|
|
328
|
+
status: String!
|
|
329
|
+
timestamp: String!
|
|
330
|
+
uptime: Float!
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
type EchoResponse {
|
|
334
|
+
message: String!
|
|
335
|
+
receivedAt: String!
|
|
336
|
+
}
|
|
337
|
+
\`;
|
|
338
|
+
|
|
339
|
+
export const resolvers = {
|
|
340
|
+
Query: {
|
|
341
|
+
hello: () => "Hello from ExpressoTS GraphQL!",
|
|
342
|
+
health: () => ({
|
|
343
|
+
status: "ok",
|
|
344
|
+
timestamp: new Date().toISOString(),
|
|
345
|
+
uptime: process.uptime(),
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
Mutation: {
|
|
349
|
+
echo: (_: unknown, { message }: { message: string }) => ({
|
|
350
|
+
message,
|
|
351
|
+
receivedAt: new Date().toISOString(),
|
|
352
|
+
}),
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
`;
|
|
356
|
+
/**
|
|
357
|
+
* Create additional source files required by specific presets.
|
|
358
|
+
* For example, the GraphQL preset ships a starter schema + resolvers.
|
|
359
|
+
*/
|
|
360
|
+
function createPresetFiles(directory, preset) {
|
|
361
|
+
const presetMatch = preset.match(/^(\w+) ::/);
|
|
362
|
+
const presetName = presetMatch ? presetMatch[1] : "API";
|
|
363
|
+
if (presetName === "GraphQL") {
|
|
364
|
+
const graphqlDir = node_path_1.default.join(directory, "src", "graphql");
|
|
365
|
+
if (!node_fs_1.default.existsSync(graphqlDir)) {
|
|
366
|
+
node_fs_1.default.mkdirSync(graphqlDir, { recursive: true });
|
|
367
|
+
}
|
|
368
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(graphqlDir, "schema.ts"), GRAPHQL_SCHEMA_CONTENT, "utf-8");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Inject preset-specific dependencies into the scaffolded project's
|
|
373
|
+
* package.json **before** `npm install` runs, so everything resolves
|
|
374
|
+
* in a single install step.
|
|
375
|
+
*/
|
|
376
|
+
function injectPresetDependencies(directory, preset) {
|
|
377
|
+
const pkgPath = node_path_1.default.join(directory, "package.json");
|
|
378
|
+
if (!node_fs_1.default.existsSync(pkgPath))
|
|
379
|
+
return;
|
|
380
|
+
const presetMatch = preset.match(/^(\w+) ::/);
|
|
381
|
+
const presetName = presetMatch ? presetMatch[1] : "API";
|
|
382
|
+
const presetDeps = PRESET_DEPENDENCIES[presetName];
|
|
383
|
+
if (!presetDeps)
|
|
384
|
+
return;
|
|
385
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf-8"));
|
|
386
|
+
if (presetDeps.dependencies) {
|
|
387
|
+
pkg.dependencies = { ...pkg.dependencies, ...presetDeps.dependencies };
|
|
388
|
+
}
|
|
389
|
+
if (presetDeps.devDependencies) {
|
|
390
|
+
pkg.devDependencies = {
|
|
391
|
+
...pkg.devDependencies,
|
|
392
|
+
...presetDeps.devDependencies,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
node_fs_1.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + "\n", "utf-8");
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Enable local template mode for testing.
|
|
399
|
+
* Opt-in via `EXPRESSOTS_DEV=1` and `EXPRESSOTS_USE_LOCAL_TEMPLATES=1`.
|
|
400
|
+
* Both must be set so a stray env var alone cannot redirect a user's
|
|
401
|
+
* `expressots new` to local files.
|
|
402
|
+
*/
|
|
403
|
+
const USE_LOCAL_TEMPLATES = process.env.EXPRESSOTS_DEV === "1" &&
|
|
404
|
+
process.env.EXPRESSOTS_USE_LOCAL_TEMPLATES === "1";
|
|
405
|
+
/**
|
|
406
|
+
* Skip the package-manager install step. Useful when iterating on
|
|
407
|
+
* templates that depend on unpublished package versions. Same dual
|
|
408
|
+
* env-var guard as `USE_LOCAL_TEMPLATES`.
|
|
409
|
+
*/
|
|
410
|
+
const SKIP_INSTALL_FOR_TESTING = process.env.EXPRESSOTS_DEV === "1" &&
|
|
411
|
+
process.env.EXPRESSOTS_SKIP_INSTALL === "1";
|
|
412
|
+
/**
|
|
413
|
+
* Local templates path (relative to CLI installation)
|
|
414
|
+
* For development: points to the templates folder in the monorepo
|
|
415
|
+
* For production: this will be replaced with the actual path
|
|
416
|
+
*/
|
|
417
|
+
const LOCAL_TEMPLATES_PATH = node_path_1.default.resolve(__dirname, "../../../templates");
|
|
418
|
+
/**
|
|
419
|
+
* Optional override for the templates ref/tag.
|
|
420
|
+
*
|
|
421
|
+
* Useful during the preview window before the matching `vX.Y.Z` tag exists
|
|
422
|
+
* on `expressots/templates`. Setting `EXPRESSOTS_TEMPLATE_REF=feature/v4.0`
|
|
423
|
+
* makes `expressots new` clone from that branch instead of the version tag.
|
|
424
|
+
*
|
|
425
|
+
* This is intentionally NOT gated by `EXPRESSOTS_DEV`: even an installed
|
|
426
|
+
* CLI consumer can opt into a custom ref to test pre-release scaffolds.
|
|
427
|
+
*/
|
|
428
|
+
const TEMPLATE_REF_OVERRIDE = process.env.EXPRESSOTS_TEMPLATE_REF?.trim() || "";
|
|
429
|
+
/**
|
|
430
|
+
* Resolve the degit ref to use when fetching a template.
|
|
431
|
+
*
|
|
432
|
+
* Priority:
|
|
433
|
+
* 1. Explicit override via `EXPRESSOTS_TEMPLATE_REF`
|
|
434
|
+
* 2. The version-pinned tag matching this CLI build (`v${BUNDLE_VERSION}`)
|
|
435
|
+
*/
|
|
436
|
+
function resolveTemplateRef() {
|
|
437
|
+
if (TEMPLATE_REF_OVERRIDE)
|
|
438
|
+
return TEMPLATE_REF_OVERRIDE;
|
|
439
|
+
return `v${cli_1.BUNDLE_VERSION}`;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Build the full degit URL for a given template folder.
|
|
443
|
+
*/
|
|
444
|
+
function buildTemplateRepo(templateFolder, ref) {
|
|
445
|
+
return `expressots/templates/${templateFolder}#${ref}`;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Detect whether the running CLI is a preview build (e.g.
|
|
449
|
+
* `4.0.0-preview.3`). During the preview window the matching templates tag
|
|
450
|
+
* may not yet exist on GitHub, so we allow a soft fallback to the active
|
|
451
|
+
* release branch.
|
|
452
|
+
*/
|
|
453
|
+
function isPreviewBuild() {
|
|
454
|
+
return /-(?:preview|alpha|beta|rc)\b/i.test(cli_1.BUNDLE_VERSION);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Fallback ref used when the primary ref is missing AND we are running a
|
|
458
|
+
* preview build. Matches the framework's working branch.
|
|
459
|
+
*/
|
|
460
|
+
const PREVIEW_FALLBACK_REF = "feature/v4.0";
|
|
461
|
+
/**
|
|
462
|
+
* Clone a template from the public `expressots/templates` repo via degit.
|
|
463
|
+
*
|
|
464
|
+
* On `MISSING_REF` we attempt one graceful retry against the preview
|
|
465
|
+
* fallback branch — this keeps `npx @expressots/cli@next new` usable during
|
|
466
|
+
* the window between a CLI publish and the matching templates-tag push.
|
|
467
|
+
* For non-preview builds we let the error propagate so the user gets a
|
|
468
|
+
* loud, accurate diagnostic.
|
|
469
|
+
*/
|
|
470
|
+
async function cloneFromGitHub({ templateFolder, targetDir, progressBar, }) {
|
|
471
|
+
const primaryRef = resolveTemplateRef();
|
|
472
|
+
const primaryRepo = buildTemplateRepo(templateFolder, primaryRef);
|
|
473
|
+
try {
|
|
474
|
+
await (0, degit_1.default)(primaryRepo, { force: false }).clone(targetDir);
|
|
475
|
+
progressBar.update(30, { doing: "Template ready" });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
const isMissingRef = err?.code === "MISSING_REF";
|
|
480
|
+
const canFallback = isMissingRef && !TEMPLATE_REF_OVERRIDE && isPreviewBuild();
|
|
481
|
+
if (!canFallback)
|
|
482
|
+
throw err;
|
|
483
|
+
// Tag for this preview hasn't been pushed yet; transparently retry
|
|
484
|
+
// against the working branch and surface a one-line warning so the
|
|
485
|
+
// user knows what they actually got. Written to stdout while the bar
|
|
486
|
+
// renders on stderr, so the streams do not interfere.
|
|
487
|
+
console.log(chalk_1.default.yellow(`\n⚠ Templates tag "${primaryRef}" not found on GitHub yet — falling back to "${PREVIEW_FALLBACK_REF}". ` +
|
|
488
|
+
`Set EXPRESSOTS_TEMPLATE_REF=<branch-or-tag> to override.`));
|
|
489
|
+
const fallbackRepo = buildTemplateRepo(templateFolder, PREVIEW_FALLBACK_REF);
|
|
490
|
+
await (0, degit_1.default)(fallbackRepo, { force: false }).clone(targetDir);
|
|
491
|
+
progressBar.update(30, { doing: "Template ready (fallback ref)" });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Main project creation form
|
|
496
|
+
*/
|
|
107
497
|
const projectForm = async (projectName, args) => {
|
|
108
498
|
let answer;
|
|
109
|
-
const [packageManager, template, directory] = args;
|
|
499
|
+
const [packageManager, template, directory, preset, events] = args;
|
|
110
500
|
if (packageManager && template) {
|
|
501
|
+
const resolvedPreset = preset ??
|
|
502
|
+
(template === "application"
|
|
503
|
+
? "api"
|
|
504
|
+
: undefined);
|
|
111
505
|
answer = {
|
|
112
506
|
name: projectName,
|
|
113
507
|
packageManager: packageManager,
|
|
114
508
|
template: Template[template],
|
|
509
|
+
preset: resolvedPreset
|
|
510
|
+
? MiddlewarePreset[resolvedPreset]
|
|
511
|
+
: undefined,
|
|
512
|
+
events: events,
|
|
115
513
|
confirm: true,
|
|
116
514
|
};
|
|
117
515
|
}
|
|
118
516
|
else {
|
|
119
|
-
|
|
517
|
+
const baseAnswers = await inquirer_1.default.prompt([
|
|
120
518
|
{
|
|
121
519
|
type: "input",
|
|
122
520
|
name: "name",
|
|
@@ -130,18 +528,56 @@ const projectForm = async (projectName, args) => {
|
|
|
130
528
|
type: "list",
|
|
131
529
|
name: "packageManager",
|
|
132
530
|
message: "Package manager",
|
|
133
|
-
choices: [
|
|
531
|
+
choices: [
|
|
532
|
+
"npm",
|
|
533
|
+
"yarn",
|
|
534
|
+
"pnpm",
|
|
535
|
+
...(process.platform !== "win32" ? ["bun"] : []),
|
|
536
|
+
],
|
|
134
537
|
},
|
|
135
538
|
{
|
|
136
539
|
type: "list",
|
|
137
540
|
name: "template",
|
|
138
541
|
message: "Select a template",
|
|
139
542
|
choices: [
|
|
140
|
-
`
|
|
141
|
-
"
|
|
142
|
-
"Micro :: A minimalistic template for building micro api's.",
|
|
543
|
+
`Application :: Full-featured ExpressoTS application. (${chalk_1.default.yellow("Recommended")})`,
|
|
544
|
+
"Micro :: A minimalistic template for building micro APIs and serverless functions.",
|
|
143
545
|
],
|
|
144
546
|
},
|
|
547
|
+
]);
|
|
548
|
+
// Only show preset selection for Application template
|
|
549
|
+
let presetAnswer = {};
|
|
550
|
+
let eventsAnswer = {};
|
|
551
|
+
if (baseAnswers.template.startsWith("Application")) {
|
|
552
|
+
presetAnswer = await inquirer_1.default.prompt([
|
|
553
|
+
{
|
|
554
|
+
type: "list",
|
|
555
|
+
name: "preset",
|
|
556
|
+
message: "Select a middleware preset",
|
|
557
|
+
choices: [
|
|
558
|
+
`API :: REST API with security, compression, and auto-logging. (${chalk_1.default.yellow("Recommended")})`,
|
|
559
|
+
"Web :: Full web app with cookies and session support.",
|
|
560
|
+
"GraphQL :: Optimized for GraphQL APIs.",
|
|
561
|
+
"Microservice :: Minimal setup for microservices.",
|
|
562
|
+
"Minimal :: Just request parsing, customize everything yourself.",
|
|
563
|
+
],
|
|
564
|
+
},
|
|
565
|
+
]);
|
|
566
|
+
// Opt-in to the type-safe Event Bus example. Defaults to No
|
|
567
|
+
// so the API/Web/GraphQL/etc. presets stay focused on what
|
|
568
|
+
// the user actually asked for. Picking Yes swaps the
|
|
569
|
+
// scaffold to `application-with-events` (extra event class
|
|
570
|
+
// + handler + `setupEventSystemForExpress` wiring).
|
|
571
|
+
eventsAnswer = await inquirer_1.default.prompt([
|
|
572
|
+
{
|
|
573
|
+
type: "confirm",
|
|
574
|
+
name: "events",
|
|
575
|
+
message: "Include the type-safe Event Bus example? (adds a sample event + handler)",
|
|
576
|
+
default: false,
|
|
577
|
+
},
|
|
578
|
+
]);
|
|
579
|
+
}
|
|
580
|
+
const confirmAnswer = await inquirer_1.default.prompt([
|
|
145
581
|
{
|
|
146
582
|
type: "confirm",
|
|
147
583
|
name: "confirm",
|
|
@@ -149,6 +585,12 @@ const projectForm = async (projectName, args) => {
|
|
|
149
585
|
default: true,
|
|
150
586
|
},
|
|
151
587
|
]);
|
|
588
|
+
answer = {
|
|
589
|
+
...baseAnswers,
|
|
590
|
+
...presetAnswer,
|
|
591
|
+
...eventsAnswer,
|
|
592
|
+
...confirmAnswer,
|
|
593
|
+
};
|
|
152
594
|
}
|
|
153
595
|
if (directory) {
|
|
154
596
|
if (!node_fs_1.default.existsSync(node_path_1.default.join(directory, answer.name))) {
|
|
@@ -159,12 +601,6 @@ const projectForm = async (projectName, args) => {
|
|
|
159
601
|
process.exit(1);
|
|
160
602
|
}
|
|
161
603
|
}
|
|
162
|
-
// Hashmap of templates and their directories
|
|
163
|
-
const templates = {
|
|
164
|
-
NonOpinionated: "non_opinionated",
|
|
165
|
-
Opinionated: "opinionated",
|
|
166
|
-
Micro: "micro",
|
|
167
|
-
};
|
|
168
604
|
if (answer.confirm) {
|
|
169
605
|
// Check if package manager is bun and OS is Windows
|
|
170
606
|
if (answer.packageManager === "bun" && process.platform === "win32") {
|
|
@@ -172,36 +608,130 @@ const projectForm = async (projectName, args) => {
|
|
|
172
608
|
process.exit(1);
|
|
173
609
|
}
|
|
174
610
|
await checkIfPackageManagerExists(answer.packageManager);
|
|
175
|
-
|
|
611
|
+
process.stdout.write(`\n ${chalk_1.default.dim("Creating")} ${chalk_1.default.bold.green(answer.name)}\n\n`);
|
|
612
|
+
const termCols = typeof process.stdout.columns === "number" &&
|
|
613
|
+
process.stdout.columns > 0
|
|
614
|
+
? process.stdout.columns
|
|
615
|
+
: 80;
|
|
616
|
+
const barsize = Math.max(20, Math.min(40, termCols - 22));
|
|
176
617
|
const progressBar = new cli_progress_1.SingleBar({
|
|
177
|
-
format: "
|
|
178
|
-
chalk_1.default.
|
|
179
|
-
"
|
|
618
|
+
format: " {bar} " +
|
|
619
|
+
chalk_1.default.bold("{percentage}") +
|
|
620
|
+
chalk_1.default.dim("%") +
|
|
621
|
+
" " +
|
|
622
|
+
chalk_1.default.dim("{doing}"),
|
|
623
|
+
barCompleteChar: "\u2588",
|
|
624
|
+
barIncompleteChar: "\u2591",
|
|
625
|
+
formatBar: (progress, options) => {
|
|
626
|
+
const completeSize = Math.round(progress * (options.barsize ?? barsize));
|
|
627
|
+
const incompleteSize = (options.barsize ?? barsize) - completeSize;
|
|
628
|
+
return (chalk_1.default.green("\u2588".repeat(completeSize)) +
|
|
629
|
+
chalk_1.default.dim("\u2591".repeat(incompleteSize)));
|
|
630
|
+
},
|
|
631
|
+
barsize,
|
|
180
632
|
hideCursor: true,
|
|
181
|
-
|
|
633
|
+
clearOnComplete: false,
|
|
634
|
+
linewrap: false,
|
|
635
|
+
}, cli_progress_1.Presets.legacy);
|
|
182
636
|
progressBar.start(100, 0, {
|
|
183
|
-
doing: "
|
|
637
|
+
doing: "Fetching template",
|
|
184
638
|
});
|
|
185
|
-
|
|
186
|
-
const
|
|
639
|
+
// Extract template name from selection
|
|
640
|
+
const templateMatch = answer.template.match(/(.*) ::/);
|
|
641
|
+
if (!templateMatch || !templateMatch[1]) {
|
|
642
|
+
progressBar.stop();
|
|
643
|
+
(0, cli_ui_1.printError)(`Could not parse selected template: ${answer.template}`, "new");
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
const templateName = templateMatch[1];
|
|
647
|
+
// The "Application with Events" template is no longer a top-level
|
|
648
|
+
// choice. When the user opts into events on the Application track
|
|
649
|
+
// (or passes `--events`), swap the folder so we still pull from
|
|
650
|
+
// `application-with-events`. The folder split is preserved on disk
|
|
651
|
+
// for now so we keep two minimal sources of truth.
|
|
652
|
+
let templateFolder = TEMPLATE_FOLDERS[templateName];
|
|
653
|
+
if (templateName === "Application" && answer.events) {
|
|
654
|
+
templateFolder = TEMPLATE_FOLDERS["Application with Events"];
|
|
655
|
+
}
|
|
656
|
+
if (!templateFolder) {
|
|
657
|
+
progressBar.stop();
|
|
658
|
+
(0, cli_ui_1.printError)(`Unknown template: ${templateName}`, "new");
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
187
661
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
662
|
+
if (USE_LOCAL_TEMPLATES) {
|
|
663
|
+
// LOCAL TEMPLATE MODE (for testing)
|
|
664
|
+
const localTemplatePath = node_path_1.default.join(LOCAL_TEMPLATES_PATH, templateFolder);
|
|
665
|
+
if (!node_fs_1.default.existsSync(localTemplatePath)) {
|
|
666
|
+
progressBar.stop();
|
|
667
|
+
(0, cli_ui_1.printError)(`Local template not found at: ${localTemplatePath}`, "Please check your templates folder");
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
// Create target directory
|
|
671
|
+
node_fs_1.default.mkdirSync(answer.name, { recursive: true });
|
|
672
|
+
// Copy template files
|
|
673
|
+
copyDirectorySync(localTemplatePath, answer.name);
|
|
674
|
+
progressBar.update(30, { doing: "Template ready" });
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
// GITHUB MODE (production)
|
|
678
|
+
// Pinned to the templates tag matching this CLI's published
|
|
679
|
+
// version (e.g. CLI 4.0.0-preview.3 -> templates/v4.0.0-preview.3).
|
|
680
|
+
// BUNDLE_VERSION reads from this package's package.json, so a
|
|
681
|
+
// CLI release and its templates tag move together.
|
|
682
|
+
await cloneFromGitHub({
|
|
683
|
+
templateFolder,
|
|
684
|
+
targetDir: answer.name,
|
|
685
|
+
progressBar,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
190
688
|
}
|
|
191
689
|
catch (err) {
|
|
192
690
|
console.log("\n");
|
|
193
|
-
|
|
691
|
+
// Surface the real failure cause so users can self-diagnose
|
|
692
|
+
// instead of guessing at "folder not empty" every time.
|
|
693
|
+
const msg = err?.message ? String(err.message) : String(err);
|
|
694
|
+
const code = err?.code ? ` [${err.code}]` : "";
|
|
695
|
+
if (err?.code === "DEST_NOT_EMPTY" ||
|
|
696
|
+
/already exists|not empty/i.test(msg)) {
|
|
697
|
+
(0, cli_ui_1.printError)(`Target folder "${answer.name}" already exists or is not empty`, answer.name);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
(0, cli_ui_1.printError)(`Failed to scaffold project${code}: ${msg}`, answer.name);
|
|
701
|
+
}
|
|
194
702
|
process.exit(1);
|
|
195
703
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
704
|
+
// Apply preset files + placeholder substitution BEFORE install so
|
|
705
|
+
// that a failed/skipped install still leaves the user with a
|
|
706
|
+
// runnable scaffold (the middleware preset placeholder must not
|
|
707
|
+
// leak into src/app.ts as a literal comment).
|
|
708
|
+
if (answer.preset &&
|
|
709
|
+
(templateFolder === "application" ||
|
|
710
|
+
templateFolder === "application-with-events")) {
|
|
711
|
+
injectPresetDependencies(answer.name, answer.preset);
|
|
712
|
+
createPresetFiles(answer.name, answer.preset);
|
|
713
|
+
applyMiddlewarePreset(answer.name, answer.preset);
|
|
714
|
+
}
|
|
715
|
+
if (SKIP_INSTALL_FOR_TESTING) {
|
|
716
|
+
progressBar.update(90, {
|
|
717
|
+
doing: "Skipping install (testing mode)",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
progressBar.update(50, {
|
|
722
|
+
doing: "Installing dependencies",
|
|
723
|
+
});
|
|
724
|
+
await packageManagerInstall({
|
|
725
|
+
packageManager: answer.packageManager,
|
|
726
|
+
directory: answer.name,
|
|
727
|
+
progressBar,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
// Progress should already be at 90% from packageManagerInstall
|
|
731
|
+
// Only update if we skipped installation
|
|
732
|
+
if (!SKIP_INSTALL_FOR_TESTING) {
|
|
733
|
+
progressBar.update(90, { doing: "Finalizing" });
|
|
734
|
+
}
|
|
205
735
|
(0, change_package_info_1.changePackageName)({
|
|
206
736
|
directory: answer.name,
|
|
207
737
|
name: projectName,
|