rspyts-cli 2.0.1

Compiler and build orchestrator for rspyts
Documentation
use super::*;

pub(super) fn typescript_api(
    items: &NamespaceItems<'_>,
    context: &TypeScriptContext<'_>,
) -> Result<String> {
    let runtime_imports = typescript_runtime_imports(items);
    let mut source = String::new();
    let local_types = typescript_local_type_names(items);
    if !local_types.is_empty() {
        writeln!(
            source,
            "import type {{ {} }} from \"./models.js\";",
            local_types.join(", ")
        )?;
    }
    for import in typescript_type_imports(items, context)? {
        writeln!(
            source,
            "import type * as {} from {};",
            namespace_alias(&import),
            ts_string(&typescript_namespace_path(
                context.namespace,
                &import,
                "models.js"
            ))
        )?;
    }
    if !runtime_imports.is_empty() {
        source.push_str("import {\n");
        for name in runtime_imports {
            writeln!(source, "  {name},")?;
        }
        writeln!(
            source,
            "}} from {};",
            ts_string(&typescript_runtime_path(context.namespace))
        )?;
    }
    for import in typescript_error_imports(items, context)? {
        writeln!(
            source,
            "import * as {} from {};",
            api_namespace_alias(&import),
            ts_string(&typescript_namespace_path(
                context.namespace,
                &import,
                "api.js"
            ))
        )?;
    }
    for error in &items.errors {
        writeln!(
            source,
            "\nexport class {} extends globalThis.Error {{\n  readonly code: string;\n\n  constructor(code: string, message: string) {{\n    super(message);\n    this.name = {};\n    this.code = code;\n  }}\n}}",
            error.name,
            ts_string(&error.name)
        )?;
    }
    for function in &items.functions {
        emit_typescript_function(&mut source, function, context, None)?;
    }
    for resource in &items.resources {
        emit_typescript_resource(&mut source, resource, context)?;
    }
    for constant in &items.constants {
        writeln!(
            source,
            "\nexport const {}: {} = restoreHost({}, {});",
            constant.host_name,
            type_ref(&constant.ty, context)?,
            typescript_value(&constant.value, &constant.ty, context.manifest)?,
            typescript_spec(&constant.ty)?
        )?;
    }
    Ok(source)
}

pub(super) fn typescript_runtime(manifest: &Manifest) -> Result<String> {
    let mut source = String::from(
        "import initializeNative, * as native from \"./native.js\";\n\nconst wasmUrl = new URL(\"./native_bg.wasm\", import.meta.url);\nlet wasmInput: any = wasmUrl;\nconst process = (globalThis as { process?: { versions?: { node?: string } } }).process;\nif (process?.versions?.node) {\n  const nodeModule = \"node:fs/promises\";\n  const { readFile } = await import(/* @vite-ignore */ nodeModule);\n  if (wasmUrl.protocol === \"file:\") {\n    wasmInput = await readFile(wasmUrl);\n  } else if (wasmUrl.pathname.startsWith(\"/@fs/\")) {\n    wasmInput = await readFile(decodeURIComponent(wasmUrl.pathname.slice(4)));\n  }\n}\nawait initializeNative({ module_or_path: wasmInput });\n\nexport { native };\n",
    );
    source.push_str(TYPESCRIPT_ADAPTERS);
    source.push_str("\nconst nativeSchemas: Record<string, any> = {\n");
    for definition in &manifest.types {
        writeln!(
            source,
            "  {}: {},",
            ts_property(&definition_key(&definition.identity())),
            typescript_named_spec(definition)?
        )?;
    }
    source.push_str("};\n");
    Ok(source)
}

fn typescript_local_type_names(items: &NamespaceItems<'_>) -> Vec<String> {
    let mut names = items
        .types
        .iter()
        .map(|definition| definition.name.clone())
        .collect::<Vec<_>>();
    if namespace_refs(items)
        .iter()
        .any(|reference| reference_contains(reference, &|item| matches!(item, TypeRef::Json)))
    {
        names.push("JsonValue".to_owned());
    }
    names.sort();
    names.dedup();
    names
}

