@dbx-tools/projen 0.9.106 → 0.9.108

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.
@@ -17,8 +17,14 @@ import {
17
17
  import { tmpdir } from "node:os";
18
18
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
19
19
  import * as projectUtils from "@dbx-tools/core/project-utils";
20
- import { Command } from "commander";
21
20
  import { parse, stringify } from "smol-toml";
21
+ import { z } from "zod";
22
+ import { runTaskMain, taskCommand, taskOptions, taskPositionals } from "./cli.ts";
23
+ import {
24
+ TaskDirectoriesOptionSchema,
25
+ TaskDryRunOptionSchema,
26
+ TaskOutputOptionSchema,
27
+ } from "./options.ts";
22
28
  import { preparePythonProjectForPublication, pythonProjectInfo } from "./python-release.ts";
23
29
  import { runTaskCommand } from "../src/_task-command.ts";
24
30
 
@@ -182,56 +188,48 @@ function buildPythonProjectsInternal(
182
188
  }
183
189
 
184
190
  /** Parse command-line publication options and run the Python release task. */
185
- export async function main(): Promise<void> {
186
- const program = new Command();
187
- program
188
- .argument("<version>", "Python package version")
189
- .option("--index-url <url>", "devpi Simple API URL")
190
- .option("--publish-url <url>", "devpi writable index URL")
191
- .option("--root <path>", "Python workspace package root", "packages/py")
192
- .option("--package <directory...>", "Build only selected package directories")
193
- .option("--output <path>", "Build distributions into a directory without publishing")
194
- .option(
195
- "--package-directories",
196
- "Keep each package's distributions in its own output directory",
197
- )
198
- .option("--dry-run", "build and inspect distributions without uploading")
199
- .action(
200
- (
201
- version: string,
202
- options: {
203
- dryRun?: boolean;
204
- indexUrl?: string;
205
- package?: string[];
206
- packageDirectories?: boolean;
207
- publishUrl?: string;
208
- root: string;
209
- output?: string;
210
- },
211
- ) => {
212
- if (options.output) {
213
- buildPythonProjects({
214
- output: options.output,
215
- packages: options.package,
216
- packageDirectories: options.packageDirectories,
217
- root: options.root,
218
- version,
219
- });
220
- } else {
221
- if (!options.indexUrl || !options.publishUrl) {
222
- throw new Error("--index-url and --publish-url are required when publishing");
223
- }
224
- publishPythonProjects({
225
- dryRun: options.dryRun,
226
- indexUrl: options.indexUrl,
227
- publishUrl: options.publishUrl,
228
- root: options.root,
229
- version,
230
- });
231
- }
232
- },
233
- );
234
- await program.parseAsync();
191
+ export const PublishPythonOptionsSchema = z.object({
192
+ indexUrl: z.string().trim().min(1).optional().describe("devpi Simple API URL"),
193
+ publishUrl: z.string().trim().min(1).optional().describe("devpi writable index URL"),
194
+ root: z.string().trim().min(1).default("packages/py").describe("Python workspace package root"),
195
+ package: TaskDirectoriesOptionSchema.describe("Repeatable Python package directory to build"),
196
+ output: TaskOutputOptionSchema.describe("Build distributions into this directory"),
197
+ packageDirectories: z
198
+ .boolean()
199
+ .default(false)
200
+ .describe("Keep each package's distributions in its own output directory"),
201
+ dryRun: TaskDryRunOptionSchema,
202
+ });
203
+
204
+ /** Parse task options and build or publish Python workspace distributions. */
205
+ export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
206
+ const command = taskCommand(
207
+ import.meta.url,
208
+ "Build or publish Python workspace distributions",
209
+ PublishPythonOptionsSchema,
210
+ ).argument("<version>", "Python package version");
211
+ const options = await taskOptions(command, PublishPythonOptionsSchema, args);
212
+ const version = String(taskPositionals(command)[0]);
213
+ if (options.output) {
214
+ buildPythonProjects({
215
+ output: options.output,
216
+ packages: options.package.length ? options.package : undefined,
217
+ packageDirectories: options.packageDirectories,
218
+ root: options.root,
219
+ version,
220
+ });
221
+ return;
222
+ }
223
+ if (!options.indexUrl || !options.publishUrl) {
224
+ throw new Error("--index-url and --publish-url are required when publishing");
225
+ }
226
+ publishPythonProjects({
227
+ dryRun: options.dryRun,
228
+ indexUrl: options.indexUrl,
229
+ publishUrl: options.publishUrl,
230
+ root: options.root,
231
+ version,
232
+ });
235
233
  }
236
234
 
237
- if (import.meta.main) await main();
235
+ await runTaskMain(import.meta, main);
package/tasks/publish.ts CHANGED
@@ -47,6 +47,15 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync
47
47
  import { tmpdir } from "node:os";
48
48
  import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
49
49
  import { asyncUtils, log, object } from "@dbx-tools/shared-core";
50
+ import { z } from "zod";
51
+ import { runTaskMain, taskCommand, taskOptions, taskPositionals, taskRoot } from "./cli.ts";
52
+ import {
53
+ TaskConcurrencyOptionSchema,
54
+ TaskDirectoriesOptionSchema,
55
+ TaskDryRunOptionSchema,
56
+ TaskOutputOptionSchema,
57
+ TaskRootOptionSchema,
58
+ } from "./options.ts";
50
59
  import {
51
60
  applyPublishConfig,
52
61
  npmReleaseMatches,
@@ -100,31 +109,34 @@ function outputDirectory(root: string, value: string | undefined): string | unde
100
109
  return output;
101
110
  }
102
111
 
112
+ export const PublishOptionsSchema = z.object({
113
+ root: TaskRootOptionSchema,
114
+ registry: z
115
+ .string()
116
+ .trim()
117
+ .min(1)
118
+ .optional()
119
+ .describe("npm registry URL")
120
+ .meta({ env: "NPM_CONFIG_REGISTRY" }),
121
+ output: TaskOutputOptionSchema,
122
+ exclude: TaskDirectoriesOptionSchema.describe("Repeatable workspace directory to exclude"),
123
+ dryRun: TaskDryRunOptionSchema,
124
+ skipCompile: z.boolean().default(false).describe("Reuse already validated compiled output"),
125
+ concurrency: TaskConcurrencyOptionSchema.default(4),
126
+ });
127
+
103
128
  export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
104
- const version = argv[0] && !argv[0].startsWith("--") ? argv[0] : undefined;
105
- const rest = version ? argv.slice(1) : argv;
106
- const root = process.cwd();
107
- if (!version) {
108
- logger.error(
109
- "usage: bun tasks/publish.ts <version> [--registry <url>] [--output <dir>] [--exclude <dir>] [--dry-run] [--skip-compile]",
110
- );
111
- process.exit(1);
112
- }
113
- const registryIdx = rest.indexOf("--registry");
114
- const registry = registryIdx >= 0 ? rest[registryIdx + 1] : undefined;
115
- const outputIdx = rest.indexOf("--output");
116
- const output = outputDirectory(root, outputIdx >= 0 ? rest[outputIdx + 1] : undefined);
117
- const dryRun = rest.includes("--dry-run");
118
- const skipCompile = rest.includes("--skip-compile");
119
- const concurrencyIdx = rest.indexOf("--concurrency");
120
- const parsedConcurrency = Number(concurrencyIdx >= 0 ? rest[concurrencyIdx + 1] : 4);
121
- if (!Number.isInteger(parsedConcurrency) || parsedConcurrency < 1) {
122
- throw new Error(`--concurrency must be a positive integer, got ${String(parsedConcurrency)}`);
123
- }
124
- const concurrency = parsedConcurrency;
125
- const excluded = new Set(
126
- rest.reduce<string[]>((acc, arg, i) => (arg === "--exclude" ? [...acc, rest[i + 1]] : acc), []),
127
- );
129
+ const command = taskCommand(
130
+ import.meta.url,
131
+ "Build and publish the current npm workspace",
132
+ PublishOptionsSchema,
133
+ ).argument("<version>", "Workspace release version");
134
+ const options = await taskOptions(command, PublishOptionsSchema, argv);
135
+ const version = String(taskPositionals(command)[0]);
136
+ const root = taskRoot(options.root);
137
+ const output = outputDirectory(root, options.output);
138
+ const { registry, dryRun, skipCompile, concurrency } = options;
139
+ const excluded = new Set(options.exclude);
128
140
 
129
141
  const path = enrichedPath(root);
130
142
  const allMembers = recordedPackages(root)
@@ -265,4 +277,4 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
265
277
  );
