convex-rust 0.0.0-stage → 0.1.0

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.
Files changed (90) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +137 -2
  3. package/dist/cli/bin.d.ts +2 -0
  4. package/dist/cli/bin.js +200 -0
  5. package/dist/cli/build.d.ts +146 -0
  6. package/dist/cli/build.js +902 -0
  7. package/dist/cli/demangle.d.ts +7 -0
  8. package/dist/cli/demangle.js +328 -0
  9. package/dist/cli/diagnostics.d.ts +34 -0
  10. package/dist/cli/diagnostics.js +296 -0
  11. package/dist/cli/generate.d.ts +29 -0
  12. package/dist/cli/generate.js +82 -0
  13. package/dist/cli/manifest.d.ts +24 -0
  14. package/dist/cli/manifest.js +75 -0
  15. package/dist/cli/rustApi.d.ts +78 -0
  16. package/dist/cli/rustApi.js +609 -0
  17. package/dist/cli/scaffold.d.ts +22 -0
  18. package/dist/cli/scaffold.js +114 -0
  19. package/dist/cli/shim.d.ts +30 -0
  20. package/dist/cli/shim.js +140 -0
  21. package/dist/cli/size.d.ts +17 -0
  22. package/dist/cli/size.js +73 -0
  23. package/dist/cli/split.d.ts +20 -0
  24. package/dist/cli/split.js +140 -0
  25. package/dist/cli/symbols.d.ts +30 -0
  26. package/dist/cli/symbols.js +133 -0
  27. package/dist/cli/toolchain.d.ts +53 -0
  28. package/dist/cli/toolchain.js +167 -0
  29. package/dist/cli/validators.d.ts +6 -0
  30. package/dist/cli/validators.js +66 -0
  31. package/dist/cli/wasisdk.d.ts +82 -0
  32. package/dist/cli/wasisdk.js +220 -0
  33. package/dist/cli/wasmHints.d.ts +29 -0
  34. package/dist/cli/wasmHints.js +172 -0
  35. package/dist/runtime/base64.d.ts +4 -0
  36. package/dist/runtime/base64.js +77 -0
  37. package/dist/runtime/crypto.d.ts +7 -0
  38. package/dist/runtime/crypto.js +421 -0
  39. package/dist/runtime/fastjson.d.ts +3 -0
  40. package/dist/runtime/fastjson.js +170 -0
  41. package/dist/runtime/host.d.ts +73 -0
  42. package/dist/runtime/host.js +1230 -0
  43. package/dist/runtime/http.d.ts +40 -0
  44. package/dist/runtime/http.js +157 -0
  45. package/dist/runtime/index.d.ts +2 -0
  46. package/dist/runtime/index.js +1 -0
  47. package/dist/runtime/intl.d.ts +5 -0
  48. package/dist/runtime/intl.js +69 -0
  49. package/dist/runtime/tmpfs.d.ts +129 -0
  50. package/dist/runtime/tmpfs.js +624 -0
  51. package/dist/runtime/wasi.d.ts +33 -0
  52. package/dist/runtime/wasi.js +216 -0
  53. package/package.json +57 -4
  54. package/rust/convex_fn/Cargo.toml +32 -0
  55. package/rust/convex_fn/LICENSE +201 -0
  56. package/rust/convex_fn/build.rs +3 -0
  57. package/rust/convex_fn/src/args.rs +416 -0
  58. package/rust/convex_fn/src/crypto.rs +237 -0
  59. package/rust/convex_fn/src/ctx.rs +1044 -0
  60. package/rust/convex_fn/src/dns.rs +356 -0
  61. package/rust/convex_fn/src/error.rs +179 -0
  62. package/rust/convex_fn/src/executor.rs +577 -0
  63. package/rust/convex_fn/src/fanout.rs +106 -0
  64. package/rust/convex_fn/src/fs.rs +19 -0
  65. package/rust/convex_fn/src/function_ref.rs +219 -0
  66. package/rust/convex_fn/src/http.rs +441 -0
  67. package/rust/convex_fn/src/intl.rs +75 -0
  68. package/rust/convex_fn/src/lib.rs +242 -0
  69. package/rust/convex_fn/src/logging.rs +222 -0
  70. package/rust/convex_fn/src/native.rs +360 -0
  71. package/rust/convex_fn/src/net.rs +122 -0
  72. package/rust/convex_fn/src/sys.rs +178 -0
  73. package/rust/convex_fn/src/values.rs +121 -0
  74. package/rust/convex_fn/src/wire.rs +579 -0
  75. package/rust/convex_fn_macros/Cargo.toml +13 -0
  76. package/rust/convex_fn_macros/LICENSE +201 -0
  77. package/rust/convex_fn_macros/src/derive.rs +632 -0
  78. package/rust/convex_fn_macros/src/lib.rs +338 -0
  79. package/rust/convex_reqwest/Cargo.toml +69 -0
  80. package/rust/convex_reqwest/LICENSE +201 -0
  81. package/rust/convex_reqwest/src/blocking.rs +287 -0
  82. package/rust/convex_reqwest/src/body.rs +152 -0
  83. package/rust/convex_reqwest/src/client.rs +359 -0
  84. package/rust/convex_reqwest/src/config_types.rs +150 -0
  85. package/rust/convex_reqwest/src/error.rs +241 -0
  86. package/rust/convex_reqwest/src/into_url.rs +57 -0
  87. package/rust/convex_reqwest/src/lib.rs +42 -0
  88. package/rust/convex_reqwest/src/multipart.rs +140 -0
  89. package/rust/convex_reqwest/src/request.rs +358 -0
  90. package/rust/convex_reqwest/src/response.rs +310 -0
