@dbx-tools/projen 0.9.6 → 0.9.8

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.
package/README.md CHANGED
@@ -254,7 +254,11 @@ and select `rust-lld` for the workspace build. Rust release rows restore and
254
254
  update dependency-only Cargo caches in the release branch's cache scope. The
255
255
  cache key excludes workspace version changes, and manual tag recovery restores
256
256
  without saving a tag-scoped copy. There is no separate cache workflow or
257
- sccache layer. Phase timings are written to each build log. Python generation executes the already-built
257
+ sccache layer. After publishing the current raw Rust bundles and build manifests,
258
+ the GitHub release job deletes those internal assets from published
259
+ `vMAJOR.MINOR.PATCH` releases strictly lower than the current version while
260
+ leaving versioned consumer binaries in place. Phase timings are written to each
261
+ build log. Python generation executes the already-built
258
262
  `target/<triple>/release/<crate>-uniffi-bindgen` directly. Artifact packaging therefore
259
263
  does no Rust compilation after the main workspace build.
260
264
 
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.9.6";
6
+ export const PACKAGE_VERSION = "0.9.8";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
@@ -36,6 +36,7 @@ export * as tsconfig from "./src/tsconfig.ts";
36
36
  export * as uniffi from "./src/uniffi.ts";
37
37
  export * as vscode from "./src/vscode.ts";
38
38
  export * as watch from "./src/watch.ts";
39
+ export * as workspaceLock from "./src/workspace-lock.ts";
39
40
  export * as workspaceVersion from "./src/workspace-version.ts";
40
41
  export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, RootBunfigFile, BunfigFile, BunDevServerFile, BunBuildFile } from "./src/bun-app.ts";
41
42
  export { BUN_VERSION } from "./src/bun-workflow.ts";
package/package.json CHANGED
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.7.0",
33
- "@dbx-tools/core": "^0.9.6",
34
- "@dbx-tools/path": "^0.9.6",
35
- "@dbx-tools/shared-core": "^0.9.6",
33
+ "@dbx-tools/core": "^0.9.8",
34
+ "@dbx-tools/path": "^0.9.8",
35
+ "@dbx-tools/shared-core": "^0.9.8",
36
36
  "commander": "^15.0.0",
37
37
  "concurrently": "^10.0.3",
38
38
  "constructs": "^10.6.0",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "main": "index.ts",
55
55
  "license": "Apache-2.0",
56
- "version": "0.9.6",
56
+ "version": "0.9.8",
57
57
  "types": "index.ts",
58
58
  "type": "module",
