@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
package/tasks/publish.ts CHANGED
@@ -1,73 +1,52 @@
1
1
  #!/usr/bin/env -S bun
2
2
  /**
3
3
  * `bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run] [--skip-compile]`
4
- * - verify every workspace member and the Bun lock carry the release version,
5
- * then pack and publish each package owned by the standard Node release.
4
+ * - verify every workspace member carries the release version, then pack and
5
+ * publish each package owned by the standard Node release.
6
6
  *
7
7
  * Bun has no `pnpm -r publish`, so this loop is the recursive-publish stand-in.
8
8
  * It leans on native bun for everything bun already does:
9
9
  *
10
10
  * - **version validation** fails when any member differs from the reviewed
11
11
  * `VERSION`; publication never repairs committed release metadata;
12
- * - **`workspace:` / `catalog:` rewriting** is NOT done here - `bun publish`
12
+ * - **`workspace:` / `catalog:` rewriting** is NOT done here - `bun pm pack`
13
13
  * strips both protocols in the PACKED tarball, resolving `workspace:*` to the
14
14
  * sibling's version and `catalog:` to the root catalog entry. (Verified: a
15
15
  * packed manifest shows `"@scope/x": "<version>"` and the real catalog range,
16
16
  * while the on-disk manifest keeps the protocols.) Setting each member's
17
17
  * version first is the only prerequisite, so a sibling resolves the release
18
- * version. When Bun's workspace lock already carries the version, its
19
- * refresh is skipped;
20
18
  * - **`publishConfig` substitution** (compiled `lib/` entry points) is done
21
- * HERE, by {@link applyPublishConfig}, NOT by bun: unlike pnpm/npm, `bun
19
+ * inside the temporary archive, NOT in the checkout: unlike pnpm/npm, `bun
22
20
  * publish`/`bun pm pack` do NOT fold `publishConfig`'s `main`/`types`/`bin`/
23
21
  * `exports` into the packed manifest (verified: the packed manifest keeps the
24
22
  * raw `.ts` source paths and an inert `publishConfig`). Left unsubstituted, a
25
23
  * published CLI's `bin` points at `./bin/x.ts`, and because the bin runs via
26
24
  * its `#!/usr/bin/env node` shebang, node chokes on the `.ts`
27
25
  * (ERR_UNKNOWN_FILE_EXTENSION). We merge `publishConfig` onto the top-level
28
- * manifest before packing so the tarball advertises the compiled `lib/` tree;
26
+ * manifest after packing so the tarball advertises the compiled `lib/` tree;
29
27
  * - **compiled output** is emitted once, before packing, by one root-level
30
28
  * filtered `bun run` that fans out to every publishable member in parallel.
31
29
  * Every package is then packed once with lifecycle scripts disabled. The
32
- * exact validated archive is passed to `bun publish`, so upload never
30
+ * exact validated archive is passed to `npm publish`, so upload never
33
31
  * repacks or repeats a member's `prepack`. Packages retain `prepack` for
34
32
  * standalone publishes.
35
33
  * - **release recovery** compares each packed archive's integrity and
36
34
  * repository identity with registry metadata. A matching immutable version
37
35
  * is skipped, while any mismatch fails the retry.
38
36
  *
39
- * `--dry-run` forwards to `bun publish`: it packs + validates
40
- * but uploads nothing, so the `release` workflow is testable end-to-end via a
41
- * `workflow_dispatch` run without anything reaching npm. `--registry` targets a
42
- * non-default registry (a local verdaccio); `--exclude <dir>` (repeatable,
43
- * repo-relative) skips a member owned by another publication flow.
37
+ * `--dry-run` forwards to `npm publish`: it packs + validates
38
+ * but uploads nothing. `--registry` targets a non-default registry (a local
39
+ * verdaccio); `--exclude <dir>` (repeatable, repo-relative) skips a member owned
40
+ * by another publication flow.
44
41
  *
45
- * `--no-restore` keeps the transient publishConfig edits on disk instead of
46
- * undoing them at exit.
47
- *
48
- * The disk manifests carry the workspace `VERSION` (projen owns them, read-only);
49
- * this unlocks each only long enough to fold in the `publishConfig` entry points
50
- * and publish, then RESTORES every one it touched
51
- * byte-for-byte (and re-locks the mode) on the way out - see
52
- * {@link restoreManifests}. Restore returns each manifest to its committed
53
- * content, which already equals the release version, so the worktree is never
54
- * left regressed.
42
+ * The checkout remains byte-for-byte unchanged. A stale manifest is a release
43
+ * error rather than something publication repairs. Local lockfiles are ignored
44
+ * because configured registries can leak into them.
55
45
  */
