browserslist/
lib.rs

1#![allow(clippy::float_cmp)]
2#![deny(clippy::if_not_else)]
3#![deny(clippy::needless_borrow)]
4#![deny(clippy::unimplemented)]
5#![warn(missing_docs)]
6
7//! **browserslist-rs** is a Rust-based implementation of [Browserslist](https://github.com/browserslist/browserslist).
8//!
9//! ## Introduction
10//!
11//! This library bundles Can I Use data, Electron versions list and Node.js releases list,
12//! so it won't and doesn't need to access any data files.
13//!
14//! Except several non-widely/non-frequently used features,
15//! this library works as same as the JavaScript-based
16//! implementation [Browserslist](https://github.com/browserslist/browserslist).
17//!
18//! ## Usage
19//!
20//! It provides a simple API for querying which accepts a sequence of strings and options [`Opts`],
21//! then returns the result.
22//!
23//! ```
24//! use browserslist::{Distrib, Opts, resolve, Error};
25//!
26//! let distribs = resolve(["ie <= 6"], &Opts::default()).unwrap();
27//! assert_eq!(distribs[0].name(), "ie");
28//! assert_eq!(distribs[0].version(), "6");
29//! assert_eq!(distribs[1].name(), "ie");
30//! assert_eq!(distribs[1].version(), "5.5");
31//!
32//! assert_eq!(
33//!     resolve(["yuru 1.0"], &Opts::default()),
34//!     Err(Error::BrowserNotFound(String::from("yuru")))
35//! );
36//! ```
37//!
38//! The result isn't a list of strings, instead, it's a tuple struct called [`Distrib`].
39//! If you need to retrieve something like JavaScript-based implementation of
40//! [Browserslist](https://github.com/browserslist/browserslist),
41//! you can convert them to strings:
42//!
43//! ```
44//! use browserslist::{Distrib, Opts, resolve, Error};
45//!
46//! let distribs = resolve(["ie <= 6"], &Opts::default()).unwrap();
47//! assert_eq!(
48//!     distribs.into_iter().map(|d| d.to_string()).collect::<Vec<_>>(),
49//!     vec![String::from("ie 6"), String::from("ie 5.5")]
50//! );
51//! ```
52//!
53//! ## WebAssembly
54//!
55//! This crate can be compiled as WebAssembly, without configuring any features manually.
56//!
57//! Please note that browser and Deno can run WebAssembly,
58//! but those environments aren't Node.js,
59//! so you will receive an error when querying `current node` in those environments.
60
61use parser::parse_browserslist_query;
62use std::cmp::Ordering;
63#[cfg(all(feature = "wasm_bindgen", target_arch = "wasm32"))]
64pub use wasm::browserslist;
65pub use {error::Error, opts::Opts, queries::Distrib};
66
67#[cfg(not(target_arch = "wasm32"))]
68mod config;
69mod data;
70mod error;
71mod opts;
72mod parser;
73mod queries;
74mod semver;
75#[cfg(test)]
76mod test;
77#[cfg(all(feature = "wasm_bindgen", target_arch = "wasm32"))]
78mod wasm;
79
80/// Resolve browserslist queries.
81///
82/// This is a low-level API.
83/// If you want to load queries from configuration file and
84/// resolve them automatically,
85/// use the higher-level API [`execute`] instead.
86///
87/// ```
88/// use browserslist::{Distrib, Opts, resolve};
89///
90/// let distribs = resolve(["ie <= 6"], &Opts::default()).unwrap();
91/// assert_eq!(distribs[0].name(), "ie");
92/// assert_eq!(distribs[0].version(), "6");
93/// assert_eq!(distribs[1].name(), "ie");
94/// assert_eq!(distribs[1].version(), "5.5");
95/// ```
96pub fn resolve<I, S>(queries: I, opts: &Opts) -> Result<Vec<Distrib>, Error>
97where
98    S: AsRef<str>,
99    I: IntoIterator<Item = S>,
100{
101    let query = queries
102        .into_iter()
103        .enumerate()
104        .fold(String::new(), |mut s, (i, query)| {
105            if i > 0 {
106                s.push_str(", ");
107            }
108            s.push_str(query.as_ref());
109            s
110        });
111
112    let mut distribs = parse_browserslist_query(&query)?
113        .1
114        .into_iter()
115        .enumerate()
116        .try_fold(vec![], |mut distribs, (i, current)| {
117            if i == 0 && current.negated {
118                return Err(Error::NotAtFirst(current.raw.to_string()));
119            }
120
121            let mut dist = queries::query(current.atom, opts)?;
122            if current.negated {
123                distribs.retain(|distrib| !dist.contains(distrib));
124            } else if current.is_and {
125                distribs.retain(|distrib| dist.contains(distrib));
126            } else {
127                distribs.append(&mut dist);
128            }
129
130            Ok::<_, Error>(distribs)
131        })?;
132
133    distribs.sort_by(|a, b| match a.name().cmp(b.name()) {
134        Ordering::Equal => {
135            let version_a = a.version().split('-').next().unwrap();
136            let version_b = b.version().split('-').next().unwrap();
137            version_b
138                .parse::<semver::Version>()
139                .unwrap_or_default()
140                .cmp(&version_a.parse().unwrap_or_default())
141        }
142        ord => ord,
143    });
144    distribs.dedup();
145
146    Ok(distribs)
147}
148
149#[cfg(not(target_arch = "wasm32"))]
150/// Load queries from configuration with environment information,
151/// then resolve those queries.
152///
153/// If you want to resolve custom queries (not from configuration file),
154/// use the lower-level API [`resolve`] instead.
155///
156/// ```
157/// use browserslist::{Opts, execute};
158///
159/// // when no config found, it use `defaults` query
160/// assert!(!execute(&Opts::default()).unwrap().is_empty());
161/// ```
162pub fn execute(opts: &Opts) -> Result<Vec<Distrib>, Error> {
163    resolve(config::load(opts)?, opts)
164}