@dbx-tools/projen 0.6.161 → 0.6.168
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -4
- package/index.ts +4 -1
- package/package.json +4 -4
- package/src/bun-workflow.ts +145 -0
- package/src/project-js.ts +40 -22
- package/src/project-py.ts +10 -7
- package/src/project-rs.ts +167 -46
- package/src/release.ts +6 -4
- package/src/uniffi.ts +8 -0
- package/tasks/rust.ts +17 -3
- package/tasks/uniffi-release.mjs +139 -9
- package/tasks/uniffi.ts +83 -53
package/tasks/uniffi-release.mjs
CHANGED
|
@@ -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),
|
|
@@ -150,13 +224,47 @@ const packageNode = ({
|
|
|
150
224
|
required("cargo-target"),
|
|
151
225
|
"--node-package-base",
|
|
152
226
|
`${nodePackage}-`,
|
|
153
|
-
"--ubrn",
|
|
154
|
-
required("ubrn"),
|
|
227
|
+
...(parsed.values.ubrn ? ["--ubrn", parsed.values.ubrn, "--skip-barrels"] : []),
|
|
155
228
|
"--skip-build",
|
|
156
|
-
"--skip-barrels",
|
|
157
229
|
]);
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
});
|
|
160
268
|
};
|
|
161
269
|
|
|
162
270
|
const packagePython = ({
|
|
@@ -174,6 +282,19 @@ const packagePython = ({
|
|
|
174
282
|
const pyproject = join(pythonRoot, "pyproject.toml");
|
|
175
283
|
writable(pyproject);
|
|
176
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);
|
|
177
298
|
|
|
178
299
|
const packageName = crate.replace(/^dbx-tools-/, "").replaceAll("-", "_");
|
|
179
300
|
const packageDirectory = resolve(pythonRoot, "src", "dbx_tools", packageName);
|
|
@@ -183,10 +304,19 @@ const packagePython = ({
|
|
|
183
304
|
"target",
|
|
184
305
|
cargoTarget,
|
|
185
306
|
"release",
|
|
186
|
-
|
|
307
|
+
`${crate}-uniffi-bindgen${os === "win32" ? ".exe" : ""}`,
|
|
187
308
|
);
|
|
188
309
|
if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
|
|
189
|
-
run(generator, [
|
|
310
|
+
run(generator, [
|
|
311
|
+
"generate",
|
|
312
|
+
"--language",
|
|
313
|
+
"python",
|
|
314
|
+
"--crate",
|
|
315
|
+
crate.replaceAll("-", "_"),
|
|
316
|
+
"--out-dir",
|
|
317
|
+
generatedDirectory,
|
|
318
|
+
library,
|
|
319
|
+
]);
|
|
190
320
|
const bindings = join(packageDirectory, "bindings.py");
|
|
191
321
|
writable(bindings);
|
|
192
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
210
|
-
"
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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);
|