zudojs-cli 2.1.1 → 2.1.3

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/src/bin/zudojs.js +1 -1
  3. package/dist/src/commands/generate.command.js +11 -16
  4. package/dist/src/constants/index.js +5 -1
  5. package/dist/src/constants/zudojsVersions.generated.js +37 -37
  6. package/dist/src/generators/command/command.generator.d.ts +7 -1
  7. package/dist/src/generators/command/command.generator.js +18 -44
  8. package/dist/src/generators/command/command.template.d.ts +24 -0
  9. package/dist/src/generators/command/command.template.js +78 -0
  10. package/dist/src/generators/command/index.d.ts +6 -0
  11. package/dist/src/generators/command/index.js +6 -0
  12. package/dist/src/generators/index.d.ts +0 -1
  13. package/dist/src/generators/index.js +0 -1
  14. package/dist/src/generators/infrastructure/infrastructure.generator.js +1 -1
  15. package/dist/src/generators/middleware/index.d.ts +2 -1
  16. package/dist/src/generators/middleware/index.js +2 -1
  17. package/dist/src/generators/middleware/middleware.generator.d.ts +28 -1
  18. package/dist/src/generators/middleware/middleware.generator.js +60 -15
  19. package/dist/src/generators/middleware/middleware.template.d.ts +21 -0
  20. package/dist/src/generators/middleware/middleware.template.js +35 -0
  21. package/dist/src/generators/query/index.d.ts +6 -0
  22. package/dist/src/generators/query/index.js +6 -0
  23. package/dist/src/generators/query/query.generator.d.ts +7 -1
  24. package/dist/src/generators/query/query.generator.js +19 -44
  25. package/dist/src/generators/query/query.template.d.ts +14 -0
  26. package/dist/src/generators/query/query.template.js +72 -0
  27. package/dist/src/generators/resource/resource.plan.d.ts +8 -2
  28. package/dist/src/generators/resource/resource.plan.js +10 -1
  29. package/dist/src/recipes/docker/docker.recipe.js +1 -1
  30. package/dist/src/recipes/docker/docker.refresh.js +1 -1
  31. package/dist/src/templates/resource/layers/resourceData.template.d.ts +1 -1
  32. package/dist/src/templates/resource/layers/resourceData.template.js +2 -1
  33. package/dist/src/templates/resource/layers/resourceRoutes.template.d.ts +1 -1
  34. package/dist/src/templates/resource/layers/resourceRoutes.template.js +6 -5
  35. package/dist/src/templates/resource/resource.names.d.ts +6 -0
  36. package/dist/src/templates/resource/resource.names.js +15 -0
  37. package/dist/src/templates/shared/dockerfile/dockerfile.install.d.ts +35 -0
  38. package/dist/src/templates/shared/dockerfile/dockerfile.install.js +89 -0
  39. package/dist/src/templates/shared/{dockerfile.template.d.ts → dockerfile/dockerfile.template.d.ts} +4 -2
  40. package/dist/src/templates/shared/{dockerfile.template.js → dockerfile/dockerfile.template.js} +17 -30
  41. package/dist/src/templates/shared/dockerfile/index.d.ts +6 -0
  42. package/dist/src/templates/shared/dockerfile/index.js +6 -0
  43. package/dist/src/templates/shared/index.d.ts +1 -1
  44. package/dist/src/templates/shared/index.js +1 -1
  45. package/dist/src/templates/shared/server.template.d.ts +7 -0
  46. package/dist/src/templates/shared/server.template.js +15 -3
  47. package/package.json +6 -6
  48. package/dist/src/generators/service/service.generator.d.ts +0 -12
  49. package/dist/src/generators/service/service.generator.js +0 -41
