1use std::{collections::BTreeSet, sync::OnceLock};
10
11use browserslist::{Distrib, Opts, resolve as resolve_browserslist};
12use omena_transform_cst::TransformPassKind;
13use omena_transform_passes::{TransformPassPlanV0, plan_transform_passes};
14use serde::{Deserialize, Serialize};
15
16const BROWSER_THRESHOLDS_SOURCE: &str = include_str!("../data/browser-thresholds.toml");
17const PASS_FEATURE_BINDINGS_SOURCE: &str = include_str!("../data/pass-feature-bindings.toml");
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
20struct BrowserThresholdDataV0 {
21 schema_version: String,
22 product: String,
23 refreshed_at: String,
24 quorum_min_sources: usize,
25 thresholds: Vec<BrowserFeatureThresholdV0>,
26 parse_error_count: usize,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
30struct BrowserFeatureThresholdV0 {
31 table: String,
32 browser: String,
33 min_major: u16,
34 min_minor: u16,
35 caniuse_key: String,
36 source_quorum: Vec<String>,
37 last_verified: String,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Default)]
41struct PassFeatureBindingDataV0 {
42 schema_version: String,
43 product: String,
44 refreshed_at: String,
45 bindings: Vec<PassFeatureBindingV0>,
46 parse_error_count: usize,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Default)]
50struct PassFeatureBindingV0 {
51 pass_id: String,
52 caniuse_keys: Vec<String>,
53 support_table: String,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
57#[serde(rename_all = "camelCase")]
58pub struct TargetFeatureSupportV0 {
59 pub vendor_prefix_required: bool,
60 pub supports_light_dark: bool,
61 pub supports_color_mix: bool,
62 pub supports_oklch_oklab: bool,
63 pub supports_color_function: bool,
64 pub supports_logical_properties: bool,
65 pub supports_css_nesting: bool,
66 pub supports_css_scope: bool,
67 pub supports_cascade_layers: bool,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
71#[serde(rename_all = "camelCase")]
72#[serde(default)]
73pub struct TargetTransformOptionsV0 {
74 pub allow_logical_to_physical: bool,
75 pub allow_scope_flatten: bool,
76 pub allow_layer_flatten: bool,
77 pub enable_supports_static_eval: bool,
78 pub enable_media_static_eval: bool,
79 pub drop_dark_mode_media_queries: bool,
80}
81
82impl Default for TargetTransformOptionsV0 {
83 fn default() -> Self {
84 conservative_target_options()
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
89#[serde(rename_all = "camelCase")]
90pub struct TransformTargetBoundarySummaryV0 {
91 pub schema_version: &'static str,
92 pub product: &'static str,
93 pub managed_pass_ids: Vec<&'static str>,
94 pub opt_in_pass_ids: Vec<&'static str>,
95 pub target_data_source: &'static str,
96 pub planner_surface: &'static str,
97 pub browser_threshold_table_count: usize,
98 pub browser_threshold_entry_count: usize,
99 pub pass_feature_binding_count: usize,
100 pub browser_data_source_files: Vec<&'static str>,
101 pub browser_data_parse_error_count: usize,
102 pub browser_data_quorum_valid: bool,
103 pub browser_data_bindings_valid: bool,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
107#[serde(rename_all = "camelCase")]
108pub struct TransformTargetQueryPlanV0 {
109 pub schema_version: &'static str,
110 pub product: &'static str,
111 pub query: String,
112 pub normalized_query: String,
113 pub profile_id: &'static str,
114 pub recognized_profile: bool,
115 pub target_data_source: &'static str,
116 pub resolved_targets: Vec<String>,
117 pub resolution_error: Option<String>,
118 pub support: TargetFeatureSupportV0,
119 pub transform_plan: TransformTargetPlanV0,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
123#[serde(rename_all = "camelCase")]
124pub struct TransformTargetPlanV0 {
125 pub schema_version: &'static str,
126 pub product: &'static str,
127 pub target_label: String,
128 pub required_pass_ids: Vec<&'static str>,
129 pub blocked_pass_ids: Vec<&'static str>,
130 pub planned_pass_ids: Vec<&'static str>,
131 pub pass_plan: TransformPassPlanV0,
132}
133
134pub fn summarize_omena_transform_target_boundary() -> TransformTargetBoundarySummaryV0 {
135 let browser_data = browser_threshold_data();
136 let bindings = pass_feature_binding_data();
137
138 TransformTargetBoundarySummaryV0 {
139 schema_version: "0",
140 product: "omena-transform-target.boundary",
141 managed_pass_ids: target_managed_passes()
142 .iter()
143 .map(|pass| pass.id())
144 .collect(),
145 opt_in_pass_ids: vec![
146 TransformPassKind::LogicalToPhysical.id(),
147 TransformPassKind::ScopeFlatten.id(),
148 TransformPassKind::LayerFlatten.id(),
149 ],
150 target_data_source: "oxcBrowserslistV3+browserThresholdsTomlV0+staticTargetProfileV0+explicitFeatureMatrixV0",
151 planner_surface: "omena-transform-passes.plan",
152 browser_threshold_table_count: browser_threshold_table_count(browser_data),
153 browser_threshold_entry_count: browser_data.thresholds.len(),
154 pass_feature_binding_count: bindings.bindings.len(),
155 browser_data_source_files: vec![
156 "data/browser-thresholds.toml",
157 "data/pass-feature-bindings.toml",
158 ],
159 browser_data_parse_error_count: browser_data.parse_error_count + bindings.parse_error_count,
160 browser_data_quorum_valid: browser_threshold_data_is_valid(browser_data),
161 browser_data_bindings_valid: pass_feature_binding_data_is_valid(browser_data, bindings),
162 }
163}
164
165pub fn plan_target_transforms_from_query(
166 query: impl Into<String>,
167 options: TargetTransformOptionsV0,
168) -> TransformTargetQueryPlanV0 {
169 let query = query.into();
170 let normalized_query = normalize_target_query(&query);
171 let target_resolution = target_feature_support_for_query(&normalized_query);
172 let transform_plan = plan_target_transforms(
173 target_resolution.profile_id,
174 target_resolution.support,
175 options,
176 );
177
178 TransformTargetQueryPlanV0 {
179 schema_version: "0",
180 product: "omena-transform-target.query-plan",
181 query,
182 normalized_query,
183 profile_id: target_resolution.profile_id,
184 recognized_profile: target_resolution.recognized_profile,
185 target_data_source: target_resolution.target_data_source,
186 resolved_targets: target_resolution.resolved_targets,
187 resolution_error: target_resolution.resolution_error,
188 support: target_resolution.support,
189 transform_plan,
190 }
191}
192
193pub fn plan_target_transforms(
194 target_label: impl Into<String>,
195 support: TargetFeatureSupportV0,
196 options: TargetTransformOptionsV0,
197) -> TransformTargetPlanV0 {
198 let target_label = target_label.into();
199 let mut required_passes = Vec::new();
200 let mut blocked_passes = Vec::new();
201
202 if support.vendor_prefix_required {
203 required_passes.push(TransformPassKind::VendorPrefixing);
204 }
205 if !support.supports_light_dark {
206 required_passes.push(TransformPassKind::LightDarkLowering);
207 }
208 if !support.supports_color_mix {
209 required_passes.push(TransformPassKind::ColorMixLowering);
210 }
211 if !support.supports_oklch_oklab {
212 required_passes.push(TransformPassKind::OklchOklabLowering);
213 }
214 if !support.supports_color_function {
215 required_passes.push(TransformPassKind::ColorFunctionLowering);
216 }
217 if !support.supports_logical_properties {
218 push_required_or_blocked(
219 TransformPassKind::LogicalToPhysical,
220 options.allow_logical_to_physical,
221 &mut required_passes,
222 &mut blocked_passes,
223 );
224 }
225 if !support.supports_css_nesting {
226 required_passes.push(TransformPassKind::NestingUnwrap);
227 }
228 if !support.supports_css_scope {
229 push_required_or_blocked(
230 TransformPassKind::ScopeFlatten,
231 options.allow_scope_flatten,
232 &mut required_passes,
233 &mut blocked_passes,
234 );
235 }
236 if !support.supports_cascade_layers {
237 push_required_or_blocked(
238 TransformPassKind::LayerFlatten,
239 options.allow_layer_flatten,
240 &mut required_passes,
241 &mut blocked_passes,
242 );
243 }
244 if options.enable_supports_static_eval {
245 required_passes.push(TransformPassKind::SupportsStaticEval);
246 }
247 if options.enable_media_static_eval {
248 required_passes.push(TransformPassKind::MediaStaticEval);
249 }
250 if options.drop_dark_mode_media_queries {
251 required_passes.push(TransformPassKind::DeadMediaBranchRemoval);
252 }
253
254 required_passes.sort_by_key(|pass| pass.ordinal());
255 required_passes.dedup();
256 blocked_passes.sort_by_key(|pass| pass.ordinal());
257 blocked_passes.dedup();
258
259 let pass_plan = plan_transform_passes(&required_passes);
260 let planned_pass_ids = pass_plan.ordered_pass_ids.clone();
261
262 TransformTargetPlanV0 {
263 schema_version: "0",
264 product: "omena-transform-target.plan",
265 target_label,
266 required_pass_ids: required_passes.iter().map(|pass| pass.id()).collect(),
267 blocked_pass_ids: blocked_passes.iter().map(|pass| pass.id()).collect(),
268 planned_pass_ids,
269 pass_plan,
270 }
271}
272
273pub const fn modern_feature_support() -> TargetFeatureSupportV0 {
274 TargetFeatureSupportV0 {
275 vendor_prefix_required: false,
276 supports_light_dark: true,
277 supports_color_mix: true,
278 supports_oklch_oklab: true,
279 supports_color_function: true,
280 supports_logical_properties: true,
281 supports_css_nesting: true,
282 supports_css_scope: true,
283 supports_cascade_layers: true,
284 }
285}
286
287pub const fn legacy_webview_feature_support() -> TargetFeatureSupportV0 {
288 TargetFeatureSupportV0 {
289 vendor_prefix_required: true,
290 supports_light_dark: false,
291 supports_color_mix: false,
292 supports_oklch_oklab: false,
293 supports_color_function: false,
294 supports_logical_properties: false,
295 supports_css_nesting: false,
296 supports_css_scope: false,
297 supports_cascade_layers: false,
298 }
299}
300
301pub const fn conservative_target_options() -> TargetTransformOptionsV0 {
302 TargetTransformOptionsV0 {
303 allow_logical_to_physical: false,
304 allow_scope_flatten: false,
305 allow_layer_flatten: false,
306 enable_supports_static_eval: false,
307 enable_media_static_eval: false,
308 drop_dark_mode_media_queries: false,
309 }
310}
311
312struct TargetQueryResolutionV0 {
313 profile_id: &'static str,
314 recognized_profile: bool,
315 target_data_source: &'static str,
316 resolved_targets: Vec<String>,
317 resolution_error: Option<String>,
318 support: TargetFeatureSupportV0,
319}
320
321fn normalize_target_query(query: &str) -> String {
322 query
323 .split_whitespace()
324 .collect::<Vec<_>>()
325 .join(" ")
326 .trim()
327 .to_ascii_lowercase()
328}
329
330fn target_feature_support_for_query(normalized_query: &str) -> TargetQueryResolutionV0 {
331 if matches!(
332 normalized_query,
333 "" | "modern" | "baseline 2024" | "baseline-2024"
334 ) {
335 return TargetQueryResolutionV0 {
336 profile_id: "modern-evergreen",
337 recognized_profile: true,
338 target_data_source: "staticTargetProfileV0",
339 resolved_targets: Vec::new(),
340 resolution_error: None,
341 support: modern_feature_support(),
342 };
343 }
344
345 if normalized_query == "legacy" || normalized_query == "legacy-webview" {
346 return TargetQueryResolutionV0 {
347 profile_id: "legacy-webview",
348 recognized_profile: true,
349 target_data_source: "staticTargetProfileV0",
350 resolved_targets: Vec::new(),
351 resolution_error: None,
352 support: legacy_webview_feature_support(),
353 };
354 }
355
356 match resolve_browserslist(&[normalized_query], &Opts::default()) {
357 Ok(distribs) if !distribs.is_empty() => {
358 let resolved_targets = distribs.iter().map(distrib_key).collect::<Vec<_>>();
359 TargetQueryResolutionV0 {
360 profile_id: "browserslist-resolved",
361 recognized_profile: true,
362 target_data_source: "oxcBrowserslistV3+browserThresholdsTomlV0+featureSubsetV0",
363 support: feature_support_for_resolved_targets(&distribs),
364 resolved_targets,
365 resolution_error: None,
366 }
367 }
368 Ok(_) => unknown_conservative_resolution(None),
369 Err(error) => unknown_conservative_resolution(Some(error.to_string())),
370 }
371}
372
373fn unknown_conservative_resolution(resolution_error: Option<String>) -> TargetQueryResolutionV0 {
374 TargetQueryResolutionV0 {
375 profile_id: "unknown-conservative",
376 recognized_profile: false,
377 target_data_source: "staticTargetProfileV0",
378 resolved_targets: Vec::new(),
379 resolution_error,
380 support: legacy_webview_feature_support(),
381 }
382}
383
384fn feature_support_for_resolved_targets(distribs: &[Distrib]) -> TargetFeatureSupportV0 {
385 let flexbox_fully_supported =
386 target_set_is_subset_of_fully_supported_feature(distribs, "flexbox");
387 let sticky_fully_supported =
388 target_set_is_subset_of_fully_supported_feature(distribs, "css-sticky");
389
390 TargetFeatureSupportV0 {
391 vendor_prefix_required: !(flexbox_fully_supported && sticky_fully_supported),
392 supports_light_dark: target_set_is_subset_of_browser_threshold_table(
393 distribs,
394 "light_dark",
395 ),
396 supports_color_mix: target_set_is_subset_of_browser_threshold_table(distribs, "color_mix"),
397 supports_oklch_oklab: target_set_is_subset_of_fully_supported_feature(
398 distribs,
399 "css-lch-lab",
400 ),
401 supports_color_function: target_set_is_subset_of_fully_supported_feature(
402 distribs,
403 "css-color-function",
404 ),
405 supports_logical_properties: target_set_is_subset_of_fully_supported_feature(
406 distribs,
407 "css-logical-props",
408 ),
409 supports_css_nesting: target_set_is_subset_of_fully_supported_feature(
410 distribs,
411 "css-nesting",
412 ),
413 supports_css_scope: target_set_is_subset_of_fully_supported_feature(
414 distribs,
415 "css-cascade-scope",
416 ),
417 supports_cascade_layers: target_set_is_subset_of_fully_supported_feature(
418 distribs,
419 "css-cascade-layers",
420 ),
421 }
422}
423
424fn target_set_is_subset_of_browser_threshold_table(distribs: &[Distrib], table: &str) -> bool {
425 !distribs.is_empty()
426 && distribs
427 .iter()
428 .all(|distrib| distrib_matches_browser_threshold_table(distrib, table))
429}
430
431fn distrib_matches_browser_threshold_table(distrib: &Distrib, table: &str) -> bool {
432 browser_threshold_data()
433 .thresholds
434 .iter()
435 .filter(|threshold| threshold.table == table)
436 .find(|threshold| threshold.browser == distrib.name())
437 .is_some_and(|threshold| {
438 browser_version_at_least(distrib.version(), threshold.min_major, threshold.min_minor)
439 })
440}
441
442fn browser_threshold_data() -> &'static BrowserThresholdDataV0 {
443 static DATA: OnceLock<BrowserThresholdDataV0> = OnceLock::new();
444 DATA.get_or_init(|| {
445 parse_browser_threshold_data(BROWSER_THRESHOLDS_SOURCE).unwrap_or_else(|_| {
446 BrowserThresholdDataV0 {
447 parse_error_count: 1,
448 ..BrowserThresholdDataV0::default()
449 }
450 })
451 })
452}
453
454fn pass_feature_binding_data() -> &'static PassFeatureBindingDataV0 {
455 static DATA: OnceLock<PassFeatureBindingDataV0> = OnceLock::new();
456 DATA.get_or_init(|| {
457 parse_pass_feature_binding_data(PASS_FEATURE_BINDINGS_SOURCE).unwrap_or_else(|_| {
458 PassFeatureBindingDataV0 {
459 parse_error_count: 1,
460 ..PassFeatureBindingDataV0::default()
461 }
462 })
463 })
464}
465
466fn parse_browser_threshold_data(source: &str) -> Result<BrowserThresholdDataV0, String> {
467 let mut data = BrowserThresholdDataV0::default();
468 let mut current_threshold: Option<BrowserFeatureThresholdV0> = None;
469
470 for line in significant_toml_lines(source) {
471 if line == "[[threshold]]" {
472 if let Some(threshold) = current_threshold.take() {
473 data.thresholds.push(threshold);
474 }
475 current_threshold = Some(BrowserFeatureThresholdV0::default());
476 continue;
477 }
478
479 let (key, value) = parse_toml_assignment(&line)?;
480 if let Some(threshold) = current_threshold.as_mut() {
481 match key {
482 "table" => threshold.table = parse_toml_string(value)?,
483 "browser" => threshold.browser = parse_toml_string(value)?,
484 "min_major" => threshold.min_major = parse_toml_u16(value)?,
485 "min_minor" => threshold.min_minor = parse_toml_u16(value)?,
486 "caniuse_key" => threshold.caniuse_key = parse_toml_string(value)?,
487 "source_quorum" => threshold.source_quorum = parse_toml_string_array(value)?,
488 "last_verified" => threshold.last_verified = parse_toml_string(value)?,
489 _ => return Err(format!("unknown browser threshold key `{key}`")),
490 }
491 } else {
492 match key {
493 "schema_version" => data.schema_version = parse_toml_string(value)?,
494 "product" => data.product = parse_toml_string(value)?,
495 "refreshed_at" => data.refreshed_at = parse_toml_string(value)?,
496 "quorum_min_sources" => data.quorum_min_sources = parse_toml_usize(value)?,
497 _ => return Err(format!("unknown browser threshold root key `{key}`")),
498 }
499 }
500 }
501
502 if let Some(threshold) = current_threshold.take() {
503 data.thresholds.push(threshold);
504 }
505
506 Ok(data)
507}
508
509fn parse_pass_feature_binding_data(source: &str) -> Result<PassFeatureBindingDataV0, String> {
510 let mut data = PassFeatureBindingDataV0::default();
511 let mut current_binding: Option<PassFeatureBindingV0> = None;
512
513 for line in significant_toml_lines(source) {
514 if line == "[[binding]]" {
515 if let Some(binding) = current_binding.take() {
516 data.bindings.push(binding);
517 }
518 current_binding = Some(PassFeatureBindingV0::default());
519 continue;
520 }
521
522 let (key, value) = parse_toml_assignment(&line)?;
523 if let Some(binding) = current_binding.as_mut() {
524 match key {
525 "pass_id" => binding.pass_id = parse_toml_string(value)?,
526 "caniuse_keys" => binding.caniuse_keys = parse_toml_string_array(value)?,
527 "support_table" => binding.support_table = parse_toml_string(value)?,
528 _ => return Err(format!("unknown pass feature binding key `{key}`")),
529 }
530 } else {
531 match key {
532 "schema_version" => data.schema_version = parse_toml_string(value)?,
533 "product" => data.product = parse_toml_string(value)?,
534 "refreshed_at" => data.refreshed_at = parse_toml_string(value)?,
535 _ => return Err(format!("unknown pass feature binding root key `{key}`")),
536 }
537 }
538 }
539
540 if let Some(binding) = current_binding.take() {
541 data.bindings.push(binding);
542 }
543
544 Ok(data)
545}
546
547fn significant_toml_lines(source: &str) -> impl Iterator<Item = String> + '_ {
548 source
549 .lines()
550 .map(|line| line.split('#').next().unwrap_or("").trim())
551 .filter(|line| !line.is_empty())
552 .map(ToOwned::to_owned)
553}
554
555fn parse_toml_assignment(line: &str) -> Result<(&str, &str), String> {
556 let Some((key, value)) = line.split_once('=') else {
557 return Err(format!("invalid assignment `{line}`"));
558 };
559 Ok((key.trim(), value.trim()))
560}
561
562fn parse_toml_string(value: &str) -> Result<String, String> {
563 let value = value.trim();
564 if value.len() < 2 || !value.starts_with('"') || !value.ends_with('"') {
565 return Err(format!("expected quoted string, got `{value}`"));
566 }
567 Ok(value[1..value.len() - 1].to_string())
568}
569
570fn parse_toml_u16(value: &str) -> Result<u16, String> {
571 value
572 .trim()
573 .parse::<u16>()
574 .map_err(|error| format!("invalid u16 `{value}`: {error}"))
575}
576
577fn parse_toml_usize(value: &str) -> Result<usize, String> {
578 value
579 .trim()
580 .parse::<usize>()
581 .map_err(|error| format!("invalid usize `{value}`: {error}"))
582}
583
584fn parse_toml_string_array(value: &str) -> Result<Vec<String>, String> {
585 let value = value.trim();
586 if value.len() < 2 || !value.starts_with('[') || !value.ends_with(']') {
587 return Err(format!("expected string array, got `{value}`"));
588 }
589 let body = value[1..value.len() - 1].trim();
590 if body.is_empty() {
591 return Ok(Vec::new());
592 }
593
594 body.split(',')
595 .map(|item| parse_toml_string(item.trim()))
596 .collect()
597}
598
599fn browser_threshold_data_is_valid(data: &BrowserThresholdDataV0) -> bool {
600 data.parse_error_count == 0
601 && data.schema_version == "0"
602 && data.product == "omena-transform-target.browser-thresholds"
603 && is_iso_date(&data.refreshed_at)
604 && data.quorum_min_sources >= 2
605 && browser_threshold_table_count(data) >= 2
606 && data.thresholds.iter().all(|threshold| {
607 !threshold.table.is_empty()
608 && !threshold.browser.is_empty()
609 && !threshold.caniuse_key.is_empty()
610 && threshold.source_quorum.len() >= data.quorum_min_sources
611 && threshold
612 .source_quorum
613 .iter()
614 .all(|source| matches!(source.as_str(), "caniuse" | "web-features" | "mdn-bcd"))
615 && is_iso_date(&threshold.last_verified)
616 })
617}
618
619fn pass_feature_binding_data_is_valid(
620 browser_data: &BrowserThresholdDataV0,
621 binding_data: &PassFeatureBindingDataV0,
622) -> bool {
623 binding_data.parse_error_count == 0
624 && binding_data.schema_version == "0"
625 && binding_data.product == "omena-transform-target.pass-feature-bindings"
626 && is_iso_date(&binding_data.refreshed_at)
627 && !binding_data.bindings.is_empty()
628 && binding_data.bindings.iter().all(|binding| {
629 !binding.pass_id.is_empty()
630 && target_managed_passes()
631 .iter()
632 .any(|pass| pass.id() == binding.pass_id)
633 && !binding.caniuse_keys.is_empty()
634 && binding.caniuse_keys.iter().all(|key| {
635 browser_data.thresholds.iter().any(|threshold| {
636 threshold.table == binding.support_table && threshold.caniuse_key == *key
637 })
638 })
639 })
640}
641
642fn browser_threshold_table_count(data: &BrowserThresholdDataV0) -> usize {
643 data.thresholds
644 .iter()
645 .map(|threshold| threshold.table.as_str())
646 .collect::<BTreeSet<_>>()
647 .len()
648}
649
650fn is_iso_date(value: &str) -> bool {
651 let bytes = value.as_bytes();
652 bytes.len() == 10
653 && bytes[4] == b'-'
654 && bytes[7] == b'-'
655 && bytes
656 .iter()
657 .enumerate()
658 .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit())
659}
660
661fn browser_version_at_least(version: &str, min_major: u16, min_minor: u16) -> bool {
662 if version.eq_ignore_ascii_case("tp") {
663 return true;
664 }
665
666 let version = version.split('-').next().unwrap_or(version);
667 let mut parts = version.split('.');
668 let Some(major) = parts.next().and_then(|part| part.parse::<u16>().ok()) else {
669 return false;
670 };
671 let minor = parts
672 .next()
673 .and_then(|part| part.parse::<u16>().ok())
674 .unwrap_or(0);
675
676 major > min_major || (major == min_major && minor >= min_minor)
677}
678
679fn target_set_is_subset_of_fully_supported_feature(distribs: &[Distrib], feature: &str) -> bool {
680 let query = format!("fully supports {feature}");
681 let Ok(feature_distribs) = resolve_browserslist(&[query.as_str()], &Opts::default()) else {
682 return false;
683 };
684 if feature_distribs.is_empty() {
685 return false;
686 }
687
688 let feature_targets = feature_distribs
689 .iter()
690 .map(distrib_key)
691 .collect::<BTreeSet<_>>();
692 distribs
693 .iter()
694 .all(|distrib| feature_targets.contains(&distrib_key(distrib)))
695}
696
697fn distrib_key(distrib: &Distrib) -> String {
698 distrib.to_string()
699}
700
701fn push_required_or_blocked(
702 pass: TransformPassKind,
703 allowed: bool,
704 required_passes: &mut Vec<TransformPassKind>,
705 blocked_passes: &mut Vec<TransformPassKind>,
706) {
707 if allowed {
708 required_passes.push(pass);
709 } else {
710 blocked_passes.push(pass);
711 }
712}
713
714fn target_managed_passes() -> [TransformPassKind; 12] {
715 [
716 TransformPassKind::VendorPrefixing,
717 TransformPassKind::LightDarkLowering,
718 TransformPassKind::ColorMixLowering,
719 TransformPassKind::OklchOklabLowering,
720 TransformPassKind::ColorFunctionLowering,
721 TransformPassKind::LogicalToPhysical,
722 TransformPassKind::NestingUnwrap,
723 TransformPassKind::ScopeFlatten,
724 TransformPassKind::LayerFlatten,
725 TransformPassKind::SupportsStaticEval,
726 TransformPassKind::MediaStaticEval,
727 TransformPassKind::DeadMediaBranchRemoval,
728 ]
729}
730
731#[cfg(test)]
732mod tests {
733 use super::{
734 TargetFeatureSupportV0, TargetTransformOptionsV0, conservative_target_options,
735 plan_target_transforms, plan_target_transforms_from_query,
736 summarize_omena_transform_target_boundary,
737 };
738
739 #[test]
740 fn exposes_target_lowering_boundary() {
741 let boundary = summarize_omena_transform_target_boundary();
742
743 assert_eq!(boundary.product, "omena-transform-target.boundary");
744 assert_eq!(boundary.managed_pass_ids.len(), 12);
745 assert_eq!(
746 boundary.target_data_source,
747 "oxcBrowserslistV3+browserThresholdsTomlV0+staticTargetProfileV0+explicitFeatureMatrixV0"
748 );
749 assert_eq!(boundary.browser_threshold_table_count, 2);
750 assert_eq!(boundary.browser_threshold_entry_count, 22);
751 assert_eq!(boundary.pass_feature_binding_count, 2);
752 assert_eq!(boundary.browser_data_parse_error_count, 0);
753 assert!(boundary.browser_data_quorum_valid);
754 assert!(boundary.browser_data_bindings_valid);
755 assert!(boundary.managed_pass_ids.contains(&"vendor-prefixing"));
756 assert!(boundary.managed_pass_ids.contains(&"media-static-eval"));
757 assert!(boundary.opt_in_pass_ids.contains(&"scope-flatten"));
758 }
759
760 #[test]
761 fn browser_data_governance_externalizes_thresholds_and_bindings() {
762 let boundary = summarize_omena_transform_target_boundary();
763
764 assert!(
765 boundary
766 .browser_data_source_files
767 .contains(&"data/browser-thresholds.toml")
768 );
769 assert!(
770 boundary
771 .browser_data_source_files
772 .contains(&"data/pass-feature-bindings.toml")
773 );
774 assert!(boundary.browser_data_quorum_valid);
775 assert!(boundary.browser_data_bindings_valid);
776 }
777
778 #[test]
779 fn plans_target_lowering_with_vendor_prefix_after_lowering_edges() {
780 let support = TargetFeatureSupportV0 {
781 vendor_prefix_required: true,
782 supports_light_dark: false,
783 supports_color_mix: false,
784 supports_oklch_oklab: true,
785 supports_color_function: true,
786 supports_logical_properties: true,
787 supports_css_nesting: false,
788 supports_css_scope: false,
789 supports_cascade_layers: false,
790 };
791 let options = TargetTransformOptionsV0 {
792 allow_logical_to_physical: false,
793 allow_scope_flatten: true,
794 allow_layer_flatten: true,
795 enable_supports_static_eval: true,
796 enable_media_static_eval: true,
797 drop_dark_mode_media_queries: false,
798 };
799
800 let plan = plan_target_transforms("legacy-webview", support, options);
801
802 assert_eq!(plan.pass_plan.violated_dag_edge_count, 0);
803 assert!(plan.required_pass_ids.contains(&"light-dark-lowering"));
804 assert!(plan.required_pass_ids.contains(&"color-mix-lowering"));
805 assert!(plan.required_pass_ids.contains(&"vendor-prefixing"));
806 assert!(plan.required_pass_ids.contains(&"scope-flatten"));
807 assert!(plan.required_pass_ids.contains(&"layer-flatten"));
808 let vendor_index = plan
809 .planned_pass_ids
810 .iter()
811 .position(|id| *id == "vendor-prefixing");
812 let light_dark_index = plan
813 .planned_pass_ids
814 .iter()
815 .position(|id| *id == "light-dark-lowering");
816 assert!(light_dark_index < vendor_index);
817 }
818
819 #[test]
820 fn blocks_opt_in_flattening_when_not_explicitly_enabled() {
821 let support = TargetFeatureSupportV0 {
822 supports_css_scope: false,
823 supports_cascade_layers: false,
824 ..super::modern_feature_support()
825 };
826
827 let plan = plan_target_transforms(
828 "modern-without-scope",
829 support,
830 conservative_target_options(),
831 );
832
833 assert!(plan.blocked_pass_ids.contains(&"scope-flatten"));
834 assert!(plan.blocked_pass_ids.contains(&"layer-flatten"));
835 assert!(!plan.required_pass_ids.contains(&"scope-flatten"));
836 assert!(!plan.required_pass_ids.contains(&"layer-flatten"));
837 }
838
839 #[test]
840 fn plans_dark_mode_media_drop_as_dead_media_branch_pass() {
841 let mut options = conservative_target_options();
842 options.drop_dark_mode_media_queries = true;
843
844 let plan = plan_target_transforms("modern", super::modern_feature_support(), options);
845
846 assert!(
847 plan.required_pass_ids
848 .contains(&"dead-media-branch-removal")
849 );
850 assert!(plan.planned_pass_ids.contains(&"dead-media-branch-removal"));
851 }
852
853 #[test]
854 fn plans_target_lowering_from_static_target_query_profiles() {
855 let options = TargetTransformOptionsV0 {
856 allow_logical_to_physical: true,
857 allow_scope_flatten: true,
858 allow_layer_flatten: true,
859 enable_supports_static_eval: true,
860 enable_media_static_eval: true,
861 drop_dark_mode_media_queries: false,
862 };
863 let plan = plan_target_transforms_from_query("legacy-webview", options);
864
865 assert!(plan.recognized_profile);
866 assert_eq!(plan.normalized_query, "legacy-webview");
867 assert_eq!(plan.profile_id, "legacy-webview");
868 assert!(plan.support.vendor_prefix_required);
869 assert_eq!(plan.transform_plan.pass_plan.violated_dag_edge_count, 0);
870 assert!(
871 plan.transform_plan
872 .required_pass_ids
873 .contains(&"vendor-prefixing")
874 );
875 assert!(
876 plan.transform_plan
877 .required_pass_ids
878 .contains(&"nesting-unwrap")
879 );
880 assert!(
881 plan.transform_plan
882 .required_pass_ids
883 .contains(&"logical-to-physical")
884 );
885
886 let modern = plan_target_transforms_from_query("modern", conservative_target_options());
887 assert!(modern.recognized_profile);
888 assert_eq!(modern.profile_id, "modern-evergreen");
889 assert_eq!(modern.target_data_source, "staticTargetProfileV0");
890 assert!(modern.transform_plan.required_pass_ids.is_empty());
891 }
892
893 #[test]
894 fn plans_target_lowering_from_resolved_browserslist_query() {
895 let options = TargetTransformOptionsV0 {
896 allow_logical_to_physical: true,
897 allow_scope_flatten: true,
898 allow_layer_flatten: true,
899 enable_supports_static_eval: false,
900 enable_media_static_eval: false,
901 drop_dark_mode_media_queries: false,
902 };
903 let plan = plan_target_transforms_from_query("ie 11", options);
904
905 assert!(plan.recognized_profile);
906 assert_eq!(plan.profile_id, "browserslist-resolved");
907 assert_eq!(
908 plan.target_data_source,
909 "oxcBrowserslistV3+browserThresholdsTomlV0+featureSubsetV0"
910 );
911 assert_eq!(plan.resolved_targets, vec!["ie 11"]);
912 assert_eq!(plan.resolution_error, None);
913 assert!(plan.support.vendor_prefix_required);
914 assert!(
915 plan.transform_plan
916 .required_pass_ids
917 .contains(&"vendor-prefixing")
918 );
919 assert!(
920 plan.transform_plan
921 .required_pass_ids
922 .contains(&"nesting-unwrap")
923 );
924 assert!(
925 plan.transform_plan
926 .required_pass_ids
927 .contains(&"logical-to-physical")
928 );
929 }
930
931 #[test]
932 fn resolves_light_dark_and_color_mix_from_static_compatibility_matrix() {
933 let chrome_122 =
934 plan_target_transforms_from_query("chrome 122", conservative_target_options());
935 assert_eq!(chrome_122.profile_id, "browserslist-resolved");
936 assert!(!chrome_122.support.supports_light_dark);
937 assert!(chrome_122.support.supports_color_mix);
938 assert!(
939 chrome_122
940 .transform_plan
941 .required_pass_ids
942 .contains(&"light-dark-lowering")
943 );
944 assert!(
945 !chrome_122
946 .transform_plan
947 .required_pass_ids
948 .contains(&"color-mix-lowering")
949 );
950
951 let chrome_123 =
952 plan_target_transforms_from_query("chrome 123", conservative_target_options());
953 assert!(chrome_123.support.supports_light_dark);
954 assert!(chrome_123.support.supports_color_mix);
955 assert!(
956 !chrome_123
957 .transform_plan
958 .required_pass_ids
959 .contains(&"light-dark-lowering")
960 );
961 assert!(
962 !chrome_123
963 .transform_plan
964 .required_pass_ids
965 .contains(&"color-mix-lowering")
966 );
967
968 let safari_16_2 =
969 plan_target_transforms_from_query("safari 16.2", conservative_target_options());
970 assert!(!safari_16_2.support.supports_light_dark);
971 assert!(safari_16_2.support.supports_color_mix);
972
973 let safari_17_5 =
974 plan_target_transforms_from_query("safari 17.5", conservative_target_options());
975 assert!(safari_17_5.support.supports_light_dark);
976 assert!(safari_17_5.support.supports_color_mix);
977 }
978
979 #[test]
980 fn invalid_target_query_uses_conservative_profile_without_claiming_recognition() {
981 let plan = plan_target_transforms_from_query("yuru 1.0", conservative_target_options());
982
983 assert!(!plan.recognized_profile);
984 assert_eq!(plan.profile_id, "unknown-conservative");
985 assert!(plan.resolution_error.is_some());
986 assert!(plan.support.vendor_prefix_required);
987 assert!(
988 plan.transform_plan
989 .required_pass_ids
990 .contains(&"vendor-prefixing")
991 );
992 assert!(
993 plan.transform_plan
994 .blocked_pass_ids
995 .contains(&"scope-flatten")
996 );
997 }
998}