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 semver and project version query to Node and Electron #513

Open
wants to merge 4 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
7 changes: 7 additions & 0 deletions README.md
Expand Up @@ -255,6 +255,13 @@ You can specify the browser and Node.js versions by queries (case insensitive):
* `current node`: Node.js version used by Browserslist right now.
* `maintained node versions`: all Node.js versions, which are [still maintained]
by Node.js Foundation.
* `node semver ^10.0.0`: all versions satisfying the [semver](https://www.npmjs.com/package/semver) range
* `project node`: all versions satisfying `engines.node` in package.json
* `electron semver ^1.0.0`: all Electron versions satisfying the semver range.
Note Browserslist has information only about major versions, so the patch
version will be assumed as zero, i.e., >= 2.1.1 will not select 2.1.0.
* `project electron`: all versions satisfying `devDependencies.electron`
in package.json
* `iOS 7`: the iOS browser version 7 directly.
* `Firefox > 20`: versions of Firefox newer than 20.
`>=`, `<` and `<=` work too. It also works with Node.js.
Expand Down
7 changes: 7 additions & 0 deletions browser.js
Expand Up @@ -30,6 +30,11 @@ module.exports = {
'Supports queries are not available in client-side build of Browserslist')
},

semverSatisfies: function semverSatisfies () {
throw new BrowserslistError(
'Semver queries are not available in client-side build of Browserslist')
},

currentNode: function currentNode (resolve, context) {
return resolve(['maintained node versions'], context)[0]
},
Expand All @@ -40,6 +45,8 @@ module.exports = {

findConfig: noop,

loadDependencies: noop,

clearCaches: noop,

oldDataWarning: noop
Expand Down
96 changes: 72 additions & 24 deletions index.js
Expand Up @@ -24,6 +24,14 @@ function isEolReleased (name) {
})
}

function getNodeVersions () {
return jsReleases.filter(function (i) {
return i.name === 'nodejs'
}).map(function (i) {
return i.version
})
}