fn typescript_runtime_imports(items: &NamespaceItems<'_>) -> Vec<&'static str> {
    let has_calls = !items.functions.is_empty() || !items.resources.is_empty();
    let has_params = items
        .functions
        .iter()
        .any(|function| !function.params.is_empty())
        || items.resources.iter().any(|resource| {
            resource
                .constructors
                .iter()
                .any(|constructor| !constructor.params.is_empty())
                || resource
                    .methods
                    .iter()
                    .any(|method| !method.params.is_empty())
        });
    let restores_values = !items.functions.is_empty()
        || items
            .resources
            .iter()
            .any(|resource| !resource.methods.is_empty())
        || !items.constants.is_empty();
    let translates_errors = items
        .functions
        .iter()
        .any(|function| function.error.is_some())
        || items.resources.iter().any(|resource| {
            resource
                .constructors
                .iter()
                .any(|constructor| constructor.error.is_some())
                || resource.methods.iter().any(|method| method.error.is_some())
        });
    let mut imports = Vec::new();
    if has_calls {
        imports.push("native");
    }
    if translates_errors {
        imports.push("nativeError");
    }
    if has_params {
        imports.push("prepareHost");
    }
    if restores_values {
        imports.push("restoreHost");
    }
    imports
}

const TYPESCRIPT_ADAPTERS: &str = r#"
export function prepareHost(value: any): any {
  if (value instanceof Date) return value.toISOString();
  if (ArrayBuffer.isView(value)) return value;
  if (Array.isArray(value)) return value.map(prepareHost);
  if (value !== null && typeof value === "object") {
    return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, prepareHost(item)]));
  }
  return value;
}

const bufferConstructors: Record<string, any> = {
  u8: Uint8Array, i8: Int8Array, u16: Uint16Array, i16: Int16Array,
  u32: Uint32Array, i32: Int32Array, u64: BigUint64Array, i64: BigInt64Array,
  f32: Float32Array, f64: Float64Array,
};

function restoreJson(value: any): any {
  if (typeof value === "bigint") {
    const number = Number(value);
    if (!Number.isSafeInteger(number) || BigInt(number) !== value) {
      throw new RangeError("JSON integer exceeds JavaScript's safe integer range");
    }
    return number;
  }
  if (Array.isArray(value)) return Object.freeze(value.map(restoreJson));
  if (value !== null && typeof value === "object") {
    return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, restoreJson(item)])));
  }
  return value;
}

export function restoreHost(value: any, spec: any): any {
  if (value == null || spec == null) return value;
  const [kind, detail, variants] = spec;
  if (kind === "bytes") return new Uint8Array(value);
  if (kind === "buffer") return new bufferConstructors[detail](value);
  if (kind === "json") return restoreJson(value);
  if (kind === "list") return Object.freeze(Array.from(value, (item: any) => restoreHost(item, detail)));
  if (kind === "map") return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, restoreHost(item, detail)])));
  if (kind === "tuple") return Object.freeze(value.map((item: any, index: number) => restoreHost(item, detail[index])));
  if (kind === "named") return restoreHost(value, nativeSchemas[detail]);
  if (kind === "alias") return restoreHost(value, detail);
  if (kind === "struct") return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, restoreHost(item, detail[key])])));
  if (kind === "tagged") {
    const fields = variants[value[detail]] ?? {};
    return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, restoreHost(item, fields[key])])));
  }
  return value;
}

export function nativeError(error: any, ErrorType: new (code: string, message: string) => Error): any {
  const text = String(error);
  const line = text.indexOf("\n");
  return line < 0 ? error : new ErrorType(text.slice(0, line), text.slice(line + 1));
}
"#;

