@dbx-tools/projen 0.6.161 → 0.6.168

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/project-rs.ts CHANGED
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { project as coreProject } from "@dbx-tools/core";
6
6
  import { string } from "@dbx-tools/shared-core";
7
7
  import { Project, TextFile, YamlFile, javascript } from "projen";
8
+ import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
8
9
  import type { DBXToolsProject } from "./project.ts";
9
10
  import {
10
11
  DBXToolsTypeScriptProject,
@@ -27,6 +28,7 @@ export interface CargoDependencyOptions {
27
28
  readonly workspace?: boolean;
28
29
  readonly path?: string;
29
30
  readonly optional?: boolean;
31
+ readonly package?: string;
30
32
  readonly defaultFeatures?: boolean;
31
33
  readonly features?: readonly string[];
32
34
  }
@@ -155,6 +157,8 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
155
157
  ] as const;
156
158
 
157
159
  const RUST_CACHE_ENV = {
160
+ CARGO_INCREMENTAL: "0",
161
+ CARGO_TERM_COLOR: "always",
158
162
  RUSTC_WRAPPER: "sccache",
159
163
  SCCACHE_GHA_ENABLED: "true",
160
164
  } as const;
@@ -202,8 +206,9 @@ function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIR
202
206
  const target = UNIFFI_RELEASE_TARGETS.find(
203
207
  (candidate) => candidate.os === platform.os && candidate.cpu === platform.cpu,
204
208
  );
205
- if (!target)
209
+ if (!target) {
206
210
  throw new Error(`Unsupported Rust release platform: ${platform.os}-${platform.cpu}`);
211
+ }
207
212
  return target;
208
213
  });
209
214
  }
@@ -246,6 +251,7 @@ export interface RustBindingMapping {
246
251
  readonly pythonPackage?: string;
247
252
  readonly pythonModule?: string;
248
253
  readonly facadeTarget?: boolean;
254
+ readonly dependencies?: readonly string[];
249
255
  }
250
256
 
251
257
  /** Persisted Rust workspace state consumed by `sync --watch`. */
@@ -256,6 +262,28 @@ export interface RustWorkspaceMapping {
256
262
  readonly releaseWorkflow?: string;
257
263
  }
258
264
 
