From 4c9d2acd8caab4ad44bf63e9c5c4810b6a149b85 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Thu, 21 May 2026 21:21:32 +0200 Subject: [PATCH] Fix Instagram bootstrap JS failures for isu issue 242 --- .isu/issues.json | 26 +- .../pages/32_instagram_mutation_observer.html | 36 +++ .../scenarios/instagram_mutation_observer.we | 4 + crates/js/src/builtins.rs | 55 +++- crates/js/src/compiler.rs | 9 +- crates/js/src/dom_bridge.rs | 242 ++++++++++++++++++ crates/js/src/streams.rs | 12 +- crates/js/src/vm.rs | 39 +++ 8 files changed, 411 insertions(+), 12 deletions(-) create mode 100644 crates/e2e/pages/32_instagram_mutation_observer.html create mode 100644 crates/e2e/scenarios/instagram_mutation_observer.we diff --git a/.isu/issues.json b/.isu/issues.json index da26677..248e0a2 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 249, + "next_id": 251, "issues": [ { "id": 1, @@ -2914,7 +2914,7 @@ ], "assigned": [], "author": "piefev", - "state": "open", + "state": "closed", "created_at": "2026-05-21T16:05:09Z" }, { @@ -2998,6 +2998,28 @@ "author": "piefev", "state": "open", "created_at": "2026-05-21T18:43:29Z" + }, + { + "id": 249, + "repo": "we", + "title": "Remove CPU renderer", + "body": null, + "labels": [], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-05-21T19:18:53Z" + }, + { + "id": 250, + "repo": "we", + "title": "macos native Keyboard shortcuts with menu bar options (File, Edit, View, Window, Help)", + "body": null, + "labels": [], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-05-21T19:22:06Z" } ] } diff --git a/crates/e2e/pages/32_instagram_mutation_observer.html b/crates/e2e/pages/32_instagram_mutation_observer.html new file mode 100644 index 0000000..10fe0cd --- /dev/null +++ b/crates/e2e/pages/32_instagram_mutation_observer.html @@ -0,0 +1,36 @@ + + + + Instagram MutationObserver regression + + +
not-run
+ + + diff --git a/crates/e2e/scenarios/instagram_mutation_observer.we b/crates/e2e/scenarios/instagram_mutation_observer.we new file mode 100644 index 0000000..362b5fa --- /dev/null +++ b/crates/e2e/scenarios/instagram_mutation_observer.we @@ -0,0 +1,4 @@ +viewport 800 600 +goto crates/e2e/pages/32_instagram_mutation_observer.html +dump_dom instagram_mutation_observer_dom.txt +assert_dom_contains "function:0:ready" diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 9375533..593bfb7 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -857,7 +857,17 @@ fn make_string_array(gc: &mut Gc, shapes: &mut ShapeTable, items: &[ /// Create a JS array from a slice of Values. fn make_value_array(gc: &mut Gc, shapes: &mut ShapeTable, items: &[Value]) -> Value { + make_value_array_with_proto(gc, shapes, items, None) +} + +fn make_value_array_with_proto( + gc: &mut Gc, + shapes: &mut ShapeTable, + items: &[Value], + prototype: Option, +) -> Value { let mut obj = ObjectData::new(); + obj.prototype = prototype; for (i, v) in items.iter().enumerate() { obj.insert_property(i.to_string(), Property::data(v.clone()), shapes); } @@ -874,6 +884,22 @@ fn make_value_array(gc: &mut Gc, shapes: &mut ShapeTable, items: &[V Value::Object(gc.alloc(HeapObject::Object(obj))) } +fn array_prototype_from_context(ctx: &NativeContext) -> Option { + let callee = ctx.callee?; + match ctx.gc.get(callee) { + Some(HeapObject::Function(fdata)) => fdata.prototype_obj.or_else(|| { + fdata + .properties + .get("__array_prototype__") + .and_then(|prop| match &prop.value { + Value::Object(r) => Some(*r), + _ => None, + }) + }), + _ => None, + } +} + // ── Array.prototype ────────────────────────────────────────── fn init_array_prototype(gc: &mut Gc, shapes: &mut ShapeTable, proto: GcRef) { @@ -1391,19 +1417,23 @@ fn init_array_constructor(gc: &mut Gc, arr_proto: GcRef) -> GcRef { set_func_prop(gc, ctor, "isArray", Value::Function(is_array)); let from = make_native(gc, "from", array_from); + set_func_prop(gc, from, "__array_prototype__", Value::Object(arr_proto)); set_func_prop(gc, ctor, "from", Value::Function(from)); let of = make_native(gc, "of", array_of); + set_func_prop(gc, of, "__array_prototype__", Value::Object(arr_proto)); set_func_prop(gc, ctor, "of", Value::Function(of)); ctor } fn array_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let prototype = array_prototype_from_context(ctx); if args.len() == 1 { if let Value::Number(n) = &args[0] { let len = *n as usize; let mut obj = ObjectData::new(); + obj.prototype = prototype; obj.insert_property( "length".to_string(), Property { @@ -1417,7 +1447,9 @@ fn array_constructor(args: &[Value], ctx: &mut NativeContext) -> Result Result { @@ -1436,6 +1468,7 @@ fn array_is_array(args: &[Value], ctx: &mut NativeContext) -> Result Result { let iterable = args.first().cloned().unwrap_or(Value::Undefined); + let prototype = array_prototype_from_context(ctx); match iterable { Value::Object(r) => { let len = array_length(ctx.gc, ctx.shapes, r); @@ -1443,18 +1476,30 @@ fn array_from(args: &[Value], ctx: &mut NativeContext) -> Result { let chars: Vec = s.chars().map(|c| Value::String(c.to_string())).collect(); - Ok(make_value_array(ctx.gc, ctx.shapes, &chars)) + Ok(make_value_array_with_proto( + ctx.gc, ctx.shapes, &chars, prototype, + )) } - _ => Ok(make_value_array(ctx.gc, ctx.shapes, &[])), + _ => Ok(make_value_array_with_proto( + ctx.gc, + ctx.shapes, + &[], + prototype, + )), } } fn array_of(args: &[Value], ctx: &mut NativeContext) -> Result { - Ok(make_value_array(ctx.gc, ctx.shapes, args)) + let prototype = array_prototype_from_context(ctx); + Ok(make_value_array_with_proto( + ctx.gc, ctx.shapes, args, prototype, + )) } // ── Error constructors ─────────────────────────────────────── diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs index 2743211..d5969b0 100644 --- a/crates/js/src/compiler.rs +++ b/crates/js/src/compiler.rs @@ -2345,10 +2345,6 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), fc.free_reg(key_reg); } - // Set `this` to the receiver object before calling. - let this_ni = fc.builder.add_name("this"); - fc.builder.emit_store_global(this_ni, obj_reg); - let args_start = fc.next_reg; let arg_count = arguments.len().min(255) as u8; for arg in arguments { @@ -2356,6 +2352,11 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(), compile_expr(fc, arg, arg_reg)?; } + // Set `this` after compiling arguments so nested method calls + // inside the argument list cannot clobber the receiver. + let this_ni = fc.builder.add_name("this"); + fc.builder.emit_store_global(this_ni, obj_reg); + fc.builder.emit_call(dst, func_reg, args_start, arg_count); // Free in LIFO order: args, func_reg, obj_reg. diff --git a/crates/js/src/dom_bridge.rs b/crates/js/src/dom_bridge.rs index 4d48d42..4dbca9a 100644 --- a/crates/js/src/dom_bridge.rs +++ b/crates/js/src/dom_bridge.rs @@ -28,6 +28,7 @@ type NativeMethod = ( // ── Font registry (lazy-initialized, single-threaded) ───────────────── use std::cell::RefCell; +use std::collections::HashMap; // ── ImageData / Uint8ClampedArray store ────────────────────────────── @@ -939,6 +940,228 @@ fn make_empty_array(gc: &mut Gc, shapes: &mut ShapeTable) -> Value { Value::Object(gc.alloc(HeapObject::Object(obj))) } +// ── MutationObserver ──────────────────────────────────────────────── + +const MUTATION_OBSERVER_CALLBACK_KEY: &str = "__mutation_observer_callback__"; +const MUTATION_OBSERVER_ACTIVE_KEY: &str = "__mutation_observer_active__"; +const MUTATION_OBSERVER_TARGET_KEY: &str = "__mutation_observer_target__"; +const MUTATION_OBSERVER_CHILD_LIST_KEY: &str = "__mutation_observer_child_list__"; +const MUTATION_OBSERVER_ATTRIBUTES_KEY: &str = "__mutation_observer_attributes__"; +const MUTATION_OBSERVER_SUBTREE_KEY: &str = "__mutation_observer_subtree__"; + +fn bool_option( + gc: &Gc, + shapes: &ShapeTable, + options: Option, + key: &str, +) -> bool { + options + .and_then(|opts_ref| match gc.get(opts_ref) { + Some(HeapObject::Object(opts)) => opts.get_property(key, shapes), + _ => None, + }) + .map(|prop| prop.value.to_boolean()) + .unwrap_or(false) +} + +fn mutation_observer_constructor( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let callback = match args.first() { + Some(Value::Function(r)) => *r, + _ => { + return Err(RuntimeError::type_error( + "MutationObserver callback must be a function", + )) + } + }; + + let mut data = ObjectData::new(); + data.prototype = ctx.callee.and_then(|callee| match ctx.gc.get(callee) { + Some(HeapObject::Function(fdata)) => fdata.prototype_obj, + _ => None, + }); + data.insert_property( + MUTATION_OBSERVER_CALLBACK_KEY.to_string(), + Property::builtin(Value::Function(callback)), + ctx.shapes, + ); + data.insert_property( + MUTATION_OBSERVER_ACTIVE_KEY.to_string(), + Property::builtin(Value::Boolean(false)), + ctx.shapes, + ); + data.insert_property( + MUTATION_OBSERVER_TARGET_KEY.to_string(), + Property::builtin(Value::Null), + ctx.shapes, + ); + Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(data)))) +} + +fn mutation_observer_observe( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let observer_ref = match ctx.this { + Value::Object(r) => r, + _ => { + return Err(RuntimeError::type_error( + "observe called on non-MutationObserver", + )) + } + }; + let is_observer = matches!( + ctx.gc.get(observer_ref), + Some(HeapObject::Object(data)) + if data + .get_property(MUTATION_OBSERVER_CALLBACK_KEY, ctx.shapes) + .is_some() + ); + if !is_observer { + return Err(RuntimeError::type_error( + "observe called on non-MutationObserver", + )); + } + + let target = match args.first() { + Some(Value::Object(target_ref)) => get_node_id(ctx.gc, ctx.shapes, *target_ref), + _ => None, + } + .ok_or_else(|| RuntimeError::type_error("MutationObserver.observe target must be a Node"))?; + + let options = match args.get(1) { + Some(Value::Object(opts_ref)) => Some(*opts_ref), + _ => None, + }; + let child_list = bool_option(ctx.gc, ctx.shapes, options, "childList"); + let attributes = bool_option(ctx.gc, ctx.shapes, options, "attributes"); + let subtree = bool_option(ctx.gc, ctx.shapes, options, "subtree"); + if !(child_list || attributes) { + return Err(RuntimeError::type_error( + "MutationObserver.observe requires childList or attributes", + )); + } + + if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(observer_ref) { + data.insert_property( + MUTATION_OBSERVER_ACTIVE_KEY.to_string(), + Property::builtin(Value::Boolean(true)), + ctx.shapes, + ); + data.insert_property( + MUTATION_OBSERVER_TARGET_KEY.to_string(), + Property::builtin(Value::Number(target.index() as f64)), + ctx.shapes, + ); + data.insert_property( + MUTATION_OBSERVER_CHILD_LIST_KEY.to_string(), + Property::builtin(Value::Boolean(child_list)), + ctx.shapes, + ); + data.insert_property( + MUTATION_OBSERVER_ATTRIBUTES_KEY.to_string(), + Property::builtin(Value::Boolean(attributes)), + ctx.shapes, + ); + data.insert_property( + MUTATION_OBSERVER_SUBTREE_KEY.to_string(), + Property::builtin(Value::Boolean(subtree)), + ctx.shapes, + ); + } + Ok(Value::Undefined) +} + +fn mutation_observer_disconnect( + _args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let observer_ref = match ctx.this { + Value::Object(r) => r, + _ => { + return Err(RuntimeError::type_error( + "disconnect called on non-MutationObserver", + )) + } + }; + if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(observer_ref) { + if data + .get_property(MUTATION_OBSERVER_CALLBACK_KEY, ctx.shapes) + .is_none() + { + return Err(RuntimeError::type_error( + "disconnect called on non-MutationObserver", + )); + } + data.insert_property( + MUTATION_OBSERVER_ACTIVE_KEY.to_string(), + Property::builtin(Value::Boolean(false)), + ctx.shapes, + ); + } + Ok(Value::Undefined) +} + +fn mutation_observer_take_records( + _args: &[Value], + ctx: &mut NativeContext, +) -> Result { + match ctx.this { + Value::Object(observer_ref) => match ctx.gc.get(observer_ref) { + Some(HeapObject::Object(data)) + if data + .get_property(MUTATION_OBSERVER_CALLBACK_KEY, ctx.shapes) + .is_some() => {} + _ => { + return Err(RuntimeError::type_error( + "takeRecords called on non-MutationObserver", + )) + } + }, + _ => { + return Err(RuntimeError::type_error( + "takeRecords called on non-MutationObserver", + )) + } + } + Ok(make_empty_array(ctx.gc, ctx.shapes)) +} + +pub fn init_mutation_observer(vm: &mut Vm) { + let mut proto = ObjectData::new(); + proto.prototype = vm.object_prototype; + let proto_ref = vm.gc.alloc(HeapObject::Object(proto)); + + let methods: &[NativeMethod] = &[ + ("observe", mutation_observer_observe), + ("disconnect", mutation_observer_disconnect), + ("takeRecords", mutation_observer_take_records), + ]; + for &(name, callback) in methods { + let func = make_native(&mut vm.gc, name, callback); + set_builtin_prop( + &mut vm.gc, + &mut vm.shapes, + proto_ref, + name, + Value::Function(func), + ); + } + + let ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "MutationObserver".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: mutation_observer_constructor, + }), + prototype_obj: Some(proto_ref), + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + vm.set_global("MutationObserver", Value::Function(ctor)); +} + // ── Method registration on wrappers ────────────────────────────────── /// Register DOM methods on a wrapper object based on node type. @@ -7071,6 +7294,25 @@ mod tests { } } + #[test] + fn test_mutation_observer_constructor_and_methods() { + let result = eval_with_doc( + "", + r#" + var o = new MutationObserver(function(records) {}); + o.observe(document.documentElement, { childList: true, subtree: true }); + var records = o.takeRecords(); + o.disconnect(); + typeof o.observe + ":" + typeof o.disconnect + ":" + records.length + "#, + ) + .unwrap(); + match result { + Value::String(s) => assert_eq!(s, "function:function:0"), + v => panic!("expected MutationObserver method summary, got {v:?}"), + } + } + #[test] fn test_element_node_type() { let result = eval_with_doc("", "document.body.nodeType").unwrap(); diff --git a/crates/js/src/streams.rs b/crates/js/src/streams.rs index f2c3b0a..40c5df4 100644 --- a/crates/js/src/streams.rs +++ b/crates/js/src/streams.rs @@ -1294,7 +1294,17 @@ function ReadableStream_pipeTo(_t, destination, options) { }; if (signal) { - if (signal.aborted) { onAbortFn(); return; } + if (signal.aborted) { + var preReason = signal.reason; + if (!preventAbort) { + try { writer.abort(preReason); } catch (e) {} + } + if (!preventCancel) { + try { reader.cancel(preReason); } catch (e) {} + } + cleanup(false, preReason); + return; + } signal.addEventListener('abort', onAbortFn); } diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index 1694bd7..a424e92 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -1279,6 +1279,7 @@ impl Vm { crate::dom_bridge::init_storage_objects(self); crate::dom_bridge::init_canvas_path_api(self); crate::dom_bridge::init_image_data_api(self); + crate::dom_bridge::init_mutation_observer(self); crate::indexeddb::init_indexeddb(self); crate::service_worker::init_service_worker_api(self); crate::cache_api::init_cache_api(self); @@ -7064,6 +7065,44 @@ mod tests { } } + #[test] + fn test_array_from_result_has_array_prototype_methods() { + let src = r#" + var sum = 0; + var arrayLike = { length: 3 }; + arrayLike[0] = 2; + arrayLike[1] = 4; + arrayLike[2] = 6; + Array.from(arrayLike).forEach(function(x) { + sum = sum + x; + }); + sum + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 12.0), + v => panic!("expected 12, got {v:?}"), + } + } + + #[test] + fn test_method_call_this_survives_nested_argument_calls() { + let src = r#" + var receiver = { + value: 10, + method: function(x) { return this.value + x; } + }; + var argument = { + value: 5, + get: function() { return this.value; } + }; + receiver.method(argument.get()) + "#; + match eval(src).unwrap() { + Value::Number(n) => assert_eq!(n, 15.0), + v => panic!("expected 15, got {v:?}"), + } + } + #[test] fn test_array_flat() { let src = r#" -- 2.51.2