zudojs-cli 1.0.0 → 1.1.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 (68) hide show
  1. package/README.md +16 -6
  2. package/dist/src/adapters/frontend/frontendAdapter.type.d.ts +6 -0
  3. package/dist/src/bin/zudojs.js +8 -32
  4. package/dist/src/cliApplication/cliApplication.core.js +2 -2
  5. package/dist/src/cliApplication/cliApplication.logger.d.ts +28 -0
  6. package/dist/src/cliApplication/cliApplication.logger.js +68 -0
  7. package/dist/src/cliApplication/index.d.ts +1 -3
  8. package/dist/src/cliApplication/index.js +1 -3
  9. package/dist/src/cliVersion/cliVersion.update.d.ts +38 -0
  10. package/dist/src/cliVersion/cliVersion.update.js +63 -0
  11. package/dist/src/cliVersion/index.d.ts +2 -1
  12. package/dist/src/cliVersion/index.js +2 -1
  13. package/dist/src/commands/add.command.d.ts +29 -0
  14. package/dist/src/commands/add.command.js +94 -109
  15. package/dist/src/commands/build.command.d.ts +8 -0
  16. package/dist/src/commands/build.command.js +12 -18
  17. package/dist/src/commands/create.command.js +8 -5
  18. package/dist/src/commands/dev.command.d.ts +30 -2
  19. package/dist/src/commands/dev.command.js +104 -183
  20. package/dist/src/commands/doctor.command.d.ts +18 -0
  21. package/dist/src/commands/doctor.command.js +141 -113
  22. package/dist/src/commands/generate.command.d.ts +2 -1
  23. package/dist/src/commands/generate.command.js +23 -42
  24. package/dist/src/commands/info.command.js +61 -30
  25. package/dist/src/constants/index.d.ts +18 -2
  26. package/dist/src/constants/index.js +31 -2
  27. package/dist/src/generators/frontend/frontendGenerator.core.d.ts +2 -0
  28. package/dist/src/generators/frontend/frontendGenerator.core.js +11 -0
  29. package/dist/src/generators/frontend/frontendPipeline.js +45 -2
  30. package/dist/src/generators/fullstack/fullstackComposer.core.d.ts +2 -0
  31. package/dist/src/generators/fullstack/fullstackComposer.core.js +8 -22
  32. package/dist/src/index.d.ts +3 -2
  33. package/dist/src/index.js +3 -2
  34. package/dist/src/installers/dependency.installer.d.ts +9 -0
  35. package/dist/src/installers/dependency.installer.js +21 -0
  36. package/dist/src/installers/index.d.ts +1 -3
  37. package/dist/src/installers/index.js +1 -3
  38. package/dist/src/resolvers/architecture.resolver.d.ts +11 -1
  39. package/dist/src/resolvers/architecture.resolver.js +12 -13
  40. package/dist/src/resolvers/configuration/configurationResolver.core.d.ts +10 -1
  41. package/dist/src/resolvers/configuration/configurationResolver.core.js +68 -29
  42. package/dist/src/resolvers/index.d.ts +1 -0
  43. package/dist/src/resolvers/index.js +1 -0
  44. package/dist/src/resolvers/layout/index.d.ts +5 -0
  45. package/dist/src/resolvers/layout/index.js +5 -0
  46. package/dist/src/resolvers/layout/projectLayout.core.d.ts +60 -0
  47. package/dist/src/resolvers/layout/projectLayout.core.js +236 -0
  48. package/dist/src/resolvers/project.resolver.d.ts +5 -0
  49. package/dist/src/resolvers/project.resolver.js +10 -6
  50. package/dist/src/templates/microservice/microservice.template.d.ts +0 -1
  51. package/dist/src/templates/microservice/microservice.template.js +2 -10
  52. package/dist/src/templates/modular-monolith/modularMonolith.template.d.ts +0 -1
  53. package/dist/src/templates/modular-monolith/modularMonolith.template.js +4 -7
  54. package/dist/src/templates/monolith/monolith.template.d.ts +0 -1
  55. package/dist/src/templates/monolith/monolith.template.js +4 -7
  56. package/dist/src/templates/shared/appRuntime.template.d.ts +0 -11
  57. package/dist/src/templates/shared/appRuntime.template.js +0 -20
  58. package/dist/src/templates/shared/index.d.ts +1 -0
  59. package/dist/src/templates/shared/index.js +1 -0
  60. package/dist/src/templates/shared/pnpm.template.d.ts +27 -0
  61. package/dist/src/templates/shared/pnpm.template.js +57 -0
  62. package/dist/src/utils/utils.detect.d.ts +9 -0
  63. package/dist/src/utils/utils.detect.js +11 -11
  64. package/dist/src/utils/utils.exec.d.ts +17 -1
  65. package/dist/src/utils/utils.exec.js +65 -4
  66. package/package.json +8 -6
  67. package/dist/src/scripts/postinstall.cjs +0 -77
  68. package/src/scripts/postinstall.cjs +0 -77
