@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.
@@ -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
- mkdirSync(resolve(output, "npm-facade"), { recursive: true });
159
- 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
+ });
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
- `uniffi-bindgen${os === "win32" ? ".exe" : ""}`,
307
+ `${crate}-uniffi-bindgen${os === "win32" ? ".exe" : ""}`,
187
308
  );
188
309
  if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
189
- 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
+ ]);
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 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);