@expressots/cli 3.0.0-beta.4 → 4.0.0-preview.2

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 (180) hide show
  1. package/README.md +41 -95
  2. package/bin/cicd/cli.d.ts +6 -0
  3. package/bin/cicd/cli.js +126 -0
  4. package/bin/cicd/form.d.ts +29 -0
  5. package/bin/cicd/form.js +345 -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 -1
  25. package/bin/cli.js +18 -3
  26. package/bin/commands/project.commands.d.ts +19 -6
  27. package/bin/commands/project.commands.js +390 -61
  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 +154 -0
  40. package/bin/containerize/generators/ci-generator.d.ts +31 -0
  41. package/bin/containerize/generators/ci-generator.js +936 -0
  42. package/bin/containerize/generators/docker-compose-generator.d.ts +8 -0
  43. package/bin/containerize/generators/docker-compose-generator.js +186 -0
  44. package/bin/containerize/generators/dockerfile-generator.d.ts +8 -0
  45. package/bin/containerize/generators/dockerfile-generator.js +635 -0
  46. package/bin/containerize/generators/kubernetes-generator.d.ts +8 -0
  47. package/bin/containerize/generators/kubernetes-generator.js +133 -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 +183 -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 +134 -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.js +29 -2
  81. package/bin/generate/form.d.ts +5 -1
  82. package/bin/generate/form.js +3 -3
  83. package/bin/generate/templates/nonopinionated/config.tpl +12 -0
  84. package/bin/generate/templates/nonopinionated/event.tpl +10 -0
  85. package/bin/generate/templates/nonopinionated/guard.tpl +18 -0
  86. package/bin/generate/templates/nonopinionated/handler.tpl +12 -0
  87. package/bin/generate/templates/nonopinionated/interceptor.tpl +27 -0
  88. package/bin/generate/templates/opinionated/config.tpl +47 -0
  89. package/bin/generate/templates/opinionated/entity.tpl +1 -8
  90. package/bin/generate/templates/opinionated/event.tpl +15 -0
  91. package/bin/generate/templates/opinionated/guard.tpl +41 -0
  92. package/bin/generate/templates/opinionated/handler.tpl +23 -0
  93. package/bin/generate/templates/opinionated/interceptor.tpl +50 -0
  94. package/bin/generate/utils/command-utils.d.ts +7 -3
  95. package/bin/generate/utils/command-utils.js +95 -31
  96. package/bin/generate/utils/nonopininated-cmd.d.ts +10 -1
  97. package/bin/generate/utils/nonopininated-cmd.js +100 -1
  98. package/bin/generate/utils/opinionated-cmd.d.ts +10 -1
  99. package/bin/generate/utils/opinionated-cmd.js +112 -7
  100. package/bin/generate/utils/string-utils.d.ts +6 -0
  101. package/bin/generate/utils/string-utils.js +13 -1
  102. package/bin/help/form.js +11 -3
  103. package/bin/migrate/analyzers/platform-detector.d.ts +14 -0
  104. package/bin/migrate/analyzers/platform-detector.js +116 -0
  105. package/bin/migrate/cli.d.ts +6 -0
  106. package/bin/migrate/cli.js +96 -0
  107. package/bin/migrate/form.d.ts +25 -0
  108. package/bin/migrate/form.js +347 -0
  109. package/bin/migrate/generators/compose-to-k8s.d.ts +2 -0
  110. package/bin/migrate/generators/compose-to-k8s.js +324 -0
  111. package/bin/migrate/generators/compose-to-railway.d.ts +2 -0
  112. package/bin/migrate/generators/compose-to-railway.js +138 -0
  113. package/bin/migrate/generators/compose-to-render.d.ts +2 -0
  114. package/bin/migrate/generators/compose-to-render.js +148 -0
  115. package/bin/migrate/generators/generic-migration.d.ts +9 -0
  116. package/bin/migrate/generators/generic-migration.js +221 -0
  117. package/bin/migrate/generators/heroku-to-fly.d.ts +2 -0
  118. package/bin/migrate/generators/heroku-to-fly.js +291 -0
  119. package/bin/migrate/generators/heroku-to-railway.d.ts +2 -0
  120. package/bin/migrate/generators/heroku-to-railway.js +283 -0
  121. package/bin/migrate/generators/heroku-to-render.d.ts +2 -0
  122. package/bin/migrate/generators/heroku-to-render.js +148 -0
  123. package/bin/migrate/generators/index.d.ts +7 -0
  124. package/bin/migrate/generators/index.js +17 -0
  125. package/bin/migrate/generators/template-loader.d.ts +21 -0
  126. package/bin/migrate/generators/template-loader.js +59 -0
  127. package/bin/migrate/index.d.ts +1 -0
  128. package/bin/migrate/index.js +5 -0
  129. package/bin/new/cli.js +21 -6
  130. package/bin/new/form.d.ts +25 -4
  131. package/bin/new/form.js +285 -70
  132. package/bin/profile/analyzers/dockerfile-analyzer.d.ts +27 -0
  133. package/bin/profile/analyzers/dockerfile-analyzer.js +122 -0
  134. package/bin/profile/analyzers/image-analyzer.d.ts +19 -0
  135. package/bin/profile/analyzers/image-analyzer.js +85 -0
  136. package/bin/profile/cli.d.ts +4 -0
  137. package/bin/profile/cli.js +92 -0
  138. package/bin/profile/form.d.ts +56 -0
  139. package/bin/profile/form.js +400 -0
  140. package/bin/profile/index.d.ts +1 -0
  141. package/bin/profile/index.js +5 -0
  142. package/bin/profile/optimizers/index.d.ts +19 -0
  143. package/bin/profile/optimizers/index.js +137 -0
  144. package/bin/providers/add/form.d.ts +1 -1
  145. package/bin/providers/add/form.js +27 -6
  146. package/bin/providers/create/form.js +2 -1
  147. package/bin/scripts/form.js +27 -5
  148. package/bin/studio/cli.d.ts +15 -0
  149. package/bin/studio/cli.js +166 -0
  150. package/bin/studio/index.d.ts +5 -0
  151. package/bin/studio/index.js +9 -0
  152. package/bin/templates/cache.d.ts +54 -0
  153. package/bin/templates/cache.js +180 -0
  154. package/bin/templates/cli.d.ts +8 -0
  155. package/bin/templates/cli.js +292 -0
  156. package/bin/templates/fetcher.d.ts +49 -0
  157. package/bin/templates/fetcher.js +208 -0
  158. package/bin/templates/index.d.ts +11 -0
  159. package/bin/templates/index.js +37 -0
  160. package/bin/templates/manager.d.ts +116 -0
  161. package/bin/templates/manager.js +323 -0
  162. package/bin/templates/renderer.d.ts +49 -0
  163. package/bin/templates/renderer.js +204 -0
  164. package/bin/templates/types.d.ts +51 -0
  165. package/bin/templates/types.js +5 -0
  166. package/bin/utils/add-module-to-container.d.ts +2 -2
  167. package/bin/utils/add-module-to-container.js +15 -5
  168. package/bin/utils/cli-ui.d.ts +30 -3
  169. package/bin/utils/cli-ui.js +95 -13
  170. package/bin/utils/index.d.ts +4 -0
  171. package/bin/utils/index.js +4 -0
  172. package/bin/utils/input-validation.d.ts +50 -0
  173. package/bin/utils/input-validation.js +143 -0
  174. package/bin/utils/package-manager-commands.d.ts +24 -0
  175. package/bin/utils/package-manager-commands.js +50 -0
  176. package/bin/utils/safe-spawn.d.ts +35 -0
  177. package/bin/utils/safe-spawn.js +51 -0
  178. package/bin/utils/update-tsconfig-paths.d.ts +35 -0
  179. package/bin/utils/update-tsconfig-paths.js +286 -0
  180. package/package.json +154 -154
