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