266
278
  }
267
279
 
268
- if (import.meta.main) await main();
280
+ await runTaskMain(import.meta, main);
@@ -3,6 +3,7 @@
3
3
  import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { log } from "@dbx-tools/shared-core";
6
+ import { runTaskMain } from "./cli.ts";
6
7
  import { runTaskCommand } from "../src/_task-command.ts";
7
8
  import { repoRoot } from "../src/packages.ts";
8
9
  import {
@@ -91,4 +92,4 @@ export async function main(): Promise<void> {
91
92
  );
92
93
  }
93
94
 
94
- if (import.meta.main) await main();
95
+ await runTaskMain(import.meta, main);
@@ -6,11 +6,13 @@
6
6
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { dirname, join, relative, resolve } from "node:path";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
- import { parseArgs } from "node:util";
10
9
  import { log, stringUtils } from "@dbx-tools/shared-core";
11
10
  import type { BunPlugin } from "bun";
12
11
  import stdLibBrowser from "node-stdlib-browser";
13
12
  import ts from "typescript";
13
+ import { z } from "zod";
14
+ import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
15
+ import { TaskCheckOptionSchema, TaskForceOptionSchema, TaskRootOptionSchema } from "./options.ts";
14
16
  import { header, makeReadonly, makeWritable } from "../src/generated.ts";