@@ -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
+
@@ -99,10 +99,14 @@ export declare const writeTemplate: ({ outputPath, template: { path, data }, }:
99
99
  };
100
100
  }) => void;
101
101
  /**
102
- * Returns the folder where the schematic should be placed
103
- * @param schematic
102
+ * Returns the folder where the schematic should be placed.
103
+ * Uses scaffoldSchematics from config if defined, otherwise falls back to defaults.
104
+ *
105
+ * @param schematic - The schematic type (usecase, controller, etc.)
106
+ * @param scaffoldSchematics - Custom folder mappings from expressots.config.ts
107
+ * @returns The folder path for the schematic
104
108
  */
105
- export declare const schematicFolder: (schematic: string) => string | undefined;
109
+ export declare const schematicFolder: (schematic: string, scaffoldSchematics?: ExpressoConfig["scaffoldSchematics"]) => string | undefined;
106
110
  /**
107
111
  * Get the name with the scaffold pattern
108
112
  * @param name
@@ -34,6 +34,34 @@ 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
+ /**
40
+ * Reject generate targets that would resolve outside the project's
41
+ * source root. We only inspect the user-supplied `rawTarget` for
42
+ * absolute-path escape (`/etc/...`, `C:\Windows\...`); the
43
+ * `relativePath` is built internally and may legitimately start with
44
+ * a leading `/` when the schematic doesn't introduce its own folder.
45
+ *
46
+ * Aborts via `process.exit(1)` after `printError` so the caller
47
+ * surfaces a friendly message and writes nothing.
48
+ */
49
+ function ensureWithinSourceRoot(folderToScaffold, relativePath, rawTarget) {
50
+ if (nodePath.isAbsolute(rawTarget)) {
51
+ (0, cli_ui_1.printError)("Absolute paths are not allowed for generate targets", rawTarget);
52
+ process.exit(1);
53
+ }
54
+ // Strip leading slashes from the synthesized relative path before
55
+ // resolution; the leading slash is a benign artifact of empty
56
+ // `path` segments, not an escape attempt.
57
+ const stripped = relativePath.replace(/^[\\/]+/, "");
58
+ const baseAbs = nodePath.resolve(process.cwd(), folderToScaffold);
59
+ const safe = (0, input_validation_1.safeResolveWithin)(baseAbs, stripped);
60
+ if (safe === null) {
61
+ (0, cli_ui_1.printError)(`Path traversal detected. Targets must stay inside ${folderToScaffold}`, rawTarget);
62
+ process.exit(1);
63
+ }
64
+ }
37
65
  /**
38
66
  * Create a template based on the schematic
39
67
  * @param fp
@@ -46,15 +74,20 @@ async function validateAndPrepareFile(fp) {
46
74
  process.exit(1);
47
75
  }
48
76
  if (opinionated) {
49
- const folderSchematic = (0, exports.schematicFolder)(fp.schematic);
77
+ const folderSchematic = (0, exports.schematicFolder)(fp.schematic, scaffoldSchematics);
50
78
  const folderToScaffold = `${sourceRoot}/${folderSchematic}`;
51
79
  const { path, file, className, moduleName, modulePath } = await (0, exports.splitTarget)({
52
80
  target: fp.target,
53
81
  schematic: fp.schematic,
54
82
  });
83
+ ensureWithinSourceRoot(folderToScaffold, `${path}/${file}`, fp.target);
55
84
  const outputPath = `${folderToScaffold}/${path}/${file}`;
56
85
  await (0, verify_file_exists_1.verifyIfFileExists)(outputPath, fp.schematic);
57
86
  (0, node_fs_1.mkdirSync)(`${folderToScaffold}/${path}`, { recursive: true });
87
+ // Update tsconfig paths dynamically (handles both default and custom folder names)
88
+ if (folderSchematic) {
89
+ await (0, update_tsconfig_paths_1.updateTsconfigPaths)(folderSchematic, sourceRoot);
90
+ }
58
91
  return {
59
92
  path,
60
93
  file,
@@ -77,6 +110,7 @@ async function validateAndPrepareFile(fp) {
77
110
  const validateFileSchema = fileBaseSchema !== undefined
78
111
  ? file.replace(fp.schematic, fileBaseSchema)
79
112
  : file;
113
+ ensureWithinSourceRoot(folderToScaffold, `${path}/${validateFileSchema}`, fp.target);
80
114
  const outputPath = `${folderToScaffold}/${path}/${validateFileSchema}`;
81
115
  await (0, verify_file_exists_1.verifyIfFileExists)(outputPath, fp.schematic);
82
116
  (0, node_fs_1.mkdirSync)(`${folderToScaffold}/${path}`, { recursive: true });
@@ -158,28 +192,45 @@ const splitTarget = async ({ target, schematic, }) => {
158
192
  schematic = "controller";
159
193
  // 1. Extract the name (first part of the target)
160
194
  const [name, ...remainingPath] = target.split("/");
161
- // 2. Check if the name is camelCase or kebab-case
195
+ // 2. Check if the name is camelCase or kebab-case (compound word)
162
196
  const camelCaseRegex = /[A-Z]/;
163
197
  const kebabCaseRegex = /[_\-\s]+/;
164
198
  const isCamelCase = camelCaseRegex.test(name);
165
199
  const isKebabCase = kebabCaseRegex.test(name);
200
+ // Schematics that should create their own subfolder (grouped resources)
201
+ const groupedSchematics = [
202
+ "usecase",
203
+ "controller",
204
+ "service",
205
+ "dto",
206
+ "module",
207
+ ];
208
+ const shouldCreateFolder = groupedSchematics.includes(schematic);
166
209
  if (isCamelCase || isKebabCase) {
167
- const [wordName, ...path] = name
168
- ? name
169
- .split(isCamelCase ? /(?=[A-Z])/ : kebabCaseRegex)
170
- .map((word) => word.toLowerCase())
171
- : [];
210
+ // Convert compound name to kebab-case for folder path (e.g., confirmLogin -> confirm-login)
211
+ const folderName = (0, string_utils_1.anyCaseToKebabCase)(name);
212
+ // Extract first word for module name
213
+ const firstWord = name
214
+ .split(isCamelCase ? /(?=[A-Z])/ : kebabCaseRegex)[0]
215
+ .toLowerCase();
216
+ // For standalone schematics (entity, provider, middleware, etc.),
217
+ // only create folder if explicit path is provided
218
+ const computedPath = shouldCreateFolder
219
+ ? `${folderName}${pathEdgeCase(remainingPath)}`
220
+ : remainingPath.length > 0
221
+ ? `${folderName}${pathEdgeCase(remainingPath)}`
222
+ : "";
172
223
  return {
173
- path: `${wordName}/${pathEdgeCase(path)}${pathEdgeCase(remainingPath)}`,
224
+ path: computedPath,
174
225
  file: `${await (0, exports.getNameWithScaffoldPattern)(name)}.${schematic}.ts`,
175
226
  className: (0, string_utils_1.anyCaseToPascalCase)(name),
176
- moduleName: wordName,
227
+ moduleName: firstWord,
177
228
  modulePath: pathContent[0].split("-")[1],
178
229
  };
179
230
  }
180
231
  // 3. Return the base case
181
232
  return {
182
- path: "",
233
+ path: shouldCreateFolder ? name : "",
183
234
  file: `${await (0, exports.getNameWithScaffoldPattern)(name)}.${schematic}.ts`,
184
235
  className: (0, string_utils_1.anyCaseToPascalCase)(name),
185
236
  moduleName: name,
@@ -219,29 +270,42 @@ const writeTemplate = ({ outputPath, template: { path, data }, }) => {
219
270
  };
220
271
  exports.writeTemplate = writeTemplate;
221
272
  /**
222
- * Returns the folder where the schematic should be placed
223
- * @param schematic
273
+ * Default folder mappings for opinionated scaffolding
274
+ */
275
+ const DEFAULT_SCHEMATIC_FOLDERS = {
276
+ usecase: "useCases",
277
+ controller: "useCases",
278
+ dto: "useCases",
279
+ service: "useCases",
280
+ provider: "providers",
281
+ entity: "entities",
282
+ middleware: "middleware",
283
+ module: "useCases",
284
+ // NEW v4.0 schematics
285
+ interceptor: "interceptors",
286
+ event: "events",
287
+ handler: "events",
288
+ guard: "guards",
289
+ config: "config",
290
+ };
291
+ /**
292
+ * Returns the folder where the schematic should be placed.
293
+ * Uses scaffoldSchematics from config if defined, otherwise falls back to defaults.
294
+ *
295
+ * @param schematic - The schematic type (usecase, controller, etc.)
296
+ * @param scaffoldSchematics - Custom folder mappings from expressots.config.ts
297
+ * @returns The folder path for the schematic
224
298
  */
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";
299
+ const schematicFolder = (schematic, scaffoldSchematics) => {
300
+ // Check if custom mapping is defined in config
301
+ if (scaffoldSchematics) {
302
+ const customFolder = scaffoldSchematics[schematic];
303
+ if (customFolder) {
304
+ return customFolder;
305
+ }
243
306
  }
244
- return undefined;
307
+ // Fall back to default mappings
308
+ return DEFAULT_SCHEMATIC_FOLDERS[schematic];
245
309
  };