@@ -0,0 +1,35 @@
1
+ /**
2
+ * zudojs-cli — Generated `<name>.middleware.ts`.
3
+ *
4
+ * `generate middleware` used to write
5
+ * `(ctx: unknown, next: () => Promise<void>) => Promise<void>`: registering
6
+ * it in the server pipeline failed tsc with TS2322, and because it returned
7
+ * nothing the response produced by `next()` was dropped. The file is now a
8
+ * real `HttpMiddleware` from @zudojs/http that returns `next()`'s response.
9
+ */
10
+ /** Renders the middleware source file. */
11
+ export function renderMiddlewareFile(options) {
12
+ return `import type { HttpMiddleware } from "@zudojs/http";
13
+
14
+ /**
15
+ * ${options.name} middleware.
16
+ *
17
+ * It runs when \`${options.factoryName}()\` is in the \`middlewares\` list of
18
+ * ${options.serverFile}, between the \`// zudojs:server-middleware\` markers:
19
+ * after the security headers, CORS and rate limit, before routing.
20
+ * \`zudojs generate middleware\` adds it there when the markers exist.
21
+ *
22
+ * To answer without calling \`next()\`, return a response such as
23
+ * \`createResponseContext({ status: 403 }).json({ error: "Forbidden" })\`.
24
+ * Otherwise return what \`next()\` resolves to, and \`clone()\` it before
25
+ * changing its headers.
26
+ */
27
+ export function ${options.factoryName}(): HttpMiddleware {
28
+ return async (_context, next) => {
29
+ const response = await next();
30
+ return response;
31
+ };
32
+ }
33
+ `;
34
+ }
35
+ //# sourceMappingURL=middleware.template.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * zudojs-cli — Query schematic: a `QueryOf` query type, its factory and a
3
+ * `QueryHandler` subclass for @zudojs/cqrs.
4
+ */
5
+ export * from "./query.generator.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * zudojs-cli — Query schematic: a `QueryOf` query type, its factory and a
3
+ * `QueryHandler` subclass for @zudojs/cqrs.
4
+ */
5
+ export * from "./query.generator.js";
6
+ //# sourceMappingURL=index.js.map
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * zudojs-cli — Query Generator
3
3
  *
4
- * Generates a CQRS query with handler.
4
+ * Generates a CQRS query, its handler and a barrel under
5
+ * `<basePath>[/<service>]/queries/<name>/`.
5
6
  */
7
+ /** Options for {@link generateQuery}. */
6
8
  export interface GenerateQueryOptions {
7
9
  readonly name: string;
8
10
  readonly service?: string;
9
11
  readonly basePath?: string;
10
12
  readonly dryRun?: boolean;
11
13
  }
14
+ /**
15
+ * Writes `<name>.query.ts`, `<name>.handler.ts` and `index.ts`, and returns
16
+ * their paths (only the paths on a dry run).
17
+ */
12
18
  export declare function generateQuery(options: GenerateQueryOptions, cwd: string): Promise<string[]>;
13
19
  //# sourceMappingURL=query.generator.d.ts.map
@@ -1,55 +1,30 @@
1
1
  /**
2
2
  * zudojs-cli — Query Generator
3
3
  *
4
- * Generates a CQRS query with handler.
4
+ * Generates a CQRS query, its handler and a barrel under
5
+ * `<basePath>[/<service>]/queries/<name>/`.
5
6
  */
6
7
  import { writeFileTree } from "../../utils/utils.fileSystem.js";
7
8
  import { CLIGenerationError } from "../../errors/index.js";
