From 0fb8b99b87292fe2e8b67788e3ea1add5cb70355 Mon Sep 17 00:00:00 2001 From: derdeka Date: Wed, 13 May 2026 17:50:40 +0200 Subject: [PATCH 01/10] Core: convert src/utils.js to typescript --- modules/asterioBidAdapter.ts | 2 + modules/conversantBidAdapter.ts | 2 +- src/bidderSettings.ts | 2 +- src/{utils.js => utils.ts} | 224 ++++++++++++++++---------------- 4 files changed, 117 insertions(+), 113 deletions(-) rename src/{utils.js => utils.ts} (81%) diff --git a/modules/asterioBidAdapter.ts b/modules/asterioBidAdapter.ts index 313eb4420b5..196ba14cceb 100644 --- a/modules/asterioBidAdapter.ts +++ b/modules/asterioBidAdapter.ts @@ -39,6 +39,8 @@ type AsterioServerBid = { mediaType?: string; format?: string; adomain?: string[]; + vastXml?: string; + meta?: Record; }; export const spec: BidderSpec = { diff --git a/modules/conversantBidAdapter.ts b/modules/conversantBidAdapter.ts index 7d6d56788af..e66cda5cbca 100644 --- a/modules/conversantBidAdapter.ts +++ b/modules/conversantBidAdapter.ts @@ -138,7 +138,7 @@ const converter = ortbConverter({ copyOptProperty(bidRequest.params?.maxduration, videoData, 'maxduration'); copyOptProperty(bidRequest.params?.protocols, videoData, 'protocols'); copyOptProperty(bidRequest.params?.api, videoData, 'api'); - imp.video = mergeDeep(videoData, imp.video); + imp.video = mergeDeep(videoData, imp.video) as any; fillVideoImp(imp, bidRequest, context); } }, diff --git a/src/bidderSettings.ts b/src/bidderSettings.ts index c9d68cdddf7..18954853877 100644 --- a/src/bidderSettings.ts +++ b/src/bidderSettings.ts @@ -98,7 +98,7 @@ export class ScopedSettings, SCOPED extends * @returns all settings in the given scope, merged with the settings for the default scope. */ settingsFor(scope): SETTINGS { - return mergeDeep({}, this.ownSettingsFor(null), this.ownSettingsFor(scope)); + return mergeDeep({}, this.ownSettingsFor(null), this.ownSettingsFor(scope)) as SETTINGS; } /** diff --git a/src/utils.js b/src/utils.ts similarity index 81% rename from src/utils.js rename to src/utils.ts index 1369f46a6e1..a4eea4709b5 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -15,14 +15,14 @@ const consoleInfoExists = Boolean(consoleExists && window.console.info); const consoleWarnExists = Boolean(consoleExists && window.console.warn); const consoleErrorExists = Boolean(consoleExists && window.console.error); -let eventEmitter; +let eventEmitter: ((...args: any[]) => void) | undefined; -export function _setEventEmitter(emitFn) { +export function _setEventEmitter(emitFn: (...args: any[]) => void) { // called from events.js - this hoop is to avoid circular imports eventEmitter = emitFn; } -function emitEvent(...args) { +function emitEvent(...args: any[]) { if (eventEmitter != null) { eventEmitter(...args); } @@ -77,10 +77,10 @@ export function getUniqueIdentifierStr() { * and y is replaced with a random hexadecimal digit from 8 to b. * https://gist.github.com/jed/982883 via node-uuid */ -export function generateUUID(placeholder) { +export function generateUUID(placeholder?: any): string { return placeholder ? (placeholder ^ _getRandomData() >> placeholder / 4).toString(16) - : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID); + : (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID); } /** @@ -95,14 +95,14 @@ function _getRandomData() { } } -export function getBidIdParameter(key, paramsObj) { +export function getBidIdParameter(key: string, paramsObj: Record) { return paramsObj?.[key] || ''; } // parse a query string object passed in bid params // bid params should be an object such as {key: "value", key1 : "value1"} // aliases to formatQS -export function parseQueryStringParameters(queryObj) { +export function parseQueryStringParameters(queryObj: Record) { let result = ''; for (var k in queryObj) { if (queryObj.hasOwnProperty(k)) { result += k + '=' + encodeURIComponent(queryObj[k]) + '&'; } @@ -112,7 +112,7 @@ export function parseQueryStringParameters(queryObj) { } // transform an AdServer targeting bids into a query string to send to the adserver -export function transformAdServerTargetingObj(targeting) { +export function transformAdServerTargetingObj(targeting: Record) { // we expect to receive targeting for a single slot at a time if (targeting && Object.getOwnPropertyNames(targeting).length > 0) { return Object.keys(targeting) @@ -125,7 +125,7 @@ export function transformAdServerTargetingObj(targeting) { /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of width, height tuples `[[300, 250]]` or '[[300,250], [970,90]]' */ -export function sizesToSizeTuples(sizes) { +export function sizesToSizeTuples(sizes: string | number[] | number[][]) { if (typeof sizes === 'string') { // multiple sizes will be comma-separated return sizes @@ -147,35 +147,35 @@ export function sizesToSizeTuples(sizes) { * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` */ -export function parseSizesInput(sizeObj) { +export function parseSizesInput(sizeObj: string | number[] | number[][]) { return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString); } -export function sizeTupleToSizeString(size) { +export function sizeTupleToSizeString(size: [number, number]) { return size[0] + 'x' + size[1] } // Parse a GPT style single size array, (i.e [300, 250]) // into an AppNexus style string, (i.e. 300x250) -export function parseGPTSingleSizeArray(singleSize) { +export function parseGPTSingleSizeArray(singleSize: number[]) { if (isValidGPTSingleSize(singleSize)) { return sizeTupleToSizeString(singleSize); } } -export function sizeTupleToRtbSize(size) { +export function sizeTupleToRtbSize(size: [number, number]) { return { w: size[0], h: size[1] }; } // Parse a GPT style single size array, (i.e [300, 250]) // into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250}) -export function parseGPTSingleSizeArrayToRtbSize(singleSize) { +export function parseGPTSingleSizeArrayToRtbSize(singleSize: number[]) { if (isValidGPTSingleSize(singleSize)) { return sizeTupleToRtbSize(singleSize) } } -function isValidGPTSingleSize(singleSize) { +function isValidGPTSingleSize(singleSize: any): singleSize is [number, number] { // if we aren't exactly 2 items in this array, it is invalid return isArray(singleSize) && singleSize.length === 2 && (!isNaN(singleSize[0]) && !isNaN(singleSize[1])); } @@ -211,7 +211,7 @@ export function canAccessWindowTop() { * @param {Window} [win] * @returns {Window} */ -export function getFallbackWindow(win) { +export function getFallbackWindow(win?: Window) { if (win) { return win; } @@ -222,42 +222,42 @@ export function getFallbackWindow(win) { * Wrappers to console.(log | info | warn | error). Takes N arguments, the same as the native methods */ // eslint-disable-next-line no-restricted-syntax -export function logMessage() { +export function logMessage(...args: any[]) { if (debugTurnedOn() && consoleLogExists) { // eslint-disable-next-line no-console - console.log.apply(console, decorateLog(arguments, 'MESSAGE:')); + console.log(...decorateLog(args, 'MESSAGE:')); } } // eslint-disable-next-line no-restricted-syntax -export function logInfo() { +export function logInfo(...args: any[]) { if (debugTurnedOn() && consoleInfoExists) { // eslint-disable-next-line no-console - console.info.apply(console, decorateLog(arguments, 'INFO:')); + console.info(...decorateLog(args, 'INFO:')); } } // eslint-disable-next-line no-restricted-syntax -export function logWarn() { +export function logWarn(...args: any[]) { if (debugTurnedOn() && consoleWarnExists) { // eslint-disable-next-line no-console - console.warn.apply(console, decorateLog(arguments, 'WARNING:')); + console.warn(...decorateLog(args, 'WARNING:')); } - emitEvent(EVENTS.AUCTION_DEBUG, { type: 'WARNING', arguments: arguments }); + emitEvent(EVENTS.AUCTION_DEBUG, { type: 'WARNING', arguments: args }); } // eslint-disable-next-line no-restricted-syntax -export function logError() { +export function logError(...args: any[]) { if (debugTurnedOn() && consoleErrorExists) { // eslint-disable-next-line no-console - console.error.apply(console, decorateLog(arguments, 'ERROR:')); + console.error(...decorateLog(args, 'ERROR:')); } - emitEvent(EVENTS.AUCTION_DEBUG, { type: 'ERROR', arguments: arguments }); + emitEvent(EVENTS.AUCTION_DEBUG, { type: 'ERROR', arguments: args }); } -export function prefixLog(prefix) { - function decorate(fn) { - return function (...args) { +export function prefixLog(prefix: string) { + function decorate(fn: (...args: any[]) => void) { + return function (...args: any[]) { fn(prefix, ...args); } } @@ -269,7 +269,7 @@ export function prefixLog(prefix) { } } -function decorateLog(args, prefix) { +function decorateLog(args: any[], prefix?: string): any[] { args = [].slice.call(args); const bidder = config.getCurrentBidder(); @@ -281,7 +281,7 @@ function decorateLog(args, prefix) { args.unshift('%cPrebid' + (bidder ? `%c${bidder}` : '')); return args; - function label(color) { + function label(color: string) { return `display: inline-block; color: #fff; background: ${color}; padding: 1px 4px; border-radius: 3px;` } } @@ -305,7 +305,7 @@ export const createIframe = (() => { frameBorder: '0', allowtransparency: 'true' } - return (doc, attrs, style = {}) => { + return (doc: Document, attrs: Record, style: Record = {}) => { const f = doc.createElement('iframe'); Object.assign(f, Object.assign({}, DEFAULTS, attrs)); Object.assign(f.style, style); @@ -331,7 +331,7 @@ export function createInvisibleIframe() { * Check if a given parameter name exists in query string * and if it does return the value */ -export function getParameterByName(name) { +export function getParameterByName(name: string) { return parseQS(getWindowLocation().search)[name] || ''; } @@ -341,7 +341,7 @@ export function getParameterByName(name) { * @param {*} object object to test * @return {Boolean} if object is empty */ -export function isEmpty(object) { +export function isEmpty(object: any) { if (!object) return true; if (isArray(object) || isStr(object)) { return !(object.length > 0); @@ -354,7 +354,7 @@ export function isEmpty(object) { * @param str string to test * @returns {boolean} if string is empty */ -export function isEmptyStr(str) { +export function isEmptyStr(str: any) { return isStr(str) && (!str || str.length === 0); } @@ -365,12 +365,12 @@ export function isEmptyStr(str) { * @param {Function} fn - The function to execute for each element. It receives three arguments: value, key, and the original object. * @returns {void} */ -export function _each(object, fn) { +export function _each(object: any[] | Record, fn: (value: any, key: any) => void) { if (isFn(object?.forEach)) return object.forEach(fn, this); Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k)); } -export function contains(a, obj) { +export function contains(a: any, obj: any) { return isFn(a?.includes) && a.includes(obj); } @@ -381,7 +381,7 @@ export function contains(a, obj) { * @param {Function} callback - The function to execute for each element. It receives three arguments: value, key, and the original object. * @return {Array} */ -export function _map(object, callback) { +export function _map(object: any[] | Record, callback: (value: any, key: any, obj?: any) => any) { if (isFn(object?.map)) return object.map(callback); return Object.entries(object || {}).map(([k, v]) => callback(v, k, object)) } @@ -394,7 +394,7 @@ export function _map(object, callback) { * @param {Boolean} [asLastChildChild] * @return {HTML Element} */ -export function insertElement(elm, doc, target, asLastChildChild) { +export function insertElement(elm: Node, doc?: Document, target?: string, asLastChildChild?: boolean) { doc = doc || document; let parentEl; if (target) { @@ -420,9 +420,9 @@ export function insertElement(elm, doc, target, asLastChildChild) { * @param {Number} [timeout] * @returns {Promise} */ -export function waitForElementToLoad(element, timeout) { - let timer = null; - return new PbPromise((resolve) => { +export function waitForElementToLoad(element: HTMLElement | HTMLImageElement, timeout?: number) { + let timer: number | null = null; + return new PbPromise((resolve) => { const onLoad = function() { element.removeEventListener('load', onLoad); element.removeEventListener('error', onLoad); @@ -445,7 +445,7 @@ export function waitForElementToLoad(element, timeout) { * @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process * @param {Number} [timeout] an optional timeout in milliseconds for the image to load before calling `done` */ -export function triggerPixel(url, done, timeout) { +export function triggerPixel(url: string, done?: () => void, timeout?: number) { const img = new Image(); if (done && internal.isFn(done)) { waitForElementToLoad(img, timeout).then(done); @@ -458,7 +458,7 @@ export function triggerPixel(url, done, timeout) { * (though could be for other purposes) * @param {string} htmlCode snippet of HTML code used for tracking purposes */ -export function insertHtmlIntoIframe(htmlCode) { +export function insertHtmlIntoIframe(htmlCode: string) { if (!htmlCode) { return; } @@ -478,11 +478,11 @@ export function insertHtmlIntoIframe(htmlCode) { * @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process * @param {Number} [timeout] an optional timeout in milliseconds for the iframe to load before calling `done` */ -export function insertUserSyncIframe(url, done, timeout) { +export function insertUserSyncIframe(url: string, done?: () => void, timeout?: number) { const iframeHtml = internal.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin'); const div = document.createElement('div'); div.innerHTML = iframeHtml; - const iframe = div.firstChild; + const iframe = div.firstChild as HTMLElement; if (done && internal.isFn(done)) { waitForElementToLoad(iframe, timeout).then(done); } @@ -495,7 +495,7 @@ export function insertUserSyncIframe(url, done, timeout) { * @param encode * @return {string} HTML snippet that contains the img src = set to `url` */ -export function createTrackPixelHtml(url, encode = encodeURI) { +export function createTrackPixelHtml(url: string, encode: (url: string) => string = encodeURI) { if (!url) { return ''; } @@ -511,8 +511,8 @@ export function createTrackPixelHtml(url, encode = encodeURI) { * @param url * @return {string} */ -export function encodeMacroURI(url) { - const macros = Array.from(url.matchAll(/\$({[^}]+})/g)).map(match => match[1]); +export function encodeMacroURI(url: string) { + const macros = Array.from((url as any).matchAll(/\$({[^}]+})/g) as Iterable).map(match => match[1]); return macros.reduce((str, macro) => { return str.replace('$' + encodeURIComponent(macro), '$' + macro) }, encodeURI(url)) @@ -525,7 +525,7 @@ export function encodeMacroURI(url) { * @param {string} sandbox string if provided the sandbox attribute will be included with the given value * @return {string} HTML snippet that contains the iframe src = set to `url` */ -export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') { +export function createTrackPixelIframeHtml(url: string, encodeUri = true, sandbox = '') { if (!url) { return ''; } @@ -547,15 +547,15 @@ export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') `; } -export function uniques(value, index, arry) { +export function uniques(value: T, index: number, arry: T[]) { return arry.indexOf(value) === index; } -export function flatten(a, b) { +export function flatten(a: T[], b: T | T[]) { return a.concat(b); } -export function getBidRequest(id, bidderRequests) { +export function getBidRequest(id: string, bidderRequests: Array<{ bids: any[] }>) { if (!id) { return; } @@ -563,11 +563,11 @@ export function getBidRequest(id, bidderRequests) { .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id)) } -export function getValue(obj, key) { +export function getValue(obj: Record, key: string) { return obj[key]; } -export function getBidderCodes(adUnits) { +export function getBidderCodes(adUnits: Array<{ bids?: Array<{ bidder?: string }> }>) { // this could memoize adUnits return adUnits.map(unit => unit.bids.map(bid => bid.bidder) .reduce(flatten, [])).reduce(flatten, []).filter((bidder) => typeof bidder !== 'undefined').filter(uniques); @@ -585,7 +585,7 @@ export function isApnGetTagDefined() { } } -export const sortByHighestCpm = (a, b) => { +export const sortByHighestCpm = (a: { cpm: number }, b: { cpm: number }) => { return b.cpm - a.cpm; } @@ -595,7 +595,7 @@ export const sortByHighestCpm = (a, b) => { * https://bost.ocks.org/mike/shuffle/ * istanbul ignore next */ -export function shuffle(array) { +export function shuffle(array: T[]) { let counter = array.length; // while there are elements in the array @@ -631,7 +631,7 @@ export function isSafeFrameWindow() { return false; } - const ws = internal.getWindowSelf(); + const ws = internal.getWindowSelf() as any; return !!(ws.$sf && ws.$sf.ext); } @@ -642,7 +642,7 @@ export function isSafeFrameWindow() { */ export function getSafeframeGeometry() { try { - const ws = getWindowSelf(); + const ws = getWindowSelf() as any; return (typeof ws.$sf.ext.geom === 'function') ? ws.$sf.ext.geom() : undefined; } catch (e) { logError('Error getting SafeFrame geometry', e); @@ -662,18 +662,18 @@ export function isChromeIOSBrowser() { return /crios|crmo/i.test(navigator.userAgent); } -export function replaceMacros(str, subs) { +export function replaceMacros(str: string | undefined, subs: Record) { if (!str) return; return Object.entries(subs).reduce((str, [key, val]) => { return str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), val || ''); }, str); } -export function replaceAuctionPrice(str, cpm) { +export function replaceAuctionPrice(str: string | undefined, cpm: number | string) { return replaceMacros(str, { AUCTION_PRICE: cpm }) } -export function replaceClickThrough(str, clicktag) { +export function replaceClickThrough(str: string | undefined, clicktag: string) { if (!str || !clicktag || typeof clicktag !== 'string') return; return str.replace(/\${CLICKTHROUGH}/g, clicktag); } @@ -698,7 +698,7 @@ export function getPerformanceNow() { * @param {Window} w The window object used to perform the api call. default to window.self * @returns {number} */ -export function getDomLoadingDuration(w) { +export function getDomLoadingDuration(w?: Window) { let domLoadingDuration = -1; w = w || getWindowSelf(); @@ -747,15 +747,15 @@ export function checkCookieSupport() { * @param {number} numRequiredCalls The number of times which the returned function needs to be called before * func is. */ -export function delayExecution(func, numRequiredCalls) { +export function delayExecution(func: (...args: any[]) => void, numRequiredCalls: number) { if (numRequiredCalls < 1) { throw new Error(`numRequiredCalls must be a positive number. Got ${numRequiredCalls}`); } let numCalls = 0; - return function () { + return function (...args: any[]) { numCalls++; if (numCalls === numRequiredCalls) { - func.apply(this, arguments); + func.apply(this, args); } } } @@ -766,9 +766,10 @@ export function delayExecution(func, numRequiredCalls) { * @param {string} key * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}} */ -export function groupBy(xs, key) { - return xs.reduce(function(rv, x) { - (rv[x[key]] = rv[x[key]] || []).push(x); +export function groupBy(xs: any[], key: string): Record { + return xs.reduce(function(rv: Record, x) { + const k = String(x[key]); + (rv[k] = rv[k] || []).push(x); return rv; }, {}); } @@ -778,7 +779,7 @@ export function groupBy(xs, key) { * @param mediaTypes mediaTypes parameter to validate * @return If object is valid */ -export function isValidMediaTypes(mediaTypes) { +export function isValidMediaTypes(mediaTypes: Record) { const SUPPORTED_MEDIA_TYPES = ['banner', 'native', 'video', 'audio']; const SUPPORTED_STREAM_TYPES = ['instream', 'outstream']; @@ -802,7 +803,7 @@ export function isValidMediaTypes(mediaTypes) { * @param {string} bidder code * @return {Array} user configured param for the given bidder adunit configuration */ -export function getUserConfiguredParams(adUnits, adUnitCode, bidder) { +export function getUserConfiguredParams(adUnits: Array<{ code: string; bids: Array<{ bidder: string; params?: any }> }>, adUnitCode: string, bidder: string) { return adUnits .filter(adUnit => adUnit.code === adUnitCode) .flatMap((adUnit) => adUnit.bids) @@ -810,26 +811,28 @@ export function getUserConfiguredParams(adUnits, adUnitCode, bidder) { .map((bidderData) => bidderData.params || {}); } -export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; +export const compareCodeAndSlot = (slot: googletag.Slot, adUnitCode: string) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; /** * Returns filter function to match adUnitCode in slot * @param slot GoogleTag slot * @return filter function */ -export function isAdUnitCodeMatchingSlot(slot) { +export function isAdUnitCodeMatchingSlot(slot: googletag.Slot) { const customGptSlotMatching = config.getConfig('customGptSlotMatching'); const match = isFn(customGptSlotMatching) && customGptSlotMatching(slot); - return isFn(match) ? match : (adUnitCode) => compareCodeAndSlot(slot, adUnitCode); + return isFn(match) ? match : (adUnitCode: string) => compareCodeAndSlot(slot, adUnitCode); } /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped + * @param {string} adUnit.code the ad unit code + * @param {Object} [adUnit.mediaTypes] the ad unit media types * @param {string} bidder bidder code that is not compatible with the adUnit * @return {string} warning message to display when condition is met */ -export function unsupportedBidderMessage(adUnit, bidder) { +export function unsupportedBidderMessage(adUnit: { code: string; mediaTypes?: Record }, bidder: string) { const mediaType = Object.keys(adUnit.mediaTypes || { 'banner': 'banner' }).join(', '); return ` @@ -843,7 +846,7 @@ export function unsupportedBidderMessage(adUnit, bidder) { * Returns a new object with undefined properties removed from given object * @param obj the object to clean */ -export function cleanObj(obj) { +export function cleanObj(obj: Record) { return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined')) } @@ -852,7 +855,7 @@ export function cleanObj(obj) { * @param obj the original object * @param properties An array of desired properties */ -export function pick(obj, properties) { +export function pick(obj: Record, properties: ReadonlyArray any)>) { if (typeof obj !== 'object') { return {}; } @@ -871,7 +874,7 @@ export function pick(obj, properties) { let value = obj[prop]; if (typeof properties[i + 1] === 'function') { - value = properties[i + 1](value, newObj); + value = (properties[i + 1] as (...args: any[]) => any)(value, newObj); } if (typeof value !== 'undefined') { newObj[newProp] = value; @@ -881,7 +884,7 @@ export function pick(obj, properties) { }, {}); } -export function parseQS(query) { +export function parseQS(query: string): Record { return !query ? {} : query .replace(/^\?/, '') .split('&') @@ -898,7 +901,7 @@ export function parseQS(query) { }, {}); } -export function formatQS(query) { +export function formatQS(query: Record) { return Object .keys(query) .map(k => Array.isArray(query[k]) @@ -907,7 +910,7 @@ export function formatQS(query) { .join('&'); } -export function parseUrl(url, options) { +export function parseUrl(url: string, options?: Record) { const parsed = document.createElement('a'); if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) { parsed.href = url; @@ -922,18 +925,18 @@ export function parseUrl(url, options) { hostname: parsed.hostname, port: +parsed.port, pathname: parsed.pathname.replace(/^(?!\/)/, '/'), - search: (qsAsString) ? parsed.search : internal.parseQS(parsed.search || ''), + search: (qsAsString ? parsed.search : internal.parseQS(parsed.search || '')) as any, hash: (parsed.hash || '').replace(/^#/, ''), host: parsed.host || window.location.host }; } -export function buildUrl(obj) { +export function buildUrl(obj: { protocol?: string; host?: string; hostname?: string; port?: number; pathname?: string; search?: string | Record; hash?: string }) { return (obj.protocol || 'http') + '://' + (obj.host || obj.hostname + (obj.port ? `:${obj.port}` : '')) + (obj.pathname || '') + - (obj.search ? `?${internal.formatQS(obj.search || '')}` : '') + + (obj.search ? `?${internal.formatQS(obj.search as Record)}` : '') + (obj.hash ? `#${obj.hash}` : ''); } @@ -945,7 +948,7 @@ export function buildUrl(obj) { * @param {boolean} [options.checkTypes=false] - If set, two objects with identical properties but different constructors will *not* be considered equivalent. * @returns {boolean} - Returns `true` if the objects are equivalent, `false` otherwise. */ -export function deepEqual(obj1, obj2, { checkTypes = false } = {}) { +export function deepEqual(obj1: any, obj2: any, { checkTypes = false } = {}) { // Quick reference check if (obj1 === obj2) return true; @@ -996,7 +999,7 @@ export function deepEqual(obj1, obj2, { checkTypes = false } = {}) { return true; } -export function mergeDeep(target, ...sources) { +export function mergeDeep(target: Record, ...sources: any[]): Record { for (let i = 0; i < sources.length; i++) { const source = sources[i]; if (!isPlainObject(source)) { @@ -1007,7 +1010,7 @@ export function mergeDeep(target, ...sources) { return target; } -function mergeDeepHelper(target, source) { +function mergeDeepHelper(target: any, source: any) { // quick check if (!isPlainObject(target) || !isPlainObject(source)) { return; @@ -1031,9 +1034,10 @@ function mergeDeepHelper(target, source) { target[key] = [...val]; } else { // deduplicate + const targetArr = target[key] as any[]; val.forEach(obj => { - if (!target[key].some(item => deepEqual(item, obj))) { - target[key].push(obj); + if (!targetArr.some(item => deepEqual(item, obj))) { + targetArr.push(obj); } }); } @@ -1051,10 +1055,8 @@ function mergeDeepHelper(target, source) { * @param seed (optional) * @returns {string} */ -export function cyrb53Hash(str, seed = 0) { - // IE doesn't support imul - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill - const imul = function(opA, opB) { +export function cyrb53Hash(str: string, seed = 0) { + const imul = function(opA: number, opB: number) { if (isFn(Math.imul)) { return Math.imul(opA, opB); } else { @@ -1074,7 +1076,7 @@ export function cyrb53Hash(str, seed = 0) { let h1 = 0xdeadbeef ^ seed; let h2 = 0x41c6ce57 ^ seed; - for (let i = 0, ch; i < str.length; i++) { + for (let i = 0, ch: number; i < str.length; i++) { ch = str.charCodeAt(i); h1 = imul(h1 ^ ch, 2654435761); h2 = imul(h2 ^ ch, 1597334677); @@ -1089,13 +1091,13 @@ export function cyrb53Hash(str, seed = 0) { * @param data * @returns {any} */ -export function safeJSONParse(data) { +export function safeJSONParse(data: string) { try { return JSON.parse(data); } catch (e) {} } -export function safeJSONEncode(data) { +export function safeJSONEncode(data: any) { try { return JSON.stringify(data); } catch (e) { @@ -1111,12 +1113,12 @@ export function safeJSONEncode(data) { * By default, the first argument is used as key. * @return {*} */ -export function memoize(fn, key = function (arg) { return arg; }) { +export function memoize any>(fn: T, key: (...args: any[]) => any = (arg) => arg) { const cache = new Map(); - const memoized = function () { - const cacheKey = key.apply(this, arguments); + const memoized = function (...args: any[]) { + const cacheKey = key.apply(this, args); if (!cache.has(cacheKey)) { - cache.set(cacheKey, fn.apply(this, arguments)); + cache.set(cacheKey, fn.apply(this, args)); } return cache.get(cacheKey); } @@ -1145,7 +1147,7 @@ export function getUnixTimestampFromNow(timeValue = 0, timeUnit = 'd') { * @param {Object} obj the object * @returns {Array} */ -export function convertObjectToArray(obj) { +export function convertObjectToArray(obj: Record) { return Object.keys(obj).map(key => { return { [key]: obj[key] }; }); @@ -1156,7 +1158,7 @@ export function convertObjectToArray(obj) { * @param {HTMLScriptElement} script * @param {object} attributes */ -export function setScriptAttributes(script, attributes) { +export function setScriptAttributes(script: HTMLScriptElement, attributes: Record) { Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v)) } @@ -1170,7 +1172,7 @@ export function setScriptAttributes(script, attributes) { * if the element is not found, return the index of the first element that's greater; * if no greater element exists, return `arr.length`) */ -export function binarySearch(arr, el, key = (el) => el) { +export function binarySearch(arr: T[], el: T, key: (el: T) => any = (el) => el) { let left = 0; let right = arr.length && arr.length - 1; const target = key(el); @@ -1196,7 +1198,7 @@ export function binarySearch(arr, el, key = (el) => el) { * @param {Set} checkedObjects - A set of properties that have already been checked. * @returns {boolean} - Returns true if the object has non-serializable properties, false otherwise. */ -export function hasNonSerializableProperty(obj, checkedObjects = new Set()) { +export function hasNonSerializableProperty(obj: Record, checkedObjects: Set = new Set()) { for (const key in obj) { const value = obj[key]; const type = typeof value; @@ -1234,8 +1236,8 @@ export function hasNonSerializableProperty(obj, checkedObjects = new Set()) { * @param {String} key - Key of nested property. * @returns {any|undefined} - Value of nested property. */ -export function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { +export function setOnAny(collection: any[], key: string) { + for (let i = 0, result: any; i < collection.length; i++) { result = deepAccess(collection[i], key); if (result) { return result; @@ -1244,7 +1246,7 @@ export function setOnAny(collection, key) { return undefined; } -export function extractDomainFromHost(pageHost) { +export function extractDomainFromHost(pageHost: string) { let domain = null; try { const domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost); @@ -1262,11 +1264,11 @@ export function extractDomainFromHost(pageHost) { return domain; } -export function triggerNurlWithCpm(bid, cpm) { +export function triggerNurlWithCpm(bid: { nurl?: string }, cpm: number | string) { if (isStr(bid.nurl) && bid.nurl !== '') { bid.nurl = bid.nurl.replace( /\${AUCTION_PRICE}/, - cpm + String(cpm) ); triggerPixel(bid.nurl); } @@ -1275,7 +1277,7 @@ export function triggerNurlWithCpm(bid, cpm) { // To ensure that isGzipCompressionSupported() doesn’t become an overhead, we have used memoization to cache the result after the first execution. // This way, even if the function is called multiple times, it will only perform the actual check once and return the cached result in subsequent calls. export const isGzipCompressionSupported = (function () { - let cachedResult; // Store the result + let cachedResult: boolean | undefined; return function () { if (cachedResult !== undefined) { @@ -1298,7 +1300,7 @@ export const isGzipCompressionSupported = (function () { })(); // Make sure to use isGzipCompressionSupported before calling this function -export async function compressDataWithGZip(data) { +export async function compressDataWithGZip(data: any) { if (typeof data !== 'string') { // TextEncoder (below) expects a string data = JSON.stringify(data); } From 7e272c88061556bde949e556a893cae90c5225a6 Mon Sep 17 00:00:00 2001 From: derdeka Date: Fri, 15 May 2026 10:10:13 +0200 Subject: [PATCH 02/10] Core: convert src/utils.js to typescript --- src/types/local/safeframe.d.ts | 64 ++++++++++++++++++++++ src/utils.ts | 98 ++++++++++++++++------------------ 2 files changed, 111 insertions(+), 51 deletions(-) create mode 100644 src/types/local/safeframe.d.ts diff --git a/src/types/local/safeframe.d.ts b/src/types/local/safeframe.d.ts new file mode 100644 index 00000000000..2eee353b46f --- /dev/null +++ b/src/types/local/safeframe.d.ts @@ -0,0 +1,64 @@ +export {}; +declare global { + interface Window { + $sf?: SafeFrameAPI; + } + + interface SafeFrameAPI { + ext: SafeFrameExternalAPI; + } + + interface SafeFrameExternalAPI { + register( + width: number, + height: number, + notify?: (event: string, data?: unknown) => void + ): void; + + expand(dimensions?: { + t?: number; + b?: number; + l?: number; + r?: number; + push?: boolean; + }): void; + + collapse(): void; + + geom(): SafeFrameGeometry; + + inViewPercentage(): number; + + status(): string; + + supports(feature: string): boolean; + } + + interface SafeFrameGeometry { + self?: { + x: number; + y: number; + w: number; + h: number; + }; + + exp?: { + t: number; + b: number; + l: number; + r: number; + }; + + win?: { + w: number; + h: number; + }; + + view?: { + x: number; + y: number; + w: number; + h: number; + }; + } +} diff --git a/src/utils.ts b/src/utils.ts index a4eea4709b5..d3cb89435f1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,14 +15,14 @@ const consoleInfoExists = Boolean(consoleExists && window.console.info); const consoleWarnExists = Boolean(consoleExists && window.console.warn); const consoleErrorExists = Boolean(consoleExists && window.console.error); -let eventEmitter: ((...args: any[]) => void) | undefined; +let eventEmitter: ((...args: unknown[]) => void) | undefined; -export function _setEventEmitter(emitFn: (...args: any[]) => void) { +export function _setEventEmitter(emitFn: (...args: unknown[]) => void): void { // called from events.js - this hoop is to avoid circular imports eventEmitter = emitFn; } -function emitEvent(...args: any[]) { +function emitEvent(...args: unknown[]): void { if (eventEmitter != null) { eventEmitter(...args); } @@ -80,7 +80,7 @@ export function getUniqueIdentifierStr() { export function generateUUID(placeholder?: any): string { return placeholder ? (placeholder ^ _getRandomData() >> placeholder / 4).toString(16) - : (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID); + : ('' + [1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID); } /** @@ -151,7 +151,7 @@ export function parseSizesInput(sizeObj: string | number[] | number[][]) { return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString); } -export function sizeTupleToSizeString(size: [number, number]) { +export function sizeTupleToSizeString(size: [number, number]): string { return size[0] + 'x' + size[1] } @@ -180,23 +180,23 @@ function isValidGPTSingleSize(singleSize: any): singleSize is [number, number] { return isArray(singleSize) && singleSize.length === 2 && (!isNaN(singleSize[0]) && !isNaN(singleSize[1])); } -export function getWindowTop() { +export function getWindowTop(): Window { return window.top; } -export function getWindowSelf() { +export function getWindowSelf(): Window { return window.self; } -export function getWindowLocation() { +export function getWindowLocation(): Location { return window.location; } -export function getDocument() { +export function getDocument(): Document { return document; } -export function canAccessWindowTop() { +export function canAccessWindowTop(): boolean { try { if (internal.getWindowTop().location.href) { return true; @@ -211,7 +211,7 @@ export function canAccessWindowTop() { * @param {Window} [win] * @returns {Window} */ -export function getFallbackWindow(win?: Window) { +export function getFallbackWindow(win?: Window): Window { if (win) { return win; } @@ -420,7 +420,7 @@ export function insertElement(elm: Node, doc?: Document, target?: string, asLast * @param {Number} [timeout] * @returns {Promise} */ -export function waitForElementToLoad(element: HTMLElement | HTMLImageElement, timeout?: number) { +export function waitForElementToLoad(element: HTMLElement, timeout?: number): Promise { let timer: number | null = null; return new PbPromise((resolve) => { const onLoad = function() { @@ -445,7 +445,7 @@ export function waitForElementToLoad(element: HTMLElement | HTMLImageElement, ti * @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process * @param {Number} [timeout] an optional timeout in milliseconds for the image to load before calling `done` */ -export function triggerPixel(url: string, done?: () => void, timeout?: number) { +export function triggerPixel(url: string, done?: () => void, timeout?: number): void { const img = new Image(); if (done && internal.isFn(done)) { waitForElementToLoad(img, timeout).then(done); @@ -458,7 +458,7 @@ export function triggerPixel(url: string, done?: () => void, timeout?: number) { * (though could be for other purposes) * @param {string} htmlCode snippet of HTML code used for tracking purposes */ -export function insertHtmlIntoIframe(htmlCode: string) { +export function insertHtmlIntoIframe(htmlCode: string): void { if (!htmlCode) { return; } @@ -478,7 +478,7 @@ export function insertHtmlIntoIframe(htmlCode: string) { * @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process * @param {Number} [timeout] an optional timeout in milliseconds for the iframe to load before calling `done` */ -export function insertUserSyncIframe(url: string, done?: () => void, timeout?: number) { +export function insertUserSyncIframe(url: string, done?: () => void, timeout?: number): void { const iframeHtml = internal.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin'); const div = document.createElement('div'); div.innerHTML = iframeHtml; @@ -495,7 +495,7 @@ export function insertUserSyncIframe(url: string, done?: () => void, timeout?: n * @param encode * @return {string} HTML snippet that contains the img src = set to `url` */ -export function createTrackPixelHtml(url: string, encode: (url: string) => string = encodeURI) { +export function createTrackPixelHtml(url: string, encode: (url: string) => string = encodeURI): string { if (!url) { return ''; } @@ -511,7 +511,7 @@ export function createTrackPixelHtml(url: string, encode: (url: string) => strin * @param url * @return {string} */ -export function encodeMacroURI(url: string) { +export function encodeMacroURI(url: string): string { const macros = Array.from((url as any).matchAll(/\$({[^}]+})/g) as Iterable).map(match => match[1]); return macros.reduce((str, macro) => { return str.replace('$' + encodeURIComponent(macro), '$' + macro) @@ -525,7 +525,7 @@ export function encodeMacroURI(url: string) { * @param {string} sandbox string if provided the sandbox attribute will be included with the given value * @return {string} HTML snippet that contains the iframe src = set to `url` */ -export function createTrackPixelIframeHtml(url: string, encodeUri = true, sandbox = '') { +export function createTrackPixelIframeHtml(url: string, encodeUri = true, sandbox = ''): string { if (!url) { return ''; } @@ -573,19 +573,15 @@ export function getBidderCodes(adUnits: Array<{ bids?: Array<{ bidder?: string } .reduce(flatten, [])).reduce(flatten, []).filter((bidder) => typeof bidder !== 'undefined').filter(uniques); } -export function isGptPubadsDefined() { - if (window.googletag && isFn(window.googletag.pubads) && isFn(window.googletag.pubads().getSlots)) { - return true; - } +export function isGptPubadsDefined(): boolean { + return !!(window.googletag && isFn(window.googletag.pubads) && isFn(window.googletag.pubads().getSlots)); } -export function isApnGetTagDefined() { - if (window.apntag && isFn(window.apntag.getTag)) { - return true; - } +export function isApnGetTagDefined(): boolean { + return !!(window.apntag && isFn(window.apntag.getTag)); } -export const sortByHighestCpm = (a: { cpm: number }, b: { cpm: number }) => { +export const sortByHighestCpm = (a: { cpm: number }, b: { cpm: number }): number => { return b.cpm - a.cpm; } @@ -595,7 +591,7 @@ export const sortByHighestCpm = (a: { cpm: number }, b: { cpm: number }) => { * https://bost.ocks.org/mike/shuffle/ * istanbul ignore next */ -export function shuffle(array: T[]) { +export function shuffle(array: T[]): T[] { let counter = array.length; // while there are elements in the array @@ -615,7 +611,7 @@ export function shuffle(array: T[]) { return array; } -export function inIframe() { +export function inIframe(): boolean { try { return internal.getWindowSelf() !== internal.getWindowTop(); } catch (e) { @@ -626,12 +622,12 @@ export function inIframe() { /** * https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf */ -export function isSafeFrameWindow() { +export function isSafeFrameWindow(): boolean { if (!inIframe()) { return false; } - const ws = internal.getWindowSelf() as any; + const ws = internal.getWindowSelf(); return !!(ws.$sf && ws.$sf.ext); } @@ -640,45 +636,45 @@ export function isSafeFrameWindow() { * @see https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf — 5.4 Function $sf.ext.geom * @returns {Object | undefined} geometric information about the container */ -export function getSafeframeGeometry() { +export function getSafeframeGeometry(): SafeFrameGeometry | undefined { try { - const ws = getWindowSelf() as any; - return (typeof ws.$sf.ext.geom === 'function') ? ws.$sf.ext.geom() : undefined; + const ws = getWindowSelf(); + return isFn(ws.$sf.ext.geom) ? ws.$sf.ext.geom() : undefined; } catch (e) { logError('Error getting SafeFrame geometry', e); return undefined; } } -export function isSafariBrowser() { +export function isSafariBrowser(): boolean { return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); } -export function isFirefoxBrowser() { +export function isFirefoxBrowser(): boolean { return /firefox|fxios/i.test(navigator.userAgent); } -export function isChromeIOSBrowser() { +export function isChromeIOSBrowser(): boolean { return /crios|crmo/i.test(navigator.userAgent); } -export function replaceMacros(str: string | undefined, subs: Record) { +export function replaceMacros(str: string | undefined, subs: Record): string | undefined { if (!str) return; return Object.entries(subs).reduce((str, [key, val]) => { return str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), val || ''); }, str); } -export function replaceAuctionPrice(str: string | undefined, cpm: number | string) { +export function replaceAuctionPrice(str: string | undefined, cpm: number | string): string | undefined { return replaceMacros(str, { AUCTION_PRICE: cpm }) } -export function replaceClickThrough(str: string | undefined, clicktag: string) { +export function replaceClickThrough(str: string | undefined, clicktag: string): string | undefined { if (!str || !clicktag || typeof clicktag !== 'string') return; return str.replace(/\${CLICKTHROUGH}/g, clicktag); } -export function timestamp() { +export function timestamp(): number { return new Date().getTime(); } @@ -686,7 +682,7 @@ export function timestamp() { * The returned value represents the time elapsed since the time origin. @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now * @returns {number} */ -export function getPerformanceNow() { +export function getPerformanceNow(): number { return (window.performance && window.performance.now && window.performance.now()) || 0; } @@ -698,7 +694,7 @@ export function getPerformanceNow() { * @param {Window} w The window object used to perform the api call. default to window.self * @returns {number} */ -export function getDomLoadingDuration(w?: Window) { +export function getDomLoadingDuration(w?: Window): number { let domLoadingDuration = -1; w = w || getWindowSelf(); @@ -721,7 +717,7 @@ export function getDomLoadingDuration(w?: Window) { * When the deviceAccess flag config option is false, no cookies should be read or set * @returns {boolean} */ -export function hasDeviceAccess() { +export function hasDeviceAccess(): boolean { return config.getConfig('deviceAccess') !== false; } @@ -948,7 +944,7 @@ export function buildUrl(obj: { protocol?: string; host?: string; hostname?: str * @param {boolean} [options.checkTypes=false] - If set, two objects with identical properties but different constructors will *not* be considered equivalent. * @returns {boolean} - Returns `true` if the objects are equivalent, `false` otherwise. */ -export function deepEqual(obj1: any, obj2: any, { checkTypes = false } = {}) { +export function deepEqual(obj1: any, obj2: any, { checkTypes = false } = {}): boolean { // Quick reference check if (obj1 === obj2) return true; @@ -1055,7 +1051,7 @@ function mergeDeepHelper(target: any, source: any) { * @param seed (optional) * @returns {string} */ -export function cyrb53Hash(str: string, seed = 0) { +export function cyrb53Hash(str: string, seed: number = 0): string { const imul = function(opA: number, opB: number) { if (isFn(Math.imul)) { return Math.imul(opA, opB); @@ -1091,13 +1087,13 @@ export function cyrb53Hash(str: string, seed = 0) { * @param data * @returns {any} */ -export function safeJSONParse(data: string) { +export function safeJSONParse(data: string): T | undefined { try { return JSON.parse(data); } catch (e) {} } -export function safeJSONEncode(data: any) { +export function safeJSONEncode(data: any): string { try { return JSON.stringify(data); } catch (e) { @@ -1132,7 +1128,7 @@ export function memoize any>(fn: T, key: (...args: * @param {string} timeUnit defaults to days (or 'd'), use 'm' for minutes. Any parameter that isn't 'd' or 'm' will return Date.now(). * @returns {number} */ -export function getUnixTimestampFromNow(timeValue = 0, timeUnit = 'd') { +export function getUnixTimestampFromNow(timeValue: number = 0, timeUnit: 'm' | 'd' = 'd'): number { const acceptableUnits = ['m', 'd']; if (acceptableUnits.indexOf(timeUnit) < 0) { return Date.now(); @@ -1158,7 +1154,7 @@ export function convertObjectToArray(obj: Record) { * @param {HTMLScriptElement} script * @param {object} attributes */ -export function setScriptAttributes(script: HTMLScriptElement, attributes: Record) { +export function setScriptAttributes(script: HTMLScriptElement, attributes: Record): void { Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v)) } @@ -1198,7 +1194,7 @@ export function binarySearch(arr: T[], el: T, key: (el: T) => any = (el) => e * @param {Set} checkedObjects - A set of properties that have already been checked. * @returns {boolean} - Returns true if the object has non-serializable properties, false otherwise. */ -export function hasNonSerializableProperty(obj: Record, checkedObjects: Set = new Set()) { +export function hasNonSerializableProperty(obj: Record, checkedObjects: Set = new Set()): boolean { for (const key in obj) { const value = obj[key]; const type = typeof value; From ae1166211fc554fdd9f070806eb21534c821e028 Mon Sep 17 00:00:00 2001 From: derdeka Date: Tue, 16 Jun 2026 23:59:19 +0200 Subject: [PATCH 03/10] Core: convert src/utils.js to typescript --- modules/asterioBidAdapter.ts | 1 + src/adRendering.ts | 4 ++-- src/utils.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/asterioBidAdapter.ts b/modules/asterioBidAdapter.ts index 379e28392b7..e5bd1d66141 100644 --- a/modules/asterioBidAdapter.ts +++ b/modules/asterioBidAdapter.ts @@ -44,6 +44,7 @@ type AsterioServerBid = { format?: string; adomain?: string[]; vastXml?: string; + native?: Record; meta?: Record; }; diff --git a/src/adRendering.ts b/src/adRendering.ts index 8c284862f45..c1ce46282f1 100644 --- a/src/adRendering.ts +++ b/src/adRendering.ts @@ -213,14 +213,14 @@ export const getRenderingData = hook('sync', function (bidResponse: Bid, options }; }); -function prepareBidForRendering(bidResponse: Bid, options?: RenderOptions): Bid { +function prepareBidForRendering(bidResponse: T, options?: RenderOptions): T { const { ad, adUrl, cpm, originalCpm, safeRenderer } = bidResponse; const repl = { AUCTION_PRICE: originalCpm || cpm, CLICKTHROUGH: options?.clickUrl || '' }; - const result = { + const result: T = { ...bidResponse, ad: replaceMacros(ad, repl), adUrl: replaceMacros(adUrl, repl) diff --git a/src/utils.ts b/src/utils.ts index fa8464471e9..d40c286fe8f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -117,7 +117,7 @@ export function transformAdServerTargetingObj(targeting: Record) { /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of width, height tuples `[[300, 250]]` or '[[300,250], [970,90]]' */ -export function sizesToSizeTuples(sizes: string | number[] | number[][]) { +export function sizesToSizeTuples(sizes: string | number[] | number[][] | unknown) { if (typeof sizes === 'string') { // multiple sizes will be comma-separated return sizes From 24d35c4b82998ea3d3e191dd2770ae121b16cda6 Mon Sep 17 00:00:00 2001 From: derdeka Date: Wed, 17 Jun 2026 00:32:40 +0200 Subject: [PATCH 04/10] Core: convert src/utils.js to typescript --- src/utils.ts | 127 +++++++++++++++++++++++-------------------- src/utils/objects.ts | 16 +++--- 2 files changed, 75 insertions(+), 68 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index d40c286fe8f..44e73781621 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -45,21 +45,21 @@ const prebidInternal = {}; /** * Returns object that is used as internal prebid namespace */ -export function getPrebidInternal() { +export function getPrebidInternal(): Record { return prebidInternal; } /* utility method to get incremental integer starting from 1 */ -var getIncrementalInteger = (function () { +var getIncrementalInteger = (function (): () => number { var count = 0; - return function () { + return function (): number { count++; return count; }; })(); // generate a random string (to be used as a dynamic JSONP callback) -export function getUniqueIdentifierStr() { +export function getUniqueIdentifierStr(): string { return getIncrementalInteger() + Math.random().toString(16).substr(2); } @@ -79,7 +79,7 @@ export function generateUUID(placeholder?: any): string { * Returns random data using the Crypto API if available and Math.random if not * Method is from https://gist.github.com/jed/982883 like generateUUID, direct link https://gist.github.com/jed/982883#gistcomment-45104 */ -function _getRandomData() { +function _getRandomData(): number { if (window && window.crypto && window.crypto.getRandomValues) { return window.crypto.getRandomValues(new Uint8Array(1))[0] % 16; } else { @@ -87,14 +87,14 @@ function _getRandomData() { } } -export function getBidIdParameter(key: string, paramsObj: Record) { +export function getBidIdParameter(key: string, paramsObj: Record): string { return paramsObj?.[key] || ''; } // parse a query string object passed in bid params // bid params should be an object such as {key: "value", key1 : "value1"} // aliases to formatQS -export function parseQueryStringParameters(queryObj: Record) { +export function parseQueryStringParameters(queryObj: Record): string { let result = ''; for (var k in queryObj) { if (queryObj.hasOwnProperty(k)) { result += k + '=' + encodeURIComponent(queryObj[k]) + '&'; } @@ -104,7 +104,7 @@ export function parseQueryStringParameters(queryObj: Record) { } // transform an AdServer targeting bids into a query string to send to the adserver -export function transformAdServerTargetingObj(targeting: Record) { +export function transformAdServerTargetingObj(targeting: Record): string { // we expect to receive targeting for a single slot at a time if (targeting && Object.getOwnPropertyNames(targeting).length > 0) { return Object.keys(targeting) @@ -117,7 +117,7 @@ export function transformAdServerTargetingObj(targeting: Record) { /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of width, height tuples `[[300, 250]]` or '[[300,250], [970,90]]' */ -export function sizesToSizeTuples(sizes: string | number[] | number[][] | unknown) { +export function sizesToSizeTuples(sizes: string | number[] | number[][] | unknown): [number, number][] { if (typeof sizes === 'string') { // multiple sizes will be comma-separated return sizes @@ -139,7 +139,7 @@ export function sizesToSizeTuples(sizes: string | number[] | number[][] | unknow * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` */ -export function parseSizesInput(sizeObj: string | number[] | number[][]) { +export function parseSizesInput(sizeObj: string | number[] | number[][]): string[] { return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString); } @@ -149,25 +149,25 @@ export function sizeTupleToSizeString(size: [number, number]): string { // Parse a GPT style single size array, (i.e [300, 250]) // into an AppNexus style string, (i.e. 300x250) -export function parseGPTSingleSizeArray(singleSize: number[]) { +export function parseGPTSingleSizeArray(singleSize: number[]): string | undefined { if (isValidGPTSingleSize(singleSize)) { return sizeTupleToSizeString(singleSize); } } -export function sizeTupleToRtbSize(size: [number, number]) { +export function sizeTupleToRtbSize(size: [number, number]): { w: number; h: number } { return { w: size[0], h: size[1] }; } // Parse a GPT style single size array, (i.e [300, 250]) // into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250}) -export function parseGPTSingleSizeArrayToRtbSize(singleSize: number[]) { +export function parseGPTSingleSizeArrayToRtbSize(singleSize: number[]): { w: number; h: number } | undefined { if (isValidGPTSingleSize(singleSize)) { return sizeTupleToRtbSize(singleSize); } } -function isValidGPTSingleSize(singleSize: any): singleSize is [number, number] { +function isValidGPTSingleSize(singleSize: unknown): singleSize is [number, number] { // if we aren't exactly 2 items in this array, it is invalid return isArray(singleSize) && singleSize.length === 2 && (!isNaN(singleSize[0]) && !isNaN(singleSize[1])); } @@ -229,7 +229,7 @@ export const createIframe = (() => { }; })(); -export function createInvisibleIframe() { +export function createInvisibleIframe(): HTMLIFrameElement { return createIframe(document, { id: getUniqueIdentifierStr(), width: 0, @@ -247,7 +247,7 @@ export function createInvisibleIframe() { * Check if a given parameter name exists in query string * and if it does return the value */ -export function getParameterByName(name: string) { +export function getParameterByName(name: string): string { return parseQS(getWindowLocation().search)[name] || ''; } @@ -257,12 +257,12 @@ export function getParameterByName(name: string) { * @param {*} object object to test * @return {Boolean} if object is empty */ -export function isEmpty(object: any) { +export function isEmpty(object: unknown): boolean { if (!object) return true; if (isArray(object) || isStr(object)) { - return !(object.length > 0); + return !((object as { length: number }).length > 0); } - return Object.keys(object).length <= 0; + return Object.keys(object as Record).length <= 0; } /** @@ -270,7 +270,7 @@ export function isEmpty(object: any) { * @param str string to test * @returns {boolean} if string is empty */ -export function isEmptyStr(str: any) { +export function isEmptyStr(str: unknown): boolean { return isStr(str) && (!str || str.length === 0); } @@ -281,13 +281,13 @@ export function isEmptyStr(str: any) { * @param {Function} fn - The function to execute for each element. It receives three arguments: value, key, and the original object. * @returns {void} */ -export function _each(object: any[] | Record, fn: (value: any, key: any) => void) { +export function _each(object: any[] | Record, fn: (value: any, key: any) => void): void { if (isFn(object?.forEach)) return object.forEach(fn, this); Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k)); } -export function contains(a: any, obj: any) { - return isFn(a?.includes) && a.includes(obj); +export function contains(a: unknown, obj: unknown): boolean { + return isFn((a as { includes?: unknown })?.includes) && (a as { includes: (o: unknown) => boolean }).includes(obj); } /** @@ -297,7 +297,7 @@ export function contains(a: any, obj: any) { * @param {Function} callback - The function to execute for each element. It receives three arguments: value, key, and the original object. * @return {Array} */ -export function _map(object: any[] | Record, callback: (value: any, key: any, obj?: any) => any) { +export function _map(object: any[] | Record, callback: (value: any, key: any, obj?: any) => any): any[] { if (isFn(object?.map)) return object.map(callback); return Object.entries(object || {}).map(([k, v]) => callback(v, k, object)); } @@ -310,7 +310,7 @@ export function _map(object: any[] | Record, callback: (value: any, * @param {Boolean} [asLastChildChild] * @return {HTML Element} */ -export function insertElement(elm: Node, doc?: Document, target?: string, asLastChildChild?: boolean) { +export function insertElement(elm: Node, doc?: Document, target?: string, asLastChildChild?: boolean): Node | undefined { doc = doc || document; let parentEl; if (target) { @@ -363,7 +363,7 @@ export function waitForElementToLoad(element: HTMLElement, timeout?: number): Pr * `'omit'` for a cookieless request — the image fallback (which cannot omit cookies) is then skipped. */ -export function politeTriggerPixel(url: string, credentials: RequestCredentials = 'include') { +export function politeTriggerPixel(url: string, credentials: RequestCredentials = 'include'): void { const triggerSync = () => { if (window.fetch && window.Request) { try { @@ -383,11 +383,11 @@ export function politeTriggerPixel(url: string, credentials: RequestCredentials runBackgroundTask(triggerSync); } -export function politeInsertUserSyncIframe(url: string) { +export function politeInsertUserSyncIframe(url: string): void { runBackgroundTask(() => insertUserSyncIframe(url)); } -export function triggerPixel(url: string, done?: () => void, timeout?: number) { +export function triggerPixel(url: string, done?: () => void, timeout?: number): void { const img = new Image(); if (done && internal.isFn(done)) { waitForElementToLoad(img, timeout).then(done); @@ -399,7 +399,7 @@ export function triggerPixel(url: string, done?: () => void, timeout?: number) { * Run a task at low priority when supported by the browser, or immediately as fallback. * @param {function} task */ -export function runBackgroundTask(task) { +export function runBackgroundTask(task: () => void): void { const scheduler = window.scheduler; if (scheduler?.postTask) { scheduler.postTask(task, { priority: 'background' }).catch(() => task()); @@ -514,7 +514,7 @@ export function flatten(a: T[], b: T | T[]) { return a.concat(b); } -export function getBidRequest(id: string, bidderRequests: Array<{ bids: any[] }>) { +export function getBidRequest(id: string, bidderRequests: Array<{ bids: any[] }>): Record | undefined { if (!id) { return; } @@ -522,11 +522,11 @@ export function getBidRequest(id: string, bidderRequests: Array<{ bids: any[] }> .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id)); } -export function getValue(obj: Record, key: string) { +export function getValue(obj: Record, key: string): unknown { return obj[key]; } -export function getBidderCodes(adUnits: Array<{ bids?: Array<{ bidder?: string }> }>) { +export function getBidderCodes(adUnits: Array<{ bids?: Array<{ bidder?: string }> }>): string[] { // this could memoize adUnits return adUnits.map(unit => unit.bids.map(bid => bid.bidder) .reduce(flatten, [])).reduce(flatten, []).filter((bidder) => typeof bidder !== 'undefined').filter(uniques); @@ -683,7 +683,7 @@ export function hasDeviceAccess(): boolean { /** * @returns {(boolean|undefined)} */ -export function checkCookieSupport() { +export function checkCookieSupport(): boolean | undefined { // eslint-disable-next-line no-restricted-properties if (window.navigator.cookieEnabled || !!document.cookie.length) { return true; @@ -702,12 +702,12 @@ export function checkCookieSupport() { * @param {number} numRequiredCalls The number of times which the returned function needs to be called before * func is. */ -export function delayExecution(func: (...args: any[]) => void, numRequiredCalls: number) { +export function delayExecution(func: (...args: any[]) => void, numRequiredCalls: number): (...args: any[]) => void { if (numRequiredCalls < 1) { throw new Error(`numRequiredCalls must be a positive number. Got ${numRequiredCalls}`); } let numCalls = 0; - return function (...args: any[]) { + return function (...args: any[]): void { numCalls++; if (numCalls === numRequiredCalls) { func.apply(this, args); @@ -734,7 +734,7 @@ export function groupBy(xs: any[], key: string): Record { * @param mediaTypes mediaTypes parameter to validate * @return If object is valid */ -export function isValidMediaTypes(mediaTypes: Record) { +export function isValidMediaTypes(mediaTypes: Record): boolean { const SUPPORTED_MEDIA_TYPES = ['banner', 'native', 'video', 'audio']; const SUPPORTED_STREAM_TYPES = ['instream', 'outstream']; @@ -758,7 +758,7 @@ export function isValidMediaTypes(mediaTypes: Record) { * @param {string} bidder code * @return {Array} user configured param for the given bidder adunit configuration */ -export function getUserConfiguredParams(adUnits: Array<{ code: string; bids: Array<{ bidder: string; params?: any }> }>, adUnitCode: string, bidder: string) { +export function getUserConfiguredParams(adUnits: Array<{ code: string; bids: Array<{ bidder: string; params?: Record }> }>, adUnitCode: string, bidder: string): Record[] { return adUnits .filter(adUnit => adUnit.code === adUnitCode) .flatMap((adUnit) => adUnit.bids) @@ -773,7 +773,7 @@ export const compareCodeAndSlot = (slot: googletag.Slot, adUnitCode: string) => * @param slot GoogleTag slot * @return filter function */ -export function isAdUnitCodeMatchingSlot(slot: googletag.Slot) { +export function isAdUnitCodeMatchingSlot(slot: googletag.Slot): (adUnitCode: string) => boolean { const customGptSlotMatching = config.getConfig('customGptSlotMatching'); const match = isFn(customGptSlotMatching) && customGptSlotMatching(slot); return isFn(match) ? match : (adUnitCode: string) => compareCodeAndSlot(slot, adUnitCode); @@ -787,7 +787,7 @@ export function isAdUnitCodeMatchingSlot(slot: googletag.Slot) { * @param {string} bidder bidder code that is not compatible with the adUnit * @return {string} warning message to display when condition is met */ -export function unsupportedBidderMessage(adUnit: { code: string; mediaTypes?: Record }, bidder: string) { +export function unsupportedBidderMessage(adUnit: { code: string; mediaTypes?: Record }, bidder: string): string { const mediaType = Object.keys(adUnit.mediaTypes || { 'banner': 'banner' }).join(', '); return ` @@ -801,7 +801,7 @@ export function unsupportedBidderMessage(adUnit: { code: string; mediaTypes?: Re * Returns a new object with undefined properties removed from given object * @param obj the object to clean */ -export function cleanObj(obj: Record) { +export function cleanObj(obj: Record): Record { return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined')); } @@ -810,7 +810,7 @@ export function cleanObj(obj: Record) { * @param obj the original object * @param properties An array of desired properties */ -export function pick(obj: Record, properties: ReadonlyArray any)>) { +export function pick(obj: Record, properties: ReadonlyArray any)>): Record { if (typeof obj !== 'object') { return {}; } @@ -856,7 +856,7 @@ export function parseQS(query: string): Record { }, {}); } -export function formatQS(query: Record) { +export function formatQS(query: Record): string { return Object .keys(query) .map(k => Array.isArray(query[k]) @@ -865,7 +865,16 @@ export function formatQS(query: Record) { .join('&'); } -export function parseUrl(url: string, options?: Record) { +export function parseUrl(url: string, options?: Record): { + href: string; + protocol: string; + hostname: string; + port: number; + pathname: string; + search: any; + hash: string; + host: string; +} { const parsed = document.createElement('a'); if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) { parsed.href = url; @@ -886,7 +895,7 @@ export function parseUrl(url: string, options?: Record) { }; } -export function buildUrl(obj: { protocol?: string; host?: string; hostname?: string; port?: number; pathname?: string; search?: string | Record; hash?: string }) { +export function buildUrl(obj: { protocol?: string; host?: string; hostname?: string; port?: number; pathname?: string; search?: string | Record; hash?: string }): string { return (obj.protocol || 'http') + '://' + (obj.host || obj.hostname + (obj.port ? `:${obj.port}` : '')) + @@ -965,7 +974,7 @@ export function mergeDeep(target: Record, ...sources: any[]): Recor return target; } -function mergeDeepHelper(target: any, source: any) { +function mergeDeepHelper(target: any, source: any): void { // quick check if (!isPlainObject(target) || !isPlainObject(source)) { return; @@ -1052,7 +1061,7 @@ export function safeJSONParse(data: string): T | undefined { } catch (e) {} } -export function safeJSONEncode(data: any): string { +export function safeJSONEncode(data: unknown): string { try { return JSON.stringify(data); } catch (e) { @@ -1068,9 +1077,9 @@ export function safeJSONEncode(data: any): string { * By default, the first argument is used as key. * @return {*} */ -export function memoize any>(fn: T, key: (...args: any[]) => any = (arg) => arg) { +export function memoize any>(fn: T, key: (...args: any[]) => any = (arg) => arg): ((...args: any[]) => ReturnType) & { clear: () => void } { const cache = new Map(); - const memoized = function (...args: any[]) { + const memoized = function (...args: any[]): ReturnType { const cacheKey = key.apply(this, args); if (!cache.has(cacheKey)) { cache.set(cacheKey, fn.apply(this, args)); @@ -1102,7 +1111,7 @@ export function getUnixTimestampFromNow(timeValue: number = 0, timeUnit: 'm' | ' * @param {Object} obj the object * @returns {Array} */ -export function convertObjectToArray(obj: Record) { +export function convertObjectToArray(obj: Record): Array> { return Object.keys(obj).map(key => { return { [key]: obj[key] }; }); @@ -1127,7 +1136,7 @@ export function setScriptAttributes(script: HTMLScriptElement, attributes: Recor * if the element is not found, return the index of the first element that's greater; * if no greater element exists, return `arr.length`) */ -export function binarySearch(arr: T[], el: T, key: (el: T) => any = (el) => el) { +export function binarySearch(arr: T[], el: T, key: (el: T) => any = (el) => el): number { let left = 0; let right = arr.length && arr.length - 1; const target = key(el); @@ -1191,7 +1200,7 @@ export function hasNonSerializableProperty(obj: Record, checkedObje * @param {String} key - Key of nested property. * @returns {any|undefined} - Value of nested property. */ -export function setOnAny(collection: any[], key: string) { +export function setOnAny(collection: any[], key: string): unknown { for (let i = 0, result: any; i < collection.length; i++) { result = deepAccess(collection[i], key); if (result) { @@ -1201,7 +1210,7 @@ export function setOnAny(collection: any[], key: string) { return undefined; } -export function extractDomainFromHost(pageHost: string) { +export function extractDomainFromHost(pageHost: string): string | null { let domain = null; try { const domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost); @@ -1219,7 +1228,7 @@ export function extractDomainFromHost(pageHost: string) { return domain; } -export function triggerNurlWithCpm(bid: { nurl?: string }, cpm: number | string) { +export function triggerNurlWithCpm(bid: { nurl?: string }, cpm: number | string): void { if (isStr(bid.nurl) && bid.nurl !== '') { bid.nurl = bid.nurl.replace( /\${AUCTION_PRICE}/, @@ -1231,10 +1240,10 @@ export function triggerNurlWithCpm(bid: { nurl?: string }, cpm: number | string) // To ensure that isGzipCompressionSupported() doesn’t become an overhead, we have used memoization to cache the result after the first execution. // This way, even if the function is called multiple times, it will only perform the actual check once and return the cached result in subsequent calls. -export const isGzipCompressionSupported = (function () { +export const isGzipCompressionSupported = (function (): () => boolean { let cachedResult: boolean | undefined; - return function () { + return function (): boolean { if (cachedResult !== undefined) { return cachedResult; // Return cached result if already computed } @@ -1255,13 +1264,11 @@ export const isGzipCompressionSupported = (function () { })(); // Make sure to use isGzipCompressionSupported before calling this function -export async function compressDataWithGZip(data: any) { - if (typeof data !== 'string') { // TextEncoder (below) expects a string - data = JSON.stringify(data); - } +export async function compressDataWithGZip(data: unknown): Promise { + const strData: string = typeof data === 'string' ? data : JSON.stringify(data); const encoder = new TextEncoder(); - const encodedData = encoder.encode(data); + const encodedData = encoder.encode(strData); const compressedStream = new Blob([encodedData]) .stream() .pipeThrough(new window.CompressionStream('gzip')); diff --git a/src/utils/objects.ts b/src/utils/objects.ts index c138ad74ff6..3bda971e0c9 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -25,25 +25,25 @@ const toString = Object.prototype.toString; * Return if the object is of the * given type. */ -export function isFn(object): object is AnyFunction { +export function isFn(object: unknown): object is AnyFunction { return typeof object === 'function'; } -export function isStr(object): object is string { +export function isStr(object: unknown): object is string { return typeof object === 'string'; } -export const isArray: (object) => object is any[] = Array.isArray.bind(Array); +export const isArray: (object: unknown) => object is any[] = Array.isArray.bind(Array); -export function isNumber(object): object is number { +export function isNumber(object: unknown): object is number { return typeof object === 'number'; } -export function isPlainObject(object): object is Record { +export function isPlainObject(object: unknown): object is Record { return toString.call(object) === '[object Object]'; } -export function isBoolean(object): object is boolean { +export function isBoolean(object: unknown): object is boolean { return typeof object === 'boolean'; } @@ -51,8 +51,8 @@ export function isBoolean(object): object is boolean { * Checks input is integer or not * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger */ -export const isInteger: (value) => value is number = Number.isInteger.bind(Number); +export const isInteger: (value: unknown) => value is number = Number.isInteger.bind(Number); -export function isArrayOfNums(val, size?: L): val is Repeat { +export function isArrayOfNums(val: unknown, size?: L): val is Repeat { return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v))); } From a20be6a9a324bc7e8fbf7871589627d0535ea5e2 Mon Sep 17 00:00:00 2001 From: derdeka Date: Mon, 9 Nov 2026 09:51:42 +0100 Subject: [PATCH 05/10] Core: convert src/utils.js to typescript --- modules/ad_spiroBidAdapter.ts | 3 +- src/adapterManager.ts | 8 ++--- src/types/local/safeframe.d.ts | 31 ++---------------- src/utils.ts | 27 ++++++++++------ src/utils/cachedApiWrapper.js | 26 --------------- src/utils/cachedApiWrapper.ts | 32 +++++++++++++++++++ src/utils/{cpm.js => cpm.ts} | 4 +-- .../{focusTimeout.js => focusTimeout.ts} | 8 ++--- src/utils/{gdpr.js => gdpr.ts} | 2 +- src/utils/{ipUtils.js => ipUtils.ts} | 8 ++--- src/utils/objects.ts | 2 +- src/utils/{reducers.js => reducers.ts} | 28 ++++++++-------- .../{winDimensions.js => winDimensions.ts} | 21 +++++++++--- 13 files changed, 102 insertions(+), 98 deletions(-) delete mode 100644 src/utils/cachedApiWrapper.js create mode 100644 src/utils/cachedApiWrapper.ts rename src/utils/{cpm.js => cpm.ts} (80%) rename src/utils/{focusTimeout.js => focusTimeout.ts} (80%) rename src/utils/{gdpr.js => gdpr.ts} (75%) rename src/utils/{ipUtils.js => ipUtils.ts} (83%) rename src/utils/{reducers.js => reducers.ts} (65%) rename src/utils/{winDimensions.js => winDimensions.ts} (74%) diff --git a/modules/ad_spiroBidAdapter.ts b/modules/ad_spiroBidAdapter.ts index 1604e0d1ece..0bd4258327f 100644 --- a/modules/ad_spiroBidAdapter.ts +++ b/modules/ad_spiroBidAdapter.ts @@ -8,6 +8,7 @@ import { import { ajax } from '../src/ajax.js'; import { AUDIO, BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { isPlainObject, logWarn } from '../src/utils.js'; +import type { ORTBResponse } from '../src/prebid.public.js'; /** * Adspiro bid parameters. @@ -106,7 +107,7 @@ export const spec: BidderSpec = { if (!isPlainObject(serverResponse?.body)) { return []; } - const { bids } = converter.fromORTB({ request: request.data, response: serverResponse.body }) as ExtendedResponse; + const { bids } = converter.fromORTB({ request: request.data, response: serverResponse.body as ORTBResponse }) as ExtendedResponse; return bids; }, diff --git a/src/adapterManager.ts b/src/adapterManager.ts index c3699037980..aa67eb50b3e 100644 --- a/src/adapterManager.ts +++ b/src/adapterManager.ts @@ -80,7 +80,7 @@ export const PBS_ADAPTER_NAME = 'pbsBidAdapter'; export const PARTITIONS = { CLIENT: 'client', SERVER: 'server' -}; +} as const; export const dep = { isAllowed: isActivityAllowed, @@ -440,13 +440,13 @@ export function getS2SBidderSet(s2sConfigs) { * @returns {Object} return.client - Array of bidder codes that should be routed to client adapters. * @returns {Object} return.server - Array of bidder codes that should be routed to server adapters. */ -export function _partitionBidders (adUnits, s2sConfigs, { getS2SBidders = getS2SBidderSet } = {}) { +export function _partitionBidders (adUnits, s2sConfigs, { getS2SBidders = getS2SBidderSet } = {}): { client: string[]; server: string[] } { const serverBidders = getS2SBidders(s2sConfigs); - return getBidderCodes(adUnits).reduce((memo, bidder) => { + return getBidderCodes(adUnits).reduce((memo: { client: string[]; server: string[] }, bidder) => { const partition = serverBidders.has(bidder) ? PARTITIONS.SERVER : PARTITIONS.CLIENT; memo[partition].push(bidder); return memo; - }, { [PARTITIONS.CLIENT]: [], [PARTITIONS.SERVER]: [] }); + }, { client: [], server: [] }); } export const partitionBidders = hook('sync', _partitionBidders, 'partitionBidders'); diff --git a/src/types/local/safeframe.d.ts b/src/types/local/safeframe.d.ts index 2eee353b46f..8c44be83f04 100644 --- a/src/types/local/safeframe.d.ts +++ b/src/types/local/safeframe.d.ts @@ -1,4 +1,5 @@ -export {}; +import type { SafeFrameGeometry } from '../../utils.ts'; + declare global { interface Window { $sf?: SafeFrameAPI; @@ -33,32 +34,4 @@ declare global { supports(feature: string): boolean; } - - interface SafeFrameGeometry { - self?: { - x: number; - y: number; - w: number; - h: number; - }; - - exp?: { - t: number; - b: number; - l: number; - r: number; - }; - - win?: { - w: number; - h: number; - }; - - view?: { - x: number; - y: number; - w: number; - h: number; - }; - } } diff --git a/src/utils.ts b/src/utils.ts index a0756e69c41..0e63c9f1e05 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,12 +1,12 @@ import { config } from './config.js'; import { PbPromise } from './utils/promise.js'; -import deepAccess from 'dlv/index.js'; +import dlv from 'dlv/index.js'; import { isArray, isFn, isPlainObject, isStr } from './utils/objects.js'; import * as logging from './utils/logging.js'; import * as debug from './utils/debug.js'; -export { deepAccess }; +export const deepAccess: (obj: any, path: string | ReadonlyArray, def?: any) => any = dlv; export { dset as deepSetValue } from 'dset'; export * from './utils/objects.js'; export { getWinDimensions, resetWinDimensions, getScreenOrientation } from './utils/winDimensions.js'; @@ -565,6 +565,16 @@ export function isSafeFrameWindow(): boolean { return !!(ws.$sf && ws.$sf.ext); } +/** + * Geometry of a SafeFrame container, as returned by `$sf.ext.geom()`. + */ +export interface SafeFrameGeometry { + self?: { x: number; y: number; w: number; h: number }; + exp?: { t: number; b: number; l: number; r: number }; + win?: { w: number; h: number }; + view?: { x: number; y: number; w: number; h: number }; +} + /** * Returns the result of calling the function $sf.ext.geom() if it exists * @see https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf — 5.4 Function $sf.ext.geom @@ -938,7 +948,7 @@ export function deepEqual(obj1: any, obj2: any, { checkTypes = false } = {}): bo return true; } -export function mergeDeep(target: Record, ...sources: any[]): Record { +export function mergeDeep(target: Record, ...sources: any[]): any { for (let i = 0; i < sources.length; i++) { const source = sources[i]; if (!isPlainObject(source)) { @@ -1052,19 +1062,18 @@ export function safeJSONEncode(data: unknown): string { * By default, the first argument is used as key. * @return {*} */ -export function memoize any>(fn: T, key: (...args: any[]) => any = (arg) => arg): ((...args: any[]) => ReturnType) & { clear: () => void } { +export function memoize any>(fn: T, key: (...args: any[]) => any = (arg) => arg): ((...args: Parameters) => ReturnType) & { clear: () => void; invalidate: (...args: any[]) => void } { const cache = new Map(); - const memoized = function (...args: any[]): ReturnType { + const memoized = function (this: unknown, ...args: Parameters): ReturnType { const cacheKey = key.apply(this, args); if (!cache.has(cacheKey)) { cache.set(cacheKey, fn.apply(this, args)); } return cache.get(cacheKey); - }; + } as ((...args: Parameters) => ReturnType) & { clear: () => void; invalidate: (...args: any[]) => void }; memoized.clear = cache.clear.bind(cache); - memoized.invalidate = function () { - // eslint-disable-next-line prefer-rest-params - cache.delete(key.apply(this, arguments)); + memoized.invalidate = function (this: unknown, ...args: any[]) { + cache.delete(key.apply(this, args)); }; return memoized; } diff --git a/src/utils/cachedApiWrapper.js b/src/utils/cachedApiWrapper.js deleted file mode 100644 index a36026ed98e..00000000000 --- a/src/utils/cachedApiWrapper.js +++ /dev/null @@ -1,26 +0,0 @@ -export function CachedApiWrapper(getTarget, props) { - const wrapper = {}; - let data = {}; - const children = []; - Object.entries(props).forEach(([key, value]) => { - if (value != null && typeof value === 'object') { - const child = new CachedApiWrapper(() => getTarget()?.[key], value); - wrapper[key] = child.obj; - children.push(child.reset); - } else if (value === true) { - Object.defineProperty(wrapper, key, { - get() { - if (!data.hasOwnProperty(key)) { - data[key] = getTarget()?.[key]; - } - return data[key]; - } - }); - } - }); - this.obj = wrapper; - this.reset = function () { - children.forEach(reset => reset()); - data = {}; - }; -} diff --git a/src/utils/cachedApiWrapper.ts b/src/utils/cachedApiWrapper.ts new file mode 100644 index 00000000000..ab4e71f2c4c --- /dev/null +++ b/src/utils/cachedApiWrapper.ts @@ -0,0 +1,32 @@ +export type CachedApiProps = { [key: string]: boolean | CachedApiProps }; + +/** + * Wraps the properties of the object returned by `getTarget` flagged in `props` with getters that + * read through to the target once, and serve the cached value until `reset()` is called. + */ +export function CachedApiWrapper(this: { obj: T; reset: () => void }, getTarget: () => any, props: CachedApiProps) { + const wrapper: Record = {}; + let data: Record = {}; + const children: Array<() => void> = []; + Object.entries(props).forEach(([key, value]) => { + if (value != null && typeof value === 'object') { + const child = new (CachedApiWrapper as any)(() => getTarget()?.[key], value); + wrapper[key] = child.obj; + children.push(child.reset); + } else if (value === true) { + Object.defineProperty(wrapper, key, { + get() { + if (!data.hasOwnProperty(key)) { + data[key] = getTarget()?.[key]; + } + return data[key]; + } + }); + } + }); + this.obj = wrapper as T; + this.reset = function () { + children.forEach(reset => reset()); + data = {}; + }; +} diff --git a/src/utils/cpm.js b/src/utils/cpm.ts similarity index 80% rename from src/utils/cpm.js rename to src/utils/cpm.ts index a52cdc45a97..08c4661ff58 100644 --- a/src/utils/cpm.js +++ b/src/utils/cpm.ts @@ -7,14 +7,14 @@ import { logError } from '../utils.js'; * precedence as publisher settings: `getOwn(bidderCode)` wins, otherwise adapter vs bidder depending on * `adjustAlternateBids` on the adapter scope. */ -export function getBidAdjustmentFn(bid, bs, key, bidRequest) { +export function getBidAdjustmentFn(bid: any, bs: any, key: string, bidRequest?: any): any { const adapterCode = bid?.adapterCode; const bidderCode = bid?.bidderCode || bidRequest?.bidder; const adjustAlternateBids = bs.get(bid?.adapterCode, 'adjustAlternateBids'); return bs.getOwn(bidderCode, key) || bs.get(adjustAlternateBids ? adapterCode : bidderCode, key); } -export function adjustCpm(cpm, bidResponse, bidRequest, { index = auctionManager.index, bs = bidderSettings } = {}) { +export function adjustCpm(cpm: number, bidResponse: any, bidRequest?: any, { index = auctionManager.index, bs = bidderSettings }: { index?: any; bs?: any } = {}): number { bidRequest = bidRequest || index.getBidRequest(bidResponse); const bidCpmAdjustment = getBidAdjustmentFn(bidResponse, bs, 'bidCpmAdjustment', bidRequest); if (bidCpmAdjustment && typeof bidCpmAdjustment === 'function') { diff --git a/src/utils/focusTimeout.js b/src/utils/focusTimeout.ts similarity index 80% rename from src/utils/focusTimeout.js rename to src/utils/focusTimeout.ts index b448f3d6201..7223e1bff16 100644 --- a/src/utils/focusTimeout.js +++ b/src/utils/focusTimeout.ts @@ -1,6 +1,6 @@ -let outOfFocusStart = null; // enforce null otherwise it could be undefined and the callback wouldn't execute +let outOfFocusStart: number | null = null; // enforce null otherwise it could be undefined and the callback wouldn't execute let timeOutOfFocus = 0; -let suspendedTimeouts = []; +let suspendedTimeouts: Array<{ callback: () => void; startTime: number; setTimerId: (id: ReturnType) => void }> = []; function trackTimeOutOfFocus() { if (document.hidden) { @@ -15,7 +15,7 @@ function trackTimeOutOfFocus() { document.addEventListener('visibilitychange', trackTimeOutOfFocus); -export function reset() { +export function reset(): void { outOfFocusStart = null; timeOutOfFocus = 0; suspendedTimeouts = []; @@ -30,7 +30,7 @@ export function reset() { * @param {number} [milliseconds] - Minimum duration (in milliseconds) that the callback will be executed after * @returns {function(): number} - Getter function for current timer id */ -export function setFocusTimeout(callback, milliseconds) { +export function setFocusTimeout(callback: () => void, milliseconds?: number): () => ReturnType { const startTime = timeOutOfFocus; let timerId = setTimeout(() => { if (timeOutOfFocus === startTime && outOfFocusStart == null) { diff --git a/src/utils/gdpr.js b/src/utils/gdpr.ts similarity index 75% rename from src/utils/gdpr.js rename to src/utils/gdpr.ts index 6151861a67b..3ffa8fabdef 100644 --- a/src/utils/gdpr.js +++ b/src/utils/gdpr.ts @@ -4,7 +4,7 @@ * @param gdprConsent GDPR consent data * @returns {boolean} true if the gdprConsent is null-y; or GDPR does not apply; or if purpose 1 consent was given. */ -export function hasPurpose1Consent(gdprConsent) { +export function hasPurpose1Consent(gdprConsent?: { gdprApplies?: boolean; vendorData?: any } | null): boolean { if (gdprConsent?.gdprApplies) { return gdprConsent?.vendorData?.purpose?.consents?.[1] === true; } diff --git a/src/utils/ipUtils.js b/src/utils/ipUtils.ts similarity index 83% rename from src/utils/ipUtils.js rename to src/utils/ipUtils.ts index cbb0c38c298..1f5c6a7c600 100644 --- a/src/utils/ipUtils.js +++ b/src/utils/ipUtils.ts @@ -1,4 +1,4 @@ -export function scrubIPv4(ip) { +export function scrubIPv4(ip?: string | null): string | null { if (!ip) { return null; } @@ -11,7 +11,7 @@ export function scrubIPv4(ip) { return null; } - const mask = []; + const mask: number[] = []; for (let i = 0; i < 4; i++) { const n = Math.max(0, Math.min(8, ones - (i * 8))); mask.push((0xff << (8 - n)) & 0xff); @@ -22,7 +22,7 @@ export function scrubIPv4(ip) { return maskedIP.join('.'); } -export function scrubIPv6(ip) { +export function scrubIPv6(ip?: string | null): string | null { if (!ip) { return null; } @@ -40,7 +40,7 @@ export function scrubIPv6(ip) { return null; } - const mask = []; + const mask: number[] = []; for (let i = 0; i < 8; i++) { const n = Math.max(0, Math.min(16, ones - (i * 16))); mask.push((0xffff << (16 - n)) & 0xffff); diff --git a/src/utils/objects.ts b/src/utils/objects.ts index 3b71ce69e22..463ca50114d 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -39,7 +39,7 @@ export function isNumber(object: unknown): object is number { return typeof object === 'number'; } -export function isPlainObject(object: unknown): object is Record { +export function isPlainObject(object: unknown): object is Record { return toString.call(object) === '[object Object]'; } diff --git a/src/utils/reducers.js b/src/utils/reducers.ts similarity index 65% rename from src/utils/reducers.js rename to src/utils/reducers.ts index 97102dadf57..5ff3d740f1a 100644 --- a/src/utils/reducers.js +++ b/src/utils/reducers.ts @@ -1,19 +1,21 @@ import { DEFAULT_PRIORITY, PRIORITY_INCREMENT } from './desirability.js'; -export function simpleCompare(a, b) { +export type Compare = (a: T, b: T) => number; + +export function simpleCompare(a: T, b: T): number { if (a === b) return 0; return a < b ? -1 : 1; } -export function keyCompare(key = (item) => item) { +export function keyCompare(key: (item: T) => unknown = (item) => item): Compare { return (a, b) => simpleCompare(key(a), key(b)); } -export function reverseCompare(compare = simpleCompare) { +export function reverseCompare(compare: Compare = simpleCompare): Compare { return (a, b) => -compare(a, b) || 0; } -export function tiebreakCompare(...compares) { +export function tiebreakCompare(...compares: Array>): Compare { return function (a, b) { for (const cmp of compares) { const val = cmp(a, b); @@ -23,28 +25,28 @@ export function tiebreakCompare(...compares) { }; } -export function minimum(compare = simpleCompare) { +export function minimum(compare: Compare = simpleCompare): (min: T, item: T) => T { return (min, item) => compare(item, min) < 0 ? item : min; } -export function maximum(compare = simpleCompare) { +export function maximum(compare: Compare = simpleCompare): (min: T, item: T) => T { return minimum(reverseCompare(compare)); } -const cpmCompare = keyCompare((bid) => bid.cpm); -const timestampCompare = keyCompare((bid) => bid.responseTimestamp); +const cpmCompare = keyCompare((bid) => bid.cpm); +const timestampCompare = keyCompare((bid) => bid.responseTimestamp); // This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond -export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))); +export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))); -export function priorityCompare(left, right) { +export function priorityCompare(left: any, right: any): number { const a = left.priority ?? DEFAULT_PRIORITY; const b = right.priority ?? DEFAULT_PRIORITY; if (a === b) return 0; return a < b ? -PRIORITY_INCREMENT : PRIORITY_INCREMENT; } -export function dealsFirst(a, b) { +export function dealsFirst(a: any, b: any): number { if (a.adserverTargeting.hb_deal !== undefined && b.adserverTargeting.hb_deal === undefined) { return -1; } @@ -54,7 +56,7 @@ export function dealsFirst(a, b) { return 0; } -export const bidDesirabilityCompare = tiebreakCompare(priorityCompare, keyCompare((bid) => bid.desirability ?? bid.cpm)); +export const bidDesirabilityCompare = tiebreakCompare(priorityCompare, keyCompare((bid) => bid.desirability ?? bid.cpm)); // Sort comparator that orders bids by `.desirability` (falling back to cpm), highest first. export const highestDesirabilityFirst = reverseCompare(bidDesirabilityCompare); @@ -62,7 +64,7 @@ export const highestDesirabilityFirst = reverseCompare(bidDesirabilityCompare); // This function will get highest bid by `.desirability` (publisher `bidDesirabilityAdjustment` when set, else mirrors cpm via `adjustBids`); // ties use lowest `timeToRespond`, matching {@link getHighestCpm}. export const getHighestDesirability = maximum( - tiebreakCompare(bidDesirabilityCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond))) + tiebreakCompare(bidDesirabilityCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond))) ); // This function will get the oldest highest cpm value bid, in case of tie it will return the bid which came in first diff --git a/src/utils/winDimensions.js b/src/utils/winDimensions.ts similarity index 74% rename from src/utils/winDimensions.js rename to src/utils/winDimensions.ts index 83bc6a0f300..34716767fa5 100644 --- a/src/utils/winDimensions.js +++ b/src/utils/winDimensions.ts @@ -2,7 +2,20 @@ import { canAccessWindowTop, internal as utilsInternals } from '../utils.js'; import { CachedApiWrapper } from './cachedApiWrapper.js'; import { endLayoutEpoch, onLayoutEpochEnd, startLayoutEpoch } from './layoutEpoch.js'; -const winDimensions = new CachedApiWrapper( +export type WinDimensions = { + innerHeight: number; + innerWidth: number; + scrollX: number; + scrollY: number; + screen: { width: number; height: number }; + visualViewport: { width: number; height: number }; + document: { + documentElement: { clientWidth: number; clientHeight: number; scrollTop: number; scrollLeft: number }; + body: { scrollTop: number; scrollLeft: number; clientWidth: number; clientHeight: number }; + }; +}; + +const winDimensions: { obj: WinDimensions; reset: () => void } = new (CachedApiWrapper as any)( () => { // called for every value read into the cache, including through an object obtained from // getWinDimensions() in an earlier epoch, so that each value is discarded when the epoch it was read in ends @@ -48,9 +61,9 @@ onLayoutEpochEnd(() => internal.winDimensions.reset()); /** * Dimensions and scroll offsets of the top window (or of this window, if the top one is not accessible). * Values are cached for the current layout epoch, together with element rects from the - * `boundingClientRect` library, so the two can be combined; see `layoutEpoch.js`. + * `boundingClientRect` library, so the two can be combined; see `layoutEpoch.ts`. */ -export function getWinDimensions() { +export function getWinDimensions(): WinDimensions { return internal.winDimensions.obj; } @@ -58,7 +71,7 @@ export function resetWinDimensions() { endLayoutEpoch(); } -export function getScreenOrientation(win) { +export function getScreenOrientation(win?: Window): 'portrait' | 'landscape' | undefined { const fallbackWin = win ?? (canAccessWindowTop() ? utilsInternals.getWindowTop() : utilsInternals.getWindowSelf()); const screenData = fallbackWin?.screen ?? utilsInternals.getWindowSelf()?.screen ?? window?.screen; const width = Number(screenData?.width); From 73703c8c06797315995e1f796bda252ef5fae84c Mon Sep 17 00:00:00 2001 From: derdeka Date: Mon, 9 Nov 2026 10:39:50 +0100 Subject: [PATCH 06/10] Core: convert src/utils.js to typescript --- modules/conversantBidAdapter.ts | 2 +- modules/multibid/index.ts | 3 ++- src/bidderSettings.ts | 2 +- src/utils.ts | 26 +++++++++++++------------- src/utils/cachedApiWrapper.ts | 4 ++-- src/utils/gdpr.ts | 2 +- src/utils/reducers.ts | 11 ++++++----- 7 files changed, 26 insertions(+), 24 deletions(-) diff --git a/modules/conversantBidAdapter.ts b/modules/conversantBidAdapter.ts index c6de56e7acf..12d29c914de 100644 --- a/modules/conversantBidAdapter.ts +++ b/modules/conversantBidAdapter.ts @@ -138,7 +138,7 @@ const converter = ortbConverter({ copyOptProperty(bidRequest.params?.maxduration, videoData, 'maxduration'); copyOptProperty(bidRequest.params?.protocols, videoData, 'protocols'); copyOptProperty(bidRequest.params?.api, videoData, 'api'); - imp.video = mergeDeep(videoData, imp.video) as any; + imp.video = mergeDeep(videoData, imp.video); fillVideoImp(imp, bidRequest, context); } }, diff --git a/modules/multibid/index.ts b/modules/multibid/index.ts index a7b103baa53..daa19ba0bfc 100644 --- a/modules/multibid/index.ts +++ b/modules/multibid/index.ts @@ -3,6 +3,7 @@ * @module modules/multibid */ +import type { Bid } from '../../src/bidfactory.ts'; import { config } from '../../src/config.js'; import { getHook, setupBeforeHookFnOnce } from '../../src/hook.js'; import { deepAccess, deepSetValue, getUniqueIdentifierStr, groupBy, logWarn } from '../../src/utils.js'; @@ -210,7 +211,7 @@ export function aliasedBiddersFirst(a, b) { * @param {Number} adUnitBidLimit bidder targeting limit, default set to 0 * @param {Boolean} hasModified default set to false, this hook modifies targeting and sets to true */ -export function targetBidPoolHook(fn, bidsReceived, winReducer, adUnitBidLimit = 0, hasModified = false, winSorter = highestDesirabilityFirst) { +export function targetBidPoolHook(fn, bidsReceived: Bid[], winReducer, adUnitBidLimit = 0, hasModified = false, winSorter = highestDesirabilityFirst) { if (!config.getConfig('multibid')) resetMultiConfig(); if (hasMultibid) { const modifiedBids = []; diff --git a/src/bidderSettings.ts b/src/bidderSettings.ts index 8a12ebddb34..f3109e987fa 100644 --- a/src/bidderSettings.ts +++ b/src/bidderSettings.ts @@ -119,7 +119,7 @@ export class ScopedSettings, SCOPED extends * @returns all settings in the given scope, merged with the settings for the default scope. */ settingsFor(scope): SETTINGS { - return mergeDeep({}, this.ownSettingsFor(null), this.ownSettingsFor(scope)) as SETTINGS; + return mergeDeep({}, this.ownSettingsFor(null), this.ownSettingsFor(scope)); } /** diff --git a/src/utils.ts b/src/utils.ts index 0e63c9f1e05..639ad45204e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -68,9 +68,9 @@ export function getUniqueIdentifierStr(): string { * and y is replaced with a random hexadecimal digit from 8 to b. * https://gist.github.com/jed/982883 via node-uuid */ -export function generateUUID(placeholder?: any): string { +export function generateUUID(placeholder?: number | string): string { return placeholder - ? (placeholder ^ _getRandomData() >> placeholder / 4).toString(16) + ? (Number(placeholder) ^ _getRandomData() >> Number(placeholder) / 4).toString(16) : ('' + [1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID); } @@ -282,7 +282,7 @@ export function isEmptyStr(str: unknown): boolean { * @param {Function} fn - The function to execute for each element. It receives three arguments: value, key, and the original object. * @returns {void} */ -export function _each(object: any[] | Record, fn: (value: any, key: any) => void): void { +export function _each(object: T[] | Record, fn: (value: T, key: any) => void): void { if (isFn(object?.forEach)) return object.forEach(fn, this); Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k)); } @@ -298,7 +298,7 @@ export function contains(a: unknown, obj: unknown): boolean { * @param {Function} callback - The function to execute for each element. It receives three arguments: value, key, and the original object. * @return {Array} */ -export function _map(object: any[] | Record, callback: (value: any, key: any, obj?: any) => any): any[] { +export function _map(object: T[] | Record, callback: (value: T, key: any, obj?: T[] | Record) => R): R[] { if (isFn(object?.map)) return object.map(callback); return Object.entries(object || {}).map(([k, v]) => callback(v, k, object)); } @@ -687,12 +687,12 @@ export function checkCookieSupport(): boolean | undefined { * @param {number} numRequiredCalls The number of times which the returned function needs to be called before * func is. */ -export function delayExecution(func: (...args: any[]) => void, numRequiredCalls: number): (...args: any[]) => void { +export function delayExecution(func: (...args: A) => void, numRequiredCalls: number): (...args: A) => void { if (numRequiredCalls < 1) { throw new Error(`numRequiredCalls must be a positive number. Got ${numRequiredCalls}`); } let numCalls = 0; - return function (...args: any[]): void { + return function (this: unknown, ...args: A): void { numCalls++; if (numCalls === numRequiredCalls) { func.apply(this, args); @@ -706,9 +706,9 @@ export function delayExecution(func: (...args: any[]) => void, numRequiredCalls: * @param {string} key * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}} */ -export function groupBy(xs: any[], key: string): Record { - return xs.reduce(function(rv: Record, x) { - const k = String(x[key]); +export function groupBy(xs: T[], key: string): Record { + return xs.reduce(function(rv: Record, x) { + const k = String((x as Record)[key]); (rv[k] = rv[k] || []).push(x); return rv; }, {}); @@ -1126,7 +1126,7 @@ export function setScriptAttributes(script: HTMLScriptElement, attributes: Recor * if the element is not found, return the index of the first element that's greater; * if no greater element exists, return `arr.length`) */ -export function binarySearch(arr: T[], el: T, key: (el: T) => any = (el) => el): number { +export function binarySearch(arr: T[], el: T, key: (el: T) => unknown = (el) => el): number { let left = 0; let right = arr.length && arr.length - 1; const target = key(el); @@ -1152,7 +1152,7 @@ export function binarySearch(arr: T[], el: T, key: (el: T) => any = (el) => e * @param {Set} checkedObjects - A set of properties that have already been checked. * @returns {boolean} - Returns true if the object has non-serializable properties, false otherwise. */ -export function hasNonSerializableProperty(obj: Record, checkedObjects: Set = new Set()): boolean { +export function hasNonSerializableProperty(obj: Record, checkedObjects: Set = new Set()): boolean { for (const key in obj) { const value = obj[key]; const type = typeof value; @@ -1190,8 +1190,8 @@ export function hasNonSerializableProperty(obj: Record, checkedObje * @param {String} key - Key of nested property. * @returns {any|undefined} - Value of nested property. */ -export function setOnAny(collection: any[], key: string): unknown { - for (let i = 0, result: any; i < collection.length; i++) { +export function setOnAny(collection: unknown[], key: string): unknown { + for (let i = 0, result: unknown; i < collection.length; i++) { result = deepAccess(collection[i], key); if (result) { return result; diff --git a/src/utils/cachedApiWrapper.ts b/src/utils/cachedApiWrapper.ts index ab4e71f2c4c..83f28a9ad80 100644 --- a/src/utils/cachedApiWrapper.ts +++ b/src/utils/cachedApiWrapper.ts @@ -5,8 +5,8 @@ export type CachedApiProps = { [key: string]: boolean | CachedApiProps }; * read through to the target once, and serve the cached value until `reset()` is called. */ export function CachedApiWrapper(this: { obj: T; reset: () => void }, getTarget: () => any, props: CachedApiProps) { - const wrapper: Record = {}; - let data: Record = {}; + const wrapper: Record = {}; + let data: Record = {}; const children: Array<() => void> = []; Object.entries(props).forEach(([key, value]) => { if (value != null && typeof value === 'object') { diff --git a/src/utils/gdpr.ts b/src/utils/gdpr.ts index 3ffa8fabdef..3cce0663ed8 100644 --- a/src/utils/gdpr.ts +++ b/src/utils/gdpr.ts @@ -4,7 +4,7 @@ * @param gdprConsent GDPR consent data * @returns {boolean} true if the gdprConsent is null-y; or GDPR does not apply; or if purpose 1 consent was given. */ -export function hasPurpose1Consent(gdprConsent?: { gdprApplies?: boolean; vendorData?: any } | null): boolean { +export function hasPurpose1Consent(gdprConsent?: { gdprApplies?: boolean; vendorData?: { purpose?: { consents?: Record } } } | null): boolean { if (gdprConsent?.gdprApplies) { return gdprConsent?.vendorData?.purpose?.consents?.[1] === true; } diff --git a/src/utils/reducers.ts b/src/utils/reducers.ts index 5ff3d740f1a..8ae395e57fa 100644 --- a/src/utils/reducers.ts +++ b/src/utils/reducers.ts @@ -1,3 +1,4 @@ +import type { Bid } from '../bidfactory.ts'; import { DEFAULT_PRIORITY, PRIORITY_INCREMENT } from './desirability.js'; export type Compare = (a: T, b: T) => number; @@ -33,11 +34,11 @@ export function maximum(compare: Compare = simpleCompare): (min: T, item: return minimum(reverseCompare(compare)); } -const cpmCompare = keyCompare((bid) => bid.cpm); -const timestampCompare = keyCompare((bid) => bid.responseTimestamp); +const cpmCompare = keyCompare((bid) => bid.cpm); +const timestampCompare = keyCompare((bid) => bid.responseTimestamp); // This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond -export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))); +export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))); export function priorityCompare(left: any, right: any): number { const a = left.priority ?? DEFAULT_PRIORITY; @@ -56,7 +57,7 @@ export function dealsFirst(a: any, b: any): number { return 0; } -export const bidDesirabilityCompare = tiebreakCompare(priorityCompare, keyCompare((bid) => bid.desirability ?? bid.cpm)); +export const bidDesirabilityCompare = tiebreakCompare(priorityCompare, keyCompare((bid) => bid.desirability ?? bid.cpm)); // Sort comparator that orders bids by `.desirability` (falling back to cpm), highest first. export const highestDesirabilityFirst = reverseCompare(bidDesirabilityCompare); @@ -64,7 +65,7 @@ export const highestDesirabilityFirst = reverseCompare(bidDesirabilityCompare); // This function will get highest bid by `.desirability` (publisher `bidDesirabilityAdjustment` when set, else mirrors cpm via `adjustBids`); // ties use lowest `timeToRespond`, matching {@link getHighestCpm}. export const getHighestDesirability = maximum( - tiebreakCompare(bidDesirabilityCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond))) + tiebreakCompare(bidDesirabilityCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond))) ); // This function will get the oldest highest cpm value bid, in case of tie it will return the bid which came in first From bd973ad0d9235e5122755a1712c3ed3594ade7f5 Mon Sep 17 00:00:00 2001 From: derdeka Date: Mon, 9 Nov 2026 13:46:32 +0100 Subject: [PATCH 07/10] Core: convert src/utils.js to typescript --- src/utils/cachedApiWrapper.ts | 66 ++++++++++++++++++++++------------- src/utils/winDimensions.ts | 2 +- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/utils/cachedApiWrapper.ts b/src/utils/cachedApiWrapper.ts index 83f28a9ad80..e5e329f3275 100644 --- a/src/utils/cachedApiWrapper.ts +++ b/src/utils/cachedApiWrapper.ts @@ -1,32 +1,48 @@ -export type CachedApiProps = { [key: string]: boolean | CachedApiProps }; +/** + * Selects the properties of `Target` to cache: `true` for a value, a nested object for a child object. + */ +export type CachedApiProps = { + [K in keyof Target]?: boolean | (NonNullable extends object ? CachedApiProps> : never); +}; + +/** + * A value that has a subset of the properties of `Target`, recursively. + */ +type Subset = { [K in keyof Target]?: Subset> }; /** * Wraps the properties of the object returned by `getTarget` flagged in `props` with getters that * read through to the target once, and serve the cached value until `reset()` is called. */ -export function CachedApiWrapper(this: { obj: T; reset: () => void }, getTarget: () => any, props: CachedApiProps) { - const wrapper: Record = {}; - let data: Record = {}; - const children: Array<() => void> = []; - Object.entries(props).forEach(([key, value]) => { - if (value != null && typeof value === 'object') { - const child = new (CachedApiWrapper as any)(() => getTarget()?.[key], value); - wrapper[key] = child.obj; - children.push(child.reset); - } else if (value === true) { - Object.defineProperty(wrapper, key, { - get() { - if (!data.hasOwnProperty(key)) { - data[key] = getTarget()?.[key]; +export class CachedApiWrapper & Record, never>> { + readonly obj: T; + readonly reset: () => void; + + constructor(getTarget: () => Target, props: CachedApiProps) { + const read = (key: string): unknown => (getTarget() as Record | undefined)?.[key]; + const wrapper: Record = {}; + let data: Record = {}; + const children: Array<() => void> = []; + Object.entries(props).forEach(([key, value]) => { + if (value != null && typeof value === 'object') { + const child = new CachedApiWrapper>(() => read(key), value as CachedApiProps); + wrapper[key] = child.obj; + children.push(child.reset); + } else if (value === true) { + Object.defineProperty(wrapper, key, { + get() { + if (!data.hasOwnProperty(key)) { + data[key] = read(key); + } + return data[key]; } - return data[key]; - } - }); - } - }); - this.obj = wrapper as T; - this.reset = function () { - children.forEach(reset => reset()); - data = {}; - }; + }); + } + }); + this.obj = wrapper as T; + this.reset = () => { + children.forEach(reset => reset()); + data = {}; + }; + } } diff --git a/src/utils/winDimensions.ts b/src/utils/winDimensions.ts index 34716767fa5..c824da9dfc1 100644 --- a/src/utils/winDimensions.ts +++ b/src/utils/winDimensions.ts @@ -15,7 +15,7 @@ export type WinDimensions = { }; }; -const winDimensions: { obj: WinDimensions; reset: () => void } = new (CachedApiWrapper as any)( +const winDimensions = new CachedApiWrapper( () => { // called for every value read into the cache, including through an object obtained from // getWinDimensions() in an earlier epoch, so that each value is discarded when the epoch it was read in ends From 717cd2a3e4d1ac12dc606010051648f4c2528d39 Mon Sep 17 00:00:00 2001 From: derdeka Date: Mon, 9 Nov 2026 14:28:55 +0100 Subject: [PATCH 08/10] Core: convert src/utils.js to typescript --- src/utils/objects.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/utils/objects.ts b/src/utils/objects.ts index 463ca50114d..e2d58b22179 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -25,26 +25,26 @@ const toString = Object.prototype.toString; * Return if the object is of the * given type. */ -export function isFn(object: unknown): object is AnyFunction { - return typeof object === 'function'; +export function isFn(value: unknown): value is AnyFunction { + return typeof value === 'function'; } -export function isStr(object: unknown): object is string { - return typeof object === 'string'; +export function isStr(value: unknown): value is string { + return typeof value === 'string'; } -export const isArray: (object: unknown) => object is any[] = Array.isArray.bind(Array); +export const isArray: (value: unknown) => value is any[] = Array.isArray.bind(Array); -export function isNumber(object: unknown): object is number { - return typeof object === 'number'; +export function isNumber(value: unknown): value is number { + return typeof value === 'number'; } -export function isPlainObject(object: unknown): object is Record { - return toString.call(object) === '[object Object]'; +export function isPlainObject(value: unknown): value is Record { + return toString.call(value) === '[object Object]'; } -export function isBoolean(object: unknown): object is boolean { - return typeof object === 'boolean'; +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; } /** From a092ebaae6fa6cbd4bfc6c6883e10272266b7e06 Mon Sep 17 00:00:00 2001 From: derdeka Date: Mon, 9 Nov 2026 14:40:09 +0100 Subject: [PATCH 09/10] Core: convert src/utils.js to typescript --- modules/asterioBidAdapter.ts | 2 +- src/adapterManager.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/asterioBidAdapter.ts b/modules/asterioBidAdapter.ts index e5bd1d66141..941b4848234 100644 --- a/modules/asterioBidAdapter.ts +++ b/modules/asterioBidAdapter.ts @@ -44,7 +44,7 @@ type AsterioServerBid = { format?: string; adomain?: string[]; vastXml?: string; - native?: Record; + native?: AsterioNativeBid; meta?: Record; }; diff --git a/src/adapterManager.ts b/src/adapterManager.ts index aa67eb50b3e..274abd4f63b 100644 --- a/src/adapterManager.ts +++ b/src/adapterManager.ts @@ -440,13 +440,13 @@ export function getS2SBidderSet(s2sConfigs) { * @returns {Object} return.client - Array of bidder codes that should be routed to client adapters. * @returns {Object} return.server - Array of bidder codes that should be routed to server adapters. */ -export function _partitionBidders (adUnits, s2sConfigs, { getS2SBidders = getS2SBidderSet } = {}): { client: string[]; server: string[] } { +export function _partitionBidders (adUnits, s2sConfigs, { getS2SBidders = getS2SBidderSet } = {}) { const serverBidders = getS2SBidders(s2sConfigs); return getBidderCodes(adUnits).reduce((memo: { client: string[]; server: string[] }, bidder) => { const partition = serverBidders.has(bidder) ? PARTITIONS.SERVER : PARTITIONS.CLIENT; memo[partition].push(bidder); return memo; - }, { client: [], server: [] }); + }, { [PARTITIONS.CLIENT]: [], [PARTITIONS.SERVER]: [] }); } export const partitionBidders = hook('sync', _partitionBidders, 'partitionBidders'); From 88dca1e7fbf33e621f8e344477ad3314644945a2 Mon Sep 17 00:00:00 2001 From: derdeka Date: Mon, 9 Nov 2026 14:43:15 +0100 Subject: [PATCH 10/10] Core: convert src/utils.js to typescript --- src/adapterManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapterManager.ts b/src/adapterManager.ts index 274abd4f63b..48c824051b8 100644 --- a/src/adapterManager.ts +++ b/src/adapterManager.ts @@ -440,9 +440,9 @@ export function getS2SBidderSet(s2sConfigs) { * @returns {Object} return.client - Array of bidder codes that should be routed to client adapters. * @returns {Object} return.server - Array of bidder codes that should be routed to server adapters. */ -export function _partitionBidders (adUnits, s2sConfigs, { getS2SBidders = getS2SBidderSet } = {}) { +export function _partitionBidders (adUnits, s2sConfigs, { getS2SBidders = getS2SBidderSet } = {}): { [PARTITIONS.CLIENT]: string[], [PARTITIONS.SERVER]: string[] } { const serverBidders = getS2SBidders(s2sConfigs); - return getBidderCodes(adUnits).reduce((memo: { client: string[]; server: string[] }, bidder) => { + return getBidderCodes(adUnits).reduce((memo, bidder) => { const partition = serverBidders.has(bidder) ? PARTITIONS.SERVER : PARTITIONS.CLIENT; memo[partition].push(bidder); return memo;