///|
priv struct ParsedDev {
help : Bool
json : Bool
project_root : String
runner : String?
runner_args : Array[String]
}
///|
fn dev_usage() -> String {
(
#|Usage: moui dev [package] [options]
#|
#|Build and watch a MoUI project. Native targets restart the app after a
#|successful rebuild. Web targets serve the project with refresh and a
#|compiler-error overlay.
#|
#|Arguments:
#| package Package path. Defaults to web_wasm or app.
#|
#|Options:
#| --package PATH Package path (same as the positional argument).
#| --target TARGET MoonBit target (native or wasm-gc).
#| --project-root PATH Project root. Defaults to the current directory.
#| --interval-ms MS Watch interval. Defaults to 500.
#| --port PORT Web server port. Defaults to 3000.
#| --state PATH Optional state file passed as MOUI_DEV_STATE_FILE.
#| --runner PATH Node runner path. Defaults to .moui/dev.mjs or scripts/moui-dev.mjs.
#| --web Serve Web output and inject the dev client.
#| --once, --build-only Build once and exit.
#| --json Emit machine-readable status lines.
#| -h, --help Show this help.
)
}
///|
fn dev_int_option(value : String, name : String) -> Result[String, String] {
match parse_int(value) {
Ok(number) if number > 0 => Ok(value)
_ => Err(name + " must be a positive integer")
}
}
///|
fn parse_dev_args(
args : Array[String],
cwd : String,
) -> Result[ParsedDev, String] {
let help = Ref(false)
let json = Ref(false)
let project_root = Ref(cwd)
let runner : Ref[String?] = Ref(None)
let package_name : Ref[String?] = Ref(None)
let runner_args : Array[String] = []
let positional : Array[String] = []
let mut index = 0
while index < args.length() {
let argument = args[index]
let (name, inline) = option_parts(argument)
match name {
"-h" | "--help" =>
match flag_without_value(inline, name) {
Ok(_) => help.val = true
Err(message) => return Err(message)
}
"--json" =>
match flag_without_value(inline, name) {
Ok(_) => json.val = true
Err(message) => return Err(message)
}
"--web" | "--once" | "--build-only" =>
match flag_without_value(inline, name) {
Ok(_) => runner_args.push(name)
Err(message) => return Err(message)
}
"--package"
| "--target"
| "--project-root"
| "--interval-ms"
| "--port"
| "--state"
| "--runner" => {
let (value, next) = match option_value(args, index, inline, name) {
Ok(result) => result
Err(message) => return Err(message)
}
match name {
"--package" => package_name.val = Some(value)
"--project-root" => project_root.val = make_absolute(value, cwd)
"--runner" => runner.val = Some(make_absolute(value, cwd))
"--interval-ms" | "--port" =>
match dev_int_option(value, name) {
Ok(validated) => runner_args.push(name + "=" + validated)
Err(message) => return Err(message)
}
"--target" | "--state" => runner_args.push(name + "=" + value)
_ => ()
}
index = next
continue
}
value if !value.has_prefix("-") => positional.push(value)
_ => return Err("unknown option for moui dev: " + name)
}
index += 1
}
if positional.length() > 1 ||
(positional.length() == 1 && package_name.val is Some(_)) {
return Err("moui dev accepts at most one package path")
}
if package_name.val is None && positional.length() == 1 {
runner_args.push("--package=" + positional[0])
} else if package_name.val is Some(value) {
runner_args.push("--package=" + value)
}
if json.val {
runner_args.push("--json")
}
Ok({
help: help.val,
json: json.val,
project_root: project_root.val,
runner: runner.val,
runner_args,
})
}
///|
fn dev_runner_candidates(
project_root : String,
requested : String?,
) -> Array[String] {
let candidates : Array[String] = []
match requested {
Some(path) => candidates.push(path)
None => ()
}
match @env.get_env_var("MOUI_DEV_RUNNER") {
Some(path) => candidates.push(path)
None => ()
}
candidates.push(join_path(project_root, ".moui/dev.mjs"))
candidates.push(join_path(project_root, "scripts/moui-dev.mjs"))
let current = Ref(normalized_path(project_root))
for _ in 0..<64 {
candidates.push(join_path(current.val, "scripts/moui-dev.mjs"))
let parent = parent_dir(current.val)
if parent == current.val {
break
}
current.val = parent
}
candidates
}
///|
fn run_dev(parsed : ParsedDev) -> CliOutcome {
if parsed.help {
return stdout_outcome(dev_usage())
}
let runner = match
first_existing(dev_runner_candidates(parsed.project_root, parsed.runner)) {
Some(path) => path
None =>
return failed_outcome(
"moui dev runner not found; generate a project with `moui new` or set MOUI_DEV_RUNNER",
parsed.json,
dev_usage(),
)
}
let argv = ["node", runner]
argv.push("--project-root=" + parsed.project_root)
for argument in parsed.runner_args {
argv.push(argument)
}
match process_exec_argv(argv, cwd=parsed.project_root, capture=false) {
Ok(result) =>
if parsed.json {
let value : Json = {
"schemaVersion": 1,
"ok": result.exit_code == 0,
"command": "dev",
"runner": runner,
"exitCode": result.exit_code,
}
if result.exit_code == 0 {
stdout_outcome(value.stringify(indent=2) + "\n")
} else {
failed_outcome(
"moui dev runner exited with code " + result.exit_code.to_string(),
true,
"",
)
}
} else if result.exit_code == 0 {
stdout_outcome("")
} else {
failed_outcome(
"moui dev runner exited with code " + result.exit_code.to_string(),
false,
"",
)
}
Err(message) => failed_outcome(message, parsed.json, dev_usage())
}
}