diff --git a/rewriter/src/lib.rs b/rewriter/src/lib.rs index 7281c23..21e2f93 100644 --- a/rewriter/src/lib.rs +++ b/rewriter/src/lib.rs @@ -53,12 +53,15 @@ fn get_config(scramjet: &Object) -> Config { Config { prefix: get_str(config, "prefix"), encode: create_encode_function(get_obj(codec, "encode")), + wrapfn: get_str(config, "wrapfn"), importfn: get_str(config, "importfn"), rewritefn: get_str(config, "rewritefn"), metafn: get_str(config, "metafn"), setrealmfn: get_str(config, "setrealmfn"), + pushsourcemapfn: get_str(config, "pushsourcemapfn"), + do_sourcemaps: get_bool(flags, "sourcemaps"), capture_errors: get_bool(flags, "captureErrors"), } } diff --git a/rewriter/src/main.rs b/rewriter/src/main.rs index cb86951..170b547 100644 --- a/rewriter/src/main.rs +++ b/rewriter/src/main.rs @@ -120,7 +120,9 @@ fn dorewrite(source_text: &str) -> String { rewritefn: "$rewrite".to_string(), metafn: "$meta".to_string(), setrealmfn: "$setrealm".to_string(), + pushsourcemapfn: "$pushsourcemap".to_string(), capture_errors: true, + do_sourcemaps: true, }, ) .as_slice(), diff --git a/rewriter/src/rewrite.rs b/rewriter/src/rewrite.rs index 40f0559..023f140 100644 --- a/rewriter/src/rewrite.rs +++ b/rewriter/src/rewrite.rs @@ -1,3 +1,5 @@ +use core::str; + use oxc_allocator::Allocator; use oxc_ast::{ ast::{ @@ -37,14 +39,17 @@ struct Rewriter { pub struct Config { pub prefix: String, + pub wrapfn: String, pub importfn: String, pub rewritefn: String, pub setrealmfn: String, pub metafn: String, - pub encode: EncodeFn, + pub pushsourcemapfn: String, + pub encode: EncodeFn, pub capture_errors: bool, + pub do_sourcemaps: bool, } impl Rewriter { @@ -85,22 +90,22 @@ impl Rewriter { impl<'a> Visit<'a> for Rewriter { 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 { span: it.span, - text: format!( - "{}({}, typeof arguments != 'undefined' && arguments)", - self.config.wrapfn, it.name - ), + text: format!("{}({})", 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 @@ -455,6 +460,12 @@ pub fn rewrite(js: &str, url: Url, config: Config) -> Vec { let size_estimate = (original_len as i32 + difference) as usize; let mut buffer: Vec = Vec::with_capacity(size_estimate); + let mut sourcemap: Vec = 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; for change in ast_pass.jschanges { match &change { @@ -462,6 +473,19 @@ pub fn rewrite(js: &str, url: Url, config: Config) -> Vec { let start = span.start 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(text.as_bytes()); @@ -500,9 +524,38 @@ pub fn rewrite(js: &str, url: Url, config: Config) -> Vec { } 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 } +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 { match op { AssignmentOperator::Assign => "=", diff --git a/src/client/shared/function.ts b/src/client/shared/function.ts index b1359b5..21f8f29 100644 --- a/src/client/shared/function.ts +++ b/src/client/shared/function.ts @@ -4,7 +4,8 @@ import { rewriteJs } from "../../shared"; function rewriteFunction(ctx: ProxyCtx, client: ScramjetClient) { 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) { diff --git a/src/client/shared/sourcemaps.ts b/src/client/shared/sourcemaps.ts new file mode 100644 index 0000000..135c097 --- /dev/null +++ b/src/client/shared/sourcemaps.ts @@ -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)); + }, + }); +} diff --git a/src/controller/index.ts b/src/controller/index.ts index bb83c19..4203d39 100644 --- a/src/controller/index.ts +++ b/src/controller/index.ts @@ -19,6 +19,7 @@ export class ScramjetController { rewritefn: "$scramjet$rewrite", metafn: "$scramjet$meta", setrealmfn: "$scramjet$setrealm", + pushsourcemapfn: "$scramjet$pushsourcemap", wasm: "/scramjet.wasm.js", shared: "/scramjet.shared.js", worker: "/scramjet.worker.js", @@ -29,6 +30,7 @@ export class ScramjetController { serviceworkers: false, naiiveRewriter: false, captureErrors: false, + sourcemaps: true, }, }; diff --git a/src/types.d.ts b/src/types.d.ts index dd0fd6d..f28c7e7 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -23,6 +23,7 @@ type ScramjetFlags = { serviceworkers: boolean; naiiveRewriter: boolean; captureErrors: boolean; + sourcemaps: boolean; }; interface ScramjetConfig { @@ -34,6 +35,7 @@ interface ScramjetConfig { rewritefn: string; metafn: string; setrealmfn: string; + pushsourcemapfn: string; wasm: string; shared: string; worker: string;