Skip to main content

lightningcss/
targets.rs

1//! Browser target options.
2
3#![allow(missing_docs)]
4
5use std::borrow::Borrow;
6
7use crate::vendor_prefix::VendorPrefix;
8use bitflags::bitflags;
9#[cfg(any(feature = "serde", feature = "nodejs"))]
10use serde::{Deserialize, Serialize};
11
12/// Browser versions to compile CSS for.
13///
14/// Versions are represented as a single 24-bit integer, with one byte
15/// per `major.minor.patch` component.
16///
17/// # Example
18///
19/// This example represents a target of Safari 13.2.0.
20///
21/// ```
22/// use lightningcss::targets::Browsers;
23///
24/// let targets = Browsers {
25///   safari: Some((13 << 16) | (2 << 8)),
26///   ..Browsers::default()
27/// };
28/// ```
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize, Deserialize))]
31#[allow(missing_docs)]
32pub struct Browsers {
33  pub android: Option<u32>,
34  pub chrome: Option<u32>,
35  pub edge: Option<u32>,
36  pub firefox: Option<u32>,
37  pub ie: Option<u32>,
38  pub ios_saf: Option<u32>,
39  pub opera: Option<u32>,
40  pub safari: Option<u32>,
41  pub samsung: Option<u32>,
42}
43
44#[cfg(feature = "browserslist")]
45pub use browserslist::Opts as BrowserslistConfig;
46
47#[cfg(feature = "browserslist")]
48#[cfg_attr(docsrs, doc(cfg(feature = "browserslist")))]
49impl Browsers {
50  /// Parses a list of browserslist queries into Lightning CSS targets.
51  pub fn from_browserslist<S: AsRef<str>, I: IntoIterator<Item = S>>(
52    query: I,
53  ) -> Result<Option<Browsers>, browserslist::Error> {
54    Self::from_browserslist_with_config(query, BrowserslistConfig::default())
55  }
56
57  /// Parses a list of browserslist queries into Lightning CSS targets.
58  pub fn from_browserslist_with_config<S: AsRef<str>, I: IntoIterator<Item = S>>(
59    query: I,
60    config: BrowserslistConfig,
61  ) -> Result<Option<Browsers>, browserslist::Error> {
62    use browserslist::resolve;
63
64    Self::from_distribs(resolve(query, &config)?)
65  }
66
67  #[cfg(not(target_arch = "wasm32"))]
68  /// Finds browserslist configuration, selects queries by environment and loads the resulting queries into LightningCSS targets.
69  ///
70  /// Configuration resolution is modeled after the original `browserslist` nodeJS package.
71  /// The configuration is resolved in the following order:
72  ///
73  /// - If a `BROWSERSLIST` environment variable is present, then load targets from its value. This is analog to the `--targets` CLI option.
74  ///   Example: `BROWSERSLIST="firefox ESR" lightningcss [OPTIONS] <INPUT_FILE>`
75  /// - If a `BROWSERSLIST_CONFIG` environment variable is present, then resolve the file at the provided path.
76  ///   Then parse and use targets from `package.json` or any browserslist configuration file pointed to by the environment variable.
77  ///   Example: `BROWSERSLIST_CONFIG="../config/browserslist" lightningcss [OPTIONS] <INPUT_FILE>`
78  /// - If none of the above apply, then find, parse and use targets from the first `browserslist`, `.browserslistrc`
79  ///   or `package.json` configuration file in any parent directory.
80  ///
81  /// When using parsed configuration from `browserslist`, `.browserslistrc` or `package.json` configuration files,
82  /// the environment determined by:
83  ///
84  /// - the `BROWSERSLIST_ENV` environment variable if present,
85  /// - otherwise the `NODE_ENV` environment varialbe if present,
86  /// - otherwise `production` is used.
87  ///
88  /// If no targets are found for the resulting environment, then the `defaults` configuration section is used.
89  pub fn load_browserslist() -> Result<Option<Browsers>, browserslist::Error> {
90    use browserslist::{execute, Opts};
91
92    Self::from_distribs(execute(&Opts::default())?)
93  }
94
95  fn from_distribs(distribs: Vec<browserslist::Distrib>) -> Result<Option<Browsers>, browserslist::Error> {
96    let mut browsers = Browsers::default();
97    let mut has_any = false;
98    for distrib in distribs {
99      macro_rules! browser {
100        ($browser: ident) => {{
101          if let Some(v) = parse_version(distrib.version()) {
102            if browsers.$browser.is_none() || v < browsers.$browser.unwrap() {
103              browsers.$browser = Some(v);
104              has_any = true;
105            }
106          }
107        }};
108      }
109
110      match distrib.name() {
111        "android" => browser!(android),
112        "chrome" | "and_chr" => browser!(chrome),
113        "edge" => browser!(edge),
114        "firefox" | "and_ff" => browser!(firefox),
115        "ie" => browser!(ie),
116        "ios_saf" => browser!(ios_saf),
117        "opera" | "op_mob" => browser!(opera),
118        "safari" => browser!(safari),
119        "samsung" => browser!(samsung),
120        _ => {}
121      }
122    }
123
124    if !has_any {
125      return Ok(None);
126    }
127
128    Ok(Some(browsers))
129  }
130}
131
132#[cfg(feature = "browserslist")]
133fn parse_version(version: &str) -> Option<u32> {
134  let version = version.split('-').next();
135  if version.is_none() {
136    return None;
137  }
138
139  let mut version = version.unwrap().split('.');
140  let major = version.next().and_then(|v| v.parse::<u32>().ok());
141  if let Some(major) = major {
142    let minor = version.next().and_then(|v| v.parse::<u32>().ok()).unwrap_or(0);
143    let patch = version.next().and_then(|v| v.parse::<u32>().ok()).unwrap_or(0);
144    let v: u32 = (major & 0xff) << 16 | (minor & 0xff) << 8 | (patch & 0xff);
145    return Some(v);
146  }
147
148  None
149}
150
151bitflags! {
152  /// Features to explicitly enable or disable.
153  #[derive(Debug, Default, Clone, Copy, Hash, Eq, PartialEq)]
154  #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
155  pub struct Features: u32 {
156    const Nesting = 1 << 0;
157    const NotSelectorList = 1 << 1;
158    const DirSelector = 1 << 2;
159    const LangSelectorList = 1 << 3;
160    const IsSelector = 1 << 4;
161    const TextDecorationThicknessPercent = 1 << 5;
162    const MediaIntervalSyntax = 1 << 6;
163    const MediaRangeSyntax = 1 << 7;
164    const CustomMediaQueries = 1 << 8;
165    const ClampFunction = 1 << 9;
166    const ColorFunction = 1 << 10;
167    const OklabColors = 1 << 11;
168    const LabColors = 1 << 12;
169    const P3Colors = 1 << 13;
170    const HexAlphaColors = 1 << 14;
171    const SpaceSeparatedColorNotation = 1 << 15;
172    const FontFamilySystemUi = 1 << 16;
173    const DoublePositionGradients = 1 << 17;
174    const VendorPrefixes = 1 << 18;
175    const LogicalProperties = 1 << 19;
176    const LightDark = 1 << 20;
177    const Selectors = Self::Nesting.bits() | Self::NotSelectorList.bits() | Self::DirSelector.bits() | Self::LangSelectorList.bits() | Self::IsSelector.bits();
178    const MediaQueries = Self::MediaIntervalSyntax.bits() | Self::MediaRangeSyntax.bits() | Self::CustomMediaQueries.bits();
179    const Colors = Self::ColorFunction.bits() | Self::OklabColors.bits() | Self::LabColors.bits() | Self::P3Colors.bits() | Self::HexAlphaColors.bits() | Self::SpaceSeparatedColorNotation.bits() | Self::LightDark.bits();
180  }
181}
182
183pub(crate) trait FeaturesIterator: Sized + Iterator {
184  fn try_union_all<T>(&mut self) -> Option<Features>
185  where
186    Self: Iterator<Item = Option<T>>,
187    T: Borrow<Features>,
188  {
189    self.try_fold(Features::empty(), |a, b| b.map(|b| a | *b.borrow()))
190  }
191}
192
193impl<I> FeaturesIterator for I where I: Iterator {}
194
195/// Target browsers and features to compile.
196#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
197#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
198pub struct Targets {
199  /// Browser targets to compile the CSS for.
200  pub browsers: Option<Browsers>,
201  /// Features that should always be compiled, even when supported by targets.
202  pub include: Features,
203  /// Features that should never be compiled, even when unsupported by targets.
204  pub exclude: Features,
205}
206
207impl From<Browsers> for Targets {
208  fn from(browsers: Browsers) -> Self {
209    Self {
210      browsers: Some(browsers),
211      ..Default::default()
212    }
213  }
214}
215
216impl From<Option<Browsers>> for Targets {
217  fn from(browsers: Option<Browsers>) -> Self {
218    Self {
219      browsers,
220      ..Default::default()
221    }
222  }
223}
224
225impl Targets {
226  pub(crate) fn is_compatible(&self, feature: crate::compat::Feature) -> bool {
227    self.browsers.map(|targets| feature.is_compatible(targets)).unwrap_or(true)
228  }
229
230  pub(crate) fn should_compile(&self, feature: crate::compat::Feature, flag: Features) -> bool {
231    self.include.contains(flag) || (!self.exclude.contains(flag) && !self.is_compatible(feature))
232  }
233
234  pub(crate) fn should_compile_logical(&self, feature: crate::compat::Feature) -> bool {
235    self.should_compile(feature, Features::LogicalProperties)
236  }
237
238  pub(crate) fn should_compile_selectors(&self) -> bool {
239    self.include.intersects(Features::Selectors)
240      || (!self.exclude.intersects(Features::Selectors) && self.browsers.is_some())
241  }
242
243  pub(crate) fn prefixes(&self, prefix: VendorPrefix, feature: crate::prefixes::Feature) -> VendorPrefix {
244    if prefix.contains(VendorPrefix::None) && !self.exclude.contains(Features::VendorPrefixes) {
245      if self.include.contains(Features::VendorPrefixes) {
246        VendorPrefix::all()
247      } else {
248        self.browsers.map(|browsers| feature.prefixes_for(browsers)).unwrap_or(prefix)
249      }
250    } else {
251      prefix
252    }
253  }
254}
255
256#[derive(Debug)]
257pub(crate) struct TargetsWithSupportsScope {
258  stack: Vec<Features>,
259  pub(crate) current: Targets,
260}
261
262impl TargetsWithSupportsScope {
263  pub fn new(targets: Targets) -> Self {
264    Self {
265      stack: Vec::new(),
266      current: targets,
267    }
268  }
269
270  /// Returns true if inserted
271  pub fn enter_supports(&mut self, features: Features) -> bool {
272    if features.is_empty() || self.current.exclude.contains(features) {
273      // Already excluding all features
274      return false;
275    }
276
277    let newly_excluded = features - self.current.exclude;
278    self.stack.push(newly_excluded);
279    self.current.exclude.insert(newly_excluded);
280    true
281  }
282
283  /// Should be only called if inserted
284  pub fn exit_supports(&mut self) {
285    if let Some(last) = self.stack.pop() {
286      self.current.exclude.remove(last);
287    }
288  }
289}
290
291#[test]
292fn supports_scope_correctly() {
293  let mut targets = TargetsWithSupportsScope::new(Targets::default());
294  assert!(!targets.current.exclude.contains(Features::OklabColors));
295  assert!(!targets.current.exclude.contains(Features::LabColors));
296  assert!(!targets.current.exclude.contains(Features::P3Colors));
297
298  targets.enter_supports(Features::OklabColors | Features::LabColors);
299  assert!(targets.current.exclude.contains(Features::OklabColors));
300  assert!(targets.current.exclude.contains(Features::LabColors));
301
302  targets.enter_supports(Features::P3Colors | Features::LabColors);
303  assert!(targets.current.exclude.contains(Features::OklabColors));
304  assert!(targets.current.exclude.contains(Features::LabColors));
305  assert!(targets.current.exclude.contains(Features::P3Colors));
306
307  targets.exit_supports();
308  assert!(targets.current.exclude.contains(Features::OklabColors));
309  assert!(targets.current.exclude.contains(Features::LabColors));
310  assert!(!targets.current.exclude.contains(Features::P3Colors));
311
312  targets.exit_supports();
313  assert!(!targets.current.exclude.contains(Features::OklabColors));
314  assert!(!targets.current.exclude.contains(Features::LabColors));
315}
316
317macro_rules! should_compile {
318  ($targets: expr, $feature: ident) => {
319    $targets.should_compile(
320      crate::compat::Feature::$feature,
321      crate::targets::Features::$feature,
322    )
323  };
324}
325
326pub(crate) use should_compile;