function normalize (versions) {
return versions.filter(function (version) {
return typeof version === 'string'
Expand Down Expand Up @@ -437,6 +445,8 @@ function browserslist (queries, opts) {
}
}

context.deps = env.loadDependencies(opts)

var cacheKey = JSON.stringify([queries, context])
if (cache[cacheKey]) return cache[cacheKey]

Expand Down Expand Up @@ -554,6 +564,7 @@ browserslist.parseConfig = env.parseConfig
browserslist.readConfig = env.readConfig
browserslist.findConfig = env.findConfig
browserslist.loadConfig = env.loadConfig
browserslist.loadDependencies = env.loadDependencies

/**
* Return browsers market coverage.
Expand Down Expand Up @@ -926,11 +937,6 @@ var QUERIES = [
{
regexp: /^node\s+([\d.]+)\s*-\s*([\d.]+)$/i,
select: function (context, from, to) {
var nodeVersions = jsReleases.filter(function (i) {
return i.name === 'nodejs'
}).map(function (i) {
return i.version
})
var semverRegExp = /^(0|[1-9]\d*)(\.(0|[1-9]\d*)){0,2}$/
if (!semverRegExp.test(from)) {
throw new BrowserslistError(
Expand All @@ -940,12 +946,10 @@ var QUERIES = [
throw new BrowserslistError(
'Unknown version ' + to + ' of Node.js')
}
return nodeVersions
return getNodeVersions()
.filter(semverFilterLoose('>=', from))
.filter(semverFilterLoose('<=', to))
.map(function (v) {
return 'node ' + v
})
.map(nameMapper('node'))
}
},
{
Expand Down Expand Up @@ -975,16 +979,9 @@ var QUERIES = [
{
regexp: /^node\s*(>=?|<=?)\s*([\d.]+)$/i,
select: function (context, sign, version) {
var nodeVersions = jsReleases.filter(function (i) {
return i.name === 'nodejs'
}).map(function (i) {
return i.version
})
return nodeVersions
return getNodeVersions()
.filter(generateSemverFilter(sign, version))
.map(function (v) {
return 'node ' + v
})
.map(nameMapper('node'))
}
},
{
Expand All @@ -1002,6 +999,25 @@ var QUERIES = [
})
}
},
{
regexp: /^electron\s+semver\s+(.+)$/i,
select: function (context, range) {
return Object.keys(e2c).filter(function (i) {
// assume patch version zero
return env.semverSatisfies(i + '.0', range)
}).map(function (i) {
return 'chrome ' + e2c[i]
})
}
},
{
regexp: /^node\s+semver\s+(.+)$/i,
select: function (context, range) {
return getNodeVersions().filter(function (i) {
return env.semverSatisfies(i, range)
}).map(nameMapper('node'))
}
},
{
regexp: /^(firefox|ff|fx)\s+esr$/i,
select: function () {
Expand Down Expand Up @@ -1029,11 +1045,8 @@ var QUERIES = [
{
regexp: /^node\s+(\d+(\.\d+)?(\.\d+)?)$/i,
select: function (context, version) {
var nodeReleases = jsReleases.filter(function (i) {
return i.name === 'nodejs'
})
var matched = nodeReleases.filter(function (i) {
return isVersionsMatch(i.version, version)
var matched = getNodeVersions().filter(function (i) {
return isVersionsMatch(i, version)
})
if (matched.length === 0) {
if (context.ignoreUnknownVersions) {
Expand All @@ -1043,7 +1056,7 @@ var QUERIES = [
'Unknown version ' + version + ' of Node.js')
}
}
return ['node ' + matched[matched.length - 1].version]
return ['node ' + matched[matched.length - 1]]
}
},
{
Expand All @@ -1066,6 +1079,41 @@ var QUERIES = [
return resolve(queries, context)
}
},
{
regexp: /^project\s+node$/i,
select: function (context) {
if (!context.deps) {
throw new BrowserslistError('Can\'t find package.json')
} else if (!context.deps.node) {
throw new BrowserslistError(
'engines.node is not specified in package.json')
}
var range = context.deps.node

return getNodeVersions().filter(function (i) {
return env.semverSatisfies(i, range)
}).map(nameMapper('node'))
}
},
{
regexp: /^project\s+electron$/i,
select: function (context) {
if (!context.deps) {
throw new BrowserslistError('Can\'t find package.json')
} else if (!context.deps.electron) {
throw new BrowserslistError(
'devDependencies.electron is not specified in package.json')
}
var range = context.deps.electron

return Object.keys(e2c).filter(function (i) {
// assume patch version zero
return env.semverSatisfies(i + '.0', range)
}).map(function (i) {
return 'chrome ' + e2c[i]
})
}
},
{
regexp: /^phantomjs\s+1.9$/i,
select: function () {
Expand Down
73 changes: 72 additions & 1 deletion node.js
@@ -1,5 +1,6 @@
var feature = require('caniuse-lite/dist/unpacker/feature').default
var region = require('caniuse-lite/dist/unpacker/region').default
var semverSatisfies = require('semver/functions/satisfies')
var path = require('path')
var fs = require('fs')

Expand All @@ -15,6 +16,7 @@ var FORMAT = 'Browserslist config should be a string or an array ' +
var dataTimeChecked = false
var filenessCache = { }
var configCache = { }
var packageCache = { }
function checkExtend (name) {
var use = ' Use `dangerousExtend` option to disable.'
if (!CONFIG_PATTERN.test(name) && !SCOPED_CONFIG__PATTERN.test(name)) {
Expand Down Expand Up @@ -81,8 +83,60 @@ function pickEnv (config, opts) {
return config[name] || config.defaults
}

function parsePackageDependencies (file, pkg) {
if (!pkg) {
pkg = JSON.parse(fs.readFileSync(file))
}

var deps = { }
if (pkg.engines) {
deps.node = pkg.engines.node
}
if (pkg.devDependencies) {
deps.electron = pkg.devDependencies.electron
}

if (!process.env.BROWSERSLIST_DISABLE_CACHE) {
packageCache[file] = deps
}
return deps
}

function findPackageDependencies (from) {
from = path.resolve(from)

var passed = []
var resolved = eachParent(from, function (dir) {
if (dir in packageCache) {
return packageCache[dir]
}

passed.push(dir)
var pkg = path.join(dir, 'package.json')

var deps
if (isFile(pkg)) {
try {
deps = parsePackageDependencies(pkg)
} catch (e) {
console.warn(
'[Browserslist] Could not parse ' + pkg + '. Ignoring it.'
)
}
}
return deps
})
if (!process.env.BROWSERSLIST_DISABLE_CACHE) {
passed.forEach(function (dir) {
packageCache[dir] = resolved
})
}
return resolved
}

function parsePackage (file) {
var config = JSON.parse(fs.readFileSync(file))
parsePackageDependencies(file, config) // cache
if (config.browserlist && !config.browserslist) {
throw new BrowserslistError(
'`browserlist` key instead of `browserslist` in ' + file
Expand Down Expand Up @@ -226,6 +280,20 @@ module.exports = {
}
},

loadDependencies: function loadDependencies (opts) {
if (opts.config || process.env.BROWSERSLIST_CONFIG) {
var file = opts.config || process.env.BROWSERSLIST_CONFIG
if (path.basename(file) === 'package.json') {
return parsePackageDependencies(file)
}
}
if (opts.path) {
return findPackageDependencies(opts.path)
} else {
return undefined
}
},

loadCountry: function loadCountry (usage, country, data) {
var code = country.replace(/[^\w-]/g, '')
if (!usage[code]) {
Expand Down Expand Up @@ -357,6 +425,7 @@ module.exports = {
dataTimeChecked = false
filenessCache = { }
configCache = { }
packageCache = { }

this.cache = { }
},
Expand All @@ -382,5 +451,7 @@ module.exports = {

currentNode: function currentNode () {
return 'node ' + process.versions.node
}
},

semverSatisfies: semverSatisfies
}
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -23,7 +23,8 @@
"caniuse-lite": "^1.0.30001125",
"electron-to-chromium": "^1.3.564",
"escalade": "^3.0.2",
"node-releases": "^1.1.61"
"node-releases": "^1.1.61",
"semver": "^7.3.2"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
Expand Down Expand Up @@ -136,6 +137,7 @@
"QQ",
"RegExp",
"sclateri",
"semver",
"stylelint",
"symlink",
"Tidelift",
Expand Down
27 changes: 27 additions & 0 deletions test/cache.test.js
Expand Up @@ -6,6 +6,7 @@ let browserslist = require('../')

let DIR = join(tmpdir(), 'browserslist-' + Math.random())
let CONFIG = join(DIR, 'browserslist')
let PACKAGE = join(DIR, 'package.json')

beforeAll(async () => {
await mkdir(DIR)
Expand Down Expand Up @@ -48,3 +49,29 @@ it('does not use cache when ENV variable set', async () => {

expect(result1).not.toEqual(result2)
})

it('caches dependencies but the cache is clearable', async () => {
await writeFile(PACKAGE, '{"engines":{"node": "6"}}', 'UTF-8')
let result1 = browserslist.loadDependencies({ path: DIR })

await writeFile(PACKAGE, '{"engines":{"node": "8"}}', 'UTF-8')
let result2 = browserslist.loadDependencies({ path: DIR })

expect(result1).toEqual(result2)

browserslist.clearCaches()
let result3 = browserslist.loadDependencies({ path: DIR })
expect(result1).not.toEqual(result3)
})

it('does not use dependency cache when ENV variable set', async () => {
process.env.BROWSERSLIST_DISABLE_CACHE = 1

await writeFile(PACKAGE, '{"engines":{"node": "6"}}', 'UTF-8')
let result1 = browserslist.loadDependencies({ path: DIR })

await writeFile(PACKAGE, '{"engines":{"node": "8"}}', 'UTF-8')
let result2 = browserslist.loadDependencies({ path: DIR })

expect(result1).not.toEqual(result2)
})
3 changes: 3 additions & 0 deletions test/config.test.js
Expand Up @@ -87,6 +87,9 @@ it('shows warning on broken package.json', () => {
defaults: ['ie 11', 'ie 10']
})
expect(console.warn).toHaveBeenCalledTimes(1)

browserslist.loadDependencies({ path: BROKEN })
expect(console.warn).toHaveBeenCalledTimes(2)
})

it('shows error on key typo', () => {
Expand Down