Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,25 @@ LightningCSS.minify(".a { color: #ff0000 }")

#### Targeting browsers

Give the browsers you support, each as its major version. Anything they cannot read is lowered, with the original kept after it so that a browser which understands it still wins.
Give the browsers you support, each with the oldest version you support. Anything they cannot read is lowered, with the original kept after it so that a browser which understands it still wins.

```ruby
LightningCSS.transform(".a { color: lab(50% 40 59) }", targets: { chrome: 80 }, minify: true).code
#=> ".a{color:#bf5702;color:lab(50% 40 59)}"
```

A version is an `Integer` for a major version, or a `Float` or `String` for a minor or patch release. Several features first shipped in a minor Safari release, so the minor version can change the output.

```ruby
LightningCSS.transform(".a { color: oklch(60% .2 30) }", targets: { safari: 15 }, minify: true).code
#=> ".a{color:lab(51.6908% 61.9679 48.0969)}"

LightningCSS.transform(".a { color: oklch(60% .2 30) }", targets: { safari: "15.4" }, minify: true).code
#=> ".a{color:oklch(60% .2 30)}"
```

Prefer a `String` for a minor version above 9, since `16.10` as a `Float` is `16.1`.

#### Bundling

`bundle` resolves the `@import` statements a stylesheet was written with by reading the files they name, so it takes a path on disk to the entry stylesheet.
Expand Down
6 changes: 3 additions & 3 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ fn transform_source(code: &str, options: &TransformOptions) -> Result<TransformR
.map_err(|error| Failure::internal(format!("Failed to scope stylesheet: {error:?}")))?;
}

let targets = options.to_targets();
let targets = options.to_targets().map_err(Failure::option)?;

