zudojs-cli 1.1.0 → 1.2.0

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 (54) hide show
  1. package/README.md +27 -0
  2. package/dist/src/adapters/databases/databaseAdapter.resolver.d.ts +21 -0
  3. package/dist/src/adapters/databases/databaseAdapter.resolver.js +40 -0
  4. package/dist/src/adapters/databases/index.d.ts +1 -0
  5. package/dist/src/adapters/databases/index.js +1 -0
  6. package/dist/src/adapters/databases/mysql.adapter.d.ts +1 -1
  7. package/dist/src/adapters/databases/mysql.adapter.js +2 -2
  8. package/dist/src/adapters/databases/postgres.adapter.d.ts +3 -2
  9. package/dist/src/adapters/databases/postgres.adapter.js +2 -2
  10. package/dist/src/adapters/databases/sqlite.adapter.d.ts +1 -1
  11. package/dist/src/adapters/databases/sqlite.adapter.js +2 -2
  12. package/dist/src/bin/zudojs.js +7 -1
  13. package/dist/src/commands/add.command.js +5 -1
  14. package/dist/src/commands/create.command.js +21 -7
  15. package/dist/src/commands/generate.command.js +37 -6
  16. package/dist/src/errors/index.d.ts +3 -14
  17. package/dist/src/errors/index.js +3 -24
  18. package/dist/src/generators/frontend/frontendPipeline.js +7 -3
  19. package/dist/src/generators/fullstack/fullstackBackend.layout.d.ts +26 -0
  20. package/dist/src/generators/fullstack/fullstackBackend.layout.js +34 -0
  21. package/dist/src/generators/fullstack/fullstackComposer.core.js +5 -1
  22. package/dist/src/generators/fullstack/index.d.ts +1 -0
  23. package/dist/src/generators/fullstack/index.js +1 -0
  24. package/dist/src/generators/infrastructure/infrastructure.generator.d.ts +5 -10
  25. package/dist/src/generators/infrastructure/infrastructure.generator.js +41 -75
  26. package/dist/src/generators/integration/integrationGenerator.core.js +8 -2
  27. package/dist/src/generators/module/index.d.ts +6 -0
  28. package/dist/src/generators/module/index.js +6 -0
  29. package/dist/src/generators/module/module.generator.d.ts +6 -1
  30. package/dist/src/generators/module/module.generator.js +15 -16
  31. package/dist/src/generators/module/module.registration.d.ts +23 -0
  32. package/dist/src/generators/module/module.registration.js +50 -0
  33. package/dist/src/resolvers/dependency/dependencyResolver.core.js +6 -1
  34. package/dist/src/resolvers/dependency/dependencyVersions.constant.d.ts +12 -0
  35. package/dist/src/resolvers/dependency/dependencyVersions.constant.js +40 -0
  36. package/dist/src/resolvers/dependency/index.d.ts +1 -0
  37. package/dist/src/resolvers/dependency/index.js +1 -0
  38. package/dist/src/templates/microservice/microservice.template.js +14 -42
  39. package/dist/src/templates/modular-monolith/modularMonolith.template.js +2 -1
  40. package/dist/src/templates/monolith/monolith.template.js +5 -2
  41. package/dist/src/templates/shared/appRuntime.template.d.ts +0 -5
  42. package/dist/src/templates/shared/appRuntime.template.js +3 -40
  43. package/dist/src/templates/shared/dockerfile.template.d.ts +28 -0
  44. package/dist/src/templates/shared/dockerfile.template.js +69 -0
  45. package/dist/src/templates/shared/index.d.ts +2 -0
  46. package/dist/src/templates/shared/index.js +2 -0
  47. package/dist/src/templates/shared/server.template.d.ts +31 -0
  48. package/dist/src/templates/shared/server.template.js +96 -0
  49. package/dist/src/utils/index.d.ts +1 -0
  50. package/dist/src/utils/index.js +1 -0
  51. package/dist/src/utils/utils.fileSystem.js +6 -0
  52. package/dist/src/utils/utils.writeGuard.d.ts +25 -0
  53. package/dist/src/utils/utils.writeGuard.js +46 -0
  54. package/package.json +6 -5
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  Command-line interface for scaffolding, generating, and managing Zudojs framework projects.
4
4
 
