commit 76053de71d5fec5076b864e2ff79a26b17fc71d1
parent ee040ce23067eeee888556086f4a65417622985b
Author: triesap <tyson@radroots.org>
Date: Thu, 11 Jun 2026 06:17:51 -0700
feat(bindings): generate core and types packages
Diffstat:
15 files changed, 247 insertions(+), 19 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -680,6 +680,8 @@ dependencies = [
name = "radroots_sdk_xtask"
version = "0.1.0"
dependencies = [
+ "radroots_core_bindings",
+ "radroots_types_bindings",
"serde_json",
]
diff --git a/crates/core_bindings/src/lib.rs b/crates/core_bindings/src/lib.rs
@@ -1 +1,16 @@
pub use radroots_core as upstream;
+
+pub const TYPES_TS: &str =
+ include_str!("../../../testdata/baseline/current-radroots-generated/core/types.ts");
+
+#[cfg(test)]
+mod tests {
+ use super::TYPES_TS;
+
+ #[test]
+ fn preserves_core_type_exports() {
+ assert!(TYPES_TS.contains("export type RadrootsCoreMoney"));
+ assert!(TYPES_TS.contains("export type RadrootsCoreQuantityPrice"));
+ assert!(TYPES_TS.contains("\"each\""));
+ }
+}
diff --git a/crates/types_bindings/src/lib.rs b/crates/types_bindings/src/lib.rs
@@ -1 +1,16 @@
pub use radroots_types as upstream;
+
+pub const TYPES_TS: &str =
+ include_str!("../../../testdata/baseline/current-radroots-generated/types/types.ts");
+
+#[cfg(test)]
+mod tests {
+ use super::TYPES_TS;
+
+ #[test]
+ fn preserves_result_wrapper_exports() {
+ assert!(TYPES_TS.contains("export type IError"));
+ assert!(TYPES_TS.contains("export type IResultList"));
+ assert!(TYPES_TS.contains("export type IResultPass"));
+ }
+}
diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml
@@ -13,4 +13,6 @@ name = "radroots_sdk_xtask"
path = "src/main.rs"
[dependencies]
+radroots_core_bindings = { path = "../core_bindings" }
+radroots_types_bindings = { path = "../types_bindings" }
serde_json = "1"
diff --git a/crates/xtask/src/check.rs b/crates/xtask/src/check.rs
@@ -2,6 +2,7 @@ use std::{fs, path::Path};
use crate::{
fs::workspace_root,
+ output::package_outputs,
package_matrix::{FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix},
};
@@ -19,6 +20,18 @@ pub fn check() -> Result<(), String> {
return Err(format!("missing package index: {}", index_path.display()));
}
}
+ for output in package_outputs() {
+ for expected in output.files() {
+ let path = root
+ .join(output.spec.package_dir)
+ .join(expected.relative_path);
+ let actual = fs::read_to_string(&path)
+ .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
+ if actual != expected.contents {
+ return Err(format!("stale generated output: {}", path.display()));
+ }
+ }
+ }
Ok(())
}
diff --git a/crates/xtask/src/generate.rs b/crates/xtask/src/generate.rs
@@ -1,22 +1,16 @@
-use crate::{
- manifest::{manifest_file_name, package_manifest},
- package_matrix::{package_specs, validate_package_matrix},
- ts::{generated_constants_file, generated_header, generated_types_file, normalize_lf},
-};
+use crate::{fs::workspace_root, output::package_outputs, package_matrix::validate_package_matrix};
pub fn generate_ts() -> Result<(), String> {
validate_package_matrix()?;
- let header = normalize_lf(generated_header());
- for spec in package_specs() {
- let manifest = package_manifest(*spec);
- println!(
- "planned TypeScript generation for {} with {}, {}, {}, and {}",
- manifest["package"],
- generated_types_file(),
- generated_constants_file(),
- manifest_file_name(),
- header.lines().next().unwrap_or_default()
- );
+ let root = workspace_root()?;
+ for output in package_outputs() {
+ for generated_file in output.files() {
+ let path = root
+ .join(output.spec.package_dir)
+ .join(generated_file.relative_path);
+ crate::fs::write_if_changed(&path, &generated_file.contents)?;
+ }
+ println!("generated TypeScript package {}", output.spec.package_name);
}
Ok(())
}
diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs
@@ -2,6 +2,7 @@ mod check;
mod fs;
mod generate;
mod manifest;
+mod output;
mod package_matrix;
mod ts;
diff --git a/crates/xtask/src/output.rs b/crates/xtask/src/output.rs
@@ -0,0 +1,120 @@
+use crate::{
+ manifest::manifest_file_name,
+ manifest::package_manifest,
+ package_matrix::{PackageSpec, package_specs},
+ ts::{
+ generated_constants_file, generated_header, generated_types_file,
+ strip_legacy_generated_header,
+ },
+};
+
+pub struct PackageOutput {
+ pub spec: PackageSpec,
+ pub types_ts: Option<&'static str>,
+ pub constants_ts: Option<&'static str>,
+}
+
+pub struct GeneratedFile {
+ pub relative_path: String,
+ pub contents: String,
+}
+
+impl PackageOutput {
+ pub fn files(&self) -> Vec<GeneratedFile> {
+ let mut files = Vec::new();
+ if let Some(types_ts) = self.types_ts {
+ files.push(GeneratedFile {
+ relative_path: format!("src/generated/{}", generated_types_file()),
+ contents: render_ts(types_ts),
+ });
+ }
+ if let Some(constants_ts) = self.constants_ts {
+ files.push(GeneratedFile {
+ relative_path: format!("src/generated/{}", generated_constants_file()),
+ contents: render_ts(constants_ts),
+ });
+ }
+ files.push(GeneratedFile {
+ relative_path: format!("src/generated/{}", manifest_file_name()),
+ contents: render_manifest(self.spec),
+ });
+ files.push(GeneratedFile {
+ relative_path: "src/index.ts".to_owned(),
+ contents: render_index(self),
+ });
+ files
+ }
+}
+
+pub fn package_outputs() -> Vec<PackageOutput> {
+ vec![
+ PackageOutput {
+ spec: spec_by_key("core"),
+ types_ts: Some(radroots_core_bindings::TYPES_TS),
+ constants_ts: None,
+ },
+ PackageOutput {
+ spec: spec_by_key("types"),
+ types_ts: Some(radroots_types_bindings::TYPES_TS),
+ constants_ts: None,
+ },
+ ]
+}
+
+fn spec_by_key(key: &str) -> PackageSpec {
+ package_specs()
+ .iter()
+ .copied()
+ .find(|spec| spec.key == key)
+ .unwrap_or_else(|| panic!("missing package spec for {key}"))
+}
+
+fn render_ts(source: &str) -> String {
+ let body = strip_legacy_generated_header(source);
+ format!("{}{}", generated_header(), body.trim_start())
+}
+
+fn render_manifest(spec: PackageSpec) -> String {
+ let mut value = package_manifest(spec);
+ value["generated"] = serde_json::Value::Bool(true);
+ format!(
+ "{}\n",
+ serde_json::to_string_pretty(&value).expect("manifest json serializes")
+ )
+}
+
+fn render_index(output: &PackageOutput) -> String {
+ let mut lines = Vec::new();
+ if output.types_ts.is_some() {
+ lines.push("export * from \"./generated/types.js\";");
+ }
+ if output.constants_ts.is_some() {
+ lines.push("export * from \"./generated/constants.js\";");
+ }
+ if lines.is_empty() {
+ lines.push("export {};");
+ }
+ format!("{}\n", lines.join("\n"))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{package_outputs, render_ts};
+
+ #[test]
+ fn renders_sdk_header() {
+ let output = render_ts("// legacy\n\nexport type A = string;\n");
+ assert!(output.starts_with("// @generated by cargo xtask generate ts"));
+ assert!(output.contains("export type A = string;"));
+ }
+
+ #[test]
+ fn includes_core_and_types_outputs() {
+ let package_names = package_outputs()
+ .into_iter()
+ .map(|output| output.spec.package_name)
+ .collect::<Vec<_>>();
+ assert!(package_names.contains(&"@radroots/core-bindings"));
+ assert!(package_names.contains(&"@radroots/types-bindings"));
+ }
+}
diff --git a/crates/xtask/src/ts.rs b/crates/xtask/src/ts.rs
@@ -14,9 +14,20 @@ pub fn normalize_lf(value: &str) -> String {
value.replace("\r\n", "\n")
}
+pub fn strip_legacy_generated_header(value: &str) -> String {
+ let normalized = normalize_lf(value);
+ normalized
+ .strip_prefix("// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\n")
+ .unwrap_or(&normalized)
+ .to_owned()
+}
+
#[cfg(test)]
mod tests {
- use super::{generated_constants_file, generated_header, generated_types_file, normalize_lf};
+ use super::{
+ generated_constants_file, generated_header, generated_types_file, normalize_lf,
+ strip_legacy_generated_header,
+ };
#[test]
fn generated_header_matches_contract() {
@@ -36,4 +47,13 @@ mod tests {
fn normalizes_line_endings() {
assert_eq!(normalize_lf("a\r\nb\n"), "a\nb\n");
}
+
+ #[test]
+ fn strips_legacy_ts_rs_header() {
+ let source = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\nexport type A = string;\n";
+ assert_eq!(
+ strip_legacy_generated_header(source),
+ "export type A = string;\n"
+ );
+ }
}
diff --git a/packages/core-bindings/src/generated/sdk-manifest.json b/packages/core-bindings/src/generated/sdk-manifest.json
@@ -0,0 +1,6 @@
+{
+ "crate": "radroots_core_bindings",
+ "generated": true,
+ "generator": "radroots_sdk_xtask",
+ "package": "@radroots/core-bindings"
+}
diff --git a/packages/core-bindings/src/generated/types.ts b/packages/core-bindings/src/generated/types.ts
@@ -0,0 +1,25 @@
+// @generated by cargo xtask generate ts
+// Do not edit by hand.
+export type RadrootsCoreCurrency = string;
+
+export type RadrootsCoreDecimal = string;
+
+export type RadrootsCoreDiscount = { scope: RadrootsCoreDiscountScope, threshold: RadrootsCoreDiscountThreshold, value: RadrootsCoreDiscountValue, };
+
+export type RadrootsCoreDiscountScope = "bin" | "order_total";
+
+export type RadrootsCoreDiscountThreshold = { "kind": "bin_count", "amount": { bin_id: string, min: number, } } | { "kind": "order_quantity", "amount": { min: RadrootsCoreQuantity, } };
+
+export type RadrootsCoreDiscountValue = { "kind": "money_per_bin", "amount": RadrootsCoreMoney } | { "kind": "percent", "amount": RadrootsCorePercent };
+
+export type RadrootsCoreMoney = { amount: string, currency: string, };
+
+export type RadrootsCorePercent = { value: string, };
+
+export type RadrootsCoreQuantity = { amount: string, unit: RadrootsCoreUnit, label: string | null, };
+
+export type RadrootsCoreQuantityPrice = { amount: RadrootsCoreMoney, quantity: RadrootsCoreQuantity, };
+
+export type RadrootsCoreUnit = "each" | "kg" | "g" | "oz" | "lb" | "l" | "ml";
+
+export type RadrootsCoreUnitDimension = "count" | "mass" | "volume";
diff --git a/packages/core-bindings/src/index.ts b/packages/core-bindings/src/index.ts
@@ -1 +1 @@
-export {};
+export * from "./generated/types.js";
diff --git a/packages/types-bindings/src/generated/sdk-manifest.json b/packages/types-bindings/src/generated/sdk-manifest.json
@@ -0,0 +1,6 @@
+{
+ "crate": "radroots_types_bindings",
+ "generated": true,
+ "generator": "radroots_sdk_xtask",
+ "package": "@radroots/types-bindings"
+}
diff --git a/packages/types-bindings/src/generated/types.ts b/packages/types-bindings/src/generated/types.ts
@@ -0,0 +1,9 @@
+// @generated by cargo xtask generate ts
+// Do not edit by hand.
+export type IError<T> = { err: T, };
+
+export type IResult<T> = { result: T, };
+
+export type IResultList<T> = { results: Array<T>, };
+
+export type IResultPass = { pass: boolean, };
diff --git a/packages/types-bindings/src/index.ts b/packages/types-bindings/src/index.ts
@@ -1 +1 @@
-export {};
+export * from "./generated/types.js";