59
59
  "exports": {
@@ -26,6 +26,19 @@ export interface RustCliOptions {
26
26
  readonly command?: string;
27
27
  /** Root CLI help text. Defaults to the Rust package description. */
28
28
  readonly description?: string;
29
+ /** Keep the binary installable by registry lookup without exposing a root command. */
30
+ readonly hidden?: boolean;
31
+ }
32
+
33
+ /** One explicit Cargo binary target and its optional release registration. */
34
+ export interface RustBinaryOptions {
35
+ readonly name: string;
36
+ readonly path: string;
37
+ readonly requiredFeatures?: readonly string[];
38
+ readonly release?: boolean;
39
+ readonly releaseExcludeOs?: readonly RustReleaseOs[];
40
+ readonly description?: string;
41
+ readonly cli?: boolean | RustCliOptions;
29
42
  }
30
43
 
31
44
  /** Explicit Cargo example target and the feature gate required to compile it. */
@@ -47,6 +60,8 @@ export interface RustCrateOptions {
47
60
  /** Omit this crate and any release binary artifact from these operating systems. */
48
61
  readonly releaseExcludeOs?: readonly RustReleaseOs[];
49
62
  readonly dependencies?: Readonly<Record<string, CargoDependency>>;
63
+ /** Cargo dependencies scoped by a target cfg expression. */
64
+ readonly targetDependencies?: Readonly<Record<string, Readonly<Record<string, CargoDependency>>>>;
50
65
  readonly devDependencies?: Readonly<Record<string, CargoDependency>>;
51
66
  readonly features?: Readonly<Record<string, readonly string[]>>;
52
67
  readonly defaultFeatures?: readonly string[];
@@ -54,6 +69,10 @@ export interface RustCrateOptions {
54
69
  readonly examples?: readonly CargoExampleOptions[];
55
70
  /** Cargo and release executable name. Defaults to the generated package name. */
56
71
  readonly binaryName?: string;
72
+ /** Additional Cargo binary targets owned by this crate. */
73
+ readonly binaries?: readonly RustBinaryOptions[];
74
+ /** Cargo binary selected by `cargo run` when this crate has multiple binaries. */
75
+ readonly defaultRun?: string;
57
76
  /** Publish this release binary through the generated `dbx` command registry. */
58
77
  readonly cli?: boolean | RustCliOptions;
59
78
  readonly bindings?: readonly ("node" | "python")[];
@@ -187,10 +206,12 @@ function packageOptions(
187
206
  ...(options.release !== undefined ? { release: options.release } : {}),
188
207
  ...(options.releaseExcludeOs ? { releaseExcludeOs: options.releaseExcludeOs } : {}),
189
208
  ...(options.dependencies ? { dependencies: options.dependencies } : {}),
209
+ ...(options.targetDependencies ? { targetDependencies: options.targetDependencies } : {}),
190
210
  ...(options.devDependencies ? { devDependencies: options.devDependencies } : {}),
191
211
  ...(options.features ? { features: options.features } : {}),
192
212
  ...(options.defaultFeatures ? { defaultFeatures: options.defaultFeatures } : {}),
193
213
  ...(options.binaryName ? { binaryName: options.binaryName } : {}),
214
+ ...(options.binaries ? { binaries: options.binaries } : {}),
194
215
  ...(options.cli !== undefined ? { cli: options.cli } : {}),
195
216
  ...(options.bindings ? { bindings: options.bindings } : {}),
196
217
  ...(options.uniffiConfig ? { uniffiConfig: options.uniffiConfig } : {}),
@@ -244,6 +265,7 @@ export class RustProject extends Project implements DBXToolsProject {
244
265
  ...(!workspace ? { workspace: {} } : {}),
245
266
  package: {
246
267
  name: this.crateName,
268
+ ...(options.defaultRun ? { "default-run": options.defaultRun } : {}),
247
269
  version: inheritedCargoValue(workspace, options.version, "0.1.0"),
248
270
  edition: inheritedCargoValue(workspace, options.edition, "2021"),
249
271
  "rust-version": inheritedCargoValue(workspace, options.rustVersion, "1.82"),
@@ -272,6 +294,13 @@ export class RustProject extends Project implements DBXToolsProject {
272
294
  ? [{ name: `${this.crateName}-uniffi-bindgen`, path: "uniffi-bindgen.rs" }]
273
295
  : []),
274
296
  ...(binary ? [{ name: binaryName, path: "src/main.rs" }] : []),
297
+ ...(options.binaries ?? []).map((target) => ({
298
+ name: target.name,
299
+ path: target.path,
300
+ ...(target.requiredFeatures?.length
301
+ ? { "required-features": [...target.requiredFeatures] }
302
+ : {}),
303
+ })),
275
304
  ],
276
305
  }
277
306
  : {}),
@@ -304,6 +333,23 @@ export class RustProject extends Project implements DBXToolsProject {
304
333
  ),
305
334
  }
306
335
  : {}),
336
+ ...(options.targetDependencies
337
+ ? {
338
+ target: Object.fromEntries(
339
+ Object.entries(options.targetDependencies).map(([target, dependencies]) => [
340
+ target,
341
+ {
342
+ dependencies: Object.fromEntries(
343
+ Object.entries(dependencies).map(([name, value]) => [
344
+ name,
345
+ cargoDependency(value, dependencyVersion),
346
+ ]),
347
+ ),
348
+ },
349
+ ]),
350
+ ),
351
+ }
352
+ : {}),
307
353
  ...(options.devDependencies
308
354
  ? {
309
355
  "dev-dependencies": Object.fromEntries(
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { stringUtils } from "@dbx-tools/shared-core";
6
7
  import { TextFile, javascript } from "projen";
7
8
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
8
9
  import type { RustProject } from "./_rust-project.ts";
@@ -127,6 +128,7 @@ interface RustReleaseBinaryPlan {
127
128
  readonly crate: string;
128
129
  readonly binary: string;
129
130
  readonly excludedOs: readonly RustReleaseOs[];
131
+ readonly requiredFeatures: readonly string[];
130
132
  }
131
133
 
132
134
  type RustReleaseTargetPlan = Readonly<Record<string, string | boolean | number>>;
@@ -164,13 +166,26 @@ export function planRustRelease(
164
166
  nodePackage: binding.nodePackage ?? "",
165
167
  pythonPackage: binding.pythonPackage ?? "",
166
168
  }));
167
- const releaseBinaries = packages
168
- .filter((pkg) => pkg.packageOptions.release)
169
- .map((pkg) => ({
170
- crate: pkg.crateName,
171
- binary: pkg.packageOptions.binaryName ?? pkg.crateName,
172
- excludedOs: pkg.packageOptions.releaseExcludeOs ?? [],
173
- }));
169
+ const releaseBinaries = packages.flatMap((pkg) => [
170
+ ...(pkg.packageOptions.release
171
+ ? [
172
+ {
173
+ crate: pkg.crateName,
174
+ binary: pkg.packageOptions.binaryName ?? pkg.crateName,
175
+ excludedOs: pkg.packageOptions.releaseExcludeOs ?? [],
176
+ requiredFeatures: [],
177
+ },
178
+ ]
179
+ : []),
180
+ ...(pkg.packageOptions.binaries ?? [])
181
+ .filter((binary) => binary.release)
182
+ .map((binary) => ({
183
+ crate: pkg.crateName,
184
+ binary: binary.name,
185
+ excludedOs: binary.releaseExcludeOs ?? [],
186
+ requiredFeatures: binary.requiredFeatures ?? [],
187
+ })),
188
+ ]);
174
189
  const publicCrates = orderRustBindings(
175
190
  packages
176
191
  .filter((pkg) => !pkg.packageOptions.private)
@@ -337,7 +352,31 @@ function rustBinaryCommands(plan: RustReleasePlan, independent: boolean): string
337
352
  });
338
353
  }
