zudojs-cli 2.1.1 → 2.1.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 (41) hide show
  1. package/README.md +1 -1
  2. package/dist/src/bin/zudojs.js +1 -1
  3. package/dist/src/commands/generate.command.js +0 -14
  4. package/dist/src/constants/index.js +5 -1
  5. package/dist/src/constants/zudojsVersions.generated.js +37 -37
  6. package/dist/src/generators/command/command.generator.d.ts +7 -1
  7. package/dist/src/generators/command/command.generator.js +18 -44
  8. package/dist/src/generators/command/command.template.d.ts +24 -0
  9. package/dist/src/generators/command/command.template.js +78 -0
  10. package/dist/src/generators/command/index.d.ts +6 -0
  11. package/dist/src/generators/command/index.js +6 -0
  12. package/dist/src/generators/index.d.ts +0 -1
  13. package/dist/src/generators/index.js +0 -1
  14. package/dist/src/generators/infrastructure/infrastructure.generator.js +1 -1
  15. package/dist/src/generators/query/index.d.ts +6 -0
  16. package/dist/src/generators/query/index.js +6 -0
  17. package/dist/src/generators/query/query.generator.d.ts +7 -1
  18. package/dist/src/generators/query/query.generator.js +19 -44
  19. package/dist/src/generators/query/query.template.d.ts +14 -0
  20. package/dist/src/generators/query/query.template.js +72 -0
  21. package/dist/src/generators/resource/resource.plan.d.ts +8 -2
  22. package/dist/src/generators/resource/resource.plan.js +10 -1
  23. package/dist/src/recipes/docker/docker.recipe.js +1 -1
  24. package/dist/src/recipes/docker/docker.refresh.js +1 -1
  25. package/dist/src/templates/resource/layers/resourceData.template.d.ts +1 -1
  26. package/dist/src/templates/resource/layers/resourceData.template.js +2 -1
  27. package/dist/src/templates/resource/layers/resourceRoutes.template.d.ts +1 -1
  28. package/dist/src/templates/resource/layers/resourceRoutes.template.js +6 -5
  29. package/dist/src/templates/resource/resource.names.d.ts +6 -0
  30. package/dist/src/templates/resource/resource.names.js +15 -0
  31. package/dist/src/templates/shared/dockerfile/dockerfile.install.d.ts +35 -0
  32. package/dist/src/templates/shared/dockerfile/dockerfile.install.js +89 -0
  33. package/dist/src/templates/shared/{dockerfile.template.d.ts → dockerfile/dockerfile.template.d.ts} +4 -2
  34. package/dist/src/templates/shared/{dockerfile.template.js → dockerfile/dockerfile.template.js} +17 -30
  35. package/dist/src/templates/shared/dockerfile/index.d.ts +6 -0
  36. package/dist/src/templates/shared/dockerfile/index.js +6 -0
  37. package/dist/src/templates/shared/index.d.ts +1 -1
  38. package/dist/src/templates/shared/index.js +1 -1
  39. package/package.json +6 -6
  40. package/dist/src/generators/service/service.generator.d.ts +0 -12
  41. package/dist/src/generators/service/service.generator.js +0 -41
package/README.md CHANGED
@@ -178,7 +178,7 @@ current directory.
178
178
  | Option | Applies to | Description |
179
179
  | ----------------- | ----------------------------- | ------------------------------------------------------------------ |
180
180
  | `--module <name>` | every schematic but `module` | Generates inside `<root>/modules/<name>` instead of the schematic's default directory |
181
- | `--service <name>`| microservice projects, CQRS | Selects the app a schematic belongs to; also names the CQRS service for `command`/`query` |
181
+ | `--service <name>`| microservice projects | Selects the microservice app a schematic (including `command`/`query`) is written into |
182
182
  | `--dry-run` | all | Lists the files without writing anything |
183
183
  | `--force` | all | Overwrites files that already exist |
184
184
 
