use std::collections::BTreeSet;
use std::fmt::Write;
use std::fs;
use std::path::Path;
use anyhow::{Context, Result};
use rspyts::ir::{
BufferElement, FieldConstraints, FieldDef, FunctionDef, Manifest, Namespace, ParamDef,
ResourceDef, TypeDef, TypeRef, TypeShape,
};
use serde_json::Value;
use wasm_bindgen_cli_support::Bindgen;
use crate::contract::{
NamespaceItems, definition_key, error_definition, named_identities, namespace_refs, namespaces,
reference_contains, tagged_variant_name, type_definition, type_namespace,
};
use crate::output::write;
use crate::project::{Project, is_identifier};
mod api;
mod declarations;
mod render;
use api::*;
use declarations::*;
pub(crate) use render::*;
pub(super) struct TypeScriptContext<'a> {
manifest: &'a Manifest,
namespace: &'a Namespace,
}
pub(super) fn emit(project: &Project, manifest: &Manifest, wasm: &Path, root: &Path) -> Result<()> {
let package = root.join("src-ts").join(&project.package_name);
let native_source = package.join("native");
let native_output = root
.join("src-ts/build")
.join(&project.package_name)
.join("native");
fs::create_dir_all(&native_source)?;
fs::create_dir_all(&native_output)?;
let bindgen_output = tempfile::tempdir()?;
let mut bindgen = Bindgen::new();
bindgen
.input_path(wasm)
.web(true)?
.typescript(true)
.omit_default_module_path(false)
.out_name("native")
.generate(bindgen_output.path())
.context("failed to generate TypeScript WebAssembly bindings")?;
for name in ["native.js", "native.d.ts"] {
let destination = native_source.join(name);
fs::copy(bindgen_output.path().join(name), &destination)?;
prepend_generated_header(&destination)?;
}
fs::copy(
bindgen_output.path().join("native_bg.wasm"),
native_output.join("native_bg.wasm"),
)?;
write(
&package.join("runtime.ts"),
&generated_typescript(&typescript_runtime(manifest)?),
)?;
let namespace_map = namespaces(manifest);
for (namespace, items) in &namespace_map {
let namespace_package = namespace
.typescript_segments()
.iter()
.fold(package.clone(), |path, segment| path.join(segment));
fs::create_dir_all(&namespace_package)?;
let context = TypeScriptContext {
manifest,
namespace,
};
write(
&namespace_package.join("models.ts"),
&generated_typescript(&typescript_models(items, &context)?),
)?;
write(
&namespace_package.join("api.ts"),
&generated_typescript(&typescript_api(items, &context)?),
)?;
if *namespace != Namespace::root() {
write(
&namespace_package.join("index.ts"),
&generated_typescript(TYPESCRIPT_INDEX),
)?;
}
}
Ok(())
}
pub(super) fn validate_project(project: &Project) -> Result<()> {
let path = project.typescript_source().join("package.json");
let source = fs::read_to_string(&path)
.with_context(|| format!("failed to read user-owned {}", path.display()))?;
let package: Value = serde_json::from_str(&source)
.with_context(|| format!("failed to parse user-owned {}", path.display()))?;
let name = package["name"]
.as_str()
.with_context(|| format!("{} must define a string `name`", path.display()))?;
if name != project.typescript_package {
anyhow::bail!(
"{} declares npm package `{name}`, but rspyts.toml configures `{}`",
path.display(),
project.typescript_package
);
}
let version = package["version"]
.as_str()
.with_context(|| format!("{} must define a string `version`", path.display()))?;
if version != project.package_version {
anyhow::bail!(
"{} declares version `{version}`, but Cargo.toml declares `{}`",
path.display(),
project.package_version
);
}
Ok(())
}
const GENERATED_HEADER: &str = "/*\n * =============================================================================\n * AUTO-GENERATED BY rspyts - DO NOT EDIT.\n *\n * This file is overwritten by `rspyts build`.\n * =============================================================================\n */\n\n";
const TYPESCRIPT_INDEX: &str = "export * from \"./models.js\";\nexport * from \"./api.js\";\n";
fn generated_typescript(source: &str) -> String {
format!("{GENERATED_HEADER}{source}")
}
fn prepend_generated_header(path: &Path) -> Result<()> {
let source = fs::read_to_string(path)?;
fs::write(path, generated_typescript(&source))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_typescript_has_an_explicit_warning() {
let generated = generated_typescript("export const value = 1;\n");
assert!(generated.starts_with(GENERATED_HEADER));
assert!(generated.contains("AUTO-GENERATED BY rspyts - DO NOT EDIT"));
}
}