339
354
 
355
+ function rustAdditionalBinaryBuildCommands(plan: RustReleasePlan, independent: boolean): string[] {
356
+ return plan.releaseBinaries
357
+ .filter((binary) => binary.requiredFeatures.length)
358
+ .flatMap((binary) => {
359
+ const command = `cargo build --release --package "${binary.crate}" --bin "${binary.binary}" --features "${binary.requiredFeatures.join(",")}"${
360
+ plan.usesCargoLock ? " --locked" : ""
361
+ } --target "\${{ matrix.cargo }}"`;
362
+ const excludedCondition = binary.excludedOs
363
+ .map((os) => `[ "\${{ matrix.os }}" != "${os}" ]`)
364
+ .join(" && ");
365
+ const platformCommands = excludedCondition
366
+ ? [`if ${excludedCondition}; then`, ` ${command}`, "fi"]
367
+ : [command];
368
+ if (!independent) return platformCommands;
369
+ const unit = defaultReleaseUnitId("rust", binary.crate);
370
+ return [
371
+ `if jq -e --arg unit "${unit}" '.units[] | select(.id == $unit)' dist/release-plan.json >/dev/null; then`,
372
+ ...platformCommands.map((line) => ` ${line}`),
373
+ "fi",
374
+ ];
375
+ });
376
+ }
377
+
340
378
  function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
