@dbx-tools/projen 0.9.19 → 0.9.28

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 (56) hide show
  1. package/README.md +83 -229
  2. package/index.ts +11 -18
  3. package/package.json +9 -5
  4. package/src/_task-command.ts +75 -0
  5. package/src/barrels.ts +1 -74
  6. package/src/module-exports.ts +175 -1
  7. package/src/openapi.ts +5 -72
  8. package/src/packages.ts +45 -0
  9. package/src/project-js.ts +5 -48
  10. package/src/project-predicate.ts +1 -2
  11. package/src/project-py.ts +87 -199
  12. package/src/project.ts +2 -2
  13. package/src/python-node-bindings.ts +138 -0
  14. package/src/python-node-bundle.ts +92 -0
  15. package/src/release-catalog.ts +40 -208
  16. package/src/{release-dispatch.ts → release-context.ts} +0 -11
  17. package/src/release.ts +74 -778
  18. package/src/scaffold.ts +2 -6
  19. package/src/workspace-version.ts +9 -121
  20. package/tasks/bump.ts +17 -34
  21. package/tasks/databricks-cli-assets.ts +129 -0
  22. package/tasks/local-publish.ts +61 -138
  23. package/tasks/publish-npm.ts +71 -30
  24. package/tasks/publish-python.ts +61 -148
  25. package/tasks/publish.ts +32 -182
  26. package/tasks/python-node-bindings-watch.ts +47 -0
  27. package/tasks/python-node-bindings.ts +1017 -0
  28. package/tasks/python-registry.ts +2 -9
  29. package/tasks/python-release.ts +74 -0
  30. package/tasks/release-version.ts +23 -2
  31. package/tasks/release.ts +99 -0
  32. package/tasks/sync.ts +13 -18
  33. package/tasks/version-check.ts +13 -72
  34. package/src/_rust-project.ts +0 -429
  35. package/src/_rust-release-workflow.ts +0 -698
  36. package/src/project-rs.ts +0 -1127
  37. package/src/release-plan.ts +0 -269
  38. package/src/release-please.ts +0 -242
  39. package/src/uniffi.ts +0 -65
  40. package/tasks/publish-uniffi-local.ts +0 -314
  41. package/tasks/release-assets.ts +0 -476
  42. package/tasks/release-bootstrap.ts +0 -59
  43. package/tasks/release-candidate.ts +0 -383
  44. package/tasks/release-manifest.ts +0 -273
  45. package/tasks/release-plan.ts +0 -89
  46. package/tasks/release-please.ts +0 -162
  47. package/tasks/release-pr.ts +0 -563
  48. package/tasks/release-request.ts +0 -166
  49. package/tasks/release-summary-units.ts +0 -75
  50. package/tasks/release-summary.ts +0 -381
  51. package/tasks/rust.ts +0 -178
  52. package/tasks/stamp-python.ts +0 -13
  53. package/tasks/uniffi-python.d.ts +0 -61
  54. package/tasks/uniffi-python.js +0 -191
  55. package/tasks/uniffi-release.mjs +0 -573
  56. package/tasks/uniffi.ts +0 -271
@@ -2,6 +2,7 @@
2
2
  /** Idempotent npm archive publication for release recovery. */
3
3
  import { createHash } from "node:crypto";
