source maps

This commit is contained in:
velzie 2024-09-01 19:37:33 -04:00
parent 36ebeb3e5c
commit fac329b1dc
No known key found for this signature in database
GPG key ID: 048413F95F0DDE1F
7 changed files with 144 additions and 14 deletions

View file

@ -53,12 +53,15 @@ fn get_config(scramjet: &Object) -> Config {
Config { Config {
prefix: get_str(config, "prefix"), prefix: get_str(config, "prefix"),
encode: create_encode_function(get_obj(codec, "encode")), encode: create_encode_function(get_obj(codec, "encode")),
wrapfn: get_str(config, "wrapfn"), wrapfn: get_str(config, "wrapfn"),
importfn: get_str(config, "importfn"), importfn: get_str(config, "importfn"),
rewritefn: get_str(config, "rewritefn"), rewritefn: get_str(config, "rewritefn"),
metafn: get_str(config, "metafn"), metafn: get_str(config, "metafn"),
setrealmfn: get_str(config, "setrealmfn"), setrealmfn: get_str(config, "setrealmfn"),
pushsourcemapfn: get_str(config, "pushsourcemapfn"),
do_sourcemaps: get_bool(flags, "sourcemaps"),
capture_errors: get_bool(flags, "captureErrors"), capture_errors: get_bool(flags, "captureErrors"),
} }
} }

View file

@ -120,7 +120,9 @@ fn dorewrite(source_text: &str) -> String {
rewritefn: "$rewrite".to_string(), rewritefn: "$rewrite".to_string(),
metafn: "$meta".to_string(), metafn: "$meta".to_string(),
setrealmfn: "$setrealm".to_string(), setrealmfn: "$setrealm".to_string(),
pushsourcemapfn: "$pushsourcemap".to_string(),
capture_errors: true, capture_errors: true,
do_sourcemaps: true,
}, },
) )
.as_slice(), .as_slice(),

View file

