Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add normalizers (byEngine and toDesktop) #511

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 14 additions & 4 deletions README.md
Expand Up @@ -515,10 +515,20 @@ Options:
Default is `false.`
* `dangerousExtend`: Disable security checks for `extend` query.
Default is `false.`
* `mobileToDesktop`: Use desktop browsers if Can I Use doesn’t have data
about this mobile version. For instance, Browserslist will return
`chrome 20` on `and_chr 20` query (Can I Use has only data only about
latest versions of mobile browsers). Default is `false`.
* `mobileToDesktop`: Resolve the mobile version using the desktop version when
Can I Use doesn't have data about the specified version. For instance, it
will return two Chrome for Android versions even if Can I Use has only
data only about the latest version. Default is `false`.
* `normalizers`: An array of names of normalizers below and functions
that accept a browser and return the normalized versions, which are
applied to resolved browsers in order. For instance, `['byEngine', 'toDesktop']`.
* `byEngine`: Normalize Chromium-based browsers to Chrome. For instance,
UC Browser, QQ Browser, Baidu browser for Android, and Samsung Internet
will return `and_chr` with the version of Chromium they are based on.
Note Edge and Opera is not normalized. Gecko-based browsers are
also normalized to Firefox, e.g., KaiOS Browser will return `and_ff`.
* `toDesktop`: Normalize mobile browsers to desktop browsers.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've named it toDesktop instead of mobileToDesktop as it may be confused with the existing option.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between normalizers: ['toDesktop'] and mobileToDesktop: true?

Copy link
Author

@ylemkimon ylemkimon Jul 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ai See #510 and 2ca9e82. mobileToDesktop only resolves the version part using desktop data. For example:

$ node cli.js "last 5 and_chr versions"
and_chr 81
$ node cli.js "last 5 chrome versions"
chrome 83
chrome 81
chrome 80
chrome 79
chrome 78
$ node cli.js --mobile-to-desktop "last 5 and_chr versions"
and_chr 83
and_chr 81
and_chr 80
and_chr 79
and_chr 78
$ node cli.js --normalizers=toDesktop "last 5 and_chr versions"
chrome 81
$ node cli.js --normalizers=toDesktop --mobile-to-desktop "last 5 and_chr versions"
chrome 83
chrome 81
chrome 80
chrome 79
chrome 78

Copy link
Member

@ai ai Jul 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JLHwung what do you think if we change mobileToDesktop behavior? On last 5 and_chr versions we will return chrome 78-83 instead of and_chr 78-83. Now it will affect Babel?

Copy link
Contributor

@JLHwung JLHwung Jul 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internally Babel maps and_chr to chrome and it seems to me this PR moves this process to browserslists. I don't think it will affect Babel.

Another thoughts about mobileToDesktop: AFAIK opera_mob is mapped to opera, this is incorrect because they are, if not always, frequently based on different chromium versions. Can we incorporate data from https://github.com/mdn/browser-compat-data/blob/master/browsers/opera_android.json ? This can help preset-env on supporting opera_mob. Surely we can address it in a different PR.

Copy link
Author

@ylemkimon ylemkimon Jul 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added 'op_mob 46': 63 to normalizedVersions (byEngine). I think removing it from desktopNames (mobileToDesktop) would be out of scope for this PR.

For instance, Browserslist will return `chrome 20` on `and_chr 20`

For non-JS environment and debug purpose you can use CLI tool:

Expand Down
3 changes: 3 additions & 0 deletions cli.js
Expand Up @@ -19,6 +19,7 @@ var USAGE = 'Usage:\n' +
' npx browserslist --env="environment name defined in config"\n' +
' npx browserslist --stats="path/to/browserlist/stats/file"\n' +
' npx browserslist --mobile-to-desktop\n' +
' npx browserslist --normalizers=byEngine,toDesktop\n' +
' npx browserslist --update-db'

