1- use std:: { borrow :: Cow , path:: Path , sync :: LazyLock } ;
1+ use std:: path:: { Path , PathBuf } ;
22
3- use regex:: Regex ;
43use rspack_collections:: { IdentifierMap , IdentifierSet } ;
5- use rspack_core:: Compilation ;
4+ use rspack_core:: { Compilation , Module , SourceType } ;
65use rspack_util:: fx_hash:: { FxHashMap , FxHashSet } ;
76use sugar_path:: SugarPath ;
87
98use crate :: EsmLibraryPlugin ;
109
11- static EXTENSION_JS : LazyLock < Regex > =
12- LazyLock :: new ( || Regex :: new ( r".+(\..+)$" ) . expect ( "failed to compile EXTENSION_REGEXP" ) ) ;
13-
1410pub fn entry_modules ( compilation : & Compilation ) -> FxHashMap < String , IdentifierSet > {
1511 let module_graph = compilation. get_module_graph ( ) ;
1612 compilation
@@ -50,6 +46,42 @@ pub fn entry_name_for_module(
5046 entry_name_for_module
5147}
5248
49+ /// Returns whether a module should be processed by `preserve_modules`.
50+ /// Only JS and CSS modules are relevant — asset, wasm and other types are
51+ /// handled by their own output pipelines.
52+ fn should_preserve_module ( module : & dyn Module , module_graph : & rspack_core:: ModuleGraph ) -> bool {
53+ let source_types = module. source_types ( module_graph) ;
54+ if source_types. iter ( ) . any ( |t| {
55+ matches ! (
56+ t,
57+ SourceType :: JavaScript | SourceType :: Css | SourceType :: CssImport
58+ )
59+ } ) {
60+ return true ;
61+ }
62+ // CSS modules created by the extract-css plugin use a `Custom` source type
63+ // (`css/mini-extract`). Detect them by their identifier prefix to keep
64+ // the dependency on `rspack_plugin_extract_css` out of this crate.
65+ module. identifier ( ) . starts_with ( "css|" )
66+ }
67+
68+ /// Returns the absolute path of a module's resource, supporting both
69+ /// `NormalModule` and the synthetic `CssModule` from extract-css.
70+ fn module_resource_path ( module : & dyn Module ) -> Option < PathBuf > {
71+ if let Some ( normal_module) = module. as_normal_module ( ) {
72+ return normal_module
73+ . resource_resolved_data ( )
74+ . path ( )
75+ . map ( |p| p. as_std_path ( ) . to_path_buf ( ) ) ;
76+ }
77+ // For non-normal modules (extract-css `CssModule`), derive the resource
78+ // path from `name_for_condition`, which strips the loader chain and query
79+ // string.
80+ let name = module. name_for_condition ( ) ?;
81+ let path = PathBuf :: from ( name. as_ref ( ) ) ;
82+ if path. is_absolute ( ) { Some ( path) } else { None }
83+ }
84+
5385pub async fn preserve_modules (
5486 root : & Path ,
5587 compilation : & mut Compilation ,
@@ -75,163 +107,172 @@ pub async fn preserve_modules(
75107 continue ;
76108 }
77109
78- let module_graph = compilation. get_module_graph ( ) ;
79- let Some ( normal_module) = module_graph
80- . module_by_identifier ( & module_id)
81- . expect ( "should have module" )
82- . as_normal_module ( )
83- else {
84- continue ;
85- } ;
110+ let abs_path = {
111+ let module_graph = compilation. get_module_graph ( ) ;
112+ let module = module_graph
113+ . module_by_identifier ( & module_id)
114+ . expect ( "should have module" ) ;
86115
87- let Some ( abs_path ) = normal_module
88- . resource_resolved_data ( )
89- . path ( )
90- . map ( |p| p . as_std_path ( ) )
91- . map ( |p| p . to_path_buf ( ) )
92- else {
93- continue ;
116+ if ! should_preserve_module ( module . as_ref ( ) , module_graph ) {
117+ continue ;
118+ }
119+ let Some ( abs_path ) = module_resource_path ( module . as_ref ( ) ) else {
120+ continue ;
121+ } ;
122+ abs_path
94123 } ;
124+ if !abs_path. starts_with ( root) {
125+ continue ;
126+ }
127+
95128 let chunk = match EsmLibraryPlugin :: get_module_chunk ( module_id, compilation) {
96129 Ok ( c) => c,
97130 Err ( e) => {
98131 errors. push ( e. into ( ) ) ;
99132 continue ;
100133 }
101134 } ;
102- let old_chunk = compilation
103- . build_chunk_graph_artifact
104- . chunk_by_ukey
105- . expect_get_mut ( & chunk) ;
106-
107- if abs_path. starts_with ( root) {
108- // split module into single chunk named root
109- let file_path = abs_path. relative ( root) ;
110- let extension = file_path. extension ( ) ;
111-
112- let new_extension = old_chunk
113- . filename_template ( )
114- . unwrap_or ( & compilation. options . output . filename )
115- . template ( )
116- . map_or ( Cow :: Borrowed ( ".js" ) , |tpl| {
117- if let Some ( captures) = EXTENSION_JS . captures ( tpl) {
118- Cow :: Owned ( captures[ 1 ] . to_string ( ) )
119- } else {
120- Cow :: Borrowed ( ".js" )
121- }
122- } ) ;
123- let new_filename = if let Some ( extension) = extension {
124- let ext_lossy = extension. to_string_lossy ( ) ;
125- let file_path_lossy = file_path. to_string_lossy ( ) ;
126- let suffix = format ! ( ".{ext_lossy}" ) ;
127- let base = file_path_lossy
128- . strip_suffix ( & suffix)
129- . unwrap_or ( & file_path_lossy) ;
130- format ! ( "{base}{new_extension}" ) . into ( )
131- } else {
132- file_path. to_string_lossy ( ) . to_string ( ) . into ( )
133- } ;
134135
135- let entry_name = if let Some ( entry_names) = entry_name_for_module. get ( & module_id) {
136- if entry_names. len ( ) > 1 {
137- errors. push (
138- rspack_error:: error!(
139- "{} is used in multiple entries: [{}], this is not allowed in preserveModules" ,
140- module_id,
141- entry_names
142- . iter( )
143- . map( |s| s. as_str( ) )
144- . collect:: <Vec <_>>( )
145- . join( ", " )
146- )
147- . into ( ) ,
148- ) ;
149- continue ;
150- }
136+ // Compute the `chunk.name` from the source path.
137+ //
138+ // Following Rollup's approach: strip the file extension and let the
139+ // per-type output template add the correct one back. rspack has separate
140+ // templates for each output type (`output.filename` → `[name].mjs` for
141+ // JS, `output.cssFilename` → `[name].css` for CSS, etc.), so we always
142+ // strip the source extension regardless of module type. This keeps
143+ // preserve_modules completely type-agnostic for naming.
144+ let file_path = abs_path. relative ( root) ;
145+ // `to_slash()` normalises to forward slashes on all platforms so chunk
146+ // names (and the asset paths derived from them) stay consistent.
147+ let file_path_str = file_path
148+ . to_slash ( )
149+ . expect ( "relative path should be valid UTF-8" )
150+ . into_owned ( ) ;
151+ let base_name: String = if let Some ( extension) = file_path. extension ( ) {
152+ let suffix = format ! ( ".{}" , extension. to_string_lossy( ) ) ;
153+ file_path_str
154+ . strip_suffix ( & suffix)
155+ . unwrap_or ( & file_path_str)
156+ . to_string ( )
157+ } else {
158+ file_path_str
159+ } ;
151160
152- entry_names. iter ( ) . next ( ) . cloned ( )
153- } else {
154- None
155- } ;
161+ let entry_name = if let Some ( entry_names) = entry_name_for_module. get ( & module_id) {
162+ if entry_names. len ( ) > 1 {
163+ errors. push (
164+ rspack_error:: error!(
165+ "{} is used in multiple entries: [{}], this is not allowed in preserveModules" ,
166+ module_id,
167+ entry_names
168+ . iter( )
169+ . map( |s| s. as_str( ) )
170+ . collect:: <Vec <_>>( )
171+ . join( ", " )
172+ )
173+ . into ( ) ,
174+ ) ;
175+ continue ;
176+ }
156177
157- if compilation
178+ entry_names. iter ( ) . next ( ) . cloned ( )
179+ } else {
180+ None
181+ } ;
182+
183+ if compilation
184+ . build_chunk_graph_artifact
185+ . chunk_graph
186+ . get_chunk_modules_identifier ( & chunk)
187+ . len ( )
188+ == 1
189+ {
190+ // This is the last module in the chunk — rename in-place.
191+ let old_chunk = compilation
158192 . build_chunk_graph_artifact
159- . chunk_graph
160- . get_chunk_modules_identifier ( & chunk)
161- . len ( )
162- == 1
193+ . chunk_by_ukey
194+ . expect_get_mut ( & chunk) ;
195+ if let Some ( old_name ) = old_chunk . name ( ) . map ( |s| s . to_string ( ) )
196+ && old_name != base_name
163197 {
164- // this is last module in chunk, we can keep this chunk, just rename it
165- old_chunk. set_filename_template ( Some ( new_filename) ) ;
166- continue ;
198+ compilation
199+ . build_chunk_graph_artifact
200+ . named_chunks
201+ . remove ( & old_name) ;
167202 }
168-
169- let new_chunk_ukey =
170- Compilation :: add_chunk ( & mut compilation. build_chunk_graph_artifact . chunk_by_ukey ) ;
203+ old_chunk. set_name ( Some ( base_name. clone ( ) ) ) ;
171204 compilation
172205 . build_chunk_graph_artifact
173- . chunk_graph
174- . add_chunk ( new_chunk_ukey) ;
175- let [ Some ( new_chunk) , Some ( old_chunk) ] = compilation
176- . build_chunk_graph_artifact
177- . chunk_by_ukey
178- . get_many_mut ( [ & new_chunk_ukey, & chunk] )
179- else {
180- unreachable ! ( "new_chunk and old_chunk should be inserted already" )
181- } ;
206+ . named_chunks
207+ . insert ( base_name, chunk) ;
208+ continue ;
209+ }
182210
183- new_chunk. set_filename_template ( Some ( new_filename) ) ;
184- old_chunk. split (
185- new_chunk,
186- & mut compilation. build_chunk_graph_artifact . chunk_group_by_ukey ,
187- ) ;
188- // disconnect module from other chunks
211+ let new_chunk_ukey =
212+ Compilation :: add_chunk ( & mut compilation. build_chunk_graph_artifact . chunk_by_ukey ) ;
213+ compilation
214+ . build_chunk_graph_artifact
215+ . chunk_graph
216+ . add_chunk ( new_chunk_ukey) ;
217+ let [ Some ( new_chunk) , Some ( old_chunk) ] = compilation
218+ . build_chunk_graph_artifact
219+ . chunk_by_ukey
220+ . get_many_mut ( [ & new_chunk_ukey, & chunk] )
221+ else {
222+ unreachable ! ( "new_chunk and old_chunk should be inserted already" )
223+ } ;
224+
225+ new_chunk. set_name ( Some ( base_name. clone ( ) ) ) ;
226+ old_chunk. split (
227+ new_chunk,
228+ & mut compilation. build_chunk_graph_artifact . chunk_group_by_ukey ,
229+ ) ;
230+ compilation
231+ . build_chunk_graph_artifact
232+ . named_chunks
233+ . insert ( base_name. clone ( ) , new_chunk_ukey) ;
234+
235+ // disconnect module from other chunks
236+ compilation
237+ . build_chunk_graph_artifact
238+ . chunk_graph
239+ . disconnect_chunk_and_module ( & chunk, module_id) ;
240+
241+ compilation
242+ . build_chunk_graph_artifact
243+ . chunk_graph
244+ . connect_chunk_and_module ( new_chunk_ukey, module_id) ;
245+
246+ if let Some ( entry_name) = entry_name {
189247 compilation
190248 . build_chunk_graph_artifact
191249 . chunk_graph
192- . disconnect_chunk_and_module ( & chunk, module_id) ;
250+ . disconnect_chunk_and_entry_module ( & chunk, module_id) ;
251+
252+ let entrypoint = compilation. entrypoint_by_name_mut ( & entry_name) ;
253+ let ukey = entrypoint. ukey ;
254+ entrypoint. set_entrypoint_chunk ( new_chunk_ukey) ;
193255
194256 compilation
195257 . build_chunk_graph_artifact
196258 . chunk_graph
197- . connect_chunk_and_module ( new_chunk_ukey, module_id) ;
198-
199- if let Some ( entry_name) = entry_name {
200- compilation
201- . build_chunk_graph_artifact
202- . chunk_graph
203- . disconnect_chunk_and_entry_module ( & chunk, module_id) ;
204-
205- let entrypoint = compilation. entrypoint_by_name_mut ( & entry_name) ;
206- let ukey = entrypoint. ukey ;
207- entrypoint. set_entrypoint_chunk ( new_chunk_ukey) ;
259+ . connect_chunk_and_entry_module ( new_chunk_ukey, module_id, ukey) ;
208260
209- compilation
210- . build_chunk_graph_artifact
211- . chunk_graph
212- . connect_chunk_and_entry_module ( new_chunk_ukey, module_id, ukey) ;
213-
214- // Transfer the chunk name from the old entry chunk to the new chunk
215- // to avoid duplicate output filenames. Without this, the old chunk
216- // retains its entry name (e.g., "index") and if it has modules outside
217- // the root (which don't get a filename_template), its output falls back
218- // to `[name].mjs` → "index.mjs", conflicting with the new chunk's
219- // filename_template "index.mjs".
220- let old_chunk = compilation
221- . build_chunk_graph_artifact
222- . chunk_by_ukey
223- . expect_get_mut ( & chunk) ;
224- if let Some ( name) = old_chunk. name ( ) . map ( |s| s. to_string ( ) ) {
225- old_chunk. set_name ( None ) ;
226- let new_chunk = compilation
227- . build_chunk_graph_artifact
228- . chunk_by_ukey
229- . expect_get_mut ( & new_chunk_ukey) ;
230- new_chunk. set_name ( Some ( name. clone ( ) ) ) ;
261+ // Remove the entry name from the old chunk to avoid filename conflicts.
262+ // Without this, the old chunk retains its entry name (e.g. "index") and
263+ // its output falls back to `[name].mjs` → "index.mjs", conflicting with
264+ // the new chunk that already owns that name.
265+ let old_chunk = compilation
266+ . build_chunk_graph_artifact
267+ . chunk_by_ukey
268+ . expect_get_mut ( & chunk) ;
269+ if let Some ( old_name) = old_chunk. name ( ) . map ( |s| s. to_string ( ) ) {
270+ old_chunk. set_name ( None ) ;
271+ if old_name != base_name {
231272 compilation
232273 . build_chunk_graph_artifact
233274 . named_chunks
234- . insert ( name , new_chunk_ukey ) ;
275+ . remove ( & old_name ) ;
235276 }
236277 }
237278 }
0 commit comments