379
+ const binaryCrates = [...new Set(plan.releaseBinaries.map((binary) => binary.crate))];
341
380
  return [
342
381
  ...plan.bindings.flatMap((binding) => [
343
382
  ...(binding.node
@@ -367,24 +406,35 @@ function rustArtifactSteps(plan: RustReleasePlan): JobStep[] {
367
406
  ]
368
407
  : []),
369
408
  ]),
370
- ...plan.releaseBinaries.map((pkg) => ({
371
- name: `Upload ${pkg.crate} release binary`,
372
- uses: "actions/upload-artifact@v7",
373
- ...(rustReleaseBinaryCondition(pkg.excludedOs)
374
- ? { if: rustReleaseBinaryCondition(pkg.excludedOs) }
375
- : {}),
376
- with: {
377
- name: `${pkg.crate}-\${{ matrix.node }}-binary`,
378
- path: `dist/release/${pkg.crate}/\${{ matrix.node }}/binary/*`,
379
- "retention-days": 7,
380
- },
381
- })),
409
+ ...binaryCrates.map((crate) => {
410
+ const binaries = plan.releaseBinaries.filter((binary) => binary.crate === crate);
411
+ const excludedOs =
412
+ binaries[0]?.excludedOs.filter((os) =>
413
+ binaries.every((binary) => binary.excludedOs.includes(os)),
414
+ ) ?? [];
415
+ return {
416
+ name: `Upload ${crate} release binary`,
417
+ uses: "actions/upload-artifact@v7",
418
+ ...(rustReleaseBinaryCondition(excludedOs)
419
+ ? { if: rustReleaseBinaryCondition(excludedOs) }
420
+ : {}),
421
+ with: {
422
+ name: `${crate}-\${{ matrix.node }}-binary`,
423
+ path: `dist/release/${crate}/\${{ matrix.node }}/binary/*`,
424
+ "retention-days": 7,
425
+ },
426
+ };
427
+ }),
382
428
  ];
383
429
  }
384
430
 