@@ -178,7 +178,7 @@ app.register(createCommand({
178
178
  {
179
179
  name: "service",
180
180
  short: "s",
181
- description: "The service name (for CQRS commands/queries)",
181
+ description: "Microservice projects: the app to generate into (gateway or a service name). Elsewhere: a folder grouping CQRS commands/queries",
182
182
  type: "string",
183
183
  },
184
184
  {
@@ -6,7 +6,6 @@
6
6
  * backend lives, then places the schematic accordingly.
7
7
  */
8
8
  import { join } from "node:path";
9
- import { generateService } from "../generators/service/service.generator.js";
10
9
  import { generateModule } from "../generators/module/module.generator.js";
11
10
  import { generateCommand } from "../generators/command/command.generator.js";
12
11
  import { generateQuery } from "../generators/query/query.generator.js";
@@ -53,10 +52,6 @@ function getArchitectureRoot(architecture, schematic, serviceName) {
53
52
  }
54
53
  return "src";
55
54
  case "microservice":
56
- if (schematic === "service") {
57
- // A new service is a new app in the workspace.
58
- return "apps/services";
59
- }
60
55
  // `--service <name>` selects which app the schematic belongs to.
61
56
  // Without it the gateway app — the one app always generated — is used.
62
57
  // Modules go to that app's src/modules, next to the scaffolded ones,
@@ -72,13 +67,6 @@ function getArchitectureRoot(architecture, schematic, serviceName) {
72
67
  if (schematic === "module") {
73
68
  return "src/modules";
74
69
  }
75
- if (schematic === "service") {
76
- // The monolith template scaffolds `src/services/app.service.ts` and a
77
- // `src/services/index.ts` barrel. Generating into `src/<name>/`
78
- // instead left two conventions in one project, and the generated
79
- // service was never exported from the barrel the template owns.
80
- return "src/services";
81
- }
82
70
  return "src";
83
71
  }
84
72
  }
@@ -254,8 +242,6 @@ async function runSchematic(schematic, name, options, cwd) {
254
242
  // writing to `<basePath>/commands/<name>`.
255
243
  const cqrsService = options.architecture === "microservice" ? undefined : options.service;
256
244
  switch (schematic) {
257
- case "service":
258
- return await generateService({ name, basePath, dryRun }, cwd);
259
245
  case "module":
260
246
  return await generateModule({ name, feature: true, basePath, dryRun, onRegistered: options.onModuleRegistered }, cwd);
261
247
  case "command":
@@ -133,7 +133,11 @@ export const SCHEMA_CHOICES = Object.freeze([
133
133
  label: "Resource",
134
134
  hint: "DTO, repository, service, controller, CRUD routes and a test, registered",
135
135
  },
136
- { value: "service", label: "Service (CQRS)" },
136
+ {
137
+ value: "service",
138
+ label: "Service",
139
+ hint: "src/services/<name>.service.ts, plus its DTO and repository when missing",
140
+ },
137
141
  { value: "module", label: "Module" },
138
142
  { value: "command", label: "Command" },
139
143
  { value: "query", label: "Query" },
@@ -6,43 +6,43 @@
6
6
  * Do not edit by hand; run `node scripts/generateZudojsVersions.mjs`.
7
7
  */
8
8
  export const ZUDOJS_PACKAGE_VERSIONS = Object.freeze({
9
- "@zudojs/adapters": "1.2.1",
10
- "@zudojs/api": "1.2.1",
11
- "@zudojs/auth": "1.3.1",
12
- "@zudojs/auth-oauth": "1.2.3",
13
- "@zudojs/cache": "1.2.1",
14
- "@zudojs/config": "1.3.1",
15
- "@zudojs/constants": "1.1.2",
16
- "@zudojs/container": "1.2.1",
17
- "@zudojs/core": "1.2.2",
18
- "@zudojs/cqrs": "1.2.1",
19
- "@zudojs/crypto": "1.3.1",
20
- "@zudojs/database": "1.3.1",
21
- "@zudojs/docs": "1.0.4",
22
- "@zudojs/errors": "1.3.0",
23
- "@zudojs/events": "1.3.1",
24
- "@zudojs/feature-flags": "1.4.0",
25
- "@zudojs/http": "1.4.2",
26
- "@zudojs/lifecycle": "1.2.1",
27
- "@zudojs/logger": "1.4.1",
28
- "@zudojs/messaging": "1.2.1",
29
- "@zudojs/middleware": "1.1.0",
30
- "@zudojs/observability": "1.2.1",
31
- "@zudojs/openapi": "1.5.0",
32
- "@zudojs/permissions": "1.4.1",
33
- "@zudojs/plugins": "1.3.1",
34
- "@zudojs/queue": "1.4.1",
35
- "@zudojs/rpc": "1.4.1",
36
- "@zudojs/runtime": "1.3.1",
37
- "@zudojs/scheduler": "1.2.0",
38
- "@zudojs/schema": "1.2.1",
39
- "@zudojs/security": "1.3.1",
40
- "@zudojs/serialization": "1.2.1",
41
- "@zudojs/storage": "1.2.1",
42
- "@zudojs/tenancy": "1.3.1",
43
- "@zudojs/testing": "1.2.2",
44
- "@zudojs/transactions": "1.2.0",
9
+ "@zudojs/adapters": "1.2.2",
10
+ "@zudojs/api": "1.2.2",
11
+ "@zudojs/auth": "1.3.2",
12
+ "@zudojs/auth-oauth": "1.2.4",
13
+ "@zudojs/cache": "1.2.2",
14
+ "@zudojs/config": "1.3.2",
15
+ "@zudojs/constants": "1.1.3",
16
+ "@zudojs/container": "1.2.2",
17
+ "@zudojs/core": "1.2.3",
18
+ "@zudojs/cqrs": "1.2.2",
19
+ "@zudojs/crypto": "1.3.2",
20
+ "@zudojs/database": "1.3.2",
21
+ "@zudojs/docs": "1.0.5",
22
+ "@zudojs/errors": "1.3.1",
23
+ "@zudojs/events": "1.3.2",
24
+ "@zudojs/feature-flags": "1.4.1",
25
+ "@zudojs/http": "1.4.3",
26
+ "@zudojs/lifecycle": "1.2.2",
27
+ "@zudojs/logger": "1.4.2",
28
+ "@zudojs/messaging": "1.2.2",
29
+ "@zudojs/middleware": "1.1.1",
30
+ "@zudojs/observability": "1.2.2",
31
+ "@zudojs/openapi": "1.5.1",
32
+ "@zudojs/permissions": "1.4.2",
33
+ "@zudojs/plugins": "1.3.2",
34
+ "@zudojs/queue": "1.5.0",
35
+ "@zudojs/rpc": "1.4.2",
36
+ "@zudojs/runtime": "1.3.2",
37
+ "@zudojs/scheduler": "1.2.1",
38
+ "@zudojs/schema": "1.2.2",
39
+ "@zudojs/security": "1.3.2",
40
+ "@zudojs/serialization": "1.2.2",
41
+ "@zudojs/storage": "1.2.2",
42
+ "@zudojs/tenancy": "1.3.2",
43
+ "@zudojs/testing": "1.2.3",
44
+ "@zudojs/transactions": "1.2.1",
45
45
  "@zudojs/types": "1.2.0",
46
- "@zudojs/validation": "1.1.0",
46
+ "@zudojs/validation": "1.1.1",
47
47
  });
48
48
  //# sourceMappingURL=zudojsVersions.generated.js.map
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * zudojs-cli — Command Generator
3
3
  *
4
- * Generates a CQRS command with handler.
4
+ * Generates a CQRS command, its handler and a barrel under
5
+ * `<basePath>[/<service>]/commands/<name>/`.
5
6
  */
7
+ /** Options for {@link generateCommand}. */
6
8
  export interface GenerateCommandOptions {
7
9
  readonly name: string;
8
10
  readonly service?: string;
9
11
  readonly basePath?: string;
10
12
  readonly dryRun?: boolean;
11
13
  }
14
+ /**
15
+ * Writes `<name>.command.ts`, `<name>.handler.ts` and `index.ts`, and
16
+ * returns their paths (only the paths on a dry run).
17
+ */
12
18
  export declare function generateCommand(options: GenerateCommandOptions, cwd: string): Promise<string[]>;
13
19
  //# sourceMappingURL=command.generator.d.ts.map
@@ -1,55 +1,29 @@
1
1
  /**
2
2
  * zudojs-cli — Command Generator
3
3
  *
4
- * Generates a CQRS command with handler.
4
+ * Generates a CQRS command, its handler and a barrel under
5
+ * `<basePath>[/<service>]/commands/<name>/`.
5
6
  */
6
7
  import { writeFileTree } from "../../utils/utils.fileSystem.js";
7
8
  import { CLIGenerationError } from "../../errors/index.js";
8
- import { normalizeName } from "../../utils/utils.name.js";
9
+ import { assertGeneratableName } from "../../utils/utils.name.js";
10
+ import { cqrsSchematicNames, renderCommandBarrel, renderCommandFile, renderCommandHandlerFile, } from "./command.template.js";
11
+ /**
12
+ * Writes `<name>.command.ts`, `<name>.handler.ts` and `index.ts`, and
13
+ * returns their paths (only the paths on a dry run).
14
+ */
9
15
  export async function generateCommand(options, cwd) {
10
- const name = normalizeName(options.name);
11
- const nameCamel = name
12
- .replace(/-([a-z])/g, (_m, c) => c.toUpperCase())
13
- .replace(/^./, (c) => c.toUpperCase());
14
- const service = options.service;
16
+ const names = cqrsSchematicNames(assertGeneratableName(options.name, "command name"));
15
17
  const basePath = options.basePath ?? "services";
16
- // When no service grouping is given the schematic is written directly under
17
- // basePath. Callers that resolved the owning app into basePath (the
18
- // microservice layout) pass no service, so the path is not nested twice.
19
- const servicePath = service ? `${basePath}/${service}` : basePath;
18
+ // Without a service group the schematic goes directly under basePath:
19
+ // callers that resolved the owning app into basePath (the microservice
20
+ // layout) pass no service, so the path is not nested twice.
21
+ const servicePath = options.service ? `${basePath}/${options.service}` : basePath;
22
+ const dir = `${servicePath}/commands/${names.slug}`;
20
23
  const files = {
21
- [`${servicePath}/commands/${name}/${name}.command.ts`]: `import type { BaseCommand } from "@zudojs/cqrs";
22
-
23
- export interface ${nameCamel}CommandPayload {
24
- readonly [key: string]: unknown;
25
- }
26
-
27
- export class ${nameCamel}Command implements BaseCommand<${nameCamel}CommandPayload> {
28
- readonly commandName = "${name}";
29
-
30
- constructor(public readonly payload: ${nameCamel}CommandPayload) {}
31
- }
32
- `,
33
- [`${servicePath}/commands/${name}/${name}.handler.ts`]: `import type { CommandHandler, CommandResult } from "@zudojs/cqrs";
34
- import { createLogger } from "@zudojs/logger";
35
- import { ${nameCamel}Command } from "./${name}.command.js";
36
-
37
- export class ${nameCamel}CommandHandler implements CommandHandler<${nameCamel}Command> {
38
- private readonly logger = createLogger({ name: "${name}-handler" });
39
-
40
- async handle(command: ${nameCamel}Command): Promise<CommandResult> {
41
- this.logger.info("Processing ${name} command", { payload: command.payload });
42
-
43
- return {
44
- success: true,
45
- data: { id: crypto.randomUUID(), ...command.payload },
46
- };
47
- }
48
- }
49
- `,
50
- [`${servicePath}/commands/${name}/index.ts`]: `export { ${nameCamel}Command } from "./${name}.command.js";
51
- export { ${nameCamel}CommandHandler } from "./${name}.handler.js";
52
- `,
24
+ [`${dir}/${names.slug}.command.ts`]: renderCommandFile(names),
25
+ [`${dir}/${names.slug}.handler.ts`]: renderCommandHandlerFile(names),
26
+ [`${dir}/index.ts`]: renderCommandBarrel(names),
53
27
  };
54
28
  if (options.dryRun) {
55
29
  return Object.keys(files);
@@ -59,7 +33,7 @@ export { ${nameCamel}CommandHandler } from "./${name}.handler.js";
59
33
  return Object.keys(files);
60
34
  }
61
35
  catch (error) {
62
- throw new CLIGenerationError(`Failed to generate command: ${name} in ${servicePath}`, error);
36
+ throw new CLIGenerationError(`Failed to generate command: ${names.slug} in ${servicePath}`, error);
63
37
  }
64
38
  }
65
39
  //# sourceMappingURL=command.generator.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * zudojs-cli — Command schematic templates, written against the
3
+ * @zudojs/cqrs API: a `CommandOf` type built with `createCommand`, and a
4
+ * `CommandHandler` subclass (`commandType` + `execute`) registered on a
5
+ * `CommandBus`.
6
+ */
7
+ /** Identifiers derived from a command name (`create-user`). */
8
+ export interface CqrsSchematicNames {
9
+ /** kebab-case: folder and file names (`create-user`). */
10
+ readonly slug: string;
11
+ /** PascalCase: class and type prefix, and the bus discriminator (`CreateUser`). */
12
+ readonly pascal: string;
13
+ /** UPPER_SNAKE: prefix of the discriminator constant (`CREATE_USER`). */
14
+ readonly constant: string;
15
+ }
16
+ /** Derives {@link CqrsSchematicNames} from an already validated slug. */
17
+ export declare function cqrsSchematicNames(slug: string): CqrsSchematicNames;
18
+ /** `<slug>.command.ts`: the discriminator, payload, command type and factory. */
19
+ export declare function renderCommandFile(n: CqrsSchematicNames): string;
20
+ /** `<slug>.handler.ts`: the handler class and its bus registration. */
21
+ export declare function renderCommandHandlerFile(n: CqrsSchematicNames): string;
22
+ /** `index.ts`: the command folder's barrel. */
23
+ export declare function renderCommandBarrel(n: CqrsSchematicNames): string;
24
+ //# sourceMappingURL=command.template.d.ts.map
@@ -0,0 +1,78 @@
1
+ /**
2
+ * zudojs-cli — Command schematic templates, written against the
3
+ * @zudojs/cqrs API: a `CommandOf` type built with `createCommand`, and a
4
+ * `CommandHandler` subclass (`commandType` + `execute`) registered on a
5
+ * `CommandBus`.
6
+ */
7
+ import { toPascalCase } from "../../utils/utils.name.js";
8
+ /** Derives {@link CqrsSchematicNames} from an already validated slug. */
9
+ export function cqrsSchematicNames(slug) {
10
+ return { slug, pascal: toPascalCase(slug), constant: slug.toUpperCase().replace(/-/g, "_") };
11
+ }
12
+ /** `<slug>.command.ts`: the discriminator, payload, command type and factory. */
13
+ export function renderCommandFile(n) {
14
+ const id = `${n.constant}_COMMAND`;
15
+ return `import { createCommand, type CommandOf } from "@zudojs/cqrs";
16
+
17
+ /** Discriminator the command bus routes ${n.pascal} commands on. */
18
+ export const ${id} = "${n.pascal}";
19
+
20
+ /** Data a ${n.pascal} command carries. Replace it with the fields the use case needs. */
21
+ export type ${n.pascal}CommandPayload = {
22
+ readonly data: Readonly<Record<string, unknown>>;
23
+ };
24
+
25
+ /** The ${n.pascal} command: \`{ type: "${n.pascal}", data }\`. */
26
+ export type ${n.pascal}Command = CommandOf<typeof ${id}, ${n.pascal}CommandPayload>;
27
+
28
+ /** Creates an immutable ${n.pascal} command. */
29
+ export function create${n.pascal}Command(payload: ${n.pascal}CommandPayload): ${n.pascal}Command {
30
+ return createCommand(${id}, payload);
31
+ }
32
+ `;
33
+ }
34
+ /** `<slug>.handler.ts`: the handler class and its bus registration. */
35
+ export function renderCommandHandlerFile(n) {
36
+ const id = `${n.constant}_COMMAND`;
37
+ return `import { randomUUID } from "node:crypto";
38
+
39
+ import { CommandHandler, type CommandBus } from "@zudojs/cqrs";
40
+
41
+ import { ${id}, type ${n.pascal}Command } from "./${n.slug}.command.js";
42
+
43
+ /** What executing a ${n.pascal} command returns. */
44
+ export interface ${n.pascal}CommandResult {
45
+ readonly id: string;
46
+ readonly data: Readonly<Record<string, unknown>>;
47
+ }
48
+
49
+ /** Handles ${n.pascal} commands: put the write-side logic in \`execute\`. */
50
+ export class ${n.pascal}CommandHandler extends CommandHandler<${n.pascal}Command, ${n.pascal}CommandResult> {
51
+ public override readonly commandType = ${id};
52
+
53
+ public override async execute(command: ${n.pascal}Command): Promise<${n.pascal}CommandResult> {
54
+ return { id: randomUUID(), data: command.data };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Registers {@link ${n.pascal}CommandHandler} on a command bus.
60
+ *
61
+ * @example
62
+ * const bus = register${n.pascal}Command(createCommandBus());
63
+ * const result = await bus.execute<${n.pascal}Command, ${n.pascal}CommandResult>(
64
+ * create${n.pascal}Command({ data: {} }),
65
+ * );
66
+ */
67
+ export function register${n.pascal}Command(bus: CommandBus): CommandBus {
68
+ return bus.register(${id}, new ${n.pascal}CommandHandler());
69
+ }
70
+ `;
71
+ }
72
+ /** `index.ts`: the command folder's barrel. */
73
+ export function renderCommandBarrel(n) {
74
+ return `export * from "./${n.slug}.command.js";
75
+ export * from "./${n.slug}.handler.js";
76
+ `;
77
+ }
78
+ //# sourceMappingURL=command.template.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * zudojs-cli — Command schematic: a `CommandOf` command type, its factory
3
+ * and a `CommandHandler` subclass for @zudojs/cqrs.
4
+ */
5
+ export * from "./command.generator.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * zudojs-cli — Command schematic: a `CommandOf` command type, its factory
3
+ * and a `CommandHandler` subclass for @zudojs/cqrs.
4
+ */
5
+ export * from "./command.generator.js";
6
+ //# sourceMappingURL=index.js.map
@@ -8,7 +8,6 @@ export { FullstackComposer } from "./fullstack/index.js";
8
8
  export { IntegrationGenerator } from "./integration/index.js";
9
9
  export { InfrastructureGenerator } from "./infrastructure/index.js";
10
10
  export { generateModule } from "./module/module.generator.js";
11
- export { generateService } from "./service/service.generator.js";
12
11
  export { generateCommand } from "./command/command.generator.js";
13
12
  export { generateQuery } from "./query/query.generator.js";
14
13
  export { generateResource, resolveResourceLayout, planResource, RESOURCE_SCHEMATICS, } from "./resource/index.js";
@@ -8,7 +8,6 @@ export { FullstackComposer } from "./fullstack/index.js";
8
8
  export { IntegrationGenerator } from "./integration/index.js";
9
9
  export { InfrastructureGenerator } from "./infrastructure/index.js";
10
10
  export { generateModule } from "./module/module.generator.js";
11
- export { generateService } from "./service/service.generator.js";
12
11
  export { generateCommand } from "./command/command.generator.js";
13
12
  export { generateQuery } from "./query/query.generator.js";
14
13
  export { generateResource, resolveResourceLayout, planResource, RESOURCE_SCHEMATICS, } from "./resource/index.js";
@@ -7,7 +7,7 @@ import { writeFileTree } from "../../utils/utils.fileSystem.js";
7
7
  import { CLIValidationError } from "../../errors/index.js";
8
8
  import { resolveDatabaseAdapter } from "../../adapters/databases/databaseAdapter.resolver.js";
9
9
  import { resolveMicroserviceServices } from "../../templates/microservice/microservice.template.js";
10
- import { renderAppPackageDockerfile, renderWorkspaceAppDockerfile, } from "../../templates/shared/dockerfile.template.js";
10
+ import { renderAppPackageDockerfile, renderWorkspaceAppDockerfile, } from "../../templates/shared/dockerfile/index.js";
11
11
  const SERVICE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
12
12
  /**
13
13
  * Database passwords are interpolated by compose from `.env`
@@ -0,0 +1,6 @@
1
+ /**
2
+ * zudojs-cli — Query schematic: a `QueryOf` query type, its factory and a
3
+ * `QueryHandler` subclass for @zudojs/cqrs.
4
+ */
5
+ export * from "./query.generator.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * zudojs-cli — Query schematic: a `QueryOf` query type, its factory and a
3
+ * `QueryHandler` subclass for @zudojs/cqrs.
4
+ */
5
+ export * from "./query.generator.js";
6
+ //# sourceMappingURL=index.js.map
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * zudojs-cli — Query Generator
3
3
  *
4
- * Generates a CQRS query with handler.
4
+ * Generates a CQRS query, its handler and a barrel under
5
+ * `<basePath>[/<service>]/queries/<name>/`.
5
6
  */
7
+ /** Options for {@link generateQuery}. */
6
8
  export interface GenerateQueryOptions {
7
9
  readonly name: string;
8
10
  readonly service?: string;
9
11
  readonly basePath?: string;
10
12
  readonly dryRun?: boolean;
11
13
  }
14
+ /**
15
+ * Writes `<name>.query.ts`, `<name>.handler.ts` and `index.ts`, and returns
16
+ * their paths (only the paths on a dry run).
17
+ */
12
18
  export declare function generateQuery(options: GenerateQueryOptions, cwd: string): Promise<string[]>;
13
19
  //# sourceMappingURL=query.generator.d.ts.map
@@ -1,55 +1,30 @@
1
1
  /**
2
2
  * zudojs-cli — Query Generator
3
3
  *
4
- * Generates a CQRS query with handler.
4
+ * Generates a CQRS query, its handler and a barrel under
5
+ * `<basePath>[/<service>]/queries/<name>/`.
5
6
  */
6
7
  import { writeFileTree } from "../../utils/utils.fileSystem.js";
7
8
  import { CLIGenerationError } from "../../errors/index.js";
8
- import { normalizeName } from "../../utils/utils.name.js";
9
+ import { assertGeneratableName } from "../../utils/utils.name.js";
10
+ import { cqrsSchematicNames } from "../command/command.template.js";
11
+ import { renderQueryBarrel, renderQueryFile, renderQueryHandlerFile, } from "./query.template.js";
12
+ /**
13
+ * Writes `<name>.query.ts`, `<name>.handler.ts` and `index.ts`, and returns
14
+ * their paths (only the paths on a dry run).
15
+ */
9
16
  export async function generateQuery(options, cwd) {
10
- const name = normalizeName(options.name);
11
- const nameCamel = name
12
- .replace(/-([a-z])/g, (_m, c) => c.toUpperCase())
13
- .replace(/^./, (c) => c.toUpperCase());
14
- const service = options.service;
17
+ const names = cqrsSchematicNames(assertGeneratableName(options.name, "query name"));
15
18
  const basePath = options.basePath ?? "services";
16
- // When no service grouping is given the schematic is written directly under
17
- // basePath. Callers that resolved the owning app into basePath (the
18
- // microservice layout) pass no service, so the path is not nested twice.
19
- const servicePath = service ? `${basePath}/${service}` : basePath;
19
+ // Without a service group the schematic goes directly under basePath:
20
+ // callers that resolved the owning app into basePath (the microservice
21
+ // layout) pass no service, so the path is not nested twice.
22
+ const servicePath = options.service ? `${basePath}/${options.service}` : basePath;
23
+ const dir = `${servicePath}/queries/${names.slug}`;
20
24
  const files = {
21
- [`${servicePath}/queries/${name}/${name}.query.ts`]: `import type { BaseQuery } from "@zudojs/cqrs";
22
-
23
- export interface ${nameCamel}QueryPayload {
24
- readonly [key: string]: unknown;
25
- }
26
-
27
- export class ${nameCamel}Query implements BaseQuery<${nameCamel}QueryPayload> {
28
- readonly queryName = "${name}";
29
-
30
- constructor(public readonly payload: ${nameCamel}QueryPayload) {}
31
- }
32
- `,
33
- [`${servicePath}/queries/${name}/${name}.handler.ts`]: `import type { QueryHandler, QueryResult } from "@zudojs/cqrs";
34
- import { createLogger } from "@zudojs/logger";
35
- import { ${nameCamel}Query } from "./${name}.query.js";
36
-
37
- export class ${nameCamel}QueryHandler implements QueryHandler<${nameCamel}Query> {
38
- private readonly logger = createLogger({ name: "${name}-handler" });
39
-
40
- async handle(query: ${nameCamel}Query): Promise<QueryResult> {
41
- this.logger.info("Processing ${name} query", { payload: query.payload });
42
-
43
- return {
44
- success: true,
45
- data: { items: [], total: 0 },
46
- };
47
- }
48
- }
49
- `,
50
- [`${servicePath}/queries/${name}/index.ts`]: `export { ${nameCamel}Query } from "./${name}.query.js";
51
- export { ${nameCamel}QueryHandler } from "./${name}.handler.js";
52
- `,
25
+ [`${dir}/${names.slug}.query.ts`]: renderQueryFile(names),
26
+ [`${dir}/${names.slug}.handler.ts`]: renderQueryHandlerFile(names),
27
+ [`${dir}/index.ts`]: renderQueryBarrel(names),
53
28
  };
54
29
  if (options.dryRun) {
55
30
  return Object.keys(files);
@@ -59,7 +34,7 @@ export { ${nameCamel}QueryHandler } from "./${name}.handler.js";
59
34
  return Object.keys(files);
60
35
  }
61
36
  catch (error) {
62
- throw new CLIGenerationError(`Failed to generate query: ${name} in ${servicePath}`, error);
37
+ throw new CLIGenerationError(`Failed to generate query: ${names.slug} in ${servicePath}`, error);
63
38
  }
64
39
  }
65
40
  //# sourceMappingURL=query.generator.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * zudojs-cli — Query schematic templates, written against the
3
+ * @zudojs/cqrs API: a `QueryOf` type built with `createQuery`, and a
4
+ * `QueryHandler` subclass (`queryType` + `execute`) registered on a
5
+ * `QueryBus`.
6
+ */
7
+ import type { CqrsSchematicNames } from "../command/command.template.js";
8
+ /** `<slug>.query.ts`: the discriminator, payload, query type and factory. */
9
+ export declare function renderQueryFile(n: CqrsSchematicNames): string;
10
+ /** `<slug>.handler.ts`: the handler class and its bus registration. */
11
+ export declare function renderQueryHandlerFile(n: CqrsSchematicNames): string;
12
+ /** `index.ts`: the query folder's barrel. */
13
+ export declare function renderQueryBarrel(n: CqrsSchematicNames): string;
14
+ //# sourceMappingURL=query.template.d.ts.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * zudojs-cli — Query schematic templates, written against the
3
+ * @zudojs/cqrs API: a `QueryOf` type built with `createQuery`, and a
4
+ * `QueryHandler` subclass (`queryType` + `execute`) registered on a
5
+ * `QueryBus`.
6
+ */
7
+ /** `<slug>.query.ts`: the discriminator, payload, query type and factory. */
8
+ export function renderQueryFile(n) {
9
+ const id = `${n.constant}_QUERY`;
10
+ return `import { createQuery, type QueryOf } from "@zudojs/cqrs";
11
+
12
+ /** Discriminator the query bus routes ${n.pascal} queries on. */
13
+ export const ${id} = "${n.pascal}";
14
+
15
+ /** Criteria a ${n.pascal} query carries. Replace it with the fields the read needs. */
16
+ export type ${n.pascal}QueryPayload = {
17
+ readonly filter: Readonly<Record<string, unknown>>;
18
+ };
19
+
20
+ /** The ${n.pascal} query: \`{ type: "${n.pascal}", filter }\`. */
21
+ export type ${n.pascal}Query = QueryOf<typeof ${id}, ${n.pascal}QueryPayload>;
22
+
23
+ /** Creates an immutable ${n.pascal} query. */
24
+ export function create${n.pascal}Query(payload: ${n.pascal}QueryPayload): ${n.pascal}Query {
25
+ return createQuery(${id}, payload);
26
+ }
27
+ `;
28
+ }
29
+ /** `<slug>.handler.ts`: the handler class and its bus registration. */
30
+ export function renderQueryHandlerFile(n) {
31
+ const id = `${n.constant}_QUERY`;
32
+ return `import { QueryHandler, type QueryBus } from "@zudojs/cqrs";
33
+
34
+ import { ${id}, type ${n.pascal}Query } from "./${n.slug}.query.js";
35
+
36
+ /** What executing a ${n.pascal} query returns. */
37
+ export interface ${n.pascal}QueryResult {
38
+ readonly filter: Readonly<Record<string, unknown>>;
39
+ readonly items: readonly unknown[];
40
+ readonly total: number;
41
+ }
42
+
43
+ /** Handles ${n.pascal} queries: put the read-side logic in \`execute\`. */
44
+ export class ${n.pascal}QueryHandler extends QueryHandler<${n.pascal}Query, ${n.pascal}QueryResult> {
45
+ public override readonly queryType = ${id};
46
+
47
+ public override async execute(query: ${n.pascal}Query): Promise<${n.pascal}QueryResult> {
48
+ return { filter: query.filter, items: [], total: 0 };
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Registers {@link ${n.pascal}QueryHandler} on a query bus.
54
+ *
55
+ * @example
56
+ * const bus = register${n.pascal}Query(createQueryBus());
57
+ * const result = await bus.execute<${n.pascal}Query, ${n.pascal}QueryResult>(
58
+ * create${n.pascal}Query({ filter: {} }),
59
+ * );
60
+ */
61
+ export function register${n.pascal}Query(bus: QueryBus): QueryBus {
62
+ return bus.register(${id}, new ${n.pascal}QueryHandler());
63
+ }
64
+ `;
65
+ }
66
+ /** `index.ts`: the query folder's barrel. */
67
+ export function renderQueryBarrel(n) {
68
+ return `export * from "./${n.slug}.query.js";
69
+ export * from "./${n.slug}.handler.js";
70
+ `;
71
+ }
72
+ //# sourceMappingURL=query.template.js.map
@@ -1,16 +1,22 @@
1
1
  /**
2
2
  * zudojs-cli — Which files a resource-family schematic writes.
3
3
  *
4
- * `resource`, `route`, `controller`, `repository` and `dto` all write part
4
+ * `resource`, `route`, `controller`, `service`, `repository` and `dto` all write part
5
5
  * of the same DTO → repository → service → controller → routes chain. Each
6
6
  * writes its own layer ("primary") and any lower layer that does not exist
7
7
  * yet ("required"), so whatever it writes compiles on its own:
8
8
  * `generate controller users` also writes the users service, repository
9
9
  * and DTO when they are missing, and never touches them when they exist.
10
+ *
11
+ * `service` used to have a generator of its own that wrote
12
+ * `services/<name>/<name>.service.ts`, a second layout next to the
13
+ * `services/<name>.service.ts` every other schematic and the container
14
+ * wiring use; `generate controller` after `generate service` then wrote a
15
+ * second, unrelated service. It is part of the chain now.
10
16
  */
11
17
  import type { ResourceLayer } from "../../templates/resource/index.js";
12
18
  /** Schematics served by the resource generator. */
13
- export declare const RESOURCE_SCHEMATICS: readonly ["resource", "route", "controller", "repository", "dto"];
19
+ export declare const RESOURCE_SCHEMATICS: readonly ["resource", "route", "controller", "service", "repository", "dto"];
14
20
  /** A schematic served by the resource generator. */
15
21
  export type ResourceSchematic = (typeof RESOURCE_SCHEMATICS)[number];
16
22
  /** Whether `schematic` is served by the resource generator. */
@@ -1,18 +1,25 @@
1
1
  /**
2
2
  * zudojs-cli — Which files a resource-family schematic writes.
3
3
  *
4
- * `resource`, `route`, `controller`, `repository` and `dto` all write part
4
+ * `resource`, `route`, `controller`, `service`, `repository` and `dto` all write part
5
5
  * of the same DTO → repository → service → controller → routes chain. Each
6
6
  * writes its own layer ("primary") and any lower layer that does not exist
7
7
  * yet ("required"), so whatever it writes compiles on its own:
8
8
  * `generate controller users` also writes the users service, repository
9
9
  * and DTO when they are missing, and never touches them when they exist.
10
+ *
11
+ * `service` used to have a generator of its own that wrote
12
+ * `services/<name>/<name>.service.ts`, a second layout next to the
13
+ * `services/<name>.service.ts` every other schematic and the container
14
+ * wiring use; `generate controller` after `generate service` then wrote a
15
+ * second, unrelated service. It is part of the chain now.
10
16
  */
11
17
  /** Schematics served by the resource generator. */
12
18
  export const RESOURCE_SCHEMATICS = [
13
19
  "resource",
14
20
  "route",
15
21
  "controller",
22
+ "service",
16
23
  "repository",
17
24
  "dto",
18
25
  ];
@@ -37,6 +44,8 @@ export function planResource(schematic) {
37
44
  };
38
45
  case "controller":
39
46
  return { primary: ["controller"], required: ["dto", "repository", "service"], register: false };
47
+ case "service":
48
+ return { primary: ["service"], required: ["dto", "repository"], register: false };
40
49
  case "repository":
41
50
  return { primary: ["repository"], required: ["dto"], register: false };
42
51
  case "dto":
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { existsSync } from "node:fs";
9
9
  import { join } from "node:path";
10
- import { renderAppPackageDockerfile } from "../../templates/shared/dockerfile.template.js";
10
+ import { renderAppPackageDockerfile } from "../../templates/shared/dockerfile/index.js";
11
11
  import { renderComposeFile, usesPostgres } from "./docker.compose.js";
12
12
  const DOCKERIGNORE = `node_modules
13
13
  **/node_modules
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
- import { renderAppPackageDockerfile } from "../../templates/shared/dockerfile.template.js";
12
+ import { renderAppPackageDockerfile } from "../../templates/shared/dockerfile/index.js";
13
13
  import { writeFile } from "../../utils/utils.fileSystem.js";
14
14
  /** Rewrites unedited CLI Dockerfiles whose Prisma setting is stale; returns their paths. */
15
15
  export async function refreshGeneratedDockerfiles(context) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * zudojs-cli — A resource's DTO schemas and in-memory repository.
3
3
  */
4
- import type { ResourceNames } from "../resource.names.js";
4
+ import { type ResourceNames } from "../resource.names.js";
5
5
  /** `dtos/<slug>.dto.ts`: @zudojs/schema schemas and their inferred types. */
6
6
  export declare function renderResourceDto(n: ResourceNames): string;
7
7
  /** `repositories/<slug>.repository.ts`: the contract and a memory store. */
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * zudojs-cli — A resource's DTO schemas and in-memory repository.
3
3
  */
4
+ import { withArticle } from "../resource.names.js";
4
5
  /** `dtos/<slug>.dto.ts`: @zudojs/schema schemas and their inferred types. */
5
6
  export function renderResourceDto(n) {
6
7
  return `import { schema, type Infer } from "@zudojs/schema";
7
8
 
8
- /** A ${n.label} as the API returns it. */
9
+ /** ${withArticle(n.label, true)} as the API returns it. */
9
10
  export const ${n.entity}Schema = schema.object({
10
11
  id: schema.string().uuid(),
11
12
  name: schema.string(),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * zudojs-cli — A resource's CRUD routes (with OpenAPI metadata) and test.
3
3
  */
4
- import type { ResourceNames } from "../resource.names.js";
4
+ import { type ResourceNames } from "../resource.names.js";
5
5
  /** `routes/<slug>.routes.ts`: `register<Pascal>Routes(router, controller)`. */
6
6
  export declare function renderResourceRoutes(n: ResourceNames): string;
7
7
  /**
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * zudojs-cli — A resource's CRUD routes (with OpenAPI metadata) and test.
3
3
  */
4
+ import { withArticle } from "../resource.names.js";
4
5
  /** `routes/<slug>.routes.ts`: `register<Pascal>Routes(router, controller)`. */
5
6
  export function renderResourceRoutes(n) {
6
7
  const schemas = `Create${n.entity}Schema, ${n.entity}ParamsSchema, ${n.entity}Schema, Update${n.entity}Schema`;
@@ -30,7 +31,7 @@ export function register${n.pascal}Routes(
30
31
 
31
32
  router.get(\`\${BASE_PATH}/:id\`, controller.get, {
32
33
  openapi: {
33
- summary: "Get a ${n.label}",
34
+ summary: "Get ${withArticle(n.label)}",
34
35
  tags: TAGS,
35
36
  params: ${n.entity}ParamsSchema,
36
37
  responses: { "200": { description: "The ${n.label}", schema: ${n.entity}Schema }, "400": invalid, "404": missing },
@@ -39,7 +40,7 @@ export function register${n.pascal}Routes(
39
40
 
40
41
  router.post(BASE_PATH, controller.create, {
41
42
  openapi: {
42
- summary: "Create a ${n.label}",
43
+ summary: "Create ${withArticle(n.label)}",
43
44
  tags: TAGS,
44
45
  body: Create${n.entity}Schema,
45
46
  responses: { "201": { description: "Created", schema: ${n.entity}Schema }, "400": invalid },
@@ -48,7 +49,7 @@ export function register${n.pascal}Routes(
48
49
 
49
50
  router.patch(\`\${BASE_PATH}/:id\`, controller.update, {
50
51
  openapi: {
51
- summary: "Update a ${n.label}",
52
+ summary: "Update ${withArticle(n.label)}",
52
53
  tags: TAGS,
53
54
  params: ${n.entity}ParamsSchema,
54
55
  body: Update${n.entity}Schema,
@@ -58,7 +59,7 @@ export function register${n.pascal}Routes(
58
59
 
59
60
  router.delete(\`\${BASE_PATH}/:id\`, controller.remove, {
60
61
  openapi: {
61
- summary: "Delete a ${n.label}",
62
+ summary: "Delete ${withArticle(n.label)}",
62
63
  tags: TAGS,
63
64
  params: ${n.entity}ParamsSchema,
64
65
  responses: { "204": { description: "Deleted" }, "400": invalid, "404": missing },
@@ -92,7 +93,7 @@ const client = createHttpTestClient(router);
92
93
  afterAll(() => client.close());
93
94
 
94
95
  describe("${n.routePath}", () => {
95
- it("creates, reads, updates and deletes a ${n.label}", async () => {
96
+ it("creates, reads, updates and deletes ${withArticle(n.label)}", async () => {
96
97
  const created = await client
97
98
  .post("${n.routePath}")
98
99
  .send({ name: "Ada" })
@@ -35,4 +35,10 @@ export declare function singularize(slug: string): string;
35
35
  * @throws {CLIValidationError} For unusable or reserved names.
36
36
  */
37
37
  export declare function resourceNames(raw: string): ResourceNames;
38
+ /**
39
+ * Prefixes `noun` with "a" or "an" by its (approximate) opening sound:
40
+ * `example` → "an example", `user` → "a user", `hour` → "an hour".
41
+ * Pass `capitalize` for the start of a sentence ("An example").
42
+ */
43
+ export declare function withArticle(noun: string, capitalize?: boolean): string;
38
44
  //# sourceMappingURL=resource.names.d.ts.map
@@ -57,4 +57,19 @@ export function resourceNames(raw) {
57
57
  routePath: `/api/v1/${slug}`,
58
58
  };
59
59
  }
60
+ /** Words spelled with a vowel but said with a consonant sound ("a user"). */
61
+ const CONSONANT_SOUND = /^(u[bcdfghjklmnpqrstvwxyz][aeiou]|uu|eu|ewe|one|once)/;
62
+ /** Words spelled with a consonant but said with a vowel sound ("an hour"). */
63
+ const VOWEL_SOUND = /^(hour|honest|honor|honour|heir)/;
64
+ /**
65
+ * Prefixes `noun` with "a" or "an" by its (approximate) opening sound:
66
+ * `example` → "an example", `user` → "a user", `hour` → "an hour".
67
+ * Pass `capitalize` for the start of a sentence ("An example").
68
+ */
69
+ export function withArticle(noun, capitalize = false) {
70
+ const word = noun.toLowerCase();
71
+ const vowel = VOWEL_SOUND.test(word) || (/^[aeiou]/.test(word) && !CONSONANT_SOUND.test(word));
72
+ const article = vowel ? "an" : "a";
73
+ return `${capitalize ? article.charAt(0).toUpperCase() + article.slice(1) : article} ${noun}`;
74
+ }
60
75
  //# sourceMappingURL=resource.names.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * zudojs-cli — How a generated Dockerfile installs dependencies.
3
+ *
4
+ * A build stage used to copy only `package.json` and run a floating install
5
+ * (`npm install`), so two builds of the same commit could ship different
6
+ * dependency trees. When the lockfile describes the package being built, it
7
+ * is copied and installed frozen: `npm ci`, `pnpm install --frozen-lockfile`,
8
+ * `yarn install --immutable` (yarn 2+) or `--frozen-lockfile` (yarn 1), and
9
+ * `bun install --frozen-lockfile`.
10
+ *
11
+ * The lockfile is copied with a wildcard (`package-lock.json*`), which
12
+ * matches nothing rather than failing when there is none yet (a project
13
+ * created with `--skip-install`), and the install falls back to resolving
14
+ * from `package.json`, as the comment written above it says.
15
+ */
16
+ /** The lockfile-related part of a Dockerfile build stage. */
17
+ export interface DockerInstallSteps {
18
+ /** Comment lines, already prefixed with `# `. */
19
+ readonly comment: readonly string[];
20
+ /** Files copied next to `package.json` before the install (may be globs). */
21
+ readonly files: readonly string[];
22
+ /** The `RUN` command that installs dependencies. */
23
+ readonly install: string;
24
+ /** The `RUN` command that removes dev dependencies after the build. */
25
+ readonly prune?: string;
26
+ }
27
+ /** Install steps when the project's lockfile describes the package being built. */
28
+ export declare function lockedInstallSteps(packageManager: string): DockerInstallSteps;
29
+ /**
30
+ * Install steps for one app of a workspace, built on its own from its
31
+ * `package.json`. The workspace lockfile at the project root lists every
32
+ * package, so it cannot pin this app's isolated install.
33
+ */
34
+ export declare function workspaceMemberInstallSteps(packageManager: string): DockerInstallSteps;
35
+ //# sourceMappingURL=dockerfile.install.d.ts.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * zudojs-cli — How a generated Dockerfile installs dependencies.
3
+ *
4
+ * A build stage used to copy only `package.json` and run a floating install
5
+ * (`npm install`), so two builds of the same commit could ship different
6
+ * dependency trees. When the lockfile describes the package being built, it
7
+ * is copied and installed frozen: `npm ci`, `pnpm install --frozen-lockfile`,
8
+ * `yarn install --immutable` (yarn 2+) or `--frozen-lockfile` (yarn 1), and
9
+ * `bun install --frozen-lockfile`.
10
+ *
11
+ * The lockfile is copied with a wildcard (`package-lock.json*`), which
12
+ * matches nothing rather than failing when there is none yet (a project
13
+ * created with `--skip-install`), and the install falls back to resolving
14
+ * from `package.json`, as the comment written above it says.
15
+ */
16
+ const FALLBACK = [
17
+ "# The lockfile is optional (copied with a wildcard): with it the install is",
18
+ "# frozen and the image reproducible; with no lockfile yet (nothing has been",
19
+ "# installed) dependencies resolve from package.json. Commit the lockfile.",
20
+ ];
21
+ /** Install steps when the project's lockfile describes the package being built. */
22
+ export function lockedInstallSteps(packageManager) {
23
+ switch (packageManager) {
24
+ case "pnpm":
25
+ return {
26
+ comment: FALLBACK,
27
+ // pnpm-workspace.yaml carries the build-script allow-list pnpm 11 needs.
28
+ files: ["pnpm-lock.yaml*", "pnpm-workspace.yaml"],
29
+ install: "corepack enable && if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; else pnpm install; fi",
30
+ prune: "pnpm prune --prod --ignore-scripts",
31
+ };
32
+ case "yarn":
33
+ return {
34
+ comment: [...FALLBACK, "# yarn 1 takes --frozen-lockfile; yarn 2+ takes --immutable."],
35
+ files: ["yarn.lock*", ".yarnrc.yml*"],
36
+ install: "corepack enable && if [ ! -f yarn.lock ]; then yarn install; " +
37
+ "elif yarn --version | grep -q '^1\\.'; then yarn install --frozen-lockfile; " +
38
+ "else yarn install --immutable; fi",
39
+ };
40
+ case "bun":
41
+ return {
42
+ comment: [...FALLBACK, "# The node image has no bun: it is installed from npm for the build."],
43
+ // bun.lock* matches bun.lock (bun 1.2+) and bun.lockb (older bun).
44
+ files: ["bun.lock*"],
45
+ install: "npm install -g bun && if [ -f bun.lock ] || [ -f bun.lockb ]; then bun install --frozen-lockfile; else bun install; fi",
46
+ // The first install wrote a lockfile if there was none, so this is frozen.
47
+ prune: "rm -rf node_modules && bun install --production --frozen-lockfile --ignore-scripts",
48
+ };
49
+ default:
50
+ return {
51
+ comment: FALLBACK,
52
+ files: ["package-lock.json*"],
53
+ install: "if [ -f package-lock.json ]; then npm ci; else npm install; fi",
54
+ prune: "npm prune --omit=dev --ignore-scripts",
55
+ };
56
+ }
57
+ }
58
+ /**
59
+ * Install steps for one app of a workspace, built on its own from its
60
+ * `package.json`. The workspace lockfile at the project root lists every
61
+ * package, so it cannot pin this app's isolated install.
62
+ */
63
+ export function workspaceMemberInstallSteps(packageManager) {
64
+ const comment = [
65
+ "# This app is one package of a workspace and is installed on its own here.",
66
+ "# The workspace lockfile at the project root describes every package, so it",
67
+ "# cannot pin this install; dependencies resolve from package.json.",
68
+ ];
69
+ switch (packageManager) {
70
+ case "pnpm":
71
+ return {
72
+ comment,
73
+ files: ["pnpm-workspace.yaml"],
74
+ install: "corepack enable && pnpm install",
75
+ prune: "pnpm prune --prod --ignore-scripts",
76
+ };
77
+ case "yarn":
78
+ return { comment, files: [], install: "corepack enable && yarn install" };
79
+ default:
80
+ // The node image has no bun, so a bun workspace app installs with npm.
81
+ return {
82
+ comment,
83
+ files: [],
84
+ install: "npm install",
85
+ prune: "npm prune --omit=dev --ignore-scripts",
86
+ };
87
+ }
88
+ }
89
+ //# sourceMappingURL=dockerfile.install.js.map
@@ -8,13 +8,15 @@
8
8
  * unprivileged `node` user and declares a /health HEALTHCHECK. The images
9
9
  * used to run as root and ship the whole dev toolchain.
10
10
  */
11
- /** The dependency install command for a package manager inside a Dockerfile. */
12
- export declare function dockerInstallCommand(packageManager: string): string;
13
11
  /**
14
12
  * A self-contained Dockerfile for one app package. The build context is the
15
13
  * project root; `appPath` is the app's directory (`.` for a single-app
16
14
  * project). pnpm's `pnpm-workspace.yaml` is copied for its build-script
17
15
  * allow-list, without which pnpm 11 refuses to install.
16
+ *
17
+ * An app at the project root copies its lockfile and installs frozen (see
18
+ * `dockerfile.install.ts`); an app inside a workspace cannot use the
19
+ * workspace lockfile on its own, and its Dockerfile says so.
18
20
  */
19
21
  export declare function renderAppPackageDockerfile(options: {
20
22
  readonly appPath: string;
@@ -8,30 +8,7 @@
8
8
  * unprivileged `node` user and declares a /health HEALTHCHECK. The images
9
9
  * used to run as root and ship the whole dev toolchain.
10
10
  */
11
- /** The dependency install command for a package manager inside a Dockerfile. */
12
- export function dockerInstallCommand(packageManager) {
13
- // No lockfile is generated, so never `npm ci`; bare node images need
14
- // corepack for pnpm/yarn; bun is not available there, so npm is used.
15
- switch (packageManager) {
16
- case "pnpm":
17
- return "corepack enable && pnpm install";
18
- case "yarn":
19
- return "corepack enable && yarn install";
20
- default:
21
- return "npm install";
22
- }
23
- }
24
- /** Removes dev dependencies after the build (scripts off: prisma is a dev tool). */
25
- function dockerPruneCommand(packageManager) {
26
- switch (packageManager) {
27
- case "pnpm":
28
- return "pnpm prune --prod --ignore-scripts";
29
- case "yarn":
30
- return undefined;
31
- default:
32
- return "npm prune --omit=dev --ignore-scripts";
33
- }
34
- }
11
+ import { lockedInstallSteps, workspaceMemberInstallSteps, } from "./dockerfile.install.js";
35
12
  function runnerFor(packageManager) {
36
13
  return packageManager === "npm" || packageManager === "bun" ? "npm" : packageManager;
37
14
  }
@@ -54,24 +31,31 @@ CMD ["node", "${dist}/server.js"]
54
31
  * project root; `appPath` is the app's directory (`.` for a single-app
55
32
  * project). pnpm's `pnpm-workspace.yaml` is copied for its build-script
56
33
  * allow-list, without which pnpm 11 refuses to install.
34
+ *
35
+ * An app at the project root copies its lockfile and installs frozen (see
36
+ * `dockerfile.install.ts`); an app inside a workspace cannot use the
37
+ * workspace lockfile on its own, and its Dockerfile says so.
57
38
  */
58
39
  export function renderAppPackageDockerfile(options) {
59
40
  const app = options.appPath.replace(/^\.\/?/, "").replace(/\/$/, "");
60
41
  const at = (file) => (app === "" ? file : `${app}/${file}`);
61
42
  const pm = options.packageManager;
62
- const prune = dockerPruneCommand(pm);
43
+ const steps = app === "" ? lockedInstallSteps(pm) : workspaceMemberInstallSteps(pm);
44
+ const manifests = app === ""
45
+ ? [`COPY ${["package.json", ...steps.files].join(" ")} ./`]
46
+ : [`COPY ${at("package.json")} ./`, ...steps.files.map((file) => `COPY ${file} ./`)];
63
47
  const lines = [
64
48
  "# syntax=docker/dockerfile:1",
65
49
  "FROM node:24-alpine AS build",
66
50
  "WORKDIR /app",
67
- `COPY ${at("package.json")} ./`,
68
- ...(pm === "pnpm" ? ["COPY pnpm-workspace.yaml ./"] : []),
51
+ ...steps.comment,
52
+ ...manifests,
69
53
  ...(options.prisma ? [`COPY ${at("prisma.config.ts")} ./`, `COPY ${at("prisma")} ./prisma`] : []),
70
- `RUN ${dockerInstallCommand(pm)}`,
54
+ `RUN ${steps.install}`,
71
55
  `COPY ${at("tsconfig.json")} ./`,
72
56
  `COPY ${at("src")} ./src`,
73
57
  `RUN ${runnerFor(pm)} run build`,
74
- ...(prune === undefined ? [] : [`RUN ${prune}`]),
58
+ ...(steps.prune === undefined ? [] : [`RUN ${steps.prune}`]),
75
59
  "",
76
60
  ];
77
61
  return `${lines.join("\n")}\n${runtimeStage(options.port, "dist", "/app")}`;
@@ -86,13 +70,16 @@ export function renderWorkspaceAppDockerfile(options) {
86
70
  const runner = runnerFor(options.packageManager);
87
71
  const build = dir === "" ? `${runner} run build` : `cd ${dir} && ${runner} run build`;
88
72
  const dist = dir === "" ? "dist" : `${dir}/dist`;
73
+ // The whole workspace is copied, lockfile included, so it installs frozen.
74
+ const steps = lockedInstallSteps(options.packageManager);
89
75
  // pnpm links workspace dependencies through the root node_modules, so
90
76
  // the runtime stage takes the whole built workspace.
91
77
  return `# syntax=docker/dockerfile:1
92
78
  FROM node:24-alpine AS build
93
79
  WORKDIR /app
80
+ ${steps.comment.join("\n")}
94
81
  COPY . .
95
- RUN ${dockerInstallCommand(options.packageManager)}
82
+ RUN ${steps.install}
96
83
  RUN ${build}
97
84
 
98
85
  FROM node:24-alpine AS runtime
@@ -0,0 +1,6 @@
1
+ /**
2
+ * zudojs-cli — Generated Dockerfiles and how they install dependencies.
3
+ */
4
+ export * from "./dockerfile.template.js";
5
+ export * from "./dockerfile.install.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * zudojs-cli — Generated Dockerfiles and how they install dependencies.
3
+ */
4
+ export * from "./dockerfile.template.js";
5
+ export * from "./dockerfile.install.js";
6
+ //# sourceMappingURL=index.js.map
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export * from "./appRuntime.template.js";
5
5
  export * from "./capability.template.js";
6
- export * from "./dockerfile.template.js";
6
+ export * from "./dockerfile/index.js";
7
7
  export * from "./pnpm.template.js";
8
8
  export * from "./server.template.js";
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export * from "./appRuntime.template.js";
5
5
  export * from "./capability.template.js";
6
- export * from "./dockerfile.template.js";
6
+ export * from "./dockerfile/index.js";
7
7
  export * from "./pnpm.template.js";
8
8
  export * from "./server.template.js";
9
9
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudojs-cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Command-line interface for scaffolding, generating, and managing Zudojs framework projects.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -32,14 +32,14 @@
32
32
  ],
33
33
  "dependencies": {
34
34
  "@clack/prompts": "^1.8.1",
35
- "@zudojs/config": "1.3.1",
36
- "@zudojs/core": "1.2.2",
37
- "@zudojs/errors": "1.3.0",
38
- "@zudojs/logger": "1.4.1"
35
+ "@zudojs/config": "1.3.2",
36
+ "@zudojs/core": "1.2.3",
37
+ "@zudojs/errors": "1.3.1",
38
+ "@zudojs/logger": "1.4.2"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^26.6.2",
42
- "@zudojs/constants": "1.1.2",
42
+ "@zudojs/constants": "1.1.3",
43
43
  "tsx": "^4.23.15",
44
44
  "typescript": "7.0.2",
45
45
  "vitest": "^5.0.1"
@@ -1,12 +0,0 @@
1
- /**
2
- * zudojs-cli — Service Generator
3
- *
4
- * Generates a new service with CQRS structure.
5
- */
6
- export interface GenerateServiceOptions {
7
- readonly name: string;
8
- readonly basePath?: string;
9
- readonly dryRun?: boolean;
10
- }
11
- export declare function generateService(options: GenerateServiceOptions, cwd: string): Promise<string[]>;
12
- //# sourceMappingURL=service.generator.d.ts.map
@@ -1,41 +0,0 @@
1
- /**
2
- * zudojs-cli — Service Generator
3
- *
4
- * Generates a new service with CQRS structure.
5
- */
6
- import { writeFileTree, mergeBarrelExport, } from "../../utils/utils.fileSystem.js";
7
- import { CLIGenerationError } from "../../errors/index.js";
8
- import { assertGeneratableName, toPascalCase } from "../../utils/utils.name.js";
9
- export async function generateService(options, cwd) {
10
- const basePath = options.basePath ?? "services";
11
- const name = assertGeneratableName(options.name, "service name");
12
- const namePascal = toPascalCase(name);
13
- const files = {
14
- [`${basePath}/${name}/${name}.service.ts`]: `import { createLogger } from "@zudojs/logger";
15
-
16
- export class ${namePascal}Service {
17
- private readonly logger = createLogger({ name: "${name}-service" });
18
-
19
- async initialize(): Promise<void> {
20
- this.logger.info("${name} service initialized");
21
- }
22
- }
23
- `,
24
- // Merge with any existing barrel content instead of overwriting it.
25
- [`${basePath}/${name}/index.ts`]: mergeBarrelExport(cwd, `${basePath}/${name}/index.ts`, `export { ${namePascal}Service } from "./${name}.service.js";`),
26
- // Preserve any existing content in the CQRS barrels.
27
- [`${basePath}/${name}/commands/index.ts`]: mergeBarrelExport(cwd, `${basePath}/${name}/commands/index.ts`, ""),
28
- [`${basePath}/${name}/queries/index.ts`]: mergeBarrelExport(cwd, `${basePath}/${name}/queries/index.ts`, ""),
29
- };
30
- if (options.dryRun) {
31
- return Object.keys(files);
32
- }
33
- try {
34
- await writeFileTree(cwd, files);
35
- return Object.keys(files);
36
- }
37
- catch (error) {
38
- throw new CLIGenerationError(`Failed to generate service: ${name}`, error);
39
- }
40
- }
41
- //# sourceMappingURL=service.generator.js.map