@dbx-tools/projen 0.9.19 → 0.9.21

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 (104) hide show
  1. package/._LICENSE +0 -0
  2. package/._README.md +0 -0
  3. package/._index.ts +0 -0
  4. package/._package.json +0 -0
  5. package/README.md +67 -83
  6. package/index.ts +2 -7
  7. package/package.json +9 -5
  8. package/src/.__lazy-require.ts +0 -0
  9. package/src/.__release-platform.ts +0 -0
  10. package/src/.__rust-project.ts +0 -0
  11. package/src/.__rust-release-workflow.ts +0 -0
  12. package/src/.__task-command.ts +0 -0
  13. package/src/._barrels.ts +0 -0
  14. package/src/._bun-app.ts +0 -0
  15. package/src/._bun-workflow.ts +0 -0
  16. package/src/._clean.ts +0 -0
  17. package/src/._codegen.ts +0 -0
  18. package/src/._dbx-tools-config.ts +0 -0
  19. package/src/._engine-root.ts +0 -0
  20. package/src/._generated.ts +0 -0
  21. package/src/._mixin.ts +0 -0
  22. package/src/._module-exports.ts +0 -0
  23. package/src/._openapi.ts +0 -0
  24. package/src/._packages.ts +0 -0
  25. package/src/._pnpm-workspace.ts +0 -0
  26. package/src/._project-js.ts +0 -0
  27. package/src/._project-predicate.ts +0 -0
  28. package/src/._project-py.ts +0 -0
  29. package/src/._project-rs.ts +0 -0
  30. package/src/._project.ts +0 -0
  31. package/src/._projen-version.ts +0 -0
  32. package/src/._publish.ts +0 -0
  33. package/src/._release-catalog.ts +0 -0
  34. package/src/._release-dispatch.ts +0 -0
  35. package/src/._release-github.ts +0 -0
  36. package/src/._release.ts +0 -0
  37. package/src/._scaffold.ts +0 -0
  38. package/src/._tags.ts +0 -0
  39. package/src/._tsconfig.ts +0 -0
  40. package/src/._uniffi.ts +0 -0
  41. package/src/._vscode.ts +0 -0
  42. package/src/._watch.ts +0 -0
  43. package/src/._workspace-lock.ts +0 -0
  44. package/src/._workspace-version.ts +0 -0
  45. package/src/_rust-release-workflow.ts +4 -391
  46. package/src/_task-command.ts +75 -0
  47. package/src/project-js.ts +2 -42
  48. package/src/project-py.ts +5 -96
  49. package/src/project-rs.ts +3 -76
  50. package/src/release-catalog.ts +37 -203
  51. package/src/release.ts +12 -634
  52. package/src/scaffold.ts +2 -6
  53. package/src/workspace-version.ts +9 -121
  54. package/tasks/._barrels.ts +0 -0
  55. package/tasks/._bump.ts +0 -0
  56. package/tasks/._clean.ts +0 -0
  57. package/tasks/._compile-workspace.ts +0 -0
  58. package/tasks/._local-publish.ts +0 -0
  59. package/tasks/._openapi.ts +0 -0
  60. package/tasks/._projenrc.ts +0 -0
  61. package/tasks/._publish-npm.ts +0 -0
  62. package/tasks/._publish-python.ts +0 -0
  63. package/tasks/._publish-uniffi-local.ts +0 -0
  64. package/tasks/._publish.ts +0 -0
  65. package/tasks/._python-registry.ts +0 -0
  66. package/tasks/._release-assets.ts +0 -0
  67. package/tasks/._release-candidate.ts +0 -0
  68. package/tasks/._release-manifest.ts +0 -0
  69. package/tasks/._release-pr.ts +0 -0
  70. package/tasks/._release-summary.ts +0 -0
  71. package/tasks/._release-version.ts +0 -0
  72. package/tasks/._rust.ts +0 -0
  73. package/tasks/._sync.ts +0 -0
  74. package/tasks/._uniffi-python.d.ts +0 -0
  75. package/tasks/._uniffi-python.js +0 -0
  76. package/tasks/._uniffi-release.mjs +0 -0
  77. package/tasks/._uniffi.ts +0 -0
  78. package/tasks/._version-check.ts +0 -0
  79. package/tasks/bump.ts +17 -34
  80. package/tasks/local-publish.ts +124 -117
  81. package/tasks/publish-npm.ts +62 -30
  82. package/tasks/publish-python.ts +38 -140
  83. package/tasks/publish-uniffi-local.ts +129 -84
  84. package/tasks/publish.ts +25 -139
  85. package/tasks/python-registry.ts +2 -9
  86. package/tasks/release-assets.ts +56 -65
  87. package/tasks/release-candidate.ts +123 -90
  88. package/tasks/release-pr.ts +266 -240
  89. package/tasks/release-summary.ts +8 -10
  90. package/tasks/release-version.ts +23 -2
  91. package/tasks/rust.ts +8 -13
  92. package/tasks/uniffi-python.d.ts +2 -3
  93. package/tasks/uniffi-python.js +7 -20
  94. package/tasks/uniffi-release.mjs +19 -13
  95. package/tasks/uniffi.ts +15 -24
  96. package/tasks/version-check.ts +24 -56
  97. package/src/release-plan.ts +0 -269
  98. package/src/release-please.ts +0 -242
  99. package/tasks/release-bootstrap.ts +0 -59
  100. package/tasks/release-plan.ts +0 -89
  101. package/tasks/release-please.ts +0 -162
  102. package/tasks/release-request.ts +0 -166
  103. package/tasks/release-summary-units.ts +0 -75
  104. package/tasks/stamp-python.ts +0 -13