function isArg (arg) {
Expand Down Expand Up @@ -75,6 +76,8 @@ if (isArg('--help') || isArg('-h')) {
mode = 'json'
} else if (name === '--mobile-to-desktop') {
opts.mobileToDesktop = true
} else if (name === '--normalizers') {
opts.normalizers = value.split(',')
} else {
error('Unknown arguments ' + args[i] + '.\n\n' + USAGE)
}
Expand Down
87 changes: 84 additions & 3 deletions index.js
Expand Up @@ -9,6 +9,7 @@ var env = require('./node') // Will load browser.js in webpack

var YEAR = 365.259641 * 24 * 60 * 60 * 1000
var ANDROID_EVERGREEN_FIRST = 37
var ANDROID_CLASSIC_REGEX = /^(?:[2-4]\.|[34]$)/

var QUERY_OR = 1
var QUERY_AND = 2
Expand Down Expand Up @@ -265,7 +266,7 @@ function normalizeAndroidVersions (androidVersions, chromeVersions) {
var firstEvergreen = ANDROID_EVERGREEN_FIRST
var last = chromeVersions[chromeVersions.length - 1]
return androidVersions
.filter(function (version) { return /^(?:[2-4]\.|[34]$)/.test(version) })
.filter(function (version) { return ANDROID_CLASSIC_REGEX.test(version) })
.concat(chromeVersions.slice(firstEvergreen - last - 1))
}

Expand Down Expand Up @@ -315,7 +316,7 @@ function resolve (queries, context) {
queries = parse(queries)
}

return queries.reduce(function (result, query, index) {
var list = queries.reduce(function (result, query, index) {
var selection = query.queryString

var isExclude = selection.indexOf('not ') === 0
Expand Down Expand Up @@ -371,6 +372,18 @@ function resolve (queries, context) {

throw unknownQuery(selection)
}, [])

if (context.normalizers) {
list = context.normalizers.reduce(function (result, normalizer) {
normalizer = NORMALIZERS[normalizer] || normalizer
if (typeof normalizer !== 'function') {
throw new BrowserslistError('Unknown normalizer ' + normalizer)
}
return result.map(normalizer)
}, list)
}

return list
}

var cache = { }
Expand All @@ -395,6 +408,10 @@ var cache = { }
* @param {boolean} [opts.mobileToDesktop] Alias mobile browsers to the desktop
* version when Can I Use doesn't have
* data about the specified version.
* @param {(string|function)[]} [opts.normalizers] An array of names of
* normalizers and functions
* that accept a browser and
* return normalized versions.
* @returns {string[]} Array with browser names in Can I Use.
*
* @example
Expand Down Expand Up @@ -425,6 +442,7 @@ function browserslist (queries, opts) {
ignoreUnknownVersions: opts.ignoreUnknownVersions,
dangerousExtend: opts.dangerousExtend,
mobileToDesktop: opts.mobileToDesktop,
normalizers: opts.normalizers,
env: opts.env
}

Expand Down Expand Up @@ -546,6 +564,46 @@ browserslist.desktopNames = {
android: 'chrome' // has extra processing logic
}

// Tools such as compat-table only provides compatibility data for
// popular browsers. Fallback to Chrome for Chromium-based browsers
// and Firefox for Gecko-based browsers. A number indicates `and_chr`
browserslist.normalizedVersions = {
// From Samsung Internet release notes at
// https://developer.samsung.com/internet/release-note.html,
// Chrome version page (chrome://version), and the user agent string
// Also available at https://github.com/mdn/browser-compat-data/blob/master
// /browsers/samsunginternet_android.json
'samsung 4': 44,
'samsung 5': 51,
'samsung 6': 56,
'samsung 7': 59,
'samsung 8': 63,
'samsung 9': 67,
'samsung 10': 71,
'samsung 11': 75,
'samsung 12': 79,

// From Opera for Android release notes at
// https://forums.opera.com/category/20/opera-for-android
// Opera version page (opera://version), and the user agent string
// Also available at https://github.com/mdn/browser-compat-data/blob/master
// /browsers/opera_android.json
'op_mob 46': 63,

// From the kernel version at https://plus.ucweb.com/download/
// and the user agent string
'and_uc 12': 57,

// From https://browser.qq.com/ and the user agent string
'and_qq 10': 70,

// From the user agent string
'baidu 7': 48,

// From https://github.com/kaiostech/gecko-b2g and the user agent string
'kaios 2': 'and_ff 48'
}

// Aliases to work with joined versions like `ios_saf 7.0-7.1`
browserslist.versionAliases = { }

Expand Down Expand Up @@ -1141,7 +1199,30 @@ var QUERIES = [
}
}
}
];
]