stylesheet
.minify(MinifyOptions {
Expand Down Expand Up @@ -244,7 +244,7 @@ fn bundle_source(path: &str, options: &TransformOptions) -> Result<TransformResu
.map_err(|error| Failure::internal(format!("Failed to scope stylesheet: {error:?}")))?;
}

let targets = options.to_targets();
let targets = options.to_targets().map_err(Failure::option)?;

stylesheet
.minify(MinifyOptions {
Expand Down Expand Up @@ -289,7 +289,7 @@ fn transform_attribute(code: &str, options: &TransformOptions) -> Result<Transfo

let mut attribute = StyleAttribute::parse(code, parser_options).map_err(|error| Failure::parse(error.to_string()))?;

let targets = options.to_targets();
let targets = options.to_targets().map_err(Failure::option)?;

attribute.minify(MinifyOptions {
targets,
Expand Down
55 changes: 50 additions & 5 deletions rust/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct TransformOptions {
pub filename: Option<String>,
pub minify: bool,
pub error_recovery: bool,
pub targets: Option<HashMap<String, u32>>,
pub targets: Option<HashMap<String, Version>>,
pub css_modules: Option<CssModulesOptions>,
pub scope: Option<String>,
}
Expand Down Expand Up @@ -60,16 +60,61 @@ impl CssModulesOptions {
}
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Version {
Major(u32),
Float(f64),
String(String),
}

impl Version {
/// Encodes a version the way Lightning CSS reads it, as `major << 16 | minor << 8 | patch`.
fn encode(&self) -> Option<u32> {
match self {
Version::Major(major) => (*major <= 0xffff).then(|| major << 16),
Version::Float(float) => Self::parse(&float.to_string()),
Version::String(string) => Self::parse(string),
}
}

fn parse(version: &str) -> Option<u32> {
let mut parts = version.trim().split('.');

let major: u16 = parts.next()?.parse().ok()?;
let minor: u8 = parts.next().map_or(Some(0), |part| part.parse().ok())?;
let patch: u8 = parts.next().map_or(Some(0), |part| part.parse().ok())?;

if parts.next().is_some() {
return None;
}

Some(u32::from(major) << 16 | u32::from(minor) << 8 | u32::from(patch))
}

fn describe(&self) -> String {
match self {
Version::Major(major) => major.to_string(),
Version::Float(float) => float.to_string(),
Version::String(string) => format!("{string:?}"),
}
}
}

impl TransformOptions {
pub fn to_targets(&self) -> Targets {
pub fn to_targets(&self) -> Result<Targets, String> {
let Some(versions) = &self.targets else {
return Targets::default();
return Ok(Targets::default());
};

let mut browsers = Browsers::default();

for (name, version) in versions {
let encoded = Some(version << 16);
let encoded = Some(
version
.encode()
.ok_or_else(|| format!("Invalid version {} for {name} in targets", version.describe()))?,
);

match name.as_str() {
"android" => browsers.android = encoded,
Expand All @@ -85,7 +130,7 @@ impl TransformOptions {
}
}

Targets::from(browsers)
Ok(Targets::from(browsers))
}
}

Expand Down
22 changes: 12 additions & 10 deletions sig/lightningcss/types.rbs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# Note: This file is hand written, `rbs-inline` does not generate this file.

module LightningCSS
type version = Integer | Float | String

type browsers = {
?android: Integer,
?chrome: Integer,
?edge: Integer,
?firefox: Integer,
?ie: Integer,
?ios: Integer,
?ios_saf: Integer,
?opera: Integer,
?safari: Integer,
?samsung: Integer
?android: version,
?chrome: version,
?edge: version,
?firefox: version,
?ie: version,
?ios: version,
?ios_saf: version,
?opera: version,
?safari: version,
?samsung: version
}

type css_modules = bool | {
Expand Down
62 changes: 62 additions & 0 deletions test/lightningcss/targets_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,68 @@ class TargetsTest < Minitest::Spec
assert_equal ".a {\n -webkit-user-select: none;\n user-select: none;\n}\n", code
end

OKLCH = ".a { color: oklch(60% .2 30) }"

test "reads an integer as a major version" do
code = LightningCSS.transform(OKLCH, targets: { safari: 15 }, minify: true).code

assert_equal ".a{color:lab(51.6908% 61.9679 48.0969)}", code
end

test "reads a float as a major and minor version" do
code = LightningCSS.transform(OKLCH, targets: { safari: 15.4 }, minify: true).code

assert_equal ".a{color:oklch(60% .2 30)}", code
end

test "reads a string as a major and minor version" do
code = LightningCSS.transform(OKLCH, targets: { safari: "15.4" }, minify: true).code

assert_equal ".a{color:oklch(60% .2 30)}", code
end

test "reads a string with a patch version" do
code = LightningCSS.transform(OKLCH, targets: { ios_saf: "15.4.1" }, minify: true).code

assert_equal ".a{color:oklch(60% .2 30)}", code
end

test "reads a minor version for a property it would otherwise prefix" do
assert_equal ".a{-webkit-appearance:none;appearance:none}", LightningCSS.transform(".a { appearance: none }", targets: { safari: 15 }, minify: true).code
assert_equal ".a{appearance:none}", LightningCSS.transform(".a { appearance: none }", targets: { safari: "15.4" }, minify: true).code
end

test "reads a minor version for a shorthand it would otherwise expand" do
assert_equal ".a{top:0;bottom:0;left:0;right:0}", LightningCSS.transform(".a { inset: 0 }", targets: { safari: 14 }, minify: true).code
assert_equal ".a{inset:0}", LightningCSS.transform(".a { inset: 0 }", targets: { safari: 14.1 }, minify: true).code
end

test "applies a minor version to a style attribute" do
code = LightningCSS.transform_style_attribute("appearance: none", targets: { safari: "15.4" }, minify: true).code

assert_equal "appearance:none", code
end

["15.a", "15.4.1.2", "", "-1", "15.256", "65536"].each do |version|
test "raises an option error for the version #{version.inspect}" do
error = assert_raises(OptionError) { LightningCSS.transform(".a{}", targets: { safari: version }) }

assert_equal "Invalid version #{version.inspect} for safari in targets", error.message
end
end

test "raises an option error for a negative version" do
error = assert_raises(OptionError) { LightningCSS.transform(".a{}", targets: { safari: -1 }) }

assert_equal "Invalid version -1 for safari in targets", error.message
end

test "raises an option error for a major version too large to encode" do
error = assert_raises(OptionError) { LightningCSS.transform(".a{}", targets: { safari: 65_536 }) }

assert_equal "Invalid version 65536 for safari in targets", error.message
end

test "applies its targets to a bundle it was not asked to minify" do
Dir.mktmpdir do |dir|
path = File.join(dir, "a.css")
Expand Down
Loading