From 3d5257376c6687fc79b4ae8fb90cdafaab037d04 Mon Sep 17 00:00:00 2001 From: dmlvr Date: Wed, 23 Sep 2026 12:17:56 +0300 Subject: [PATCH 1/2] Core: fix eslint/ts errors after rename version & browser version.ts Introduce the ComparableVersion type (string | number | (string | number)[]) that the QUnit suite and the six call sites already relied on, and move the argument normalization out of compare() into a module-level toParts() that returns number[]. The name avoids the Version interface that js/__internal/utils/version.ts already exports with an incompatible shape. A number argument goes through the same parseInt map as strings and arrays. Returning it early would keep its fractional part, which the old code always truncated - 4624 argument combinations run against the previous implementation show 726 differences without that map and none with it. compare(13.3, [13.3]) is the clearest: the same version written two ways has to stay equal. parseInt(x[i] || 0, 10) split into two distinct cases: inside toParts the `|| 0` is kept, because '1.'.split('.') yields an empty string that has to read as 0 and `??` would not catch it; the out-of-range lookup became xParts[i] ?? 0, where the only missing value is undefined. maxLevel got an explicit undefined check so the typed signature holds, but the finite test stays on the global isFinite. compare is re-exported from the deprecated yet still public core/utils/version module, where untyped JS callers may pass maxLevel as a numeric string; Number.isFinite would reject those and silently stop capping the comparison depth. browser.ts Type the detection result with Browser and BrowserName, both built on the BrowserInfo that the public js/core/utils/browser.d.ts already declares. Importing that type instead of restating it keeps one source of truth; the import is type-only, so it is erased and adds no runtime cycle with the shim. One behaviour difference, on malformed input only: when the inner version regexes miss, browserVersion is undefined rather than null, and extend then drops the key from the singleton entirely. browser.d.ts declares version?: string, so null was never a value the type allowed. All 34 real-world user agents in the suite are unaffected. The `exec() || cond && exec() || []` chain became a `??` chain, and `browserVersion && browserVersion[1]` became `exec(...)?.[1]`. Typing `ua` as string also made prefer-includes fire, so indexOf(x) >= 0 is now includes(x). extend() is still used to build the singleton, so the undefined-valued keys it skips keep being skipped; it is typed in its own block later. widget.ts devices.real().version is number[] | undefined, which the newly typed compare() rejects. Pass `version ?? []`. The rule body only runs on iOS, where version is always an array, so runtime behaviour is unchanged - previously an undefined would have thrown on x.length. Both paths get the test they lacked: a fractional number argument, and a user agent whose version regex misses. Verified: eslint clean on all three files, build:ts:internal green, and the QUnit suites utils.version.tests.js (16/16) and utils.browser.tests.js (29/29) pass. Reverting either fix turns the matching test red. Co-Authored-By: Claude Opus 5 --- .../js/__internal/core/utils/m_browser.ts | 45 ++++++++++--------- .../js/__internal/core/utils/m_version.ts | 37 ++++++++------- .../js/__internal/core/widget/widget.ts | 2 +- .../DevExpress.core/utils.browser.tests.js | 10 ++++- .../DevExpress.core/utils.version.tests.js | 11 +++++ 5 files changed, 65 insertions(+), 40 deletions(-) diff --git a/packages/devextreme/js/__internal/core/utils/m_browser.ts b/packages/devextreme/js/__internal/core/utils/m_browser.ts index 185f2d95c47e..bb4720a1f979 100644 --- a/packages/devextreme/js/__internal/core/utils/m_browser.ts +++ b/packages/devextreme/js/__internal/core/utils/m_browser.ts @@ -1,42 +1,43 @@ +import type { BrowserInfo } from '@js/core/utils/browser'; import { extend } from '@js/core/utils/extend'; import { getNavigator } from '@js/core/utils/window'; +export type BrowserName = Exclude; + +export type Browser = BrowserInfo & { + _fromUA: (userAgent: string) => BrowserInfo; +}; + const navigator = getNavigator(); const webkitRegExp = /(webkit)[ /]([\w.]+)/; const mozillaRegExp = /(mozilla)(?:.*? rv:([\w.]+))/; -const browserFromUA = (ua) => { - ua = ua.toLowerCase(); +const browserFromUA = (userAgent: string): BrowserInfo => { + const ua = userAgent.toLowerCase(); - const result: any = {}; + const result: BrowserInfo = {}; const matches = webkitRegExp.exec(ua) - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - || ua.indexOf('compatible') < 0 && mozillaRegExp.exec(ua) - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - || []; - let browserName = matches[1]; - let browserVersion: any = matches[2]; + ?? (!ua.includes('compatible') ? mozillaRegExp.exec(ua) : null) + ?? []; + let browserName = matches[1] as BrowserName | undefined; + let browserVersion = matches[2] as string | undefined; if (browserName === 'webkit') { result.webkit = true; - if (ua.indexOf('chrome') >= 0 || ua.indexOf('crios') >= 0) { + if (ua.includes('chrome') || ua.includes('crios')) { browserName = 'chrome'; - browserVersion = /(?:chrome|crios)\/(\d+\.\d+)/.exec(ua); - browserVersion = browserVersion && browserVersion[1]; - } else if (ua.indexOf('fxios') >= 0) { + browserVersion = /(?:chrome|crios)\/(\d+\.\d+)/.exec(ua)?.[1]; + } else if (ua.includes('fxios')) { browserName = 'mozilla'; - browserVersion = /fxios\/(\d+\.\d+)/.exec(ua); - browserVersion = browserVersion && browserVersion[1]; - } else if (ua.indexOf('safari') >= 0 && /version|phantomjs/.test(ua)) { + browserVersion = /fxios\/(\d+\.\d+)/.exec(ua)?.[1]; + } else if (ua.includes('safari') && /version|phantomjs/.test(ua)) { browserName = 'safari'; - browserVersion = /(?:version|phantomjs)\/([0-9.]+)/.exec(ua); - browserVersion = browserVersion && browserVersion[1]; + browserVersion = /(?:version|phantomjs)\/([0-9.]+)/.exec(ua)?.[1]; } else { browserName = 'unknown'; - browserVersion = /applewebkit\/([0-9.]+)/.exec(ua); - browserVersion = browserVersion && browserVersion[1]; + browserVersion = /applewebkit\/([0-9.]+)/.exec(ua)?.[1]; } } @@ -47,5 +48,7 @@ const browserFromUA = (ua) => { return result; }; -const browser = extend({ _fromUA: browserFromUA }, browserFromUA(navigator.userAgent)); + +const browser: Browser = extend({ _fromUA: browserFromUA }, browserFromUA(navigator.userAgent)); + export { browser }; diff --git a/packages/devextreme/js/__internal/core/utils/m_version.ts b/packages/devextreme/js/__internal/core/utils/m_version.ts index df53961d1a65..886e13eece07 100644 --- a/packages/devextreme/js/__internal/core/utils/m_version.ts +++ b/packages/devextreme/js/__internal/core/utils/m_version.ts @@ -1,26 +1,29 @@ -export function compare(x, y, maxLevel?) { - function normalizeArg(value) { - if (typeof value === 'string') { - return value.split('.'); - } - if (typeof value === 'number') { - return [value]; - } - return value; - } +export type ComparableVersion = string | number | (string | number)[]; + +function toParts(value: ComparableVersion): number[] { + const source = typeof value === 'number' ? [value] : value; + const parts = typeof source === 'string' ? source.split('.') : source; + + return parts.map((part) => parseInt(String(part || 0), 10)); +} - x = normalizeArg(x); - y = normalizeArg(y); +export function compare( + x: ComparableVersion, + y: ComparableVersion, + maxLevel?: number, +): number { + const xParts = toParts(x); + const yParts = toParts(y); - let length = Math.max(x.length, y.length); + let length = Math.max(xParts.length, yParts.length); - if (isFinite(maxLevel)) { + if (maxLevel !== undefined && isFinite(maxLevel)) { length = Math.min(length, maxLevel); } - for (let i = 0; i < length; i++) { - const xItem = parseInt(x[i] || 0, 10); - const yItem = parseInt(y[i] || 0, 10); + for (let i = 0; i < length; i += 1) { + const xItem = xParts[i] ?? 0; + const yItem = yParts[i] ?? 0; if (xItem < yItem) { return -1; diff --git a/packages/devextreme/js/__internal/core/widget/widget.ts b/packages/devextreme/js/__internal/core/widget/widget.ts index 577eeb992f38..24fff81b1743 100644 --- a/packages/devextreme/js/__internal/core/widget/widget.ts +++ b/packages/devextreme/js/__internal/core/widget/widget.ts @@ -151,7 +151,7 @@ class Widget< const device = devices.real(); const { platform } = device; const { version } = device; - return platform === 'ios' && compareVersions(version, '13.3') <= 0; + return platform === 'ios' && compareVersions(version ?? [], '13.3') <= 0; }, options: { useResizeObserver: false, diff --git a/packages/devextreme/testing/tests/DevExpress.core/utils.browser.tests.js b/packages/devextreme/testing/tests/DevExpress.core/utils.browser.tests.js index 73f57f1e2e9e..3a6d86bf9c6a 100644 --- a/packages/devextreme/testing/tests/DevExpress.core/utils.browser.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.core/utils.browser.tests.js @@ -9,7 +9,8 @@ const userAgents = { chrome_ios: 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/74.0.3729.157 Mobile/13B143 Safari/601.1.46', mozilla_ios: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/18.2b15817 Mobile/15E148 Safari/605.1.15', phantom: 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1', - google_app_ios: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/88.0.281793270 Mobile/15E148 Safari/604.1' + google_app_ios: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/88.0.281793270 Mobile/15E148 Safari/604.1', + chrome_without_version: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36' }; QUnit.module('browser'); @@ -69,6 +70,13 @@ QUnit.test('browser is mozilla (mobile)', function(assert) { assert.equal(browserObject.version, '18.2', 'version was detect correctly'); }); +QUnit.test('version is undefined when the inner regexp does not match', function(assert) { + const browserObject = browser._fromUA(userAgents.chrome_without_version); + + assert.ok(browserObject.chrome, 'chrome detected'); + assert.strictEqual(browserObject.version, undefined, 'version is undefined, as browser.d.ts declares it optional'); +}); + QUnit.test('google app is chrome (mobile)', function(assert) { const browserObject = browser._fromUA(userAgents.google_app_ios); diff --git a/packages/devextreme/testing/tests/DevExpress.core/utils.version.tests.js b/packages/devextreme/testing/tests/DevExpress.core/utils.version.tests.js index 7d92557fef71..d70c2c7a117c 100644 --- a/packages/devextreme/testing/tests/DevExpress.core/utils.version.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.core/utils.version.tests.js @@ -18,3 +18,14 @@ QUnit.test('compareVersions', function(assert) { assert.equal(compare('1.10.0', [1, 10]), 0); assert.equal(compare('1.11.3', [1, 10]), 1); }); + +QUnit.test('compareVersions truncates a fractional number argument', function(assert) { + assert.equal(compare(13.3, [13.3]), 0, 'the same version written two ways is equal'); + assert.equal(compare(13.3, 13), 0); + assert.equal(compare(13, 13.3), 0); + + assert.equal(compare(2.5, '2.0.1'), -1); + assert.equal(compare([2, 0], 2.5), 0); + + assert.equal(compare(0.5, 0), 0); +}); From 0e85d5214176ba1530311c04d1b57ab75c3acacf Mon Sep 17 00:00:00 2001 From: dmlvr Date: Mon, 21 Sep 2026 20:22:53 +0300 Subject: [PATCH 2/2] Core: remove m_ prefix from version & browser files Rename js/__internal/core/utils/m_version.ts and m_browser.ts, updating the two public re-export shims and the six jQuery/Knockout integration imports. No content change - the rename is kept as its own commit so git and GitHub preserve the file history. The renamed files now fall under the strict eslint ruleset, so this commit alone does not lint clean; the follow-up commit fixes that. Committed with --no-verify for that reason. Co-Authored-By: Claude Opus 5 --- .../js/__internal/core/utils/{m_browser.ts => browser.ts} | 0 .../js/__internal/core/utils/{m_version.ts => version.ts} | 0 packages/devextreme/js/__internal/integration/jquery.ts | 2 +- .../devextreme/js/__internal/integration/jquery/deferred.ts | 2 +- packages/devextreme/js/__internal/integration/jquery/hooks.ts | 2 +- packages/devextreme/js/__internal/integration/knockout.ts | 2 +- .../devextreme/js/__internal/integration/knockout/clean_node.ts | 2 +- .../js/__internal/integration/knockout/clean_node_old.ts | 2 +- packages/devextreme/js/core/utils/browser.js | 2 +- packages/devextreme/js/core/utils/version.js | 2 +- 10 files changed, 8 insertions(+), 8 deletions(-) rename packages/devextreme/js/__internal/core/utils/{m_browser.ts => browser.ts} (100%) rename packages/devextreme/js/__internal/core/utils/{m_version.ts => version.ts} (100%) diff --git a/packages/devextreme/js/__internal/core/utils/m_browser.ts b/packages/devextreme/js/__internal/core/utils/browser.ts similarity index 100% rename from packages/devextreme/js/__internal/core/utils/m_browser.ts rename to packages/devextreme/js/__internal/core/utils/browser.ts diff --git a/packages/devextreme/js/__internal/core/utils/m_version.ts b/packages/devextreme/js/__internal/core/utils/version.ts similarity index 100% rename from packages/devextreme/js/__internal/core/utils/m_version.ts rename to packages/devextreme/js/__internal/core/utils/version.ts diff --git a/packages/devextreme/js/__internal/integration/jquery.ts b/packages/devextreme/js/__internal/integration/jquery.ts index 95588c2628c5..d43ff3399ed7 100644 --- a/packages/devextreme/js/__internal/integration/jquery.ts +++ b/packages/devextreme/js/__internal/integration/jquery.ts @@ -1,6 +1,6 @@ /* eslint-disable import/first */ import errors from '@ts/core/utils/m_error'; -import { compare as compareVersions } from '@ts/core/utils/m_version'; +import { compare as compareVersions } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import jQuery from 'jquery'; diff --git a/packages/devextreme/js/__internal/integration/jquery/deferred.ts b/packages/devextreme/js/__internal/integration/jquery/deferred.ts index b0e3d85322fa..b9f0feceac21 100644 --- a/packages/devextreme/js/__internal/integration/jquery/deferred.ts +++ b/packages/devextreme/js/__internal/integration/jquery/deferred.ts @@ -1,6 +1,6 @@ import type { DeferredObj } from '@js/core/utils/deferred'; import { setStrategy } from '@ts/core/utils/m_deferred'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import jQuery from 'jquery'; diff --git a/packages/devextreme/js/__internal/integration/jquery/hooks.ts b/packages/devextreme/js/__internal/integration/jquery/hooks.ts index d5333716262a..e593a8dc74f0 100644 --- a/packages/devextreme/js/__internal/integration/jquery/hooks.ts +++ b/packages/devextreme/js/__internal/integration/jquery/hooks.ts @@ -1,6 +1,6 @@ import { each } from '@ts/core/utils/m_iterator'; import { isNumeric } from '@ts/core/utils/m_type'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; import registerEvent from '@ts/events/core/event_registrator'; import hookTouchProps from '@ts/events/core/hook_touch_props'; import { setEventFixMethod } from '@ts/events/utils/index'; diff --git a/packages/devextreme/js/__internal/integration/knockout.ts b/packages/devextreme/js/__internal/integration/knockout.ts index dc751718986e..4d1f3d3ea267 100644 --- a/packages/devextreme/js/__internal/integration/knockout.ts +++ b/packages/devextreme/js/__internal/integration/knockout.ts @@ -1,6 +1,6 @@ /* eslint-disable import/first */ import errors from '@ts/core/utils/m_error'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import ko from 'knockout'; diff --git a/packages/devextreme/js/__internal/integration/knockout/clean_node.ts b/packages/devextreme/js/__internal/integration/knockout/clean_node.ts index ad94487dcdcd..7daf29a23376 100644 --- a/packages/devextreme/js/__internal/integration/knockout/clean_node.ts +++ b/packages/devextreme/js/__internal/integration/knockout/clean_node.ts @@ -1,5 +1,5 @@ import { afterCleanData, cleanData, strategyChanging } from '@ts/core/element_data'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import ko from 'knockout'; diff --git a/packages/devextreme/js/__internal/integration/knockout/clean_node_old.ts b/packages/devextreme/js/__internal/integration/knockout/clean_node_old.ts index 6f59702285ec..69ece80af84e 100644 --- a/packages/devextreme/js/__internal/integration/knockout/clean_node_old.ts +++ b/packages/devextreme/js/__internal/integration/knockout/clean_node_old.ts @@ -1,6 +1,6 @@ /* eslint-disable func-names */ import { strategyChanging } from '@ts/core/element_data'; -import { compare as compareVersion } from '@ts/core/utils/m_version'; +import { compare as compareVersion } from '@ts/core/utils/version'; // eslint-disable-next-line import/no-extraneous-dependencies import ko from 'knockout'; diff --git a/packages/devextreme/js/core/utils/browser.js b/packages/devextreme/js/core/utils/browser.js index f61e0a712a42..ce2b6d020d17 100644 --- a/packages/devextreme/js/core/utils/browser.js +++ b/packages/devextreme/js/core/utils/browser.js @@ -1,3 +1,3 @@ // deprecated -import { browser } from '../../__internal/core/utils/m_browser'; +import { browser } from '../../__internal/core/utils/browser'; export default browser; diff --git a/packages/devextreme/js/core/utils/version.js b/packages/devextreme/js/core/utils/version.js index 491cc6781004..9ed24fdd9598 100644 --- a/packages/devextreme/js/core/utils/version.js +++ b/packages/devextreme/js/core/utils/version.js @@ -1,2 +1,2 @@ // deprecated -export { compare } from '../../__internal/core/utils/m_version'; +export { compare } from '../../__internal/core/utils/version';