fn emit_typescript_function(
    source: &mut String,
    function: &FunctionDef,
    context: &TypeScriptContext<'_>,
    receiver: Option<&str>,
) -> Result<()> {
    let params = typescript_params(&function.params, context)?;
    let calls = function
        .params
        .iter()
        .map(|param| format!("prepareHost({})", param.host_name))
        .collect::<Vec<_>>()
        .join(", ");
    let mut result_name = "nativeResult".to_owned();
    let parameter_names = function
        .params
        .iter()
        .map(|param| param.host_name.as_str())
        .collect::<BTreeSet<_>>();
    while parameter_names.contains(result_name.as_str()) {
        result_name.push_str("Value");
    }
    let native_name = &function.native_name;
    let (indent, signature, call) = if receiver.is_some() {
        (
            "  ",
            format!(
                "  {}({params}): {} {{",
                function.host_name,
                return_type_ref(&function.returns, context)?
            ),
            format!("this.nativeResource.{}({calls})", function.host_name),
        )
    } else {
        (
            "",
            format!(
                "export function {}({params}): {} {{",
                function.host_name,
                return_type_ref(&function.returns, context)?
            ),
            format!("native[{quoted}]({calls})", quoted = ts_string(native_name)),
        )
    };
    writeln!(source, "\n{signature}")?;
    if function.error.is_some() {
        writeln!(source, "{indent}  try {{")?;
        writeln!(source, "{indent}    const {result_name} = {call};")?;
        writeln!(
            source,
            "{indent}    return restoreHost({result_name}, {});",
            typescript_spec(&function.returns)?
        )?;
        writeln!(source, "{indent}  }} catch (error) {{")?;
        writeln!(
            source,
            "{indent}    throw nativeError(error, {});",
            typescript_error_ref(function.error.as_ref(), context)?
        )?;
        writeln!(source, "{indent}  }}")?;
    } else {
        writeln!(source, "{indent}  const {result_name} = {call};")?;
        writeln!(
            source,
            "{indent}  return restoreHost({result_name}, {});",
            typescript_spec(&function.returns)?
        )?;
    }
    writeln!(source, "{indent}}}")?;
    Ok(())
}

fn emit_typescript_resource(
    source: &mut String,
    resource: &ResourceDef,
    context: &TypeScriptContext<'_>,
) -> Result<()> {
    let constructor = resource
        .constructors
        .iter()
        .find(|item| item.rust_name == "new")
        .or_else(|| resource.constructors.first())
        .context("resource has no constructor")?;
    let params = typescript_params(&constructor.params, context)?;
    let calls = constructor
        .params
        .iter()
        .map(|item| format!("prepareHost({})", item.host_name))
        .collect::<Vec<_>>()
        .join(", ");
    writeln!(source, "\nexport class {} {{", resource.name)?;
    source.push_str("  private nativeResource!: any;\n\n");
    writeln!(source, "  constructor({params}) {{")?;
    let native_call = format!(
        "new native[{quoted}]({calls})",
        quoted = ts_string(&resource.native_name)
    );
    if constructor.error.is_some() {
        source.push_str("    try {\n");
        writeln!(source, "      this.nativeResource = {native_call};")?;
        source.push_str("    } catch (error) {\n");
        writeln!(
            source,
            "      throw nativeError(error, {});",
            typescript_error_ref(constructor.error.as_ref(), context)?
        )?;
        source.push_str("    }\n");
    } else {
        writeln!(source, "    this.nativeResource = {native_call};")?;
    }
    source.push_str("  }\n");
    for factory in resource
        .constructors
        .iter()
        .filter(|item| !std::ptr::eq(*item, constructor))
    {
        let params = typescript_params(&factory.params, context)?;
        let calls = factory
            .params
            .iter()
            .map(|item| format!("prepareHost({})", item.host_name))
            .collect::<Vec<_>>()
            .join(", ");
        writeln!(
            source,
            "\n  static {}({params}): {} {{",
            factory.host_name, resource.name
        )?;
        writeln!(
            source,
            "    const value = Object.create({}.prototype);",
            resource.name
        )?;
        let native_call = format!(
            "native[{quoted}].{}({calls})",
            factory.host_name,
            quoted = ts_string(&resource.native_name)
        );
        if factory.error.is_some() {
            source.push_str("    try {\n");
            writeln!(source, "      value.nativeResource = {native_call};")?;
            source.push_str("    } catch (error) {\n");
            writeln!(
                source,
                "      throw nativeError(error, {});",
                typescript_error_ref(factory.error.as_ref(), context)?
            )?;
            source.push_str("    }\n");
        } else {
            writeln!(source, "    value.nativeResource = {native_call};")?;
        }
        source.push_str("    return value;\n  }\n");
    }
    for method in &resource.methods {
        let function = FunctionDef {
            owner: resource.owner.clone(),
            rust_module: resource.rust_module.clone(),
            rust_name: method.rust_name.clone(),
            host_name: method.host_name.clone(),
            native_name: resource.native_name.clone(),
            docs: method.docs.clone(),
            params: method.params.clone(),
            returns: method.returns.clone(),
            error: method.error.clone(),
        };
        emit_typescript_function(source, &function, context, Some(&resource.name))?;
    }
    source.push_str("\n  close(): void {\n    this.nativeResource.close();\n  }\n}\n");
    Ok(())
}