zudojs-cli 1.2.1 → 2.0.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.
- package/README.md +35 -1
- package/dist/src/adapters/frontend/flutter.adapter.d.ts +9 -0
- package/dist/src/adapters/frontend/flutter.adapter.js +16 -1
- package/dist/src/bin/zudojs.js +16 -5
- package/dist/src/cliApplication/cliApplication.builtins.d.ts +4 -0
- package/dist/src/cliApplication/cliApplication.builtins.js +28 -9
- package/dist/src/cliApplication/cliApplication.core.d.ts +8 -0
- package/dist/src/cliApplication/cliApplication.core.js +45 -5
- package/dist/src/cliApplication/cliApplication.help.d.ts +17 -0
- package/dist/src/cliApplication/cliApplication.help.js +94 -0
- package/dist/src/cliApplication/cliApplication.logger.js +4 -4
- package/dist/src/cliCommand/cliCommand.validator.js +22 -0
- package/dist/src/cliConstant/cliConstant.value.d.ts +18 -12
- package/dist/src/cliConstant/cliConstant.value.js +25 -19
- package/dist/src/cliError/cliError.argument.js +2 -2
- package/dist/src/cliError/cliError.base.js +28 -5
- package/dist/src/cliError/cliError.command.js +5 -2
- package/dist/src/cliError/cliError.execution.js +3 -3
- package/dist/src/cliParser/cliParser.core.js +11 -5
- package/dist/src/cliParser/cliParser.helper.d.ts +11 -0
- package/dist/src/cliParser/cliParser.helper.js +19 -0
- package/dist/src/cliParser/cliParser.longOption.js +4 -4
- package/dist/src/cliParser/cliParser.shortOption.js +7 -4
- package/dist/src/cliVersion/cliVersion.update.js +5 -3
- package/dist/src/commands/add.command.js +17 -2
- package/dist/src/commands/commandError.helper.d.ts +15 -0
- package/dist/src/commands/commandError.helper.js +28 -0
- package/dist/src/commands/create.command.js +284 -46
- package/dist/src/commands/doctor.command.d.ts +8 -0
- package/dist/src/commands/doctor.command.js +38 -5
- package/dist/src/commands/generate.command.js +71 -32
- package/dist/src/constants/index.d.ts +40 -68
- package/dist/src/constants/index.js +73 -23
- package/dist/src/generators/frontend/frontendPipeline.js +16 -4
- package/dist/src/generators/integration/integrationGenerator.core.js +10 -6
- package/dist/src/generators/project/project.generator.js +5 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +3 -3
- package/dist/src/manifest/index.d.ts +1 -0
- package/dist/src/manifest/index.js +1 -0
- package/dist/src/manifest/manifestFile.helper.d.ts +42 -0
- package/dist/src/manifest/manifestFile.helper.js +141 -0
- package/dist/src/manifest/manifestManager.core.d.ts +16 -0
- package/dist/src/manifest/manifestManager.core.js +65 -35
- package/dist/src/prompts/backend/api-style.prompt.js +2 -5
- package/dist/src/prompts/backend/backend-architecture.prompt.js +5 -22
- package/dist/src/prompts/backend/database.prompt.js +5 -23
- package/dist/src/prompts/backend/services.prompt.js +12 -10
- package/dist/src/prompts/cancel.prompt.d.ts +18 -0
- package/dist/src/prompts/cancel.prompt.js +26 -0
- package/dist/src/prompts/capabilities/capabilities.prompt.js +2 -5
- package/dist/src/prompts/frontend/framework.prompt.js +2 -5
- package/dist/src/prompts/frontend/frontend-architecture.prompt.js +2 -5
- package/dist/src/prompts/index.d.ts +1 -0
- package/dist/src/prompts/index.js +1 -0
- package/dist/src/prompts/project/confirmation.prompt.js +2 -5
- package/dist/src/prompts/project/project-name.prompt.js +6 -7
- package/dist/src/prompts/project/project-type.prompt.js +2 -5
- package/dist/src/prompts/workspace/package-manager.prompt.js +5 -12
- package/dist/src/registries/adapter/frontendAdapterRegistry.core.d.ts +6 -0
- package/dist/src/registries/adapter/frontendAdapterRegistry.core.js +12 -0
- package/dist/src/registries/adapter/packageManagerRegistry.core.d.ts +5 -0
- package/dist/src/registries/adapter/packageManagerRegistry.core.js +11 -0
- package/dist/src/registries/dependency/dependencyRegistry.core.d.ts +9 -0
- package/dist/src/registries/dependency/dependencyRegistry.core.js +16 -0
- package/dist/src/registries/generator/generatorRegistry.core.d.ts +8 -0
- package/dist/src/registries/generator/generatorRegistry.core.js +14 -0
- package/dist/src/resolvers/capability/capabilityResolver.core.d.ts +1 -1
- package/dist/src/resolvers/capability/capabilityResolver.core.js +0 -2
- package/dist/src/resolvers/layout/projectLayout.core.js +1 -6
- package/dist/src/resolvers/project.resolver.d.ts +9 -3
- package/dist/src/resolvers/project.resolver.js +11 -7
- package/dist/src/rollback/index.d.ts +1 -1
- package/dist/src/rollback/index.js +1 -1
- package/dist/src/rollback/rollbackManager.core.d.ts +19 -1
- package/dist/src/rollback/rollbackManager.core.js +21 -2
- package/dist/src/runners/package-manager/index.d.ts +1 -1
- package/dist/src/runners/package-manager/index.js +1 -1
- package/dist/src/runners/package-manager/packageManagerRunner.core.d.ts +13 -1
- package/dist/src/runners/package-manager/packageManagerRunner.core.js +19 -1
- package/dist/src/runners/process/processRunner.core.d.ts +11 -0
- package/dist/src/runners/process/processRunner.core.js +41 -6
- package/dist/src/runners/task/index.d.ts +1 -1
- package/dist/src/runners/task/taskRunner.core.d.ts +22 -1
- package/dist/src/runners/task/taskRunner.core.js +35 -6
- package/dist/src/scaffolders/framework.scaffolder.js +3 -0
- package/dist/src/scaffolders/scaffolder.helper.d.ts +10 -0
- package/dist/src/scaffolders/scaffolder.helper.js +25 -2
- package/dist/src/templates/microservice/index.d.ts +1 -1
- package/dist/src/templates/microservice/index.js +1 -1
- package/dist/src/templates/microservice/microservice.template.d.ts +8 -4
- package/dist/src/templates/microservice/microservice.template.js +30 -19
- package/dist/src/templates/modular-monolith/modularMonolith.template.js +27 -8
- package/dist/src/templates/monolith/monolith.template.js +11 -17
- package/dist/src/templates/shared/capability.template.d.ts +25 -0
- package/dist/src/templates/shared/capability.template.js +46 -0
- package/dist/src/templates/shared/index.d.ts +1 -0
- package/dist/src/templates/shared/index.js +1 -0
- package/dist/src/utils/utils.exec.d.ts +22 -0
- package/dist/src/utils/utils.exec.js +51 -4
- package/dist/src/utils/utils.fileSystem.js +46 -2
- package/dist/src/utils/utils.name.d.ts +9 -0
- package/dist/src/utils/utils.name.js +29 -3
- package/dist/src/validators/environment/environmentValidator.core.d.ts +11 -1
- package/dist/src/validators/environment/environmentValidator.core.js +40 -73
- package/dist/src/validators/project/index.d.ts +1 -1
- package/dist/src/validators/project/projectValidator.core.d.ts +11 -1
- package/dist/src/validators/project/projectValidator.core.js +20 -4
- package/package.json +1 -1
|
@@ -9,5 +9,15 @@ export interface ScaffolderOptions {
|
|
|
9
9
|
readonly targetPath: string;
|
|
10
10
|
readonly fallbackFiles: Record<string, string>;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Timeout for an official framework scaffolder, in milliseconds.
|
|
14
|
+
*
|
|
15
|
+
* `npm create vite@latest` and `npx create-next-app@latest` download a
|
|
16
|
+
* package tree from the registry, which routinely takes several minutes on
|
|
17
|
+
* a cold cache or a slow link. The 2-minute default of `execCommand` killed
|
|
18
|
+
* them mid-download and the user silently received the built-in fallback
|
|
19
|
+
* template instead of the real framework scaffold.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SCAFFOLD_TIMEOUT_MS = 900000;
|
|
12
22
|
export declare function scaffoldWithFallback(options: ScaffolderOptions): Promise<boolean>;
|
|
13
23
|
//# sourceMappingURL=scaffolder.helper.d.ts.map
|
|
@@ -5,17 +5,40 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { execCommand } from "../utils/utils.exec.js";
|
|
7
7
|
import { writeFileTree } from "../utils/utils.fileSystem.js";
|
|
8
|
+
/**
|
|
9
|
+
* Timeout for an official framework scaffolder, in milliseconds.
|
|
10
|
+
*
|
|
11
|
+
* `npm create vite@latest` and `npx create-next-app@latest` download a
|
|
12
|
+
* package tree from the registry, which routinely takes several minutes on
|
|
13
|
+
* a cold cache or a slow link. The 2-minute default of `execCommand` killed
|
|
14
|
+
* them mid-download and the user silently received the built-in fallback
|
|
15
|
+
* template instead of the real framework scaffold.
|
|
16
|
+
*/
|
|
17
|
+
export const SCAFFOLD_TIMEOUT_MS = 900000;
|
|
18
|
+
/** Describes why a scaffolder failed, including the child's own stderr. */
|
|
19
|
+
function describeFailure(error) {
|
|
20
|
+
if (!(error instanceof Error))
|
|
21
|
+
return String(error);
|
|
22
|
+
const killed = error.killed === true;
|
|
23
|
+
const signal = error.signal;
|
|
24
|
+
const stderr = error.stderr;
|
|
25
|
+
const detail = typeof stderr === "string" ? stderr.trim() : "";
|
|
26
|
+
const headline = killed && (signal === "SIGTERM" || signal === null || signal === undefined)
|
|
27
|
+
? `timed out after ${Math.round(SCAFFOLD_TIMEOUT_MS / 1000)}s`
|
|
28
|
+
: `failed: ${error.message.split("\n")[0]}`;
|
|
29
|
+
return detail ? `${headline}; ${detail}` : headline;
|
|
30
|
+
}
|
|
8
31
|
export async function scaffoldWithFallback(options) {
|
|
9
32
|
try {
|
|
10
33
|
await execCommand(options.command, Array.from(options.args), options.targetPath, {
|
|
11
34
|
// Suppress interactive prompts from create-* tools.
|
|
12
35
|
env: { ...process.env, CI: "1" },
|
|
36
|
+
timeout: SCAFFOLD_TIMEOUT_MS,
|
|
13
37
|
});
|
|
14
38
|
return true;
|
|
15
39
|
}
|
|
16
40
|
catch (error) {
|
|
17
|
-
|
|
18
|
-
console.warn(`Warning: official scaffolder "${options.command} ${options.args.join(" ")}" failed (${message.split("\n")[0]}). Using built-in fallback template.`);
|
|
41
|
+
console.warn(`Warning: official scaffolder "${options.command} ${options.args.join(" ")}" ${describeFailure(error)}. Using built-in fallback template.`);
|
|
19
42
|
await writeFileTree(options.targetPath, options.fallbackFiles);
|
|
20
43
|
return false;
|
|
21
44
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* zudojs-cli — Microservice Template Index
|
|
3
3
|
*/
|
|
4
|
-
export { generateMicroserviceFiles, resolveMicroserviceServices,
|
|
4
|
+
export { generateMicroserviceFiles, resolveMicroserviceServices, RESERVED_MICROSERVICE_APP_NAMES, } from "./microservice.template.js";
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* zudojs-cli — Microservice Template Index
|
|
3
3
|
*/
|
|
4
|
-
export { generateMicroserviceFiles, resolveMicroserviceServices,
|
|
4
|
+
export { generateMicroserviceFiles, resolveMicroserviceServices, RESERVED_MICROSERVICE_APP_NAMES, } from "./microservice.template.js";
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -22,8 +22,6 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import type { ScaffoldOptions } from "../../types/index.js";
|
|
25
|
-
/** Default service names used when none are provided. */
|
|
26
|
-
export declare const DEFAULT_MICROSERVICE_SERVICES: readonly ["identity", "enrollment", "assessment", "notification"];
|
|
27
25
|
/**
|
|
28
26
|
* The gateway app is always generated at `apps/gateway`, so "gateway" is a
|
|
29
27
|
* reserved name: a service called `gateway` would produce a second, competing
|
|
@@ -32,8 +30,14 @@ export declare const DEFAULT_MICROSERVICE_SERVICES: readonly ["identity", "enrol
|
|
|
32
30
|
export declare const RESERVED_MICROSERVICE_APP_NAMES: readonly ["gateway"];
|
|
33
31
|
/**
|
|
34
32
|
* Normalizes a requested service list into the service apps generated under
|
|
35
|
-
* `apps/services/`. Reserved names and duplicates are dropped
|
|
36
|
-
*
|
|
33
|
+
* `apps/services/`. Reserved names and duplicates are dropped.
|
|
34
|
+
*
|
|
35
|
+
* An empty request yields an empty list: a new project gets the gateway and
|
|
36
|
+
* nothing else. This used to substitute four example names
|
|
37
|
+
* (`identity`, `enrollment`, `assessment`, `notification`), which put four
|
|
38
|
+
* domains nobody asked for into every microservice project and left the
|
|
39
|
+
* author deleting them before they could start. Services are created when
|
|
40
|
+
* they are named — here, or later with `zudojs generate service <name>`.
|
|
37
41
|
*/
|
|
38
42
|
export declare function resolveMicroserviceServices(requested: readonly string[]): readonly string[];
|
|
39
43
|
export declare function generateMicroserviceFiles(options: ScaffoldOptions): Record<string, string>;
|
|
@@ -23,14 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { ZUDOJS_PACKAGES_VERSION } from "../../constants/index.js";
|
|
25
25
|
import { normalizeName } from "../../utils/utils.name.js";
|
|
26
|
-
import { RUNTIME_APP_DEPENDENCIES, moduleSpec, renderAppPackageDockerfile, renderAppFile, renderModuleFile, renderServerFile, renderPnpmWorkspaceFile, } from "../shared/index.js";
|
|
27
|
-
/** Default service names used when none are provided. */
|
|
28
|
-
export const DEFAULT_MICROSERVICE_SERVICES = [
|
|
29
|
-
"identity",
|
|
30
|
-
"enrollment",
|
|
31
|
-
"assessment",
|
|
32
|
-
"notification",
|
|
33
|
-
];
|
|
26
|
+
import { RUNTIME_APP_DEPENDENCIES, capabilityPackages, moduleSpec, renderAppPackageDockerfile, renderAppFile, renderModuleFile, renderServerFile, renderPnpmWorkspaceFile, resolveProjectCapabilities, } from "../shared/index.js";
|
|
34
27
|
/**
|
|
35
28
|
* The gateway app is always generated at `apps/gateway`, so "gateway" is a
|
|
36
29
|
* reserved name: a service called `gateway` would produce a second, competing
|
|
@@ -39,22 +32,32 @@ export const DEFAULT_MICROSERVICE_SERVICES = [
|
|
|
39
32
|
export const RESERVED_MICROSERVICE_APP_NAMES = ["gateway"];
|
|
40
33
|
/**
|
|
41
34
|
* Normalizes a requested service list into the service apps generated under
|
|
42
|
-
* `apps/services/`. Reserved names and duplicates are dropped
|
|
43
|
-
*
|
|
35
|
+
* `apps/services/`. Reserved names and duplicates are dropped.
|
|
36
|
+
*
|
|
37
|
+
* An empty request yields an empty list: a new project gets the gateway and
|
|
38
|
+
* nothing else. This used to substitute four example names
|
|
39
|
+
* (`identity`, `enrollment`, `assessment`, `notification`), which put four
|
|
40
|
+
* domains nobody asked for into every microservice project and left the
|
|
41
|
+
* author deleting them before they could start. Services are created when
|
|
42
|
+
* they are named — here, or later with `zudojs generate service <name>`.
|
|
44
43
|
*/
|
|
45
44
|
export function resolveMicroserviceServices(requested) {
|
|
46
45
|
const normalized = requested
|
|
47
46
|
.map((service) => normalizeName(service))
|
|
48
47
|
.filter((service) => service.length > 0 &&
|
|
49
48
|
!RESERVED_MICROSERVICE_APP_NAMES.some((reserved) => reserved === service));
|
|
50
|
-
|
|
51
|
-
return unique.length > 0 ? unique : [...DEFAULT_MICROSERVICE_SERVICES];
|
|
49
|
+
return [...new Set(normalized)];
|
|
52
50
|
}
|
|
53
51
|
export function generateMicroserviceFiles(options) {
|
|
54
52
|
const nameSlug = options.projectName
|
|
55
53
|
.replace(/[^a-z0-9-]+/gi, "-")
|
|
56
54
|
.toLowerCase();
|
|
57
55
|
const services = resolveMicroserviceServices(options.services);
|
|
56
|
+
// The workspace root records the capabilities for the project as a whole
|
|
57
|
+
// (`zudojs info` reads it when there is no manifest); the service apps
|
|
58
|
+
// record them next to the dependencies that back them, which is where
|
|
59
|
+
// `zudojs add` writes and `zudojs doctor` reads.
|
|
60
|
+
const capabilities = resolveProjectCapabilities(options);
|
|
58
61
|
const files = {};
|
|
59
62
|
// Root package.json (workspace root)
|
|
60
63
|
const rootScripts = {
|
|
@@ -91,7 +94,7 @@ export function generateMicroserviceFiles(options) {
|
|
|
91
94
|
zudojs: {
|
|
92
95
|
projectType: "backend",
|
|
93
96
|
architecture: "microservice",
|
|
94
|
-
features:
|
|
97
|
+
features: capabilities,
|
|
95
98
|
},
|
|
96
99
|
...(options.packageManager === "pnpm"
|
|
97
100
|
? {}
|
|
@@ -152,7 +155,14 @@ A microservice architecture built with the Zudojs framework.
|
|
|
152
155
|
## Services
|
|
153
156
|
|
|
154
157
|
- **gateway** - Port 3000
|
|
155
|
-
${services.
|
|
158
|
+
${services.length > 0
|
|
159
|
+
? services.map((s, i) => `- **${s}** - Port ${3001 + i}`).join("\n")
|
|
160
|
+
: `
|
|
161
|
+
No services yet. Add one with:
|
|
162
|
+
|
|
163
|
+
\`\`\`bash
|
|
164
|
+
npx zudojs generate service <name>
|
|
165
|
+
\`\`\``}
|
|
156
166
|
|
|
157
167
|
## Getting Started
|
|
158
168
|
|
|
@@ -171,8 +181,10 @@ docker-compose up
|
|
|
171
181
|
|
|
172
182
|
MIT
|
|
173
183
|
`;
|
|
174
|
-
//
|
|
175
|
-
|
|
184
|
+
// No `src/` at the workspace root: there is no package.json, tsconfig or
|
|
185
|
+
// app there, so nothing would compile a file placed in it. Every app owns
|
|
186
|
+
// its own `src/types/`, and code genuinely shared between apps belongs in a
|
|
187
|
+
// workspace package the author creates deliberately.
|
|
176
188
|
const appTsconfig = `{
|
|
177
189
|
"compilerOptions": {
|
|
178
190
|
"target": "ES2024",
|
|
@@ -242,10 +254,8 @@ MIT
|
|
|
242
254
|
"@zudojs/config",
|
|
243
255
|
"@zudojs/errors",
|
|
244
256
|
"@zudojs/http",
|
|
257
|
+
...capabilityPackages(capabilities),
|
|
245
258
|
];
|
|
246
|
-
if (options.enableCQRS) {
|
|
247
|
-
serviceDeps.push("@zudojs/cqrs");
|
|
248
|
-
}
|
|
249
259
|
for (const svc of services) {
|
|
250
260
|
const svcName = svc;
|
|
251
261
|
const svcIndex = services.indexOf(svcName);
|
|
@@ -256,6 +266,7 @@ MIT
|
|
|
256
266
|
version: "0.1.0",
|
|
257
267
|
private: true,
|
|
258
268
|
type: "module",
|
|
269
|
+
zudojs: { features: capabilities },
|
|
259
270
|
scripts: {
|
|
260
271
|
dev: "tsx watch src/server.ts",
|
|
261
272
|
start: "node dist/server.js",
|
|
@@ -34,17 +34,25 @@
|
|
|
34
34
|
import { renderDatabaseEnv } from "../../adapters/databases/databaseAdapter.resolver.js";
|
|
35
35
|
import { ZUDOJS_PACKAGES_VERSION } from "../../constants/index.js";
|
|
36
36
|
import { normalizeName } from "../../utils/utils.name.js";
|
|
37
|
-
import { RUNTIME_APP_DEPENDENCIES, moduleSpec, renderAppFile, renderModuleFile, renderServerFile, renderPnpmWorkspaceFile, } from "../shared/index.js";
|
|
37
|
+
import { RUNTIME_APP_DEPENDENCIES, capabilityPackages, moduleSpec, renderAppFile, renderModuleFile, renderServerFile, renderPnpmWorkspaceFile, resolveProjectCapabilities, } from "../shared/index.js";
|
|
38
38
|
export function generateModularMonolithFiles(options) {
|
|
39
39
|
const nameSlug = options.projectName
|
|
40
40
|
.replace(/[^a-z0-9-]+/gi, "-")
|
|
41
41
|
.toLowerCase();
|
|
42
42
|
// Normalized: each name becomes a directory segment and a class name.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
//
|
|
44
|
+
// An empty list generates no modules. This used to substitute three example
|
|
45
|
+
// names (`identity`, `enrollment`, `assessment`), so every modular monolith
|
|
46
|
+
// arrived with three domains nobody asked for. Modules are created when they
|
|
47
|
+
// are named — with `--services`, or later with `zudojs generate module <name>`.
|
|
48
|
+
const modules = options.services
|
|
46
49
|
.map((m) => normalizeName(m))
|
|
47
50
|
.filter((m) => m.length > 0);
|
|
51
|
+
// Recorded in package.json and backed by the packages below, so the
|
|
52
|
+
// `zudojs doctor` feature check has something real to verify. The
|
|
53
|
+
// architecture's own CQRS structure means cqrs/messaging are always
|
|
54
|
+
// installed, whether or not they were requested as capabilities.
|
|
55
|
+
const capabilities = resolveProjectCapabilities(options);
|
|
48
56
|
const deps = [
|
|
49
57
|
...RUNTIME_APP_DEPENDENCIES,
|
|
50
58
|
"@zudojs/config",
|
|
@@ -54,6 +62,7 @@ export function generateModularMonolithFiles(options) {
|
|
|
54
62
|
"@zudojs/cqrs",
|
|
55
63
|
"@zudojs/messaging",
|
|
56
64
|
"@zudojs/http",
|
|
65
|
+
...capabilityPackages(capabilities),
|
|
57
66
|
];
|
|
58
67
|
const devDeps = {
|
|
59
68
|
tsx: "^4.7.0",
|
|
@@ -73,7 +82,7 @@ export function generateModularMonolithFiles(options) {
|
|
|
73
82
|
zudojs: {
|
|
74
83
|
projectType: "backend",
|
|
75
84
|
architecture: "modular-monolith",
|
|
76
|
-
features:
|
|
85
|
+
features: capabilities,
|
|
77
86
|
},
|
|
78
87
|
scripts: {
|
|
79
88
|
dev: "tsx watch src/server.ts",
|
|
@@ -121,9 +130,17 @@ A modular monolith built with the Zudojs framework.
|
|
|
121
130
|
|
|
122
131
|
## Architecture
|
|
123
132
|
|
|
124
|
-
This project uses a **modular monolith** architecture
|
|
133
|
+
This project uses a **modular monolith** architecture.
|
|
125
134
|
|
|
126
|
-
${modules.
|
|
135
|
+
${modules.length > 0
|
|
136
|
+
? `Modules:
|
|
137
|
+
|
|
138
|
+
${modules.map((m) => `- **${m}**`).join("\n")}`
|
|
139
|
+
: `No modules yet. Add one with:
|
|
140
|
+
|
|
141
|
+
\`\`\`bash
|
|
142
|
+
npx zudojs generate module <name>
|
|
143
|
+
\`\`\``}
|
|
127
144
|
|
|
128
145
|
## Getting Started
|
|
129
146
|
|
|
@@ -182,7 +199,9 @@ MIT
|
|
|
182
199
|
files[`src/modules/${spec.name}/commands/index.ts`] = ``;
|
|
183
200
|
files[`src/modules/${spec.name}/queries/index.ts`] = ``;
|
|
184
201
|
}
|
|
185
|
-
|
|
202
|
+
// The name must match vitest's default `include`
|
|
203
|
+
// (`**/*.{test,spec}.?(c|m)[jt]s?(x)`); `tests/index.ts` did not.
|
|
204
|
+
files["tests/app.test.ts"] = `import { describe, it, expect } from "vitest";
|
|
186
205
|
|
|
187
206
|
describe("Application", () => {
|
|
188
207
|
it("should bootstrap correctly", () => {
|
|
@@ -40,10 +40,13 @@
|
|
|
40
40
|
import { renderDatabaseEnv } from "../../adapters/databases/databaseAdapter.resolver.js";
|
|
41
41
|
import { ZUDOJS_PACKAGES_VERSION } from "../../constants/index.js";
|
|
42
42
|
import { normalizeName, toPascalCase } from "../../utils/utils.name.js";
|
|
43
|
-
import { RUNTIME_APP_DEPENDENCIES, moduleSpec, renderAppFile, renderModuleFile, renderServerFile, renderServiceFile, renderPnpmWorkspaceFile, } from "../shared/index.js";
|
|
43
|
+
import { RUNTIME_APP_DEPENDENCIES, capabilityPackages, moduleSpec, renderAppFile, renderModuleFile, renderServerFile, renderServiceFile, renderPnpmWorkspaceFile, resolveProjectCapabilities, } from "../shared/index.js";
|
|
44
44
|
export function generateMonolithFiles(options) {
|
|
45
45
|
const name = options.projectName;
|
|
46
46
|
const nameSlug = name.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
|
|
47
|
+
// Recorded in package.json and backed by the packages below, so the
|
|
48
|
+
// `zudojs doctor` feature check has something real to verify.
|
|
49
|
+
const capabilities = resolveProjectCapabilities(options);
|
|
47
50
|
const deps = [
|
|
48
51
|
...RUNTIME_APP_DEPENDENCIES,
|
|
49
52
|
"@zudojs/config",
|
|
@@ -52,21 +55,10 @@ export function generateMonolithFiles(options) {
|
|
|
52
55
|
"@zudojs/validation",
|
|
53
56
|
"@zudojs/schema",
|
|
54
57
|
"@zudojs/http",
|
|
58
|
+
...capabilityPackages(capabilities),
|
|
55
59
|
];
|
|
56
60
|
if (options.enableCQRS) {
|
|
57
|
-
deps.push("@zudojs/
|
|
58
|
-
}
|
|
59
|
-
if (options.enableDatabase) {
|
|
60
|
-
deps.push("@zudojs/database");
|
|
61
|
-
}
|
|
62
|
-
if (options.enableQueue) {
|
|
63
|
-
deps.push("@zudojs/queue");
|
|
64
|
-
}
|
|
65
|
-
if (options.enableObservability) {
|
|
66
|
-
deps.push("@zudojs/observability");
|
|
67
|
-
}
|
|
68
|
-
if (options.enableOpenAPI) {
|
|
69
|
-
deps.push("@zudojs/openapi");
|
|
61
|
+
deps.push("@zudojs/events");
|
|
70
62
|
}
|
|
71
63
|
const devDeps = {
|
|
72
64
|
tsx: "^4.7.0",
|
|
@@ -86,7 +78,7 @@ export function generateMonolithFiles(options) {
|
|
|
86
78
|
zudojs: {
|
|
87
79
|
projectType: "backend",
|
|
88
80
|
architecture: "monolith",
|
|
89
|
-
features:
|
|
81
|
+
features: capabilities,
|
|
90
82
|
},
|
|
91
83
|
scripts: {
|
|
92
84
|
dev: "tsx watch src/server.ts",
|
|
@@ -240,8 +232,10 @@ MIT
|
|
|
240
232
|
files["src/controllers/index.ts"] =
|
|
241
233
|
`export { HealthController } from "./health.controller.js";
|
|
242
234
|
`;
|
|
243
|
-
// Tests
|
|
244
|
-
|
|
235
|
+
// Tests. The file name must match vitest's default `include`
|
|
236
|
+
// (`**/*.{test,spec}.?(c|m)[jt]s?(x)`); `tests/index.ts` did not, so the
|
|
237
|
+
// generated `test` script exited 1 with "No test files found".
|
|
238
|
+
files["tests/app.test.ts"] = `import { describe, it, expect } from "vitest";
|
|
245
239
|
|
|
246
240
|
describe("Application", () => {
|
|
247
241
|
it("should be configured correctly", () => {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zudojs-cli — Capabilities a generated project declares.
|
|
3
|
+
*
|
|
4
|
+
* Every template used to stamp `zudojs.features: []` into the package.json it
|
|
5
|
+
* wrote, although it had already used the `enable*` flags to choose the
|
|
6
|
+
* project's dependencies. `zudojs doctor` reads only `pkg.zudojs.features`,
|
|
7
|
+
* so its check ("every declared feature has its package") passed vacuously in
|
|
8
|
+
* every project the CLI created, and `zudojs add` was the only thing that
|
|
9
|
+
* could ever put a value there.
|
|
10
|
+
*
|
|
11
|
+
* The list produced here is the same one `zudojs create` records as
|
|
12
|
+
* `capabilities` in `.zudojs/manifest.json`, and {@link capabilityPackages}
|
|
13
|
+
* gives the packages that back it, so a declared feature is always installed.
|
|
14
|
+
*/
|
|
15
|
+
import type { ScaffoldOptions } from "../../types/index.js";
|
|
16
|
+
/**
|
|
17
|
+
* The capability ids a scaffolded project was created with.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveProjectCapabilities(options: ScaffoldOptions): readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* The `@zudojs/*` packages that back `capabilities`, using the same mapping
|
|
22
|
+
* `zudojs add` and `zudojs doctor` use.
|
|
23
|
+
*/
|
|
24
|
+
export declare function capabilityPackages(capabilities: readonly string[]): readonly string[];
|
|
25
|
+
//# sourceMappingURL=capability.template.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zudojs-cli — Capabilities a generated project declares.
|
|
3
|
+
*
|
|
4
|
+
* Every template used to stamp `zudojs.features: []` into the package.json it
|
|
5
|
+
* wrote, although it had already used the `enable*` flags to choose the
|
|
6
|
+
* project's dependencies. `zudojs doctor` reads only `pkg.zudojs.features`,
|
|
7
|
+
* so its check ("every declared feature has its package") passed vacuously in
|
|
8
|
+
* every project the CLI created, and `zudojs add` was the only thing that
|
|
9
|
+
* could ever put a value there.
|
|
10
|
+
*
|
|
11
|
+
* The list produced here is the same one `zudojs create` records as
|
|
12
|
+
* `capabilities` in `.zudojs/manifest.json`, and {@link capabilityPackages}
|
|
13
|
+
* gives the packages that back it, so a declared feature is always installed.
|
|
14
|
+
*/
|
|
15
|
+
import { FEATURE_PACKAGES } from "../../constants/index.js";
|
|
16
|
+
/** Capability flags, in the order the manifest records them. */
|
|
17
|
+
const CAPABILITY_FLAGS = [
|
|
18
|
+
["cqrs", "enableCQRS"],
|
|
19
|
+
["messaging", "enableMessaging"],
|
|
20
|
+
["observability", "enableObservability"],
|
|
21
|
+
["openapi", "enableOpenAPI"],
|
|
22
|
+
["database", "enableDatabase"],
|
|
23
|
+
["queue", "enableQueue"],
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* The capability ids a scaffolded project was created with.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveProjectCapabilities(options) {
|
|
29
|
+
return CAPABILITY_FLAGS.filter(([, flag]) => options[flag] === true).map(([capability]) => capability);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The `@zudojs/*` packages that back `capabilities`, using the same mapping
|
|
33
|
+
* `zudojs add` and `zudojs doctor` use.
|
|
34
|
+
*/
|
|
35
|
+
export function capabilityPackages(capabilities) {
|
|
36
|
+
const packages = new Set();
|
|
37
|
+
for (const capability of capabilities) {
|
|
38
|
+
for (const name of FEATURE_PACKAGES[capability] ?? [
|
|
39
|
+
`@zudojs/${capability}`,
|
|
40
|
+
]) {
|
|
41
|
+
packages.add(name);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [...packages];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=capability.template.js.map
|
|
@@ -12,6 +12,9 @@ export interface ExecOptions {
|
|
|
12
12
|
* Quotes one argument for cmd.exe. Arguments are validated before they reach
|
|
13
13
|
* here (project and service names are `[A-Za-z0-9_-]`), so this is defence
|
|
14
14
|
* in depth rather than the only barrier.
|
|
15
|
+
*
|
|
16
|
+
* @throws {CLIValidationError} If the argument contains `"`, `%` or `!`,
|
|
17
|
+
* none of which cmd.exe can be made to treat as literal text.
|
|
15
18
|
*/
|
|
16
19
|
export declare function quoteForWindowsShell(argument: string): string;
|
|
17
20
|
/** The spawn arguments and shell flag a command needs on this platform. */
|
|
@@ -20,6 +23,25 @@ export declare function resolveSpawnTarget(file: string, args: readonly string[]
|
|
|
20
23
|
readonly args: string[];
|
|
21
24
|
readonly shell: boolean;
|
|
22
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* The environment a child gets, hardened for the Windows shell path.
|
|
28
|
+
*
|
|
29
|
+
* `cmd.exe` searches the current directory before `PATH`, so a `pnpm.cmd`
|
|
30
|
+
* or `npm.bat` committed into a cloned repository would win over the real
|
|
31
|
+
* package manager. `NoDefaultCurrentDirectoryInExePath` removes the current
|
|
32
|
+
* directory from that search. It is only meaningful on win32 and only when
|
|
33
|
+
* a shell is involved; everywhere else the caller's env is passed through
|
|
34
|
+
* unchanged (POSIX `PATH` lookup never implies the cwd, and `shell` is
|
|
35
|
+
* false there).
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveChildEnv(shell: boolean, env: NodeJS.ProcessEnv | undefined, platform?: NodeJS.Platform): NodeJS.ProcessEnv | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Default timeout for {@link execCommand}, in milliseconds.
|
|
40
|
+
*
|
|
41
|
+
* Sized for quick probes. Anything that downloads from a registry needs an
|
|
42
|
+
* explicit, much larger timeout — see `SCAFFOLD_TIMEOUT_MS`.
|
|
43
|
+
*/
|
|
44
|
+
export declare const DEFAULT_EXEC_TIMEOUT_MS = 120000;
|
|
23
45
|
/**
|
|
24
46
|
* Runs a short-lived command and buffers its output.
|
|
25
47
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { CLIValidationError } from "../errors/index.js";
|
|
3
4
|
const execFileAsync = promisify(execFile);
|
|
4
5
|
/**
|
|
5
6
|
* Commands that are `.cmd` shims rather than executables on Windows.
|
|
@@ -23,17 +24,35 @@ const WINDOWS_SHIM_COMMANDS = new Set([
|
|
|
23
24
|
]);
|
|
24
25
|
/** Characters that need quoting when a command line goes through cmd.exe. */
|
|
25
26
|
const CMD_UNSAFE = /[\s"&|<>^()%!]/;
|
|
27
|
+
/**
|
|
28
|
+
* Characters that cannot be made safe by quoting on the cmd.exe path.
|
|
29
|
+
*
|
|
30
|
+
* `cmd.exe` has no escape character inside a quoted string: it toggles quote
|
|
31
|
+
* state on every `"`, so an embedded quote ends the string and everything
|
|
32
|
+
* after it — `&`, `|`, `>` — is parsed as shell syntax. The C-runtime `\"`
|
|
33
|
+
* escape means nothing to cmd. `%VAR%` and delayed-expansion `!VAR!` are
|
|
34
|
+
* likewise expanded inside quotes. There is no correct escaping, so an
|
|
35
|
+
* argument carrying one of these is rejected instead.
|
|
36
|
+
*/
|
|
37
|
+
const CMD_UNQUOTABLE = /["%!]/;
|
|
26
38
|
/**
|
|
27
39
|
* Quotes one argument for cmd.exe. Arguments are validated before they reach
|
|
28
40
|
* here (project and service names are `[A-Za-z0-9_-]`), so this is defence
|
|
29
41
|
* in depth rather than the only barrier.
|
|
42
|
+
*
|
|
43
|
+
* @throws {CLIValidationError} If the argument contains `"`, `%` or `!`,
|
|
44
|
+
* none of which cmd.exe can be made to treat as literal text.
|
|
30
45
|
*/
|
|
31
46
|
export function quoteForWindowsShell(argument) {
|
|
47
|
+
if (CMD_UNQUOTABLE.test(argument)) {
|
|
48
|
+
throw new CLIValidationError(`Cannot pass the argument ${JSON.stringify(argument)} to a Windows shell command: ` +
|
|
49
|
+
`the characters ", % and ! cannot be escaped for cmd.exe. Remove them and try again.`);
|
|
50
|
+
}
|
|
32
51
|
if (argument.length === 0)
|
|
33
52
|
return '""';
|
|
34
53
|
if (!CMD_UNSAFE.test(argument))
|
|
35
54
|
return argument;
|
|
36
|
-
return `"${argument
|
|
55
|
+
return `"${argument}"`;
|
|
37
56
|
}
|
|
38
57
|
/** The spawn arguments and shell flag a command needs on this platform. */
|
|
39
58
|
export function resolveSpawnTarget(file, args, platform = process.platform) {
|
|
@@ -46,6 +65,32 @@ export function resolveSpawnTarget(file, args, platform = process.platform) {
|
|
|
46
65
|
}
|
|
47
66
|
return { file, args: Array.from(args), shell: false };
|
|
48
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The environment a child gets, hardened for the Windows shell path.
|
|
70
|
+
*
|
|
71
|
+
* `cmd.exe` searches the current directory before `PATH`, so a `pnpm.cmd`
|
|
72
|
+
* or `npm.bat` committed into a cloned repository would win over the real
|
|
73
|
+
* package manager. `NoDefaultCurrentDirectoryInExePath` removes the current
|
|
74
|
+
* directory from that search. It is only meaningful on win32 and only when
|
|
75
|
+
* a shell is involved; everywhere else the caller's env is passed through
|
|
76
|
+
* unchanged (POSIX `PATH` lookup never implies the cwd, and `shell` is
|
|
77
|
+
* false there).
|
|
78
|
+
*/
|
|
79
|
+
export function resolveChildEnv(shell, env, platform = process.platform) {
|
|
80
|
+
if (!shell || platform !== "win32")
|
|
81
|
+
return env;
|
|
82
|
+
return {
|
|
83
|
+
...(env ?? process.env),
|
|
84
|
+
NoDefaultCurrentDirectoryInExePath: "1",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Default timeout for {@link execCommand}, in milliseconds.
|
|
89
|
+
*
|
|
90
|
+
* Sized for quick probes. Anything that downloads from a registry needs an
|
|
91
|
+
* explicit, much larger timeout — see `SCAFFOLD_TIMEOUT_MS`.
|
|
92
|
+
*/
|
|
93
|
+
export const DEFAULT_EXEC_TIMEOUT_MS = 120000;
|
|
49
94
|
/**
|
|
50
95
|
* Runs a short-lived command and buffers its output.
|
|
51
96
|
*
|
|
@@ -55,11 +100,12 @@ export function resolveSpawnTarget(file, args, platform = process.platform) {
|
|
|
55
100
|
*/
|
|
56
101
|
export async function execCommand(file, args, cwd, options = {}) {
|
|
57
102
|
const target = resolveSpawnTarget(file, args);
|
|
103
|
+
const env = resolveChildEnv(target.shell, options.env);
|
|
58
104
|
const { stdout, stderr } = await execFileAsync(target.file, target.args, {
|
|
59
105
|
cwd,
|
|
60
|
-
timeout: options.timeout ??
|
|
106
|
+
timeout: options.timeout ?? DEFAULT_EXEC_TIMEOUT_MS,
|
|
61
107
|
shell: target.shell,
|
|
62
|
-
...(
|
|
108
|
+
...(env ? { env } : {}),
|
|
63
109
|
...(options.signal ? { signal: options.signal } : {}),
|
|
64
110
|
});
|
|
65
111
|
return { stdout, stderr };
|
|
@@ -74,12 +120,13 @@ export async function execCommand(file, args, cwd, options = {}) {
|
|
|
74
120
|
*/
|
|
75
121
|
export function runStreaming(file, args, cwd, options = {}) {
|
|
76
122
|
const target = resolveSpawnTarget(file, args);
|
|
123
|
+
const env = resolveChildEnv(target.shell, options.env);
|
|
77
124
|
return new Promise((resolve, reject) => {
|
|
78
125
|
const child = spawn(target.file, target.args, {
|
|
79
126
|
cwd,
|
|
80
127
|
stdio: "inherit",
|
|
81
128
|
shell: target.shell,
|
|
82
|
-
...(
|
|
129
|
+
...(env ? { env } : {}),
|
|
83
130
|
...(options.signal ? { signal: options.signal } : {}),
|
|
84
131
|
});
|
|
85
132
|
child.on("error", (error) => {
|
|
@@ -1,13 +1,57 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { mkdir, writeFile as writeFileAsync } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
4
4
|
import { activeWriteCapture } from "./utils.writeGuard.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolves `path` through symlinks as far as it exists on disk, keeping the
|
|
7
|
+
* components that do not exist yet verbatim.
|
|
8
|
+
*
|
|
9
|
+
* Containment cannot be decided on the literal string alone: `mkdir` with
|
|
10
|
+
* `recursive: true` is a no-op on an existing directory, so if a component of
|
|
11
|
+
* the path is a symlink the write follows it to wherever it points. Only the
|
|
12
|
+
* real path of the deepest existing ancestor answers where the bytes land.
|
|
13
|
+
*/
|
|
14
|
+
function realizePath(path) {
|
|
15
|
+
const absolute = resolve(path);
|
|
16
|
+
const tail = [];
|
|
17
|
+
let current = absolute;
|
|
18
|
+
for (;;) {
|
|
19
|
+
try {
|
|
20
|
+
const real = realpathSync(current);
|
|
21
|
+
return tail.length === 0 ? real : join(real, ...tail);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
const parent = dirname(current);
|
|
25
|
+
// Reached the filesystem root without finding anything that exists.
|
|
26
|
+
if (parent === current)
|
|
27
|
+
return absolute;
|
|
28
|
+
tail.unshift(basename(current));
|
|
29
|
+
current = parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Whether `target` is `base` itself or lives underneath it. */
|
|
34
|
+
function isContained(base, target) {
|
|
35
|
+
if (base === target)
|
|
36
|
+
return true;
|
|
37
|
+
const relativePath = relative(base, target);
|
|
38
|
+
return (relativePath !== "" &&
|
|
39
|
+
!isAbsolute(relativePath) &&
|
|
40
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
41
|
+
relativePath !== "..");
|
|
42
|
+
}
|
|
5
43
|
function assertSafePath(basePath, filePath) {
|
|
6
44
|
const resolved = join(basePath, filePath);
|
|
7
45
|
const relativePath = relative(basePath, resolved);
|
|
8
46
|
if (relativePath.startsWith("..") || relativePath.includes("..")) {
|
|
9
47
|
throw new Error(`Path traversal detected: ${filePath}`);
|
|
10
48
|
}
|
|
49
|
+
// Re-assert containment against what is actually on disk, so a symlinked
|
|
50
|
+
// directory inside the project cannot carry the write outside it.
|
|
51
|
+
if (!isContained(realizePath(basePath), realizePath(resolved))) {
|
|
52
|
+
throw new Error(`Path escapes the project through a symlink: ${filePath}. ` +
|
|
53
|
+
`Replace the symlinked directory with a real one, or generate into a different path.`);
|
|
54
|
+
}
|
|
11
55
|
}
|
|
12
56
|
export async function writeFile(basePath, filePath, content) {
|
|
13
57
|
assertSafePath(basePath, filePath);
|
|
@@ -15,6 +15,15 @@ export declare const SAFE_PATH_SEGMENT: RegExp;
|
|
|
15
15
|
* `normalizeName` strips everything outside `[a-z0-9-]`, so a name such as
|
|
16
16
|
* `"..."` or `"!!!"` collapses to the empty string and the generator writes
|
|
17
17
|
* `src//.event.ts` with an anonymous exported class. Reject it up front.
|
|
18
|
+
*
|
|
19
|
+
* A name whose normalized form starts with a digit (`2fa`, `401-handler`) is
|
|
20
|
+
* rejected for the same reason: it becomes a class name, and `2faModule` is
|
|
21
|
+
* not a TypeScript identifier. The generated file, the barrel it is appended
|
|
22
|
+
* to and the `app.ts` it is registered in would all stop compiling, and the
|
|
23
|
+
* appends mean the overwrite guard cannot undo it. The converters would
|
|
24
|
+
* rather rename it to `_2faModule`, but a class whose name no longer contains
|
|
25
|
+
* the name that was asked for — and disagrees with its own file name — is a
|
|
26
|
+
* worse surprise than a one-line failure the author can act on.
|
|
18
27
|
*/
|
|
19
28
|
export declare function assertGeneratableName(name: string, kind?: string): string;
|
|
20
29
|
/**
|