@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/src/bun-app.ts CHANGED
@@ -23,6 +23,7 @@
23
23
  * `src/`), they are outside the package's `tsconfig` include, so their `Bun.*`
24
24
  * usage never trips the `app` package's DOM-only `compile`.
25
25
  */
26
+ import { stringUtils } from "@dbx-tools/shared-core";
26
27
  import { type Project, TextFile } from "projen";
27
28
 
28
29
  /** Unmanaged dev-server override module, merged over the generated options if present. */
@@ -90,34 +91,38 @@ export class BunfigFile extends TextFile {
90
91
  /** `dev.ts`: a `Bun.serve()` fullstack dev server with React HMR. */
91
92
  export class BunDevServerFile extends TextFile {
92
93
  constructor(project: Project) {
93
- super(project, "dev.ts", {
94
- marker: true,
95
- readonly: true,
96
- lines: String.raw`
97
- import { existsSync } from "node:fs";
98
- import index from "./index.html";
94
+ // prettier-ignore
95
+ const source = (
96
+ // ============================================================================
97
+ /*ts*/`
98
+ import { existsSync } from "node:fs";
99
+ import index from "./index.html";
99
100
 
100
- // Unmanaged override (${BUN_DEV_OVERRIDE}): its default export is merged over the
101
- // generated serve options - later wins - so a package tweaks the dev server
102
- // without editing this generated file.
103
- let options: Bun.ServeFunctionOptions<unknown, {}> = {
104
- port: Number(process.env.PORT ?? 5173),
105
- development: { hmr: true, console: true },
106
- routes: { "/*": index },
107
- };
101
+ // Unmanaged override (${BUN_DEV_OVERRIDE}): its default export is merged over the
102
+ // generated serve options - later wins - so a package tweaks the dev server
103
+ // without editing this generated file.
104
+ let options: Bun.ServeFunctionOptions<unknown, {}> = {
105
+ port: Number(process.env.PORT ?? 5173),
106
+ development: { hmr: true, console: true },
107
+ routes: { "/*": index },
108
+ };
108
109
 
109
- const overrideUrl = new URL("./${BUN_DEV_OVERRIDE}", import.meta.url);
110
- if (existsSync(overrideUrl)) {
111
- const mod = await import(overrideUrl.href);
112
- const override = typeof mod.default === "function" ? await mod.default() : mod.default;
113
- options = { ...options, ...override };
114
- }
110
+ const overrideUrl = new URL("./${BUN_DEV_OVERRIDE}", import.meta.url);
111
+ if (existsSync(overrideUrl)) {
112
+ const mod = await import(overrideUrl.href);
113
+ const override = typeof mod.default === "function" ? await mod.default() : mod.default;
114
+ options = { ...options, ...override };
115
+ }
115
116
 
116
- const server = Bun.serve(options);
117
- console.log("dev server running at " + server.url.href);
118
- `
119
- .trimStart()
120
- .split("\n"),
117
+ const server = Bun.serve(options);
118
+ console.log("dev server running at " + server.url.href);
119
+ `
120
+ // ============================================================================
121
+ );
122
+ super(project, "dev.ts", {
123
+ marker: true,
124
+ readonly: true,
125
+ lines: stringUtils.dedentLines(source, { trimEnd: false }),
121
126
  });
122
127
  }
123
128
  }
@@ -125,73 +130,79 @@ console.log("dev server running at " + server.url.href);
125
130
  /** `build.ts`: a `Bun.build()` production bundle (Tailwind via the plugin API). */
126
131
  export class BunBuildFile extends TextFile {
127
132
  constructor(project: Project) {
128
- super(project, "build.ts", {
129
- marker: true,
130
- readonly: true,
131
- lines: String.raw`
132
- import { existsSync } from "node:fs";
133
- import { cp, rm } from "node:fs/promises";
134
- import { basename } from "node:path";
135
- import tailwind from "bun-plugin-tailwind";
133
+ // Raw template preserves backslashes in generated regular expression literals.
134
+ // prettier-ignore
135
+ const source = (
136
+ String.raw
137
+ // ============================================================================
138
+ /*ts*/`
139
+ import { existsSync } from "node:fs";
140
+ import { cp, rm } from "node:fs/promises";
141
+ import { basename } from "node:path";
142
+ import tailwind from "bun-plugin-tailwind";
136
143
 
137
- // Unmanaged override (${BUN_BUILD_OVERRIDE}): its default export is merged over
138
- // the generated build options - later wins.
139
- let options: Bun.BuildConfig = {
140
- entrypoints: ["./index.html"],
141
- outdir: "./dist",
142
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
143
- minify: true,
144
- splitting: true,
145
- publicPath: "/",
146
- external: ["/fonts/*"],
147
- sourcemap: "none",
148
- plugins: [tailwind],
149
- };
144
+ // Unmanaged override (${BUN_BUILD_OVERRIDE}): its default export is merged over
145
+ // the generated build options - later wins.
146
+ let options: Bun.BuildConfig = {
147
+ entrypoints: ["./index.html"],
148
+ outdir: "./dist",
149
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
150
+ minify: true,
151
+ splitting: true,
152
+ publicPath: "/",
153
+ external: ["/fonts/*"],
154
+ sourcemap: "none",
155
+ plugins: [tailwind],
156
+ };
150
157
 
151
- const overrideUrl = new URL("./${BUN_BUILD_OVERRIDE}", import.meta.url);
152
- if (existsSync(overrideUrl)) {
153
- const mod = await import(overrideUrl.href);
154
- const override = typeof mod.default === "function" ? await mod.default() : mod.default;
155
- options = { ...options, ...override, plugins: [...(options.plugins ?? []), ...(override.plugins ?? [])] };
156
- }
158
+ const overrideUrl = new URL("./${BUN_BUILD_OVERRIDE}", import.meta.url);
159
+ if (existsSync(overrideUrl)) {
160
+ const mod = await import(overrideUrl.href);
161
+ const override = typeof mod.default === "function" ? await mod.default() : mod.default;
162
+ options = { ...options, ...override, plugins: [...(options.plugins ?? []), ...(override.plugins ?? [])] };
163
+ }
157
164
 
158
- const outdir = options.outdir ?? "./dist";
159
- await rm(outdir, { recursive: true, force: true });
165
+ const outdir = options.outdir ?? "./dist";
166
+ await rm(outdir, { recursive: true, force: true });
160
167
 
161
- const result = await Bun.build(options);
162
- if (!result.success) {
163
- for (const message of result.logs) console.error(message);
164
- process.exit(1);
165
- }
168
+ const result = await Bun.build(options);
169
+ if (!result.success) {
170
+ for (const message of result.logs) console.error(message);
171
+ process.exit(1);
172
+ }
166
173
 
167
- // Fix a Bun bug: with \`splitting: true\` and an HTML entrypoint, the emitted
168
- // HTML's <script src> is wired to an arbitrary chunk instead of the JS
169
- // entry-point, so the app never boots (a blank page). Rewrite the HTML script
170
- // (and stylesheet) to the real entry-point outputs. No-op when they already
171
- // match (e.g. splitting off), so it is always safe to run.
172
- const htmlOut = result.outputs.find((o) => o.path.endsWith(".html"));
173
- const entryJs = result.outputs.find((o) => o.kind === "entry-point" && o.path.endsWith(".js"));
174
- const entryCss = result.outputs.find((o) => o.kind === "entry-point" && o.path.endsWith(".css"));
175
- if (htmlOut && entryJs) {
176
- let html = await Bun.file(htmlOut.path).text();
177
- const jsName = basename(entryJs.path);
178
- html = html.replace(/(<script[^>]*\bsrc=")([^"]*\/)?[^"/]+\.js(")/i, "$1$2" + jsName + "$3");
179
- if (entryCss) {
180
- const cssName = basename(entryCss.path);
181
- html = html.replace(/(<link[^>]*\bhref=")([^"]*\/)?[^"/]+\.css(")/i, "$1$2" + cssName + "$3");
182
- }
183
- await Bun.write(htmlOut.path, html);
184
- }
174
+ // Fix a Bun bug: with \`splitting: true\` and an HTML entrypoint, the emitted
175
+ // HTML's <script src> is wired to an arbitrary chunk instead of the JS
176
+ // entry-point, so the app never boots (a blank page). Rewrite the HTML script
177
+ // (and stylesheet) to the real entry-point outputs. No-op when they already
178
+ // match (e.g. splitting off), so it is always safe to run.
179
+ const htmlOut = result.outputs.find((o) => o.path.endsWith(".html"));
180
+ const entryJs = result.outputs.find((o) => o.kind === "entry-point" && o.path.endsWith(".js"));
181
+ const entryCss = result.outputs.find((o) => o.kind === "entry-point" && o.path.endsWith(".css"));
182
+ if (htmlOut && entryJs) {
183
+ let html = await Bun.file(htmlOut.path).text();
184
+ const jsName = basename(entryJs.path);
185
+ html = html.replace(/(<script[^>]*\bsrc=")([^"]*\/)?[^"/]+\.js(")/i, "$1$2" + jsName + "$3");
186
+ if (entryCss) {
187
+ const cssName = basename(entryCss.path);
188
+ html = html.replace(/(<link[^>]*\bhref=")([^"]*\/)?[^"/]+\.css(")/i, "$1$2" + cssName + "$3");
189
+ }
190
+ await Bun.write(htmlOut.path, html);
191
+ }
185
192
 
186
- const publicDir = "./public";
187
- if (existsSync(publicDir)) {
188
- await cp(publicDir, outdir, { recursive: true });
189
- }
193
+ const publicDir = "./public";
194
+ if (existsSync(publicDir)) {
195
+ await cp(publicDir, outdir, { recursive: true });
196
+ }
190
197
 
191
- console.log("built " + result.outputs.length + " files to " + options.outdir);
192
- `
193
- .trimStart()
194
- .split("\n"),
198
+ console.log("built " + result.outputs.length + " files to " + options.outdir);
199
+ `
200
+ // ============================================================================
201
+ );
202
+ super(project, "build.ts", {
203
+ marker: true,
204
+ readonly: true,
205
+ lines: stringUtils.dedentLines(source, { trimEnd: false }),
195
206
  });
196
207
  }
