|
1 | 1 | use indexmap::IndexMap; |
2 | 2 | use log::debug; |
3 | 3 | use rolldown::{ |
4 | | - Bundler, BundlerOptions, InputItem, OutputFormat, RawMinifyOptions, ResolveOptions, |
| 4 | + Bundler, BundlerOptions, InputItem, ModuleType, OutputFormat, RawMinifyOptions, ResolveOptions, |
5 | 5 | SourceMapType, |
6 | 6 | }; |
7 | 7 | use rustc_hash::FxHasher; |
@@ -242,6 +242,16 @@ pub fn bundle_common( |
242 | 242 | Some(OutputFormat::Esm) |
243 | 243 | }, |
244 | 244 | 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 | + }, |
245 | 255 | // Add additional options as needed |
246 | 256 | ..Default::default() |
247 | 257 | }; |
@@ -680,4 +690,257 @@ mod tests { |
680 | 690 | ); |
681 | 691 | } |
682 | 692 | } |
| 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 | + } |
683 | 946 | } |
0 commit comments