///|
priv enum TsmbtDirection {
Auto
TsToMbt
MbtToTs
}
///|
priv struct UnifiedTsmbtOptions {
input : String?
out : String?
direction : TsmbtDirection
module_spec : String?
import_rewrite_path : String?
diagnostics_path : String?
strict : Bool
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 diagnostics_path : String? = None
let mut strict = false
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
}
"--diagnostics" => {
if idx + 1 >= args.length() {
return (None, Some("missing value for --diagnostics"))
}
diagnostics_path = Some(args[idx + 1])
idx += 2
continue
}
"--strict" => {
strict = true
idx += 1
continue
}
"--no-strict" => {
strict = false
idx += 1
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("--diagnostics=") {
diagnostics_path = Some(
arg["--diagnostics=".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,
diagnostics_path,
strict,
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)
}
let scan_order : Array[String] = []
for root in roots {
if root.has_suffix("/github.com") && !scan_order.contains(root) {
scan_order.push(root)
}
}
for root in roots {
if !root.has_suffix("/github.com") && !scan_order.contains(root) {
scan_order.push(root)
}
}
for root in scan_order {
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
}
}
///|
fn unified_ts_scaffold_diagnostics_path(
output_dir : String,
diagnostics_path : String?,
) -> String {
match diagnostics_path {
Some(path) => path
None => main_join_path(output_dir, "SCAFFOLD_DIAGNOSTICS.md")
}
}
///|
fn unified_ts_fallback_policy_for_package(
package_spec : String,
) -> (String, String) {
match package_spec {
"clsx"
| "node:path"
| "node:crypto"
| "node:os"
| "node:url"
| "node:querystring"
| "node:buffer" =>
(
"zero-target", "Keep public JSValue surface at zero; any fallback is a regression.",
)
"hono" =>
(
"naturalize-target", "Reduce generic context/router fallbacks while keeping route handlers and response helpers natural.",
)
"react-router" =>
(
"naturalize-target", "Reduce route/path utility overload and option-object fallbacks; keep typed navigation helpers usable from MoonBit.",
)
"jose" =>
(
"naturalize-target", "Reduce builder option and compact JWS/JWT overload fallbacks around the smoke-tested APIs.",
)
"glob" =>
(
"naturalize-target", "Reduce pattern/options namespace fallbacks for common sync glob calls.",
)
"node:assert" | "node:util" =>
(
"naturalize-target", "Shrink remaining Node built-in overload/unknown fallbacks toward zero for the common API surface.",
)
"date-fns" | "magic-string" | "source-map" | "node:sqlite" | "node:fs" =>
(
"naturalize-target", "Keep reducing fallback around finite option bags, tuple results, and class/value helper APIs.",
)
"zod" | "valibot" =>
(
"budgeted-fallback", "Schema/parser generics are intentionally smoke-tested and budgeted, not treated as naturally typed MoonBit APIs yet.",
)
"preact" =>
(
"budgeted-fallback", "JSX/component/children generics are intentionally budgeted until a dedicated JSX/component binding layer exists.",
)
"playwright" =>
(
"budgeted-fallback", "Large event/callback-heavy API is smoke-tested; only selected launch/device/options surfaces are naturalization targets.",
)
"chalk"
| "dotenv"
| "ignore"
| "colorette"
| "immer"
| "execa"
| "vitest/runtime"
| "express" =>
(
"low-fallback-maintain", "Current fallback is small and explicitly budgeted; naturalize only when a real smoke use case needs it.",
)
_ =>
(
"unclassified", "Add an explicit fallback policy before accepting this package into the real-world corpus.",
)
}
}
///|
fn render_unified_ts_fallback_policy_md(module_spec : String) -> String {
let (class_name, policy) = unified_ts_fallback_policy_for_package(module_spec)
[
"## Fallback Policy",
"",
"This classification mirrors the real-world bridge quality policy. It is informational in non-strict mode; strict mode still rejects generated `JSValue` fallbacks.",
"",
"| package | class | policy |",
"| --- | --- | --- |",
"| `\{module_spec}` | \{class_name} | \{policy} |",
].join("\n")
}
///|
fn render_unified_ts_scaffold_diagnostics_md(
module_spec : String,
unsupported_exports : Array[String],
jsvalue_fallbacks : Array[String],
) -> String {
let mut base = render_moonbit_scaffold_diagnostics_md(unsupported_exports)
// Avoid the contradictory "No unsupported exports were detected." banner
// when the JSValue fallback list below it is non-empty: those JSValue
// occurrences are themselves widened TypeScript boundary surfaces and
// count as unsupported in any practical sense.
if unsupported_exports.length() == 0 && jsvalue_fallbacks.length() > 0 {
base = base.replace(
old="No unsupported exports were detected.",
new="No structural unsupported exports were detected; \{jsvalue_fallbacks.length()} `JSValue` boundary fallbacks are listed below.",
)
}
let fallback_policy = render_unified_ts_fallback_policy_md(module_spec)
if jsvalue_fallbacks.length() == 0 {
return [base, "", fallback_policy].join("\n")
}
let lines = [
base, "", fallback_policy, "", "## JSValue Fallbacks", "", "Strict mode treats these generated `JSValue` occurrences as unbudgeted TypeScript boundary fallbacks.",
"",
]
for item in jsvalue_fallbacks {
lines.push("- " + item)
}
lines.join("\n")
}
///|
async fn write_unified_ts_scaffold_diagnostics(
output_dir : String,
diagnostics_path : String?,
module_spec : String,
unsupported_exports : Array[String],
jsvalue_fallbacks : Array[String],
) -> Bool {
let path = unified_ts_scaffold_diagnostics_path(output_dir, diagnostics_path)
if !write_text_file(
path,
render_unified_ts_scaffold_diagnostics_md(
module_spec, unsupported_exports, jsvalue_fallbacks,
),
) {
return false
}
println("Wrote scaffold diagnostics to \{path}")
true
}
///|
fn collect_unified_jsvalue_fallbacks_from_source(
relative_path : String,
source : String,
out : Array[String],
) -> Unit {
let mut line_no = 1
for line_view in source.split("\n") {
let line = line_view.trim().to_string()
if unified_jsvalue_line_is_fallback(line) {
out.push("\{relative_path}:\{line_no}: \{line}")
}
line_no += 1
}
}
///|
fn unified_jsvalue_line_is_fallback(line : String) -> Bool {
if !line.contains("JSValue") {
return false
}
match line {
"/// Complex or unsupported TypeScript types are widened to JSValue."
| "declare pub type JSValue"
| "pub type JSValue"
| "pub type JSValue = @js.Any" => false
_ => true
}
}
///|
async fn collect_unified_moonbit_scaffold_jsvalue_fallbacks(
output_dir : String,
) -> Array[String] {
let fallbacks : Array[String] = []
for relative_path in ["bridge.mbti", "bridge.mbt"] {
let path = main_join_path(output_dir, relative_path)
let source = @fs.read_file(path).text() catch { _ => continue }
collect_unified_jsvalue_fallbacks_from_source(
relative_path, source, fallbacks,
)
}
fallbacks.sort()
fallbacks
}
///|
fn unified_autolink_diagnostics_has_omissions(source : String) -> Bool {
source.contains("\n## ")
}
///|
async fn write_requested_unified_autolink_diagnostics(
output_dir : String,
diagnostics_path : String?,
) -> Bool {
let path = match diagnostics_path {
Some(path) => path
None => return true
}
let autolink_path = main_join_path(output_dir, "AUTOLINK_DIAGNOSTICS.md")
let diagnostics_md = @fs.read_file(autolink_path).text() catch {
e => {
println("Read error: \{e}")
return false
}
}
if !write_text_file(path, diagnostics_md) {
return false
}
println("Wrote autolink diagnostics to \{path}")
true
}
///|
async fn emit_unified_tsmbt_scaffold(
input : String,
out : String,
direction~ : TsmbtDirection,
module_spec~ : String?,
import_rewrite_path~ : String?,
diagnostics_path? : String? = None,
strict? : Bool = false,
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
}
}
let ok = 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)
}
if !ok {
return false
}
let diagnostics_md = @fs.read_file(
main_join_path(out, "AUTOLINK_DIAGNOSTICS.md"),
).text() catch {
e => {
println("Read error: \{e}")
return false
}
}
if !write_requested_unified_autolink_diagnostics(out, diagnostics_path) {
return false
}
if strict && unified_autolink_diagnostics_has_omissions(diagnostics_md) {
println(
"Strict mode rejected MoonBit scaffold: omitted autolink members detected.",
)
return false
}
true
}
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
}
let unsupported_exports = collect_moonbit_ts_scaffold_unsupported_exports(
entry_path,
) catch {
@bridge.ModuleGraphError::ReadError(msg)
| @bridge.ModuleGraphError::ParseError(msg)
| @bridge.ModuleGraphError::ResolveError(msg) => {
println("Emit error: \{msg}")
return false
}
}
if strict && unsupported_exports.length() > 0 {
if !write_unified_ts_scaffold_diagnostics(
out,
diagnostics_path,
runtime_module_spec,
unsupported_exports,
[],
) {
return false
}
println(
"Strict mode rejected TypeScript scaffold: unsupported exports detected.",
)
return false
}
if !emit_moonbit_scaffold_from_ts(
entry_path,
runtime_module_spec,
out,
write_diagnostics=false,
) {
return false
}
let jsvalue_fallbacks = collect_unified_moonbit_scaffold_jsvalue_fallbacks(
out,
)
if !write_unified_ts_scaffold_diagnostics(
out, diagnostics_path, runtime_module_spec, unsupported_exports, jsvalue_fallbacks,
) {
return false
}
if strict && jsvalue_fallbacks.length() > 0 {
println(
"Strict mode rejected TypeScript scaffold: JSValue fallbacks detected.",
)
return false
}
true
}
TsmbtDirection::Auto => true
}
}
///|
/// Run the unified `--input/--out/...` driver with a forced direction.
/// `print_help` is invoked on `--help`, missing required flags, or option
/// errors so each cmd module owns its own help banner.
async fn run_unified_cli_with_direction(
args : Array[String],
start : Int,
forced_direction~ : TsmbtDirection,
print_help~ : () -> Unit,
) -> Bool {
let options = match parse_unified_tsmbt_options(args, start) {
(Some(options), None) => options
(_, Some("help")) => {
print_help()
return true
}
(_, Some(message)) => {
println("Error: \{message}")
print_help()
return false
}
_ => {
print_help()
return false
}
}
let direction = match (forced_direction, options.direction) {
(TsmbtDirection::Auto, requested) => requested
(TsmbtDirection::TsToMbt, TsmbtDirection::Auto)
| (TsmbtDirection::TsToMbt, TsmbtDirection::TsToMbt) =>
TsmbtDirection::TsToMbt
(TsmbtDirection::MbtToTs, TsmbtDirection::Auto)
| (TsmbtDirection::MbtToTs, TsmbtDirection::MbtToTs) =>
TsmbtDirection::MbtToTs
_ => {
println(
"Error: --direction conflicts with this command's fixed direction.",
)
return false
}
}
let input = match options.input {
Some(input) => input
None => {
println("Error: expected --input ")
print_help()
return false
}
}
let out = match options.out {
Some(out) => out
None => {
println("Error: expected --out ")
print_help()
return false
}
}
emit_unified_tsmbt_scaffold(
input,
out,
direction~,
module_spec=options.module_spec,
import_rewrite_path=options.import_rewrite_path,
diagnostics_path=options.diagnostics_path,
strict=options.strict,
facade=options.facade,
)
}
///|
/// Public entry: run the unified TS -> MoonBit scaffold driver with the
/// direction locked to `ts-to-mbt`. Used by `src/cmd/ts2mbt`.
pub async fn run_ts_to_mbt_unified_cli(
args : Array[String],
start : Int,
print_help~ : () -> Unit,
) -> Bool {
run_unified_cli_with_direction(
args,
start,
forced_direction=TsmbtDirection::TsToMbt,
print_help~,
)
}
///|
/// Public entry: run the unified MoonBit -> TypeScript scaffold driver with
/// the direction locked to `mbt-to-ts`. Used by `src/cmd/mbt2ts`.
pub async fn run_mbt_to_ts_unified_cli(
args : Array[String],
start : Int,
print_help~ : () -> Unit,
) -> Bool {
run_unified_cli_with_direction(
args,
start,
forced_direction=TsmbtDirection::MbtToTs,
print_help~,
)
}