@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
|
@@ -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
|
+
|
|
@@ -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
|
+
|
|
@@ -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,0 +1,47 @@
|
|
|
1
|
+
import { defineConfig, Env, loadEnvSync } from "@expressots/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* {{className}} Configuration
|
|
5
|
+
*
|
|
6
|
+
* Type-safe configuration with full TypeScript inference.
|
|
7
|
+
* Features:
|
|
8
|
+
* - Multi-environment defaults
|
|
9
|
+
* - Secret management with auto-redaction
|
|
10
|
+
* - Validation with helpful errors
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Load environment files before config resolution
|
|
14
|
+
const envFiles = {
|
|
15
|
+
development: ".env.local",
|
|
16
|
+
production: ".env.prod",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
loadEnvSync({ files: envFiles });
|
|
20
|
+
|
|
21
|
+
export const {{moduleName}}Config = defineConfig({
|
|
22
|
+
// Add your configuration schema here
|
|
23
|
+
enabled: Env.boolean("{{envPrefix}}_ENABLED", {
|
|
24
|
+
default: true,
|
|
25
|
+
description: "Enable/disable {{className}} feature",
|
|
26
|
+
}),
|
|
27
|
+
// Example settings - customize as needed
|
|
28
|
+
setting1: Env.string("{{envPrefix}}_SETTING1", {
|
|
29
|
+
default: "default-value",
|
|
30
|
+
}),
|
|
31
|
+
setting2: Env.number("{{envPrefix}}_SETTING2", {
|
|
32
|
+
default: 100,
|
|
33
|
+
min: 0,
|
|
34
|
+
max: 1000,
|
|
35
|
+
}),
|
|
36
|
+
bootstrap: {
|
|
37
|
+
envFileConfig: {
|
|
38
|
+
autoCreateTemplate: true,
|
|
39
|
+
files: envFiles,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Export typed config values
|
|
45
|
+
export const config = {{moduleName}}Config.values;
|
|
46
|
+
export type {{className}}Config = typeof config;
|
|
47
|
+
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import { provide } from "@expressots/core";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
2
|
|
|
4
3
|
@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 {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{className}} Event
|
|
3
|
+
*
|
|
4
|
+
* Type-safe event - no strings!
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* await this.eventEmitter.emit(new {{className}}Event(data));
|
|
8
|
+
*/
|
|
9
|
+
export class {{className}}Event {
|
|
10
|
+
constructor(
|
|
11
|
+
public readonly data: Record<string, unknown>,
|
|
12
|
+
public readonly timestamp: Date = new Date(),
|
|
13
|
+
) {}
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { provide } from "@expressots/core";
|
|
2
|
+
import { Request, Response, NextFunction } from "express";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {{className}} Guard
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* @controller("/protected")
|
|
9
|
+
* @Use({{className}}Guard)
|
|
10
|
+
* export class ProtectedController { }
|
|
11
|
+
*/
|
|
12
|
+
@provide({{className}}Guard)
|
|
13
|
+
export class {{className}}Guard {
|
|
14
|
+
/**
|
|
15
|
+
* Check if the request is authorized
|
|
16
|
+
*/
|
|
17
|
+
canActivate(req: Request, res: Response, next: NextFunction): void {
|
|
18
|
+
// TODO: Implement authorization logic
|
|
19
|
+
const isAuthorized = this.checkAuthorization(req);
|
|
20
|
+
|
|
21
|
+
if (!isAuthorized) {
|
|
22
|
+
res.status(401).json({ message: "Unauthorized" });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
next();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private checkAuthorization(req: Request): boolean {
|
|
30
|
+
// Example: Check for authorization header
|
|
31
|
+
const authHeader = req.headers.authorization;
|
|
32
|
+
|
|
33
|
+
if (!authHeader) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// TODO: Validate token/credentials
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { provide, OnEvent, IEventHandler } from "@expressots/core";
|
|
2
|
+
import { {{{eventName}}} } from "{{{eventPath}}}";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handler for {{eventName}}
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Auto-discovered
|
|
9
|
+
* - Priority: {{priority}}
|
|
10
|
+
* - Full type safety
|
|
11
|
+
*/
|
|
12
|
+
@provide({{className}}Handler)
|
|
13
|
+
@OnEvent({{eventName}}, { priority: {{priority}} })
|
|
14
|
+
export class {{className}}Handler implements IEventHandler<{{eventName}}> {
|
|
15
|
+
async handle(event: {{eventName}}): Promise<void> {
|
|
16
|
+
console.log(`Handling ${event.constructor.name}`, {
|
|
17
|
+
timestamp: event.timestamp,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// TODO: Implement handler logic
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IInterceptor,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
CallHandler,
|
|
5
|
+
Interceptor,
|
|
6
|
+
provide,
|
|
7
|
+
} from "@expressots/core";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* {{className}} Interceptor
|
|
11
|
+
*
|
|
12
|
+
* Priority: {{priority}} (lower = earlier execution)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* @UseInterceptors({{className}}Interceptor)
|
|
16
|
+
* @controller("/route")
|
|
17
|
+
* export class MyController { }
|
|
18
|
+
*/
|
|
19
|
+
@Interceptor({ priority: {{priority}} })
|
|
20
|
+
@provide({{className}}Interceptor)
|
|
21
|
+
export class {{className}}Interceptor implements IInterceptor {
|
|
22
|
+
async intercept(context: ExecutionContext, next: CallHandler) {
|
|
23
|
+
const request = context.getRequest();
|
|
24
|
+
|
|
25
|
+
// Pre-processing logic
|
|
26
|
+
console.log(`[{{className}}] Before handler`, {
|
|
27
|
+
method: request.method,
|
|
28
|
+
path: request.path,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Execute next interceptor or handler
|
|
35
|
+
const result = await next.handle();
|
|
36
|
+
|
|
37
|
+
// Post-processing logic
|
|
38
|
+
const duration = Date.now() - startTime;
|
|
39
|
+
console.log(`[{{className}}] After handler`, {
|
|
40
|
+
duration: `${duration}ms`,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(`[{{className}}] Handler error`, error);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
@@ -69,9 +69,10 @@ export declare function getFileNameWithoutExtension(filePath: string): string;
|
|
|
69
69
|
* @param schematic
|
|
70
70
|
* @returns the split target
|
|
71
71
|
*/
|
|
72
|
-
export declare const splitTarget: ({ target, schematic, }: {
|
|
72
|
+
export declare const splitTarget: ({ target, schematic, opinionated, }: {
|
|
73
73
|
target: string;
|
|
74
74
|
schematic: string;
|
|
75
|
+
opinionated?: boolean;
|
|
75
76
|
}) => Promise<{
|
|
76
77
|
path: string;
|
|
77
78
|
file: string;
|
|
@@ -99,10 +100,14 @@ export declare const writeTemplate: ({ outputPath, template: { path, data }, }:
|
|
|
99
100
|
};
|
|
100
101
|
}) => void;
|
|
101
102
|
/**
|
|
102
|
-
* Returns the folder where the schematic should be placed
|
|
103
|
-
*
|
|
103
|
+
* Returns the folder where the schematic should be placed.
|
|
104
|
+
* Uses scaffoldSchematics from config if defined, otherwise falls back to defaults.
|
|
105
|
+
*
|
|
106
|
+
* @param schematic - The schematic type (usecase, controller, etc.)
|
|
107
|
+
* @param scaffoldSchematics - Custom folder mappings from expressots.config.ts
|
|
108
|
+
* @returns The folder path for the schematic
|
|
104
109
|
*/
|
|
105
|
-
export declare const schematicFolder: (schematic: string) => string | undefined;
|
|
110
|
+
export declare const schematicFolder: (schematic: string, scaffoldSchematics?: ExpressoConfig["scaffoldSchematics"]) => string | undefined;
|
|
106
111
|
/**
|
|
107
112
|
* Get the name with the scaffold pattern
|
|
108
113
|
* @param name
|
|
@@ -116,7 +121,17 @@ export declare const getNameWithScaffoldPattern: (name: string) => Promise<strin
|
|
|
116
121
|
*/
|
|
117
122
|
export declare function extractFirstWord(file: string): Promise<string>;
|
|
118
123
|
/**
|
|
119
|
-
*
|
|
124
|
+
* Determine the path style for a generate target.
|
|
125
|
+
*
|
|
126
|
+
* - `Nested`: contains an explicit separator (`billing/invoice`) → grouped
|
|
127
|
+
* under the parent folder.
|
|
128
|
+
* - `Sugar`: a single segment that normalizes to more than one word
|
|
129
|
+
* (`userCreate`, `user-create`, `user_create`, `UserCreate`) → grouped under
|
|
130
|
+
* its first word as a shared module (e.g. `UserModule`). camelCase and
|
|
131
|
+
* kebab-case forms of the same name therefore produce identical output.
|
|
132
|
+
* - `Single`: a true single-word target (`user`) → self-contained module in its
|
|
133
|
+
* own folder.
|
|
134
|
+
*
|
|
120
135
|
* @param path
|
|
121
136
|
* @returns the path style
|
|
122
137
|
*/
|
|
@@ -34,6 +34,35 @@ const string_utils_1 = require("./string-utils");
|
|
|
34
34
|
const cli_ui_1 = require("../../utils/cli-ui");
|
|
35
35
|
const verify_file_exists_1 = require("../../utils/verify-file-exists");
|
|
36
36
|
const compiler_1 = __importDefault(require("../../utils/compiler"));
|
|
37
|
+
const update_tsconfig_paths_1 = require("../../utils/update-tsconfig-paths");
|
|
38
|
+
const input_validation_1 = require("../../utils/input-validation");
|
|
39
|
+
const shared_1 = require("@expressots/shared");
|
|
40
|
+
/**
|
|
41
|
+
* Reject generate targets that would resolve outside the project's
|
|
42
|
+
* source root. We only inspect the user-supplied `rawTarget` for
|
|
43
|
+
* absolute-path escape (`/etc/...`, `C:\Windows\...`); the
|
|
44
|
+
* `relativePath` is built internally and may legitimately start with
|
|
45
|
+
* a leading `/` when the schematic doesn't introduce its own folder.
|
|
46
|
+
*
|
|
47
|
+
* Aborts via `process.exit(1)` after `printError` so the caller
|
|
48
|
+
* surfaces a friendly message and writes nothing.
|
|
49
|
+
*/
|
|
50
|
+
function ensureWithinSourceRoot(folderToScaffold, relativePath, rawTarget) {
|
|
51
|
+
if (nodePath.isAbsolute(rawTarget)) {
|
|
52
|
+
(0, cli_ui_1.printError)("Absolute paths are not allowed for generate targets", rawTarget);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
// Strip leading slashes from the synthesized relative path before
|
|
56
|
+
// resolution; the leading slash is a benign artifact of empty
|
|
57
|
+
// `path` segments, not an escape attempt.
|
|
58
|
+
const stripped = relativePath.replace(/^[\\/]+/, "");
|
|
59
|
+
const baseAbs = nodePath.resolve(process.cwd(), folderToScaffold);
|
|
60
|
+
const safe = (0, input_validation_1.safeResolveWithin)(baseAbs, stripped);
|
|
61
|
+
if (safe === null) {
|
|
62
|
+
(0, cli_ui_1.printError)(`Path traversal detected. Targets must stay inside ${folderToScaffold}`, rawTarget);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
37
66
|
/**
|
|
38
67
|
* Create a template based on the schematic
|
|
39
68
|
* @param fp
|
|
@@ -46,15 +75,21 @@ async function validateAndPrepareFile(fp) {
|
|
|
46
75
|
process.exit(1);
|
|
47
76
|
}
|
|
48
77
|
if (opinionated) {
|
|
49
|
-
const folderSchematic = (0, exports.schematicFolder)(fp.schematic);
|
|
78
|
+
const folderSchematic = (0, exports.schematicFolder)(fp.schematic, scaffoldSchematics);
|
|
50
79
|
const folderToScaffold = `${sourceRoot}/${folderSchematic}`;
|
|
51
80
|
const { path, file, className, moduleName, modulePath } = await (0, exports.splitTarget)({
|
|
52
81
|
target: fp.target,
|
|
53
82
|
schematic: fp.schematic,
|
|
83
|
+
opinionated: true,
|
|
54
84
|
});
|
|
85
|
+
ensureWithinSourceRoot(folderToScaffold, `${path}/${file}`, fp.target);
|
|
55
86
|
const outputPath = `${folderToScaffold}/${path}/${file}`;
|
|
56
87
|
await (0, verify_file_exists_1.verifyIfFileExists)(outputPath, fp.schematic);
|
|
57
88
|
(0, node_fs_1.mkdirSync)(`${folderToScaffold}/${path}`, { recursive: true });
|
|
89
|
+
// Update tsconfig paths dynamically (handles both default and custom folder names)
|
|
90
|
+
if (folderSchematic) {
|
|
91
|
+
await (0, update_tsconfig_paths_1.updateTsconfigPaths)(folderSchematic, sourceRoot);
|
|
92
|
+
}
|
|
58
93
|
return {
|
|
59
94
|
path,
|
|
60
95
|
file,
|
|
@@ -77,6 +112,7 @@ async function validateAndPrepareFile(fp) {
|
|
|
77
112
|
const validateFileSchema = fileBaseSchema !== undefined
|
|
78
113
|
? file.replace(fp.schematic, fileBaseSchema)
|
|
79
114
|
: file;
|
|
115
|
+
ensureWithinSourceRoot(folderToScaffold, `${path}/${validateFileSchema}`, fp.target);
|
|
80
116
|
const outputPath = `${folderToScaffold}/${path}/${validateFileSchema}`;
|
|
81
117
|
await (0, verify_file_exists_1.verifyIfFileExists)(outputPath, fp.schematic);
|
|
82
118
|
(0, node_fs_1.mkdirSync)(`${folderToScaffold}/${path}`, { recursive: true });
|
|
@@ -108,7 +144,7 @@ exports.getFileNameWithoutExtension = getFileNameWithoutExtension;
|
|
|
108
144
|
* @param schematic
|
|
109
145
|
* @returns the split target
|
|
110
146
|
*/
|
|
111
|
-
const splitTarget = async ({ target, schematic, }) => {
|
|
147
|
+
const splitTarget = async ({ target, schematic, opinionated = false, }) => {
|
|
112
148
|
const pathContent = target
|
|
113
149
|
.split("/")
|
|
114
150
|
.filter((item) => item !== "");
|
|
@@ -158,28 +194,69 @@ const splitTarget = async ({ target, schematic, }) => {
|
|
|
158
194
|
schematic = "controller";
|
|
159
195
|
// 1. Extract the name (first part of the target)
|
|
160
196
|
const [name, ...remainingPath] = target.split("/");
|
|
161
|
-
// 2. Check if the name is camelCase or kebab-case
|
|
197
|
+
// 2. Check if the name is camelCase or kebab-case (compound word)
|
|
162
198
|
const camelCaseRegex = /[A-Z]/;
|
|
163
199
|
const kebabCaseRegex = /[_\-\s]+/;
|
|
164
200
|
const isCamelCase = camelCaseRegex.test(name);
|
|
165
201
|
const isKebabCase = kebabCaseRegex.test(name);
|
|
202
|
+
// Schematics that should create their own subfolder (grouped resources)
|
|
203
|
+
const groupedSchematics = [
|
|
204
|
+
"usecase",
|
|
205
|
+
"controller",
|
|
206
|
+
"service",
|
|
207
|
+
"dto",
|
|
208
|
+
"module",
|
|
209
|
+
];
|
|
210
|
+
const shouldCreateFolder = groupedSchematics.includes(schematic);
|
|
166
211
|
if (isCamelCase || isKebabCase) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
: []
|
|
212
|
+
// Convert compound name to kebab-case for folder path (e.g., confirmLogin -> confirm-login)
|
|
213
|
+
const folderName = (0, string_utils_1.anyCaseToKebabCase)(name);
|
|
214
|
+
// Extract first word for module name
|
|
215
|
+
const firstWord = name
|
|
216
|
+
.split(isCamelCase ? /(?=[A-Z])/ : kebabCaseRegex)[0]
|
|
217
|
+
.toLowerCase();
|
|
218
|
+
// Opinionated "syntactic sugar": decompose a compound name into a
|
|
219
|
+
// nested feature/use-case layout so every use-case of a feature is
|
|
220
|
+
// grouped under one module at the feature root. For example
|
|
221
|
+
// `userLogin` -> `user/login/login.{controller,usecase,dto}.ts` with
|
|
222
|
+
// the shared module at `user/user.module.ts`. A later `userLogout`
|
|
223
|
+
// adds `user/logout/...` and joins the same `UserModule`.
|
|
224
|
+
//
|
|
225
|
+
// Only applies to grouped schematics in opinionated mode; standalone
|
|
226
|
+
// schematics and non-opinionated mode keep the flat kebab folder so
|
|
227
|
+
// the developer retains full control over structure.
|
|
228
|
+
if (opinionated && shouldCreateFolder) {
|
|
229
|
+
const words = folderName.split("-").filter(Boolean);
|
|
230
|
+
if (words.length > 1) {
|
|
231
|
+
const feature = words[0];
|
|
232
|
+
const useCase = words.slice(1).join("-");
|
|
233
|
+
return {
|
|
234
|
+
path: `${feature}/${useCase}`,
|
|
235
|
+
file: `${await (0, exports.getNameWithScaffoldPattern)(useCase)}.${schematic}.ts`,
|
|
236
|
+
className: (0, string_utils_1.anyCaseToPascalCase)(useCase),
|
|
237
|
+
moduleName: feature,
|
|
238
|
+
modulePath: feature,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// For standalone schematics (entity, provider, middleware, etc.),
|
|
243
|
+
// only create folder if explicit path is provided
|
|
244
|
+
const computedPath = shouldCreateFolder
|
|
245
|
+
? `${folderName}${pathEdgeCase(remainingPath)}`
|
|
246
|
+
: remainingPath.length > 0
|
|
247
|
+
? `${folderName}${pathEdgeCase(remainingPath)}`
|
|
248
|
+
: "";
|
|
172
249
|
return {
|
|
173
|
-
path:
|
|
250
|
+
path: computedPath,
|
|
174
251
|
file: `${await (0, exports.getNameWithScaffoldPattern)(name)}.${schematic}.ts`,
|
|
175
252
|
className: (0, string_utils_1.anyCaseToPascalCase)(name),
|
|
176
|
-
moduleName:
|
|
253
|
+
moduleName: firstWord,
|
|
177
254
|
modulePath: pathContent[0].split("-")[1],
|
|
178
255
|
};
|
|
179
256
|
}
|
|
180
257
|
// 3. Return the base case
|
|
181
258
|
return {
|
|
182
|
-
path: "",
|
|
259
|
+
path: shouldCreateFolder ? name : "",
|
|
183
260
|
file: `${await (0, exports.getNameWithScaffoldPattern)(name)}.${schematic}.ts`,
|
|
184
261
|
className: (0, string_utils_1.anyCaseToPascalCase)(name),
|
|
185
262
|
moduleName: name,
|
|
@@ -219,29 +296,42 @@ const writeTemplate = ({ outputPath, template: { path, data }, }) => {
|
|
|
219
296
|
};
|
|
220
297
|
exports.writeTemplate = writeTemplate;
|
|
221
298
|
/**
|
|
222
|
-
*
|
|
223
|
-
|
|
299
|
+
* Default folder mappings for opinionated scaffolding
|
|
300
|
+
*/
|
|
301
|
+
const DEFAULT_SCHEMATIC_FOLDERS = {
|
|
302
|
+
usecase: "useCases",
|
|
303
|
+
controller: "useCases",
|
|
304
|
+
dto: "useCases",
|
|
305
|
+
service: "useCases",
|
|
306
|
+
provider: "providers",
|
|
307
|
+
entity: "entities",
|
|
308
|
+
middleware: "middleware",
|
|
309
|
+
module: "useCases",
|
|
310
|
+
// NEW v4.0 schematics
|
|
311
|
+
interceptor: "interceptors",
|
|
312
|
+
event: "events",
|
|
313
|
+
handler: "events",
|
|
314
|
+
guard: "guards",
|
|
315
|
+
config: "config",
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Returns the folder where the schematic should be placed.
|
|
319
|
+
* Uses scaffoldSchematics from config if defined, otherwise falls back to defaults.
|
|
320
|
+
*
|
|
321
|
+
* @param schematic - The schematic type (usecase, controller, etc.)
|
|
322
|
+
* @param scaffoldSchematics - Custom folder mappings from expressots.config.ts
|
|
323
|
+
* @returns The folder path for the schematic
|
|
224
324
|
*/
|
|
225
|
-
const schematicFolder = (schematic) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
return "useCases";
|
|
233
|
-
case "service":
|
|
234
|
-
return "useCases";
|
|
235
|
-
case "provider":
|
|
236
|
-
return "providers";
|
|
237
|
-
case "entity":
|
|
238
|
-
return "entities";
|
|
239
|
-
case "middleware":
|
|
240
|
-
return "providers/middlewares";
|
|
241
|
-
case "module":
|
|
242
|
-
return "useCases";
|
|
325
|
+
const schematicFolder = (schematic, scaffoldSchematics) => {
|
|
326
|
+
// Check if custom mapping is defined in config
|
|
327
|
+
if (scaffoldSchematics) {
|
|
328
|
+
const customFolder = scaffoldSchematics[schematic];
|
|
329
|
+
if (customFolder) {
|
|
330
|
+
return customFolder;
|
|
331
|
+
}
|
|
243
332
|
}
|
|
244
|
-
|
|
333
|
+
// Fall back to default mappings
|
|
334
|
+
return DEFAULT_SCHEMATIC_FOLDERS[schematic];
|
|
245
335
|
};
|
|
246
336
|
exports.schematicFolder = schematicFolder;
|
|
247
337
|
/**
|
|
@@ -252,13 +342,13 @@ exports.schematicFolder = schematicFolder;
|
|
|
252
342
|
const getNameWithScaffoldPattern = async (name) => {
|
|
253
343
|
const configObject = await compiler_1.default.loadConfig();
|
|
254
344
|
switch (configObject.scaffoldPattern) {
|
|
255
|
-
case
|
|
345
|
+
case shared_1.Pattern.LOWER_CASE:
|
|
256
346
|
return (0, string_utils_1.anyCaseToLowerCase)(name);
|
|
257
|
-
case
|
|
347
|
+
case shared_1.Pattern.KEBAB_CASE:
|
|
258
348
|
return (0, string_utils_1.anyCaseToKebabCase)(name);
|
|
259
|
-
case
|
|
349
|
+
case shared_1.Pattern.PASCAL_CASE:
|
|
260
350
|
return (0, string_utils_1.anyCaseToPascalCase)(name);
|
|
261
|
-
case
|
|
351
|
+
case shared_1.Pattern.CAMEL_CASE:
|
|
262
352
|
return (0, string_utils_1.anyCaseToCamelCase)(name);
|
|
263
353
|
}
|
|
264
354
|
};
|
|
@@ -282,33 +372,40 @@ async function extractFirstWord(file) {
|
|
|
282
372
|
const firstWord = f.split(regex)[0];
|
|
283
373
|
const config = await compiler_1.default.loadConfig();
|
|
284
374
|
switch (config.scaffoldPattern) {
|
|
285
|
-
case
|
|
375
|
+
case shared_1.Pattern.LOWER_CASE:
|
|
286
376
|
return (0, string_utils_1.anyCaseToLowerCase)(firstWord);
|
|
287
|
-
case
|
|
377
|
+
case shared_1.Pattern.KEBAB_CASE:
|
|
288
378
|
return (0, string_utils_1.anyCaseToKebabCase)(firstWord);
|
|
289
|
-
case
|
|
379
|
+
case shared_1.Pattern.PASCAL_CASE:
|
|
290
380
|
return (0, string_utils_1.anyCaseToPascalCase)(firstWord);
|
|
291
|
-
case
|
|
381
|
+
case shared_1.Pattern.CAMEL_CASE:
|
|
292
382
|
return (0, string_utils_1.anyCaseToCamelCase)(firstWord);
|
|
293
383
|
}
|
|
294
384
|
}
|
|
295
385
|
exports.extractFirstWord = extractFirstWord;
|
|
296
386
|
/**
|
|
297
|
-
*
|
|
387
|
+
* Determine the path style for a generate target.
|
|
388
|
+
*
|
|
389
|
+
* - `Nested`: contains an explicit separator (`billing/invoice`) → grouped
|
|
390
|
+
* under the parent folder.
|
|
391
|
+
* - `Sugar`: a single segment that normalizes to more than one word
|
|
392
|
+
* (`userCreate`, `user-create`, `user_create`, `UserCreate`) → grouped under
|
|
393
|
+
* its first word as a shared module (e.g. `UserModule`). camelCase and
|
|
394
|
+
* kebab-case forms of the same name therefore produce identical output.
|
|
395
|
+
* - `Single`: a true single-word target (`user`) → self-contained module in its
|
|
396
|
+
* own folder.
|
|
397
|
+
*
|
|
298
398
|
* @param path
|
|
299
399
|
* @returns the path style
|
|
300
400
|
*/
|
|
301
401
|
const checkPathStyle = (path) => {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
if (singleOrNestedPathRegex.test(path)) {
|
|
402
|
+
const nestedPathRegex = /\/|\\/;
|
|
403
|
+
if (nestedPathRegex.test(path)) {
|
|
305
404
|
return "nested" /* PathStyle.Nested */;
|
|
306
405
|
}
|
|
307
|
-
|
|
406
|
+
if ((0, string_utils_1.anyCaseToKebabCase)(path).includes("-")) {
|
|
308
407
|
return "sugar" /* PathStyle.Sugar */;
|
|
309
408
|
}
|
|
310
|
-
|
|
311
|
-
return "single" /* PathStyle.Single */;
|
|
312
|
-
}
|
|
409
|
+
return "single" /* PathStyle.Single */;
|
|
313
410
|
};
|
|
314
411
|
exports.checkPathStyle = checkPathStyle;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { ExpressoConfig } from "@expressots/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Additional options for v4.0 schematics
|
|
4
|
+
*/
|
|
5
|
+
type V4Options = {
|
|
6
|
+
event?: string;
|
|
7
|
+
priority?: number;
|
|
8
|
+
};
|
|
2
9
|
/**
|
|
3
10
|
* Process the non-opinionated command
|
|
4
11
|
* @param schematic - The schematic
|
|
5
12
|
* @param target - The target
|
|
6
13
|
* @param method - The method
|
|
7
14
|
* @param expressoConfig - The expresso config
|
|
15
|
+
* @param v4Options - Additional options for v4.0 schematics
|
|
8
16
|
*/
|
|
9
|
-
export declare function nonOpinionatedProcess(schematic: string, target: string, method: string, expressoConfig: ExpressoConfig): Promise<string>;
|
|
17
|
+
export declare function nonOpinionatedProcess(schematic: string, target: string, method: string, expressoConfig: ExpressoConfig, v4Options?: V4Options): Promise<string>;
|
|
18
|
+
export {};
|