4
4
  import {
5
+ chmodSync,
5
6
  lstatSync,
6
7
  mkdtempSync,
7
8
  readFileSync,
@@ -15,6 +16,7 @@ import { join, relative, resolve } from "node:path";
15
16
  import * as exec from "@dbx-tools/core/exec";
16
17
  import { log } from "@dbx-tools/shared-core";
17
18
  import { Command } from "commander";
19
+ import { runTaskCommand } from "../src/_task-command.ts";
18
20
 
19
21
  const DEFAULT_REGISTRY = "https://registry.npmjs.org";
20
22
  const logger = log.logger("projen:publish-npm");
@@ -29,6 +31,7 @@ export interface NpmReleaseIdentity {
29
31
  }
30
32
 
31
33
  interface NpmArchiveManifest {
34
+ readonly [key: string]: unknown;
32
35
  readonly dependencies?: Readonly<Record<string, string>>;
33
36
  readonly optionalDependencies?: Readonly<Record<string, string>>;
34
37
  readonly name?: string;
@@ -37,6 +40,19 @@ interface NpmArchiveManifest {
37
40
  readonly version?: string;
38
41
  }
39
42
 
43
+ export type NpmArchiveManifestTransform = (
44
+ manifest: NpmArchiveManifest,
45
+ ) => Readonly<Record<string, unknown>>;
46
+
47
+ function archiveFileEntries(directory: string, prefix = ""): string[] {
48
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
49
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
50
+ return entry.isDirectory()
51
+ ? archiveFileEntries(join(directory, entry.name), relativePath)
52
+ : [relativePath];
53
+ });
54
+ }
55
+
40
56
  function normalizedRepository(value: unknown): string | undefined {
41
57
  const url =
42
58
  typeof value === "string"
@@ -241,25 +257,60 @@ export function packNpmPackage(
241
257
  directory: string,
242
258
  destination: string,
243
259
  path = process.env.PATH ?? "",
260
+ transformManifest?: NpmArchiveManifestTransform,
244
261
  ): string {
245
262
  const executable = process.versions.bun ? process.execPath : "bun";
246
- exec.spawnSync(
247
- executable,
248
- ["pm", "pack", "--destination", destination, "--ignore-scripts", "--quiet"],
249
- {
250
- cwd: directory,
251
- env: { ...process.env, PATH: path },
252
- stdout: "inherit",
253
- stderr: "inherit",
254
- stdin: "ignore",
255
- check: true,
256
- },
257
- );
263
+ const manifest = join(directory, "package.json");
264
+ const manifestMode = lstatSync(manifest).mode & 0o777;
265
+ const restoreManifestMode = (manifestMode & 0o200) === 0;
266
+ if (restoreManifestMode) chmodSync(manifest, manifestMode | 0o200);
267
+ try {
268
+ exec.spawnSync(
269
+ executable,
270
+ ["pm", "pack", "--destination", destination, "--ignore-scripts", "--quiet"],
271
+ {
272
+ cwd: directory,
273
+ env: { ...process.env, PATH: path },
274
+ stdout: "inherit",
275
+ stderr: "inherit",
276
+ stdin: "ignore",
277
+ check: true,
278
+ },
279
+ );
280
+ } finally {
281
+ if (restoreManifestMode) chmodSync(manifest, manifestMode);
282
+ }
258
283
  const archives = readdirSync(destination).filter((file) => file.endsWith(".tgz"));
259
284
  if (archives.length !== 1) {
260
285
  throw new Error(`Expected one packed npm archive, found ${archives.length}`);
261
286
  }
262
- return join(destination, archives[0]);
287
+ const archive = join(destination, archives[0]);
288
+ if (!transformManifest) return archive;
289
+
290
+ const temp = mkdtempSync(join(tmpdir(), "projen-npm-project-"));
291
+ try {
292
+ exec.spawnSync("tar", ["-xzf", archive, "-C", temp], {
293
+ cwd: process.cwd(),
294
+ stdout: "ignore",
295
+ stderr: "inherit",
296
+ stdin: "ignore",
297
+ check: true,
298
+ });
299
+ const manifestPath = join(temp, "package", "package.json");
300
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as NpmArchiveManifest;
301
+ writeFileSync(manifestPath, `${JSON.stringify(transformManifest(manifest), null, 2)}\n`);
302
+ rmSync(archive, { force: true });
303
+ exec.spawnSync("tar", ["-czf", archive, "-C", temp, ...archiveFileEntries(temp)], {
304
+ cwd: process.cwd(),
305
+ stdout: "ignore",
306
+ stderr: "inherit",
307
+ stdin: "ignore",
308
+ check: true,
309
+ });
310
+ return archive;
311
+ } finally {
312
+ rmSync(temp, { recursive: true, force: true });
313
+ }
263
314
  }
264
315
 
265
316
  export async function publishNpmArchives(options: {
@@ -291,23 +342,13 @@ export async function publishNpmArchives(options: {
291
342
  continue;
292
343
  }
293
344
  }
294
- exec.spawnSync(
295
- "npm",
296
- [
297
- "publish",
298
- archive,
299
- ...(local.access ? ["--access", local.access] : []),
300
- ...(options.registry ? ["--registry", options.registry] : []),
301
- ...(options.dryRun ? ["--dry-run"] : []),
302
- ],
303
- {
304
- cwd: process.cwd(),
305
- stdout: "inherit",
306
- stderr: "inherit",
307
- stdin: "ignore",
308
- check: true,
309
- },
310
- );
345
+ runTaskCommand(process.cwd(), "npm", [
346
+ "publish",
347
+ archive,
348
+ ...(local.access ? ["--access", local.access] : []),
349
+ ...(options.registry ? ["--registry", options.registry] : []),
350
+ ...(options.dryRun ? ["--dry-run"] : []),
351
+ ]);
311
352
  }
312
353
  }
313
354
 
@@ -1,38 +1,29 @@
1
1
  #!/usr/bin/env -S bun
2
2
  import {
3
3
  chmodSync,
4
+ cpSync,
4
5
  existsSync,
5
6
  mkdirSync,
6
7
  mkdtempSync,
7
8
  readFileSync,
8
9
  readdirSync,
9
10
  rmSync,
10
- statSync,
11
11
  writeFileSync,
12
12
  } from "node:fs";
13
13
  import { tmpdir } from "node:os";
14
- import { basename, join, resolve } from "node:path";
15
- import * as exec from "@dbx-tools/core/exec";
14
+ import { basename, dirname, join, resolve } from "node:path";
16
15
  import { Command } from "commander";
17
16
  import { parse, stringify } from "smol-toml";
18
- import { pythonProjectInfo, stampPythonProject } from "./uniffi-python.js";
19
- import type { ReleasePlan } from "../src/release-plan.ts";
17
+ import { preparePythonProjectForPublication, pythonProjectInfo } from "./python-release.ts";
18
+ import { runTaskCommand } from "../src/_task-command.ts";
20
19
 
21
20
  interface PythonProjectFile {
22
21
  readonly directory: string;
23
- readonly mode: number;
24
22
  readonly name: string;
25
23
  readonly version: string;
26
24
  readonly path: string;
27
25
  readonly private: boolean;
28
26
  readonly source: string;
29
- readonly uniffi: boolean;
30
- }
31
-
32
- export interface StampPythonProjectsOptions {
33
- readonly excludeUniFFI?: boolean;
34
- readonly rewriteDependencies?: boolean;
35
- readonly versions?: ReadonlyMap<string, string>;
36
27
  }
37
28
 
38
29
  /** Python wheel and source archives accepted by Twine and package indexes. */
@@ -43,16 +34,7 @@ export function pythonDistributionPaths(directory: string): string[] {
43
34
  .map((file) => join(directory, file));
44
35
  }
45
36
 
46
- export interface RestorePythonProjects {
47
- (): void;
48
- readonly paths: readonly string[];
49
- }
50
-
51
- export function stampPythonProjects(
52
- root: string,
53
- version: string,
54
- options: StampPythonProjectsOptions = {},
55
- ): RestorePythonProjects {
37
+ function pythonProjects(root: string): PythonProjectFile[] {
56
38
  const packageFiles = readdirSync(root, { withFileTypes: true })
57
39
  .filter((entry) => entry.isDirectory())
58
40
  .map((entry) => resolve(root, entry.name, "pyproject.toml"))
@@ -60,59 +42,17 @@ export function stampPythonProjects(
60
42
  .sort();
61
43
  const allProjects: PythonProjectFile[] = packageFiles.map((path) => {
62
44
  const source = readFileSync(path, "utf8");
63
- const info = pythonProjectInfo(source, { parse, stringify });
45
+ const info = pythonProjectInfo(source, { parse });
64
46
  return {
65
47
  directory: basename(resolve(path, "..")),
66
- mode: statSync(path).mode,
67
48
  name: info.name,
68
49
  version: info.version,
69
50
  path,
70
51
  private: info.private,
71
52
  source,
72
- uniffi: info.uniffi,
73
53
  };
74
54
  });
75
- const projects = allProjects.filter(
76
- (project) =>
77
- !project.private &&
78
- (!options.excludeUniFFI || !project.uniffi) &&
79
- (!options.versions || options.versions.has(project.directory)),
80
- );
81
- if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
82
-
83
- try {
84
- for (const project of projects) {
85
- const stamped = stampPythonProject(project.source, {
86
- packages: allProjects,
87
- rewriteDependencies: options.rewriteDependencies,
88
- usePackageVersions: options.versions !== undefined,
89
- toml: { parse, stringify },
90
- version: options.versions?.get(project.directory) ?? version,
91
- });
92
- chmodSync(project.path, project.mode | 0o200);
93
- writeFileSync(project.path, stamped);
94
- chmodSync(project.path, project.mode);
95
- }
96
- } catch (error) {
97
- for (const project of projects) {
98
- chmodSync(project.path, project.mode | 0o200);
99
- writeFileSync(project.path, project.source);
100
- chmodSync(project.path, project.mode);
101
- }
102
- throw error;
103
- }
104
-
105
- const restore = () => {
106
- for (const project of projects) {
107
- chmodSync(project.path, project.mode | 0o200);
108
- writeFileSync(project.path, project.source);
109
- chmodSync(project.path, project.mode);
110
- }
111
- };
112
- Object.defineProperty(restore, "paths", {
113
- value: projects.map((project) => project.path),
114
- });
115
- return restore as RestorePythonProjects;
55
+ return allProjects;
116
56
  }
117
57
 
118
58
  export function publishPythonProjects(options: {
@@ -121,12 +61,12 @@ export function publishPythonProjects(options: {
121
61
  readonly publishUrl: string;
122
62
  readonly root: string;
123
63
  readonly version: string;
124
- readonly plan?: ReleasePlan;
125
64
  }): void {
126
65
  const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
127
66
  try {
128
67
  buildPythonProjects({ ...options, output });
129
- exec.spawnSync(
68
+ runTaskCommand(
69
+ process.cwd(),
130
70
  "uvx",
131
71
  [
132
72
  "--from",
@@ -139,14 +79,7 @@ export function publishPythonProjects(options: {
139
79
  ...(options.dryRun ? ["--dry-run"] : []),
140
80
  output,
141
81
  ],
142
- {
143
- cwd: process.cwd(),
144
- env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl },
145
- stdout: "inherit",
146
- stderr: "inherit",
147
- stdin: "ignore",
148
- check: true,
149
- },
82
+ { env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl } },
150
83
  );
151
84
  } finally {
152
85
  rmSync(output, { recursive: true, force: true });
@@ -156,117 +89,97 @@ export function publishPythonProjects(options: {
156
89
  /** Build the exact Python release distributions without publishing them. */
157
90
  export function buildPythonProjects(options: {
158
91
  readonly allowEmpty?: boolean;
159
- readonly excludeUniFFI?: boolean;
160
92
  readonly output: string;
93
+ readonly packages?: readonly string[];
161
94
  readonly root: string;
162
95
  readonly version: string;
163
- readonly plan?: ReleasePlan;
164
96
  }): void {
165
97
  const root = resolve(options.root);
166
98
  const output = resolve(options.output);
167
99
  rmSync(output, { recursive: true, force: true });
168
- const standardProjects = options.excludeUniFFI
169
- ? readdirSync(root, { withFileTypes: true })
170
- .filter((entry) => entry.isDirectory())
171
- .map((entry) => resolve(root, entry.name, "pyproject.toml"))
172
- .filter(existsSync)
173
- .map((path) => pythonProjectInfo(readFileSync(path, "utf8"), { parse, stringify }))
174
- .filter((project) => !project.private && !project.uniffi)
175
- : undefined;
176
- if (standardProjects?.length === 0 && options.allowEmpty) {
100
+ const allProjects = pythonProjects(root);
101
+ const requested = new Set(options.packages ?? []);
102
+ const missing = [...requested].filter(
103
+ (directory) => !allProjects.some((project) => project.directory === directory),
104
+ );
105
+ if (missing.length > 0) throw new Error(`Unknown Python packages: ${missing.join(", ")}`);
106
+ const projects = allProjects.filter(
107
+ (project) => !project.private && (requested.size === 0 || requested.has(project.directory)),
108
+ );
109
+ if (projects.length === 0 && options.allowEmpty) {
177
110
  mkdirSync(output, { recursive: true });
178
111
  return;
179
112
  }
180
- const planned = new Map(
181
- options.plan?.pythonPackages.map((pkg) => [pkg.path.replace(/^.*\//, ""), pkg.version]) ?? [],
182
- );
183
- const stamp = stampPythonProjects(root, options.version, {
184
- excludeUniFFI: options.excludeUniFFI,
185
- ...(options.plan ? { versions: planned } : {}),
186
- });
113
+ if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
114
+
115
+ const temporaryRoot = mkdtempSync(join(tmpdir(), "projen-python-release-"));
187
116
  try {
188
- const packages = options.plan?.pythonPackages ?? [];
189
- if (packages.length > 0) {
190
- for (const pkg of packages) {
191
- exec.spawnSync("uv", ["build", "--package", pkg.identity, "--out-dir", output], {
192
- cwd: process.cwd(),
193
- stdout: "inherit",
194
- stderr: "inherit",
195
- stdin: "ignore",
196
- check: true,
197
- });
198
- }
199
- } else if (options.excludeUniFFI) {
200
- for (const project of standardProjects ?? []) {
201
- exec.spawnSync("uv", ["build", "--package", project.name, "--out-dir", output], {
202
- cwd: process.cwd(),
203
- stdout: "inherit",
204
- stderr: "inherit",
205
- stdin: "ignore",
206
- check: true,
207
- });
208
- }
209
- } else {
210
- exec.spawnSync("uv", ["build", "--all-packages", "--out-dir", output], {
211
- cwd: process.cwd(),
212
- stdout: "inherit",
213
- stderr: "inherit",
214
- stdin: "ignore",
215
- check: true,
216
- });
117
+ mkdirSync(output, { recursive: true });
118
+ for (const project of projects) {
119
+ const packageRoot = join(temporaryRoot, project.directory);
120
+ cpSync(dirname(project.path), packageRoot, { recursive: true });
121
+ const manifestPath = join(packageRoot, "pyproject.toml");
122
+ chmodSync(manifestPath, 0o644);
123
+ writeFileSync(
124
+ manifestPath,
125
+ preparePythonProjectForPublication(project.source, {
126
+ packages: allProjects,
127
+ toml: { parse, stringify },
128
+ version: options.version,
129
+ }),
130
+ );
131
+ runTaskCommand(root, "uv", ["build", "--out-dir", output, packageRoot]);
217
132
  }
218
133
  const distributions = pythonDistributionPaths(output);
219
134
  if (distributions.length === 0) {
220
135
  throw new Error(`No Python distributions found in ${output}`);
221
136
  }
222
- exec.spawnSync("uvx", ["twine", "check", ...distributions], {
223
- cwd: process.cwd(),
224
- stdout: "inherit",
225
- stderr: "inherit",
226
- stdin: "ignore",
227
- check: true,
228
- });
137
+ runTaskCommand(process.cwd(), "uvx", ["twine", "check", ...distributions]);
229
138
  } finally {
230
- stamp();
139
+ rmSync(temporaryRoot, { recursive: true, force: true });
231
140
  }
232
141
  }
233
142
 
234
143
  if (import.meta.main) {
235
144
  const program = new Command();
236
145
  program
237
- .argument("[version]", "Python package version")
238
- .requiredOption("--index-url <url>", "devpi Simple API URL")
239
- .requiredOption("--publish-url <url>", "devpi writable index URL")
146
+ .argument("<version>", "Python package version")
147
+ .option("--index-url <url>", "devpi Simple API URL")
148
+ .option("--publish-url <url>", "devpi writable index URL")
240
149
  .option("--root <path>", "Python workspace package root", "packages/py")
241
- .option("--plan <path>", "affected release plan")
150
+ .option("--package <directory...>", "Build only selected package directories")
242
151
  .option("--output <path>", "Build distributions into a directory without publishing")
243
152
  .option("--dry-run", "build and inspect distributions without uploading")
244
153
  .action(
245
154
  (
246
- version: string | undefined,
155
+ version: string,
247
156
  options: {
248
157
  dryRun?: boolean;
249
- indexUrl: string;
250
- publishUrl: string;
158
+ indexUrl?: string;
159
+ package?: string[];
160
+ publishUrl?: string;
251
161
  root: string;
252
- plan?: string;
253
162
  output?: string;
254
163
  },
255
164
  ) => {
256
- const plan = options.plan
257
- ? (JSON.parse(readFileSync(options.plan, "utf8")) as ReleasePlan)
258
- : undefined;
259
- const fallbackVersion = version ?? plan?.pythonPackages[0]?.version;
260
- if (!fallbackVersion) throw new Error("Python publication requires a version or plan");
261
165
  if (options.output) {
262
166
  buildPythonProjects({
263
167
  output: options.output,
168
+ packages: options.package,
264
169
  root: options.root,
265
- version: fallbackVersion,
266
- plan,
170
+ version,
267
171
  });
268
172
  } else {
269
- publishPythonProjects({ ...options, version: fallbackVersion, plan });
173
+ if (!options.indexUrl || !options.publishUrl) {
174
+ throw new Error("--index-url and --publish-url are required when publishing");
175
+ }
176
+ publishPythonProjects({
177
+ dryRun: options.dryRun,
178
+ indexUrl: options.indexUrl,
179
+ publishUrl: options.publishUrl,
180
+ root: options.root,
181
+ version,
182
+ });
270
183
  }
271
184
  },
272
185
  );