Developer tools

Source maps

Minified JavaScript stack traces mapped back to the original files, lines, columns and function names with their source maps, and the code around each place.

Version 1.0.0 · 4 functions · 1 crate

#javascript#debugging#code#monitoring

Functions

What use source_maps.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use sourcemap_lookup, sourcemap_context from source_maps (Quillo 0.6).

sourcemap_lookup(map: str, line: int, column: int) → any?or an error

Where a place in the minified file comes from: map is a .map file's path or its JSON text, line and column count from 1, as in stack traces. { source, line, column, name, function, code } (lines and columns from 1; code the original line, when the map carries the sources), or null when the map has nothing there.

sourcemap_context(map: str, line: int, column: int, around: int) → any?or an error

The original lines around a place in the minified file: { source, line, column, lines }, lines being { n, text, here } from around lines before to around after (here marks the line itself). For an error page that shows the code. Needs the map to carry sourcesContent.

sourcemap_stack(stack: str, maps_dir: str) → anyor an error

A minified stack trace mapped back: each frame whose file has a map in the folder maps_dir (app.min.js looks for app.min.js.map; the minified file beside it, when there, gives the original function names) is rewritten as at total (src/cart.ts:6:3). { text, frames }: text the trace rewritten, frames each { function, file, line, column, mapped, source, source_line, source_column, code }. Frames without a map stay as they were.

sourcemap_info(map: str) → anyor an error

What a map holds: { file, sources, names, mappings, has_contents }: the minified file's name, the original files, how many names and mappings, and whether the sources' text is in it (then a server needs nothing else to show code).

Get it

$ quillo install source_maps               # source_maps.qwer into this folder$ quillo install source_maps --example     # and its example, source_maps.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/source_maps.qwer$ curl -O https://quillolang.org/plugins/files/source_maps.qwe$ curl -O https://quillolang.org/plugins/files/source_maps_sample.js$ curl -O https://quillolang.org/plugins/files/source_maps_sample.js.map

Then, in your program, all of it or only what you call:

main.qwe
use source_maps.qweruse sourcemap_lookup, sourcemap_context from source_maps      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
sourcemap = "=9.3.2"

Example

source_maps.qwe
// A shop's error reports from the browser, mapped back from the minified bundle to the// TypeScript it came from. The bundle and its map are deployed as source_maps_sample.js(.map).////   quillo run source_maps.qwe      then open http://localhost:8080/reportuse "source_maps.qwer"let STACK = `Error: negative total    at r (https://shop.example/assets/source_maps_sample.js:1:96)    at a (https://shop.example/assets/source_maps_sample.js:1:64)    at HTMLButtonElement.<anonymous> (https://shop.example/assets/source_maps_sample.js:1:217)    at https://cdn.example/vendor.js:2:1180`// What the page's window.onerror sends: { stack }. Here, the trace above.route POST /errors { json: { stack: str } } = sourcemap_stack(request.json.stack, ".")?route GET /report = respond(200, sourcemap_stack(STACK, ".")?.text, { "content-type": "text/plain; charset=utf-8" })route GET /frames = sourcemap_stack(STACK, ".")?.framesroute GET /where = {    throw: sourcemap_lookup("source_maps_sample.js.map", 1, 96)?,    context: sourcemap_context("source_maps_sample.js.map", 1, 96, 2)?,    info: sourcemap_info("source_maps_sample.js.map")?,}
$ quillo run source_maps.qwe$ curl 'localhost:8080/report'$ curl 'localhost:8080/where'$ curl 'localhost:8080/errors'

Source