197
208
  }
@@ -1,4 +1,5 @@
1
1
  /** Shared Bun setup and package-cache steps for generated workflows. */
2
+ import { stringUtils } from "@dbx-tools/shared-core";
2
3
  import { TextFile, javascript } from "projen";
3
4
 
4
5
  export const BUN_VERSION = "1.3.14";
@@ -16,61 +17,68 @@ const DEFAULT_CACHE_IGNORE_PATHS = [
16
17
 
17
18
  function cacheKeyScript(extraIgnorePaths: readonly string[]): string {
18
19
  const ignorePaths = [...new Set(extraIgnorePaths)].sort();
19
- return `#!/usr/bin/env node
20
- import { createHash } from "node:crypto";
21
- import { readdirSync, readFileSync } from "node:fs";
22
- import { join, sep } from "node:path";
20
+ // prettier-ignore
21
+ const source = (
22
+ // ============================================================================
23
+ /*js*/`
24
+ #!/usr/bin/env node
25
+ import { createHash } from "node:crypto";
26
+ import { readdirSync, readFileSync } from "node:fs";
27
+ import { join, sep } from "node:path";
23
28
 
24
- const root = process.cwd();
25
- const ignoredNames = new Set(${JSON.stringify(DEFAULT_CACHE_IGNORE_PATHS)});
26
- const ignoredPaths = new Set(${JSON.stringify(ignorePaths)});
27
- const manifests = [];
28
- const walk = (directory) => {
29
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
30
- const path = join(directory, entry.name);
31
- const relativePath = path.slice(root.length + 1).split(sep).join("/");
32
- if (ignoredNames.has(entry.name) || ignoredPaths.has(relativePath)) continue;
33
- if (entry.isDirectory()) walk(path);
34
- else if (entry.name === "package.json") manifests.push(path);
35
- }
36
- };
37
- walk(root);
29
+ const root = process.cwd();
30
+ const ignoredNames = new Set(${JSON.stringify(DEFAULT_CACHE_IGNORE_PATHS)});
31
+ const ignoredPaths = new Set(${JSON.stringify(ignorePaths)});
32
+ const manifests = [];
33
+ const walk = (directory) => {
34
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
35
+ const path = join(directory, entry.name);
36
+ const relativePath = path.slice(root.length + 1).split(sep).join("/");
37
+ if (ignoredNames.has(entry.name) || ignoredPaths.has(relativePath)) continue;
38
+ if (entry.isDirectory()) walk(path);
39
+ else if (entry.name === "package.json") manifests.push(path);
40
+ }
41
+ };
42
+ walk(root);
38
43
 