8
- import { normalizeName } from "../../utils/utils.name.js";
9
+ import { assertGeneratableName } from "../../utils/utils.name.js";
10
+ import { cqrsSchematicNames } from "../command/command.template.js";
11
+ import { renderQueryBarrel, renderQueryFile, renderQueryHandlerFile, } from "./query.template.js";
12
+ /**
13
+ * Writes `<name>.query.ts`, `<name>.handler.ts` and `index.ts`, and returns
14
+ * their paths (only the paths on a dry run).
15
+ */
9
16
  export async function generateQuery(options, cwd) {
10
- const name = normalizeName(options.name);
11
- const nameCamel = name
12
- .replace(/-([a-z])/g, (_m, c) => c.toUpperCase())
13
- .replace(/^./, (c) => c.toUpperCase());
14
- const service = options.service;
17
+ const names = cqrsSchematicNames(assertGeneratableName(options.name, "query name"));
15
18
  const basePath = options.basePath ?? "services";
16
- // When no service grouping is given the schematic is written directly under
17
- // basePath. Callers that resolved the owning app into basePath (the
18
- // microservice layout) pass no service, so the path is not nested twice.
19
- const servicePath = service ? `${basePath}/${service}` : basePath;
19
+ // Without a service group the schematic goes directly under basePath:
20
+ // callers that resolved the owning app into basePath (the microservice
21
+ // layout) pass no service, so the path is not nested twice.
22
+ const servicePath = options.service ? `${basePath}/${options.service}` : basePath;
23
+ const dir = `${servicePath}/queries/${names.slug}`;
20
24
  const files = {
21
- [`${servicePath}/queries/${name}/${name}.query.ts`]: `import type { BaseQuery } from "@zudojs/cqrs";
22
-
23
- export interface ${nameCamel}QueryPayload {
24
- readonly [key: string]: unknown;
25
- }
26
-
27
- export class ${nameCamel}Query implements BaseQuery<${nameCamel}QueryPayload> {
28
- readonly queryName = "${name}";
29
-
30
- constructor(public readonly payload: ${nameCamel}QueryPayload) {}
31
- }
32
- `,
33
- [`${servicePath}/queries/${name}/${name}.handler.ts`]: `import type { QueryHandler, QueryResult } from "@zudojs/cqrs";
34
- import { createLogger } from "@zudojs/logger";
35
- import { ${nameCamel}Query } from "./${name}.query.js";
36
-
37
- export class ${nameCamel}QueryHandler implements QueryHandler<${nameCamel}Query> {
38
- private readonly logger = createLogger({ name: "${name}-handler" });
39
-
40
- async handle(query: ${nameCamel}Query): Promise<QueryResult> {
41
- this.logger.info("Processing ${name} query", { payload: query.payload });
42
-
43
- return {
44
- success: true,
45
- data: { items: [], total: 0 },
46
- };
47
- }
48
- }
49
- `,
50
- [`${servicePath}/queries/${name}/index.ts`]: `export { ${nameCamel}Query } from "./${name}.query.js";
51
- export { ${nameCamel}QueryHandler } from "./${name}.handler.js";
52
- `,
25
+ [`${dir}/${names.slug}.query.ts`]: renderQueryFile(names),
26
+ [`${dir}/${names.slug}.handler.ts`]: renderQueryHandlerFile(names),
27
+ [`${dir}/index.ts`]: renderQueryBarrel(names),
53
28
  };
54
29
  if (options.dryRun) {
55
30
  return Object.keys(files);
@@ -59,7 +34,7 @@ export { ${nameCamel}QueryHandler } from "./${name}.handler.js";
59
34
  return Object.keys(files);
60
35
  }
61
36
  catch (error) {
62
- throw new CLIGenerationError(`Failed to generate query: ${name} in ${servicePath}`, error);
37
+ throw new CLIGenerationError(`Failed to generate query: ${names.slug} in ${servicePath}`, error);
63
38
  }
64
39
  }
65
40
  //# sourceMappingURL=query.generator.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * zudojs-cli — Query schematic templates, written against the