5
+ <!-- zudo-docs:start -->
6
+
7
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-cli](https://zudojs.oyinlola.site/docs/packages-cli) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-cli.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
8
+
9
+ <!-- zudo-docs:end -->
10
+
5
11
  ## Installation
6
12
 
7
13
  ### First time
@@ -75,6 +81,27 @@ block of `package.json`. Every follow-up command reads those; no
75
81
  ranges, and projects created for pnpm carry a `pnpm-workspace.yaml` that
76
82
  allows esbuild's build script, which pnpm 10+ would otherwise refuse to run.
77
83
 
84
+ - **A running server.** `src/server.ts` starts the runtime and serves HTTP
85
+ with `@zudojs/http` on `PORT` (default 3000; each microservice app on its
86
+ own port), answering `GET /health`. SIGINT/SIGTERM stop the HTTP server,
87
+ then the runtime.
88
+ - **Databases:** `--database postgresql|mysql|sqlite` sets `DATABASE_URL` in
89
+ `.env.example` and the database container. `mongodb` is not supported.
90
+ - **Language:** backend code is TypeScript. `--language javascript` is
91
+ rejected for `--type backend`; in a fullstack project it applies to the
92
+ frontend only.
93
+ - **Fullstack + microservice:** the gateway and services are written at
94
+ `apps/gateway` and `apps/services/<name>`, next to `apps/web`, and are part
95
+ of the root workspace.
96
+
97
+ ### Generating code
98
+
99
+ `zudojs generate module <name>` writes a runtime module (a `BaseModule`
100
+ subclass), exports it from the modules barrel and registers it in `app.ts`.
101
+ `zudojs generate` refuses to overwrite a file that already exists and would
102
+ change, and lists those files; pass `--force` to overwrite them. Barrels are
103
+ only appended to. `--dry-run` lists the files without writing anything.
104
+
78
105
  ## Commands
79
106
 
80
107
  | Command | Description |
@@ -0,0 +1,21 @@
1
+ /**
2
+ * zudojs-cli — Database adapter lookup.
3
+ *
4
+ * `--database mysql|sqlite` used to be accepted and then ignored: every
5
+ * template hard-coded a PostgreSQL `DATABASE_URL`, and the MySQL/SQLite
6
+ * adapters were exported but never called. Templates and the
7
+ * InfrastructureGenerator resolve the adapter here instead. `mongodb` has
8
+ * no adapter, so it is rejected rather than silently turned into Postgres.
9
+ */
10
+ import type { DatabaseAdapter } from "./postgres.adapter.js";
11
+ /** Database engines the CLI can scaffold. */
12
+ export declare const SUPPORTED_DATABASES: readonly ["postgresql", "mysql", "sqlite"];
13
+ /**
14
+ * Returns the adapter for a database engine.
15
+ *
16
+ * @throws {CLIValidationError} For an engine with no adapter (e.g. `mongodb`).
17
+ */
18
+ export declare function resolveDatabaseAdapter(database?: string): DatabaseAdapter;
19
+ /** Renders the adapter's environment variables as `.env` lines. */
20
+ export declare function renderDatabaseEnv(database: string | undefined, dbName: string): string;
21
+ //# sourceMappingURL=databaseAdapter.resolver.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * zudojs-cli — Database adapter lookup.
3
+ *
4
+ * `--database mysql|sqlite` used to be accepted and then ignored: every
5
+ * template hard-coded a PostgreSQL `DATABASE_URL`, and the MySQL/SQLite
6
+ * adapters were exported but never called. Templates and the
7
+ * InfrastructureGenerator resolve the adapter here instead. `mongodb` has
8
+ * no adapter, so it is rejected rather than silently turned into Postgres.
9
+ */
10
+ import { CLIValidationError } from "../../errors/index.js";
11
+ import { MySqlAdapter } from "./mysql.adapter.js";
12
+ import { PostgresAdapter } from "./postgres.adapter.js";
13
+ import { SqliteAdapter } from "./sqlite.adapter.js";
14
+ /** Database engines the CLI can scaffold. */
15
+ export const SUPPORTED_DATABASES = ["postgresql", "mysql", "sqlite"];
16
+ const FACTORIES = new Map([
17
+ ["postgresql", () => new PostgresAdapter()],
18
+ ["mysql", () => new MySqlAdapter()],
19
+ ["sqlite", () => new SqliteAdapter()],
20
+ ]);
21
+ /**
22
+ * Returns the adapter for a database engine.
23
+ *
24
+ * @throws {CLIValidationError} For an engine with no adapter (e.g. `mongodb`).
25
+ */
26
+ export function resolveDatabaseAdapter(database = "postgresql") {
27
+ const factory = FACTORIES.get(database);
28
+ if (!factory) {
29
+ throw new CLIValidationError(`Unsupported database "${database}". Supported: ${SUPPORTED_DATABASES.join(", ")}.`);
30
+ }
31
+ return factory();
32
+ }
33
+ /** Renders the adapter's environment variables as `.env` lines. */
34
+ export function renderDatabaseEnv(database, dbName) {
35
+ const variables = resolveDatabaseAdapter(database).getEnvironmentVariables(dbName);
36
+ return Object.entries(variables)
37
+ .map(([key, value]) => `${key}=${value}`)
38
+ .join("\n");
39
+ }
40
+ //# sourceMappingURL=databaseAdapter.resolver.js.map
@@ -1,4 +1,5 @@
1
1
  export { PostgresAdapter } from "./postgres.adapter.js";
2
2
  export { MySqlAdapter } from "./mysql.adapter.js";
3
3
  export { SqliteAdapter } from "./sqlite.adapter.js";
4
+ export { SUPPORTED_DATABASES, renderDatabaseEnv, resolveDatabaseAdapter, } from "./databaseAdapter.resolver.js";
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
1
  export { PostgresAdapter } from "./postgres.adapter.js";
2
2
  export { MySqlAdapter } from "./mysql.adapter.js";
3
3
  export { SqliteAdapter } from "./sqlite.adapter.js";
4
+ export { SUPPORTED_DATABASES, renderDatabaseEnv, resolveDatabaseAdapter, } from "./databaseAdapter.resolver.js";
4
5
  //# sourceMappingURL=index.js.map
@@ -7,6 +7,6 @@ export declare class MySqlAdapter implements DatabaseAdapter {
7
7
  readonly driver = "mysql";
8
8
  getConnectionString(dbName: string): string;
9
9
  getDependencies(): readonly string[];
10
- getEnvironmentVariables(): Record<string, string>;
10
+ getEnvironmentVariables(dbName?: string): Record<string, string>;
11
11
  }
12
12
  //# sourceMappingURL=mysql.adapter.d.ts.map
@@ -10,9 +10,9 @@ export class MySqlAdapter {
10
10
  getDependencies() {
11
11
  return ["@zudojs/database"];
12
12
  }
13
- getEnvironmentVariables() {
13
+ getEnvironmentVariables(dbName = "mydb") {
14
14
  return {
15
- DATABASE_URL: "mysql://localhost:3306/mydb",
15
+ DATABASE_URL: this.getConnectionString(dbName),
16
16
  };
17
17
  }
18
18
  }
@@ -8,13 +8,14 @@ export interface DatabaseAdapter {
8
8
  readonly driver: string;
9
9
  getConnectionString(dbName: string): string;
10
10
  getDependencies(): readonly string[];
11
- getEnvironmentVariables(): Record<string, string>;
11
+ /** Environment variables for a database named `dbName` (default "mydb"). */
12
+ getEnvironmentVariables(dbName?: string): Record<string, string>;
12
13
  }
13
14
  export declare class PostgresAdapter implements DatabaseAdapter {
14
15
  readonly name = "postgresql";
15
16
  readonly driver = "postgres";
16
17
  getConnectionString(dbName: string): string;
17
18
  getDependencies(): readonly string[];
18
- getEnvironmentVariables(): Record<string, string>;
19
+ getEnvironmentVariables(dbName?: string): Record<string, string>;
19
20
  }
20
21
  //# sourceMappingURL=postgres.adapter.d.ts.map
@@ -12,9 +12,9 @@ export class PostgresAdapter {
12
12
  getDependencies() {
13
13
  return ["@zudojs/database"];
14
14
  }
15
- getEnvironmentVariables() {
15
+ getEnvironmentVariables(dbName = "mydb") {
16
16
  return {
17
- DATABASE_URL: "postgresql://localhost:5432/mydb",
17
+ DATABASE_URL: this.getConnectionString(dbName),
18
18
  };
19
19
  }
20
20
  }
@@ -7,6 +7,6 @@ export declare class SqliteAdapter implements DatabaseAdapter {
7
7
  readonly driver = "sqlite";
8
8
  getConnectionString(dbName: string): string;
9
9
  getDependencies(): readonly string[];
10
- getEnvironmentVariables(): Record<string, string>;
10
+ getEnvironmentVariables(dbName?: string): Record<string, string>;
11
11
  }
12
12
  //# sourceMappingURL=sqlite.adapter.d.ts.map
@@ -10,9 +10,9 @@ export class SqliteAdapter {
10
10
  getDependencies() {
11
11
  return ["@zudojs/database"];
12
12
  }
13
- getEnvironmentVariables() {
13
+ getEnvironmentVariables(dbName = "mydb") {
14
14
  return {
15
- DATABASE_URL: "sqlite:mydb.db",
15
+ DATABASE_URL: this.getConnectionString(dbName),
16
16
  };
17
17
  }
18
18
  }
@@ -56,7 +56,7 @@ app.register(createCommand({
56
56
  {
57
57
  name: "database",
58
58
  short: "d",
59
- description: "Database engine (postgresql, mysql, sqlite, mongodb)",
59
+ description: "Database engine (postgresql, mysql, sqlite)",
60
60
  type: "string",
61
61
  defaultValue: "postgresql",
62
62
  },
@@ -176,6 +176,12 @@ app.register(createCommand({
176
176
  type: "boolean",
177
177
  defaultValue: false,
178
178
  },
179
+ {
180
+ name: "force",
181
+ description: "Overwrite existing files the schematic would change",
182
+ type: "boolean",
183
+ defaultValue: false,
184
+ },
179
185
  ],
180
186
  execute: async (context) => {
181
187
  await runGenerateCommand(context);
@@ -64,7 +64,11 @@ export async function runAddCommand(context) {
64
64
  if (!feature) {
65
65
  throw new CLIValidationError(`Feature name is required. Available: ${available}`);
66
66
  }
67
- const packages = FEATURE_PACKAGES[feature];
67
+ // Own keys only: `constructor` or `__proto__` resolved to Object.prototype
68
+ // members and crashed with "packages.join is not a function".
69
+ const packages = Object.hasOwn(FEATURE_PACKAGES, feature)
70
+ ? FEATURE_PACKAGES[feature]
71
+ : undefined;
68
72
  if (!packages) {
69
73
  throw new CLIValidationError(`Unknown feature: "${feature}". Available: ${available}`);
70
74
  }
@@ -12,6 +12,7 @@ import { FrontendGenerator } from "../generators/frontend/frontendGenerator.core
12
12
  import { FullstackComposer } from "../generators/fullstack/fullstackComposer.core.js";
13
13
  import { IntegrationGenerator } from "../generators/integration/integrationGenerator.core.js";
14
14
  import { InfrastructureGenerator } from "../generators/infrastructure/infrastructure.generator.js";
15
+ import { layoutFullstackBackend } from "../generators/fullstack/fullstackBackend.layout.js";
15
16
  import { BackendGenerator } from "../generators/backend/backend.generator.js";
16
17
  import { RollbackManager } from "../rollback/rollbackManager.core.js";
17
18
  import { promptProjectName, promptProjectType, promptConfirmation, } from "../prompts/project/index.js";
@@ -31,7 +32,9 @@ const VALID_ARCHITECTURES = [
31
32
  "modular-monolith",
32
33
  "microservice",
33
34
  ];
34
- const VALID_DATABASES = ["postgresql", "mysql", "sqlite", "mongodb"];
35
+ // No mongodb: there is no adapter for it, and it used to be accepted and
36
+ // scaffolded as PostgreSQL (tooling/CLI-04).
37
+ const VALID_DATABASES = ["postgresql", "mysql", "sqlite"];
35
38
  const VALID_PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
36
39
  const SERVICE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
37
40
  const VALID_FRONTENDS = [
@@ -285,6 +288,17 @@ export async function runCreateCommand(context) {
285
288
  if (!answers.projectName) {
286
289
  throw new CLIValidationError("Project name is required.");
287
290
  }
291
+ // Backend templates only emit TypeScript. `--language javascript` used to
292
+ // be accepted and ignored (tooling/CLI-09); in a fullstack project it
293
+ // applies to the frontend only, which is said out loud.
294
+ if (answers.language === "javascript") {
295
+ if (answers.projectType === "backend") {
296
+ throw new CLIValidationError("Backend projects are generated in TypeScript only; --language javascript is not supported for --type backend.");
297
+ }
298
+ if (answers.projectType === "fullstack") {
299
+ context.logger.warn("--language javascript applies to the frontend (apps/web) only; the backend is generated in TypeScript.");
300
+ }
301
+ }
288
302
  await createProject(answers, context);
289
303
  }
290
304
  async function createProject(options, context) {
@@ -453,12 +467,11 @@ async function generateFullstackProject(options, projectPath, rollback) {
453
467
  }
454
468
  // Uses the same BackendGenerator the generator registry exposes rather
455
469
  // than a third private copy of the architecture switch.
456
- const backendFiles = await new BackendGenerator().generate(options, join(projectPath, "apps/api"));
457
- // The backend template may carry its own workspace definition; nested in
458
- // apps/api it would create a second workspace root, so strip it.
459
- delete backendFiles["pnpm-workspace.yaml"];
460
- await writeFileTree(join(projectPath, "apps/api"), backendFiles);
461
- rollback.trackDirectory(join(projectPath, "apps/api"));
470
+ const backend = layoutFullstackBackend(options.architecture, await new BackendGenerator().generate(options, join(projectPath, "apps/api")));
471
+ await writeFileTree(join(projectPath, backend.directory), backend.files);
472
+ for (const directory of backend.createdDirectories) {
473
+ rollback.trackDirectory(join(projectPath, directory));
474
+ }
462
475
  const integrationGenerator = new IntegrationGenerator();
463
476
  await integrationGenerator.generate({
464
477
  project: {
@@ -492,6 +505,7 @@ async function generateFullstackProject(options, projectPath, rollback) {
492
505
  database: options.database ?? "postgresql",
493
506
  packageManager: options.packageManager,
494
507
  services: options.services,
508
+ appDirectory: "apps/api",
495
509
  }, projectPath);
496
510
  }
497
511
  async function generateFrontendProject(options, projectPath) {
@@ -5,6 +5,7 @@
5
5
  * Reads the project manifest to determine the architecture and where the
6
6
  * backend lives, then places the schematic accordingly.
7
7
  */
8
+ import { join } from "node:path";
8
9
  import { generateService } from "../generators/service/service.generator.js";
9
10
  import { generateModule } from "../generators/module/module.generator.js";
10
11
  import { generateCommand } from "../generators/command/command.generator.js";
@@ -21,6 +22,7 @@ import { generateValidator } from "../generators/validator/validator.generator.j
21
22
  import { CLIGenerationError, CLIValidationError } from "../errors/index.js";
22
23
  import { assertGeneratableName, assertSafePathSegment, } from "../utils/utils.name.js";
23
24
  import { resolveProjectLayout } from "../resolvers/layout/projectLayout.core.js";
25
+ import { captureWrites, findWriteConflicts } from "../utils/utils.writeGuard.js";
24
26
  const VALID_SCHEMATICS = [
25
27
  "service",
26
28
  "module",
@@ -71,9 +73,14 @@ function getArchitectureRoot(architecture, schematic, serviceName) {
71
73
  }
72
74
  // `--service <name>` selects which app the schematic belongs to.
73
75
  // Without it the gateway app — the one app always generated — is used.
74
- return serviceName !== undefined
75
- ? `apps/services/${serviceName}/src`
76
- : "apps/gateway/src";
76
+ // Modules go to that app's src/modules, next to the scaffolded ones,
77
+ // so they can be registered in its app.ts.
78
+ {
79
+ const appRoot = serviceName !== undefined
80
+ ? `apps/services/${serviceName}/src`
81
+ : "apps/gateway/src";
82
+ return schematic === "module" ? `${appRoot}/modules` : appRoot;
83
+ }
77
84
  case "monolith":
78
85
  default:
79
86
  if (schematic === "module") {
@@ -92,7 +99,11 @@ function getArchitectureRoot(architecture, schematic, serviceName) {
92
99
  function backendPrefix(cwd) {
93
100
  const layout = resolveProjectLayout(cwd);
94
101
  const [first] = layout?.backendDirs ?? [];
95
- if (layout?.projectType === "fullstack" && first !== undefined && first !== cwd) {
102
+ // Only a monolith fullstack backend lives in apps/api; a microservice
103
+ // fullstack project keeps apps/gateway and apps/services at the root.
104
+ if (layout?.projectType === "fullstack" &&
105
+ first !== undefined &&
106
+ first === join(cwd, "apps", "api")) {
96
107
  return "apps/api/";
97
108
  }
98
109
  return "";
@@ -103,6 +114,7 @@ export async function runGenerateCommand(context) {
103
114
  const service = context.values.service;
104
115
  const moduleName = context.values.module;
105
116
  const dryRun = context.values["dry-run"] === true;
117
+ const force = context.values.force === true;
106
118
  if (!schematic ||
107
119
  !VALID_SCHEMATICS.includes(schematic)) {
108
120
  throw new CLIValidationError(`Schematic name is required. Available: ${VALID_SCHEMATICS.join(", ")}`);
@@ -137,11 +149,30 @@ export async function runGenerateCommand(context) {
137
149
  if (architecture === "modular-monolith" && schematic === "service") {
138
150
  context.logger.info('Mapping "service" → "module" for modular-monolith architecture.');
139
151
  }
140
- const result = await runSchematic(schematic, name, {
152
+ const schematicOptions = {
141
153
  service,
142
154
  module: moduleName,
143
155
  dryRun,
144
156
  architecture: architecture ?? undefined,
157
+ };
158
+ if (!dryRun && !force) {
159
+ const planned = await captureWrites(() => runSchematic(schematic, name, schematicOptions, cwd));
160
+ const conflicts = findWriteConflicts(cwd, planned);
161
+ if (conflicts.length > 0) {
162
+ throw new CLIValidationError(`Refusing to overwrite existing files:\n${conflicts
163
+ .map((file) => ` - ${file}`)
164
+ .join("\n")}\nRe-run with --force to overwrite them.`);
165
+ }
166
+ }
167
+ const result = await runSchematic(schematic, name, {
168
+ ...schematicOptions,
169
+ onModuleRegistered: (registration) => {
170
+ if (registration.registered)
171
+ return;
172
+ context.logger.warn(`Could not register the module in app.ts automatically. Add:\n${registration.manualSteps
173
+ .map((step) => ` ${step}`)
174
+ .join("\n")}`);
175
+ },
145
176
  }, cwd);
146
177
  if (dryRun) {
147
178
  context.logger.info(`Dry run: ${result.length} files would be generated (nothing written):`);
@@ -167,7 +198,7 @@ async function runSchematic(schematic, name, options, cwd) {
167
198
  case "service":
168
199
  return await generateService({ name, basePath, dryRun }, cwd);
169
200
  case "module":
170
- return await generateModule({ name, feature: true, basePath, dryRun }, cwd);
201
+ return await generateModule({ name, feature: true, basePath, dryRun, onRegistered: options.onModuleRegistered }, cwd);
171
202
  case "command":
172
203
  return await generateCommand({ name, ...(cqrsService ? { service: cqrsService } : {}), basePath, dryRun }, cwd);
173
204
  case "query":
@@ -1,19 +1,8 @@
1
1
  /**
2
2
  * zudojs-cli — Errors
3
3
  *
4
- * Error classes for the CLI scaffolding system.
4
+ * Error classes for the CLI scaffolding system. They live in
5
+ * `@zudojs/errors` and are re-exported here for existing imports.
5
6
  */
6
- import { ApplicationError } from "@zudojs/errors";
7
- export declare class CLIValidationError extends ApplicationError {
8
- constructor(message: string);
9
- }
10
- export declare class CLIGenerationError extends ApplicationError {
11
- constructor(message: string, cause?: unknown);
12
- }
13
- export declare class CLINotInProjectError extends ApplicationError {
14
- constructor();
15
- }
16
- export declare class CLITemplateError extends ApplicationError {
17
- constructor(message: string);
18
- }
7
+ export { CLIValidationError, CLIGenerationError, CLINotInProjectError, CLITemplateError, } from "@zudojs/errors";
19
8
  //# sourceMappingURL=index.d.ts.map
@@ -1,29 +1,8 @@
1
1
  /**
2
2
  * zudojs-cli — Errors
3
3
  *
4
- * Error classes for the CLI scaffolding system.
4
+ * Error classes for the CLI scaffolding system. They live in
5
+ * `@zudojs/errors` and are re-exported here for existing imports.
5
6
  */
6
- import { ApplicationError } from "@zudojs/errors";
7
- export class CLIValidationError extends ApplicationError {
8
- constructor(message) {
9
- super(message, { isOperational: true });
10
- }
11
- }
12
- export class CLIGenerationError extends ApplicationError {
13
- constructor(message, cause) {
14
- super(message, { cause });
15
- }
16
- }
17
- export class CLINotInProjectError extends ApplicationError {
18
- constructor() {
19
- super("This command must be run inside a Zudojs project directory.", {
20
- isOperational: true,
21
- });
22
- }
23
- }
24
- export class CLITemplateError extends ApplicationError {
25
- constructor(message) {
26
- super(message);
27
- }
28
- }
7
+ export { CLIValidationError, CLIGenerationError, CLINotInProjectError, CLITemplateError, } from "@zudojs/errors";
29
8
  //# sourceMappingURL=index.js.map
@@ -13,6 +13,7 @@
13
13
  import { existsSync, readFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { writeFileTree } from "../../utils/utils.fileSystem.js";
16
+ import { CLIGenerationError } from "../../errors/index.js";
16
17
  /**
17
18
  * Rejects package names a package manager would read as a flag.
18
19
  *
@@ -107,18 +108,21 @@ async function recordDependencies(projectPath, dependencies, devDependencies) {
107
108
  const pkgPath = join(projectPath, "package.json");
108
109
  let pkg = {};
109
110
  if (existsSync(pkgPath)) {
111
+ // Fail the scaffold rather than replace an unparsable package.json with
112
+ // one holding only dependencies: that silently dropped name, scripts and
113
+ // type (tooling/CLI-07).
110
114
  try {
111
115
  pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
112
116
  }
113
- catch {
114
- pkg = {};
117
+ catch (error) {
118
+ throw new CLIGenerationError(`Could not parse ${pkgPath}; refusing to overwrite it with only the resolved dependencies.`, error);
115
119
  }
116
120
  }
117
121
  const merge = (target, entries) => {
118
122
  const merged = { ...(target ?? {}) };
119
123
  for (const entry of entries) {
120
124
  if (!(entry.name in merged)) {
121
- merged[entry.name] = entry.version || "latest";
125
+ merged[entry.name] = entry.version;
122
126
  }
123
127
  }
124
128
  return merged;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * zudojs-cli — Where a fullstack project's backend files go.
3
+ *
4
+ * A monolith backend is a single app written to `apps/api`. A microservice
5
+ * backend is a set of apps, and nesting it under `apps/api` put the gateway
6
+ * and services at `apps/api/apps/*`, outside the root workspace globs, so
7
+ * their dependencies were never installed and the root docker-compose built
8
+ * directories holding only a Dockerfile. Its apps are written at the root
9
+ * instead (`apps/gateway`, `apps/services/<name>`), next to `apps/web`.
10
+ */
11
+ /** Backend files and the directory they are written under. */
12
+ export interface FullstackBackendLayout {
13
+ /** Directory relative to the project root ("" for the root). */
14
+ readonly directory: string;
15
+ /** Files relative to `directory`. */
16
+ readonly files: Record<string, string>;
17
+ /** Directories created, relative to the project root, for rollback. */
18
+ readonly createdDirectories: readonly string[];
19
+ }
20
+ /**
21
+ * Places generated backend files inside a fullstack project. Root-level
22
+ * files of a microservice backend (its own package.json, workspace file,
23
+ * compose file, README) are dropped: the fullstack root owns those.
24
+ */
25
+ export declare function layoutFullstackBackend(architecture: string, backendFiles: Readonly<Record<string, string>>): FullstackBackendLayout;
26
+ //# sourceMappingURL=fullstackBackend.layout.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * zudojs-cli — Where a fullstack project's backend files go.
3
+ *
4
+ * A monolith backend is a single app written to `apps/api`. A microservice
5
+ * backend is a set of apps, and nesting it under `apps/api` put the gateway
6
+ * and services at `apps/api/apps/*`, outside the root workspace globs, so
7
+ * their dependencies were never installed and the root docker-compose built
8
+ * directories holding only a Dockerfile. Its apps are written at the root
9
+ * instead (`apps/gateway`, `apps/services/<name>`), next to `apps/web`.
10
+ */
11
+ /**
12
+ * Places generated backend files inside a fullstack project. Root-level
13
+ * files of a microservice backend (its own package.json, workspace file,
14
+ * compose file, README) are dropped: the fullstack root owns those.
15
+ */
16
+ export function layoutFullstackBackend(architecture, backendFiles) {
17
+ if (architecture !== "microservice") {
18
+ const files = { ...backendFiles };
19
+ delete files["pnpm-workspace.yaml"];
20
+ return { directory: "apps/api", files, createdDirectories: ["apps/api"] };
21
+ }
22
+ const files = {};
23
+ for (const [path, content] of Object.entries(backendFiles)) {
24
+ if (path.startsWith("apps/gateway/") || path.startsWith("apps/services/")) {
25
+ files[path] = content;
26
+ }
27
+ }
28
+ return {
29
+ directory: "",
30
+ files,
31
+ createdDirectories: ["apps/gateway", "apps/services"],
32
+ };
33
+ }
34
+ //# sourceMappingURL=fullstackBackend.layout.js.map
@@ -63,7 +63,11 @@ export class FullstackComposer {
63
63
  }
64
64
  async createWorkspace(context) {
65
65
  const packageManager = this.getPackageManager(context);
66
- const workspaceGlobs = ["apps/*", "packages/*"];
66
+ // A microservice backend lives at apps/gateway and apps/services/<name>
67
+ // (tooling/CLI-02); `apps/*` alone does not reach the services.
68
+ const workspaceGlobs = context.project.backend?.architecture === "microservice"
69
+ ? ["apps/*", "apps/services/*", "packages/*"]
70
+ : ["apps/*", "packages/*"];
67
71
  const scriptFor = (script) => {
68
72
  switch (packageManager) {
69
73
  case "npm":
@@ -4,4 +4,5 @@
4
4
  * Composes fullstack projects by orchestrating backend, frontend, and integration generators.
5
5
  */
6
6
  export { FullstackComposer } from "./fullstackComposer.core.js";
7
+ export { layoutFullstackBackend, type FullstackBackendLayout, } from "./fullstackBackend.layout.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,5 @@
4
4
  * Composes fullstack projects by orchestrating backend, frontend, and integration generators.
5
5
  */
6
6
  export { FullstackComposer } from "./fullstackComposer.core.js";
7
+ export { layoutFullstackBackend, } from "./fullstackBackend.layout.js";
7
8
  //# sourceMappingURL=index.js.map
@@ -9,23 +9,18 @@ export interface InfrastructureOptions {
9
9
  readonly database: string;
10
10
  readonly packageManager: string;
11
11
  readonly services?: readonly string[];
12
+ /**
13
+ * Directory holding the non-microservice server, relative to the project
14
+ * root. The fullstack scaffold passes `"apps/api"`; defaults to `"."`.
15
+ */
16
+ readonly appDirectory?: string;
12
17
  }
13
18
  export declare class InfrastructureGenerator {
14
19
  generate(options: InfrastructureOptions, basePath: string): Promise<void>;
15
20
  private getFiles;
16
- /**
17
- * Install and build commands for Dockerfiles.
18
- *
19
- * Generated projects have no lockfile yet, so never use `npm ci` or
20
- * `--frozen-lockfile`. pnpm and yarn need `corepack enable` on the bare
21
- * node:24-alpine image; bun is not available there so it falls back to npm.
22
- */
23
- private getDockerCommands;
24
21
  private getDatabaseCompose;
25
22
  private getDatabaseUrl;
26
23
  private getDbServiceBlock;
27
- private getAppDockerfile;
28
- private getServiceDockerfile;
29
24
  private getSimpleDockerCompose;
30
25
  private getDockerCompose;
31
26
  }