diff --git a/lib/constants.ts b/lib/constants.ts index 7773fd59..0dd67afa 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -11,6 +11,7 @@ import path from "node:path"; import fs from "node:fs"; export const BASE_DIR = new URL("..", import.meta.url); +export const RUNTIME_IDS_WITH_PATCH_VERSIONING = new Set(["bun"]); // List of browsers/runtimes that might add features in patches /** * Tests a specified path to see if it's a local checkout of mdn/browser-compat-data diff --git a/lib/ua-parser.test.ts b/lib/ua-parser.test.ts index 9ca4625b..97d85e81 100644 --- a/lib/ua-parser.test.ts +++ b/lib/ua-parser.test.ts @@ -11,6 +11,7 @@ import {assert} from "chai"; import {getMajorMinorVersion, parseUA} from "./ua-parser.js"; const browsers = { + bun: {name: "Bun", releases: {"1.0.0": {}, "1.1.15": {}, "1.2.0": {}}}, chrome: {name: "Chrome", releases: {82: {}, 83: {}, 84: {}, 85: {}}}, chrome_android: {name: "Chrome Android", releases: {85: {}}}, deno: {name: "Deno", releases: {1.42: {}}}, @@ -522,6 +523,46 @@ describe("parseUA", () => { }); }); + it("Bun 1.0.0 (preserves patch version)", () => { + assert.deepEqual(parseUA("!! bun/1.0.0", browsers), { + browser: {id: "bun", name: "Bun"}, + version: "1.0.0", + fullVersion: "1.0.0", + os: {name: "", version: ""}, + inBcd: true, + }); + }); + + it("Bun 1.1.15 (preserves patch version)", () => { + assert.deepEqual(parseUA("!! bun/1.1.15", browsers), { + browser: {id: "bun", name: "Bun"}, + version: "1.1.15", + fullVersion: "1.1.15", + os: {name: "", version: ""}, + inBcd: true, + }); + }); + + it("Bun 1.2.0 (preserves patch version)", () => { + assert.deepEqual(parseUA("!! bun/1.2.0", browsers), { + browser: {id: "bun", name: "Bun"}, + version: "1.2.0", + fullVersion: "1.2.0", + os: {name: "", version: ""}, + inBcd: true, + }); + }); + + it("Bun 1.1.16 (not in BCD)", () => { + assert.deepEqual(parseUA("!! bun/1.1.16", browsers), { + browser: {id: "bun", name: "Bun"}, + version: "1.1.16", + fullVersion: "1.1.16", + os: {name: "", version: ""}, + inBcd: false, + }); + }); + it("Chrome on iOS (not in BCD)", () => { assert.deepEqual( parseUA( diff --git a/lib/ua-parser.ts b/lib/ua-parser.ts index f2c69ae1..0f08f402 100644 --- a/lib/ua-parser.ts +++ b/lib/ua-parser.ts @@ -13,6 +13,7 @@ import { } from "compare-versions"; import {UAParser} from "ua-parser-js"; import {ParsedUserAgent} from "../types/types"; +import {RUNTIME_IDS_WITH_PATCH_VERSIONING} from "./constants"; /** * Returns the major version from a given version string. @@ -110,7 +111,12 @@ const parseUA = (userAgent: string, browsers: Browsers): ParsedUserAgent => { } data.fullVersion = data.fullVersion || ua.browser.version || "0"; - data.version = getMajorMinorVersion(data.fullVersion); + + if (RUNTIME_IDS_WITH_PATCH_VERSIONING.has(data.browser.id)) { + data.version = data.fullVersion; + } else { + data.version = getMajorMinorVersion(data.fullVersion); + } if (data.browser.id == "bun") { data.version = data.fullVersion; @@ -154,16 +160,24 @@ const parseUA = (userAgent: string, browsers: Browsers): ParsedUserAgent => { // with this, find the pair of versions in |versions| that sandwiches // |version|, and use the first of this pair. For example, given |version| // "10.1" and |versions| entries "10.0" and "10.2", return "10.0". - for (let i = 0; i < versions.length - 1; i++) { - const current = versions[i]; - const next = versions[i + 1]; - if ( - compareVersions(data.version, current, ">=") && - compareVersions(data.version, next, "<") - ) { + // However, for Bun, we need exact version matches because patch versions can add features. + if (RUNTIME_IDS_WITH_PATCH_VERSIONING.has(data.browser.id)) { + // For Bun, only mark as inBcd if exact version exists + if (versions.includes(data.version)) { data.inBcd = true; - data.version = current; - break; + } + } else { + for (let i = 0; i < versions.length - 1; i++) { + const current = versions[i]; + const next = versions[i + 1]; + if ( + compareVersions(data.version, current, ">=") && + compareVersions(data.version, next, "<") + ) { + data.inBcd = true; + data.version = current; + break; + } } } @@ -171,19 +185,21 @@ const parseUA = (userAgent: string, browsers: Browsers): ParsedUserAgent => { // we have to check if it looks like a significant release or not. By default // that means a new major version, but for Safari and Samsung Internet the // major and minor version are significant. - let normalize = getMajorVersion; - if ( - data.browser.id.startsWith("safari") || - data.browser.id === "samsunginternet_android" - ) { - normalize = getMajorMinorVersion; - } - if ( - data.inBcd == false && - normalize(data.version) === normalize(versions[versions.length - 1]) - ) { - data.inBcd = true; - data.version = versions[versions.length - 1]; + if (!RUNTIME_IDS_WITH_PATCH_VERSIONING.has(data.browser.id)) { + let normalize = getMajorVersion; + if ( + data.browser.id.startsWith("safari") || + data.browser.id === "samsunginternet_android" + ) { + normalize = getMajorMinorVersion; + } + if ( + data.inBcd == false && + normalize(data.version) === normalize(versions[versions.length - 1]) + ) { + data.inBcd = true; + data.version = versions[versions.length - 1]; + } } return data; diff --git a/scripts/find-missing-reports.ts b/scripts/find-missing-reports.ts index c53c85bf..acf96894 100644 --- a/scripts/find-missing-reports.ts +++ b/scripts/find-missing-reports.ts @@ -50,7 +50,7 @@ const generateReportMap = (filter: string) => { for (const browser of Object.keys(browsers)) { if ( filter !== "all" && - ["ie", "nodejs", "deno", "oculus"].includes(browser) + ["ie", "nodejs", "deno", "bun", "oculus"].includes(browser) ) { continue; }