246
310
  exports.schematicFolder = schematicFolder;
247
311
  /**
@@ -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 {};
@@ -10,8 +10,9 @@ const command_utils_1 = require("./command-utils");
10
10
  * @param target - The target
11
11
  * @param method - The method
12
12
  * @param expressoConfig - The expresso config
13
+ * @param v4Options - Additional options for v4.0 schematics
13
14
  */
14
- async function nonOpinionatedProcess(schematic, target, method, expressoConfig) {
15
+ async function nonOpinionatedProcess(schematic, target, method, expressoConfig, v4Options = {}) {
15
16
  let f = await (0, command_utils_1.validateAndPrepareFile)({
16
17
  schematic,
17
18
  target,
@@ -81,6 +82,27 @@ async function nonOpinionatedProcess(schematic, target, method, expressoConfig)
81
82
  await generateModule(f.outputPath, f.className, f.moduleName, f.path, f.schematic);
82
83
  await (0, cli_ui_1.printGenerateSuccess)(f.schematic, f.file);
83
84
  break;
85
+ // NEW v4.0 schematics
86
+ case "interceptor":
87
+ await generateInterceptor(f.outputPath, f.className, v4Options.priority ?? 10);
88
+ await (0, cli_ui_1.printGenerateSuccess)("interceptor", f.file);
89
+ break;
90
+ case "event":
91
+ await generateEvent(f.outputPath, f.className);
92
+ await (0, cli_ui_1.printGenerateSuccess)("event", f.file);
93
+ break;
94
+ case "handler":
95
+ await generateHandler(f.outputPath, f.className, v4Options.event ?? "MyEvent", v4Options.priority ?? 10);
96
+ await (0, cli_ui_1.printGenerateSuccess)("handler", f.file);
97
+ break;
98
+ case "guard":
99
+ await generateGuard(f.outputPath, f.className);
100
+ await (0, cli_ui_1.printGenerateSuccess)("guard", f.file);
101
+ break;
102
+ case "config":
103
+ await generateConfig(f.outputPath, f.className, f.moduleName);
104
+ await (0, cli_ui_1.printGenerateSuccess)("config", f.file);
105
+ break;
84
106
  }
85
107
  return f.file;
86
108
  }
@@ -248,3 +270,80 @@ async function generateModule(outputPath, className, moduleName, path, schematic
248
270
  },
249
271
  });