@@ -1,7 +1,17 @@
1
1
  #!/usr/bin/env -S bun
2
- import { chmodSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
- import { arch, platform } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
2
+ import {
3
+ cpSync,
4
+ chmodSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { arch, platform, tmpdir } from "node:os";
14
+ import { dirname, join, relative, resolve } from "node:path";
5
15
  import { fileURLToPath } from "node:url";
6
16
  import { parseArgs } from "node:util";
7
17
  import { json, log, object } from "@dbx-tools/shared-core";
@@ -12,33 +22,21 @@ import type { RustBindingMapping, RustWorkspaceMapping } from "../src/project-rs
12
22
 
13
23
  const logger = log.logger("projen:publish-uniffi-local");
14
24
 
15
- const parsed = parseArgs({
16
- options: {
17
- version: { type: "string" },
18
- registry: { type: "string" },
19
- "pypi-publish-url": { type: "string" },
20
- "cargo-registry": { type: "string" },
21
- },
22
- });
23
-
24
- function run(command: string, args: string[], capture = false): string {
25
- if (capture) return captureTaskCommand(repoRoot, command, args, { check: true });
26
- runTaskCommand(repoRoot, command, args);
27
- return "";
28
- }
29
-
30
25
  function commandAvailable(command: string): boolean {
31
26
  return taskCommandSucceeds(repoRoot, command, ["--version"]);
32
27
  }
33
28
 
34
29
  function rustHost(): string {
35
- const host = run("rustc", ["-vV"], true).match(/^host:\s+(.+)$/m)?.[1];
30
+ const host = captureTaskCommand(repoRoot, "rustc", ["-vV"], { check: true }).match(
31
+ /^host:\s+(.+)$/m,
32
+ )?.[1];
36
33
  if (!host) throw new Error("Unable to detect the local Rust target from rustc -vV");
37
34
  return host;
38
35
  }
39
36
 
40
37
  function pythonTag(): string {
41
- const value = run(
38
+ const value = captureTaskCommand(
39
+ repoRoot,
42
40
  "uv",
43
41
  [
44
42
  "run",
@@ -47,7 +45,7 @@ function pythonTag(): string {
47
45
  "-c",
48
46
  "import sysconfig; print(sysconfig.get_platform().replace('-', '_').replace('.', '_'))",
49
47
  ],
50
- true,
48
+ { check: true },
51
49
  );
52
50
  if (!value) throw new Error("Unable to detect the local Python wheel platform tag");
53
51
  return value;
@@ -99,12 +97,18 @@ interface CargoMetadataPackage {
99
97
  dependencies: Array<{ name: string; source?: string | null }>;
100
98
  manifest_path: string;
101
99
  name: string;
100
+ version: string;
102
101
  }
103
102
 
104
- function orderedCargoManifests(config: RustWorkspaceMapping): string[] {
105
- const configured = new Set(config.crates.map((crate) => resolve(repoRoot, crate, "Cargo.toml")));
103
+ function orderedCargoManifests(root: string, config: RustWorkspaceMapping): CargoMetadataPackage[] {
104
+ const configured = new Set(config.crates.map((crate) => resolve(root, crate, "Cargo.toml")));
106
105
  const metadata = json.parseRecord(
107
- run("cargo", ["metadata", "--format-version", "1", "--no-deps", "--locked"], true),
106
+ captureTaskCommand(
107
+ root,
108
+ "cargo",
109
+ ["metadata", "--format-version", "1", "--no-deps", "--locked"],
110
+ { check: true },
111
+ ),
108
112
  );
109
113
  if (!metadata || !Array.isArray(metadata.packages)) {
110
114
  throw new Error("Cargo metadata did not return workspace packages");
@@ -112,6 +116,7 @@ function orderedCargoManifests(config: RustWorkspaceMapping): string[] {
112
116
  const packages = metadata.packages.filter(object.isRecord).map((pkg) => {
113
117
  if (
114
118
  typeof pkg.name !== "string" ||
119
+ typeof pkg.version !== "string" ||
115
120
  typeof pkg.manifest_path !== "string" ||
116
121
  !Array.isArray(pkg.dependencies)
117
122
  ) {
@@ -119,6 +124,7 @@ function orderedCargoManifests(config: RustWorkspaceMapping): string[] {
119
124
  }
120
125
  return {
121
126
  name: pkg.name,
127
+ version: pkg.version,
122
128
  manifest_path: resolve(pkg.manifest_path),
123
129
  dependencies: pkg.dependencies.filter(object.isRecord).map((dependency) => {
124
130
  if (typeof dependency.name !== "string") {
@@ -161,79 +167,103 @@ function orderedCargoManifests(config: RustWorkspaceMapping): string[] {
161
167
  ordered.push(pkg);
162
168
  };
163
169
  for (const pkg of publishable.values()) visit(pkg);
164
- return ordered.map((pkg) => pkg.manifest_path);
170
+ return ordered;
171
+ }
172
+
173
+ function copyCargoWorkspace(config: RustWorkspaceMapping): string {
174
+ const root = mkdtempSync(join(tmpdir(), "dbx-tools-local-cargo-"));
175
+ for (const path of ["Cargo.toml", "Cargo.lock", "LICENSE", "README.md", "rust-toolchain.toml"]) {
176
+ const source = join(repoRoot, path);
177
+ if (!existsSync(source)) continue;
178
+ cpSync(source, join(root, path));
179
+ }
180
+ const cargoConfig = join(repoRoot, ".cargo");
181
+ if (existsSync(cargoConfig)) cpSync(cargoConfig, join(root, ".cargo"), { recursive: true });
182
+ for (const crate of config.crates) {
183
+ const source = resolve(repoRoot, crate);
184
+ const destination = resolve(root, relative(repoRoot, source));
185
+ mkdirSync(dirname(destination), { recursive: true });
186
+ cpSync(source, destination, { recursive: true });
187
+ }
188
+ return root;
189
+ }
190
+
191
+ export function projectCargoRegistry(
192
+ document: Record<string, unknown>,
193
+ crateNames: ReadonlySet<string>,
194
+ registry: string,
195
+ ): void {
196
+ const sections: unknown[] = [
197
+ document.dependencies,
198
+ document["dev-dependencies"],
199
+ document["build-dependencies"],
200
+ ];
201
+ const workspace = document.workspace;
202
+ if (workspace && typeof workspace === "object" && !Array.isArray(workspace)) {
203
+ sections.push((workspace as Record<string, unknown>).dependencies);
204
+ }
205
+ for (const value of sections) {
206
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
207
+ const section = value as Record<string, unknown>;
208
+ for (const crateName of crateNames) {
209
+ const dependency = section[crateName];
210
+ if (!dependency || typeof dependency !== "object" || Array.isArray(dependency)) continue;
211
+ const record = dependency as Record<string, unknown>;
212
+ if (record.workspace === true) continue;
213
+ record.registry = registry;
214
+ }
215
+ }
165
216
  }
166
217
 
167
218
  function publishCargo(config: RustWorkspaceMapping, registry: string, version: string): void {
168
- const manifests = orderedCargoManifests(config);
169
- const originals = new Map(
170
- manifests.map((manifest) => [manifest, readFileSync(manifest, "utf8")]),
171
- );
219
+ const root = copyCargoWorkspace(config);
172
220
  try {
173
- const crateNames = manifests.map((manifest) => {
174
- const document = parse(readFileSync(manifest, "utf8")) as {
175
- package?: { name?: string };
176
- };
177
- if (!document.package?.name) {
178
- throw new Error(`Cargo manifest has no package name: ${manifest}`);
221
+ const packages = orderedCargoManifests(root, config);
222
+ const crateNames = new Set(packages.map((pkg) => pkg.name));
223
+ for (const pkg of packages) {
224
+ if (pkg.version !== version) {
225
+ throw new Error(`${pkg.name} version ${pkg.version} does not match release ${version}`);
179
226
  }
180
- return document.package.name;
181
- });
227
+ }
228
+ const manifests = [join(root, "Cargo.toml"), ...packages.map((pkg) => pkg.manifest_path)];
182
229
  for (const manifest of manifests) {
183
- const mode = statSync(manifest).mode;
184
230
  const document = parse(readFileSync(manifest, "utf8")) as Record<string, unknown>;
185
- for (const crateName of crateNames) {
186
- if (!crateName) continue;
187
- for (const sectionName of ["dependencies", "dev-dependencies", "build-dependencies"]) {
188
- const section = document[sectionName];
189
- if (!section || typeof section !== "object" || Array.isArray(section)) continue;
190
- const dependency = (section as Record<string, unknown>)[crateName];
191
- if (!dependency || typeof dependency !== "object" || Array.isArray(dependency)) {
192
- continue;
193
- }
194
- (dependency as Record<string, unknown>).registry = registry;
195
- }
196
- }
197
- chmodSync(manifest, mode | 0o200);
231
+ projectCargoRegistry(document, crateNames, registry);
232
+ chmodSync(manifest, 0o644);
198
233
  writeFileSync(manifest, `${stringify(document).trimEnd()}\n`);
199
- chmodSync(manifest, mode);
200
234
  }
201
- for (const crateName of crateNames) {
202
- if (!crateName) continue;
203
- if (cargoVersionExists(crateName, version, registry)) {
204
- logger.info(`skip published ${crateName} @ ${version}`);
235
+ for (const pkg of packages) {
236
+ if (cargoVersionExists(pkg.name, version, registry)) {
237
+ logger.info(`skip published ${pkg.name} @ ${version}`);
205
238
  continue;
206
239
  }
207
- run("cargo", [
240
+ runTaskCommand(root, "cargo", [
208
241
  "publish",
209
- "--package",
210
- crateName,
242
+ "--manifest-path",
243
+ pkg.manifest_path,
211
244
  "--registry",
212
245
  registry,
213
- "--allow-dirty",
214
- "--no-verify",
246
+ "--locked",
215
247
  ]);
216
248
  }
217
249
  } finally {
218
- for (const [manifest, source] of originals) {
219
- const mode = statSync(manifest).mode;
220
- chmodSync(manifest, mode | 0o200);
221
- writeFileSync(manifest, source);
222
- chmodSync(manifest, mode);
223
- }
250
+ rmSync(root, { recursive: true, force: true });
224
251
  }
225
252
  }
226
253
 
227
- function buildAndPublish(binding: RustBindingMapping, version: string): void {
228
- const registry = parsed.values.registry;
229
- const pypiPublishUrl = parsed.values["pypi-publish-url"];
254
+ function buildAndPublish(
255
+ binding: RustBindingMapping,
256
+ version: string,
257
+ registry: string | undefined,
258
+ pypiPublishUrl: string | undefined,
259
+ ): void {
230
260
  const includeNode = Boolean(registry && binding.node && binding.nodePackage);
231
261
  const includePython = Boolean(pypiPublishUrl && binding.python && binding.pythonPackage);
232
262
  if (!includeNode && !includePython) return;
233
263
 
234
264
  const target = nativeTarget();
235
265
  const output = resolve(repoRoot, "dist/uniffi");
236
- run("node", [
266
+ runTaskCommand(repoRoot, "node", [
237
267
  resolve(dirname(fileURLToPath(import.meta.url)), "uniffi-release.mjs"),
238
268
  "build",
239
269
  "--crate",
@@ -272,7 +302,7 @@ function buildAndPublish(binding: RustBindingMapping, version: string): void {
272
302
  const publishNpmScript = resolve(dirname(fileURLToPath(import.meta.url)), "publish-npm.ts");
273
303
  for (const directory of [join(output, "npm"), join(output, "npm-facade")]) {
274
304
  if (artifacts(directory, ".tgz").length === 0) continue;
275
- run(process.execPath, [
305
+ runTaskCommand(repoRoot, process.execPath, [
276
306
  publishNpmScript,
277
307
  "--directory",
278
308
  directory,
@@ -286,7 +316,7 @@ function buildAndPublish(binding: RustBindingMapping, version: string): void {
286
316
  if (includePython) {
287
317
  const wheels = artifacts(join(output, "python"), ".whl");
288
318
  if (wheels.length === 0) throw new Error(`No wheel produced for ${binding.crate}`);
289
- run("uvx", [
319
+ runTaskCommand(repoRoot, "uvx", [
290
320
  "--from",
291
321
  "devpi-client",
292
322
  "devpi",
@@ -299,16 +329,31 @@ function buildAndPublish(binding: RustBindingMapping, version: string): void {
299
329
  }
300
330
  }
301
331
 
302
- const version = parsed.values.version;
303
- if (!version) throw new Error("Missing --version");
304
- const config = rustConfig();
305
- if (config?.bindings.length) {
306
- if (!commandAvailable("cargo") || !commandAvailable("rustc")) {
307
- throw new Error("Cargo and rustc are required because UniFFI Rust projects were detected");
332
+ function main(args: string[] = process.argv.slice(2)): void {
333
+ const parsed = parseArgs({
334
+ args,
335
+ options: {
336
+ version: { type: "string" },
337
+ registry: { type: "string" },
338
+ "pypi-publish-url": { type: "string" },
339
+ "cargo-registry": { type: "string" },
340
+ },
341
+ });
342
+ const version = parsed.values.version;
343
+ if (!version) throw new Error("Missing --version");
344
+ const config = rustConfig();
345
+ if (config?.bindings.length) {
346
+ if (!commandAvailable("cargo") || !commandAvailable("rustc")) {
347
+ throw new Error("Cargo and rustc are required because UniFFI Rust projects were detected");
348
+ }
349
+ for (const binding of config.bindings) {
350
+ buildAndPublish(binding, version, parsed.values.registry, parsed.values["pypi-publish-url"]);
351
+ }
352
+ }
353
+ const cargoRegistry = parsed.values["cargo-registry"];
354
+ if (cargoRegistry && config?.crates.length) {
355
+ publishCargo(config, cargoRegistry, version);
308
356
  }
309
- for (const binding of config.bindings) buildAndPublish(binding, version);
310
- }
311
- const cargoRegistry = parsed.values["cargo-registry"];
312
- if (cargoRegistry && config?.crates.length) {
313
- publishCargo(config, cargoRegistry, version);
314
357
  }
358
+
359
+ if (import.meta.main) main();
package/tasks/publish.ts CHANGED
@@ -18,14 +18,14 @@
18
18
  * version. When Bun's workspace lock already carries the version, its
19
19
  * refresh is skipped;
20
20
  * - **`publishConfig` substitution** (compiled `lib/` entry points) is done
21
- * HERE, by {@link applyPublishConfig}, NOT by bun: unlike pnpm/npm, `bun
21
+ * inside the temporary archive, NOT in the checkout: unlike pnpm/npm, `bun
22
22
  * publish`/`bun pm pack` do NOT fold `publishConfig`'s `main`/`types`/`bin`/
23
23
  * `exports` into the packed manifest (verified: the packed manifest keeps the
24
24
  * raw `.ts` source paths and an inert `publishConfig`). Left unsubstituted, a
25
25
  * published CLI's `bin` points at `./bin/x.ts`, and because the bin runs via
26
26
  * its `#!/usr/bin/env node` shebang, node chokes on the `.ts`
27
27
  * (ERR_UNKNOWN_FILE_EXTENSION). We merge `publishConfig` onto the top-level
28
- * manifest before packing so the tarball advertises the compiled `lib/` tree;
28
+ * manifest after packing so the tarball advertises the compiled `lib/` tree;
29
29
  * - **compiled output** is emitted once, before packing, by one root-level
30
30
  * filtered `bun run` that fans out to every publishable member in parallel.
31
31
  * Every package is then packed once with lifecycle scripts disabled. The
@@ -42,28 +42,10 @@
42
42
  * non-default registry (a local verdaccio); `--exclude <dir>` (repeatable,
43
43
  * repo-relative) skips a member owned by another publication flow.
44
44
  *
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.
45
+ * The checkout remains byte-for-byte unchanged. A stale manifest or lockfile is
46
+ * a release error rather than something publication repairs.
55
47
  */
56
- import {
57
- chmodSync,
58
- copyFileSync,
59
- existsSync,
60
- mkdirSync,
61
- mkdtempSync,
62
- readFileSync,
63
- rmSync,
64
- statSync,
65
- writeFileSync,
66
- } from "node:fs";
48
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
67
49
  import { tmpdir } from "node:os";
68
50
  import { delimiter, dirname, join, resolve } from "node:path";
69
51
  import { asyncUtils, log } from "@dbx-tools/shared-core";
@@ -77,25 +59,9 @@ import {
77
59
  } from "./publish-npm.ts";
78
60
  import { runTaskCommand, runTaskCommandAsync } from "../src/_task-command.ts";
79
61
  import { toPosix } from "../src/packages.ts";
80
- import type { ReleasePlan } from "../src/release-plan.ts";
81
62
 
82
63
  const logger = log.logger("projen:publish");
83
64
 
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
65
  /**
100
66
  * Workspace member dirs (absolute), read from the root `pnpm-workspace.yaml` - the
101
67
  * file the engine keeps for the Databricks Apps pnpm deploy, which also lists every
@@ -109,18 +75,13 @@ function workspaceMembers(root: string): string[] {
109
75
  return (doc?.packages ?? []).map((m) => resolve(root, m));
110
76
  }
111
77
 
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 {
78
+ /** Whether selected workspace members carry the reviewed workspace version. */
79
+ function manifestsMatchVersion(members: readonly string[], version: string): boolean {
118
80
  return members.every((dir) => {
119
81
  const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
120
82
  version?: string;
121
83
  };
122
- const path = toPosix(dir.slice(resolve(root).length + 1));
123
- return pkg.version === expected.get(path);
84
+ return pkg.version === version;
124
85
  });
125
86
  }
126
87
 
@@ -146,41 +107,6 @@ function lockfileMatchesManifestVersions(root: string, members: readonly string[
146
107
  }
147
108
  }
148
109
 
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
110
  /** Entry-point fields projen writes as `.ts` source in-repo and rewrites to `lib/` for publish. */
185
111
  const PUBLISH_CONFIG_ENTRY_FIELDS = ["main", "types", "bin", "exports"] as const;
186
112
 
@@ -195,23 +121,18 @@ function compiledPublishTargets(value: unknown): string[] {
195
121
  /**
196
122
  * Fold a package's `publishConfig` entry-point fields onto the top-level manifest,
197
123
  * 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.
124
+ * doc). The input is the temporary archive manifest, never the checkout.
202
125
  */
203
- function applyPublishConfig(pkgPath: string): void {
204
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
126
+ export function applyPublishConfig(
127
+ source: Readonly<Record<string, unknown>>,
128
+ ): Readonly<Record<string, unknown>> {
129
+ const pkg = { ...source };
205
130
  const publishConfig = pkg.publishConfig as Record<string, unknown> | undefined;
206
- if (!publishConfig) return;
207
- let changed = false;
131
+ if (!publishConfig) return pkg;
208
132
  for (const field of PUBLISH_CONFIG_ENTRY_FIELDS) {
209
- if (field in publishConfig) {
210
- pkg[field] = publishConfig[field];
211
- changed = true;
212
- }
133
+ if (field in publishConfig) pkg[field] = publishConfig[field];
213
134
  }
214
- if (changed) writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
135
+ return pkg;
215
136
  }
216
137
 
217
138
  /**
@@ -229,14 +150,9 @@ const argv = process.argv.slice(2);
229
150
  const version = argv[0] && !argv[0].startsWith("--") ? argv[0] : undefined;
230
151
  const rest = version ? argv.slice(1) : argv;
231
152
  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) {
153
+ if (!version) {
238
154
  logger.error(
239
- "usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--output <dir>] [--exclude <dir>] [--dry-run] [--skip-compile]",
155
+ "usage: bun tasks/publish.ts <version> [--registry <url>] [--output <dir>] [--exclude <dir>] [--dry-run] [--skip-compile]",
240
156
  );
241
157
  process.exit(1);
242
158
  }
@@ -252,46 +168,25 @@ if (!Number.isInteger(parsedConcurrency) || parsedConcurrency < 1) {
252
168
  throw new Error(`--concurrency must be a positive integer, got ${String(parsedConcurrency)}`);
253
169
  }
254
170
  const concurrency = parsedConcurrency;
255
- // Undo transient publishConfig edits at exit.
256
- const restore = !rest.includes("--no-restore");
257
171
  const excluded = new Set(
258
172
  rest.reduce<string[]>((acc, arg, i) => (arg === "--exclude" ? [...acc, rest[i + 1]] : acc), []),
259
173
  );
260
174
 
261
175
  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
176
  const allMembers = workspaceMembers(root)
267
177
  .filter((dir) => existsSync(join(dir, "package.json")))
268
178
  .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;
179
+ const members = allMembers;
277
180
 
278
- if (!manifestsMatchVersions(root, members, expectedVersions)) {
279
- throw new Error("workspace manifests do not match the reviewed release plan; run projen");
181
+ if (!manifestsMatchVersion(members, version)) {
182
+ throw new Error(`workspace manifests do not match release ${version}; run projen`);
280
183
  }
281
184
  logger.info(`validated ${members.length} selected member manifests`);
282
185
 
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 } });
186
+ if (!lockfileMatchesManifestVersions(root, allMembers)) {
187
+ throw new Error("bun.lock does not match workspace versions; run bun install and commit it");
294
188
  }
189
+ logger.info("validated workspace lock versions");
295
190
 
296
191
  const publishArgs = [
297
192
  "--ignore-scripts",
@@ -365,15 +260,6 @@ if (compiled.length > 0) {
365
260
  }
366
261
  }
367
262
 
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
263
  logger.info(
378
264
  `${output ? "packing" : dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
379
265
  );
@@ -386,7 +272,7 @@ await asyncUtils.mapConcurrent(
386
272
  async ({ dir, name, version: packageVersion, access }) => {
387
273
  const packed = mkdtempSync(join(tmpdir(), "projen-npm-release-"));
388
274
  try {
389
- const archive = packNpmPackage(dir, packed, path);
275
+ const archive = packNpmPackage(dir, packed, path, applyPublishConfig);
390
276
  const local = readNpmArchiveIdentity(archive);
391
277
  if (local.name !== name || local.version !== packageVersion) {
392
278
  throw new Error(
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { resolve } from "node:path";
4
- import * as exec from "@dbx-tools/core/exec";
5
4
  import { net } from "@dbx-tools/shared-core";
5
+ import { captureTaskCommand } from "../src/_task-command.ts";
6
6
 
7
7
  export interface LocalPythonRegistry {
8
8
  readonly indexUrl: string;
@@ -126,14 +126,7 @@ function splitIndexList(value: string | undefined): string[] {
126
126
 
127
127
  /** Read a `pip config get <key>`, treating pip's literal "undefined" as unset. */
128
128
  function pipConfig(key: string): string | undefined {
129
- const res = exec.spawnSync("python", ["-m", "pip", "config", "get", key], {
130
- cwd: process.cwd(),
131
- stdout: "capture",
132
- stderr: "ignore",
133
- stdin: "ignore",
134
- check: false,
135
- });
136
- const out = res.stdout?.trim();
129
+ const out = captureTaskCommand(process.cwd(), "python", ["-m", "pip", "config", "get", key]);
137
130
  return out && out !== "undefined" ? out : undefined;
138
131
  }
139
132