Skip to content

Commit a2673d7

Browse files
committed
Support query continuations across entries
1 parent f46a5b7 commit a2673d7

3 files changed

Lines changed: 180 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ browsers in `package.json` with `browserslist` key:
374374

375375
Separated Browserslist config should be named `.browserslistrc`
376376
and have browsers queries split by a new line.
377-
Each line is combined with the `or` combiner. Comments starts with `#` symbol:
377+
Each line is combined with the `or` combiner by default.
378+
Comments starts with `#` symbol:
378379

379380
```yaml
380381
# Browsers that we support
@@ -384,6 +385,20 @@ last 1 version
384385
not dead # no browsers without security updates
385386
```
386387

388+
To continue a query onto the next line, end the preceding line with `and`,
389+
`or`, or `,`. For example, this selects the intersection of the two queries:
390+
391+
```yaml
392+
last 2 versions and
393+
> 0.5%
394+
```
395+
396+
The same continuation syntax works between entries in a `package.json`
397+
`browserslist` array or an array passed to the JavaScript API:
398+
`["last 2 versions and", "> 0.5%"]`. An operator must have a following query
399+
in the same configuration section. Blank lines and comments do not end a
400+
continuation, and entries without an explicit operator still use `or`.
401+
387402
Browserslist will check config in every directory in `path`.
388403
So, if tool process `app/styles/main.css`, you can put config to root,
389404
`app/` or `app/styles`.

‎parse.js‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
var SPACE = /\s/
22

