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.
- package/LICENSE +201 -0
- package/README.md +137 -2
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +200 -0
- package/dist/cli/build.d.ts +146 -0
- package/dist/cli/build.js +902 -0
- package/dist/cli/demangle.d.ts +7 -0
- package/dist/cli/demangle.js +328 -0
- package/dist/cli/diagnostics.d.ts +34 -0
- package/dist/cli/diagnostics.js +296 -0
- package/dist/cli/generate.d.ts +29 -0
- package/dist/cli/generate.js +82 -0
- package/dist/cli/manifest.d.ts +24 -0
- package/dist/cli/manifest.js +75 -0
- package/dist/cli/rustApi.d.ts +78 -0
- package/dist/cli/rustApi.js +609 -0
- package/dist/cli/scaffold.d.ts +22 -0
- package/dist/cli/scaffold.js +114 -0
- package/dist/cli/shim.d.ts +30 -0
- package/dist/cli/shim.js +140 -0
- package/dist/cli/size.d.ts +17 -0
- package/dist/cli/size.js +73 -0
- package/dist/cli/split.d.ts +20 -0
- package/dist/cli/split.js +140 -0
- package/dist/cli/symbols.d.ts +30 -0
- package/dist/cli/symbols.js +133 -0
- package/dist/cli/toolchain.d.ts +53 -0
- package/dist/cli/toolchain.js +167 -0
- package/dist/cli/validators.d.ts +6 -0
- package/dist/cli/validators.js +66 -0
- package/dist/cli/wasisdk.d.ts +82 -0
- package/dist/cli/wasisdk.js +220 -0
- package/dist/cli/wasmHints.d.ts +29 -0
- package/dist/cli/wasmHints.js +172 -0
- package/dist/runtime/base64.d.ts +4 -0
- package/dist/runtime/base64.js +77 -0
- package/dist/runtime/crypto.d.ts +7 -0
- package/dist/runtime/crypto.js +421 -0
- package/dist/runtime/fastjson.d.ts +3 -0
- package/dist/runtime/fastjson.js +170 -0
- package/dist/runtime/host.d.ts +73 -0
- package/dist/runtime/host.js +1230 -0
- package/dist/runtime/http.d.ts +40 -0
- package/dist/runtime/http.js +157 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/intl.d.ts +5 -0
- package/dist/runtime/intl.js +69 -0
- package/dist/runtime/tmpfs.d.ts +129 -0
- package/dist/runtime/tmpfs.js +624 -0
- package/dist/runtime/wasi.d.ts +33 -0
- package/dist/runtime/wasi.js +216 -0
- package/package.json +57 -4
- package/rust/convex_fn/Cargo.toml +32 -0
- package/rust/convex_fn/LICENSE +201 -0
- package/rust/convex_fn/build.rs +3 -0
- package/rust/convex_fn/src/args.rs +416 -0
- package/rust/convex_fn/src/crypto.rs +237 -0
- package/rust/convex_fn/src/ctx.rs +1044 -0
- package/rust/convex_fn/src/dns.rs +356 -0
- package/rust/convex_fn/src/error.rs +179 -0
- package/rust/convex_fn/src/executor.rs +577 -0
- package/rust/convex_fn/src/fanout.rs +106 -0
- package/rust/convex_fn/src/fs.rs +19 -0
- package/rust/convex_fn/src/function_ref.rs +219 -0
- package/rust/convex_fn/src/http.rs +441 -0
- package/rust/convex_fn/src/intl.rs +75 -0
- package/rust/convex_fn/src/lib.rs +242 -0
- package/rust/convex_fn/src/logging.rs +222 -0
- package/rust/convex_fn/src/native.rs +360 -0
- package/rust/convex_fn/src/net.rs +122 -0
- package/rust/convex_fn/src/sys.rs +178 -0
- package/rust/convex_fn/src/values.rs +121 -0
- package/rust/convex_fn/src/wire.rs +579 -0
- package/rust/convex_fn_macros/Cargo.toml +13 -0
- package/rust/convex_fn_macros/LICENSE +201 -0
- package/rust/convex_fn_macros/src/derive.rs +632 -0
- package/rust/convex_fn_macros/src/lib.rs +338 -0
- package/rust/convex_reqwest/Cargo.toml +69 -0
- package/rust/convex_reqwest/LICENSE +201 -0
- package/rust/convex_reqwest/src/blocking.rs +287 -0
- package/rust/convex_reqwest/src/body.rs +152 -0
- package/rust/convex_reqwest/src/client.rs +359 -0
- package/rust/convex_reqwest/src/config_types.rs +150 -0
- package/rust/convex_reqwest/src/error.rs +241 -0
- package/rust/convex_reqwest/src/into_url.rs +57 -0
- package/rust/convex_reqwest/src/lib.rs +42 -0
- package/rust/convex_reqwest/src/multipart.rs +140 -0
- package/rust/convex_reqwest/src/request.rs +358 -0
- 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;
|