rspyts-cli 3.0.3

Compiler and build orchestrator for rspyts
Documentation
//! TypeScript source and wasm-bindgen artifact emission.
//!
//! User-owned npm and compiler configuration are validated in place. Generated
//! TypeScript, JavaScript declarations, loader code, and Wasm assets are
//! emitted directly at their package destinations in the temporary tree.

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::*;
/// TypeScript syntax and reference-rendering primitives shared by emitters.
pub(crate) use render::*;

/// Shared contract and namespace state for one generated TypeScript module.
pub(super) struct TypeScriptContext<'a> {
    manifest: &'a Manifest,
    namespace: &'a Namespace,
}

/// Emit TypeScript sources and wasm-bindgen artifacts into a staging tree.
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(())
}

/// Validate the user-owned npm package name and version before publication.
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";

/// Prefix generated TypeScript or JavaScript with the ownership warning.
fn generated_typescript(source: &str) -> String {
    format!("{GENERATED_HEADER}{source}")
}

/// Add the ownership warning to a UTF-8 wasm-bindgen output file.
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"));
    }
}