///|
/// Vendor a single TypeScript package's types into the consumer's
/// `/internal/generated//` sub-package.
///
/// `pkg_spec` is an npm-style specifier (`hono`, `@types/react`,
/// `@scope/foo`). The resolver walks the consumer's `node_modules` to
/// find the type entrypoint, generates a MoonBit bridge package, and
/// writes it under `/internal/generated//`.
///
/// `module_spec_override` overrides the runtime import the generated
/// `bridge.js` uses. Defaults to `pkg_spec` (or, for `@types/`
/// inputs, the unscoped runtime name).
///
/// `vendor_root_override` overrides the output root. Defaults to
/// `/internal/generated`.
pub async fn ts2mbt_vendor_package(
pkg_spec : String,
module_spec_override? : String? = None,
vendor_root_override? : String? = None,
) -> Bool {
let vendor_root = match vendor_root_override {
Some(root) => root
None =>
match resolve_default_vendor_root() {
Some(root) => root
None => {
println(
"vendor: could not resolve `/internal/generated`; run inside a moon module or pass --out ",
)
return false
}
}
}
let entry_path = match
@parser.resolve_type_module_specifier("__tsmbt_entry__.ts", pkg_spec) {
Some(path) => path
None => {
println(
"vendor: could not resolve TypeScript types for '\{pkg_spec}'; check that it's installed in node_modules",
)
return false
}
}
let runtime_spec = match module_spec_override {
Some(spec) => spec
None => default_runtime_module_spec(pkg_spec)
}
let safe_dir = vendor_safe_pkg_dir(pkg_spec)
let output_dir = main_join_path(vendor_root, safe_dir)
let bare_specifier = "@tsmbt-bridge/\{safe_dir}"
println(
"vendor: \{pkg_spec} -> \{output_dir} (#module \"\{bare_specifier}\")",
)
// `emit_moonbit_scaffold_from_ts` writes `bridge.{mbti,mbt,js}`,
// `package.json`, `moon.pkg.json`, and refreshes the
// `/node_modules/@tsmbt-bridge/` symlink so the bare
// `#module("@tsmbt-bridge/")` baked into `bridge.mbt` resolves
// at build time.
emit_moonbit_scaffold_from_ts(
entry_path,
runtime_spec,
output_dir,
bare_module_specifier=Some(bare_specifier),
)
}
///|
/// Vendor every dependency listed in a `package.json` (`dependencies` +
/// `devDependencies`). Emits a per-package summary at the end and
/// returns `true` only when every package generated successfully.
///
/// `package_json_path_override` defaults to `./package.json`.
pub async fn ts2mbt_sync_package_json(
package_json_path_override? : String? = None,
vendor_root_override? : String? = None,
) -> Bool {
let package_json_path = match package_json_path_override {
Some(path) => path
None => "package.json"
}
let deps = read_package_json_dependency_names(package_json_path)
if deps.length() == 0 {
println(
"sync: no dependencies / devDependencies found in '\{package_json_path}'",
)
return true
}
let succeeded : Array[String] = []
let failed : Array[String] = []
for pkg_spec in deps {
if ts2mbt_vendor_package(pkg_spec, vendor_root_override~) {
succeeded.push(pkg_spec)
} else {
failed.push(pkg_spec)
}
}
println("")
println(
"sync summary: \{succeeded.length()} ok, \{failed.length()} failed (of \{deps.length()})",
)
if failed.length() > 0 {
println("failed packages:")
for pkg in failed {
println(" - \{pkg}")
}
}
failed.length() == 0
}
///|
/// Sanitize an npm package specifier into a directory-name-safe slug.
/// `@scope/foo-bar` -> `scope__foo_bar`; `react` -> `react`.
fn vendor_safe_pkg_dir(name : String) -> String {
let mut result = ""
for c in name {
if c == '@' {
// Strip leading scope marker
()
} else if (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') {
result += c.to_string()
} else if c == '/' {
result += "__"
} else {
result += "_"
}
}
if result == "" {
"package"
} else {
result
}
}
///|
/// Default runtime module spec for a package vendored from node_modules.
/// `@types/react` -> `react`; everything else passes through.
fn default_runtime_module_spec(pkg_spec : String) -> String {
let prefix = "@types/"
if pkg_spec.has_prefix(prefix) {
pkg_spec[prefix.length():pkg_spec.length()].to_string()
} else {
pkg_spec
}
}
///|
/// Resolve `/internal/generated` from the cwd's
/// nearest `moon.mod.json`. Returns `None` if no module is found.
async fn resolve_default_vendor_root() -> String? {
let module_root = match find_nearest_moon_mod_dir(".") {
Some(root) => root
None => return None
}
let moon_mod_path = main_join_path(module_root, "moon.mod.json")
let source = @fs.read_file(moon_mod_path).text() catch { _ => return None }
let source_dir_rel = match parse_moon_mod_string_field(source, "source") {
Some(value) => value
None => "src"
}
let source_dir = if source_dir_rel == "" || source_dir_rel == "." {
module_root
} else {
main_join_path(module_root, source_dir_rel)
}
Some(main_join_path(source_dir, "internal/generated"))
}
///|
/// Read the names listed under `dependencies` and `devDependencies` in
/// the given `package.json`. Order: deps first, devDeps second, with
/// each block in the order JSON returned. Duplicates are de-duped.
async fn read_package_json_dependency_names(path : String) -> Array[String] {
let result : Array[String] = []
let source = @fs.read_file(path).text() catch {
_ => {
println("vendor: could not read '\{path}'")
return result
}
}
let json = @json.parse(source) catch {
_ => {
println("vendor: '\{path}' is not valid JSON")
return result
}
}
guard json is Object(members) else { return result }
for field in ["dependencies", "devDependencies"] {
for pair in members {
let (name, value) = pair
if name != field {
continue
}
guard value is Object(deps) else { continue }
for dep_pair in deps {
let (dep_name, _) = dep_pair
if !result.contains(dep_name) {
result.push(dep_name)
}
}
}
}
result
}