@dbx-tools/projen 0.6.168 → 0.6.174

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/release.ts CHANGED
@@ -6,11 +6,16 @@
6
6
  */
7
7
  import { existsSync, rmSync } from "node:fs";
8
8
  import { resolve } from "node:path";
9
+ import { object } from "@dbx-tools/shared-core";
9
10
  import { Component, YamlFile } from "projen";
10
11
  import { GithubWorkflow } from "projen/lib/github";
11
12
  import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
12
- import { object } from "@dbx-tools/shared-core";
13
13
  import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
14
+ import {
15
+ orderRustBindings,
16
+ type RustBindingMapping,
17
+ type RustWorkspaceMapping,
18
+ } from "./project-rs.ts";
14
19
  import { applyTasks, taskScript, type DBXToolsNodeProject } from "./project.ts";
15
20
  import {
16
21
  DOWNSTREAM_RELEASE_EVENT,
@@ -70,6 +75,7 @@ interface PublishWorkflow {
70
75
  readonly steps: readonly JobStep[];
71
76
  readonly workingDirectory?: string;
72
77
  readonly upstreamWorkflow?: string;
78
+ readonly rustArtifacts?: boolean;
73
79
  }
74
80
 
75
81
  /** Shared checkout and toolchain setup for every npm publish workflow. */
@@ -300,7 +306,7 @@ export class DBXToolsRelease extends Component {
300
306
  /** Author the common tag trigger, concurrency policy, permissions, and publish job. */
301
307
  private authorPublishWorkflow(
302
308
  project: DBXToolsNodeProject,
303
- { name, tagPrefix, steps, workingDirectory, upstreamWorkflow }: PublishWorkflow,
309
+ { name, tagPrefix, steps, workingDirectory, upstreamWorkflow, rustArtifacts }: PublishWorkflow,
304
310
  ): void {
305
311
  const setupSteps = publishSetupSteps(project);
306
312
  const branchDispatch = upstreamWorkflow === undefined;
@@ -314,7 +320,7 @@ export class DBXToolsRelease extends Component {
314
320
  // the publish job below overrides it with the `id-token` it needs.
315
321
  workflow.file?.addOverride("permissions", {
316
322
  contents: "read",
317
- ...(upstreamWorkflow ? { actions: "read" } : {}),
323
+ ...(upstreamWorkflow || rustArtifacts ? { actions: "read" } : {}),
318
324
  });
319
325
  if (upstreamWorkflow) {
320
326
  workflow.file?.addOverride("on.workflow_run", {
@@ -362,7 +368,7 @@ export class DBXToolsRelease extends Component {
362
368
  runsOn: ["ubuntu-latest"],
363
369
  // `id-token: write` lets npm mint the OIDC token for provenance attestation.
364
370
  permissions: {
365
- ...(upstreamWorkflow ? { actions: JobPermission.READ } : {}),
371
+ ...(upstreamWorkflow || rustArtifacts ? { actions: JobPermission.READ } : {}),
366
372
  contents: JobPermission.READ,
367
373
  idToken: JobPermission.WRITE,
368
374
  },
@@ -483,14 +489,112 @@ export class DBXToolsRelease extends Component {
483
489
  * version on every package first makes the pushed tag the published version
484
490
  * (no bump math).
485
491
  */
492
+ private nodeBindingReleaseSteps(project: DBXToolsNodeProject): {
493
+ before: JobStep[];
494
+ after: JobStep[];
495
+ } {
496
+ const config = project.dbxToolsConfig.rust;
497
+ if (!object.isRecord(config) || !Array.isArray(config.bindings)) {
498
+ return { before: [], after: [] };
499
+ }
500
+ const bindings = orderRustBindings(config.bindings as RustWorkspaceMapping["bindings"]).filter(
501
+ (binding): binding is RustBindingMapping & { node: string; nodePackage: string } =>
502
+ Boolean(binding.node && binding.nodePackage),
503
+ );
504
+ if (bindings.length === 0) return { before: [], after: [] };
505
+
506
+ const before: JobStep[] = [
507
+ {
508
+ name: "Require Rust artifact handoff",
509
+ if: "${{ github.event_name == 'repository_dispatch' }}",
510
+ shell: "bash",
511
+ env: {
512
+ RUST_RUN_ID: "${{ github.event.client_payload.rust_run_id }}",
513
+ RUST_RUN_ATTEMPT: "${{ github.event.client_payload.rust_run_attempt }}",
514
+ },
515
+ run: ['test -n "$RUST_RUN_ID"', 'test -n "$RUST_RUN_ATTEMPT"'].join("\n"),
516
+ },
517
+ {
518
+ name: "Download native npm packages",
519
+ if: "${{ github.event_name == 'repository_dispatch' }}",
520
+ uses: "actions/download-artifact@v8",
521
+ with: {
522
+ pattern: "*-npm",
523
+ path: "dist/uniffi/native",
524
+ "merge-multiple": true,
525
+ "run-id": "${{ github.event.client_payload.rust_run_id }}",
526
+ "github-token": "${{ github.token }}",
527
+ },
528
+ },
529
+ {
530
+ name: "Publish native npm packages",
531
+ if: "${{ github.event_name == 'repository_dispatch' }}",
532
+ env: {
533
+ NPM_CONFIG_PROVENANCE: "true",
534
+ NPM_CONFIG_TOKEN: "${{ secrets.NPM_TOKEN }}",
535
+ NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}",
536
+ },
537
+ run: [
538
+ "test \"$(find dist/uniffi/native -name '*.tgz' | wc -l | tr -d ' ')\" -gt 0",
539
+ 'for package in dist/uniffi/native/*.tgz; do npm publish "$package" --access public; done',
540
+ ].join("\n"),
541
+ },
542
+ {
543
+ name: "Refresh workspace after native publication",
544
+ if: "${{ github.event_name == 'repository_dispatch' }}",
545
+ run: "bun install --force",
546
+ },
547
+ ];
548
+
549
+ const facadeCommands = [
550
+ 'if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then',
551
+ ' VERSION="0.0.0-dry.${GITHUB_RUN_NUMBER}"',
552
+ " DRY_RUN=--dry-run",
553
+ "else",
554
+ ' VERSION="$RELEASE_VERSION"',
555
+ " DRY_RUN=",
556
+ "fi",
557
+ ...bindings.flatMap((binding) => {
558
+ const output = `dist/uniffi/facades/${binding.crate}`;
559
+ return [
560
+ `NATIVE_ARG=""`,
561
+ 'if [ "$GITHUB_EVENT_NAME" = "repository_dispatch" ]; then',
562
+ ` NATIVE_PACKAGE="$(find dist/uniffi/native -name '${binding.crate}-linux-x64-gnu-*.tgz' -print -quit)"`,
563
+ ' test -n "$NATIVE_PACKAGE"',
564
+ ' NATIVE_ARG="--native-package $NATIVE_PACKAGE"',
565
+ "fi",
566
+ `node .projen/uniffi-release.mjs facade --node "${binding.node}" --node-package "${binding.nodePackage}" --node-triple "linux-x64-gnu" --version "$VERSION" --output "${output}" $NATIVE_ARG`,
567
+ `for package in ${output}/npm-facade/*.tgz; do npm publish "$package" --access public $DRY_RUN; done`,
568
+ ];
569
+ }),
570
+ ];
571
+ return {
572
+ before,
573
+ after: [
574
+ {
575
+ name: "Build and publish UniFFI npm facades",
576
+ env: {
577
+ NPM_CONFIG_PROVENANCE: "true",
578
+ NPM_CONFIG_TOKEN: "${{ secrets.NPM_TOKEN }}",
579
+ NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}",
580
+ },
581
+ run: facadeCommands.join("\n"),
582
+ },
583
+ ],
584
+ };
585
+ }
586
+
486
587
  private authorReleaseWorkflow(project: DBXToolsNodeProject): void {
487
588
  if (!this.workflowName) return;
488
589
  if (this.workflowName !== "release") project.tryRemoveFile(".github/workflows/release.yml");
590
+ const bindingSteps = this.nodeBindingReleaseSteps(project);
489
591
  this.authorPublishWorkflow(project, {
490
592
  name: this.workflowName,
491
593
  tagPrefix: this.tagPrefix,
492
594
  upstreamWorkflow: this.upstreamWorkflow,
595
+ rustArtifacts: bindingSteps.before.length > 0,
493
596
  steps: [
597
+ ...bindingSteps.before,
494
598
  // The pushed tag is the version: `<prefix>1.2.3` -> `1.2.3`. Stamp it on
495
599
  // every workspace package (manifests are projen-readonly, so unlock
496
600
  // first), rewriting `workspace:*` sibling deps to `^<version>` so the
@@ -512,6 +616,7 @@ export class DBXToolsRelease extends Component {
512
616
  NPM_CONFIG_PROVENANCE: "true",
513
617
  },
514
618
  },
619
+ ...bindingSteps.after,
515
620
  ],
516
621
  });
517
622
  }
package/src/uniffi.ts CHANGED
@@ -56,3 +56,40 @@ export const addExplicitInterfaceReexports = (
56
56
  if (exports.length === 0) return facade;
57
57
  return `${facade.trimEnd()}\n${exports.join("\n")}\n`;
58
58
  };
59
+
60
+ const GENERATED_BINDINGS_HEADER = "GENERATED by UniFFI binding generation";
61
+
62
+ function pythonAllNames(source: string, file: string): string[] {
63
+ const body = /^__all__\s*=\s*\[([\s\S]*?)\]/m.exec(source)?.[1];
64
+ if (body === undefined) {
65
+ throw new Error(`Python exports must define a literal __all__ list: ${file}`);
66
+ }
67
+ return [...body.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]!);
68
+ }
69
+
70
+ /** Generate one package root from the binding module's explicit exports. */
71
+ export function mergePythonBindingExports(
72
+ initSource: string,
73
+ bindingsSource: string,
74
+ options: { readonly crate: string; readonly file: string },
75
+ ): string {
76
+ if (initSource.trim() && !initSource.includes(GENERATED_BINDINGS_HEADER)) {
77
+ throw new Error(`Cannot replace non-generated Python package exports: ${options.file}`);
78
+ }
79
+ const bindingNames = pythonAllNames(bindingsSource, options.file);
80
+ const publicNames = [...bindingNames].sort((left, right) =>
81
+ left < right ? -1 : left > right ? 1 : 0,
82
+ );
83
+ return [
84
+ "# GENERATED by UniFFI binding generation - DO NOT EDIT.",
85
+ `# Regenerated from the ${options.crate} Rust exports.`,
86
+ "# Hand edits are overwritten on the next watch; this file is read-only.",
87
+ "",
88
+ "from .bindings import *",
89
+ "",
90
+ "__all__ = [",
91
+ ...publicNames.map((name) => ` ${JSON.stringify(name)},`),
92
+ "]",
93
+ "",
94
+ ].join("\n");
95
+ }
package/tasks/bump.ts CHANGED
@@ -135,8 +135,8 @@ function assertReleaseTagsPointToHead(tags: readonly string[]): void {
135
135
  * (not-yet-published) version into the COMMITTED manifest breaks the release
136
136
  * workflow's initial `bun install`, which checks out that commit before anything
137
137
  * is published. Sibling versions are resolved transiently at publish time instead:
138
- * `projen-release` runs `tasks/publish.ts --stamp-only` (set versions + refresh
139
- * lockfile), then `bun publish` in `projen/` strips `workspace:*` to those versions.
138
+ * The bump stamps every workspace member before committing so independently
139
+ * synthesized members and the Bun lock resolve the same release version.
140
140
  */
141
141
  function writeManifestVersion(pkgPath: string, version: string): void {
142
142
  const { mode } = statSync(pkgPath);
@@ -306,6 +306,16 @@ program
306
306
  },
307
307
  });
308
308
  }
309
+ if (opts.version) {
310
+ const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
311
+ exec.spawnSync("bun", [publishScript, version, "--stamp-only"], {
312
+ cwd: process.cwd(),
313
+ stdout: "inherit",
314
+ stderr: "inherit",
315
+ stdin: "ignore",
316
+ check: true,
317
+ });
318
+ }
309
319
 
310
320
  if (opts.commit) {
311
321
  // Stage the whole tree so the release commit captures the version bump
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env -S bun
2
+ import { spawnSync } from "node:child_process";
2
3
  import { chmodSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
4
  import { arch, platform } from "node:os";
4
5
  import { dirname, join, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { parseArgs } from "node:util";
7
- import { spawnSync } from "node:child_process";
8
8
  import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
9
9
  import type { RustBindingMapping, RustWorkspaceMapping } from "../src/project-rs.ts";
10
10
 
@@ -155,8 +155,6 @@ function buildAndPublish(binding: RustBindingMapping, version: string): void {
155
155
  includePython ? binding.python! : "",
156
156
  "--node-package",
157
157
  includeNode ? binding.nodePackage! : "",
158
- "--node-generator",
159
- "projen/tasks/uniffi.ts",
160
158
  "--python-package",
161
159
  includePython ? binding.pythonPackage! : "",
162
160
  "--cargo-target",
@@ -184,8 +182,9 @@ function buildAndPublish(binding: RustBindingMapping, version: string): void {
184
182
  ...artifacts(join(output, "npm"), ".tgz"),
185
183
  ...artifacts(join(output, "npm-facade"), ".tgz"),
186
184
  ];
187
- for (const packageFile of packages)
185
+ for (const packageFile of packages) {
188
186
  run("npm", ["publish", packageFile, "--registry", registry!]);
187
+ }
189
188
  }
190
189
  if (includePython) {
191
190
  const wheels = artifacts(join(output, "python"), ".whl");
package/tasks/publish.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * `bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run]`
4
4
  * - ensure every workspace member and the Bun lock carry the release version,
5
- * then publish each non-private one with `bun publish`.
5
+ * then publish each package owned by the standard Node release with `bun publish`.
6
6
  *
7
7
  * Bun has no `pnpm -r publish`, so this loop is the recursive-publish stand-in.
8
8
  * It leans on native bun for everything bun already does:
@@ -41,8 +41,7 @@
41
41
  * but uploads nothing, so the `release` workflow is testable end-to-end via a
42
42
  * `workflow_dispatch` run without anything reaching npm. `--registry` targets a
43
43
  * non-default registry (a local verdaccio); `--exclude <dir>` (repeatable,
44
- * repo-relative) skips a member that releases on its OWN tag namespace (e.g.
45
- * `projen`, published by `projen-release`, not the main `release`).
44
+ * repo-relative) skips a member owned by another publication flow.
46
45
  *
47
46
  * `--no-restore` keeps the edits on disk instead of undoing them at exit. Only
48
47
  * needed when a LATER process must still see them - `--stamp-only` implies it,
@@ -140,7 +139,8 @@ function lockfileMatchesVersion(
140
139
 
141
140
  /** Spawn `command` in `cwd` with `PATH` overridden, failing the task on non-zero. */
142
141
  function run(cwd: string, command: string, args: string[], path: string): void {
143
- exec.spawnSync(command, args, {
142
+ const executable = command === "bun" && process.versions.bun ? process.execPath : command;
143
+ exec.spawnSync(executable, args, {
144
144
  cwd,
145
145
  stdout: "inherit",
146
146
  stderr: "inherit",
@@ -152,7 +152,8 @@ function run(cwd: string, command: string, args: string[], path: string): void {
152
152
 
153
153
  /** Asynchronous counterpart used for bounded parallel stamping and publishing. */
154
154
  async function runAsync(cwd: string, command: string, args: string[], path: string): Promise<void> {
155
- await exec.spawn(command, args, {
155
+ const executable = command === "bun" && process.versions.bun ? process.execPath : command;
156
+ await exec.spawn(executable, args, {
156
157
  cwd,
157
158
  stdout: "inherit",
158
159
  stderr: "inherit",
@@ -274,9 +275,8 @@ if (!Number.isInteger(parsedConcurrency) || parsedConcurrency < 1) {
274
275
  }
275
276
  const concurrency = parsedConcurrency;
276
277
  // `--stamp-only`: set versions + refresh the lockfile, then STOP (no publish).
277
- // The standalone `projen-release` uses this to version-stamp the workspace so its
278
- // own `bun publish` (run separately, in `projen/`) resolves `workspace:*` siblings
279
- // to the release version; publishing every member here would double-publish them.
278
+ // A separate package command can use this when its own publish process needs
279
+ // workspace sibling versions resolved before it starts.
280
280
  const stampOnly = rest.includes("--stamp-only");
281
281
  // Undo the manifest edits at exit unless a later process still needs them. Implied
282
282
  // off by `--stamp-only`, whose stamps exist precisely for a subsequent `bun publish`.
@@ -338,17 +338,17 @@ for (const dir of members) {
338
338
  const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
339
339
  name?: string;
340
340
  private?: boolean;
341
+ dbxToolsConfig?: { uniffi?: boolean };
341
342
  scripts?: Record<string, string>;
342
343
  };
343
344
  if (pkg.private) {
344
345
  logger.info(`skip private ${pkg.name ?? dirname(dir)}`);
345
346
  continue;
346
347
  }
347
- // bun won't fold publishConfig into the packed manifest, so do it ourselves -
348
- // otherwise the tarball's `bin`/`main`/`exports` stay pointed at `.ts` source.
349
- const manifestPath = join(dir, "package.json");
350
- unlockManifest(manifestPath);
351
- applyPublishConfig(manifestPath);
348
+ if (pkg.dbxToolsConfig?.uniffi === true) {
349
+ logger.info(`skip UniFFI ${pkg.name ?? dirname(dir)}`);
350
+ continue;
351
+ }
352
352
  publishable.push({
353
353
  dir,
354
354
  name: pkg.name ?? dirname(dir),
@@ -362,6 +362,15 @@ if (compiled.length > 0) {
362
362
  run(root, "bun", ["run", ...compiled.flatMap((pkg) => ["--filter", pkg.name]), "compile"], path);
363
363
  }
364
364
 
365
+ // Compile against workspace source exports first. Switching manifests to their
366
+ // publishConfig entries before compilation makes consumers resolve sibling
367
+ // `lib/*.d.ts` files that have not been emitted yet in a clean checkout.
368
+ for (const { dir } of publishable) {
369
+ const manifestPath = join(dir, "package.json");
370
+ unlockManifest(manifestPath);
371
+ applyPublishConfig(manifestPath);
372
+ }
373
+
365
374
  logger.info(
366
375
  `${dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
367
376
  );
@@ -25,8 +25,7 @@ const parsed = parseArgs({
25
25
  node: { type: "string" },
26
26
  python: { type: "string" },
27
27
  "node-package": { type: "string" },
28
- "node-generator": { type: "string" },
29
- ubrn: { type: "string" },
28
+ "native-package": { type: "string" },
30
29
  "python-package": { type: "string" },
31
30
  "cargo-target": { type: "string" },
32
31
  "node-triple": { type: "string" },
@@ -88,10 +87,42 @@ const localWorkspacePackages = () => {
88
87
  );
89
88
  };
90
89
 
91
- const testNodeFacade = ({ facadePackage, manifest, nativePackage, nodePackage }) => {
90
+ /**
91
+ * Read one bundled facade's static imports so local workspace dependency stubs
92
+ * expose every requested ESM name. Callable proxy values support module helpers
93
+ * and base classes that execute while the facade is imported.
94
+ */
95
+ const importedNames = (source, packageName) => {
96
+ const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
97
+ const pattern = new RegExp(`(?:^|\\n)import\\s+([^;]+?)\\s+from\\s+["']${escaped}["'];`, "g");
98
+ const names = new Set();
99
+ let hasDefault = false;
100
+ for (const match of source.matchAll(pattern)) {
101
+ const clause = match[1].trim();
102
+ const named = /\{([\s\S]*?)\}/.exec(clause)?.[1];
103
+ if (named) {
104
+ for (const specifier of named.split(",")) {
105
+ const name = specifier.trim().split(/\s+as\s+/)[0];
106
+ if (name) names.add(name);
107
+ }
108
+ }
109
+ const beforeNamed = clause.split(/[,{\s]/, 1)[0];
110
+ if (beforeNamed && beforeNamed !== "*" && !clause.startsWith("{")) hasDefault = true;
111
+ }
112
+ return { names: [...names].sort(), hasDefault };
113
+ };
114
+
115
+ const testNodeFacade = ({
116
+ facadeDirectory,
117
+ facadePackage,
118
+ manifest,
119
+ nativePackage,
120
+ nodePackage,
121
+ }) => {
92
122
  const installDirectory = mkdtempSync(join(tmpdir(), "uniffi-facade-install-"));
93
123
  try {
94
124
  const workspacePackages = localWorkspacePackages();
125
+ const facadeSource = readFileSync(join(facadeDirectory, "lib", "index.js"), "utf8");
95
126
  const bindings =
96
127
  JSON.parse(readFileSync(join(root, "package.json"), "utf8")).dbxToolsConfig?.rust?.bindings ??
97
128
  [];
@@ -101,7 +132,10 @@ const testNodeFacade = ({ facadePackage, manifest, nativePackage, nodePackage })
101
132
  const binding = bindings.find((binding) => binding.nodePackage === name);
102
133
  if (binding) {
103
134
  const output = resolve(root, "dist/release", binding.crate, required("node-triple"));
104
- return [singlePackage(join(output, "npm-facade")), singlePackage(join(output, "npm"))];
135
+ if (existsSync(join(output, "npm-facade")) && existsSync(join(output, "npm"))) {
136
+ return [singlePackage(join(output, "npm-facade")), singlePackage(join(output, "npm"))];
137
+ }
138
+ return [];
105
139
  }
106
140
  const directory = join(installDirectory, "local-dependencies", String(index));
107
141
  mkdirSync(directory, { recursive: true });
@@ -114,7 +148,16 @@ const testNodeFacade = ({ facadePackage, manifest, nativePackage, nodePackage })
114
148
  exports: "./index.js",
115
149
  })}\n`,
116
150
  );
117
- writeFileSync(join(directory, "index.js"), "export {};\n");
151
+ const imports = importedNames(facadeSource, name);
152
+ writeFileSync(
153
+ join(directory, "index.js"),
154
+ [
155
+ "const stub = new Proxy(function () {}, { get: () => stub, apply: () => stub });",
156
+ ...imports.names.map((name) => `export const ${name} = stub;`),
157
+ ...(imports.hasDefault ? ["export default stub;"] : []),
158
+ "",
159
+ ].join("\n"),
160
+ );
118
161
  return [directory];
119
162
  });
120
163
  writeFileSync(
@@ -144,6 +187,16 @@ const testNodeFacade = ({ facadePackage, manifest, nativePackage, nodePackage })
144
187
  const replaceVersion = (source, version) =>
145
188
  source.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
146
189
 
190
+ /** Resolve workspace and generated catalog protocols for a publishable facade. */
191
+ const facadeDependency = (name, dependency, version, catalog) => {
192
+ if (typeof dependency !== "string") return dependency;
193
+ if (dependency.startsWith("workspace:")) return version;
194
+ if (!dependency.startsWith("catalog:")) return dependency;
195
+ const resolved = catalog[name];
196
+ if (!resolved) throw new Error(`Missing root catalog entry for ${name}`);
197
+ return resolved;
198
+ };
199
+
147
200
  const writable = (path) => {
148
201
  if (existsSync(path)) chmodSync(path, statSync(path).mode | 0o200);
149
202
  };
@@ -156,7 +209,6 @@ const libraryPath = (crate, cargoTarget, os) => {
156
209
  };
157
210
 
158
211
  const packageNode = ({
159
- crate,
160
212
  library,
161
213
  output,
162
214
  nodeDirectory,
@@ -166,9 +218,11 @@ const packageNode = ({
166
218
  cpu,
167
219
  version,
168
220
  facade,
169
- nodeGenerator,
170
221
  }) => {
171
222
  const libraryFile = basename(library);
223
+ const sourceManifest = JSON.parse(
224
+ readFileSync(resolve(root, nodeDirectory, "package.json"), "utf8"),
225
+ );
172
226
  const nativePackage = resolve(output, "native-node");
173
227
  mkdirSync(nativePackage, { recursive: true });
174
228
  cpSync(library, join(nativePackage, libraryFile));
@@ -179,6 +233,7 @@ const packageNode = ({
179
233
  name: `${nodePackage}-${nodeTriple}`,
180
234
  version,
181
235
  description: `Native ${nodeTriple} library for ${nodePackage}`,
236
+ repository: sourceManifest.repository,
182
237
  license: "Apache-2.0",
183
238
  os: [os],
184
239
  cpu: [cpu],
@@ -193,12 +248,40 @@ const packageNode = ({
193
248
  run("npm", ["pack", "--pack-destination", resolve(output, "npm")], nativePackage);
194
249
 
195
250
  if (!facade) return;
251
+ packageNodeFacade({
252
+ output,
253
+ nodeDirectory,
254
+ nodePackage,
255
+ version,
256
+ nativePackage: singlePackage(resolve(output, "npm")),
257
+ });
258
+ };
259
+
260
+ const packageNodeFacade = ({ output, nodeDirectory, nodePackage, version, nativePackage }) => {
196
261
  const facadeDirectory = resolve(output, "facade-node");
262
+ rmSync(facadeDirectory, { recursive: true, force: true });
263
+ rmSync(resolve(output, "npm-facade"), { recursive: true, force: true });
264
+ mkdirSync(output, { recursive: true });
197
265
  cpSync(resolve(root, nodeDirectory), facadeDirectory, { recursive: true });
198
- rmSync(join(facadeDirectory, "src", libraryFile), { force: true });
266
+ const barrel = join(facadeDirectory, "index.ts");
267
+ writable(barrel);
268
+ writeFileSync(
269
+ barrel,
270
+ readFileSync(barrel, "utf8").replace(
271
+ /^export const PACKAGE_VERSION = .*;$/m,
272
+ `export const PACKAGE_VERSION = ${JSON.stringify(version)};`,
273
+ ),
274
+ );
275
+ for (const file of readdirSync(join(facadeDirectory, "src"))) {
276
+ if (/\.(dll|dylib|so)$/.test(file)) {
277
+ rmSync(join(facadeDirectory, "src", file), { force: true });
278
+ }
279
+ }
199
280
  const manifestPath = join(facadeDirectory, "package.json");
200
281
  writable(manifestPath);
201
282
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
283
+ const workspaceManifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
284
+ const catalog = workspaceManifest.catalog ?? {};
202
285
  manifest.version = version;
203
286
  manifest.private = false;
204
287
  manifest.license = manifest.license === "UNLICENSED" ? "Apache-2.0" : manifest.license;
@@ -208,28 +291,12 @@ const packageNode = ({
208
291
  manifest.dependencies = Object.fromEntries(
209
292
  Object.entries(manifest.dependencies ?? {}).map(([name, dependency]) => [
210
293
  name,
211
- typeof dependency === "string" && dependency.startsWith("workspace:") ? version : dependency,
294
+ facadeDependency(name, dependency, version, catalog),
212
295
  ]),
213
296
  );
214
297
  writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
215
- run("bun", [
216
- resolve(root, nodeGenerator),
217
- "--root",
218
- root,
219
- "--crate",
220
- crate,
221
- "--node",
222
- facadeDirectory,
223
- "--cargo-target",
224
- required("cargo-target"),
225
- "--node-package-base",
226
- `${nodePackage}-`,
227
- ...(parsed.values.ubrn ? ["--ubrn", parsed.values.ubrn, "--skip-barrels"] : []),
228
- "--skip-build",
229
- ]);
230
298
  // 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.
299
+ // must be emitted and advertised as JavaScript.
233
300
  rmSync(join(facadeDirectory, "lib"), { recursive: true, force: true });
234
301
  run(
235
302
  "bun",
@@ -259,12 +326,15 @@ const packageNode = ({
259
326
  const facadeOutput = resolve(output, "npm-facade");
260
327
  mkdirSync(facadeOutput, { recursive: true });
261
328
  run("npm", ["pack", "--pack-destination", facadeOutput], facadeDirectory);
262
- testNodeFacade({
263
- facadePackage: singlePackage(facadeOutput),
264
- manifest,
265
- nativePackage: singlePackage(resolve(output, "npm")),
266
- nodePackage,
267
- });
329
+ if (nativePackage) {
330
+ testNodeFacade({
331
+ facadeDirectory,
332
+ facadePackage: singlePackage(facadeOutput),
333
+ manifest,
334
+ nativePackage,
335
+ nodePackage,
336
+ });
337
+ }
268
338
  };
269
339
 
270
340
  const packagePython = ({
@@ -372,7 +442,6 @@ const build = () => {
372
442
  const nodePackage = parsed.values["node-package"];
373
443
  if (nodeDirectory && nodePackage) {
374
444
  packageNode({
375
- crate,
376
445
  library,
377
446
  output,
378
447
  nodeDirectory,
@@ -382,7 +451,6 @@ const build = () => {
382
451
  cpu,
383
452
  version,
384
453
  facade: parsed.values.facade === "true",
385
- nodeGenerator: required("node-generator"),
386
454
  });
387
455
  }
388
456
 
@@ -401,5 +469,18 @@ const build = () => {
401
469
  }
402
470
  };
403
471
 
404
- if (parsed.positionals[0] !== "build") throw new Error("Expected build command");
405
- build();
472
+ if (parsed.positionals[0] === "build") {
473
+ build();
474
+ } else if (parsed.positionals[0] === "facade") {
475
+ packageNodeFacade({
476
+ output: resolve(root, required("output")),
477
+ nodeDirectory: required("node"),
478
+ nodePackage: required("node-package"),
479
+ version: required("version"),
480
+ nativePackage: parsed.values["native-package"]
481
+ ? resolve(root, parsed.values["native-package"])
482
+ : undefined,
483
+ });
484
+ } else {
485
+ throw new Error("Expected build or facade command");
486
+ }
package/tasks/uniffi.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  addExplicitInterfaceReexports,
22
22
  addTypeScriptExtensionsToBindingImports,
23
23
  makeDefaultedInterfaceParametersOptional,
24
+ mergePythonBindingExports,
24
25
  } from "../src/uniffi.ts";
25
26
 
26
27
  const { values } = parseArgs({
@@ -176,6 +177,14 @@ if (values.node) {
176
177
  );
177
178
  const nodeBindings = join(nodeSource, "bindings.ts");
178
179
  const generatedFiles = [join(nodeSource, "_bindings.ts"), join(nodeSource, "_bindings-ffi.ts")];
180
+ const legacyExports = resolve(root, values.node, "exports.ts");
181
+ if (
182
+ existsSync(legacyExports) &&
183
+ readFileSync(legacyExports, "utf8").trim() === 'export * from "./src/bindings.ts";'
184
+ ) {
185
+ makeWritable(legacyExports);
186
+ rmSync(legacyExports, { force: true });
187
+ }
179
188
  replaceGenerated(join(nodeOutput, "index.ts"), nodeBindings);
180
189
  replaceGenerated(join(nodeOutput, libraryName + ".ts"), generatedFiles[0]);
181
190
  replaceGenerated(join(nodeOutput, libraryName + "-ffi.ts"), generatedFiles[1]);
@@ -259,9 +268,16 @@ if (values.python) {
259
268
  replaceGenerated(generated, pythonBindings);
260
269
  stampGeneratedPython(pythonBindings);
261
270
  const pythonInit = join(pythonPackage, "__init__.py");
262
- if (!existsSync(pythonInit)) {
263
- writeFileSync(pythonInit, "");
264
- }
271
+ makeWritable(pythonInit);
272
+ writeFileSync(
273
+ pythonInit,
274
+ mergePythonBindingExports(
275
+ existsSync(pythonInit) ? readFileSync(pythonInit, "utf8") : "",
276
+ readFileSync(pythonBindings, "utf8"),
277
+ { crate, file: pythonInit },
278
+ ),
279
+ );
280
+ makeReadonly(pythonInit);
265
281
  const pythonLibrary = join(pythonPackage, basename(library));
266
282
  makeWritable(pythonLibrary);
267
283
  cpSync(library, pythonLibrary);