@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/README.md +35 -45
- package/index.ts +1 -0
- package/package.json +4 -5
- package/src/barrels.ts +134 -34
- package/src/project-py.ts +73 -14
- package/src/project-rs.ts +36 -290
- package/src/release.ts +109 -4
- package/src/uniffi.ts +37 -0
- package/tasks/bump.ts +12 -2
- package/tasks/publish-uniffi-local.ts +3 -4
- package/tasks/publish.ts +22 -13
- package/tasks/uniffi-release.mjs +117 -36
- package/tasks/uniffi.ts +19 -3
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
|
-
*
|
|
139
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
278
|
-
//
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
);
|
package/tasks/uniffi-release.mjs
CHANGED
|
@@ -25,8 +25,7 @@ const parsed = parseArgs({
|
|
|
25
25
|
node: { type: "string" },
|
|
26
26
|
python: { type: "string" },
|
|
27
27
|
"node-package": { type: "string" },
|
|
28
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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]
|
|
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
|
-
|
|
263
|
-
|
|
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);
|