diff --git a/README.md b/README.md index 0d2b72df7..c21595620 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,9 @@ npm install @mrhenry/polyfill-library --save ```javascript const polyfillLibrary = require('@mrhenry/polyfill-library'); -const UA = require('@financial-times/polyfill-useragent-normaliser'); const polyfillBundle = polyfillLibrary.getPolyfillString({ - ua: new UA('Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)'), + ua: yourUserAgentParser('Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)') minify: true, features: { 'es6': { flags: ['gated'] } @@ -82,6 +81,55 @@ Create a polyfill bundle. Returns a polyfill bundle as either a utf-8 ReadStream or as a Promise of a utf-8 String. +## User Agent normalizing + +Support data only exists for the most common browsers (e.g. Chrome, Firefox, Safari, ...). To map any user agent to a list of needed polyfills you need to normalize the user agent to one of the known browser families and their corresponding version. + +Since the `@financial-times/polyfill-useragent-normaliser` package was deprecated you might need to provide your own solution. + +A minimal UA normalizer needs to be able to parse a user agent string and return an object with these functions: + +```js +const semver = require('semver'); + +function parse(uaString) { + const parsed = yourParser(uaString); + + // Depending on your implementation this will differ: + const version = parsed.version; + const browser = parsed.browser; + + return { + isUnknown: () => { + // When the browser and/or version are unrecognized + return this.getFamily() === 'other'; + }, + getFamily: () => { + // Must be one of: + // - android + // - bb + // - chrome + // - edge + // - edge_mob + // - firefox + // - firefox_mob + // - ie + // - ie_mob + // - opera + // - op_mob + // - op_mini + // - safari + // - ios_saf + // - samsung_mob + return browser; + }, + satisfies: (range) => { + return semver.satisfies(this.getVersion(), range); + } + } +} +``` + ## Contributing Development of @mrhenry/polyfill-library happens on GitHub. Read below to learn how you can take part in contributing. diff --git a/package-lock.json b/package-lock.json index fca0bb9dd..ca53ce2e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "polyfills/**/*" ], "devDependencies": { - "@financial-times/polyfill-useragent-normaliser": "^2.0.1", "@iarna/toml": "^2.2.5", "apicache": "^1.6.3", "browserstack": "1.6.1", @@ -2109,28 +2108,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@financial-times/polyfill-useragent-normaliser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@financial-times/polyfill-useragent-normaliser/-/polyfill-useragent-normaliser-2.0.1.tgz", - "integrity": "sha512-I6zZJgy3cwM9ojo9NVNj5kNTkt96KIKhJQIPQ7vjROaDCpy+sTRQA2HLCjBFKgo0AzsqdMB43Tx0Szt/mf+00g==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "dependencies": { - "@financial-times/useragent_parser": "^1.6.1", - "semver": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@financial-times/useragent_parser": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@financial-times/useragent_parser/-/useragent_parser-1.6.3.tgz", - "integrity": "sha512-TlQiXt/vS5ZwY0V3salvlyQzIzMGZEyw9inmJA25A8heL2kBVENbToiEc64R6ETNf5YHa2lwnc2I7iNHP9SqeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.8.0.tgz", @@ -5759,9 +5736,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", "dependencies": { @@ -5784,7 +5761,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -5799,6 +5776,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -8729,9 +8710,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 30a727689..25f59a0da 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "/types" ], "devDependencies": { - "@financial-times/polyfill-useragent-normaliser": "^2.0.1", "@iarna/toml": "^2.2.5", "apicache": "^1.6.3", "browserstack": "1.6.1", diff --git a/tasks/buildsources/polyfill.js b/tasks/buildsources/polyfill.js index 21f785876..cbd720c19 100644 --- a/tasks/buildsources/polyfill.js +++ b/tasks/buildsources/polyfill.js @@ -3,16 +3,14 @@ const fs = require('node:fs'); const path = require('node:path'); const uglify = require('uglify-js'); -const UA = require('@financial-times/polyfill-useragent-normaliser'); const TOML = require('@iarna/toml'); const validateSource = require('./validate-source'); const semver = require('semver'); const osiApproved = require('./osi-approved'); -const uaBaselines = UA.getBaselines(); -delete uaBaselines.ios_chr; // https://github.com/Financial-Times/polyfill-library/issues/1202#issuecomment-1193403165 -const supportedBrowsers = Object.keys(uaBaselines).sort((a, b) => a.localeCompare(b)); +const configTemplate = fs.readFileSync(path.join(__dirname, '..', 'polyfill-templates', 'config.toml'), { encoding: 'utf-8' }); +const supportedBrowsers = Object.keys(TOML.parse(configTemplate).browsers).sort((a, b) => a.localeCompare(b)); /** * Polyfill represents a single polyfill directory. @@ -147,7 +145,7 @@ module.exports = class Polyfill { .then(data => { this.config = TOML.parse(data); - // Each internal polyfill needs to target all supported browsers at all versions. + // Each internal polyfill needs to target all default browsers at all versions. if (this.path.relative.startsWith('_') && !supportedBrowsers.every(browser => this.config.browsers[browser] === "*")) { const browserSupport = {}; for (const browser of supportedBrowsers) browserSupport[browser] = "*"; diff --git a/test/end-to-end/generating-bundles.test.js b/test/end-to-end/generating-bundles.test.js index 519595e2b..8f11d9f7a 100644 --- a/test/end-to-end/generating-bundles.test.js +++ b/test/end-to-end/generating-bundles.test.js @@ -4,7 +4,7 @@ const test = require('node:test'); const { describe, it } = test; const assert = require('node:assert'); -const UA = require("@financial-times/polyfill-useragent-normaliser"); +const ua_parser = require('../polyfills/ua-parser'); const polyfillLibrary = require('../..'); describe("polyfill-library", function () { @@ -13,7 +13,7 @@ describe("polyfill-library", function () { features: { all: {} }, - ua: new UA('other/0.0.0'), + ua: ua_parser('other/0.0.0'), unknown: 'polyfill' }); @@ -21,7 +21,7 @@ describe("polyfill-library", function () { features: { all: {} }, - ua: new UA('other/0.0.0'), + ua: ua_parser('other/0.0.0'), unknown: 'polyfill' }); @@ -44,7 +44,7 @@ describe("polyfill-library", function () { '__proto__': {}, 'toLocaleString': {}, }, - ua: new UA('other/0.0.0'), + ua: ua_parser('other/0.0.0'), unknown: 'polyfill' }); }); diff --git a/test/node/lib/index.test.js b/test/node/lib/index.test.js index 1eab8580e..0df043576 100644 --- a/test/node/lib/index.test.js +++ b/test/node/lib/index.test.js @@ -4,7 +4,7 @@ const test = require('node:test'); const { describe, it } = test; const assert = require('node:assert'); -const UA = require("@financial-times/polyfill-useragent-normaliser"); +const ua_parser = require('../../polyfills/ua-parser'); const appVersion = require("../../../package.json").version; @@ -16,7 +16,7 @@ describe(".getPolyfills(features)", async () => { features: { 'Promise': {} }, - ua: new UA('chrome/45') + ua: ua_parser('chrome/45') }; return polyfillio.getPolyfills(input).then(result => assert.deepEqual(result, {})); }); @@ -29,7 +29,7 @@ describe(".getPolyfills(features)", async () => { 'es7': {}, }, excludes: ['Array.prototype.values'], - ua: new UA('chrome/61') + ua: ua_parser('chrome/61') }; return polyfillio.getPolyfills(input).then(result => assert.deepEqual(result, { "Array.prototype.sort": { @@ -65,7 +65,7 @@ describe(".getPolyfills(features)", async () => { features: { 'Math.sign': {} }, - ua: new UA('') + ua: ua_parser('') }).then(result => assert.deepEqual(result, { 'Math.sign': { "flags": new Set(["gated"]), @@ -87,7 +87,7 @@ describe(".getPolyfills(features)", async () => { features: { 'Math.sign': {} }, - ua: new UA(''), + ua: ua_parser(''), unknown: 'ignore', }).then(result => assert.deepEqual(result, {})); }); @@ -98,7 +98,7 @@ describe(".getPolyfills(features)", async () => { 'Math.sign': {} }, unknown: 'polyfill', - ua: new UA('') + ua: ua_parser('') }).then(result => assert.deepEqual(result, { 'Math.sign': { "flags": new Set(["gated"]), @@ -145,7 +145,7 @@ describe(".getPolyfills(features)", async () => { flags: [] } }, - ua: new UA('ie/8') + ua: ua_parser('ie/8') }).then(result => assert(Object.keys(result).length > 0)); }); @@ -154,7 +154,7 @@ describe(".getPolyfills(features)", async () => { features: { "Math.fround": {} }, - ua: new UA("ie/9") + ua: ua_parser("ie/9") }); assert.deepEqual(noExcludes, { @@ -180,7 +180,7 @@ describe(".getPolyfills(features)", async () => { "Math.fround": {} }, excludes: ["ArrayBuffer", "non-existent-feature"], - ua: new UA("ie/9") + ua: ua_parser("ie/9") }); assert.deepEqual(excludes, { @@ -206,7 +206,7 @@ describe('.getPolyfillString', async () => { features: { default: {} }, - ua: new UA('chrome/30') + ua: ua_parser('chrome/30') }), polyfillio.getPolyfillString({ features: { @@ -214,7 +214,7 @@ describe('.getPolyfillString', async () => { flags: new Set(['gated']) } }, - ua: new UA('chrome/30') + ua: ua_parser('chrome/30') }) ]).then(results => { assert.notEqual(results[0], results[1]); @@ -231,14 +231,14 @@ describe('.getPolyfillString', async () => { features: { default: {} }, - ua: new UA('chrome/30'), + ua: ua_parser('chrome/30'), minify: false }), polyfillio.getPolyfillString({ features: { default: {} }, - ua: new UA('chrome/30'), + ua: ua_parser('chrome/30'), minify: true }) ]).then(results => { @@ -260,7 +260,7 @@ describe('.getPolyfillString', async () => { features: { default: {} }, - ua: new UA('chrome/30'), + ua: ua_parser('chrome/30'), stream: true, minify: false }); @@ -330,7 +330,7 @@ describe('.getPolyfillString', async () => { features: { default: {} }, - ua: new UA('ie/9'), + ua: ua_parser('ie/9'), stream: true, minify: false }); @@ -345,7 +345,7 @@ describe('.getPolyfillString', async () => { features: { default: {} }, - ua: new UA('ie/9'), + ua: ua_parser('ie/9'), stream: true, minify: false }); diff --git a/test/polyfills/browsers.toml b/test/polyfills/browsers.toml index 2e3998ce8..5a9417592 100644 --- a/test/polyfills/browsers.toml +++ b/test/polyfills/browsers.toml @@ -10,21 +10,21 @@ browsers = [ # "android/13.0", # "android/14.0", # "android/15.0", - "chrome/14.0", - "chrome/15.0", - "chrome/16.0", - "chrome/17.0", - "chrome/18.0", - "chrome/19.0", - "chrome/20.0", - "chrome/21.0", - "chrome/22.0", - "chrome/23.0", - "chrome/24.0", - "chrome/25.0", - "chrome/26.0", - "chrome/27.0", - "chrome/28.0", + # "chrome/14.0", + # "chrome/15.0", + # "chrome/16.0", + # "chrome/17.0", + # "chrome/18.0", + # "chrome/19.0", + # "chrome/20.0", + # "chrome/21.0", + # "chrome/22.0", + # "chrome/23.0", + # "chrome/24.0", + # "chrome/25.0", + # "chrome/26.0", + # "chrome/27.0", + # "chrome/28.0", "chrome/29.0", "chrome/30.0", "chrome/31.0", @@ -182,41 +182,41 @@ browsers = [ # "edge/129.0", "edge/130.0", # most recent edge at the time of writing # "edge/131.0 beta", - "firefox/3.6", - "firefox/4.0", - "firefox/5.0", - "firefox/6.0", - "firefox/7.0", - "firefox/8.0", - "firefox/9.0", - "firefox/10.0", - "firefox/11.0", - "firefox/12.0", - "firefox/13.0", - "firefox/14.0", - "firefox/15.0", - "firefox/16.0", - "firefox/17.0", - "firefox/18.0", - "firefox/19.0", - "firefox/20.0", - "firefox/21.0", - "firefox/22.0", - "firefox/23.0", - "firefox/24.0", - "firefox/25.0", - "firefox/26.0", - "firefox/27.0", - "firefox/28.0", - "firefox/29.0", - "firefox/30.0", - "firefox/31.0", - "firefox/32.0", - "firefox/33.0", - "firefox/34.0", - "firefox/35.0", - "firefox/36.0", - "firefox/37.0", + # "firefox/3.6", + # "firefox/4.0", + # "firefox/5.0", + # "firefox/6.0", + # "firefox/7.0", + # "firefox/8.0", + # "firefox/9.0", + # "firefox/10.0", + # "firefox/11.0", + # "firefox/12.0", + # "firefox/13.0", + # "firefox/14.0", + # "firefox/15.0", + # "firefox/16.0", + # "firefox/17.0", + # "firefox/18.0", + # "firefox/19.0", + # "firefox/20.0", + # "firefox/21.0", + # "firefox/22.0", + # "firefox/23.0", + # "firefox/24.0", + # "firefox/25.0", + # "firefox/26.0", + # "firefox/27.0", + # "firefox/28.0", + # "firefox/29.0", + # "firefox/30.0", + # "firefox/31.0", + # "firefox/32.0", + # "firefox/33.0", + # "firefox/34.0", + # "firefox/35.0", + # "firefox/36.0", + # "firefox/37.0", "firefox/38.0", "firefox/39.0", "firefox/40.0", @@ -313,9 +313,9 @@ browsers = [ "firefox/131.0", "firefox/132.0", # "firefox/133.0 beta", - "ie/6.0", - "ie/7.0", - "ie/8.0", + # "ie/6.0", + # "ie/7.0", + # "ie/8.0", "ie/9.0", "ie/10.0", "ie/11.0", @@ -330,11 +330,11 @@ browsers = [ "ios/18", "opera/12.15", "opera/12.16", - "safari/5.1", - "safari/6.0", - "safari/6.2", - "safari/7.1", - "safari/8.0", + # "safari/5.1", + # "safari/6.0", + # "safari/6.2", + # "safari/7.1", + # "safari/8.0", "safari/9.1", "safari/10.1", "safari/11.1", diff --git a/test/polyfills/remotetest.js b/test/polyfills/remotetest.js index 54df268ee..b3c6864f7 100644 --- a/test/polyfills/remotetest.js +++ b/test/polyfills/remotetest.js @@ -12,11 +12,10 @@ const promisify = require("node:util").promisify; const path = require("node:path"); const fs = require("node:fs"); const _ = require("lodash"); -const normalizeUserAgent = require('@financial-times/polyfill-useragent-normaliser').normalize; const TestJob = require("./test-job"); const Tunnel = require("browserstack-local").Local; const modifiedPolyfillsWithTests = require('../utils/modified-polyfills-with-tests').modifiedPolyfillsWithTests; -const UA = require("@financial-times/polyfill-useragent-normaliser"); +const ua_parser = require('./ua-parser'); const { URL } = require('node:url'); // Grab all the browsers from BrowserStack which are officially supported by the polyfill service. @@ -50,7 +49,8 @@ async function main() { .replace("browser=", "") .split("/"); - console.log({ browser, browserVersionRanges }); + console.log('browser:', browser || '-'); + console.log('browser version ranges:', browserVersionRanges || '-'); const browsers = browserlist .filter(b => { @@ -66,20 +66,18 @@ async function main() { return true; } - return semver.satisfies( - semver.coerce(b.split("/")[1]), - browserVersionRanges, - { - - } - ); + return semver.satisfies(semver.coerce(b.split("/")[1]), browserVersionRanges, {}); }) .filter(uaString => { if (uaString.startsWith("ios/")) { uaString = uaString.replace("ios", "ios_saf"); } - if (normalizeUserAgent(uaString) === "other/0.0.0") { + const ua = ua_parser(uaString); + + if (ua.normalize() === "other/0.0.0") { + console.log(uaString, ua.normalize()); + return false; } @@ -90,8 +88,6 @@ async function main() { // - and this browser version satisfies the version range for the polyfill // -> include this browser in the test run - const ua = new UA(uaString); - let isNeeded = false; for (const polyfillName in modified.affectedPolyfills) { const polyfill = modified.affectedPolyfills[polyfillName]; @@ -112,7 +108,7 @@ async function main() { process.exit(0); } - console.log({ browsers }); + browsers.forEach((x) => { console.log(x) }); const useragentToBrowserObject = browserWithVersion => { const [browser, version] = browserWithVersion.split("/"); @@ -334,7 +330,7 @@ async function main() { .run() .then(job => { if (job.state === "complete") { - const [family, version] = normalizeUserAgent(job.useragent).split("/"); + const [family, version] = ua_parser(job.useragent).normalize().split("/"); _.set( testResults, [family, version, job.mode], diff --git a/test/polyfills/ua-parser.js b/test/polyfills/ua-parser.js index 95aaba5c6..1190bf41c 100644 --- a/test/polyfills/ua-parser.js +++ b/test/polyfills/ua-parser.js @@ -1,11 +1,35 @@ const semver = require('semver'); const uap = require('ua-parser-js'); +const SHORT_UA_REGEX = /^(android|bb|chrome|edge|edge_mob|firefox|firefox_mob|ie|ie_mob|safari|ios_saf|opera|op_mob|op_mini|samsung_mob|other)\/((?:\d+\.)*\d+)$/i; + /** * @param {string} ua * @returns {import('../../lib/index').UaParser} */ function ua_parser(ua) { + if (SHORT_UA_REGEX.test(ua)) { + const [, family, version] = ua.match(SHORT_UA_REGEX); + + return { + normalize: function normalize() { + return this.getFamily() + "/" + this.getVersion(); + }, + isUnknown: function isUnknown() { + return this.getFamily() === 'other'; + }, + getVersion: function getVersion() { + return semver.coerce(version).toString(); + }, + getFamily: function getFamily() { + return family; + }, + satisfies: function satisfies(range) { + return semver.satisfies(this.getVersion(), range); + } + } + } + const p = uap(ua); return { @@ -18,7 +42,7 @@ function ua_parser(ua) { getVersion: function getVersion() { if (this.isUnknown()) return '0.0.0'; - if (this.getFamily() === 'ios_saf' && p.os.version) return semver.coerce(p.os.version).toString(); + if (p.os.version && this.getFamily() === 'ios_saf') return semver.coerce(p.os.version).toString(); return semver.coerce(p.browser.version).toString(); }, @@ -44,14 +68,24 @@ function ua_parser(ua) { return "ie"; case "IEMobile": return "ie_mob"; + case "Opera": + return "opera"; + case "Opera Mobi": + return "op_mob"; + case "Opera Mini": + return "op_mini"; case "Safari": return "safari"; case "Mobile Safari": case "Safari Mobile": return "ios_saf"; + case "Samsung Internet": + return "samsung_mob" default: - return "other"; + break; } + + return "other"; }, satisfies: function satisfies(range) { return semver.satisfies(this.getVersion(), range); diff --git a/test/unit/lib/index.test.js b/test/unit/lib/index.test.js index bdd78319f..9d7a2cdce 100644 --- a/test/unit/lib/index.test.js +++ b/test/unit/lib/index.test.js @@ -4,7 +4,7 @@ const test = require('node:test'); const { describe, it } = test; const assert = require('node:assert'); -const UA = require('@financial-times/polyfill-useragent-normaliser'); +const ua_parser = require('../../polyfills/ua-parser'); describe('exported property/properties', () => { it('is an object', () => { @@ -47,7 +47,7 @@ describe('exported property/properties', () => { features: { default: {} }, - ua: new UA('chrome/30') + ua: ua_parser('chrome/30') }); assert.ok(generator[Symbol.asyncIterator]); @@ -217,7 +217,7 @@ describe('.getOptions(opts)', () => { { const actual = polyfillio.getOptions({ - ua: new UA('example') + ua: ua_parser('example') }); assert.deepStrictEqual({ @@ -241,7 +241,7 @@ describe('.getOptions(opts)', () => { { const actual = polyfillio.getOptions({ - ua: new UA('chrome/38') + ua: ua_parser('chrome/38') }); assert.deepStrictEqual({ @@ -395,7 +395,7 @@ describe('.getPolyfills()', () => { '__proto__': {}, 'toLocaleString': {}, }, - ua: new UA('ie/9') + ua: ua_parser('ie/9') }; try { await polyfillio.getPolyfills(options); @@ -411,7 +411,7 @@ describe('.getPolyfills()', () => { features: { 'console': {} }, - ua: new UA('chrome/120') + ua: ua_parser('chrome/120') }; return polyfillio.getPolyfills(options).then(result => { @@ -428,7 +428,7 @@ describe('.getPolyfills()', () => { flags: new Set(['always']) } }, - ua: new UA('chrome/120') + ua: ua_parser('chrome/120') }; return polyfillio.getPolyfills(options).then(result => { @@ -454,7 +454,7 @@ describe('.getPolyfills()', () => { flags: new Set(['always']) } }, - ua: new UA('chrome/120') + ua: ua_parser('chrome/120') }; return polyfillio.getPolyfills(options).then(result => {