var NORMALIZERS = {
byEngine: function (browser) {
var major = browser.split('.')[0]
var normalized = browserslist.normalizedVersions[major]
if (!normalized) {
return browser
} else if (typeof normalized === 'number') {
normalized = 'and_chr ' + normalized
}
return normalized
},
toDesktop: function (browser) {
browser = browser.split(' ')
var name = browser[0]
var version = browser[1]
if (browserslist.desktopNames[name] &&
!(name === 'android' && ANDROID_CLASSIC_REGEX.test(version))) {
name = browserslist.desktopNames[name]
}
return name + ' ' + version
}
};

// Get and convert Can I Use data

Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -131,6 +131,7 @@
"JSDoc",
"jspm",
"KaiOS",
"normalizers",
"npm",
"postcss",
"QQ",
Expand Down
83 changes: 83 additions & 0 deletions test/normalizer.test.js
@@ -0,0 +1,83 @@
let browserslist = require('../')

let originData = browserslist.data
let originNormalizedVersions = browserslist.normalizedVersions

beforeEach(() => {
browserslist.data = {
samsung: {
name: 'samsung',
released: ['4'],
versions: ['4']
},
kaios: {
name: 'kaios',
released: ['2.5'],
versions: ['2.5']
},
android: {
name: 'android',
released: ['4.4', '4.4.3-4.4.4', '37'],
versions: ['4.4', '4.4.3-4.4.4', '37']
},
and_chr: {
name: 'and_chr',
released: ['83'],
versions: ['83']
}
}

browserslist.normalizedVersions = {
'samsung 4': 44,
'kaios 2': 'and_ff 48'
}
})

afterEach(() => {
browserslist.data = originData
browserslist.normalizedVersions = originNormalizedVersions
})

it('normalizes mobile browsers to desktop browsers', () => {
let opts = { normalizers: ['toDesktop'] }
expect(browserslist('android 37', opts)).toEqual(['chrome 37'])
expect(browserslist('last 1 and_chr versions', opts)).toEqual(['chrome 83'])
expect(browserslist('samsung 4', opts)).toEqual(['samsung 4'])
})

it('does not normalize classic Android webview versions', () => {
let opts = { normalizers: ['toDesktop'] }
expect(browserslist('android 4.4-37', opts)).toEqual([
'android 4.4.3-4.4.4', 'android 4.4', 'chrome 37'
])
})

it('normalizes Chromium-based browsers to Chrome', () => {
let opts = { normalizers: ['byEngine'] }
expect(browserslist('samsung 4', opts)).toEqual(['and_chr 44'])
expect(browserslist('kaios 2.5', opts)).toEqual(['and_ff 48'])
expect(browserslist('and_chr 83', opts)).toEqual(['and_chr 83'])
})

it('should apply each normalizer in order', () => {
let opts = { normalizers: ['byEngine', 'toDesktop'] }
expect(browserslist('samsung 4', opts)).toEqual(['chrome 44'])
expect(browserslist('kaios 2.5', opts)).toEqual(['firefox 48'])
expect(browserslist('and_chr 83', opts)).toEqual(['chrome 83'])
})

it('accepts a normalizer function', () => {
let opts = { normalizers: [() => 'samsung 4'] }
expect(browserslist('android 37', opts)).toEqual(['samsung 4'])
})

it('raises on unknown normalizer', () => {
let opts1 = { normalizers: ['_'] }
let opts2 = { normalizers: [{}] }
expect(() => {
browserslist('samsung 4', opts1)
}).toThrow('Unknown normalizer _')
expect(() => {
browserslist('samsung 4', opts2)
}).toThrow('Unknown normalizer [object Object]')
})