265
+ export function orderRustBindings(bindings: readonly RustBindingMapping[]): RustBindingMapping[] {
266
+ const ordered: RustBindingMapping[] = [];
267
+ const visiting = new Set<string>();
268
+ const completed = new Set<string>();
269
+ const visit = (binding: RustBindingMapping): void => {
270
+ if (completed.has(binding.crate)) return;
271
+ if (visiting.has(binding.crate))
272
+ throw new Error(`Cyclic Rust binding dependency: ${binding.crate}`);
273
+ visiting.add(binding.crate);
274
+ for (const name of binding.dependencies ?? []) {
275
+ const dependency = bindings.find((candidate) => candidate.crate === name);
276
+ if (!dependency) throw new Error(`Missing Rust binding dependency: ${name}`);
277
+ visit(dependency);
278
+ }
279
+ visiting.delete(binding.crate);
280
+ completed.add(binding.crate);
281
+ ordered.push(binding);
282
+ };
283
+ for (const binding of bindings) visit(binding);
284
+ return ordered;
285
+ }
286
+
259
287
  function rustSources(directory: string): string[] {
260
288
  if (!existsSync(directory)) return [];
261
289
  const files: string[] = [];
@@ -293,6 +321,7 @@ function cargoDependency(
293
321
  ...(!value.version && value.path && workspaceVersion ? { version: workspaceVersion } : {}),
294
322
  ...(value.workspace ? { workspace: true } : {}),
295
323
  ...(value.path ? { path: value.path } : {}),
324
+ ...(value.package ? { package: value.package } : {}),
296
325
  ...(value.optional ? { optional: true } : {}),
297
326
  ...(value.defaultFeatures === false ? { "default-features": false } : {}),
298
327
  ...(value.features?.length ? { features: [...value.features] } : {}),
@@ -365,7 +394,7 @@ export class DBXToolsRustProject extends Project implements DBXToolsProject {
365
394
  ...(this.uniffi || binary
366
395
  ? {
367
396
  bin: this.uniffi
368
- ? { name: "uniffi-bindgen", path: "uniffi-bindgen.rs" }
397
+ ? { name: `${crateName}-uniffi-bindgen`, path: "uniffi-bindgen.rs" }
369
398
  : { name: binaryName, path: "src/main.rs" },
370
399
  }
371
400
  : {}),
@@ -446,27 +475,75 @@ export class DBXToolsRustWorkspace {
446
475
  );
447
476
 
448
477
  const bindings = this.packages.filter((pkg) => pkg.uniffi);
449
- this.bindingMappings = bindings.map((pkg) => {
450
- const targets = pkg.packageOptions.bindings ?? ["node", "python"];
451
- const packageName = pkg.packageOptions.directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
452
- return {
453
- crate: pkg.crateName,
454
- rust: `${root}/${pkg.packageOptions.directory}`,
455
- ...(targets.includes("node")
456
- ? {
457
- node: `${nodeRoot}/${pkg.packageOptions.directory}`,
458
- nodePackage: `@${scope}/${packageName}`,
459
- }
460
- : {}),
461
- ...(targets.includes("python")
462
- ? {
463
- python: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`,
464
- pythonPackage: pkg.crateName,
465
- pythonModule: pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory),
466
- }
467
- : {}),
468
- };
469
- });
478
+ const bindingDependencies = (pkg: DBXToolsRustProject, language: "node" | "python") =>
479
+ bindings.filter(
480
+ (dependency) =>
481
+ dependency !== pkg &&
482
+ (dependency.packageOptions.bindings ?? ["node", "python"]).includes(language) &&
483
+ Object.entries(pkg.packageOptions.dependencies ?? {}).some(([name, value]) => {
484
+ const resolved =
485
+ typeof value === "object" && value.workspace
486
+ ? (options.workspaceDependencies?.[name] ?? value)
487
+ : value;
488
+ return (
489
+ name === dependency.crateName ||
490
+ (typeof resolved === "object" &&
491
+ (resolved.package === dependency.crateName ||
492
+ (resolved.path !== undefined &&
493
+ resolve(
494
+ typeof value === "object" && value.workspace ? project.outdir : pkg.outdir,
495
+ resolved.path,
496
+ ) === dependency.outdir)))
497
+ );
498
+ }),
499
+ );
500
+ this.bindingMappings = orderRustBindings(
501
+ bindings.map((pkg) => {
502
+ const targets = pkg.packageOptions.bindings ?? ["node", "python"];
503
+ const packageName = pkg.packageOptions.directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
504
+ const dependencies = [
505
+ ...new Set([...bindingDependencies(pkg, "node"), ...bindingDependencies(pkg, "python")]),
506
+ ].map((dependency) => dependency.crateName);
507
+ return {
508
+ crate: pkg.crateName,
509
+ ...(dependencies.length ? { dependencies } : {}),
510
+ rust: `${root}/${pkg.packageOptions.directory}`,
511
+ ...(targets.includes("node")
512
+ ? {
513
+ node: `${nodeRoot}/${pkg.packageOptions.directory}`,
514
+ nodePackage: `@${scope}/${packageName}`,
515
+ }
516
+ : {}),
517
+ ...(targets.includes("python")
518
+ ? {
519
+ python: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`,
520
+ pythonPackage: pkg.crateName,
521
+ pythonModule: pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory),
522
+ }
523
+ : {}),
524
+ };
525
+ }),
526
+ );
527
+ for (const pkg of bindings) {
528
+ const dependencies = bindingDependencies(pkg, "python");
529
+ if (dependencies.length === 0) continue;
530
+ pkg.tryRemoveFile("uniffi.toml");
531
+ new TextFile(pkg, "uniffi.toml", {
532
+ lines: renderToml({
533
+ "bindings.python": { cdylib_name: pkg.crateName.replaceAll("-", "_") },
534
+ "bindings.typescript": { strictTypeChecking: true },
535
+ ...pkg.packageOptions.uniffiConfig,
536
+ "bindings.python.external_packages": Object.fromEntries(
537
+ dependencies.map((dependency) => [
538
+ dependency.crateName.replaceAll("-", "_"),
539
+ `${pythonModuleName(pythonModulePrefix, dependency.packageOptions.directory)}.bindings`,
540
+ ]),
541
+ ),
542
+ })
543
+ .trimEnd()
544
+ .split("\n"),
545
+ });
546
+ }
470
547
  const releaseEnabled =
471
548
  (options.release ?? true) &&
472
549
  (this.bindingMappings.length > 0 ||
@@ -483,13 +560,7 @@ export class DBXToolsRustWorkspace {
483
560
  project.gitignore.addPatterns(
484
561
  "target/",
485
562
  ...this.bindingMappings.flatMap((binding) => [
486
- ...(binding.node
487
- ? [
488
- `${binding.node}/src/bindings.ts`,
489
- `${binding.node}/src/_bindings*.ts`,
490
- `${binding.node}/src/*${binding.crate.replaceAll("-", "_")}.*`,
491
- ]
492
- : []),
563
+ ...(binding.node ? [`${binding.node}/src/*${binding.crate.replaceAll("-", "_")}.*`] : []),
493
564
  ...(binding.python && binding.pythonModule
494
565
  ? [
495
566
  `${binding.python}/src/${binding.pythonModule.replaceAll(".", "/")}/bindings.py`,
@@ -498,6 +569,13 @@ export class DBXToolsRustWorkspace {
498
569
  : []),
499
570
  ]),
500
571
  );
572
+ for (const binding of this.bindingMappings) {
573
+ if (!binding.node) {
574
+ continue;
575
+ }
576
+ project.prettier?.addIgnorePattern(`${binding.node}/src/bindings.ts`);
577
+ project.prettier?.addIgnorePattern(`${binding.node}/src/_bindings*.ts`);
578
+ }
501
579
  this.pythonPackages = bindings
502
580
  .filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
503
581
  .map((pkg) => {
@@ -508,6 +586,9 @@ export class DBXToolsRustWorkspace {
508
586
  module,
509
587
  description: `Python bindings for ${pkg.crateName}`,
510
588
  private: true,
589
+ internalDependencies: bindingDependencies(pkg, "python").map(
590
+ (dependency) => dependency.packageOptions.directory,
591
+ ),
511
592
  generatedSources: [`src/${module.replaceAll(".", "/")}/bindings.py`],
512
593
  trustedPublisher: {
513
594
  workflowName: options.releaseWorkflowName ?? "rust-release",
@@ -543,7 +624,6 @@ export class DBXToolsRustWorkspace {
543
624
  `@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
544
625
  );
545
626
  node.package.addField("private", true);
546
- node.package.file.addDeletionOverride("publishConfig");
547
627
  node.package.addField("description", `Node bindings for ${binding.crateName}`);
548
628
  const nativeTargets = releaseTargets(options);
549
629
  if (options.release ?? true) {
@@ -558,6 +638,11 @@ export class DBXToolsRustWorkspace {
558
638
  );
559
639
  }
560
640
  node.addDeps("@ubjs/core@0.31.0-5", "@ubjs/node@0.31.0-5");
641
+ node.addDeps(
642
+ ...bindingDependencies(binding, "node").map(
643
+ (dependency) => `@${scope}/${dependency.packageOptions.directory}@workspace:*`,
644
+ ),
645
+ );
561
646
  if (binding.packageOptions.nodeDependencies?.length) {
562
647
  node.addDeps(...binding.packageOptions.nodeDependencies);
563
648
  }
@@ -565,7 +650,6 @@ export class DBXToolsRustWorkspace {
565
650
  if (binding.packageOptions.nodeDevDependencies?.length) {
566
651
  node.addDevDeps(...binding.packageOptions.nodeDevDependencies);
567
652
  }
568
- node.compileTask.reset();
569
653
  new TextFile(node, "exports.ts", {
570
654
  lines: ['export * from "./src/bindings.ts";', ""],
571
655
  });
@@ -606,10 +690,13 @@ export class DBXToolsRustWorkspace {
606
690
  project.addTask("rs:lint", { exec: "cargo clippy --workspace --all-targets --all-features" });
607
691
  project.addTask("rs:test", { exec: "cargo test --workspace" });
608
692
  project.addTask("rs:build", { exec: "cargo build --workspace" });
609
- project.addTask("rs:bindings", {
693
+ const bindingsTask = project.addTask("rs:bindings", {
610
694
  exec: "bun node_modules/@dbx-tools/projen/tasks/rust.ts",
611
695
  description: "Generate language bindings for UniFFI-enabled Rust crates",
612
696
  });
697
+ if (this.bindingMappings.some((binding) => binding.node)) {
698
+ project.tasks.tryFind("pre-compile")?.spawn(bindingsTask);
699
+ }
613
700
  project.addTask("rs:bindings:demo", {
614
701
  description: "Generate and run UniFFI Node and Python example CLIs",
615
702
  exec: [
@@ -661,12 +748,20 @@ export class DBXToolsRustWorkspace {
661
748
  }));
662
749
  const publicCrates = this.packages
663
750
  .filter((pkg) => !pkg.packageOptions.private)
751
+ .sort((first, second) => {
752
+ const order = this.bindingMappings.map((binding) => binding.crate);
753
+ return order.indexOf(first.crateName) - order.indexOf(second.crateName);
754
+ })
664
755
  .map((pkg) => pkg.crateName);
665
756
  const targetMatrix = targets.map((target, index) => ({
666
757
  target: { ...target, facade: index === 0 },
667
758
  }));
668
759
  const hasNodeBindings = bindings.some((binding) => binding.node);
669
760
  const hasPythonBindings = bindings.some((binding) => binding.python);
761
+ const facadeCondition = "matrix.target.facade";
762
+ const facadeCacheMissCondition =
763
+ "matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true'";
764
+ const usePreinstalledWindowsRust = releaseRustVersion === "stable";
670
765
  const hasTargetOutputs =
671
766
  bindings.length > 0 || releaseBinaries.length > 0 || publicCrates.length > 0;
672
767
  if (hasTargetOutputs && targetMatrix.length === 0) {
@@ -766,6 +861,7 @@ export class DBXToolsRustWorkspace {
766
861
  "runs-on": "${{ matrix.target.runner }}",
767
862
  env: {
768
863
  ...RUST_CACHE_ENV,
864
+ BUN_VERSION,
769
865
  SCCACHE_GHA_VERSION: `release-\${{ matrix.target.cargo }}-rust-${releaseRustVersion}`,
770
866
  },
771
867
  strategy: {
@@ -774,27 +870,35 @@ export class DBXToolsRustWorkspace {
774
870
  },
775
871
  steps: [
776
872
  ...releaseSourceSteps(),
777
- ...(hasNodeBindings
778
- ? [
779
- {
780
- name: "Setup Bun",
781
- uses: "oven-sh/setup-bun@v2",
782
- with: { "bun-version": "1.3.14" },
783
- },
784
- ]
785
- : []),
786
873
  ...(hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
787
874
  {
788
875
  name: "Setup Rust",
876
+ ...(usePreinstalledWindowsRust ? { if: "${{ matrix.target.os != 'win32' }}" } : {}),
789
877
  uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
790
878
  with: { targets: "${{ matrix.target.cargo }}" },
791
879
  },
880
+ ...(usePreinstalledWindowsRust
881
+ ? [
882
+ {
883
+ name: "Verify preinstalled Windows Rust",
884
+ if: "${{ matrix.target.os == 'win32' }}",
885
+ shell: "bash",
886
+ run: [
887
+ "rustc --version --verbose",
888
+ "cargo --version",
889
+ 'rustup target list --installed | grep -Fx "${{ matrix.target.cargo }}"',
890
+ 'test -f "$(rustc --print sysroot)/lib/rustlib/${{ matrix.target.cargo }}/bin/rust-lld.exe"',
891
+ ].join("\n"),
892
+ },
893
+ ]
894
+ : []),
792
895
  ...rustCacheSteps(`release-\${{ matrix.target.cargo }}-rust-${releaseRustVersion}`),
793
896
  ...(hasNodeBindings
794
897
  ? [
795
898
  {
796
899
  name: "Restore UBRN executable",
797
900
  id: "ubrn_cache",
901
+ if: "${{ matrix.target.facade }}",
798
902
  uses: "actions/cache/restore@v5",
799
903
  with: {
800
904
  path: ".cache/ubrn",
@@ -803,6 +907,12 @@ export class DBXToolsRustWorkspace {
803
907
  },
804
908
  ]
805
909
  : []),
910
+ ...(hasNodeBindings
911
+ ? bunCacheRestoreSteps(project, {
912
+ setupCondition: facadeCondition,
913
+ condition: facadeCacheMissCondition,
914
+ })
915
+ : []),
806
916
  {
807
917
  name: "Log cache configuration",
808
918
  shell: "bash",
@@ -829,17 +939,20 @@ export class DBXToolsRustWorkspace {
829
939
  ? [
830
940
  {
831
941
  name: "Install Node tooling",
832
- if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
942
+ if: "${{ matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true' }}",
833
943
  shell: "bash",
834
944
  run: timedBash("node_tooling", "bun install"),
835
945
  },
946
+ bunCacheSaveStep({
947
+ condition: facadeCacheMissCondition,
948
+ }),
836
949
  ]
837
950
  : []),
838
951
  ...(hasNodeBindings
839
952
  ? [
840
953
  {
841
954
  name: "Prepare UBRN generator",
842
- if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
955
+ if: "${{ matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true' }}",
843
956
  env: {
844
957
  CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
845
958
  UBRN_EXECUTABLE: ubrnExecutable,
@@ -857,13 +970,14 @@ export class DBXToolsRustWorkspace {
857
970
  },
858
971
  {
859
972
  name: "Verify UBRN executable",
973
+ if: "${{ matrix.target.facade }}",
860
974
  env: { UBRN_EXECUTABLE: ubrnExecutable },
861
975
  shell: "bash",
862
976
  run: 'test -f "$UBRN_EXECUTABLE"',
863
977
  },
864
978
  {
865
979
  name: "Save UBRN executable",
866
- if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
980
+ if: "${{ matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true' }}",
867
981
  uses: "actions/cache/save@v5",
868
982
  with: {
869
983
  path: ".cache/ubrn",
@@ -875,6 +989,10 @@ export class DBXToolsRustWorkspace {
875
989
  {
876
990
  name: "Build Rust outputs",
877
991
  shell: "bash",
992
+ env: {
993
+ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER:
994
+ "${{ matrix.target.os == 'win32' && 'rust-lld' || '' }}",
995
+ },
878
996
  run: timedBash(
879
997
  "rust_workspace",
880
998
  'cargo build --release --workspace --target "${{ matrix.target.cargo }}"',
@@ -920,7 +1038,10 @@ export class DBXToolsRustWorkspace {
920
1038
  `publish-${binding.crate}`,
921
1039
  {
922
1040
  if: "${{ github.event_name == 'repository_dispatch' }}",
923
- needs: ["build"],
1041
+ needs: [
1042
+ "build",
1043
+ ...(binding.dependencies ?? []).map((dependency) => `publish-${dependency}`),
1044
+ ],
924
1045
  "runs-on": "ubuntu-latest",
925
1046
  permissions: {
926
1047
  contents: "read",
package/src/release.ts CHANGED
@@ -10,6 +10,7 @@ import { Component, YamlFile } from "projen";
10
10
  import { GithubWorkflow } from "projen/lib/github";
11
11
  import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
12
12
  import { object } from "@dbx-tools/shared-core";
13
+ import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
13
14
  import { applyTasks, taskScript, type DBXToolsNodeProject } from "./project.ts";
14
15
  import {
15
16
  DOWNSTREAM_RELEASE_EVENT,
@@ -21,7 +22,6 @@ import {
21
22
 
22
23
  const NODE_VERSION = "lts/*";
23
24
  const NPM_REGISTRY_URL = "https://registry.npmjs.org";
24
- const BUN_VERSION = "1.3.14";
25
25
 
26
26
  /**
27
27
  * The `release` workflow's version-stamp + publish step, as a shell script.
@@ -73,10 +73,10 @@ interface PublishWorkflow {
73
73
  }
74
74
 
75
75
  /** Shared checkout and toolchain setup for every npm publish workflow. */
76
- function publishSetupSteps(): JobStep[] {
76
+ function publishSetupSteps(project: DBXToolsNodeProject): JobStep[] {
77
77
  return [
78
78
  { name: "Checkout", uses: "actions/checkout@v6", with: { "fetch-depth": 0 } },
79
- { name: "Setup Bun", uses: "oven-sh/setup-bun@v2", with: { "bun-version": BUN_VERSION } },
79
+ ...bunCacheRestoreSteps(project),
80
80
  {
81
81
  name: "Setup Node.js",
82
82
  uses: "actions/setup-node@v6",
@@ -88,6 +88,7 @@ function publishSetupSteps(): JobStep[] {
88
88
  },
89
89
  // Bun's install; the lockfile may be absent or stale in CI so it is not frozen.
90
90
  { name: "Install", run: "bun install" },
91
+ bunCacheSaveStep(),
91
92
  ];
92
93
  }
93
94
 
@@ -301,7 +302,7 @@ export class DBXToolsRelease extends Component {
301
302
  project: DBXToolsNodeProject,
302
303
  { name, tagPrefix, steps, workingDirectory, upstreamWorkflow }: PublishWorkflow,
303
304
  ): void {
304
- const setupSteps = publishSetupSteps();
305
+ const setupSteps = publishSetupSteps(project);
305
306
  const branchDispatch = upstreamWorkflow === undefined;
306
307
  const workflow = new GithubWorkflow(project.github!, name, {
307
308
  // A newer release supersedes an older run even when publication has
@@ -369,6 +370,7 @@ export class DBXToolsRelease extends Component {
369
370
  // `DRY_RUN_INPUT` is `--dry-run` when the dispatch input is true, else empty;
370
371
  // the publish script also FORCES it on any `workflow_dispatch` run.
371
372
  env: {
373
+ BUN_VERSION,
372
374
  CI: "true",
373
375
  DRY_RUN_INPUT: "${{ github.event.inputs.dry_run == 'true' && '--dry-run' || '' }}",
374
376
  },
package/src/uniffi.ts CHANGED
@@ -36,6 +36,14 @@ export interface TypeScriptBindingModule {
36
36
  readonly source: string;
37
37
  }
38
38
 
39
+ /** Add source extensions to UBRN's generated relative binding imports. */
40
+ export const addTypeScriptExtensionsToBindingImports = (source: string): string =>
41
+ source.replace(
42
+ /(["'])\.\/_bindings(-ffi)?\1/g,
43
+ (_specifier, quote: string, suffix: string | undefined) =>
44
+ `${quote}./_bindings${suffix ?? ""}.ts${quote}`,
45
+ );
46
+
39
47
  /** Add explicit type exports for interfaces that TypeScript misses through UBRN's star exports. */
40
48
  export const addExplicitInterfaceReexports = (
41
49
  facade: string,
package/tasks/rust.ts CHANGED
@@ -8,6 +8,7 @@ import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
8
8
  import {
9
9
  discoverRustCrates,
10
10
  hasUniFFIBindings,
11
+ orderRustBindings,
11
12
  type RustBindingMapping,
12
13
  type RustWorkspaceMapping,
13
14
  } from "../src/project-rs.ts";
@@ -84,6 +85,19 @@ function generate(binding: RustBindingMapping): void {
84
85
  if (result.status !== 0) throw new Error(`binding generation exited with ${result.status}`);
85
86
  }
86
87
 
88
+ export function affectedRustBindings(
89
+ bindings: readonly RustBindingMapping[],
90
+ changed: ReadonlySet<string>,
91
+ ): RustBindingMapping[] {
92
+ const affected = new Set(changed);
93
+ const ordered = orderRustBindings(bindings);
94
+ for (const binding of ordered) {
95
+ if (binding.dependencies?.some((dependency) => affected.has(dependency)))
96
+ affected.add(binding.crate);
97
+ }
98
+ return ordered.filter((binding) => affected.has(binding.crate));
99
+ }
100
+
87
101
  const config = rustConfig();
88
102
 
89
103
  async function main(): Promise<void> {
@@ -92,7 +106,7 @@ async function main(): Promise<void> {
92
106
  throw new Error("Cargo is required because Rust projects were detected");
93
107
  }
94
108
  if (!process.argv.includes("--watch")) {
95
- for (const binding of config.bindings) generate(binding);
109
+ for (const binding of orderRustBindings(config.bindings)) generate(binding);
96
110
  return;
97
111
  }
98
112
 
@@ -109,7 +123,7 @@ async function main(): Promise<void> {
109
123
  const binding = ownerBinding(path, refreshed.bindings);
110
124
  if (binding) targets.set(binding.crate, binding);
111
125
  }
112
- for (const binding of targets.values()) {
126
+ for (const binding of affectedRustBindings(refreshed.bindings, new Set(targets.keys()))) {
113
127
  logger.start(`generating ${binding.crate} bindings`);
114
128
  generate(binding);
115
129
  logger.success(`generated ${binding.crate} bindings`);
@@ -121,7 +135,7 @@ async function main(): Promise<void> {
121
135
  const binding = ownerBinding(path, latest.bindings);
122
136
  if (binding) targets.set(binding.crate, binding);
123
137
  }
124
- for (const binding of targets.values()) {
138
+ for (const binding of affectedRustBindings(latest.bindings, new Set(targets.keys()))) {
125
139
  logger.start(`generating ${binding.crate} bindings`);
126
140
  generate(binding);
127
141
  logger.success(`generated ${binding.crate} bindings`);