@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.
Files changed (194) hide show
  1. package/README.md +41 -95
  2. package/bin/cicd/cli.d.ts +6 -0
  3. package/bin/cicd/cli.js +128 -0
  4. package/bin/cicd/form.d.ts +29 -0
  5. package/bin/cicd/form.js +346 -0
  6. package/bin/cicd/generators/azure-devops.d.ts +2 -0
  7. package/bin/cicd/generators/azure-devops.js +370 -0
  8. package/bin/cicd/generators/bitbucket.d.ts +2 -0
  9. package/bin/cicd/generators/bitbucket.js +217 -0
  10. package/bin/cicd/generators/circleci.d.ts +2 -0
  11. package/bin/cicd/generators/circleci.js +274 -0
  12. package/bin/cicd/generators/github-actions.d.ts +14 -0
  13. package/bin/cicd/generators/github-actions.js +426 -0
  14. package/bin/cicd/generators/gitlab-ci.d.ts +2 -0
  15. package/bin/cicd/generators/gitlab-ci.js +237 -0
  16. package/bin/cicd/generators/index.d.ts +6 -0
  17. package/bin/cicd/generators/index.js +15 -0
  18. package/bin/cicd/generators/jenkins.d.ts +2 -0
  19. package/bin/cicd/generators/jenkins.js +248 -0
  20. package/bin/cicd/generators/template-loader.d.ts +17 -0
  21. package/bin/cicd/generators/template-loader.js +128 -0
  22. package/bin/cicd/index.d.ts +1 -0
  23. package/bin/cicd/index.js +5 -0
  24. package/bin/cli.d.ts +1 -5
  25. package/bin/cli.js +72 -7
  26. package/bin/commands/project.commands.d.ts +19 -6
  27. package/bin/commands/project.commands.js +602 -66
  28. package/bin/config/index.d.ts +5 -0
  29. package/bin/config/index.js +10 -0
  30. package/bin/config/manager.d.ts +98 -0
  31. package/bin/config/manager.js +222 -0
  32. package/bin/containerize/analyzers/bootstrap-analyzer.d.ts +46 -0
  33. package/bin/containerize/analyzers/bootstrap-analyzer.js +187 -0
  34. package/bin/containerize/analyzers/project-analyzer.d.ts +20 -0
  35. package/bin/containerize/analyzers/project-analyzer.js +150 -0
  36. package/bin/containerize/cli.d.ts +4 -0
  37. package/bin/containerize/cli.js +113 -0
  38. package/bin/containerize/form.d.ts +15 -0
  39. package/bin/containerize/form.js +152 -0
  40. package/bin/containerize/generators/ci-generator.d.ts +31 -0
  41. package/bin/containerize/generators/ci-generator.js +940 -0
  42. package/bin/containerize/generators/docker-compose-generator.d.ts +8 -0
  43. package/bin/containerize/generators/docker-compose-generator.js +187 -0
  44. package/bin/containerize/generators/dockerfile-generator.d.ts +8 -0
  45. package/bin/containerize/generators/dockerfile-generator.js +657 -0
  46. package/bin/containerize/generators/kubernetes-generator.d.ts +8 -0
  47. package/bin/containerize/generators/kubernetes-generator.js +134 -0
  48. package/bin/containerize/generators/template-loader.d.ts +36 -0
  49. package/bin/containerize/generators/template-loader.js +129 -0
  50. package/bin/containerize/index.d.ts +4 -0
  51. package/bin/containerize/index.js +13 -0
  52. package/bin/containerize/presets/preset-registry.d.ts +20 -0
  53. package/bin/containerize/presets/preset-registry.js +102 -0
  54. package/bin/costs/cli.d.ts +5 -0
  55. package/bin/costs/cli.js +185 -0
  56. package/bin/costs/form.d.ts +44 -0
  57. package/bin/costs/form.js +412 -0
  58. package/bin/costs/index.d.ts +4 -0
  59. package/bin/costs/index.js +25 -0
  60. package/bin/costs/pricing-manager.d.ts +84 -0
  61. package/bin/costs/pricing-manager.js +342 -0
  62. package/bin/costs/providers/index.d.ts +32 -0
  63. package/bin/costs/providers/index.js +153 -0
  64. package/bin/costs/sources/api-source.d.ts +10 -0
  65. package/bin/costs/sources/api-source.js +32 -0
  66. package/bin/costs/sources/index.d.ts +6 -0
  67. package/bin/costs/sources/index.js +15 -0
  68. package/bin/costs/sources/local-json-source.d.ts +23 -0
  69. package/bin/costs/sources/local-json-source.js +59 -0
  70. package/bin/costs/sources/remote-json-source.d.ts +11 -0
  71. package/bin/costs/sources/remote-json-source.js +53 -0
  72. package/bin/costs/types.d.ts +53 -0
  73. package/bin/costs/types.js +5 -0
  74. package/bin/dev/cli.d.ts +4 -0
  75. package/bin/dev/cli.js +136 -0
  76. package/bin/dev/form.d.ts +36 -0
  77. package/bin/dev/form.js +254 -0
  78. package/bin/dev/index.d.ts +1 -0
  79. package/bin/dev/index.js +5 -0
  80. package/bin/generate/cli.d.ts +1 -1
  81. package/bin/generate/cli.js +29 -2
  82. package/bin/generate/form.d.ts +5 -1
  83. package/bin/generate/form.js +3 -3
  84. package/bin/generate/templates/nonopinionated/config.tpl +12 -0
  85. package/bin/generate/templates/nonopinionated/event.tpl +10 -0
  86. package/bin/generate/templates/nonopinionated/guard.tpl +18 -0
  87. package/bin/generate/templates/nonopinionated/handler.tpl +12 -0
  88. package/bin/generate/templates/nonopinionated/interceptor.tpl +27 -0
  89. package/bin/generate/templates/opinionated/config.tpl +47 -0
  90. package/bin/generate/templates/opinionated/entity.tpl +1 -8
  91. package/bin/generate/templates/opinionated/event.tpl +15 -0
  92. package/bin/generate/templates/opinionated/guard.tpl +41 -0
  93. package/bin/generate/templates/opinionated/handler.tpl +23 -0
  94. package/bin/generate/templates/opinionated/interceptor.tpl +50 -0
  95. package/bin/generate/utils/command-utils.d.ts +20 -5
  96. package/bin/generate/utils/command-utils.js +145 -48
  97. package/bin/generate/utils/nonopininated-cmd.d.ts +10 -1
  98. package/bin/generate/utils/nonopininated-cmd.js +100 -1
  99. package/bin/generate/utils/opinionated-cmd.d.ts +10 -1
  100. package/bin/generate/utils/opinionated-cmd.js +128 -16
  101. package/bin/generate/utils/string-utils.d.ts +6 -0
  102. package/bin/generate/utils/string-utils.js +13 -1
  103. package/bin/help/cli.d.ts +1 -1
  104. package/bin/help/command-help-registry.d.ts +23 -0
  105. package/bin/help/command-help-registry.js +303 -0
  106. package/bin/help/command-help.d.ts +36 -0
  107. package/bin/help/command-help.js +56 -0
  108. package/bin/help/form.js +127 -22
  109. package/bin/help/main-help.d.ts +8 -0
  110. package/bin/help/main-help.js +126 -0
  111. package/bin/help/render.d.ts +32 -0
  112. package/bin/help/render.js +46 -0
  113. package/bin/info/cli.d.ts +1 -1
  114. package/bin/info/form.d.ts +1 -1
  115. package/bin/info/form.js +11 -11
  116. package/bin/migrate/analyzers/platform-detector.d.ts +14 -0
  117. package/bin/migrate/analyzers/platform-detector.js +116 -0
  118. package/bin/migrate/cli.d.ts +6 -0
  119. package/bin/migrate/cli.js +98 -0
  120. package/bin/migrate/form.d.ts +25 -0
  121. package/bin/migrate/form.js +348 -0
  122. package/bin/migrate/generators/compose-to-k8s.d.ts +2 -0
  123. package/bin/migrate/generators/compose-to-k8s.js +324 -0
  124. package/bin/migrate/generators/compose-to-railway.d.ts +2 -0
  125. package/bin/migrate/generators/compose-to-railway.js +138 -0
  126. package/bin/migrate/generators/compose-to-render.d.ts +2 -0
  127. package/bin/migrate/generators/compose-to-render.js +148 -0
  128. package/bin/migrate/generators/generic-migration.d.ts +9 -0
  129. package/bin/migrate/generators/generic-migration.js +221 -0
  130. package/bin/migrate/generators/heroku-to-fly.d.ts +2 -0
  131. package/bin/migrate/generators/heroku-to-fly.js +291 -0
  132. package/bin/migrate/generators/heroku-to-railway.d.ts +2 -0
  133. package/bin/migrate/generators/heroku-to-railway.js +283 -0
  134. package/bin/migrate/generators/heroku-to-render.d.ts +2 -0
  135. package/bin/migrate/generators/heroku-to-render.js +148 -0
  136. package/bin/migrate/generators/index.d.ts +7 -0
  137. package/bin/migrate/generators/index.js +17 -0
  138. package/bin/migrate/generators/template-loader.d.ts +21 -0
  139. package/bin/migrate/generators/template-loader.js +59 -0
  140. package/bin/migrate/index.d.ts +1 -0
  141. package/bin/migrate/index.js +5 -0
  142. package/bin/new/cli.d.ts +5 -1
  143. package/bin/new/cli.js +77 -14
  144. package/bin/new/form.d.ts +27 -4
  145. package/bin/new/form.js +605 -75
  146. package/bin/profile/analyzers/dockerfile-analyzer.d.ts +27 -0
  147. package/bin/profile/analyzers/dockerfile-analyzer.js +122 -0
  148. package/bin/profile/analyzers/image-analyzer.d.ts +19 -0
  149. package/bin/profile/analyzers/image-analyzer.js +85 -0
  150. package/bin/profile/cli.d.ts +4 -0
  151. package/bin/profile/cli.js +94 -0
  152. package/bin/profile/form.d.ts +56 -0
  153. package/bin/profile/form.js +401 -0
  154. package/bin/profile/index.d.ts +1 -0
  155. package/bin/profile/index.js +5 -0
  156. package/bin/profile/optimizers/index.d.ts +19 -0
  157. package/bin/profile/optimizers/index.js +137 -0
  158. package/bin/providers/add/form.d.ts +1 -1
  159. package/bin/providers/add/form.js +27 -6
  160. package/bin/providers/create/form.js +53 -3
  161. package/bin/scripts/form.js +27 -5
  162. package/bin/studio/cli.d.ts +15 -0
  163. package/bin/studio/cli.js +172 -0
  164. package/bin/studio/index.d.ts +5 -0
  165. package/bin/studio/index.js +9 -0
  166. package/bin/templates/cache.d.ts +54 -0
  167. package/bin/templates/cache.js +180 -0
  168. package/bin/templates/cli.d.ts +8 -0
  169. package/bin/templates/cli.js +294 -0
  170. package/bin/templates/fetcher.d.ts +49 -0
  171. package/bin/templates/fetcher.js +208 -0
  172. package/bin/templates/index.d.ts +11 -0
  173. package/bin/templates/index.js +37 -0
  174. package/bin/templates/manager.d.ts +116 -0
  175. package/bin/templates/manager.js +323 -0
  176. package/bin/templates/renderer.d.ts +49 -0
  177. package/bin/templates/renderer.js +204 -0
  178. package/bin/templates/types.d.ts +51 -0
  179. package/bin/templates/types.js +5 -0
  180. package/bin/utils/add-module-to-container.d.ts +14 -3
  181. package/bin/utils/add-module-to-container.js +327 -98
  182. package/bin/utils/cli-ui.d.ts +49 -3
  183. package/bin/utils/cli-ui.js +133 -13
  184. package/bin/utils/index.d.ts +4 -0
  185. package/bin/utils/index.js +4 -0
  186. package/bin/utils/input-validation.d.ts +50 -0
  187. package/bin/utils/input-validation.js +143 -0
  188. package/bin/utils/package-manager-commands.d.ts +24 -0
  189. package/bin/utils/package-manager-commands.js +50 -0
  190. package/bin/utils/safe-spawn.d.ts +35 -0
  191. package/bin/utils/safe-spawn.js +51 -0
  192. package/bin/utils/update-tsconfig-paths.d.ts +35 -0
  193. package/bin/utils/update-tsconfig-paths.js +326 -0
  194. 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