3
+ * @zudojs/cqrs API: a `QueryOf` type built with `createQuery`, and a
4
+ * `QueryHandler` subclass (`queryType` + `execute`) registered on a
5
+ * `QueryBus`.
6
+ */
7
+ import type { CqrsSchematicNames } from "../command/command.template.js";
8
+ /** `<slug>.query.ts`: the discriminator, payload, query type and factory. */
9
+ export declare function renderQueryFile(n: CqrsSchematicNames): string;
10
+ /** `<slug>.handler.ts`: the handler class and its bus registration. */
11
+ export declare function renderQueryHandlerFile(n: CqrsSchematicNames): string;
12
+ /** `index.ts`: the query folder's barrel. */
13
+ export declare function renderQueryBarrel(n: CqrsSchematicNames): string;
14
+ //# sourceMappingURL=query.template.d.ts.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * zudojs-cli — Query schematic templates, written against the
3
+ * @zudojs/cqrs API: a `QueryOf` type built with `createQuery`, and a
4
+ * `QueryHandler` subclass (`queryType` + `execute`) registered on a
5
+ * `QueryBus`.
6
+ */
7
+ /** `<slug>.query.ts`: the discriminator, payload, query type and factory. */
8
+ export function renderQueryFile(n) {
9
+ const id = `${n.constant}_QUERY`;
10
+ return `import { createQuery, type QueryOf } from "@zudojs/cqrs";
11
+
12
+ /** Discriminator the query bus routes ${n.pascal} queries on. */
13
+ export const ${id} = "${n.pascal}";
14
+
15
+ /** Criteria a ${n.pascal} query carries. Replace it with the fields the read needs. */
16
+ export type ${n.pascal}QueryPayload = {
17
+ readonly filter: Readonly<Record<string, unknown>>;
18
+ };
19
+
20
+ /** The ${n.pascal} query: \`{ type: "${n.pascal}", filter }\`. */
21
+ export type ${n.pascal}Query = QueryOf<typeof ${id}, ${n.pascal}QueryPayload>;
22
+
23
+ /** Creates an immutable ${n.pascal} query. */
24
+ export function create${n.pascal}Query(payload: ${n.pascal}QueryPayload): ${n.pascal}Query {
25
+ return createQuery(${id}, payload);
26
+ }
27
+ `;
28
+ }
29
+ /** `<slug>.handler.ts`: the handler class and its bus registration. */
30
+ export function renderQueryHandlerFile(n) {
31
+ const id = `${n.constant}_QUERY`;
32
+ return `import { QueryHandler, type QueryBus } from "@zudojs/cqrs";
33
+
34
+ import { ${id}, type ${n.pascal}Query } from "./${n.slug}.query.js";
35
+
36
+ /** What executing a ${n.pascal} query returns. */
37
+ export interface ${n.pascal}QueryResult {
38
+ readonly filter: Readonly<Record<string, unknown>>;
39
+ readonly items: readonly unknown[];
40
+ readonly total: number;
41
+ }
42
+
43
+ /** Handles ${n.pascal} queries: put the read-side logic in \`execute\`. */
44
+ export class ${n.pascal}QueryHandler extends QueryHandler<${n.pascal}Query, ${n.pascal}QueryResult> {
45
+ public override readonly queryType = ${id};
46
+
47
+ public override async execute(query: ${n.pascal}Query): Promise<${n.pascal}QueryResult> {
48
+ return { filter: query.filter, items: [], total: 0 };
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Registers {@link ${n.pascal}QueryHandler} on a query bus.
54
+ *
55
+ * @example
56
+ * const bus = register${n.pascal}Query(createQueryBus());
57
+ * const result = await bus.execute<${n.pascal}Query, ${n.pascal}QueryResult>(
58
+ * create${n.pascal}Query({ filter: {} }),
59
+ * );
60
+ */
61
+ export function register${n.pascal}Query(bus: QueryBus): QueryBus {
62
+ return bus.register(${id}, new ${n.pascal}QueryHandler());
63
+ }
64
+ `;
65
+ }
66
+ /** `index.ts`: the query folder's barrel. */
67
+ export function renderQueryBarrel(n) {
68
+ return `export * from "./${n.slug}.query.js";
69
+ export * from "./${n.slug}.handler.js";
70
+ `;
71
+ }
72
+ //# sourceMappingURL=query.template.js.map
@@ -1,16 +1,22 @@
1
1
  /**
2
2
  * zudojs-cli — Which files a resource-family schematic writes.
3
3
  *
4
- * `resource`, `route`, `controller`, `repository` and `dto` all write part
4
+ * `resource`, `route`, `controller`, `service`, `repository` and `dto` all write part
5
5
  * of the same DTO → repository → service → controller → routes chain. Each
6
6
  * writes its own layer ("primary") and any lower layer that does not exist
7
7
  * yet ("required"), so whatever it writes compiles on its own:
8
8
  * `generate controller users` also writes the users service, repository
9
9
  * and DTO when they are missing, and never touches them when they exist.
10
+ *
11
+ * `service` used to have a generator of its own that wrote
12
+ * `services/<name>/<name>.service.ts`, a second layout next to the
13
+ * `services/<name>.service.ts` every other schematic and the container
14
+ * wiring use; `generate controller` after `generate service` then wrote a
15
+ * second, unrelated service. It is part of the chain now.
10
16
  */
11
17
  import type { ResourceLayer } from "../../templates/resource/index.js";
12
18
  /** Schematics served by the resource generator. */
13
- export declare const RESOURCE_SCHEMATICS: readonly ["resource", "route", "controller", "repository", "dto"];
19
+ export declare const RESOURCE_SCHEMATICS: readonly ["resource", "route", "controller", "service", "repository", "dto"];
14
20
  /** A schematic served by the resource generator. */
