Repository: swc-project/swc (rust)

Issue:
[css/modules] composes chain is broken due to order of class names
### Describe the bug

After merging https://github.com/swc-project/swc/pull/8218, allows out-of-order class names for composes.
However composes chain is still affected by order of class names.

### Input code

```typescript
.root-class {
    composes: chain1;
}
.chain1 {
    composes: chain2;
}
.chain2 {
}
```


### Config

_No response_

### Playground link (or link to the minimal reproduction)

https://github.com/thiry1/swc/commit/0f027ca3e2355ffb31c58bc5f94ce3c4f949655e

### SWC Info output

_No response_

### Expected behavior

```diff
{
  "chain1": [
    {
      "name": "__local__chain1",
      "type": "local"
    },
    {
      "name": "__local__chain2",
      "type": "local"
    }
  ],
  "chain2": [
    {
      "name": "__local__chain2",
      "type": "local"
    }
  ],
  "root-class": [
    {
      "name": "__local__root-class",
      "type": "local"
    },
    {
      "name": "__local__chain1",
      "type": "local"
    },
+  {
+    "name": "__local__chain2",
+    "type": "local"
+  }
  ]
}

```

### Actual behavior

```diff
{
  "chain1": [
    {
      "name": "__local__chain1",
      "type": "local"
    },
    {
      "name": "__local__chain2",
      "type": "local"
    }
  ],
  "chain2": [
    {
      "name": "__local__chain2",
      "type": "local"
    }
  ],
  "root-class": [
    {
      "name": "__local__root-class",
      "type": "local"
    },
    {
      "name": "__local__chain1",
      "type": "local"
    },
-    {
-      "name": "__local__chain2",
-      "type": "local"
-    }
  ]
}
```

### Version

1.3.96

### Additional context

_No response_

Pull request description:
fix(css/modules): Allow any order of composes
**Description:**


**Related issue:**

 - Closes #8240

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index c0a7b910f6..1dfae89d49 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4099,6 +4099,7 @@ dependencies = [
 name = "swc_css_modules"
 version = "0.29.35"
 dependencies = [
+ "indexmap 2.1.0",
  "rustc-hash",
  "serde",
  "serde_json",
diff --git a/crates/swc_css_modules/Cargo.toml b/crates/swc_css_modules/Cargo.toml
index 3a7d1f6004..c384f43988 100644
--- a/crates/swc_css_modules/Cargo.toml
+++ b/crates/swc_css_modules/Cargo.toml
@@ -26,6 +26,7 @@ swc_css_parser  = { version = "0.150.33", path = "../swc_css_parser" }
 swc_css_visit   = { version = "0.139.22", path = "../swc_css_visit" }
 
 [dev-dependencies]
+indexmap   = { workspace = true, features = ["serde"] }
 serde_json = { workspace = true }
 
 swc_css_compat = { version = "0.27.35", path = "../swc_css_compat" }
diff --git a/crates/swc_css_modules/src/lib.rs b/crates/swc_css_modules/src/lib.rs
index a0cc0a8c15..a408691839 100644
--- a/crates/swc_css_modules/src/lib.rs
+++ b/crates/swc_css_modules/src/lib.rs
@@ -60,6 +60,50 @@ pub fn compile<'a>(ss: &mut Stylesheet, config: impl 'a + TransformConfig) -> Tr
 
     ss.visit_mut_with(&mut compiler);
 
+    fn add(result: &mut TransformResult, data: &Data, key: &JsWord, composes: &[CssClassName]) {
+        let mut extra_classes = vec![];
+        {
+            let class_names = result.renamed.entry(key.clone()).or_default();
+
+            class_names.extend(composes.iter().cloned());
+        }
+
+        for composed_class_name in composes.iter() {
+            if let CssClassName::Local { name } = composed_class_name {
+                if let Some(original_class_name) = data.renamed_to_orig.get(&name.value) {
+                    extra_classes.extend(
+                        result
+                            .renamed
+                            .entry(original_class_name.clone())
+                            .or_default()
+                            .split_at(1)
+                            .1
+                            .to_vec(),
+                    );
+                }
+            }
+        }
+
+        {
+            let class_names = result.renamed.entry(key.clone()).or_default();
+
+            class_names.extend(extra_classes);
+        }
+    }
+
+    let composes = compiler.data.composes_inherit.take();
+
+    for (key, composes) in &composes {
+        add(&mut compiler.result, &compiler.data, key, composes);
+    }
+    for (key, composes) in &composes {
+        add(&mut compiler.result, &compiler.data, key, composes);
+    }
+    compiler.result.renamed.iter_mut().for_each(|(_, v)| {
+        v.sort();
+        v.dedup();
+    });
+
     compiler.result
 }
 
