wasm-rquickjs 0.4.2

Tool for wrapping JavaScript modules as WebAssembly components using the QuickJS engine
Documentation
use std::io::Write;
use std::sync::{Arc, Mutex};

use swc_common::{
    GLOBALS, Globals, SourceMap,
    errors::{HANDLER, Handler},
    sync::Lrc,
};
use swc_ts_fast_strip::{ErrorCode, Mode, Options, operate};

#[derive(Copy, Clone)]
pub(crate) enum TypeScriptMode {
    Strip,
    Transform,
}

pub(crate) fn runtime_mode() -> TypeScriptMode {
    if cfg!(feature = "typescript-transform-runtime") {
        TypeScriptMode::Transform
    } else {
        TypeScriptMode::Strip
    }
}

pub(crate) fn transform_module(
    source: String,
    filename: &str,
    source_map: bool,
    module: Option<bool>,
) -> Result<TypeScriptOutput, TypeScriptError> {
    if std::path::Path::new(filename)
        .components()
        .any(|component| component.as_os_str() == "node_modules")
    {
        return Err(TypeScriptError {
            code: "ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING",
            kind: TypeScriptErrorKind::Error,
            message: format!(
                "Stripping types is currently unsupported for files under node_modules, for \"{filename}\""
            ),
        });
    }
    transform(source, filename, runtime_mode(), source_map, module)
}

#[derive(Debug)]
pub(crate) struct TypeScriptOutput {
    pub(crate) code: String,
    pub(crate) source_map: Option<String>,
}

#[derive(Debug)]
pub(crate) struct TypeScriptError {
    pub(crate) code: &'static str,
    pub(crate) kind: TypeScriptErrorKind,
    pub(crate) message: String,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum TypeScriptErrorKind {
    Error,
    SyntaxError,
}

struct DiagnosticWriter(Arc<Mutex<Vec<u8>>>);

impl Write for DiagnosticWriter {
    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
        self.0
            .lock()
            .expect("diagnostic buffer poisoned")
            .extend_from_slice(bytes);
        Ok(bytes.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

pub(crate) fn transform(
    source: String,
    filename: &str,
    mode: TypeScriptMode,
    source_map: bool,
    module: Option<bool>,
) -> Result<TypeScriptOutput, TypeScriptError> {
    let source_map_owner: Lrc<SourceMap> = Default::default();
    let diagnostics = Arc::new(Mutex::new(Vec::new()));
    let handler = Handler::with_emitter_writer(
        Box::new(DiagnosticWriter(diagnostics.clone())),
        Some(source_map_owner.clone()),
    );
    let options = Options {
        module,
        filename: Some(filename.to_string()),
        mode: match mode {
            TypeScriptMode::Strip => Mode::StripOnly,
            TypeScriptMode::Transform => Mode::Transform,
        },
        source_map,
        deprecated_ts_module_as_error: Some(true),
        ..Default::default()
    };
    let result = GLOBALS.set(&Globals::default(), || {
        HANDLER.set(&handler, || {
            operate(&source_map_owner, &handler, source, options)
        })
    });
    match result {
        Ok(output) => Ok(TypeScriptOutput {
            code: output.code,
            source_map: output.map,
        }),
        Err(error) => {
            let diagnostics = diagnostics.lock().expect("diagnostic buffer poisoned");
            let message = String::from_utf8_lossy(&diagnostics).trim().to_string();
            if message.is_empty() {
                Err(TypeScriptError {
                    code: typescript_error_code(error.code),
                    kind: TypeScriptErrorKind::SyntaxError,
                    message: error.to_string(),
                })
            } else {
                Err(TypeScriptError {
                    code: typescript_error_code(error.code),
                    kind: TypeScriptErrorKind::SyntaxError,
                    message,
                })
            }
        }
    }
}

fn typescript_error_code(code: ErrorCode) -> &'static str {
    match code {
        ErrorCode::UnsupportedSyntax => "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX",
        ErrorCode::InvalidSyntax | ErrorCode::Unknown => "ERR_INVALID_TYPESCRIPT_SYNTAX",
        _ => "ERR_INVALID_TYPESCRIPT_SYNTAX",
    }
}

#[cfg(test)]
mod tests {
    use super::{TypeScriptMode, transform};

    #[test]
    fn strip_preserves_source_positions() {
        let output = transform(
            "const value: number = 1;".to_string(),
            "input.ts",
            TypeScriptMode::Strip,
            false,
            Some(true),
        )
        .expect("strip should succeed");
        assert_eq!(output.code, "const value         = 1;");
        assert!(output.source_map.is_none());
    }

    #[test]
    fn transform_supports_runtime_typescript_syntax() {
        let output = transform(
            "enum Direction { Up, Down }\nexport default Direction.Up;".to_string(),
            "input.ts",
            TypeScriptMode::Transform,
            true,
            Some(true),
        )
        .expect("transform should succeed");
        assert!(output.code.contains("Direction"));
        assert!(output.source_map.is_some());
    }

    #[test]
    fn strip_rejects_runtime_typescript_syntax() {
        let error = transform(
            "enum Direction { Up, Down }".to_string(),
            "input.ts",
            TypeScriptMode::Strip,
            false,
            Some(true),
        )
        .expect_err("strip-only mode must reject enums");
        assert_eq!(error.code, "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX");
        assert_eq!(error.kind, super::TypeScriptErrorKind::SyntaxError);
        assert!(
            error
                .message
                .contains("TypeScript enum is not supported in strip-only mode")
        );
    }
}