15
21
  export type ResourceSchematic = (typeof RESOURCE_SCHEMATICS)[number];
16
22
  /** Whether `schematic` is served by the resource generator. */
@@ -1,18 +1,25 @@
1
1
  /**
2
2
  * zudojs-cli — Which files a resource-family schematic writes.
3
3
  *
4
- * `resource`, `route`, `controller`, `repository` and `dto` all write part
4
+ * `resource`, `route`, `controller`, `service`, `repository` and `dto` all write part
5
5
  * of the same DTO → repository → service → controller → routes chain. Each
6
6
  * writes its own layer ("primary") and any lower layer that does not exist
7
7
  * yet ("required"), so whatever it writes compiles on its own:
8
8
  * `generate controller users` also writes the users service, repository
9
9
  * and DTO when they are missing, and never touches them when they exist.
10
+ *
11
+ * `service` used to have a generator of its own that wrote
12
+ * `services/<name>/<name>.service.ts`, a second layout next to the
13
+ * `services/<name>.service.ts` every other schematic and the container
14
+ * wiring use; `generate controller` after `generate service` then wrote a
15
+ * second, unrelated service. It is part of the chain now.
10
16
  */
11
17
  /** Schematics served by the resource generator. */
12
18
  export const RESOURCE_SCHEMATICS = [
13
19
  "resource",
14
20
  "route",
15
21
  "controller",
22
+ "service",
16
23
  "repository",
17
24
  "dto",
18
25
  ];
@@ -37,6 +44,8 @@ export function planResource(schematic) {
37
44
  };
38
45
  case "controller":
39
46
  return { primary: ["controller"], required: ["dto", "repository", "service"], register: false };
47
+ case "service":
48
+ return { primary: ["service"], required: ["dto", "repository"], register: false };
40
49
  case "repository":
41
50
  return { primary: ["repository"], required: ["dto"], register: false };
42
51
  case "dto":
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { existsSync } from "node:fs";
9
9
  import { join } from "node:path";
10
- import { renderAppPackageDockerfile } from "../../templates/shared/dockerfile.template.js";
10
+ import { renderAppPackageDockerfile } from "../../templates/shared/dockerfile/index.js";
11
11
  import { renderComposeFile, usesPostgres } from "./docker.compose.js";
12
12
  const DOCKERIGNORE = `node_modules
13
13
  **/node_modules
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
- import { renderAppPackageDockerfile } from "../../templates/shared/dockerfile.template.js";
12
+ import { renderAppPackageDockerfile } from "../../templates/shared/dockerfile/index.js";
13
13
  import { writeFile } from "../../utils/utils.fileSystem.js";
14
14
  /** Rewrites unedited CLI Dockerfiles whose Prisma setting is stale; returns their paths. */
15
15
  export async function refreshGeneratedDockerfiles(context) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * zudojs-cli — A resource's DTO schemas and in-memory repository.
3
3
  */
4
- import type { ResourceNames } from "../resource.names.js";
4
+ import { type ResourceNames } from "../resource.names.js";
5
5
  /** `dtos/<slug>.dto.ts`: @zudojs/schema schemas and their inferred types. */
6
6
  export declare function renderResourceDto(n: ResourceNames): string;
7
7
  /** `repositories/<slug>.repository.ts`: the contract and a memory store. */
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * zudojs-cli — A resource's DTO schemas and in-memory repository.
3
3
  */
4
+ import { withArticle } from "../resource.names.js";
4
5
  /** `dtos/<slug>.dto.ts`: @zudojs/schema schemas and their inferred types. */
5
6
  export function renderResourceDto(n) {
6
7
  return `import { schema, type Infer } from "@zudojs/schema";
7
8
 