@ -1,3 +1,5 @@
use core::str;
use oxc_allocator::Allocator; use oxc_allocator::Allocator;
use oxc_ast::{ use oxc_ast::{
ast::{ ast::{
@ -37,14 +39,17 @@ struct Rewriter {
pub struct Config { pub struct Config {
pub prefix: String, pub prefix: String,
pub wrapfn: String, pub wrapfn: String,
pub importfn: String, pub importfn: String,
pub rewritefn: String, pub rewritefn: String,
pub setrealmfn: String, pub setrealmfn: String,
pub metafn: String, pub metafn: String,
pub encode: EncodeFn, pub pushsourcemapfn: String,
pub encode: EncodeFn,
pub capture_errors: bool, pub capture_errors: bool,
pub do_sourcemaps: bool,
} }
impl Rewriter { impl Rewriter {
@ -85,22 +90,22 @@ impl Rewriter {
impl<'a> Visit<'a> for Rewriter { impl<'a> Visit<'a> for Rewriter {
fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) { fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) {
if self.config.capture_errors { // if self.config.capture_errors {
// self.jschanges.push(JsChange::GenericChange {
// span: it.span,
// text: format!(
// "{}({}, typeof arguments != 'undefined' && arguments)",
// self.config.wrapfn, it.name
// ),
// });
// } else {
if UNSAFE_GLOBALS.contains(&it.name.to_string().as_str()) {
self.jschanges.push(JsChange::GenericChange { self.jschanges.push(JsChange::GenericChange {
span: it.span, span: it.span,
text: format!( text: format!("{}({})", self.config.wrapfn, it.name),
"{}({}, typeof arguments != 'undefined' && arguments)",
self.config.wrapfn, it.name
),
}); });
} else {
if UNSAFE_GLOBALS.contains(&it.name.to_string().as_str()) {
self.jschanges.push(JsChange::GenericChange {
span: it.span,
text: format!("{}({})", self.config.wrapfn, it.name),
});
}
} }
// }
} }
// we need to rewrite `new Something` to `new (wrapfn(Something))` instead of `new wrapfn(Something)`, that's why there's weird extra code here // we need to rewrite `new Something` to `new (wrapfn(Something))` instead of `new wrapfn(Something)`, that's why there's weird extra code here
@ -455,6 +460,12 @@ pub fn rewrite(js: &str, url: Url, config: Config) -> Vec<u8> {
let size_estimate = (original_len as i32 + difference) as usize; let size_estimate = (original_len as i32 + difference) as usize;
let mut buffer: Vec<u8> = Vec::with_capacity(size_estimate); let mut buffer: Vec<u8> = Vec::with_capacity(size_estimate);
let mut sourcemap: Vec<u8> = Vec::new();
if ast_pass.config.do_sourcemaps {
sourcemap.reserve(size_estimate * 2);
sourcemap.extend_from_slice(&format!("{}([", ast_pass.config.pushsourcemapfn).as_bytes());
}
let mut offset = 0; let mut offset = 0;
for change in ast_pass.jschanges { for change in ast_pass.jschanges {
match &change { match &change {
@ -462,6 +473,19 @@ pub fn rewrite(js: &str, url: Url, config: Config) -> Vec<u8> {
let start = span.start as usize; let start = span.start as usize;
let end = span.end as usize; let end = span.end as usize;
if ast_pass.config.do_sourcemaps {
let spliced = &js[start..end];
sourcemap.extend_from_slice(
format!(
"[\"{}\",{},{}],",
json_escape_string(spliced),
start,
start + text.len()
)
.as_bytes(),
);
}
buffer.extend_from_slice(unsafe { js.get_unchecked(offset..start) }.as_bytes()); buffer.extend_from_slice(unsafe { js.get_unchecked(offset..start) }.as_bytes());
buffer.extend_from_slice(text.as_bytes()); buffer.extend_from_slice(text.as_bytes());
@ -500,9 +524,38 @@ pub fn rewrite(js: &str, url: Url, config: Config) -> Vec<u8> {
} }
buffer.extend_from_slice(js[offset..].as_bytes()); buffer.extend_from_slice(js[offset..].as_bytes());
if ast_pass.config.do_sourcemaps {
sourcemap.extend_from_slice(b"],");
sourcemap.extend_from_slice(b"\"");
sourcemap
.extend_from_slice(json_escape_string(str::from_utf8(&buffer).unwrap()).as_bytes());
sourcemap.extend_from_slice(b"\");\n");
sourcemap.extend_from_slice(&buffer);
return sourcemap;
}
buffer buffer
} }
fn json_escape_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\x08' => out.push_str("\\b"),
'\x0C' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out
}
fn fmt_op(op: AssignmentOperator) -> &'static str { fn fmt_op(op: AssignmentOperator) -> &'static str {
match op { match op {
AssignmentOperator::Assign => "=", AssignmentOperator::Assign => "=",

View file

@ -4,7 +4,8 @@ import { rewriteJs } from "../../shared";
function rewriteFunction(ctx: ProxyCtx, client: ScramjetClient) { function rewriteFunction(ctx: ProxyCtx, client: ScramjetClient) {
const stringifiedFunction = ctx.call().toString(); const stringifiedFunction = ctx.call().toString();
ctx.return(ctx.fn(`return ${rewriteJs(stringifiedFunction, client.meta)}`)()); let content = rewriteJs(`return ${stringifiedFunction}`, client.meta);
ctx.return(ctx.fn(content)());
} }
export default function (client: ScramjetClient, self: Self) { export default function (client: ScramjetClient, self: Self) {

View file

@ -0,0 +1,67 @@
import { ScramjetClient } from "../client";
let sourcemaps: {
source: string;
map: [string, number, number][];
}[] = [];
export const enabled = self.$scramjet.config.flags.sourcemaps;
export default function (client: ScramjetClient, self: Self) {
// every script will push a sourcemap
Object.defineProperty(self, "$scramjet$pushsourcemap", {
value: (map, source) => {
sourcemaps.push({ map, source });
},
enumerable: false,
writable: false,
configurable: false,
});
// when we rewrite javascript it will make function.toString leak internals
// this can lead to double rewrites which is bad
client.Proxy("Function.prototype.toString", {
apply(ctx) {
const stringified = ctx.fn.call(ctx.this);
let newString = "";
// find the sourcemap, brute force, just check every file until the body of the function shows up
// it doesnt matter if there's multiple with the same content because it will be the same function
const sourcemap = sourcemaps.find(({ source }) =>
source.includes(stringified)
);
// i don't know what cases this would happen under, but it does
if (!sourcemap) return ctx.return(stringified);
const { source, map } = sourcemap;
// first we need to find the character # where the function starts relative to the *transformed* source
let starting = source.indexOf(stringified);
const beforeFunctionRewrites = map.filter(
([str, start, end]) => start < starting
);
// map the offsets of the original source to the transformed source
for (const [str, start, end] of beforeFunctionRewrites) {
starting -= end - start - str.length;
}
const relevantRewrites = map.filter(
([str, start, end]) =>
start >= starting && end <= starting + stringified.length
);
let i = 0;
let offset = 0;
for (const [str, start, end] of relevantRewrites) {
// ooh i should really document this before i forget how it works
newString += stringified.slice(i, start - starting + offset);
newString += str;
offset += end - start - str.length;
i = start - starting + offset + str.length;
}
return ctx.return(newString + stringified.slice(i));
},
});
}

View file

@ -19,6 +19,7 @@ export class ScramjetController {
rewritefn: "$scramjet$rewrite", rewritefn: "$scramjet$rewrite",
metafn: "$scramjet$meta", metafn: "$scramjet$meta",
setrealmfn: "$scramjet$setrealm", setrealmfn: "$scramjet$setrealm",
pushsourcemapfn: "$scramjet$pushsourcemap",
wasm: "/scramjet.wasm.js", wasm: "/scramjet.wasm.js",
shared: "/scramjet.shared.js", shared: "/scramjet.shared.js",
worker: "/scramjet.worker.js", worker: "/scramjet.worker.js",
@ -29,6 +30,7 @@ export class ScramjetController {
serviceworkers: false, serviceworkers: false,
naiiveRewriter: false, naiiveRewriter: false,
captureErrors: false, captureErrors: false,
sourcemaps: true,
}, },
}; };

2
src/types.d.ts vendored
View file

@ -23,6 +23,7 @@ type ScramjetFlags = {
serviceworkers: boolean; serviceworkers: boolean;
naiiveRewriter: boolean; naiiveRewriter: boolean;
captureErrors: boolean; captureErrors: boolean;
sourcemaps: boolean;
}; };
interface ScramjetConfig { interface ScramjetConfig {
@ -34,6 +35,7 @@ interface ScramjetConfig {
rewritefn: string; rewritefn: string;
metafn: string; metafn: string;
setrealmfn: string; setrealmfn: string;
pushsourcemapfn: string;
wasm: string; wasm: string;
shared: string; shared: string;
worker: string; worker: string;