diff --git a/crates/browser/src/module_loader.rs b/crates/browser/src/module_loader.rs index a46aa98..8dfd17e 100644 --- a/crates/browser/src/module_loader.rs +++ b/crates/browser/src/module_loader.rs @@ -244,6 +244,48 @@ pub fn load_and_execute_modules( } } +/// Eagerly load modules referenced by literal `import("")` calls in +/// a parsed classic or module program. +/// +/// The JS runtime resolves dynamic imports from the shared `__we_modules` +/// registry. Document-level module scripts populate that registry through +/// `load_and_execute_modules`; classic scripts can also contain dynamic imports, +/// so the browser layer preloads their statically-discoverable targets here +/// before the event loop settles the returned promises. +pub fn preload_dynamic_imports_for_program( + vm: &mut Vm, + loader: &mut ResourceLoader, + origin: &Origin, + base: &Url, + import_map: &ImportMap, + program: &Program, +) { + let mut entries = Vec::new(); + + for specifier in module::dynamic_import_specifiers(program) { + let Some(dep_url) = resolve_specifier(&specifier, base, import_map) else { + eprintln!("[module] cannot resolve dynamic import specifier {specifier:?}"); + continue; + }; + let dep_key = dep_url.serialize(); + if entries + .iter() + .any(|entry: &EntryModule| entry.key == dep_key) + { + continue; + } + if let Some(source) = fetch_module_source(loader, &dep_url, origin) { + entries.push(EntryModule { + key: dep_key, + base: dep_url, + source, + }); + } + } + + load_and_execute_modules(vm, loader, origin, entries, import_map); +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/browser/src/script_loader.rs b/crates/browser/src/script_loader.rs index 79c0ffe..408b9eb 100644 --- a/crates/browser/src/script_loader.rs +++ b/crates/browser/src/script_loader.rs @@ -7,6 +7,7 @@ use crate::csp::DirectiveType; use crate::import_map::ImportMap; use crate::loader::{Resource, ResourceLoader, ResourceRequestType}; use we_dom::{Document, NodeId}; +use we_js::ast::Program; use we_js::compiler; use we_js::parser::Parser; use we_js::vm::{ConsoleOutput, Vm}; @@ -191,31 +192,28 @@ fn fetch_script_text( } } -/// Compile and execute a classic script in the VM. Errors are logged, not -/// propagated. -fn execute_script(vm: &mut Vm, source: &str, label: &str) { - execute_source(vm, source, label, ScriptKind::Classic); -} - -/// Compile and execute a script in the VM, parsing it as a classic script or -/// an ES module per `kind`. Errors are logged, not propagated. -fn execute_source(vm: &mut Vm, source: &str, label: &str, kind: ScriptKind) { +/// Parse a script as a classic script or an ES module per `kind`. +fn parse_source(source: &str, label: &str, kind: ScriptKind) -> Option { let parsed = match kind { ScriptKind::Module => Parser::parse_module(source), _ => Parser::parse(source), }; - let ast = match parsed { - Ok(ast) => ast, + match parsed { + Ok(ast) => Some(ast), Err(e) => { eprintln!( "[script] parse error in {label}: {e}\n preview: {}", script_preview(source) ); - return; + None } - }; + } +} - let func = match compiler::compile(&ast) { +/// Compile and execute an already-parsed script. Errors are logged, not +/// propagated. +fn execute_program(vm: &mut Vm, program: &Program, source: &str, label: &str) { + let func = match compiler::compile(program) { Ok(f) => f, Err(e) => { eprintln!( @@ -234,6 +232,39 @@ fn execute_source(vm: &mut Vm, source: &str, label: &str, kind: ScriptKind) { } } +struct ScriptExecutionContext<'a> { + loader: &'a mut ResourceLoader, + base_url: &'a Url, + document_origin: &'a Origin, + import_map: &'a ImportMap, +} + +/// Execute a script and then preload modules referenced by literal +/// `import("")` calls in classic script source. +fn execute_source_with_dynamic_import_preloads( + vm: &mut Vm, + ctx: &mut ScriptExecutionContext<'_>, + source: &str, + label: &str, + kind: ScriptKind, +) { + let Some(program) = parse_source(source, label, kind) else { + return; + }; + execute_program(vm, &program, source, label); + + if kind == ScriptKind::Classic { + crate::module_loader::preload_dynamic_imports_for_program( + vm, + ctx.loader, + ctx.document_origin, + ctx.base_url, + ctx.import_map, + &program, + ); + } +} + /// Trim a script to its first ~120 chars of "meaningful" content for error /// logging. Strips leading whitespace, comment fences, and CDATA wrappers /// so the snippet starts with code the reader can recognise. @@ -477,7 +508,19 @@ pub fn execute_scripts_into_vm_with_console( // Synchronous and async scripts: execute immediately. // Async scripts have no ordering guarantee, but since we // fetch synchronously they execute in document order. - execute_script(&mut vm, &source, &label); + let mut script_ctx = ScriptExecutionContext { + loader, + base_url, + document_origin, + import_map: &import_map, + }; + execute_source_with_dynamic_import_preloads( + &mut vm, + &mut script_ctx, + &source, + &label, + ScriptKind::Classic, + ); } } @@ -503,7 +546,19 @@ pub fn execute_scripts_into_vm_with_console( // Execute deferred scripts in document order. for (source, label) in &deferred_sources { - execute_script(&mut vm, source, label); + let mut script_ctx = ScriptExecutionContext { + loader, + base_url, + document_origin, + import_map: &import_map, + }; + execute_source_with_dynamic_import_preloads( + &mut vm, + &mut script_ctx, + source, + label, + ScriptKind::Classic, + ); } // Pump the event loop to handle any pending microtasks/timers. @@ -515,7 +570,14 @@ pub fn execute_scripts_into_vm_with_console( // injecting a + "#; + let doc = run_scripts(html); + assert_eq!( + find_text_by_id(&doc, doc.root(), "target").as_deref(), + Some("dynamic"), + "classic-script dynamic import should resolve from the preloaded module registry" + ); + } } diff --git a/crates/e2e/real-web/snapshots/bing.com/assets.msn.com/bundles/v1/bingHomepage/latest/widget-initializer.js b/crates/e2e/real-web/snapshots/bing.com/assets.msn.com/bundles/v1/bingHomepage/latest/widget-initializer.js new file mode 100644 index 0000000..854cd45 --- /dev/null +++ b/crates/e2e/real-web/snapshots/bing.com/assets.msn.com/bundles/v1/bingHomepage/latest/widget-initializer.js @@ -0,0 +1,2 @@ +let e,t,n,r,i,o,s,a,c,l,u;var d,p,f,g,h,m,w,y,b={},v={};function _(e){var t=v[e];if(void 0!==t)return t.exports;var n=v[e]={exports:{}};return b[e](n,n.exports,_),n.exports}_.d=(e,t)=>{for(var n in t)_.o(t,n)&&!_.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},_.g=(()=>{if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}})(),_.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var C={};function S(){return!!("u">typeof window&&window.document&&window.document.createElement&&!window.isRenderServiceEnv)}_.d(C,{v:()=>tg});let x=new Map,P=Object.freeze({set(e,t){return x.set(e,t),this},get:function(e,t){let n=x.get(e);return void 0===n&&t&&(n=t(),x.set(e,n)),n},clear(){x.clear()},delete:e=>x.delete(e),has:e=>x.has(e)}),A="__RequestDataInstance__";class E{constructor(e,t){if(this.url=new URL(e.href),this.innerHeight=e.innerHeight,this.devicePixelRatio=e.devicePixelRatio,this.canUseCssGrid=e.canUseCssGrid,this.requestId=e.requestId,this.cookie=e.cookie,this.referer=e.referer,this.userAgent=e.userAgent,this.clientData=e.clientData,this.vpCetoToken=e.vpCetoToken,this.oneServiceHeaders=function(e){try{if(e)return JSON.parse(e)}catch(e){}}(e.oneServiceHeaders)||{},this.isPssrMode=t,t){let e=T("OSATE",this.cookie),t=!!e&&"1"===e,n=T("OSAT",this.cookie);if(t&&n||!e&&!n){this.msalAuthReady=!0,n&&(this.oneServiceHeaders.Authorization=`Bearer ${n}`);return}this.msalAuthReady=!1,e&&!t?this.pssrRejectedReason="interactiveLogin":e&&!n?this.pssrRejectedReason="missOSAT":this.pssrRejectedReason="missOSATE"}}static getInstance(){let e=P.get(A);return e||(_.g.TEST_ENV?I({href:"http://localhost:8080/",innerHeight:768,devicePixelRatio:1,canUseCssGrid:!1,requestId:"0",cookie:"",userAgent:"",referer:"",oneServiceHeaders:"",vpCetoToken:""}):I({href:"http://localhost:8080/",innerHeight:0,devicePixelRatio:0,canUseCssGrid:!1,requestId:"0",cookie:"",userAgent:"",referer:"",oneServiceHeaders:"",vpCetoToken:""}))}static resetInstance(e,t=!1){let n=new E(e,t);return P.set(A,n),n}}function T(e,t){if(t&&e){let n=RegExp("\\b"+e+"\\s*=\\s*([^;]*)","i").exec(t);return n&&n.length>1?n[1]:null}return null}let I=E.resetInstance;function O(e,t,n=!0,r="anonymous",i,o){if(!S())throw Error("createScriptTag is unsupported server side. This call should be wrapped in canUseDOM().");let s=document.createElement("script");return t&&(s.id=t),i&&s.setAttribute("class",i),o&&(s.onload=o),s.type="text/javascript",e&&(window&&window.trustedTypesURLValidation?s.src=window.trustedTypesURLValidation(e,"scriptSrcUrlPolicy"):s.src=e,s.async=n,s.crossOrigin=r),window&&window.NONCE_ID&&(s.nonce=window.NONCE_ID),s}function k(e,t){if(e){let n=RegExp("\\b"+t+"\\s*=\\s*([^;]*)","i").exec(e);return n&&n.length>1?n[1]:null}return null}function $(e){try{if(e)return JSON.parse(e)}catch(e){}return null}function D(e){return document.head.dataset[e]||R()?.dataset[e]}function R(){return void 0===e&&(e=document.getElementsByClassName("peregrine-widget-settings")[0]||null),e}function L(e){if(!e||!e.startsWith("f:"))return;let t=(e.match(/f:\s*([^;]+)/i)||[])[1];return t?t.toLowerCase().split(",")?.sort():[]}(d=w||(w={})).Treatment1="prg-ad-xan-tmx1",d.Treatment2="prg-ad-xan-tmx2",d.Treatment3="prg-ad-xan-tmx3",d.Treatment4="prg-ad-xan-tmx4",d.Treatment5="prg-ad-xan-tmx5",d.Treatment6="prg-ad-xan-tmx6",(p=y||(y={})).INITIALIZE="initialize",p.CHANGE="change",p.ALL="all";let j="CustomWidgetErrorEvent";function N(e,t){window.dispatchEvent(new CustomEvent(e,{bubbles:!0,cancelable:!0,composed:!0,detail:{type:"AppError",payload:t}}))}let U="peregrine-widgetgroup",W="peregrine-widget-settings",z="peregrine-core-bundle",F="data-widget",M="peregrine-widget-load-icon",q=new Map;function B(e,t){if(e instanceof Error){let n=e.toString();return t&&e.stack?`${n}, Stack: ${e.stack}`:n}if("string"==typeof e)return e;var n=e;if(void 0===n)return"undefined";try{return JSON.stringify(n)}catch{return"[serialization-error]"}}function J(e,t){if(e&&(n=e),t&&(r=t),!i){let{connection:e={},deviceMemory:t="",hardwareConcurrency:n}=window.navigator||{};i={connectionEffectiveType:e&&e.effectiveType,deviceMemory:t,hardwareConcurrency:n}}}async function H(e,r,i){try{var o;if(!function(e){if(!e)return!0;let t=q.get(e)||0;return t>=10?(console.error(`App Error ${e} throttled: Maximum 10 events exceeded (current: ${t})`),!1):(q.set(e,t+1),!0)}(r))return;let s=function(e){if(e){let{pcsInfo:r,pageGenTime:i}=e,o=new Date(i).getTime(),s=!r||"prod"===r.env;return t=s?`browser.events.data.msn.${n.useCnDomain?"cn":"com"}`:`events-sandbox.data.msn.${n.useCnDomain?"cn":"com"}`,{cors:"true","content-type":"application/x-json-stream","client-id":"NO_AUTH","client-version":"1DS-Web-JS-2.2.2",apikey:s?"0ded60c75e44443aa3484c42c1c43fe8-9fc57d3f-fdac-4bcf-b927-75eafe60192e-7279":"f8857dedc6f54ca8962cfb713e01e7d7-e9250191-fe0b-446f-95ae-07516262f98c-7028","upload-time":o,w:"0",anoncknm:"app_anon"}}return null}({}),a="";s&&s.apikey&&(a=function(e){if(e){let t=e.indexOf("-");if(t>0)return e.substring(0,t)}return""}(s.apikey));let c=await G(r,e,a,i);Q(e,s,c),s&&c?(console.error(c),navigator.sendBeacon("https://"+t+"/OneCollector/1.0"+(o=s,"?"+Object.keys(o).map(function(e){return e+"="+encodeURIComponent(o[e])}).join("&")),c)):console.log("missing required params. "+e)}catch(t){console.log(`Error ${t} logging the app error. ${e}`),Q(e,{},"")}}function Q(e,t,n){N(j,{stage:y.ALL,message:e,extra:{payload:n,params:t}})}async function G(e,t,o,s){if(!n)return;let{appType:a,pageType:c,cm:l,telemetry:u={},pcsAdditionalQsp:d=""}=n,{pageProduct:p,pagetype:f}=u;if((s=s||{}).dataWidgetAttributes=n,r){let{bundleInfo:e,pcsInfo:t}=r;t&&(s.pcsVersion=t.v,s.pcsEnv=t.env);let{alias:n,v:i}=e||{};s.build=i,s.buildAlias=n}i&&(s.navigator=i);let g="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".replace(/[xy]/g,function(e){let t=16*Math.random()|0;return("x"==e?t:3&t|8).toString(16)}),h="object"==typeof window&&window._tryGetCustomAppErrorEventNameAsync,m={name:"function"==typeof h&&await h()||"MS.News.Web.AppError",time:new Date().toISOString(),ver:"4.0",iKey:`o:${o}`,data:{baseData:{},baseType:"MS.News.Web.Base",page:{name:"default",product:function(e,t=null,n=null){return t&&t.length>0?t:"windowsNewsPlus"===e||"winWidgets"===e?n&&n.includes("query=enterprise")?"entwindowsdash":"windowsdash":e}(a,p,d),type:V(a)?"dhp":f||c||"dhp",content:{category:"standaloneError"}},browser:{clientId:""},flight:{id:""},request:{activityId:g,requestId:g,afdMuid:r&&r.fd_muid?r.fd_muid:""},locale:{mkt:l},extSchema:{id:e,severity:0,pb:s,message:t}}};return m?JSON.stringify(m):null}function V(e){return"windowsNewsPlus"===e||"winWidgets"===e}let K=!1;function Z(){if(K)return;let e=X();if(!e)return;let t=e.getElementsByClassName(M)[0];t&&(e.removeChild(t),K=!0)}function X(){return document.body.getElementsByClassName(U)[0]}let Y="microsoftstart",ee="item=pcsenv:microsoftstart",et="_appTimings";function en(e){performance&&performance.now&&((window._pageTimings||(window._pageTimings={}))[e]=Math.round(performance.now()))}function er(e){performance&&performance.now&&((window[et]||(window[et]={}))[e]=performance.now())}function ei(){return"object"==typeof window&&null!==window?window._pageTimings||(window._pageTimings={}):{}}var eo=Array.isArray,es="object"==typeof global&&global&&global.Object===Object&&global,ea="object"==typeof self&&self&&self.Object===Object&&self,ec=es||ea||Function("return this")(),el=ec.Symbol,eu=Object.prototype,ed=eu.hasOwnProperty,ep=eu.toString,ef=el?el.toStringTag:void 0;let eg=function(e){var t=ed.call(e,ef),n=e[ef];try{e[ef]=void 0;var r=!0}catch(e){}var i=ep.call(e);return r&&(t?e[ef]=n:delete e[ef]),i};var eh=Object.prototype.toString,em=el?el.toStringTag:void 0;let ew=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":em&&em in Object(e)?eg(e):eh.call(e)},ey=function(e){return"symbol"==typeof e||null!=e&&"object"==typeof e&&"[object Symbol]"==ew(e)};var eb=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,ev=/^\w*$/;let e_=function(e,t){if(eo(e))return!1;var n=typeof e;return!!("number"==n||"symbol"==n||"boolean"==n||null==e||ey(e))||ev.test(e)||!eb.test(e)||null!=t&&e in Object(t)},eC=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)},eS=function(e){if(!eC(e))return!1;var t=ew(e);return"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t};var ex=ec["__core-js_shared__"],eP=(f=/[^.]+$/.exec(ex&&ex.keys&&ex.keys.IE_PROTO||""))?"Symbol(src)_1."+f:"",eA=Function.prototype.toString;let eE=function(e){if(null!=e){try{return eA.call(e)}catch(e){}try{return e+""}catch(e){}}return""};var eT=/^\[object .+?Constructor\]$/,eI=Object.prototype,eO=Function.prototype.toString,ek=eI.hasOwnProperty,e$=RegExp("^"+eO.call(ek).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");let eD=function(e){return!!eC(e)&&(!eP||!(eP in e))&&(eS(e)?e$:eT).test(eE(e))},eR=function(e,t){var n=null==e?void 0:e[t];return eD(n)?n:void 0};var eL=eR(Object,"create"),ej=Object.prototype.hasOwnProperty,eN=Object.prototype.hasOwnProperty;function eU(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t-1},eF.prototype.set=function(e,t){var n=this.__data__,r=eW(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this};var eM=eR(ec,"Map");let eq=function(e){var t=typeof e;return"string"==t||"number"==t||"symbol"==t||"boolean"==t?"__proto__"!==e:null===e},eB=function(e,t){var n=e.__data__;return eq(t)?n["string"==typeof t?"string":"hash"]:n.map};function eJ(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t=0)return this.storage.key(e)}keys(){return this.supported?Object.keys(this.storage):[]}setObject(e,t){void 0!==t?this.setItem(e,JSON.stringify(t)):this.removeItem(e)}removeObject(e){let t=this.removeItem(e);if(null!=t)return $(t)}setItem(e,t,n=!0){if(this.supported)try{if(!t)throw"Attempted to store null/undefined value: "+t;this.storage.setItem(e,t)}catch(e){if(n)e8.logError(e);else throw e}else if(!n)throw Error("WebStorage not supported")}get length(){if(this.supported)return this.storage.length}removeItem(e){if(this.supported){let t=this.getItem(e);return this.storage.removeItem(e),t}}clear(){this.supported&&this.storage.clear()}removeSubstringKeys(e){if(!this.supported||!e)return;let t=[];for(let n=0;n{try{let e=await ti();tc(e)}catch(e){H("Error writing PCS response to local storage",52008,{error:B(e,!0)})}},r=await ts()):window.__useCachedApp&&(r=await ts()),!r&&a&&a.customPrefetchStrategy){let e=null;if("string"==typeof a.customPrefetchStrategy&&(e=e3(window,a.customPrefetchStrategy,null)),e)try{r=await e,r=tu(r,o.useCnDomain)}catch(e){r=null}}return r?n[tt]=1:s=await ti(),!s&&window.__useCachedApp&&(r=await ts(!0)),window.__useCachedApp&&(t=new Promise(e=>{ti().then(t=>{tc(t),e(t)})}),window._pw_tryGetPcsResponseFromNetwork=t),en("WidgetsPcsCallEnd"),er("WidgetsPcsCall-end"),s||r}let ti=(m=async()=>{let e,t;if(!o)return H("dataWidgetAttributes is null in getPcsResponseFromNetwork",43715,{}),null;let n=tp();try{let r=new Promise(t=>{e=setTimeout(()=>{H("PCS Widgets API call timed out. Trying to get response from statics",43713,{url:n}),t(null)},15e3)}),i=fetch(n,{credentials:"include"}).then(async t=>{if(e&&clearTimeout(e),t.ok)return await t.json();let r=tf(t);return H("Verify PCS response for pcsData and bundleData for request url",43716,{fetchUrl:n,afdAId:r}),null});t=await Promise.race([i,r])}catch(i){e&&clearTimeout(e);let{appType:t,isCallFromWidgetsChanged:r}=o||{};V(t)&&r||H("Fetch failed for PCS url",43715,{originalUrl:n,error:B(i,!1)})}return tl(t)||(t=await to(n)),navigator&&navigator.onLine,tl(t)||(t=await to(n,!0)),tu(t,o.useCnDomain)},l=!1,(u=function(){return l||(l=!0,c=m()),c}).cache={clear:()=>{l=!1,c=void 0}},u);async function to(e,t=!1){let n,{instance:r,staticsEnv:i}=o&&o.staticsAttributes||{},s=ei();s[te]=(s[te]||0)+1,r&&(e+=`&instance=${r}`),i&&(e+=`&staticsEnv=${i}`);let a=new URL(e),c=e.replace(a.hostname,"staticview.msn.com");a.hostname.includes(".cn")&&(c=e.replace(a.hostname,"staticview.msn.cn"));try{let r=new Promise(r=>{n=setTimeout(()=>{let[n,i]=t?["second",52009]:["first",52006];H(`PCS static Response timed out in ${n} attempt`,i,{originalUrl:e,staticUrl:c}),r(null)},15e3)}),i=fetch(c,{credentials:"include"}).then(async r=>{n&&clearTimeout(n);let i=tf(r);if(r.ok)return await r.json();{let[n,r]=t?["second",52010]:["first",52004];return H(`Response not-ok in ${n} attempt to get PCS staticview`,r,{originalUrl:e,staticUrl:c,afdAId:i}),null}});return await Promise.race([i,r])}catch(o){n&&clearTimeout(n);let[r,i]=t?["second",52011]:["first",52005];return H(`Exception in ${r} attempt to get PCS static response`,i,{error:B(o,!1),originalUrl:e,staticUrl:c}),null}}async function ts(e=!1){try{let n=td(),r=JSON.parse(localStorage.getItem(n));if(!r&&window.__bridgeSdk&&window.__useCachedApp){let e=await window.__bridgeSdk.promiseWithTimeout?.(window.__bridgeSdk.loadDataAsync?.({key:n}),500).then(e=>e?JSON.parse(e).value:null);e&&(r=JSON.parse(e))}let{appType:i}=o||{},s=r&&r.storageTimestamp,a=r&&r.pcsUrl,c=tp();if(r||ta("Empty cache response"),(window.__useCachedApp&&!navigator.onLine||e)&&a===c&&tl(r))return r;if(i&&"winWidgets"===i){var t;let e;return o.useCnDomain&&(t=r,(e=t?.pcsData?.info||"").substring(2,e.length-1).split(",").includes("prg-pr2-akmpcs"))&&(o.pcsEnvironment="assets"),null}let l=window.__useCachedApp?864e5:1728e5,u=new Date().getTime(),d=!s||u-s>=l;if(!d&&a===c&&tl(r))return r;d?ta("Cache is stale"):a!==c?ta("Cached PCS URL does not match current PCS URL"):ta("Cached PCS response is not valid")}catch(t){let e="Error reading PCS response from local storage";ta(e),H(e,52007,{error:B(t,!0)})}return null}function ta(e){let t=ei();t[tn]||(t[tn]=e)}function tc(e){if(e){e.storageTimestamp=new Date().getTime(),e.pcsUrl=tp();let t=td();localStorage.setItem(t,JSON.stringify(e)),window.__bridgeSdk&&window.__useCachedApp&&window.__bridgeSdk.saveDataAsync?.({key:t,value:JSON.stringify(e)}).catch(e=>{H("Error writing PCS response to native storage",52008,{error:B(e,!0)})})}else H("PCS response was null",52008,{})}function tl(e){return e&&e.pcsData&&e.pcsData.info&&e.pcsData.clientSettings&&e.bundleData&&e.bundleData.bootstrappingRefs}function tu(e,t){return tl(e)?(e.pcsData.clientSettings.useCnDomain=t,e.pcsData.clientSettings.widgetAttributes=o,e):null}function td(){if(!o)return"";let{appType:e,cm:t,pageType:n}=o;return`pw-pcsresponse_${e}_${n}_${t}`}function tp(){if(!o)return"";let{appType:e,useCnDomain:t,pageType:n,pcsEnvironment:r="www",pcsAdditionalQsp:i}=o,s=i;if(V(e)&&"u">typeof location&&location.search&&"u">typeof URLSearchParams){let e=new URLSearchParams(location.search),t=e.get("uxmode");if(t){let e=`uxmode=${t}`;s=s?`${s}&${e}`:e}if("vp"===e.get("reqsrc")){let e="reqsrc=vp";s=s?`${s}&${e}`:e}}let a=e9.getItem("Prong2MarketSelectorEnabled"),c=e9.getItem("Prong2PcsUrl");return"1"===a&&c&&c.includes("/pcs/api/widget/")?`${c}${s?`&${s.split("&").filter(e=>!e.startsWith("locale=")&&!e.startsWith("region=")).join("&")}`:""}`:`https://${r}.msn.${t?"cn":"com"}/pcs/api/widget/${e}/${n||""}${s?`?${s}`:""}`}function tf(e){return e&&e.headers&&e.headers.get("x-msedge-ref")||""}async function tg(e){if(!(!S()||"u"{window.addEventListener("online",()=>e(),{once:!0})});await e}let t=document.body.getElementsByClassName(U)[0],n=document.body.getElementsByClassName(z);if(t&&0==n.length){let n=function(){let e=document.body.getElementsByClassName(U),t=(void 0)??(e&&e[0]?.getAttribute(F));if(!t)return void console.error("Missing getElementsByClassName, peregrine-widgetgroup or data-widget");let n={};try{n=JSON.parse(t)}catch(e){console.error("dataWidget to parse is not valid JSON");return}let r=function(){let e=location.search&&location.search.replace("?","").split("&"),t={};if(!e)return t;for(let n=0;n-1)return"int1";if(n.indexOf("item=pcsenv:int")>-1||n.indexOf("item=pcsenv:intpr")>-1||n.indexOf("item=pcsenv:intperf")>-1)return"int";if("distribution"===e||n.indexOf(ee)>-1)return Y}let r=t&&t.replace("?","").split("&");return r&&r.indexOf(ee)>-1?Y:"www"}(i,n.pcsAdditionalQsp),telemetry:r.telemetry||n.telemetry,useCnDomain:r.useCnDomain||n.useCnDomain||!1,propertyBag:n.propertyBag,staticsAttributes:r.staticsAttributes||n.staticsAttributes};if(J(o),!o.appType||!o.pageType||!o.pcsEnvironment){let e="Missing required params in "+F;H(e+="\n appType: "+o.appType+"\n pageType: "+o.pageType+"\n pcsEnvironment: "+o.pcsEnvironment,43711,{});return}let s=[];return r.pcsAdditionalQsp&&s.push(r.pcsAdditionalQsp),n.pcsAdditionalQsp&&s.push(n.pcsAdditionalQsp),o.cm&&s.push(`cm=${o.cm}`),o.dl&&s.push(`dl=${o.dl}`),s&&s.length>0&&(o.pcsAdditionalQsp=s.join("&")),o}(),o=await (a??tr(n)),s=ei();if(s.PCSStart=s.WidgetsPcsCallStart,s.PCSEnd=s.WidgetsPcsCallEnd,s.PCSRetry=s.PcsRetry,o&&o.bundleData){var r,i;!function(e){var t;if(!e||!e.clientPerf||!e.clientPerf.shouldShowLoadingIcon)return;let n=X();if(!n)return;let r=document.createElement("img");r.setAttribute("class",M),r.setAttribute("src",(t=e.useCnDomain,`https://assets.msn.${t?"cn":"com"}/staticsb/statics/latest/widget/LoadingImg.gif`)),r.setAttribute("alt","Loading"),r.setAttribute("style","display:block;margin:auto;"),n.appendChild(r),window._pw_removeLoadingIcon=Z,setTimeout(Z,5e3)}(n);let{canvasInfo:s,clientSettings:a,info:c}=o.pcsData;(J(void 0,a),(r=o.bundleData.cookieBanner)&&r.cmpSdkPath)?!function(e){if(!e)return;let{cmpSdkPath:t}=e;if(!t)return;let n=O(t,"cmp-sdk",!0,"anonymous"),r=document.createAttribute("data-document-language");r.value="true",n.attributes.setNamedItem(r);let i=document.createAttribute("charset");i.value="UTF-8",n.attributes.setNamedItem(i),document.head.appendChild(n)}(o.bundleData.cookieBanner):(i=o.bundleData.cookieBanner)&&i.oneTrustDataDomainScriptID&&i.oneTrustSdkPath&&function(e){if(!e)return;let{oneTrustDataDomainScriptID:t,oneTrustSdkPath:n}=e;if(!t||!n)return;let r=O(n,"onetrust-sdk",!0,"anonymous"),i=document.createAttribute("data-document-language");i.value="true",r.attributes.setNamedItem(i);let o=document.createAttribute("charset");o.value="UTF-8",r.attributes.setNamedItem(o);let s=document.createAttribute("data-domain-script");s.value=t,r.attributes.setNamedItem(s),document.head.appendChild(r)}(o.bundleData.cookieBanner);let l=function(e){let t=e.getElementsByClassName(W)[0];if(t)return t;let n=document.createElement("div");return n.setAttribute("class",W),e.appendChild(n),n}(t);for(let e in l.setAttribute("data-info",c),s&&l.setAttribute("data-canvas-info",s),l.setAttribute("data-client-settings",JSON.stringify(a)),!function(e={}){let t={};try{if(!S())return t;let n=$(D("clientSettings"))||{},r=n.apptype?.toLocaleLowerCase(),i=e.crsOptions||{};("winwidgets"===r||"homepage"===r)&&(i.crsPrefetch=!0),n.widgetAttributes?.clientPerf?.earlyCRSCall&&(i.crsPrefetch=!0),"edgechromium"===r&&(r="edgechr");let o=!!n.widgetAttributes?.clientPerf?.shouldUsePWOnlyFlights,{all:s,prg:a}=function(e=!1,t=""){let n=L(D("info"))||[],r=L(D("canvasInfo"));return r||(r=function(e,t="prg-"){let n=t.length,r=[];return e&&e.length&&(e.forEach(e=>{let i=e&&e.trim();i&&i.length>=n&&i.substring(0,n).toLowerCase()===t&&r.push(i.toLowerCase())}),r.sort()),r}(n,function(e=!1,t=""){let n="prg-";return e?n="prg-pw-":t.toLocaleLowerCase()==="windowsShell".toLocaleLowerCase()||t.toLocaleLowerCase()==="windowsShellV2".toLocaleLowerCase()?n="prg-1sw-":t.toLocaleLowerCase()!=="winWidgets".toLocaleLowerCase()||window?.location?.search?.includes("reqsrc=vp")||window?.location?.search?.includes("vptest")||(n="prg-pr2-"),n}(e,t))),{all:n,prg:r}}(o,r);(i.crsPrefetch||s.includes("inlinecrscall")||s.includes(`icrscall-${r}`))&&(function(e,t,n){var r,i;let o=function(e,t){let n=decodeURIComponent(k(e,"aace")),{child:r}=n&&$(n)||{},i=k(e,"child");return n&&(1===r||i)?"kids":t.modes?.audienceMode||t.audienceMode||"adult"}(document.cookie,e),{apptype:s,browser:a,domain:c,bundleInfo:{v:l}={},deviceFormFactor:u,locale:{content:d,display:p}={},ocid:f,os:g,modes:h,pagetype:m,servicesEndpoints:{crs:w}={}}=e,y=(R()?S()?window.location.hostname:E.getInstance().url.hostname:c)||"";if(!s||!l||!w)return;let b={audienceMode:o,uxMode:h?.uxMode},v=new URLSearchParams([["expType","AppConfig"],["expInstance","default"],["apptype",s],["v",l]]),_=S()?window.location.search:E.getInstance().url.search,C=_.includes("?item=")||_.includes("&item=")?"&citem=dev:true&instance=Int1":"",x=`${w.path}${w.v}/config/?${v}&targetScope=${JSON.stringify({audienceMode:o,browser:a,deviceFormFactor:u,domain:y,locale:{content:d,display:p},ocid:f,os:g,modes:b,platform:"web",pageType:m,pageExperiments:t})}${C}`,P=e=>{let t,r=new URL(`https://${e}${x}`),{timeout:i=5e3}=n;if(window.crsRequestData={url:r.toString()},i>=0){let e=new AbortController;setTimeout(()=>e.abort(),i),t=e.signal}return fetch(r,{signal:t})},A=`msn.${(r=location,i=e,r.hostname.toLowerCase().endsWith(".cn")||i.isChinaCompliance||i.useCnDomain||r.search.match(/isChinaBuild=true/i)||r.search.match(/item=revip:cn/))?"cn":"com"}`,{domain:T}=w,I=P(T&&new URL(T).host||`assets.${A}`);n.disableFallback||(I=I.catch(()=>{let{hostname:e}=location;return P(e.match(/\.msn\.(com|cn)$/)?e:`www.${A}`)})),n.silenceUnhandledError&&(I=I.catch(e=>{console.error("InlineHeadCommon: CRS fetch error in silence",e)})),window.crsPromise=I}(n,a,i),t.inlineCRS=!0)}catch(e){console.error("InlineHeadCommon: "+e)}}({crsOptions:{disableFallback:!0,timeout:-1}}),o.bundleData.bootstrappingRefs){let n=e&&o.bundleData.bootstrappingRefs[e];if(n){let e=O(n,null,!0,"anonymous",z);e.onerror=()=>{window.navigator?.onLine&&(window.__useCachedApp||localStorage.removeItem(td()))},t.appendChild(e)}}"string"==typeof e?window.__widgetTrackingJoinId=e:"object"==typeof e&&(window.__pw_options=e),function(e=["vendors","common","microsoft"],t="Error when loading bundle(s): ",n=15e3){setTimeout(()=>{let n=window.webpackJsonp;if(n){let r=n.length,i=e.length+1;r{t.length&&t[0][0]&&(e=e.splice(e.indexOf(t[0][0]),1))}),H(t+e.toString(),20202,{}))}},n)}(["vendors","common","microsoft"],"WidgetInitializer - Error loading bundle(s): ",15e3)}else N(j,{stage:y.INITIALIZE,message:"Call to PCS API did not return data when widget initialized!",extra:{attributes:n,response:o}})}}var th=C.v;export{th as startWidgetInitializer}; +//# sourceMappingURL=widget-initializer.js.map \ No newline at end of file diff --git a/crates/e2e/scenarios/real-web/bing.com.we b/crates/e2e/scenarios/real-web/bing.com.we index 0f7ea7b..1590f65 100644 --- a/crates/e2e/scenarios/real-web/bing.com.we +++ b/crates/e2e/scenarios/real-web/bing.com.we @@ -33,6 +33,7 @@ cache_put bing https://r.bing.com/rs/2A/4x/cc,nc/4PRBJAfIvdmgyFOSepob3sG79T4.css cache_put bing https://r.bing.com/rb/2A/cir3,cc,nc/t6ilQvdm1Kf1VaabvAwljycjim4.css?bu=CPIB9QGWAqgCmQKZApkC4gI&or=w ../../real-web/snapshots/bing.com/r.bing.com/rb/2A/cir3,cc,nc/t6ilQvdm1Kf1VaabvAwljycjim4.css text/css cache_put bing https://www.bing.com/th?id=OHR.SichuanTea_ROW1006834324_1920x1080.jpg ../../real-web/snapshots/bing.com/www.bing.com/th-OHR.SichuanTea_ROW1006834324_1920x1080.jpg image/jpeg +cache_put bing https://assets.msn.com/bundles/v1/bingHomepage/latest/widget-initializer.js ../../real-web/snapshots/bing.com/assets.msn.com/bundles/v1/bingHomepage/latest/widget-initializer.js application/javascript # First-level script bundles from the snapshot document. These let the # offline run exercise Bing's bootstrap/hydration path rather than stopping at diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 2a227f5..27bb901 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -275,6 +275,12 @@ pub fn init_builtins(vm: &mut Vm) { ); vm.set_global("Object", Value::Function(obj_ctor)); + // Create and register Function constructor/prototype. The engine already + // exposes Function.prototype.apply/call/bind through function property + // lookup; this object covers direct reads such as Function.prototype.toString. + let function_ctor = init_function_constructor(&mut vm.gc, &mut vm.shapes, obj_proto); + vm.set_global("Function", Value::Function(function_ctor)); + // Create and register Array constructor. let arr_ctor = init_array_constructor(&mut vm.gc, arr_proto); vm.set_global("Array", Value::Function(arr_ctor)); @@ -440,6 +446,88 @@ fn object_proto_value_of(_args: &[Value], ctx: &mut NativeContext) -> Result, + shapes: &mut ShapeTable, + obj_proto: GcRef, +) -> GcRef { + let mut proto_data = ObjectData::new(); + proto_data.prototype = Some(obj_proto); + let proto = gc.alloc(HeapObject::Object(proto_data)); + + let to_string = make_native(gc, "toString", function_proto_to_string); + set_builtin_prop(gc, shapes, proto, "toString", Value::Function(to_string)); + + let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { + private_fields: std::collections::HashMap::new(), + name: "Function".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: function_constructor, + }), + prototype_obj: Some(proto), + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + + set_builtin_prop(gc, shapes, proto, "constructor", Value::Function(ctor)); + ctor +} + +fn function_proto_to_string( + _args: &[Value], + ctx: &mut NativeContext, +) -> Result { + match ctx.this { + Value::Function(func_ref) => Ok(Value::String( + Value::Function(func_ref).to_js_string(ctx.gc), + )), + _ => Err(RuntimeError::type_error( + "Function.prototype.toString requires a function", + )), + } +} + +fn function_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let body = args + .last() + .map(|arg| arg.to_js_string(ctx.gc)) + .unwrap_or_default(); + let normalized = body.trim().trim_end_matches(';').trim(); + let callback: fn(&[Value], &mut NativeContext) -> Result = + if normalized == "return this" { + constructed_function_return_this + } else { + constructed_function_noop + }; + + Ok(Value::Function(ctx.gc.alloc(HeapObject::Function( + Box::new(FunctionData { + private_fields: std::collections::HashMap::new(), + name: "anonymous".to_string(), + kind: FunctionKind::Native(NativeFunc { callback }), + prototype_obj: None, + properties: HashMap::new(), + upvalues: Vec::new(), + }), + )))) +} + +fn constructed_function_return_this( + _args: &[Value], + ctx: &mut NativeContext, +) -> Result { + Ok(ctx.this.clone()) +} + +fn constructed_function_noop( + _args: &[Value], + _ctx: &mut NativeContext, +) -> Result { + Ok(Value::Undefined) +} + // ── Object constructor + static methods ────────────────────── #[allow(clippy::too_many_arguments)] @@ -8921,6 +9009,27 @@ mod uri_tests { vm.execute(&func).expect_err("execute should error") } + #[test] + fn function_constructor_and_prototype_are_exposed() { + assert_eq!( + eval("typeof Function + ':' + typeof Function.prototype.toString"), + "function:function" + ); + } + + #[test] + fn function_prototype_to_string_accepts_functions() { + assert_eq!( + eval("Function.prototype.toString.call(Object.prototype.hasOwnProperty).indexOf('hasOwnProperty') >= 0"), + "true" + ); + } + + #[test] + fn function_constructor_supports_return_this_probe() { + assert_eq!(eval("Function('return this')() === this"), "true"); + } + #[test] fn promise_prototype_exposes_then_method() { assert_eq!(