├── .node-version ├── .gitignore ├── pnpm-workspace.yaml ├── .cargo └── config.toml ├── .typos.toml ├── xtask ├── src │ ├── lib.rs │ ├── utils │ │ ├── mod.rs │ │ ├── paths.rs │ │ └── file.rs │ ├── data │ │ ├── mod.rs │ │ ├── browser.rs │ │ └── caniuse.rs │ ├── generators │ │ ├── mod.rs │ │ ├── caniuse │ │ │ ├── mod.rs │ │ │ ├── browsers.rs │ │ │ ├── global_usage.rs │ │ │ ├── features.rs │ │ │ └── regions.rs │ │ ├── electron.rs │ │ └── node.rs │ └── main.rs └── Cargo.toml ├── rust-toolchain.toml ├── .oxfmtrc.json ├── .rustfmt.toml ├── src ├── generated │ ├── node_versions.bin.deflate │ ├── caniuse_browsers.bin.deflate │ ├── caniuse_region_browsers.bin.deflate │ ├── caniuse_region_versions.bin.deflate │ ├── caniuse_feature_matching.bin.deflate │ ├── caniuse_region_percentages.bin.deflate │ ├── node_release_schedule.rs │ ├── caniuse_global_usage.rs │ ├── caniuse_region_matching.rs │ └── electron_to_chromium.rs ├── generated.rs ├── wasm.rs ├── queries │ ├── op_mini.rs │ ├── unreleased_electron.rs │ ├── firefox_esr.rs │ ├── browserslist_config.rs │ ├── phantom.rs │ ├── defaults.rs │ ├── last_n_node.rs │ ├── last_n_electron.rs │ ├── cover.rs │ ├── last_n_node_major.rs │ ├── maintained_node.rs │ ├── dead.rs │ ├── unreleased_browsers.rs │ ├── last_n_electron_major.rs │ ├── unreleased_x_browsers.rs │ ├── last_n_browsers.rs │ ├── percentage.rs │ ├── years.rs │ ├── last_n_x_browsers.rs │ ├── cover_by_region.rs │ ├── node_bounded_range.rs │ ├── since.rs │ ├── electron_unbounded_range.rs │ ├── current_node.rs │ ├── last_n_major_browsers.rs │ ├── electron_accurate.rs │ ├── node_unbounded_range.rs │ ├── electron_bounded_range.rs │ ├── last_n_x_major_browsers.rs │ ├── percentage_by_region.rs │ ├── browser_bounded_range.rs │ ├── browser_unbounded_range.rs │ ├── node_accurate.rs │ ├── browser_accurate.rs │ ├── supports.rs │ ├── extends.rs │ └── mod.rs ├── data │ ├── caniuse │ │ ├── compression.rs │ │ ├── features.rs │ │ └── region.rs │ ├── node.rs │ ├── mod.rs │ ├── electron.rs │ └── caniuse.rs ├── opts.rs ├── test.rs ├── semver.rs ├── error.rs ├── lib.rs ├── config │ └── parser.rs └── parser.rs ├── .ignore ├── CLAUDE.md ├── .github ├── renovate.json ├── codecov.yml └── workflows │ ├── release.yml │ ├── autofix.yml │ ├── zizmor.yml │ ├── benchmark.yml │ ├── miri.yml │ ├── cron.yml │ ├── codecov.yml │ ├── copilot-setup-steps.yml │ └── ci.yml ├── .editorconfig ├── justfile ├── package.json ├── examples └── inspect.rs ├── LICENSE ├── benches └── resolve.rs ├── Cargo.toml ├── README.md └── AGENTS.md /.node-version: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | verifyDepsBeforeRun: install 2 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | codegen = "run -p xtask" 3 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["src/generated"] 3 | -------------------------------------------------------------------------------- /xtask/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub mod generators; 3 | pub mod utils; 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.92.0" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /.oxfmtrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": ["CHANGELOG.md", "pnpm-lock.yaml"] 3 | } 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | style_edition = "2024" 2 | use_small_heuristics = "Max" 3 | use_field_init_shorthand = true 4 | reorder_modules = true 5 | -------------------------------------------------------------------------------- /src/generated/node_versions.bin.deflate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxc-project/oxc-browserslist/main/src/generated/node_versions.bin.deflate -------------------------------------------------------------------------------- /src/generated/caniuse_browsers.bin.deflate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxc-project/oxc-browserslist/main/src/generated/caniuse_browsers.bin.deflate -------------------------------------------------------------------------------- /src/generated/caniuse_region_browsers.bin.deflate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxc-project/oxc-browserslist/main/src/generated/caniuse_region_browsers.bin.deflate -------------------------------------------------------------------------------- /src/generated/caniuse_region_versions.bin.deflate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxc-project/oxc-browserslist/main/src/generated/caniuse_region_versions.bin.deflate -------------------------------------------------------------------------------- /xtask/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod file; 2 | pub mod paths; 3 | 4 | pub use file::{create_range_vec, generate_file, save_bin_compressed}; 5 | pub use paths::root; 6 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | # For watchexec https://github.com/watchexec/watchexec/tree/main/crates/cli#features 2 | 3 | target/** 4 | **/node_modules/** 5 | src/generated/**/*.rs 6 | -------------------------------------------------------------------------------- /src/generated/caniuse_feature_matching.bin.deflate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxc-project/oxc-browserslist/main/src/generated/caniuse_feature_matching.bin.deflate -------------------------------------------------------------------------------- /src/generated/caniuse_region_percentages.bin.deflate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxc-project/oxc-browserslist/main/src/generated/caniuse_region_percentages.bin.deflate -------------------------------------------------------------------------------- /xtask/src/utils/paths.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use project_root::get_project_root; 4 | 5 | pub fn root() -> PathBuf { 6 | get_project_root().unwrap() 7 | } 8 | -------------------------------------------------------------------------------- /src/generated.rs: -------------------------------------------------------------------------------- 1 | pub mod caniuse_feature_matching; 2 | pub mod caniuse_global_usage; 3 | pub mod caniuse_region_matching; 4 | pub mod electron_to_chromium; 5 | pub mod node_release_schedule; 6 | -------------------------------------------------------------------------------- /xtask/src/data/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod browser; 2 | pub mod caniuse; 3 | 4 | pub use browser::encode_browser_name; 5 | pub use caniuse::{Agent, Caniuse, Feature, VersionDetail, parse_caniuse_global}; 6 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Project: oxc-browserslist 2 | 3 | Rust implementation of Browserslist. 4 | 5 | ## Commands 6 | 7 | ```bash 8 | cargo test 9 | cargo build 10 | cargo fmt 11 | cargo clippy 12 | ``` 13 | -------------------------------------------------------------------------------- /xtask/src/generators/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod caniuse; 2 | pub mod electron; 3 | pub mod node; 4 | 5 | pub use electron::build_electron_to_chromium; 6 | pub use node::{build_node_release_schedule, build_node_versions}; 7 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>Boshen/renovate"], 4 | "ignoreDeps": ["caniuse-db", "electron-to-chromium", "node-releases"] 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{mjs,js,ts,json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | changes: 10 | default: 11 | informational: true 12 | 13 | ignore: 14 | - "src/generated" 15 | -------------------------------------------------------------------------------- /xtask/src/generators/caniuse/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod browsers; 2 | pub mod features; 3 | pub mod global_usage; 4 | pub mod regions; 5 | 6 | pub use browsers::build_caniuse_browsers; 7 | pub use features::build_caniuse_feature_matching; 8 | pub use global_usage::build_caniuse_global_usage; 9 | pub use regions::build_caniuse_region_matching; 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | release-plz: 12 | name: Release-plz 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | id-token: write 18 | steps: 19 | - uses: oxc-project/release-plz@44b98e8dda1a7783d4ec2ef66e2f37a3e8c1c759 # v1.0.4 20 | with: 21 | PAT: ${{ secrets.OXC_BOT_PAT }} 22 | -------------------------------------------------------------------------------- /src/wasm.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | use crate::{opts::Opts, resolve}; 4 | 5 | #[doc(hidden)] 6 | #[wasm_bindgen] 7 | pub fn browserslist(query: String, opts: JsValue) -> Result { 8 | let opts: Option = serde_wasm_bindgen::from_value(opts)?; 9 | 10 | serde_wasm_bindgen::to_value( 11 | &resolve(&[query], &opts.unwrap_or_default()) 12 | .map_err(|e| format!("{}", e))? 13 | .into_iter() 14 | .map(|d| d.to_string()) 15 | .collect::>(), 16 | ) 17 | .map_err(JsValue::from) 18 | } 19 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S just --justfile 2 | 3 | _default: 4 | @just --list -u 5 | 6 | alias r := ready 7 | 8 | init: 9 | cargo binstall watchexec-cli typos-cli cargo-shear -y 10 | 11 | ready: 12 | git diff --exit-code --quiet 13 | typos 14 | pnpm install 15 | cargo codegen 16 | just fmt 17 | cargo check 18 | just lint 19 | cargo test 20 | 21 | fmt: 22 | -cargo shear --fix # remove all unused dependencies 23 | cargo fmt 24 | node --run fmt 25 | 26 | lint: 27 | cargo clippy --all-targets --all-features -- -D warnings 28 | 29 | watch *args='': 30 | watchexec --no-vcs-ignore {{args}} 31 | 32 | watch-check: 33 | just watch "'cargo check; cargo clippy'" 34 | -------------------------------------------------------------------------------- /src/queries/op_mini.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | 3 | pub(super) fn op_mini() -> QueryResult { 4 | Ok(vec![Distrib::new("op_mini", "all")]) 5 | } 6 | 7 | #[cfg(all(test, not(miri)))] 8 | mod tests { 9 | use test_case::test_case; 10 | 11 | use crate::{opts::Opts, test::run_compare}; 12 | 13 | #[test_case("op_mini all"; "short")] 14 | #[test_case("Op_Mini All"; "short case insensitive")] 15 | #[test_case("operamini all"; "long")] 16 | #[test_case("OperaMini All"; "long case insensitive")] 17 | #[test_case("op_mini all"; "more spaces")] 18 | fn valid(query: &str) { 19 | run_compare(query, &Opts::default(), None); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/queries/unreleased_electron.rs: -------------------------------------------------------------------------------- 1 | use super::QueryResult; 2 | 3 | pub(super) fn unreleased_electron() -> QueryResult { 4 | Ok(vec![]) 5 | } 6 | 7 | #[cfg(all(test, not(miri)))] 8 | mod tests { 9 | use test_case::test_case; 10 | 11 | use crate::{opts::Opts, test::run_compare}; 12 | 13 | #[test_case("unreleased electron versions"; "basic")] 14 | #[test_case("Unreleased Electron Versions"; "case insensitive")] 15 | #[test_case("unreleased electron version"; "support pluralization")] 16 | #[test_case("unreleased electron versions"; "more spaces")] 17 | fn valid(query: &str) { 18 | run_compare(query, &Opts::default(), None); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /xtask/src/data/browser.rs: -------------------------------------------------------------------------------- 1 | pub fn encode_browser_name(name: &str) -> u8 { 2 | match name { 3 | "ie" => 1, 4 | "edge" => 2, 5 | "firefox" => 3, 6 | "chrome" => 4, 7 | "safari" => 5, 8 | "opera" => 6, 9 | "ios_saf" => 7, 10 | "op_mini" => 8, 11 | "android" => 9, 12 | "bb" => 10, 13 | "op_mob" => 11, 14 | "and_chr" => 12, 15 | "and_ff" => 13, 16 | "ie_mob" => 14, 17 | "and_uc" => 15, 18 | "samsung" => 16, 19 | "and_qq" => 17, 20 | "baidu" => 18, 21 | "kaios" => 19, 22 | _ => unreachable!("unknown browser name"), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/queries/firefox_esr.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | 3 | pub(super) fn firefox_esr() -> QueryResult { 4 | Ok(vec![Distrib::new("firefox", "140")]) 5 | } 6 | 7 | #[cfg(all(test, not(miri)))] 8 | mod tests { 9 | use test_case::test_case; 10 | 11 | use crate::{opts::Opts, test::run_compare}; 12 | 13 | #[test_case("firefox esr"; "firefox")] 14 | #[test_case("Firefox ESR"; "firefox case insensitive")] 15 | #[test_case("ff esr"; "ff")] 16 | #[test_case("FF ESR"; "ff case insensitive")] 17 | #[test_case("fx esr"; "fx")] 18 | #[test_case("Fx ESR"; "fx case insensitive")] 19 | fn valid(query: &str) { 20 | run_compare(query, &Opts::default(), None); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lints] 8 | workspace = true 9 | 10 | [dependencies] 11 | anyhow = { workspace = true } 12 | bincode = { workspace = true } 13 | flate2 = { workspace = true } 14 | indexmap = { workspace = true, features = ["serde"] } 15 | prettyplease = { workspace = true } 16 | proc-macro2 = { workspace = true } 17 | project-root = { workspace = true } 18 | quote = { workspace = true } 19 | serde = { workspace = true, features = ["derive"] } 20 | serde_json = { workspace = true, features = ["preserve_order"] } 21 | syn = { workspace = true } 22 | time = { workspace = true, features = ["formatting", "macros", "parsing"] } 23 | -------------------------------------------------------------------------------- /src/data/caniuse/compression.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | 3 | use bincode::BorrowDecode; 4 | use flate2::read::DeflateDecoder; 5 | 6 | /// Decompress gzip-compressed data 7 | pub fn decompress_deflate(compressed_data: &[u8]) -> Vec { 8 | let mut decoder = DeflateDecoder::new(compressed_data); 9 | let mut decompressed = Vec::new(); 10 | decoder.read_to_end(&mut decompressed).expect("Failed to decompress data"); 11 | decompressed 12 | } 13 | 14 | pub fn decode<'a, T: BorrowDecode<'a, ()>>(data: &'a [u8], start: u32, end: u32) -> Vec { 15 | bincode::borrow_decode_from_slice( 16 | &data[start as usize..end as usize], 17 | bincode::config::standard(), 18 | ) 19 | .unwrap() 20 | .0 21 | } 22 | -------------------------------------------------------------------------------- /src/queries/browserslist_config.rs: -------------------------------------------------------------------------------- 1 | use super::QueryResult; 2 | use crate::opts::Opts; 3 | 4 | pub(super) fn browserslist_config(opts: &Opts) -> QueryResult { 5 | #[cfg(target_arch = "wasm32")] 6 | { 7 | crate::resolve(&["defaults"], opts) 8 | } 9 | 10 | #[cfg(not(target_arch = "wasm32"))] 11 | { 12 | crate::execute(opts) 13 | } 14 | } 15 | 16 | #[cfg(all(test, not(miri)))] 17 | mod tests { 18 | use test_case::test_case; 19 | 20 | use super::*; 21 | use crate::test::run_compare; 22 | 23 | #[test_case("browserslist config"; "basic")] 24 | #[test_case("Browserslist Config"; "case insensitive")] 25 | fn valid(query: &str) { 26 | run_compare(query, &Opts::default(), None); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/queries/phantom.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | 3 | pub(super) fn phantom(is_later_version: bool) -> QueryResult { 4 | let version = if is_later_version { "6" } else { "5" }; 5 | Ok(vec![Distrib::new("safari", version)]) 6 | } 7 | 8 | #[cfg(all(test, not(miri)))] 9 | mod tests { 10 | use test_case::test_case; 11 | 12 | use crate::{opts::Opts, test::run_compare}; 13 | 14 | #[test_case("phantomjs 2.1"; "2.1")] 15 | #[test_case("PhantomJS 2.1"; "2.1 case insensitive")] 16 | #[test_case("phantomjs 1.9"; "1.9")] 17 | #[test_case("PhantomJS 1.9"; "1.9 case insensitive")] 18 | #[test_case("phantomjs 2.1"; "more spaces")] 19 | fn valid(query: &str) { 20 | run_compare(query, &Opts::default(), None); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oxc-browserslist", 3 | "private": true, 4 | "license": "MIT", 5 | "scripts": { 6 | "browserslist": "browserslist", 7 | "fmt": "oxfmt" 8 | }, 9 | "dependencies": { 10 | "browserslist": "4.28.1" 11 | }, 12 | "devDependencies": { 13 | "lint-staged": "^16.1.6", 14 | "oxfmt": "^0.19.0", 15 | "simple-git-hooks": "^2.13.1" 16 | }, 17 | "peerDependencies": { 18 | "caniuse-db": "1.x", 19 | "electron-to-chromium": "1.x", 20 | "node-releases": "2.x" 21 | }, 22 | "simple-git-hooks": { 23 | "pre-commit": "npx lint-staged" 24 | }, 25 | "lint-staged": { 26 | "pnpm-lock.yaml": "cargo codegen", 27 | "*": "oxfmt --no-error-on-unmatched-pattern" 28 | }, 29 | "packageManager": "pnpm@10.25.0" 30 | } 31 | -------------------------------------------------------------------------------- /src/data/node.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use crate::semver::Version; 4 | 5 | pub use crate::generated::node_release_schedule::RELEASE_SCHEDULE; 6 | 7 | #[allow(non_snake_case)] 8 | pub fn NODE_VERSIONS() -> &'static [Version] { 9 | static NODE_VERSIONS: OnceLock> = OnceLock::new(); 10 | NODE_VERSIONS.get_or_init(|| { 11 | const COMPRESSED: &[u8] = include_bytes!("../generated/node_versions.bin.deflate"); 12 | let decompressed = super::caniuse::compression::decompress_deflate(COMPRESSED); 13 | let versions: Vec<(u16, u16, u16)> = 14 | bincode::decode_from_slice(&decompressed, bincode::config::standard()).unwrap().0; 15 | versions.into_iter().map(|(major, minor, patch)| Version(major, minor, patch)).collect() 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/queries/defaults.rs: -------------------------------------------------------------------------------- 1 | use super::QueryResult; 2 | use crate::{opts::Opts, resolve}; 3 | 4 | pub(super) fn defaults(opts: &Opts) -> QueryResult { 5 | resolve(&["> 0.5%", "last 2 versions", "Firefox ESR", "not dead"], opts) 6 | } 7 | 8 | #[cfg(all(test, not(miri)))] 9 | mod tests { 10 | use test_case::test_case; 11 | 12 | use super::*; 13 | use crate::test::run_compare; 14 | 15 | #[test_case("defaults", &Opts::default(); "no options")] 16 | #[test_case("Defaults", &Opts::default(); "case insensitive")] 17 | #[test_case("defaults", &Opts { mobile_to_desktop: true, ..Default::default() }; "respect options")] 18 | #[test_case("defaults, ie 6", &Opts::default(); "with other queries")] 19 | fn valid(query: &str, opts: &Opts) { 20 | run_compare(query, opts, None); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/queries/last_n_node.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::data::node::NODE_VERSIONS; 3 | 4 | pub(super) fn last_n_node(count: usize) -> QueryResult { 5 | let distribs = NODE_VERSIONS() 6 | .iter() 7 | .rev() 8 | .take(count) 9 | .map(|version| Distrib::new("node", version.to_string())) 10 | .collect(); 11 | Ok(distribs) 12 | } 13 | 14 | #[cfg(all(test, not(miri)))] 15 | mod tests { 16 | use test_case::test_case; 17 | 18 | use crate::{opts::Opts, test::run_compare}; 19 | 20 | #[test_case("last 2 node versions"; "basic")] 21 | #[test_case("last 2 Node versions"; "case insensitive")] 22 | #[test_case("last 2 node version"; "support pluralization")] 23 | fn valid(query: &str) { 24 | run_compare(query, &Opts::default(), None); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/queries/last_n_electron.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::data::electron::ELECTRON_VERSIONS; 3 | 4 | pub(super) fn last_n_electron(count: usize) -> QueryResult { 5 | let distribs = ELECTRON_VERSIONS 6 | .iter() 7 | .rev() 8 | .take(count) 9 | .map(|(_, version)| Distrib::new("chrome", *version)) 10 | .collect(); 11 | Ok(distribs) 12 | } 13 | 14 | #[cfg(all(test, not(miri)))] 15 | mod tests { 16 | use test_case::test_case; 17 | 18 | use crate::{opts::Opts, test::run_compare}; 19 | 20 | #[test_case("last 2 electron versions"; "basic")] 21 | #[test_case("last 2 Electron versions"; "case insensitive")] 22 | #[test_case("last 2 electron version"; "support pluralization")] 23 | fn valid(query: &str) { 24 | run_compare(query, &Opts::default(), None); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/inspect.rs: -------------------------------------------------------------------------------- 1 | #![expect(clippy::print_stdout)] 2 | 3 | use browserslist::{Opts, resolve}; 4 | use pico_args::Arguments; 5 | 6 | fn main() { 7 | let mut args = Arguments::from_env(); 8 | let mobile_to_desktop = args.contains("--mobile-to-desktop"); 9 | let ignore_unknown_versions = args.contains("--ignore-unknown-versions"); 10 | let queries = args 11 | .finish() 12 | .into_iter() 13 | .filter_map(|s| s.to_str().map(ToString::to_string)) 14 | .collect::>(); 15 | 16 | match resolve( 17 | &queries, 18 | &Opts { mobile_to_desktop, ignore_unknown_versions, ..Default::default() }, 19 | ) { 20 | Ok(versions) => { 21 | for version in versions { 22 | println!("{version}"); 23 | } 24 | } 25 | Err(error) => println!("{error}"), 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/queries/cover.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::data::{caniuse::CANIUSE_GLOBAL_USAGE, decode_browser_name}; 3 | 4 | pub(super) fn cover(coverage: f32) -> QueryResult { 5 | let mut distribs = vec![]; 6 | let mut total = 0.0; 7 | for (name, version, usage) in CANIUSE_GLOBAL_USAGE { 8 | if total >= coverage || *usage == 0.0 { 9 | break; 10 | } 11 | distribs.push(Distrib::new(decode_browser_name(*name), *version)); 12 | total += usage; 13 | } 14 | Ok(distribs) 15 | } 16 | 17 | #[cfg(all(test, not(miri)))] 18 | mod tests { 19 | use test_case::test_case; 20 | 21 | use crate::{opts::Opts, test::run_compare}; 22 | 23 | #[test_case("cover 0.1%"; "global")] 24 | #[test_case("Cover 0.1%"; "global case insensitive")] 25 | fn valid(query: &str) { 26 | run_compare(query, &Opts::default(), None); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci # For security reasons, the workflow in which the autofix.ci action is used must be named "autofix.ci". 2 | 3 | permissions: {} 4 | 5 | on: 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 11 | cancel-in-progress: ${{ github.ref_name != 'main' }} 12 | 13 | jobs: 14 | autofix: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 18 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 19 | with: 20 | restore-cache: true 21 | tools: just,cargo-shear@1 22 | components: rustfmt 23 | - uses: oxc-project/setup-node@141eb77546de6702f92d320926403fe3f9f6a6f2 # v1.0.5 24 | - run: cargo codegen 25 | - run: just fmt 26 | - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2 27 | -------------------------------------------------------------------------------- /xtask/src/data/caniuse.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use anyhow::Result; 4 | use indexmap::IndexMap; 5 | use serde::Deserialize; 6 | 7 | use crate::utils::root; 8 | 9 | #[derive(Deserialize)] 10 | pub struct Caniuse { 11 | pub agents: IndexMap, 12 | pub data: IndexMap, 13 | } 14 | 15 | #[derive(Deserialize)] 16 | pub struct Agent { 17 | pub usage_global: IndexMap, 18 | pub version_list: Vec, 19 | } 20 | 21 | #[derive(Deserialize, Clone)] 22 | pub struct VersionDetail { 23 | pub version: String, 24 | pub global_usage: f32, 25 | pub release_date: Option, // unix timestamp (seconds) 26 | } 27 | 28 | #[derive(Deserialize, Clone)] 29 | pub struct Feature { 30 | pub stats: IndexMap>, 31 | } 32 | 33 | pub fn parse_caniuse_global() -> Result { 34 | let path = root().join("node_modules/caniuse-db/fulldata-json/data-2.0.json"); 35 | let json = fs::read_to_string(path)?; 36 | Ok(serde_json::from_str(&json)?) 37 | } 38 | -------------------------------------------------------------------------------- /src/opts.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] 4 | #[serde(rename_all = "camelCase", default)] 5 | /// Options for controlling the behavior of browserslist. 6 | pub struct Opts { 7 | /// Use desktop browsers if Can I Use doesn’t have data about this mobile version. 8 | pub mobile_to_desktop: bool, 9 | 10 | /// If `true`, ignore unknown versions then return empty result; 11 | /// otherwise, reject with an error. 12 | pub ignore_unknown_versions: bool, 13 | 14 | /// Path to configuration file with queries. 15 | pub config: Option, 16 | 17 | /// Processing environment. It will be used to take right queries from config file. 18 | pub env: Option, 19 | 20 | /// File or directory path for looking for configuration file. 21 | pub path: Option, 22 | 23 | /// Throw error on missing env. 24 | pub throw_on_missing: bool, 25 | 26 | /// Disable security checks for `extends` query. 27 | pub dangerous_extend: bool, 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | types: [opened, synchronize] 9 | paths: 10 | - ".github/workflows/**" 11 | push: 12 | branches: 13 | - main 14 | paths: 15 | - ".github/workflows/**" 16 | 17 | jobs: 18 | zizmor: 19 | name: zizmor 20 | runs-on: ubuntu-latest 21 | permissions: 22 | security-events: write 23 | steps: 24 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 25 | with: 26 | persist-credentials: false 27 | 28 | - uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1 29 | with: 30 | tool: zizmor 31 | 32 | - run: zizmor --format sarif . > results.sarif 33 | env: 34 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 37 | with: 38 | sarif_file: results.sarif 39 | category: zizmor 40 | -------------------------------------------------------------------------------- /src/queries/last_n_node_major.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::data::node::NODE_VERSIONS; 3 | 4 | pub(super) fn last_n_node_major(count: usize) -> QueryResult { 5 | let mut vec = NODE_VERSIONS().iter().rev().map(|version| version.major()).collect::>(); 6 | vec.dedup(); 7 | let minimum = vec.into_iter().nth(count - 1).unwrap_or_default(); 8 | 9 | let distribs = NODE_VERSIONS() 10 | .iter() 11 | .filter(|version| version.major() >= minimum) 12 | .rev() 13 | .map(|version| Distrib::new("node", version.to_string())) 14 | .collect(); 15 | 16 | Ok(distribs) 17 | } 18 | 19 | #[cfg(all(test, not(miri)))] 20 | mod tests { 21 | use test_case::test_case; 22 | 23 | use crate::{opts::Opts, test::run_compare}; 24 | 25 | #[test_case("last 2 node major versions"; "basic")] 26 | #[test_case("last 2 Node major versions"; "case insensitive")] 27 | #[test_case("last 2 node major version"; "support pluralization")] 28 | fn valid(query: &str) { 29 | run_compare(query, &Opts::default(), None); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /xtask/src/generators/caniuse/browsers.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bincode::encode_to_vec; 3 | 4 | use crate::{data::caniuse::Caniuse, utils::save_bin_compressed}; 5 | 6 | pub fn build_caniuse_browsers(data: &Caniuse) -> Result<()> { 7 | // Prepare data for serialization - convert IndexMap to Vec for bincode compatibility 8 | let browser_data: Vec<(String, String, Vec<(String, f32, Option)>)> = data 9 | .agents 10 | .iter() 11 | .map(|(name, agent)| { 12 | let version_list = agent 13 | .version_list 14 | .iter() 15 | .map(|version| { 16 | (version.version.clone(), version.global_usage, version.release_date) 17 | }) 18 | .collect(); 19 | (name.clone(), name.clone(), version_list) 20 | }) 21 | .collect(); 22 | 23 | // Serialize and compress the data 24 | let serialized = encode_to_vec(&browser_data, bincode::config::standard())?; 25 | save_bin_compressed("caniuse_browsers.bin", &serialized); 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /src/queries/maintained_node.rs: -------------------------------------------------------------------------------- 1 | use time::OffsetDateTime; 2 | 3 | use super::{Distrib, QueryResult}; 4 | use crate::data::node::{NODE_VERSIONS, RELEASE_SCHEDULE}; 5 | 6 | pub(super) fn maintained_node() -> QueryResult { 7 | let now = OffsetDateTime::now_utc().to_julian_day(); 8 | 9 | let versions = RELEASE_SCHEDULE 10 | .iter() 11 | .filter(|(_, start, end)| *start < now && now < *end) 12 | .filter_map(|(version, _, _)| { 13 | NODE_VERSIONS().iter().rfind(|v| v.major() == version.major()) 14 | }) 15 | .map(|version| Distrib::new("node", version.to_string())) 16 | .collect(); 17 | Ok(versions) 18 | } 19 | 20 | #[cfg(all(test, not(miri)))] 21 | mod tests { 22 | use test_case::test_case; 23 | 24 | use crate::{opts::Opts, test::run_compare}; 25 | 26 | #[test_case("maintained node versions"; "basic")] 27 | #[test_case("Maintained Node Versions"; "case insensitive")] 28 | #[test_case("maintained node versions"; "more spaces")] 29 | fn valid(query: &str) { 30 | run_compare(query, &Opts::default(), None); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod caniuse; 2 | pub mod electron; 3 | pub mod node; 4 | 5 | use std::borrow::Cow; 6 | 7 | pub(crate) type BrowserName = Cow<'static, str>; 8 | 9 | pub(crate) fn decode_browser_name(id: u8) -> BrowserName { 10 | match id { 11 | 1 => Cow::Borrowed("ie"), 12 | 2 => Cow::Borrowed("edge"), 13 | 3 => Cow::Borrowed("firefox"), 14 | 4 => Cow::Borrowed("chrome"), 15 | 5 => Cow::Borrowed("safari"), 16 | 6 => Cow::Borrowed("opera"), 17 | 7 => Cow::Borrowed("ios_saf"), 18 | 8 => Cow::Borrowed("op_mini"), 19 | 9 => Cow::Borrowed("android"), 20 | 10 => Cow::Borrowed("bb"), 21 | 11 => Cow::Borrowed("op_mob"), 22 | 12 => Cow::Borrowed("and_chr"), 23 | 13 => Cow::Borrowed("and_ff"), 24 | 14 => Cow::Borrowed("ie_mob"), 25 | 15 => Cow::Borrowed("and_uc"), 26 | 16 => Cow::Borrowed("samsung"), 27 | 17 => Cow::Borrowed("and_qq"), 28 | 18 => Cow::Borrowed("baidu"), 29 | 19 => Cow::Borrowed("kaios"), 30 | _ => unreachable!("cannot recognize browser id"), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /xtask/src/utils/file.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io::Write}; 2 | 3 | use flate2::{Compression, write::DeflateEncoder}; 4 | 5 | use super::paths::root; 6 | 7 | pub fn save_bin_compressed(file: &str, bytes: &[u8]) { 8 | let mut encoder = DeflateEncoder::new(Vec::new(), Compression::best()); 9 | encoder.write_all(bytes).unwrap(); 10 | let compressed = encoder.finish().unwrap(); 11 | let file = format!("{}.deflate", file); 12 | fs::write(root().join("src/generated").join(file), compressed).unwrap(); 13 | } 14 | 15 | pub fn generate_file(file: &str, token_stream: proc_macro2::TokenStream) { 16 | let syntax_tree = syn::parse2(token_stream).unwrap(); 17 | let code = prettyplease::unparse(&syntax_tree); 18 | fs::write(root().join("src/generated").join(file), code).unwrap(); 19 | } 20 | 21 | pub fn create_range_vec(v: &Vec>) -> Vec { 22 | let mut offset = 0; 23 | // [start0, start1, ..., startN, endN] 24 | let mut ranges = vec![]; 25 | for values in v { 26 | ranges.push(offset as u32); 27 | offset += values.len(); 28 | } 29 | ranges.push(offset as u32); 30 | ranges 31 | } 32 | -------------------------------------------------------------------------------- /src/queries/dead.rs: -------------------------------------------------------------------------------- 1 | use super::QueryResult; 2 | use crate::{opts::Opts, resolve}; 3 | 4 | pub(super) fn dead(opts: &Opts) -> QueryResult { 5 | resolve( 6 | &["Baidu >= 0", "ie <= 11", "ie_mob <= 11", "bb <= 10", "op_mob <= 12.1", "samsung 4"], 7 | opts, 8 | ) 9 | } 10 | 11 | #[cfg(all(test, not(miri)))] 12 | mod tests { 13 | use test_case::test_case; 14 | 15 | use super::*; 16 | use crate::{ 17 | error::Error, 18 | test::{run_compare, should_failed}, 19 | }; 20 | 21 | #[test_case("dead"; "basic")] 22 | #[test_case("Dead"; "case insensitive")] 23 | fn default_options(query: &str) { 24 | run_compare(query, &Opts::default(), None); 25 | } 26 | 27 | #[test_case("> 0%, dead"; "all browsers")] 28 | fn mobile_to_desktop(query: &str) { 29 | run_compare(query, &Opts { mobile_to_desktop: true, ..Default::default() }, None); 30 | } 31 | 32 | #[test] 33 | fn invalid() { 34 | assert_eq!( 35 | should_failed("not dead", &Opts::default()), 36 | Error::NotAtFirst(String::from("not dead")) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/queries/unreleased_browsers.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{ 3 | data::caniuse::{caniuse_browsers, get_browser_stat}, 4 | opts::Opts, 5 | }; 6 | 7 | pub(super) fn unreleased_browsers(opts: &Opts) -> QueryResult { 8 | let distribs = caniuse_browsers() 9 | .keys() 10 | .filter_map(|name| get_browser_stat(name, opts.mobile_to_desktop)) 11 | .flat_map(|(name, stat)| { 12 | stat.version_list 13 | .iter() 14 | .filter(|version| version.release_date().is_none()) 15 | .map(move |version| Distrib::new(name, version.version())) 16 | }) 17 | .collect(); 18 | Ok(distribs) 19 | } 20 | 21 | #[cfg(all(test, not(miri)))] 22 | mod tests { 23 | use test_case::test_case; 24 | 25 | use super::*; 26 | use crate::test::run_compare; 27 | 28 | #[test_case("unreleased versions"; "basic")] 29 | #[test_case("Unreleased Versions"; "case insensitive")] 30 | #[test_case("unreleased versions"; "more spaces")] 31 | fn valid(query: &str) { 32 | run_compare(query, &Opts::default(), None); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/queries/last_n_electron_major.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::data::electron::ELECTRON_VERSIONS; 3 | 4 | pub(super) fn last_n_electron_major(count: usize) -> QueryResult { 5 | let minimum = ELECTRON_VERSIONS 6 | .iter() 7 | .rev() 8 | .nth(count - 1) 9 | .map(|(electron_version, _)| *electron_version) 10 | .unwrap_or_default(); 11 | 12 | let distribs = ELECTRON_VERSIONS 13 | .iter() 14 | .filter(|(electron_version, _)| *electron_version >= minimum) 15 | .rev() 16 | .map(|(_, chromium_version)| Distrib::new("chrome", *chromium_version)) 17 | .collect(); 18 | 19 | Ok(distribs) 20 | } 21 | 22 | #[cfg(all(test, not(miri)))] 23 | mod tests { 24 | use test_case::test_case; 25 | 26 | use crate::{opts::Opts, test::run_compare}; 27 | 28 | #[test_case("last 2 electron major versions"; "basic")] 29 | #[test_case("last 2 Electron major versions"; "case insensitive")] 30 | #[test_case("last 2 electron major version"; "support pluralization")] 31 | fn valid(query: &str) { 32 | run_compare(query, &Opts::default(), None); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/queries/unreleased_x_browsers.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{data::caniuse::get_browser_stat, error::Error, opts::Opts}; 3 | 4 | pub(super) fn unreleased_x_browsers(name: &str, opts: &Opts) -> QueryResult { 5 | let (name, stat) = get_browser_stat(name, opts.mobile_to_desktop) 6 | .ok_or_else(|| Error::BrowserNotFound(name.to_string()))?; 7 | let distribs = stat 8 | .version_list 9 | .iter() 10 | .filter(|version| version.release_date().is_none()) 11 | .map(|version| Distrib::new(name, version.version())) 12 | .collect(); 13 | Ok(distribs) 14 | } 15 | 16 | #[cfg(all(test, not(miri)))] 17 | mod tests { 18 | use test_case::test_case; 19 | 20 | use super::*; 21 | use crate::test::run_compare; 22 | 23 | #[test_case("unreleased edge versions"; "basic")] 24 | #[test_case("Unreleased Chrome Versions"; "case insensitive")] 25 | #[test_case("unreleased firefox version"; "support pluralization")] 26 | #[test_case("unreleased safari versions"; "more spaces")] 27 | fn valid(query: &str) { 28 | run_compare(query, &Opts::default(), None); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-present Pig Fang 4 | Copyright (c) 2024-present Boshen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /xtask/src/generators/electron.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use anyhow::Result; 4 | use indexmap::IndexMap; 5 | use quote::quote; 6 | 7 | use crate::utils::{generate_file, root}; 8 | 9 | pub fn build_electron_to_chromium() -> Result<()> { 10 | let data_path = root().join("node_modules/electron-to-chromium/versions.json"); 11 | let data = serde_json::from_slice::>(&fs::read(data_path)?)? 12 | .into_iter() 13 | .map(|(electron_version, chromium_version)| { 14 | let split = electron_version.split('.').collect::>(); 15 | assert!(split.len() == 2, "electron version must be in major.minor format"); 16 | let major: u16 = split[0].parse().unwrap(); 17 | let minor: u16 = split[1].parse().unwrap(); 18 | quote! { 19 | (ElectronVersion::new(#major, #minor), #chromium_version) 20 | } 21 | }); 22 | 23 | let output = quote! { 24 | use crate::data::electron::ElectronVersion; 25 | pub static ELECTRON_VERSIONS: &[(ElectronVersion, &str)] = &[#(#data),*]; 26 | }; 27 | 28 | generate_file("electron_to_chromium.rs", output); 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /benches/resolve.rs: -------------------------------------------------------------------------------- 1 | use browserslist::{Opts, resolve}; 2 | use criterion::{Criterion, black_box, criterion_group, criterion_main}; 3 | 4 | pub fn bench(c: &mut Criterion) { 5 | c.bench_function("resolve 'defaults, not dead'", |b| { 6 | b.iter(|| resolve(black_box(&["defaults, not dead"]), &black_box(Opts::default()))); 7 | }); 8 | 9 | c.bench_function("resolve '> 0.5%'", |b| { 10 | b.iter(|| resolve(black_box(&["> 0.5%"]), &black_box(Opts::default()))); 11 | }); 12 | 13 | c.bench_function("resolve 'cover 99%'", |b| { 14 | b.iter(|| resolve(black_box(&["cover 99%"]), &black_box(Opts::default()))); 15 | }); 16 | 17 | c.bench_function("resolve 'electron >= 10'", |b| { 18 | b.iter(|| resolve(black_box(&["electron >= 10"]), &black_box(Opts::default()))); 19 | }); 20 | 21 | c.bench_function("resolve 'node >= 8'", |b| { 22 | b.iter(|| resolve(black_box(&["node >= 8"]), &black_box(Opts::default()))); 23 | }); 24 | 25 | c.bench_function("resolve 'supports es6-module'", |b| { 26 | b.iter(|| resolve(black_box(&["supports es6-module"]), &black_box(Opts::default()))); 27 | }); 28 | } 29 | 30 | criterion_group!(browserslist, bench); 31 | criterion_main!(browserslist); 32 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, process::Command}; 2 | 3 | use anyhow::Result; 4 | 5 | fn main() -> Result<()> { 6 | run()?; 7 | Ok(()) 8 | } 9 | 10 | fn run() -> Result<()> { 11 | // Clean and create the generated directory 12 | let dir = project_root::get_project_root()?.join("src/generated"); 13 | let _ = fs::remove_dir_all(&dir); 14 | let _ = fs::create_dir(&dir); 15 | 16 | // Generate electron to chromium mappings 17 | xtask::generators::build_electron_to_chromium()?; 18 | 19 | // Generate node version data 20 | xtask::generators::build_node_versions()?; 21 | xtask::generators::build_node_release_schedule()?; 22 | 23 | // Parse caniuse data once and use for all generators 24 | let caniuse = xtask::data::parse_caniuse_global()?; 25 | 26 | // Generate caniuse data 27 | xtask::generators::caniuse::build_caniuse_browsers(&caniuse)?; 28 | xtask::generators::caniuse::build_caniuse_feature_matching(&caniuse)?; 29 | xtask::generators::caniuse::build_caniuse_global_usage(&caniuse)?; 30 | xtask::generators::caniuse::build_caniuse_region_matching(&caniuse)?; 31 | 32 | // Format the generated code 33 | Command::new("cargo").arg("fmt").status()?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /xtask/src/generators/caniuse/global_usage.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use quote::quote; 3 | 4 | use crate::data::{Caniuse, encode_browser_name}; 5 | use crate::utils::generate_file; 6 | 7 | pub fn build_caniuse_global_usage(data: &Caniuse) -> Result<()> { 8 | let mut global_usage = data 9 | .agents 10 | .iter() 11 | .flat_map(|(name, agent)| { 12 | let browser_id = encode_browser_name(name); 13 | agent.usage_global.iter().filter(|(_, usage)| **usage > 0.0f32).map( 14 | move |(version, usage)| { 15 | ( 16 | usage, 17 | quote! { 18 | (#browser_id, #version, #usage) 19 | }, 20 | ) 21 | }, 22 | ) 23 | }) 24 | .collect::>(); 25 | global_usage.sort_unstable_by(|(a, _), (b, _)| b.partial_cmp(a).unwrap()); 26 | let push_usage = global_usage.into_iter().map(|(_, tokens)| tokens); 27 | 28 | let output = quote! { 29 | /// only includes browsers with global usage > 0.0% 30 | pub static CANIUSE_GLOBAL_USAGE: &[(u8, &str, f32)] = &[ 31 | #(#push_usage),* 32 | ]; 33 | }; 34 | 35 | generate_file("caniuse_global_usage.rs", output); 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /src/queries/last_n_browsers.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult, count_filter_versions}; 2 | use crate::{ 3 | data::caniuse::{caniuse_browsers, get_browser_stat}, 4 | opts::Opts, 5 | }; 6 | 7 | pub(super) fn last_n_browsers(count: usize, opts: &Opts) -> QueryResult { 8 | let distribs = caniuse_browsers() 9 | .keys() 10 | .filter_map(|name| get_browser_stat(name, opts.mobile_to_desktop)) 11 | .flat_map(|(name, stat)| { 12 | let count = count_filter_versions(name, opts.mobile_to_desktop, count); 13 | 14 | stat.version_list 15 | .iter() 16 | .filter(|version| version.release_date().is_some()) 17 | .rev() 18 | .take(count) 19 | .map(move |version| Distrib::new(name, version.version())) 20 | }) 21 | .collect(); 22 | 23 | Ok(distribs) 24 | } 25 | 26 | #[cfg(all(test, not(miri)))] 27 | mod tests { 28 | use test_case::test_case; 29 | 30 | use super::*; 31 | use crate::test::run_compare; 32 | 33 | #[test_case("last 2 versions"; "basic")] 34 | #[test_case("last 31 versions"; "android")] 35 | #[test_case("last 1 version"; "support pluralization")] 36 | #[test_case("Last 02 Versions"; "case insensitive")] 37 | fn valid(query: &str) { 38 | run_compare(query, &Opts::default(), None); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/data/electron.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | pub use crate::generated::electron_to_chromium::ELECTRON_VERSIONS; 3 | 4 | #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] 5 | pub struct ElectronVersion { 6 | pub major: u16, 7 | pub minor: u16, 8 | } 9 | 10 | impl ElectronVersion { 11 | pub const fn new(major: u16, minor: u16) -> Self { 12 | Self { major, minor } 13 | } 14 | 15 | pub fn parse(major: &str, minor: &str) -> Result { 16 | let major = major.parse()?; 17 | let minor = minor.parse()?; 18 | Ok(Self { major, minor }) 19 | } 20 | } 21 | 22 | pub fn parse_version(version: &str) -> Result { 23 | let mut split = version.split('.'); 24 | 25 | let Some(first) = split.next() else { 26 | return Err(err(version)); 27 | }; 28 | 29 | let Some(second) = split.next().filter(|n| check_number(n)) else { 30 | return Err(err(version)); 31 | }; 32 | 33 | if split.next().is_some() && split.next().is_some() { 34 | return Err(err(version)); 35 | } 36 | 37 | let election_version = ElectronVersion::parse(first, second).map_err(|_| err(version))?; 38 | Ok(election_version) 39 | } 40 | 41 | fn check_number(n: &str) -> bool { 42 | if n == "0" { true } else { !n.starts_with('0') } 43 | } 44 | 45 | fn err(version: &str) -> Error { 46 | Error::UnknownElectronVersion(version.to_string()) 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | types: [opened, synchronize] 9 | paths: 10 | - "**/*.rs" 11 | - "Cargo.lock" 12 | - ".github/workflows/benchmark.yml" 13 | push: 14 | branches: 15 | - main 16 | paths: 17 | - "**/*.rs" 18 | - "Cargo.lock" 19 | - ".github/workflows/benchmark.yml" 20 | 21 | concurrency: 22 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 23 | cancel-in-progress: true 24 | 25 | defaults: 26 | run: 27 | shell: bash 28 | 29 | jobs: 30 | benchmark: 31 | name: Benchmark 32 | runs-on: ubuntu-latest 33 | permissions: 34 | id-token: write # required for OIDC authentication with CodSpeed 35 | steps: 36 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 37 | 38 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 39 | with: 40 | cache-key: benchmark 41 | save-cache: ${{ github.ref_name == 'main' }} 42 | tools: cargo-codspeed 43 | 44 | - name: Build benchmark 45 | run: cargo codspeed build -p oxc-browserslist --features codspeed 46 | 47 | - name: Run benchmark 48 | uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1 49 | timeout-minutes: 15 50 | with: 51 | mode: instrumentation 52 | run: cargo codspeed run 53 | -------------------------------------------------------------------------------- /.github/workflows/miri.yml: -------------------------------------------------------------------------------- 1 | name: Miri 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | types: [opened, synchronize] 9 | paths: 10 | - "src/**" 11 | - ".github/workflows/miri.yml" 12 | push: 13 | branches: 14 | - main 15 | paths: 16 | - "src/**" 17 | - ".github/workflows/miri.yml" 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | miri: 25 | name: Miri 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 30 | 31 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 32 | with: 33 | cache-key: miri 34 | save-cache: ${{ github.ref_name == 'main' }} 35 | 36 | - uses: oxc-project/setup-node@141eb77546de6702f92d320926403fe3f9f6a6f2 # v1.0.5 37 | 38 | - name: Install Miri 39 | run: | 40 | rustup toolchain install nightly --component miri 41 | rustup override set nightly 42 | cargo miri setup 43 | 44 | # `--lib --bins --tests` omits doctests, which Miri can't run 45 | # https://github.com/oxc-project/oxc/pull/11092 46 | - name: Test with Miri 47 | run: | 48 | cargo miri test --lib --bins --tests --all-features 49 | env: 50 | MIRIFLAGS: -Zmiri-disable-isolation 51 | -------------------------------------------------------------------------------- /src/queries/percentage.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{data::caniuse::caniuse_browsers, parser::Comparator}; 3 | 4 | pub(super) fn percentage(comparator: Comparator, popularity: f32) -> QueryResult { 5 | let distribs = caniuse_browsers() 6 | .iter() 7 | .flat_map(|(name, stat)| { 8 | stat.version_list 9 | .iter() 10 | .filter(|version| { 11 | let usage = version.global_usage(); 12 | match comparator { 13 | Comparator::Greater => usage > popularity, 14 | Comparator::GreaterOrEqual => usage >= popularity, 15 | Comparator::Less => usage < popularity, 16 | Comparator::LessOrEqual => usage <= popularity, 17 | } 18 | }) 19 | .map(move |version| Distrib::new(name.as_ref(), version.version())) 20 | }) 21 | .collect(); 22 | Ok(distribs) 23 | } 24 | 25 | #[cfg(all(test, not(miri)))] 26 | mod tests { 27 | use test_case::test_case; 28 | 29 | use crate::{opts::Opts, test::run_compare}; 30 | 31 | #[test_case("> 10%"; "greater")] 32 | #[test_case(">= 5%"; "greater or equal")] 33 | #[test_case("< 5%"; "less")] 34 | #[test_case("<= 5%"; "less or equal")] 35 | #[test_case(">10%"; "no space")] 36 | #[test_case("> 10.2%"; "with float")] 37 | #[test_case("> .2%"; "with float that has a leading dot")] 38 | fn valid(query: &str) { 39 | run_compare(query, &Opts::default(), None); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/queries/years.rs: -------------------------------------------------------------------------------- 1 | use time::{Duration, OffsetDateTime}; 2 | 3 | use super::{Distrib, QueryResult}; 4 | use crate::{ 5 | data::caniuse::{caniuse_browsers, get_browser_stat}, 6 | error::Error, 7 | opts::Opts, 8 | }; 9 | 10 | const ONE_YEAR_IN_SECONDS: f64 = 365.259_641 * 24.0 * 60.0 * 60.0; 11 | 12 | pub(super) fn years(count: f64, opts: &Opts) -> QueryResult { 13 | let duration = 14 | Duration::checked_seconds_f64(count * ONE_YEAR_IN_SECONDS).ok_or(Error::YearOverflow)?; 15 | let time = (OffsetDateTime::now_utc() - duration).unix_timestamp(); 16 | 17 | let distribs = caniuse_browsers() 18 | .keys() 19 | .filter_map(|name| get_browser_stat(name, opts.mobile_to_desktop)) 20 | .flat_map(|(name, stat)| { 21 | stat.version_list 22 | .iter() 23 | .filter( 24 | |version| matches!(version.release_date(), Some(date) if date.get() >= time), 25 | ) 26 | .map(move |version| Distrib::new(name, version.version())) 27 | }) 28 | .collect(); 29 | Ok(distribs) 30 | } 31 | 32 | #[cfg(all(test, not(miri)))] 33 | mod tests { 34 | use test_case::test_case; 35 | 36 | use super::*; 37 | use crate::test::run_compare; 38 | 39 | #[test_case("last 2 years"; "basic")] 40 | #[test_case("last 1 year"; "one year")] 41 | #[test_case("last 1.4 years"; "year fraction")] 42 | #[test_case("Last 5 Years"; "case insensitive")] 43 | #[test_case("last 2 years"; "more spaces")] 44 | fn valid(query: &str) { 45 | run_compare(query, &Opts::default(), None); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/queries/last_n_x_browsers.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult, count_filter_versions}; 2 | use crate::{data::caniuse::get_browser_stat, error::Error, opts::Opts}; 3 | 4 | pub(super) fn last_n_x_browsers(count: usize, name: &str, opts: &Opts) -> QueryResult { 5 | let (name, stat) = get_browser_stat(name, opts.mobile_to_desktop) 6 | .ok_or_else(|| Error::BrowserNotFound(name.to_string()))?; 7 | let count = count_filter_versions(name, opts.mobile_to_desktop, count); 8 | 9 | let distribs = stat 10 | .version_list 11 | .iter() 12 | .filter(|version| version.release_date().is_some()) 13 | .rev() 14 | .take(count) 15 | .map(|version| Distrib::new(name, version.version())) 16 | .collect(); 17 | Ok(distribs) 18 | } 19 | 20 | #[cfg(all(test, not(miri)))] 21 | mod tests { 22 | use test_case::test_case; 23 | 24 | use super::*; 25 | use crate::test::run_compare; 26 | 27 | #[test_case("last 2 ie versions"; "basic")] 28 | #[test_case("last 2 safari versions"; "do not include unreleased versions")] 29 | #[test_case("last 1 ie version"; "support pluralization")] 30 | #[test_case("last 01 Explorer version"; "alias")] 31 | #[test_case("Last 01 IE Version"; "case insensitive")] 32 | #[test_case("last 4 android versions"; "android 1")] 33 | #[test_case("last 5 android versions"; "android 2")] 34 | #[test_case("last 31 android versions"; "android 3")] 35 | #[test_case("last 4 op_mob versions"; "op_mob 1")] 36 | #[test_case("last 5 op_mob versions"; "op_mob 2")] 37 | fn valid(query: &str) { 38 | run_compare(query, &Opts::default(), None); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/data/caniuse/features.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use super::{ 4 | BrowserName, 5 | compression::{decode, decompress_deflate}, 6 | }; 7 | 8 | use crate::data::decode_browser_name; 9 | pub use crate::generated::caniuse_feature_matching::get_feature_stat; 10 | 11 | static FEATURES_COMPRESSED: &[u8] = 12 | include_bytes!("../../generated/caniuse_feature_matching.bin.deflate"); 13 | static FEATURES_DECOMPRESSED: OnceLock> = OnceLock::new(); 14 | 15 | pub struct FeatureSet { 16 | yes: Vec, 17 | partial: Vec, 18 | } 19 | 20 | impl FeatureSet { 21 | pub fn new(yes: Vec<&'static str>, partial: Vec<&'static str>) -> Self { 22 | Self { yes, partial } 23 | } 24 | 25 | pub fn supports(&self, version: &str, include_partial: bool) -> bool { 26 | self.yes.binary_search(&version).is_ok() 27 | || (include_partial && self.partial.binary_search(&version).is_ok()) 28 | } 29 | } 30 | 31 | pub struct Feature { 32 | start: u32, 33 | end: u32, 34 | } 35 | 36 | impl Feature { 37 | pub fn new(start: u32, end: u32) -> Self { 38 | Self { start, end } 39 | } 40 | 41 | pub fn create_data(&self) -> Vec<(BrowserName, FeatureSet)> { 42 | let data = FEATURES_DECOMPRESSED.get_or_init(|| decompress_deflate(FEATURES_COMPRESSED)); 43 | let features = 44 | decode::<(u8, Vec<&'static str>, Vec<&'static str>)>(data, self.start, self.end); 45 | features 46 | .into_iter() 47 | .map(|(b, yes, partial)| (decode_browser_name(b), FeatureSet::new(yes, partial))) 48 | .collect::>() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/queries/cover_by_region.rs: -------------------------------------------------------------------------------- 1 | use std::ops::ControlFlow; 2 | 3 | use super::{Distrib, QueryResult}; 4 | use crate::{data::caniuse::region::get_usage_by_region, error::Error}; 5 | 6 | pub(super) fn cover_by_region(coverage: f32, region: &str) -> QueryResult { 7 | let normalized_region = 8 | if region.len() == 2 { region.to_uppercase() } else { region.to_lowercase() }; 9 | 10 | if let Some(region_data) = get_usage_by_region(&normalized_region) { 11 | let result = region_data.iter().try_fold( 12 | (vec![], 0.0), 13 | |(mut distribs, total), (name, version, usage)| { 14 | if total >= coverage || usage == 0.0 { 15 | ControlFlow::Break((distribs, total)) 16 | } else { 17 | distribs.push(Distrib::new(name, version)); 18 | ControlFlow::Continue((distribs, total + usage)) 19 | } 20 | }, 21 | ); 22 | match result { 23 | ControlFlow::Break((distribs, _)) => Ok(distribs), 24 | _ => unreachable!(), 25 | } 26 | } else { 27 | Err(Error::UnknownRegion(region.to_string())) 28 | } 29 | } 30 | 31 | #[cfg(all(test, not(miri)))] 32 | mod tests { 33 | use test_case::test_case; 34 | 35 | use crate::{opts::Opts, test::run_compare}; 36 | 37 | #[test_case("cover 0.1% in US"; "country")] 38 | #[test_case("Cover 0.1% in us"; "country case insensitive")] 39 | #[test_case("cover 0.1% in alt-eu"; "country alt")] 40 | #[test_case("Cover 0.1% in Alt-EU"; "country alt case insensitive")] 41 | fn valid(query: &str) { 42 | run_compare(query, &Opts::default(), None); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/queries/node_bounded_range.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use super::{Distrib, QueryResult}; 4 | use crate::data::node::NODE_VERSIONS; 5 | 6 | pub(super) fn node_bounded_range(from: &str, to: &str) -> QueryResult { 7 | let distribs = NODE_VERSIONS() 8 | .iter() 9 | .filter(|version| { 10 | matches!(version.loose_compare(from), Ordering::Greater | Ordering::Equal) 11 | && matches!(version.loose_compare(to), Ordering::Less | Ordering::Equal) 12 | }) 13 | .map(|version| Distrib::new("node", version.to_string())) 14 | .collect(); 15 | Ok(distribs) 16 | } 17 | 18 | #[cfg(all(test, not(miri)))] 19 | mod tests { 20 | use test_case::test_case; 21 | 22 | use crate::{ 23 | error::Error, 24 | opts::Opts, 25 | test::{run_compare, should_failed}, 26 | }; 27 | 28 | #[test_case("node 4-6"; "semver major only")] 29 | #[test_case("node 4-6.0.0"; "different semver formats")] 30 | #[test_case("node 6.5-7.5"; "with semver minor")] 31 | #[test_case("node 6.6.4-7.7.5"; "with semver patch")] 32 | #[test_case("Node 4 - 6"; "more spaces 1")] 33 | #[test_case("node 6.5 - 7.5"; "more spaces 2")] 34 | #[test_case("node 6.6.4 - 7.7.5"; "more spaces 3")] 35 | #[test_case("node 8.8.8.8-9.9.9.9"; "malformed version")] 36 | fn valid(query: &str) { 37 | run_compare(query, &Opts::default(), None); 38 | } 39 | 40 | #[test_case( 41 | "node 6-8.a", Error::Parse(String::from("a")); 42 | "malformed version" 43 | )] 44 | fn invalid(query: &str, error: Error) { 45 | assert_eq!(should_failed(query, &Opts::default()), error); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/generated/node_release_schedule.rs: -------------------------------------------------------------------------------- 1 | use crate::semver::Version; 2 | pub static RELEASE_SCHEDULE: &[(Version, i32, i32)] = &[ 3 | (Version(0u16, 8u16, 0u16), 2456104i32, 2456870i32), 4 | (Version(0u16, 10u16, 0u16), 2456363i32, 2457693i32), 5 | (Version(0u16, 12u16, 0u16), 2457060i32, 2457754i32), 6 | (Version(4u16, 0u16, 0u16), 2457274i32, 2458239i32), 7 | (Version(5u16, 0u16, 0u16), 2457325i32, 2457570i32), 8 | (Version(6u16, 0u16, 0u16), 2457505i32, 2458604i32), 9 | (Version(7u16, 0u16, 0u16), 2457687i32, 2457935i32), 10 | (Version(8u16, 0u16, 0u16), 2457904i32, 2458849i32), 11 | (Version(9u16, 0u16, 0u16), 2458028i32, 2458300i32), 12 | (Version(10u16, 0u16, 0u16), 2458233i32, 2459335i32), 13 | (Version(11u16, 0u16, 0u16), 2458415i32, 2458636i32), 14 | (Version(12u16, 0u16, 0u16), 2458597i32, 2459700i32), 15 | (Version(13u16, 0u16, 0u16), 2458779i32, 2459002i32), 16 | (Version(14u16, 0u16, 0u16), 2458961i32, 2460065i32), 17 | (Version(15u16, 0u16, 0u16), 2459143i32, 2459367i32), 18 | (Version(16u16, 0u16, 0u16), 2459325i32, 2460199i32), 19 | (Version(17u16, 0u16, 0u16), 2459507i32, 2459732i32), 20 | (Version(18u16, 0u16, 0u16), 2459689i32, 2460796i32), 21 | (Version(19u16, 0u16, 0u16), 2459871i32, 2460097i32), 22 | (Version(20u16, 0u16, 0u16), 2460053i32, 2461161i32), 23 | (Version(21u16, 0u16, 0u16), 2460235i32, 2460463i32), 24 | (Version(22u16, 0u16, 0u16), 2460425i32, 2461526i32), 25 | (Version(23u16, 0u16, 0u16), 2460600i32, 2460828i32), 26 | (Version(24u16, 0u16, 0u16), 2460802i32, 2461892i32), 27 | (Version(25u16, 0u16, 0u16), 2460964i32, 2461193i32), 28 | (Version(26u16, 0u16, 0u16), 2461153i32, 2462257i32), 29 | ]; 30 | -------------------------------------------------------------------------------- /src/queries/since.rs: -------------------------------------------------------------------------------- 1 | use time::{Date, Month, OffsetDateTime, Time}; 2 | 3 | use super::{Distrib, QueryResult}; 4 | use crate::{ 5 | data::caniuse::{caniuse_browsers, get_browser_stat}, 6 | error::Error, 7 | opts::Opts, 8 | }; 9 | 10 | pub(super) fn since(year: i32, month: u32, day: u32, opts: &Opts) -> QueryResult { 11 | let month = Month::try_from(month as u8) 12 | .map_err(|_| Error::InvalidDate(format!("{year}-{month}-{day}")))?; 13 | let date = Date::from_calendar_date(year, month, day as u8) 14 | .map_err(|_| Error::InvalidDate(format!("{year}-{month}-{day}")))?; 15 | let time = OffsetDateTime::new_utc(date, Time::MIDNIGHT).unix_timestamp(); 16 | 17 | let distribs = caniuse_browsers() 18 | .keys() 19 | .filter_map(|name| get_browser_stat(name, opts.mobile_to_desktop)) 20 | .flat_map(|(name, stat)| { 21 | stat.version_list 22 | .iter() 23 | .filter( 24 | |version| matches!(version.release_date(), Some(date) if date.get() >= time), 25 | ) 26 | .map(move |version| Distrib::new(name, version.version())) 27 | }) 28 | .collect(); 29 | Ok(distribs) 30 | } 31 | 32 | #[cfg(all(test, not(miri)))] 33 | mod tests { 34 | use test_case::test_case; 35 | 36 | use super::*; 37 | use crate::test::run_compare; 38 | 39 | #[test_case("since 2017"; "year only")] 40 | #[test_case("Since 2017"; "case insensitive")] 41 | #[test_case("since 2017-02"; "with month")] 42 | #[test_case("since 2017-02-15"; "with day")] 43 | #[test_case("since 1970"; "unix timestamp zero")] 44 | fn valid(query: &str) { 45 | run_compare(query, &Opts::default(), None); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/queries/electron_unbounded_range.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{ 3 | data::electron::{ELECTRON_VERSIONS, parse_version}, 4 | parser::Comparator, 5 | }; 6 | 7 | pub(super) fn electron_unbounded_range(comparator: Comparator, version: &str) -> QueryResult { 8 | let version = parse_version(version)?; 9 | 10 | let distribs = ELECTRON_VERSIONS 11 | .iter() 12 | .filter(|(electron_version, _)| match comparator { 13 | Comparator::Greater => *electron_version > version, 14 | Comparator::Less => *electron_version < version, 15 | Comparator::GreaterOrEqual => *electron_version >= version, 16 | Comparator::LessOrEqual => *electron_version <= version, 17 | }) 18 | .map(|(_, chromium_version)| Distrib::new("chrome", *chromium_version)) 19 | .collect(); 20 | Ok(distribs) 21 | } 22 | 23 | #[cfg(all(test, not(miri)))] 24 | mod tests { 25 | use test_case::test_case; 26 | 27 | use crate::{ 28 | error::Error, 29 | opts::Opts, 30 | test::{run_compare, should_failed}, 31 | }; 32 | 33 | #[test_case("electron <= 0.21"; "basic")] 34 | #[test_case("Electron < 0.21"; "case insensitive")] 35 | #[test_case("Electron < 0.21.5"; "with semver patch version")] 36 | fn valid(query: &str) { 37 | run_compare(query, &Opts::default(), None); 38 | } 39 | 40 | #[test_case( 41 | "electron < 8.a", Error::Parse(String::from("a")); 42 | "malformed version 1" 43 | )] 44 | #[test_case( 45 | "electron >= 1.1.1.1", Error::UnknownElectronVersion(String::from("1.1.1.1")); 46 | "malformed version 2" 47 | )] 48 | fn invalid(query: &str, error: Error) { 49 | assert_eq!(should_failed(query, &Opts::default()), error); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, path::Path, process::Command}; 2 | 3 | use crate::{Error, Opts, resolve}; 4 | 5 | #[expect(clippy::print_stdout)] 6 | #[track_caller] 7 | pub fn run_compare(query: &str, opts: &Opts, cwd: Option<&Path>) { 8 | #[cfg(target_os = "windows")] 9 | let path = "./node_modules/.bin/browserslist.exe"; 10 | #[cfg(not(target_os = "windows"))] 11 | let path = "./node_modules/.bin/browserslist"; 12 | let mut command = Command::new(Path::new(path).canonicalize().unwrap()); 13 | if opts.mobile_to_desktop { 14 | command.arg("--mobile-to-desktop"); 15 | } 16 | if opts.ignore_unknown_versions { 17 | command.arg("--ignore-unknown-versions"); 18 | } 19 | if let Some(env) = &opts.env { 20 | command.env("BROWSERSLIST_ENV", env); 21 | } 22 | if opts.dangerous_extend { 23 | command.env("BROWSERSLIST_DANGEROUS_EXTEND", "1"); 24 | } 25 | command.arg(query); 26 | if let Some(cwd) = cwd { 27 | command.current_dir(cwd); 28 | } 29 | let output = String::from_utf8(command.output().unwrap().stdout).unwrap(); 30 | let expected = output 31 | .trim() 32 | .split('\n') 33 | .filter(|line| !line.is_empty()) 34 | .map(|s| s.to_string()) 35 | .collect::>(); 36 | 37 | let actual = 38 | resolve(&[query], opts).unwrap().iter().map(ToString::to_string).collect::>(); 39 | 40 | if expected != actual { 41 | println!("actual - expected: {:?}", actual.difference(&expected).collect::>()); 42 | println!("expected - actual: {:?}", expected.difference(&actual).collect::>()); 43 | panic!(); 44 | } 45 | } 46 | 47 | #[track_caller] 48 | pub fn should_failed(query: &str, opts: &Opts) -> Error { 49 | resolve(&[query], opts).unwrap_err() 50 | } 51 | -------------------------------------------------------------------------------- /src/queries/current_node.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::error::Error; 3 | 4 | pub(super) fn current_node() -> QueryResult { 5 | #[cfg(target_arch = "wasm32")] 6 | { 7 | #[cfg(feature = "wasm_bindgen")] 8 | { 9 | use js_sys::{Reflect, global}; 10 | 11 | let obj_process = Reflect::get(&global(), &"process".into()) 12 | .map_err(|_| Error::UnsupportedCurrentNode)?; 13 | let obj_versions = Reflect::get(&obj_process, &"versions".into()) 14 | .map_err(|_| Error::UnsupportedCurrentNode)?; 15 | let version = Reflect::get(&obj_versions, &"node".into()) 16 | .map_err(|_| Error::UnsupportedCurrentNode)? 17 | .as_string() 18 | .ok_or(Error::UnsupportedCurrentNode)?; 19 | return Ok(vec![Distrib::new("node", version)]); 20 | } 21 | 22 | #[cfg(not(feature = "wasm_bindgen"))] 23 | Err(Error::UnsupportedCurrentNode) 24 | } 25 | 26 | #[cfg(not(target_arch = "wasm32"))] 27 | { 28 | use std::process::Command; 29 | 30 | let output = 31 | Command::new("node").arg("-v").output().map_err(|_| Error::UnsupportedCurrentNode)?; 32 | let version = 33 | String::from_utf8_lossy(&output.stdout).trim().trim_start_matches('v').to_owned(); 34 | 35 | Ok(vec![Distrib::new("node", version)]) 36 | } 37 | } 38 | 39 | #[cfg(all(test, not(miri)))] 40 | mod tests { 41 | use test_case::test_case; 42 | 43 | use crate::{opts::Opts, test::run_compare}; 44 | 45 | #[test_case("current node"; "basic")] 46 | #[test_case("Current Node"; "case insensitive")] 47 | #[test_case("current node"; "more spaces")] 48 | fn valid(query: &str) { 49 | run_compare(query, &Opts::default(), None); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/queries/last_n_major_browsers.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult, count_filter_versions}; 2 | use crate::{ 3 | data::caniuse::{caniuse_browsers, get_browser_stat}, 4 | opts::Opts, 5 | }; 6 | 7 | pub(super) fn last_n_major_browsers(count: usize, opts: &Opts) -> QueryResult { 8 | let distribs = caniuse_browsers() 9 | .keys() 10 | .filter_map(|name| get_browser_stat(name, opts.mobile_to_desktop)) 11 | .flat_map(|(name, stat)| { 12 | let count = count_filter_versions(name, opts.mobile_to_desktop, count); 13 | 14 | let mut vec = stat 15 | .version_list 16 | .iter() 17 | .filter(|version| version.release_date().is_some()) 18 | .rev() 19 | .map(|version| version.version().split('.').next().unwrap()) 20 | .collect::>(); 21 | vec.dedup(); 22 | let minimum = vec.get(count - 1).and_then(|minimum| minimum.parse().ok()).unwrap_or(0); 23 | 24 | stat.version_list 25 | .iter() 26 | .filter(|version| version.release_date().is_some()) 27 | .map(|version| version.version()) 28 | .filter(move |version| { 29 | version.split('.').next().unwrap().parse().unwrap_or(0) >= minimum 30 | }) 31 | .rev() 32 | .map(move |version| Distrib::new(name, version)) 33 | }) 34 | .collect(); 35 | 36 | Ok(distribs) 37 | } 38 | 39 | #[cfg(all(test, not(miri)))] 40 | mod tests { 41 | use test_case::test_case; 42 | 43 | use super::*; 44 | use crate::test::run_compare; 45 | 46 | #[test_case("last 2 major versions"; "basic")] 47 | #[test_case("last 1 major version"; "support pluralization")] 48 | #[test_case("Last 01 MaJoR Version"; "case insensitive")] 49 | fn valid(query: &str) { 50 | run_compare(query, &Opts::default(), None); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Update browserslist 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 0 * * *" # Everyday 9 | 10 | jobs: 11 | cron: 12 | name: Cron 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 16 | 17 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 18 | with: 19 | save-cache: true 20 | components: rustfmt # for `cargo codegen` 21 | 22 | - uses: oxc-project/setup-node@141eb77546de6702f92d320926403fe3f9f6a6f2 # v1.0.5 23 | 24 | - name: Get old browserslist version 25 | id: old-version 26 | run: echo "version=$(jq -r '.version' node_modules/browserslist/package.json)" >> $GITHUB_OUTPUT 27 | 28 | - run: pnpm update --prod 29 | 30 | - name: Get new browserslist version 31 | id: new-version 32 | run: echo "version=$(jq -r '.version' node_modules/browserslist/package.json)" >> $GITHUB_OUTPUT 33 | 34 | - run: cargo codegen 35 | 36 | - run: cargo test 37 | 38 | - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 39 | id: cpr 40 | with: 41 | token: ${{ secrets.OXC_BOT_PAT }} 42 | commit-message: Update browserslist 43 | branch: update 44 | branch-suffix: timestamp 45 | title: Update browserslist from ${{ steps.old-version.outputs.version }} to ${{ steps.new-version.outputs.version }} 46 | assignees: Boshen 47 | base: main 48 | 49 | - uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3 50 | if: steps.cpr.outputs.pull-request-operation == 'created' 51 | with: 52 | token: ${{ secrets.OXC_BOT_PAT }} 53 | pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} 54 | merge-method: squash 55 | -------------------------------------------------------------------------------- /src/queries/electron_accurate.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{ 3 | data::electron::{ELECTRON_VERSIONS, parse_version}, 4 | error::Error, 5 | }; 6 | 7 | pub(super) fn electron_accurate(version: &str) -> QueryResult { 8 | let version_str = version; 9 | let version = parse_version(version)?; 10 | 11 | let distribs = ELECTRON_VERSIONS 12 | .iter() 13 | .find(|(electron_version, _)| *electron_version == version) 14 | .map(|(_, chromium_version)| vec![Distrib::new("chrome", *chromium_version)]) 15 | .ok_or_else(|| Error::UnknownElectronVersion(version_str.to_string()))?; 16 | Ok(distribs) 17 | } 18 | 19 | #[cfg(all(test, not(miri)))] 20 | mod tests { 21 | use test_case::test_case; 22 | 23 | use super::*; 24 | use crate::{ 25 | opts::Opts, 26 | test::{run_compare, should_failed}, 27 | }; 28 | 29 | #[test_case("electron 1.1"; "basic")] 30 | #[test_case("electron 4.0.4"; "with semver patch version")] 31 | #[test_case("Electron 1.1"; "case insensitive")] 32 | fn valid(query: &str) { 33 | run_compare(query, &Opts::default(), None); 34 | } 35 | 36 | #[test_case( 37 | "electron 0.19", Error::UnknownElectronVersion(String::from("0.19")); 38 | "unknown version" 39 | )] 40 | #[test_case( 41 | "electron 8.a", Error::Parse(String::from("a")); 42 | "malformed version 1" 43 | )] 44 | #[test_case( 45 | "electron 1.1.1.1", Error::UnknownElectronVersion(String::from("1.1.1.1")); 46 | "malformed version 2" 47 | )] 48 | #[test_case( 49 | "electron 7.01", Error::UnknownElectronVersion(String::from("7.01")); 50 | "malformed version 3" 51 | )] 52 | #[test_case( 53 | "electron 999.0", Error::UnknownElectronVersion(String::from("999.0")); 54 | "malformed version 4" 55 | )] 56 | fn invalid(query: &str, error: Error) { 57 | assert_eq!(should_failed(query, &Opts::default()), error); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/queries/node_unbounded_range.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering, str::FromStr}; 2 | 3 | use super::{Distrib, QueryResult}; 4 | use crate::{data::node::NODE_VERSIONS, parser::Comparator, semver::Version}; 5 | 6 | pub(super) fn node_unbounded_range(comparator: Comparator, version: &str) -> QueryResult { 7 | let version = Version::from_str(version).unwrap(); 8 | let distribs = NODE_VERSIONS() 9 | .iter() 10 | .filter(|v| { 11 | let ord = (*v).cmp(&version); 12 | match comparator { 13 | Comparator::Greater => matches!(ord, Ordering::Greater), 14 | Comparator::Less => matches!(ord, Ordering::Less), 15 | Comparator::GreaterOrEqual => matches!(ord, Ordering::Greater | Ordering::Equal), 16 | Comparator::LessOrEqual => matches!(ord, Ordering::Less | Ordering::Equal), 17 | } 18 | }) 19 | .map(|version| Distrib::new("node", version.to_string())) 20 | .collect(); 21 | Ok(distribs) 22 | } 23 | 24 | #[cfg(all(test, not(miri)))] 25 | mod tests { 26 | use test_case::test_case; 27 | 28 | use crate::{ 29 | error::Error, 30 | opts::Opts, 31 | test::{run_compare, should_failed}, 32 | }; 33 | 34 | #[test_case("node <= 5"; "less or equal")] 35 | #[test_case("node < 5"; "less")] 36 | #[test_case("node >= 9"; "greater or equal")] 37 | #[test_case("node > 9"; "greater")] 38 | #[test_case("Node <= 5"; "case insensitive")] 39 | #[test_case("node > 10.12"; "with semver minor")] 40 | #[test_case("node > 10.12.1"; "with semver patch")] 41 | #[test_case("node >= 8.8.8.8"; "malformed version")] 42 | fn valid(query: &str) { 43 | run_compare(query, &Opts::default(), None); 44 | } 45 | 46 | #[test_case( 47 | "node < 8.a", Error::Parse(String::from("a")); 48 | "malformed version" 49 | )] 50 | fn invalid(query: &str, error: Error) { 51 | assert_eq!(should_failed(query, &Opts::default()), error); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/queries/electron_bounded_range.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{ 3 | data::electron::{ELECTRON_VERSIONS, parse_version}, 4 | error::Error, 5 | }; 6 | 7 | pub(super) fn electron_bounded_range(from: &str, to: &str) -> QueryResult { 8 | let from_str = from; 9 | let to_str = to; 10 | let from = parse_version(from)?; 11 | let to = parse_version(to)?; 12 | 13 | if ELECTRON_VERSIONS.iter().all(|(version, _)| *version != from) { 14 | return Err(Error::UnknownElectronVersion(from_str.to_string())); 15 | } 16 | if ELECTRON_VERSIONS.iter().all(|(version, _)| *version != to) { 17 | return Err(Error::UnknownElectronVersion(to_str.to_string())); 18 | } 19 | 20 | let distribs = ELECTRON_VERSIONS 21 | .iter() 22 | .filter(|(version, _)| from <= *version && *version <= to) 23 | .map(|(_, version)| Distrib::new("chrome", *version)) 24 | .collect(); 25 | Ok(distribs) 26 | } 27 | 28 | #[cfg(all(test, not(miri)))] 29 | mod tests { 30 | use test_case::test_case; 31 | 32 | use super::*; 33 | use crate::{ 34 | opts::Opts, 35 | test::{run_compare, should_failed}, 36 | }; 37 | 38 | #[test_case("electron 0.36-1.2"; "basic")] 39 | #[test_case("Electron 0.37-1.0"; "case insensitive")] 40 | #[test_case("electron 0.37.5-1.0.3"; "with semver patch version")] 41 | fn valid(query: &str) { 42 | run_compare(query, &Opts::default(), None); 43 | } 44 | 45 | #[test_case( 46 | "electron 0.1-1.2", Error::UnknownElectronVersion(String::from("0.1")); 47 | "unknown version 1" 48 | )] 49 | #[test_case( 50 | "electron 0.37-999.0", Error::UnknownElectronVersion(String::from("999.0")); 51 | "unknown version 2" 52 | )] 53 | #[test_case( 54 | "electron 1-8.a", Error::Parse(String::from("a")); 55 | "malformed version 1" 56 | )] 57 | #[test_case( 58 | "electron 1.1.1.1-2", Error::UnknownElectronVersion(String::from("1.1.1.1")); 59 | "malformed version 2" 60 | )] 61 | fn invalid(query: &str, error: Error) { 62 | assert_eq!(should_failed(query, &Opts::default()), error); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/queries/last_n_x_major_browsers.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult, count_filter_versions}; 2 | use crate::{data::caniuse::get_browser_stat, error::Error, opts::Opts}; 3 | 4 | pub(super) fn last_n_x_major_browsers(count: usize, name: &str, opts: &Opts) -> QueryResult { 5 | let (name, stat) = get_browser_stat(name, opts.mobile_to_desktop) 6 | .ok_or_else(|| Error::BrowserNotFound(name.to_string()))?; 7 | let count = count_filter_versions(name, opts.mobile_to_desktop, count); 8 | let mut vec = stat 9 | .version_list 10 | .iter() 11 | .filter(|version| version.release_date().is_some()) 12 | .map(|version| version.version()) 13 | .rev() 14 | .map(|version| version.split('.').next().unwrap()) 15 | .collect::>(); 16 | vec.dedup(); 17 | let minimum = vec.get(count - 1).and_then(|minimum| minimum.parse().ok()).unwrap_or(0); 18 | 19 | let distribs = stat 20 | .version_list 21 | .iter() 22 | .filter(|version| version.release_date().is_some()) 23 | .map(|version| version.version()) 24 | .filter(move |version| version.split('.').next().unwrap().parse().unwrap_or(0) >= minimum) 25 | .rev() 26 | .map(move |version| Distrib::new(name, version)) 27 | .collect(); 28 | 29 | Ok(distribs) 30 | } 31 | 32 | #[cfg(all(test, not(miri)))] 33 | mod tests { 34 | use test_case::test_case; 35 | 36 | use super::*; 37 | use crate::test::run_compare; 38 | 39 | #[test_case("last 2 edge major versions"; "basic")] 40 | #[test_case("last 1 bb major version"; "support pluralization")] 41 | #[test_case("last 3 Chrome major versions"; "case insensitive")] 42 | #[test_case("last 2 android major versions"; "android")] 43 | #[test_case("last 2 bb major versions"; "non-sequential version numbers")] 44 | #[test_case("last 3 bb major versions"; "more versions than have been released")] 45 | fn default_options(query: &str) { 46 | run_compare(query, &Opts::default(), None); 47 | } 48 | 49 | #[test] 50 | fn mobile_to_desktop() { 51 | run_compare( 52 | "last 2 android major versions", 53 | &Opts { mobile_to_desktop: true, ..Default::default() }, 54 | None, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage # Run cargo-llvm-cov and upload to codecov.io 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | types: [opened, synchronize] 9 | paths: 10 | - "**.rs" 11 | - ".github/workflows/codecov.yml" 12 | push: 13 | branches: 14 | - main 15 | paths: 16 | - "**.rs" 17 | - ".github/workflows/codecov.yml" 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | cancel-in-progress: ${{ github.ref_name != 'main' }} 22 | 23 | jobs: 24 | coverage: 25 | name: Code Coverage 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 29 | 30 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 31 | with: 32 | cache-key: codecov 33 | save-cache: ${{ github.ref_name == 'main' }} 34 | tools: cargo-llvm-cov 35 | components: llvm-tools-preview 36 | 37 | - uses: oxc-project/setup-node@141eb77546de6702f92d320926403fe3f9f6a6f2 # v1.0.5 38 | 39 | - run: cargo llvm-cov --lcov --output-path lcov.info 40 | 41 | - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 42 | with: 43 | name: codecov 44 | path: lcov.info 45 | 46 | # codecov often fails, use another workflow for retry 47 | upload-codecov: 48 | name: Upload coverage file 49 | runs-on: ubuntu-latest 50 | needs: coverage 51 | # Check if the event is not triggered by a fork by checking whether CODECOV_TOKEN is set 52 | env: 53 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 54 | steps: 55 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 56 | if: env.CODECOV_TOKEN 57 | 58 | - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 59 | if: env.CODECOV_TOKEN 60 | with: 61 | name: codecov 62 | 63 | - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 64 | if: env.CODECOV_TOKEN 65 | with: 66 | token: ${{ secrets.CODECOV_TOKEN }} 67 | fail_ci_if_error: true 68 | files: lcov.info 69 | -------------------------------------------------------------------------------- /src/queries/percentage_by_region.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{data::caniuse::region::get_usage_by_region, error::Error, parser::Comparator}; 3 | 4 | pub(super) fn percentage_by_region( 5 | comparator: Comparator, 6 | popularity: f32, 7 | region: &str, 8 | ) -> QueryResult { 9 | let normalized_region = 10 | if region.len() == 2 { region.to_uppercase() } else { region.to_lowercase() }; 11 | 12 | if let Some(region_data) = get_usage_by_region(&normalized_region) { 13 | let distribs = region_data 14 | .iter() 15 | .filter(|(_, _, usage)| match comparator { 16 | Comparator::Greater => *usage > popularity, 17 | Comparator::Less => *usage < popularity, 18 | Comparator::GreaterOrEqual => *usage >= popularity, 19 | Comparator::LessOrEqual => *usage <= popularity, 20 | }) 21 | .map(|(name, version, _)| Distrib::new(name, version)) 22 | .collect(); 23 | Ok(distribs) 24 | } else { 25 | Err(Error::UnknownRegion(region.to_string())) 26 | } 27 | } 28 | 29 | #[cfg(all(test, not(miri)))] 30 | mod tests { 31 | use test_case::test_case; 32 | 33 | use super::*; 34 | use crate::{ 35 | opts::Opts, 36 | test::{run_compare, should_failed}, 37 | }; 38 | 39 | #[test_case("> 10% in US"; "greater")] 40 | #[test_case(">= 5% in US"; "greater or equal")] 41 | #[test_case("< 5% in US"; "less")] 42 | #[test_case("<= 5% in US"; "less or equal")] 43 | #[test_case("> 10.2% in US"; "with float")] 44 | #[test_case("> .2% in US"; "with float that has a leading dot")] 45 | #[test_case("> 10.2% in us"; "fixes country case")] 46 | #[test_case("> 1% in RU"; "load country")] 47 | #[test_case("> 1% in alt-AS"; "load continents")] 48 | #[test_case(">10% in US"; "no space")] 49 | #[test_case("> 1% in CN"; "normalize incorrect caniuse versions for and-prefixed")] 50 | fn valid(query: &str) { 51 | run_compare(query, &Opts::default(), None); 52 | } 53 | 54 | #[test] 55 | fn invalid() { 56 | assert_eq!( 57 | should_failed("> 1% in XX", &Opts::default()), 58 | Error::UnknownRegion(String::from("XX")) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/queries/browser_bounded_range.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{ 3 | data::caniuse::{get_browser_stat, normalize_version}, 4 | error::Error, 5 | opts::Opts, 6 | semver::Version, 7 | }; 8 | 9 | pub(super) fn browser_bounded_range(name: &str, from: &str, to: &str, opts: &Opts) -> QueryResult { 10 | let (name, stat) = get_browser_stat(name, opts.mobile_to_desktop) 11 | .ok_or_else(|| Error::BrowserNotFound(name.to_string()))?; 12 | let from_normalized = normalize_version(stat, from); 13 | let from_str = from_normalized.as_deref().unwrap_or(from); 14 | let from: Version = from_str.parse().unwrap_or_default(); 15 | 16 | let to_normalized = normalize_version(stat, to); 17 | let to_str = to_normalized.as_deref().unwrap_or(to); 18 | let to: Version = to_str.parse().unwrap_or_default(); 19 | 20 | let distribs = stat 21 | .version_list 22 | .iter() 23 | .filter(|version| version.release_date().is_some()) 24 | .map(|version| version.version()) 25 | .filter(|version| { 26 | let version = version.parse().unwrap_or_default(); 27 | from <= version && version <= to 28 | }) 29 | .map(|version| Distrib::new(name, version)) 30 | .collect(); 31 | Ok(distribs) 32 | } 33 | 34 | #[cfg(all(test, not(miri)))] 35 | mod tests { 36 | use test_case::test_case; 37 | 38 | use super::*; 39 | use crate::test::{run_compare, should_failed}; 40 | 41 | #[test_case("ie 8-10"; "basic")] 42 | #[test_case("ie 8 - 10"; "more spaces")] 43 | #[test_case("ie 1-12"; "out of range")] 44 | #[test_case("android 4.3-37"; "android")] 45 | fn default_options(query: &str) { 46 | run_compare(query, &Opts::default(), None); 47 | } 48 | 49 | #[test_case("and_chr 52-53"; "chrome")] 50 | #[test_case("android 4.4-38"; "android")] 51 | fn mobile_to_desktop(query: &str) { 52 | run_compare(query, &Opts { mobile_to_desktop: true, ..Default::default() }, None); 53 | } 54 | 55 | #[test_case( 56 | "unknown 4-7", Error::BrowserNotFound(String::from("unknown")); 57 | "unknown browser" 58 | )] 59 | fn invalid(query: &str, error: Error) { 60 | assert_eq!(should_failed(query, &Opts::default()), error); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/data/caniuse/region.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use super::{ 4 | BrowserName, 5 | compression::{decode, decompress_deflate}, 6 | }; 7 | 8 | use crate::data::decode_browser_name; 9 | pub use crate::generated::caniuse_region_matching::get_usage_by_region; 10 | 11 | static BROWSER_NAMES_COMPRESSED: &[u8] = 12 | include_bytes!("../../generated/caniuse_region_browsers.bin.deflate"); 13 | static VERSIONS_COMPRESSED: &[u8] = 14 | include_bytes!("../../generated/caniuse_region_versions.bin.deflate"); 15 | static PERCENTAGES_COMPRESSED: &[u8] = 16 | include_bytes!("../../generated/caniuse_region_percentages.bin.deflate"); 17 | 18 | static BROWSER_NAMES: OnceLock> = OnceLock::new(); 19 | static VERSIONS: OnceLock> = OnceLock::new(); 20 | static PERCENTAGES: OnceLock> = OnceLock::new(); 21 | 22 | pub struct RegionData { 23 | browser_names_start: u32, 24 | browser_names_end: u32, 25 | versions_start: u32, 26 | versions_end: u32, 27 | percentages_start: u32, 28 | percentages_end: u32, 29 | } 30 | 31 | impl RegionData { 32 | pub fn new( 33 | browser_names_start: u32, 34 | browser_names_end: u32, 35 | versions_start: u32, 36 | versions_end: u32, 37 | percentages_start: u32, 38 | percentages_end: u32, 39 | ) -> Self { 40 | Self { 41 | browser_names_start, 42 | browser_names_end, 43 | versions_start, 44 | versions_end, 45 | percentages_start, 46 | percentages_end, 47 | } 48 | } 49 | 50 | pub fn iter(&self) -> impl Iterator { 51 | let browser_names = 52 | BROWSER_NAMES.get_or_init(|| decompress_deflate(BROWSER_NAMES_COMPRESSED)); 53 | let browser_names = 54 | &browser_names[self.browser_names_start as usize..self.browser_names_end as usize]; 55 | 56 | let versions_data = VERSIONS.get_or_init(|| decompress_deflate(VERSIONS_COMPRESSED)); 57 | let versions = 58 | decode::<&'static str>(versions_data, self.versions_start, self.versions_end); 59 | 60 | let percentages_data = 61 | PERCENTAGES.get_or_init(|| decompress_deflate(PERCENTAGES_COMPRESSED)); 62 | let percentages = 63 | decode::(percentages_data, self.percentages_start, self.percentages_end); 64 | 65 | browser_names 66 | .iter() 67 | .zip(versions) 68 | .zip(percentages) 69 | .map(|((b, v), p)| (decode_browser_name(*b), v, p)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxc-browserslist" 3 | version = "2.1.4" 4 | authors = ["Boshen ", "Pig Fang "] 5 | categories = ["config", "web-programming"] 6 | edition = "2024" 7 | include = ["/benches", "/examples", "/src"] 8 | keywords = ["javascript", "web"] 9 | license = "MIT" 10 | repository = "https://github.com/oxc-project/oxc-browserslist" 11 | rust-version = "1.85.0" 12 | description = "Rust-ported Browserslist for Oxc." 13 | 14 | [workspace] 15 | members = [".", "xtask"] 16 | 17 | [workspace.lints.rust] 18 | unsafe_code = "warn" 19 | absolute_paths_not_starting_with_crate = "warn" 20 | non_ascii_idents = "warn" 21 | unit-bindings = "warn" 22 | 23 | [workspace.lints.clippy] 24 | all = { level = "warn", priority = -1 } 25 | dbg_macro = "warn" 26 | todo = "warn" 27 | unimplemented = "warn" 28 | print_stdout = "warn" # Must be opt-in 29 | print_stderr = "warn" # Must be opt-in 30 | cargo = { level = "warn", priority = -1 } 31 | 32 | [workspace.dependencies] 33 | anyhow = "1" 34 | bincode = "2" 35 | criterion2 = { version = "3", default-features = false } 36 | flate2 = "1" 37 | indexmap = "2" 38 | nom = "8.0" 39 | pico-args = "0.5.0" 40 | prettyplease = "0.2.36" 41 | proc-macro2 = "1.0.95" 42 | project-root = "0.2.2" 43 | quote = "1.0" 44 | rustc-hash = "2" 45 | serde = "1.0" 46 | serde_json = "1.0" 47 | syn = "2" 48 | test-case = "3.3" 49 | thiserror = "2.0" 50 | time = "0.3" 51 | 52 | [lints] 53 | workspace = true 54 | 55 | [lib] 56 | name = "browserslist" 57 | 58 | [[bench]] 59 | name = "resolve" 60 | harness = false 61 | 62 | [dependencies] 63 | bincode = { workspace = true } 64 | flate2 = { workspace = true } 65 | nom = { workspace = true } 66 | rustc-hash = { workspace = true } 67 | serde = { workspace = true, features = ["derive"] } 68 | serde_json = { workspace = true } 69 | thiserror = { workspace = true } 70 | time = { workspace = true } 71 | 72 | [dev-dependencies] 73 | criterion2 = { workspace = true } 74 | pico-args = { workspace = true } 75 | test-case = { workspace = true } 76 | 77 | [target.'cfg(target_arch = "wasm32")'.dependencies] 78 | js-sys = { version = "0.3", optional = true } 79 | wasm-bindgen = { version = "0.2", optional = true } 80 | serde-wasm-bindgen = { version = "0.6", optional = true } 81 | 82 | [features] 83 | default = [] 84 | wasm_bindgen = ["js-sys", "serde-wasm-bindgen", "time/wasm-bindgen", "wasm-bindgen"] 85 | codspeed = ["criterion2/codspeed"] 86 | 87 | [profile.release] 88 | opt-level = 3 89 | lto = "fat" 90 | codegen-units = 1 91 | strip = "symbols" # Set to `false` for debug information 92 | debug = false # Set to `true` for debug information 93 | -------------------------------------------------------------------------------- /src/queries/browser_unbounded_range.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{ 3 | data::caniuse::{browser_version_aliases, get_browser_stat}, 4 | error::Error, 5 | opts::Opts, 6 | parser::Comparator, 7 | semver::Version, 8 | }; 9 | 10 | pub(super) fn browser_unbounded_range( 11 | name: &str, 12 | comparator: Comparator, 13 | version: &str, 14 | opts: &Opts, 15 | ) -> QueryResult { 16 | let (name, stat) = get_browser_stat(name, opts.mobile_to_desktop) 17 | .ok_or_else(|| Error::BrowserNotFound(name.to_string()))?; 18 | let version: Version = browser_version_aliases() 19 | .get(name) 20 | .and_then(|alias| alias.get(version).copied()) 21 | .unwrap_or(version) 22 | .parse() 23 | .unwrap_or_default(); 24 | 25 | let distribs = stat 26 | .version_list 27 | .iter() 28 | .filter(|version| version.release_date().is_some()) 29 | .map(|version| version.version()) 30 | .filter(|v| { 31 | let v: Version = v.parse().unwrap_or_default(); 32 | match comparator { 33 | Comparator::Greater => v > version, 34 | Comparator::Less => v < version, 35 | Comparator::GreaterOrEqual => v >= version, 36 | Comparator::LessOrEqual => v <= version, 37 | } 38 | }) 39 | .map(|version| Distrib::new(name, version)) 40 | .collect(); 41 | Ok(distribs) 42 | } 43 | 44 | #[cfg(all(test, not(miri)))] 45 | mod tests { 46 | use test_case::test_case; 47 | 48 | use super::*; 49 | use crate::test::{run_compare, should_failed}; 50 | 51 | #[test_case("ie > 9"; "greater")] 52 | #[test_case("ie >= 10"; "greater or equal")] 53 | #[test_case("ie < 10"; "less")] 54 | #[test_case("ie <= 9"; "less or equal")] 55 | #[test_case("Explorer > 10"; "case insensitive")] 56 | #[test_case("android >= 4.2"; "android 1")] 57 | #[test_case("android >= 4.3"; "android 2")] 58 | #[test_case("ie<=9"; "no spaces")] 59 | #[test_case("and_qq > 0"; "browser with one version")] 60 | fn default_options(query: &str) { 61 | run_compare(query, &Opts::default(), None); 62 | } 63 | 64 | #[test_case("chromeandroid >= 52 and chromeandroid < 54"; "chrome")] 65 | fn mobile_to_desktop(query: &str) { 66 | run_compare(query, &Opts { mobile_to_desktop: true, ..Default::default() }, None); 67 | } 68 | 69 | #[test_case( 70 | "unknown > 10", Error::BrowserNotFound(String::from("unknown")); 71 | "unknown browser" 72 | )] 73 | fn invalid(query: &str, error: Error) { 74 | assert_eq!(should_failed(query, &Opts::default()), error); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/copilot-setup-steps.yml: -------------------------------------------------------------------------------- 1 | name: Copilot Setup Steps 2 | 3 | # This workflow defines the setup steps that GitHub Copilot agents will use 4 | # to prepare the development environment for the oxc project. 5 | # It preinstalls tools and dependencies needed for Rust and Node.js development. 6 | 7 | on: 8 | workflow_dispatch: 9 | pull_request: 10 | types: [opened, synchronize] 11 | paths: 12 | - .github/workflows/copilot-setup-steps.yml 13 | push: 14 | branches: 15 | - main 16 | paths: 17 | - .github/workflows/copilot-setup-steps.yml 18 | 19 | permissions: {} 20 | 21 | jobs: 22 | copilot-setup-steps: 23 | name: Setup Development Environment for Copilot 24 | runs-on: ubuntu-latest 25 | steps: 26 | # Checkout full repo for git history. 27 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 28 | with: 29 | persist-credentials: false 30 | 31 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 32 | with: 33 | cache-key: warm 34 | save-cache: false 35 | tools: just,watchexec-cli,typos-cli,cargo-shear 36 | components: clippy rust-docs rustfmt 37 | 38 | - uses: oxc-project/setup-node@141eb77546de6702f92d320926403fe3f9f6a6f2 # v1.0.5 39 | 40 | - name: Verify installations 41 | run: | 42 | echo "=== Rust toolchain ===" 43 | rustc --version 44 | cargo --version 45 | 46 | echo "=== Node.js and pnpm ===" 47 | node --version 48 | pnpm --version 49 | 50 | echo "=== Command runner ===" 51 | just --version 52 | 53 | echo "=== Development tools ===" 54 | watchexec --version 55 | typos --version 56 | cargo shear --version 57 | 58 | echo "=== Project commands ===" 59 | just --list 60 | 61 | - name: Setup complete 62 | run: | 63 | echo "🎉 Development environment setup complete!" 64 | echo "The following tools are now available:" 65 | echo " - Rust toolchain (version from rust-toolchain.toml)" 66 | echo " - Node.js and pnpm (versions from .node-version and" 67 | echo " package.json)" 68 | echo " - just command runner" 69 | echo " - Development tools: watchexec, typos, cargo-shear, dprint" 70 | echo " - All Node.js dependencies installed" 71 | echo "" 72 | echo "You can now use 'just' commands to work with the project:" 73 | echo " - just ready # Run all checks" 74 | echo " - just fmt # Format code" 75 | echo " - just test # Run tests" 76 | echo " - just lint # Run linting" 77 | -------------------------------------------------------------------------------- /src/semver.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering, fmt, num::ParseIntError, str::FromStr}; 2 | 3 | /// Semver 4 | #[derive(PartialEq, Eq, PartialOrd, Ord, Default, Debug, Copy, Clone)] 5 | pub struct Version(pub u16, pub u16, pub u16); 6 | 7 | impl Version { 8 | #[inline] 9 | pub fn major(&self) -> u16 { 10 | self.0 11 | } 12 | 13 | #[inline] 14 | pub fn minor(&self) -> u16 { 15 | self.1 16 | } 17 | 18 | #[inline] 19 | pub fn patch(&self) -> u16 { 20 | self.2 21 | } 22 | 23 | pub fn parse(s: &str) -> Result { 24 | let mut segments = s.split('.'); 25 | let major = match segments.next() { 26 | Some(n) => n.parse()?, 27 | None => 0, 28 | }; 29 | let minor = match segments.next() { 30 | Some(n) => n.parse()?, 31 | None => 0, 32 | }; 33 | let patch = match segments.next() { 34 | Some(n) => n.parse()?, 35 | None => 0, 36 | }; 37 | Ok(Self(major, minor, patch)) 38 | } 39 | 40 | pub fn loose_compare(&self, b: &str) -> Ordering { 41 | let mut b = b.split('.'); 42 | let Some(first) = b.next() else { 43 | return Ordering::Equal; 44 | }; 45 | let first: u16 = first.parse().unwrap_or_default(); 46 | let x = self.0.cmp(&first); 47 | if !x.is_eq() { 48 | return x; 49 | } 50 | let Some(second) = b.next() else { 51 | return Ordering::Equal; 52 | }; 53 | let second: u16 = second.parse().unwrap_or_default(); 54 | self.1.cmp(&second) 55 | } 56 | } 57 | 58 | impl FromStr for Version { 59 | type Err = ParseIntError; 60 | 61 | fn from_str(s: &str) -> Result { 62 | // this allows something like `4.4.3-4.4.4` 63 | let s = s.split_once('-').map_or(s, |(v, _)| v); 64 | Self::parse(s) 65 | } 66 | } 67 | 68 | impl fmt::Display for Version { 69 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 70 | write!(f, "{}.{}.{}", self.0, self.1, self.2) 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::*; 77 | 78 | #[test] 79 | fn parse_version() { 80 | assert_eq!(Ok(Version(1, 0, 0)), "1".parse()); 81 | assert_eq!(Ok(Version(1, 2, 0)), "1.2".parse()); 82 | assert_eq!(Ok(Version(1, 2, 3)), "1.2.3".parse()); 83 | assert_eq!(Ok(Version(12, 34, 56)), "12.34.56".parse()); 84 | 85 | assert_eq!(Ok(Version(1, 0, 0)), "1-2".parse()); 86 | assert_eq!(Ok(Version(1, 2, 0)), "1.2-1.3".parse()); 87 | assert_eq!(Ok(Version(1, 2, 3)), "1.2.3-1.2.4".parse()); 88 | assert_eq!(Ok(Version(12, 34, 56)), "12.34.56-78.9".parse()); 89 | 90 | assert!("tp".parse::().is_err()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /xtask/src/generators/caniuse/features.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bincode::encode_to_vec; 3 | use quote::quote; 4 | 5 | use crate::data::{Caniuse, encode_browser_name}; 6 | use crate::utils::{create_range_vec, generate_file, save_bin_compressed}; 7 | 8 | pub fn build_caniuse_feature_matching(data: &Caniuse) -> Result<()> { 9 | let mut sorted_data = data.data.clone(); 10 | sorted_data.sort_unstable_keys(); 11 | let features = sorted_data 12 | .values() 13 | .map(|feature| { 14 | feature 15 | .stats 16 | .iter() 17 | .filter_map(|(name, versions)| { 18 | let name = encode_browser_name(name); 19 | let versions = versions 20 | .into_iter() 21 | .filter(|(_version, flag)| *flag != "n") 22 | .collect::>(); 23 | let mut y = versions 24 | .iter() 25 | .filter(|(_, flag)| flag.contains('y')) 26 | .map(|x| x.0.clone()) 27 | .collect::>(); 28 | y.sort_unstable(); 29 | let mut a = versions 30 | .iter() 31 | .filter(|(_, flag)| flag.contains('a')) 32 | .map(|x| x.0.clone()) 33 | .collect::>(); 34 | a.sort_unstable(); 35 | if y.is_empty() && a.is_empty() { None } else { Some((name, y, a)) } 36 | }) 37 | .collect::>() 38 | }) 39 | .collect::>(); 40 | 41 | let keys = sorted_data.keys().cloned().collect::>(); 42 | 43 | let data = features 44 | .iter() 45 | .map(|v| encode_to_vec(v, bincode::config::standard()).unwrap()) 46 | .collect::>(); 47 | let data_bytes = data.iter().flat_map(|x| x.iter()).copied().collect::>(); 48 | save_bin_compressed("caniuse_feature_matching.bin", &data_bytes); 49 | 50 | let data_range = create_range_vec(&data); 51 | 52 | let output = quote! { 53 | use crate::data::caniuse::features::Feature; 54 | 55 | static KEYS: &[&str] = &[#(#keys),*]; 56 | static RANGES: &[u32] = &[#(#data_range),*]; 57 | 58 | pub fn get_feature_stat(name: &str) -> Option { 59 | match KEYS.binary_search(&name) { 60 | Ok(idx) => { 61 | let start = RANGES[idx]; 62 | let end = RANGES[idx + 1]; 63 | Some(Feature::new(start, end)) 64 | }, 65 | Err(_) => None, 66 | } 67 | } 68 | }; 69 | 70 | generate_file("caniuse_feature_matching.rs", output); 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /src/queries/node_accurate.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{data::node::NODE_VERSIONS, error::Error, opts::Opts}; 3 | 4 | pub(super) fn node_accurate(version_str: &str, opts: &Opts) -> QueryResult { 5 | for v in version_str.split('.') { 6 | let is_valid = if v == "0" { true } else { !v.starts_with('0') }; 7 | if !is_valid { 8 | return Err(Error::UnknownNodejsVersion(version_str.to_string())); 9 | } 10 | } 11 | 12 | let mut s = version_str.split('.'); 13 | let major = s.next().map(|n| n.parse::().unwrap_or_default()); 14 | let minor = s.next().map(|n| n.parse::().unwrap_or_default()); 15 | let patch = s.next().map(|n| n.parse::().unwrap_or_default()); 16 | 17 | let distribs = NODE_VERSIONS() 18 | .iter() 19 | .rev() 20 | .find(|v| { 21 | if let Some(major) = major { 22 | let major_eq = major == v.0; 23 | if let Some(minor) = minor { 24 | let minor_eq = minor == v.1; 25 | if let Some(patch) = patch { 26 | return major_eq && minor_eq && patch == v.2; 27 | } 28 | return major_eq && minor_eq; 29 | } 30 | return major_eq; 31 | } 32 | false 33 | }) 34 | .map(|version| vec![Distrib::new("node", version.to_string())]); 35 | if opts.ignore_unknown_versions { 36 | Ok(distribs.unwrap_or_default()) 37 | } else { 38 | distribs.ok_or_else(|| Error::UnknownNodejsVersion(version_str.to_string())) 39 | } 40 | } 41 | 42 | #[cfg(all(test, not(miri)))] 43 | mod tests { 44 | use test_case::test_case; 45 | 46 | use super::*; 47 | use crate::test::{run_compare, should_failed}; 48 | 49 | #[test_case("node 7.5.0"; "basic")] 50 | #[test_case("Node 7.5.0"; "case insensitive")] 51 | #[test_case("node 5.1"; "without semver patch")] 52 | #[test_case("node 5"; "semver major only")] 53 | fn valid(query: &str) { 54 | run_compare(query, &Opts::default(), None); 55 | } 56 | 57 | #[test_case( 58 | "node 3", Error::UnknownNodejsVersion(String::from("3")); 59 | "unknown version" 60 | )] 61 | #[test_case( 62 | "node 8.a", Error::Parse(String::from("a")); 63 | "malformed version 1" 64 | )] 65 | #[test_case( 66 | "node 8.8.8.8", Error::UnknownNodejsVersion(String::from("8.8.8.8")); 67 | "malformed version 2" 68 | )] 69 | #[test_case( 70 | "node 8.01", Error::UnknownNodejsVersion(String::from("8.01")); 71 | "malformed version 3" 72 | )] 73 | fn invalid(query: &str, error: Error) { 74 | assert_eq!(should_failed(query, &Opts::default()), error); 75 | } 76 | 77 | #[test] 78 | fn ignore_unknown_versions() { 79 | run_compare("node 3", &Opts { ignore_unknown_versions: true, ..Default::default() }, None); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | types: [opened, synchronize] 9 | paths-ignore: 10 | - "**/*.md" 11 | - "!.github/workflows/ci.yml" 12 | push: 13 | branches: 14 | - main 15 | - "renovate/**" 16 | paths-ignore: 17 | - "**/*.md" 18 | - "!.github/workflows/ci.yml" 19 | 20 | concurrency: 21 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 22 | cancel-in-progress: ${{ github.ref_name != 'main' }} 23 | 24 | defaults: 25 | run: 26 | shell: bash 27 | 28 | env: 29 | CARGO_INCREMENTAL: 0 30 | RUSTFLAGS: "-D warnings" 31 | 32 | jobs: 33 | test: 34 | name: Test 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | include: 39 | # - os: windows-latest 40 | - os: ubuntu-latest 41 | - os: macos-latest 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 45 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 46 | with: 47 | save-cache: ${{ github.ref_name == 'main' }} 48 | components: rustfmt # for `cargo codegen` 49 | - uses: oxc-project/setup-node@141eb77546de6702f92d320926403fe3f9f6a6f2 # v1.0.5 50 | - run: cargo check --all-targets --all-features 51 | - run: cargo test 52 | 53 | lint: 54 | name: Clippy 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 58 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 59 | with: 60 | components: clippy 61 | - run: cargo clippy --all-targets --all-features -- -D warnings 62 | 63 | doc: 64 | name: Doc 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 68 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 69 | with: 70 | components: rust-docs 71 | - run: RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items 72 | 73 | wasm: 74 | name: Wasm 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 78 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 79 | with: 80 | cache-key: wasm 81 | save-cache: ${{ github.ref_name == 'main' }} 82 | - name: Check 83 | run: | 84 | rustup target add wasm32-unknown-unknown 85 | cargo check --target wasm32-unknown-unknown --features wasm_bindgen 86 | 87 | typos: 88 | name: Spell Check 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 92 | - uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # v1.40.0 93 | with: 94 | files: . 95 | -------------------------------------------------------------------------------- /xtask/src/generators/node.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use anyhow::Result; 4 | use bincode::encode_to_vec; 5 | use indexmap::IndexMap; 6 | use quote::quote; 7 | use serde::Deserialize; 8 | use time::OffsetDateTime; 9 | 10 | use crate::utils::{generate_file, root, save_bin_compressed}; 11 | 12 | // Node versions structures 13 | #[derive(Deserialize)] 14 | struct NodeRelease { 15 | version: String, 16 | } 17 | 18 | // Node release schedule structures 19 | #[derive(Deserialize)] 20 | struct NodeScheduleRelease { 21 | start: String, 22 | end: String, 23 | } 24 | 25 | pub fn build_node_versions() -> Result<()> { 26 | let releases_path = root().join("node_modules/node-releases/data/processed/envs.json"); 27 | let releases: Vec = serde_json::from_slice(&fs::read(releases_path)?)?; 28 | 29 | // Convert releases to a Vec of (u16, u16, u16) tuples for compression 30 | let versions: Vec<(u16, u16, u16)> = releases 31 | .into_iter() 32 | .map(|release| { 33 | let version = release.version.split('.').collect::>(); 34 | assert_eq!(version.len(), 3); 35 | let major: u16 = version[0].parse().unwrap(); 36 | let minor: u16 = version[1].parse().unwrap(); 37 | let patch: u16 = version[2].parse().unwrap(); 38 | (major, minor, patch) 39 | }) 40 | .collect(); 41 | 42 | // Serialize and compress the data 43 | let serialized = encode_to_vec(&versions, bincode::config::standard())?; 44 | save_bin_compressed("node_versions.bin", &serialized); 45 | 46 | Ok(()) 47 | } 48 | 49 | fn parse_date(s: &str) -> i32 { 50 | let format = time::format_description::well_known::Iso8601::DATE; 51 | let s = format!("{s}T00:00:00.000000000-00:00"); 52 | OffsetDateTime::parse(&s, &format).unwrap().to_julian_day() 53 | } 54 | 55 | pub fn build_node_release_schedule() -> Result<()> { 56 | let schedule_path = 57 | root().join("node_modules/node-releases/data/release-schedule/release-schedule.json"); 58 | let schedule: IndexMap = 59 | serde_json::from_slice(&fs::read(schedule_path)?)?; 60 | let versions = schedule 61 | .into_iter() 62 | .map(|(version, NodeScheduleRelease { start, end })| { 63 | let version = version.trim_start_matches('v'); 64 | let version = version.split('.').collect::>(); 65 | assert!(version.len() > 0); 66 | let major: u16 = version[0].parse().unwrap(); 67 | let minor: u16 = version.get(1).map(|v| v.parse().unwrap()).unwrap_or_default(); 68 | let patch: u16 = version.get(2).map(|v| v.parse().unwrap()).unwrap_or_default(); 69 | let start_julian_day = parse_date(&start); 70 | let end_julian_day = parse_date(&end); 71 | quote! { 72 | (Version(#major, #minor, #patch), #start_julian_day, #end_julian_day) 73 | } 74 | }) 75 | .collect::>(); 76 | 77 | let output = quote! { 78 | use crate::semver::Version; 79 | pub static RELEASE_SCHEDULE: &[(Version, /* julian day */ i32, /* julian day */ i32)] = &[#(#versions),*]; 80 | }; 81 | 82 | generate_file("node_release_schedule.rs", output); 83 | 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error, PartialEq, Eq, Clone)] 4 | /// The errors may occur when querying with browserslist. 5 | pub enum Error { 6 | #[error("failed to parse the rest of input: ...'{0}'")] 7 | /// Error of parsing query. 8 | Parse(String), 9 | 10 | #[error("invalid date: {0}")] 11 | /// Date format is invalid. 12 | InvalidDate(String), 13 | 14 | #[error("query cannot start with 'not'; add any other queries before '{0}'")] 15 | /// Query can't start with a negated query which starts with `not`. 16 | NotAtFirst(String), 17 | 18 | #[error("unknown browser: '{0}'")] 19 | /// The given browser name can't be found. 20 | BrowserNotFound(String), 21 | 22 | #[error("unknown Electron version: {0}")] 23 | /// The given Electron version can't be found. 24 | UnknownElectronVersion(String), 25 | 26 | #[error("unknown Node.js version: {0}")] 27 | /// The given Node.js version can't be found. 28 | UnknownNodejsVersion(String), 29 | 30 | #[error("unknown version '{1}' of browser '{0}'")] 31 | /// The given version of the given browser can't be found. 32 | UnknownBrowserVersion(String, String), 33 | 34 | #[error("current environment for querying `current node` is not supported")] 35 | /// Current environment doesn't support querying `current node`, 36 | /// for example, running this library on Non-Node.js platform or 37 | /// no Node.js installed. 38 | UnsupportedCurrentNode, 39 | 40 | #[error("current environment for querying `extends ...` is not supported")] 41 | /// Current environment doesn't support querying `extends`. 42 | UnsupportedExtends, 43 | 44 | #[error("unknown browser feature: '{0}'")] 45 | /// Unknown browser feature. 46 | UnknownBrowserFeature(String), 47 | 48 | #[error("unknown region: '{0}'")] 49 | /// Unknown Can I Use region. 50 | UnknownRegion(String), 51 | 52 | #[error("unknown browser query: '{0}'")] 53 | /// Query can't be recognized. 54 | UnknownQuery(String), 55 | 56 | #[error("duplicated section '{0}' in config")] 57 | /// Duplicated section in configuration. 58 | DuplicatedSection(String), 59 | 60 | #[error("failed to read config file: {0}")] 61 | /// Failed to read config. 62 | FailedToReadConfig(String), 63 | 64 | #[error("missing 'browserslist' field in '{0}' file")] 65 | /// Missing `browserslist` field in `package.json` file. 66 | MissingFieldInPkg(String), 67 | 68 | #[error("duplicated: '{0}' directory contains both {1} and {2}.")] 69 | /// Duplicated configuration found. 70 | DuplicatedConfig(String, &'static str, &'static str), 71 | 72 | #[error("failed to access current working directory")] 73 | /// Failed to access the current working directory. 74 | FailedToAccessCurrentDir, 75 | 76 | #[error("missing config for Browserslist environment '{0}'")] 77 | /// Missing config corresponding to specific environment. 78 | MissingEnv(String), 79 | 80 | #[error("invalid extend name: {0}")] 81 | /// Invalid extend name 82 | InvalidExtendName(&'static str), 83 | 84 | #[error("failed to resolve '{0}' package in `extends` query")] 85 | /// Failed to resolve package in `extends` query. 86 | FailedToResolveExtend(String), 87 | 88 | #[error("year overflow")] 89 | /// Year overflow. 90 | YearOverflow, 91 | } 92 | 93 | impl<'a> From>> for Error { 94 | fn from(e: nom::Err>) -> Self { 95 | match e { 96 | nom::Err::Error(e) | nom::Err::Failure(e) => Self::Parse(e.input.to_owned()), 97 | _ => unreachable!(), 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Crates.io][crates-badge]][crates-url] 4 | [![Docs.rs][docs-badge]][docs-url] 5 | 6 | [![MIT licensed][license-badge]][license-url] 7 | [![Build Status][ci-badge]][ci-url] 8 | [![Code Coverage][code-coverage-badge]][code-coverage-url] 9 | [![CodSpeed Badge][codspeed-badge]][codspeed-url] 10 | [![Sponsors][sponsors-badge]][sponsors-url] 11 | [![Discord chat][discord-badge]][discord-url] 12 | 13 |
14 | 15 | # oxc-browserslist 16 | 17 | Rust port of [Browserslist](https://github.com/browserslist/browserslist), forked from [browserslist-rs](https://github.com/browserslist/browserslist-rs). 18 | 19 | The original crate did not meet the criteria of `oxc`, the following changes are made: 20 | 21 | - reduced compilation speed from one minute to a few seconds 22 | - improved some runtime performance, e.g. [improve sort method](https://github.com/oxc-project/oxc-browserslist/pull/28), [precompute versions](https://github.com/oxc-project/oxc-browserslist/pull/10) 23 | - removed all unnecessary, heavy or slow dependencies: `ahash`, `chrono`, `either`, `indexmap`, `itertools`, `once_cell`, `string_cache` 24 | - reduced binary size through data compression. 911K (this crate) vs 3.2M (original crate). 25 | 26 | ## Usage 27 | 28 | See [docs.rs/oxc-browserslist](https://docs.rs/oxc-browserslist). 29 | 30 | ## Limitation 31 | 32 | Only custom usage is not supported: `> 0.5% in my stats` or `cover 99.5% in my stats`. 33 | 34 | ## Example 35 | 36 | Inspect query result by running the example: 37 | 38 | ```sh 39 | cargo run --example inspect -- 40 | ``` 41 | 42 | You can also specify additional options, for example: 43 | 44 | ```sh 45 | cargo run --example inspect -- --mobile-to-desktop 'last 2 versions, not dead' 46 | ``` 47 | 48 | ## Future Work (Pull Request Welcome) 49 | 50 | - `nom` can be replaced by a hand written parser to improve runtime and compilation speed 51 | - improve test coverage 52 | - [improve compilation speed and reduce compiled binary size](https://github.com/oxc-project/oxc-browserslist/issues/23) 53 | - improve runtime performance 54 | - all semver versions with their string representation can be precomputed and code generated, current code is calling `parse` and `to_string` on semver versions 55 | - add more benchmarks 56 | - see [codspeed][codspeed-url] for current run performance 57 | 58 | ## [Sponsored By](https://github.com/sponsors/Boshen) 59 | 60 |