@@ -76,6 +120,7 @@ where
 struct Data {
     /// Context for `composes`
     composes_for_current: Option<Vec<CssClassName>>,
+    composes_inherit: Vec<(JsWord, Vec<CssClassName>)>,
 
     renamed_to_orig: FxHashMap<JsWord, JsWord>,
     orig_to_renamed: FxHashMap<JsWord, JsWord>,
@@ -214,27 +259,7 @@ where
                                     .cloned();
 
                                 if let Some(key) = key {
-                                    let mut renamed = self.result.renamed.clone();
-                                    let class_names = self.result.renamed.entry(key).or_default();
-
-                                    class_names.extend(composes.clone());
-
-                                    for composed_class_name in composes.iter() {
-                                        if let CssClassName::Local { name } = composed_class_name {
-                                            if let Some(original_class_name) =
-                                                self.data.renamed_to_orig.get(&name.value)
-                                            {
-                                                class_names.extend(
-                                                    renamed
-                                                        .entry(original_class_name.clone())
-                                                        .or_default()
-                                                        .split_at(1)
-                                                        .1
-                                                        .to_vec(),
-                                                );
-                                            }
-                                        }
-                                    }
+                                    self.data.composes_inherit.push((key, composes.clone()));
                                 }
                             }
                         }


Test patch:
diff --git a/crates/swc_css_modules/tests/fixture.rs b/crates/swc_css_modules/tests/fixture.rs
index 051ee81806..6294a8e81b 100644
--- a/crates/swc_css_modules/tests/fixture.rs
+++ b/crates/swc_css_modules/tests/fixture.rs
@@ -1,6 +1,6 @@
 use std::path::PathBuf;
 
