@dbx-tools/projen 0.6.163 → 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/README.md CHANGED
@@ -96,10 +96,29 @@ derives the crate name and repository from the parent project. A crate containin
96
96
  automatically wires matching private Node and Python binding packages using the
97
97
  same capability name. Repository-specific dependencies and features remain
98
98
  declarative options in `.projenrc.ts`; generated bindings are built separately
99
- from projen synthesis.
99
+ from projen synthesis. Target-independent Node binding TypeScript is committed
100
+ and remains generated/read-only, while native libraries stay ignored. Node
101
+ facades compile to `lib/` and publish JavaScript entry points that plain Node
102
+ can load from `node_modules`. The generated barrel exports the binding API from
103
+ the package root. Do not create a `nodeExports` binding subpath or a
104
+ handwritten type facade.
105
+
106
+ Rust dependencies between binding-enabled workspace crates become Node
107
+ `workspace:*` and Python `internalDependencies` automatically. Python generation
108
+ selects the owning crate and supplies `external_packages` imports. UBRN currently
109
+ emits every linked component; the generation task discards dependency outputs
110
+ and rewrites their imports to the owning package root, including its generated
111
+ `uniffiModule` converter table. It never copies a dependency's records or enums.
112
+ Foreign callback adapters must first be wrapped in an object created by their
113
+ own native library (`createStorageHandle` for auth), because callback registries
114
+ are library-local. Release wheels replace sibling Git requirements with matching
115
+ native-wheel versions, and dependent publishers wait for their dependencies.
116
+ Each crate builds its own `<crate>-uniffi-bindgen` executable so a workspace
117
+ build has no colliding binary outputs. Packaging runs that prebuilt executable.
100
118
 
101
119
  `sync --watch` runs a focused Rust watcher beside the OpenAPI watcher. Changes
102
- inside an existing UniFFI crate regenerate only that crate's bindings; adding or
120
+ inside an existing UniFFI crate regenerate that crate and its dependent bindings
121
+ in dependency order; adding or
103
122
  removing a crate or `setup_scaffolding!()` marker triggers a full synth. Repos
104
123
  without Rust crates start no Rust watcher. When Rust projects are detected,
105
124
  Cargo is required and the focused task fails immediately if it is unavailable.
@@ -133,11 +152,17 @@ and the UBRN Cargo build. Release packaging passes the cached executable directl
133
152
  to the Node binding generator and keeps the package barrel copied from source,
134
153
  so it does not need the installed projen dependency graph. A miss saves the
135
154
  validated executable immediately, before workspace build and packaging can fail.
136
- Bun runtime setup still runs because the binding generator is TypeScript. Cargo registry caches
155
+ Bun runtime setup still runs in the single facade row because the binding
156
+ generator and facade bundle are TypeScript. The bundle points its type surface
157
+ at the committed generated source and needs no standalone TypeScript compiler
158
+ or workspace install on a UBRN cache hit. Non-facade rows skip the complete
159
+ Bun/UBRN path.
160
+ Stable Windows rows verify and use the hosted runner's installed Rust toolchain
161
+ and select `rust-lld` for the workspace build. Cargo registry caches
137
162
  and the `SCCACHE_GHA_VERSION` namespace stay stable per target/toolchain across
138
163
  version tags. Cache keys, restore results, sccache statistics, and phase timings
139
164
  are written to each build log. Python generation executes the already-built
140
- `target/<triple>/release/uniffi-bindgen` directly. Artifact packaging therefore
165
+ `target/<triple>/release/<crate>-uniffi-bindgen` directly. Artifact packaging therefore
141
166
  does no Rust compilation after the main workspace build.
142
167
 
143
168
  Artifacts identify their crate, target, and type. Download-only publication