385
431
  export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly JobStep[]): Job {
386
432
  const bindingCommands = rustBindingCommands(plan, Boolean(independentSetup));
387
433
  const binaryCommands = rustBinaryCommands(plan, Boolean(independentSetup));
434
+ const additionalBinaryBuildCommands = rustAdditionalBinaryBuildCommands(
435
+ plan,
436
+ Boolean(independentSetup),
437
+ );
388
438
  return {
389
439
  if: independentSetup
390
440
  ? "${{ needs.release-plan.outputs.rust_targets != '[]' && (github.event_name == 'push' || inputs.stage != 'docs') }}"
@@ -482,29 +532,34 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
482
532
  id: "raw-native",
483
533
  shell: "bash",
484
534
  env: { GH_TOKEN: "${{ github.token }}" },
485
- run: [
486
- 'KEY="${{ steps.rust-fingerprint.outputs.key }}"',
487
- 'test -n "$KEY"',
488
- 'ASSET="rust-raw-${{ matrix.node }}-$KEY.tar.gz"',
489
- "mkdir -p dist/rust-raw",
490
- 'MATCH="$(gh api --paginate "repos/${{ github.repository }}/releases?per_page=100" --jq \'.[] | select(.draft == false) | . as $release | .assets[] | select(.name == "\'"$ASSET"\'") | [$release.tag_name, .url] | @tsv\' | head -n 1)"',
491
- 'if [ -n "$MATCH" ]; then',
492
- " TAG=\"${MATCH%%$'\\t'*}\"",
493
- " URL=\"${MATCH#*$'\\t'}\"",
494
- ' CHECKSUM_URL="$(gh api "repos/${{ github.repository }}/releases/tags/$TAG" --jq \'.assets[] | select(.name == "\'"$ASSET.sha256"\'") | .url\')"',
495
- ' rm -rf "target/${{ matrix.cargo }}/release"',
496
- ' if test -n "$CHECKSUM_URL" && gh api "$URL" -H "Accept: application/octet-stream" > "dist/rust-raw/$ASSET" && gh api "$CHECKSUM_URL" -H "Accept: application/octet-stream" > "dist/rust-raw/$ASSET.sha256" && (cd dist/rust-raw && if command -v sha256sum >/dev/null 2>&1; then sha256sum --check "$ASSET.sha256"; else shasum -a 256 --check "$ASSET.sha256"; fi) && tar -xzf "dist/rust-raw/$ASSET"; then',
497
- ' echo "hit=true" >> "$GITHUB_OUTPUT"',
498
- " else",
499
- ' rm -rf "target/${{ matrix.cargo }}/release" "dist/rust-raw/$ASSET" "dist/rust-raw/$ASSET.sha256"',
500
- ' echo "::warning::matching raw Rust bundle failed validation; rebuilding"',
501
- ' echo "hit=false" >> "$GITHUB_OUTPUT"',
502
- " fi",
503
- "else",
504
- ' echo "hit=false" >> "$GITHUB_OUTPUT"',
505
- "fi",
506
- 'echo "asset=$ASSET" >> "$GITHUB_OUTPUT"',
507
- ].join("\n"),
535
+ // prettier-ignore
536
+ run: stringUtils.dedent(
537
+ // ============================================================================
538
+ /*bash*/`
539
+ KEY="\${{ steps.rust-fingerprint.outputs.key }}"
540
+ test -n "$KEY"
541
+ ASSET="rust-raw-\${{ matrix.node }}-$KEY.tar.gz"
542
+ mkdir -p dist/rust-raw
543
+ MATCH="$(gh api --paginate "repos/\${{ github.repository }}/releases?per_page=100" --jq '.[] | select(.draft == false) | . as $release | .assets[] | select(.name == "'"$ASSET"'") | [$release.tag_name, .url] | @tsv' | head -n 1)"
544
+ if [ -n "$MATCH" ]; then
545
+ TAG="\${MATCH%%$'\\t'*}"
546
+ URL="\${MATCH#*$'\\t'}"
547
+ CHECKSUM_URL="$(gh api "repos/\${{ github.repository }}/releases/tags/$TAG" --jq '.assets[] | select(.name == "'"$ASSET.sha256"'") | .url')"
548
+ rm -rf "target/\${{ matrix.cargo }}/release"
549
+ if test -n "$CHECKSUM_URL" && gh api "$URL" -H "Accept: application/octet-stream" > "dist/rust-raw/$ASSET" && gh api "$CHECKSUM_URL" -H "Accept: application/octet-stream" > "dist/rust-raw/$ASSET.sha256" && (cd dist/rust-raw && if command -v sha256sum >/dev/null 2>&1; then sha256sum --check "$ASSET.sha256"; else shasum -a 256 --check "$ASSET.sha256"; fi) && tar -xzf "dist/rust-raw/$ASSET"; then
550
+ echo "hit=true" >> "$GITHUB_OUTPUT"
551
+ else
552
+ rm -rf "target/\${{ matrix.cargo }}/release" "dist/rust-raw/$ASSET" "dist/rust-raw/$ASSET.sha256"
553
+ echo "::warning::matching raw Rust bundle failed validation; rebuilding"
554
+ echo "hit=false" >> "$GITHUB_OUTPUT"
555
+ fi
556
+ else
557
+ echo "hit=false" >> "$GITHUB_OUTPUT"
558
+ fi
559
+ echo "asset=$ASSET" >> "$GITHUB_OUTPUT"
560
+ `
561
+ // ============================================================================
562
+ ),
508
563
  },
509
564
  ]
510
565
  : []),
@@ -518,16 +573,19 @@ export function rustBuildJob(plan: RustReleasePlan, independentSetup?: readonly
518
573
  },
519
574
  run: timedBash(
520
575
  "rust_workspace",
521
- independentSetup
522
- ? [
523
- "mapfile -t PACKAGES < <(jq -r '.[]' <<<'${{ toJSON(matrix.packages) }}')",
524
- "PACKAGE_ARGS=()",
525
- 'for PACKAGE in "${PACKAGES[@]}"; do PACKAGE_ARGS+=(--package "$PACKAGE"); done',
526
- `cargo build --release --timings "\${PACKAGE_ARGS[@]}"${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"`,
527
- ].join("\n")
528
- : `cargo build --release --timings --workspace${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"${
529
- plan.hasReleaseExclusions ? " ${{ matrix.cargoExcludes }}" : ""
530
- }`,
576
+ [
577
+ independentSetup
578
+ ? [
579
+ "mapfile -t PACKAGES < <(jq -r '.[]' <<<'${{ toJSON(matrix.packages) }}')",
580
+ "PACKAGE_ARGS=()",
581
+ 'for PACKAGE in "${PACKAGES[@]}"; do PACKAGE_ARGS+=(--package "$PACKAGE"); done',
582
+ `cargo build --release --timings "\${PACKAGE_ARGS[@]}"${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"`,
583
+ ].join("\n")
584
+ : `cargo build --release --timings --workspace${plan.usesCargoLock ? " --locked" : ""} --target "\${{ matrix.cargo }}"${
585
+ plan.hasReleaseExclusions ? " ${{ matrix.cargoExcludes }}" : ""
586
+ }`,
587
+ ...additionalBinaryBuildCommands,
588
+ ].join("\n"),
531
589
  ),