56
- import {
57
- chmodSync,
58
- copyFileSync,
59
- existsSync,
60
- mkdirSync,
61
- mkdtempSync,
62
- readFileSync,
63
- rmSync,
64
- statSync,
65
- writeFileSync,
66
- } from "node:fs";
46
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
67
47
  import { tmpdir } from "node:os";
68
48
  import { delimiter, dirname, join, resolve } from "node:path";
69
49
  import { asyncUtils, log } from "@dbx-tools/shared-core";
70
- import ts from "typescript";
71
50
  import { parse } from "yaml";
72
51
  import {
73
52
  npmReleaseMatches,
@@ -76,26 +55,9 @@ import {
76
55
  readNpmArchiveIdentity,
77
56
  } from "./publish-npm.ts";
78
57
  import { runTaskCommand, runTaskCommandAsync } from "../src/_task-command.ts";
79
- import { toPosix } from "../src/packages.ts";
80
- import type { ReleasePlan } from "../src/release-plan.ts";
81
58
 
82
59
  const logger = log.logger("projen:publish");
83
60
 
84
- /** A manifest's on-disk bytes + mode, captured before this task edits it. */
85
- interface ManifestBackup {
86
- readonly content: string;
87
- readonly mode: number;
88
- }
89
-
90
- /**
91
- * Original state of every manifest this task has unlocked, keyed by path.
92
- *
93
- * Bun does not apply `publishConfig` while packing, so publication temporarily
94
- * projects those entry points onto projen-owned manifests. The projection is
95
- * only meant for the packed tarball and is restored before the process exits.
96
- */
97
- const manifestBackups = new Map<string, ManifestBackup>();
98
-
99
61
  /**
100
62
  * Workspace member dirs (absolute), read from the root `pnpm-workspace.yaml` - the
101
63
  * file the engine keeps for the Databricks Apps pnpm deploy, which also lists every
@@ -109,78 +71,16 @@ function workspaceMembers(root: string): string[] {
109
71
  return (doc?.packages ?? []).map((m) => resolve(root, m));
110
72
  }
111
73
 
112
- /** Whether selected workspace members carry their planned versions. */
113
- function manifestsMatchVersions(
114
- root: string,
115
- members: readonly string[],
116
- expected: ReadonlyMap<string, string>,
117
- ): boolean {
74
+ /** Whether selected workspace members carry the reviewed workspace version. */
75
+ function manifestsMatchVersion(members: readonly string[], version: string): boolean {
118
76
  return members.every((dir) => {
119
77
  const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
120
78
  version?: string;
121
79
  };
122
- const path = toPosix(dir.slice(resolve(root).length + 1));
123
- return pkg.version === expected.get(path);
80
+ return pkg.version === version;
124
81
  });
125
82
  }
126
83
 
127
- /** Whether Bun's workspace lock records each live workspace manifest version. */
128
- function lockfileMatchesManifestVersions(root: string, members: readonly string[]): boolean {
129
- const lockfile = join(root, "bun.lock");
130
- if (!existsSync(lockfile)) return false;
131
- try {
132
- const parsed = ts.parseConfigFileTextToJson(lockfile, readFileSync(lockfile, "utf8"));
133
- if (parsed.error) return false;
134
- const lock = parsed.config as {
135
- workspaces?: Record<string, { version?: string }>;
136
- };
137
- return members.every((dir) => {
138
- const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
139
- version?: string;
140
- };
141
- const relative = toPosix(dir.slice(resolve(root).length + 1));
142
- return lock.workspaces?.[relative]?.version === manifest.version;
143
- });
144
- } catch {
145
- return false;
146
- }
147
- }
148
-
149
- /**
150
- * Make a projen-readonly manifest writable so the publishConfig projection can
151
- * edit it, recording its original bytes and mode for {@link restoreManifests}
152
- * the first time it is seen.
153
- */
154
- function unlockManifest(pkgPath: string): void {
155
- const mode = statSync(pkgPath).mode;
156
- if (!manifestBackups.has(pkgPath)) {
157
- manifestBackups.set(pkgPath, { content: readFileSync(pkgPath, "utf8"), mode });
158
- }
159
- chmodSync(pkgPath, mode | 0o200);
160
- }
161
-
162
- /**
163
- * Put every manifest this task edited back exactly as it was found, mode included.
164
- *
165
- * Runs from an `exit` handler so a clean finish and a failure partway through the
166
- * publish loop are covered alike - a `bun publish` that dies on package 12 of 34
167
- * must not leave the first 11 projected. Best-effort per file: one unwritable
168
- * manifest is logged and the rest are still restored, since a partial restore
169
- * beats none.
170
- */
171
- function restoreManifests(): void {
172
- for (const [pkgPath, backup] of manifestBackups) {
173
- try {
174
- chmodSync(pkgPath, backup.mode | 0o200);
175
- writeFileSync(pkgPath, backup.content);
176
- chmodSync(pkgPath, backup.mode);
177
- } catch (cause) {
178
- logger.warn(`could not restore ${pkgPath}`, { cause });
179
- }
180
- }
181
- manifestBackups.clear();
182
- }
183
-
184
84
  /** Entry-point fields projen writes as `.ts` source in-repo and rewrites to `lib/` for publish. */
