1#[cfg(test)]
10mod carrier_hygiene_assertions;
11mod emission_items;
12mod emission_order;
13
14pub use emission_items::{
15 EmissionItemFactCategoryV0, EmissionItemInputV0, EmissionItemKindV0, EmissionItemOrderKeyV0,
16 EmissionItemPlanV0, EmissionItemProjectionDisclosureV0, EmissionItemProjectionDispositionV0,
17 EmissionItemProjectionReasonV0, EmissionItemV0, LinkedEmissionItemMaterializationErrorV0,
18 LinkedEmissionItemOrderV0, LinkedEmissionItemV0, TransformBundleEmissionItemProjectionV0,
19};
20pub use emission_order::{
21 EmissionCycleClassV0, EmissionCycleDialectV0, EmissionCycleGroupV0, EmissionCyclePolicyV0,
22 EmissionDependencyFactV0, EmissionOrderKeyV0, EmissionOrderingPolicyV0, EmissionPlanV0,
23};
24
25use omena_cascade::{
26 CascadeKey, CascadeLevel, LayerOrdinal, ModuleRank, OpenWorldTieEvidence, Specificity,
27 normalized_layer_rank,
28};
29use omena_cross_file_summary::{EdgeOrderRelevanceV0, OmenaCrossFileSummaryRawEdgeKindV0};
30use omena_parser::{
31 ClosedWorldBundleBuildErrorV0, ClosedWorldBundleV0, ClosedWorldComposesEdgeV0,
32 ClosedWorldLinkedModuleV0, ClosedWorldModuleMetadataV0,
33 ClosedWorldModuleReachabilityEvidenceV0, ConfigurationHashV0, ModuleIdV0, ModuleInstanceKeyV0,
34 ParsedAnimationFactKind, ParsedCssModuleComposesEdgeKind, ParsedCssModuleValueFactKind,
35 ParsedEmissionSelectorFactsV0, ParsedSassModuleEdgeFactKind, ParsedSelectorFactKind,
36 ParsedStyleFacts, ParsedVariableFactKind, StyleDialect, collect_style_fact_collection,
37 collect_style_facts,
38};
39use omena_syntax::ident::{AuthoredPropertyTextV0, CanonicalCustomPropertyNameV0};
40use omena_transform_cst::{
41 IrNodeKindV0, TransformPassKind, lower_transform_ir_from_source, transform_pass_sort_ordinal,
42};
43use omena_transform_passes::{TransformPassPlanV0, plan_transform_passes};
44use serde::Serialize;
45use std::{
46 collections::{BTreeMap, BTreeSet},
47 path::{Component, Path, PathBuf},
48};
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub enum TransformBundleEdgeKind {
53 SassUse,
54 SassForward,
55 SassImport,
56 CssImport,
57 LessImport,
58 CssModuleValueImport,
59 CssModuleComposesLocal,
60 CssModuleComposesExternal,
61 IcssImport,
62}
63
64pub const TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0: [TransformBundleEdgeKind; 9] = [
65 TransformBundleEdgeKind::SassUse,
66 TransformBundleEdgeKind::SassForward,
67 TransformBundleEdgeKind::SassImport,
68 TransformBundleEdgeKind::CssImport,
69 TransformBundleEdgeKind::LessImport,
70 TransformBundleEdgeKind::CssModuleValueImport,
71 TransformBundleEdgeKind::CssModuleComposesLocal,
72 TransformBundleEdgeKind::CssModuleComposesExternal,
73 TransformBundleEdgeKind::IcssImport,
74];
75
76impl TransformBundleEdgeKind {
77 pub const fn as_wire_label(self) -> &'static str {
78 match self {
79 Self::SassUse => "sassUse",
80 Self::SassForward => "sassForward",
81 Self::SassImport => "sassImport",
82 Self::CssImport => "cssImport",
83 Self::LessImport => "lessImport",
84 Self::CssModuleValueImport => "cssModuleValueImport",
85 Self::CssModuleComposesLocal => "cssModuleComposesLocal",
86 Self::CssModuleComposesExternal => "cssModuleComposesExternal",
87 Self::IcssImport => "icssImport",
88 }
89 }
90
91 pub const fn order_relevance(self) -> EdgeOrderRelevanceV0 {
92 self.raw_edge_kind().order_relevance()
93 }
94
95 pub const fn order_relevance_reason(self) -> &'static str {
96 match self {
97 Self::SassUse => "Sass module use sequence participates in evaluation order",
98 Self::SassForward => "Sass forwarding sequence participates in module exposure order",
99 Self::SassImport => "Sass import sequence participates in emitted rule order",
100 Self::CssImport => "CSS import sequence participates in emitted rule order",
101 Self::LessImport => "Less import sequence participates in evaluation order",
102 Self::CssModuleValueImport => {
103 "CSS Modules value imports participate in dependency evaluation order"
104 }
105 Self::CssModuleComposesLocal => "local composition preserves selector dependency order",
106 Self::CssModuleComposesExternal => {
107 "external composition preserves module dependency order"
108 }
109 Self::IcssImport => "ICSS imports participate in dependency evaluation order",
110 }
111 }
112
113 const fn raw_edge_kind(self) -> OmenaCrossFileSummaryRawEdgeKindV0 {
114 match self {
115 Self::SassUse => OmenaCrossFileSummaryRawEdgeKindV0::SassUse,
116 Self::SassForward => OmenaCrossFileSummaryRawEdgeKindV0::SassForward,
117 Self::SassImport => OmenaCrossFileSummaryRawEdgeKindV0::SassImport,
118 Self::CssImport => OmenaCrossFileSummaryRawEdgeKindV0::CssModulesImport,
119 Self::LessImport => OmenaCrossFileSummaryRawEdgeKindV0::LessImport,
120 Self::CssModuleValueImport => OmenaCrossFileSummaryRawEdgeKindV0::CssModulesValueImport,
121 Self::CssModuleComposesLocal => OmenaCrossFileSummaryRawEdgeKindV0::ComposesLocal,
122 Self::CssModuleComposesExternal => OmenaCrossFileSummaryRawEdgeKindV0::ComposesExternal,
123 Self::IcssImport => OmenaCrossFileSummaryRawEdgeKindV0::CssModulesIcssImport,
124 }
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
129#[serde(rename_all = "camelCase")]
130pub struct TransformBundleEdgeV0 {
131 pub kind: TransformBundleEdgeKind,
132 pub source_path: String,
133 pub import_source: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub import_ordinal: Option<u32>,
136 pub namespace: Option<String>,
137 pub local_names: Vec<String>,
138 pub remote_names: Vec<String>,
139 pub range_start: u32,
140 pub range_end: u32,
141 pub provenance_required: bool,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
145#[serde(rename_all = "camelCase")]
146pub enum TransformBundleAssetUrlKind {
147 Relative,
148 AbsolutePath,
149 External,
150 Data,
151 Fragment,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
155#[serde(rename_all = "camelCase")]
156pub struct TransformBundleAssetUrlV0 {
157 pub source_path: String,
158 pub raw_url: String,
159 pub normalized_url: String,
160 pub kind: TransformBundleAssetUrlKind,
161 pub resolved_path: Option<String>,
162 pub range_start: u32,
163 pub range_end: u32,
164 pub bundler_resolution_required: bool,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
168#[serde(rename_all = "camelCase")]
169pub struct TransformBundleAssetUrlRewriteSummaryV0 {
170 pub schema_version: &'static str,
171 pub product: &'static str,
172 pub source_path: String,
173 pub asset_url_count: usize,
174 pub rewrite_count: usize,
175 pub output_css: String,
176 pub rewritten_asset_urls: Vec<TransformBundleAssetUrlV0>,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub enum TransformBundleChunkKind {
182 Entry,
183 StyleImport,
184 Asset,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
188#[serde(rename_all = "camelCase")]
189pub struct TransformBundleChunkV0 {
190 pub chunk_id: String,
191 pub kind: TransformBundleChunkKind,
192 pub source_path: String,
193 pub import_source: Option<String>,
194 pub asset_url: Option<String>,
195 pub resolved_path: Option<String>,
196 pub depends_on: Vec<String>,
197 pub split_boundary: &'static str,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
201#[serde(rename_all = "camelCase")]
202pub struct TransformBundleSourceSummaryV0 {
203 pub schema_version: &'static str,
204 pub product: &'static str,
205 pub source_path: String,
206 pub dialect: &'static str,
207 pub bundle_edges: Vec<TransformBundleEdgeV0>,
208 pub asset_urls: Vec<TransformBundleAssetUrlV0>,
209 pub code_split_chunks: Vec<TransformBundleChunkV0>,
210 pub required_pass_ids: Vec<&'static str>,
211 pub planned_pass_ids: Vec<&'static str>,
212 pub import_inline_required: bool,
213 pub module_evaluation_required: bool,
214 pub css_modules_resolution_required: bool,
215 pub class_hashing_required: bool,
216 pub value_resolution_required: bool,
217 pub code_splitting_required: bool,
218 pub pass_plan: TransformPassPlanV0,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct TransformBundleModuleInputV0 {
223 pub source_path: String,
224 pub source: String,
225 pub dialect: StyleDialect,
226 pub configuration_hash: ConfigurationHashV0,
227}
228
229impl TransformBundleModuleInputV0 {
230 pub fn new(
231 source_path: impl Into<String>,
232 source: impl Into<String>,
233 dialect: StyleDialect,
234 ) -> Self {
235 Self {
236 source_path: source_path.into(),
237 source: source.into(),
238 dialect,
239 configuration_hash: ConfigurationHashV0::none(),
240 }
241 }
242
243 pub fn with_configuration_hash(mut self, configuration_hash: ConfigurationHashV0) -> Self {
244 self.configuration_hash = configuration_hash;
245 self
246 }
247
248 pub fn module_instance_key(&self) -> ModuleInstanceKeyV0 {
249 ModuleInstanceKeyV0::new(
250 ModuleIdV0::new(normalize_bundle_path(PathBuf::from(&self.source_path))),
251 self.configuration_hash.clone(),
252 )
253 }
254}
255
256#[non_exhaustive]
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct TransformBundleParsedModuleInputV0 {
259 source_path: String,
260 dialect: StyleDialect,
261 facts: ParsedStyleFacts,
262 emission_selectors: ParsedEmissionSelectorFactsV0,
263 configuration_hashes: Vec<ConfigurationHashV0>,
264}
265
266impl TransformBundleParsedModuleInputV0 {
267 pub fn new(
268 source_path: impl Into<String>,
269 dialect: StyleDialect,
270 facts: ParsedStyleFacts,
271 ) -> Self {
272 Self {
273 source_path: source_path.into(),
274 dialect,
275 facts,
276 emission_selectors: ParsedEmissionSelectorFactsV0::default(),
277 configuration_hashes: vec![ConfigurationHashV0::none()],
278 }
279 }
280
281 pub fn with_emission_selectors(
282 mut self,
283 emission_selectors: ParsedEmissionSelectorFactsV0,
284 ) -> Self {
285 self.emission_selectors = emission_selectors;
286 self
287 }
288
289 pub fn with_configuration_hashes(
290 mut self,
291 configuration_hashes: Vec<ConfigurationHashV0>,
292 ) -> Self {
293 self.configuration_hashes = configuration_hashes
294 .into_iter()
295 .collect::<BTreeSet<_>>()
296 .into_iter()
297 .collect();
298 if self.configuration_hashes.is_empty() {
299 self.configuration_hashes.push(ConfigurationHashV0::none());
300 }
301 self
302 }
303
304 pub fn source_path(&self) -> &str {
305 self.source_path.as_str()
306 }
307
308 pub fn configuration_hashes(&self) -> &[ConfigurationHashV0] {
309 self.configuration_hashes.as_slice()
310 }
311
312 pub fn module_instance_keys(&self) -> Vec<ModuleInstanceKeyV0> {
313 let module = ModuleIdV0::new(normalize_bundle_path(PathBuf::from(&self.source_path)));
314 self.configuration_hashes
315 .iter()
316 .cloned()
317 .map(|configuration| ModuleInstanceKeyV0::new(module.clone(), configuration))
318 .collect()
319 }
320}
321
322#[deprecated(
323 note = "use TransformBundleInstanceReachabilityInputV0 so the module instance and derivation are explicit"
324)]
325#[derive(Debug, Clone, Default)]
326pub struct TransformBundleSemanticReachabilityInputV0 {
327 pub source_path: String,
328 pub class_names: Vec<String>,
329 pub keyframe_names: Vec<String>,
330 pub value_names: Vec<String>,
331 pub custom_property_names: Vec<AuthoredPropertyTextV0>,
332 pub analysis: TransformBundleReachabilityAnalysisV0,
333}
334
335#[allow(deprecated)]
336impl PartialEq for TransformBundleSemanticReachabilityInputV0 {
337 fn eq(&self, other: &Self) -> bool {
338 self.source_path == other.source_path
339 && self.class_names == other.class_names
340 && self.keyframe_names == other.keyframe_names
341 && self.value_names == other.value_names
342 && authored_custom_property_sequences_same(
343 &self.custom_property_names,
344 &other.custom_property_names,
345 )
346 && self.analysis == other.analysis
347 }
348}
349
350#[allow(deprecated)]
351impl Eq for TransformBundleSemanticReachabilityInputV0 {}
352
353#[non_exhaustive]
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
355#[serde(rename_all = "camelCase")]
356pub enum TransformBundleReachabilityUnanalyzedCauseV0 {
357 InputNotProvided,
358 AnalysisNotAttempted,
359 AnalysisResultUnavailable,
360}
361
362#[non_exhaustive]
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
364#[serde(
365 tag = "state",
366 rename_all = "camelCase",
367 rename_all_fields = "camelCase"
368)]
369pub enum TransformBundleReachabilityAnalysisV0 {
370 Analyzed,
371 Unanalyzed {
372 cause: TransformBundleReachabilityUnanalyzedCauseV0,
373 },
374}
375
376impl Default for TransformBundleReachabilityAnalysisV0 {
377 fn default() -> Self {
378 Self::Unanalyzed {
379 cause: TransformBundleReachabilityUnanalyzedCauseV0::InputNotProvided,
380 }
381 }
382}
383
384impl TransformBundleReachabilityAnalysisV0 {
385 pub fn is_analyzed(self) -> bool {
386 matches!(self, Self::Analyzed)
387 }
388}
389
390#[non_exhaustive]
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
392#[serde(rename_all = "camelCase")]
393pub enum InstanceReachabilityDerivationV0 {
394 InstanceAttributed,
398 PathUnionNoInstanceDiscriminator,
399}
400
401#[non_exhaustive]
402#[derive(Debug, Clone)]
403pub struct TransformBundleInstanceReachabilityInputV0 {
404 pub module_instance: ModuleInstanceKeyV0,
405 pub class_names: Vec<String>,
406 pub keyframe_names: Vec<String>,
407 pub value_names: Vec<String>,
408 pub custom_property_names: Vec<AuthoredPropertyTextV0>,
409 pub derivation: InstanceReachabilityDerivationV0,
410 pub analysis: TransformBundleReachabilityAnalysisV0,
411}
412
413impl PartialEq for TransformBundleInstanceReachabilityInputV0 {
414 fn eq(&self, other: &Self) -> bool {
415 self.module_instance == other.module_instance
416 && self.class_names == other.class_names
417 && self.keyframe_names == other.keyframe_names
418 && self.value_names == other.value_names
419 && authored_custom_property_sequences_same(
420 &self.custom_property_names,
421 &other.custom_property_names,
422 )
423 && self.derivation == other.derivation
424 && self.analysis == other.analysis
425 }
426}
427
428impl Eq for TransformBundleInstanceReachabilityInputV0 {}
429
430impl TransformBundleInstanceReachabilityInputV0 {
431 pub fn new(
432 module_instance: ModuleInstanceKeyV0,
433 derivation: InstanceReachabilityDerivationV0,
434 ) -> Self {
435 Self {
436 module_instance,
437 class_names: Vec::new(),
438 keyframe_names: Vec::new(),
439 value_names: Vec::new(),
440 custom_property_names: Vec::new(),
441 derivation,
442 analysis: TransformBundleReachabilityAnalysisV0::Analyzed,
443 }
444 }
445
446 pub fn unanalyzed(
447 module_instance: ModuleInstanceKeyV0,
448 derivation: InstanceReachabilityDerivationV0,
449 cause: TransformBundleReachabilityUnanalyzedCauseV0,
450 ) -> Self {
451 Self {
452 analysis: TransformBundleReachabilityAnalysisV0::Unanalyzed { cause },
453 ..Self::new(module_instance, derivation)
454 }
455 }
456
457 pub fn has_reachable_symbols(&self) -> bool {
458 !self.class_names.is_empty()
459 || !self.keyframe_names.is_empty()
460 || !self.value_names.is_empty()
461 || !self.custom_property_names.is_empty()
462 }
463}
464
465#[allow(deprecated)]
466impl TransformBundleSemanticReachabilityInputV0 {
467 pub fn new(source_path: impl Into<String>) -> Self {
468 Self {
469 source_path: source_path.into(),
470 analysis: TransformBundleReachabilityAnalysisV0::Analyzed,
471 ..Self::default()
472 }
473 }
474
475 pub fn unanalyzed(
476 source_path: impl Into<String>,
477 cause: TransformBundleReachabilityUnanalyzedCauseV0,
478 ) -> Self {
479 Self {
480 source_path: source_path.into(),
481 analysis: TransformBundleReachabilityAnalysisV0::Unanalyzed { cause },
482 ..Self::default()
483 }
484 }
485
486 pub fn has_reachable_symbols(&self) -> bool {
487 !self.class_names.is_empty()
488 || !self.keyframe_names.is_empty()
489 || !self.value_names.is_empty()
490 || !self.custom_property_names.is_empty()
491 }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
495#[serde(rename_all = "camelCase")]
496pub struct LinkerDependencyEdgeV0 {
497 pub kind: TransformBundleEdgeKind,
498 pub import_source: String,
499 pub import_ordinal: Option<u32>,
500 pub local_names: Vec<String>,
501 pub remote_names: Vec<String>,
502}
503
504#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
505#[serde(rename_all = "camelCase")]
506pub struct LinkerRuleV0 {
507 pub selector_name: String,
508 #[serde(serialize_with = "serialize_selector_fact_kind")]
509 pub selector_kind: ParsedSelectorFactKind,
510 pub range_start: u32,
511 pub range_end: u32,
512}
513
514#[derive(Debug, Clone, Serialize)]
515#[serde(rename_all = "camelCase")]
516pub struct LinkerInputV0 {
517 pub source_path: String,
518 #[serde(serialize_with = "serialize_style_dialect")]
519 pub dialect: StyleDialect,
520 pub instance: ModuleInstanceKeyV0,
521 pub dependency_edges: Vec<LinkerDependencyEdgeV0>,
522 pub class_names: Vec<String>,
523 pub keyframe_names: Vec<String>,
524 pub value_names: Vec<String>,
525 pub custom_property_names: Vec<AuthoredPropertyTextV0>,
526 pub ordered_rules: Vec<LinkerRuleV0>,
527}
528
529impl PartialEq for LinkerInputV0 {
530 fn eq(&self, other: &Self) -> bool {
531 self.source_path == other.source_path
532 && self.dialect == other.dialect
533 && self.instance == other.instance
534 && self.dependency_edges == other.dependency_edges
535 && self.class_names == other.class_names
536 && self.keyframe_names == other.keyframe_names
537 && self.value_names == other.value_names
538 && authored_custom_property_sequences_same(
539 &self.custom_property_names,
540 &other.custom_property_names,
541 )
542 && self.ordered_rules == other.ordered_rules
543 }
544}
545
546impl Eq for LinkerInputV0 {}
547
548#[non_exhaustive]
549#[derive(Debug, Clone, PartialEq, Eq)]
550pub struct TransformBundleLinkerProjectionV0 {
551 inputs: Vec<LinkerInputV0>,
552 module_reachability_evidence:
553 BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleReachabilityEvidenceV0>,
554 module_reachability_derivations:
555 BTreeMap<ModuleInstanceKeyV0, InstanceReachabilityDerivationV0>,
556 module_reachability_analysis:
557 BTreeMap<ModuleInstanceKeyV0, TransformBundleReachabilityAnalysisV0>,
558}
559
560#[non_exhaustive]
561#[derive(Debug, Clone, PartialEq, Eq)]
562pub struct TransformBundleLinkProjectionSetV0 {
563 linker_projection: TransformBundleLinkerProjectionV0,
564 emission_item_projection: TransformBundleEmissionItemProjectionV0,
565}
566
567impl TransformBundleLinkProjectionSetV0 {
568 pub fn linker_projection(&self) -> &TransformBundleLinkerProjectionV0 {
569 &self.linker_projection
570 }
571
572 pub fn emission_item_projection(&self) -> &TransformBundleEmissionItemProjectionV0 {
573 &self.emission_item_projection
574 }
575}
576
577impl TransformBundleLinkerProjectionV0 {
578 pub fn inputs(&self) -> &[LinkerInputV0] {
579 &self.inputs
580 }
581
582 pub fn module_reachability_evidence(
583 &self,
584 module_instance: &ModuleInstanceKeyV0,
585 ) -> ClosedWorldModuleReachabilityEvidenceV0 {
586 self.module_reachability_evidence
587 .get(module_instance)
588 .copied()
589 .unwrap_or_default()
590 }
591
592 pub fn module_reachability_derivation(
593 &self,
594 module_instance: &ModuleInstanceKeyV0,
595 ) -> Option<InstanceReachabilityDerivationV0> {
596 self.module_reachability_derivations
597 .get(module_instance)
598 .copied()
599 }
600
601 pub fn module_reachability_analysis(
602 &self,
603 module_instance: &ModuleInstanceKeyV0,
604 ) -> TransformBundleReachabilityAnalysisV0 {
605 self.module_reachability_analysis
606 .get(module_instance)
607 .copied()
608 .unwrap_or_default()
609 }
610
611 pub fn analyzed_empty_reachability_input_count(&self) -> usize {
612 self.inputs
613 .iter()
614 .filter(|input| {
615 self.module_reachability_analysis(&input.instance)
616 .is_analyzed()
617 && input.class_names.is_empty()
618 && input.keyframe_names.is_empty()
619 && input.value_names.is_empty()
620 && input.custom_property_names.is_empty()
621 })
622 .count()
623 }
624
625 pub fn unanalyzed_reachability_input_count(&self) -> usize {
626 self.inputs
627 .iter()
628 .filter(|input| {
629 !self
630 .module_reachability_analysis(&input.instance)
631 .is_analyzed()
632 })
633 .count()
634 }
635}
636
637#[non_exhaustive]
638#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
639#[serde(rename_all = "camelCase")]
640pub struct TransformBundleDependencyResolutionV0 {
641 pub attempt_state: &'static str,
642 pub policy_step_keys: Vec<&'static str>,
643 pub resolution_kind: Option<&'static str>,
644 pub candidate_count: usize,
645 pub target_instance: Option<ModuleInstanceKeyV0>,
646}
647
648impl TransformBundleDependencyResolutionV0 {
649 pub fn attempted(
650 policy_step_keys: Vec<&'static str>,
651 resolution_kind: &'static str,
652 candidate_count: usize,
653 target_instance: Option<ModuleInstanceKeyV0>,
654 ) -> Self {
655 Self {
656 attempt_state: "attempted",
657 policy_step_keys,
658 resolution_kind: Some(resolution_kind),
659 candidate_count,
660 target_instance,
661 }
662 }
663
664 pub fn never_attempted(policy_step_keys: Vec<&'static str>) -> Self {
665 Self {
666 attempt_state: "never-attempted",
667 policy_step_keys,
668 resolution_kind: None,
669 candidate_count: 0,
670 target_instance: None,
671 }
672 }
673}
674
675#[non_exhaustive]
676#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
677#[serde(rename_all = "camelCase")]
678pub struct TransformBundleResolvedDependencyV0 {
679 pub source_instance: ModuleInstanceKeyV0,
680 pub edge_kind: TransformBundleEdgeKind,
681 pub import_source: String,
682 pub import_ordinal: Option<u32>,
683 pub resolution: TransformBundleDependencyResolutionV0,
684}
685
686impl TransformBundleResolvedDependencyV0 {
687 pub fn new(
688 source_instance: ModuleInstanceKeyV0,
689 edge_kind: TransformBundleEdgeKind,
690 import_source: impl Into<String>,
691 import_ordinal: Option<u32>,
692 resolution: TransformBundleDependencyResolutionV0,
693 ) -> Self {
694 Self {
695 source_instance,
696 edge_kind,
697 import_source: import_source.into(),
698 import_ordinal,
699 resolution,
700 }
701 }
702}
703
704#[non_exhaustive]
705#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize)]
706#[serde(rename_all = "camelCase")]
707pub enum BundleResolutionAuthorityV0 {
708 #[default]
710 Resolved,
711 #[deprecated(
713 since = "0.5.0",
714 note = "legacy path-inferred dependency authority is scheduled for removal before 1.0"
715 )]
716 LegacyPathInferred,
717}
718
719#[non_exhaustive]
720#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
721#[serde(rename_all = "camelCase")]
722pub struct BundleDependencyResolutionDisclosureV0 {
723 pub source_instance: ModuleInstanceKeyV0,
724 pub import_source: String,
725 pub import_ordinal: Option<u32>,
726 pub authority: BundleResolutionAuthorityV0,
727}
728
729#[non_exhaustive]
730#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
731#[serde(rename_all = "camelCase")]
732pub struct TransformBundleLinkOptionsV0 {
733 pub emission_ordering_policy: EmissionOrderingPolicyV0,
734 pub dependency_resolution_authority: BundleResolutionAuthorityV0,
735}
736
737#[allow(deprecated)]
738impl TransformBundleLinkOptionsV0 {
739 #[deprecated(
740 since = "0.5.0",
741 note = "legacy module-id ordering and path inference are scheduled for removal before 1.0"
742 )]
743 pub const fn legacy_compatibility() -> Self {
744 Self {
745 emission_ordering_policy: EmissionOrderingPolicyV0::ModuleIdLegacy,
746 dependency_resolution_authority: BundleResolutionAuthorityV0::LegacyPathInferred,
747 }
748 }
749
750 pub const fn with_emission_ordering_policy(
751 mut self,
752 emission_ordering_policy: EmissionOrderingPolicyV0,
753 ) -> Self {
754 self.emission_ordering_policy = emission_ordering_policy;
755 self
756 }
757
758 pub const fn with_dependency_resolution_authority(
759 mut self,
760 dependency_resolution_authority: BundleResolutionAuthorityV0,
761 ) -> Self {
762 self.dependency_resolution_authority = dependency_resolution_authority;
763 self
764 }
765}
766
767#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
768#[serde(rename_all = "camelCase")]
769pub struct LinkedStylesheetRuleV0 {
770 pub global_order_index: u32,
771 pub module_instance: ModuleInstanceKeyV0,
772 pub selector_name: String,
773 pub selector_kind: &'static str,
774 pub range_start: u32,
775 pub range_end: u32,
776}
777
778impl LinkedStylesheetRuleV0 {
779 pub fn cascade_key_with_global_source_order(
780 &self,
781 level: CascadeLevel,
782 layer_ordinal: LayerOrdinal,
783 important: bool,
784 scope_proximity: u32,
785 specificity: Specificity,
786 module_rank: ModuleRank,
787 ) -> (CascadeKey, OpenWorldTieEvidence) {
788 (
789 CascadeKey::new(
790 level,
791 normalized_layer_rank(important, Some(layer_ordinal)),
792 scope_proximity,
793 specificity,
794 self.global_order_index,
795 ),
796 OpenWorldTieEvidence::new(module_rank),
797 )
798 }
799}
800
801#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
802#[serde(rename_all = "camelCase")]
803pub struct GlobalRuleOrderV0 {
804 pub rules: Vec<LinkedStylesheetRuleV0>,
805}
806
807#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
808#[serde(rename_all = "camelCase")]
809pub struct LinkedStylesheetV0 {
810 pub schema_version: &'static str,
811 pub product: &'static str,
812 pub entrypoints: Vec<ModuleInstanceKeyV0>,
813 pub module_instances: Vec<ModuleInstanceKeyV0>,
814 #[serde(skip_serializing)]
815 pub emission_plan: EmissionPlanV0,
816 pub global_rule_order: GlobalRuleOrderV0,
817 pub closed_world_bundle: ClosedWorldBundleV0,
818}
819
820#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
821#[serde(rename_all = "camelCase")]
822#[non_exhaustive]
823pub struct LinkedStylesheetWithEmissionItemsV0 {
824 pub linked_stylesheet: LinkedStylesheetV0,
825 pub emission_item_plan: EmissionItemPlanV0,
826 pub emission_item_order: LinkedEmissionItemOrderV0,
827 pub projection_disclosures: Vec<EmissionItemProjectionDisclosureV0>,
828 pub dependency_resolution_disclosures: Vec<BundleDependencyResolutionDisclosureV0>,
829}
830
831#[non_exhaustive]
833#[derive(Debug, Clone, PartialEq, Eq)]
834pub struct TransformBundleEmissionAdmissionV0 {
835 module_id_legacy_open: bool,
836 requested_policy_result:
837 Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0>,
838}
839
840impl TransformBundleEmissionAdmissionV0 {
841 pub const fn module_id_legacy_open(&self) -> bool {
843 self.module_id_legacy_open
844 }
845
846 pub fn requested_policy_result(
848 &self,
849 ) -> &Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
850 &self.requested_policy_result
851 }
852
853 pub fn into_requested_policy_result(
855 self,
856 ) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
857 self.requested_policy_result
858 }
859
860 pub fn into_parts(
862 self,
863 ) -> (
864 bool,
865 Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0>,
866 ) {
867 (self.module_id_legacy_open, self.requested_policy_result)
868 }
869}
870
871#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
872#[serde(rename_all = "camelCase")]
873pub struct TransformBundleTransformedModuleV0 {
874 pub module_instance: ModuleInstanceKeyV0,
875 pub output_css: String,
876 pub non_empty_import_replacement_count: usize,
877}
878
879impl TransformBundleTransformedModuleV0 {
880 pub fn new(module_instance: ModuleInstanceKeyV0, output_css: impl Into<String>) -> Self {
881 Self {
882 module_instance,
883 output_css: output_css.into(),
884 non_empty_import_replacement_count: 0,
885 }
886 }
887
888 pub const fn with_non_empty_import_replacement_count(mut self, count: usize) -> Self {
889 self.non_empty_import_replacement_count = count;
890 self
891 }
892}
893
894#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
895#[serde(rename_all = "camelCase")]
896pub struct LinkedEmissionModuleRegionV0 {
897 pub module_instance: ModuleInstanceKeyV0,
898 pub first_global_order_index: Option<u32>,
899 pub generated_start: usize,
900 pub generated_end: usize,
901}
902
903#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
904#[serde(rename_all = "camelCase")]
905pub struct LinkedEmissionOrderEntryRegionV0 {
906 pub global_order_index: u32,
907 pub module_instance: ModuleInstanceKeyV0,
908 pub generated_start: usize,
909 pub generated_end: usize,
910}
911
912#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
913#[serde(rename_all = "camelCase")]
914pub struct LinkedEmissionArtifactV0 {
915 pub schema_version: &'static str,
916 pub product: &'static str,
917 pub output_css: String,
918 pub module_regions: Vec<LinkedEmissionModuleRegionV0>,
919 pub order_entry_regions: Vec<LinkedEmissionOrderEntryRegionV0>,
920 pub emitted_module_count: usize,
921 pub global_order_entry_count: usize,
922}
923
924#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
925#[serde(rename_all = "camelCase")]
926pub enum LinkedEmissionMaterializationErrorV0 {
927 DuplicateTransformedModule {
928 module_instance: ModuleInstanceKeyV0,
929 },
930 MissingTransformedModule {
931 module_instance: ModuleInstanceKeyV0,
932 },
933 UnexpectedTransformedModule {
934 module_instance: ModuleInstanceKeyV0,
935 },
936 ImportReplacementWouldDuplicateModule {
937 module_instance: ModuleInstanceKeyV0,
938 replacement_count: usize,
939 },
940 InvalidGlobalOrderIndex {
941 expected: u32,
942 actual: u32,
943 },
944}
945
946#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
947#[serde(rename_all = "camelCase")]
948pub struct EmissionPolicyDifferenceV0 {
949 pub output_index: u32,
950 pub module_id_legacy_module: Option<ModuleInstanceKeyV0>,
951 pub module_id_legacy_selector: Option<String>,
952 pub import_order_module: Option<ModuleInstanceKeyV0>,
953 pub import_order_selector: Option<String>,
954}
955
956#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
957#[serde(rename_all = "camelCase")]
958pub struct EmissionPolicyDifferentialReportV0 {
959 pub schema_version: &'static str,
960 pub product: &'static str,
961 pub module_id_legacy_rule_count: usize,
962 pub import_order_rule_count: usize,
963 pub difference_count: usize,
964 pub equivalent: bool,
965 pub differences: Vec<EmissionPolicyDifferenceV0>,
966}
967
968#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
969#[serde(rename_all = "camelCase")]
970pub enum TransformBundleLinkErrorV0 {
971 MissingEntrypoint {
972 source_path: String,
973 },
974 AmbiguousModulePath {
975 source_path: String,
976 },
977 MissingDependency {
978 source_path: String,
979 import_source: String,
980 },
981 UnresolvedDependencyEdge {
982 source_path: String,
983 import_source: String,
984 import_ordinal: Option<u32>,
985 },
986 ClosedWorldBundle {
987 error: ClosedWorldBundleBuildErrorV0,
988 },
989 InvalidEmissionPlan {
990 reason: String,
991 },
992 UnsupportedEmissionCycle {
993 edge_kind: TransformBundleEdgeKind,
994 },
995 UnsupportedDialectEmissionCycle {
996 dialect: EmissionCycleDialectV0,
997 class: EmissionCycleClassV0,
998 edge_kinds: Vec<TransformBundleEdgeKind>,
999 },
1000}
1001
1002pub fn summarize_omena_transform_bundle_from_source(
1003 source_path: impl Into<String>,
1004 source: &str,
1005 dialect: StyleDialect,
1006) -> TransformBundleSourceSummaryV0 {
1007 let source_path = source_path.into();
1008 let facts = collect_style_facts(source, dialect);
1009 let bundle_edges = collect_bundle_edges_from_facts(&source_path, dialect, &facts);
1010 let asset_urls = collect_transform_ir_bundle_asset_urls(&source_path, source, dialect);
1011 let code_split_chunks = plan_bundle_code_split_chunks(&source_path, &bundle_edges, &asset_urls);
1012 let mut required_passes =
1013 required_passes_for_source(&source_path, dialect, &facts, &bundle_edges);
1014 required_passes.sort_by_key(|pass| transform_pass_sort_ordinal(*pass));
1015 required_passes.dedup();
1016 let pass_plan = plan_transform_passes(&required_passes);
1017 let planned_pass_ids = pass_plan.ordered_pass_ids.clone();
1018 let required_pass_ids = required_passes
1019 .iter()
1020 .map(|pass| pass.id())
1021 .collect::<Vec<_>>();
1022
1023 TransformBundleSourceSummaryV0 {
1024 schema_version: "0",
1025 product: "omena-transform-bundle.source",
1026 source_path,
1027 dialect: dialect_label(dialect),
1028 bundle_edges,
1029 asset_urls,
1030 code_splitting_required: code_split_chunks.len() > 1,
1031 code_split_chunks,
1032 required_pass_ids,
1033 planned_pass_ids,
1034 import_inline_required: required_passes.contains(&TransformPassKind::ImportInline),
1035 module_evaluation_required: required_passes.iter().any(|pass| {
1036 matches!(
1037 pass,
1038 TransformPassKind::ScssModuleEvaluate | TransformPassKind::LessModuleEvaluate
1039 )
1040 }),
1041 css_modules_resolution_required: required_passes.iter().any(|pass| {
1042 matches!(
1043 pass,
1044 TransformPassKind::HashCssModuleClassNames
1045 | TransformPassKind::ResolveCssModulesComposes
1046 )
1047 }),
1048 class_hashing_required: required_passes
1049 .contains(&TransformPassKind::HashCssModuleClassNames),
1050 value_resolution_required: required_passes.contains(&TransformPassKind::ValueResolution),
1051 pass_plan,
1052 }
1053}
1054
1055pub fn link_omena_transform_bundle_modules<P: AsRef<str>>(
1057 entrypoint_paths: &[P],
1058 modules: &[TransformBundleModuleInputV0],
1059) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1060 link_omena_transform_bundle_modules_with_semantic_reachability(entrypoint_paths, modules, &[])
1061}
1062
1063#[allow(deprecated)]
1065pub fn link_omena_transform_bundle_modules_with_semantic_reachability<P: AsRef<str>>(
1066 entrypoint_paths: &[P],
1067 modules: &[TransformBundleModuleInputV0],
1068 reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
1069) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1070 link_omena_transform_bundle_modules_with_semantic_reachability_and_metadata(
1071 entrypoint_paths,
1072 modules,
1073 reachability_inputs,
1074 &[],
1075 )
1076}
1077
1078#[allow(deprecated)]
1080pub fn link_omena_transform_bundle_modules_with_semantic_reachability_and_metadata<
1081 P: AsRef<str>,
1082>(
1083 entrypoint_paths: &[P],
1084 modules: &[TransformBundleModuleInputV0],
1085 reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
1086 module_metadata: &[ClosedWorldModuleMetadataV0],
1087) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1088 link_omena_transform_bundle_modules_with_options(
1089 entrypoint_paths,
1090 modules,
1091 reachability_inputs,
1092 module_metadata,
1093 TransformBundleLinkOptionsV0::default(),
1094 )
1095}
1096
1097#[allow(deprecated)]
1099pub fn link_omena_transform_bundle_modules_with_options<P: AsRef<str>>(
1100 entrypoint_paths: &[P],
1101 modules: &[TransformBundleModuleInputV0],
1102 reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
1103 module_metadata: &[ClosedWorldModuleMetadataV0],
1104 options: TransformBundleLinkOptionsV0,
1105) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1106 let projection = project_omena_transform_bundle_linker_inputs(modules, reachability_inputs);
1107 let resolved_dependencies = derive_facade_resolved_dependencies(
1108 projection.inputs(),
1109 options.dependency_resolution_authority,
1110 )?;
1111 link_omena_transform_bundle_projection_with_resolved_dependencies_and_options(
1112 entrypoint_paths,
1113 &projection,
1114 resolved_dependencies.as_slice(),
1115 module_metadata,
1116 options,
1117 )
1118}
1119
1120#[allow(deprecated)]
1121pub fn project_omena_transform_bundle_linker_inputs(
1122 modules: &[TransformBundleModuleInputV0],
1123 reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
1124) -> TransformBundleLinkerProjectionV0 {
1125 let parsed_modules = modules
1126 .iter()
1127 .map(|module| {
1128 TransformBundleParsedModuleInputV0::new(
1129 module.source_path.as_str(),
1130 module.dialect,
1131 collect_style_facts(module.source.as_str(), module.dialect),
1132 )
1133 .with_configuration_hashes(vec![module.configuration_hash.clone()])
1134 })
1135 .collect::<Vec<_>>();
1136 project_omena_transform_bundle_linker_inputs_from_parsed_modules(
1137 parsed_modules.as_slice(),
1138 reachability_inputs,
1139 )
1140}
1141
1142#[allow(deprecated)]
1143pub fn project_omena_transform_bundle_linker_and_emission_items(
1144 modules: &[TransformBundleModuleInputV0],
1145 reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
1146) -> TransformBundleLinkProjectionSetV0 {
1147 let parsed_modules = modules
1148 .iter()
1149 .map(|module| {
1150 let collection = collect_style_fact_collection(module.source.as_str(), module.dialect);
1151 TransformBundleParsedModuleInputV0::new(
1152 module.source_path.as_str(),
1153 module.dialect,
1154 collection.facts,
1155 )
1156 .with_emission_selectors(collection.emission_selectors)
1157 .with_configuration_hashes(vec![module.configuration_hash.clone()])
1158 })
1159 .collect::<Vec<_>>();
1160 project_omena_transform_bundle_linker_and_emission_items_from_parsed_modules(
1161 parsed_modules.as_slice(),
1162 reachability_inputs,
1163 )
1164}
1165
1166#[allow(deprecated)]
1167pub fn project_omena_transform_bundle_linker_inputs_from_parsed_modules(
1168 modules: &[TransformBundleParsedModuleInputV0],
1169 reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
1170) -> TransformBundleLinkerProjectionV0 {
1171 let instance_reachability_inputs =
1172 fan_out_path_reachability_to_instances(modules, reachability_inputs);
1173 project_omena_transform_bundle_linker_inputs_from_parsed_modules_with_instance_reachability(
1174 modules,
1175 instance_reachability_inputs.as_slice(),
1176 )
1177}
1178
1179pub fn project_omena_transform_bundle_linker_inputs_from_parsed_modules_with_instance_reachability(
1180 modules: &[TransformBundleParsedModuleInputV0],
1181 reachability_inputs: &[TransformBundleInstanceReachabilityInputV0],
1182) -> TransformBundleLinkerProjectionV0 {
1183 let mut inputs = Vec::new();
1184 for module in modules {
1185 let source_path = normalize_bundle_path(PathBuf::from(module.source_path.as_str()));
1186 let bundle_edges =
1187 collect_bundle_edges_from_facts(&source_path, module.dialect, &module.facts);
1188 for instance in module.module_instance_keys() {
1189 inputs.push(linker_input_from_module_facts(
1190 source_path.as_str(),
1191 module.dialect,
1192 instance,
1193 &module.facts,
1194 bundle_edges.as_slice(),
1195 ));
1196 }
1197 }
1198 let (
1199 module_reachability_evidence,
1200 module_reachability_derivations,
1201 module_reachability_analysis,
1202 ) = apply_semantic_reachability_to_linker_inputs(inputs.as_mut_slice(), reachability_inputs);
1203 TransformBundleLinkerProjectionV0 {
1204 inputs,
1205 module_reachability_evidence,
1206 module_reachability_derivations,
1207 module_reachability_analysis,
1208 }
1209}
1210
1211#[allow(deprecated)]
1212pub fn project_omena_transform_bundle_linker_and_emission_items_from_parsed_modules(
1213 modules: &[TransformBundleParsedModuleInputV0],
1214 reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
1215) -> TransformBundleLinkProjectionSetV0 {
1216 let instance_reachability_inputs =
1217 fan_out_path_reachability_to_instances(modules, reachability_inputs);
1218 project_omena_transform_bundle_linker_and_emission_items_from_parsed_modules_with_instance_reachability(
1219 modules,
1220 instance_reachability_inputs.as_slice(),
1221 )
1222}
1223
1224pub fn project_omena_transform_bundle_linker_and_emission_items_from_parsed_modules_with_instance_reachability(
1225 modules: &[TransformBundleParsedModuleInputV0],
1226 reachability_inputs: &[TransformBundleInstanceReachabilityInputV0],
1227) -> TransformBundleLinkProjectionSetV0 {
1228 let linker_projection =
1229 project_omena_transform_bundle_linker_inputs_from_parsed_modules_with_instance_reachability(
1230 modules,
1231 reachability_inputs,
1232 );
1233 let mut emission_item_inputs = Vec::new();
1234 for module in modules {
1235 let items =
1236 emission_items::collect_emission_items(&module.facts, &module.emission_selectors);
1237 let disclosure = emission_items::emission_item_projection_disclosure(&module.facts);
1238 for instance in module.module_instance_keys() {
1239 emission_item_inputs.push(EmissionItemInputV0 {
1240 module_instance: instance,
1241 items: items.clone(),
1242 disclosure: disclosure.clone(),
1243 });
1244 }
1245 }
1246 TransformBundleLinkProjectionSetV0 {
1247 linker_projection,
1248 emission_item_projection: TransformBundleEmissionItemProjectionV0::new(
1249 emission_item_inputs,
1250 ),
1251 }
1252}
1253
1254pub fn link_omena_transform_bundle_projection_with_resolved_dependencies_and_options<
1256 P: AsRef<str>,
1257>(
1258 entrypoint_paths: &[P],
1259 projection: &TransformBundleLinkerProjectionV0,
1260 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1261 module_metadata: &[ClosedWorldModuleMetadataV0],
1262 options: TransformBundleLinkOptionsV0,
1263) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1264 let entrypoint_paths = entrypoint_paths
1265 .iter()
1266 .map(|path| path.as_ref())
1267 .collect::<Vec<_>>();
1268
1269 link_stylesheet_from_projection_with_metadata_and_options(
1270 entrypoint_paths.as_slice(),
1271 projection.inputs(),
1272 resolved_dependencies,
1273 module_metadata,
1274 &projection.module_reachability_evidence,
1275 options,
1276 )
1277}
1278
1279pub fn link_omena_transform_bundle_projection_with_emission_items<P: AsRef<str>>(
1280 entrypoint_paths: &[P],
1281 linker_projection: &TransformBundleLinkerProjectionV0,
1282 emission_item_projection: &TransformBundleEmissionItemProjectionV0,
1283 module_metadata: &[ClosedWorldModuleMetadataV0],
1284) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1285 let options = TransformBundleLinkOptionsV0::default();
1286 let resolved_dependencies = derive_facade_resolved_dependencies(
1287 linker_projection.inputs(),
1288 options.dependency_resolution_authority,
1289 )?;
1290 link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
1291 entrypoint_paths,
1292 linker_projection,
1293 emission_item_projection,
1294 resolved_dependencies.as_slice(),
1295 module_metadata,
1296 options,
1297 )
1298}
1299
1300pub fn link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options<
1301 P: AsRef<str>,
1302>(
1303 entrypoint_paths: &[P],
1304 linker_projection: &TransformBundleLinkerProjectionV0,
1305 emission_item_projection: &TransformBundleEmissionItemProjectionV0,
1306 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1307 module_metadata: &[ClosedWorldModuleMetadataV0],
1308 options: TransformBundleLinkOptionsV0,
1309) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1310 let entrypoint_paths = entrypoint_paths
1311 .iter()
1312 .map(|path| path.as_ref())
1313 .collect::<Vec<_>>();
1314
1315 link_stylesheet_from_projection_with_emission_items_and_metadata_and_options(
1316 entrypoint_paths.as_slice(),
1317 linker_projection.inputs(),
1318 emission_item_projection.inputs(),
1319 resolved_dependencies,
1320 module_metadata,
1321 &linker_projection.module_reachability_evidence,
1322 options,
1323 )
1324}
1325
1326pub fn link_resolved_bundle<P: AsRef<str>>(
1327 entrypoint_paths: &[P],
1328 linker_projection: &TransformBundleLinkerProjectionV0,
1329 emission_item_projection: &TransformBundleEmissionItemProjectionV0,
1330 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1331 module_metadata: &[ClosedWorldModuleMetadataV0],
1332 emission_ordering_policy: EmissionOrderingPolicyV0,
1333) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1334 link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
1335 entrypoint_paths,
1336 linker_projection,
1337 emission_item_projection,
1338 resolved_dependencies,
1339 module_metadata,
1340 TransformBundleLinkOptionsV0::default()
1341 .with_emission_ordering_policy(emission_ordering_policy)
1342 .with_dependency_resolution_authority(BundleResolutionAuthorityV0::Resolved),
1343 )
1344}
1345
1346#[deprecated(
1347 note = "supply resolved dependencies and use link_resolved_bundle when dependency authority must be complete"
1348)]
1349pub fn link_legacy_path_inferred_bundle<P: AsRef<str>>(
1350 entrypoint_paths: &[P],
1351 linker_projection: &TransformBundleLinkerProjectionV0,
1352 emission_item_projection: &TransformBundleEmissionItemProjectionV0,
1353 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1354 module_metadata: &[ClosedWorldModuleMetadataV0],
1355 emission_ordering_policy: EmissionOrderingPolicyV0,
1356) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1357 #[allow(deprecated)]
1358 let options = TransformBundleLinkOptionsV0::legacy_compatibility()
1359 .with_emission_ordering_policy(emission_ordering_policy);
1360 link_omena_transform_bundle_projection_with_emission_items_and_resolved_dependencies_and_options(
1361 entrypoint_paths,
1362 linker_projection,
1363 emission_item_projection,
1364 resolved_dependencies,
1365 module_metadata,
1366 options,
1367 )
1368}
1369
1370pub fn evaluate_omena_transform_bundle_projection_emission_admission_with_resolved_dependencies_and_options<
1372 P: AsRef<str>,
1373>(
1374 entrypoint_paths: &[P],
1375 linker_projection: &TransformBundleLinkerProjectionV0,
1376 emission_item_projection: &TransformBundleEmissionItemProjectionV0,
1377 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1378 module_metadata: &[ClosedWorldModuleMetadataV0],
1379 options: TransformBundleLinkOptionsV0,
1380) -> TransformBundleEmissionAdmissionV0 {
1381 let entrypoint_paths = entrypoint_paths
1382 .iter()
1383 .map(|path| path.as_ref())
1384 .collect::<Vec<_>>();
1385
1386 evaluate_stylesheet_emission_admission_from_projection(
1387 entrypoint_paths.as_slice(),
1388 linker_projection.inputs(),
1389 emission_item_projection.inputs(),
1390 resolved_dependencies,
1391 module_metadata,
1392 &linker_projection.module_reachability_evidence,
1393 options,
1394 )
1395}
1396
1397pub fn compare_omena_transform_bundle_emission_policies<P: AsRef<str>>(
1398 entrypoint_paths: &[P],
1399 modules: &[TransformBundleModuleInputV0],
1400) -> Result<EmissionPolicyDifferentialReportV0, TransformBundleLinkErrorV0> {
1401 #[allow(deprecated)]
1402 let legacy_options = TransformBundleLinkOptionsV0::legacy_compatibility();
1403 let module_id_legacy = link_omena_transform_bundle_modules_with_options(
1404 entrypoint_paths,
1405 modules,
1406 &[],
1407 &[],
1408 legacy_options,
1409 )?;
1410 let import_order = link_omena_transform_bundle_modules_with_options(
1411 entrypoint_paths,
1412 modules,
1413 &[],
1414 &[],
1415 TransformBundleLinkOptionsV0::default(),
1416 )?;
1417 let module_id_legacy_rules = &module_id_legacy.global_rule_order.rules;
1418 let import_order_rules = &import_order.global_rule_order.rules;
1419 let mut differences = Vec::new();
1420 for output_index in 0..module_id_legacy_rules.len().max(import_order_rules.len()) {
1421 let module_id_legacy_rule = module_id_legacy_rules.get(output_index);
1422 let import_order_rule = import_order_rules.get(output_index);
1423 if module_id_legacy_rule == import_order_rule {
1424 continue;
1425 }
1426 differences.push(EmissionPolicyDifferenceV0 {
1427 output_index: u32::try_from(output_index).map_err(|_| {
1428 TransformBundleLinkErrorV0::InvalidEmissionPlan {
1429 reason: "policy differential has more rows than the output index can represent"
1430 .to_string(),
1431 }
1432 })?,
1433 module_id_legacy_module: module_id_legacy_rule.map(|rule| rule.module_instance.clone()),
1434 module_id_legacy_selector: module_id_legacy_rule.map(|rule| rule.selector_name.clone()),
1435 import_order_module: import_order_rule.map(|rule| rule.module_instance.clone()),
1436 import_order_selector: import_order_rule.map(|rule| rule.selector_name.clone()),
1437 });
1438 }
1439 let difference_count = differences.len();
1440 Ok(EmissionPolicyDifferentialReportV0 {
1441 schema_version: "0",
1442 product: "omena-bundler.emission-policy-differential",
1443 module_id_legacy_rule_count: module_id_legacy_rules.len(),
1444 import_order_rule_count: import_order_rules.len(),
1445 difference_count,
1446 equivalent: difference_count == 0,
1447 differences,
1448 })
1449}
1450
1451pub fn materialize_omena_transform_bundle_linked_stylesheet(
1452 linked: &LinkedStylesheetV0,
1453 transformed_modules: &[TransformBundleTransformedModuleV0],
1454) -> Result<LinkedEmissionArtifactV0, LinkedEmissionMaterializationErrorV0> {
1455 let (module_order, first_order_index_by_instance) =
1456 legacy_materialization_module_order(linked)?;
1457 materialize_linked_stylesheet_in_module_order(
1458 linked,
1459 transformed_modules,
1460 module_order,
1461 &first_order_index_by_instance,
1462 )
1463}
1464
1465pub fn materialize_omena_transform_bundle_linked_stylesheet_with_emission_items(
1466 linked: &LinkedStylesheetWithEmissionItemsV0,
1467 transformed_modules: &[TransformBundleTransformedModuleV0],
1468) -> Result<LinkedEmissionArtifactV0, LinkedEmissionItemMaterializationErrorV0> {
1469 let linked_modules = linked
1470 .linked_stylesheet
1471 .module_instances
1472 .iter()
1473 .cloned()
1474 .collect::<BTreeSet<_>>();
1475 let mut represented_modules = BTreeSet::new();
1476 let mut module_order = Vec::new();
1477 for (expected_index, item) in linked.emission_item_order.items.iter().enumerate() {
1478 let expected_index = u32::try_from(expected_index).unwrap_or(u32::MAX);
1479 if item.global_order_index != expected_index {
1480 return Err(
1481 LinkedEmissionItemMaterializationErrorV0::InvalidItemOrderIndex {
1482 expected: expected_index,
1483 actual: item.global_order_index,
1484 },
1485 );
1486 }
1487 if !linked_modules.contains(&item.module_instance) {
1488 return Err(
1489 LinkedEmissionItemMaterializationErrorV0::UnknownEmissionItemModule {
1490 module_instance: item.module_instance.clone(),
1491 },
1492 );
1493 }
1494 if represented_modules.insert(item.module_instance.clone()) {
1495 module_order.push(item.module_instance.clone());
1496 }
1497 }
1498 for module_instance in &linked.linked_stylesheet.module_instances {
1499 if !represented_modules.contains(module_instance) {
1500 return Err(
1501 LinkedEmissionItemMaterializationErrorV0::MissingEmissionItem {
1502 module_instance: module_instance.clone(),
1503 },
1504 );
1505 }
1506 }
1507
1508 let (_, first_order_index_by_instance) =
1509 selector_materialization_module_order(&linked.linked_stylesheet)?;
1510 materialize_linked_stylesheet_in_module_order(
1511 &linked.linked_stylesheet,
1512 transformed_modules,
1513 module_order,
1514 &first_order_index_by_instance,
1515 )
1516 .map_err(LinkedEmissionItemMaterializationErrorV0::from)
1517}
1518
1519fn legacy_materialization_module_order(
1520 linked: &LinkedStylesheetV0,
1521) -> Result<
1522 (Vec<ModuleInstanceKeyV0>, BTreeMap<ModuleInstanceKeyV0, u32>),
1523 LinkedEmissionMaterializationErrorV0,
1524> {
1525 let (mut module_order, first_order_index_by_instance) =
1526 selector_materialization_module_order(linked)?;
1527 for module_instance in &linked.module_instances {
1528 if !first_order_index_by_instance.contains_key(module_instance) {
1529 module_order.push(module_instance.clone());
1530 }
1531 }
1532 Ok((module_order, first_order_index_by_instance))
1533}
1534
1535fn selector_materialization_module_order(
1536 linked: &LinkedStylesheetV0,
1537) -> Result<
1538 (Vec<ModuleInstanceKeyV0>, BTreeMap<ModuleInstanceKeyV0, u32>),
1539 LinkedEmissionMaterializationErrorV0,
1540> {
1541 let mut first_order_index_by_instance = BTreeMap::new();
1542 let mut module_order = Vec::new();
1543 for (expected_index, rule) in linked.global_rule_order.rules.iter().enumerate() {
1544 let expected_index = u32::try_from(expected_index).unwrap_or(u32::MAX);
1545 if rule.global_order_index != expected_index {
1546 return Err(
1547 LinkedEmissionMaterializationErrorV0::InvalidGlobalOrderIndex {
1548 expected: expected_index,
1549 actual: rule.global_order_index,
1550 },
1551 );
1552 }
1553 if first_order_index_by_instance
1554 .insert(rule.module_instance.clone(), rule.global_order_index)
1555 .is_none()
1556 {
1557 module_order.push(rule.module_instance.clone());
1558 }
1559 }
1560 Ok((module_order, first_order_index_by_instance))
1561}
1562
1563fn materialize_linked_stylesheet_in_module_order(
1564 linked: &LinkedStylesheetV0,
1565 transformed_modules: &[TransformBundleTransformedModuleV0],
1566 module_order: Vec<ModuleInstanceKeyV0>,
1567 first_order_index_by_instance: &BTreeMap<ModuleInstanceKeyV0, u32>,
1568) -> Result<LinkedEmissionArtifactV0, LinkedEmissionMaterializationErrorV0> {
1569 let linked_modules = linked
1570 .module_instances
1571 .iter()
1572 .cloned()
1573 .collect::<BTreeSet<_>>();
1574 let mut transformed_by_instance = BTreeMap::new();
1575 for transformed in transformed_modules {
1576 if !linked_modules.contains(&transformed.module_instance) {
1577 return Err(
1578 LinkedEmissionMaterializationErrorV0::UnexpectedTransformedModule {
1579 module_instance: transformed.module_instance.clone(),
1580 },
1581 );
1582 }
1583 if transformed.non_empty_import_replacement_count > 0 {
1584 return Err(
1585 LinkedEmissionMaterializationErrorV0::ImportReplacementWouldDuplicateModule {
1586 module_instance: transformed.module_instance.clone(),
1587 replacement_count: transformed.non_empty_import_replacement_count,
1588 },
1589 );
1590 }
1591 if transformed_by_instance
1592 .insert(transformed.module_instance.clone(), transformed)
1593 .is_some()
1594 {
1595 return Err(
1596 LinkedEmissionMaterializationErrorV0::DuplicateTransformedModule {
1597 module_instance: transformed.module_instance.clone(),
1598 },
1599 );
1600 }
1601 }
1602
1603 for module_instance in &linked.module_instances {
1604 if !transformed_by_instance.contains_key(module_instance) {
1605 return Err(
1606 LinkedEmissionMaterializationErrorV0::MissingTransformedModule {
1607 module_instance: module_instance.clone(),
1608 },
1609 );
1610 }
1611 }
1612
1613 let mut output_css = String::new();
1614 let mut module_regions = Vec::with_capacity(module_order.len());
1615 let mut generated_region_by_instance = BTreeMap::new();
1616 for module_instance in module_order {
1617 let Some(transformed) = transformed_by_instance.get(&module_instance) else {
1618 return Err(
1619 LinkedEmissionMaterializationErrorV0::MissingTransformedModule { module_instance },
1620 );
1621 };
1622 if !output_css.is_empty()
1623 && !output_css.ends_with('\n')
1624 && !transformed.output_css.is_empty()
1625 {
1626 output_css.push('\n');
1627 }
1628 let generated_start = output_css.len();
1629 output_css.push_str(&transformed.output_css);
1630 let generated_end = output_css.len();
1631 generated_region_by_instance
1632 .insert(module_instance.clone(), (generated_start, generated_end));
1633 module_regions.push(LinkedEmissionModuleRegionV0 {
1634 first_global_order_index: first_order_index_by_instance.get(&module_instance).copied(),
1635 module_instance,
1636 generated_start,
1637 generated_end,
1638 });
1639 }
1640
1641 let mut order_entry_regions = Vec::with_capacity(linked.global_rule_order.rules.len());
1642 for rule in &linked.global_rule_order.rules {
1643 let Some((generated_start, generated_end)) = generated_region_by_instance
1644 .get(&rule.module_instance)
1645 .copied()
1646 else {
1647 return Err(
1648 LinkedEmissionMaterializationErrorV0::MissingTransformedModule {
1649 module_instance: rule.module_instance.clone(),
1650 },
1651 );
1652 };
1653 order_entry_regions.push(LinkedEmissionOrderEntryRegionV0 {
1654 global_order_index: rule.global_order_index,
1655 module_instance: rule.module_instance.clone(),
1656 generated_start,
1657 generated_end,
1658 });
1659 }
1660
1661 Ok(LinkedEmissionArtifactV0 {
1662 schema_version: "0",
1663 product: "omena-transform-bundle.linked-emission",
1664 emitted_module_count: module_regions.len(),
1665 global_order_entry_count: order_entry_regions.len(),
1666 output_css,
1667 module_regions,
1668 order_entry_regions,
1669 })
1670}
1671
1672#[allow(deprecated)]
1673fn derive_facade_resolved_dependencies(
1674 inputs: &[LinkerInputV0],
1675 resolution_authority: BundleResolutionAuthorityV0,
1676) -> Result<Vec<TransformBundleResolvedDependencyV0>, TransformBundleLinkErrorV0> {
1677 if resolution_authority == BundleResolutionAuthorityV0::LegacyPathInferred {
1678 return Ok(Vec::new());
1679 }
1680 let instances_by_path = inputs.iter().fold(
1681 BTreeMap::<String, Vec<ModuleInstanceKeyV0>>::new(),
1682 |mut instances_by_path, input| {
1683 let instances = instances_by_path
1684 .entry(input.source_path.clone())
1685 .or_default();
1686 instances.push(input.instance.clone());
1687 instances_by_path
1688 },
1689 );
1690 let mut resolved_dependencies = Vec::new();
1691 for input in inputs {
1692 for edge in input
1693 .dependency_edges
1694 .iter()
1695 .filter(|edge| bundle_edge_is_module_dependency(edge.kind))
1696 {
1697 let target_instance = resolve_imported_module_instance(
1698 input.source_path.as_str(),
1699 edge.import_source.as_str(),
1700 &instances_by_path,
1701 )?;
1702 resolved_dependencies.push(TransformBundleResolvedDependencyV0::new(
1703 input.instance.clone(),
1704 edge.kind,
1705 edge.import_source.as_str(),
1706 edge.import_ordinal,
1707 TransformBundleDependencyResolutionV0::attempted(
1708 vec!["facadeModuleSet"],
1709 "facadeModuleSet",
1710 usize::from(target_instance.is_some()),
1711 target_instance,
1712 ),
1713 ));
1714 }
1715 }
1716 Ok(resolved_dependencies)
1717}
1718
1719pub fn link_stylesheet_from_projection(
1721 entrypoint_paths: &[&str],
1722 inputs: &[LinkerInputV0],
1723) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1724 link_stylesheet_from_projection_with_options(
1725 entrypoint_paths,
1726 inputs,
1727 TransformBundleLinkOptionsV0::default(),
1728 )
1729}
1730
1731pub fn link_stylesheet_from_projection_with_options(
1733 entrypoint_paths: &[&str],
1734 inputs: &[LinkerInputV0],
1735 options: TransformBundleLinkOptionsV0,
1736) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1737 let resolved_dependencies =
1738 derive_facade_resolved_dependencies(inputs, options.dependency_resolution_authority)?;
1739 link_stylesheet_from_projection_with_resolved_dependencies_and_options(
1740 entrypoint_paths,
1741 inputs,
1742 resolved_dependencies.as_slice(),
1743 options,
1744 )
1745}
1746
1747pub fn link_stylesheet_from_projection_with_resolved_dependencies_and_options(
1749 entrypoint_paths: &[&str],
1750 inputs: &[LinkerInputV0],
1751 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1752 options: TransformBundleLinkOptionsV0,
1753) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1754 link_stylesheet_from_projection_with_metadata_and_options(
1755 entrypoint_paths,
1756 inputs,
1757 resolved_dependencies,
1758 &[],
1759 &BTreeMap::new(),
1760 options,
1761 )
1762}
1763
1764fn link_stylesheet_from_projection_with_metadata_and_options(
1765 entrypoint_paths: &[&str],
1766 inputs: &[LinkerInputV0],
1767 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1768 module_metadata: &[ClosedWorldModuleMetadataV0],
1769 module_reachability_evidence: &BTreeMap<
1770 ModuleInstanceKeyV0,
1771 ClosedWorldModuleReachabilityEvidenceV0,
1772 >,
1773 options: TransformBundleLinkOptionsV0,
1774) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1775 let prepared = prepare_linked_stylesheet_context(
1776 entrypoint_paths,
1777 inputs,
1778 resolved_dependencies,
1779 module_metadata,
1780 module_reachability_evidence,
1781 options.dependency_resolution_authority,
1782 )?;
1783 link_stylesheet_from_prepared_context(prepared, inputs, resolved_dependencies, options)
1784}
1785
1786fn link_stylesheet_from_prepared_context(
1787 prepared: PreparedLinkedStylesheetContextV0,
1788 inputs: &[LinkerInputV0],
1789 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1790 options: TransformBundleLinkOptionsV0,
1791) -> Result<LinkedStylesheetV0, TransformBundleLinkErrorV0> {
1792 let (emission_plan, global_rule_order) = build_linked_stylesheet_order_from_prepared_context(
1793 &prepared,
1794 inputs,
1795 resolved_dependencies,
1796 options,
1797 )?;
1798 Ok(LinkedStylesheetV0 {
1799 schema_version: "0",
1800 product: "omena-transform-bundle.linked-stylesheet",
1801 entrypoints: prepared.entrypoints,
1802 module_instances: prepared.closed_world_bundle.linked_modules().to_vec(),
1803 emission_plan,
1804 global_rule_order,
1805 closed_world_bundle: prepared.closed_world_bundle,
1806 })
1807}
1808
1809fn build_linked_stylesheet_order_from_prepared_context(
1810 prepared: &PreparedLinkedStylesheetContextV0,
1811 inputs: &[LinkerInputV0],
1812 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1813 options: TransformBundleLinkOptionsV0,
1814) -> Result<(EmissionPlanV0, GlobalRuleOrderV0), TransformBundleLinkErrorV0> {
1815 let emission_plan = emission_order::build_emission_plan(
1816 inputs,
1817 prepared.closed_world_bundle.linked_modules(),
1818 &prepared.entrypoints,
1819 resolved_dependencies,
1820 options.emission_ordering_policy,
1821 options.dependency_resolution_authority,
1822 )?;
1823 let global_rule_order =
1824 emission_order::build_global_rule_order_from_plan(inputs, &emission_plan)?;
1825 Ok((emission_plan, global_rule_order))
1826}
1827
1828struct PreparedLinkedStylesheetContextV0 {
1829 entrypoints: Vec<ModuleInstanceKeyV0>,
1830 closed_world_bundle: ClosedWorldBundleV0,
1831 dependency_resolution_disclosures: Vec<BundleDependencyResolutionDisclosureV0>,
1832}
1833
1834fn prepare_linked_stylesheet_context(
1835 entrypoint_paths: &[&str],
1836 inputs: &[LinkerInputV0],
1837 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1838 module_metadata: &[ClosedWorldModuleMetadataV0],
1839 module_reachability_evidence: &BTreeMap<
1840 ModuleInstanceKeyV0,
1841 ClosedWorldModuleReachabilityEvidenceV0,
1842 >,
1843 resolution_authority: BundleResolutionAuthorityV0,
1844) -> Result<PreparedLinkedStylesheetContextV0, TransformBundleLinkErrorV0> {
1845 let instances_by_path = module_instances_by_linker_path(inputs);
1846 let entrypoints = entrypoint_paths
1847 .iter()
1848 .map(|path| {
1849 resolve_entrypoint_module_instance_by_path(path, &instances_by_path)?.ok_or_else(|| {
1850 TransformBundleLinkErrorV0::MissingEntrypoint {
1851 source_path: normalize_bundle_path(PathBuf::from(*path)),
1852 }
1853 })
1854 })
1855 .collect::<Result<Vec<_>, _>>()?;
1856 let (linked_modules, dependency_resolution_disclosures) =
1857 collect_closed_world_linked_modules_from_projection(
1858 inputs,
1859 resolved_dependencies,
1860 &instances_by_path,
1861 resolution_authority,
1862 )?;
1863 let module_metadata =
1864 module_metadata_with_reachability_evidence(module_metadata, module_reachability_evidence);
1865 let closed_world_bundle = ClosedWorldBundleV0::try_from_linked_modules_with_metadata(
1866 entrypoints.clone(),
1867 linked_modules,
1868 module_metadata,
1869 )
1870 .map_err(|error| TransformBundleLinkErrorV0::ClosedWorldBundle { error })?;
1871 Ok(PreparedLinkedStylesheetContextV0 {
1872 entrypoints,
1873 closed_world_bundle,
1874 dependency_resolution_disclosures,
1875 })
1876}
1877
1878fn link_stylesheet_from_projection_with_emission_items_and_metadata_and_options(
1879 entrypoint_paths: &[&str],
1880 linker_inputs: &[LinkerInputV0],
1881 emission_item_inputs: &[EmissionItemInputV0],
1882 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1883 module_metadata: &[ClosedWorldModuleMetadataV0],
1884 module_reachability_evidence: &BTreeMap<
1885 ModuleInstanceKeyV0,
1886 ClosedWorldModuleReachabilityEvidenceV0,
1887 >,
1888 options: TransformBundleLinkOptionsV0,
1889) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1890 let prepared = prepare_linked_stylesheet_context(
1891 entrypoint_paths,
1892 linker_inputs,
1893 resolved_dependencies,
1894 module_metadata,
1895 module_reachability_evidence,
1896 options.dependency_resolution_authority,
1897 )?;
1898 link_stylesheet_from_prepared_context_with_emission_items(
1899 prepared,
1900 linker_inputs,
1901 emission_item_inputs,
1902 resolved_dependencies,
1903 options,
1904 )
1905}
1906
1907fn evaluate_stylesheet_emission_admission_from_projection(
1908 entrypoint_paths: &[&str],
1909 linker_inputs: &[LinkerInputV0],
1910 emission_item_inputs: &[EmissionItemInputV0],
1911 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1912 module_metadata: &[ClosedWorldModuleMetadataV0],
1913 module_reachability_evidence: &BTreeMap<
1914 ModuleInstanceKeyV0,
1915 ClosedWorldModuleReachabilityEvidenceV0,
1916 >,
1917 options: TransformBundleLinkOptionsV0,
1918) -> TransformBundleEmissionAdmissionV0 {
1919 let prepared = match prepare_linked_stylesheet_context(
1920 entrypoint_paths,
1921 linker_inputs,
1922 resolved_dependencies,
1923 module_metadata,
1924 module_reachability_evidence,
1925 options.dependency_resolution_authority,
1926 ) {
1927 Ok(prepared) => prepared,
1928 Err(error) => {
1929 return TransformBundleEmissionAdmissionV0 {
1930 module_id_legacy_open: true,
1931 requested_policy_result: Err(error),
1932 };
1933 }
1934 };
1935 let module_id_legacy_open = build_linked_stylesheet_order_from_prepared_context(
1936 &prepared,
1937 linker_inputs,
1938 resolved_dependencies,
1939 TransformBundleLinkOptionsV0::default()
1940 .with_dependency_resolution_authority(options.dependency_resolution_authority),
1941 )
1942 .is_err();
1943 let requested_policy_result = link_stylesheet_from_prepared_context_with_emission_items(
1944 prepared,
1945 linker_inputs,
1946 emission_item_inputs,
1947 resolved_dependencies,
1948 options,
1949 );
1950 TransformBundleEmissionAdmissionV0 {
1951 module_id_legacy_open,
1952 requested_policy_result,
1953 }
1954}
1955
1956fn link_stylesheet_from_prepared_context_with_emission_items(
1957 prepared: PreparedLinkedStylesheetContextV0,
1958 linker_inputs: &[LinkerInputV0],
1959 emission_item_inputs: &[EmissionItemInputV0],
1960 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
1961 options: TransformBundleLinkOptionsV0,
1962) -> Result<LinkedStylesheetWithEmissionItemsV0, TransformBundleLinkErrorV0> {
1963 let module_plan = emission_order::build_emission_module_plan(
1964 linker_inputs,
1965 prepared.closed_world_bundle.linked_modules(),
1966 &prepared.entrypoints,
1967 resolved_dependencies,
1968 options.emission_ordering_policy,
1969 options.dependency_resolution_authority,
1970 )?;
1971 let emission_plan =
1972 emission_order::build_emission_plan_from_module_plan(linker_inputs, &module_plan)?;
1973 let legacy_global_rule_order =
1974 emission_order::build_global_rule_order_from_plan(linker_inputs, &emission_plan)?;
1975 let emission_item_plan =
1976 emission_items::build_emission_item_plan(emission_item_inputs, &module_plan)?;
1977 let emission_item_order = emission_items::build_linked_emission_item_order(
1978 emission_item_inputs,
1979 &emission_item_plan,
1980 )?;
1981 let global_rule_order =
1982 emission_items::build_global_rule_order_from_emission_items(&emission_item_order)?;
1983 if global_rule_order != legacy_global_rule_order {
1984 return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
1985 reason: "selector projection from emission items changed global rule order".to_string(),
1986 });
1987 }
1988 let projection_disclosures = emission_item_inputs
1989 .first()
1990 .map(|input| input.disclosure.clone())
1991 .unwrap_or_default();
1992 if emission_item_inputs
1993 .iter()
1994 .any(|input| input.disclosure != projection_disclosures)
1995 {
1996 return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
1997 reason: "emission-item projection disclosure differs between modules".to_string(),
1998 });
1999 }
2000 let linked_stylesheet = LinkedStylesheetV0 {
2001 schema_version: "0",
2002 product: "omena-transform-bundle.linked-stylesheet",
2003 entrypoints: prepared.entrypoints,
2004 module_instances: prepared.closed_world_bundle.linked_modules().to_vec(),
2005 emission_plan,
2006 global_rule_order,
2007 closed_world_bundle: prepared.closed_world_bundle,
2008 };
2009 Ok(LinkedStylesheetWithEmissionItemsV0 {
2010 linked_stylesheet,
2011 emission_item_plan,
2012 emission_item_order,
2013 projection_disclosures,
2014 dependency_resolution_disclosures: prepared.dependency_resolution_disclosures,
2015 })
2016}
2017
2018pub fn rewrite_omena_transform_bundle_asset_urls_in_source(
2019 source_path: impl Into<String>,
2020 source: &str,
2021) -> TransformBundleAssetUrlRewriteSummaryV0 {
2022 let source_path = source_path.into();
2023 let asset_urls = collect_transform_ir_bundle_asset_urls(
2024 &source_path,
2025 source,
2026 dialect_for_bundle_source_path(&source_path),
2027 );
2028 let mut output_css = source.to_string();
2029 let mut rewritten_asset_urls = Vec::new();
2030
2031 for asset in asset_urls.iter().rev() {
2032 let Some(resolved_path) = asset.resolved_path.as_deref() else {
2033 continue;
2034 };
2035 if !asset.bundler_resolution_required || asset.normalized_url == resolved_path {
2036 continue;
2037 }
2038 let range_start = asset.range_start as usize;
2039 let range_end = asset.range_end as usize;
2040 if range_start > range_end || range_end > output_css.len() {
2041 continue;
2042 }
2043 output_css.replace_range(range_start..range_end, &format!("url(\"{resolved_path}\")"));
2044 rewritten_asset_urls.push(asset.clone());
2045 }
2046
2047 rewritten_asset_urls.reverse();
2048 TransformBundleAssetUrlRewriteSummaryV0 {
2049 schema_version: "0",
2050 product: "omena-transform-bundle.asset-url-rewrite",
2051 source_path,
2052 asset_url_count: asset_urls.len(),
2053 rewrite_count: rewritten_asset_urls.len(),
2054 output_css,
2055 rewritten_asset_urls,
2056 }
2057}
2058
2059fn linker_input_from_module_facts(
2060 source_path: &str,
2061 dialect: StyleDialect,
2062 instance: ModuleInstanceKeyV0,
2063 facts: &ParsedStyleFacts,
2064 bundle_edges: &[TransformBundleEdgeV0],
2065) -> LinkerInputV0 {
2066 LinkerInputV0 {
2067 source_path: source_path.to_string(),
2068 dialect,
2069 instance,
2070 dependency_edges: bundle_edges
2071 .iter()
2072 .filter(|edge| bundle_edge_is_module_dependency(edge.kind))
2073 .filter_map(|edge| {
2074 edge.import_source
2075 .as_ref()
2076 .map(|import_source| LinkerDependencyEdgeV0 {
2077 kind: edge.kind,
2078 import_source: import_source.clone(),
2079 import_ordinal: edge.import_ordinal,
2080 local_names: edge.local_names.clone(),
2081 remote_names: edge.remote_names.clone(),
2082 })
2083 })
2084 .collect(),
2085 class_names: dedupe_names(
2086 facts
2087 .selectors
2088 .iter()
2089 .filter(|selector| selector.kind == ParsedSelectorFactKind::Class)
2090 .map(|selector| selector.name.clone()),
2091 ),
2092 keyframe_names: dedupe_names(
2093 facts
2094 .animations
2095 .iter()
2096 .filter(|animation| animation.kind == ParsedAnimationFactKind::KeyframesDeclaration)
2097 .map(|animation| animation.name.clone()),
2098 ),
2099 value_names: dedupe_names(
2100 facts
2101 .css_module_values
2102 .iter()
2103 .filter(|value| value.kind == ParsedCssModuleValueFactKind::Definition)
2104 .map(|value| value.name.clone()),
2105 ),
2106 custom_property_names: dedupe_custom_property_names(
2107 facts
2108 .variables
2109 .iter()
2110 .filter(|variable| {
2111 variable.kind == ParsedVariableFactKind::CustomPropertyDeclaration
2112 })
2113 .filter_map(|variable| variable.name.as_custom_property().cloned()),
2114 ),
2115 ordered_rules: collect_ordered_linker_rules(facts),
2116 }
2117}
2118
2119fn collect_ordered_linker_rules(facts: &ParsedStyleFacts) -> Vec<LinkerRuleV0> {
2120 let mut selectors = facts.selectors.clone();
2121 selectors.sort_by_key(|selector| {
2122 (
2123 u32::from(selector.range.start()),
2124 u32::from(selector.range.end()),
2125 selector.kind,
2126 selector.name.clone(),
2127 )
2128 });
2129 selectors
2130 .into_iter()
2131 .map(|selector| LinkerRuleV0 {
2132 selector_name: selector.name,
2133 selector_kind: selector.kind,
2134 range_start: u32::from(selector.range.start()),
2135 range_end: u32::from(selector.range.end()),
2136 })
2137 .collect()
2138}
2139
2140#[allow(deprecated)]
2141fn fan_out_path_reachability_to_instances(
2142 modules: &[TransformBundleParsedModuleInputV0],
2143 reachability_inputs: &[TransformBundleSemanticReachabilityInputV0],
2144) -> Vec<TransformBundleInstanceReachabilityInputV0> {
2145 let instances_by_path = modules.iter().fold(
2146 BTreeMap::<String, Vec<ModuleInstanceKeyV0>>::new(),
2147 |mut by_path, module| {
2148 by_path
2149 .entry(normalize_bundle_path(PathBuf::from(module.source_path())))
2150 .or_default()
2151 .extend(module.module_instance_keys());
2152 by_path
2153 },
2154 );
2155 let mut reachability_by_path =
2156 BTreeMap::<String, TransformBundleSemanticReachabilityInputV0>::new();
2157 for input in reachability_inputs {
2158 let normalized_path = normalize_bundle_path(PathBuf::from(&input.source_path));
2159 let merged = reachability_by_path
2160 .entry(normalized_path.clone())
2161 .or_insert_with(|| TransformBundleSemanticReachabilityInputV0::new(normalized_path));
2162 merged.analysis = merge_reachability_analysis(merged.analysis, input.analysis);
2163 merged.class_names.extend(input.class_names.iter().cloned());
2164 merged
2165 .keyframe_names
2166 .extend(input.keyframe_names.iter().cloned());
2167 merged.value_names.extend(input.value_names.iter().cloned());
2168 merged
2169 .custom_property_names
2170 .extend(input.custom_property_names.iter().cloned());
2171 merged.class_names = dedupe_names(merged.class_names.drain(..));
2172 merged.keyframe_names = dedupe_names(merged.keyframe_names.drain(..));
2173 merged.value_names = dedupe_names(merged.value_names.drain(..));
2174 merged.custom_property_names =
2175 dedupe_custom_property_names(merged.custom_property_names.drain(..));
2176 }
2177
2178 reachability_by_path
2179 .into_iter()
2180 .flat_map(|(path, reachability)| {
2181 instances_by_path
2182 .get(path.as_str())
2183 .into_iter()
2184 .flatten()
2185 .map(move |instance| {
2186 let mut input = TransformBundleInstanceReachabilityInputV0::new(
2187 instance.clone(),
2188 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
2189 );
2190 input.class_names.clone_from(&reachability.class_names);
2191 input
2192 .keyframe_names
2193 .clone_from(&reachability.keyframe_names);
2194 input.value_names.clone_from(&reachability.value_names);
2195 input
2196 .custom_property_names
2197 .clone_from(&reachability.custom_property_names);
2198 input.analysis = reachability.analysis;
2199 input
2200 })
2201 })
2202 .collect()
2203}
2204
2205fn apply_semantic_reachability_to_linker_inputs(
2206 inputs: &mut [LinkerInputV0],
2207 reachability_inputs: &[TransformBundleInstanceReachabilityInputV0],
2208) -> (
2209 BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleReachabilityEvidenceV0>,
2210 BTreeMap<ModuleInstanceKeyV0, InstanceReachabilityDerivationV0>,
2211 BTreeMap<ModuleInstanceKeyV0, TransformBundleReachabilityAnalysisV0>,
2212) {
2213 let (reachability_inputs, incomplete_composes_target_instances) =
2214 instance_reachability_inputs_closed_over_composes(inputs, reachability_inputs);
2215 let module_index_by_instance = inputs
2216 .iter()
2217 .enumerate()
2218 .map(|(index, input)| (input.instance.clone(), index))
2219 .collect::<BTreeMap<_, _>>();
2220 let mut evidence_by_instance = inputs
2221 .iter()
2222 .map(|input| {
2223 (
2224 input.instance.clone(),
2225 ClosedWorldModuleReachabilityEvidenceV0::ModuleReachabilityInputAbsent,
2226 )
2227 })
2228 .collect::<BTreeMap<_, _>>();
2229 let mut derivation_by_instance = BTreeMap::new();
2230 let mut analysis_by_instance = inputs
2231 .iter()
2232 .map(|input| {
2233 (
2234 input.instance.clone(),
2235 TransformBundleReachabilityAnalysisV0::default(),
2236 )
2237 })
2238 .collect::<BTreeMap<_, _>>();
2239
2240 for input in reachability_inputs.values() {
2241 let Some(index) = module_index_by_instance
2242 .get(&input.module_instance)
2243 .copied()
2244 else {
2245 continue;
2246 };
2247 derivation_by_instance.insert(input.module_instance.clone(), input.derivation);
2248 analysis_by_instance.insert(input.module_instance.clone(), input.analysis);
2249 if incomplete_composes_target_instances.contains(&input.module_instance) {
2250 analysis_by_instance.insert(
2253 input.module_instance.clone(),
2254 TransformBundleReachabilityAnalysisV0::Unanalyzed {
2255 cause: TransformBundleReachabilityUnanalyzedCauseV0::AnalysisResultUnavailable,
2256 },
2257 );
2258 continue;
2259 }
2260 if !input.analysis.is_analyzed() {
2261 continue;
2262 }
2263 evidence_by_instance.insert(
2264 input.module_instance.clone(),
2265 ClosedWorldModuleReachabilityEvidenceV0::Supplied,
2266 );
2267 inputs[index].class_names.clear();
2268 inputs[index]
2269 .class_names
2270 .extend(input.class_names.iter().cloned());
2271 inputs[index].class_names = dedupe_names(inputs[index].class_names.drain(..));
2272 inputs[index].keyframe_names.clear();
2273 inputs[index]
2274 .keyframe_names
2275 .extend(input.keyframe_names.iter().cloned());
2276 inputs[index].keyframe_names = dedupe_names(inputs[index].keyframe_names.drain(..));
2277 inputs[index].value_names.clear();
2278 inputs[index]
2279 .value_names
2280 .extend(input.value_names.iter().cloned());
2281 inputs[index].value_names = dedupe_names(inputs[index].value_names.drain(..));
2282 inputs[index].custom_property_names.clear();
2283 inputs[index]
2284 .custom_property_names
2285 .extend(input.custom_property_names.iter().cloned());
2286 inputs[index].custom_property_names =
2287 dedupe_custom_property_names(inputs[index].custom_property_names.drain(..));
2288 }
2289 (
2290 evidence_by_instance,
2291 derivation_by_instance,
2292 analysis_by_instance,
2293 )
2294}
2295
2296fn instance_reachability_inputs_closed_over_composes(
2297 inputs: &[LinkerInputV0],
2298 reachability_inputs: &[TransformBundleInstanceReachabilityInputV0],
2299) -> (
2300 BTreeMap<ModuleInstanceKeyV0, TransformBundleInstanceReachabilityInputV0>,
2301 BTreeSet<ModuleInstanceKeyV0>,
2302) {
2303 let instances_by_path = module_instances_by_linker_path(inputs);
2304 let mut by_instance =
2305 BTreeMap::<ModuleInstanceKeyV0, TransformBundleInstanceReachabilityInputV0>::new();
2306 let mut incomplete_composes_target_instances = BTreeSet::new();
2307 for input in reachability_inputs {
2308 let merged = by_instance
2309 .entry(input.module_instance.clone())
2310 .or_insert_with(|| {
2311 TransformBundleInstanceReachabilityInputV0::new(
2312 input.module_instance.clone(),
2313 input.derivation,
2314 )
2315 });
2316 if input.derivation == InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator {
2317 merged.derivation = InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator;
2318 }
2319 merged.analysis = merge_reachability_analysis(merged.analysis, input.analysis);
2320 merged.class_names.extend(input.class_names.iter().cloned());
2321 merged
2322 .keyframe_names
2323 .extend(input.keyframe_names.iter().cloned());
2324 merged.value_names.extend(input.value_names.iter().cloned());
2325 merged
2326 .custom_property_names
2327 .extend(input.custom_property_names.iter().cloned());
2328 merged.class_names = dedupe_names(merged.class_names.drain(..));
2329 merged.keyframe_names = dedupe_names(merged.keyframe_names.drain(..));
2330 merged.value_names = dedupe_names(merged.value_names.drain(..));
2331 merged.custom_property_names =
2332 dedupe_custom_property_names(merged.custom_property_names.drain(..));
2333 }
2334
2335 loop {
2336 let snapshot = by_instance.clone();
2337 let mut additions = BTreeMap::<ModuleInstanceKeyV0, Vec<String>>::new();
2338 for input in inputs {
2339 let Some(source_reachability) = snapshot.get(&input.instance) else {
2340 continue;
2341 };
2342 if !source_reachability.analysis.is_analyzed() {
2343 continue;
2344 }
2345 for edge in input
2346 .dependency_edges
2347 .iter()
2348 .filter(|edge| edge.kind == TransformBundleEdgeKind::CssModuleComposesExternal)
2349 {
2350 let target_path =
2351 import_path_candidates(input.source_path.as_str(), edge.import_source.as_str())
2352 .into_iter()
2353 .find(|candidate| instances_by_path.contains_key(candidate));
2354 let Some(target_path) = target_path else {
2355 continue;
2356 };
2357 let Some(target_instances) = instances_by_path.get(&target_path) else {
2358 continue;
2359 };
2360 if edge.local_names.is_empty() || edge.remote_names.is_empty() {
2361 incomplete_composes_target_instances.extend(target_instances.iter().cloned());
2362 continue;
2363 }
2364 for local_name in &edge.local_names {
2365 if !source_reachability
2366 .class_names
2367 .iter()
2368 .any(|reachable| reachable == local_name)
2369 {
2370 continue;
2371 }
2372 for remote_name in &edge.remote_names {
2373 for target_instance in target_instances {
2374 additions
2375 .entry(target_instance.clone())
2376 .or_default()
2377 .push(remote_name.clone());
2378 }
2379 }
2380 }
2381 }
2382 }
2383 let mut changed = false;
2384 for (target_instance, class_names) in additions {
2385 let target = by_instance
2386 .entry(target_instance.clone())
2387 .or_insert_with(|| {
2388 TransformBundleInstanceReachabilityInputV0::new(
2389 target_instance,
2390 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
2391 )
2392 });
2393 target.derivation = InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator;
2394 let before = target.class_names.len();
2395 target.class_names.extend(class_names);
2396 target.class_names = dedupe_names(target.class_names.drain(..));
2397 changed |= target.class_names.len() != before;
2398 }
2399 if !changed {
2400 break;
2401 }
2402 }
2403 (by_instance, incomplete_composes_target_instances)
2404}
2405
2406fn merge_reachability_analysis(
2407 current: TransformBundleReachabilityAnalysisV0,
2408 incoming: TransformBundleReachabilityAnalysisV0,
2409) -> TransformBundleReachabilityAnalysisV0 {
2410 match (current, incoming) {
2411 (TransformBundleReachabilityAnalysisV0::Analyzed, next) => next,
2412 (unavailable @ TransformBundleReachabilityAnalysisV0::Unanalyzed { .. }, _) => unavailable,
2413 }
2414}
2415
2416fn module_metadata_with_reachability_evidence(
2417 module_metadata: &[ClosedWorldModuleMetadataV0],
2418 module_reachability_evidence: &BTreeMap<
2419 ModuleInstanceKeyV0,
2420 ClosedWorldModuleReachabilityEvidenceV0,
2421 >,
2422) -> Vec<ClosedWorldModuleMetadataV0> {
2423 let mut metadata_by_instance = module_metadata
2424 .iter()
2425 .cloned()
2426 .map(|metadata| (metadata.module_instance().clone(), metadata))
2427 .collect::<BTreeMap<_, _>>();
2428 for (module_instance, reachability_evidence) in module_reachability_evidence {
2429 metadata_by_instance
2430 .entry(module_instance.clone())
2431 .and_modify(|metadata| {
2432 *metadata = metadata
2433 .clone()
2434 .with_reachability_evidence(*reachability_evidence);
2435 })
2436 .or_insert_with(|| {
2437 ClosedWorldModuleMetadataV0::new(module_instance.clone())
2438 .with_reachability_evidence(*reachability_evidence)
2439 });
2440 }
2441 metadata_by_instance.into_values().collect()
2442}
2443
2444pub(crate) fn module_instances_by_linker_path(
2445 inputs: &[LinkerInputV0],
2446) -> BTreeMap<String, Vec<ModuleInstanceKeyV0>> {
2447 let mut by_path = BTreeMap::<String, Vec<ModuleInstanceKeyV0>>::new();
2448 for input in inputs {
2449 by_path
2450 .entry(input.source_path.clone())
2451 .or_default()
2452 .push(input.instance.clone());
2453 }
2454 for instances in by_path.values_mut() {
2455 instances.sort();
2456 instances.dedup();
2457 }
2458 by_path
2459}
2460
2461fn resolve_entrypoint_module_instance_by_path(
2462 source_path: &str,
2463 instances_by_path: &BTreeMap<String, Vec<ModuleInstanceKeyV0>>,
2464) -> Result<Option<ModuleInstanceKeyV0>, TransformBundleLinkErrorV0> {
2465 let normalized = normalize_bundle_path(PathBuf::from(source_path));
2466 let Some(instances) = instances_by_path.get(&normalized) else {
2467 return Ok(None);
2468 };
2469 match instances.as_slice() {
2470 [instance] => Ok(Some(instance.clone())),
2471 instances => {
2472 let unconfigured = omena_parser::ConfigurationHashV0::none();
2473 let mut matches = instances
2474 .iter()
2475 .filter(|instance| instance.configuration() == &unconfigured);
2476 let selected = matches.next().cloned();
2477 if selected.is_some() && matches.next().is_none() {
2478 Ok(selected)
2479 } else {
2480 Err(TransformBundleLinkErrorV0::AmbiguousModulePath {
2481 source_path: normalized,
2482 })
2483 }
2484 }
2485 }
2486}
2487
2488fn collect_closed_world_linked_modules_from_projection(
2489 inputs: &[LinkerInputV0],
2490 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
2491 instances_by_path: &BTreeMap<String, Vec<ModuleInstanceKeyV0>>,
2492 resolution_authority: BundleResolutionAuthorityV0,
2493) -> Result<
2494 (
2495 Vec<ClosedWorldLinkedModuleV0>,
2496 Vec<BundleDependencyResolutionDisclosureV0>,
2497 ),
2498 TransformBundleLinkErrorV0,
2499> {
2500 let linked_with_disclosures = inputs
2501 .iter()
2502 .map(|input| {
2503 let mut linked = ClosedWorldLinkedModuleV0::new(input.instance.clone());
2504 let mut disclosures = Vec::new();
2505 for edge in &input.dependency_edges {
2506 let resolution = resolve_imported_module_instance_for_edge(
2507 input,
2508 edge,
2509 resolved_dependencies,
2510 instances_by_path,
2511 resolution_authority,
2512 )?;
2513 let dependency = resolution.target_instance.clone().ok_or_else(|| {
2514 TransformBundleLinkErrorV0::MissingDependency {
2515 source_path: input.source_path.clone(),
2516 import_source: edge.import_source.clone(),
2517 }
2518 })?;
2519 disclosures.push(BundleDependencyResolutionDisclosureV0 {
2520 source_instance: input.instance.clone(),
2521 import_source: edge.import_source.clone(),
2522 import_ordinal: edge.import_ordinal,
2523 authority: resolution.authority,
2524 });
2525 if edge.kind == TransformBundleEdgeKind::CssModuleComposesExternal {
2526 for local_name in &edge.local_names {
2527 for remote_name in &edge.remote_names {
2528 linked = linked.with_composes_edge(ClosedWorldComposesEdgeV0 {
2529 from_module: input.instance.clone(),
2530 from_symbol: local_name.clone(),
2531 to_module: dependency.clone(),
2532 to_symbol: remote_name.clone(),
2533 });
2534 }
2535 }
2536 }
2537 linked = linked.with_dependency(dependency);
2538 }
2539 for name in dedupe_names(input.class_names.iter().cloned()) {
2540 linked = linked.with_class_name(name);
2541 }
2542 for name in dedupe_names(input.keyframe_names.iter().cloned()) {
2543 linked = linked.with_keyframe_name(name);
2544 }
2545 for name in dedupe_names(input.value_names.iter().cloned()) {
2546 linked = linked.with_value_name(name);
2547 }
2548 for name in dedupe_custom_property_names(input.custom_property_names.iter().cloned()) {
2549 linked = linked.with_custom_property_name(name);
2550 }
2551 linked.dependencies.sort();
2552 linked.dependencies.dedup();
2553 linked.composes_edges.sort_by(|left, right| {
2554 (
2555 &left.from_module,
2556 &left.from_symbol,
2557 &left.to_module,
2558 &left.to_symbol,
2559 )
2560 .cmp(&(
2561 &right.from_module,
2562 &right.from_symbol,
2563 &right.to_module,
2564 &right.to_symbol,
2565 ))
2566 });
2567 linked.composes_edges.dedup();
2568 linked.composes_edge_observation_count = linked.composes_edges.len();
2569 Ok((linked, disclosures))
2570 })
2571 .collect::<Result<Vec<_>, TransformBundleLinkErrorV0>>()?;
2572 let (linked_modules, disclosure_groups): (Vec<_>, Vec<_>) =
2573 linked_with_disclosures.into_iter().unzip();
2574 let mut disclosures = disclosure_groups.into_iter().flatten().collect::<Vec<_>>();
2575 disclosures.sort();
2576 disclosures.dedup();
2577 Ok((linked_modules, disclosures))
2578}
2579
2580const fn bundle_edge_module_dependency_reason(
2581 kind: TransformBundleEdgeKind,
2582) -> Option<&'static str> {
2583 match kind {
2584 TransformBundleEdgeKind::SassUse => Some("loads a Sass module instance"),
2585 TransformBundleEdgeKind::SassForward => Some("forwards a Sass module instance"),
2586 TransformBundleEdgeKind::SassImport => Some("loads Sass stylesheet rules"),
2587 TransformBundleEdgeKind::CssImport => Some("loads CSS stylesheet rules"),
2588 TransformBundleEdgeKind::LessImport => Some("loads Less stylesheet rules"),
2589 TransformBundleEdgeKind::CssModuleValueImport => Some("loads CSS Modules values"),
2590 TransformBundleEdgeKind::CssModuleComposesExternal => {
2591 Some("loads selectors from an external CSS Module")
2592 }
2593 TransformBundleEdgeKind::IcssImport => Some("loads ICSS values"),
2594 TransformBundleEdgeKind::CssModuleComposesLocal => None,
2595 }
2596}
2597
2598pub const fn bundle_edge_is_module_dependency(kind: TransformBundleEdgeKind) -> bool {
2603 bundle_edge_module_dependency_reason(kind).is_some()
2604}
2605
2606pub(crate) fn resolve_imported_module_instance(
2607 source_path: &str,
2608 import_source: &str,
2609 instances_by_path: &BTreeMap<String, Vec<ModuleInstanceKeyV0>>,
2610) -> Result<Option<ModuleInstanceKeyV0>, TransformBundleLinkErrorV0> {
2611 for candidate in import_path_candidates(source_path, import_source) {
2612 if let Some(instances) = instances_by_path.get(candidate.as_str()) {
2613 return match instances.as_slice() {
2614 [instance] => Ok(Some(instance.clone())),
2615 _ => Err(TransformBundleLinkErrorV0::AmbiguousModulePath {
2616 source_path: candidate,
2617 }),
2618 };
2619 }
2620 }
2621 Ok(None)
2622}
2623
2624pub(crate) struct DependencyResolutionOutcomeV0 {
2625 pub(crate) target_instance: Option<ModuleInstanceKeyV0>,
2626 pub(crate) authority: BundleResolutionAuthorityV0,
2627}
2628
2629#[allow(deprecated)]
2630pub(crate) fn resolve_imported_module_instance_for_edge(
2631 input: &LinkerInputV0,
2632 edge: &LinkerDependencyEdgeV0,
2633 resolved_dependencies: &[TransformBundleResolvedDependencyV0],
2634 instances_by_path: &BTreeMap<String, Vec<ModuleInstanceKeyV0>>,
2635 resolution_authority: BundleResolutionAuthorityV0,
2636) -> Result<DependencyResolutionOutcomeV0, TransformBundleLinkErrorV0> {
2637 if !bundle_edge_is_module_dependency(edge.kind) {
2638 return Ok(DependencyResolutionOutcomeV0 {
2639 target_instance: resolve_imported_module_instance(
2640 input.source_path.as_str(),
2641 edge.import_source.as_str(),
2642 instances_by_path,
2643 )?,
2644 authority: BundleResolutionAuthorityV0::Resolved,
2645 });
2646 }
2647 let mut matches = resolved_dependencies.iter().filter(|dependency| {
2648 dependency.source_instance == input.instance
2649 && dependency.edge_kind == edge.kind
2650 && dependency.import_source == edge.import_source
2651 && dependency.import_ordinal == edge.import_ordinal
2652 });
2653 if let Some(resolved) = matches.next() {
2654 if matches.next().is_some() {
2655 return Err(TransformBundleLinkErrorV0::InvalidEmissionPlan {
2656 reason: format!(
2657 "dependency {} in {} has more than one resolved-edge record",
2658 edge.import_source,
2659 input.instance.module().as_str()
2660 ),
2661 });
2662 }
2663 return Ok(DependencyResolutionOutcomeV0 {
2664 target_instance: resolved
2665 .resolution
2666 .target_instance
2667 .as_ref()
2668 .and_then(|target| {
2669 instances_by_path
2670 .get(target.module().as_str())
2671 .filter(|instances| instances.contains(target))
2672 .map(|_| target.clone())
2673 }),
2674 authority: BundleResolutionAuthorityV0::Resolved,
2675 });
2676 }
2677 if resolution_authority == BundleResolutionAuthorityV0::Resolved {
2678 return Err(TransformBundleLinkErrorV0::UnresolvedDependencyEdge {
2679 source_path: input.source_path.clone(),
2680 import_source: edge.import_source.clone(),
2681 import_ordinal: edge.import_ordinal,
2682 });
2683 }
2684 Ok(DependencyResolutionOutcomeV0 {
2685 target_instance: resolve_imported_module_instance(
2686 input.source_path.as_str(),
2687 edge.import_source.as_str(),
2688 instances_by_path,
2689 )?,
2690 authority: BundleResolutionAuthorityV0::LegacyPathInferred,
2691 })
2692}
2693
2694fn import_path_candidates(source_path: &str, import_source: &str) -> Vec<String> {
2695 let base = if import_source.starts_with('/') {
2696 PathBuf::from(import_source)
2697 } else {
2698 Path::new(source_path)
2699 .parent()
2700 .unwrap_or_else(|| Path::new(""))
2701 .join(import_source)
2702 };
2703 let normalized = normalize_bundle_path(base);
2704 let mut candidates = vec![normalized.clone()];
2705 if Path::new(&normalized).extension().is_none() {
2706 for extension in ["css", "scss", "sass", "less"] {
2707 candidates.push(format!("{normalized}.{extension}"));
2708 }
2709 let path = Path::new(&normalized);
2710 if let Some(file_name) = path.file_name().and_then(|name| name.to_str()) {
2711 let mut partial = path.parent().unwrap_or_else(|| Path::new("")).to_path_buf();
2712 partial.push(format!("_{file_name}"));
2713 let partial = normalize_bundle_path(partial);
2714 for extension in ["scss", "sass"] {
2715 candidates.push(format!("{partial}.{extension}"));
2716 }
2717 }
2718 }
2719 candidates.sort();
2720 candidates.dedup();
2721 candidates
2722}
2723
2724pub(crate) fn selector_kind_label(kind: ParsedSelectorFactKind) -> &'static str {
2725 match kind {
2726 ParsedSelectorFactKind::Class => "class",
2727 ParsedSelectorFactKind::Id => "id",
2728 ParsedSelectorFactKind::Placeholder => "placeholder",
2729 }
2730}
2731
2732fn serialize_selector_fact_kind<S>(
2733 kind: &ParsedSelectorFactKind,
2734 serializer: S,
2735) -> Result<S::Ok, S::Error>
2736where
2737 S: serde::Serializer,
2738{
2739 serializer.serialize_str(selector_kind_label(*kind))
2740}
2741
2742fn serialize_style_dialect<S>(dialect: &StyleDialect, serializer: S) -> Result<S::Ok, S::Error>
2743where
2744 S: serde::Serializer,
2745{
2746 serializer.serialize_str(dialect_label(*dialect))
2747}
2748
2749fn dedupe_names(names: impl IntoIterator<Item = String>) -> Vec<String> {
2750 names
2751 .into_iter()
2752 .collect::<BTreeSet<_>>()
2753 .into_iter()
2754 .collect()
2755}
2756
2757fn dedupe_custom_property_names(
2758 names: impl IntoIterator<Item = AuthoredPropertyTextV0>,
2759) -> Vec<AuthoredPropertyTextV0> {
2760 let mut by_identity = BTreeMap::<CanonicalCustomPropertyNameV0, AuthoredPropertyTextV0>::new();
2761 for authored in names {
2762 by_identity
2763 .entry(authored.to_custom_key())
2764 .or_insert(authored);
2765 }
2766 by_identity.into_values().collect()
2767}
2768
2769fn authored_custom_property_sequences_same(
2770 left: &[AuthoredPropertyTextV0],
2771 right: &[AuthoredPropertyTextV0],
2772) -> bool {
2773 left.len() == right.len()
2774 && left
2775 .iter()
2776 .zip(right)
2777 .all(|(left, right)| left.to_custom_key() == right.to_custom_key())
2778}
2779
2780fn collect_bundle_edges_from_facts(
2781 source_path: &str,
2782 dialect: StyleDialect,
2783 facts: &omena_parser::ParsedStyleFacts,
2784) -> Vec<TransformBundleEdgeV0> {
2785 let mut edges = Vec::new();
2786
2787 for edge in &facts.sass_module_edges {
2788 let kind = match edge.kind {
2789 ParsedSassModuleEdgeFactKind::Use => TransformBundleEdgeKind::SassUse,
2790 ParsedSassModuleEdgeFactKind::Forward => TransformBundleEdgeKind::SassForward,
2791 ParsedSassModuleEdgeFactKind::Import => import_edge_kind_for_dialect(dialect),
2792 };
2793 edges.push(TransformBundleEdgeV0 {
2794 kind,
2795 source_path: source_path.to_string(),
2796 import_source: Some(edge.source.clone()),
2797 import_ordinal: None,
2798 namespace: edge.namespace.clone(),
2799 local_names: Vec::new(),
2800 remote_names: Vec::new(),
2801 range_start: u32::from(edge.range.start()),
2802 range_end: u32::from(edge.range.end()),
2803 provenance_required: true,
2804 });
2805 }
2806
2807 for edge in &facts.css_module_value_import_edges {
2808 edges.push(TransformBundleEdgeV0 {
2809 kind: TransformBundleEdgeKind::CssModuleValueImport,
2810 source_path: source_path.to_string(),
2811 import_source: Some(edge.import_source.clone()),
2812 import_ordinal: None,
2813 namespace: None,
2814 local_names: vec![edge.local_name.clone()],
2815 remote_names: vec![edge.remote_name.clone()],
2816 range_start: u32::from(edge.range.start()),
2817 range_end: u32::from(edge.range.end()),
2818 provenance_required: true,
2819 });
2820 }
2821
2822 for edge in &facts.css_module_composes_edges {
2823 let kind = match edge.kind {
2824 ParsedCssModuleComposesEdgeKind::External => {
2825 TransformBundleEdgeKind::CssModuleComposesExternal
2826 }
2827 ParsedCssModuleComposesEdgeKind::Local | ParsedCssModuleComposesEdgeKind::Global => {
2828 TransformBundleEdgeKind::CssModuleComposesLocal
2829 }
2830 };
2831 edges.push(TransformBundleEdgeV0 {
2832 kind,
2833 source_path: source_path.to_string(),
2834 import_source: edge.import_source.clone(),
2835 import_ordinal: None,
2836 namespace: None,
2837 local_names: edge.owner_selector_names.clone(),
2838 remote_names: edge.target_names.clone(),
2839 range_start: u32::from(edge.range.start()),
2840 range_end: u32::from(edge.range.end()),
2841 provenance_required: true,
2842 });
2843 }
2844
2845 for edge in &facts.icss_import_edges {
2846 edges.push(TransformBundleEdgeV0 {
2847 kind: TransformBundleEdgeKind::IcssImport,
2848 source_path: source_path.to_string(),
2849 import_source: Some(edge.import_source.clone()),
2850 import_ordinal: None,
2851 namespace: None,
2852 local_names: vec![edge.local_name.clone()],
2853 remote_names: vec![edge.remote_name.clone()],
2854 range_start: u32::from(edge.range.start()),
2855 range_end: u32::from(edge.range.end()),
2856 provenance_required: true,
2857 });
2858 }
2859
2860 assign_parser_origin_import_ordinals(&mut edges);
2861 edges
2862}
2863
2864fn assign_parser_origin_import_ordinals(edges: &mut [TransformBundleEdgeV0]) {
2865 let mut order_bearing_indices = edges
2866 .iter()
2867 .enumerate()
2868 .filter(|(_, edge)| {
2869 edge.import_source.is_some()
2870 && edge.kind.order_relevance() == EdgeOrderRelevanceV0::OrderBearing
2871 })
2872 .map(|(index, edge)| (index, edge.range_start, edge.range_end))
2873 .collect::<Vec<_>>();
2874 order_bearing_indices
2875 .sort_by_key(|(index, range_start, range_end)| (*range_start, *range_end, *index));
2876 for (ordinal, (index, _, _)) in order_bearing_indices.into_iter().enumerate() {
2877 edges[index].import_ordinal = u32::try_from(ordinal).ok();
2878 }
2879}
2880
2881fn import_edge_kind_for_dialect(dialect: StyleDialect) -> TransformBundleEdgeKind {
2882 match dialect {
2883 StyleDialect::Css => TransformBundleEdgeKind::CssImport,
2884 StyleDialect::Less => TransformBundleEdgeKind::LessImport,
2885 StyleDialect::Scss | StyleDialect::Sass => TransformBundleEdgeKind::SassImport,
2886 }
2887}
2888
2889fn collect_transform_ir_bundle_asset_urls(
2890 source_path: &str,
2891 source: &str,
2892 dialect: StyleDialect,
2893) -> Vec<TransformBundleAssetUrlV0> {
2894 let ir = lower_transform_ir_from_source(source, dialect, source_path);
2895 ir.nodes
2896 .iter()
2897 .filter(|node| !node.deleted && node.kind == IrNodeKindV0::UrlValue)
2898 .filter_map(|url_value| {
2899 let start = url_value.source_span_start;
2900 let end = url_value.source_span_end;
2901 if start >= end
2902 || end > source.len()
2903 || !source.is_char_boundary(start)
2904 || !source.is_char_boundary(end)
2905 {
2906 return None;
2907 }
2908 let (raw_url, normalized_url, parsed_end) = parse_bundle_url_function(source, start)?;
2909 if parsed_end != end {
2910 return None;
2911 }
2912 let (kind, resolved_path) = classify_bundle_asset_url(source_path, &normalized_url);
2913 Some(TransformBundleAssetUrlV0 {
2914 source_path: source_path.to_string(),
2915 raw_url,
2916 normalized_url,
2917 kind,
2918 resolved_path,
2919 range_start: start as u32,
2920 range_end: parsed_end as u32,
2921 bundler_resolution_required: matches!(
2922 kind,
2923 TransformBundleAssetUrlKind::Relative
2924 | TransformBundleAssetUrlKind::AbsolutePath
2925 ),
2926 })
2927 })
2928 .collect()
2929}
2930
2931#[cfg(test)]
2932fn raw_scan_bundle_asset_urls_for_oracle(
2933 source_path: &str,
2934 source: &str,
2935) -> Vec<TransformBundleAssetUrlV0> {
2936 let bytes = source.as_bytes();
2937 let mut urls = Vec::new();
2938 let mut index = 0usize;
2939
2940 while index + 4 <= bytes.len() {
2941 if !bytes[index].eq_ignore_ascii_case(&b'u')
2942 || !bytes[index + 1].eq_ignore_ascii_case(&b'r')
2943 || !bytes[index + 2].eq_ignore_ascii_case(&b'l')
2944 || bytes[index + 3] != b'('
2945 {
2946 index += 1;
2947 continue;
2948 }
2949 let Some((raw_url, normalized_url, end)) = parse_bundle_url_function(source, index) else {
2950 index += 4;
2951 continue;
2952 };
2953 let (kind, resolved_path) = classify_bundle_asset_url(source_path, &normalized_url);
2954 urls.push(TransformBundleAssetUrlV0 {
2955 source_path: source_path.to_string(),
2956 raw_url,
2957 normalized_url,
2958 kind,
2959 resolved_path,
2960 range_start: index as u32,
2961 range_end: end as u32,
2962 bundler_resolution_required: matches!(
2963 kind,
2964 TransformBundleAssetUrlKind::Relative | TransformBundleAssetUrlKind::AbsolutePath
2965 ),
2966 });
2967 index = end;
2968 }
2969
2970 urls
2971}
2972
2973fn dialect_for_bundle_source_path(source_path: &str) -> StyleDialect {
2974 let extension = Path::new(source_path)
2975 .extension()
2976 .and_then(|extension| extension.to_str())
2977 .unwrap_or_default()
2978 .to_ascii_lowercase();
2979 match extension.as_str() {
2980 "scss" => StyleDialect::Scss,
2981 "sass" => StyleDialect::Sass,
2982 "less" => StyleDialect::Less,
2983 _ => StyleDialect::Css,
2984 }
2985}
2986
2987fn parse_bundle_url_function(source: &str, start: usize) -> Option<(String, String, usize)> {
2988 let open_end = start.checked_add(4)?;
2989 let mut index = open_end;
2990 let mut quote = None;
2991 let mut escaped = false;
2992
2993 while index < source.len() {
2994 let ch = source[index..].chars().next()?;
2995 let next = index + ch.len_utf8();
2996 if let Some(active_quote) = quote {
2997 if escaped {
2998 escaped = false;
2999 } else if ch == '\\' {
3000 escaped = true;
3001 } else if ch == active_quote {
3002 quote = None;
3003 }
3004 index = next;
3005 continue;
3006 }
3007
3008 match ch {
3009 '"' | '\'' => quote = Some(ch),
3010 ')' => {
3011 let raw_url = source[start..next].to_string();
3012 let inner = source[open_end..index].trim();
3013 let normalized_url = unquote_bundle_url_inner(inner)?;
3014 return Some((raw_url, normalized_url, next));
3015 }
3016 _ => {}
3017 }
3018 index = next;
3019 }
3020
3021 None
3022}
3023
3024fn unquote_bundle_url_inner(inner: &str) -> Option<String> {
3025 if inner.is_empty() {
3026 return None;
3027 }
3028 let bytes = inner.as_bytes();
3029 if bytes.len() >= 2
3030 && ((bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
3031 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\''))
3032 {
3033 return Some(inner[1..inner.len() - 1].to_string());
3034 }
3035 Some(inner.to_string())
3036}
3037
3038fn classify_bundle_asset_url(
3039 source_path: &str,
3040 normalized_url: &str,
3041) -> (TransformBundleAssetUrlKind, Option<String>) {
3042 let lower = normalized_url.to_ascii_lowercase();
3043 if lower.starts_with("data:") {
3044 return (TransformBundleAssetUrlKind::Data, None);
3045 }
3046 if normalized_url.starts_with('#') {
3047 return (TransformBundleAssetUrlKind::Fragment, None);
3048 }
3049 if lower.starts_with("http://")
3050 || lower.starts_with("https://")
3051 || normalized_url.starts_with("//")
3052 {
3053 return (TransformBundleAssetUrlKind::External, None);
3054 }
3055 if normalized_url.starts_with('/') {
3056 return (
3057 TransformBundleAssetUrlKind::AbsolutePath,
3058 Some(normalized_url.to_string()),
3059 );
3060 }
3061
3062 (
3063 TransformBundleAssetUrlKind::Relative,
3064 Some(resolve_relative_bundle_asset_path(
3065 source_path,
3066 normalized_url,
3067 )),
3068 )
3069}
3070
3071fn resolve_relative_bundle_asset_path(source_path: &str, normalized_url: &str) -> String {
3072 let base = Path::new(source_path)
3073 .parent()
3074 .unwrap_or_else(|| Path::new(""));
3075 normalize_bundle_path(base.join(normalized_url))
3076}
3077
3078pub fn normalize_omena_transform_bundle_path(path: &str) -> String {
3079 normalize_bundle_path(PathBuf::from(path.replace('\\', "/"))).replace('\\', "/")
3080}
3081
3082fn normalize_bundle_path(path: PathBuf) -> String {
3083 let mut normalized = PathBuf::new();
3084 for component in path.components() {
3085 match component {
3086 Component::CurDir => {}
3087 Component::ParentDir => match normalized.components().next_back() {
3088 Some(Component::Normal(_)) => {
3089 normalized.pop();
3090 }
3091 Some(Component::RootDir) => {}
3092 _ => normalized.push(".."),
3093 },
3094 _ => normalized.push(component.as_os_str()),
3095 }
3096 }
3097 normalized.to_string_lossy().into_owned()
3098}
3099
3100fn plan_bundle_code_split_chunks(
3101 source_path: &str,
3102 bundle_edges: &[TransformBundleEdgeV0],
3103 asset_urls: &[TransformBundleAssetUrlV0],
3104) -> Vec<TransformBundleChunkV0> {
3105 let mut chunks: Vec<TransformBundleChunkV0> = Vec::new();
3106 let mut entry_dependencies = Vec::new();
3107
3108 for edge in bundle_edges {
3109 let Some(import_source) = edge.import_source.as_ref() else {
3110 continue;
3111 };
3112 let chunk_id = bundle_chunk_id("style", source_path, import_source);
3113 if !entry_dependencies.contains(&chunk_id) {
3114 entry_dependencies.push(chunk_id.clone());
3115 }
3116 if chunks.iter().any(|chunk| chunk.chunk_id == chunk_id) {
3117 continue;
3118 }
3119 chunks.push(TransformBundleChunkV0 {
3120 chunk_id,
3121 kind: TransformBundleChunkKind::StyleImport,
3122 source_path: source_path.to_string(),
3123 import_source: Some(import_source.clone()),
3124 asset_url: None,
3125 resolved_path: None,
3126 depends_on: Vec::new(),
3127 split_boundary: "styleDependency",
3128 });
3129 }
3130
3131 for asset in asset_urls {
3132 if !asset.bundler_resolution_required {
3133 continue;
3134 }
3135 let chunk_id = bundle_chunk_id("asset", source_path, asset.normalized_url.as_str());
3136 if !entry_dependencies.contains(&chunk_id) {
3137 entry_dependencies.push(chunk_id.clone());
3138 }
3139 if chunks.iter().any(|chunk| chunk.chunk_id == chunk_id) {
3140 continue;
3141 }
3142 chunks.push(TransformBundleChunkV0 {
3143 chunk_id,
3144 kind: TransformBundleChunkKind::Asset,
3145 source_path: source_path.to_string(),
3146 import_source: None,
3147 asset_url: Some(asset.normalized_url.clone()),
3148 resolved_path: asset.resolved_path.clone(),
3149 depends_on: Vec::new(),
3150 split_boundary: "assetDependency",
3151 });
3152 }
3153
3154 entry_dependencies.sort();
3155 chunks.sort_by(|left, right| left.chunk_id.cmp(&right.chunk_id));
3156 let mut ordered = vec![TransformBundleChunkV0 {
3157 chunk_id: bundle_chunk_id("entry", source_path, source_path),
3158 kind: TransformBundleChunkKind::Entry,
3159 source_path: source_path.to_string(),
3160 import_source: None,
3161 asset_url: None,
3162 resolved_path: Some(source_path.to_string()),
3163 depends_on: entry_dependencies,
3164 split_boundary: "entry",
3165 }];
3166 ordered.extend(chunks);
3167 ordered
3168}
3169
3170fn bundle_chunk_id(kind: &str, source_path: &str, target: &str) -> String {
3171 format!(
3172 "{kind}:{}:{}",
3173 sanitize_bundle_chunk_id_part(source_path),
3174 sanitize_bundle_chunk_id_part(target)
3175 )
3176}
3177
3178fn sanitize_bundle_chunk_id_part(value: &str) -> String {
3179 let mut sanitized = String::with_capacity(value.len());
3180 for ch in value.chars() {
3181 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
3182 sanitized.push(ch);
3183 } else {
3184 sanitized.push('-');
3185 }
3186 }
3187 sanitized.trim_matches('-').to_string()
3188}
3189
3190fn required_passes_for_source(
3191 source_path: &str,
3192 dialect: StyleDialect,
3193 facts: &omena_parser::ParsedStyleFacts,
3194 bundle_edges: &[TransformBundleEdgeV0],
3195) -> Vec<TransformPassKind> {
3196 let mut passes = Vec::new();
3197
3198 if bundle_edges.iter().any(|edge| {
3199 matches!(
3200 edge.kind,
3201 TransformBundleEdgeKind::SassImport
3202 | TransformBundleEdgeKind::CssImport
3203 | TransformBundleEdgeKind::LessImport
3204 | TransformBundleEdgeKind::CssModuleValueImport
3205 | TransformBundleEdgeKind::CssModuleComposesExternal
3206 | TransformBundleEdgeKind::IcssImport
3207 )
3208 }) {
3209 passes.push(TransformPassKind::ImportInline);
3210 }
3211
3212 if matches!(dialect, StyleDialect::Scss | StyleDialect::Sass) {
3213 passes.push(TransformPassKind::ScssModuleEvaluate);
3214 }
3215
3216 if matches!(dialect, StyleDialect::Less) {
3217 passes.push(TransformPassKind::LessModuleEvaluate);
3218 }
3219
3220 if is_css_module_path(source_path) && facts.selector_count > 0 {
3221 passes.push(TransformPassKind::HashCssModuleClassNames);
3222 }
3223
3224 if facts.css_module_composes_edge_count > 0 {
3225 passes.push(TransformPassKind::ResolveCssModulesComposes);
3226 }
3227
3228 if facts.css_module_value_count > 0 || facts.css_module_value_import_edge_count > 0 {
3229 passes.push(TransformPassKind::ValueResolution);
3230 }
3231
3232 passes
3233}
3234
3235fn is_css_module_path(source_path: &str) -> bool {
3236 let file_name = source_path
3237 .rsplit(['/', '\\'])
3238 .next()
3239 .unwrap_or(source_path)
3240 .to_ascii_lowercase();
3241 let Some((stem, extension)) = file_name.rsplit_once('.') else {
3242 return false;
3243 };
3244 matches!(extension, "css" | "scss" | "sass" | "less") && stem.ends_with(".module")
3245}
3246
3247fn dialect_label(dialect: StyleDialect) -> &'static str {
3248 match dialect {
3249 StyleDialect::Css => "css",
3250 StyleDialect::Scss => "scss",
3251 StyleDialect::Sass => "sass",
3252 StyleDialect::Less => "less",
3253 }
3254}
3255
3256#[cfg(test)]
3257#[allow(deprecated)]
3258mod tests {
3259 use std::collections::{BTreeMap, BTreeSet};
3260
3261 use super::{
3262 InstanceReachabilityDerivationV0, LinkedStylesheetRuleV0, LinkerDependencyEdgeV0,
3263 LinkerInputV0, LinkerRuleV0, TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0,
3264 TransformBundleAssetUrlKind, TransformBundleChunkKind,
3265 TransformBundleDependencyResolutionV0, TransformBundleEdgeKind,
3266 TransformBundleInstanceReachabilityInputV0, TransformBundleLinkErrorV0,
3267 TransformBundleLinkOptionsV0, TransformBundleModuleInputV0,
3268 TransformBundleReachabilityAnalysisV0, TransformBundleReachabilityUnanalyzedCauseV0,
3269 TransformBundleResolvedDependencyV0, TransformBundleSemanticReachabilityInputV0,
3270 TransformBundleTransformedModuleV0, apply_semantic_reachability_to_linker_inputs,
3271 bundle_edge_is_module_dependency, bundle_edge_module_dependency_reason,
3272 carrier_hygiene_assertions, collect_transform_ir_bundle_asset_urls,
3273 compare_omena_transform_bundle_emission_policies, link_omena_transform_bundle_modules,
3274 link_omena_transform_bundle_modules_with_options,
3275 link_omena_transform_bundle_modules_with_semantic_reachability,
3276 link_omena_transform_bundle_projection_with_resolved_dependencies_and_options,
3277 link_stylesheet_from_projection, materialize_omena_transform_bundle_linked_stylesheet,
3278 normalize_omena_transform_bundle_path, project_omena_transform_bundle_linker_inputs,
3279 raw_scan_bundle_asset_urls_for_oracle, rewrite_omena_transform_bundle_asset_urls_in_source,
3280 summarize_omena_transform_bundle_from_source,
3281 };
3282 use omena_cross_file_summary::EdgeOrderRelevanceV0;
3283 use omena_parser::{
3284 ClosedWorldModuleReachabilityEvidenceV0, ConfigurationHashV0, ModuleIdV0,
3285 ModuleInstanceKeyV0, ParsedSelectorFactKind, StyleDialect,
3286 };
3287 use omena_syntax::ident::AuthoredPropertyTextV0;
3288
3289 fn bundle_test_instance() -> ModuleInstanceKeyV0 {
3290 ModuleInstanceKeyV0::new(
3291 ModuleIdV0::new("/workspace/src/app.module.css"),
3292 ConfigurationHashV0::new("default"),
3293 )
3294 }
3295
3296 #[test]
3297 fn semantic_reachability_input_identity_uses_custom_property_keys() {
3298 let input = |property: &str| {
3299 let mut input =
3300 TransformBundleSemanticReachabilityInputV0::new("/workspace/src/app.module.css");
3301 input.custom_property_names = vec![AuthoredPropertyTextV0::new(property)];
3302 input
3303 };
3304
3305 assert_eq!(input(r"--f\6f o"), input("--foo"));
3306 assert_ne!(input("--foo"), input("--FOO"));
3307 }
3308
3309 #[test]
3310 fn instance_reachability_input_identity_uses_custom_property_keys() {
3311 let input = |property: &str| {
3312 let mut input = TransformBundleInstanceReachabilityInputV0::new(
3313 bundle_test_instance(),
3314 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
3315 );
3316 input.custom_property_names = vec![AuthoredPropertyTextV0::new(property)];
3317 input
3318 };
3319
3320 assert_eq!(input(r"--f\6f o"), input("--foo"));
3321 assert_ne!(input("--foo"), input("--FOO"));
3322 }
3323
3324 #[test]
3325 fn linker_input_identity_uses_custom_property_keys() {
3326 let input = |property: &str| LinkerInputV0 {
3327 source_path: "/workspace/src/app.module.css".to_string(),
3328 dialect: StyleDialect::Css,
3329 instance: bundle_test_instance(),
3330 dependency_edges: Vec::new(),
3331 class_names: Vec::new(),
3332 keyframe_names: Vec::new(),
3333 value_names: Vec::new(),
3334 custom_property_names: vec![AuthoredPropertyTextV0::new(property)],
3335 ordered_rules: Vec::new(),
3336 };
3337
3338 assert_eq!(input(r"--f\6f o"), input("--foo"));
3339 assert_ne!(input("--foo"), input("--FOO"));
3340 }
3341
3342 #[test]
3343 fn public_path_normalizer_collapses_equivalent_cross_platform_spellings() {
3344 assert_eq!(
3345 normalize_omena_transform_bundle_path("/workspace/src/./nested/../Button.module.css"),
3346 "/workspace/src/Button.module.css"
3347 );
3348 assert_eq!(
3349 normalize_omena_transform_bundle_path(
3350 r"C:\workspace\src\.\nested\..\Button.module.css"
3351 ),
3352 "C:/workspace/src/Button.module.css"
3353 );
3354 }
3355
3356 #[test]
3357 fn builds_bundle_plan_from_scss_and_css_modules_parser_facts() {
3358 let source = r#"
3359@use "./tokens" as tokens;
3360@forward "./theme";
3361@value primary from "./colors.module.css";
3362.button {
3363 composes: reset from "./reset.module.css";
3364 color: tokens.$brand;
3365}
3366"#;
3367 let summary = summarize_omena_transform_bundle_from_source(
3368 "Button.module.scss",
3369 source,
3370 StyleDialect::Scss,
3371 );
3372
3373 assert_eq!(summary.product, "omena-transform-bundle.source");
3374 assert_eq!(summary.dialect, "scss");
3375 assert!(summary.import_inline_required);
3376 assert!(summary.module_evaluation_required);
3377 assert!(summary.css_modules_resolution_required);
3378 assert!(summary.class_hashing_required);
3379 assert!(summary.value_resolution_required);
3380 assert!(summary.pass_plan.violated_dag_edge_count == 0);
3381 assert!(summary.bundle_edges.iter().any(|edge| {
3382 edge.kind == TransformBundleEdgeKind::CssModuleComposesExternal
3383 && edge.import_source.as_deref() == Some("./reset.module.css")
3384 }));
3385 assert_eq!(
3386 summary.planned_pass_ids,
3387 vec![
3388 "import-inline",
3389 "scss-module-evaluate",
3390 "composes-resolution",
3391 "css-modules-class-hashing",
3392 "value-resolution"
3393 ]
3394 );
3395 }
3396
3397 #[test]
3398 fn bundle_edge_catalog_has_total_order_relevance() {
3399 assert_eq!(TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0.len(), 9);
3400 assert!(TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0.iter().all(|kind| {
3401 kind.order_relevance() == EdgeOrderRelevanceV0::OrderBearing
3402 && !kind.order_relevance_reason().is_empty()
3403 }));
3404 }
3405
3406 #[test]
3407 fn module_dependency_edge_authority_excludes_only_local_composition() {
3408 let module_dependencies = TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0
3409 .iter()
3410 .copied()
3411 .filter(|kind| bundle_edge_is_module_dependency(*kind))
3412 .collect::<Vec<_>>();
3413
3414 assert_eq!(module_dependencies.len(), 8);
3415 assert!(!module_dependencies.contains(&TransformBundleEdgeKind::CssModuleComposesLocal));
3416 assert!(TRANSFORM_BUNDLE_EDGE_KIND_VARIANTS_V0.iter().all(|kind| {
3417 bundle_edge_is_module_dependency(*kind)
3418 == bundle_edge_module_dependency_reason(*kind).is_some()
3419 }));
3420 }
3421
3422 #[test]
3423 fn recognizes_less_module_evaluation_from_dialect() {
3424 let summary = summarize_omena_transform_bundle_from_source(
3425 "Theme.module.less",
3426 r#"@import (reference) "tokens.less"; .card { color: @brand; }"#,
3427 StyleDialect::Less,
3428 );
3429
3430 assert!(summary.module_evaluation_required);
3431 assert!(summary.import_inline_required);
3432 assert!(
3433 summary
3434 .bundle_edges
3435 .iter()
3436 .any(|edge| edge.kind == TransformBundleEdgeKind::LessImport)
3437 );
3438 assert!(summary.required_pass_ids.contains(&"less-module-evaluate"));
3439 assert!(!summary.required_pass_ids.contains(&"scss-module-evaluate"));
3440 assert!(
3441 summary
3442 .required_pass_ids
3443 .contains(&"css-modules-class-hashing")
3444 );
3445 }
3446
3447 #[test]
3448 fn plans_plain_css_import_inline_without_scss_module_evaluation() {
3449 let summary = summarize_omena_transform_bundle_from_source(
3450 "App.css",
3451 r#"@import "./tokens.css"; .button { color: red; }"#,
3452 StyleDialect::Css,
3453 );
3454
3455 assert!(summary.import_inline_required);
3456 assert!(!summary.module_evaluation_required);
3457 assert_eq!(summary.required_pass_ids, vec!["import-inline"]);
3458 assert_eq!(summary.planned_pass_ids, vec!["import-inline"]);
3459 assert!(
3460 summary
3461 .bundle_edges
3462 .iter()
3463 .any(|edge| edge.kind == TransformBundleEdgeKind::CssImport)
3464 );
3465 assert!(
3466 !summary
3467 .bundle_edges
3468 .iter()
3469 .any(|edge| edge.kind == TransformBundleEdgeKind::SassImport)
3470 );
3471 }
3472
3473 #[test]
3474 fn rejects_module_substring_false_positive_paths() {
3475 let source = ".button { color: red; }";
3476 let backup_summary = summarize_omena_transform_bundle_from_source(
3477 "Button.module.backup.scss",
3478 source,
3479 StyleDialect::Scss,
3480 );
3481 let unrelated_summary = summarize_omena_transform_bundle_from_source(
3482 "module/Button.scss",
3483 source,
3484 StyleDialect::Scss,
3485 );
3486
3487 assert!(!backup_summary.class_hashing_required);
3488 assert!(!unrelated_summary.class_hashing_required);
3489 assert!(
3490 !backup_summary
3491 .required_pass_ids
3492 .contains(&"css-modules-class-hashing")
3493 );
3494 assert!(
3495 !unrelated_summary
3496 .required_pass_ids
3497 .contains(&"css-modules-class-hashing")
3498 );
3499 }
3500
3501 #[test]
3502 fn recognizes_css_module_path_by_final_stem_and_supported_extension() {
3503 let summary = summarize_omena_transform_bundle_from_source(
3504 "components\\Button.MODULE.SCSS",
3505 ".button { color: red; }",
3506 StyleDialect::Scss,
3507 );
3508
3509 assert!(summary.class_hashing_required);
3510 assert!(
3511 summary
3512 .required_pass_ids
3513 .contains(&"css-modules-class-hashing")
3514 );
3515 }
3516
3517 #[test]
3518 fn resolves_relative_asset_urls_from_source_path() {
3519 let summary = summarize_omena_transform_bundle_from_source(
3520 "src/components/Button.module.css",
3521 r#".button { background: url("../assets/icon.svg"); mask: url(/static/mask.svg); cursor: url(data:image/svg+xml,abc); filter: url(#shadow); border-image-source: URL(https://cdn.example.com/frame.png); }"#,
3522 StyleDialect::Css,
3523 );
3524
3525 assert_eq!(summary.asset_urls.len(), 5);
3526 assert!(summary.asset_urls.iter().any(|asset| {
3527 asset.normalized_url == "../assets/icon.svg"
3528 && asset.kind == TransformBundleAssetUrlKind::Relative
3529 && asset.resolved_path.as_deref() == Some("src/assets/icon.svg")
3530 && asset.bundler_resolution_required
3531 }));
3532 assert!(summary.asset_urls.iter().any(|asset| {
3533 asset.normalized_url == "/static/mask.svg"
3534 && asset.kind == TransformBundleAssetUrlKind::AbsolutePath
3535 && asset.resolved_path.as_deref() == Some("/static/mask.svg")
3536 && asset.bundler_resolution_required
3537 }));
3538
3539 assert!(summary.asset_urls.iter().any(|asset| {
3540 asset.kind == TransformBundleAssetUrlKind::Data && !asset.bundler_resolution_required
3541 }));
3542 assert!(summary.asset_urls.iter().any(|asset| {
3543 asset.kind == TransformBundleAssetUrlKind::Fragment
3544 && !asset.bundler_resolution_required
3545 }));
3546 assert!(summary.asset_urls.iter().any(|asset| {
3547 asset.kind == TransformBundleAssetUrlKind::External
3548 && !asset.bundler_resolution_required
3549 }));
3550 }
3551
3552 #[test]
3553 fn value_ir_asset_urls_match_raw_scan_byte_identical() {
3554 let corpus = [
3555 (
3556 "src/components/Button.module.css",
3557 StyleDialect::Css,
3558 r#".button { background: url("../assets/icon.svg"); mask: url(/static/mask.svg); cursor: url(data:image/svg+xml,abc); filter: url(#shadow); border-image-source: URL(https://cdn.example.com/frame.png); }"#,
3559 ),
3560 (
3561 "src/components/Card.module.scss",
3562 StyleDialect::Scss,
3563 r#".카드 { background-image: url(./img/아이콘.svg); }"#,
3564 ),
3565 (
3566 "src/components/Theme.module.less",
3567 StyleDialect::Less,
3568 r#".theme { background: url('../assets/theme.svg'); }"#,
3569 ),
3570 ];
3571
3572 for (source_path, dialect, source) in corpus {
3573 let transform_ir_urls =
3574 collect_transform_ir_bundle_asset_urls(source_path, source, dialect);
3575 let raw_urls = raw_scan_bundle_asset_urls_for_oracle(source_path, source);
3576 assert_eq!(transform_ir_urls, raw_urls, "{source_path}");
3577 }
3578 }
3579
3580 #[test]
3581 fn plans_code_split_chunks_for_style_and_asset_dependencies() {
3582 let summary = summarize_omena_transform_bundle_from_source(
3583 "src/components/Button.module.css",
3584 r#"@import "../theme.css"; .button { background: url("../assets/icon.svg"); }"#,
3585 StyleDialect::Css,
3586 );
3587
3588 assert!(summary.code_splitting_required);
3589 assert_eq!(summary.code_split_chunks.len(), 3);
3590 let entry_chunk_id = summary
3591 .code_split_chunks
3592 .iter()
3593 .find(|chunk| chunk.kind == TransformBundleChunkKind::Entry)
3594 .map(|chunk| {
3595 assert_eq!(chunk.split_boundary, "entry");
3596 assert_eq!(chunk.depends_on.len(), 2);
3597 chunk.chunk_id.clone()
3598 });
3599 assert!(entry_chunk_id.is_some());
3600
3601 let style_chunk_id = summary
3602 .code_split_chunks
3603 .iter()
3604 .find(|chunk| chunk.kind == TransformBundleChunkKind::StyleImport)
3605 .map(|chunk| {
3606 assert_eq!(chunk.import_source.as_deref(), Some("../theme.css"));
3607 assert_eq!(chunk.split_boundary, "styleDependency");
3608 chunk.chunk_id.clone()
3609 });
3610 assert!(style_chunk_id.is_some());
3611
3612 let asset_chunk_id = summary
3613 .code_split_chunks
3614 .iter()
3615 .find(|chunk| chunk.kind == TransformBundleChunkKind::Asset)
3616 .map(|chunk| {
3617 assert_eq!(chunk.asset_url.as_deref(), Some("../assets/icon.svg"));
3618 assert_eq!(chunk.resolved_path.as_deref(), Some("src/assets/icon.svg"));
3619 assert_eq!(chunk.split_boundary, "assetDependency");
3620 chunk.chunk_id.clone()
3621 });
3622 assert!(asset_chunk_id.is_some());
3623 let entry_dependencies = summary
3624 .code_split_chunks
3625 .iter()
3626 .find(|chunk| chunk.kind == TransformBundleChunkKind::Entry)
3627 .map(|chunk| chunk.depends_on.as_slice())
3628 .unwrap_or(&[]);
3629 assert!(style_chunk_id.is_some_and(|chunk_id| entry_dependencies.contains(&chunk_id)));
3630 assert!(asset_chunk_id.is_some_and(|chunk_id| entry_dependencies.contains(&chunk_id)));
3631 }
3632
3633 #[test]
3634 fn resolves_asset_urls_after_non_ascii_source_text() {
3635 let summary = summarize_omena_transform_bundle_from_source(
3636 "src/카드.module.css",
3637 ".카드 { background-image: url(./img/아이콘.svg); }",
3638 StyleDialect::Css,
3639 );
3640
3641 assert_eq!(summary.asset_urls.len(), 1);
3642 let asset = &summary.asset_urls[0];
3643 assert_eq!(asset.kind, TransformBundleAssetUrlKind::Relative);
3644 assert_eq!(asset.normalized_url, "./img/아이콘.svg");
3645 assert_eq!(asset.resolved_path.as_deref(), Some("src/img/아이콘.svg"));
3646 }
3647
3648 #[test]
3649 fn preserves_leading_parent_segments_without_source_parent() {
3650 let summary = summarize_omena_transform_bundle_from_source(
3651 "Button.module.css",
3652 ".button { background-image: url(../assets/icon.svg); }",
3653 StyleDialect::Css,
3654 );
3655
3656 assert_eq!(
3657 summary.asset_urls[0].resolved_path.as_deref(),
3658 Some("../assets/icon.svg")
3659 );
3660 }
3661
3662 #[test]
3663 fn rewrites_relative_asset_urls_to_resolved_bundle_paths() {
3664 let summary = rewrite_omena_transform_bundle_asset_urls_in_source(
3665 "src/components/Button.module.css",
3666 r#".button { background: url("../assets/icon.svg"); mask: url(/static/mask.svg); filter: url(#shadow); }"#,
3667 );
3668
3669 assert_eq!(summary.product, "omena-transform-bundle.asset-url-rewrite");
3670 assert_eq!(summary.asset_url_count, 3);
3671 assert_eq!(summary.rewrite_count, 1);
3672 assert!(summary.output_css.contains(r#"url("src/assets/icon.svg")"#));
3673 assert!(summary.output_css.contains("url(/static/mask.svg)"));
3674 assert!(summary.output_css.contains("url(#shadow)"));
3675 assert_eq!(
3676 summary
3677 .rewritten_asset_urls
3678 .first()
3679 .and_then(|asset| asset.resolved_path.as_deref()),
3680 Some("src/assets/icon.svg")
3681 );
3682 }
3683
3684 #[test]
3685 fn linker_global_rule_order_is_a_total_order_over_linked_rules() -> Result<(), String> {
3686 let modules = vec![
3687 TransformBundleModuleInputV0::new(
3688 "src/app.module.css",
3689 r#"@import "./theme.css"; .button { color: var(--brand); }"#,
3690 StyleDialect::Css,
3691 ),
3692 TransformBundleModuleInputV0::new(
3693 "src/theme.css",
3694 r#":root { --brand: red; } .theme { color: red; }"#,
3695 StyleDialect::Css,
3696 ),
3697 ];
3698
3699 let linked = link_omena_transform_bundle_modules(&["src/app.module.css"], &modules)
3700 .map_err(|err| format!("{err:?}"))?;
3701
3702 assert_eq!(linked.product, "omena-transform-bundle.linked-stylesheet");
3703 assert_eq!(linked.entrypoints.len(), 1);
3704 assert_eq!(linked.module_instances.len(), 2);
3705 assert_eq!(
3706 linked
3707 .global_rule_order
3708 .rules
3709 .iter()
3710 .map(|rule| rule.global_order_index)
3711 .collect::<Vec<_>>(),
3712 vec![0, 1]
3713 );
3714 assert!(
3715 linked
3716 .global_rule_order
3717 .rules
3718 .iter()
3719 .any(|rule| rule.selector_name == "button")
3720 );
3721 assert!(
3722 linked
3723 .closed_world_bundle
3724 .reachability()
3725 .class_names()
3726 .contains(&"theme".to_string())
3727 );
3728 assert!(
3729 linked
3730 .closed_world_bundle
3731 .reachability()
3732 .custom_property_names()
3733 .iter()
3734 .any(|name| name.to_custom_key()
3735 == AuthoredPropertyTextV0::new("--brand").to_custom_key())
3736 );
3737 Ok(())
3738 }
3739
3740 #[test]
3741 fn emission_plan_is_the_only_rule_order_authority() -> Result<(), String> {
3742 let first = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("a.css"));
3743 let second = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("b.css"));
3744 let inputs = [
3745 LinkerInputV0 {
3746 source_path: "a.css".to_string(),
3747 dialect: StyleDialect::Css,
3748 instance: first.clone(),
3749 dependency_edges: Vec::new(),
3750 class_names: vec!["first".to_string()],
3751 keyframe_names: Vec::new(),
3752 value_names: Vec::new(),
3753 custom_property_names: Vec::new(),
3754 ordered_rules: vec![LinkerRuleV0 {
3755 selector_name: "first".to_string(),
3756 selector_kind: ParsedSelectorFactKind::Class,
3757 range_start: 0,
3758 range_end: 6,
3759 }],
3760 },
3761 LinkerInputV0 {
3762 source_path: "b.css".to_string(),
3763 dialect: StyleDialect::Css,
3764 instance: second.clone(),
3765 dependency_edges: Vec::new(),
3766 class_names: vec!["second".to_string()],
3767 keyframe_names: Vec::new(),
3768 value_names: Vec::new(),
3769 custom_property_names: Vec::new(),
3770 ordered_rules: vec![LinkerRuleV0 {
3771 selector_name: "second".to_string(),
3772 selector_kind: ParsedSelectorFactKind::Class,
3773 range_start: 0,
3774 range_end: 7,
3775 }],
3776 },
3777 ];
3778 let mut plan = super::emission_order::build_emission_plan(
3779 &inputs,
3780 &[first.clone(), second],
3781 &[first],
3782 &[],
3783 super::EmissionOrderingPolicyV0::ModuleIdLegacy,
3784 super::BundleResolutionAuthorityV0::LegacyPathInferred,
3785 )
3786 .map_err(|error| format!("{error:?}"))?;
3787 let original = super::emission_order::build_global_rule_order_from_plan(&inputs, &plan)
3788 .map_err(|error| format!("{error:?}"))?;
3789
3790 plan.entries.swap(0, 1);
3791 let perturbed = super::emission_order::build_global_rule_order_from_plan(&inputs, &plan)
3792 .map_err(|error| format!("{error:?}"))?;
3793
3794 assert_eq!(original.rules[0].selector_name, "first");
3795 assert_eq!(perturbed.rules[0].selector_name, "second");
3796 assert_ne!(original, perturbed);
3797 Ok(())
3798 }
3799
3800 #[test]
3801 fn parser_import_order_controls_default_output() -> Result<(), String> {
3802 fn link(imports: &str) -> Result<super::LinkedStylesheetV0, String> {
3803 link_omena_transform_bundle_modules(
3804 &["src/app.css"],
3805 &[
3806 TransformBundleModuleInputV0::new(
3807 "src/app.css",
3808 format!("{imports} .app {{ color: red; }}"),
3809 StyleDialect::Css,
3810 ),
3811 TransformBundleModuleInputV0::new(
3812 "src/a.css",
3813 ".a { color: blue; }",
3814 StyleDialect::Css,
3815 ),
3816 TransformBundleModuleInputV0::new(
3817 "src/z.css",
3818 ".z { color: green; }",
3819 StyleDialect::Css,
3820 ),
3821 ],
3822 )
3823 .map_err(|error| format!("{error:?}"))
3824 }
3825
3826 let a_then_z = link(r#"@import "./a.css"; @import "./z.css";"#)?;
3827 let z_then_a = link(r#"@import "./z.css"; @import "./a.css";"#)?;
3828 let targets = |linked: &super::LinkedStylesheetV0| {
3829 linked
3830 .emission_plan
3831 .dependency_facts
3832 .iter()
3833 .map(|fact| fact.to_module.module().as_str().to_string())
3834 .collect::<Vec<_>>()
3835 };
3836
3837 assert_eq!(targets(&a_then_z), vec!["src/a.css", "src/z.css"]);
3838 assert_eq!(targets(&z_then_a), vec!["src/z.css", "src/a.css"]);
3839 assert_ne!(
3840 serde_json::to_vec(&a_then_z).map_err(|error| format!("{error:?}"))?,
3841 serde_json::to_vec(&z_then_a).map_err(|error| format!("{error:?}"))?
3842 );
3843 Ok(())
3844 }
3845
3846 #[test]
3847 fn default_link_options_are_import_ordered_and_resolved() -> Result<(), String> {
3848 let modules = [
3849 TransformBundleModuleInputV0::new(
3850 "src/app.css",
3851 r#"@import "./z.css"; .app { color: red; }"#,
3852 StyleDialect::Css,
3853 ),
3854 TransformBundleModuleInputV0::new(
3855 "src/z.css",
3856 ".z { color: green; }",
3857 StyleDialect::Css,
3858 ),
3859 ];
3860 let implicit = link_omena_transform_bundle_modules(&["src/app.css"], &modules)
3861 .map_err(|error| format!("{error:?}"))?;
3862 let default_options = TransformBundleLinkOptionsV0::default();
3863 let explicit = link_omena_transform_bundle_modules_with_options(
3864 &["src/app.css"],
3865 &modules,
3866 &[],
3867 &[],
3868 default_options,
3869 )
3870 .map_err(|error| format!("{error:?}"))?;
3871
3872 assert_eq!(
3873 implicit.emission_plan.policy,
3874 super::EmissionOrderingPolicyV0::ImportOrderPreserving
3875 );
3876 assert_eq!(
3877 default_options.dependency_resolution_authority,
3878 super::BundleResolutionAuthorityV0::Resolved
3879 );
3880 assert_eq!(
3881 serde_json::to_vec(&implicit).map_err(|error| format!("{error:?}"))?,
3882 serde_json::to_vec(&explicit).map_err(|error| format!("{error:?}"))?
3883 );
3884 #[allow(deprecated)]
3885 let legacy_options = TransformBundleLinkOptionsV0::legacy_compatibility();
3886 assert_eq!(
3887 legacy_options.emission_ordering_policy,
3888 super::EmissionOrderingPolicyV0::ModuleIdLegacy
3889 );
3890 assert_eq!(
3891 legacy_options.dependency_resolution_authority,
3892 super::BundleResolutionAuthorityV0::LegacyPathInferred
3893 );
3894 Ok(())
3895 }
3896
3897 #[test]
3898 fn import_order_policy_reports_real_output_differences() -> Result<(), String> {
3899 let modules = [
3900 TransformBundleModuleInputV0::new(
3901 "src/app.css",
3902 r#"@import "./z.css"; @import "./a.css"; .app { color: red; }"#,
3903 StyleDialect::Css,
3904 ),
3905 TransformBundleModuleInputV0::new(
3906 "src/a.css",
3907 ".a { color: blue; }",
3908 StyleDialect::Css,
3909 ),
3910 TransformBundleModuleInputV0::new(
3911 "src/z.css",
3912 ".z { color: green; }",
3913 StyleDialect::Css,
3914 ),
3915 ];
3916 let linked = link_omena_transform_bundle_modules_with_options(
3917 &["src/app.css"],
3918 &modules,
3919 &[],
3920 &[],
3921 TransformBundleLinkOptionsV0 {
3922 emission_ordering_policy: super::EmissionOrderingPolicyV0::ImportOrderPreserving,
3923 ..TransformBundleLinkOptionsV0::default()
3924 },
3925 )
3926 .map_err(|error| format!("{error:?}"))?;
3927 let report = compare_omena_transform_bundle_emission_policies(&["src/app.css"], &modules)
3928 .map_err(|error| format!("{error:?}"))?;
3929
3930 assert_eq!(
3931 linked
3932 .global_rule_order
3933 .rules
3934 .iter()
3935 .map(|rule| rule.selector_name.as_str())
3936 .collect::<Vec<_>>(),
3937 vec!["z", "a", "app"]
3938 );
3939 assert!(!report.equivalent);
3940 assert_eq!(report.difference_count, report.differences.len());
3941 assert!(report.difference_count >= 2);
3942 Ok(())
3943 }
3944
3945 #[test]
3946 fn linked_emission_materializes_the_global_module_order() -> Result<(), String> {
3947 let modules = [
3948 TransformBundleModuleInputV0::new(
3949 "src/app.css",
3950 r#"@import "./z.css"; @import "./a.css"; .app { color: red; }"#,
3951 StyleDialect::Css,
3952 ),
3953 TransformBundleModuleInputV0::new(
3954 "src/a.css",
3955 ".a { color: blue; }",
3956 StyleDialect::Css,
3957 ),
3958 TransformBundleModuleInputV0::new(
3959 "src/z.css",
3960 ".z { color: green; }",
3961 StyleDialect::Css,
3962 ),
3963 ];
3964 let link = |policy| {
3965 link_omena_transform_bundle_modules_with_options(
3966 &["src/app.css"],
3967 &modules,
3968 &[],
3969 &[],
3970 TransformBundleLinkOptionsV0 {
3971 emission_ordering_policy: policy,
3972 ..TransformBundleLinkOptionsV0::default()
3973 },
3974 )
3975 .map_err(|error| format!("{error:?}"))
3976 };
3977 let legacy = link(super::EmissionOrderingPolicyV0::ModuleIdLegacy)?;
3978 let import_order = link(super::EmissionOrderingPolicyV0::ImportOrderPreserving)?;
3979 let transformed_modules = legacy
3980 .module_instances
3981 .iter()
3982 .cloned()
3983 .map(|module_instance| {
3984 let marker = module_instance.module().as_str().replace(['/', '.'], "-");
3985 TransformBundleTransformedModuleV0::new(
3986 module_instance,
3987 format!(".{marker} {{ order: linked; }}"),
3988 )
3989 })
3990 .collect::<Vec<_>>();
3991
3992 let legacy_output =
3993 materialize_omena_transform_bundle_linked_stylesheet(&legacy, &transformed_modules)
3994 .map_err(|error| format!("{error:?}"))?;
3995 let import_order_output = materialize_omena_transform_bundle_linked_stylesheet(
3996 &import_order,
3997 &transformed_modules,
3998 )
3999 .map_err(|error| format!("{error:?}"))?;
4000
4001 assert_ne!(legacy_output.output_css, import_order_output.output_css);
4002 assert_eq!(
4003 import_order_output
4004 .module_regions
4005 .iter()
4006 .map(|region| region.module_instance.module().as_str())
4007 .collect::<Vec<_>>(),
4008 vec!["src/z.css", "src/a.css", "src/app.css"]
4009 );
4010 assert_eq!(import_order_output.emitted_module_count, 3);
4011 assert_eq!(
4012 import_order_output.global_order_entry_count,
4013 import_order.global_rule_order.rules.len()
4014 );
4015 for transformed in &transformed_modules {
4016 assert_eq!(
4017 import_order_output
4018 .output_css
4019 .matches(&transformed.output_css)
4020 .count(),
4021 1,
4022 "each transformed module must be emitted exactly once"
4023 );
4024 }
4025 for entry_region in &import_order_output.order_entry_regions {
4026 let module_region = import_order_output
4027 .module_regions
4028 .iter()
4029 .find(|region| region.module_instance == entry_region.module_instance)
4030 .ok_or_else(|| "ordered entry has no generated module region".to_string())?;
4031 assert_eq!(entry_region.generated_start, module_region.generated_start);
4032 assert_eq!(entry_region.generated_end, module_region.generated_end);
4033 }
4034 Ok(())
4035 }
4036
4037 #[test]
4038 fn linked_emission_rejects_preinlined_module_bytes() -> Result<(), String> {
4039 let modules = [
4040 TransformBundleModuleInputV0::new(
4041 "src/app.css",
4042 r#"@import "./theme.css"; .app { color: red; }"#,
4043 StyleDialect::Css,
4044 ),
4045 TransformBundleModuleInputV0::new(
4046 "src/theme.css",
4047 ".theme { color: blue; }",
4048 StyleDialect::Css,
4049 ),
4050 ];
4051 let linked = link_omena_transform_bundle_modules(&["src/app.css"], &modules)
4052 .map_err(|error| format!("{error:?}"))?;
4053 let transformed_modules = linked
4054 .module_instances
4055 .iter()
4056 .cloned()
4057 .enumerate()
4058 .map(|(index, module_instance)| {
4059 TransformBundleTransformedModuleV0::new(
4060 module_instance,
4061 format!(".module-{index} {{ order: linked; }}"),
4062 )
4063 .with_non_empty_import_replacement_count(usize::from(index == 0))
4064 })
4065 .collect::<Vec<_>>();
4066
4067 let result =
4068 materialize_omena_transform_bundle_linked_stylesheet(&linked, &transformed_modules);
4069
4070 assert!(matches!(
4071 result,
4072 Err(
4073 super::LinkedEmissionMaterializationErrorV0::ImportReplacementWouldDuplicateModule {
4074 replacement_count: 1,
4075 ..
4076 }
4077 )
4078 ));
4079 Ok(())
4080 }
4081
4082 #[test]
4083 fn external_composition_cycles_are_recorded_with_an_explicit_policy() -> Result<(), String> {
4084 let first = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("a.css"));
4085 let second = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("b.css"));
4086 let input = |source_path: &str,
4087 instance: ModuleInstanceKeyV0,
4088 import_source: &str,
4089 selector: &str| LinkerInputV0 {
4090 source_path: source_path.to_string(),
4091 dialect: StyleDialect::Css,
4092 instance,
4093 dependency_edges: vec![LinkerDependencyEdgeV0 {
4094 kind: TransformBundleEdgeKind::CssModuleComposesExternal,
4095 import_source: import_source.to_string(),
4096 import_ordinal: Some(0),
4097 local_names: Vec::new(),
4098 remote_names: Vec::new(),
4099 }],
4100 class_names: vec![selector.to_string()],
4101 keyframe_names: Vec::new(),
4102 value_names: Vec::new(),
4103 custom_property_names: Vec::new(),
4104 ordered_rules: vec![LinkerRuleV0 {
4105 selector_name: selector.to_string(),
4106 selector_kind: ParsedSelectorFactKind::Class,
4107 range_start: 0,
4108 range_end: selector.len() as u32,
4109 }],
4110 };
4111 let linked = link_stylesheet_from_projection(
4112 &["a.css"],
4113 &[
4114 input("a.css", first, "./b.css", "a"),
4115 input("b.css", second, "./a.css", "b"),
4116 ],
4117 )
4118 .map_err(|error| format!("{error:?}"))?;
4119
4120 assert_eq!(linked.emission_plan.cycle_groups.len(), 1);
4121 let group = &linked.emission_plan.cycle_groups[0];
4122 assert_eq!(group.class, super::EmissionCycleClassV0::Composition);
4123 assert_eq!(group.dialect, super::EmissionCycleDialectV0::Css);
4124 assert_eq!(group.policy, super::EmissionCyclePolicyV0::ModuleIdentity);
4125 assert_eq!(group.members, group.chosen_order);
4126 Ok(())
4127 }
4128
4129 #[test]
4130 #[allow(clippy::expect_used)]
4131 fn dialect_import_cycles_fail_closed_with_typed_classification() {
4132 let fixtures = [
4133 (
4134 "css",
4135 StyleDialect::Css,
4136 super::EmissionCycleDialectV0::Css,
4137 TransformBundleEdgeKind::CssImport,
4138 "a.css",
4139 "b.css",
4140 "@import \"./b.css\"; .a { color: red; }",
4141 "@import \"./a.css\"; .b { color: blue; }",
4142 ),
4143 (
4144 "scss",
4145 StyleDialect::Scss,
4146 super::EmissionCycleDialectV0::Scss,
4147 TransformBundleEdgeKind::SassUse,
4148 "a.scss",
4149 "b.scss",
4150 "@use \"./b.scss\"; .a { color: red; }",
4151 "@use \"./a.scss\"; .b { color: blue; }",
4152 ),
4153 (
4154 "sass",
4155 StyleDialect::Sass,
4156 super::EmissionCycleDialectV0::Sass,
4157 TransformBundleEdgeKind::SassForward,
4158 "a.sass",
4159 "b.sass",
4160 "@forward \"./b.sass\"\n.a\n color: red",
4161 "@forward \"./a.sass\"\n.b\n color: blue",
4162 ),
4163 (
4164 "less",
4165 StyleDialect::Less,
4166 super::EmissionCycleDialectV0::Less,
4167 TransformBundleEdgeKind::LessImport,
4168 "a.less",
4169 "b.less",
4170 "@import \"./b.less\"; .a { color: red; }",
4171 "@import \"./a.less\"; .b { color: blue; }",
4172 ),
4173 ];
4174
4175 for (
4176 label,
4177 dialect,
4178 expected_dialect,
4179 expected_edge_kind,
4180 first_path,
4181 second_path,
4182 first_source,
4183 second_source,
4184 ) in fixtures
4185 {
4186 let modules = vec![
4187 TransformBundleModuleInputV0::new(first_path, first_source, dialect),
4188 TransformBundleModuleInputV0::new(second_path, second_source, dialect),
4189 ];
4190 let error = link_omena_transform_bundle_modules(&[first_path], modules.as_slice())
4191 .expect_err("dialect import cycle must fail closed");
4192 assert_eq!(
4193 error,
4194 TransformBundleLinkErrorV0::UnsupportedDialectEmissionCycle {
4195 dialect: expected_dialect,
4196 class: super::EmissionCycleClassV0::Import,
4197 edge_kinds: vec![expected_edge_kind],
4198 },
4199 "{label} import cycle must preserve its dialect and edge classification"
4200 );
4201 eprintln!(
4202 "EMISSION_DIALECT_CYCLE_ERROR={}",
4203 serde_json::to_string(&error).expect("cycle error must serialize")
4204 );
4205 }
4206 }
4207
4208 #[test]
4209 fn dialect_import_acyclic_chains_remain_linkable() -> Result<(), String> {
4210 let fixtures = [
4211 (
4212 StyleDialect::Css,
4213 "a.css",
4214 "b.css",
4215 "@import \"./b.css\"; .a { color: red; }",
4216 ".b { color: blue; }",
4217 ),
4218 (
4219 StyleDialect::Scss,
4220 "a.scss",
4221 "b.scss",
4222 "@use \"./b.scss\"; .a { color: red; }",
4223 ".b { color: blue; }",
4224 ),
4225 (
4226 StyleDialect::Sass,
4227 "a.sass",
4228 "b.sass",
4229 "@forward \"./b.sass\"\n.a\n color: red",
4230 ".b\n color: blue",
4231 ),
4232 (
4233 StyleDialect::Less,
4234 "a.less",
4235 "b.less",
4236 "@import \"./b.less\"; .a { color: red; }",
4237 ".b { color: blue; }",
4238 ),
4239 ];
4240
4241 for (dialect, first_path, second_path, first_source, second_source) in fixtures {
4242 let modules = vec![
4243 TransformBundleModuleInputV0::new(first_path, first_source, dialect),
4244 TransformBundleModuleInputV0::new(second_path, second_source, dialect),
4245 ];
4246 let linked = link_omena_transform_bundle_modules(&[first_path], modules.as_slice())
4247 .map_err(|error| format!("{dialect:?} acyclic chain: {error:?}"))?;
4248 assert!(linked.emission_plan.cycle_groups.is_empty());
4249 }
4250 Ok(())
4251 }
4252
4253 #[test]
4254 fn unsupported_module_cycle_edge_fails_closed() {
4255 let first = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("a.css"));
4256 let second = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("b.css"));
4257 let input =
4258 |source_path: &str, instance: ModuleInstanceKeyV0, import_source: &str| LinkerInputV0 {
4259 source_path: source_path.to_string(),
4260 dialect: StyleDialect::Css,
4261 instance,
4262 dependency_edges: vec![LinkerDependencyEdgeV0 {
4263 kind: TransformBundleEdgeKind::CssModuleComposesLocal,
4264 import_source: import_source.to_string(),
4265 import_ordinal: Some(0),
4266 local_names: Vec::new(),
4267 remote_names: Vec::new(),
4268 }],
4269 class_names: Vec::new(),
4270 keyframe_names: Vec::new(),
4271 value_names: Vec::new(),
4272 custom_property_names: Vec::new(),
4273 ordered_rules: Vec::new(),
4274 };
4275
4276 let result = link_stylesheet_from_projection(
4277 &["a.css"],
4278 &[
4279 input("a.css", first, "./b.css"),
4280 input("b.css", second, "./a.css"),
4281 ],
4282 );
4283
4284 assert_eq!(
4285 result,
4286 Err(TransformBundleLinkErrorV0::UnsupportedEmissionCycle {
4287 edge_kind: TransformBundleEdgeKind::CssModuleComposesLocal,
4288 })
4289 );
4290 }
4291
4292 #[test]
4293 fn public_cascade_key_helper_normalizes_the_layer_ordinal() {
4294 let rule = LinkedStylesheetRuleV0 {
4295 global_order_index: 7,
4296 module_instance: ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("entry.css")),
4297 selector_name: "target".to_string(),
4298 selector_kind: "class",
4299 range_start: 0,
4300 range_end: 7,
4301 };
4302 let layer_ordinal = omena_cascade::LayerOrdinal::new(2);
4303 assert_eq!(layer_ordinal.map(omena_cascade::LayerOrdinal::get), Some(2));
4304 let Some(layer_ordinal) = layer_ordinal else {
4305 return;
4306 };
4307 let module_rank = omena_cascade::ModuleRank::new(3, 2, 1);
4308 let (key, open_world_tie_evidence) = rule.cascade_key_with_global_source_order(
4309 omena_cascade::CascadeLevel::AuthorNormal,
4310 layer_ordinal,
4311 false,
4312 0,
4313 omena_cascade::Specificity::new(0, 1, 0),
4314 module_rank,
4315 );
4316
4317 assert_eq!(
4318 key.layer_rank,
4319 omena_cascade::normalized_layer_rank(false, Some(layer_ordinal))
4320 );
4321 assert_eq!(key.source_order, 7);
4322 assert_eq!(open_world_tie_evidence.module_rank, module_rank);
4323 }
4324
4325 #[test]
4326 fn cascade_source_order_is_fed_by_global_rule_order() -> Result<(), String> {
4327 let modules = vec![
4328 TransformBundleModuleInputV0::new(
4329 "src/app.module.css",
4330 r#"@import "./theme.css"; .button { color: red; }"#,
4331 StyleDialect::Css,
4332 ),
4333 TransformBundleModuleInputV0::new(
4334 "src/theme.css",
4335 r#".button { color: blue; }"#,
4336 StyleDialect::Css,
4337 ),
4338 ];
4339
4340 let linked = link_omena_transform_bundle_modules(&["src/app.module.css"], &modules)
4341 .map_err(|err| format!("{err:?}"))?;
4342 let button_rules = linked
4343 .global_rule_order
4344 .rules
4345 .iter()
4346 .filter(|rule| rule.selector_name == "button")
4347 .collect::<Vec<_>>();
4348
4349 assert_eq!(button_rules.len(), 2);
4350 assert_eq!(
4351 button_rules
4352 .iter()
4353 .map(|rule| rule.global_order_index)
4354 .collect::<Vec<_>>(),
4355 vec![0, 1]
4356 );
4357
4358 let Some(layer_ordinal) = omena_cascade::LayerOrdinal::new(0) else {
4359 return Err("zero must remain a sentinel-safe layer ordinal".to_string());
4360 };
4361 let declarations = button_rules
4362 .iter()
4363 .map(|rule| {
4364 let value = if rule.global_order_index == 0 {
4365 "red"
4366 } else {
4367 "blue"
4368 };
4369 let (key, open_world_tie_evidence) = rule.cascade_key_with_global_source_order(
4370 omena_cascade::CascadeLevel::AuthorNormal,
4371 layer_ordinal,
4372 false,
4373 0,
4374 omena_cascade::Specificity::new(0, 1, 0),
4375 if rule.global_order_index == 0 {
4376 omena_cascade::ModuleRank::new(u32::MAX, u32::MAX, u32::MAX)
4377 } else {
4378 omena_cascade::ModuleRank::ZERO
4379 },
4380 );
4381 omena_cascade::CascadeDeclaration {
4382 id: format!(
4383 "{}:{}",
4384 rule.module_instance.module().as_str(),
4385 rule.global_order_index
4386 ),
4387 property: omena_cascade::AuthoredPropertyTextV0::new("color"),
4388 property_key: omena_cascade::PropertyNameV0::standard("color").canonical_key(),
4389 value: omena_cascade::CascadeValue::Literal(value.to_string()),
4390 key,
4391 open_world_tie_evidence,
4392 specificity_exactness: omena_cascade::SpecificityExactnessV0::Exact,
4393 }
4394 })
4395 .collect::<Vec<_>>();
4396
4397 let outcome = omena_cascade::cascade_property(declarations, "color");
4398 let omena_cascade::CascadeOutcome::Definite { winner, proof, .. } = outcome else {
4399 return Err("expected definite cascade winner".to_string());
4400 };
4401 assert_eq!(
4402 winner.value,
4403 omena_cascade::CascadeValue::Literal("blue".to_string())
4404 );
4405 assert_eq!(winner.key.source_order, 1);
4406 assert_eq!(proof.source_order, 1);
4407 Ok(())
4408 }
4409
4410 #[test]
4411 fn cascade_closed_world_order_matches_module_rank_key_byte_identical() -> Result<(), String> {
4412 let modules = vec![
4413 TransformBundleModuleInputV0::new(
4414 "src/app.module.css",
4415 r#"@import "./theme.css"; .button { color: red; }"#,
4416 StyleDialect::Css,
4417 ),
4418 TransformBundleModuleInputV0::new(
4419 "src/theme.css",
4420 r#".button { color: blue; }"#,
4421 StyleDialect::Css,
4422 ),
4423 ];
4424
4425 let linked = link_omena_transform_bundle_modules(&["src/app.module.css"], &modules)
4426 .map_err(|err| format!("{err:?}"))?;
4427 let Some(layer_ordinal) = omena_cascade::LayerOrdinal::new(0) else {
4428 return Err("zero must remain a sentinel-safe layer ordinal".to_string());
4429 };
4430 let declarations = linked
4431 .global_rule_order
4432 .rules
4433 .iter()
4434 .filter(|rule| rule.selector_name == "button")
4435 .map(|rule| {
4436 let linked_later = rule.global_order_index == 1;
4437 let (key, open_world_tie_evidence) = rule.cascade_key_with_global_source_order(
4438 omena_cascade::CascadeLevel::AuthorNormal,
4439 layer_ordinal,
4440 false,
4441 0,
4442 omena_cascade::Specificity::new(0, 1, 0),
4443 if linked_later {
4444 omena_cascade::ModuleRank::new(u32::MAX, u32::MAX, u32::MAX)
4445 } else {
4446 omena_cascade::ModuleRank::ZERO
4447 },
4448 );
4449 omena_cascade::CascadeDeclaration {
4450 id: format!(
4451 "{}:{}",
4452 rule.module_instance.module().as_str(),
4453 rule.global_order_index
4454 ),
4455 property: omena_cascade::AuthoredPropertyTextV0::new("color"),
4456 property_key: omena_cascade::PropertyNameV0::standard("color").canonical_key(),
4457 value: omena_cascade::CascadeValue::Literal(if linked_later {
4458 "blue".to_string()
4459 } else {
4460 "red".to_string()
4461 }),
4462 key,
4463 open_world_tie_evidence,
4464 specificity_exactness: omena_cascade::SpecificityExactnessV0::Exact,
4465 }
4466 })
4467 .collect::<Vec<_>>();
4468
4469 let linked_order_css = definite_color_css(omena_cascade::cascade_property(
4470 declarations.clone(),
4471 "color",
4472 ))?;
4473 let module_rank_keyed_css = legacy_module_rank_keyed_color_css(&declarations)?;
4474
4475 assert_eq!(
4476 linked_order_css.as_bytes(),
4477 module_rank_keyed_css.as_bytes()
4478 );
4479 Ok(())
4480 }
4481
4482 fn definite_color_css(outcome: omena_cascade::CascadeOutcome) -> Result<String, String> {
4483 let omena_cascade::CascadeOutcome::Definite { winner, .. } = outcome else {
4484 return Err("expected definite cascade winner".to_string());
4485 };
4486 let omena_cascade::CascadeValue::Literal(value) = winner.value else {
4487 return Err("expected literal cascade value".to_string());
4488 };
4489 Ok(format!("color:{value};"))
4490 }
4491
4492 fn legacy_module_rank_keyed_color_css(
4493 declarations: &[omena_cascade::CascadeDeclaration],
4494 ) -> Result<String, String> {
4495 let mut matching = declarations.to_vec();
4496 matching.sort_by(|left, right| {
4497 legacy_module_rank_key(right)
4498 .cmp(&legacy_module_rank_key(left))
4499 .then_with(|| right.key.source_order.cmp(&left.key.source_order))
4500 });
4501 let Some(winner) = matching.first() else {
4502 return Err("expected cascade declarations".to_string());
4503 };
4504 let omena_cascade::CascadeValue::Literal(value) = &winner.value else {
4505 return Err("expected literal cascade value".to_string());
4506 };
4507 Ok(format!("color:{value};"))
4508 }
4509
4510 fn legacy_module_rank_key(
4511 declaration: &omena_cascade::CascadeDeclaration,
4512 ) -> (
4513 omena_cascade::CascadeLevel,
4514 omena_cascade::LayerRank,
4515 std::cmp::Reverse<u32>,
4516 omena_cascade::Specificity,
4517 omena_cascade::ModuleRank,
4518 ) {
4519 (
4520 declaration.key.level,
4521 declaration.key.layer_rank,
4522 std::cmp::Reverse(declaration.key.scope_proximity),
4523 declaration.key.specificity,
4524 declaration.open_world_tie_evidence.module_rank,
4525 )
4526 }
4527
4528 #[test]
4529 fn linker_distinguishes_configured_module_instances() {
4530 use omena_parser::{ConfigurationHashV0, ModuleIdV0, ModuleInstanceKeyV0};
4531
4532 let module = ModuleIdV0::new("src/theme.scss");
4533 let blue =
4534 ModuleInstanceKeyV0::new(module.clone(), ConfigurationHashV0::new("with:brand=blue"));
4535 let red = ModuleInstanceKeyV0::new(module, ConfigurationHashV0::new("with:brand=red"));
4536
4537 assert_ne!(blue, red);
4538 assert_eq!(blue.module(), red.module());
4539 assert_ne!(blue.configuration(), red.configuration());
4540 }
4541
4542 #[test]
4543 fn entrypoint_prefers_the_unconfigured_instance() -> Result<(), String> {
4544 let modules = vec![
4545 TransformBundleModuleInputV0::new(
4546 "src/theme.scss",
4547 ".theme { color: black; }",
4548 StyleDialect::Scss,
4549 ),
4550 TransformBundleModuleInputV0::new(
4551 "src/theme.scss",
4552 ".theme { color: blue; }",
4553 StyleDialect::Scss,
4554 )
4555 .with_configuration_hash(ConfigurationHashV0::new("with|5:brand=4:blue")),
4556 TransformBundleModuleInputV0::new(
4557 "src/theme.scss",
4558 ".theme { color: red; }",
4559 StyleDialect::Scss,
4560 )
4561 .with_configuration_hash(ConfigurationHashV0::new("with|5:brand=3:red")),
4562 ];
4563
4564 let linked = link_omena_transform_bundle_modules(&["src/theme.scss"], &modules)
4565 .map_err(|error| format!("unconfigured entrypoint should be selected: {error:?}"))?;
4566 assert_eq!(linked.entrypoints.len(), 1);
4567 assert_eq!(
4568 linked.entrypoints[0].configuration(),
4569 &ConfigurationHashV0::none()
4570 );
4571 Ok(())
4572 }
4573
4574 #[test]
4575 fn entrypoint_without_an_unconfigured_instance_reports_ambiguity() {
4576 let modules = vec![
4577 TransformBundleModuleInputV0::new(
4578 "src/theme.scss",
4579 ".theme { color: blue; }",
4580 StyleDialect::Scss,
4581 )
4582 .with_configuration_hash(ConfigurationHashV0::new("with|5:brand=4:blue")),
4583 TransformBundleModuleInputV0::new(
4584 "src/theme.scss",
4585 ".theme { color: red; }",
4586 StyleDialect::Scss,
4587 )
4588 .with_configuration_hash(ConfigurationHashV0::new("with|5:brand=3:red")),
4589 ];
4590
4591 assert_eq!(
4592 link_omena_transform_bundle_modules(&["src/theme.scss"], &modules),
4593 Err(TransformBundleLinkErrorV0::AmbiguousModulePath {
4594 source_path: "src/theme.scss".to_string(),
4595 })
4596 );
4597 }
4598
4599 #[test]
4600 fn semantic_reachability_input_feeds_closed_world_bundle() -> Result<(), String> {
4601 let modules = vec![TransformBundleModuleInputV0::new(
4602 "Button.module.css",
4603 ".used { color: blue; } .dead { color: red; }",
4604 StyleDialect::Css,
4605 )];
4606 let mut reachability = TransformBundleSemanticReachabilityInputV0::new("Button.module.css");
4607 reachability.class_names.push("used".to_string());
4608
4609 let linked = link_omena_transform_bundle_modules_with_semantic_reachability(
4610 &["Button.module.css"],
4611 &modules,
4612 &[reachability],
4613 )
4614 .map_err(|err| format!("semantic reachability bundle should link: {err:?}"))?;
4615
4616 assert_eq!(
4617 linked.closed_world_bundle.reachability().class_names(),
4618 &["used".to_string()]
4619 );
4620 let instance = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("Button.module.css"));
4621 assert_eq!(
4622 linked
4623 .closed_world_bundle
4624 .module_reachability_evidence(&instance),
4625 ClosedWorldModuleReachabilityEvidenceV0::Supplied
4626 );
4627 Ok(())
4628 }
4629
4630 #[test]
4631 fn analyzed_empty_semantic_reachability_narrows_the_module_to_no_symbols() -> Result<(), String>
4632 {
4633 let modules = vec![TransformBundleModuleInputV0::new(
4634 "Button.module.css",
4635 ".used { color: blue; } .dead { color: red; }",
4636 StyleDialect::Css,
4637 )];
4638 let reachability = TransformBundleSemanticReachabilityInputV0::new("Button.module.css");
4639 let projection = project_omena_transform_bundle_linker_inputs(
4640 modules.as_slice(),
4641 std::slice::from_ref(&reachability),
4642 );
4643 let instance = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("Button.module.css"));
4644
4645 assert_eq!(
4646 projection.module_reachability_analysis(&instance),
4647 TransformBundleReachabilityAnalysisV0::Analyzed
4648 );
4649 assert_eq!(projection.analyzed_empty_reachability_input_count(), 1);
4650 assert_eq!(projection.unanalyzed_reachability_input_count(), 0);
4651 eprintln!(
4652 "REACHABILITY_ANALYSIS_CELL={{\"state\":\"analyzed\",\"cause\":null,\"analyzedEmptyCount\":{},\"unanalyzedCount\":{},\"projectedClassNameCount\":{}}}",
4653 projection.analyzed_empty_reachability_input_count(),
4654 projection.unanalyzed_reachability_input_count(),
4655 projection.inputs()[0].class_names.len(),
4656 );
4657
4658 let linked = link_omena_transform_bundle_modules_with_semantic_reachability(
4659 &["Button.module.css"],
4660 &modules,
4661 &[reachability],
4662 )
4663 .map_err(|err| format!("semantic reachability bundle should link: {err:?}"))?;
4664
4665 assert!(
4666 linked
4667 .closed_world_bundle
4668 .reachability()
4669 .class_names()
4670 .is_empty(),
4671 "an analyzed empty set must not be collapsed into missing analysis"
4672 );
4673 assert_eq!(
4674 linked
4675 .closed_world_bundle
4676 .module_reachability_evidence(&instance),
4677 ClosedWorldModuleReachabilityEvidenceV0::Supplied
4678 );
4679 Ok(())
4680 }
4681
4682 #[test]
4683 fn missing_semantic_reachability_preserves_symbols_with_typed_absence() -> Result<(), String> {
4684 let modules = vec![TransformBundleModuleInputV0::new(
4685 "Button.module.css",
4686 ".used { color: blue; } .dead { color: red; }",
4687 StyleDialect::Css,
4688 )];
4689 let projection = project_omena_transform_bundle_linker_inputs(modules.as_slice(), &[]);
4690 let instance = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("Button.module.css"));
4691
4692 assert_eq!(
4693 projection.module_reachability_analysis(&instance),
4694 TransformBundleReachabilityAnalysisV0::Unanalyzed {
4695 cause: TransformBundleReachabilityUnanalyzedCauseV0::InputNotProvided,
4696 }
4697 );
4698 assert_eq!(projection.analyzed_empty_reachability_input_count(), 0);
4699 assert_eq!(projection.unanalyzed_reachability_input_count(), 1);
4700 eprintln!(
4701 "REACHABILITY_ANALYSIS_CELL={{\"state\":\"unanalyzed\",\"cause\":\"inputNotProvided\",\"analyzedEmptyCount\":{},\"unanalyzedCount\":{},\"projectedClassNameCount\":{}}}",
4702 projection.analyzed_empty_reachability_input_count(),
4703 projection.unanalyzed_reachability_input_count(),
4704 projection.inputs()[0].class_names.len(),
4705 );
4706
4707 let linked = link_omena_transform_bundle_modules_with_semantic_reachability(
4708 &["Button.module.css"],
4709 &modules,
4710 &[],
4711 )
4712 .map_err(|err| format!("semantic reachability bundle should link: {err:?}"))?;
4713
4714 assert_eq!(
4715 linked.closed_world_bundle.reachability().class_names(),
4716 &["dead".to_string(), "used".to_string()]
4717 );
4718 assert_eq!(
4719 linked
4720 .closed_world_bundle
4721 .module_reachability_evidence(&instance),
4722 ClosedWorldModuleReachabilityEvidenceV0::ModuleReachabilityInputAbsent
4723 );
4724 Ok(())
4725 }
4726
4727 #[test]
4728 fn instance_reachability_keeps_configured_consumers_distinct() {
4729 let red = ModuleInstanceKeyV0::new(
4730 ModuleIdV0::new("shared.module.css"),
4731 ConfigurationHashV0::new("with:red"),
4732 );
4733 let blue = ModuleInstanceKeyV0::new(
4734 ModuleIdV0::new("shared.module.css"),
4735 ConfigurationHashV0::new("with:blue"),
4736 );
4737 let mut inputs = vec![
4738 LinkerInputV0 {
4739 source_path: "shared.module.css".to_string(),
4740 dialect: StyleDialect::Css,
4741 instance: red.clone(),
4742 dependency_edges: Vec::new(),
4743 class_names: vec!["alpha".to_string(), "beta".to_string()],
4744 keyframe_names: Vec::new(),
4745 value_names: Vec::new(),
4746 custom_property_names: Vec::new(),
4747 ordered_rules: Vec::new(),
4748 },
4749 LinkerInputV0 {
4750 source_path: "shared.module.css".to_string(),
4751 dialect: StyleDialect::Css,
4752 instance: blue.clone(),
4753 dependency_edges: Vec::new(),
4754 class_names: vec!["alpha".to_string(), "beta".to_string()],
4755 keyframe_names: Vec::new(),
4756 value_names: Vec::new(),
4757 custom_property_names: Vec::new(),
4758 ordered_rules: Vec::new(),
4759 },
4760 ];
4761 let mut red_reachability = TransformBundleInstanceReachabilityInputV0::new(
4762 red.clone(),
4763 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4764 );
4765 red_reachability.class_names.push("alpha".to_string());
4766 let mut blue_reachability = TransformBundleInstanceReachabilityInputV0::new(
4767 blue.clone(),
4768 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4769 );
4770 blue_reachability.class_names.push("beta".to_string());
4771
4772 let (evidence, _, _) = apply_semantic_reachability_to_linker_inputs(
4773 inputs.as_mut_slice(),
4774 &[red_reachability, blue_reachability],
4775 );
4776
4777 carrier_hygiene_assertions::assert_configured_instance_reachability(
4778 inputs[0].class_names.as_slice(),
4779 inputs[1].class_names.as_slice(),
4780 &evidence,
4781 &red,
4782 &blue,
4783 );
4784 }
4785
4786 #[test]
4787 fn instance_reachability_unions_duplicate_rows() {
4788 let instance = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("shared.module.css"));
4789 let mut inputs = vec![LinkerInputV0 {
4790 source_path: "shared.module.css".to_string(),
4791 dialect: StyleDialect::Css,
4792 instance: instance.clone(),
4793 dependency_edges: Vec::new(),
4794 class_names: vec!["alpha".to_string(), "beta".to_string()],
4795 keyframe_names: Vec::new(),
4796 value_names: Vec::new(),
4797 custom_property_names: Vec::new(),
4798 ordered_rules: Vec::new(),
4799 }];
4800 let mut alpha = TransformBundleInstanceReachabilityInputV0::new(
4801 instance.clone(),
4802 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4803 );
4804 alpha.class_names.push("alpha".to_string());
4805 let mut beta = TransformBundleInstanceReachabilityInputV0::new(
4806 instance,
4807 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4808 );
4809 beta.class_names.push("beta".to_string());
4810
4811 apply_semantic_reachability_to_linker_inputs(inputs.as_mut_slice(), &[alpha, beta]);
4812
4813 carrier_hygiene_assertions::assert_duplicate_instance_reachability(
4814 inputs[0].class_names.as_slice(),
4815 );
4816 }
4817
4818 #[test]
4819 fn instance_reachability_dedupes_custom_property_escapes_without_folding_case() {
4820 let instance = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("shared.module.css"));
4821 let mut inputs = vec![LinkerInputV0 {
4822 source_path: "shared.module.css".to_string(),
4823 dialect: StyleDialect::Css,
4824 instance: instance.clone(),
4825 dependency_edges: Vec::new(),
4826 class_names: Vec::new(),
4827 keyframe_names: Vec::new(),
4828 value_names: Vec::new(),
4829 custom_property_names: Vec::new(),
4830 ordered_rules: Vec::new(),
4831 }];
4832 let mut first = TransformBundleInstanceReachabilityInputV0::new(
4833 instance.clone(),
4834 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4835 );
4836 first.custom_property_names.extend([
4837 AuthoredPropertyTextV0::new("--foo"),
4838 AuthoredPropertyTextV0::new(r"--f\6f o"),
4839 ]);
4840 let mut second = TransformBundleInstanceReachabilityInputV0::new(
4841 instance,
4842 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4843 );
4844 second
4845 .custom_property_names
4846 .push(AuthoredPropertyTextV0::new("--FOO"));
4847
4848 apply_semantic_reachability_to_linker_inputs(inputs.as_mut_slice(), &[first, second]);
4849
4850 assert_eq!(
4851 inputs[0]
4852 .custom_property_names
4853 .iter()
4854 .map(AuthoredPropertyTextV0::to_custom_key)
4855 .collect::<Vec<_>>(),
4856 vec![
4857 AuthoredPropertyTextV0::new("--FOO").to_custom_key(),
4858 AuthoredPropertyTextV0::new("--foo").to_custom_key(),
4859 ]
4860 );
4861 }
4862
4863 #[test]
4864 fn legacy_path_reachability_unions_normalized_rows_across_symbol_sets() {
4865 let modules = vec![TransformBundleModuleInputV0::new(
4866 "shared.module.css",
4867 r#"
4868@value primary: red;
4869@value secondary: blue;
4870@keyframes enter { from { opacity: 0; } to { opacity: 1; } }
4871@keyframes leave { from { opacity: 1; } to { opacity: 0; } }
4872:root { --primary: red; --secondary: blue; }
4873.alpha { animation: enter 1s; }
4874.beta { animation: leave 1s; }
4875"#,
4876 StyleDialect::Css,
4877 )];
4878 let mut first = TransformBundleSemanticReachabilityInputV0::new("./shared.module.css");
4879 first.class_names.push("alpha".to_string());
4880 first.keyframe_names.push("enter".to_string());
4881 first.value_names.push("primary".to_string());
4882 first
4883 .custom_property_names
4884 .push(AuthoredPropertyTextV0::new("--primary"));
4885 let mut second = TransformBundleSemanticReachabilityInputV0::new("shared.module.css");
4886 second.class_names.push("beta".to_string());
4887 second.keyframe_names.push("leave".to_string());
4888 second.value_names.push("secondary".to_string());
4889 second
4890 .custom_property_names
4891 .push(AuthoredPropertyTextV0::new("--secondary"));
4892
4893 let projection = project_omena_transform_bundle_linker_inputs(&modules, &[first, second]);
4894 let input = &projection.inputs()[0];
4895
4896 carrier_hygiene_assertions::assert_legacy_path_union(input);
4897 }
4898
4899 fn incomplete_composes_carrier_fixture() -> (
4900 Vec<LinkerInputV0>,
4901 BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleReachabilityEvidenceV0>,
4902 ModuleInstanceKeyV0,
4903 ModuleInstanceKeyV0,
4904 ) {
4905 let source = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("entry.module.css"));
4906 let target = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("base.module.css"));
4907 let mut inputs = vec![
4908 LinkerInputV0 {
4909 source_path: "entry.module.css".to_string(),
4910 dialect: StyleDialect::Css,
4911 instance: source.clone(),
4912 dependency_edges: vec![LinkerDependencyEdgeV0 {
4913 kind: TransformBundleEdgeKind::CssModuleComposesExternal,
4914 import_source: "./base.module.css".to_string(),
4915 import_ordinal: Some(0),
4916 local_names: Vec::new(),
4917 remote_names: vec!["base".to_string()],
4918 }],
4919 class_names: vec!["card".to_string(), "other".to_string()],
4920 keyframe_names: Vec::new(),
4921 value_names: Vec::new(),
4922 custom_property_names: Vec::new(),
4923 ordered_rules: Vec::new(),
4924 },
4925 LinkerInputV0 {
4926 source_path: "base.module.css".to_string(),
4927 dialect: StyleDialect::Css,
4928 instance: target.clone(),
4929 dependency_edges: Vec::new(),
4930 class_names: vec!["base".to_string(), "other".to_string()],
4931 keyframe_names: Vec::new(),
4932 value_names: Vec::new(),
4933 custom_property_names: Vec::new(),
4934 ordered_rules: Vec::new(),
4935 },
4936 ];
4937 let mut source_reachability = TransformBundleInstanceReachabilityInputV0::new(
4938 source.clone(),
4939 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4940 );
4941 source_reachability.class_names.push("card".to_string());
4942 let mut target_reachability = TransformBundleInstanceReachabilityInputV0::new(
4943 target.clone(),
4944 InstanceReachabilityDerivationV0::PathUnionNoInstanceDiscriminator,
4945 );
4946 target_reachability.class_names.push("base".to_string());
4947
4948 let (evidence, _, _) = apply_semantic_reachability_to_linker_inputs(
4949 inputs.as_mut_slice(),
4950 &[source_reachability, target_reachability],
4951 );
4952 (inputs, evidence, source, target)
4953 }
4954
4955 #[test]
4956 fn incomplete_composes_carrier_marks_closure_target_evidence_absent() {
4957 let (_, evidence, source, target) = incomplete_composes_carrier_fixture();
4958 carrier_hygiene_assertions::assert_incomplete_composes_target_evidence(
4959 &evidence, &source, &target,
4960 );
4961 }
4962
4963 #[test]
4964 fn incomplete_composes_carrier_keeps_closure_target_symbols_fail_open() {
4965 let (inputs, _, _, _) = incomplete_composes_carrier_fixture();
4966 carrier_hygiene_assertions::assert_incomplete_composes_target_symbols(
4967 inputs[0].class_names.as_slice(),
4968 inputs[1].class_names.as_slice(),
4969 );
4970 }
4971
4972 #[test]
4973 fn external_composes_names_reach_the_sealed_closed_world_bundle() -> Result<(), String> {
4974 let modules = vec![
4975 TransformBundleModuleInputV0::new(
4976 "entry.module.css",
4977 ".card { composes: base from \"./base.module.css\"; color: red; }",
4978 StyleDialect::Css,
4979 ),
4980 TransformBundleModuleInputV0::new(
4981 "base.module.css",
4982 ".base { padding: 8px; }",
4983 StyleDialect::Css,
4984 ),
4985 ];
4986 let linked = link_omena_transform_bundle_modules(&["entry.module.css"], &modules)
4987 .map_err(|error| format!("composes fixture should link: {error:?}"))?;
4988
4989 let edges = linked.closed_world_bundle.composes_edges();
4990 assert_eq!(edges.len(), 1);
4991 assert_eq!(edges[0].from_module.module().as_str(), "entry.module.css");
4992 assert_eq!(edges[0].from_symbol, "card");
4993 assert_eq!(edges[0].to_module.module().as_str(), "base.module.css");
4994 assert_eq!(edges[0].to_symbol, "base");
4995 Ok(())
4996 }
4997
4998 #[test]
4999 fn composes_closure_expands_module_qualified_semantic_reachability() -> Result<(), String> {
5000 let modules = vec![
5001 TransformBundleModuleInputV0::new(
5002 "entry.module.css",
5003 ".card { composes: base from \"./base.module.css\"; color: red; }",
5004 StyleDialect::Css,
5005 ),
5006 TransformBundleModuleInputV0::new(
5007 "base.module.css",
5008 ".base { padding: 8px; } .other { color: green; }",
5009 StyleDialect::Css,
5010 ),
5011 ];
5012 let mut entry_reachability =
5013 TransformBundleSemanticReachabilityInputV0::new("entry.module.css");
5014 entry_reachability.class_names.push("card".to_string());
5015 let mut base_reachability =
5016 TransformBundleSemanticReachabilityInputV0::new("base.module.css");
5017 base_reachability.class_names.push("other".to_string());
5018
5019 let linked = link_omena_transform_bundle_modules_with_semantic_reachability(
5020 &["base.module.css"],
5021 &modules,
5022 &[entry_reachability, base_reachability],
5023 )
5024 .map_err(|error| format!("composes reachability fixture should link: {error:?}"))?;
5025 let entry = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("entry.module.css"));
5026 let base = ModuleInstanceKeyV0::unconfigured(ModuleIdV0::new("base.module.css"));
5027
5028 assert_eq!(
5029 linked.closed_world_bundle.linked_modules(),
5030 std::slice::from_ref(&base),
5031 "workspace scan evidence must not widen the emission module set"
5032 );
5033 assert_eq!(
5034 linked
5035 .closed_world_bundle
5036 .reachability()
5037 .symbols_for_module(&base)
5038 .map(|symbols| symbols.class_names()),
5039 Some(&["base".to_string(), "other".to_string()][..])
5040 );
5041 assert_eq!(linked.closed_world_bundle.composes_edges().len(), 1);
5042 assert_eq!(
5043 linked
5044 .closed_world_bundle
5045 .composes_origin_symbol_is_reachable(&entry, "card"),
5046 Some(true)
5047 );
5048 Ok(())
5049 }
5050
5051 #[test]
5052 fn projection_linker_core_links_without_module_sources() -> Result<(), String> {
5053 let app = ModuleInstanceKeyV0::new(
5054 ModuleIdV0::new("src/app.module.css"),
5055 ConfigurationHashV0::none(),
5056 );
5057 let theme = ModuleInstanceKeyV0::new(
5058 ModuleIdV0::new("src/theme.css"),
5059 ConfigurationHashV0::none(),
5060 );
5061 let linked = link_stylesheet_from_projection(
5062 &["src/app.module.css"],
5063 &[
5064 LinkerInputV0 {
5065 source_path: "src/app.module.css".to_string(),
5066 dialect: StyleDialect::Css,
5067 instance: app.clone(),
5068 dependency_edges: vec![LinkerDependencyEdgeV0 {
5069 kind: TransformBundleEdgeKind::CssImport,
5070 import_source: "./theme.css".to_string(),
5071 import_ordinal: Some(0),
5072 local_names: Vec::new(),
5073 remote_names: Vec::new(),
5074 }],
5075 class_names: vec!["app".to_string()],
5076 keyframe_names: Vec::new(),
5077 value_names: Vec::new(),
5078 custom_property_names: Vec::new(),
5079 ordered_rules: vec![LinkerRuleV0 {
5080 selector_name: "app".to_string(),
5081 selector_kind: ParsedSelectorFactKind::Class,
5082 range_start: 0,
5083 range_end: 4,
5084 }],
5085 },
5086 LinkerInputV0 {
5087 source_path: "src/theme.css".to_string(),
5088 dialect: StyleDialect::Css,
5089 instance: theme,
5090 dependency_edges: Vec::new(),
5091 class_names: vec!["theme".to_string()],
5092 keyframe_names: Vec::new(),
5093 value_names: Vec::new(),
5094 custom_property_names: vec![AuthoredPropertyTextV0::new("--brand")],
5095 ordered_rules: vec![LinkerRuleV0 {
5096 selector_name: "theme".to_string(),
5097 selector_kind: ParsedSelectorFactKind::Class,
5098 range_start: 0,
5099 range_end: 6,
5100 }],
5101 },
5102 ],
5103 )
5104 .map_err(|err| format!("{err:?}"))?;
5105
5106 assert_eq!(linked.module_instances.len(), 2);
5107 assert_eq!(
5108 linked
5109 .global_rule_order
5110 .rules
5111 .iter()
5112 .map(|rule| rule.selector_name.as_str())
5113 .collect::<Vec<_>>(),
5114 vec!["theme", "app"]
5115 );
5116 assert!(
5117 linked
5118 .closed_world_bundle
5119 .reachability()
5120 .custom_property_names()
5121 .iter()
5122 .any(|name| name.to_custom_key()
5123 == AuthoredPropertyTextV0::new("--brand").to_custom_key())
5124 );
5125 Ok(())
5126 }
5127
5128 #[test]
5129 fn linker_reports_missing_module_dependency() {
5130 let modules = vec![TransformBundleModuleInputV0::new(
5131 "src/app.css",
5132 r#"@import "./missing.css"; .button { color: red; }"#,
5133 StyleDialect::Css,
5134 )];
5135
5136 let err = link_omena_transform_bundle_modules(&["src/app.css"], &modules);
5137
5138 assert_eq!(
5139 err,
5140 Err(TransformBundleLinkErrorV0::MissingDependency {
5141 source_path: "src/app.css".to_string(),
5142 import_source: "./missing.css".to_string(),
5143 })
5144 );
5145 }
5146
5147 #[test]
5148 fn resolved_dependency_carrier_links_package_export_target() -> Result<(), String> {
5149 let modules = vec![
5150 TransformBundleModuleInputV0::new(
5151 "src/app.css",
5152 r#"@import "@acme/theme/tokens.css"; .app { color: green; }"#,
5153 StyleDialect::Css,
5154 ),
5155 TransformBundleModuleInputV0::new(
5156 "node_modules/@acme/theme/dist/tokens.css",
5157 ".token { color: rebeccapurple; }",
5158 StyleDialect::Css,
5159 ),
5160 ];
5161 let projection = project_omena_transform_bundle_linker_inputs(&modules, &[]);
5162 let resolved = TransformBundleResolvedDependencyV0::new(
5163 modules[0].module_instance_key(),
5164 TransformBundleEdgeKind::CssImport,
5165 "@acme/theme/tokens.css",
5166 Some(0),
5167 TransformBundleDependencyResolutionV0::attempted(
5168 vec![
5169 "externalUrlBoundary",
5170 "bundlerPathMapping",
5171 "tsconfigPathMapping",
5172 "sassPkgImporter",
5173 "fileRelativeOrAbsolute",
5174 "packageManifestSubpath",
5175 "nodePackageFallback",
5176 "sassLoadPathRoot",
5177 ],
5178 "packageStyleModule",
5179 1,
5180 Some(modules[1].module_instance_key()),
5181 ),
5182 );
5183
5184 let linked = link_omena_transform_bundle_projection_with_resolved_dependencies_and_options(
5185 &["src/app.css"],
5186 &projection,
5187 std::slice::from_ref(&resolved),
5188 &[],
5189 TransformBundleLinkOptionsV0::default(),
5190 )
5191 .map_err(|error| format!("resolved package export should link: {error:?}"))?;
5192
5193 assert_eq!(linked.module_instances.len(), 2);
5194 assert!(
5195 linked
5196 .module_instances
5197 .contains(&modules[1].module_instance_key())
5198 );
5199 assert_eq!(resolved.resolution.attempt_state, "attempted");
5200 assert_eq!(
5201 resolved.resolution.resolution_kind,
5202 Some("packageStyleModule")
5203 );
5204 assert_eq!(resolved.resolution.policy_step_keys.len(), 8);
5205 Ok(())
5206 }
5207
5208 #[test]
5209 fn resolution_authority_is_enforced_per_dependency_edge() -> Result<(), String> {
5210 let modules = vec![
5211 TransformBundleModuleInputV0::new(
5212 "src/app.css",
5213 r#"@import "./tokens.css"; @import "./theme"; .app { color: green; }"#,
5214 StyleDialect::Css,
5215 ),
5216 TransformBundleModuleInputV0::new(
5217 "src/tokens.css",
5218 ".token { color: rebeccapurple; }",
5219 StyleDialect::Css,
5220 ),
5221 TransformBundleModuleInputV0::new(
5222 "src/theme.scss",
5223 ".theme { color: purple; }",
5224 StyleDialect::Scss,
5225 ),
5226 ];
5227 let projections =
5228 super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
5229 let resolved_tokens = TransformBundleResolvedDependencyV0::new(
5230 modules[0].module_instance_key(),
5231 TransformBundleEdgeKind::CssImport,
5232 "./tokens.css",
5233 Some(0),
5234 TransformBundleDependencyResolutionV0::attempted(
5235 vec!["fileRelativeOrAbsolute"],
5236 "fileRelative",
5237 1,
5238 Some(modules[1].module_instance_key()),
5239 ),
5240 );
5241 let resolved_theme = TransformBundleResolvedDependencyV0::new(
5242 modules[0].module_instance_key(),
5243 TransformBundleEdgeKind::CssImport,
5244 "./theme",
5245 Some(1),
5246 TransformBundleDependencyResolutionV0::attempted(
5247 vec!["fileRelativeOrAbsolute"],
5248 "fileRelative",
5249 1,
5250 Some(modules[2].module_instance_key()),
5251 ),
5252 );
5253
5254 let strict_error = super::link_resolved_bundle(
5255 &["src/app.css"],
5256 projections.linker_projection(),
5257 projections.emission_item_projection(),
5258 std::slice::from_ref(&resolved_tokens),
5259 &[],
5260 super::EmissionOrderingPolicyV0::ImportOrderPreserving,
5261 );
5262 let legacy = super::link_legacy_path_inferred_bundle(
5263 &["src/app.css"],
5264 projections.linker_projection(),
5265 projections.emission_item_projection(),
5266 std::slice::from_ref(&resolved_tokens),
5267 &[],
5268 super::EmissionOrderingPolicyV0::ImportOrderPreserving,
5269 )
5270 .map_err(|error| format!("legacy fallback should link: {error:?}"))?;
5271 let inferred = legacy
5272 .dependency_resolution_disclosures
5273 .iter()
5274 .filter(|disclosure| {
5275 disclosure.authority == super::BundleResolutionAuthorityV0::LegacyPathInferred
5276 })
5277 .collect::<Vec<_>>();
5278 let strict = super::link_resolved_bundle(
5279 &["src/app.css"],
5280 projections.linker_projection(),
5281 projections.emission_item_projection(),
5282 &[resolved_tokens, resolved_theme],
5283 &[],
5284 super::EmissionOrderingPolicyV0::ImportOrderPreserving,
5285 )
5286 .map_err(|error| format!("complete resolved edge set should link: {error:?}"))?;
5287 carrier_hygiene_assertions::assert_resolution_authority(
5288 &strict_error,
5289 inferred.as_slice(),
5290 strict.dependency_resolution_disclosures.as_slice(),
5291 );
5292 Ok(())
5293 }
5294
5295 #[test]
5296 fn emission_item_projection_preserves_the_legacy_selector_order() -> Result<(), String> {
5297 let modules = vec![TransformBundleModuleInputV0::new(
5298 "src/theme.css",
5299 ":root { --brand: red; }\n\
5300 @layer reset;\n\
5301 div, .theme, [hidden], *::before { color: var(--brand); }\n\
5302 @keyframes pulse { from { opacity: 0; } }",
5303 StyleDialect::Css,
5304 )];
5305 let legacy = link_omena_transform_bundle_modules(&["src/theme.css"], &modules)
5306 .map_err(|error| format!("legacy link failed: {error:?}"))?;
5307 let projections =
5308 super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
5309 let widened = super::link_omena_transform_bundle_projection_with_emission_items(
5310 &["src/theme.css"],
5311 projections.linker_projection(),
5312 projections.emission_item_projection(),
5313 &[],
5314 )
5315 .map_err(|error| format!("emission-item link failed: {error:?}"))?;
5316
5317 assert_eq!(widened.linked_stylesheet, legacy);
5318 assert_eq!(
5319 widened
5320 .linked_stylesheet
5321 .global_rule_order
5322 .rules
5323 .iter()
5324 .map(|rule| (rule.global_order_index, rule.selector_name.as_str()))
5325 .collect::<Vec<_>>(),
5326 vec![(0, "theme")]
5327 );
5328 assert!(widened.emission_item_order.items.iter().any(|item| {
5329 item.kind == super::EmissionItemKindV0::SelectorPseudoClass && item.name == ":root"
5330 }));
5331 assert!(widened.emission_item_order.items.iter().any(|item| {
5332 item.kind == super::EmissionItemKindV0::KeyframesDeclaration && item.name == "pulse"
5333 }));
5334 assert!(
5335 widened
5336 .emission_item_order
5337 .items
5338 .windows(2)
5339 .all(|pair| pair[0].global_order_index + 1 == pair[1].global_order_index)
5340 );
5341 Ok(())
5342 }
5343
5344 #[test]
5345 fn emission_admission_separates_legacy_success_from_requested_projection_failure() {
5346 let modules = vec![TransformBundleModuleInputV0::new(
5347 "src/app.css",
5348 ".app { color: green; }",
5349 StyleDialect::Css,
5350 )];
5351 let projections =
5352 super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
5353 let empty_emission_projection =
5354 super::TransformBundleEmissionItemProjectionV0::new(Vec::new());
5355
5356 let admission =
5357 super::evaluate_omena_transform_bundle_projection_emission_admission_with_resolved_dependencies_and_options(
5358 &["src/app.css"],
5359 projections.linker_projection(),
5360 &empty_emission_projection,
5361 &[],
5362 &[],
5363 TransformBundleLinkOptionsV0::default(),
5364 );
5365
5366 assert!(!admission.module_id_legacy_open());
5367 assert!(matches!(
5368 admission.requested_policy_result(),
5369 Err(TransformBundleLinkErrorV0::InvalidEmissionPlan { reason })
5370 if reason.contains("has no emission-item input")
5371 ));
5372 }
5373
5374 #[test]
5375 fn emission_admission_marks_both_paths_open_when_preparation_fails() {
5376 let modules = vec![TransformBundleModuleInputV0::new(
5377 "src/app.css",
5378 ".app { color: green; }",
5379 StyleDialect::Css,
5380 )];
5381 let projections =
5382 super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
5383
5384 let admission =
5385 super::evaluate_omena_transform_bundle_projection_emission_admission_with_resolved_dependencies_and_options(
5386 &["src/missing.css"],
5387 projections.linker_projection(),
5388 projections.emission_item_projection(),
5389 &[],
5390 &[],
5391 TransformBundleLinkOptionsV0::default(),
5392 );
5393
5394 assert!(admission.module_id_legacy_open());
5395 assert!(matches!(
5396 admission.requested_policy_result(),
5397 Err(TransformBundleLinkErrorV0::MissingEntrypoint { source_path })
5398 if source_path == "src/missing.css"
5399 ));
5400 }
5401
5402 #[test]
5403 fn emission_item_projection_parses_each_module_once() -> Result<(), String> {
5404 let modules = vec![
5405 TransformBundleModuleInputV0::new(
5406 "src/reset.css",
5407 "html { box-sizing: border-box; }",
5408 StyleDialect::Css,
5409 ),
5410 TransformBundleModuleInputV0::new(
5411 "src/theme.css",
5412 ":root { --brand: red; }",
5413 StyleDialect::Css,
5414 ),
5415 TransformBundleModuleInputV0::new(
5416 "src/app.css",
5417 ".app { color: var(--brand); }",
5418 StyleDialect::Css,
5419 ),
5420 ];
5421 let (linked, parser_snapshot) =
5422 omena_parser::with_omena_parser_parse_instrumentation(|| {
5423 let projections =
5424 super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
5425 super::link_omena_transform_bundle_projection_with_emission_items(
5426 &["src/reset.css", "src/theme.css", "src/app.css"],
5427 projections.linker_projection(),
5428 projections.emission_item_projection(),
5429 &[],
5430 )
5431 });
5432 let linked = linked.map_err(|error| format!("emission-item link failed: {error:?}"))?;
5433
5434 assert_eq!(parser_snapshot.parse_invocation_count, 3);
5435 assert_eq!(linked.linked_stylesheet.module_instances.len(), 3);
5436 Ok(())
5437 }
5438
5439 fn materialize_with_emission_items(
5440 entrypoint: &str,
5441 modules: &[TransformBundleModuleInputV0],
5442 transformed_css: &[(&str, &str)],
5443 ) -> Result<
5444 (
5445 super::LinkedStylesheetWithEmissionItemsV0,
5446 super::LinkedEmissionArtifactV0,
5447 ),
5448 String,
5449 > {
5450 let projections =
5451 super::project_omena_transform_bundle_linker_and_emission_items(modules, &[]);
5452 let linked = super::link_omena_transform_bundle_projection_with_emission_items(
5453 &[entrypoint],
5454 projections.linker_projection(),
5455 projections.emission_item_projection(),
5456 &[],
5457 )
5458 .map_err(|error| format!("emission-item link failed: {error:?}"))?;
5459 let transformed_modules = transformed_css
5460 .iter()
5461 .map(|(source_path, css)| {
5462 let module = modules
5463 .iter()
5464 .find(|module| module.source_path == *source_path)
5465 .ok_or_else(|| format!("missing module input for {source_path}"))?;
5466 Ok(TransformBundleTransformedModuleV0::new(
5467 module.module_instance_key(),
5468 *css,
5469 ))
5470 })
5471 .collect::<Result<Vec<_>, String>>()?;
5472 let artifact =
5473 super::materialize_omena_transform_bundle_linked_stylesheet_with_emission_items(
5474 &linked,
5475 &transformed_modules,
5476 )
5477 .map_err(|error| format!("emission-item materialization failed: {error:?}"))?;
5478 Ok((linked, artifact))
5479 }
5480
5481 #[test]
5482 fn emission_items_place_element_only_import_before_the_importer() -> Result<(), String> {
5483 let modules = [
5484 TransformBundleModuleInputV0::new(
5485 "src/app.css",
5486 "@import \"./reset.css\"; div { color: red; }",
5487 StyleDialect::Css,
5488 ),
5489 TransformBundleModuleInputV0::new(
5490 "src/reset.css",
5491 "div { color: green; }",
5492 StyleDialect::Css,
5493 ),
5494 ];
5495 let (linked, artifact) = materialize_with_emission_items(
5496 "src/app.css",
5497 &modules,
5498 &[
5499 ("src/app.css", "div { color: red; }"),
5500 ("src/reset.css", "div { color: green; }"),
5501 ],
5502 )?;
5503
5504 let mut seen_modules = BTreeSet::new();
5505 let first_module_occurrences = linked
5506 .emission_item_order
5507 .items
5508 .iter()
5509 .filter_map(|item| {
5510 seen_modules
5511 .insert(item.module_instance.clone())
5512 .then_some(item.module_instance.module().as_str())
5513 })
5514 .collect::<Vec<_>>();
5515 assert_eq!(
5516 first_module_occurrences,
5517 vec!["src/reset.css", "src/app.css"]
5518 );
5519 assert_eq!(
5520 artifact.output_css,
5521 "div { color: green; }\ndiv { color: red; }"
5522 );
5523 Ok(())
5524 }
5525
5526 #[test]
5527 fn emission_items_do_not_relocate_an_element_rule_past_named_rules() -> Result<(), String> {
5528 let modules = [
5529 TransformBundleModuleInputV0::new(
5530 "src/app.css",
5531 "@import \"./reset.css\"; .card { padding: 1px; } div { color: red; }",
5532 StyleDialect::Css,
5533 ),
5534 TransformBundleModuleInputV0::new(
5535 "src/reset.css",
5536 "div { color: green; }",
5537 StyleDialect::Css,
5538 ),
5539 ];
5540 let (_, artifact) = materialize_with_emission_items(
5541 "src/app.css",
5542 &modules,
5543 &[
5544 ("src/app.css", ".card { padding: 1px; } div { color: red; }"),
5545 ("src/reset.css", "div { color: green; }"),
5546 ],
5547 )?;
5548
5549 let green = artifact
5550 .output_css
5551 .find("green")
5552 .ok_or_else(|| "missing imported declaration".to_string())?;
5553 let card = artifact
5554 .output_css
5555 .find(".card")
5556 .ok_or_else(|| "missing named selector declaration".to_string())?;
5557 let red = artifact
5558 .output_css
5559 .find("red")
5560 .ok_or_else(|| "missing importing declaration".to_string())?;
5561 assert!(green < card && card < red);
5562 Ok(())
5563 }
5564
5565 #[test]
5566 fn emission_item_placement_is_independent_of_module_names() -> Result<(), String> {
5567 let modules = [
5568 TransformBundleModuleInputV0::new(
5569 "src/zzz-app.css",
5570 "@import \"./aaa-reset.css\"; div { color: red; }",
5571 StyleDialect::Css,
5572 ),
5573 TransformBundleModuleInputV0::new(
5574 "src/aaa-reset.css",
5575 "div { color: green; }",
5576 StyleDialect::Css,
5577 ),
5578 ];
5579 let (_, artifact) = materialize_with_emission_items(
5580 "src/zzz-app.css",
5581 &modules,
5582 &[
5583 ("src/zzz-app.css", "div { color: red; }"),
5584 ("src/aaa-reset.css", "div { color: green; }"),
5585 ],
5586 )?;
5587
5588 assert_eq!(
5589 artifact.output_css,
5590 "div { color: green; }\ndiv { color: red; }"
5591 );
5592 Ok(())
5593 }
5594
5595 #[test]
5596 fn emission_items_preserve_cascade_layer_declaration_order() -> Result<(), String> {
5597 let modules = [
5598 TransformBundleModuleInputV0::new(
5599 "src/app.css",
5600 "@import \"./layers.css\"; @layer theme { .card { color: blue; } } \
5601 @layer base { .card { color: orange; } }",
5602 StyleDialect::Css,
5603 ),
5604 TransformBundleModuleInputV0::new(
5605 "src/layers.css",
5606 "@layer base, theme;",
5607 StyleDialect::Css,
5608 ),
5609 ];
5610 let (_, artifact) = materialize_with_emission_items(
5611 "src/app.css",
5612 &modules,
5613 &[
5614 (
5615 "src/app.css",
5616 "@layer theme { .card { color: blue; } } \
5617 @layer base { .card { color: orange; } }",
5618 ),
5619 ("src/layers.css", "@layer base, theme;"),
5620 ],
5621 )?;
5622
5623 assert!(artifact.output_css.starts_with("@layer base, theme;"));
5624 assert!(
5625 artifact
5626 .output_css
5627 .find("@layer base, theme;")
5628 .unwrap_or(usize::MAX)
5629 < artifact.output_css.find("@layer theme").unwrap_or_default()
5630 );
5631 Ok(())
5632 }
5633
5634 #[test]
5635 fn emission_item_materializer_preserves_empty_module_placement() -> Result<(), String> {
5636 let modules = [
5637 TransformBundleModuleInputV0::new(
5638 "src/app.css",
5639 "@import \"./empty.css\"; @import \"./license.css\"; .app { color: red; }",
5640 StyleDialect::Css,
5641 ),
5642 TransformBundleModuleInputV0::new("src/empty.css", "", StyleDialect::Css),
5643 TransformBundleModuleInputV0::new(
5644 "src/license.css",
5645 "/* license */",
5646 StyleDialect::Css,
5647 ),
5648 ];
5649 let projections =
5650 super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
5651 let linked = super::link_omena_transform_bundle_projection_with_emission_items(
5652 &["src/app.css"],
5653 projections.linker_projection(),
5654 projections.emission_item_projection(),
5655 &[],
5656 )
5657 .map_err(|error| format!("emission-item link failed: {error:?}"))?;
5658 let transformed = modules
5659 .iter()
5660 .map(|module| {
5661 let output_css = match module.source_path.as_str() {
5662 "src/app.css" => ".app { color: red; }",
5663 "src/license.css" => "/* license */",
5664 _ => "",
5665 };
5666 TransformBundleTransformedModuleV0::new(
5667 module.module_instance_key(),
5668 output_css.to_string(),
5669 )
5670 })
5671 .collect::<Vec<_>>();
5672
5673 let artifact =
5674 super::materialize_omena_transform_bundle_linked_stylesheet_with_emission_items(
5675 &linked,
5676 &transformed,
5677 )
5678 .map_err(|error| format!("emission-item materialization failed: {error:?}"))?;
5679 assert_eq!(artifact.emitted_module_count, 3);
5680 assert_eq!(
5681 artifact
5682 .module_regions
5683 .iter()
5684 .map(|region| region.module_instance.module().as_str())
5685 .collect::<Vec<_>>(),
5686 ["src/empty.css", "src/license.css", "src/app.css"]
5687 );
5688 assert_eq!(artifact.output_css, "/* license */\n.app { color: red; }");
5689 Ok(())
5690 }
5691
5692 #[test]
5693 fn emission_item_materializer_rejects_incomplete_module_coverage() -> Result<(), String> {
5694 let modules = [
5695 TransformBundleModuleInputV0::new(
5696 "src/app.css",
5697 "@import \"./reset.css\"; .app { color: red; }",
5698 StyleDialect::Css,
5699 ),
5700 TransformBundleModuleInputV0::new(
5701 "src/reset.css",
5702 "html { box-sizing: border-box; }",
5703 StyleDialect::Css,
5704 ),
5705 ];
5706 let projections =
5707 super::project_omena_transform_bundle_linker_and_emission_items(&modules, &[]);
5708 let mut linked = super::link_omena_transform_bundle_projection_with_emission_items(
5709 &["src/app.css"],
5710 projections.linker_projection(),
5711 projections.emission_item_projection(),
5712 &[],
5713 )
5714 .map_err(|error| format!("emission-item link failed: {error:?}"))?;
5715 let missing_module = modules[1].module_instance_key();
5716 linked
5717 .emission_item_order
5718 .items
5719 .retain(|item| item.module_instance != missing_module);
5720 for (index, item) in linked.emission_item_order.items.iter_mut().enumerate() {
5721 item.global_order_index = u32::try_from(index)
5722 .map_err(|_| "emission-item test order exceeds u32".to_string())?;
5723 }
5724 let transformed = modules
5725 .iter()
5726 .map(|module| {
5727 TransformBundleTransformedModuleV0::new(
5728 module.module_instance_key(),
5729 module.source.clone(),
5730 )
5731 })
5732 .collect::<Vec<_>>();
5733
5734 match super::materialize_omena_transform_bundle_linked_stylesheet_with_emission_items(
5735 &linked,
5736 &transformed,
5737 ) {
5738 Err(super::LinkedEmissionItemMaterializationErrorV0::MissingEmissionItem {
5739 module_instance,
5740 }) if module_instance == missing_module => Ok(()),
5741 result => Err(format!(
5742 "incomplete emission-item coverage returned an unexpected result: {result:?}"
5743 )),
5744 }
5745 }
5746}