use std::path::Path;
use serde_json::Value;
pub(super) fn transpile_ts(source: &str, path: &Path) -> Result<String, String> {
use oxc_allocator::Allocator;
use oxc_codegen::{Codegen, CodegenOptions};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_transformer::{HelperLoaderMode, TransformOptions, Transformer, TypeScriptOptions};
let allocator = Allocator::default();
let source_type = SourceType::from_path(path)
.map_err(|_| format!("unsupported extension: {}", path.display()))?
.with_typescript(true)
.with_module(true);
let ret = Parser::new(&allocator, source, source_type).parse();
if !ret.errors.is_empty() {
let diags = ret
.errors
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join("; ");
return Err(format!("parse error: {diags}"));
}
let mut program = ret.program;
let semantic = SemanticBuilder::new().build(&program);
let scoping = semantic.semantic.into_scoping();
let mut options = TransformOptions {
typescript: TypeScriptOptions::default(),
..Default::default()
};
options.helper_loader.mode = HelperLoaderMode::Inline;
let ret =
Transformer::new(&allocator, path, &options).build_with_scoping(scoping, &mut program);
if !ret.errors.is_empty() {
let diags = ret
.errors
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join("; ");
return Err(format!("transform error: {diags}"));
}
let code = Codegen::new()
.with_options(CodegenOptions::default())
.build(&program)
.code;
Ok(code)
}
pub(super) fn read_kind_export(js: &str) -> Option<String> {
run_module_js(js, |_ctx, namespace| {
if namespace.contains_key("kind").ok()? {
namespace.get::<_, String>("kind").ok()
} else {
None
}
})
.ok()
.flatten()
}
pub(super) fn run_hook_js(js: &str, hook: &str, arg: &Value) -> Result<Option<Value>, String> {
let arg_literal = serde_json::to_string(&arg.to_string()).map_err(|e| e.to_string())?;
let script = format!(
"JSON.stringify((() => {{ \
const f = globalThis.__theway_ext.{hook}; \
if (typeof f !== 'function') return null; \
const r = f(JSON.parse({arg_literal})); \
return r === undefined ? null : r; \
}})())"
);
let value = run_module_js(js, |ctx, _namespace| {
let out: String = ctx.eval(script.as_str()).ok()?;
let value: Value = serde_json::from_str(&out).ok()?;
if value.is_null() { None } else { Some(value) }
})?;
Ok(value)
}
fn run_module_js<T>(
js: &str,
f: impl FnOnce(rquickjs::Ctx<'_>, rquickjs::Object<'_>) -> Option<T>,
) -> Result<Option<T>, String> {
use rquickjs::{Context, Module, Runtime};
let runtime = Runtime::new().map_err(|e| e.to_string())?;
let ctx = Context::full(&runtime).map_err(|e| e.to_string())?;
ctx.with(|ctx| {
let module =
Module::declare(ctx.clone(), "theway-extension", js).map_err(|e| e.to_string())?;
let (module, _promise) = module.eval().map_err(|e| e.to_string())?;
let namespace = module.namespace().map_err(|e| e.to_string())?;
ctx.globals()
.set("__theway_ext", namespace.clone())
.map_err(|e| e.to_string())?;
Ok(f(ctx, namespace))
})
}