1- const browserslist = require ( 'browserslist' )
2- const semver = require ( 'semver' )
3- const useragent = require ( 'useragent' )
4- const e2c = require ( 'electron-to-chromium/versions' )
1+ import browserslist from 'browserslist' ;
2+ import semver from 'semver' ;
3+ import UAParser from 'ua-parser-js' ;
54
65// @see https://github.com/ai/browserslist#browsers
76
87// map of equivalent browsers,
98// see https://github.com/ai/browserslist/issues/156
109
11- const browserNameMap = {
10+ const browserNameMap : Record < string , string > = {
1211 bb : 'BlackBerry' ,
1312 and_chr : 'Chrome' ,
1413 ChromeAndroid : 'Chrome' ,
@@ -24,35 +23,18 @@ const browserNameMap = {
2423 and_uc : 'UCAndroid' ,
2524}
2625
27- function resolveUserAgent ( uaString ) {
28- // Chrome and Opera on iOS uses a UIWebView of the underlying platform to render
29- // content, by stripping the CriOS or OPiOS strings the useragent parser will alias the
30- // user agent to ios_saf for the UIWebView, which is closer to the actual
31- // renderer
32- // @see https://github.com/Financial-Times/polyfill-service/pull/416
26+ function resolveUserAgent ( uaString : string ) : { family : string | null , version : string | null } {
27+ const parsedUA = UAParser ( uaString )
28+ const parsedBrowserVersion = semverify ( parsedUA . browser . version )
29+ const parsedOSVersion = semverify ( parsedUA . os . version )
30+ const parsedEngineVersion = semverify ( parsedUA . engine . version )
3331
34- let strippedUA = uaString . replace (
35- / ( ( C r i O S | O P i O S ) \/ ( \d + ) \. ( \d + ) \. ( \d + ) \. ( \d + ) ) / ,
36- ''
37- )
38-
39- // Yandex Browser uses Chromium as the udnerlying engine
40- strippedUA = strippedUA . replace ( / Y a B r o w s e r \/ ( \d + \. ? ) + / g, '' )
41-
42- // Yandex Search uses Chromium as the udnerlying engine
43- strippedUA = strippedUA . replace ( / Y a n d e x S e a r c h \/ ( \d + \. ? ) + / g, '' )
44-
45- // Facebook Webview
46- strippedUA = strippedUA . replace ( / F B _ I A B / g, '' ) . replace ( / F B A N \/ F B I O S / g, '' )
47-
48- const parsedUA = useragent . parse ( strippedUA )
49-
50- // Case A: For Safari, Chrome and others browsers on iOS
51- // that report as Safari after stripping tags
52- if ( parsedUA . family . includes ( 'Safari' ) && parsedUA . os . family === 'iOS' ) {
32+ // Case A: For Safari on iOS, the use the browser version
33+ if (
34+ parsedUA . browser . name === 'Safari' && parsedUA . os . name === 'iOS' ) {
5335 return {
5436 family : 'iOS' ,
55- version : [ parsedUA . major , parsedUA . minor , parsedUA . patch ] . join ( '.' ) ,
37+ version : parsedBrowserVersion ,
5638 }
5739 }
5840
@@ -61,111 +43,121 @@ function resolveUserAgent(uaString) {
6143 // version. This is based on the assumption that the
6244 // underlying Safari Engine used will be *atleast* equal
6345 // to the iOS version it's running on.
64- if ( parsedUA . os . family === 'iOS' ) {
46+ if ( parsedUA . os . name === 'iOS' ) {
6547 return {
6648 family : 'iOS' ,
67- version : [ parsedUA . os . major , parsedUA . os . minor , parsedUA . os . patch ] . join (
68- '.'
69- ) ,
49+ version : parsedOSVersion
7050 }
7151 }
7252
73- // Case C: The caniuse database does not contain
74- // historical browser versions for so called `minor`
75- // browsers like Chrome for Android, Firefox for Android etc
76- // In this case, we proxy to the desktop version
77- // @see https://github.com/Fyrd/caniuse/issues/3518
78-
7953 if (
80- parsedUA . family . includes ( 'Chrome Mobile' ) ||
81- parsedUA . family . includes ( 'Chrome Mobile WebView' ) ||
82- parsedUA . family . includes ( 'Chromium' ) ||
83- parsedUA . family . includes ( 'HeadlessChrome' )
54+ ( parsedUA . browser . name === 'Opera' && parsedUA . device . type === 'mobile' ) ||
55+ parsedUA . browser . name === 'Opera Mobi'
8456 ) {
8557 return {
86- family : 'Chrome ' ,
87- version : [ parsedUA . major , parsedUA . minor , parsedUA . patch ] . join ( '.' ) ,
58+ family : 'OperaMobile ' ,
59+ version : parsedBrowserVersion
8860 }
8961 }
9062
91- if ( parsedUA . family === 'Opera Mobile ' ) {
63+ if ( parsedUA . browser . name === 'Samsung Browser ' ) {
9264 return {
93- family : 'OperaMobile ' ,
94- version : [ parsedUA . major , parsedUA . minor , parsedUA . patch ] . join ( '.' ) ,
65+ family : 'Samsung ' ,
66+ version : parsedBrowserVersion
9567 }
9668 }
9769
98- if ( parsedUA . family === 'Samsung Internet ' ) {
70+ if ( parsedUA . browser . name === 'IE ' ) {
9971 return {
100- family : 'Samsung ' ,
101- version : [ parsedUA . major , parsedUA . minor , parsedUA . patch ] . join ( '.' ) ,
72+ family : 'Explorer ' ,
73+ version : parsedBrowserVersion
10274 }
10375 }
10476
105- if ( parsedUA . family === 'Firefox Mobile ' ) {
77+ if ( parsedUA . browser . name === 'IEMobile ' ) {
10678 return {
107- family : 'Firefox ' ,
108- version : [ parsedUA . major , parsedUA . minor , parsedUA . patch ] . join ( '.' ) ,
79+ family : 'ExplorerMobile ' ,
80+ version : parsedBrowserVersion
10981 }
11082 }
11183
112- if ( parsedUA . family === 'IE' ) {
84+ // Use engine version for gecko-based browsers
85+ if ( parsedUA . engine . name === 'Gecko' ) {
11386 return {
114- family : 'Explorer ' ,
115- version : [ parsedUA . major , parsedUA . minor , parsedUA . patch ] . join ( '.' ) ,
87+ family : 'Firefox ' ,
88+ version : parsedEngineVersion
11689 }
11790 }
11891
119- if ( parsedUA . family === 'IE Mobile' ) {
92+ // Use engine version for blink-based browsers
93+ if ( parsedUA . engine . name === 'Blink' ) {
12094 return {
121- family : 'ExplorerMobile ' ,
122- version : [ parsedUA . major , parsedUA . minor , parsedUA . patch ] . join ( '.' ) ,
95+ family : 'Chrome ' ,
96+ version : parsedEngineVersion
12397 }
12498 }
12599
126- if ( parsedUA . family === 'Electron' ) {
127- const electronVersion = [ parsedUA . major , parsedUA . minor ] . join ( '.' )
100+ // Chrome based browsers pre-blink (WebKit)
101+ if (
102+ parsedUA . browser . name &&
103+ [ 'Chrome' , 'Chromium' , 'Chrome WebView' , 'Chrome Headless' ] . includes ( parsedUA . browser . name )
104+ ) {
128105 return {
129106 family : 'Chrome' ,
130- version : e2c [ electronVersion ] ,
107+ version : parsedBrowserVersion
108+ }
109+ }
110+
111+ if ( parsedUA . browser . name === 'Android Browser' ) {
112+ // Versions prior to Blink were based
113+ // on the OS version. Only after this
114+ // did android start using system chrome for web-views
115+ return {
116+ family : 'Android' ,
117+ version : parsedOSVersion
131118 }
132119 }
133120
134121 return {
135- family : parsedUA . family ,
136- version : [ parsedUA . major , parsedUA . minor , parsedUA . patch ] . join ( '.' ) ,
122+ family : parsedUA . browser . name || null ,
123+ version : parsedBrowserVersion
137124 }
138125}
139126
140127// Convert version to a semver value.
141128// 2.5 -> 2.5.0; 1 -> 1.0.0;
142- const semverify = ( version ) => semver . coerce ( version , { loose : true } ) . version
143-
144- function flatten ( arr ) {
145- return arr . reduce ( function ( flat , toFlatten ) {
146- return flat . concat (
147- Array . isArray ( toFlatten ) ? flatten ( toFlatten ) : toFlatten
148- )
149- } , [ ] )
129+ const semverify = ( version : string | undefined | null ) => {
130+ if ( ! version ) {
131+ return null
132+ }
133+ const cooerced = semver . coerce ( version , { loose : true } )
134+ if ( ! cooerced ) {
135+ return null
136+ }
137+ return cooerced . version
150138}
151139
152140// 10.0-10.2 -> 10.0, 10.1, 10.2
153- function generateSemversInRange ( versionRange ) {
141+ function generateSemversInRange ( versionRange : string ) {
154142 const [ start , end ] = versionRange . split ( '-' )
155143 const startSemver = semverify ( start )
156144 const endSemver = semverify ( end )
145+
146+ if ( ! startSemver || ! endSemver ) {
147+ return [ ]
148+ }
157149 const versionsInRange = [ ]
158150 let curVersion = startSemver
159151
160152 while ( semver . gte ( endSemver , curVersion ) ) {
161153 versionsInRange . push ( curVersion )
162- curVersion = semver . inc ( curVersion , 'minor' )
154+ curVersion = semver . inc ( curVersion , 'minor' ) as string
163155 }
164156
165157 return versionsInRange
166158}
167159
168- function normalizeQuery ( query ) {
160+ function normalizeQuery ( query : string ) {
169161 let normalizedQuery = query
170162 const regex = `(${ Object . keys ( browserNameMap ) . join ( '|' ) } )`
171163 const match = query . match ( new RegExp ( regex ) )
@@ -177,7 +169,7 @@ function normalizeQuery(query) {
177169 return normalizedQuery
178170}
179171
180- const parseBrowsersList = ( browsersList ) => {
172+ const parseBrowsersList = ( browsersList : string [ ] ) : { family : string , version : string | null } [ ] => {
181173 const browsers = browsersList
182174 . map ( ( browser ) => {
183175 const [ name , version ] = browser . split ( ' ' )
@@ -208,12 +200,16 @@ const parseBrowsersList = (browsersList) => {
208200 }
209201 } )
210202
211- return flatten ( browsers )
203+ return browsers . flat ( )
212204}
213205
214- const compareBrowserSemvers = ( versionA , versionB , options ) => {
206+ const compareBrowserSemvers = ( versionA : string , versionB : string , options : Options ) => {
215207 const semverifiedA = semverify ( versionA )
216208 const semverifiedB = semverify ( versionB )
209+
210+ if ( ! semverifiedA || ! semverifiedB ) {
211+ return false
212+ }
217213 let referenceVersion = semverifiedB
218214
219215 if ( options . ignorePatch ) {
@@ -231,7 +227,16 @@ const compareBrowserSemvers = (versionA, versionB, options) => {
231227 }
232228}
233229
234- const matchesUA = ( uaString , opts = { } ) => {
230+ type Options = {
231+ browsers ?: string [ ] ,
232+ env ?: string ,
233+ path ?: string ,
234+ ignoreMinor ?: boolean ,
235+ ignorePatch ?: boolean ,
236+ allowHigherVersions ?: boolean
237+ }
238+
239+ const matchesUA = ( uaString : string , opts : Options = { } ) => {
235240 // bail out early if the user agent is invalid
236241 if ( ! uaString ) {
237242 return false
@@ -246,8 +251,10 @@ const matchesUA = (uaString, opts = {}) => {
246251 path : opts . path || process . cwd ( ) ,
247252 } )
248253
254+
249255 const parsedBrowsers = parseBrowsersList ( browsers )
250256
257+
251258 const resolvedUserAgent = resolveUserAgent ( uaString )
252259
253260 const options = {
@@ -257,15 +264,20 @@ const matchesUA = (uaString, opts = {}) => {
257264 }
258265
259266 return parsedBrowsers . some ( ( browser ) => {
267+ if ( ! resolvedUserAgent . family ) return false
268+ if ( ! resolvedUserAgent . version ) return false
269+ if ( ! browser . version ) return false
270+
271+
260272 return (
261273 browser . family . toLowerCase ( ) ===
262- resolvedUserAgent . family . toLocaleLowerCase ( ) &&
274+ resolvedUserAgent . family . toLocaleLowerCase ( ) &&
263275 compareBrowserSemvers ( resolvedUserAgent . version , browser . version , options )
264276 )
265277 } )
266278}
267279
268- module . exports = {
280+ export {
269281 matchesUA ,
270282 resolveUserAgent ,
271283 normalizeQuery ,
0 commit comments