@@ -1,15 +1,85 @@
1
1
  /**
2
2
  * zudojs-cli — Dev Command
3
3
  *
4
- * The `zudojs dev` command.
5
- * Starts development servers based on project configuration.
4
+ * The `zudojs dev` command. Starts the development servers of the project
5
+ * in the current directory.
6
+ *
7
+ * Servers are started through the project's package manager (`pnpm run
8
+ * dev`, `npm run dev`, …) rather than by spawning `tsx` or `ng` directly:
9
+ * those binaries are devDependencies of the generated project and are not
10
+ * on the user's PATH, so the direct spawn failed with ENOENT for everyone
11
+ * who had not installed them globally. Running the script also means the
12
+ * project's own `dev` script is what runs — `tsx watch src` used to be
13
+ * hard-coded here, which watched `src/index.ts`, a file that only exports
14
+ * `createApp` and never starts the server.
6
15
  */
7
- import { existsSync, readFileSync } from "node:fs";
8
- import { join } from "node:path";
16
+ import { existsSync } from "node:fs";
17
+ import { basename, join, relative } from "node:path";
9
18
  import { runStreaming } from "../utils/utils.exec.js";
10
19
  import { CLIValidationError, CLIGenerationError } from "../errors/index.js";
11
- import { ManifestManager } from "../manifest/manifestManager.core.js";
12
- import { SAFE_PATH_SEGMENT } from "../utils/utils.name.js";
20
+ import { getRunScriptCommand } from "../installers/dependency.installer.js";
21
+ import { resolveProjectLayout, } from "../resolvers/layout/projectLayout.core.js";
22
+ /**
23
+ * Computes the servers `zudojs dev` starts for a project layout.
24
+ *
25
+ * Exported so the selection can be tested without spawning anything.
26
+ */
27
+ export function planDevServers(layout, options = {}) {
28
+ const servers = [];
29
+ const env = options.port !== undefined
30
+ ? { ...process.env, PORT: String(options.port) }
31
+ : undefined;
32
+ if (!options.frontendOnly) {
33
+ for (const dir of layout.backendDirs) {
34
+ if (!existsSync(join(dir, "package.json")))
35
+ continue;
36
+ const [file, ...args] = getRunScriptCommand(layout.packageManager, "dev");
37
+ servers.push({
38
+ label: labelFor(layout.root, dir, "backend"),
39
+ cwd: dir,
40
+ file,
41
+ args,
42
+ ...(env ? { env } : {}),
43
+ });
44
+ }
45
+ }
46
+ if (!options.backendOnly && layout.frontendDir !== undefined) {
47
+ const spec = frontendServer(layout, layout.frontendDir);
48
+ if (spec)
49
+ servers.push(spec);
50
+ }
51
+ return servers;
52
+ }
53
+ function labelFor(root, dir, fallback) {
54
+ const rel = relative(root, dir);
55
+ return rel === "" ? fallback : basename(dir);
56
+ }
57
+ function frontendServer(layout, dir) {
58
+ const framework = layout.frontendFramework ?? "react";
59
+ if (framework === "none")
60
+ return null;
61
+ if (framework === "flutter") {
62
+ if (!existsSync(join(dir, "pubspec.yaml")))
63
+ return null;
64
+ return {
65
+ label: "web",
66
+ cwd: dir,
67
+ file: "flutter",
68
+ args: ["run", "--debug"],
69
+ };
70
+ }
71
+ if (!existsSync(join(dir, "package.json")))
72
+ return null;
73
+ // Angular's package.json has `start: ng serve`; React Native's has `start`.
74
+ const script = framework === "angular" || framework === "react-native" ? "start" : "dev";
75
+ const [file, ...args] = getRunScriptCommand(layout.packageManager, script);
76
+ return {
77
+ label: labelFor(layout.root, dir, "frontend"),
78
+ cwd: dir,
79
+ file,
80
+ args,
81
+ };
82
+ }
13
83
  export async function runDevCommand(context) {
14
84
  const frontendOnly = context.values["frontend-only"] === true;
15
85
  const backendOnly = context.values["backend-only"] === true;
@@ -17,194 +87,45 @@ export async function runDevCommand(context) {
17
87
  if (frontendOnly && backendOnly) {
18
88
  throw new CLIValidationError("Cannot use --frontend-only and --backend-only together.");
19
89
  }
20
- const manifest = await new ManifestManager(context.cwd).read();
21
- const config = readProjectConfig(context.cwd) ?? configFromManifest(manifest);
22
- if (!config) {
23
- throw new CLIValidationError("No Zudojs project found. Run `zudojs create` first.");
90
+ const layout = resolveProjectLayout(context.cwd);
91
+ if (!layout) {
92
+ throw new CLIValidationError("No Zudojs project found in this directory. Run `zudojs create` first.");
24
93
  }
25
- context.logger.info(`Starting development server for: ${config.name}`);
26
- context.logger.info(`Type: ${config.type}`);
27
- if (config.backend) {
28
- context.logger.info(`Backend architecture: ${config.backend.architecture}`);
94
+ context.logger.info(`Project type: ${layout.projectType}`);
95
+ if (layout.backendDirs.length > 0) {
96
+ context.logger.info(`Backend architecture: ${layout.architecture}`);
29
97
  }
30
- if (config.frontend) {
31
- context.logger.info(`Frontend: ${config.frontend.framework}`);
98
+ if (layout.frontendFramework) {
99
+ context.logger.info(`Frontend: ${layout.frontendFramework}`);
32
100
  }
33
- // Service names come from a manifest file on disk and are joined into
34
- // filesystem paths below; a crafted entry such as "../../.." would make the
35
- // dev command probe and run outside the project.
36
- const services = (manifest?.services ?? []).filter((service) => {
37
- if (SAFE_PATH_SEGMENT.test(service))
38
- return true;
39
- context.logger.warn(`Ignoring invalid service name in .zudojs/manifest.json: "${service}"`);
40
- return false;
101
+ const servers = planDevServers(layout, {
102
+ frontendOnly,
103
+ backendOnly,
104
+ ...(port !== undefined ? { port } : {}),
41
105
  });
42
- const processes = [];
43
- if (!frontendOnly &&
44
- (config.type === "backend" || config.type === "fullstack")) {
45
- if (config.backend?.architecture === "microservice") {
46
- processes.push(...startMicroserviceDev(context.cwd, services));
47
- }
48
- else {
49
- processes.push(startBackendDev(context.cwd, config, port));
50
- }
51
- }
52
- if (!backendOnly &&
53
- (config.type === "frontend" || config.type === "fullstack")) {
54
- if (config.frontend && config.frontend.framework !== "none") {
55
- processes.push(startFrontendDev(context.cwd, config));
56
- }
57
- }
58
- if (processes.length === 0) {
106
+ if (servers.length === 0) {
59
107
  context.logger.warn("No development servers to start.");
60
108
  return;
61
109
  }
62
- context.logger.info("Starting development servers...");
110
+ for (const server of servers) {
111
+ context.logger.info(`Starting ${server.label}: ${server.file} ${server.args.join(" ")}`);
112
+ }
113
+ // One controller for every server: when any of them exits with an error
114
+ // the others are stopped too, instead of being left running detached
115
+ // after the command has already reported failure.
116
+ const controller = new AbortController();
117
+ const runs = servers.map((server) => runStreaming(server.file, server.args, server.cwd, {
118
+ signal: controller.signal,
119
+ ...(server.env ? { env: server.env } : {}),
120
+ }).catch((error) => {
121
+ controller.abort();
122
+ throw error;
123
+ }));
63
124
  try {
64
- await Promise.all(processes);
125
+ await Promise.all(runs);
65
126
  }
66
127
  catch (error) {
67
128
  throw new CLIGenerationError("Development server failed to start.", error);
68
129
  }
69
130
  }
70
- function configFromManifest(manifest) {
71
- if (!manifest)
72
- return null;
73
- return {
74
- name: "zudojs-project",
75
- type: manifest.projectType ?? "backend",
76
- backend: manifest.backend
77
- ? { architecture: manifest.backend.architecture }
78
- : { architecture: manifest.architecture },
79
- frontend: manifest.frontend
80
- ? { framework: manifest.frontend.framework }
81
- : undefined,
82
- };
83
- }
84
- function readProjectConfig(cwd) {
85
- const configPath = join(cwd, "zudojs.config.ts");
86
- const configPathJs = join(cwd, "zudojs.config.js");
87
- if (existsSync(configPath)) {
88
- const content = readFileSync(configPath, "utf-8");
89
- return parseConfigContent(content, cwd);
90
- }
91
- if (existsSync(configPathJs)) {
92
- const content = readFileSync(configPathJs, "utf-8");
93
- return parseConfigContent(content, cwd);
94
- }
95
- const pkgPath = join(cwd, "package.json");
96
- if (existsSync(pkgPath)) {
97
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
98
- if (pkg.zudojs) {
99
- return {
100
- name: pkg.name ?? "unknown",
101
- type: pkg.zudojs.projectType ?? "backend",
102
- backend: pkg.zudojs.architecture
103
- ? { architecture: pkg.zudojs.architecture }
104
- : undefined,
105
- frontend: pkg.zudojs.frontend
106
- ? { framework: pkg.zudojs.frontend }
107
- : undefined,
108
- };
109
- }
110
- }
111
- return null;
112
- }
113
- function parseConfigContent(content, cwd) {
114
- const nameMatch = content.match(/name:\s*["']([^"']+)["']/);
115
- const typeMatch = content.match(/projectType:\s*["'](\w+)["']/);
116
- const architectureMatch = content.match(/architecture:\s*["'](\w+)["']/);
117
- const frontendMatch = content.match(/frontend:\s*\{[\s\S]*?framework:\s*["']([^"']+)["']/);
118
- const pkgPath = join(cwd, "package.json");
119
- let name = "unknown";
120
- if (existsSync(pkgPath)) {
121
- try {
122
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
123
- name = pkg.name ?? name;
124
- }
125
- catch {
126
- // ignore
127
- }
128
- }
129
- return {
130
- name: nameMatch?.[1] ?? name,
131
- type: typeMatch?.[1] ?? "backend",
132
- backend: architectureMatch?.[1]
133
- ? { architecture: architectureMatch[1] }
134
- : undefined,
135
- frontend: frontendMatch?.[1] ? { framework: frontendMatch[1] } : undefined,
136
- };
137
- }
138
- async function startBackendDev(cwd, config, port) {
139
- // `--port=N` was appended to the tsx argv, where it became an argument of
140
- // the watched script and was ignored. Generated servers read PORT from the
141
- // environment, so that is where the option has to land.
142
- const options = port !== undefined
143
- ? { env: { ...process.env, PORT: String(port) } }
144
- : undefined;
145
- const entry = config.backend?.architecture === "microservice"
146
- ? "apps/gateway/src"
147
- : "src";
148
- await runStreaming("tsx", ["watch", entry], cwd, options);
149
- }
150
- function startMicroserviceDev(cwd, services) {
151
- const serviceDirs = services.map((service) => {
152
- const nested = join(cwd, "apps", "services", service);
153
- return existsSync(join(nested, "src"))
154
- ? nested
155
- : join(cwd, "apps", service);
156
- });
157
- const promises = [];
158
- const gatewayDir = join(cwd, "apps", "gateway");
159
- if (existsSync(join(gatewayDir, "src"))) {
160
- promises.push(runStreaming("tsx", ["watch", "src"], gatewayDir));
161
- }
162
- for (const dir of serviceDirs) {
163
- if (existsSync(join(dir, "src"))) {
164
- promises.push(runStreaming("tsx", ["watch", "src"], dir));
165
- }
166
- }
167
- return promises;
168
- }
169
- async function startFrontendDev(cwd, config) {
170
- const framework = config.frontend?.framework ?? "react";
171
- const frontendDir = join(cwd, "apps", "web");
172
- switch (framework) {
173
- case "react":
174
- await runStreaming("npm", ["run", "dev"], frontendDir);
175
- break;
176
- case "next":
177
- await runStreaming("npm", ["run", "dev"], frontendDir);
178
- break;
179
- case "vue":
180
- await runStreaming("npm", ["run", "dev"], frontendDir);
181
- break;
182
- case "nuxt":
183
- await runStreaming("npm", ["run", "dev"], frontendDir);
184
- break;
185
- case "angular":
186
- await runStreaming("ng", ["serve"], frontendDir);
187
- break;
188
- case "svelte":
189
- await runStreaming("npm", ["run", "dev"], frontendDir);
190
- break;
191
- case "sveltekit":
192
- await runStreaming("npm", ["run", "dev"], frontendDir);
193
- break;
194
- case "astro":
195
- await runStreaming("npm", ["run", "dev"], frontendDir);
196
- break;
197
- case "vanilla":
198
- await runStreaming("npm", ["run", "dev"], frontendDir);
199
- break;
200
- case "flutter":
201
- await runStreaming("flutter", ["run", "--debug"], frontendDir);
202
- break;
203
- case "react-native":
204
- await runStreaming("npx", ["react-native", "start"], frontendDir);
205
- break;
206
- default:
207
- await runStreaming("npm", ["run", "dev"], frontendDir);
208
- }
209
- }
210
131
  //# sourceMappingURL=dev.command.js.map
@@ -4,5 +4,23 @@
4
4
  * The `zudojs doctor` command for project diagnostics.
5
5
  */
6
6
  import type { CLIContext } from "../cliType/cliType.type.js";
7
+ export interface DoctorCheck {
8
+ readonly name: string;
9
+ readonly passed: boolean;
10
+ readonly message: string;
11
+ /**
12
+ * Whether a failed check is fatal. Advisory checks (a missing lock file in
13
+ * a project created with `--no-install`, for example) are reported as
14
+ * warnings and do not fail the command.
15
+ */
16
+ readonly severity: "error" | "warning";
17
+ }
18
+ /**
19
+ * Runs every diagnostic for the project at `cwd`.
20
+ *
21
+ * Exported so the checks can be run against a scaffolded directory in tests
22
+ * without going through the logger.
23
+ */
24
+ export declare function runDoctorChecks(cwd: string): DoctorCheck[];
7
25
  export declare function runDoctorCommand(context: CLIContext): Promise<void>;
8
26
  //# sourceMappingURL=doctor.command.d.ts.map
@@ -4,22 +4,33 @@
4
4
  * The `zudojs doctor` command for project diagnostics.
5
5
  */
6
6
  import { existsSync, readFileSync } from "node:fs";
7
- import { join } from "node:path";
7
+ import { join, relative } from "node:path";
8
8
  import { CLIValidationError } from "../errors/index.js";
9
+ import { FEATURE_PACKAGES } from "../constants/index.js";
10
+ import { resolveProjectLayout, } from "../resolvers/layout/projectLayout.core.js";
11
+ /**
12
+ * Runs every diagnostic for the project at `cwd`.
13
+ *
14
+ * Exported so the checks can be run against a scaffolded directory in tests
15
+ * without going through the logger.
16
+ */
17
+ export function runDoctorChecks(cwd) {
18
+ const layout = resolveProjectLayout(cwd);
19
+ const checks = [checkNodeVersion(), checkProject(layout)];
20
+ if (!layout) {
21
+ return checks;
22
+ }
23
+ checks.push(checkPackageManager(layout), checkInstalled(layout), checkTypeScriptConfig(layout), checkDependencies(layout), checkFeatures(layout));
24
+ return checks;
25
+ }
9
26
  export async function runDoctorCommand(context) {
10
- const checks = [];
27
+ const checks = runDoctorChecks(context.cwd);
11
28
  const warnings = [];
12
29
  const errors = [];
13
- checks.push(checkNodeVersion());
14
- checks.push(checkPackageManager(context.cwd));
15
- checks.push(checkTypeScriptConfig(context.cwd));
16
- checks.push(checkZudojsConfig(context.cwd));
17
- checks.push(checkDependencies(context));
18
- checks.push(checkArchitectureViolations(context.cwd));
19
30
  context.logger.info("Zudojs Doctor - Project Diagnostics");
20
31
  context.logger.info("");
21
32
  for (const check of checks) {
22
- const symbol = check.passed ? "✔" : "✖";
33
+ const symbol = check.passed ? "✔" : check.severity === "error" ? "✖" : "⚠";
23
34
  context.logger.info(`${symbol} ${check.name}: ${check.message}`);
24
35
  if (!check.passed) {
25
36
  if (check.severity === "warning") {
@@ -61,153 +72,170 @@ function checkNodeVersion() {
61
72
  severity: "error",
62
73
  passed,
63
74
  message: passed
64
- ? `Node.js ${version} (✓ meets minimum v24)`
65
- : `Node.js ${version} (✗ requires >= v24)`,
75
+ ? `Node.js ${version} (meets minimum v24)`
76
+ : `Node.js ${version} (requires >= v24)`,
66
77
  };
67
78
  }
68
- function checkPackageManager(cwd) {
69
- const hasPnpm = existsSync(join(cwd, "pnpm-lock.yaml"));
70
- const hasNpm = existsSync(join(cwd, "package-lock.json"));
71
- const hasYarn = existsSync(join(cwd, "yarn.lock"));
72
- const passed = hasPnpm || hasNpm || hasYarn;
73
- const manager = hasPnpm ? "pnpm" : hasNpm ? "npm" : hasYarn ? "yarn" : "none";
79
+ function checkProject(layout) {
80
+ if (!layout) {
81
+ return {
82
+ name: "Zudojs project",
83
+ severity: "error",
84
+ passed: false,
85
+ message: "No Zudojs project found (no .zudojs/manifest.json, zudojs.config.ts or zudojs block in package.json)",
86
+ };
87
+ }
88
+ const source = layout.source === "manifest"
89
+ ? ".zudojs/manifest.json"
90
+ : layout.source === "config"
91
+ ? "zudojs.config.ts"
92
+ : "package.json#zudojs";
93
+ return {
94
+ name: "Zudojs project",
95
+ severity: "error",
96
+ passed: true,
97
+ message: `${layout.projectType} (${layout.architecture}) from ${source}`,
98
+ };
99
+ }
100
+ function checkPackageManager(layout) {
101
+ const lockFiles = {
102
+ pnpm: ["pnpm-lock.yaml"],
103
+ npm: ["package-lock.json"],
104
+ yarn: ["yarn.lock"],
105
+ bun: ["bun.lock", "bun.lockb"],
106
+ };
107
+ const expected = lockFiles[layout.packageManager] ?? [];
108
+ const passed = expected.some((file) => existsSync(join(layout.root, file)));
74
109
  return {
75
110
  name: "Package manager",
76
111
  severity: "warning",
77
112
  passed,
78
- message: passed ? `Detected: ${manager}` : "No lock file found",
113
+ message: passed
114
+ ? `${layout.packageManager} (lock file present)`
115
+ : `${layout.packageManager} configured but no lock file found; run "${layout.packageManager} install"`,
79
116
  };
80
117
  }
81
- function checkTypeScriptConfig(cwd) {
82
- const passed = existsSync(join(cwd, "tsconfig.json")) ||
83
- existsSync(join(cwd, "tsconfig.base.json"));
118
+ function checkInstalled(layout) {
119
+ const passed = existsSync(join(layout.root, "node_modules"));
84
120
  return {
85
- name: "TypeScript configuration",
86
- severity: "error",
121
+ name: "Dependencies installed",
122
+ severity: "warning",
87
123
  passed,
88
- message: passed ? "tsconfig.json found" : "No tsconfig.json found",
124
+ message: passed
125
+ ? "node_modules present"
126
+ : `node_modules missing; run "${layout.packageManager} install"`,
89
127
  };
90
128
  }
91
- function checkZudojsConfig(cwd) {
92
- const hasPkgConfig = checkZudojsInPackageJson(cwd);
93
- const hasConfig = existsSync(join(cwd, "zudojs.config.ts")) ||
94
- existsSync(join(cwd, "zudojs.config.js"));
95
- const passed = hasPkgConfig || hasConfig;
96
- let message = "No Zudojs configuration found.";
97
- if (passed) {
98
- message = hasPkgConfig
99
- ? "Zudojs config in package.json"
100
- : hasConfig
101
- ? "zudojs.config.ts found"
102
- : "Zudojs config in package.json";
129
+ /** Directories that must each carry a tsconfig.json. */
130
+ function appDirs(layout) {
131
+ const dirs = [...layout.backendDirs];
132
+ if (layout.frontendDir !== undefined &&
133
+ layout.frontendFramework !== "flutter" &&
134
+ existsSync(join(layout.frontendDir, "package.json"))) {
135
+ dirs.push(layout.frontendDir);
103
136
  }
137
+ return dirs;
138
+ }
139
+ function describe(layout, dir) {
140
+ const rel = relative(layout.root, dir);
141
+ return rel === "" ? "." : rel;
142
+ }
143
+ /**
144
+ * A workspace root has no tsconfig.json of its own — the apps do. This
145
+ * check used to look only at the root, so every fullstack and microservice
146
+ * project failed the doctor the moment it was created.
147
+ */
148
+ function checkTypeScriptConfig(layout) {
149
+ const missing = appDirs(layout).filter((dir) => !existsSync(join(dir, "tsconfig.json")) &&
150
+ !existsSync(join(dir, "tsconfig.base.json")));
151
+ const passed = missing.length === 0;
104
152
  return {
105
- name: "Zudojs configuration",
153
+ name: "TypeScript configuration",
106
154
  severity: "error",
107
155
  passed,
108
- message,
156
+ message: passed
157
+ ? "tsconfig.json found in every app"
158
+ : `No tsconfig.json in: ${missing.map((d) => describe(layout, d)).join(", ")}`,
109
159
  };
110
160
  }
111
- function checkZudojsInPackageJson(cwd) {
161
+ function readPackageJson(dir) {
112
162
  try {
113
- const pkgPath = join(cwd, "package.json");
114
- if (!existsSync(pkgPath))
115
- return false;
116
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
117
- return typeof pkg.zudojs === "object" && pkg.zudojs !== null;
163
+ return JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
118
164
  }
119
165
  catch {
120
- return false;
166
+ return null;
121
167
  }
122
168
  }
123
- function checkDependencies(context) {
124
- const pkgPath = join(context.cwd, "package.json");
125
- if (!existsSync(pkgPath)) {
126
- return {
127
- name: "Dependencies",
128
- severity: "error",
129
- passed: false,
130
- message: "No package.json found",
131
- };
132
- }
133
- try {
134
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
135
- const zudojsDeps = Object.keys(pkg.dependencies ?? {}).filter((d) => d.startsWith("@zudojs/"));
136
- const passed = zudojsDeps.length > 0;
169
+ function checkDependencies(layout) {
170
+ if (layout.backendDirs.length === 0) {
137
171
  return {
138
172
  name: "Zudojs dependencies",
139
173
  severity: "warning",
140
- passed,
141
- message: passed
142
- ? `${zudojsDeps.length} Zudojs packages installed`
143
- : "No Zudojs packages found",
174
+ passed: true,
175
+ message: "Frontend-only project; no framework packages expected",
144
176
  };
145
177
  }
146
- catch {
178
+ const found = new Set();
179
+ const unreadable = [];
180
+ for (const dir of layout.backendDirs) {
181
+ const pkg = readPackageJson(dir);
182
+ if (!pkg) {
183
+ unreadable.push(describe(layout, dir));
184
+ continue;
185
+ }
186
+ for (const name of Object.keys(pkg.dependencies ?? {})) {
187
+ if (name.startsWith("@zudojs/"))
188
+ found.add(name);
189
+ }
190
+ }
191
+ if (unreadable.length > 0) {
147
192
  return {
148
- name: "Dependencies",
193
+ name: "Zudojs dependencies",
149
194
  severity: "error",
150
195
  passed: false,
151
- message: "Failed to read package.json",
196
+ message: `Failed to read package.json in: ${unreadable.join(", ")}`,
152
197
  };
153
198
  }
199
+ const passed = found.size > 0;
200
+ return {
201
+ name: "Zudojs dependencies",
202
+ severity: "warning",
203
+ passed,
204
+ message: passed
205
+ ? `${found.size} Zudojs package(s) declared`
206
+ : "No @zudojs/* packages declared",
207
+ };
154
208
  }
155
- function checkArchitectureViolations(cwd) {
156
- const srcDir = join(cwd, "src");
209
+ /**
210
+ * Every feature recorded in a backend package.json must be backed by the
211
+ * package `zudojs add` installs for it.
212
+ */
213
+ function checkFeatures(layout) {
157
214
  const violations = [];
158
- if (!existsSync(srcDir)) {
159
- return {
160
- name: "Architecture",
161
- severity: "warning",
162
- passed: true,
163
- message: "No src/ directory (not a Zudojs project?)",
164
- };
165
- }
166
- const configPath = join(cwd, "zudojs.config.ts");
167
- let architecture = "monolith";
168
- if (existsSync(configPath)) {
169
- const configContent = readFileSync(configPath, "utf-8");
170
- const archMatch = configContent.match(/architecture:\s*["'](\w[\w-]*)["']/);
171
- if (archMatch?.[1]) {
172
- architecture = archMatch[1];
173
- }
174
- }
175
- if (architecture === "modular-monolith" || architecture === "microservice") {
176
- const servicesDir = join(srcDir, "services");
177
- if (existsSync(servicesDir)) {
178
- violations.push("src/services/ should be split into modules/ (modular-monolith) or apps/services/ (microservice)");
179
- }
180
- }
181
- if (architecture === "monolith") {
182
- const modulesDir = join(srcDir, "modules");
183
- if (existsSync(modulesDir)) {
184
- violations.push("src/modules/ found in monolith architecture — consider modular-monolith or microservice architecture");
185
- }
186
- }
187
- const pkgPath = join(cwd, "package.json");
188
- if (existsSync(pkgPath)) {
189
- try {
190
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
191
- const features = pkg.zudojs?.features ?? [];
192
- const deps = Object.keys(pkg.dependencies ?? {});
193
- for (const feature of features) {
194
- const pkgName = `@zudojs/${feature}`;
195
- if (!deps.includes(pkgName)) {
196
- violations.push(`Feature "${feature}" declared in package.json#zudojs.features but ${pkgName} not in dependencies`);
215
+ for (const dir of layout.backendDirs) {
216
+ const pkg = readPackageJson(dir);
217
+ if (!pkg)
218
+ continue;
219
+ const features = Array.isArray(pkg.zudojs?.features)
220
+ ? pkg.zudojs.features.filter((f) => typeof f === "string")
221
+ : [];
222
+ const deps = Object.keys(pkg.dependencies ?? {});
223
+ for (const feature of features) {
224
+ const required = FEATURE_PACKAGES[feature] ?? [`@zudojs/${feature}`];
225
+ for (const name of required) {
226
+ if (!deps.includes(name)) {
227
+ violations.push(`${describe(layout, dir)}: feature "${feature}" declared but ${name} is not a dependency`);
197
228
  }
198
229
  }
199
230
  }
200
- catch {
201
- // ignore parse errors
202
- }
203
231
  }
204
232
  return {
205
- name: "Architecture",
233
+ name: "Features",
206
234
  severity: "warning",
207
235
  passed: violations.length === 0,
208
236
  message: violations.length === 0
209
- ? "No violations detected"
210
- : `${violations.length} potential violation(s): ${violations.join("; ")}`,
237
+ ? "Every declared feature has its package"
238
+ : violations.join("; "),
211
239
  };
212
240
  }
213
241
  //# sourceMappingURL=doctor.command.js.map
@@ -2,7 +2,8 @@
2
2
  * zudojs-cli — Generate Command
3
3
  *
4
4
  * The `zudojs generate` (alias: `g`) command.
5
- * Reads zudojs.config.ts to determine project architecture.
5
+ * Reads the project manifest to determine the architecture and where the
6
+ * backend lives, then places the schematic accordingly.
6
7
  */
7
8
  import type { CLIContext } from "../cliType/cliType.type.js";
8
9
  export declare function runGenerateCommand(context: CLIContext): Promise<void>;