Skip to content

Commit 58f1cb4

Browse files
Merge pull request #226 from piercefreeman/feature/ignore-client-css
Ignore client css
1 parent 201cb82 commit 58f1cb4

File tree

1 file changed

+264
-1
lines changed

1 file changed

+264
-1
lines changed

src/bundle_common.rs

Lines changed: 264 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use indexmap::IndexMap;
22
use log::debug;
33
use rolldown::{
4-
Bundler, BundlerOptions, InputItem, OutputFormat, RawMinifyOptions, ResolveOptions,
4+
Bundler, BundlerOptions, InputItem, ModuleType, OutputFormat, RawMinifyOptions, ResolveOptions,
55
SourceMapType,
66
};
77
use rustc_hash::FxHasher;
@@ -242,6 +242,16 @@ pub fn bundle_common(
242242
Some(OutputFormat::Esm)
243243
},
244244
minify: Some(RawMinifyOptions::Bool(minify)),
245+
// For SSR, treat CSS files as empty modules since they can't be executed in V8
246+
// CSS imports are client-only - setting them to Empty completely removes them from the bundle
247+
module_types: if is_ssr {
248+
let mut types: HashMap<String, ModuleType, rustc_hash::FxBuildHasher> =
249+
HashMap::with_hasher(rustc_hash::FxBuildHasher);
250+
types.insert(".css".to_string(), ModuleType::Empty);
251+
Some(types)
252+
} else {
253+
None
254+
},
245255
// Add additional options as needed
246256
..Default::default()
247257
};
@@ -680,4 +690,257 @@ mod tests {
680690
);
681691
}
682692
}
693+
694+
#[test]
695+
fn test_ssr_bundle_excludes_css_imports() {
696+
// Create a temporary directory for test files
697+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
698+
let temp_path = temp_dir.path();
699+
700+
// Create a CSS file
701+
let css_content = ".test { color: red; }";
702+
let css_path = temp_path.join("style.css");
703+
let mut css_file = File::create(&css_path).unwrap();
704+
css_file.write_all(css_content.as_bytes()).unwrap();
705+
706+
// Create a JavaScript file that imports CSS (side-effect import)
707+
// Note: We need to export SSR so it doesn't get tree-shaken away
708+
let server_js = r#"
709+
import './style.css';
710+
711+
export var SSR = {
712+
x: () => '<div>Hello World</div>'
713+
};
714+
"#;
715+
716+
let entry_path = create_test_js_file(temp_path, "server.js", server_js)
717+
.expect("Failed to create server.js file");
718+
719+
// Create a mock node_modules path
720+
let node_modules_path = temp_path.join("node_modules").to_string_lossy().to_string();
721+
fs::create_dir(temp_path.join("node_modules"))
722+
.expect("Failed to create node_modules directory");
723+
724+
// Bundle for SSR (SingleServer mode)
725+
let result = bundle_common(
726+
vec![entry_path],
727+
BundleMode::SingleServer,
728+
"production".to_string(),
729+
node_modules_path,
730+
None,
731+
None,
732+
false,
733+
);
734+
735+
// The bundle should succeed (CSS is external, so no error)
736+
assert!(
737+
result.is_ok(),
738+
"SSR bundle with CSS import should succeed: {:?}",
739+
result.err()
740+
);
741+
742+
let bundles = result.unwrap();
743+
let bundle_result = bundles.entrypoints.iter().next().unwrap().1;
744+
745+
// The bundle should NOT contain any CSS content or __*_css variable references
746+
assert!(
747+
!bundle_result.script.contains("color: red"),
748+
"SSR bundle should not contain CSS content"
749+
);
750+
751+
// Check for CSS variable pattern
752+
let has_css_var = bundle_result.script.contains("_css");
753+
assert!(
754+
!has_css_var,
755+
"SSR bundle should not contain CSS variable references"
756+
);
757+
758+
// The bundle should still contain our SSR component
759+
assert!(
760+
bundle_result.script.contains("Hello World"),
761+
"SSR bundle should contain the component output"
762+
);
763+
}
764+
765+
#[test]
766+
fn test_ssr_bundle_excludes_node_modules_css() {
767+
// Test that CSS imports from node_modules are also excluded
768+
// This mirrors the real-world case: import '@xyflow/react/dist/style.css'
769+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
770+
let temp_path = temp_dir.path();
771+
772+
// Create a fake node_modules structure with CSS
773+
let node_modules = temp_path.join("node_modules");
774+
let fake_package = node_modules.join("@fake-lib").join("dist");
775+
fs::create_dir_all(&fake_package).expect("Failed to create fake package directory");
776+
777+
let css_path = fake_package.join("styles.css");
778+
let mut css_file = File::create(&css_path).unwrap();
779+
css_file
780+
.write_all(b".fake-lib { display: flex; }")
781+
.unwrap();
782+
783+
// Create JS that imports the node_modules CSS
784+
let server_js = r#"
785+
import '@fake-lib/dist/styles.css';
786+
787+
export var SSR = {
788+
x: () => '<div>Component with external CSS</div>'
789+
};
790+
"#;
791+
792+
let entry_path = create_test_js_file(temp_path, "server.js", server_js)
793+
.expect("Failed to create server.js file");
794+
795+
let result = bundle_common(
796+
vec![entry_path],
797+
BundleMode::SingleServer,
798+
"production".to_string(),
799+
node_modules.to_string_lossy().to_string(),
800+
None,
801+
None,
802+
false,
803+
);
804+
805+
assert!(
806+
result.is_ok(),
807+
"SSR bundle with node_modules CSS import should succeed: {:?}",
808+
result.err()
809+
);
810+
811+
let bundles = result.unwrap();
812+
let bundle_result = bundles.entrypoints.iter().next().unwrap().1;
813+
814+
// Should not contain CSS content or variable references
815+
assert!(
816+
!bundle_result.script.contains("display: flex"),
817+
"SSR bundle should not contain node_modules CSS content"
818+
);
819+
assert!(
820+
!bundle_result.script.contains("_css"),
821+
"SSR bundle should not contain CSS variable references from node_modules"
822+
);
823+
824+
// Should contain the component
825+
assert!(
826+
bundle_result.script.contains("Component with external CSS"),
827+
"SSR bundle should contain the component"
828+
);
829+
}
830+
831+
#[test]
832+
fn test_ssr_bundle_excludes_multiple_css_imports() {
833+
// Test multiple CSS imports are all excluded
834+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
835+
let temp_path = temp_dir.path();
836+
837+
// Create multiple CSS files
838+
for (name, content) in [
839+
("reset.css", "* { margin: 0; }"),
840+
("theme.css", ":root { --color: blue; }"),
841+
("components.css", ".btn { padding: 10px; }"),
842+
] {
843+
let css_path = temp_path.join(name);
844+
let mut css_file = File::create(&css_path).unwrap();
845+
css_file.write_all(content.as_bytes()).unwrap();
846+
}
847+
848+
let server_js = r#"
849+
import './reset.css';
850+
import './theme.css';
851+
import './components.css';
852+
853+
export var SSR = {
854+
x: () => '<div>Multi-CSS Component</div>'
855+
};
856+
"#;
857+
858+
let entry_path = create_test_js_file(temp_path, "server.js", server_js)
859+
.expect("Failed to create server.js file");
860+
861+
let node_modules_path = temp_path.join("node_modules").to_string_lossy().to_string();
862+
fs::create_dir(temp_path.join("node_modules"))
863+
.expect("Failed to create node_modules directory");
864+
865+
let result = bundle_common(
866+
vec![entry_path],
867+
BundleMode::SingleServer,
868+
"production".to_string(),
869+
node_modules_path,
870+
None,
871+
None,
872+
false,
873+
);
874+
875+
assert!(
876+
result.is_ok(),
877+
"SSR bundle with multiple CSS imports should succeed: {:?}",
878+
result.err()
879+
);
880+
881+
let bundles = result.unwrap();
882+
let bundle_result = bundles.entrypoints.iter().next().unwrap().1;
883+
884+
// None of the CSS content should be present
885+
assert!(!bundle_result.script.contains("margin: 0"));
886+
assert!(!bundle_result.script.contains("--color: blue"));
887+
assert!(!bundle_result.script.contains("padding: 10px"));
888+
assert!(!bundle_result.script.contains("_css"));
889+
890+
// Component should be present
891+
assert!(bundle_result.script.contains("Multi-CSS Component"));
892+
}
893+
894+
#[test]
895+
fn test_client_bundle_does_not_exclude_css() {
896+
// Verify that client bundles (non-SSR) don't have CSS excluded
897+
// We only set module_types to Empty for SSR mode, not client mode
898+
let temp_dir = TempDir::new().expect("Failed to create temp directory");
899+
let temp_path = temp_dir.path();
900+
901+
let css_path = temp_path.join("style.css");
902+
let mut css_file = File::create(&css_path).unwrap();
903+
css_file.write_all(b".client { color: green; }").unwrap();
904+
905+
let client_js = r#"
906+
import './style.css';
907+
908+
export function render() {
909+
return '<div class="client">Client Component</div>';
910+
}
911+
"#;
912+
913+
let entry_path = create_test_js_file(temp_path, "client.js", client_js)
914+
.expect("Failed to create client.js file");
915+
916+
let node_modules_path = temp_path.join("node_modules").to_string_lossy().to_string();
917+
fs::create_dir(temp_path.join("node_modules"))
918+
.expect("Failed to create node_modules directory");
919+
920+
// Use SingleClient mode (not SSR)
921+
let result = bundle_common(
922+
vec![entry_path],
923+
BundleMode::SingleClient,
924+
"development".to_string(),
925+
node_modules_path,
926+
None,
927+
None,
928+
false,
929+
);
930+
931+
// The key assertion: bundling should succeed without errors
932+
// (CSS is processed by rolldown, not treated as empty)
933+
assert!(
934+
result.is_ok(),
935+
"Client bundle with CSS import should succeed: {:?}",
936+
result.err()
937+
);
938+
939+
// Verify we got some output (rolldown may extract CSS to separate file)
940+
let bundles = result.unwrap();
941+
assert!(
942+
!bundles.entrypoints.is_empty() || !bundles.extras.is_empty(),
943+
"Client bundle should produce output"
944+
);
945+
}
683946
}

0 commit comments

Comments
 (0)