package/index.ts CHANGED
@@ -58,7 +58,7 @@ export { DOWNSTREAM_RELEASE_EVENT, RUST_RELEASE_EVENT, RELEASE_TAG, RELEASE_SHA
58
58
  export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
59
59
  export type { PackageTag } from "./src/tags.ts";
60
60
  export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
61
- export { makeDefaultedInterfaceParametersOptional, addExplicitInterfaceReexports } from "./src/uniffi.ts";
61
+ export { makeDefaultedInterfaceParametersOptional, addTypeScriptExtensionsToBindingImports, addExplicitInterfaceReexports } from "./src/uniffi.ts";
62
62
  export type { TypeScriptBindingModule } from "./src/uniffi.ts";
63
63
  export { DBXToolsVsCode } from "./src/vscode.ts";
64
64
  export type { IgnoreGroupOptions } from "./src/watch.ts";
package/package.json CHANGED
@@ -26,9 +26,9 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@clack/prompts": "^1.7.0",
29
- "@dbx-tools/core": "0.6.163",
30
- "@dbx-tools/path": "0.6.163",
31
- "@dbx-tools/shared-core": "0.6.163",
29
+ "@dbx-tools/core": "0.6.168",
30
+ "@dbx-tools/path": "0.6.168",
31
+ "@dbx-tools/shared-core": "0.6.168",
32
32
  "commander": "^15.0.0",
33
33
  "concurrently": "^10.0.3",
34
34
  "constructs": "^10.6.0",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "main": "index.ts",
50
50
  "license": "Apache-2.0",
51
- "version": "0.6.163",
51
+ "version": "0.6.168",
52
52
  "types": "index.ts",
53
53
  "type": "module",
54
54
  "exports": {
package/src/project-js.ts CHANGED
@@ -446,13 +446,13 @@ function defaultProjectOptions(
446
446
  devDeps: ["@types/node@^24.6.0"],
447
447
  ...(isRoot
448
448
  ? {
449
- prettier: true,
450
- prettierOptions: {
451
- settings: PRETTIER_SETTINGS,
452
- ignoreFile: true,
453
- ignoreFileOptions: { ignorePatterns: [...ignore.ignorePatterns({ test: false })] },
454
- },
455
- }
449
+ prettier: true,
450
+ prettierOptions: {
451
+ settings: PRETTIER_SETTINGS,
452
+ ignoreFile: true,
453
+ ignoreFileOptions: { ignorePatterns: [...ignore.ignorePatterns({ test: false })] },
454
+ },
455
+ }
456
456
  : {}),
457
457
  ...options,
458
458
  ...copiedGitIgnoreOptions(options),
@@ -495,24 +495,23 @@ function defaultTypeScriptProjectOptions(
495
495
  // ESLint is configured once on the ROOT (see initProject) and lints the whole
496
496
  // tree, so packages don't emit their own config. A caller can still override.
497
497
  eslint: false,
498
- devDeps: [...(base.devDeps ?? []), "typescript@^5.9.3", "@types/bun@^1.3.14"],
498
+ devDeps: [...(base.devDeps ?? []), "typescript@^5.9.3", `@types/bun@${BUN_VERSION}`],
499
499
  ...options,
500
500
  ...copiedGitIgnoreOptions(options),
501
501
  };
502
502
  }
503
503
 
504
- // Pinned to match the subproject defaults so bun resolves a single typescript
505
- // across the workspace (a bare name -> `*` could pull a second, newer major).
506
- // `@types/bun` gives the `Bun.*` globals the server/app tags now use.
507
- const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", "@types/bun@^1.3.14"];
504
+ // Pinned to match the subproject defaults so bun resolves one TypeScript and
505
+ // one runtime-compatible set of Bun globals across the workspace.
506
+ const DEV_DEPS_ROOT: string[] = ["typescript@^5.9.3", `@types/bun@${BUN_VERSION}`];
508
507
 
509
508
  /** Options for {@link DBXToolsNodeProject} (the monorepo root). */
510
509
  export interface DBXToolsJavaScriptProjectOptions
511
510
  extends
512
- CommonProjectOptions,
513
- Partial<javascript.NodeProjectOptions>,
514
- DBXToolsConfigOptions,
515
- DBXToolsPNPMWorkspaceOptions {
511
+ CommonProjectOptions,
512
+ Partial<javascript.NodeProjectOptions>,
513
+ DBXToolsConfigOptions,
514
+ DBXToolsPNPMWorkspaceOptions {
516
515
  /**
517
516
  * The npm scope for generated package names (`@<scope>/<seg-...>`). Defaults to
518
517
  * the (resolved) project name; a leading `@` is optional.
@@ -593,8 +592,7 @@ export interface DBXToolsTypeScriptProjectOptions
593
592
  */
594
593
  export class DBXToolsNodeProject
595
594
  extends javascript.NodeProject
596
- implements DBXToolsJavaScriptProject
597
- {
595
+ implements DBXToolsJavaScriptProject {
598
596
  readonly language = "javascript" as const;
599
597
  readonly scope: string;
600
598
  readonly dbxToolsConfig: DBXToolsConfig;
@@ -694,8 +692,8 @@ export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
694
692
  installDependencies(trigger: unknown): void;
695
693
  logInstallTrigger(trigger: unknown): void;
696
694
  };
697
- nodePackage.installDependencies = () => {};
698
- nodePackage.logInstallTrigger = () => {};
695
+ nodePackage.installDependencies = () => { };
696
+ nodePackage.logInstallTrigger = () => { };
699
697
  },
700
698
  );
701
699
 
@@ -708,8 +706,7 @@ export const ROOT_INSTALL_ONLY_MIXIN = mixin.create(
708
706
  */
709
707
  export class DBXToolsTypeScriptProject
710
708
  extends typescript.TypeScriptProject
711
- implements DBXToolsJavaScriptProject
712
- {
709
+ implements DBXToolsJavaScriptProject {
713
710
  readonly language = "javascript" as const;
714
711
  readonly scope: string;
715
712
  readonly dbxToolsConfig: DBXToolsConfig;
package/src/project-py.ts CHANGED
@@ -683,7 +683,10 @@ export class DBXToolsPythonWorkspace extends Component {
683
683
  const linesBeforeAuthentication = [
684
684
  "# PyPI Trusted Publisher Setup Instructions",
685
685
  "",
686
- "Use a browser to audit and configure the PyPI trusted publishers below.",
686
+ "Use the system browser to audit and configure the PyPI trusted publishers below.",
687
+ "Do not use an in-app browser or embedded webview.",
688
+ "Do not visit GitHub or use the GitHub API or CLI. Every required GitHub owner, repository, workflow, environment, and branch value is provided below and is authoritative.",
689
+ "Use only PyPI pages for this task.",
687
690
  "",
688
691
  "## Audit and confirmation",
689
692
  "",
@@ -691,7 +694,7 @@ export class DBXToolsPythonWorkspace extends Component {
691
694
  "",
692
695
  "- Confirm that the active PyPI account can administer the listed projects and pending publishers.",
693
696
  "- Start at https://pypi.org/manage/projects/ and determine which listed projects exist.",
694
- "- Inspect every existing GitHub publisher for each project and compare its owner, repository name, workflow name, and environment name with the desired values below.",
697
+ "- On PyPI, inspect every existing GitHub Actions publisher for each project and compare its owner, repository name, workflow name, and environment name with the desired values below.",
695
698
  "- Identify duplicate and mismatched trusted publishers that must be replaced by removing the publisher entry and adding the correct one.",
696
699
  "- For projects that do not exist, identify the pending publisher that must be created.",
697
700
  "- Report the active account and a proposed reconciliation plan grouped by publishers that will be left unchanged, updated, replaced, created, or removed.",
@@ -700,9 +703,9 @@ export class DBXToolsPythonWorkspace extends Component {
700
703
  "",
701
704
  "## GitHub environment policy",
702
705
  "",
703
- `- Configure every environment listed below to permit deployments from the ${releaseBranch} branch.`,
704
- "- Branch-scoped release workflows use the environment names below; tag-only deployment policies reject their trusted-publishing jobs.",
705
- "- Keep the GitHub environment name synchronized with the PyPI trusted publisher environment.",
706
+ `- The supplied branch policy value is ${releaseBranch}.`,
707
+ "- Do not inspect or configure GitHub environments during this task.",
708
+ "- GitHub environment administration is a separate task; use the supplied values only when comparing PyPI publisher entries.",
706
709
  "",
707
710
  "## Authentication",
708
711
  "",
@@ -714,7 +717,7 @@ export class DBXToolsPythonWorkspace extends Component {
714
717
  "",
715
718
  "## Efficient browser workflow",
716
719
  "",
717
- "- Reuse or claim an existing PyPI browser tab when available.",
720
+ "- Reuse an existing PyPI tab in the system browser when available.",
718
721
  "- For an existing project, open its publisher page directly at https://pypi.org/manage/project/<project-name>/settings/publishing/.",
719
722
  "- For a project that does not exist, use the pending-publisher page at https://pypi.org/manage/account/publishing/.",
720
723
  "- Treat the publisher table shown after submission as authoritative confirmation of success, even if the navigation header unexpectedly appears signed out.",
@@ -769,7 +772,7 @@ export class DBXToolsPythonWorkspace extends Component {
769
772
  ],
770
773
  });
771
774
  project.root.addTask("pypiTrustedPublisherInstructions", {
772
- description: "Print browser-agent instructions for PyPI trusted publishers",
775
+ description: "Print system-browser instructions for PyPI trusted publishers",
773
776
  exec: `node ${helper}`,
774
777
  receiveArgs: true,
775
778
  });
package/src/project-rs.ts CHANGED
@@ -28,6 +28,7 @@ export interface CargoDependencyOptions {
28
28
  readonly workspace?: boolean;
29
29
  readonly path?: string;
30
30
  readonly optional?: boolean;
31
+ readonly package?: string;
31
32
  readonly defaultFeatures?: boolean;
32
33
  readonly features?: readonly string[];
33
34
  }
@@ -156,6 +157,8 @@ export const UNIFFI_RELEASE_TARGETS: readonly UniFFIReleaseTarget[] = [
156
157
  ] as const;
157
158
 
158
159
  const RUST_CACHE_ENV = {
160
+ CARGO_INCREMENTAL: "0",
161
+ CARGO_TERM_COLOR: "always",
159
162
  RUSTC_WRAPPER: "sccache",
160
163
  SCCACHE_GHA_ENABLED: "true",
161
164
  } as const;
@@ -203,8 +206,9 @@ function releaseTargets(options: DBXToolsRustWorkspaceOptions): readonly UniFFIR
203
206
  const target = UNIFFI_RELEASE_TARGETS.find(
204
207
  (candidate) => candidate.os === platform.os && candidate.cpu === platform.cpu,
205
208
  );
206
- if (!target)
209
+ if (!target) {
207
210
  throw new Error(`Unsupported Rust release platform: ${platform.os}-${platform.cpu}`);
211
+ }
208
212
  return target;
209
213
  });
210
214
  }
@@ -247,6 +251,7 @@ export interface RustBindingMapping {
247
251
  readonly pythonPackage?: string;
248
252
  readonly pythonModule?: string;
249
253
  readonly facadeTarget?: boolean;
254
+ readonly dependencies?: readonly string[];
250
255
  }
251
256
 
252
257
  /** Persisted Rust workspace state consumed by `sync --watch`. */
@@ -257,6 +262,28 @@ export interface RustWorkspaceMapping {
257
262
  readonly releaseWorkflow?: string;
258
263
  }
259
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
+
260
287
  function rustSources(directory: string): string[] {
261
288
  if (!existsSync(directory)) return [];
262
289
  const files: string[] = [];
@@ -294,6 +321,7 @@ function cargoDependency(
294
321
  ...(!value.version && value.path && workspaceVersion ? { version: workspaceVersion } : {}),
295
322
  ...(value.workspace ? { workspace: true } : {}),
296
323
  ...(value.path ? { path: value.path } : {}),
324
+ ...(value.package ? { package: value.package } : {}),
297
325
  ...(value.optional ? { optional: true } : {}),
298
326
  ...(value.defaultFeatures === false ? { "default-features": false } : {}),
299
327
  ...(value.features?.length ? { features: [...value.features] } : {}),
@@ -366,7 +394,7 @@ export class DBXToolsRustProject extends Project implements DBXToolsProject {
366
394
  ...(this.uniffi || binary
367
395
  ? {
368
396
  bin: this.uniffi
369
- ? { name: "uniffi-bindgen", path: "uniffi-bindgen.rs" }
397
+ ? { name: `${crateName}-uniffi-bindgen`, path: "uniffi-bindgen.rs" }
370
398
  : { name: binaryName, path: "src/main.rs" },
371
399
  }
372
400
  : {}),
@@ -447,27 +475,75 @@ export class DBXToolsRustWorkspace {
447
475
  );
448
476
 
449
477
  const bindings = this.packages.filter((pkg) => pkg.uniffi);
450
- this.bindingMappings = bindings.map((pkg) => {
451
- const targets = pkg.packageOptions.bindings ?? ["node", "python"];
452
- const packageName = pkg.packageOptions.directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
453
- return {
454
- crate: pkg.crateName,
455
- rust: `${root}/${pkg.packageOptions.directory}`,
456
- ...(targets.includes("node")
457
- ? {
458
- node: `${nodeRoot}/${pkg.packageOptions.directory}`,
459
- nodePackage: `@${scope}/${packageName}`,
460
- }
461
- : {}),
462
- ...(targets.includes("python")
463
- ? {
464
- python: `${options.pythonRoot ?? "packages/py"}/${pkg.packageOptions.directory}`,
465
- pythonPackage: pkg.crateName,
466
- pythonModule: pythonModuleName(pythonModulePrefix, pkg.packageOptions.directory),
467
- }
468
- : {}),
469
- };
470
- });
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
+ }
471
547
  const releaseEnabled =
472
548
  (options.release ?? true) &&
473
549
  (this.bindingMappings.length > 0 ||
@@ -484,13 +560,7 @@ export class DBXToolsRustWorkspace {
484
560
  project.gitignore.addPatterns(
485
561
  "target/",
486
562
  ...this.bindingMappings.flatMap((binding) => [
487
- ...(binding.node
488
- ? [
489
- `${binding.node}/src/bindings.ts`,
490
- `${binding.node}/src/_bindings*.ts`,
491
- `${binding.node}/src/*${binding.crate.replaceAll("-", "_")}.*`,
492
- ]
493
- : []),
563
+ ...(binding.node ? [`${binding.node}/src/*${binding.crate.replaceAll("-", "_")}.*`] : []),
494
564
  ...(binding.python && binding.pythonModule
495
565
  ? [
496
566
  `${binding.python}/src/${binding.pythonModule.replaceAll(".", "/")}/bindings.py`,
@@ -499,6 +569,13 @@ export class DBXToolsRustWorkspace {
499
569
  : []),
500
570
  ]),
501
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
+ }
502
579
  this.pythonPackages = bindings
503
580
  .filter((pkg) => (pkg.packageOptions.bindings ?? ["node", "python"]).includes("python"))
504
581
  .map((pkg) => {
@@ -509,6 +586,9 @@ export class DBXToolsRustWorkspace {
509
586
  module,
510
587
  description: `Python bindings for ${pkg.crateName}`,
511
588
  private: true,
589
+ internalDependencies: bindingDependencies(pkg, "python").map(
590
+ (dependency) => dependency.packageOptions.directory,
591
+ ),
512
592
  generatedSources: [`src/${module.replaceAll(".", "/")}/bindings.py`],
513
593
  trustedPublisher: {
514
594
  workflowName: options.releaseWorkflowName ?? "rust-release",
@@ -544,7 +624,6 @@ export class DBXToolsRustWorkspace {
544
624
  `@${scope}/${directory.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`,
545
625
  );
546
626
  node.package.addField("private", true);
547
- node.package.file.addDeletionOverride("publishConfig");
548
627
  node.package.addField("description", `Node bindings for ${binding.crateName}`);
549
628
  const nativeTargets = releaseTargets(options);
550
629
  if (options.release ?? true) {
@@ -559,6 +638,11 @@ export class DBXToolsRustWorkspace {
559
638
  );
560
639
  }
561
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
+ );
562
646
  if (binding.packageOptions.nodeDependencies?.length) {
563
647
  node.addDeps(...binding.packageOptions.nodeDependencies);
564
648
  }
@@ -566,7 +650,6 @@ export class DBXToolsRustWorkspace {
566
650
  if (binding.packageOptions.nodeDevDependencies?.length) {
567
651
  node.addDevDeps(...binding.packageOptions.nodeDevDependencies);
568
652
  }
569
- node.compileTask.reset();
570
653
  new TextFile(node, "exports.ts", {
571
654
  lines: ['export * from "./src/bindings.ts";', ""],
572
655
  });
@@ -607,10 +690,13 @@ export class DBXToolsRustWorkspace {
607
690
  project.addTask("rs:lint", { exec: "cargo clippy --workspace --all-targets --all-features" });
608
691
  project.addTask("rs:test", { exec: "cargo test --workspace" });
609
692
  project.addTask("rs:build", { exec: "cargo build --workspace" });
610
- project.addTask("rs:bindings", {
693
+ const bindingsTask = project.addTask("rs:bindings", {
611
694
  exec: "bun node_modules/@dbx-tools/projen/tasks/rust.ts",
612
695
  description: "Generate language bindings for UniFFI-enabled Rust crates",
613
696
  });
697
+ if (this.bindingMappings.some((binding) => binding.node)) {
698
+ project.tasks.tryFind("pre-compile")?.spawn(bindingsTask);
699
+ }
614
700
  project.addTask("rs:bindings:demo", {
615
701
  description: "Generate and run UniFFI Node and Python example CLIs",
616
702
  exec: [
@@ -662,12 +748,20 @@ export class DBXToolsRustWorkspace {
662
748
  }));
663
749
  const publicCrates = this.packages
664
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
+ })
665
755
  .map((pkg) => pkg.crateName);
666
756
  const targetMatrix = targets.map((target, index) => ({
667
757
  target: { ...target, facade: index === 0 },
668
758
  }));
669
759
  const hasNodeBindings = bindings.some((binding) => binding.node);
670
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";
671
765
  const hasTargetOutputs =
672
766
  bindings.length > 0 || releaseBinaries.length > 0 || publicCrates.length > 0;
673
767
  if (hasTargetOutputs && targetMatrix.length === 0) {
@@ -779,15 +873,32 @@ export class DBXToolsRustWorkspace {
779
873
  ...(hasPythonBindings ? [{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" }] : []),
780
874
  {
781
875
  name: "Setup Rust",
876
+ ...(usePreinstalledWindowsRust ? { if: "${{ matrix.target.os != 'win32' }}" } : {}),
782
877
  uses: `dtolnay/rust-toolchain@${releaseRustVersion}`,
783
878
  with: { targets: "${{ matrix.target.cargo }}" },
784
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
+ : []),
785
895
  ...rustCacheSteps(`release-\${{ matrix.target.cargo }}-rust-${releaseRustVersion}`),
786
896
  ...(hasNodeBindings
787
897
  ? [
788
898
  {
789
899
  name: "Restore UBRN executable",
790
900
  id: "ubrn_cache",
901
+ if: "${{ matrix.target.facade }}",
791
902
  uses: "actions/cache/restore@v5",
792
903
  with: {
793
904
  path: ".cache/ubrn",
@@ -798,7 +909,8 @@ export class DBXToolsRustWorkspace {
798
909
  : []),
799
910
  ...(hasNodeBindings
800
911
  ? bunCacheRestoreSteps(project, {
801
- condition: "steps.ubrn_cache.outputs.cache-hit != 'true'",
912
+ setupCondition: facadeCondition,
913
+ condition: facadeCacheMissCondition,
802
914
  })
803
915
  : []),
804
916
  {
@@ -827,12 +939,12 @@ export class DBXToolsRustWorkspace {
827
939
  ? [
828
940
  {
829
941
  name: "Install Node tooling",
830
- if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
942
+ if: "${{ matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true' }}",
831
943
  shell: "bash",
832
944
  run: timedBash("node_tooling", "bun install"),
833
945
  },
834
946
  bunCacheSaveStep({
835
- condition: "steps.ubrn_cache.outputs.cache-hit != 'true'",
947
+ condition: facadeCacheMissCondition,
836
948
  }),
837
949
  ]
838
950
  : []),
@@ -840,7 +952,7 @@ export class DBXToolsRustWorkspace {
840
952
  ? [
841
953
  {
842
954
  name: "Prepare UBRN generator",
843
- if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
955
+ if: "${{ matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true' }}",
844
956
  env: {
845
957
  CARGO_TARGET_DIR: "${{ github.workspace }}/target/ubrn",
846
958
  UBRN_EXECUTABLE: ubrnExecutable,
@@ -858,13 +970,14 @@ export class DBXToolsRustWorkspace {
858
970
  },
859
971
  {
860
972
  name: "Verify UBRN executable",
973
+ if: "${{ matrix.target.facade }}",
861
974
  env: { UBRN_EXECUTABLE: ubrnExecutable },
862
975
  shell: "bash",
863
976
  run: 'test -f "$UBRN_EXECUTABLE"',
864
977
  },
865
978
  {
866
979
  name: "Save UBRN executable",
867
- if: "${{ steps.ubrn_cache.outputs.cache-hit != 'true' }}",
980
+ if: "${{ matrix.target.facade && steps.ubrn_cache.outputs.cache-hit != 'true' }}",
868
981
  uses: "actions/cache/save@v5",
869
982
  with: {
870
983
  path: ".cache/ubrn",
@@ -876,6 +989,10 @@ export class DBXToolsRustWorkspace {
876
989
  {
877
990
  name: "Build Rust outputs",
878
991
  shell: "bash",
992
+ env: {
993
+ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER:
994
+ "${{ matrix.target.os == 'win32' && 'rust-lld' || '' }}",
995
+ },
879
996
  run: timedBash(
880
997
  "rust_workspace",
881
998
  'cargo build --release --workspace --target "${{ matrix.target.cargo }}"',
@@ -921,7 +1038,10 @@ export class DBXToolsRustWorkspace {
921
1038
  `publish-${binding.crate}`,
922
1039
  {
923
1040
  if: "${{ github.event_name == 'repository_dispatch' }}",
924
- needs: ["build"],
1041
+ needs: [
1042
+ "build",
1043
+ ...(binding.dependencies ?? []).map((dependency) => `publish-${dependency}`),
1044
+ ],
925
1045
  "runs-on": "ubuntu-latest",
926
1046
  permissions: {
927
1047
  contents: "read",
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`);
@@ -65,6 +65,82 @@ const run = (command, args, cwd = root) => {
65
65
  if (result.status !== 0) throw new Error(`${invocation.command} exited with ${result.status}`);
66
66
  };
67
67
 
68
+ const singlePackage = (directory) => {
69
+ const packages = readdirSync(directory)
70
+ .filter((file) => file.endsWith(".tgz"))
71
+ .map((file) => resolve(directory, file));
72
+ if (packages.length !== 1) {
73
+ throw new Error(`Expected one npm package in ${directory}, found ${packages.length}`);
74
+ }
75
+ return packages[0];
76
+ };
77
+
78
+ const localWorkspacePackages = () => {
79
+ const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
80
+ const workspaces = Array.isArray(manifest.workspaces) ? manifest.workspaces : [];
81
+ return new Map(
82
+ workspaces.flatMap((directory) => {
83
+ const manifestPath = resolve(root, directory, "package.json");
84
+ if (!existsSync(manifestPath)) return [];
85
+ const workspaceManifest = JSON.parse(readFileSync(manifestPath, "utf8"));
86
+ return workspaceManifest.name ? [[workspaceManifest.name, workspaceManifest.version]] : [];
87
+ }),
88
+ );
89
+ };
90
+
91
+ const testNodeFacade = ({ facadePackage, manifest, nativePackage, nodePackage }) => {
92
+ const installDirectory = mkdtempSync(join(tmpdir(), "uniffi-facade-install-"));
93
+ try {
94
+ const workspacePackages = localWorkspacePackages();
95
+ const bindings =
96
+ JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dbxToolsConfig?.rust?.bindings ??
97
+ [];
98
+ const localDependencies = Object.keys(manifest.dependencies ?? {}).flatMap((name, index) => {
99
+ const workspaceVersion = workspacePackages.get(name);
100
+ if (!workspaceVersion) return [];
101
+ const binding = bindings.find((binding) => binding.nodePackage === name);
102
+ if (binding) {
103
+ const output = resolve(root, "dist/release", binding.crate, required("node-triple"));
104
+ return [singlePackage(join(output, "npm-facade")), singlePackage(join(output, "npm"))];
105
+ }
106
+ const directory = join(installDirectory, "local-dependencies", String(index));
107
+ mkdirSync(directory, { recursive: true });
108
+ writeFileSync(
109
+ join(directory, "package.json"),
110
+ `${JSON.stringify({
111
+ name,
112
+ version: workspaceVersion,
113
+ type: "module",
114
+ exports: "./index.js",
115
+ })}\n`,
116
+ );
117
+ writeFileSync(join(directory, "index.js"), "export {};\n");
118
+ return [directory];
119
+ });
120
+ writeFileSync(
121
+ join(installDirectory, "package.json"),
122
+ `${JSON.stringify({ private: true, type: "module" })}\n`,
123
+ );
124
+ run(
125
+ "npm",
126
+ [
127
+ "install",
128
+ "--ignore-scripts",
129
+ "--no-audit",
130
+ "--no-fund",
131
+ "--package-lock=false",
132
+ facadePackage,
133
+ nativePackage,
134
+ ...localDependencies,
135
+ ],
136
+ installDirectory,
137
+ );
138
+ run("node", ["-e", `import(${JSON.stringify(nodePackage)})`], installDirectory);
139
+ } finally {
140
+ rmSync(installDirectory, { recursive: true, force: true });
141
+ }
142
+ };
143
+
68
144
  const replaceVersion = (source, version) =>
69
145
  source.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
70
146
 
@@ -135,8 +211,6 @@ const packageNode = ({
135
211
  typeof dependency === "string" && dependency.startsWith("workspace:") ? version : dependency,
136
212
  ]),
137
213
  );
138
- delete manifest.scripts;
139
- delete manifest.devDependencies;
140
214
  writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
141
215
  run("bun", [
142
216
  resolve(root, nodeGenerator),
@@ -153,8 +227,44 @@ const packageNode = ({
153
227
  ...(parsed.values.ubrn ? ["--ubrn", parsed.values.ubrn, "--skip-barrels"] : []),
154
228
  "--skip-build",
155
229
  ]);
156
- mkdirSync(resolve(output, "npm-facade"), { recursive: true });
157
- run("npm", ["pack", "--pack-destination", resolve(output, "npm-facade")], facadeDirectory);
230
+ // Node loads the facade from node_modules, so every TypeScript entry point
231
+ // must be emitted and advertised as JavaScript. Bun is always available in
232
+ // the facade row, including when the UBRN cache skips the workspace install.
233
+ rmSync(join(facadeDirectory, "lib"), { recursive: true, force: true });
234
+ run(
235
+ "bun",
236
+ [
237
+ "build",
238
+ "index.ts",
239
+ "--outdir",
240
+ "lib",
241
+ "--target",
242
+ "node",
243
+ "--format",
244
+ "esm",
245
+ "--packages",
246
+ "external",
247
+ ],
248
+ facadeDirectory,
249
+ );
250
+ const compiledPublish = { ...(manifest.publishConfig ?? {}) };
251
+ delete compiledPublish.access;
252
+ Object.assign(manifest, compiledPublish);
253
+ manifest.types = "./index.ts";
254
+ manifest.exports["."].types = "./index.ts";
255
+ delete manifest.publishConfig;
256
+ delete manifest.scripts;
257
+ delete manifest.devDependencies;
258
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
259
+ const facadeOutput = resolve(output, "npm-facade");
260
+ mkdirSync(facadeOutput, { recursive: true });
261
+ run("npm", ["pack", "--pack-destination", facadeOutput], facadeDirectory);
262
+ testNodeFacade({
263
+ facadePackage: singlePackage(facadeOutput),
264
+ manifest,
265
+ nativePackage: singlePackage(resolve(output, "npm")),
266
+ nodePackage,
267
+ });
158
268
  };
159
269
 
160
270
  const packagePython = ({
@@ -172,6 +282,19 @@ const packagePython = ({
172
282
  const pyproject = join(pythonRoot, "pyproject.toml");
173
283
  writable(pyproject);
174
284
  writeFileSync(pyproject, replaceVersion(readFileSync(pyproject, "utf8"), version));
285
+ const workspaceBindings =
286
+ JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dbxToolsConfig?.rust?.bindings ??
287
+ [];
288
+ let metadata = readFileSync(pyproject, "utf8");
289
+ for (const binding of workspaceBindings) {
290
+ if (!binding.pythonPackage) continue;
291
+ const escaped = binding.pythonPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
292
+ metadata = metadata.replace(
293
+ new RegExp(`"${escaped} @ git\\+[^"\\n]+"`, "g"),
294
+ JSON.stringify(`${binding.pythonPackage}==${version}`),
295
+ );
296
+ }
297
+ writeFileSync(pyproject, metadata);
175
298
 
176
299
  const packageName = crate.replace(/^dbx-tools-/, "").replaceAll("-", "_");
177
300
  const packageDirectory = resolve(pythonRoot, "src", "dbx_tools", packageName);
@@ -181,10 +304,19 @@ const packagePython = ({
181
304
  "target",
182
305
  cargoTarget,
183
306
  "release",
184
- `uniffi-bindgen${os === "win32" ? ".exe" : ""}`,
307
+ `${crate}-uniffi-bindgen${os === "win32" ? ".exe" : ""}`,
185
308
  );
186
309
  if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
187
- run(generator, ["generate", "--language", "python", "--out-dir", generatedDirectory, library]);
310
+ run(generator, [
311
+ "generate",
312
+ "--language",
313
+ "python",
314
+ "--crate",
315
+ crate.replaceAll("-", "_"),
316
+ "--out-dir",
317
+ generatedDirectory,
318
+ library,
319
+ ]);
188
320
  const bindings = join(packageDirectory, "bindings.py");
189
321
  writable(bindings);
190
322
  const body = readFileSync(join(generatedDirectory, `${crate.replaceAll("-", "_")}.py`), "utf8");
package/tasks/uniffi.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env -S bun
2
+ import { spawnSync } from "node:child_process";
2
3
  import {
3
4
  cpSync,
4
5
  existsSync,
@@ -14,10 +15,11 @@ import { tmpdir } from "node:os";
14
15
  import { basename, dirname, join, resolve } from "node:path";
15
16
  import { fileURLToPath } from "node:url";
16
17
  import { parseArgs } from "node:util";
17
- import { spawnSync } from "node:child_process";
18
18
  import { makeReadonly, makeWritable, stampGenerated } from "../src/generated.ts";
19
+ import type { RustWorkspaceMapping } from "../src/project-rs.ts";
19
20
  import {
20
21
  addExplicitInterfaceReexports,
22
+ addTypeScriptExtensionsToBindingImports,
21
23
  makeDefaultedInterfaceParametersOptional,
22
24
  } from "../src/uniffi.ts";
23
25
 
@@ -43,6 +45,11 @@ const root = values.root
43
45
  : resolve(dirname(fileURLToPath(import.meta.url)), "../..");
44
46
  const crate = values.crate;
45
47
  const libraryName = crate.replaceAll("-", "_");
48
+ const workspace = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dbxToolsConfig
49
+ ?.rust as RustWorkspaceMapping | undefined;
50
+ const binding = workspace?.bindings.find((binding) => binding.crate === crate);
51
+ const dependencies =
52
+ workspace?.bindings.filter((candidate) => binding?.dependencies?.includes(candidate.crate)) ?? [];
46
53
  const extension =
47
54
  process.platform === "darwin" ? "dylib" : process.platform === "win32" ? "dll" : "so";
48
55
  const prefix = process.platform === "win32" ? "" : "lib";
@@ -61,6 +68,9 @@ const run = (command: string, args: string[]) => {
61
68
  const stderr = normalizedOutput(result.stderr ?? "");
62
69
  if (stdout) process.stdout.write(`${stdout}\n`);
63
70
  if (stderr) process.stderr.write(`${stderr}\n`);
71
+ if (result.error) {
72
+ throw new Error(`${command} failed: ${result.error.message}`, { cause: result.error });
73
+ }
64
74
  if (result.status !== 0) throw new Error(`${command} exited with ${result.status}`);
65
75
  };
66
76
 
@@ -126,42 +136,60 @@ if (values.node) {
126
136
  const generatedModules = readdirSync(nodeOutput).filter(
127
137
  (file) => file.endsWith(".ts") && file !== "index.ts",
128
138
  );
129
- const linkedComponents = generatedModules.length > 2;
130
- const nodeBindings = join(nodeSource, "bindings.ts");
131
- const linkedNames = new Map(generatedModules.map((file) => [file, `_bindings-${file}`]));
132
- const generatedFiles = linkedComponents
133
- ? generatedModules.map((file) => join(nodeSource, linkedNames.get(file)!))
134
- : [join(nodeSource, "_bindings.ts"), join(nodeSource, "_bindings-ffi.ts")];
135
- replaceGenerated(join(nodeOutput, "index.ts"), nodeBindings);
136
- if (linkedComponents) {
137
- for (const file of generatedModules) {
138
- replaceGenerated(join(nodeOutput, file), join(nodeSource, linkedNames.get(file)!));
139
+ for (const dependency of dependencies) {
140
+ if (!dependency.nodePackage) throw new Error(`Missing Node binding for ${dependency.crate}`);
141
+ const namespace = dependency.crate.replaceAll("-", "_");
142
+ for (const file of readdirSync(nodeOutput).filter((file) => file.endsWith(".ts"))) {
143
+ const path = join(nodeOutput, file);
144
+ const source = readFileSync(path, "utf8")
145
+ .replace(
146
+ new RegExp(`import (\\w+) from ["']\\./${namespace}["'];`, "g"),
147
+ `import { uniffiModule as $1 } from "${dependency.nodePackage}";`,
148
+ )
149
+ .replaceAll(`'./${namespace}'`, `'${dependency.nodePackage}'`)
150
+ .replaceAll(`"./${namespace}"`, `"${dependency.nodePackage}"`);
151
+ writeFileSync(path, source);
139
152
  }
140
- } else {
141
- replaceGenerated(join(nodeOutput, `${libraryName}.ts`), generatedFiles[0]);
142
- replaceGenerated(join(nodeOutput, `${libraryName}-ffi.ts`), generatedFiles[1]);
143
- writeFileSync(
144
- nodeBindings,
145
- readFileSync(nodeBindings, "utf8").replaceAll(`./${libraryName}`, "./_bindings"),
146
- );
147
- writeFileSync(
148
- generatedFiles[0],
149
- readFileSync(generatedFiles[0], "utf8").replaceAll(`./${libraryName}-ffi`, "./_bindings-ffi"),
150
- );
151
- }
152
- if (linkedComponents) {
153
- for (const file of [nodeBindings, ...generatedFiles]) {
154
- let source = readFileSync(file, "utf8");
155
- for (const [generated, destination] of linkedNames) {
156
- const from = generated.slice(0, -3);
157
- const to = destination.slice(0, -3);
158
- source = source
159
- .replaceAll(`'./${from}'`, `'./${to}'`)
160
- .replaceAll(`"./${from}"`, `"./${to}"`);
161
- }
162
- writeFileSync(file, source);
153
+ for (const suffix of [".ts", "-ffi.ts"]) {
154
+ const file = `${namespace}${suffix}`;
155
+ rmSync(join(nodeOutput, file), { force: true });
156
+ const index = generatedModules.indexOf(file);
157
+ if (index >= 0) generatedModules.splice(index, 1);
163
158
  }
164
159
  }
160
+ const ownModules = generatedModules.filter(
161
+ (file) => file === `${libraryName}.ts` || file === `${libraryName}-ffi.ts`,
162
+ );
163
+ if (ownModules.length !== 2 || generatedModules.length !== 2) {
164
+ throw new Error(`Unmapped UniFFI components in ${crate}: ${generatedModules.join(", ")}`);
165
+ }
166
+ writeFileSync(
167
+ join(nodeOutput, "index.ts"),
168
+ [
169
+ `export * from './${libraryName}';`,
170
+ `import uniffiModule from './${libraryName}';`,
171
+ "uniffiModule.initialize();",
172
+ "export { uniffiModule };",
173
+ "export async function uniffiInitAsync() {}",
174
+ "",
175
+ ].join("\n"),
176
+ );
177
+ const nodeBindings = join(nodeSource, "bindings.ts");
178
+ const generatedFiles = [join(nodeSource, "_bindings.ts"), join(nodeSource, "_bindings-ffi.ts")];
179
+ replaceGenerated(join(nodeOutput, "index.ts"), nodeBindings);
180
+ replaceGenerated(join(nodeOutput, libraryName + ".ts"), generatedFiles[0]);
181
+ replaceGenerated(join(nodeOutput, libraryName + "-ffi.ts"), generatedFiles[1]);
182
+ writeFileSync(
183
+ nodeBindings,
184
+ readFileSync(nodeBindings, "utf8").replaceAll("./" + libraryName, "./_bindings"),
185
+ );
186
+ writeFileSync(
187
+ generatedFiles[0],
188
+ readFileSync(generatedFiles[0], "utf8").replaceAll(
189
+ "./" + libraryName + "-ffi",
190
+ "./_bindings-ffi",
191
+ ),
192
+ );
165
193
  for (const file of generatedFiles) {
166
194
  writeFileSync(file, makeDefaultedInterfaceParametersOptional(readFileSync(file, "utf8")));
167
195
  }
@@ -176,6 +204,13 @@ if (values.node) {
176
204
  ),
177
205
  );
178
206
  for (const file of [nodeBindings, ...generatedFiles]) {
207
+ // Consumers type-check these files through workspace source exports, while
208
+ // the generated runtime internals are validated by UniFFI's own build.
209
+ const source = addTypeScriptExtensionsToBindingImports(readFileSync(file, "utf8")).replace(
210
+ /[ \t]+$/gm,
211
+ "",
212
+ );
213
+ writeFileSync(file, `/* eslint-disable */\n// @ts-nocheck\n${source}`);
179
214
  stampGenerated(file, {
180
215
  tool: "UniFFI binding generation",
181
216
  source: `the ${crate} Rust exports`,
@@ -206,31 +241,26 @@ if (values.python) {
206
241
  crate.replace(/^dbx-tools-/, "").replaceAll("-", "_"),
207
242
  );
208
243
  const pythonOutput = mkdtempSync(join(tmpdir(), `${libraryName}-python-`));
209
- run("cargo", [
210
- "run",
211
- "--release",
212
- "--package",
213
- crate,
214
- "--bin",
215
- "uniffi-bindgen",
216
- "--",
217
- "generate",
218
- "--language",
219
- "python",
220
- "--out-dir",
221
- pythonOutput,
222
- library,
223
- ]);
244
+ run(
245
+ join(targetDirectory, `${crate}-uniffi-bindgen${process.platform === "win32" ? ".exe" : ""}`),
246
+ [
247
+ "generate",
248
+ "--language",
249
+ "python",
250
+ "--crate",
251
+ libraryName,
252
+ "--out-dir",
253
+ pythonOutput,
254
+ library,
255
+ ],
256
+ );
224
257
  const generated = join(pythonOutput, `${libraryName}.py`);
225
258
  const pythonBindings = join(pythonPackage, "bindings.py");
226
259
  replaceGenerated(generated, pythonBindings);
227
260
  stampGeneratedPython(pythonBindings);
228
261
  const pythonInit = join(pythonPackage, "__init__.py");
229
262
  if (!existsSync(pythonInit)) {
230
- writeFileSync(
231
- pythonInit,
232
- `\"\"\"Python bindings for ${crate}.\"\"\"\n\nfrom .bindings import * # noqa: F403\n`,
233
- );
263
+ writeFileSync(pythonInit, "");
234
264
  }
235
265
  const pythonLibrary = join(pythonPackage, basename(library));
236
266
  makeWritable(pythonLibrary);