///|
priv enum TsmbtDirection {
Auto
TsToMbt
MbtToTs
}
///|
priv struct UnifiedTsmbtOptions {
input : String?
out : String?
direction : TsmbtDirection
module_spec : String?
import_rewrite_path : String?
facade : Bool
}
///|
fn parse_tsmbt_direction(value : String) -> TsmbtDirection? {
match value {
"auto" => Some(TsmbtDirection::Auto)
"ts-to-mbt" | "ts2mbt" | "ts" => Some(TsmbtDirection::TsToMbt)
"mbt-to-ts" | "mbt2ts" | "mbt" => Some(TsmbtDirection::MbtToTs)
_ => None
}
}
///|
fn parse_unified_tsmbt_options(
args : Array[String],
start : Int,
) -> (UnifiedTsmbtOptions?, String?) {
let mut input : String? = None
let mut out : String? = None
let mut direction = TsmbtDirection::Auto
let mut module_spec : String? = None
let mut import_rewrite_path : String? = None
let mut facade = true
let mut idx = start
while idx < args.length() {
let arg = args[idx]
match arg {
"--help" | "-h" => return (None, Some("help"))
"--input" | "-i" => {
if idx + 1 >= args.length() {
return (None, Some("missing value for \{arg}"))
}
input = Some(args[idx + 1])
idx += 2
continue
}
"--out" | "-o" => {
if idx + 1 >= args.length() {
return (None, Some("missing value for \{arg}"))
}
out = Some(args[idx + 1])
idx += 2
continue
}
"--direction" => {
if idx + 1 >= args.length() {
return (None, Some("missing value for --direction"))
}
match parse_tsmbt_direction(args[idx + 1]) {
Some(parsed) => direction = parsed
None =>
return (
None,
Some(
"invalid --direction '\{args[idx + 1]}', expected auto, mbt-to-ts, or ts-to-mbt",
),
)
}
idx += 2
continue
}
"--module-spec" | "--runtime-module" => {
if idx + 1 >= args.length() {
return (None, Some("missing value for \{arg}"))
}
module_spec = Some(args[idx + 1])
idx += 2
continue
}
"--import-rewrites" | "--import-rewrite" => {
if idx + 1 >= args.length() {
return (None, Some("missing value for \{arg}"))
}
import_rewrite_path = Some(args[idx + 1])
idx += 2
continue
}
"--facade" => {
facade = true
idx += 1
continue
}
"--no-facade" => {
facade = false
idx += 1
continue
}
_ => ()
}
if arg.has_prefix("--input=") {
input = Some(arg["--input=".length():arg.length()].to_string())
} else if arg.has_prefix("--out=") {
out = Some(arg["--out=".length():arg.length()].to_string())
} else if arg.has_prefix("--direction=") {
let value = arg["--direction=".length():arg.length()].to_string()
match parse_tsmbt_direction(value) {
Some(parsed) => direction = parsed
None =>
return (
None,
Some(
"invalid --direction '\{value}', expected auto, mbt-to-ts, or ts-to-mbt",
),
)
}
} else if arg.has_prefix("--module-spec=") {
module_spec = Some(
arg["--module-spec=".length():arg.length()].to_string(),
)
} else if arg.has_prefix("--runtime-module=") {
module_spec = Some(
arg["--runtime-module=".length():arg.length()].to_string(),
)
} else if arg.has_prefix("--import-rewrites=") {
import_rewrite_path = Some(
arg["--import-rewrites=".length():arg.length()].to_string(),
)
} else if arg.has_prefix("--import-rewrite=") {
import_rewrite_path = Some(
arg["--import-rewrite=".length():arg.length()].to_string(),
)
} else if !arg.has_prefix("-") && input is None {
input = Some(arg)
} else if !arg.has_prefix("-") && out is None {
out = Some(arg)
} else {
return (None, Some("unknown option: \{arg}"))
}
idx += 1
}
(
Some({ input, out, direction, module_spec, import_rewrite_path, facade }),
None,
)
}
///|
async fn main_file_exists(path : String) -> Bool {
let kind = @fs.kind(path, follow_symlink=false) catch {
_ => @fs.FileKind::Unknown
}
kind is @fs.FileKind::Regular
}
///|
fn unified_tsmbt_skip_scan_dir(name : String) -> Bool {
name == ".git" ||
name == ".mooncakes" ||
name == "_build" ||
name == "target" ||
name == "node_modules" ||
name == "test262"
}
///|
async fn collect_pkg_generated_mbti_files(
path : String,
out : Array[String],
) -> Unit {
let kind = @fs.kind(path, follow_symlink=false) catch {
_ => @fs.FileKind::Unknown
}
match kind {
@fs.FileKind::Directory => {
let entries = @fs.readdir(
path,
include_hidden=false,
include_special=false,
sort=true,
) catch {
_ => []
}
for name in entries {
if unified_tsmbt_skip_scan_dir(name) {
continue
}
collect_pkg_generated_mbti_files(main_join_path(path, name), out)
}
}
@fs.FileKind::Regular =>
if path.has_suffix("pkg.generated.mbti") {
out.push(path)
}
_ => ()
}
}
///|
fn parse_mbti_package_name_from_source(source : String) -> String? {
for line_view in source.split("\n") {
let line = line_view.trim().to_string()
if !line.has_prefix("package \"") {
continue
}
let rest = line["package \"".length():line.length()].to_string()
match rest.find("\"") {
Some(end_idx) => return Some(rest[:end_idx].to_string())
None => ()
}
}
None
}
///|
async fn resolve_unified_mbt_input_path(input : String) -> String? {
let roots : Array[String] = ["."]
match infer_ghq_github_root_from_cwd() {
Some(root) => roots.push(root)
None => ()
}
resolve_unified_mbt_input_path_from_roots(input, roots)
}
///|
fn ghq_candidate_repo_paths(
github_root : String,
input : String,
) -> Array[String] {
let paths = [main_join_path(github_root, input)]
if !input.has_suffix(".mbt") {
paths.push(main_join_path(github_root, input + ".mbt"))
}
paths
}
///|
async fn ghq_module_name_candidate_repo_paths(
github_root : String,
input : String,
) -> Array[String] {
let paths : Array[String] = []
let owner = match input.find("/") {
Some(sep_idx) => input[:sep_idx].to_string()
None => return paths
}
let owner_root = main_join_path(github_root, owner)
if !@fs.exists(owner_root) {
return paths
}
let entries = @fs.readdir(
owner_root,
include_hidden=false,
include_special=false,
sort=true,
) catch {
_ => return paths
}
for name in entries {
if unified_tsmbt_skip_scan_dir(name) {
continue
}
let repo_path = main_join_path(owner_root, name)
let kind = @fs.kind(repo_path, follow_symlink=false) catch {
_ => @fs.FileKind::Unknown
}
if !(kind is @fs.FileKind::Directory) {
continue
}
let moon_mod_path = main_join_path(repo_path, "moon.mod.json")
if !main_file_exists(moon_mod_path) {
continue
}
let moon_mod_source = @fs.read_file(moon_mod_path).text() catch {
_ => continue
}
match parse_moon_mod_string_field(moon_mod_source, "name") {
Some(module_name) if module_name == input => paths.push(repo_path)
_ => ()
}
}
paths
}
///|
async fn infer_ghq_github_root_from_cwd() -> String? {
let cwd = @fs.realpath(".") catch { _ => "." }
match cwd.find("/ghq/github.com/") {
Some(idx) => Some(cwd[:idx + "/ghq/github.com".length()].to_string())
None => None
}
}
///|
async fn resolve_unified_mbt_input_path_from_roots(
input : String,
roots : Array[String],
) -> String? {
if main_file_exists(input) {
return Some(input)
}
let direct_pkg_path = main_join_path(input, "pkg.generated.mbti")
if main_file_exists(direct_pkg_path) {
return Some(direct_pkg_path)
}
for root in roots {
let scan_roots = if root.has_suffix("/github.com") && input.contains("/") {
let paths = ghq_candidate_repo_paths(root, input)
for path in ghq_module_name_candidate_repo_paths(root, input) {
if !paths.contains(path) {
paths.push(path)
}
}
paths
} else {
[root]
}
for scan_root in scan_roots {
if !@fs.exists(scan_root) {
continue
}
let candidates : Array[String] = []
collect_pkg_generated_mbti_files(scan_root, candidates)
for path in candidates {
let source = @fs.read_file(path).text() catch { _ => continue }
match parse_mbti_package_name_from_source(source) {
Some(package_name) if package_name == input => return Some(path)
_ => ()
}
}
}
}
None
}
///|
fn is_ts_input_path_like(input : String) -> Bool {
input.has_suffix(".d.ts") ||
input.has_suffix(".d.mts") ||
input.has_suffix(".d.cts") ||
input.has_suffix(".ts") ||
input.has_suffix(".tsx") ||
input.has_suffix(".mts") ||
input.has_suffix(".cts")
}
///|
async fn resolve_unified_ts_input_path(input : String) -> String? {
if main_file_exists(input) {
return Some(input)
}
@parser.resolve_type_module_specifier("__tsmbt_entry__.ts", input)
}
///|
async fn infer_unified_tsmbt_direction(input : String) -> TsmbtDirection? {
if is_ts_input_path_like(input) {
return Some(TsmbtDirection::TsToMbt)
}
if input.has_suffix(".mbti") {
return Some(TsmbtDirection::MbtToTs)
}
match resolve_unified_mbt_input_path(input) {
Some(_) => return Some(TsmbtDirection::MbtToTs)
None => ()
}
match resolve_unified_ts_input_path(input) {
Some(_) => Some(TsmbtDirection::TsToMbt)
None => None
}
}
///|
async fn emit_unified_tsmbt_scaffold(
input : String,
out : String,
direction~ : TsmbtDirection,
module_spec~ : String?,
import_rewrite_path~ : String?,
facade~ : Bool,
) -> Bool {
let resolved_direction = match direction {
TsmbtDirection::Auto =>
match infer_unified_tsmbt_direction(input) {
Some(direction) => direction
None => {
println(
"Error: could not infer direction for '\{input}'. Use --direction mbt-to-ts or --direction ts-to-mbt.",
)
return false
}
}
_ => direction
}
match resolved_direction {
TsmbtDirection::MbtToTs => {
let mbti_path = match resolve_unified_mbt_input_path(input) {
Some(path) => path
None => {
println(
"Error: could not resolve MoonBit package or pkg.generated.mbti '\{input}'. Run moon info first or pass a pkg.generated.mbti path.",
)
return false
}
}
if facade {
emit_typescript_facade_scaffold_from_mbti(
mbti_path, out, import_rewrite_path,
)
} else {
emit_typescript_scaffold_from_mbti(mbti_path, out, import_rewrite_path)
}
}
TsmbtDirection::TsToMbt => {
let entry_path = match resolve_unified_ts_input_path(input) {
Some(path) => path
None => {
println(
"Error: could not resolve TypeScript entrypoint '\{input}'. Pass a .d.ts/.ts path or an installed package specifier.",
)
return false
}
}
let runtime_module_spec = match module_spec {
Some(spec) => spec
None => input
}
emit_moonbit_scaffold_from_ts(entry_path, runtime_module_spec, out)
true
}
TsmbtDirection::Auto => true
}
}
///|
async fn run_unified_tsmbt_cli(args : Array[String], start : Int) -> Bool {
let options = match parse_unified_tsmbt_options(args, start) {
(Some(options), None) => options
(_, Some("help")) => {
print_usage()
return true
}
(_, Some(message)) => {
println("Error: \{message}")
print_usage()
return false
}
_ => {
print_usage()
return false
}
}
let input = match options.input {
Some(input) => input
None => {
println("Error: expected --input ")
print_usage()
return false
}
}
let out = match options.out {
Some(out) => out
None => {
println("Error: expected --out ")
print_usage()
return false
}
}
emit_unified_tsmbt_scaffold(
input,
out,
direction=options.direction,
module_spec=options.module_spec,
import_rewrite_path=options.import_rewrite_path,
facade=options.facade,
)
}