///| fn usage() -> String { ( #|MoonSourceMap 0.1.1 #|Usage: #| moon-sourcemap validate [--format text|json] #| moon-sourcemap inspect [--format text|json] #| moon-sourcemap lookup --line N --column N [--bias glb|lub | --all] [--strict] [--format text|json] #| moon-sourcemap flatten -o [--format text|json] #| moon-sourcemap compose -o [--format text|json] #| moon-sourcemap symbolize [--context N] [--strict] [--format text|json] #| #|Coordinates are zero-based. Exit codes: 0 success, 1 usage/I/O, #|2 invalid source map, 3 strict lookup/symbolization miss. ) } ///| fn option(args : Array[String], name : String) -> String? { match args.search_by(value => value == name) { Some(index) => if index + 1 < args.length() { Some(args[index + 1]) } else { None } None => None } } ///| fn has_flag(args : Array[String], name : String) -> Bool { args.any(value => value == name) } ///| fn format_of(args : Array[String]) -> String { option(args, "--format").unwrap_or("text") } ///| fn output_of(args : Array[String]) -> String? { match option(args, "-o") { Some(path) => Some(path) None => option(args, "--output") } } ///| fn parse_coordinate(args : Array[String], name : String) -> Int raise { let value = match option(args, name) { Some(value) => value None => fail("missing required option \{name}") } let coordinate = @strconv.parse_int(value) if coordinate < 0 { fail("\{name} must be non-negative") } coordinate } ///| fn read_document(path : String) -> @lib.SourceMapDocument raise { @lib.parse_document(@fs.read_file_to_string(path)) } ///| fn write_output(path : String, content : String) -> Unit raise { if path == "-" { println(content) } else { @fs.write_string_to_file(path, content) } } ///| fn diagnostic_text(diagnostics : ArrayView[@lib.Diagnostic]) -> String { if diagnostics.is_empty() { "valid" } else { diagnostics.map(diagnostic => diagnostic.render()).join("\n") } } ///| fn validate_command(args : Array[String]) -> Int raise { if args.length() < 3 { fail("validate requires a map path") } let document = read_document(args[2]) let diagnostics = @lib.validate(document) if format_of(args) == "json" { println(@lib.diagnostics_to_json(diagnostics).stringify(indent=2)) } else { println(diagnostic_text(diagnostics)) } if @lib.diagnostics_have_errors(diagnostics) { 2 } else { 0 } } ///| fn inspect_command(args : Array[String]) -> Int raise { if args.length() < 3 { fail("inspect requires a map path") } let map = @lib.decode_document(read_document(args[2]), map_url=args[2]) let report = @lib.analyze_source_map(map) let diagnostics = report.diagnostics() if format_of(args) == "json" { println( Json::object({ "report": report.to_json(), "diagnostics": @lib.diagnostics_to_json(diagnostics), }).stringify(indent=2), ) } else { println(report.render()) if !diagnostics.is_empty() { println(diagnostic_text(diagnostics)) } } 0 } ///| fn bias_of(args : Array[String]) -> @lib.LookupBias raise { match option(args, "--bias").unwrap_or("glb") { "glb" => GreatestLowerBound "lub" => LeastUpperBound other => fail("unsupported lookup bias: \{other}") } } ///| fn lookup_json(map : @lib.DecodedSourceMap, mapping : @lib.Mapping?) -> Json { match mapping { Some(mapping) => match mapping.original { Some(original) => { let source = if original.source_index < map.sources.length() { map.sources[original.source_index].url } else { None } Json::object({ "found": Json::boolean(true), "source": match source { Some(source) => Json::string(source) None => Json::null() }, "line": Json::number(original.line.to_double()), "column": Json::number(original.column.to_double()), "name": match mapping.name { Some(name) => Json::string(name) None => Json::null() }, }) } None => Json::object({ "found": Json::boolean(false) }) } None => Json::object({ "found": Json::boolean(false) }) } } ///| fn lookup_text(map : @lib.DecodedSourceMap, mapping : @lib.Mapping?) -> String { match mapping { Some(mapping) => match mapping.original { Some(original) => { let source = if original.source_index < map.sources.length() { map.sources[original.source_index].url.unwrap_or("") } else { "" } let name = match mapping.name { Some(name) => " " + name None => "" } "\{source}:\{original.line}:\{original.column}\{name}" } None => "" } None => "" } } ///| fn original_position_json( map : @lib.DecodedSourceMap, original : @lib.OriginalPosition?, ) -> Json { match original { None => Json::null() Some(original) => { let source = if original.source_index >= 0 && original.source_index < map.sources.length() { map.sources[original.source_index].url } else { None } Json::object({ "source_index": Json::number(original.source_index.to_double()), "source": match source { Some(source) => Json::string(source) None => Json::null() }, "line": Json::number(original.line.to_double()), "column": Json::number(original.column.to_double()), }) } } } ///| fn lookup_all_json( map : @lib.DecodedSourceMap, originals : ArrayView[@lib.OriginalPosition?], ) -> Json { Json::object({ "found": Json::boolean(!originals.is_empty()), "originals": Json::array( originals.map(original => original_position_json(map, original)), ), }) } ///| fn lookup_all_text( map : @lib.DecodedSourceMap, originals : ArrayView[@lib.OriginalPosition?], ) -> String { if originals.is_empty() { return "" } originals .map(original => { match original { None => "" Some(original) => { let source = if original.source_index >= 0 && original.source_index < map.sources.length() { map.sources[original.source_index].url.unwrap_or("") } else { "" } "\{source}:\{original.line}:\{original.column}" } } }) .join("\n") } ///| fn lookup_command(args : Array[String]) -> Int raise { if args.length() < 3 { fail("lookup requires a map path") } let all = has_flag(args, "--all") if all && has_flag(args, "--bias") { fail("--all and --bias are mutually exclusive") } let line = parse_coordinate(args, "--line") let column = parse_coordinate(args, "--column") let map = @lib.decode_document(read_document(args[2])) let generated = @lib.Position::new(line~, column~) if all { let originals = @lib.get_original_positions(map, generated~) if format_of(args) == "json" { println(lookup_all_json(map, originals).stringify(indent=2)) } else { println(lookup_all_text(map, originals)) } if originals.is_empty() && has_flag(args, "--strict") { 3 } else { 0 } } else { let mapping = @lib.original_position_for( map, generated~, bias=bias_of(args), ) if format_of(args) == "json" { println(lookup_json(map, mapping).stringify(indent=2)) } else { println(lookup_text(map, mapping)) } if mapping is None && has_flag(args, "--strict") { 3 } else { 0 } } } ///| fn flatten_command(args : Array[String]) -> Int raise { if args.length() < 3 { fail("flatten requires a map path") } let output = output_of(args).unwrap_or("-") let regular = @lib.flatten(read_document(args[2])) write_output(output, @lib.encode_document(Regular(regular))) if format_of(args) == "json" { println( Json::object({ "ok": Json::boolean(true), "output": Json::string(output) }).stringify(), ) } 0 } ///| fn compose_command(args : Array[String]) -> Int raise { if args.length() < 4 { fail("compose requires outer and inner map paths") } let output = output_of(args).unwrap_or("-") let outer = @lib.decode_document(read_document(args[2])) let inner = @lib.decode_document(read_document(args[3])) let regular = @lib.encode_decoded(@lib.compose(outer, inner)) write_output(output, @lib.encode_document(Regular(regular))) if format_of(args) == "json" { println( Json::object({ "ok": Json::boolean(true), "output": Json::string(output) }).stringify(), ) } 0 } ///| fn symbolize_command(args : Array[String]) -> Int raise { if args.length() < 4 { fail("symbolize requires a map and frames path") } let map = @lib.decode_document(read_document(args[2])) let frame_text = @fs.read_file_to_string(args[3]) match option(args, "--context") { Some(_) => { let radius = parse_coordinate(args, "--context") let report = @lib.symbolize_with_context( map, frame_text, context_radius=radius, ) if format_of(args) == "json" { println(report.to_json().stringify(indent=2)) } else { println(report.render()) } if has_flag(args, "--strict") && !report.is_complete() { 3 } else { 0 } } None => { let frames = @lib.symbolize(map, frame_text) if format_of(args) == "json" { println(@lib.symbolized_frames_to_json(frames).stringify(indent=2)) } else { println(frames.map(frame => frame.render()).join("\n")) } if has_flag(args, "--strict") && frames.any(frame => !frame.matched) { 3 } else { 0 } } } } ///| fn dispatch(args : Array[String]) -> Int raise { if args.length() < 2 { println(usage()) return 1 } match args[1] { "validate" => validate_command(args) "inspect" => inspect_command(args) "lookup" => lookup_command(args) "flatten" => flatten_command(args) "compose" => compose_command(args) "symbolize" => symbolize_command(args) "help" | "--help" | "-h" => { println(usage()) 0 } other => { println("unknown command: \{other}\n\n\{usage()}") 1 } } } ///| fn main { let code = dispatch(@env.args()) catch { @lib.InvalidJson(message~) => { println("source map error: \{message}") 2 } @lib.InvalidField(path~, message~) => { println("source map error at \{path}: \{message}") 2 } @lib.InvalidVlq(offset~, message~) => { println("source map error at VLQ offset \{offset}: \{message}") 2 } @lib.InvalidMappings(line~, segment~, offset~, message~) => { println( "source map error at mapping \{line}:\{segment} offset \{offset}: \{message}", ) 2 } @lib.InvalidDocument(message~) => { println("source map error: \{message}") 2 } @lib.Io(message~) => { println("I/O error: \{message}") 1 } error => { println("error: \{error}") 1 } } if code != 0 { @sys.exit(code) } }