61 | 62 | My sponsors 63 | 64 |

65 | 66 | [discord-badge]: https://img.shields.io/discord/1079625926024900739?logo=discord&label=Discord 67 | [discord-url]: https://discord.gg/9uXCAwqQZW 68 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg 69 | [license-url]: https://github.com/oxc-project/oxc-browserslist/blob/main/LICENSE 70 | [ci-badge]: https://github.com/oxc-project/oxc-browserslist/actions/workflows/ci.yml/badge.svg?event=push&branch=main 71 | [ci-url]: https://github.com/oxc-project/oxc-browserslist/actions/workflows/ci.yml?query=event%3Apush+branch%3Amain 72 | [code-coverage-badge]: https://codecov.io/github/oxc-project/oxc-browserslist/branch/main/graph/badge.svg 73 | [code-coverage-url]: https://codecov.io/gh/oxc-project/oxc-browserslist 74 | [sponsors-badge]: https://img.shields.io/github/sponsors/Boshen 75 | [sponsors-url]: https://github.com/sponsors/Boshen 76 | [codspeed-badge]: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json 77 | [codspeed-url]: https://codspeed.io/oxc-project/oxc-browserslist 78 | [crates-badge]: https://img.shields.io/crates/d/oxc-browserslist?label=crates.io 79 | [crates-url]: https://crates.io/crates/oxc-browserslist 80 | [docs-badge]: https://img.shields.io/docsrs/oxc-browserslist 81 | [docs-url]: https://docs.rs/oxc-browserslist 82 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AI Agent Guidelines for oxc-browserslist 2 | 3 | This document provides guidelines for AI agents and assistants working with the oxc-browserslist project. 4 | 5 | ## Project Overview 6 | 7 | oxc-browserslist is a Rust port of [Browserslist](https://github.com/browserslist/browserslist), optimized for the [oxc](https://github.com/oxc-project/oxc) project. It helps determine which browsers to support based on usage statistics and version queries. 8 | 9 | ## Code Guidelines for AI Agents 10 | 11 | ### Understanding the Codebase 12 | 13 | - **Language**: Primary language is Rust with some JavaScript/TypeScript for Node.js compatibility 14 | - **Core functionality**: Browser query parsing and resolution 15 | - **Performance focus**: This port prioritizes fast compilation and runtime performance 16 | - **Dependencies**: Minimal dependencies (removed heavy deps like `chrono`, `itertools`, etc.) 17 | 18 | ### Making Changes 19 | 20 | 1. **Minimal modifications**: Always prefer the smallest possible changes 21 | 2. **Performance considerations**: Be mindful of compilation time and runtime performance 22 | 3. **Testing**: Run `cargo test` and `cargo check` before suggesting changes 23 | 4. **Compatibility**: Maintain compatibility with existing browserslist queries 24 | 25 | ### Key Areas 26 | 27 | - **Query parsing** (`src/parser/`): Handle browserslist query syntax 28 | - **Browser data** (`src/data/`): Browser version and usage data 29 | - **Query resolution** (`src/queries/`): Logic for resolving queries to browser lists 30 | - **Config handling** (`src/config/`): Configuration file parsing 31 | 32 | ### Common Tasks 33 | 34 | - **Adding new query types**: Follow existing patterns in `src/queries/` 35 | - **Updating browser data**: Use the codegen system (`cargo codegen`) 36 | - **Performance optimization**: Focus on reducing allocations and improving algorithms 37 | - **Bug fixes**: Write minimal test cases to reproduce issues 38 | 39 | ### Testing Guidelines 40 | 41 | - Add tests for new functionality in the appropriate test modules 42 | - Use `#[test_case]` macro for parameterized tests when applicable 43 | - Focus on edge cases and browser compatibility scenarios 44 | - Run benchmarks (`cargo bench`) for performance-critical changes 45 | 46 | ### Prerequisites 47 | 48 | The project uses several tools for development: 49 | 50 | - **Rust**: Latest stable (MSRV: 1.86.0) 51 | - **Node.js**: Version specified in `.node-version` 52 | - **pnpm**: Package manager for Node.js dependencies 53 | - **just**: Command runner (alternative to make) 54 | 55 | ### Build and Development 56 | 57 | `just init` has already been run, all tools (`watchexec-cli`, `typos-cli`, `cargo-shear`, `dprint`) are already installed, do not run `just init`. 58 | 59 | Rust and `cargo` components `clippy`, `rust-docs` and `rustfmt` has already been installed, do not install them. 60 | 61 | - Use `cargo check` for quick feedback during development 62 | - Run `just lint` for linting 63 | - Use `just fmt` for code formatting 64 | - Generate updated browser data with `cargo codegen` when needed 65 | - Rust `just ready` after all code changes are complete. 66 | 67 | ### Documentation 68 | 69 | - Update API documentation for public interfaces 70 | - Add inline comments for complex algorithms 71 | - Follow Rust documentation conventions 72 | 73 | ## Browser Query Examples 74 | 75 | When working with browser queries, common patterns include: 76 | 77 | ``` 78 | last 2 versions 79 | > 1% 80 | not dead 81 | Chrome > 90 82 | Safari >= 14 83 | ``` 84 | 85 | ## Resources 86 | 87 | - [Original Browserslist documentation](https://github.com/browserslist/browserslist) 88 | - [oxc project documentation](https://github.com/oxc-project/oxc) 89 | - [Rust documentation](https://doc.rust-lang.org/) 90 | 91 | ## Support 92 | 93 | For questions or issues specific to AI agent contributions, please refer to the main project documentation and issue tracker. 94 | -------------------------------------------------------------------------------- /src/queries/browser_accurate.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use super::{Distrib, QueryResult}; 4 | use crate::{ 5 | data::caniuse::{get_browser_stat, normalize_version}, 6 | error::Error, 7 | opts::Opts, 8 | }; 9 | 10 | pub(super) fn browser_accurate(name: &str, version: &str, opts: &Opts) -> QueryResult { 11 | let original_name = name; 12 | let original_version = version; 13 | let version = if original_version.eq_ignore_ascii_case("tp") { "TP" } else { version }; 14 | 15 | let (name, stat) = get_browser_stat(name, opts.mobile_to_desktop) 16 | .ok_or_else(|| Error::BrowserNotFound(name.to_string()))?; 17 | 18 | if let Some(version) = normalize_version( 19 | stat, 20 | if original_version.eq_ignore_ascii_case("tp") { "TP" } else { version }, 21 | ) { 22 | Ok(vec![Distrib::new(name, version.into_owned())]) 23 | } else { 24 | let version = if version.contains('.') { 25 | Cow::Borrowed(version.trim_end_matches(".0")) 26 | } else { 27 | let mut v = version.to_owned(); 28 | v.push_str(".0"); 29 | Cow::Owned(v) 30 | }; 31 | if let Some(version) = normalize_version(stat, &version) { 32 | Ok(vec![Distrib::new(name, version.into_owned())]) 33 | } else if opts.ignore_unknown_versions { 34 | Ok(vec![]) 35 | } else { 36 | Err(Error::UnknownBrowserVersion( 37 | original_name.to_string(), 38 | original_version.to_string(), 39 | )) 40 | } 41 | } 42 | } 43 | 44 | #[cfg(all(test, not(miri)))] 45 | mod tests { 46 | use test_case::test_case; 47 | 48 | use super::*; 49 | use crate::test::{run_compare, should_failed}; 50 | 51 | #[test_case("ie 10"; "by name")] 52 | #[test_case("IE 10"; "case insensitive")] 53 | #[test_case("Explorer 10"; "alias")] 54 | #[test_case("ios 7.0"; "work with joined versions 1")] 55 | #[test_case("ios 7.1"; "work with joined versions 2")] 56 | #[test_case("ios 7"; "allow missing zero 1")] 57 | #[test_case("ios 8.0"; "allow missing zero 2")] 58 | #[test_case("safari tp"; "safari tp")] 59 | #[test_case("Safari TP"; "safari tp case insensitive")] 60 | #[test_case("and_uc 10"; "cut version")] 61 | #[test_case("chromeandroid 53"; "missing mobile versions 1")] 62 | #[test_case("and_ff 60"; "missing mobile versions 2")] 63 | fn default_options(query: &str) { 64 | run_compare(query, &Opts::default(), None); 65 | } 66 | 67 | #[test_case("chromeandroid 53"; "chrome 1")] 68 | #[test_case("and_ff 60"; "firefox")] 69 | #[test_case("ie_mob 9"; "ie mobile")] 70 | fn mobile_to_desktop(query: &str) { 71 | run_compare(query, &Opts { mobile_to_desktop: true, ..Default::default() }, None); 72 | } 73 | 74 | #[test] 75 | fn ignore_unknown_versions() { 76 | run_compare( 77 | "IE 1, IE 9", 78 | &Opts { ignore_unknown_versions: true, ..Default::default() }, 79 | None, 80 | ); 81 | } 82 | 83 | #[test_case( 84 | "unknown 10", Error::BrowserNotFound(String::from("unknown")); 85 | "unknown browser" 86 | )] 87 | #[test_case( 88 | "IE 1", Error::UnknownBrowserVersion(String::from("IE"), String::from("1")); 89 | "unknown version" 90 | )] 91 | #[test_case( 92 | "chrome 70, ie 11, safari 12.2, safari 12", 93 | Error::UnknownBrowserVersion(String::from("safari"), String::from("12.2")); 94 | "use correct browser name in error" 95 | )] 96 | #[test_case( 97 | "ie_mob 9", Error::UnknownBrowserVersion(String::from("ie_mob"), String::from("9")); 98 | "missing mobile versions 1" 99 | )] 100 | #[test_case( 101 | "op_mob 30", Error::UnknownBrowserVersion(String::from("op_mob"), String::from("30")); 102 | "missing mobile versions 2" 103 | )] 104 | fn invalid(query: &str, error: Error) { 105 | assert_eq!(should_failed(query, &Opts::default()), error); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/queries/supports.rs: -------------------------------------------------------------------------------- 1 | use super::{Distrib, QueryResult}; 2 | use crate::{ 3 | Opts, 4 | data::caniuse::{features::get_feature_stat, get_browser_stat, to_desktop_name}, 5 | error::Error, 6 | parser::SupportKind, 7 | }; 8 | 9 | pub(super) fn supports(name: &str, kind: Option, opts: &Opts) -> QueryResult { 10 | let include_partial = matches!(kind, Some(SupportKind::Partially) | None); 11 | 12 | if let Some(stat) = get_feature_stat(name) { 13 | let feature = stat.create_data(); 14 | let feature = feature.as_slice(); 15 | let distribs = feature 16 | .iter() 17 | .filter_map(|(name, versions)| { 18 | get_browser_stat(name, opts.mobile_to_desktop) 19 | .map(|(name, stat)| (name, stat, versions)) 20 | }) 21 | .flat_map(|(name, browser_stat, versions)| { 22 | let desktop_name = opts.mobile_to_desktop.then(|| to_desktop_name(name)).flatten(); 23 | let check_desktop = desktop_name.is_some() 24 | && browser_stat 25 | .version_list 26 | .iter() 27 | .filter(|version| version.release_date().is_some()) 28 | .rfind(|latest_version| { 29 | versions 30 | .supports(latest_version.version(), /* include_partial */ true) 31 | }) 32 | .is_some_and(|latest_version| { 33 | versions.supports(latest_version.version(), include_partial) 34 | }); 35 | 36 | browser_stat 37 | .version_list 38 | .iter() 39 | .filter_map(move |version_detail| { 40 | let version = version_detail.version(); 41 | if versions.supports(version, include_partial) { 42 | return Some(version); 43 | } 44 | if check_desktop { 45 | if let Some(desktop_name) = desktop_name { 46 | if let Some(versions) = 47 | feature.iter().find_map(|(name, versions)| { 48 | (*name == desktop_name).then_some(versions) 49 | }) 50 | { 51 | if versions.supports(version, include_partial) { 52 | return Some(version); 53 | } 54 | } 55 | } 56 | } 57 | None 58 | }) 59 | .map(move |version| Distrib::new(name, version)) 60 | }) 61 | .collect(); 62 | Ok(distribs) 63 | } else { 64 | Err(Error::UnknownBrowserFeature(name.to_string())) 65 | } 66 | } 67 | 68 | #[cfg(all(test, not(miri)))] 69 | mod tests { 70 | use test_case::test_case; 71 | 72 | use super::*; 73 | use crate::{ 74 | opts::Opts, 75 | test::{run_compare, should_failed}, 76 | }; 77 | 78 | #[test_case("supports objectrtc"; "case 1")] 79 | #[test_case("supports rtcpeerconnection"; "case 2")] 80 | #[test_case("supports arrow-functions"; "case 3")] 81 | #[test_case("partially supports rtcpeerconnection"; "partially")] 82 | #[test_case("fully supports rtcpeerconnection"; "fully")] 83 | fn default_options(query: &str) { 84 | run_compare(query, &Opts::default(), None); 85 | } 86 | 87 | #[test_case("supports filesystem"; "case 1")] 88 | #[test_case("supports font-smooth"; "case 2")] 89 | fn mobile_to_desktop(query: &str) { 90 | run_compare(query, &Opts { mobile_to_desktop: true, ..Default::default() }, None); 91 | } 92 | 93 | #[test] 94 | fn invalid() { 95 | assert_eq!( 96 | should_failed("supports xxxyyyzzz", &Opts::default()), 97 | Error::UnknownBrowserFeature(String::from("xxxyyyzzz")) 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /xtask/src/generators/caniuse/regions.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use anyhow::Result; 4 | use bincode::{config::Configuration, encode_to_vec}; 5 | use indexmap::IndexMap; 6 | use quote::quote; 7 | use serde::Deserialize; 8 | 9 | use crate::data::{Caniuse, encode_browser_name}; 10 | use crate::utils::{create_range_vec, generate_file, root, save_bin_compressed}; 11 | 12 | #[derive(Deserialize)] 13 | struct RegionData { 14 | data: IndexMap>>, 15 | } 16 | 17 | struct RegionDatum { 18 | browser: u8, 19 | version: String, 20 | usage: f32, 21 | } 22 | 23 | const STANDARD: Configuration = bincode::config::standard(); 24 | 25 | pub fn build_caniuse_region_matching(data: &Caniuse) -> Result<()> { 26 | let agents = &data.agents; 27 | let files_path = root().join("node_modules/caniuse-db/region-usage-json"); 28 | let files = fs::read_dir(files_path)? 29 | .map(|entry| entry.map_err(anyhow::Error::from)) 30 | .collect::>>()?; 31 | 32 | let mut data = files 33 | .iter() 34 | .map(|file| { 35 | let RegionData { data } = 36 | serde_json::from_slice(&fs::read(file.path()).unwrap()).unwrap(); 37 | let mut usage = data 38 | .into_iter() 39 | .flat_map(|(name, stat)| { 40 | let agent = agents.get(&name).unwrap(); 41 | stat.into_iter().filter_map(move |(version, usage)| { 42 | let version = if version.as_str() == "0" { 43 | agent.version_list.last().unwrap().version.clone() 44 | } else { 45 | version 46 | }; 47 | usage.map(|usage| RegionDatum { 48 | browser: encode_browser_name(&name), 49 | version, 50 | usage, 51 | }) 52 | }) 53 | }) 54 | .collect::>(); 55 | usage.sort_unstable_by(|a, b| b.usage.partial_cmp(&a.usage).unwrap()); 56 | let key = file.path().file_stem().unwrap().to_str().map(|s| s.to_owned()).unwrap(); 57 | (key, usage) 58 | }) 59 | .collect::>(); 60 | 61 | data.sort_unstable_by(|a, b| a.0.cmp(&b.0)); 62 | 63 | let keys = data.iter().map(|(key, _)| key).collect::>(); 64 | 65 | let browsers = data 66 | .iter() 67 | .map(|(_region, datums)| datums.iter().map(|x| x.browser).collect::>()) 68 | .collect::>(); 69 | let browsers_ranges = create_range_vec(&browsers); 70 | let browsers_bytes = browsers.iter().flat_map(|x| x.iter()).copied().collect::>(); 71 | save_bin_compressed("caniuse_region_browsers.bin", &browsers_bytes); 72 | 73 | let versions = data 74 | .iter() 75 | .map(|(_, datums)| { 76 | let versions = datums.iter().map(|x| x.version.clone()).collect::>(); 77 | encode_to_vec(versions, STANDARD).unwrap() 78 | }) 79 | .collect::>(); 80 | let version_ranges = create_range_vec(&versions); 81 | let version_bytes = versions.iter().flat_map(|x| x.iter()).copied().collect::>(); 82 | save_bin_compressed("caniuse_region_versions.bin", &version_bytes); 83 | 84 | let percentages = data 85 | .iter() 86 | .map(|(_region, datums)| { 87 | let percentages = datums.iter().map(|x| x.usage).collect::>(); 88 | encode_to_vec(percentages, STANDARD).unwrap() 89 | }) 90 | .collect::>(); 91 | let percent_ranges = create_range_vec(&percentages); 92 | let percent_bytes = percentages.iter().flat_map(|x| x.iter()).copied().collect::>(); 93 | save_bin_compressed("caniuse_region_percentages.bin", &percent_bytes); 94 | 95 | let output = quote! { 96 | use crate::data::caniuse::region::RegionData; 97 | 98 | const KEYS: &[&str] = &[#(#keys,)*]; 99 | const BROWSER_RANGES: &[u32] = &[#(#browsers_ranges,)*]; 100 | const VERSION_RANGES: &[u32] = &[#(#version_ranges,)*]; 101 | const PERCENT_RANGES: &[u32] = &[#(#percent_ranges,)*]; 102 | 103 | pub fn get_usage_by_region(region: &str) -> Option { 104 | let index = KEYS.binary_search(®ion).ok()?; 105 | let browser_start = BROWSER_RANGES[index]; 106 | let browser_end = BROWSER_RANGES[index + 1]; 107 | let version_start = VERSION_RANGES[index]; 108 | let version_end = VERSION_RANGES[index + 1]; 109 | let percent_start = PERCENT_RANGES[index]; 110 | let percent_end = PERCENT_RANGES[index + 1]; 111 | Some(RegionData::new(browser_start, browser_end, version_start, version_end, percent_start, percent_end)) 112 | } 113 | }; 114 | generate_file("caniuse_region_matching.rs", output); 115 | 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! **oxc-browserslist** is a Rust-based implementation of [Browserslist](https://github.com/browserslist/browserslist). 2 | //! 3 | //! ## Introduction 4 | //! 5 | //! This library bundles Can I Use data, Electron versions list and Node.js releases list, 6 | //! so it won't and doesn't need to access any data files. 7 | //! 8 | //! Except several non-widely/non-frequently used features, 9 | //! this library works as same as the JavaScript-based 10 | //! implementation [Browserslist](https://github.com/browserslist/browserslist). 11 | //! 12 | //! ## Usage 13 | //! 14 | //! It provides a simple API for querying which accepts a sequence of strings and options [`Opts`], 15 | //! then returns the result. 16 | //! 17 | //! ``` 18 | //! use browserslist::{Distrib, Opts, resolve, Error}; 19 | //! 20 | //! let distribs = resolve(&["ie <= 6"], &Opts::default()).unwrap(); 21 | //! assert_eq!(distribs[0].name(), "ie"); 22 | //! assert_eq!(distribs[0].version(), "6"); 23 | //! assert_eq!(distribs[1].name(), "ie"); 24 | //! assert_eq!(distribs[1].version(), "5.5"); 25 | //! 26 | //! assert_eq!( 27 | //! resolve(&["yuru 1.0"], &Opts::default()), 28 | //! Err(Error::BrowserNotFound(String::from("yuru"))) 29 | //! ); 30 | //! ``` 31 | //! 32 | //! The result isn't a list of strings, instead, it's a tuple struct called [`Distrib`]. 33 | //! If you need to retrieve something like JavaScript-based implementation of 34 | //! [Browserslist](https://github.com/browserslist/browserslist), 35 | //! you can convert them to strings: 36 | //! 37 | //! ``` 38 | //! use browserslist::{Distrib, Opts, resolve, Error}; 39 | //! 40 | //! let distribs = resolve(&["ie <= 6"], &Opts::default()).unwrap(); 41 | //! assert_eq!( 42 | //! distribs.into_iter().map(|d| d.to_string()).collect::>(), 43 | //! vec![String::from("ie 6"), String::from("ie 5.5")] 44 | //! ); 45 | //! ``` 46 | //! 47 | //! ## WebAssembly 48 | //! 49 | //! This crate can be compiled as WebAssembly, without configuring any features manually. 50 | //! 51 | //! Please note that browser and Deno can run WebAssembly, 52 | //! but those environments aren't Node.js, 53 | //! so you will receive an error when querying `current node` in those environments. 54 | 55 | pub use error::Error; 56 | pub use opts::Opts; 57 | use parser::parse_browserslist_query; 58 | pub use queries::Distrib; 59 | pub use semver::Version; 60 | #[cfg(all(feature = "wasm_bindgen", target_arch = "wasm32"))] 61 | pub use wasm::browserslist; 62 | 63 | #[cfg(not(target_arch = "wasm32"))] 64 | mod config; 65 | mod data; 66 | mod error; 67 | mod generated; 68 | mod opts; 69 | mod parser; 70 | mod queries; 71 | mod semver; 72 | #[cfg(test)] 73 | mod test; 74 | #[cfg(all(feature = "wasm_bindgen", target_arch = "wasm32"))] 75 | mod wasm; 76 | 77 | /// Resolve browserslist queries. 78 | /// 79 | /// This is a low-level API. 80 | /// If you want to load queries from configuration file and 81 | /// resolve them automatically, 82 | /// use the higher-level API [`execute`] instead. 83 | /// 84 | /// ``` 85 | /// use browserslist::{Distrib, Opts, resolve}; 86 | /// 87 | /// let distribs = resolve(&["ie <= 6"], &Opts::default()).unwrap(); 88 | /// assert_eq!(distribs[0].name(), "ie"); 89 | /// assert_eq!(distribs[0].version(), "6"); 90 | /// assert_eq!(distribs[1].name(), "ie"); 91 | /// assert_eq!(distribs[1].version(), "5.5"); 92 | /// ``` 93 | pub fn resolve(queries: &[S], opts: &Opts) -> Result, Error> 94 | where 95 | S: AsRef, 96 | { 97 | match queries.len() { 98 | 1 => _resolve(queries[0].as_ref(), opts), 99 | _ => { 100 | // Pre-calculate capacity to avoid reallocations 101 | let total_len: usize = queries.iter().map(|q| q.as_ref().len() + 1).sum(); 102 | let mut s = String::with_capacity(total_len); 103 | for (i, q) in queries.iter().enumerate() { 104 | if i > 0 { 105 | s.push(','); 106 | } 107 | s.push_str(q.as_ref()); 108 | } 109 | _resolve(&s, opts) 110 | } 111 | } 112 | } 113 | 114 | // reduce generic monomorphization 115 | fn _resolve(query: &str, opts: &Opts) -> Result, Error> { 116 | let queries = parse_browserslist_query(query)?; 117 | let mut distribs = vec![]; 118 | for (i, current) in queries.1.into_iter().enumerate() { 119 | if i == 0 && current.negated { 120 | return handle_first_negated_error(current.raw.to_string()); 121 | } 122 | 123 | let dist = queries::query(current.atom, opts)?; 124 | apply_query_operation(&mut distribs, dist, current.negated, current.is_and); 125 | } 126 | 127 | sort_and_dedup_distribs(&mut distribs); 128 | Ok(distribs) 129 | } 130 | 131 | // Separate function to reduce _resolve size and improve inlining decisions 132 | fn apply_query_operation( 133 | distribs: &mut Vec, 134 | dist: Vec, 135 | negated: bool, 136 | is_and: bool, 137 | ) { 138 | if negated { 139 | distribs.retain(|d| !dist.contains(d)); 140 | } else if is_and { 141 | distribs.retain(|d| dist.contains(d)); 142 | } else { 143 | distribs.extend(dist); 144 | } 145 | } 146 | 147 | // Optimized sorting that parses versions only once 148 | fn sort_and_dedup_distribs(distribs: &mut Vec) { 149 | if distribs.is_empty() { 150 | return; 151 | } 152 | 153 | // Use sort_by_cached_key to parse each version only once 154 | distribs.sort_by_cached_key(|d| { 155 | let version = d.version().parse::().unwrap_or_default(); 156 | (d.name().to_string(), std::cmp::Reverse(version)) 157 | }); 158 | 159 | // Dedup in place 160 | distribs.dedup(); 161 | } 162 | 163 | // Cold path for error handling 164 | #[cold] 165 | fn handle_first_negated_error(raw: String) -> Result, Error> { 166 | Err(Error::NotAtFirst(raw)) 167 | } 168 | 169 | #[cfg(not(target_arch = "wasm32"))] 170 | /// Load queries from configuration with environment information, 171 | /// then resolve those queries. 172 | /// 173 | /// If you want to resolve custom queries (not from configuration file), 174 | /// use the lower-level API [`resolve`] instead. 175 | /// 176 | /// ``` 177 | /// use browserslist::{Opts, execute}; 178 | /// 179 | /// // when no config found, it use `defaults` query 180 | /// assert!(!execute(&Opts::default()).unwrap().is_empty()); 181 | /// ``` 182 | pub fn execute(opts: &Opts) -> Result, Error> { 183 | resolve(&config::load(opts)?, opts) 184 | } 185 | -------------------------------------------------------------------------------- /src/queries/extends.rs: -------------------------------------------------------------------------------- 1 | use super::QueryResult; 2 | use crate::{error::Error, opts::Opts}; 3 | 4 | #[cfg(test)] 5 | fn base_test_dir() -> &'static std::path::Path { 6 | use std::{env::temp_dir, path::PathBuf, sync::OnceLock}; 7 | static BASE_TEST_DIR: OnceLock = OnceLock::new(); 8 | BASE_TEST_DIR.get_or_init(|| temp_dir().join("browserslist-test-pkgs")) 9 | } 10 | 11 | #[cfg(target_arch = "wasm32")] 12 | pub(super) fn extends(pkg: &str, opts: &Opts) -> QueryResult { 13 | if opts.dangerous_extend { 14 | Err(Error::UnsupportedExtends) 15 | } else { 16 | check_extend_name(pkg).map(|_| Default::default()) 17 | } 18 | } 19 | 20 | #[cfg(not(target_arch = "wasm32"))] 21 | pub(super) fn extends(pkg: &str, opts: &Opts) -> QueryResult { 22 | use std::{env, process}; 23 | 24 | use crate::{config, resolve}; 25 | 26 | let dangerous_extend = 27 | opts.dangerous_extend || env::var("BROWSERSLIST_DANGEROUS_EXTEND").is_ok(); 28 | if !dangerous_extend { 29 | check_extend_name(pkg)?; 30 | } 31 | 32 | let mut command = process::Command::new("node"); 33 | command.args(["-p", &format!("JSON.stringify(require('{pkg}'))")]); 34 | #[cfg(test)] 35 | command.current_dir(base_test_dir()); 36 | let output = command.output().map_err(|_| Error::UnsupportedExtends)?.stdout; 37 | let config = serde_json::from_str(&String::from_utf8_lossy(&output)) 38 | .map_err(|_| Error::FailedToResolveExtend(pkg.to_string()))?; 39 | 40 | resolve(&config::load_with_config(config, opts)?, opts) 41 | } 42 | 43 | fn check_extend_name(pkg: &str) -> Result<(), Error> { 44 | let unscoped = 45 | pkg.strip_prefix('@').and_then(|s| s.find('/').and_then(|i| s.get(i + 1..))).unwrap_or(pkg); 46 | if !(unscoped.starts_with("browserslist-config-") 47 | || pkg.starts_with('@') && unscoped == "browserslist-config") 48 | { 49 | return Err(Error::InvalidExtendName( 50 | "Browserslist config needs `browserslist-config-` prefix.", 51 | )); 52 | } 53 | if unscoped.contains('.') { 54 | return Err(Error::InvalidExtendName("`.` not allowed in Browserslist config name.")); 55 | } 56 | if pkg.contains("node_modules") { 57 | return Err(Error::InvalidExtendName("`node_modules` not allowed in Browserslist config.")); 58 | } 59 | 60 | Ok(()) 61 | } 62 | 63 | #[cfg(all(test, not(miri)))] 64 | mod tests { 65 | use std::fs; 66 | 67 | use serde_json::json; 68 | use test_case::test_case; 69 | 70 | use super::*; 71 | use crate::{ 72 | opts::Opts, 73 | test::{run_compare, should_failed}, 74 | }; 75 | 76 | fn mock(name: &str, value: serde_json::Value) { 77 | let dir = base_test_dir().join("node_modules").join(name); 78 | fs::create_dir_all(&dir).unwrap(); 79 | fs::write( 80 | dir.join("index.js"), 81 | format!("module.exports = {}", serde_json::to_string(&value).unwrap()), 82 | ) 83 | .unwrap(); 84 | } 85 | 86 | fn clean(name: &str) { 87 | let _ = fs::remove_dir_all(base_test_dir().join("node_modules").join(name)); 88 | } 89 | 90 | #[test_case("browserslist-config-test", json!(["ie 11"]), "extends browserslist-config-test"; "package")] 91 | #[test_case("browserslist-config-test-file/ie", json!(["ie 11"]), "extends browserslist-config-test-file/ie"; "file in package")] 92 | #[test_case("@scope/browserslist-config-test", json!(["ie 11"]), "extends @scope/browserslist-config-test"; "scoped package")] 93 | #[test_case("@example.com/browserslist-config-test", json!(["ie 11"]), "extends @example.com/browserslist-config-test"; "scoped package with dot in name")] 94 | #[test_case("@scope/browserslist-config-test-file/ie", json!(["ie 11"]), "extends @scope/browserslist-config-test-file/ie"; "file in scoped package")] 95 | #[test_case("@scope/browserslist-config", json!(["ie 11"]), "extends @scope/browserslist-config"; "file-less scoped package")] 96 | #[test_case("browserslist-config-rel", json!(["ie 9-10"]), "extends browserslist-config-rel and not ie 9"; "with override")] 97 | #[test_case("browserslist-config-with-env-a", json!({ "someEnv": ["ie 10"] }), "extends browserslist-config-with-env-a"; "no default env")] 98 | #[test_case("browserslist-config-with-defaults", json!({ "defaults": ["ie 10"] }), "extends browserslist-config-with-defaults"; "default env")] 99 | fn valid(pkg: &str, value: serde_json::Value, query: &str) { 100 | mock(pkg, value); 101 | run_compare(query, &Default::default(), Some(base_test_dir())); 102 | clean(pkg); 103 | } 104 | 105 | #[test] 106 | fn dangerous_extend() { 107 | mock("pkg", json!(["ie 11"])); 108 | run_compare( 109 | "extends pkg", 110 | &Opts { dangerous_extend: true, ..Default::default() }, 111 | Some(base_test_dir()), 112 | ); 113 | clean("pkg"); 114 | } 115 | 116 | #[test] 117 | fn recursively_import() { 118 | mock("browserslist-config-a", json!(["extends browserslist-config-b", "ie 9"])); 119 | mock("browserslist-config-b", json!(["ie 10"])); 120 | run_compare("extends browserslist-config-a", &Default::default(), Some(base_test_dir())); 121 | clean("browserslist-config-a"); 122 | clean("browserslist-config-b"); 123 | } 124 | 125 | #[test] 126 | fn specific_env() { 127 | mock("browserslist-config-with-env-b", json!(["ie 11"])); 128 | run_compare( 129 | "extends browserslist-config-with-env-b", 130 | &Opts { env: Some("someEnv".into()), ..Default::default() }, 131 | Some(base_test_dir()), 132 | ); 133 | clean("browserslist-config-with-env-b"); 134 | } 135 | 136 | #[test_case("browserslist-config-wrong", json!(null), "extends browserslist-config-wrong"; "empty export")] 137 | fn invalid(pkg: &str, value: serde_json::Value, query: &str) { 138 | mock(pkg, value); 139 | assert!(matches!( 140 | should_failed(query, &Default::default()), 141 | Error::FailedToResolveExtend(..) 142 | )); 143 | clean(pkg); 144 | } 145 | 146 | #[test_case("extends thing-without-prefix"; "without prefix")] 147 | #[test_case("extends browserslist-config-package/../something"; "has dot")] 148 | #[test_case("extends browserslist-config-test/node_modules/a"; "has node_modules")] 149 | fn invalid_name(query: &str) { 150 | assert!(matches!(should_failed(query, &Default::default()), Error::InvalidExtendName(..))); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/config/parser.rs: -------------------------------------------------------------------------------- 1 | use rustc_hash::FxHashSet; 2 | 3 | use super::PartialConfig; 4 | use crate::error::Error; 5 | 6 | pub fn parse(source: &str, env: &str, throw_on_missing: bool) -> Result { 7 | let mut encountered_sections = FxHashSet::default(); 8 | let mut current_section = Some("defaults"); 9 | let mut defaults_queries = Vec::new(); 10 | let mut env_queries: Option> = None; 11 | 12 | // Process lines efficiently in a single loop 13 | for line in source.lines() { 14 | // Remove comments and trim in one step 15 | let line = 16 | if let Some(index) = line.find('#') { line[..index].trim() } else { line.trim() }; 17 | 18 | if line.is_empty() { 19 | continue; 20 | } 21 | 22 | if line.starts_with('[') && line.ends_with(']') { 23 | // Parse section header inline 24 | let sections: Vec<&str> = line[1..line.len() - 1].split_whitespace().collect(); 25 | 26 | // Check for duplicates and collect into owned strings 27 | for section in §ions { 28 | if encountered_sections.contains(*section) { 29 | return Err(Error::DuplicatedSection(section.to_string())); 30 | } 31 | encountered_sections.insert(section.to_string()); 32 | } 33 | 34 | // Update current section 35 | current_section = sections.iter().find(|&&s| s == env).copied(); 36 | 37 | // Initialize env queries if needed 38 | if env_queries.is_none() && encountered_sections.contains(env) { 39 | env_queries = Some(Vec::new()); 40 | } 41 | } else if current_section.is_some() { 42 | // Add query to appropriate collection 43 | if let Some(ref mut env_queries) = env_queries { 44 | env_queries.push(line.to_string()); 45 | } else { 46 | defaults_queries.push(line.to_string()); 47 | } 48 | } 49 | } 50 | 51 | // Validate environment requirement 52 | if throw_on_missing && env != "defaults" && !encountered_sections.contains(env) { 53 | return Err(Error::MissingEnv(env.to_string())); 54 | } 55 | 56 | Ok(PartialConfig { defaults: defaults_queries, env: env_queries }) 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | 63 | #[test] 64 | fn empty() { 65 | let source = " \t \n \r\n # comment "; 66 | let config = parse(source, "production", false).unwrap(); 67 | assert!(config.defaults.is_empty()); 68 | assert!(config.env.is_none()); 69 | } 70 | 71 | #[test] 72 | fn no_sections() { 73 | let source = r" 74 | last 2 versions 75 | not dead 76 | "; 77 | let config = parse(source, "production", false).unwrap(); 78 | assert_eq!(&*config.defaults, ["last 2 versions", "not dead"]); 79 | assert!(config.env.is_none()); 80 | } 81 | 82 | #[test] 83 | fn single_line() { 84 | let source = r"last 2 versions, not dead"; 85 | let config = parse(source, "production", false).unwrap(); 86 | assert_eq!(&*config.defaults, ["last 2 versions, not dead"]); 87 | assert!(config.env.is_none()); 88 | } 89 | 90 | #[test] 91 | fn empty_lines() { 92 | let source = r" 93 | last 2 versions 94 | 95 | 96 | not dead 97 | "; 98 | let config = parse(source, "production", false).unwrap(); 99 | assert_eq!(&*config.defaults, ["last 2 versions", "not dead"]); 100 | assert!(config.env.is_none()); 101 | } 102 | 103 | #[test] 104 | fn comments() { 105 | let source = r" 106 | last 2 versions #trailing comment 107 | #line comment 108 | not dead 109 | "; 110 | let config = parse(source, "production", false).unwrap(); 111 | assert_eq!(&*config.defaults, ["last 2 versions", "not dead"]); 112 | assert!(config.env.is_none()); 113 | } 114 | 115 | #[test] 116 | fn spaces() { 117 | let source = " last 2 versions \n not dead "; 118 | let config = parse(source, "production", false).unwrap(); 119 | assert_eq!(&*config.defaults, ["last 2 versions", "not dead"]); 120 | assert!(config.env.is_none()); 121 | } 122 | 123 | #[test] 124 | fn one_section() { 125 | let source = r" 126 | [production] 127 | last 2 versions 128 | not dead 129 | "; 130 | let config = parse(source, "production", false).unwrap(); 131 | assert!(config.defaults.is_empty()); 132 | assert_eq!(config.env.as_deref().unwrap(), ["last 2 versions", "not dead"]); 133 | } 134 | 135 | #[test] 136 | fn defaults_and_env_mixed() { 137 | let source = r" 138 | > 1% 139 | 140 | [production] 141 | last 2 versions 142 | not dead 143 | "; 144 | let config = parse(source, "production", false).unwrap(); 145 | assert_eq!(&*config.defaults, ["> 1%"]); 146 | assert_eq!(config.env.as_deref().unwrap(), ["last 2 versions", "not dead"]); 147 | } 148 | 149 | #[test] 150 | fn multi_sections() { 151 | let source = r" 152 | [production] 153 | > 1% 154 | ie 10 155 | 156 | [ modern] 157 | last 1 chrome version 158 | last 1 firefox version 159 | 160 | [ssr ] 161 | node 12 162 | "; 163 | let config = parse(source, "production", false).unwrap(); 164 | assert!(config.defaults.is_empty()); 165 | assert_eq!(config.env.as_deref().unwrap(), ["> 1%", "ie 10"]); 166 | 167 | let config = parse(source, "modern", false).unwrap(); 168 | assert!(config.defaults.is_empty()); 169 | assert_eq!( 170 | config.env.as_deref().unwrap(), 171 | ["last 1 chrome version", "last 1 firefox version"] 172 | ); 173 | 174 | let config = parse(source, "ssr", false).unwrap(); 175 | assert!(config.defaults.is_empty()); 176 | assert_eq!(config.env.as_deref().unwrap(), ["node 12"]); 177 | } 178 | 179 | #[test] 180 | fn shared_multi_sections() { 181 | let source = r" 182 | [production development] 183 | > 1% 184 | ie 10 185 | "; 186 | let config = parse(source, "development", false).unwrap(); 187 | assert!(config.defaults.is_empty()); 188 | assert_eq!(config.env.as_deref().unwrap(), ["> 1%", "ie 10"]); 189 | } 190 | 191 | #[test] 192 | fn duplicated_sections() { 193 | let source = r" 194 | [production production] 195 | > 1% 196 | ie 10 197 | "; 198 | assert_eq!( 199 | parse(source, "testing", false), 200 | Err(Error::DuplicatedSection("production".into())) 201 | ); 202 | 203 | let source = r" 204 | [development] 205 | last 1 chrome version 206 | 207 | [production] 208 | > 1 % 209 | not dead 210 | 211 | [development] 212 | last 1 firefox version 213 | "; 214 | assert_eq!( 215 | parse(source, "testing", false), 216 | Err(Error::DuplicatedSection("development".into())) 217 | ); 218 | } 219 | 220 | #[test] 221 | fn mismatch_section() { 222 | let source = r" 223 | [production] 224 | > 1% 225 | ie 10 226 | "; 227 | let config = parse(source, "development", false).unwrap(); 228 | assert!(config.defaults.is_empty()); 229 | assert!(config.env.is_none()); 230 | } 231 | 232 | #[test] 233 | fn throw_on_missing_env() { 234 | let source = "node 16"; 235 | let err = parse(source, "SSR", true).unwrap_err(); 236 | assert_eq!(err, Error::MissingEnv("SSR".into())); 237 | } 238 | 239 | #[test] 240 | fn dont_throw_if_existed() { 241 | let source = r" 242 | [production] 243 | > 1% 244 | ie 10 245 | "; 246 | let config = parse(source, "production", true).unwrap(); 247 | assert!(config.defaults.is_empty()); 248 | assert!(config.env.is_some()); 249 | } 250 | 251 | #[test] 252 | fn dont_throw_for_defaults() { 253 | let source = r" 254 | [production] 255 | > 1% 256 | ie 10 257 | "; 258 | let config = parse(source, "defaults", true).unwrap(); 259 | assert!(config.defaults.is_empty()); 260 | assert!(config.env.is_none()); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/generated/caniuse_global_usage.rs: -------------------------------------------------------------------------------- 1 | /// only includes browsers with global usage > 0.0% 2 | pub static CANIUSE_GLOBAL_USAGE: &[(u8, &str, f32)] = &[ 3 | (12u8, "143", 41.8556f32), 4 | (4u8, "142", 11.1809f32), 5 | (7u8, "18.5-18.7", 8.23969f32), 6 | (4u8, "141", 3.75978f32), 7 | (2u8, "142", 3.72227f32), 8 | (4u8, "139", 3.44099f32), 9 | (4u8, "112", 2.29712f32), 10 | (16u8, "29", 1.50594f32), 11 | (4u8, "134", 1.08762f32), 12 | (4u8, "130", 0.89072f32), 13 | (11u8, "80", 0.825856f32), 14 | (4u8, "109", 0.731328f32), 15 | (3u8, "145", 0.72664f32), 16 | (4u8, "140", 0.684448f32), 17 | (3u8, "144", 0.614128f32), 18 | (15u8, "15.5", 0.573696f32), 19 | (7u8, "26.0", 0.563942f32), 20 | (4u8, "126", 0.525056f32), 21 | (7u8, "26.1", 0.515571f32), 22 | (2u8, "141", 0.501616f32), 23 | (4u8, "125", 0.49224f32), 24 | (4u8, "137", 0.482864f32), 25 | (9u8, "143", 0.461543f32), 26 | (6u8, "123", 0.417232f32), 27 | (7u8, "16.6-16.7", 0.34568f32), 28 | (1u8, "11", 0.330309f32), 29 | (4u8, "138", 0.318784f32), 30 | (13u8, "146", 0.302784f32), 31 | (4u8, "131", 0.290656f32), 32 | (6u8, "122", 0.290656f32), 33 | (7u8, "17.6-17.7", 0.280791f32), 34 | (7u8, "15.6-15.8", 0.256016f32), 35 | (7u8, "18.3", 0.23006f32), 36 | (5u8, "26.1", 0.229712f32), 37 | (16u8, "28", 0.227516f32), 38 | (7u8, "11.0-11.2", 0.219442f32), 39 | (5u8, "26.0", 0.206272f32), 40 | (4u8, "120", 0.196896f32), 41 | (4u8, "114", 0.182832f32), 42 | (4u8, "105", 0.173456f32), 43 | (4u8, "110", 0.168768f32), 44 | (6u8, "124", 0.16408f32), 45 | (4u8, "127", 0.159392f32), 46 | (4u8, "128", 0.150016f32), 47 | (17u8, "14.9", 0.148736f32), 48 | (3u8, "115", 0.145328f32), 49 | (4u8, "117", 0.14064f32), 50 | (5u8, "17.6", 0.14064f32), 51 | (4u8, "129", 0.135952f32), 52 | (7u8, "18.1", 0.132137f32), 53 | (5u8, "16.6", 0.131264f32), 54 | (7u8, "18.4", 0.11798f32), 55 | (5u8, "18.5-18.6", 0.1172f32), 56 | (4u8, "121", 0.1172f32), 57 | (3u8, "118", 0.1172f32), 58 | (7u8, "17.5", 0.11444f32), 59 | (4u8, "106", 0.112512f32), 60 | (4u8, "132", 0.103136f32), 61 | (4u8, "118", 0.103136f32), 62 | (4u8, "122", 0.098448f32), 63 | (5u8, "17.1", 0.09376f32), 64 | (4u8, "111", 0.089072f32), 65 | (5u8, "15.6", 0.089072f32), 66 | (4u8, "103", 0.079696f32), 67 | (4u8, "123", 0.079696f32), 68 | (3u8, "140", 0.079696f32), 69 | (4u8, "124", 0.075008f32), 70 | (4u8, "116", 0.075008f32), 71 | (6u8, "92", 0.075008f32), 72 | (4u8, "79", 0.075008f32), 73 | (7u8, "18.2", 0.0707877f32), 74 | (4u8, "107", 0.07032f32), 75 | (4u8, "133", 0.07032f32), 76 | (4u8, "135", 0.065632f32), 77 | (4u8, "136", 0.065632f32), 78 | (2u8, "140", 0.065632f32), 79 | (7u8, "18.0", 0.0625291f32), 80 | (4u8, "113", 0.060944f32), 81 | (7u8, "17.4", 0.0601696f32), 82 | (7u8, "16.1", 0.0589898f32), 83 | (7u8, "16.3", 0.0566302f32), 84 | (7u8, "12.2-12.5", 0.0554504f32), 85 | (16u8, "27", 0.0541704f32), 86 | (1u8, "9", 0.052154f32), 87 | (5u8, "18.3", 0.051568f32), 88 | (3u8, "11", 0.051568f32), 89 | (4u8, "119", 0.04688f32), 90 | (4u8, "83", 0.04688f32), 91 | (16u8, "26", 0.0433363f32), 92 | (4u8, "98", 0.042192f32), 93 | (4u8, "108", 0.042192f32), 94 | (4u8, "143", 0.042192f32), 95 | (8u8, "all", 0.04f32), 96 | (5u8, "17.5", 0.037504f32), 97 | (3u8, "43", 0.037504f32), 98 | (2u8, "114", 0.037504f32), 99 | (4u8, "115", 0.037504f32), 100 | (4u8, "87", 0.037504f32), 101 | (7u8, "17.3", 0.0365737f32), 102 | (7u8, "17.1", 0.0353939f32), 103 | (1u8, "8", 0.0347693f32), 104 | (3u8, "143", 0.032816f32), 105 | (2u8, "121", 0.032816f32), 106 | (2u8, "109", 0.032816f32), 107 | (7u8, "16.0", 0.0318545f32), 108 | (7u8, "16.2", 0.0306747f32), 109 | (7u8, "17.0", 0.0294949f32), 110 | (2u8, "138", 0.028128f32), 111 | (5u8, "18.4", 0.028128f32), 112 | (6u8, "95", 0.028128f32), 113 | (2u8, "120", 0.028128f32), 114 | (2u8, "139", 0.028128f32), 115 | (7u8, "17.2", 0.0259555f32), 116 | (7u8, "16.5", 0.0235959f32), 117 | (5u8, "14.1", 0.02344f32), 118 | (5u8, "17.4", 0.02344f32), 119 | (5u8, "18.1", 0.02344f32), 120 | (3u8, "128", 0.02344f32), 121 | (7u8, "14.5-14.8", 0.0224161f32), 122 | (16u8, "23", 0.0216682f32), 123 | (16u8, "24", 0.0216682f32), 124 | (16u8, "25", 0.0216682f32), 125 | (19u8, "2.5", 0.021248f32), 126 | (7u8, "10.3", 0.0188767f32), 127 | (7u8, "15.0-15.1", 0.0188767f32), 128 | (2u8, "131", 0.018752f32), 129 | (5u8, "13.1", 0.018752f32), 130 | (5u8, "16.3", 0.018752f32), 131 | (4u8, "66", 0.018752f32), 132 | (4u8, "91", 0.018752f32), 133 | (4u8, "99", 0.018752f32), 134 | (7u8, "14.0-14.4", 0.0176969f32), 135 | (7u8, "15.5", 0.0176969f32), 136 | (7u8, "15.4", 0.0165171f32), 137 | (7u8, "15.2-15.3", 0.0153373f32), 138 | (7u8, "16.4", 0.0141575f32), 139 | (4u8, "52", 0.014064f32), 140 | (5u8, "18.0", 0.014064f32), 141 | (3u8, "142", 0.014064f32), 142 | (4u8, "81", 0.014064f32), 143 | (2u8, "136", 0.014064f32), 144 | (2u8, "137", 0.014064f32), 145 | (5u8, "18.2", 0.014064f32), 146 | (4u8, "48", 0.014064f32), 147 | (4u8, "92", 0.014064f32), 148 | (4u8, "49", 0.014064f32), 149 | (4u8, "93", 0.014064f32), 150 | (4u8, "97", 0.014064f32), 151 | (4u8, "69", 0.014064f32), 152 | (5u8, "16.5", 0.014064f32), 153 | (4u8, "101", 0.014064f32), 154 | (4u8, "77", 0.014064f32), 155 | (4u8, "104", 0.014064f32), 156 | (3u8, "52", 0.014064f32), 157 | (5u8, "17.3", 0.014064f32), 158 | (2u8, "135", 0.014064f32), 159 | (16u8, "21", 0.0108341f32), 160 | (16u8, "22", 0.0108341f32), 161 | (7u8, "9.3", 0.0106182f32), 162 | (7u8, "13.4-13.7", 0.0106182f32), 163 | (3u8, "135", 0.009376f32), 164 | (3u8, "132", 0.009376f32), 165 | (4u8, "45", 0.009376f32), 166 | (5u8, "26.2", 0.009376f32), 167 | (6u8, "93", 0.009376f32), 168 | (4u8, "47", 0.009376f32), 169 | (6u8, "120", 0.009376f32), 170 | (3u8, "141", 0.009376f32), 171 | (4u8, "144", 0.009376f32), 172 | (5u8, "14", 0.009376f32), 173 | (2u8, "132", 0.009376f32), 174 | (5u8, "15.5", 0.009376f32), 175 | (4u8, "85", 0.009376f32), 176 | (4u8, "86", 0.009376f32), 177 | (4u8, "53", 0.009376f32), 178 | (5u8, "16.1", 0.009376f32), 179 | (5u8, "16.2", 0.009376f32), 180 | (2u8, "133", 0.009376f32), 181 | (5u8, "16.4", 0.009376f32), 182 | (2u8, "134", 0.009376f32), 183 | (5u8, "17.2", 0.009376f32), 184 | (4u8, "102", 0.009376f32), 185 | (2u8, "92", 0.009376f32), 186 | (2u8, "122", 0.009376f32), 187 | (2u8, "143", 0.009376f32), 188 | (4u8, "56", 0.009376f32), 189 | (4u8, "58", 0.009376f32), 190 | (3u8, "136", 0.009376f32), 191 | (7u8, "11.3-11.4", 0.00707877f32), 192 | (7u8, "13.2", 0.00589898f32), 193 | (7u8, "6.0-6.1", 0.00471918f32), 194 | (4u8, "41", 0.004688f32), 195 | (5u8, "15.1", 0.004688f32), 196 | (3u8, "137", 0.004688f32), 197 | (5u8, "16.0", 0.004688f32), 198 | (4u8, "88", 0.004688f32), 199 | (4u8, "50", 0.004688f32), 200 | (4u8, "51", 0.004688f32), 201 | (5u8, "15.4", 0.004688f32), 202 | (4u8, "54", 0.004688f32), 203 | (5u8, "17.0", 0.004688f32), 204 | (4u8, "55", 0.004688f32), 205 | (4u8, "100", 0.004688f32), 206 | (4u8, "80", 0.004688f32), 207 | (4u8, "39", 0.004688f32), 208 | (4u8, "40", 0.004688f32), 209 | (4u8, "78", 0.004688f32), 210 | (2u8, "126", 0.004688f32), 211 | (4u8, "70", 0.004688f32), 212 | (3u8, "5", 0.004688f32), 213 | (2u8, "127", 0.004688f32), 214 | (2u8, "128", 0.004688f32), 215 | (5u8, "11.1", 0.004688f32), 216 | (3u8, "78", 0.004688f32), 217 | (4u8, "57", 0.004688f32), 218 | (4u8, "60", 0.004688f32), 219 | (4u8, "42", 0.004688f32), 220 | (4u8, "43", 0.004688f32), 221 | (3u8, "138", 0.004688f32), 222 | (2u8, "129", 0.004688f32), 223 | (2u8, "130", 0.004688f32), 224 | (3u8, "125", 0.004688f32), 225 | (4u8, "44", 0.004688f32), 226 | (3u8, "139", 0.004688f32), 227 | (4u8, "59", 0.004688f32), 228 | (4u8, "46", 0.004688f32), 229 | (7u8, "7.0-7.1", 0.00353939f32), 230 | (7u8, "12.0-12.1", 0.00235959f32), 231 | (7u8, "13.3", 0.00235959f32), 232 | (7u8, "10.0-10.2", 0.0011798f32), 233 | (7u8, "4.2-4.3", 0.0011798f32), 234 | (9u8, "4.4.3-4.4.4", 0.000231072f32), 235 | (9u8, "4.2-4.3", 0.0000924288f32), 236 | ]; 237 | -------------------------------------------------------------------------------- /src/queries/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fmt::Display}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{ 6 | data::caniuse, 7 | error::Error, 8 | opts::Opts, 9 | parser::{QueryAtom, Stats, VersionRange}, 10 | semver::Version, 11 | }; 12 | 13 | mod browser_accurate; 14 | mod browser_bounded_range; 15 | mod browser_unbounded_range; 16 | mod browserslist_config; 17 | mod cover; 18 | mod cover_by_region; 19 | mod current_node; 20 | mod dead; 21 | mod defaults; 22 | mod electron_accurate; 23 | mod electron_bounded_range; 24 | mod electron_unbounded_range; 25 | mod extends; 26 | mod firefox_esr; 27 | mod last_n_browsers; 28 | mod last_n_electron; 29 | mod last_n_electron_major; 30 | mod last_n_major_browsers; 31 | mod last_n_node; 32 | mod last_n_node_major; 33 | mod last_n_x_browsers; 34 | mod last_n_x_major_browsers; 35 | mod maintained_node; 36 | mod node_accurate; 37 | mod node_bounded_range; 38 | mod node_unbounded_range; 39 | mod op_mini; 40 | mod percentage; 41 | mod percentage_by_region; 42 | mod phantom; 43 | mod since; 44 | mod supports; 45 | mod unreleased_browsers; 46 | mod unreleased_electron; 47 | mod unreleased_x_browsers; 48 | mod years; 49 | 50 | /// Representation of browser name (or `node`) and its version. 51 | /// 52 | /// When converting it to string, it will be formatted as the output of 53 | /// [browserslist](https://github.com/browserslist/browserslist). For example: 54 | /// 55 | /// ``` 56 | /// use browserslist::{Opts, resolve}; 57 | /// 58 | /// let distrib = &resolve(&["firefox 93"], &Opts::default()).unwrap()[0]; 59 | /// 60 | /// assert_eq!(distrib.name(), "firefox"); 61 | /// assert_eq!(distrib.version(), "93"); 62 | /// ``` 63 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 64 | pub struct Distrib(Cow<'static, str>, Cow<'static, str>); 65 | 66 | impl Distrib { 67 | #[inline] 68 | fn new>, S: Into>>(name: N, version: S) -> Self { 69 | Self(name.into(), version.into()) 70 | } 71 | 72 | #[inline] 73 | /// Return browser name, or `node`. 74 | /// 75 | /// ``` 76 | /// use browserslist::{Opts, resolve}; 77 | /// 78 | /// let distrib = &resolve(&["firefox 93"], &Opts::default()).unwrap()[0]; 79 | /// 80 | /// assert_eq!(distrib.name(), "firefox"); 81 | /// ``` 82 | #[must_use] 83 | pub fn name(&self) -> &str { 84 | &self.0 85 | } 86 | 87 | #[inline] 88 | /// Return version string. 89 | /// 90 | /// ``` 91 | /// use browserslist::{Opts, resolve}; 92 | /// 93 | /// let distrib = &resolve(&["firefox 93"], &Opts::default()).unwrap()[0]; 94 | /// 95 | /// assert_eq!(distrib.version(), "93"); 96 | /// ``` 97 | #[must_use] 98 | pub fn version(&self) -> &str { 99 | &self.1 100 | } 101 | } 102 | 103 | impl Display for Distrib { 104 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 105 | write!(f, "{} {}", self.0, self.1) 106 | } 107 | } 108 | 109 | pub type QueryResult = Result, Error>; 110 | 111 | pub fn query(atom: QueryAtom, opts: &Opts) -> QueryResult { 112 | match atom { 113 | QueryAtom::Last { count, major, name: Some(name) } 114 | if name.eq_ignore_ascii_case("electron") => 115 | { 116 | let count = count as usize; 117 | if major { 118 | last_n_electron_major::last_n_electron_major(count) 119 | } else { 120 | last_n_electron::last_n_electron(count) 121 | } 122 | } 123 | QueryAtom::Last { count, major, name: Some(name) } if name.eq_ignore_ascii_case("node") => { 124 | let count = count as usize; 125 | if major { 126 | last_n_node_major::last_n_node_major(count) 127 | } else { 128 | last_n_node::last_n_node(count) 129 | } 130 | } 131 | QueryAtom::Last { count, major, name: Some(name) } => { 132 | let count = count as usize; 133 | if major { 134 | last_n_x_major_browsers::last_n_x_major_browsers(count, name, opts) 135 | } else { 136 | last_n_x_browsers::last_n_x_browsers(count, name, opts) 137 | } 138 | } 139 | QueryAtom::Last { count, major, name: None } => { 140 | let count = count as usize; 141 | if major { 142 | last_n_major_browsers::last_n_major_browsers(count, opts) 143 | } else { 144 | last_n_browsers::last_n_browsers(count, opts) 145 | } 146 | } 147 | QueryAtom::Unreleased(Some(name)) if name.eq_ignore_ascii_case("electron") => { 148 | unreleased_electron::unreleased_electron() 149 | } 150 | QueryAtom::Unreleased(Some(name)) => { 151 | unreleased_x_browsers::unreleased_x_browsers(name, opts) 152 | } 153 | QueryAtom::Unreleased(None) => unreleased_browsers::unreleased_browsers(opts), 154 | QueryAtom::Years(count) => years::years(count, opts), 155 | QueryAtom::Since { year, month, day } => since::since(year, month, day, opts), 156 | QueryAtom::Percentage { comparator, popularity, stats: Stats::Global } => { 157 | percentage::percentage(comparator, popularity) 158 | } 159 | QueryAtom::Percentage { comparator, popularity, stats: Stats::Region(region) } => { 160 | percentage_by_region::percentage_by_region(comparator, popularity, region) 161 | } 162 | QueryAtom::Cover { coverage, stats: Stats::Global } => cover::cover(coverage), 163 | QueryAtom::Cover { coverage, stats: Stats::Region(region) } => { 164 | cover_by_region::cover_by_region(coverage, region) 165 | } 166 | QueryAtom::Supports(name, kind) => supports::supports(name, kind, opts), 167 | QueryAtom::Electron(VersionRange::Bounded(from, to)) => { 168 | electron_bounded_range::electron_bounded_range(from, to) 169 | } 170 | QueryAtom::Electron(VersionRange::Unbounded(comparator, version)) => { 171 | electron_unbounded_range::electron_unbounded_range(comparator, version) 172 | } 173 | QueryAtom::Electron(VersionRange::Accurate(version)) => { 174 | electron_accurate::electron_accurate(version) 175 | } 176 | QueryAtom::Node(VersionRange::Bounded(from, to)) => { 177 | node_bounded_range::node_bounded_range(from, to) 178 | } 179 | QueryAtom::Node(VersionRange::Unbounded(comparator, version)) => { 180 | node_unbounded_range::node_unbounded_range(comparator, version) 181 | } 182 | QueryAtom::Node(VersionRange::Accurate(version)) => { 183 | node_accurate::node_accurate(version, opts) 184 | } 185 | QueryAtom::Browser(name, VersionRange::Bounded(from, to)) => { 186 | browser_bounded_range::browser_bounded_range(name, from, to, opts) 187 | } 188 | QueryAtom::Browser(name, VersionRange::Unbounded(comparator, version)) => { 189 | browser_unbounded_range::browser_unbounded_range(name, comparator, version, opts) 190 | } 191 | QueryAtom::Browser(name, VersionRange::Accurate(version)) => { 192 | browser_accurate::browser_accurate(name, version, opts) 193 | } 194 | QueryAtom::FirefoxESR => firefox_esr::firefox_esr(), 195 | QueryAtom::OperaMini => op_mini::op_mini(), 196 | QueryAtom::CurrentNode => current_node::current_node(), 197 | QueryAtom::MaintainedNode => maintained_node::maintained_node(), 198 | QueryAtom::Phantom(is_later_version) => phantom::phantom(is_later_version), 199 | QueryAtom::BrowserslistConfig => browserslist_config::browserslist_config(opts), 200 | QueryAtom::Defaults => defaults::defaults(opts), 201 | QueryAtom::Dead => dead::dead(opts), 202 | QueryAtom::Extends(pkg) => extends::extends(pkg, opts), 203 | QueryAtom::Unknown(query) => Err(Error::UnknownQuery(query.into())), 204 | } 205 | } 206 | 207 | pub fn count_filter_versions(name: &str, mobile_to_desktop: bool, count: usize) -> usize { 208 | let jump = match name { 209 | "android" => { 210 | if mobile_to_desktop { 211 | return count; 212 | } else { 213 | let last_released = &caniuse::get_browser_stat("android", mobile_to_desktop) 214 | .unwrap() 215 | .1 216 | .version_list 217 | .iter() 218 | .filter(|version| version.release_date().is_some()) 219 | .map(|version| version.version()) 220 | .next_back() 221 | .unwrap() 222 | .parse::() 223 | .unwrap(); 224 | (last_released - caniuse::ANDROID_EVERGREEN_FIRST) as usize 225 | } 226 | } 227 | "op_mob" => { 228 | let latest = caniuse::get_browser_stat("android", mobile_to_desktop) 229 | .unwrap() 230 | .1 231 | .version_list 232 | .last() 233 | .unwrap(); 234 | (latest.version().parse::().unwrap().major() - caniuse::OP_MOB_BLINK_FIRST + 1) 235 | as usize 236 | } 237 | _ => return count, 238 | }; 239 | if count <= jump { 1 } else { count + 1 - jump } 240 | } 241 | -------------------------------------------------------------------------------- /src/generated/caniuse_region_matching.rs: -------------------------------------------------------------------------------- 1 | use crate::data::caniuse::region::RegionData; 2 | const KEYS: &[&str] = &[ 3 | "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", 4 | "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BM", "BN", "BO", "BR", "BS", "BT", "BW", "BY", 5 | "BZ", "CA", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CX", 6 | "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ER", "ES", "ET", "FI", "FJ", 7 | "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", 8 | "GQ", "GR", "GT", "GU", "GW", "GY", "HK", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", 9 | "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", 10 | "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", 11 | "MD", "ME", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", 12 | "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", 13 | "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", 14 | "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SK", "SL", "SM", 15 | "SN", "SO", "SR", "ST", "SV", "SY", "SZ", "TC", "TD", "TG", "TH", "TJ", "TL", "TM", "TN", "TO", 16 | "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", 17 | "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW", "alt-af", "alt-an", "alt-as", "alt-eu", 18 | "alt-na", "alt-oc", "alt-sa", "alt-ww", 19 | ]; 20 | const BROWSER_RANGES: &[u32] = &[ 21 | 0u32, 180u32, 384u32, 653u32, 816u32, 952u32, 1143u32, 1332u32, 1596u32, 1776u32, 1904u32, 22 | 2150u32, 2402u32, 2576u32, 2712u32, 2914u32, 3127u32, 3287u32, 3461u32, 3697u32, 3925u32, 23 | 4174u32, 4348u32, 4580u32, 4822u32, 4930u32, 5108u32, 5293u32, 5491u32, 5645u32, 5839u32, 24 | 6025u32, 6249u32, 6419u32, 6673u32, 6932u32, 7077u32, 7283u32, 7522u32, 7752u32, 7876u32, 25 | 8083u32, 8386u32, 8618u32, 8799u32, 8972u32, 9301u32, 9500u32, 9561u32, 9760u32, 9990u32, 26 | 10270u32, 10448u32, 10635u32, 10806u32, 11010u32, 11276u32, 11432u32, 11620u32, 11853u32, 27 | 11977u32, 12233u32, 12491u32, 12711u32, 12919u32, 13011u32, 13132u32, 13301u32, 13578u32, 28 | 13761u32, 14004u32, 14158u32, 14388u32, 14566u32, 14707u32, 14985u32, 15139u32, 15284u32, 29 | 15523u32, 15780u32, 15962u32, 16145u32, 16350u32, 16531u32, 16696u32, 16855u32, 17026u32, 30 | 17294u32, 17463u32, 17673u32, 17919u32, 18139u32, 18356u32, 18551u32, 18776u32, 18936u32, 31 | 19127u32, 19335u32, 19589u32, 19769u32, 20025u32, 20182u32, 20357u32, 20543u32, 20799u32, 32 | 21007u32, 21150u32, 21341u32, 21447u32, 21623u32, 21772u32, 21844u32, 22058u32, 22250u32, 33 | 22404u32, 22607u32, 22805u32, 22997u32, 23165u32, 23332u32, 23509u32, 23737u32, 23919u32, 34 | 24154u32, 24361u32, 24575u32, 24811u32, 25046u32, 25223u32, 25430u32, 25630u32, 25898u32, 35 | 26013u32, 26226u32, 26441u32, 26663u32, 26877u32, 27085u32, 27214u32, 27389u32, 27584u32, 36 | 27679u32, 27856u32, 28054u32, 28237u32, 28495u32, 28712u32, 28946u32, 29208u32, 29424u32, 37 | 29602u32, 29850u32, 29922u32, 30185u32, 30360u32, 30613u32, 30818u32, 30996u32, 31080u32, 38 | 31143u32, 31368u32, 31559u32, 31724u32, 31922u32, 32099u32, 32321u32, 32514u32, 32742u32, 39 | 32962u32, 33078u32, 33143u32, 33334u32, 33541u32, 33758u32, 33867u32, 34007u32, 34193u32, 40 | 34387u32, 34618u32, 34870u32, 35139u32, 35385u32, 35589u32, 35761u32, 36040u32, 36249u32, 41 | 36472u32, 36670u32, 36747u32, 36954u32, 37168u32, 37422u32, 37564u32, 37778u32, 37995u32, 42 | 38164u32, 38297u32, 38486u32, 38729u32, 38956u32, 39100u32, 39306u32, 39546u32, 39762u32, 43 | 40001u32, 40248u32, 40372u32, 40584u32, 40711u32, 40985u32, 41169u32, 41251u32, 41480u32, 44 | 41742u32, 42007u32, 42267u32, 42534u32, 42707u32, 42920u32, 43020u32, 43177u32, 43361u32, 45 | 43476u32, 43624u32, 43905u32, 44055u32, 44141u32, 44291u32, 44504u32, 44678u32, 44916u32, 46 | 45148u32, 45401u32, 45623u32, 45729u32, 45945u32, 46184u32, 46410u32, 46634u32, 46792u32, 47 | 47037u32, 48 | ]; 49 | const VERSION_RANGES: &[u32] = &[ 50 | 0u32, 882u32, 1850u32, 3047u32, 3864u32, 4574u32, 5484u32, 6392u32, 7557u32, 8425u32, 9120u32, 51 | 10241u32, 11389u32, 12258u32, 12965u32, 13912u32, 14900u32, 15709u32, 16547u32, 17634u32, 52 | 18657u32, 19770u32, 20620u32, 21677u32, 22769u32, 23381u32, 24249u32, 25133u32, 26049u32, 53 | 26842u32, 27756u32, 28639u32, 29665u32, 30509u32, 31654u32, 32794u32, 33509u32, 34453u32, 54 | 35555u32, 36598u32, 37256u32, 38205u32, 39503u32, 40560u32, 41441u32, 42295u32, 43684u32, 55 | 44634u32, 45026u32, 45982u32, 47031u32, 48268u32, 49126u32, 50030u32, 50867u32, 51829u32, 56 | 52982u32, 53754u32, 54676u32, 55724u32, 56357u32, 57504u32, 58622u32, 59625u32, 60602u32, 57 | 61118u32, 61759u32, 62595u32, 63820u32, 64687u32, 65788u32, 66566u32, 67620u32, 68487u32, 58 | 69219u32, 70448u32, 71226u32, 71970u32, 73029u32, 74170u32, 75066u32, 75927u32, 76897u32, 59 | 77782u32, 78602u32, 79382u32, 80217u32, 81416u32, 82247u32, 83232u32, 84353u32, 85372u32, 60 | 86383u32, 87321u32, 88368u32, 89175u32, 90073u32, 91050u32, 92183u32, 93072u32, 94221u32, 61 | 95021u32, 95876u32, 96773u32, 97915u32, 98875u32, 99604u32, 100523u32, 101093u32, 101934u32, 62 | 102684u32, 103118u32, 104102u32, 105030u32, 105811u32, 106769u32, 107704u32, 108620u32, 63 | 109450u32, 110276u32, 111137u32, 112168u32, 113036u32, 114110u32, 115098u32, 116095u32, 64 | 117163u32, 118227u32, 119084u32, 120050u32, 120999u32, 122169u32, 122791u32, 123779u32, 65 | 124751u32, 125772u32, 126781u32, 127773u32, 128455u32, 129310u32, 130229u32, 130761u32, 66 | 131641u32, 132593u32, 133481u32, 134612u32, 135625u32, 136686u32, 137839u32, 138846u32, 67 | 139712u32, 140793u32, 141229u32, 142402u32, 143259u32, 144379u32, 145352u32, 146224u32, 68 | 146707u32, 147107u32, 148152u32, 149065u32, 149882u32, 150813u32, 151693u32, 152711u32, 69 | 153641u32, 154673u32, 155687u32, 156333u32, 156741u32, 157662u32, 158643u32, 159648u32, 70 | 160245u32, 160949u32, 161839u32, 162776u32, 163832u32, 164966u32, 166151u32, 167249u32, 71 | 168219u32, 169064u32, 170317u32, 171268u32, 172299u32, 173219u32, 173673u32, 174662u32, 72 | 175675u32, 176816u32, 177544u32, 178533u32, 179523u32, 180346u32, 181025u32, 181930u32, 73 | 183018u32, 184051u32, 184789u32, 185732u32, 186796u32, 187812u32, 188900u32, 190023u32, 74 | 190668u32, 191651u32, 192315u32, 193508u32, 194401u32, 194881u32, 195945u32, 197097u32, 75 | 198264u32, 199412u32, 200598u32, 201444u32, 202432u32, 202986u32, 203761u32, 204630u32, 76 | 205249u32, 205995u32, 207248u32, 208003u32, 208510u32, 209269u32, 210253u32, 211097u32, 77 | 212176u32, 213217u32, 214342u32, 215351u32, 215951u32, 216947u32, 218025u32, 219062u32, 78 | 220096u32, 220872u32, 221971u32, 79 | ]; 80 | const PERCENT_RANGES: &[u32] = &[ 81 | 0u32, 721u32, 1538u32, 2617u32, 3270u32, 3815u32, 4580u32, 5337u32, 6396u32, 7117u32, 7630u32, 82 | 8615u32, 9626u32, 10323u32, 10868u32, 11677u32, 12530u32, 13171u32, 13868u32, 14813u32, 83 | 15726u32, 16723u32, 17420u32, 18349u32, 19318u32, 19751u32, 20464u32, 21205u32, 21998u32, 84 | 22615u32, 23392u32, 24137u32, 25034u32, 25715u32, 26734u32, 27773u32, 28354u32, 29179u32, 85 | 30136u32, 31057u32, 31554u32, 32383u32, 33598u32, 34527u32, 35252u32, 35945u32, 37264u32, 86 | 38061u32, 38306u32, 39103u32, 40024u32, 41147u32, 41860u32, 42609u32, 43294u32, 44111u32, 87 | 45178u32, 45803u32, 46556u32, 47489u32, 47986u32, 49013u32, 50048u32, 50929u32, 51762u32, 88 | 52131u32, 52616u32, 53293u32, 54404u32, 55137u32, 56110u32, 56727u32, 57648u32, 58361u32, 89 | 58926u32, 60041u32, 60658u32, 61239u32, 62196u32, 63227u32, 63956u32, 64689u32, 65510u32, 90 | 66235u32, 66896u32, 67533u32, 68218u32, 69293u32, 69970u32, 70811u32, 71796u32, 72677u32, 91 | 73546u32, 74327u32, 75228u32, 75869u32, 76634u32, 77467u32, 78486u32, 79207u32, 80234u32, 92 | 80863u32, 81564u32, 82309u32, 83336u32, 84169u32, 84742u32, 85507u32, 85932u32, 86637u32, 93 | 87234u32, 87523u32, 88380u32, 89149u32, 89766u32, 90579u32, 91372u32, 92141u32, 92814u32, 94 | 93483u32, 94192u32, 95105u32, 95834u32, 96775u32, 97604u32, 98461u32, 99406u32, 100347u32, 95 | 101056u32, 101885u32, 102686u32, 103761u32, 104222u32, 105075u32, 105936u32, 106825u32, 96 | 107682u32, 108515u32, 109032u32, 109733u32, 110514u32, 110895u32, 111604u32, 112397u32, 97 | 113130u32, 114165u32, 115034u32, 115971u32, 117022u32, 117887u32, 118600u32, 119593u32, 98 | 119882u32, 120937u32, 121638u32, 122653u32, 123474u32, 124187u32, 124524u32, 124777u32, 99 | 125678u32, 126443u32, 127104u32, 127897u32, 128606u32, 129495u32, 130268u32, 131181u32, 100 | 132062u32, 132527u32, 132788u32, 133553u32, 134382u32, 135251u32, 135688u32, 136249u32, 101 | 136994u32, 137771u32, 138696u32, 139707u32, 140786u32, 141771u32, 142588u32, 143277u32, 102 | 144396u32, 145233u32, 146126u32, 146919u32, 147228u32, 148057u32, 148914u32, 149933u32, 103 | 150502u32, 151359u32, 152228u32, 152905u32, 153438u32, 154195u32, 155168u32, 156077u32, 104 | 156654u32, 157479u32, 158440u32, 159305u32, 160262u32, 161251u32, 161748u32, 162597u32, 105 | 163106u32, 164205u32, 164942u32, 165271u32, 166188u32, 167239u32, 168302u32, 169345u32, 106 | 170416u32, 171109u32, 171962u32, 172363u32, 172992u32, 173729u32, 174190u32, 174783u32, 107 | 175910u32, 176511u32, 176856u32, 177457u32, 178310u32, 179007u32, 179960u32, 180889u32, 108 | 181904u32, 182793u32, 183218u32, 184083u32, 185040u32, 185945u32, 186842u32, 187475u32, 109 | 188456u32, 110 | ]; 111 | pub fn get_usage_by_region(region: &str) -> Option { 112 | let index = KEYS.binary_search(®ion).ok()?; 113 | let browser_start = BROWSER_RANGES[index]; 114 | let browser_end = BROWSER_RANGES[index + 1]; 115 | let version_start = VERSION_RANGES[index]; 116 | let version_end = VERSION_RANGES[index + 1]; 117 | let percent_start = PERCENT_RANGES[index]; 118 | let percent_end = PERCENT_RANGES[index + 1]; 119 | Some(RegionData::new( 120 | browser_start, 121 | browser_end, 122 | version_start, 123 | version_end, 124 | percent_start, 125 | percent_end, 126 | )) 127 | } 128 | -------------------------------------------------------------------------------- /src/data/caniuse.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, num::NonZero, sync::OnceLock}; 2 | 3 | use rustc_hash::FxHashMap; 4 | 5 | use crate::data::BrowserName; 6 | 7 | pub mod compression; 8 | pub mod features; 9 | pub mod region; 10 | 11 | pub const ANDROID_EVERGREEN_FIRST: f32 = 37.0; 12 | pub const OP_MOB_BLINK_FIRST: u16 = 14; 13 | 14 | #[derive(Clone, Debug)] 15 | pub struct BrowserStat { 16 | pub name: Cow<'static, str>, 17 | pub version_list: Vec, 18 | } 19 | 20 | #[derive(Debug, Clone)] 21 | pub struct VersionDetail( 22 | /* version */ pub Cow<'static, str>, 23 | /* global_usage */ pub f32, 24 | /* release_date */ pub Option>, 25 | ); 26 | 27 | impl VersionDetail { 28 | pub fn version(&self) -> &str { 29 | &self.0 30 | } 31 | 32 | pub fn global_usage(&self) -> f32 { 33 | self.1 34 | } 35 | 36 | pub fn release_date(&self) -> Option> { 37 | self.2 38 | } 39 | } 40 | 41 | pub type CaniuseData = FxHashMap; 42 | 43 | pub use crate::generated::caniuse_global_usage::CANIUSE_GLOBAL_USAGE; 44 | 45 | pub fn caniuse_browsers() -> &'static CaniuseData { 46 | static CANIUSE_BROWSERS: OnceLock = OnceLock::new(); 47 | CANIUSE_BROWSERS.get_or_init(|| { 48 | const COMPRESSED: &[u8] = include_bytes!("../generated/caniuse_browsers.bin.deflate"); 49 | let decompressed = compression::decompress_deflate(COMPRESSED); 50 | type BrowserData = Vec<(String, String, Vec<(String, f32, Option)>)>; 51 | let data: BrowserData = 52 | bincode::decode_from_slice(&decompressed, bincode::config::standard()).unwrap().0; 53 | data.into_iter() 54 | .map(|(_key, name, version_list)| { 55 | let name_cow = Cow::Owned(name.clone()); 56 | let stat = BrowserStat { 57 | name: Cow::Owned(name), 58 | version_list: version_list 59 | .into_iter() 60 | .map(|(ver, usage, date)| { 61 | VersionDetail(Cow::Owned(ver), usage, date.and_then(NonZero::new)) 62 | }) 63 | .collect(), 64 | }; 65 | (name_cow, stat) 66 | }) 67 | .collect() 68 | }) 69 | } 70 | 71 | pub fn browser_version_aliases() 72 | -> &'static FxHashMap, FxHashMap<&'static str, &'static str>> { 73 | static BROWSER_VERSION_ALIASES: OnceLock< 74 | FxHashMap, FxHashMap<&'static str, &'static str>>, 75 | > = OnceLock::new(); 76 | BROWSER_VERSION_ALIASES.get_or_init(|| { 77 | let mut aliases = caniuse_browsers() 78 | .iter() 79 | .filter_map(|(name, stat)| { 80 | let aliases = stat 81 | .version_list 82 | .iter() 83 | .filter_map(|version| { 84 | version 85 | .version() 86 | .split_once('-') 87 | .map(|(bottom, top)| (bottom, top, version.version())) 88 | }) 89 | .fold( 90 | FxHashMap::<&str, &str>::default(), 91 | move |mut aliases, (bottom, top, version)| { 92 | let _ = aliases.insert(bottom, version); 93 | let _ = aliases.insert(top, version); 94 | aliases 95 | }, 96 | ); 97 | if aliases.is_empty() { None } else { Some((name.clone(), aliases)) } 98 | }) 99 | .collect::, _>>(); 100 | let _ = aliases.insert(Cow::Borrowed("op_mob"), { 101 | let mut aliases = FxHashMap::default(); 102 | let _ = aliases.insert("59", "58"); 103 | aliases 104 | }); 105 | aliases 106 | }) 107 | } 108 | 109 | fn android_to_desktop() -> &'static BrowserStat { 110 | static ANDROID_TO_DESKTOP: OnceLock = OnceLock::new(); 111 | ANDROID_TO_DESKTOP.get_or_init(|| { 112 | let chrome = &caniuse_browsers()["chrome"]; 113 | let android = &caniuse_browsers()["android"]; 114 | 115 | // Pre-calculate chrome skip index to avoid repeated work 116 | let chrome_skip_index = find_chrome_evergreen_start(chrome); 117 | 118 | // Build version list more efficiently 119 | let mut version_list = Vec::new(); 120 | 121 | // Add legacy android versions (2.x, 3.x, 4.x) 122 | version_list.extend( 123 | android 124 | .version_list 125 | .iter() 126 | .filter(|version| is_legacy_android_version(version.version())) 127 | .cloned(), 128 | ); 129 | 130 | // Add chrome versions from evergreen point onwards 131 | version_list.extend(chrome.version_list.iter().skip(chrome_skip_index).cloned()); 132 | 133 | BrowserStat { name: android.name.clone(), version_list } 134 | }) 135 | } 136 | 137 | // Extract filtering logic to separate functions for better optimization 138 | #[inline] 139 | fn is_legacy_android_version(version: &str) -> bool { 140 | version.starts_with("2.") 141 | || version.starts_with("3.") 142 | || version.starts_with("4.") 143 | || version == "3" 144 | || version == "4" 145 | } 146 | 147 | // Extract chrome start index calculation 148 | fn find_chrome_evergreen_start(chrome: &BrowserStat) -> usize { 149 | chrome 150 | .version_list 151 | .iter() 152 | .position(|version| { 153 | version 154 | .version() 155 | .parse::() 156 | .map(|v| v == ANDROID_EVERGREEN_FIRST as usize) 157 | .unwrap_or(false) 158 | }) 159 | .unwrap_or(0) 160 | } 161 | 162 | pub fn get_browser_stat( 163 | name: &str, 164 | mobile_to_desktop: bool, 165 | ) -> Option<(&'static str, &'static BrowserStat)> { 166 | // Optimize string processing: fast path for already lowercase names 167 | let normalized_name = if name.bytes().all(|b| b.is_ascii_lowercase()) { 168 | get_browser_alias(name) 169 | } else { 170 | get_browser_alias_lowercase(name) 171 | }; 172 | 173 | if mobile_to_desktop { 174 | get_browser_stat_mobile_to_desktop(normalized_name.as_ref()) 175 | } else { 176 | caniuse_browsers().get(&normalized_name).map(|stat| (stat.name.as_ref(), stat)) 177 | } 178 | } 179 | 180 | // Extract mobile-to-desktop logic - preserves original semantics 181 | fn get_browser_stat_mobile_to_desktop(name: &str) -> Option<(&'static str, &'static BrowserStat)> { 182 | // Reproduce original logic: first check if we have a desktop mapping 183 | match name { 184 | // Browsers that have desktop equivalents 185 | "and_chr" => caniuse_browsers().get(&Cow::Borrowed("chrome")).map(|stat| ("and_chr", stat)), 186 | "android" => Some(("android", android_to_desktop())), // Special case for android 187 | "and_ff" => caniuse_browsers().get(&Cow::Borrowed("firefox")).map(|stat| ("and_ff", stat)), 188 | "ie_mob" => caniuse_browsers().get(&Cow::Borrowed("ie")).map(|stat| ("ie_mob", stat)), 189 | // All other browsers (including op_mob) return their own data 190 | _ => caniuse_browsers().get(name).map(|stat| (stat.name.as_ref(), stat)), 191 | } 192 | } 193 | 194 | // Cold path for case conversion - only called when input contains uppercase 195 | #[cold] 196 | fn get_browser_alias_lowercase(name: &str) -> Cow<'static, str> { 197 | // Convert to lowercase and apply aliases 198 | let lowercase = name.to_ascii_lowercase(); 199 | match lowercase.as_str() { 200 | "fx" | "ff" => Cow::Borrowed("firefox"), 201 | "ios" => Cow::Borrowed("ios_saf"), 202 | "explorer" => Cow::Borrowed("ie"), 203 | "blackberry" => Cow::Borrowed("bb"), 204 | "explorermobile" => Cow::Borrowed("ie_mob"), 205 | "operamini" => Cow::Borrowed("op_mini"), 206 | "operamobile" => Cow::Borrowed("op_mob"), 207 | "chromeandroid" => Cow::Borrowed("and_chr"), 208 | "firefoxandroid" => Cow::Borrowed("and_ff"), 209 | "ucandroid" => Cow::Borrowed("and_uc"), 210 | "qqandroid" => Cow::Borrowed("and_qq"), 211 | // For browsers that don't have aliases, return the lowercase version if it exists 212 | _ => { 213 | if caniuse_browsers().contains_key(&Cow::Owned(lowercase.clone())) { 214 | Cow::Owned(lowercase) 215 | } else { 216 | // Fallback to original name as owned 217 | Cow::Owned(name.to_string()) 218 | } 219 | } 220 | } 221 | } 222 | 223 | fn get_browser_alias(name: &str) -> Cow<'static, str> { 224 | match name { 225 | "fx" | "ff" => Cow::Borrowed("firefox"), 226 | "ios" => Cow::Borrowed("ios_saf"), 227 | "explorer" => Cow::Borrowed("ie"), 228 | "blackberry" => Cow::Borrowed("bb"), 229 | "explorermobile" => Cow::Borrowed("ie_mob"), 230 | "operamini" => Cow::Borrowed("op_mini"), 231 | "operamobile" => Cow::Borrowed("op_mob"), 232 | "chromeandroid" => Cow::Borrowed("and_chr"), 233 | "firefoxandroid" => Cow::Borrowed("and_ff"), 234 | "ucandroid" => Cow::Borrowed("and_uc"), 235 | "qqandroid" => Cow::Borrowed("and_qq"), 236 | _ => Cow::Owned(name.to_string()), 237 | } 238 | } 239 | 240 | pub fn to_desktop_name(name: &str) -> Option<&'static str> { 241 | match name { 242 | "and_chr" | "android" => Some("chrome"), 243 | "and_ff" => Some("firefox"), 244 | "ie_mob" => Some("ie"), 245 | _ => None, 246 | } 247 | } 248 | 249 | pub fn normalize_version<'a>(stat: &'static BrowserStat, version: &'a str) -> Option> { 250 | if stat.version_list.iter().any(|v| v.version() == version) { 251 | Some(Cow::Borrowed(version)) 252 | } else if let Some(version) = 253 | browser_version_aliases().get(&stat.name).and_then(|aliases| aliases.get(version)) 254 | { 255 | Some(Cow::Borrowed(version)) 256 | } else if stat.version_list.len() == 1 { 257 | stat.version_list.first().map(|s| Cow::Owned(s.version().to_string())) 258 | } else { 259 | None 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/generated/electron_to_chromium.rs: -------------------------------------------------------------------------------- 1 | use crate::data::electron::ElectronVersion; 2 | pub static ELECTRON_VERSIONS: &[(ElectronVersion, &str)] = &[ 3 | (ElectronVersion::new(0u16, 20u16), "39"), 4 | (ElectronVersion::new(0u16, 21u16), "41"), 5 | (ElectronVersion::new(0u16, 22u16), "41"), 6 | (ElectronVersion::new(0u16, 23u16), "41"), 7 | (ElectronVersion::new(0u16, 24u16), "41"), 8 | (ElectronVersion::new(0u16, 25u16), "42"), 9 | (ElectronVersion::new(0u16, 26u16), "42"), 10 | (ElectronVersion::new(0u16, 27u16), "43"), 11 | (ElectronVersion::new(0u16, 28u16), "43"), 12 | (ElectronVersion::new(0u16, 29u16), "43"), 13 | (ElectronVersion::new(0u16, 30u16), "44"), 14 | (ElectronVersion::new(0u16, 31u16), "45"), 15 | (ElectronVersion::new(0u16, 32u16), "45"), 16 | (ElectronVersion::new(0u16, 33u16), "45"), 17 | (ElectronVersion::new(0u16, 34u16), "45"), 18 | (ElectronVersion::new(0u16, 35u16), "45"), 19 | (ElectronVersion::new(0u16, 36u16), "47"), 20 | (ElectronVersion::new(0u16, 37u16), "49"), 21 | (ElectronVersion::new(1u16, 0u16), "49"), 22 | (ElectronVersion::new(1u16, 1u16), "50"), 23 | (ElectronVersion::new(1u16, 2u16), "51"), 24 | (ElectronVersion::new(1u16, 3u16), "52"), 25 | (ElectronVersion::new(1u16, 4u16), "53"), 26 | (ElectronVersion::new(1u16, 5u16), "54"), 27 | (ElectronVersion::new(1u16, 6u16), "56"), 28 | (ElectronVersion::new(1u16, 7u16), "58"), 29 | (ElectronVersion::new(1u16, 8u16), "59"), 30 | (ElectronVersion::new(2u16, 0u16), "61"), 31 | (ElectronVersion::new(2u16, 1u16), "61"), 32 | (ElectronVersion::new(3u16, 0u16), "66"), 33 | (ElectronVersion::new(3u16, 1u16), "66"), 34 | (ElectronVersion::new(4u16, 0u16), "69"), 35 | (ElectronVersion::new(4u16, 1u16), "69"), 36 | (ElectronVersion::new(4u16, 2u16), "69"), 37 | (ElectronVersion::new(5u16, 0u16), "73"), 38 | (ElectronVersion::new(6u16, 0u16), "76"), 39 | (ElectronVersion::new(6u16, 1u16), "76"), 40 | (ElectronVersion::new(7u16, 0u16), "78"), 41 | (ElectronVersion::new(7u16, 1u16), "78"), 42 | (ElectronVersion::new(7u16, 2u16), "78"), 43 | (ElectronVersion::new(7u16, 3u16), "78"), 44 | (ElectronVersion::new(8u16, 0u16), "80"), 45 | (ElectronVersion::new(8u16, 1u16), "80"), 46 | (ElectronVersion::new(8u16, 2u16), "80"), 47 | (ElectronVersion::new(8u16, 3u16), "80"), 48 | (ElectronVersion::new(8u16, 4u16), "80"), 49 | (ElectronVersion::new(8u16, 5u16), "80"), 50 | (ElectronVersion::new(9u16, 0u16), "83"), 51 | (ElectronVersion::new(9u16, 1u16), "83"), 52 | (ElectronVersion::new(9u16, 2u16), "83"), 53 | (ElectronVersion::new(9u16, 3u16), "83"), 54 | (ElectronVersion::new(9u16, 4u16), "83"), 55 | (ElectronVersion::new(10u16, 0u16), "85"), 56 | (ElectronVersion::new(10u16, 1u16), "85"), 57 | (ElectronVersion::new(10u16, 2u16), "85"), 58 | (ElectronVersion::new(10u16, 3u16), "85"), 59 | (ElectronVersion::new(10u16, 4u16), "85"), 60 | (ElectronVersion::new(11u16, 0u16), "87"), 61 | (ElectronVersion::new(11u16, 1u16), "87"), 62 | (ElectronVersion::new(11u16, 2u16), "87"), 63 | (ElectronVersion::new(11u16, 3u16), "87"), 64 | (ElectronVersion::new(11u16, 4u16), "87"), 65 | (ElectronVersion::new(11u16, 5u16), "87"), 66 | (ElectronVersion::new(12u16, 0u16), "89"), 67 | (ElectronVersion::new(12u16, 1u16), "89"), 68 | (ElectronVersion::new(12u16, 2u16), "89"), 69 | (ElectronVersion::new(13u16, 0u16), "91"), 70 | (ElectronVersion::new(13u16, 1u16), "91"), 71 | (ElectronVersion::new(13u16, 2u16), "91"), 72 | (ElectronVersion::new(13u16, 3u16), "91"), 73 | (ElectronVersion::new(13u16, 4u16), "91"), 74 | (ElectronVersion::new(13u16, 5u16), "91"), 75 | (ElectronVersion::new(13u16, 6u16), "91"), 76 | (ElectronVersion::new(14u16, 0u16), "93"), 77 | (ElectronVersion::new(14u16, 1u16), "93"), 78 | (ElectronVersion::new(14u16, 2u16), "93"), 79 | (ElectronVersion::new(15u16, 0u16), "94"), 80 | (ElectronVersion::new(15u16, 1u16), "94"), 81 | (ElectronVersion::new(15u16, 2u16), "94"), 82 | (ElectronVersion::new(15u16, 3u16), "94"), 83 | (ElectronVersion::new(15u16, 4u16), "94"), 84 | (ElectronVersion::new(15u16, 5u16), "94"), 85 | (ElectronVersion::new(16u16, 0u16), "96"), 86 | (ElectronVersion::new(16u16, 1u16), "96"), 87 | (ElectronVersion::new(16u16, 2u16), "96"), 88 | (ElectronVersion::new(17u16, 0u16), "98"), 89 | (ElectronVersion::new(17u16, 1u16), "98"), 90 | (ElectronVersion::new(17u16, 2u16), "98"), 91 | (ElectronVersion::new(17u16, 3u16), "98"), 92 | (ElectronVersion::new(17u16, 4u16), "98"), 93 | (ElectronVersion::new(18u16, 0u16), "100"), 94 | (ElectronVersion::new(18u16, 1u16), "100"), 95 | (ElectronVersion::new(18u16, 2u16), "100"), 96 | (ElectronVersion::new(18u16, 3u16), "100"), 97 | (ElectronVersion::new(19u16, 0u16), "102"), 98 | (ElectronVersion::new(19u16, 1u16), "102"), 99 | (ElectronVersion::new(20u16, 0u16), "104"), 100 | (ElectronVersion::new(20u16, 1u16), "104"), 101 | (ElectronVersion::new(20u16, 2u16), "104"), 102 | (ElectronVersion::new(20u16, 3u16), "104"), 103 | (ElectronVersion::new(21u16, 0u16), "106"), 104 | (ElectronVersion::new(21u16, 1u16), "106"), 105 | (ElectronVersion::new(21u16, 2u16), "106"), 106 | (ElectronVersion::new(21u16, 3u16), "106"), 107 | (ElectronVersion::new(21u16, 4u16), "106"), 108 | (ElectronVersion::new(22u16, 0u16), "108"), 109 | (ElectronVersion::new(22u16, 1u16), "108"), 110 | (ElectronVersion::new(22u16, 2u16), "108"), 111 | (ElectronVersion::new(22u16, 3u16), "108"), 112 | (ElectronVersion::new(23u16, 0u16), "110"), 113 | (ElectronVersion::new(23u16, 1u16), "110"), 114 | (ElectronVersion::new(23u16, 2u16), "110"), 115 | (ElectronVersion::new(23u16, 3u16), "110"), 116 | (ElectronVersion::new(24u16, 0u16), "112"), 117 | (ElectronVersion::new(24u16, 1u16), "112"), 118 | (ElectronVersion::new(24u16, 2u16), "112"), 119 | (ElectronVersion::new(24u16, 3u16), "112"), 120 | (ElectronVersion::new(24u16, 4u16), "112"), 121 | (ElectronVersion::new(24u16, 5u16), "112"), 122 | (ElectronVersion::new(24u16, 6u16), "112"), 123 | (ElectronVersion::new(24u16, 7u16), "112"), 124 | (ElectronVersion::new(24u16, 8u16), "112"), 125 | (ElectronVersion::new(25u16, 0u16), "114"), 126 | (ElectronVersion::new(25u16, 1u16), "114"), 127 | (ElectronVersion::new(25u16, 2u16), "114"), 128 | (ElectronVersion::new(25u16, 3u16), "114"), 129 | (ElectronVersion::new(25u16, 4u16), "114"), 130 | (ElectronVersion::new(25u16, 5u16), "114"), 131 | (ElectronVersion::new(25u16, 6u16), "114"), 132 | (ElectronVersion::new(25u16, 7u16), "114"), 133 | (ElectronVersion::new(25u16, 8u16), "114"), 134 | (ElectronVersion::new(25u16, 9u16), "114"), 135 | (ElectronVersion::new(26u16, 0u16), "116"), 136 | (ElectronVersion::new(26u16, 1u16), "116"), 137 | (ElectronVersion::new(26u16, 2u16), "116"), 138 | (ElectronVersion::new(26u16, 3u16), "116"), 139 | (ElectronVersion::new(26u16, 4u16), "116"), 140 | (ElectronVersion::new(26u16, 5u16), "116"), 141 | (ElectronVersion::new(26u16, 6u16), "116"), 142 | (ElectronVersion::new(27u16, 0u16), "118"), 143 | (ElectronVersion::new(27u16, 1u16), "118"), 144 | (ElectronVersion::new(27u16, 2u16), "118"), 145 | (ElectronVersion::new(27u16, 3u16), "118"), 146 | (ElectronVersion::new(28u16, 0u16), "120"), 147 | (ElectronVersion::new(28u16, 1u16), "120"), 148 | (ElectronVersion::new(28u16, 2u16), "120"), 149 | (ElectronVersion::new(28u16, 3u16), "120"), 150 | (ElectronVersion::new(29u16, 0u16), "122"), 151 | (ElectronVersion::new(29u16, 1u16), "122"), 152 | (ElectronVersion::new(29u16, 2u16), "122"), 153 | (ElectronVersion::new(29u16, 3u16), "122"), 154 | (ElectronVersion::new(29u16, 4u16), "122"), 155 | (ElectronVersion::new(30u16, 0u16), "124"), 156 | (ElectronVersion::new(30u16, 1u16), "124"), 157 | (ElectronVersion::new(30u16, 2u16), "124"), 158 | (ElectronVersion::new(30u16, 3u16), "124"), 159 | (ElectronVersion::new(30u16, 4u16), "124"), 160 | (ElectronVersion::new(30u16, 5u16), "124"), 161 | (ElectronVersion::new(31u16, 0u16), "126"), 162 | (ElectronVersion::new(31u16, 1u16), "126"), 163 | (ElectronVersion::new(31u16, 2u16), "126"), 164 | (ElectronVersion::new(31u16, 3u16), "126"), 165 | (ElectronVersion::new(31u16, 4u16), "126"), 166 | (ElectronVersion::new(31u16, 5u16), "126"), 167 | (ElectronVersion::new(31u16, 6u16), "126"), 168 | (ElectronVersion::new(31u16, 7u16), "126"), 169 | (ElectronVersion::new(32u16, 0u16), "128"), 170 | (ElectronVersion::new(32u16, 1u16), "128"), 171 | (ElectronVersion::new(32u16, 2u16), "128"), 172 | (ElectronVersion::new(32u16, 3u16), "128"), 173 | (ElectronVersion::new(33u16, 0u16), "130"), 174 | (ElectronVersion::new(33u16, 1u16), "130"), 175 | (ElectronVersion::new(33u16, 2u16), "130"), 176 | (ElectronVersion::new(33u16, 3u16), "130"), 177 | (ElectronVersion::new(33u16, 4u16), "130"), 178 | (ElectronVersion::new(34u16, 0u16), "132"), 179 | (ElectronVersion::new(34u16, 1u16), "132"), 180 | (ElectronVersion::new(34u16, 2u16), "132"), 181 | (ElectronVersion::new(34u16, 3u16), "132"), 182 | (ElectronVersion::new(34u16, 4u16), "132"), 183 | (ElectronVersion::new(34u16, 5u16), "132"), 184 | (ElectronVersion::new(35u16, 0u16), "134"), 185 | (ElectronVersion::new(35u16, 1u16), "134"), 186 | (ElectronVersion::new(35u16, 2u16), "134"), 187 | (ElectronVersion::new(35u16, 3u16), "134"), 188 | (ElectronVersion::new(35u16, 4u16), "134"), 189 | (ElectronVersion::new(35u16, 5u16), "134"), 190 | (ElectronVersion::new(35u16, 6u16), "134"), 191 | (ElectronVersion::new(35u16, 7u16), "134"), 192 | (ElectronVersion::new(36u16, 0u16), "136"), 193 | (ElectronVersion::new(36u16, 1u16), "136"), 194 | (ElectronVersion::new(36u16, 2u16), "136"), 195 | (ElectronVersion::new(36u16, 3u16), "136"), 196 | (ElectronVersion::new(36u16, 4u16), "136"), 197 | (ElectronVersion::new(36u16, 5u16), "136"), 198 | (ElectronVersion::new(36u16, 6u16), "136"), 199 | (ElectronVersion::new(36u16, 7u16), "136"), 200 | (ElectronVersion::new(36u16, 8u16), "136"), 201 | (ElectronVersion::new(36u16, 9u16), "136"), 202 | (ElectronVersion::new(37u16, 0u16), "138"), 203 | (ElectronVersion::new(37u16, 1u16), "138"), 204 | (ElectronVersion::new(37u16, 2u16), "138"), 205 | (ElectronVersion::new(37u16, 3u16), "138"), 206 | (ElectronVersion::new(37u16, 4u16), "138"), 207 | (ElectronVersion::new(37u16, 5u16), "138"), 208 | (ElectronVersion::new(37u16, 6u16), "138"), 209 | (ElectronVersion::new(37u16, 7u16), "138"), 210 | (ElectronVersion::new(37u16, 8u16), "138"), 211 | (ElectronVersion::new(37u16, 9u16), "138"), 212 | (ElectronVersion::new(37u16, 10u16), "138"), 213 | (ElectronVersion::new(38u16, 0u16), "140"), 214 | (ElectronVersion::new(38u16, 1u16), "140"), 215 | (ElectronVersion::new(38u16, 2u16), "140"), 216 | (ElectronVersion::new(38u16, 3u16), "140"), 217 | (ElectronVersion::new(38u16, 4u16), "140"), 218 | (ElectronVersion::new(38u16, 5u16), "140"), 219 | (ElectronVersion::new(38u16, 6u16), "140"), 220 | (ElectronVersion::new(38u16, 7u16), "140"), 221 | (ElectronVersion::new(39u16, 0u16), "142"), 222 | (ElectronVersion::new(39u16, 1u16), "142"), 223 | (ElectronVersion::new(39u16, 2u16), "142"), 224 | (ElectronVersion::new(40u16, 0u16), "144"), 225 | ]; 226 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use nom::{ 2 | IResult, Parser, 3 | branch::alt, 4 | bytes::complete::{tag, tag_no_case, take_while_m_n, take_while1}, 5 | character::complete::{anychar, char, i32, one_of, space0, space1, u16, u32}, 6 | combinator::{all_consuming, consumed, map, opt, recognize, value, verify}, 7 | multi::{many_till, many0}, 8 | number::complete::{double, float}, 9 | sequence::{delimited, pair, preceded, separated_pair, terminated}, 10 | }; 11 | 12 | type PResult<'a, Output> = IResult<&'a str, Output>; 13 | 14 | #[derive(Debug, Clone)] 15 | pub enum QueryAtom<'a> { 16 | Last { count: u16, major: bool, name: Option<&'a str> }, 17 | Unreleased(Option<&'a str>), 18 | Years(f64), 19 | Since { year: i32, month: u32, day: u32 }, 20 | Percentage { comparator: Comparator, popularity: f32, stats: Stats<'a> }, 21 | Cover { coverage: f32, stats: Stats<'a> }, 22 | Supports(&'a str, Option), 23 | Electron(VersionRange<'a>), 24 | Node(VersionRange<'a>), 25 | Browser(&'a str, VersionRange<'a>), 26 | FirefoxESR, 27 | OperaMini, 28 | CurrentNode, 29 | MaintainedNode, 30 | Phantom(bool), 31 | BrowserslistConfig, 32 | Defaults, 33 | Dead, 34 | Extends(&'a str), 35 | Unknown(&'a str), // unnecessary, but for better error report 36 | } 37 | 38 | #[derive(Debug, Clone)] 39 | pub enum Stats<'a> { 40 | Global, 41 | Region(&'a str), 42 | } 43 | 44 | #[derive(Debug, Clone)] 45 | pub enum SupportKind { 46 | Fully, 47 | Partially, 48 | } 49 | 50 | fn parse_version_keyword(input: &str) -> PResult<'_, &str> { 51 | terminated(tag_no_case("version"), opt(char('s'))).parse(input) 52 | } 53 | 54 | fn parse_last(input: &str) -> PResult<'_, QueryAtom<'_>> { 55 | map( 56 | ( 57 | terminated(tag_no_case("last"), space1), 58 | terminated(u16, space1), 59 | opt(terminated( 60 | verify(take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), |s: &str| { 61 | !s.eq_ignore_ascii_case("version") 62 | && !s.eq_ignore_ascii_case("versions") 63 | && !s.eq_ignore_ascii_case("major") 64 | }), 65 | space1, 66 | )), 67 | opt(terminated(tag_no_case("major"), space1)), 68 | parse_version_keyword, 69 | ), 70 | |(_, count, name, major, _)| { 71 | if matches!(name, Some(name) if name.eq_ignore_ascii_case("major")) && major.is_none() { 72 | QueryAtom::Last { count, major: true, name: None } 73 | } else { 74 | QueryAtom::Last { count, major: major.is_some(), name } 75 | } 76 | }, 77 | ) 78 | .parse(input) 79 | } 80 | 81 | fn parse_unreleased(input: &str) -> PResult<'_, QueryAtom<'_>> { 82 | map( 83 | delimited( 84 | terminated(tag_no_case("unreleased"), space1), 85 | opt(terminated(take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), space1)), 86 | parse_version_keyword, 87 | ), 88 | QueryAtom::Unreleased, 89 | ) 90 | .parse(input) 91 | } 92 | 93 | fn parse_years(input: &str) -> PResult<'_, QueryAtom<'_>> { 94 | map( 95 | delimited( 96 | terminated(tag_no_case("last"), space1), 97 | terminated(double, space1), 98 | terminated(tag_no_case("year"), opt(char('s'))), 99 | ), 100 | QueryAtom::Years, 101 | ) 102 | .parse(input) 103 | } 104 | 105 | fn parse_since(input: &str) -> PResult<'_, QueryAtom<'_>> { 106 | map( 107 | ( 108 | terminated(tag_no_case("since"), one_of(" \t")), 109 | i32, 110 | opt(preceded(char('-'), u32)), 111 | opt(preceded(char('-'), u32)), 112 | ), 113 | |(_, year, month, day)| QueryAtom::Since { 114 | year, 115 | month: month.unwrap_or(1), 116 | day: day.unwrap_or(1), 117 | }, 118 | ) 119 | .parse(input) 120 | } 121 | 122 | #[derive(Debug, Clone)] 123 | pub enum Comparator { 124 | Less, 125 | LessOrEqual, 126 | Greater, 127 | GreaterOrEqual, 128 | } 129 | 130 | fn parse_compare_operator(input: &str) -> PResult<'_, Comparator> { 131 | map((alt((char('<'), char('>'))), opt(char('='))), |(relation, equals)| match relation { 132 | '<' if equals.is_some() => Comparator::LessOrEqual, 133 | '<' => Comparator::Less, 134 | '>' if equals.is_some() => Comparator::GreaterOrEqual, 135 | _ => Comparator::Greater, 136 | }) 137 | .parse(input) 138 | } 139 | 140 | fn parse_region(input: &str) -> PResult<'_, Stats<'_>> { 141 | map( 142 | recognize(preceded(opt(tag_no_case("alt-")), take_while_m_n(2, 2, char::is_alphabetic))), 143 | Stats::Region, 144 | ) 145 | .parse(input) 146 | } 147 | 148 | fn parse_percentage(input: &str) -> PResult<'_, QueryAtom<'_>> { 149 | map( 150 | ( 151 | terminated(parse_compare_operator, space0), 152 | terminated(float, char('%')), 153 | opt(preceded((space1, tag_no_case("in"), space1), parse_region)), 154 | ), 155 | |(comparator, value, stats)| QueryAtom::Percentage { 156 | comparator, 157 | popularity: value, 158 | stats: stats.unwrap_or(Stats::Global), 159 | }, 160 | ) 161 | .parse(input) 162 | } 163 | 164 | fn parse_cover(input: &str) -> PResult<'_, QueryAtom<'_>> { 165 | map( 166 | ( 167 | preceded(terminated(tag_no_case("cover"), space1), terminated(float, char('%'))), 168 | opt(preceded((space1, tag_no_case("in"), space1), parse_region)), 169 | ), 170 | |(value, stats)| QueryAtom::Cover { 171 | coverage: value, 172 | stats: stats.unwrap_or(Stats::Global), 173 | }, 174 | ) 175 | .parse(input) 176 | } 177 | 178 | fn parse_supports(input: &str) -> PResult<'_, QueryAtom<'_>> { 179 | map( 180 | separated_pair( 181 | opt(terminated( 182 | alt(( 183 | value(SupportKind::Fully, tag_no_case("fully")), 184 | value(SupportKind::Partially, tag_no_case("partially")), 185 | )), 186 | space1, 187 | )), 188 | terminated(tag_no_case("supports"), space1), 189 | take_while1(|c: char| c.is_alphanumeric() || c == '-'), 190 | ), 191 | |(kind, name)| QueryAtom::Supports(name, kind), 192 | ) 193 | .parse(input) 194 | } 195 | 196 | #[derive(Debug, Clone)] 197 | pub enum VersionRange<'a> { 198 | Bounded(&'a str, &'a str), 199 | Unbounded(Comparator, &'a str), 200 | Accurate(&'a str), 201 | } 202 | 203 | fn parse_version(input: &str) -> PResult<'_, &str> { 204 | take_while1(|c: char| c.is_ascii_digit() || c == '.')(input) 205 | } 206 | 207 | fn parse_version_range(input: &str) -> PResult<'_, VersionRange<'_>> { 208 | alt(( 209 | map( 210 | preceded( 211 | space1, 212 | separated_pair(parse_version, delimited(space0, char('-'), space0), parse_version), 213 | ), 214 | |(from, to)| VersionRange::Bounded(from, to), 215 | ), 216 | map( 217 | preceded(space0, separated_pair(parse_compare_operator, space0, parse_version)), 218 | |(comparator, version)| VersionRange::Unbounded(comparator, version), 219 | ), 220 | map(preceded(space1, parse_version), VersionRange::Accurate), 221 | )) 222 | .parse(input) 223 | } 224 | 225 | fn parse_electron(input: &str) -> PResult<'_, QueryAtom<'_>> { 226 | map(preceded(tag_no_case("electron"), parse_version_range), QueryAtom::Electron).parse(input) 227 | } 228 | 229 | fn parse_node(input: &str) -> PResult<'_, QueryAtom<'_>> { 230 | map(preceded(tag_no_case("node"), parse_version_range), QueryAtom::Node).parse(input) 231 | } 232 | 233 | fn parse_browser(input: &str) -> PResult<'_, QueryAtom<'_>> { 234 | map( 235 | pair( 236 | take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'), 237 | alt(( 238 | parse_version_range, 239 | map(preceded(space1, tag_no_case("tp")), VersionRange::Accurate), 240 | )), 241 | ), 242 | |(name, version)| QueryAtom::Browser(name, version), 243 | ) 244 | .parse(input) 245 | } 246 | 247 | fn parse_firefox_esr(input: &str) -> PResult<'_, QueryAtom<'_>> { 248 | value( 249 | QueryAtom::FirefoxESR, 250 | ( 251 | alt((tag_no_case("firefox"), tag_no_case("fx"), tag_no_case("ff"))), 252 | space1, 253 | tag_no_case("esr"), 254 | ), 255 | ) 256 | .parse(input) 257 | } 258 | 259 | fn parse_opera_mini(input: &str) -> PResult<'_, QueryAtom<'_>> { 260 | value( 261 | QueryAtom::OperaMini, 262 | (alt((tag_no_case("operamini"), tag_no_case("op_mini"))), space1, tag_no_case("all")), 263 | ) 264 | .parse(input) 265 | } 266 | 267 | fn parse_current_node(input: &str) -> PResult<'_, QueryAtom<'_>> { 268 | value(QueryAtom::CurrentNode, (tag_no_case("current"), space1, tag_no_case("node"))) 269 | .parse(input) 270 | } 271 | 272 | fn parse_maintained_node(input: &str) -> PResult<'_, QueryAtom<'_>> { 273 | value( 274 | QueryAtom::MaintainedNode, 275 | (tag_no_case("maintained"), space1, tag_no_case("node"), space1, tag_no_case("versions")), 276 | ) 277 | .parse(input) 278 | } 279 | 280 | fn parse_phantom(input: &str) -> PResult<'_, QueryAtom<'_>> { 281 | map( 282 | preceded(terminated(tag_no_case("phantomjs"), space1), alt((tag("1.9"), tag("2.1")))), 283 | |version| QueryAtom::Phantom(version == "2.1"), 284 | ) 285 | .parse(input) 286 | } 287 | 288 | fn parse_browserslist_config(input: &str) -> PResult<'_, QueryAtom<'_>> { 289 | value(QueryAtom::BrowserslistConfig, tag_no_case("browserslist config")).parse(input) 290 | } 291 | 292 | fn parse_defaults(input: &str) -> PResult<'_, QueryAtom<'_>> { 293 | value(QueryAtom::Defaults, tag_no_case("defaults")).parse(input) 294 | } 295 | 296 | fn parse_dead(input: &str) -> PResult<'_, QueryAtom<'_>> { 297 | value(QueryAtom::Dead, tag_no_case("dead")).parse(input) 298 | } 299 | 300 | fn parse_extends(input: &str) -> PResult<'_, QueryAtom<'_>> { 301 | map( 302 | preceded( 303 | terminated(tag_no_case("extends"), space1), 304 | take_while1(|c: char| { 305 | c.is_alphanumeric() || c == '-' || c == '_' || c == '@' || c == '/' || c == '.' 306 | }), 307 | ), 308 | QueryAtom::Extends, 309 | ) 310 | .parse(input) 311 | } 312 | 313 | fn parse_unknown(input: &str) -> PResult<'_, QueryAtom<'_>> { 314 | map(recognize(many_till(anychar, parse_composition_operator)), QueryAtom::Unknown).parse(input) 315 | } 316 | 317 | fn parse_query_atom(input: &str) -> PResult<'_, QueryAtom<'_>> { 318 | alt(( 319 | parse_last, 320 | parse_unreleased, 321 | parse_years, 322 | parse_since, 323 | parse_percentage, 324 | parse_cover, 325 | parse_supports, 326 | parse_electron, 327 | parse_node, 328 | parse_firefox_esr, 329 | parse_opera_mini, 330 | parse_current_node, 331 | parse_maintained_node, 332 | parse_phantom, 333 | parse_browser, 334 | parse_browserslist_config, 335 | parse_defaults, 336 | parse_dead, 337 | parse_extends, 338 | parse_unknown, 339 | )) 340 | .parse(input) 341 | } 342 | 343 | #[derive(Debug)] 344 | pub struct SingleQuery<'a> { 345 | pub(crate) raw: &'a str, 346 | pub(crate) atom: QueryAtom<'a>, 347 | pub(crate) negated: bool, 348 | pub(crate) is_and: bool, 349 | } 350 | 351 | fn parse_and(input: &str) -> PResult<'_, bool> { 352 | value(true, delimited(space1, tag_no_case("and"), space1)).parse(input) 353 | } 354 | 355 | fn parse_or(input: &str) -> PResult<'_, bool> { 356 | alt(( 357 | value(false, delimited(space0, char(','), space0)), 358 | value(false, delimited(space1, tag_no_case("or"), space1)), 359 | )) 360 | .parse(input) 361 | } 362 | 363 | fn parse_composition_operator(input: &str) -> PResult<'_, bool> { 364 | alt((parse_and, parse_or)).parse(input) 365 | } 366 | 367 | fn parse_single_query(input: &str) -> PResult<'_, SingleQuery<'_>> { 368 | map( 369 | ( 370 | parse_composition_operator, 371 | consumed(pair(opt(terminated(tag_no_case("not"), space1)), parse_query_atom)), 372 | ), 373 | |(is_and, (raw, (negated, atom)))| SingleQuery { 374 | raw, 375 | atom, 376 | negated: negated.is_some(), 377 | is_and, 378 | }, 379 | ) 380 | .parse(input) 381 | } 382 | 383 | pub fn parse_browserslist_query(input: &str) -> PResult<'_, Vec>> { 384 | let input = input.trim(); 385 | // `many0` doesn't allow empty input, so we detect it here 386 | if input.is_empty() { 387 | return Ok(("", vec![])); 388 | } 389 | 390 | map( 391 | all_consuming(( 392 | consumed(pair( 393 | // this isn't allowed, but for better error report 394 | opt(terminated(tag_no_case("not"), space1)), 395 | parse_query_atom, 396 | )), 397 | many0(parse_single_query), 398 | )), 399 | |((first_raw, (negated, first)), mut queries)| { 400 | queries.insert( 401 | 0, 402 | SingleQuery { 403 | raw: first_raw, 404 | atom: first, 405 | negated: negated.is_some(), 406 | is_and: false, 407 | }, 408 | ); 409 | queries 410 | }, 411 | ) 412 | .parse(input) 413 | } 414 | 415 | #[cfg(all(test, not(miri)))] 416 | mod tests { 417 | use test_case::test_case; 418 | 419 | use crate::{opts::Opts, test::run_compare}; 420 | 421 | #[test_case(""; "empty")] 422 | #[test_case("ie >= 6, ie <= 7"; "comma")] 423 | #[test_case("ie >= 6 and ie <= 7"; "and")] 424 | #[test_case("ie < 11 and not ie 7"; "and with not")] 425 | #[test_case("last 1 Baidu version and not <2%"; "with not and one-version browsers as and query")] 426 | #[test_case("ie >= 6 or ie <= 7"; "or")] 427 | #[test_case("ie < 11 or not ie 7"; "or with not")] 428 | #[test_case("last 2 versions and > 1%"; "swc issue 4871")] 429 | fn valid(query: &str) { 430 | run_compare(query, &Opts::default(), None); 431 | } 432 | } 433 | --------------------------------------------------------------------------------