From 931cf7ed5beb2166d3c8925963ee150c0cd3edd6 Mon Sep 17 00:00:00 2001 From: Zachary Miller Date: Sat, 7 Nov 2026 17:27:05 -0500 Subject: [PATCH 1/2] Match ajax.trackMethods as before when it is not an array "Replace CoffeeScript helpers with built-in features" replaced the indexOf polyfill with trackMethods.includes() and said nothing else changed. That holds for arrays, but a misconfigured string now matched substrings ('GET' matched GET), and a number made the page's own xhr.open() throw. Calling Array.prototype.includes on the value restores the old result: a string or number matches nothing. --- pace.js | 5 ++++- pace.min.js | 2 +- tests/ajax.spec.mjs | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pace.js b/pace.js index c5e4770..c7fbc0a 100644 --- a/pace.js +++ b/pace.js @@ -304,7 +304,10 @@ if (!ignoreStack.length && options.ajax) { if (method === 'socket' && options.ajax.trackWebSockets) { return true; - } else if (options.ajax.trackMethods.includes(method.toUpperCase())) { + } + // Array.prototype.includes, so a string or number from a misconfiguration + // matches nothing instead of matching substrings or throwing. + if (Array.prototype.includes.call(options.ajax.trackMethods, method.toUpperCase())) { return true; } } diff --git a/pace.min.js b/pace.min.js index 7fed67c..6a6f327 100644 --- a/pace.min.js +++ b/pace.min.js @@ -2,4 +2,4 @@ * pace.js v1.3.1 * https://github.com/CodeByZach/pace/ * Licensed MIT © HubSpot, Inc. - */(function(){const E={className:"",catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},l=()=>performance.now(),I=window.requestAnimationFrame,W=window.cancelAnimationFrame,H=s=>{let e=l();const t=()=>{const r=l()-e;return r>=33?(e=l(),s(r,()=>I(t))):setTimeout(t,33-r)};return t()},L=(s,e,...t)=>typeof s[e]=="function"?s[e].apply(s,t):s[e],y=(s,...e)=>{for(const t of e)if(t)for(const r in t){if(!Object.prototype.hasOwnProperty.call(t,r)||r==="__proto__"||r==="constructor"||r==="prototype")continue;const n=t[r];s[r]!=null&&typeof s[r]=="object"&&n!=null&&typeof n=="object"?y(s[r],n):s[r]=n}return s},X=s=>{let e=0,t=0;for(const r of s)e+=Math.abs(r),t++;return e/t},$=(s="options",e=!0)=>{const t=document.querySelector(`script[data-pace-${s}]`);if(!t)return;const r=t.getAttribute(`data-pace-${s}`);if(!e)return r;try{return JSON.parse(r)}catch(n){console.error("Error parsing inline pace options",n)}},i=window.Pace||{};window.Pace=i,Object.assign(i,{on(s,e,t,r){r??=!1,this.bindings??={},this.bindings[s]??=[],this.bindings[s].push({handler:e,ctx:t,once:r})},once(s,e,t){this.on(s,e,t,!0)},off(s,e){if(this.bindings?.[s]==null)return;if(e==null){delete this.bindings[s];return}let t=0;for(;t=100?t="99":(t=this.progress<10?"0":"",t+=this.progress|0),e.children[0].setAttribute("data-progress",t)}i.trigger("change",this.progress),this.lastRenderedProgress=this.progress}done(){return this.progress>=100}}class D{constructor(){this.bindings={}}trigger(e,t){if(this.bindings[e]!=null)for(const r of this.bindings[e].slice())r.call(this,t)}on(e,t){this.bindings[e]??=[],this.bindings[e].push(t)}}const q=window.XMLHttpRequest,b=window.WebSocket,R=(s,e)=>{for(const t in e.prototype)try{s[t]==null&&typeof e[t]!="function"&&Object.defineProperty(s,t,{get:()=>e.prototype[t],configurable:!0,enumerable:!0})}catch{}},p=[];i.ignore=(s,...e)=>{p.unshift("ignore");const t=s.apply(null,e);return p.shift(),t},i.track=(s,...e)=>{p.unshift("track");const t=s.apply(null,e);return p.shift(),t};const k=s=>{if(s??="GET",p[0]==="track")return"force";if(!p.length&&o.ajax){if(s==="socket"&&o.ajax.trackWebSockets)return!0;if(o.ajax.trackMethods.includes(s.toUpperCase()))return!0}return!1},G=s=>{const e=t=>{const r=t.open;t.open=(...n)=>{const[a,c]=n;return k(a)&&s.trigger("request",{type:a,url:c,request:t}),r.apply(t,n)}};window.XMLHttpRequest=function(t){const r=new q(t);return e(r),r};try{R(window.XMLHttpRequest,q)}catch{}if(o.ajax.trackWebSockets){window.WebSocket=function(t,r){let n;return r!=null?n=new b(t,r):n=new b(t),k("socket")&&s.trigger("request",{type:"socket",url:t,protocols:r,request:n}),n};try{R(window.WebSocket,b)}catch{}}};class J extends D{constructor(){super(),G(this)}}let P=null;const M=()=>(P??=new J,P),v=s=>{const e=o.ajax.ignoreURLs;for(let t=0,r=e.length;t{const{type:e,request:t,url:r}=s;if(!v(r)&&!i.running&&(o.restartOnRequestAfter!==!1||k(e)==="force")){let n=o.restartOnRequestAfter||0;typeof n=="boolean"&&(n=0),setTimeout(()=>{let a;if(e==="socket"?a=t.readyState<1:a=0this.watch(e))}watch({type:e,request:t,url:r}){if(v(r))return;let n;e==="socket"?n=new Y(t,this.complete):n=new K(t,this.complete),this.elements.push(n)}complete(e){this.elements=this.elements.filter(t=>t!==e)}}class K{constructor(e,t){this.progress=0,e.addEventListener("progress",r=>{r.lengthComputable?this.progress=100*r.loaded/r.total:this.progress=this.progress+(100-this.progress)/2},!1);for(const r of["load","abort","timeout","error"])e.addEventListener(r,()=>{t(this),this.progress=100},!1)}}class Y{constructor(e,t){this.progress=0;for(const r of["error","open"])e.addEventListener(r,()=>{t(this),this.progress=100},!1)}}class z{constructor(e){e??={},this.complete=this.complete.bind(this),this.elements=[],e.selectors??=[];const t=e.selectors;for(let r=0,n=t.length;rt!==e)}}class Q{constructor(e,t){this.selector=e,this.completeCallback=t,this.progress=0,this.check()}check(){document.querySelector(this.selector)?this.done():setTimeout(()=>this.check(),o.elements.checkInterval)}done(){this.completeCallback(this),this.completeCallback=null,this.progress=100}}class O{constructor(){this.progress=this.states[document.readyState]??100;const e=document.onreadystatechange;document.onreadystatechange=(...t)=>(this.states[document.readyState]!=null&&(this.progress=this.states[document.readyState]),typeof e=="function"?e.apply(null,t):void 0)}}O.prototype.states={loading:0,interactive:50,complete:100};class V{constructor(e){this.progress=0;let t=0;const r=[];let n=0,a=l();const c=setInterval(()=>{const u=l()-a-50;a=l(),r.push(u),r.length>e.sampleCount&&r.shift(),t=X(r),++n>=e.minSamples&&t=100&&(this.done=!0),t===this.last?this.sinceLastUpdate+=e:(this.sinceLastUpdate&&(this.rate=(t-this.last)/this.sinceLastUpdate),this.catchup=(t-this.progress)/o.catchupTime,this.sinceLastUpdate=0,this.last=t),t>this.progress&&(this.progress+=this.catchup*e);const r=1-Math.pow(this.progress/100,o.easeFactor);return this.progress+=r*this.rate*e,this.progress=Math.min(this.lastProgress+o.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress}}let d=null,A=null,h=null,C=null,g=null,S=null;i.running=!1;const j=()=>{o.restartOnPushState&&i.restart()},Z=window.history.pushState;window.history.pushState=(...s)=>(j(),Z.apply(window.history,s));const ee=window.history.replaceState;window.history.replaceState=(...s)=>(j(),ee.apply(window.history,s));const te={ajax:N,elements:z,document:O,eventLag:V},_=()=>{i.sources=d=[];for(const e of["ajax","elements","document","eventLag"])o[e]!==!1&&d.push(new te[e](o[e]));const s=o.extraSources??[];for(let e=0,t=s.length;e{i.trigger("stop"),i.running=!1,h.destroy(),S=!0,g!=null&&(W(g),g=null),_()},i.restart=()=>{i.trigger("restart"),i.stop(),i.start()},i.go=()=>{i.running=!0,h.render();const s=l();S=!1,g=H((e,t)=>{let r=0,n=0,a=!0;for(let u=0,f=d.length;u{h.finish(),i.running=!1,i.trigger("hide")},Math.max(o.ghostTime,Math.max(o.minTime-(l()-s),0)))):t()})},i.start=s=>{y(o,s),i.running=!0;try{h.render()}catch(e){w=e}document.querySelector(".pace")?(i.trigger("start"),i.go()):setTimeout(i.start,50)},typeof define=="function"&&define.amd?define(()=>i):typeof exports=="object"?module.exports=i:o.startOnPageLoad&&i.start()})(); + */(function(){const E={className:"",catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},l=()=>performance.now(),I=window.requestAnimationFrame,W=window.cancelAnimationFrame,H=s=>{let e=l();const t=()=>{const r=l()-e;return r>=33?(e=l(),s(r,()=>I(t))):setTimeout(t,33-r)};return t()},L=(s,e,...t)=>typeof s[e]=="function"?s[e].apply(s,t):s[e],y=(s,...e)=>{for(const t of e)if(t)for(const r in t){if(!Object.prototype.hasOwnProperty.call(t,r)||r==="__proto__"||r==="constructor"||r==="prototype")continue;const n=t[r];s[r]!=null&&typeof s[r]=="object"&&n!=null&&typeof n=="object"?y(s[r],n):s[r]=n}return s},X=s=>{let e=0,t=0;for(const r of s)e+=Math.abs(r),t++;return e/t},$=(s="options",e=!0)=>{const t=document.querySelector(`script[data-pace-${s}]`);if(!t)return;const r=t.getAttribute(`data-pace-${s}`);if(!e)return r;try{return JSON.parse(r)}catch(n){console.error("Error parsing inline pace options",n)}},i=window.Pace||{};window.Pace=i,Object.assign(i,{on(s,e,t,r){r??=!1,this.bindings??={},this.bindings[s]??=[],this.bindings[s].push({handler:e,ctx:t,once:r})},once(s,e,t){this.on(s,e,t,!0)},off(s,e){if(this.bindings?.[s]==null)return;if(e==null){delete this.bindings[s];return}let t=0;for(;t=100?t="99":(t=this.progress<10?"0":"",t+=this.progress|0),e.children[0].setAttribute("data-progress",t)}i.trigger("change",this.progress),this.lastRenderedProgress=this.progress}done(){return this.progress>=100}}class D{constructor(){this.bindings={}}trigger(e,t){if(this.bindings[e]!=null)for(const r of this.bindings[e].slice())r.call(this,t)}on(e,t){this.bindings[e]??=[],this.bindings[e].push(t)}}const q=window.XMLHttpRequest,b=window.WebSocket,R=(s,e)=>{for(const t in e.prototype)try{s[t]==null&&typeof e[t]!="function"&&Object.defineProperty(s,t,{get:()=>e.prototype[t],configurable:!0,enumerable:!0})}catch{}},p=[];i.ignore=(s,...e)=>{p.unshift("ignore");const t=s.apply(null,e);return p.shift(),t},i.track=(s,...e)=>{p.unshift("track");const t=s.apply(null,e);return p.shift(),t};const k=s=>(s??="GET",p[0]==="track"?"force":!!(!p.length&&o.ajax&&(s==="socket"&&o.ajax.trackWebSockets||Array.prototype.includes.call(o.ajax.trackMethods,s.toUpperCase())))),G=s=>{const e=t=>{const r=t.open;t.open=(...n)=>{const[a,c]=n;return k(a)&&s.trigger("request",{type:a,url:c,request:t}),r.apply(t,n)}};window.XMLHttpRequest=function(t){const r=new q(t);return e(r),r};try{R(window.XMLHttpRequest,q)}catch{}if(o.ajax.trackWebSockets){window.WebSocket=function(t,r){let n;return r!=null?n=new b(t,r):n=new b(t),k("socket")&&s.trigger("request",{type:"socket",url:t,protocols:r,request:n}),n};try{R(window.WebSocket,b)}catch{}}};class J extends D{constructor(){super(),G(this)}}let P=null;const M=()=>(P??=new J,P),v=s=>{const e=o.ajax.ignoreURLs;for(let t=0,r=e.length;t{const{type:e,request:t,url:r}=s;if(!v(r)&&!i.running&&(o.restartOnRequestAfter!==!1||k(e)==="force")){let n=o.restartOnRequestAfter||0;typeof n=="boolean"&&(n=0),setTimeout(()=>{let a;if(e==="socket"?a=t.readyState<1:a=0this.watch(e))}watch({type:e,request:t,url:r}){if(v(r))return;let n;e==="socket"?n=new Y(t,this.complete):n=new K(t,this.complete),this.elements.push(n)}complete(e){this.elements=this.elements.filter(t=>t!==e)}}class K{constructor(e,t){this.progress=0,e.addEventListener("progress",r=>{r.lengthComputable?this.progress=100*r.loaded/r.total:this.progress=this.progress+(100-this.progress)/2},!1);for(const r of["load","abort","timeout","error"])e.addEventListener(r,()=>{t(this),this.progress=100},!1)}}class Y{constructor(e,t){this.progress=0;for(const r of["error","open"])e.addEventListener(r,()=>{t(this),this.progress=100},!1)}}class z{constructor(e){e??={},this.complete=this.complete.bind(this),this.elements=[],e.selectors??=[];const t=e.selectors;for(let r=0,n=t.length;rt!==e)}}class Q{constructor(e,t){this.selector=e,this.completeCallback=t,this.progress=0,this.check()}check(){document.querySelector(this.selector)?this.done():setTimeout(()=>this.check(),o.elements.checkInterval)}done(){this.completeCallback(this),this.completeCallback=null,this.progress=100}}class O{constructor(){this.progress=this.states[document.readyState]??100;const e=document.onreadystatechange;document.onreadystatechange=(...t)=>(this.states[document.readyState]!=null&&(this.progress=this.states[document.readyState]),typeof e=="function"?e.apply(null,t):void 0)}}O.prototype.states={loading:0,interactive:50,complete:100};class V{constructor(e){this.progress=0;let t=0;const r=[];let n=0,a=l();const c=setInterval(()=>{const u=l()-a-50;a=l(),r.push(u),r.length>e.sampleCount&&r.shift(),t=X(r),++n>=e.minSamples&&t=100&&(this.done=!0),t===this.last?this.sinceLastUpdate+=e:(this.sinceLastUpdate&&(this.rate=(t-this.last)/this.sinceLastUpdate),this.catchup=(t-this.progress)/o.catchupTime,this.sinceLastUpdate=0,this.last=t),t>this.progress&&(this.progress+=this.catchup*e);const r=1-Math.pow(this.progress/100,o.easeFactor);return this.progress+=r*this.rate*e,this.progress=Math.min(this.lastProgress+o.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress}}let d=null,A=null,h=null,C=null,g=null,S=null;i.running=!1;const j=()=>{o.restartOnPushState&&i.restart()},Z=window.history.pushState;window.history.pushState=(...s)=>(j(),Z.apply(window.history,s));const ee=window.history.replaceState;window.history.replaceState=(...s)=>(j(),ee.apply(window.history,s));const te={ajax:N,elements:z,document:O,eventLag:V},_=()=>{i.sources=d=[];for(const e of["ajax","elements","document","eventLag"])o[e]!==!1&&d.push(new te[e](o[e]));const s=o.extraSources??[];for(let e=0,t=s.length;e{i.trigger("stop"),i.running=!1,h.destroy(),S=!0,g!=null&&(W(g),g=null),_()},i.restart=()=>{i.trigger("restart"),i.stop(),i.start()},i.go=()=>{i.running=!0,h.render();const s=l();S=!1,g=H((e,t)=>{let r=0,n=0,a=!0;for(let u=0,f=d.length;u{h.finish(),i.running=!1,i.trigger("hide")},Math.max(o.ghostTime,Math.max(o.minTime-(l()-s),0)))):t()})},i.start=s=>{y(o,s),i.running=!0;try{h.render()}catch(e){w=e}document.querySelector(".pace")?(i.trigger("start"),i.go()):setTimeout(i.start,50)},typeof define=="function"&&define.amd?define(()=>i):typeof exports=="object"?module.exports=i:o.startOnPageLoad&&i.start()})(); diff --git a/tests/ajax.spec.mjs b/tests/ajax.spec.mjs index 11c19f1..2bd9320 100644 --- a/tests/ajax.spec.mjs +++ b/tests/ajax.spec.mjs @@ -58,3 +58,20 @@ test.describe('with ajax options', () => { expect(await page.evaluate(() => events)).toEqual([]); }); }); + +test.describe('with ajax.trackMethods that is not an array', () => { + test('a string matches nothing, not substrings', async ({ page, open }) => { + await open({ head: "" }); + await idle(page); + await clearEvents(page); + await request(page, { ms: 900 }); + expect(await page.evaluate(() => events)).toEqual([]); + }); + + test('a number does not break the page\'s requests', async ({ page, open }) => { + await open({ head: '' }); + await idle(page); + // Resolves only if the page's xhr.open() didn't throw. + await request(page, { ms: 100 }); + }); +}); From 60615bdf2ffd73579212df34264a9266cbd87c50 Mon Sep 17 00:00:00 2001 From: Zachary Miller Date: Sat, 7 Nov 2026 17:27:10 -0500 Subject: [PATCH 2/2] Correct claims about the build check and browser support Corrections to earlier commits: - "Build for the 2.0 browser targets": the build.mjs comment said the esbuild target and .browserslistrc "can't drift apart unnoticed", but lowering the target goes unnoticed. The comment now says what the test checks. Its message gave Array.prototype.at as an example es-check catches; es-check detects it only in some code patterns (Object.hasOwn or findLast are reliable examples). - "Remove code for browsers 2.0 doesn't support" said no behavior changes. In jsdom without its pretendToBeVisual option, which has no requestAnimationFrame, pace now throws once it starts. Jest and Vitest turn the option on; the README now says to. - "Update the README's browser support and size": "browsers built on the same engines" was too loose (Samsung Internet 16 is Chromium-based but not supported). The README now names Opera 82+, Samsung Internet 17+ and Chromium 96 or later. --- README.md | 7 +++++-- scripts/build.mjs | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 640739f..bf98dc6 100644 --- a/README.md +++ b/README.md @@ -297,8 +297,11 @@ None! Support ------- -Pace supports Chrome and Edge 96+, Firefox 95+, and Safari 15.2+ on macOS and iOS, plus browsers built -on the same engines, such as Opera and Samsung Internet. For older browsers, use pace-js 1.x. +Pace supports Chrome and Edge 96+, Firefox 95+, Safari 15.2+ on macOS and iOS, Opera 82+, Samsung +Internet 17+, and other browsers built on Chromium 96 or later. For older browsers, use pace-js 1.x. + +In tests that run on jsdom, turn on its `pretendToBeVisual` option (Jest and Vitest already do): without +it, jsdom has no `requestAnimationFrame`. If you run into a compatibility issue, please create an issue. diff --git a/scripts/build.mjs b/scripts/build.mjs index c157fc1..4b159aa 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -19,8 +19,9 @@ const root = path.resolve(import.meta.dirname, '..'); // The oldest browser versions in .browserslistrc ("baseline 2021 with // downstream"). esbuild doesn't read Browserslist, so they're listed here: -// https://esbuild.github.io/api/#target. `npm test` checks the minified build -// against .browserslistrc, so the two can't drift apart unnoticed. +// https://esbuild.github.io/api/#target. `npm test` runs es-check with +// .browserslistrc on pace.js and pace.min.js, so syntax and many built-ins +// newer than these browsers fail the test. const TARGET = ['chrome96', 'edge96', 'firefox95', 'safari15.2', 'ios15.2']; // The 10 theme colors, from the original Gruntfile.coffee `themeColors`.