- * @param schematic
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
- * Check if the path is a nested path, a single path or a sugar path
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
- const [wordName, ...path] = name
168
- ? name
169
- .split(isCamelCase ? /(?=[A-Z])/ : kebabCaseRegex)
170
- .map((word) => word.toLowerCase())
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: `${wordName}/${pathEdgeCase(path)}${pathEdgeCase(remainingPath)}`,
250
+ path: computedPath,
174
251
  file: `${await (0, exports.getNameWithScaffoldPattern)(name)}.${schematic}.ts`,
175
252
  className: (0, string_utils_1.anyCaseToPascalCase)(name),
176
- moduleName: wordName,
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
- * Returns the folder where the schematic should be placed
223
- * @param schematic
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
- switch (schematic) {
227
- case "usecase":
228
- return "useCases";
229
- case "controller":
230
- return "useCases";
231
- case "dto":
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
- return undefined;
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 "lowercase" /* Pattern.LOWER_CASE */:
345
+ case shared_1.Pattern.LOWER_CASE:
256
346
  return (0, string_utils_1.anyCaseToLowerCase)(name);
257
- case "kebab-case" /* Pattern.KEBAB_CASE */:
347
+ case shared_1.Pattern.KEBAB_CASE:
258
348
  return (0, string_utils_1.anyCaseToKebabCase)(name);
259
- case "PascalCase" /* Pattern.PASCAL_CASE */:
349
+ case shared_1.Pattern.PASCAL_CASE:
260
350
  return (0, string_utils_1.anyCaseToPascalCase)(name);
261
- case "camelCase" /* Pattern.CAMEL_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 "lowercase" /* Pattern.LOWER_CASE */:
375
+ case shared_1.Pattern.LOWER_CASE:
286
376
  return (0, string_utils_1.anyCaseToLowerCase)(firstWord);
287
- case "kebab-case" /* Pattern.KEBAB_CASE */:
377
+ case shared_1.Pattern.KEBAB_CASE:
288
378
  return (0, string_utils_1.anyCaseToKebabCase)(firstWord);
289
- case "PascalCase" /* Pattern.PASCAL_CASE */:
379
+ case shared_1.Pattern.PASCAL_CASE:
290
380
  return (0, string_utils_1.anyCaseToPascalCase)(firstWord);
291
- case "camelCase" /* Pattern.CAMEL_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
- * Check if the path is a nested path, a single path or a sugar path
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 singleOrNestedPathRegex = /\/|\\/;
303
- const sugarPathRegex = /^\w+-\w+$/;
304
- if (singleOrNestedPathRegex.test(path)) {
402
+ const nestedPathRegex = /\/|\\/;
403
+ if (nestedPathRegex.test(path)) {
305
404
  return "nested" /* PathStyle.Nested */;
306
405
  }
307
- else if (sugarPathRegex.test(path)) {
406
+ if ((0, string_utils_1.anyCaseToKebabCase)(path).includes("-")) {
308
407
  return "sugar" /* PathStyle.Sugar */;
309
408
  }
310
- else {
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 {};