8
- /** A ${n.label} as the API returns it. */
9
+ /** ${withArticle(n.label, true)} as the API returns it. */
9
10
  export const ${n.entity}Schema = schema.object({
10
11
  id: schema.string().uuid(),
11
12
  name: schema.string(),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * zudojs-cli — A resource's CRUD routes (with OpenAPI metadata) and test.
3
3
  */
4
- import type { ResourceNames } from "../resource.names.js";
4
+ import { type ResourceNames } from "../resource.names.js";
5
5
  /** `routes/<slug>.routes.ts`: `register<Pascal>Routes(router, controller)`. */
6
6
  export declare function renderResourceRoutes(n: ResourceNames): string;
7
7
  /**
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * zudojs-cli — A resource's CRUD routes (with OpenAPI metadata) and test.
3
3
  */
4
+ import { withArticle } from "../resource.names.js";
4
5
  /** `routes/<slug>.routes.ts`: `register<Pascal>Routes(router, controller)`. */
5
6
  export function renderResourceRoutes(n) {
6
7
  const schemas = `Create${n.entity}Schema, ${n.entity}ParamsSchema, ${n.entity}Schema, Update${n.entity}Schema`;
@@ -30,7 +31,7 @@ export function register${n.pascal}Routes(
30
31
 
31
32
  router.get(\`\${BASE_PATH}/:id\`, controller.get, {
32
33
  openapi: {
33
- summary: "Get a ${n.label}",
34
+ summary: "Get ${withArticle(n.label)}",
34
35
  tags: TAGS,
35
36
  params: ${n.entity}ParamsSchema,
36
37
  responses: { "200": { description: "The ${n.label}", schema: ${n.entity}Schema }, "400": invalid, "404": missing },
@@ -39,7 +40,7 @@ export function register${n.pascal}Routes(
39
40
 
40
41
  router.post(BASE_PATH, controller.create, {
41
42
  openapi: {
42
- summary: "Create a ${n.label}",
43
+ summary: "Create ${withArticle(n.label)}",
43
44
  tags: TAGS,
44
45
  body: Create${n.entity}Schema,
45
46
  responses: { "201": { description: "Created", schema: ${n.entity}Schema }, "400": invalid },
@@ -48,7 +49,7 @@ export function register${n.pascal}Routes(
48
49
 
49
50
  router.patch(\`\${BASE_PATH}/:id\`, controller.update, {
50
51
  openapi: {
51
- summary: "Update a ${n.label}",
52
+ summary: "Update ${withArticle(n.label)}",
52
53
  tags: TAGS,
53
54
  params: ${n.entity}ParamsSchema,
54
55
  body: Update${n.entity}Schema,
@@ -58,7 +59,7 @@ export function register${n.pascal}Routes(
58
59
 
59
60
  router.delete(\`\${BASE_PATH}/:id\`, controller.remove, {
60
61
  openapi: {
61
- summary: "Delete a ${n.label}",
62
+ summary: "Delete ${withArticle(n.label)}",
62
63
  tags: TAGS,
63
64
  params: ${n.entity}ParamsSchema,
64
65
  responses: { "204": { description: "Deleted" }, "400": invalid, "404": missing },
@@ -92,7 +93,7 @@ const client = createHttpTestClient(router);
92
93
  afterAll(() => client.close());
93
94
 
94
95
  describe("${n.routePath}", () => {
95
- it("creates, reads, updates and deletes a ${n.label}", async () => {
96
+ it("creates, reads, updates and deletes ${withArticle(n.label)}", async () => {
96
97
  const created = await client
97
98
  .post("${n.routePath}")
98
99
  .send({ name: "Ada" })
@@ -35,4 +35,10 @@ export declare function singularize(slug: string): string;
35
35
  * @throws {CLIValidationError} For unusable or reserved names.
36
36
  */
37
37
  export declare function resourceNames(raw: string): ResourceNames;
38
+ /**
39
+ * Prefixes `noun` with "a" or "an" by its (approximate) opening sound:
40
+ * `example` → "an example", `user` → "a user", `hour` → "an hour".
41
+ * Pass `capitalize` for the start of a sentence ("An example").
42
+ */
43
+ export declare function withArticle(noun: string, capitalize?: boolean): string;
38
44
  //# sourceMappingURL=resource.names.d.ts.map
@@ -57,4 +57,19 @@ export function resourceNames(raw) {
57
57
  routePath: `/api/v1/${slug}`,
58
58
  };
59
59
  }
60
+ /** Words spelled with a vowel but said with a consonant sound ("a user"). */
61
+ const CONSONANT_SOUND = /^(u[bcdfghjklmnpqrstvwxyz][aeiou]|uu|eu|ewe|one|once)/;
62
+ /** Words spelled with a consonant but said with a vowel sound ("an hour"). */
63
+ const VOWEL_SOUND = /^(hour|honest|honor|honour|heir)/;
64
+ /**
65
+ * Prefixes `noun` with "a" or "an" by its (approximate) opening sound:
66
+ * `example` → "an example", `user` → "a user", `hour` → "an hour".
67
+ * Pass `capitalize` for the start of a sentence ("An example").
68
+ */
69
+ export function withArticle(noun, capitalize = false) {
70
+ const word = noun.toLowerCase();
71
+ const vowel = VOWEL_SOUND.test(word) || (/^[aeiou]/.test(word) && !CONSONANT_SOUND.test(word));
72
+ const article = vowel ? "an" : "a";
73
+ return `${capitalize ? article.charAt(0).toUpperCase() + article.slice(1) : article} ${noun}`;
74
+ }
60
75
  //# sourceMappingURL=resource.names.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * zudojs-cli — How a generated Dockerfile installs dependencies.
3
+ *
4
+ * A build stage used to copy only `package.json` and run a floating install
5
+ * (`npm install`), so two builds of the same commit could ship different
6
+ * dependency trees. When the lockfile describes the package being built, it
7
+ * is copied and installed frozen: `npm ci`, `pnpm install --frozen-lockfile`,
8
+ * `yarn install --immutable` (yarn 2+) or `--frozen-lockfile` (yarn 1), and
9
+ * `bun install --frozen-lockfile`.
10
+ *
11
+ * The lockfile is copied with a wildcard (`package-lock.json*`), which
12
+ * matches nothing rather than failing when there is none yet (a project
13
+ * created with `--skip-install`), and the install falls back to resolving
14
+ * from `package.json`, as the comment written above it says.
15
+ */
16
+ /** The lockfile-related part of a Dockerfile build stage. */
17
+ export interface DockerInstallSteps {
18
+ /** Comment lines, already prefixed with `# `. */
19
+ readonly comment: readonly string[];
20
+ /** Files copied next to `package.json` before the install (may be globs). */
21
+ readonly files: readonly string[];
22
+ /** The `RUN` command that installs dependencies. */
23
+ readonly install: string;
24
+ /** The `RUN` command that removes dev dependencies after the build. */
25
+ readonly prune?: string;
26
+ }
27
+ /** Install steps when the project's lockfile describes the package being built. */
28
+ export declare function lockedInstallSteps(packageManager: string): DockerInstallSteps;
29
+ /**
30
+ * Install steps for one app of a workspace, built on its own from its
31
+ * `package.json`. The workspace lockfile at the project root lists every
32
+ * package, so it cannot pin this app's isolated install.
33
+ */
34
+ export declare function workspaceMemberInstallSteps(packageManager: string): DockerInstallSteps;
35
+ //# sourceMappingURL=dockerfile.install.d.ts.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * zudojs-cli — How a generated Dockerfile installs dependencies.
3
+ *
4
+ * A build stage used to copy only `package.json` and run a floating install
5
+ * (`npm install`), so two builds of the same commit could ship different
6
+ * dependency trees. When the lockfile describes the package being built, it
7
+ * is copied and installed frozen: `npm ci`, `pnpm install --frozen-lockfile`,
8
+ * `yarn install --immutable` (yarn 2+) or `--frozen-lockfile` (yarn 1), and
9
+ * `bun install --frozen-lockfile`.
10
+ *
11
+ * The lockfile is copied with a wildcard (`package-lock.json*`), which
12
+ * matches nothing rather than failing when there is none yet (a project
13
+ * created with `--skip-install`), and the install falls back to resolving
14
+ * from `package.json`, as the comment written above it says.
15
+ */
16
+ const FALLBACK = [
17
+ "# The lockfile is optional (copied with a wildcard): with it the install is",
18
+ "# frozen and the image reproducible; with no lockfile yet (nothing has been",
19
+ "# installed) dependencies resolve from package.json. Commit the lockfile.",
20
+ ];
21
+ /** Install steps when the project's lockfile describes the package being built. */
22
+ export function lockedInstallSteps(packageManager) {
23
+ switch (packageManager) {
24
+ case "pnpm":
25
+ return {
26
+ comment: FALLBACK,
27
+ // pnpm-workspace.yaml carries the build-script allow-list pnpm 11 needs.
28
+ files: ["pnpm-lock.yaml*", "pnpm-workspace.yaml"],
29
+ install: "corepack enable && if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; else pnpm install; fi",
30
+ prune: "pnpm prune --prod --ignore-scripts",
31
+ };
32
+ case "yarn":
33
+ return {
34
+ comment: [...FALLBACK, "# yarn 1 takes --frozen-lockfile; yarn 2+ takes --immutable."],
35
+ files: ["yarn.lock*", ".yarnrc.yml*"],
36
+ install: "corepack enable && if [ ! -f yarn.lock ]; then yarn install; " +
37
+ "elif yarn --version | grep -q '^1\\.'; then yarn install --frozen-lockfile; " +
38
+ "else yarn install --immutable; fi",
39
+ };
40
+ case "bun":
41
+ return {
42
+ comment: [...FALLBACK, "# The node image has no bun: it is installed from npm for the build."],
43
+ // bun.lock* matches bun.lock (bun 1.2+) and bun.lockb (older bun).
44
+ files: ["bun.lock*"],
45
+ install: "npm install -g bun && if [ -f bun.lock ] || [ -f bun.lockb ]; then bun install --frozen-lockfile; else bun install; fi",
46
+ // The first install wrote a lockfile if there was none, so this is frozen.
47
+ prune: "rm -rf node_modules && bun install --production --frozen-lockfile --ignore-scripts",
48
+ };
49
+ default:
50
+ return {
51
+ comment: FALLBACK,
52
+ files: ["package-lock.json*"],
53
+ install: "if [ -f package-lock.json ]; then npm ci; else npm install; fi",
54
+ prune: "npm prune --omit=dev --ignore-scripts",
55
+ };
56
+ }
57
+ }
58
+ /**
59
+ * Install steps for one app of a workspace, built on its own from its
60
+ * `package.json`. The workspace lockfile at the project root lists every
61
+ * package, so it cannot pin this app's isolated install.
62
+ */
63
+ export function workspaceMemberInstallSteps(packageManager) {
64
+ const comment = [
65
+ "# This app is one package of a workspace and is installed on its own here.",
66
+ "# The workspace lockfile at the project root describes every package, so it",
67
+ "# cannot pin this install; dependencies resolve from package.json.",
68
+ ];
69
+ switch (packageManager) {
70
+ case "pnpm":
71
+ return {
72
+ comment,
73
+ files: ["pnpm-workspace.yaml"],
74
+ install: "corepack enable && pnpm install",
75
+ prune: "pnpm prune --prod --ignore-scripts",
76
+ };
77
+ case "yarn":
78
+ return { comment, files: [], install: "corepack enable && yarn install" };
79
+ default:
80
+ // The node image has no bun, so a bun workspace app installs with npm.
81
+ return {
82
+ comment,
83
+ files: [],
84
+ install: "npm install",
85
+ prune: "npm prune --omit=dev --ignore-scripts",
86
+ };
87
+ }
88
+ }
89
+ //# sourceMappingURL=dockerfile.install.js.map
@@ -8,13 +8,15 @@
8
8
  * unprivileged `node` user and declares a /health HEALTHCHECK. The images
9
9
  * used to run as root and ship the whole dev toolchain.
10
10
  */
11
- /** The dependency install command for a package manager inside a Dockerfile. */
12
- export declare function dockerInstallCommand(packageManager: string): string;
13
11
  /**
14
12
  * A self-contained Dockerfile for one app package. The build context is the
15
13
  * project root; `appPath` is the app's directory (`.` for a single-app
16
14
  * project). pnpm's `pnpm-workspace.yaml` is copied for its build-script
17
15
  * allow-list, without which pnpm 11 refuses to install.
16
+ *
17
+ * An app at the project root copies its lockfile and installs frozen (see
18
+ * `dockerfile.install.ts`); an app inside a workspace cannot use the
19
+ * workspace lockfile on its own, and its Dockerfile says so.
18
20
  */
19
21
  export declare function renderAppPackageDockerfile(options: {
20
22
  readonly appPath: string;