tanxium 0.3.3

Embeddable JavaScript and TypeScript runtime with Yasumu APIs
#![allow(dead_code)]

#[path = "src/ops.rs"]
mod ops;
#[path = "src/state.rs"]
mod state;
#[path = "src/types.rs"]
mod types;

use deno_core::{ModuleCodeString, ModuleName, wrap_lazy_ext_script};
use deno_runtime::ops::bootstrap::SnapshotOptions;
use deno_runtime::snapshot::{
    CreateRuntimeSnapshotOutput, LazyExtensionFileKind, create_runtime_snapshot,
};
use deno_runtime::transpile::{maybe_transpile_and_minify_source, maybe_transpile_source};
use std::collections::HashSet;
use std::io::Write;
use std::path::{Path, PathBuf};

const SNAPSHOT_FILE: &str = "TANXIUM_SNAPSHOT.bin";
const RESIDUAL_TABLE_FILE: &str = "TANXIUM_RESIDUAL_SOURCES.rs";

fn main() {
    println!("cargo:rerun-if-env-changed=DENO_SNAPSHOT_IMPORT_GRAPH");
    println!("cargo:rerun-if-env-changed=DENO_SNAPSHOT_MINIFY_SOURCES");

    let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR is not set"));
    let snapshot_path = out_dir.join(SNAPSHOT_FILE);

    let mut extensions = ops::tanxium_runtime_extensions();
    // The real worker installs Tanxium's host-backed state. During snapshot
    // creation Deno owns SnapshotOptions, and the bootstrap only needs the op
    // declarations to be registered so its bindings can be captured.
    for extension in &mut extensions {
        extension.op_state_fn = None;
    }

    let output = create_runtime_snapshot(
        snapshot_path,
        SnapshotOptions {
            ts_version: deno_snapshots::TS_VERSION.to_string(),
            v8_version: deno_core::v8::VERSION_STRING,
            target: std::env::var("TARGET").expect("TARGET is not set"),
        },
        extensions,
    );

    write_residual_sources(&out_dir, output);
}

fn write_residual_sources(out_dir: &Path, output: CreateRuntimeSnapshotOutput) {
    let consumed = output
        .consumed_lazy_specifiers
        .iter()
        .map(String::as_str)
        .collect::<HashSet<_>>();
    let residual_dir = out_dir.join("tanxium_residual_sources");
    std::fs::create_dir_all(&residual_dir).expect("failed to create residual source directory");

    let minify = std::env::var_os("DENO_SNAPSHOT_MINIFY_SOURCES").is_some();
    let mut residual_js = Vec::new();
    let mut residual_esm = Vec::new();

    for file in output.lazy_extension_files {
        if consumed.contains(file.specifier.as_str()) {
            continue;
        }

        println!("cargo:rerun-if-changed={}", file.path.display());
        let generated =
            transpile_residual_source(&residual_dir, &file.specifier, &file.path, minify);

        match file.kind {
            LazyExtensionFileKind::Js => {
                wrap_residual_js_source(&generated);
                assert_residual_ascii(&generated, &file.specifier);
                residual_js.push((file.specifier, generated));
            }
            LazyExtensionFileKind::Esm => {
                assert_residual_ascii(&generated, &file.specifier);
                residual_esm.push((file.specifier, generated));
            }
        }
    }

    let mut table = std::fs::File::create(out_dir.join(RESIDUAL_TABLE_FILE))
        .expect("failed to create residual source table");
    writeln!(
        table,
        "// @generated by crates/tanxium/build.rs - do not edit.\n"
    )
    .expect("failed to write residual source table header");
    write_residual_table(
        &mut table,
        out_dir,
        "TANXIUM_RESIDUAL_LAZY_JS",
        &residual_js,
    );
    write_residual_table(
        &mut table,
        out_dir,
        "TANXIUM_RESIDUAL_LAZY_ESM",
        &residual_esm,
    );
}

fn transpile_residual_source(
    out_dir: &Path,
    specifier: &str,
    source_path: &Path,
    minify: bool,
) -> PathBuf {
    let source = std::fs::read_to_string(source_path).unwrap_or_else(|error| {
        panic!(
            "failed to read residual source {} for {specifier}: {error}",
            source_path.display()
        )
    });
    let name = ModuleName::from(specifier.to_string());
    let source = ModuleCodeString::from(source);
    let (transpiled, _) = if minify {
        maybe_transpile_and_minify_source(name, source)
    } else {
        maybe_transpile_source(name, source)
    }
    .unwrap_or_else(|error| panic!("failed to transpile residual source {specifier}: {error}"));

    let sanitized = specifier
        .chars()
        .map(|character| {
            if character.is_ascii_alphanumeric() {
                character
            } else {
                '_'
            }
        })
        .collect::<String>();
    let output_path = out_dir.join(format!("{sanitized}.js"));
    std::fs::write(&output_path, transpiled.as_bytes()).unwrap_or_else(|error| {
        panic!(
            "failed to write residual source {} for {specifier}: {error}",
            output_path.display()
        )
    });
    output_path
}

fn wrap_residual_js_source(path: &Path) {
    let source = std::fs::read_to_string(path)
        .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
    std::fs::write(path, wrap_lazy_ext_script(&source))
        .unwrap_or_else(|error| panic!("failed to write {}: {error}", path.display()));
}

fn assert_residual_ascii(path: &Path, specifier: &str) {
    assert!(
        specifier.is_ascii(),
        "residual source specifier {specifier} contains non-ASCII bytes"
    );
    let source = std::fs::read(path)
        .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
    assert!(
        source.is_ascii(),
        "generated residual source for {specifier} at {} contains non-ASCII bytes",
        path.display()
    );
}

fn write_residual_table(
    file: &mut std::fs::File,
    out_dir: &Path,
    name: &str,
    entries: &[(String, PathBuf)],
) {
    writeln!(file, "pub static {name}: &[(&str, &str)] = &[")
        .expect("failed to write residual source table");
    let mut entries = entries.iter().collect::<Vec<_>>();
    entries.sort_by(|left, right| left.0.cmp(&right.0));
    for (specifier, generated_path) in entries {
        let relative = generated_path
            .strip_prefix(out_dir)
            .expect("generated residual source is outside OUT_DIR");
        writeln!(
            file,
            "    ({specifier:?}, include_str!(concat!(env!(\"OUT_DIR\"), {:?}))),",
            format!("/{}", relative.display())
        )
        .expect("failed to write residual source table entry");
    }
    writeln!(file, "];\n").expect("failed to finish residual source table");
}