Skip to content

Commit 79d525d

Browse files
committed
chore: update version to 4.0.0-beta.1 and introduce new v4.0 schematics including interceptor, event, handler, guard, and config generation
1 parent 3f43061 commit 79d525d

22 files changed

Lines changed: 918 additions & 102 deletions

commitlint.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import type { UserConfig } from "@commitlint/types";
22

33
const Configuration: UserConfig = {
44
extends: ["@commitlint/config-conventional"],
5+
rules: {
6+
"subject-max-length": [0],
7+
"body-max-line-length": [0],
8+
"header-max-length": [0],
9+
},
510
};
611

712
export default Configuration;

src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { scriptsCommand } from "./scripts";
2222
* The current version of the ExpressoTS Bundle.
2323
* core, adapters, and cli.
2424
*/
25-
export const BUNDLE_VERSION = "3.0.0";
25+
export const BUNDLE_VERSION = "4.0.0-beta.1";
2626

2727
stdout.write(`\n${[chalk.bold.green("🐎 Expressots")]}\n\n`);
2828

src/commands/project.commands.ts

Lines changed: 136 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ async function buildDevArgs(
8888
"--signal",
8989
"SIGTERM", // Use SIGTERM for graceful shutdown
9090
"--delay",
91-
"500ms", // Debounce rapid file changes
91+
"1000ms", // Debounce rapid file changes (allow time for port release)
9292
"--watch",
9393
"src",
9494
"--ext",
@@ -203,24 +203,138 @@ const compileTypescript = async () => {
203203
printSuccess("Built successfully", "compile-typescript");
204204
};
205205

206+
/**
207+
* Transform path aliases to relative paths in compiled JavaScript files.
208+
* This runs after TypeScript compilation to ensure production builds work
209+
* without runtime path resolution.
210+
*
211+
* @param outDir - The output directory (e.g., "./dist")
212+
*/
213+
const transformPathAliases = async (outDir: string): Promise<void> => {
214+
const tsconfigPath = join(process.cwd(), "tsconfig.build.json");
215+
216+
if (!existsSync(tsconfigPath)) {
217+
return; // No tsconfig.build.json, skip transformation
218+
}
219+
220+
const tsconfig = JSON.parse(readFileSync(tsconfigPath, "utf-8"));
221+
const paths = tsconfig.compilerOptions?.paths;
222+
const baseUrl = tsconfig.compilerOptions?.baseUrl;
223+
224+
if (!paths || !baseUrl) {
225+
return; // No path aliases defined, skip
226+
}
227+
228+
// Build regex patterns for each alias
229+
const aliasPatterns: Array<{
230+
pattern: RegExp;
231+
alias: string;
232+
target: string;
233+
}> = [];
234+
235+
for (const [alias, targets] of Object.entries(paths)) {
236+
if (!Array.isArray(targets) || targets.length === 0) continue;
237+
238+
// Convert @alias/* to regex pattern
239+
// Matches: require("@alias/something") or require('@alias/something')
240+
const aliasBase = alias.replace("/*", "");
241+
const targetBase = (targets[0] as string).replace("/*", "");
242+
243+
// Pattern to match require("@alias/...") or require('@alias/...')
244+
const pattern = new RegExp(
245+
`require\\(["']${aliasBase.replace("@", "\\@")}/([^"']+)["']\\)`,
246+
"g",
247+
);
248+
249+
aliasPatterns.push({
250+
pattern,
251+
alias: aliasBase,
252+
target: targetBase,
253+
});
254+
}
255+
256+
if (aliasPatterns.length === 0) {
257+
return;
258+
}
259+
260+
// Recursively find all .js files in outDir
261+
const findJsFiles = async (dir: string): Promise<Array<string>> => {
262+
const files: Array<string> = [];
263+
const entries = await fs.readdir(dir, { withFileTypes: true });
264+
265+
for (const entry of entries) {
266+
const fullPath = join(dir, entry.name);
267+
if (entry.isDirectory()) {
268+
files.push(...(await findJsFiles(fullPath)));
269+
} else if (entry.name.endsWith(".js")) {
270+
files.push(fullPath);
271+
}
272+
}
273+
274+
return files;
275+
};
276+
277+
const jsFiles = await findJsFiles(outDir);
278+
let transformedCount = 0;
279+
280+
for (const file of jsFiles) {
281+
let content = await fs.readFile(file, "utf-8");
282+
let modified = false;
283+
284+
// Get the directory of the current file relative to outDir
285+
const fileDir = path.dirname(file);
286+
287+
for (const { pattern, alias, target } of aliasPatterns) {
288+
// Calculate the relative path from this file to the target
289+
const targetDir = join(outDir, baseUrl.replace("./", ""), target);
290+
let relativePath = path.relative(fileDir, targetDir);
291+
292+
// Ensure it starts with ./ or ../
293+
if (!relativePath.startsWith(".")) {
294+
relativePath = "./" + relativePath;
295+
}
296+
297+
// Replace Windows backslashes with forward slashes
298+
relativePath = relativePath.replace(/\\/g, "/");
299+
300+
// Replace the alias with the relative path
301+
const newContent = content.replace(pattern, (match, subPath) => {
302+
modified = true;
303+
return `require("${relativePath}/${subPath}")`;
304+
});
305+
306+
if (newContent !== content) {
307+
content = newContent;
308+
}
309+
}
310+
311+
if (modified) {
312+
await fs.writeFile(file, content, "utf-8");
313+
transformedCount++;
314+
}
315+
}
316+
317+
if (transformedCount > 0) {
318+
printSuccess(
319+
`Path aliases resolved in ${transformedCount} files`,
320+
"transform-paths",
321+
);
322+
}
323+
};
324+
206325
/**
207326
* Helper function to copy files to the dist directory
208327
*/
209328
const copyFiles = async (outDir: string) => {
210-
const { opinionated } = await Compiler.loadConfig();
211-
let filesToCopy: Array<string> = [];
212-
if (opinionated) {
213-
filesToCopy = [
214-
"./register-path.js",
215-
"tsconfig.build.json",
216-
"package.json",
217-
];
218-
} else {
219-
filesToCopy = ["tsconfig.json", "package.json"];
329+
// Only copy package.json - path aliases are resolved at build time
330+
// No need for tsconfig files or register-path.js in production
331+
const filesToCopy = ["package.json"];
332+
333+
for (const file of filesToCopy) {
334+
if (existsSync(file)) {
335+
await fs.copyFile(file, join(outDir, path.basename(file)));
336+
}
220337
}
221-
filesToCopy.forEach((file) => {
222-
fs.copyFile(file, join(outDir, path.basename(file)));
223-
});
224338
};
225339

226340
/**
@@ -269,6 +383,10 @@ export const runCommand = async ({
269383
}
270384
await cleanDist(outDir);
271385
await compileTypescript();
386+
// Transform path aliases to relative paths for production
387+
if (opinionated) {
388+
await transformPathAliases(outDir);
389+
}
272390
await copyFiles(outDir);
273391
break;
274392
case "prod": {
@@ -281,12 +399,11 @@ export const runCommand = async ({
281399
}
282400

283401
let config: Array<string> = [];
402+
403+
// ✅ NEW: Simplified - no more register-path.js
404+
// Path resolution is now built-in to @expressots/core
284405
if (opinionated) {
285-
config = [
286-
"-r",
287-
`./${outDir}/register-path.js`,
288-
`./${outDir}/src/${entryPoint}.js`,
289-
];
406+
config = [`./${outDir}/src/${entryPoint}.js`];
290407
} else {
291408
config = [`./${outDir}/${entryPoint}.js`];
292409
}

src/generate/cli.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ const coerceSchematicAliases = (arg: string) => {
2222
return "module";
2323
case "mi":
2424
return "middleware";
25+
// NEW v4.0 schematics
26+
case "i":
27+
return "interceptor";
28+
case "ev":
29+
return "event";
30+
case "h":
31+
return "handler";
32+
case "gu":
33+
return "guard";
34+
case "cfg":
35+
return "config";
2536
default:
2637
return arg;
2738
}
@@ -43,6 +54,12 @@ const generateProject = (): CommandModule<CommandModuleArgs, any> => {
4354
"entity",
4455
"module",
4556
"middleware",
57+
// NEW v4.0 schematics
58+
"interceptor",
59+
"event",
60+
"handler",
61+
"guard",
62+
"config",
4663
] as const,
4764
describe: "The schematic to generate",
4865
type: "string",
@@ -62,10 +79,23 @@ const generateProject = (): CommandModule<CommandModuleArgs, any> => {
6279
alias: "m",
6380
});
6481

82+
// NEW: Options for v4.0 schematics
83+
yargs.option("event", {
84+
describe: "Event class name for handler generation",
85+
type: "string",
86+
});
87+
88+
yargs.option("priority", {
89+
describe:
90+
"Priority for interceptors/handlers (lower = earlier execution)",
91+
type: "number",
92+
default: 10,
93+
});
94+
6595
return yargs;
6696
},
67-
handler: async ({ schematic, path, method }) => {
68-
await createTemplate({ schematic, path, method });
97+
handler: async ({ schematic, path, method, event, priority }) => {
98+
await createTemplate({ schematic, path, method, event, priority });
6999
},
70100
};
71101
};

src/generate/form.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import { opinionatedProcess } from "./utils/opinionated-cmd";
88
* @param schematic
99
* @param path
1010
* @param method
11+
* @param event - Event class name (for handler generation)
12+
* @param priority - Priority for interceptors/handlers
1113
*/
1214
type CreateTemplateProps = {
1315
schematic: string;
1416
path: string;
1517
method: string;
18+
event?: string;
19+
priority?: number;
1620
};
1721

1822
/**
@@ -26,6 +30,8 @@ export const createTemplate = async ({
2630
schematic,
2731
path: target,
2832
method,
33+
event,
34+
priority = 10,
2935
}: CreateTemplateProps) => {
3036
const config = await Compiler.loadConfig();
3137
const pathStyle = checkPathStyle(target);
@@ -38,13 +44,15 @@ export const createTemplate = async ({
3844
method,
3945
config,
4046
pathStyle,
47+
{ event, priority },
4148
);
4249
} else {
4350
returnFile = await nonOpinionatedProcess(
4451
schematic,
4552
target,
4653
method,
4754
config,
55+
{ event, priority },
4856
);
4957
}
5058

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig, Env, loadEnvSync } from "@expressots/core";
2+
3+
loadEnvSync({ files: { development: ".env.local", production: ".env.prod" } });
4+
5+
export const {{moduleName}}Config = defineConfig({
6+
enabled: Env.boolean("{{envPrefix}}_ENABLED", { default: true }),
7+
// Add more config options as needed
8+
});
9+
10+
export const config = {{moduleName}}Config.values;
11+
export type {{className}}Config = typeof config;
12+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* {{className}} Event
3+
*/
4+
export class {{className}}Event {
5+
constructor(
6+
public readonly data: Record<string, unknown>,
7+
public readonly timestamp: Date = new Date(),
8+
) {}
9+
}
10+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { provide } from "@expressots/core";
2+
import { Request, Response, NextFunction } from "express";
3+
4+
@provide({{className}}Guard)
5+
export class {{className}}Guard {
6+
canActivate(req: Request, res: Response, next: NextFunction): void {
7+
const authHeader = req.headers.authorization;
8+
9+
if (!authHeader) {
10+
res.status(401).json({ message: "Unauthorized" });
11+
return;
12+
}
13+
14+
// TODO: Validate token
15+
next();
16+
}
17+
}
18+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { provide, OnEvent, IEventHandler } from "@expressots/core";
2+
import { {{{eventName}}} } from "{{{eventPath}}}";
3+
4+
@provide({{className}}Handler)
5+
@OnEvent({{eventName}}, { priority: {{priority}} })
6+
export class {{className}}Handler implements IEventHandler<{{eventName}}> {
7+
async handle(event: {{eventName}}): Promise<void> {
8+
// TODO: Implement handler logic
9+
console.log(`Handling ${event.constructor.name}`);
10+
}
11+
}
12+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {
2+
IInterceptor,
3+
ExecutionContext,
4+
CallHandler,
5+
Interceptor,
6+
provide,
7+
} from "@expressots/core";
8+
9+
@Interceptor({ priority: {{priority}} })
10+
@provide({{className}}Interceptor)
11+
export class {{className}}Interceptor implements IInterceptor {
12+
async intercept(context: ExecutionContext, next: CallHandler) {
13+
const request = context.getRequest();
14+
15+
// Pre-processing
16+
const startTime = Date.now();
17+
18+
const result = await next.handle();
19+
20+
// Post-processing
21+
const duration = Date.now() - startTime;
22+
console.log(`[{{className}}] ${request.method} ${request.path} - ${duration}ms`);
23+
24+
return result;
25+
}
26+
}
27+

0 commit comments

Comments
 (0)