airtap-match-browsers
Advanced tools
Comparing version 0.0.2 to 0.1.0
215
index.js
@@ -8,3 +8,6 @@ 'use strict' | ||
const names = require('browser-names') | ||
const defaults = { version: 'latest' } | ||
const prerelease = /[^\d.]/ | ||
const numeric = /^\d+$/ | ||
@@ -24,3 +27,3 @@ module.exports = matchAll | ||
for (const manifest of available) { | ||
const name = lower(manifest.name) | ||
const name = manifest.name | ||
@@ -34,23 +37,22 @@ if (groups.has(name)) { | ||
// Presort versions and add aliases | ||
for (const [name, group] of groups) { | ||
group.sort((a, b) => cmpVersion(a.version, b.version)) | ||
for (const w of wanted) { | ||
const explicit = new Set() | ||
for (const alias of names(name)) { | ||
if (alias !== name) groups.set(alias, group) | ||
// Match by name | ||
let group = findName(groups, w.name) || [] | ||
// Lazily sort by version | ||
if (!group.sorted) { | ||
group.sort((a, b) => cmpVersion(a.version, b.version)) | ||
group.sorted = true | ||
} | ||
} | ||
for (const w of wanted) { | ||
const explicit = new Set() | ||
// Match by other properties | ||
const skip = ['name', 'version'] | ||
group = group.filter(m => match(m, w, explicit, skip)) | ||
// Match by name and version | ||
let group = groups.get(w.name) || [] | ||
// Match by version | ||
w.version = lower(w.version) | ||
group = filterVersions(group, w.version) | ||
// Match by properties other than name and version | ||
const skip = ['name', 'version'] | ||
group = group.filter(m => match(m, w, explicit, skip)) | ||
if (group.length === 0) { | ||
@@ -63,3 +65,3 @@ throw new Error('Zero matches for ' + JSON.stringify(w, null, 2)) | ||
const a = group[i] | ||
const alternatives = [a] | ||
let winner = a | ||
@@ -70,3 +72,4 @@ for (let j = i + 1; j < group.length; j++) { | ||
if (same(a, b, explicit)) { | ||
alternatives.push(b) | ||
// Last manifest wins (for no particular reason) | ||
winner = b | ||
group.splice(j--, 1) | ||
@@ -76,25 +79,2 @@ } | ||
// Pick winner by preferredOver rules (or short of that, the last manifest) | ||
let winner = alternatives[alternatives.length - 1] | ||
let max = 0 | ||
// TODO: optimize by merging logic into above loop | ||
// TODO: find a simpler solution to deduplication overall | ||
for (let x = 0; x < alternatives.length; x++) { | ||
for (let y = x + 1; y < alternatives.length; y++) { | ||
const weightX = preferredOver(alternatives[x], alternatives[y]) | ||
const weightY = preferredOver(alternatives[y], alternatives[x]) | ||
if (weightX > max) { | ||
max = weightX | ||
winner = alternatives[x] | ||
} | ||
if (weightY > max) { | ||
max = weightY | ||
winner = alternatives[y] | ||
} | ||
} | ||
} | ||
// Don't merge options into the manifest yet, so that we can | ||
@@ -111,2 +91,14 @@ // perform fast deduplication by object identity (below). We | ||
function findName (groups, name) { | ||
if (groups.has(name)) { | ||
return groups.get(name) | ||
} | ||
for (const alias of names(name)) { | ||
if (alias !== name && groups.has(alias)) { | ||
return groups.get(alias) | ||
} | ||
} | ||
} | ||
function consolidate (matches) { | ||
@@ -150,3 +142,3 @@ for (let i = 0; i < matches.length; i++) { | ||
if (typeof manifest.name !== 'string' || manifest.name === '') { | ||
throw new TypeError('Browser "name" is required') | ||
throw new TypeError('Manifest "name" is required') | ||
} | ||
@@ -171,24 +163,2 @@ | ||
function preferredOver (a, b) { | ||
let weight = 0 | ||
if (a.preferredOver) { | ||
for (const k of Object.keys(a.preferredOver)) { | ||
const values = a.preferredOver[k].map(lower) | ||
const value = deep(b, k) | ||
if (value == null) { | ||
continue | ||
} else if (values.includes(lower(value))) { | ||
// A specific value has more weight than "any" | ||
weight += 1e3 | ||
} else if (values.includes('any')) { | ||
weight += 1 | ||
} | ||
} | ||
} | ||
return weight | ||
} | ||
function lower (value) { | ||
@@ -202,4 +172,6 @@ return value != null ? String(value).toLowerCase() : '' | ||
} else if (Array.isArray(wanted)) { | ||
throw new Error('Array is not yet supported on ' + key) | ||
// TODO: explode into multiple browsers, instead of this "oneof" behavior | ||
return wanted.some(el => match(available, el, explicit, skip, key)) | ||
// return wanted.some(el => match(available, el, explicit, skip, key)) | ||
} else if (isObject(wanted)) { | ||
@@ -212,3 +184,3 @@ if (!isObject(available)) return false | ||
if (!hasOwnProperty.call(wanted, k)) continue | ||
if (fqk === 'options' || fqk === 'preferredOver') continue | ||
if (fqk === 'options') continue | ||
if (!match(available[k], wanted[k], explicit, skip, fqk)) return false | ||
@@ -247,45 +219,51 @@ } | ||
function filterVersions (manifests, version) { | ||
const [gte, lte] = range(version, manifests) | ||
let start = 0 | ||
let end = manifests.length | ||
if (gte) { | ||
while (start < end && cmpVersion(manifests[start].version, gte) < 0) { | ||
start++ | ||
} | ||
if (!matchVersion(gte, manifests[start] && manifests[start].version)) { | ||
throw new Error(`Version not found: ${gte}`) | ||
} | ||
if (manifests.length === 0) { | ||
return manifests | ||
} | ||
if (lte) { | ||
while (end > start && cmpVersion(manifests[end - 1].version, lte) > 0) { | ||
end-- | ||
} | ||
const test = range(version, manifests) | ||
const result = [] | ||
if (!matchVersion(lte, manifests[end - 1] && manifests[end - 1].version)) { | ||
throw new Error(`Version not found: ${lte}`) | ||
for (const m of manifests) { | ||
if (test(m.version)) { | ||
result.push(m) | ||
} else if (result.length) { | ||
break | ||
} | ||
} | ||
return manifests.slice(start, end) | ||
return result | ||
} | ||
// Assumes manifests are sorted by version. | ||
function range (version, manifests) { | ||
const arr = version.split('..') | ||
let gte | ||
let lte | ||
if (arr.length === 1) { | ||
arr.push(arr[0]) | ||
if (version.indexOf('..') === -1) { | ||
gte = lte = resolve(version || 'latest') | ||
} else { | ||
const arr = version.split('..') | ||
gte = resolve(arr[0] || 'oldest') | ||
lte = resolve(arr[1] || 'latest') | ||
} | ||
return arr.map(function (v) { | ||
if (!manifests.length) return | ||
return function test (v) { | ||
const c1 = cmpRange(v, gte, false) | ||
if (c1 < 0) return false | ||
const c2 = cmpRange(v, lte, true) | ||
if (c2 > 0) return false | ||
return true | ||
} | ||
function resolve (v) { | ||
if (v === 'oldest') return manifests[0].version | ||
if (v === 'latest') return latest(manifests, 0) | ||
if (!isNaN(v) && v < 0) return latest(manifests, v * -1) | ||
if (/^-\d+$/.test(v) && v < 0) return latest(manifests, v * -1) | ||
return v | ||
}) | ||
} | ||
} | ||
@@ -295,3 +273,3 @@ | ||
for (let i = manifests.length - 1; i >= 0; i--) { | ||
if (!isBeta(manifests[i].version) && !n--) { | ||
if (!isPrerelease(manifests[i].version) && (!n-- || i === 0)) { | ||
return manifests[i].version | ||
@@ -301,28 +279,51 @@ } | ||
return manifests[0].version | ||
// All are prereleases, return the last | ||
return manifests[manifests.length - 1].version | ||
} | ||
function isBeta (version) { | ||
return version && isNaN(version) | ||
function isPrerelease (version) { | ||
return !version || prerelease.test(version) | ||
} | ||
function matchVersion (wanted, available) { | ||
if (!available) return false | ||
if (isBeta(available)) return available === wanted | ||
return available.startsWith(wanted) | ||
function cmpVersion (a, b) { | ||
return cmpRange(a, b, false) | ||
} | ||
function cmpVersion (a, b) { | ||
if (a == null) return b == null ? 0 : 1 | ||
if (b == null) return -1 | ||
function cmpRange (a, b, prefixOnly) { | ||
// Missing version behaves like last prerelease | ||
if (!a) return !b ? 0 : 1 | ||
if (!b) return -1 | ||
if (isNaN(a)) return isNaN(b) ? a.localeCompare(b) : 1 | ||
if (isNaN(b)) return -1 | ||
const ap = isPrerelease(a) | ||
const bp = isPrerelease(b) | ||
return Number(a) - Number(b) | ||
if (ap !== bp) return ap ? 1 : -1 | ||
const av = a.split('.') | ||
const bv = b.split('.') | ||
for (let i = 0; i < Math.min(av.length, bv.length); i++) { | ||
const cmp = cmpElement(av[i], bv[i]) | ||
if (cmp > 0) return 1 | ||
if (cmp < 0) return -1 | ||
} | ||
if (prefixOnly || av.length === bv.length) { | ||
return 0 | ||
} else { | ||
return av.length > bv.length ? 1 : -1 | ||
} | ||
} | ||
function cmpElement (a, b) { | ||
if (numeric.test(a) && numeric.test(b)) { | ||
return a - b | ||
} else { | ||
return a.localeCompare(b) | ||
} | ||
} | ||
function isObject (o) { | ||
return typeof o === 'object' && o !== null && !Array.isArray(o) | ||
} |
{ | ||
"name": "airtap-match-browsers", | ||
"version": "0.0.2", | ||
"version": "0.1.0", | ||
"description": "Match browser manifests to a desired set of browsers", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
# airtap-match-browsers | ||
> **Match [browser manifests](https://github.com/airtap/browser-manifest) to a desired set of browsers.** | ||
> Intended to replace [`sauce-browsers`](https://github.com/lpinca/sauce-browsers) in conjunction with [`airtap-sauce-browsers`](https://github.com/airtap/sauce-browsers). | ||
> Replaces [`sauce-browsers`](https://github.com/lpinca/sauce-browsers) in conjunction with [`airtap-sauce-browsers`](https://github.com/airtap/sauce-browsers). | ||
@@ -45,6 +45,6 @@ [![npm status](http://img.shields.io/npm/v/airtap-match-browsers.svg)](https://www.npmjs.org/package/airtap-match-browsers) | ||
- A negative range in the form of `-<n>..latest`, for example `-1..latest` which means the last 2 numeric versions. | ||
- A non-numeric version like "dev" and "beta". Such versions sort after numeric versions, so that `oldest..latest` excludes "dev" and `latest..dev` includes latest, "beta" and "dev". | ||
- A prerelease version like "dev", "beta", "80.0a1". Such versions sort after numeric versions, so that `oldest..latest` excludes "dev" and `latest..dev` includes e.g. latest, "beta" and "dev". | ||
- An array of versions. | ||
If a version is not found (including the start and end of ranges) an error is thrown. | ||
If a manifest doesn't have a `version`, it behaves like a prerelease. | ||
@@ -51,0 +51,0 @@ ### `platform` and any other (nested) property |
12249