250
272
  }
273
+ // NEW v4.0 Schematic Generators
274
+ /**
275
+ * Generate an interceptor
276
+ */
277
+ async function generateInterceptor(outputPath, className, priority) {
278
+ (0, command_utils_1.writeTemplate)({
279
+ outputPath,
280
+ template: {
281
+ path: "../templates/nonopinionated/interceptor.tpl",
282
+ data: {
283
+ className: (0, string_utils_1.anyCaseToPascalCase)(className),
284
+ priority: priority.toString(),
285
+ },
286
+ },
287
+ });
288
+ }
289
+ /**
290
+ * Generate an event
291
+ */
292
+ async function generateEvent(outputPath, className) {
293
+ (0, command_utils_1.writeTemplate)({
294
+ outputPath,
295
+ template: {
296
+ path: "../templates/nonopinionated/event.tpl",
297
+ data: {
298
+ className: (0, string_utils_1.anyCaseToPascalCase)(className),
299
+ },
300
+ },
301
+ });
302
+ }
303
+ /**
304
+ * Generate an event handler
305
+ */
306
+ async function generateHandler(outputPath, className, eventName, priority) {
307
+ (0, command_utils_1.writeTemplate)({
308
+ outputPath,
309
+ template: {
310
+ path: "../templates/nonopinionated/handler.tpl",
311
+ data: {
312
+ className: (0, string_utils_1.anyCaseToPascalCase)(className),
313
+ eventName: (0, string_utils_1.anyCaseToPascalCase)(eventName),
314
+ eventPath: `./${(0, string_utils_1.anyCaseToKebabCase)(eventName)}.event`,
315
+ priority: priority.toString(),
316
+ },
317
+ },
318
+ });
319
+ }
320
+ /**
321
+ * Generate a guard
322
+ */
323
+ async function generateGuard(outputPath, className) {
324
+ (0, command_utils_1.writeTemplate)({
325
+ outputPath,
326
+ template: {
327
+ path: "../templates/nonopinionated/guard.tpl",
328
+ data: {
329
+ className: (0, string_utils_1.anyCaseToPascalCase)(className),
330
+ },
331
+ },
332
+ });
333
+ }
334
+ /**
335
+ * Generate a config module
336
+ */
337
+ async function generateConfig(outputPath, className, moduleName) {
338
+ (0, command_utils_1.writeTemplate)({
339
+ outputPath,
340
+ template: {
341
+ path: "../templates/nonopinionated/config.tpl",
342
+ data: {
343
+ className: (0, string_utils_1.anyCaseToPascalCase)(className),
344
+ moduleName: (0, string_utils_1.anyCaseToCamelCase)(moduleName || className),
345
+ envPrefix: (0, string_utils_1.anyCaseToUpperSnakeCase)(className),
346
+ },
347
+ },
348
+ });
349
+ }
@@ -1,4 +1,11 @@
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 commands for opinionated service scaffolding
4
11
  * @param schematic - Resource to scaffold
@@ -6,6 +13,8 @@ import { ExpressoConfig } from "@expressots/shared";
6
13
  * @param method - HTTP method
7
14
  * @param expressoConfig - Expresso configuration [expressots.config.ts]
8
15
  * @param pathStyle - Path command style [sugar, nested, single]
16
+ * @param v4Options - Additional options for v4.0 schematics
9
17
  * @returns
10
18
  */
11
- export declare function opinionatedProcess(schematic: string, target: string, method: string, expressoConfig: ExpressoConfig, pathStyle: string): Promise<string>;
19
+ export declare function opinionatedProcess(schematic: string, target: string, method: string, expressoConfig: ExpressoConfig, pathStyle: string, v4Options?: V4Options): Promise<string>;
20
+ export {};