39
- const dependencyFields = [
40
- "catalog",
41
- "dependencies",
42
- "devDependencies",
43
- "optionalDependencies",
44
- "overrides",
45
- "peerDependencies",
46
- "peerDependenciesMeta",
47
- "resolutions",
48
- "trustedDependencies",
49
- ];
50
- const canonical = (value) => {
51
- if (Array.isArray(value)) return value.map(canonical);
52
- if (!value || typeof value !== "object") return value;
53
- return Object.fromEntries(
54
- Object.entries(value)
55
- .sort(([left], [right]) => left.localeCompare(right))
56
- .map(([key, child]) => [key, canonical(child)]),
44
+ const dependencyFields = [
45
+ "catalog",
46
+ "dependencies",
47
+ "devDependencies",
48
+ "optionalDependencies",
49
+ "overrides",
50
+ "peerDependencies",
51
+ "peerDependenciesMeta",
52
+ "resolutions",
53
+ "trustedDependencies",
54
+ ];
55
+ const canonical = (value) => {
56
+ if (Array.isArray(value)) return value.map(canonical);
57
+ if (!value || typeof value !== "object") return value;
58
+ return Object.fromEntries(
59
+ Object.entries(value)
60
+ .sort(([left], [right]) => left.localeCompare(right))
61
+ .map(([key, child]) => [key, canonical(child)]),
62
+ );
63
+ };
64
+ const dependencies = manifests
65
+ .sort()
66
+ .map((path) => {
67
+ const manifest = JSON.parse(readFileSync(path, "utf8"));
68
+ return [
69
+ path.slice(root.length + 1),
70
+ Object.fromEntries(
71
+ dependencyFields
72
+ .filter((field) => manifest[field] !== undefined)
73
+ .map((field) => [field, canonical(manifest[field])]),
74
+ ),
75
+ ];
76
+ });
77
+ process.stdout.write(createHash("sha256").update(JSON.stringify(dependencies)).digest("hex"));
78
+ `
79
+ // ============================================================================
57
80
  );
58
- };
59
- const dependencies = manifests
60
- .sort()
61
- .map((path) => {
62
- const manifest = JSON.parse(readFileSync(path, "utf8"));
63
- return [
64
- path.slice(root.length + 1),
65
- Object.fromEntries(
66
- dependencyFields
67
- .filter((field) => manifest[field] !== undefined)
68
- .map((field) => [field, canonical(manifest[field])]),
69
- ),
70
- ];
71
- });
72
- process.stdout.write(createHash("sha256").update(JSON.stringify(dependencies)).digest("hex"));
73
- `;
81
+ return stringUtils.dedent(source, { trimEnd: false });
74
82
  }
75
83
 
76
84
  const configured = new WeakMap<javascript.NodeProject, string>();
package/src/openapi.ts CHANGED
@@ -37,7 +37,7 @@ import { join } from "node:path";
37
37
  import { promisify } from "node:util";
38
38
  import * as bin from "@dbx-tools/core/bin";
39
39
  import { find } from "@dbx-tools/path";
40
- import { log } from "@dbx-tools/shared-core";
40
+ import { log, stringUtils } from "@dbx-tools/shared-core";
41
41
  import type * as ts from "typescript";
42
42
  import { lazyRequire } from "./_lazy-require.ts";
43
43
  import { makeReadonly, makeWritable, stampGenerated } from "./generated.ts";
@@ -61,14 +61,21 @@ const SPEAKEASY_OPENAPI_VERSION = "1.24.0";
61
61
  const SPEAKEASY_OPENAPI_RELEASE_URL = `https://github.com/speakeasy-api/openapi/releases/download/v${SPEAKEASY_OPENAPI_VERSION}`;
62
62
  const execFileAsync = promisify(execFile);
63
63
 
64
- const CLIENT_SRC = `import createClient, { type ClientOptions } from "openapi-fetch";
65
- import type { paths } from "./schema";
64
+ // prettier-ignore
65
+ const CLIENT_TEMPLATE = (
66
+ // ============================================================================
67
+ /*ts*/`
68
+ import createClient, { type ClientOptions } from "openapi-fetch";
69
+ import type { paths } from "./schema";
66
70
 
67
- /** Create a typed OpenAPI client (openapi-fetch); safe to use in the browser. */
68
- export function createApiClient(options?: ClientOptions) {
69
- return createClient<paths>(options);
70
- }
71
- `;
71
+ /** Create a typed OpenAPI client (openapi-fetch); safe to use in the browser. */
72
+ export function createApiClient(options?: ClientOptions) {
73
+ return createClient<paths>(options);
74
+ }
75
+ `
76
+ // ============================================================================
77
+ );
78
+ const CLIENT_SRC = stringUtils.dedent(CLIENT_TEMPLATE, { trimEnd: false });
72
79
 