-use rustc_hash::FxHashMap;
+use indexmap::IndexMap;
 use serde::Serialize;
 use swc_atoms::JsWord;
 use swc_css_codegen::{
@@ -89,7 +89,7 @@ fn compile(input: PathBuf) {
             .unwrap();
 
         if !transform_result.renamed.is_empty() {
-            let transformed_classes = &transform_result
+            let mut transformed_classes = transform_result
                 .renamed
                 .into_iter()
                 .map(|(k, v)| {
@@ -113,7 +113,9 @@ fn compile(input: PathBuf) {
                             .collect::<Vec<_>>(),
                     )
                 })
-                .collect::<FxHashMap<_, _>>();
+                .collect::<IndexMap<_, _>>();
+
+            transformed_classes.sort_keys();
 
             NormalizedOutput::compare_json_to_file(
                 &transformed_classes,
diff --git a/crates/swc_css_modules/tests/fixture/issue-8240.compiled.css b/crates/swc_css_modules/tests/fixture/issue-8240.compiled.css
new file mode 100644
index 0000000000..6c866ff78e
--- /dev/null
+++ b/crates/swc_css_modules/tests/fixture/issue-8240.compiled.css
@@ -0,0 +1,3 @@
+.__local__root-class {}
+.__local__chain1 {}
+.__local__chain2 {}
diff --git a/crates/swc_css_modules/tests/fixture/issue-8240.css b/crates/swc_css_modules/tests/fixture/issue-8240.css
new file mode 100644
index 0000000000..00e99b1a5b
--- /dev/null
+++ b/crates/swc_css_modules/tests/fixture/issue-8240.css
@@ -0,0 +1,8 @@
+.root-class {
+    composes: chain1;
+}
+.chain1 {
+    composes: chain2;
+}
+.chain2 {
+}
\ No newline at end of file
diff --git a/crates/swc_css_modules/tests/fixture/issue-8240.transform.json b/crates/swc_css_modules/tests/fixture/issue-8240.transform.json
new file mode 100644
index 0000000000..6306d9a900
--- /dev/null
+++ b/crates/swc_css_modules/tests/fixture/issue-8240.transform.json
@@ -0,0 +1,32 @@
+{
+    "chain1": [
+        {
+            "name": "__local__chain1",
+            "type": "local"
+        },
+        {
+            "name": "__local__chain2",
+            "type": "local"
+        }
+    ],
+    "chain2": [
+        {
+            "name": "__local__chain2",
+            "type": "local"
+        }
+    ],
+    "root-class": [
+        {
+            "name": "__local__root-class",
+            "type": "local"
+        },
+        {
+            "name": "__local__chain1",
+            "type": "local"
+        },
+        {
+            "name": "__local__chain2",
+            "type": "local"
+        }
+    ]
+}
diff --git a/crates/swc_css_modules/tests/fixture/modules/issue-7737/issue-7737.transform.json b/crates/swc_css_modules/tests/fixture/modules/issue-7737/issue-7737.transform.json
index eb2ecd0fc4..315945e49c 100644
--- a/crates/swc_css_modules/tests/fixture/modules/issue-7737/issue-7737.transform.json
+++ b/crates/swc_css_modules/tests/fixture/modules/issue-7737/issue-7737.transform.json
@@ -1,86 +1,86 @@
 {
-  "chain2": [
-    {
-      "type": "local",
-      "name": "__local__chain2"
-    },
-    {
-      "type": "global",
-      "name": "e"
-    },
-    {
-      "type": "import",
-      "name": "f",
-      "from": "./f.css"
-    }
-  ],
-  "chain1": [
-    {
-      "type": "local",
-      "name": "__local__chain1"
-    },
-    {
-      "type": "local",
-      "name": "__local__chain2"
-    },
-    {
-      "type": "global",
-      "name": "c"
-    },
-    {
-      "type": "import",
-      "name": "d",
-      "from": "./d.css"
-    },
-    {
-      "type": "global",
-      "name": "e"
-    },
-    {
-      "type": "import",
-      "name": "f",
-      "from": "./f.css"
-    }
-  ],
-  "root-class": [
-    {
-      "type": "local",
-      "name": "__local__root-class"
-    },
-    {
-      "type": "local",
-      "name": "__local__chain1"
-    },
-    {
-      "type": "global",
-      "name": "a"
-    },
-    {
-      "type": "import",
-      "name": "b",
-      "from": "./b.css"
-    },
-    {
-      "type": "local",
-      "name": "__local__chain2"
-    },
-    {
-      "type": "global",
-      "name": "c"
-    },
-    {
-      "type": "import",
-      "name": "d",
-      "from": "./d.css"
-    },
-    {
-      "type": "global",
-      "name": "e"
-    },
-    {
-      "type": "import",
-      "name": "f",
-      "from": "./f.css"
-    }
-  ]
+    "chain1": [
+        {
+            "name": "__local__chain1",
+            "type": "local"
+        },
+        {
+            "name": "__local__chain2",
+            "type": "local"
+        },
+        {
+            "name": "e",
+            "type": "global"
+        },
+        {
+            "name": "c",
+            "type": "global"
+        },
+        {
+            "from": "./f.css",
+            "name": "f",
+            "type": "import"
+        },
+        {
+            "from": "./d.css",
+            "name": "d",
+            "type": "import"
+        }
+    ],
+    "chain2": [
+        {
+            "name": "__local__chain2",
+            "type": "local"
+        },
+        {
+            "name": "e",
+            "type": "global"
+        },
+        {
+            "from": "./f.css",
+            "name": "f",
+            "type": "import"
+        }
+    ],
+    "root-class": [
+        {
+            "name": "__local__chain2",
+            "type": "local"
+        },
+        {
+            "name": "__local__root-class",
+            "type": "local"
+        },
+        {
+            "name": "__local__chain1",
+            "type": "local"
+        },
+        {
+            "name": "e",
+            "type": "global"
+        },
+        {
+            "name": "c",
+            "type": "global"
+        },
+        {
+            "name": "a",
+            "type": "global"
+        },
+        {
+            "from": "./f.css",
+            "name": "f",
+            "type": "import"
+        },
+        {
+            "from": "./d.css",
+            "name": "d",
+            "type": "import"
+        },
+        {
+            "from": "./b.css",
+            "name": "b",
+            "type": "import"
+        }
+    ]
 }
diff --git a/crates/swc_css_modules/tests/fixture/modules/tests-cases/composes-1/source.transform.json b/crates/swc_css_modules/tests/fixture/modules/tests-cases/composes-1/source.transform.json
index 6aa2e89e42..2f1ba30000 100644
--- a/crates/swc_css_modules/tests/fixture/modules/tests-cases/composes-1/source.transform.json
+++ b/crates/swc_css_modules/tests/fixture/modules/tests-cases/composes-1/source.transform.json
@@ -1,44 +1,44 @@
 {
-  "c1": [
-    {
-      "type": "local",
-      "name": "__local__c1"
-    },
-    {
-      "type": "import",
-      "name": "c2",
-      "from": "./file.css"
-    }
-  ],
-  "c3": [
-    {
-      "type": "local",
-      "name": "__local__c3"
-    },
-    {
-      "type": "local",
-      "name": "__local__c1"
-    },
-    {
-      "type": "import",
-      "name": "c2",
-      "from": "./file.css"
-    }
-  ],
-  "c5": [
-    {
-      "type": "local",
-      "name": "__local__c5"
-    },
-    {
-      "type": "import",
-      "name": "c2",
-      "from": "./file.css"
-    },
-    {
-      "type": "import",
-      "name": "c4",
-      "from": "./file.css"
-    }
-  ]
+    "c1": [
+        {
+            "name": "__local__c1",
+            "type": "local"
+        },
+        {
+            "from": "./file.css",
+            "name": "c2",
+            "type": "import"
+        }
+    ],
+    "c3": [
+        {
+            "name": "__local__c3",
+            "type": "local"
+        },
+        {
+            "name": "__local__c1",
+            "type": "local"
+        },
+        {
+            "from": "./file.css",
+            "name": "c2",
+            "type": "import"
+        }
+    ],
+    "c5": [
+        {
+            "name": "__local__c5",
+            "type": "local"
+        },
+        {
+            "from": "./file.css",
+            "name": "c2",
+            "type": "import"
+        },
+        {
+            "from": "./file.css",
+            "name": "c4",
+            "type": "import"
+        }
+    ]
 }
diff --git a/crates/swc_css_modules/tests/fixture/modules/tests-cases/composes-2/source.transform.json b/crates/swc_css_modules/tests/fixture/modules/tests-cases/composes-2/source.transform.json
index c7fcc9f0b7..ffc56ec7ce 100644
--- a/crates/swc_css_modules/tests/fixture/modules/tests-cases/composes-2/source.transform.json
+++ b/crates/swc_css_modules/tests/fixture/modules/tests-cases/composes-2/source.transform.json
@@ -1,44 +1,44 @@
 {
-  "c1": [
-    {
-      "type": "local",
-      "name": "__local__c1"
-    },
-    {
-      "type": "import",
-      "name": "c-2",
-      "from": "./file.css"
-    }
-  ],
-  "c3": [
-    {
-      "type": "local",
-      "name": "__local__c3"
-    },
-    {
-      "type": "local",
-      "name": "__local__c1"
-    },
-    {
-      "type": "import",
-      "name": "c-2",
-      "from": "./file.css"
-    }
-  ],
-  "c5": [
-    {
-      "type": "local",
-      "name": "__local__c5"
-    },
-    {
-      "type": "import",
-      "name": "c-2",
-      "from": "./file.css"
-    },
-    {
-      "type": "import",
-      "name": "c4",
-      "from": "./file.css"
-    }
-  ]
+    "c1": [
+        {
+            "name": "__local__c1",
+            "type": "local"
+        },
+        {
+            "from": "./file.css",
+            "name": "c-2",
+            "type": "import"
+        }
+    ],
+    "c3": [
+        {
+            "name": "__local__c3",
+            "type": "local"
+        },
+        {
+            "name": "__local__c1",
+            "type": "local"
+        },
+        {
+            "from": "./file.css",
+            "name": "c-2",
+            "type": "import"
+        }
+    ],
+    "c5": [
+        {
+            "name": "__local__c5",
+            "type": "local"
+        },
+        {
+            "from": "./file.css",
+            "name": "c-2",
+            "type": "import"
+        },
+        {
+            "from": "./file.css",
+            "name": "c4",
+            "type": "import"
+        }
+    ]
 }