15
17
  import { publicFunctionExports } from "../src/module-exports.ts";
16
18
  import { resolveRepoRoot } from "../src/packages.ts";
@@ -34,26 +36,37 @@ const logger = log.logger("projen:python-node-bindings");
34
36
 
35
37
  type FunctionOverride = ResolvedPythonNodeFunctionOverride;
36
38
 
37
- function bindingIndex(value: string, count: number, project: string): number {
38
- const index = Number.parseInt(value, 10);
39
- if (!Number.isSafeInteger(index) || String(index) !== value || index < 0 || index >= count) {
40
- throw new Error(`Node binding index ${value} is not configured for ${project}`);
39
+ function bindingIndex(index: number, count: number, project: string): number {
40
+ if (index >= count) {
41
+ throw new Error(`Node binding index ${index} is not configured for ${project}`);
41
42
  }
42
43
  return index;
43
44
  }
44
45
 
46
+ export const PythonNodeBindingsOptionsSchema = z.object({
47
+ binding: z.coerce
48
+ .number()
49
+ .int()
50
+ .nonnegative()
51
+ .optional()
52
+ .describe("Configured binding index within the selected project"),
53
+ check: TaskCheckOptionSchema,
54
+ force: TaskForceOptionSchema,
55
+ module: z.string().trim().min(1).optional().describe("Configured package module to generate"),
56
+ project: z.string().trim().min(1).optional().describe("Python project directory"),
57
+ root: TaskRootOptionSchema,
58
+ });
59
+
45
60
  export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
46
- const { values } = parseArgs({
61
+ const values = await taskOptions(
62
+ taskCommand(
63
+ import.meta.url,
64
+ "Generate PythonMonkey runtime and typed Python wrappers",
65
+ PythonNodeBindingsOptionsSchema,
66
+ ),
67
+ PythonNodeBindingsOptionsSchema,
47
68
  args,
48
- options: {
49
- binding: { type: "string" },
50
- check: { type: "boolean" },
51
- force: { type: "boolean" },
52
- module: { type: "string" },
53
- project: { type: "string" },
54
- root: { type: "string" },
55
- },
56
- });
69
+ );
57
70
  const root = values.root ? resolve(values.root) : resolveRepoRoot();
58
71
  if (!values.project) {
59
72
  if (values.binding !== undefined || values.module !== undefined) {
@@ -1186,4 +1199,4 @@ function pythonRuntimeLoader(source: string): string {
1186
1199
  ].join("\n");
1187
1200
  }
1188
1201
 
1189
- if (import.meta.main) await main();
1202
+ await runTaskMain(import.meta, main);
@@ -2,8 +2,10 @@
2
2
  /** Keep the one package-owned PythonMonkey shim runtime current. */
3
3
  import { existsSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
- import { parseArgs } from "node:util";
6
5
  import { log } from "@dbx-tools/shared-core";
6
+ import { z } from "zod";
7
+ import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
8
+ import { TaskProjectOptionSchema } from "./options.ts";
7
9
  import { runTaskCommand } from "../src/_task-command.ts";
8
10
  import { repoRoot } from "../src/packages.ts";
9
11
  import { watchLoop } from "../src/watch.ts";
@@ -12,13 +14,12 @@ import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
12
14
  const logger = log.logger("projen:python-node-runtime-watch");
13
15
 
14
16
  export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
15
- const { values } = parseArgs({
17
+ const schema = z.object({ project: TaskProjectOptionSchema });
18
+ const { project } = await taskOptions(
19
+ taskCommand(import.meta.url, "Watch and rebuild the shared Python Node runtime", schema),
20
+ schema,
16
21
  args,
17
- options: { project: { type: "string" } },
18
- strict: true,
19
- });
20
- const project = values.project?.trim();
21
- if (!project) throw new Error("--project is required");
22
+ );
22
23
 
23
24
  const projectDirectory = resolve(repoRoot, project);
24
25
  const buildScript = resolve(projectDirectory, "build-runtime.ts");
@@ -37,4 +38,4 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
37
38
  });
38
39
  }
39
40
 
40
- if (import.meta.main) await main();
41
+ await runTaskMain(import.meta, main);
@@ -23,6 +23,13 @@ import { basename, dirname, join, resolve } from "node:path";
23
23
  import * as exec from "@dbx-tools/core/exec";
24
24
  import { withFileLock } from "@dbx-tools/core/file-lock";
25
25
  import { parse } from "smol-toml";
26
+ import { z } from "zod";
27
+ import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
28
+ import {
29
+ TaskCheckOptionSchema,
30
+ TaskForceOptionSchema,
31
+ TaskProjectOptionSchema,
32
+ } from "./options.ts";
26
33
  import { PYTHON_GENERATED_PACKAGE, PYTHON_SYNC_PACKAGE } from "../src/generated.ts";
27
34
 
28
35
  /** AST rewriter that points absolute imports of synchronized modules at their generated package. */
@@ -50,13 +57,17 @@ interface ParsedSource {
50
57
  }
51
58
 
52
59
  export async function main(args: readonly string[] = process.argv.slice(2)): Promise<void> {
53
- const projectIndex = args.indexOf("--project");
54
- if (projectIndex < 0 || !args[projectIndex + 1]) {
55
- throw new Error("python-sync requires --project <directory>");
56
- }
57
- const project = resolve(args[projectIndex + 1]!);
58
- const force = args.includes("--force");
59
- const check = args.includes("--check");
60
+ const schema = z.object({
61
+ project: TaskProjectOptionSchema,
62
+ force: TaskForceOptionSchema,
63
+ check: TaskCheckOptionSchema,
64
+ });
65
+ const options = await taskOptions(
66
+ taskCommand(import.meta.url, "Synchronize pinned Git sources into Python packages", schema),
67
+ schema,
68
+ args,
69
+ );
70
+ const project = resolve(options.project);
60
71
  const pyproject = parse(readFileSync(join(project, "pyproject.toml"), "utf8")) as {
61
72
  tool?: {
62
73
  uv?: { "build-backend"?: { "module-name"?: string; "module-root"?: string } };
@@ -73,7 +84,7 @@ export async function main(args: readonly string[] = process.argv.slice(2)): Pro
73
84
  const generatedPackage = [moduleName, PYTHON_GENERATED_PACKAGE, PYTHON_SYNC_PACKAGE].join(".");
74
85
  const generatedRoot = join(moduleRoot, ...generatedPackage.split("."));
75
86
  for (const config of configs) {
76
- await synchronize(config, generatedRoot, generatedPackage, { check, force });
87
+ await synchronize(config, generatedRoot, generatedPackage, options);
77
88
  }
78
89
  }
79
90
 
@@ -327,4 +338,4 @@ function makeReadonly(path: string): void {
327
338
  }
328
339
  }
329
340
 
330
- if (import.meta.main) await main();
341
+ await runTaskMain(import.meta, main);
@@ -1,18 +1,27 @@
1
1
  #!/usr/bin/env -S bun
2
2
  /** Export the current annotated release tag's step selection to GitHub Actions. */
3
3
  import { appendFileSync } from "node:fs";
4
- import { parseArgs } from "node:util";
4
+ import { z } from "zod";
5
+ import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
5
6
  import { captureTaskCommand } from "../src/_task-command.ts";
6
7
  import { parseReleaseTagAnnotation } from "../src/release-options.ts";
7
8
 
9
+ export const ReleaseOptionsTaskSchema = z.object({
10
+ tag: z.string().trim().min(1).describe("Annotated release tag"),
11
+ output: z.string().trim().min(1).describe("GitHub Actions output file"),
12
+ });
13
+
8
14
  /** Verify and export a tag policy using the shared annotated-tag contract. */
9
- export function main(args: string[] = process.argv.slice(2)): void {
10
- const { values } = parseArgs({
15
+ export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
16
+ const values = await taskOptions(
17
+ taskCommand(
18
+ import.meta.url,
19
+ "Export release tag selections to GitHub Actions",
20
+ ReleaseOptionsTaskSchema,
21
+ ),
22
+ ReleaseOptionsTaskSchema,
11
23
  args,
12
- options: { tag: { type: "string" }, output: { type: "string" } },
13
- strict: true,
14
- });
15
- if (!values.tag || !values.output) throw new Error("--tag and --output are required");
24
+ );
16
25
  const annotation = captureTaskCommand(
17
26
  process.cwd(),
18
27
  "git",
@@ -29,4 +38,4 @@ export function main(args: string[] = process.argv.slice(2)): void {
29
38
  );
30
39
  }
31
40
 
32
- if (import.meta.main) main();
41
+ await runTaskMain(import.meta, main);
@@ -1,11 +1,20 @@
1
1
  #!/usr/bin/env -S bun
2
2
  /** Validate one stable release version against the canonical workspace policy. */
3
3
  import { resolve } from "node:path";
4
- import { parseArgs } from "node:util";
5
4
  import * as projectUtils from "@dbx-tools/core/project-utils";
5
+ import { z } from "zod";
6
+ import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
7
+ import { TaskRootOptionSchema } from "./options.ts";
6
8
  import { captureTaskCommand } from "../src/_task-command.ts";
7
9
  import { compareSemver, parseSemver, type Semver } from "../src/workspace-version.ts";
8
10
 
11
+ export const ReleaseVersionOptionsSchema = z.object({
12
+ version: z.string().trim().min(1).describe("Release version to validate"),
13
+ root: TaskRootOptionSchema,
14
+ prefix: z.array(z.string().trim().min(1)).default([]).describe("Allowed release tag prefix"),
15
+ assertNext: z.boolean().default(false).describe("Require a version newer than published tags"),
16
+ });
17
+
9
18
  function publishedVersion(root: string, prefixes: readonly string[]): string | undefined {
10
19
  let best: Semver | undefined;
11
20
  for (const prefix of prefixes) {
@@ -46,23 +55,21 @@ export function assertReleaseVersion(
46
55
  }
47
56
  }
48
57
 
49
- export function main(args: string[] = process.argv.slice(2)): void {
50
- const { values } = parseArgs({
58
+ export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
59
+ const values = await taskOptions(
60
+ taskCommand(
61
+ import.meta.url,
62
+ "Validate a stable workspace release version",
63
+ ReleaseVersionOptionsSchema,
64
+ ),
65
+ ReleaseVersionOptionsSchema,
51
66
  args,
52
- options: {
53
- version: { type: "string" },
54
- root: { type: "string" },
55
- prefix: { type: "string", multiple: true },
56
- "assert-next": { type: "boolean", default: false },
57
- },
58
- strict: true,
59
- });
60
- if (!values.version) throw new Error("--version is required");
67
+ );
61
68
  assertReleaseVersion(values.version, {
62
69
  ...(values.root ? { root: values.root } : {}),
63
- ...(values.prefix ? { prefixes: values.prefix } : {}),
64
- assertNext: values["assert-next"],
70
+ ...(values.prefix.length ? { prefixes: values.prefix } : {}),
71
+ assertNext: values.assertNext,
65
72
  });
66
73
  }
67
74
 
68
- if (import.meta.main) main();
75
+ await runTaskMain(import.meta, main);