532
590
  },
533
591
  ...(!independentSetup
@@ -757,6 +815,53 @@ export function rustGitHubReleaseJob(): Job {
757
815
  target_commitish: RELEASE_SHA,
758
816
  },
759
817
  },
818
+ {
819
+ name: "Delete superseded raw Rust release assets",
820
+ uses: "actions/github-script@v8",
821
+ env: { CURRENT_RELEASE_TAG: RELEASE_TAG },
822
+ with: {
823
+ // prettier-ignore
824
+ script: stringUtils.dedent(
825
+ // ============================================================================
826
+ /*js*/`
827
+ const currentTag = process.env.CURRENT_RELEASE_TAG;
828
+ if (!currentTag) throw new Error("CURRENT_RELEASE_TAG is required");
829
+ const parseVersion = (tag) => {
830
+ const match = /^v(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(tag);
831
+ return match ? match.slice(1).map(Number) : undefined;
832
+ };
833
+ const compareVersions = (left, right) => {
834
+ for (let index = 0; index < 3; index += 1) {
835
+ if (left[index] !== right[index]) return left[index] - right[index];
836
+ }
837
+ return 0;
838
+ };
839
+ const currentVersion = parseVersion(currentTag);
840
+ if (!currentVersion) throw new Error(\`Unsupported release tag: \${currentTag}\`);
841
+ const releases = await github.paginate(github.rest.repos.listReleases, {
842
+ ...context.repo,
843
+ per_page: 100,
844
+ });
845
+ const assets = releases.flatMap((release) => {
846
+ const version = parseVersion(release.tag_name);
847
+ if (release.draft || !version || compareVersions(version, currentVersion) >= 0) {
848
+ return [];
849
+ }
850
+ return release.assets.filter((asset) => /^rust-(?:raw|build)-/.test(asset.name));
851
+ });
852
+ for (const asset of assets) {
853
+ core.info(\`Deleting \${asset.name} (\${asset.id})\`);
854
+ await github.rest.repos.deleteReleaseAsset({
855
+ ...context.repo,
856
+ asset_id: asset.id,
857
+ });
858
+ }
859
+ core.info(\`Deleted \${assets.length} superseded raw Rust release assets\`);
860
+ `
861
+ // ============================================================================
862
+ ),
863
+ },
864
+ },
760
865
  ],
761
866
  };
762
867
  }
package/src/barrels.ts CHANGED
@@ -523,10 +523,9 @@ const WRITE_ATTEMPTS = 3;
523
523
  * and the projenrc watcher's post-synth `generateBarrels()` sweep), so any gap
524
524
  * between `makeWritable` and the write is a window in which another process's
525
525
  * `makeReadonly` lands and this write fails with
526
- * `EACCES: permission denied, open '<pkg>/index.ts'`. The gap used to span all of
527
- * the oxc parsing done for export hoisting, which made it wide enough to hit
528
- * routinely. Keeping the unlock adjacent to the write shrinks it to nothing much,
529
- * and a retry absorbs what is left.
526
+ * `EACCES: permission denied, open '<pkg>/index.ts'`. Keeping the unlock adjacent
527
+ * to the write avoids a gap during export parsing, and a retry absorbs what is
528
+ * left.
530
529
  *
531
530
  * Do NOT "simplify" this back to a single unlock-then-write: the failure is
532
531
  * timing-dependent, so it looks fine until a full-repo sweep runs against a