185
85
  const PUBLISH_CONFIG_ENTRY_FIELDS = ["main", "types", "bin", "exports"] as const;
186
86
 
@@ -195,23 +95,18 @@ function compiledPublishTargets(value: unknown): string[] {
195
95
  /**
196
96
  * Fold a package's `publishConfig` entry-point fields onto the top-level manifest,
197
97
  * the way pnpm/npm do at pack time but `bun publish` does NOT (see the module
198
- * doc). Idempotent, writes only when something changes, and leaves `publishConfig`
199
- * in place (npm ignores it once the top-level fields already point at `lib/`). The
200
- * manifest must already be unlocked. {@link restoreManifests} puts the `.ts` entry
201
- * points back at exit, so this only ever affects the packed tarball.
98
+ * doc). The input is the temporary archive manifest, never the checkout.
202
99
  */
203
- function applyPublishConfig(pkgPath: string): void {
204
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
100
+ export function applyPublishConfig(
101
+ source: Readonly<Record<string, unknown>>,
102
+ ): Readonly<Record<string, unknown>> {
103
+ const pkg = { ...source };
205
104
  const publishConfig = pkg.publishConfig as Record<string, unknown> | undefined;
206
- if (!publishConfig) return;
207
- let changed = false;
105
+ if (!publishConfig) return pkg;
208
106
  for (const field of PUBLISH_CONFIG_ENTRY_FIELDS) {
209
- if (field in publishConfig) {
210
- pkg[field] = publishConfig[field];
211
- changed = true;
212
- }
107
+ if (field in publishConfig) pkg[field] = publishConfig[field];
213
108
  }
214
- if (changed) writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
109
+ return pkg;
215
110
  }
216
111
 
217
112
  /**
@@ -229,14 +124,9 @@ const argv = process.argv.slice(2);
229
124
  const version = argv[0] && !argv[0].startsWith("--") ? argv[0] : undefined;
230
125
  const rest = version ? argv.slice(1) : argv;
231
126
  const root = process.cwd();
232
- const planIdx = rest.indexOf("--plan");
233
- const plan =
234
- planIdx >= 0
235
- ? (JSON.parse(readFileSync(resolve(root, rest[planIdx + 1]), "utf8")) as ReleasePlan)
236
- : undefined;
237
- if (!version && !plan) {
127
+ if (!version) {
238
128
  logger.error(
239
- "usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--output <dir>] [--exclude <dir>] [--dry-run] [--skip-compile]",
129
+ "usage: bun tasks/publish.ts <version> [--registry <url>] [--output <dir>] [--exclude <dir>] [--dry-run] [--skip-compile]",
240
130
  );
241
131
  process.exit(1);
242
132
  }
@@ -252,47 +142,21 @@ if (!Number.isInteger(parsedConcurrency) || parsedConcurrency < 1) {
252
142
  throw new Error(`--concurrency must be a positive integer, got ${String(parsedConcurrency)}`);
253
143
  }
254
144
  const concurrency = parsedConcurrency;
255
- // Undo transient publishConfig edits at exit.
256
- const restore = !rest.includes("--no-restore");
257
145
  const excluded = new Set(
258
146
  rest.reduce<string[]>((acc, arg, i) => (arg === "--exclude" ? [...acc, rest[i + 1]] : acc), []),
259
147
  );
260
148
 
261
149
  const path = enrichedPath(root);
262
- // Registered BEFORE the first manifest edit so no exit path can skip it: a clean
263
- // finish and a `bun publish` failure mid-loop both land here, so a publish that
264
- // dies partway does not leave half the workspace stamped.
265
- if (restore) process.on("exit", restoreManifests);
266
150
  const allMembers = workspaceMembers(root)
267
151
  .filter((dir) => existsSync(join(dir, "package.json")))
268
152
  .filter((dir) => !excluded.has(resolve(root, dir).replace(`${resolve(root)}/`, "")));
269
- const expectedVersions = new Map(
270
- plan
271
- ? plan.nodePackages.map((pkg) => [pkg.path, pkg.version] as const)
272
- : allMembers.map((dir) => [toPosix(dir.slice(resolve(root).length + 1)), version!]),
273
- );
274
- const members = plan
275
- ? allMembers.filter((dir) => expectedVersions.has(toPosix(dir.slice(resolve(root).length + 1))))
276
- : allMembers;
153
+ const members = allMembers;
277
154
 
278
- if (!manifestsMatchVersions(root, members, expectedVersions)) {
279
- throw new Error("workspace manifests do not match the reviewed release plan; run projen");
155
+ if (!manifestsMatchVersion(members, version)) {
156
+ throw new Error(`workspace manifests do not match release ${version}; run projen`);
280
157
  }
281
158
  logger.info(`validated ${members.length} selected member manifests`);
282
159
 
283
- // Ensure the lockfile resolves each `workspace:*` to the release version.
284
- // `bun publish`/`pm pack` reads workspace versions from the LOCKFILE, not just
285
- // live manifests. A normal bump's synth/install already makes it current; after
286
- // fallback stamping, deleting it before install is what forces re-resolution.
287
- const lockfile = join(root, "bun.lock");
288
- if (lockfileMatchesManifestVersions(root, allMembers)) {
289
- logger.info("workspace lock already resolves live member versions");
290
- } else {
291
- if (existsSync(lockfile)) rmSync(lockfile);
292
- logger.info("refreshing lockfile so workspace deps resolve to the release version");
293
- runTaskCommand(root, "bun", ["install"], { env: { ...process.env, PATH: path } });
294
- }
295
-
296
160
  const publishArgs = [
297
161
  "--ignore-scripts",
298
162
  ...(registry ? ["--registry", registry] : []),
@@ -311,17 +175,12 @@ for (const dir of members) {
311
175
  name?: string;
312
176
  version?: string;
313
177
  private?: boolean;
314
- dbxToolsConfig?: { uniffi?: boolean };
315
178
  publishConfig?: { access?: unknown };
316
179
  };
317
180
  if (pkg.private) {
318
181
  logger.info(`skip private ${pkg.name ?? dirname(dir)}`);
319
182
  continue;
320
183
  }
321
- if (pkg.dbxToolsConfig?.uniffi === true) {
322
- logger.info(`skip UniFFI ${pkg.name ?? dirname(dir)}`);
323
- continue;
324
- }
325
184
  if (!pkg.version) throw new Error(`Missing package version for ${pkg.name ?? dirname(dir)}`);
326
185
  const access = pkg.publishConfig?.access;
327
186
  if (access !== undefined && access !== "public" && access !== "restricted") {
@@ -365,15 +224,6 @@ if (compiled.length > 0) {
365
224
  }
366
225
  }
367
226
 
368
- // Compile against workspace source exports first. Switching manifests to their
369
- // publishConfig entries before compilation makes consumers resolve sibling
370
- // `lib/*.d.ts` files that have not been emitted yet in a clean checkout.
371
- for (const { dir } of publishable) {
372
- const manifestPath = join(dir, "package.json");
373
- unlockManifest(manifestPath);
374
- applyPublishConfig(manifestPath);
375
- }
376
-
377
227
  logger.info(
378
228
  `${output ? "packing" : dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
379
229
  );
@@ -386,7 +236,7 @@ await asyncUtils.mapConcurrent(
386
236
  async ({ dir, name, version: packageVersion, access }) => {
387
237
  const packed = mkdtempSync(join(tmpdir(), "projen-npm-release-"));
388
238
  try {
389
- const archive = packNpmPackage(dir, packed, path);
239
+ const archive = packNpmPackage(dir, packed, path, applyPublishConfig);
390
240
  const local = readNpmArchiveIdentity(archive);
391
241
  if (local.name !== name || local.version !== packageVersion) {
392
242
  throw new Error(
@@ -417,7 +267,7 @@ await asyncUtils.mapConcurrent(
417
267
  logger.info(`${dryRun ? "dry-run publishing" : "publishing"} ${name} @ ${packageVersion}`);
418
268
  await runTaskCommandAsync(
419
269
  dir,
420
- "bun",
270
+ "npm",
421
271
  ["publish", ...(access ? ["--access", access] : []), ...publishArgs, archive],
422
272
  { env: { ...process.env, PATH: path } },
423
273
  );
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env -S bun
2
+ import { dirname, isAbsolute, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseArgs } from "node:util";
5
+ import { log } from "@dbx-tools/shared-core";
6
+ import { runTaskCommand } from "../src/_task-command.ts";
7
+ import { pythonNodeBindingProjects, repoRoot } from "../src/packages.ts";
8
+ import {
9
+ pythonNodeBindingWatchInputs,
10
+ resolvePythonNodeBindings,
11
+ type ResolvedPythonNodeBindings,
12
+ } from "../src/python-node-bindings.ts";
13
+ import { watchLoop } from "../src/watch.ts";
14
+
15
+ const logger = log.logger("projen:python-node-bindings");
16
+ const task = resolve(dirname(fileURLToPath(import.meta.url)), "python-node-bindings.ts");
17
+ const { values } = parseArgs({ options: { project: { type: "string" } } });
18
+ const projects = values.project ? [values.project] : pythonNodeBindingProjects(repoRoot);
19
+ const configs = projects.map((project) => resolvePythonNodeBindings(repoRoot, project));
20
+
21
+ if (configs.length > 0) {
22
+ watchLoop(
23
+ "python-node-bindings",
24
+ [...new Set(configs.flatMap(pythonNodeBindingWatchInputs))],
25
+ (changed) => {
26
+ for (const config of configs.filter((candidate) => affected(candidate, changed))) {
27
+ logger.start(`generating ${config.project} Node bindings`);
28
+ runTaskCommand(repoRoot, "bun", [task, "--project", config.project]);
29
+ logger.success(`generated ${config.project} Node bindings`);
30
+ }
31
+ },
32
+ {
33
+ check: (changed) => configs.some((config) => affected(config, changed)),
34
+ },
35
+ );
36
+ }
37
+
38
+ function affected(config: ResolvedPythonNodeBindings, changed: readonly string[]): boolean {
39
+ const inputs = pythonNodeBindingWatchInputs(config);
40
+ return changed.some((path) => inputs.some((input) => contains(input, path)));
41
+ }
42
+
43
+ function contains(input: string, candidate: string): boolean {
44
+ const root = isAbsolute(input) ? input : resolve(repoRoot, input);
45
+ const path = isAbsolute(candidate) ? candidate : resolve(repoRoot, candidate);
46
+ return path === root || path.startsWith(root + sep);
47
+ }