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,167 @@ 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- } ;
134-
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- }
151135
152- entry_names. iter ( ) . next ( ) . cloned ( )
153- } else {
154- None
155- } ;
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+ let file_path_lossy = file_path. to_string_lossy ( ) . into_owned ( ) ;
146+ let base_name: String = if let Some ( extension) = file_path. extension ( ) {
147+ let suffix = format ! ( ".{}" , extension. to_string_lossy( ) ) ;
148+ file_path_lossy
149+ . strip_suffix ( & suffix)
150+ . unwrap_or ( & file_path_lossy)
151+ . to_string ( )
152+ } else {
153+ file_path_lossy
154+ } ;
156155
157- if compilation
158- . build_chunk_graph_artifact
159- . chunk_graph
160- . get_chunk_modules_identifier ( & chunk)
161- . len ( )
162- == 1
163- {
164- // this is last module in chunk, we can keep this chunk, just rename it
165- old_chunk. set_filename_template ( Some ( new_filename) ) ;
156+ let entry_name = if let Some ( entry_names) = entry_name_for_module. get ( & module_id) {
157+ if entry_names. len ( ) > 1 {
158+ errors. push (
159+ rspack_error:: error!(
160+ "{} is used in multiple entries: [{}], this is not allowed in preserveModules" ,
161+ module_id,
162+ entry_names
163+ . iter( )
164+ . map( |s| s. as_str( ) )
165+ . collect:: <Vec <_>>( )
166+ . join( ", " )
167+ )
168+ . into ( ) ,
169+ ) ;
166170 continue ;
167171 }
168172
169- let new_chunk_ukey =
170- Compilation :: add_chunk ( & mut compilation. build_chunk_graph_artifact . chunk_by_ukey ) ;
171- compilation
172- . build_chunk_graph_artifact
173- . chunk_graph
174- . add_chunk ( new_chunk_ukey) ;
175- let [ Some ( new_chunk) , Some ( old_chunk) ] = compilation
173+ entry_names. iter ( ) . next ( ) . cloned ( )
174+ } else {
175+ None
176+ } ;
177+
178+ if compilation
179+ . build_chunk_graph_artifact
180+ . chunk_graph
181+ . get_chunk_modules_identifier ( & chunk)
182+ . len ( )
183+ == 1
184+ {
185+ // This is the last module in the chunk — rename in-place.
186+ let old_chunk = compilation
176187 . build_chunk_graph_artifact
177188 . 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- } ;
189+ . expect_get_mut ( & chunk) ;
190+ if let Some ( old_name) = old_chunk. name ( ) . map ( |s| s. to_string ( ) ) {
191+ if old_name != base_name {
192+ compilation
193+ . build_chunk_graph_artifact
194+ . named_chunks
195+ . remove ( & old_name) ;
196+ }
197+ }
198+ old_chunk. set_name ( Some ( base_name. clone ( ) ) ) ;
199+ compilation
200+ . build_chunk_graph_artifact
201+ . named_chunks
202+ . insert ( base_name, chunk) ;
203+ continue ;
204+ }
182205
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
206+ let new_chunk_ukey =
207+ Compilation :: add_chunk ( & mut compilation. build_chunk_graph_artifact . chunk_by_ukey ) ;
208+ compilation
209+ . build_chunk_graph_artifact
210+ . chunk_graph
211+ . add_chunk ( new_chunk_ukey) ;
212+ let [ Some ( new_chunk) , Some ( old_chunk) ] = compilation
213+ . build_chunk_graph_artifact
214+ . chunk_by_ukey
215+ . get_many_mut ( [ & new_chunk_ukey, & chunk] )
216+ else {
217+ unreachable ! ( "new_chunk and old_chunk should be inserted already" )
218+ } ;
219+
220+ new_chunk. set_name ( Some ( base_name. clone ( ) ) ) ;
221+ old_chunk. split (
222+ new_chunk,
223+ & mut compilation. build_chunk_graph_artifact . chunk_group_by_ukey ,
224+ ) ;
225+ compilation
226+ . build_chunk_graph_artifact
227+ . named_chunks
228+ . insert ( base_name. clone ( ) , new_chunk_ukey) ;
229+
230+ // disconnect module from other chunks
231+ compilation
232+ . build_chunk_graph_artifact
233+ . chunk_graph
234+ . disconnect_chunk_and_module ( & chunk, module_id) ;
235+
236+ compilation
237+ . build_chunk_graph_artifact
238+ . chunk_graph
239+ . connect_chunk_and_module ( new_chunk_ukey, module_id) ;
240+
241+ if let Some ( entry_name) = entry_name {
189242 compilation
190243 . build_chunk_graph_artifact
191244 . chunk_graph
192- . disconnect_chunk_and_module ( & chunk, module_id) ;
245+ . disconnect_chunk_and_entry_module ( & chunk, module_id) ;
246+
247+ let entrypoint = compilation. entrypoint_by_name_mut ( & entry_name) ;
248+ let ukey = entrypoint. ukey ;
249+ entrypoint. set_entrypoint_chunk ( new_chunk_ukey) ;
193250
194251 compilation
195252 . build_chunk_graph_artifact
196253 . 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) ;
208-
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 ( ) ) ) ;
254+ . connect_chunk_and_entry_module ( new_chunk_ukey, module_id, ukey) ;
255+
256+ // Remove the entry name from the old chunk to avoid filename conflicts.
257+ // Without this, the old chunk retains its entry name (e.g. "index") and
258+ // its output falls back to `[name].mjs` → "index.mjs", conflicting with
259+ // the new chunk that already owns that name.
260+ let old_chunk = compilation
261+ . build_chunk_graph_artifact
262+ . chunk_by_ukey
263+ . expect_get_mut ( & chunk) ;
264+ if let Some ( old_name) = old_chunk. name ( ) . map ( |s| s. to_string ( ) ) {
265+ old_chunk. set_name ( None ) ;
266+ if old_name != base_name {
231267 compilation
232268 . build_chunk_graph_artifact
233269 . named_chunks
234- . insert ( name , new_chunk_ukey ) ;
270+ . remove ( & old_name ) ;
235271 }
236272 }
237273 }
0 commit comments