3+
function endsWithOperator(block) {
4+
// Scan the suffix once rather than backtracking over long whitespace runs.
5+
var end = block.length
6+
while (end > 0 && SPACE.test(block[end - 1])) end--
7+
8+
if (block[end - 1] === ',') {
9+
return block.slice(0, end - 1).trim().length > 0
10+
}
11+
12+
var tail = block.slice(Math.max(0, end - 3), end).toLowerCase()
13+
var size = 0
14+
if (tail === 'and') {
15+
size = 3
16+
} else if (tail.slice(-2) === 'or') {
17+
size = 2
18+
}
19+
if (size === 0 || end <= size || !SPACE.test(block[end - size - 1])) {
20+
return false
21+
}
22+
return block.slice(0, end - size).trim().length > 0
23+
}
24+
325
function flatten(array) {
426
if (!Array.isArray(array)) return [array]
527
// Iterative flatten: `reduce`+`concat` copies the accumulator on every step,
@@ -126,8 +148,22 @@ function parseBlock(all, block, qs) {
126148

127149
module.exports = function parse(all, queries) {
128150
if (!Array.isArray(queries)) queries = [queries]
151+
// Only explicitly connected entries share a block; all other entries keep
152+
// the parser's implicit OR. Joining each group once keeps this pass linear.
153+
var blocks = []
154+
var pending = []
155+
queries.forEach(function (block) {
156+
if (block.length === 0) return
157+
pending.push(block)
158+
if (endsWithOperator(block)) return
159+
blocks.push(pending.join('\n'))
160+
pending = []
161+
})
162+
// Parse an unfinished continuation too, so a dangling operator still errors.
163+
if (pending.length > 0) blocks.push(pending.join('\n'))
164+
129165
return flatten(
130-
queries.map(function (block) {
166+
blocks.map(function (block) {
131167
var qs = []
132168
parseBlock(all, block, qs)
133169
return qs

‎test/continuation.test.js‎

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
let { mkdtempSync, rmdirSync, unlinkSync, writeFileSync } = require('fs')
2+
let { tmpdir } = require('os')
3+
let { join } = require('path')
4+
let { test } = require('uvu')
5+
let { equal, throws } = require('uvu/assert')
6+
7+
delete require.cache[require.resolve('..')]
8+
let browserslist = require('..')
9+
10+
test('continues intersections between query-array entries', () => {
11+
equal(browserslist(['ie >= 6 and', 'ie <= 7']), ['ie 7', 'ie 6'])
12+
equal(browserslist(['ie >= 6 AND\t', '\tie <= 7']), ['ie 7', 'ie 6'])
13+
})
14+
15+
test('continues explicit unions between query-array entries', () => {
16+
equal(browserslist(['ie 6 or', 'ie 7']), ['ie 7', 'ie 6'])
17+
equal(browserslist(['ie 6 OR\t', 'ie 7']), ['ie 7', 'ie 6'])
18+
equal(browserslist(['ie 6,', 'ie 7']), ['ie 7', 'ie 6'])
19+
})
20+
21+
test('preserves left-to-right composition across continuations', () => {
22+
equal(browserslist(['ie 6 or', 'ie 7 and', 'ie <= 6']), ['ie 6'])
23+
equal(browserslist(['ie 6 and', 'ie 7', 'ie 8']), ['ie 8'])
24+
equal(browserslist(['ie >= 6 and', 'not ie > 7']), ['ie 7', 'ie 6'])
25+
})
26+
27+
test('keeps unconnected entries as implicit unions', () => {
28+
equal(browserslist(['ie 6', 'ie 7']), ['ie 7', 'ie 6'])
29+
equal(browserslist(['ie 6', 'ie 7 and ie <= 6']), ['ie 6'])
30+
equal(browserslist(['', 'ie 6', '']), ['ie 6'])
31+
})
32+
33+
test('rejects incomplete and malformed continuations', () => {
34+
for (let queries of [
35+
['ie 6 and'],
36+
['ie 6 or'],
37+
['ie 6 and', ''],
38+
['ie 6 and', 'or ie 7'],
39+
['ie 6 and or', 'ie 7'],
40+
['ie 6,'],
41+
[',', 'ie 7'],
42+
[' and ', 'ie 7']
43+
]) {
44+
throws(() => browserslist(queries), /Unknown browser query/)
45+
}
46+
})
47+
48+
test('retains continuation operators in the parsed AST', () => {
49+
equal(browserslist.parse(['ie 6 or', 'ie 7 and', 'ie <= 6']), [
50+
{
51+
query: 'ie 6',
52+
type: 'browser_version',
53+
browser: 'ie',
54+
version: '6',
55+
compose: 'or'
56+
},
57+
{
58+
query: 'ie 7',
59+
type: 'browser_version',
60+
browser: 'ie',
61+
version: '7',
62+
compose: 'or'
63+
},
64+
{
65+
query: 'ie <= 6',
66+
type: 'browser_ray',
67+
browser: 'ie',
68+
sign: '<=',
69+
version: '6',
70+
compose: 'and'
71+
}
72+
])
73+
})
74+
75+
test('loads continuations from config lines and package arrays', () => {
76+
for (let [name, content] of [
77+
[
78+
'.browserslistrc',
79+
'[production]\nie 11\n[test]\nie >= 6 and # intersection\n\n# comment\nie <= 7\n'
80+
],
81+
[
82+
'package.json',
83+
JSON.stringify({ browserslist: { test: ['ie >= 6 and', 'ie <= 7'] } })
84+
]
85+
]) {
86+
let dir = mkdtempSync(join(tmpdir(), 'browserslist-'))
87+
try {
88+
writeFileSync(join(dir, name), content)
89+
equal(browserslist(null, { config: join(dir, name), env: 'test' }), [
90+
'ie 7',
91+
'ie 6'
92+
])
93+
} finally {
94+
browserslist.clearCaches()
95+
unlinkSync(join(dir, name))
96+
rmdirSync(dir)
97+
}
98+
}
99+
})
100+
101+
test('does not continue an operator into another config section', () => {
102+
let dir = mkdtempSync(join(tmpdir(), 'browserslist-'))
103+
try {
104+
writeFileSync(
105+
join(dir, '.browserslistrc'),
106+
'[test]\nie >= 6 and\n[production]\nie 7\n'
107+
)
108+
throws(
109+
() =>
110+
browserslist(null, { config: join(dir, '.browserslistrc'), env: 'test' }),
111+
/Unknown browser query/
112+
)
113+
equal(
114+
browserslist(null, {
115+
config: join(dir, '.browserslistrc'),
116+
env: 'production'
117+
}),
118+
['ie 7']
119+
)
120+
} finally {
121+
browserslist.clearCaches()
122+
unlinkSync(join(dir, '.browserslistrc'))
123+
rmdirSync(dir)
124+
}
125+
})
126+
127+
test.run()

0 commit comments

Comments
 (0)