@@ -0,0 +1,609 @@
1
+ // Typed `src/_generated/api.rs`: Rust references to Convex functions for ctx.run_query & co.
2
+ // Port of the fork CLI's codegen_templates/rust_api.ts. Instead of the fork's bundler, the TS
3
+ // functions are analysed locally with esbuild + a short-lived Node subprocess (no backend).
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { createRequire } from "node:module";
8
+ import { pathToFileURL } from "node:url";
9
+ import { convexModuleName } from "./manifest.js";
10
+ const compareStrings = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
11
+ export const RUST_API_HEADER = `// @generated by \`convex-rust\`. Do not edit; changes are overwritten.
12
+ //
13
+ // Typed references to your Convex functions for \`ctx.run_query\`,
14
+ // \`ctx.run_mutation\` and \`ctx.run_action\`. Include it from \`lib.rs\` with
15
+ //
16
+ // #[path = "_generated/api.rs"]
17
+ // pub mod api;
18
+ #![allow(
19
+ non_upper_case_globals,
20
+ non_snake_case,
21
+ non_camel_case_types,
22
+ dead_code,
23
+ unused_imports,
24
+ clippy::all
25
+ )]
26
+ `;
27
+ /** Placeholder `api.rs` until the first codegen fills it in. */
28
+ export const RUST_API_STUB = `${RUST_API_HEADER}
29
+ // No functions yet: this file is filled in by the next \`convex-rust build\` or \`convex-rust codegen\`.
30
+ `;
31
+ const RUST_KEYWORDS = new Set([
32
+ "as",
33
+ "break",
34
+ "const",
35
+ "continue",
36
+ "else",
37
+ "enum",
38
+ "extern",
39
+ "false",
40
+ "fn",
41
+ "for",
42
+ "if",
43
+ "impl",
44
+ "in",
45
+ "let",
46
+ "loop",
47
+ "match",
48
+ "mod",
49
+ "move",
50
+ "mut",
51
+ "pub",
52
+ "ref",
53
+ "return",
54
+ "static",
55
+ "struct",
56
+ "trait",
57
+ "true",
58
+ "type",
59
+ "unsafe",
60
+ "use",
61
+ "where",
62
+ "while",
63
+ "async",
64
+ "await",
65
+ "dyn",
66
+ "abstract",
67
+ "become",
68
+ "box",
69
+ "do",
70
+ "final",
71
+ "macro",
72
+ "override",
73
+ "priv",
74
+ "typeof",
75
+ "unsized",
76
+ "virtual",
77
+ "yield",
78
+ "try",
79
+ "gen",
80
+ ]);
81
+ const NON_RAW_KEYWORDS = new Set(["self", "Self", "super", "crate", "_"]);
82
+ /** A Rust identifier for `name` (a Convex module, function or field name). */
83
+ export function rustIdent(name) {
84
+ let id = name.replace(/[^A-Za-z0-9_]/g, "_");
85
+ if (id === "" || /^[0-9]/.test(id))
86
+ id = `_${id}`;
87
+ if (NON_RAW_KEYWORDS.has(id))
88
+ return `${id}_`;
89
+ if (RUST_KEYWORDS.has(id))
90
+ return `r#${id}`;
91
+ return id;
92
+ }
93
+ /** `list_messages` / `listMessages` -> `ListMessages`. */
94
+ export function pascalCase(name) {
95
+ const parts = name.split(/[^A-Za-z0-9]+/).filter((p) => p !== "");
96
+ const out = parts.map((p) => p[0].toUpperCase() + p.slice(1)).join("");
97
+ return /^[0-9]/.test(out) || out === "" ? `T${out}` : out;
98
+ }
99
+ function parseValidatorJson(json) {
100
+ if (!json)
101
+ return null;
102
+ return JSON.parse(json);
103
+ }
104
+ function derive() {
105
+ return `#[derive(Clone, Debug, Serialize, Deserialize)]\n#[serde(crate = "convex_fn::serde")]`;
106
+ }
107
+ /**
108
+ * Rust type for a validator. Objects become structs named after `hint`
109
+ * (pushed to `gen.structs`); anything the Rust types can't express exactly
110
+ * (general unions, mixed literals) becomes `serde_json::Value`.
111
+ */
112
+ export function rustType(v, hint, gen) {
113
+ switch (v.type) {
114
+ case "null":
115
+ return "()";
116
+ case "number":
117
+ return "f64";
118
+ case "bigint":
119
+ return "Int64";
120
+ case "boolean":
121
+ return "bool";
122
+ case "string":
123
+ case "id":
124
+ return "String";
125
+ case "bytes":
126
+ return "Bytes";
127
+ case "commitTs":
128
+ return "Int64";
129
+ case "any":
130
+ return "Value";
131
+ case "literal": {
132
+ const value = v.value;
133
+ if (typeof value === "string")
134
+ return "String";
135
+ if (typeof value === "boolean")
136
+ return "bool";
137
+ if (typeof value === "number")
138
+ return "f64";
139
+ return "Value";
140
+ }
141
+ case "array":
142
+ return `Vec<${rustType(v.value, `${hint}Item`, gen)}>`;
143
+ case "record":
144
+ return `BTreeMap<String, ${rustType(v.values.fieldType, `${hint}Value`, gen)}>`;
145
+ case "union": {
146
+ const members = v.value;
147
+ const nonNull = members.filter((m) => m.type !== "null");
148
+ if (members.length === 2 && nonNull.length === 1) {
149
+ return `Option<${rustType(nonNull[0], hint, gen)}>`;
150
+ }
151
+ if (members.every((m) => m.type === "literal" && typeof m.value === "string")) {
152
+ return "String";
153
+ }
154
+ return "Value";
155
+ }
156
+ case "object": {
157
+ gen.structs.push(structDef(v, hint, gen));
158
+ return hint;
159
+ }
160
+ default:
161
+ return "Value";
162
+ }
163
+ }
164
+ function structDef(v, name, gen) {
165
+ const lines = [derive(), `pub struct ${name} {`];
166
+ const fields = Object.entries(v.value).sort(([a], [b]) => compareStrings(a, b));
167
+ for (const [fieldName, field] of fields) {
168
+ const ident = rustIdent(fieldName);
169
+ const bare = ident.replace(/^r#/, "");
170
+ const ty = rustType(field.fieldType, `${name}${pascalCase(fieldName)}`, gen);
171
+ if (field.fieldType.type === "id") {
172
+ lines.push(` /// \`Id<"${field.fieldType.tableName}">\``);
173
+ }
174
+ const attrs = [];
175
+ if (bare !== fieldName)
176
+ attrs.push(`rename = ${JSON.stringify(fieldName)}`);
177
+ if (field.optional) {
178
+ attrs.push("default", 'skip_serializing_if = "Option::is_none"');
179
+ }
180
+ if (attrs.length > 0)
181
+ lines.push(` #[serde(${attrs.join(", ")})]`);
182
+ lines.push(` pub ${ident}: ${field.optional && !ty.startsWith("Option<") ? `Option<${ty}>` : ty},`);
183
+ }
184
+ lines.push("}");
185
+ return lines.join("\n");
186
+ }
187
+ function newNode() {
188
+ return { fns: [], structs: [], kids: new Map() };
189
+ }
190
+ function descend(root, modulePath) {
191
+ let node = root;
192
+ for (const part of modulePath.split("/")) {
193
+ const key = rustIdent(part);
194
+ if (!node.kids.has(key))
195
+ node.kids.set(key, newNode());
196
+ node = node.kids.get(key);
197
+ }
198
+ return node;
199
+ }
200
+ function renderFunction(node, fnPath, name, argsJson, returnsJson) {
201
+ const gen = { structs: [] };
202
+ const base = pascalCase(name);
203
+ let argsType = "Value";
204
+ try {
205
+ const args = parseValidatorJson(argsJson);
206
+ if (args && args.type === "object") {
207
+ argsType = rustType(args, `${base}Args`, gen);
208
+ }
209
+ }
210
+ catch {
211
+ // Keep `Value`.
212
+ }
213
+ let returnsType = "Value";
214
+ try {
215
+ const returns = parseValidatorJson(returnsJson);
216
+ if (returns)
217
+ returnsType = rustType(returns, `${base}Returns`, gen);
218
+ }
219
+ catch {
220
+ // Keep `Value`.
221
+ }
222
+ node.structs.push(...gen.structs);
223
+ node.fns.push(`pub const ${rustIdent(name)}: FunctionRef<${argsType}, ${returnsType}> =\n FunctionRef::new(${JSON.stringify(fnPath)});`);
224
+ }
225
+ function indent(text, depth) {
226
+ const pad = " ".repeat(depth);
227
+ return text
228
+ .split("\n")
229
+ .map((l) => (l === "" ? l : pad + l))
230
+ .join("\n");
231
+ }
232
+ function renderNode(node, depth) {
233
+ const parts = [];
234
+ parts.push(indent("use convex_fn::serde::{Deserialize, Serialize};\nuse convex_fn::{Bytes, FunctionRef, Int64};\nuse serde_json::Value;\nuse std::collections::BTreeMap;", depth));
235
+ for (const s of node.structs)
236
+ parts.push(indent(s, depth));
237
+ for (const f of node.fns)
238
+ parts.push(indent(f, depth));
239
+ for (const [name, kid] of [...node.kids.entries()].sort(([a], [b]) => compareStrings(a, b))) {
240
+ parts.push(`${indent(`pub mod ${name} {`, depth)}\n${renderNode(kid, depth + 1)}\n${indent("}", depth)}`);
241
+ }
242
+ return parts.join("\n\n");
243
+ }
244
+ /** Contents of `api.rs` for the analyzed modules (`messages.js`, ...). */
245
+ export function rustApiCodegen(modules) {
246
+ const pub = newNode();
247
+ const internal = newNode();
248
+ const paths = Object.keys(modules).sort(compareStrings);
249
+ for (const modulePath of paths) {
250
+ const moduleName = modulePath.replace(/\.js$/, "");
251
+ for (const f of modules[modulePath].functions) {
252
+ const visibility = f.visibility?.kind;
253
+ if (visibility !== "public" && visibility !== "internal")
254
+ continue;
255
+ const target = visibility === "public" ? pub : internal;
256
+ renderFunction(descend(target, moduleName), `${moduleName}:${f.name}`, f.name, f.args, f.returns);
257
+ }
258
+ }
259
+ const out = [RUST_API_HEADER, renderNode(pub, 0)];
260
+ if (internal.kids.size > 0) {
261
+ if (pub.kids.has("internal")) {
262
+ out.push("// `internal` functions are omitted: a module is named `internal`.");
263
+ }
264
+ else {
265
+ out.push(`/// \`internal\` functions (\`internalQuery\`, \`internalMutation\`, \`internalAction\`).\npub mod internal {\n${renderNode(internal, 1)}\n}`);
266
+ }
267
+ }
268
+ return out.join("\n") + "\n";
269
+ }
270
+ // ---------------------------------------------------------------------------------------------
271
+ // Local analysis of the project's TypeScript functions
272
+ /** Runs in the subprocess: prints one JSON line `{ [module]: fn[] }`. */
273
+ export const ANALYZE_SCRIPT = `
274
+ import { pathToFileURL } from "node:url";
275
+ const [dir, ...mods] = process.argv.slice(1);
276
+ const out = {};
277
+ for (const m of mods) {
278
+ let ns;
279
+ try {
280
+ ns = await import(pathToFileURL(dir + "/" + m).href);
281
+ } catch (e) {
282
+ process.stderr.write("convex-rust: could not load " + m + ": " + (e && e.message || e) + "\\n");
283
+ continue;
284
+ }
285
+ const fns = [];
286
+ for (const [name, f] of Object.entries(ns)) {
287
+ if (typeof f !== "function") continue;
288
+ const udfType = f.isQuery ? "Query" : f.isMutation ? "Mutation" : f.isAction ? "Action" : null;
289
+ if (udfType === null) continue;
290
+ const kind = f.isPublic ? "public" : f.isInternal ? "internal" : null;
291
+ if (kind === null) continue;
292
+ let args = null, returns = null;
293
+ try { args = f.exportArgs(); returns = f.exportReturns(); } catch {}
294
+ fns.push({ name, udfType, visibility: { kind }, args, returns });
295
+ }
296
+ out[m] = fns;
297
+ }
298
+ process.stdout.write("\\n@@convex-analysis@@" + JSON.stringify(out) + "\\n");
299
+ `;
300
+ export function parseAnalysisOutput(stdout) {
301
+ const marker = "@@convex-analysis@@";
302
+ const i = stdout.lastIndexOf(marker);
303
+ if (i < 0)
304
+ return null;
305
+ try {
306
+ const raw = JSON.parse(stdout.slice(i + marker.length).split("\n")[0]);
307
+ const result = {};
308
+ for (const [mod, functions] of Object.entries(raw))
309
+ result[mod] = { functions };
310
+ return result;
311
+ }
312
+ catch {
313
+ return null;
314
+ }
315
+ }
316
+ const ENTRY_EXTS = [".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx"];
317
+ const SKIP_DIRS = new Set(["node_modules", "_generated", "target", "src", "_wasm", ".git"]);
318
+ /**
319
+ * TS/JS entry points of the functions directory, following the stock bundler's rules
320
+ * (skips `_generated`, dotfiles, `schema.*`, files with several dots, component directories).
321
+ * `skipDirs` are absolute directories to leave out (e.g. the Rust output directory).
322
+ */
323
+ export function findEntryPoints(functionsDir, skipDirs = []) {
324
+ const out = [];
325
+ const skip = new Set(skipDirs.map((d) => path.resolve(d)));
326
+ const visit = (dir) => {
327
+ let entries;
328
+ try {
329
+ entries = fs.readdirSync(dir, { withFileTypes: true });
330
+ }
331
+ catch {
332
+ return;
333
+ }
334
+ entries.sort((a, b) => compareStrings(a.name, b.name));
335
+ for (const e of entries) {
336
+ const p = path.join(dir, e.name);
337
+ if (e.isDirectory()) {
338
+ if (SKIP_DIRS.has(e.name) || e.name.startsWith(".") || skip.has(p))
339
+ continue;
340
+ if (fs.existsSync(path.join(p, "convex.config.ts")))
341
+ continue;
342
+ visit(p);
343
+ }
344
+ else if (e.isFile()) {
345
+ const base = e.name;
346
+ if (!ENTRY_EXTS.some((x) => base.toLowerCase().endsWith(x)))
347
+ continue;
348
+ if (base.endsWith(".d.ts") || base.startsWith(".") || base.startsWith("#"))
349
+ continue;
350
+ if (base === "schema.ts" || base === "schema.js")
351
+ continue;
352
+ if ((base.match(/\./g) || []).length > 1)
353
+ continue;
354
+ if (path.relative(functionsDir, p).includes(" "))
355
+ continue;
356
+ out.push(p);
357
+ }
358
+ }
359
+ };
360
+ visit(functionsDir);
361
+ return out;
362
+ }
363
+ function runNode(script, args, cwd, timeoutMs) {
364
+ return new Promise((resolve) => {
365
+ const child = spawn(process.execPath, ["--input-type=module", "-e", script, "--", ...args], {
366
+ cwd,
367
+ stdio: ["ignore", "pipe", "pipe"],
368
+ env: process.env,
369
+ });
370
+ let stdout = "";
371
+ let stderr = "";
372
+ child.stdout.setEncoding("utf8");
373
+ child.stdout.on("data", (d) => (stdout += d));
374
+ child.stderr.setEncoding("utf8");
375
+ child.stderr.on("data", (d) => (stderr += d));
376
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
377
+ child.on("error", () => {
378
+ clearTimeout(timer);
379
+ resolve({ stdout, stderr, ok: false });
380
+ });
381
+ child.on("close", (code) => {
382
+ clearTimeout(timer);
383
+ resolve({ stdout, stderr, ok: code === 0 });
384
+ });
385
+ });
386
+ }
387
+ async function loadEsbuild(projectRoot) {
388
+ // Prefer the esbuild that ships with the project's `convex` package.
389
+ try {
390
+ const req = createRequire(path.join(projectRoot, "package.json"));
391
+ const convexDir = path.dirname(req.resolve("convex/package.json"));
392
+ const esb = createRequire(path.join(convexDir, "package.json")).resolve("esbuild");
393
+ return (await import(pathToFileURL(esb).href)).default ?? (await import(pathToFileURL(esb).href));
394
+ }
395
+ catch {
396
+ // fall through
397
+ }
398
+ try {
399
+ return (await import("esbuild"));
400
+ }
401
+ catch {
402
+ return null;
403
+ }
404
+ }
405
+ /**
406
+ * Analyzes the TS functions under `functionsDir` without a backend: bundles each module with
407
+ * esbuild (npm packages external, so `convex/server` is the project's), imports the bundles in a
408
+ * Node subprocess and reads each registered function's visibility and validators. Returns null
409
+ * when analysis is not possible (callers keep the existing api.rs).
410
+ */
411
+ export async function analyzeTsFunctions(projectRoot, functionsDir, skipDirs = [], timeoutMs = 60_000, log = () => { }) {
412
+ const entries = findEntryPoints(functionsDir, skipDirs);
413
+ if (entries.length === 0)
414
+ return { modules: {}, warnings: [] };
415
+ const esbuild = await loadEsbuild(projectRoot);
416
+ if (!esbuild)
417
+ return null;
418
+ // Inside the project so that external imports (`convex/server`) resolve from its node_modules.
419
+ const base = path.join(projectRoot, ".convex-rust");
420
+ fs.mkdirSync(base, { recursive: true });
421
+ const tmp = fs.mkdtempSync(path.join(base, "analysis-"));
422
+ try {
423
+ fs.writeFileSync(path.join(tmp, "package.json"), '{"type":"module"}\n');
424
+ const names = entries.map((p) => path.relative(functionsDir, p).split(path.sep).join("/").replace(/\.[^./]+$/, ""));
425
+ await esbuild.build({
426
+ entryPoints: Object.fromEntries(entries.map((p, i) => [names[i], p])),
427
+ outdir: tmp,
428
+ outExtension: { ".js": ".mjs" },
429
+ bundle: true,
430
+ format: "esm",
431
+ platform: "node",
432
+ packages: "external",
433
+ logLevel: "silent",
434
+ write: true,
435
+ loader: { ".wasm": "empty" },
436
+ absWorkingDir: projectRoot,
437
+ // Per-module failures should not sink the whole run.
438
+ metafile: false,
439
+ }).catch(async (e) => {
440
+ // Retry module by module so one broken file does not hide the rest.
441
+ const failed = [];
442
+ for (let i = 0; i < entries.length; i++) {
443
+ try {
444
+ await esbuild.build({
445
+ entryPoints: { [names[i]]: entries[i] },
446
+ outdir: tmp,
447
+ outExtension: { ".js": ".mjs" },
448
+ bundle: true,
449
+ format: "esm",
450
+ platform: "node",
451
+ packages: "external",
452
+ logLevel: "silent",
453
+ loader: { ".wasm": "empty" },
454
+ absWorkingDir: projectRoot,
455
+ });
456
+ }
457
+ catch {
458
+ failed.push(names[i]);
459
+ }
460
+ }
461
+ if (failed.length === entries.length)
462
+ throw e;
463
+ log(`warning: could not bundle ${failed.join(", ")}: ${String(e?.errors?.[0]?.text ?? e?.message ?? e)}`);
464
+ });
465
+ const present = names.filter((n) => fs.existsSync(path.join(tmp, `${n}.mjs`)));
466
+ const { stdout, stderr, ok } = await runNode(ANALYZE_SCRIPT, [tmp, ...present.map((n) => `${n}.mjs`)], projectRoot, timeoutMs);
467
+ if (!ok) {
468
+ log(`warning: the analysis process failed: ${stderr.trim().split("\n").slice(-3).join(" | ")}`);
469
+ return null;
470
+ }
471
+ const raw = parseAnalysisOutput(stdout);
472
+ if (raw === null)
473
+ return null;
474
+ const modules = {};
475
+ for (const [k, v] of Object.entries(raw))
476
+ modules[k.replace(/\.mjs$/, ".js")] = v;
477
+ const warnings = stderr
478
+ .split("\n")
479
+ .filter((l) => l.startsWith("convex-rust: could not load"))
480
+ .map((l) => l.replace(/^convex-rust: /, ""));
481
+ for (const n of names)
482
+ if (!present.includes(n))
483
+ warnings.push(`could not bundle ${n}`);
484
+ return { modules, warnings };
485
+ }
486
+ catch (e) {
487
+ const first = e?.errors?.[0];
488
+ const why = first ? `${first.text}${first.location ? ` (${first.location.file}:${first.location.line})` : ""}` : String(e?.message ?? e);
489
+ log(`warning: could not bundle the TypeScript functions: ${why}` +
490
+ (/_generated/.test(why) ? " (run `npx convex dev` or `npx convex codegen` once so convex/_generated exists)" : ""));
491
+ return null;
492
+ }
493
+ finally {
494
+ fs.rmSync(tmp, { recursive: true, force: true });
495
+ }
496
+ }
497
+ /** Rust functions (from the wasm manifest) as analysed modules under `<outRel>/<module>.js`. */
498
+ export function rustEntriesAsModules(entries, outRel) {
499
+ const mods = {};
500
+ for (const e of entries) {
501
+ // HTTP actions are routed in http.ts, not called through `api`.
502
+ if (e.kind === "http")
503
+ continue;
504
+ const key = `${outRel ? outRel + "/" : ""}${convexModuleName(e.modulePath)}.js`;
505
+ (mods[key] ??= { functions: [] }).functions.push({
506
+ name: e.name,
507
+ udfType: "Action",
508
+ visibility: { kind: e.visibility },
509
+ args: e.args == null ? null : JSON.stringify(e.args),
510
+ returns: e.returns == null ? null : JSON.stringify(e.returns),
511
+ });
512
+ }
513
+ return mods;
514
+ }
515
+ // ---------------------------------------------------------------------------------------------
516
+ // Orchestration used by build / codegen
517
+ const RELATIVE_API = path.join("src", "_generated", "api.rs");
518
+ export function apiRsPath(crateDir) {
519
+ return path.join(crateDir, RELATIVE_API);
520
+ }
521
+ /** Function paths (`messages:list`) present in an `api.rs`. */
522
+ export function functionPathsInApiRs(contents) {
523
+ const out = new Set();
524
+ for (const m of contents.matchAll(/FunctionRef::new\("([^"]+)"\)/g))
525
+ out.add(m[1]);
526
+ return out;
527
+ }
528
+ export function rustSourcesReferenceApi(crateDir) {
529
+ const visit = (dir) => {
530
+ let entries;
531
+ try {
532
+ entries = fs.readdirSync(dir, { withFileTypes: true });
533
+ }
534
+ catch {
535
+ return false;
536
+ }
537
+ for (const e of entries) {
538
+ const p = path.join(dir, e.name);
539
+ if (e.isDirectory()) {
540
+ if (e.name !== "_generated" && visit(p))
541
+ return true;
542
+ }
543
+ else if (e.name.endsWith(".rs")) {
544
+ try {
545
+ if (/\bapi\s*::/.test(fs.readFileSync(p, "utf8")))
546
+ return true;
547
+ }
548
+ catch {
549
+ // ignore
550
+ }
551
+ }
552
+ }
553
+ return false;
554
+ };
555
+ return visit(path.join(crateDir, "src"));
556
+ }
557
+ function writeIfChanged(file, text) {
558
+ try {
559
+ if (fs.readFileSync(file, "utf8") === text)
560
+ return false;
561
+ }
562
+ catch {
563
+ // new
564
+ }
565
+ fs.mkdirSync(path.dirname(file), { recursive: true });
566
+ fs.writeFileSync(file, text);
567
+ return true;
568
+ }
569
+ /**
570
+ * Regenerates `<crate>/src/_generated/api.rs` from the TS functions plus the Rust ones. Only
571
+ * runs when the file exists or the Rust sources use `api::`. A failed analysis leaves the file
572
+ * alone. Returns true when the file changed.
573
+ */
574
+ export async function updateApiRs(o) {
575
+ const file = apiRsPath(o.crateDir);
576
+ const exists = fs.existsSync(file);
577
+ if (!exists && !rustSourcesReferenceApi(o.crateDir))
578
+ return false;
579
+ const res = await analyzeTsFunctions(o.projectRoot, o.functionsDir, [o.outDir], 60_000, (m) => o.log?.(m));
580
+ if (res === null) {
581
+ o.log?.("warning: could not analyze the TypeScript functions; leaving src/_generated/api.rs as is");
582
+ if (!exists)
583
+ writeIfChanged(file, RUST_API_STUB);
584
+ return false;
585
+ }
586
+ for (const w of res.warnings)
587
+ o.log?.(`warning: ${w}`);
588
+ const outRel = path.relative(o.functionsDir, o.outDir).split(path.sep).join("/");
589
+ const modules = { ...res.modules, ...rustEntriesAsModules(o.rustEntries ?? [], outRel) };
590
+ return writeIfChanged(file, rustApiCodegen(modules));
591
+ }
592
+ /** Rust functions of the last build, kept in the cargo target dir so the first compile sees them. */
593
+ export function readRustCache(cacheFile) {
594
+ try {
595
+ return JSON.parse(fs.readFileSync(cacheFile, "utf8"));
596
+ }
597
+ catch {
598
+ return [];
599
+ }
600
+ }
601
+ export function writeRustCache(cacheFile, entries) {
602
+ try {
603
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
604
+ fs.writeFileSync(cacheFile, JSON.stringify(entries));
605
+ }
606
+ catch {
607
+ // best effort
608
+ }
609
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Stable Rust is enough for the guest crate (verified with stable 1.99.1: build, run and HTTP action on a
3
+ * stock backend); nothing in convex_fn or the macros needs nightly features. Override with CONVEX_RUST_CHANNEL.
4
+ */
5
+ export declare const DEFAULT_RUST_CHANNEL = "stable";
6
+ export interface ScaffoldOptions {
7
+ /** Path to the `convex_fn` crate (env CONVEX_FN_PATH). Unset: registry placeholder. */
8
+ convexFnPath?: string | undefined;
9
+ /** Rust toolchain channel (env CONVEX_RUST_CHANNEL). */
10
+ channel?: string | undefined;
11
+ /** `[build] target-dir` for the generated `.cargo/config.toml`, relative to the crate. */
12
+ targetDir?: string | undefined;
13
+ }
14
+ /** Cargo target directory kept outside the Convex functions directory (relative to the crate). */
15
+ export declare function defaultTargetDir(projectRoot: string, crateDir: string): string;
16
+ export declare function scaffoldFiles(rustDir: string, opts?: ScaffoldOptions): Record<string, string>;
17
+ export interface ScaffoldResult {
18
+ created: string[];
19
+ skipped: string[];
20
+ }
21
+ /** Writes the scaffold; never overwrites existing files. */
22
+ export declare function scaffoldRust(rustDir: string, opts?: ScaffoldOptions): ScaffoldResult;