73
80
  /** True if any module file in `<pkg>/src` matches {@link TSOA_IMPORT}. */
74
81
  function hasTsoaControllers(pkg: Pick<RecordedPackage, "dir">): boolean {
package/src/project-py.ts CHANGED
@@ -766,23 +766,28 @@ export class DBXToolsPythonWorkspace extends Component {
766
766
  ];
767
767
  const helper = ".projen/pypi-trusted-publisher-instructions.mjs";
768
768
  new TextFile(project.root, helper, {
769
- lines: [
770
- "#!/usr/bin/env node",
771
- 'import { parseArgs } from "node:util";',
772
- "",
773
- "const { values } = parseArgs({",
774
- ' options: { secretFile: { type: "string" } },',
775
- "});",
776
- `const beforeAuthentication = ${JSON.stringify(linesBeforeAuthentication)};`,
777
- `const afterAuthentication = ${JSON.stringify(linesAfterAuthentication)};`,
778
- "const authentication = values.secretFile",
779
- " ? [",
780
- " `- If browser authentication is required, read credentials from ${values.secretFile} directly into the browser without printing, logging, or exposing secret values.`,",
781
- " `- If ${values.secretFile} is absent, invalid, or insufficient for authentication, pause and ask the user to complete authentication.`,",
782
- " ]",
783
- ' : ["- If browser authentication is required, pause and ask the user to complete authentication."];',
784
- 'process.stdout.write(`${[...beforeAuthentication, ...authentication, ...afterAuthentication].join("\\n").trimEnd()}\\n`);',
785
- ],
769
+ // prettier-ignore
770
+ lines: stringUtils.dedent(
771
+ // ============================================================================
772
+ /*js*/`
773
+ #!/usr/bin/env node
774
+ import { parseArgs } from "node:util";
775
+
776
+ const { values } = parseArgs({
777
+ options: { secretFile: { type: "string" } },
778
+ });
779
+ const beforeAuthentication = ${JSON.stringify(linesBeforeAuthentication)};
780
+ const afterAuthentication = ${JSON.stringify(linesAfterAuthentication)};
781
+ const authentication = values.secretFile
782
+ ? [
783
+ \`- If browser authentication is required, read credentials from \${values.secretFile} directly into the browser without printing, logging, or exposing secret values.\`,
784
+ \`- If \${values.secretFile} is absent, invalid, or insufficient for authentication, pause and ask the user to complete authentication.\`,
785
+ ]
786
+ : ["- If browser authentication is required, pause and ask the user to complete authentication."];
787
+ process.stdout.write(\`\${[...beforeAuthentication, ...authentication, ...afterAuthentication].join("\\n").trimEnd()}\\n\`);
788
+ `
789
+ // ============================================================================
790
+ ).split("\n"),
786
791
  });
787
792
  project.root.addTask("pypiTrustedPublisherInstructions", {
788
793
  description: "Print system-browser instructions for PyPI trusted publishers",
package/src/project-rs.ts CHANGED
@@ -289,9 +289,11 @@ export interface RustReleaseBinaryMapping {
289
289
  readonly command: string;
290
290
  readonly description: string;
291
291
  readonly binaryName: string;
292
+ readonly hidden: boolean;
292
293
  readonly unit: string;
293
294
  readonly component: string;
294
295
  readonly version: string;
296
+ readonly tagPrefix: string;
295
297
  readonly tag: string;
296
298
  readonly repository: string;
297
299
  readonly assets: readonly RustReleaseBinaryAssetMapping[];
@@ -379,17 +381,40 @@ function planRustReleaseBinaries(
379
381
  resolved: ResolvedRustWorkspaceOptions,
380
382
  ): RustReleaseBinaryMapping[] {
381
383
  const commands = new Set<string>();
382
- const binaries = packages
383
- .filter((pkg) => Boolean(pkg.packageOptions.cli))
384
- .map((pkg) => {
385
- if (!pkg.packageOptions.release) {
384
+ const binaries = packages.flatMap((pkg) => {
385
+ const configuredBinaries = [
386
+ ...(pkg.packageOptions.cli
387
+ ? [
388
+ {
389
+ name: pkg.packageOptions.binaryName ?? pkg.crateName,
390
+ defaultCommand: pkg.packageOptions.directory,
391
+ description: pkg.packageOptions.description,
392
+ release: pkg.packageOptions.release,
393
+ excludedOs: pkg.packageOptions.releaseExcludeOs,
394
+ cli: pkg.packageOptions.cli,
395
+ },
396
+ ]
397
+ : []),
398
+ ...(pkg.packageOptions.binaries ?? [])
399
+ .filter((binary) => Boolean(binary.cli))
400
+ .map((binary) => ({
401
+ name: binary.name,
402
+ defaultCommand: binary.name,
403
+ description: binary.description ?? pkg.packageOptions.description,
404
+ release: binary.release,
405
+ excludedOs: binary.releaseExcludeOs,
406
+ cli: binary.cli!,
407
+ })),
408
+ ];
409
+ return configuredBinaries.map((binary) => {
410
+ if (!binary.release) {
386
411
  throw new Error(`${pkg.crateName} must set release when cli is configured`);
387
412
  }
388
- const configured = pkg.packageOptions.cli;
413
+ const configured = binary.cli;
389
414
  const command =
390
415
  typeof configured === "object" && configured.command
391
416
  ? configured.command
392
- : pkg.packageOptions.directory;
417
+ : binary.defaultCommand;
393
418
  if (!/^[a-z0-9][a-z0-9-]*$/.test(command)) {
394
419
  throw new Error(`Invalid Rust CLI command: ${command}`);
395
420
  }
@@ -399,24 +424,31 @@ function planRustReleaseBinaries(
399
424
  commands.add(command);
400
425
  const description =
401
426
  (typeof configured === "object" ? configured.description : undefined) ??
402
- pkg.packageOptions.description ??
427
+ binary.description ??
403
428
  pkg.crateName;
404
- const excludedOs = new Set(pkg.packageOptions.releaseExcludeOs ?? []);
405
- const binaryName = pkg.packageOptions.binaryName ?? pkg.crateName;
429
+ const hidden = typeof configured === "object" && configured.hidden === true;
430
+ const excludedOs = new Set(binary.excludedOs ?? []);
431
+ const binaryName = binary.name;
406
432
  const identity = isDBXToolsJavaScriptProject()(project)
407
433
  ? project.releaseCatalog.releaseIdentityFor(pkg)
408
434
  : {
409
435
  component: defaultReleaseUnitId("rust", pkg.crateName),
410
436
  version: readWorkspaceVersion(project.outdir),
411
437
  };
438
+ const tagPrefix =
439
+ isDBXToolsJavaScriptProject()(project) && project.releaseCatalog.mode === "independent"
440
+ ? `${identity.component}-v`
441
+ : "v";
412
442
  return {
413
443
  command,
414
444
  description,
415
445
  binaryName,
446
+ hidden,
416
447
  unit: "id" in identity ? identity.id : identity.component,
417
448
  component: identity.component,
418
449
  version: identity.version,
419
- tag: `${identity.component}-v${identity.version}`,
450
+ tagPrefix,
451
+ tag: `${tagPrefix}${identity.version}`,
420
452
  repository: resolved.repository,
421
453
  assets: resolved.nativeTargets
422
454
  .filter((target) => !excludedOs.has(target.os))
@@ -427,6 +459,7 @@ function planRustReleaseBinaries(
427
459
  })),
428
460
  };
429
461
  });
462
+ });
430
463
  if (binaries.length && !resolved.repository) {
431
464
  throw new Error("A repository URL is required when Rust CLI commands are configured");
432
465
  }
@@ -894,6 +927,10 @@ export class DBXToolsRustWorkspace extends Component {
894
927
  identity: pkg.crateName,
895
928
  publish: !pkg.packageOptions.private || pkg.packageOptions.release === true,
896
929
  sourcePaths: [`${toPosix(relative(project.outdir, pkg.outdir))}/src`],
930
+ });
931
+ }
932
+ for (const pkg of this.packages) {
933
+ project.releaseCatalog.configureProject(pkg, {
897
934
  dependencies: [
898
935
  ...rustReleaseDependencies(
899
936
  project,
@@ -921,7 +958,7 @@ export class DBXToolsRustWorkspace extends Component {
921
958
  if (isDBXToolsJavaScriptProject()(project)) {
922
959
  for (const binary of this.releaseBinaries) {
923
960
  project.releaseCatalog.registerArtifact(binary.unit, {
924
- id: `${binary.unit}:github-binary`,
961
+ id: `${binary.unit}:github-binary:${binary.binaryName}`,
925
962
  kind: "github-binary",
926
963
  name: binary.binaryName,
927
964
  generated: true,
@@ -311,7 +311,10 @@ export class DBXToolsReleaseCatalog extends Component {
311
311
  /** Replace grouping metadata for an already registered project. */
312
312
  configureProject(
313
313
  project: Project,
314
- options: Pick<ReleaseProjectRegistration, "unit" | "component" | "publish" | "sourcePaths">,
314
+ options: Pick<
315
+ ReleaseProjectRegistration,
316
+ "unit" | "component" | "publish" | "sourcePaths" | "dependencies"
317
+ >,
315
318
  ): void {
316
319
  const current = this.projects.get(project);
317
320
  if (!current) throw new Error(`Release project is not registered: ${project.outdir}`);