// Sandboxed Marketplace entry point for `mpkl`.
//
// The native / JS CLI resolves files, imports, packages, and network resources
// asynchronously. `moon runwasm` exposes a synchronous local-file subset: it
// accepts either one self-contained module via `--source` or a filesystem entry
// point whose relative imports and module relations are loaded recursively.
///|
priv struct WasmCommandResult {
exit_code : Int
output : String
}
///|
fn wasm_usage() -> String {
"usage: mpkl (--source | ) [--format ] [--package-cache ]"
}
///|
fn wasm_result(exit_code : Int, output : String) -> WasmCommandResult {
{ exit_code, output }
}
///|
fn render_wasm_diagnostics(diagnostics : Array[@pkl.Diagnostic]) -> String {
let output = StringBuilder()
let mut first = true
for diagnostic in diagnostics {
if first {
first = false
} else {
output.write_char('\n')
}
output.write_string(
"error[\{diagnostic.start}..\{diagnostic.end}]: \{diagnostic.message}",
)
}
output.to_string()
}
///|
fn trim_final_newline(text : String) -> String {
if text.has_suffix("\n") {
String::unsafe_substring(text, start=0, end=text.length() - 1)
} else {
text
}
}
///|
fn is_wasm_format_supported(format : String) -> Bool {
format == "pcf" ||
format == "json" ||
format == "yaml" ||
format == "properties" ||
format == "plist" ||
format == "textproto" ||
format == "xml" ||
format == "jsonnet"
}
///|
fn render_wasm_value(value : @pkl.Value, format : String) -> String {
let rendered = match format {
"json" => @pkl.render_value_as_json(value)
"yaml" => @pkl.render_value_as_yaml(value)
"properties" => @pkl.render_value_as_properties(value)
"plist" => @pkl.render_value_as_plist(value)
"textproto" => @pkl.render_value_as_textproto(value)
"xml" => @pkl.render_value_as_xml(value)
"jsonnet" => @pkl.render_value_as_jsonnet_document(value)
_ => @pkl.render_value(value)
}
trim_final_newline(rendered)
}
///|
fn wasm_stack_contains(stack : Array[String], path : String) -> Bool {
for item in stack {
if item == path {
return true
}
}
false
}
///|
fn push_wasm_stack(stack : Array[String], path : String) -> Array[String] {
let next : Array[String] = []
for item in stack {
next.push(item)
}
next.push(path)
next
}
///|
fn normalize_wasm_path(path : String) -> String {
let path = path.replace_all(old="\\", new="/")
let scheme_end = match path.find("://") {
Some(index) => index + 3
None => 0
}
let head = String::unsafe_substring(path, start=0, end=scheme_end)
let body = String::unsafe_substring(path, start=scheme_end, end=path.length())
let leading_slash = body.has_prefix("/")
let raw_segments : Array[String] = []
let mut start = if leading_slash { 1 } else { 0 }
let mut i = start
while i < body.length() {
if body.unsafe_get(i) == '/' {
raw_segments.push(String::unsafe_substring(body, start~, end=i))
start = i + 1
}
i = i + 1
}
if start <= body.length() {
raw_segments.push(String::unsafe_substring(body, start~, end=body.length()))
}
let resolved : Array[String] = []
for segment in raw_segments {
if segment == "" || segment == "." {
continue
}
if segment == ".." {
if resolved.length() > 0 && resolved[resolved.length() - 1] != ".." {
let _ = resolved.pop()
} else if !leading_slash {
resolved.push("..")
}
continue
}
resolved.push(segment)
}
let output = StringBuilder()
output.write_string(head)
if leading_slash {
output.write_char('/')
}
for i, segment in resolved {
if i > 0 {
output.write_char('/')
}
output.write_string(segment)
}
output.to_string()
}
///|
fn is_wasm_absolute_path(path : String) -> Bool {
path.has_prefix("/") ||
(path.length() >= 3 && path[1] == ':' && path[2] == '/')
}
///|
fn resolve_wasm_import_path(from : String, uri : String) -> String? {
if uri.has_prefix("pkl:") {
return Some(uri)
}
if is_package_uri_like(uri) {
return Some(normalize_wasm_path(uri))
}
if is_wasm_absolute_path(uri) {
return Some(normalize_wasm_path(uri))
}
if uri.find(":") is Some(_) {
return None
}
let combined = match from.rev_find("/") {
Some(index) => String::unsafe_substring(from, start=0, end=index + 1) + uri
None => uri
}
Some(normalize_wasm_path(combined))
}
///|
fn read_wasm_source(
path : String,
package_cache_roots : Array[String],
) -> Result[String, String] {
let filesystem_path = if is_package_uri_like(path) {
match resolve_package_uri_to_cache_in_roots(path, "", package_cache_roots) {
Some(cached_path) => cached_path
None =>
return Err(
"Cannot find package module `\{path}` in the configured package cache. Pre-fetch it with native mpkl or pass --package-cache .",
)
}
} else {
path
}
Ok(@fs.read_file_to_string(filesystem_path)) catch {
_ => Err("Cannot find module `\{path}`.")
}
}
///|
fn load_wasm_expr_imports(
session : @pkl.AnalysisSession,
from : String,
expr : @pkl.Expr,
package_cache_roots : Array[String],
stack : Array[String],
loaded : Map[String, Unit],
) -> String? {
match expr {
ImportExpr(uri) =>
match resolve_wasm_import_path(from, uri) {
Some(resolved) =>
load_wasm_path(session, resolved, package_cache_roots, stack, loaded)
None => Some("unsupported module URI in Wasm file mode: \{uri}")
}
ImportGlobExpr(pattern) =>
Some("import glob is not supported in Wasm file mode: \{pattern}")
ObjectLiteral(members) | TypedObjectLiteral(_, members) => {
for object_member in members {
match
load_wasm_expr_imports(
session,
from,
object_member.value,
package_cache_roots,
stack,
loaded,
) {
Some(error) => return Some(error)
None => ()
}
}
None
}
ListingLiteral(elements) | InterpolatedString(elements) => {
for element in elements {
match
load_wasm_expr_imports(
session, from, element, package_cache_roots, stack, loaded,
) {
Some(error) => return Some(error)
None => ()
}
}
None
}
MappingLiteral(entries) => {
for entry in entries {
match
load_wasm_expr_imports(
session,
from,
entry.key,
package_cache_roots,
stack,
loaded,
) {
Some(error) => return Some(error)
None => ()
}
match
load_wasm_expr_imports(
session,
from,
entry.value,
package_cache_roots,
stack,
loaded,
) {
Some(error) => return Some(error)
None => ()
}
}
None
}
MemberAccess(target, _)
| SafeMemberAccess(target, _)
| NonNullExpr(target)
| UnaryExpr(_, target)
| WhenSpread(target) =>
load_wasm_expr_imports(
session, from, target, package_cache_roots, stack, loaded,
)
SubscriptAccess(target, key) | BinaryExpr(_, target, key) =>
match
load_wasm_expr_imports(
session, from, target, package_cache_roots, stack, loaded,
) {
Some(error) => Some(error)
None =>
load_wasm_expr_imports(
session, from, key, package_cache_roots, stack, loaded,
)
}
AmendExpr(base, members) => {
match
load_wasm_expr_imports(
session, from, base, package_cache_roots, stack, loaded,
) {
Some(error) => return Some(error)
None => ()
}
for object_member in members {
match
load_wasm_expr_imports(
session,
from,
object_member.value,
package_cache_roots,
stack,
loaded,
) {
Some(error) => return Some(error)
None => ()
}
}
None
}
CallExpr(callee, arguments) | NullSafeCallExpr(callee, arguments) => {
match
load_wasm_expr_imports(
session, from, callee, package_cache_roots, stack, loaded,
) {
Some(error) => return Some(error)
None => ()
}
for argument in arguments {
match
load_wasm_expr_imports(
session, from, argument, package_cache_roots, stack, loaded,
) {
Some(error) => return Some(error)
None => ()
}
}
None
}
LambdaExpr(_, body, _) =>
load_wasm_expr_imports(
session, from, body, package_cache_roots, stack, loaded,
)
LetExpr(_, _, value, body) =>
match
load_wasm_expr_imports(
session, from, value, package_cache_roots, stack, loaded,
) {
Some(error) => Some(error)
None =>
load_wasm_expr_imports(
session, from, body, package_cache_roots, stack, loaded,
)
}
ConditionalExpr(condition, then_expr, else_expr) => {
match
load_wasm_expr_imports(
session, from, condition, package_cache_roots, stack, loaded,
) {
Some(error) => return Some(error)
None => ()
}
match
load_wasm_expr_imports(
session, from, then_expr, package_cache_roots, stack, loaded,
) {
Some(error) => return Some(error)
None => ()
}
load_wasm_expr_imports(
session, from, else_expr, package_cache_roots, stack, loaded,
)
}
ForGenerator(_, _, source, members, _, _) => {
match
load_wasm_expr_imports(
session, from, source, package_cache_roots, stack, loaded,
) {
Some(error) => return Some(error)
None => ()
}
for object_member in members {
match
load_wasm_expr_imports(
session,
from,
object_member.value,
package_cache_roots,
stack,
loaded,
) {
Some(error) => return Some(error)
None => ()
}
}
None
}
_ => None
}
}
///|
fn load_wasm_path(
session : @pkl.AnalysisSession,
path : String,
package_cache_roots : Array[String],
stack : Array[String],
loaded : Map[String, Unit],
) -> String? {
if path.has_prefix("pkl:") || loaded.contains(path) {
return None
}
if wasm_stack_contains(stack, path) {
return None
}
let source = match read_wasm_source(path, package_cache_roots) {
Ok(source) => source
Err(error) => return Some(error)
}
loaded[path] = ()
session.set_source(path, source)
let parsed = @pkl.parse_source(source)
let next_stack = push_wasm_stack(stack, path)
for import_decl in parsed.program.imports {
if import_decl.is_glob {
return Some(
"import glob is not supported in Wasm file mode: \{import_decl.uri}",
)
}
let resolved = match resolve_wasm_import_path(path, import_decl.uri) {
Some(resolved) => resolved
None =>
return Some(
"unsupported module URI in Wasm file mode: \{import_decl.uri}",
)
}
match
load_wasm_path(session, resolved, package_cache_roots, next_stack, loaded) {
Some(error) => return Some(error)
None => ()
}
}
match parsed.program.module_relation {
Some(relation) => {
let resolved = match resolve_wasm_import_path(path, relation.uri) {
Some(resolved) => resolved
None =>
return Some(
"unsupported module URI in Wasm file mode: \{relation.uri}",
)
}
match
load_wasm_path(
session, resolved, package_cache_roots, next_stack, loaded,
) {
Some(error) => return Some(error)
None => ()
}
}
None => ()
}
for binding in parsed.program.bindings {
match
load_wasm_expr_imports(
session,
path,
binding.value,
package_cache_roots,
next_stack,
loaded,
) {
Some(error) => return Some(error)
None => ()
}
}
match parsed.program.body {
Some(expr) =>
load_wasm_expr_imports(
session, path, expr, package_cache_roots, next_stack, loaded,
)
None => None
}
}
///|
fn run_wasm_source_command(
command : String,
source : String,
format : String,
) -> WasmCommandResult {
match command {
"parse" => {
let parsed = @pkl.parse_source(source)
if parsed.diagnostics.length() > 0 {
wasm_result(1, render_wasm_diagnostics(parsed.diagnostics))
} else {
wasm_result(0, "ok")
}
}
"check" =>
match @pkl.typecheck_source(source) {
TypeOk(typ) => wasm_result(0, @pkl.render_type(typ))
TypeError(diagnostics) =>
wasm_result(1, render_wasm_diagnostics(diagnostics))
}
"eval" =>
match @pkl.eval_source(source) {
EvalOk(value) =>
wasm_result(
0,
render_wasm_value(@pkl.extract_output_value(value), format),
)
EvalError(diagnostics) =>
wasm_result(1, render_wasm_diagnostics(diagnostics))
}
"format" =>
match @pkl.eval_source(source) {
EvalOk(value) => wasm_result(0, render_wasm_value(value, "pcf"))
EvalError(diagnostics) =>
wasm_result(1, render_wasm_diagnostics(diagnostics))
}
_ => wasm_result(2, wasm_usage())
}
}
///|
fn run_wasm_file_command(
command : String,
path : String,
format : String,
configured_package_caches : Array[String],
) -> WasmCommandResult {
let path = normalize_wasm_path(path)
let package_cache_roots = package_cache_roots_from(configured_package_caches)
if command == "parse" {
return match read_wasm_source(path, package_cache_roots) {
Ok(source) => run_wasm_source_command(command, source, format)
Err(error) => wasm_result(1, error)
}
}
let session = @pkl.AnalysisSession::new()
match load_wasm_path(session, path, package_cache_roots, [], Map([])) {
Some(error) => return wasm_result(1, error)
None => ()
}
match command {
"check" =>
match session.typecheck_path(path) {
TypeOk(typ) => wasm_result(0, @pkl.render_type(typ))
TypeError(diagnostics) =>
wasm_result(1, render_wasm_diagnostics(diagnostics))
}
"eval" =>
match session.eval_path(path) {
EvalOk(value) =>
wasm_result(
0,
render_wasm_value(@pkl.extract_output_value(value), format),
)
EvalError(diagnostics) =>
wasm_result(1, render_wasm_diagnostics(diagnostics))
}
"format" =>
match session.eval_path(path) {
EvalOk(value) => wasm_result(0, render_wasm_value(value, "pcf"))
EvalError(diagnostics) =>
wasm_result(1, render_wasm_diagnostics(diagnostics))
}
_ => wasm_result(2, wasm_usage())
}
}
///|
fn run_wasm_command(args : Array[String]) -> WasmCommandResult {
if args.length() == 0 {
return wasm_result(2, wasm_usage())
}
let command = args[0]
if command == "help" || command == "--help" || command == "-h" {
return wasm_result(0, wasm_usage())
}
if command != "parse" &&
command != "check" &&
command != "eval" &&
command != "format" {
return wasm_result(2, "unsupported command: \{command}\n\{wasm_usage()}")
}
let mut file : String? = None
let mut source : String? = None
let package_caches : Array[String] = []
let mut format = "pcf"
let mut format_explicit = false
let mut i = 1
while i < args.length() {
let token = args[i]
if token == "--source" {
if i + 1 >= args.length() {
return wasm_result(2, "--source requires a value")
}
if source is Some(_) {
return wasm_result(2, "--source may only be specified once")
}
source = Some(args[i + 1])
i = i + 2
} else if token == "--format" || token == "-f" {
if i + 1 >= args.length() {
return wasm_result(2, "\{token} requires a value")
}
format = args[i + 1]
format_explicit = true
i = i + 2
} else if token == "--package-cache" {
if i + 1 >= args.length() {
return wasm_result(2, "--package-cache requires a value")
}
package_caches.push(args[i + 1])
i = i + 2
} else if token.has_prefix("-") {
return wasm_result(2, "unsupported argument: \{token}\n\{wasm_usage()}")
} else {
if file is Some(_) {
return wasm_result(2, "only one input file may be specified")
}
file = Some(token)
i = i + 1
}
}
if !is_wasm_format_supported(format) {
return wasm_result(2, "unsupported format: \{format}")
}
if format_explicit && command != "eval" {
return wasm_result(2, "--format is only supported by eval")
}
match (source, file) {
(Some(_), Some(_)) =>
wasm_result(2, "pass either --source or a file path, not both")
(Some(source), None) => run_wasm_source_command(command, source, format)
(None, Some(path)) =>
run_wasm_file_command(command, path, format, package_caches)
(None, None) => wasm_result(2, "missing input: pass --source or ")
}
}
///|
fn keep_wasm_imports_linked() -> Unit {
// Imports are shared with the native / JS CLI at package scope. Keep the
// target-specific aliases referenced so `--deny-warn` remains useful on
// wasm without granting these capabilities to the Marketplace command.
ignore(@fs.path_exists)
let _http_ref : @http.Response? = None
ignore(_http_ref)
ignore(@zlib.crc32(b""))
ignore(@async.is_being_cancelled)
ignore(@utf8.decode(b""[:]) catch { _ => "" })
ignore(@env.current_dir)
let _json_path_ref : @json.JsonPath? = None
ignore(_json_path_ref)
ignore(@crypto.sha256(b""))
}
///|
fn main {
keep_wasm_imports_linked()
let cli_args = @env.args()
let args : Array[String] = []
for i = 1; i < cli_args.length(); i = i + 1 {
args.push(cli_args[i])
}
let result = run_wasm_command(args)
if result.output != "" {
println(result.output)
}
if result.exit_code != 0 {
@sys.exit(result.exit_code)
}
}