Skip to content

Commit cc4ef0c

Browse files
committed
feat: enhance CLI and project commands with new utilities and path aliasing support
1 parent 79d525d commit cc4ef0c

File tree

11 files changed

+624
-78
lines changed

11 files changed

+624
-78
lines changed

src/cli.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#!/usr/bin/env node
22

33
import chalk from "chalk";
4-
import { stdout } from "process";
54
import yargs from "yargs";
65
import { hideBin } from "yargs/helpers";
76
import {
@@ -15,7 +14,7 @@ import { infoProject } from "./info";
1514
import { createProject } from "./new";
1615
import { addProviderCMD, removeProviderCMD } from "./providers";
1716
import { createExternalProviderCMD } from "./providers/create/cli";
18-
import { printError } from "./utils/cli-ui";
17+
import { printError, printHeader } from "./utils/cli-ui";
1918
import { scriptsCommand } from "./scripts";
2019

2120
/**
@@ -24,7 +23,7 @@ import { scriptsCommand } from "./scripts";
2423
*/
2524
export const BUNDLE_VERSION = "4.0.0-beta.1";
2625

27-
stdout.write(`\n${[chalk.bold.green("🐎 Expressots")]}\n\n`);
26+
printHeader();
2827

2928
yargs(hideBin(process.argv))
3029
.scriptName("expressots")

src/commands/project.commands.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,13 @@ async function buildTsxArgs(opinionated: boolean): Promise<Array<string>> {
6868
*
6969
* @param opinionated - Whether to use opinionated configuration
7070
* @param verbose - Whether to show verbose nodemon output (for debugging)
71+
* @param clear - Whether to clear console on restart (default: true)
7172
* @returns The nodemon arguments array
7273
*/
7374
async function buildDevArgs(
7475
opinionated: boolean,
7576
verbose: boolean = false,
77+
clear: boolean = true,
7678
): Promise<Array<string>> {
7779
const tsxArgs = await buildTsxArgs(opinionated);
7880

@@ -83,6 +85,14 @@ async function buildDevArgs(
8385
args.push("--quiet");
8486
}
8587

88+
// Build the exec command with optional clear
89+
const clearCmd = clear
90+
? os.platform() === "win32"
91+
? "cls &&"
92+
: "clear &&"
93+
: "";
94+
const execCommand = `${clearCmd} tsx ${tsxArgs.join(" ")}`.trim();
95+
8696
// Core nodemon configuration
8797
args.push(
8898
"--signal",
@@ -98,7 +108,7 @@ async function buildDevArgs(
98108
"--ignore",
99109
"src/**/*.test.ts",
100110
"--exec",
101-
`tsx ${tsxArgs.join(" ")}`,
111+
execCommand,
102112
);
103113

104114
return args;
@@ -109,6 +119,7 @@ async function buildDevArgs(
109119
*/
110120
interface DevCommandOptions {
111121
verbose?: boolean;
122+
clear?: boolean;
112123
}
113124

114125
/**
@@ -126,9 +137,19 @@ export const devCommand: CommandModule<object, DevCommandOptions> = {
126137
default: false,
127138
description: "Show verbose nodemon output for debugging",
128139
},
140+
clear: {
141+
alias: "c",
142+
type: "boolean",
143+
default: true,
144+
description: "Clear console on restart (default: true)",
145+
},
129146
},
130147
handler: async (argv) => {
131-
await runCommand({ command: "dev", verbose: argv.verbose });
148+
await runCommand({
149+
command: "dev",
150+
verbose: argv.verbose,
151+
clear: argv.clear,
152+
});
132153
},
133154
};
134155

@@ -352,6 +373,7 @@ const clearScreen = () => {
352373
interface RunCommandOptions {
353374
command: string;
354375
verbose?: boolean;
376+
clear?: boolean;
355377
}
356378

357379
/**
@@ -361,6 +383,7 @@ interface RunCommandOptions {
361383
export const runCommand = async ({
362384
command,
363385
verbose = false,
386+
clear = true,
364387
}: RunCommandOptions): Promise<void> => {
365388
const { opinionated, entryPoint } = await Compiler.loadConfig();
366389
const outDir = getOutDir();
@@ -370,7 +393,7 @@ export const runCommand = async ({
370393
case "dev":
371394
await execCmd(
372395
"nodemon",
373-
await buildDevArgs(opinionated, verbose),
396+
await buildDevArgs(opinionated, verbose, clear),
374397
);
375398
break;
376399
case "build":
Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
11
import { provide } from "@expressots/core";
2-
import { randomUUID } from "node:crypto";
32

43
@provide({{className}}Entity)
5-
export class {{className}}Entity {
6-
id: string;
7-
8-
constructor() {
9-
this.id = randomUUID();
10-
}
11-
}
4+
export class {{className}}Entity {}

src/generate/utils/command-utils.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { printError } from "../../utils/cli-ui";
1212
import { verifyIfFileExists } from "../../utils/verify-file-exists";
1313
import Compiler from "../../utils/compiler";
14+
import { updateTsconfigPaths } from "../../utils/update-tsconfig-paths";
1415
import { ExpressoConfig, Pattern } from "@expressots/shared";
1516

1617
export const enum PathStyle {
@@ -90,6 +91,11 @@ export async function validateAndPrepareFile(fp: FilePreparation) {
9091
await verifyIfFileExists(outputPath, fp.schematic);
9192
mkdirSync(`${folderToScaffold}/${path}`, { recursive: true });
9293

94+
// Update tsconfig paths dynamically (handles both default and custom folder names)
95+
if (folderSchematic) {
96+
await updateTsconfigPaths(folderSchematic, sourceRoot);
97+
}
98+
9399
return {
94100
path,
95101
file,
@@ -219,34 +225,52 @@ export const splitTarget = async ({
219225
if (schematic === "service") schematic = "controller";
220226
// 1. Extract the name (first part of the target)
221227
const [name, ...remainingPath] = target.split("/");
222-
// 2. Check if the name is camelCase or kebab-case
228+
// 2. Check if the name is camelCase or kebab-case (compound word)
223229
const camelCaseRegex = /[A-Z]/;
224230
const kebabCaseRegex = /[_\-\s]+/;
225231
const isCamelCase = camelCaseRegex.test(name);
226232
const isKebabCase = kebabCaseRegex.test(name);
233+
234+
// Schematics that should create their own subfolder (grouped resources)
235+
const groupedSchematics = [
236+
"usecase",
237+
"controller",
238+
"service",
239+
"dto",
240+
"module",
241+
];
242+
const shouldCreateFolder = groupedSchematics.includes(schematic);
243+
227244
if (isCamelCase || isKebabCase) {
228-
const [wordName, ...path] = name
229-
? name
230-
.split(isCamelCase ? /(?=[A-Z])/ : kebabCaseRegex)
231-
.map((word) => word.toLowerCase())
232-
: [];
245+
// Convert compound name to kebab-case for folder path (e.g., confirmLogin -> confirm-login)
246+
const folderName = anyCaseToKebabCase(name);
247+
// Extract first word for module name
248+
const firstWord = name
249+
.split(isCamelCase ? /(?=[A-Z])/ : kebabCaseRegex)[0]
250+
.toLowerCase();
251+
252+
// For standalone schematics (entity, provider, middleware, etc.),
253+
// only create folder if explicit path is provided
254+
const computedPath = shouldCreateFolder
255+
? `${folderName}${pathEdgeCase(remainingPath)}`
256+
: remainingPath.length > 0
257+
? `${folderName}${pathEdgeCase(remainingPath)}`
258+
: "";
233259

234260
return {
235-
path: `${wordName}/${pathEdgeCase(path)}${pathEdgeCase(
236-
remainingPath,
237-
)}`,
261+
path: computedPath,
238262
file: `${await getNameWithScaffoldPattern(
239263
name,
240264
)}.${schematic}.ts`,
241265
className: anyCaseToPascalCase(name),
242-
moduleName: wordName,
266+
moduleName: firstWord,
243267
modulePath: pathContent[0].split("-")[1],
244268
};
245269
}
246270

247271
// 3. Return the base case
248272
return {
249-
path: "",
273+
path: shouldCreateFolder ? name : "",
250274
file: `${await getNameWithScaffoldPattern(name)}.${schematic}.ts`,
251275
className: anyCaseToPascalCase(name),
252276
moduleName: name,

src/generate/utils/opinionated-cmd.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,10 +559,13 @@ async function generateModuleServiceSugarPath(
559559
},
560560
});
561561

562+
// Extract folder name from folderToScaffold (e.g., "src/useCases" -> "useCases")
563+
const folderName = nodePath.basename(folderToScaffold);
562564
await addModuleToContainer(
563565
anyCaseToPascalCase(moduleName),
564566
`${moduleName}/${file.replace(".ts", "")}`,
565567
path,
568+
folderName,
566569
);
567570
}
568571

@@ -621,10 +624,13 @@ async function generateModuleServiceSinglePath(
621624
},
622625
});
623626

627+
// Extract folder name from folderToScaffold (e.g., "src/useCases" -> "useCases")
628+
const folderName = nodePath.basename(folderToScaffold);
624629
await addModuleToContainer(
625630
anyCaseToPascalCase(moduleName),
626631
`${moduleName}/${file.replace(".ts", "")}`,
627632
path,
633+
folderName,
628634
);
629635
}
630636

@@ -685,9 +691,12 @@ async function generateModuleServiceNestedPath(
685691
},
686692
});
687693

694+
// Extract folder name from folderToScaffold (e.g., "src/useCases" -> "useCases")
695+
const folderName = nodePath.basename(folderToScaffold);
688696
await addModuleToContainerNestedPath(
689697
anyCaseToPascalCase(moduleFileName),
690698
path,
699+
folderName,
691700
);
692701
}
693702

@@ -773,7 +782,7 @@ async function generateHandler(
773782
data: {
774783
className: anyCaseToPascalCase(className),
775784
eventName: anyCaseToPascalCase(eventName),
776-
eventPath: `./${anyCaseToKebabCase(eventName)}.event`,
785+
eventPath: `@events/${anyCaseToKebabCase(eventName)}.event`,
777786
priority: priority.toString(),
778787
},
779788
},

src/new/form.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,27 @@ async function packageManagerInstall({
3838
});
3939

4040
// Simulate incremental progress
41-
let progress = 0;
41+
// Start from 50% (where we left off) and go up to 88% max
42+
// This leaves room for the actual completion and final steps
43+
// On Windows, npm can be slow, so we continue updating to show activity
44+
let progress = 50;
45+
let lastProgressUpdate = Date.now();
4246
const interval = setInterval(() => {
43-
if (progress < 90) {
44-
progress += 5;
45-
progressBar.update(progress);
47+
const now = Date.now();
48+
// If we haven't received real progress updates in a while, continue incrementing
49+
// This prevents the progress bar from appearing stuck on slow Windows systems
50+
if (progress < 88) {
51+
// Increment slower as we approach the limit to avoid hitting it too quickly
52+
const increment = progress < 70 ? 3 : 1;
53+
progress = Math.min(progress + increment, 88);
54+
progressBar.update(progress, {
55+
doing: "Installing dependencies...",
56+
});
57+
} else if (now - lastProgressUpdate > 3000) {
58+
// Even at max, update the "doing" text to show it's still working
59+
progressBar.update(progress, {
60+
doing: "Installing dependencies...",
61+
});
4662
}
4763
}, 1000);
4864

@@ -56,15 +72,35 @@ async function packageManagerInstall({
5672

5773
if (npmProgressMatch) {
5874
const [, current, total, task] = npmProgressMatch;
59-
progress = Math.round(
60-
(parseInt(current) / parseInt(total)) * 100,
61-
);
75+
// Map npm progress (0-100%) to our range (50-90%)
76+
const npmProgress = (parseInt(current) / parseInt(total)) * 100;
77+
progress = Math.round(50 + npmProgress * 0.4); // 50% + (0-100% * 0.4) = 50-90%
78+
lastProgressUpdate = Date.now();
6279
progressBar.update(progress, { doing: task });
63-
} else {
80+
} else if (cleanedOutput) {
81+
lastProgressUpdate = Date.now();
6482
progressBar.update(progress, { doing: cleanedOutput });
6583
}
6684
});
6785

86+
// On Windows, npm may output progress to stderr
87+
installProcess.stderr?.on("data", (data: Buffer) => {
88+
const output = data.toString().trim();
89+
const cleanedOutput = output.replace(/\|\|.*$/g, "");
90+
const npmProgressMatch = cleanedOutput.match(
91+
/\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/,
92+
);
93+
94+
if (npmProgressMatch) {
95+
const [, current, total, task] = npmProgressMatch;
96+
// Map npm progress (0-100%) to our range (50-90%)
97+
const npmProgress = (parseInt(current) / parseInt(total)) * 100;
98+
progress = Math.round(50 + npmProgress * 0.4); // 50% + (0-100% * 0.4) = 50-90%
99+
lastProgressUpdate = Date.now();
100+
progressBar.update(progress, { doing: task });
101+
}
102+
});
103+
68104
installProcess.on("error", (error) => {
69105
clearInterval(interval);
70106
progressBar.stop();
@@ -74,8 +110,8 @@ async function packageManagerInstall({
74110
installProcess.on("close", (code) => {
75111
clearInterval(interval);
76112
if (code === 0) {
77-
progressBar.update(100, { doing: "Complete!" });
78-
progressBar.stop();
113+
// Update to 90% to leave room for final steps (package name change)
114+
progressBar.update(90, { doing: "Dependencies installed" });
79115
resolve("Installation Done!");
80116
} else {
81117
progressBar.stop();
@@ -338,7 +374,11 @@ const projectForm = async (
338374
});
339375
}
340376

341-
progressBar.update(90);
377+
// Progress should already be at 90% from packageManagerInstall
378+
// Only update if we skipped installation
379+
if (!SKIP_INSTALL_FOR_TESTING) {
380+
progressBar.update(90, { doing: "Finalizing project" });
381+
}
342382

343383
changePackageName({
344384
directory: answer.name,

0 commit comments

Comments
 (0)