source_maps.qwer
//! Minified JavaScript stack traces mapped back to the original files, lines, columns and function names with their source maps: error reports you can read.//! version: 1.0.0//!//! [dependencies]//! sourcemap = "=9.3.2"use serde_json::{json, Value};use sourcemap::{DecodedMap, SourceMap, SourceView};fn load(map: &str) -> Result<SourceMap, String> {    let bytes = if map.trim_start().starts_with('{') {        map.as_bytes().to_vec()    } else {        std::fs::read(map).map_err(|e| format!("{}: {}", map, e))?    };    match sourcemap::decode_slice(&bytes).map_err(|e| format!("not a source map: {}", e))? {        DecodedMap::Regular(sm) => Ok(sm),        DecodedMap::Index(ix) => ix.flatten().map_err(|e| format!("an index map that does not flatten: {}", e)),        _ => Err("a kind of source map that is not supported (a RAM bundle or Hermes map)".into()),    }}/// Looks up a 1-based line and column, as stack traces give them.fn find(sm: &SourceMap, line: i64, column: i64, minified: Option<&SourceView>, minified_name: Option<&str>) -> Option<Value> {    if line < 1 || column < 1 {        return None;    }    let (l, c) = ((line - 1) as u32, (column - 1) as u32);    let t = sm.lookup_token(l, c)?;    let source = t.get_source()?;    let function = match (minified, minified_name) {        (Some(v), Some(n)) if !n.is_empty() => sm.get_original_function_name(l, c, n, v).map(String::from),        _ => None,    };    let code = sm.get_source_view(t.get_src_id()).and_then(|v| v.get_line(t.get_src_line())).map(|s| s.trim().to_string());    Some(json!({        "source": source,        "line": t.get_src_line() + 1,        "column": t.get_src_col() + 1,        "name": t.get_name(),        "function": function,        "code": code,    }))}/// Where a place in the minified file comes from: `map` is a .map file's path or its JSON text, `line` and `column` count from 1, as/// in stack traces. `{ source, line, column, name, function, code }` (lines and columns from 1; `code` the original line, when the map/// carries the sources), or null when the map has nothing there.pub fn sourcemap_lookup(map: &str, line: i64, column: i64) -> Result<Option<Value>, String> {    let sm = load(map)?;    Ok(find(&sm, line, column, None, None))}/// The original lines around a place in the minified file: `{ source, line, column, lines }`, `lines` being `{ n, text, here }` from/// `around` lines before to `around` after (`here` marks the line itself). For an error page that shows the code. Needs the map to carry/// `sourcesContent`.pub fn sourcemap_context(map: &str, line: i64, column: i64, around: i64) -> Result<Option<Value>, String> {    let sm = load(map)?;    let Some(hit) = find(&sm, line, column, None, None) else { return Ok(None) };    let src = hit["source"].as_str().unwrap_or("");    let idx = (0..sm.get_source_count()).find(|i| sm.get_source(*i) == Some(src)).ok_or("no such source")?;    let view = sm.get_source_view(idx).ok_or("the map does not carry this file's text (sourcesContent)")?;    let at = hit["line"].as_u64().unwrap_or(1) as i64;    let around = around.clamp(0, 50);    let mut lines = vec![];    for n in (at - around).max(1)..=at + around {        if let Some(text) = view.get_line((n - 1) as u32) {            lines.push(json!({ "n": n, "text": text, "here": n == at }));        }    }    Ok(Some(json!({ "source": src, "line": at, "column": hit["column"], "lines": lines })))}struct Frame {    raw: String,    indent: String,    function: Option<String>,    url: String,    line: i64,    column: i64,}/// A stack trace line: Chrome and Node's `    at fn (url:12:34)` or `    at url:12:34`, Firefox and Safari's `fn@url:12:34`.fn frame(raw: &str) -> Option<Frame> {    let indent: String = raw.chars().take_while(|c| c.is_whitespace()).collect();    let t = raw.trim();    let (function, place) = if let Some(rest) = t.strip_prefix("at ") {        match rest.rfind(" (") {            Some(i) if rest.ends_with(')') => (Some(rest[..i].to_string()), &rest[i + 2..rest.len() - 1]),            _ => (None, rest),        }    } else if let Some(i) = t.find('@') {        (Some(t[..i].to_string()).filter(|s| !s.is_empty()), &t[i + 1..])    } else {        return None;    };    let (rest, col) = place.rsplit_once(':')?;    let (url, line) = rest.rsplit_once(':')?;    Some(Frame { raw: raw.to_string(), indent, function, url: url.to_string(), line: line.parse().ok()?, column: col.parse().ok()? })}fn base_name(url: &str) -> &str {    let u = url.split(['?', '#']).next().unwrap_or(url);    u.rsplit('/').next().unwrap_or(u)}/// A minified stack trace mapped back: each frame whose file has a map in the folder `maps_dir` (`app.min.js` looks for/// `app.min.js.map`; the minified file beside it, when there, gives the original function names) is rewritten as/// `at total (src/cart.ts:6:3)`. `{ text, frames }`: `text` the trace rewritten, `frames` each `{ function, file, line, column, mapped,/// source, source_line, source_column, code }`. Frames without a map stay as they were.pub fn sourcemap_stack(stack: &str, maps_dir: &str) -> Result<Value, String> {    let mut maps: std::collections::HashMap<String, Option<(SourceMap, Option<SourceView>)>> = std::collections::HashMap::new();    let mut text = vec![];    let mut frames = vec![];    for raw in stack.lines() {        let Some(f) = frame(raw) else {            text.push(raw.to_string());            continue;        };        let name = base_name(&f.url).to_string();        let entry = maps.entry(name.clone()).or_insert_with(|| {            if name.is_empty() || name.contains("..") {                return None;            }            let dir = std::path::Path::new(maps_dir);            let sm = load(dir.join(format!("{}.map", name)).to_str()?).ok()?;            let view = std::fs::read_to_string(dir.join(&name)).ok().map(SourceView::from_string);            Some((sm, view))        });        let short = f.function.as_deref().map(|s| s.rsplit('.').next().unwrap_or(s));        let hit = entry.as_ref().and_then(|(sm, view)| find(sm, f.line, f.column, view.as_ref(), short));        match &hit {            Some(h) => {                let func = h["function"].as_str().map(String::from).or_else(|| f.function.clone());                let place = format!("{}:{}:{}", h["source"].as_str().unwrap_or(""), h["line"], h["column"]);                text.push(match &func {                    Some(fun) => format!("{}at {} ({})", f.indent, fun, place),                    None => format!("{}at {}", f.indent, place),                });                frames.push(json!({                    "function": func, "file": f.url, "line": f.line, "column": f.column, "mapped": true,                    "source": h["source"], "source_line": h["line"], "source_column": h["column"], "code": h["code"],                }));            }            None => {                text.push(f.raw.clone());                frames.push(json!({                    "function": f.function, "file": f.url, "line": f.line, "column": f.column, "mapped": false,                    "source": null, "source_line": null, "source_column": null, "code": null,                }));            }        }    }    Ok(json!({ "text": text.join("\n"), "frames": frames }))}/// What a map holds: `{ file, sources, names, mappings, has_contents }`: the minified file's name, the original files, how many names and/// mappings, and whether the sources' text is in it (then a server needs nothing else to show code).pub fn sourcemap_info(map: &str) -> Result<Value, String> {    let sm = load(map)?;    let sources: Vec<&str> = sm.sources().collect();    let has = (0..sm.get_source_count()).all(|i| sm.get_source_contents(i).is_some());    Ok(json!({        "file": sm.get_file(),        "sources": sources,        "names": sm.get_name_count(),        "mappings": sm.get_token_count(),        "has_contents": has && !sources.is_empty(),    }))}
JSON types json_types.qwer · 1.0.0Types inferred from JSON samples: TypeScript interfaces, a JSON Schema and Rust structs for an API's answers or a webhook's body, with optional and nullable fields found across samples.jsonapicodevalidationjavascript7 views · 7 downloadsSentry sentry.qwer · 1.0.0Errors and messages sent to Sentry, GlitchTip or any Sentry-compatible server as envelope events from a DSN, with level, tags, user, request, release, breadcrumbs and a stack trace, without an SDK.monitoringobservabilitydebuggingalerts7 views · 7 downloadsHTTP Archive files har.qwer · 1.0.0HAR files from a browser's network panel read: pages with their load times, each request flat with timings and sizes (-1 given as null, SSL not counted twice), the slowest and largest requests, totals by host and type. Secrets masked from headers, cookies, query strings, form and JSON bodies before the file goes on a ticket, with a list of what was found and never its value.httpperformancedebuggingsecurity3 views · 6 downloadsLog parser log_parse.qwer · 1.0.0Log lines into rows: nginx and Apache access logs, syslog, logfmt and JSON lines, the format found by itself, with counts by any field.logsmonitoringobservabilitydevopsanalytics7 views · 7 downloadsPrometheus metrics prometheus.qwer · 1.0.0Counters, gauges and histograms with labels kept in memory, and the /metrics text Prometheus and Grafana scrape.monitoringmetricsobservabilitygrafana8 views · 8 downloadsSQL formatting sql_format.qwer · 1.0.0SQL pretty-printed with indents and keywords in capitals, placeholders filled in for logs, and squeezed back onto one line.sqldatabaseslogsdebugging9 views · 5 downloads
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On Source maps: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.