@@ -206,44 +206,76 @@ type RenderChunkHook = (
206206 code : string ,
207207 chunk : {
208208 fileName : string ;
209+ facadeModuleId ?: string | null ;
209210 }
210211) => {
211212 code : string ;
212213 map : SourceMap ;
213214} | null ;
214215
216+ /**
217+ * Checks if a chunk should be skipped for code injection
218+ *
219+ * This is necessary to handle Vite's MPA (multi-page application) mode where
220+ * HTML entry points create "facade" chunks that should not contain injected code.
221+ * See: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829
222+ *
223+ * @param code - The chunk's code content
224+ * @param facadeModuleId - The facade module ID (if any) - HTML files create facade chunks
225+ * @returns true if the chunk should be skipped
226+ */
227+ function shouldSkipCodeInjection ( code : string , facadeModuleId : string | null | undefined ) : boolean {
228+ // Skip empty chunks - these are placeholder chunks that should be optimized away
229+ if ( code . trim ( ) . length === 0 ) {
230+ return true ;
231+ }
232+
233+ // Skip HTML facade chunks
234+ // They only contain import statements and should not have Sentry code injected
235+ if ( facadeModuleId && stripQueryAndHashFromPath ( facadeModuleId ) . endsWith ( ".html" ) ) {
236+ return true ;
237+ }
238+
239+ return false ;
240+ }
241+
215242export function createRollupReleaseInjectionHooks ( injectionCode : string ) : {
216243 renderChunk : RenderChunkHook ;
217244} {
218245 return {
219- renderChunk ( code : string , chunk : { fileName : string } ) {
246+ renderChunk ( code : string , chunk : { fileName : string ; facadeModuleId ?: string | null } ) {
220247 if (
221248 // chunks could be any file (html, md, ...)
222- [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) =>
249+ ! [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) =>
223250 stripQueryAndHashFromPath ( chunk . fileName ) . endsWith ( ending )
224251 )
225252 ) {
226- const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
253+ return null ; // returning null means not modifying the chunk at all
254+ }
227255
228- const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
256+ // Skip empty chunks and HTML facade chunks (Vite MPA)
257+ if ( shouldSkipCodeInjection ( code , chunk . facadeModuleId ) ) {
258+ return null ;
259+ }
229260
230- if ( match ) {
231- // Add injected code after any comments or "use strict" at the beginning of the bundle.
232- ms . appendLeft ( match . length , injectionCode ) ;
233- } else {
234- // ms.replace() doesn't work when there is an empty string match (which happens if
235- // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
236- // need this special case here.
237- ms . prepend ( injectionCode ) ;
238- }
261+ const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
239262
240- return {
241- code : ms . toString ( ) ,
242- map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
243- } ;
263+ const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
264+
265+ if ( match ) {
266+ // Add injected code after any comments or "use strict" at the beginning of the bundle.
267+ ms . appendLeft ( match . length , injectionCode ) ;
244268 } else {
245- return null ; // returning null means not modifying the chunk at all
269+ // ms.replace() doesn't work when there is an empty string match (which happens if
270+ // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
271+ // need this special case here.
272+ ms . prepend ( injectionCode ) ;
246273 }
274+
275+ return {
276+ code : ms . toString ( ) ,
277+ map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
278+ } ;
247279 } ,
248280 } ;
249281}
@@ -262,48 +294,53 @@ export function createRollupDebugIdInjectionHooks(): {
262294 renderChunk : RenderChunkHook ;
263295} {
264296 return {
265- renderChunk ( code : string , chunk : { fileName : string } ) {
297+ renderChunk ( code : string , chunk : { fileName : string ; facadeModuleId ?: string | null } ) {
266298 if (
267299 // chunks could be any file (html, md, ...)
268- [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) =>
300+ ! [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) =>
269301 stripQueryAndHashFromPath ( chunk . fileName ) . endsWith ( ending )
270302 )
271303 ) {
272- // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI)
273- const chunkStartSnippet = code . slice ( 0 , 6000 ) ;
274- const chunkEndSnippet = code . slice ( - 500 ) ;
275-
276- if (
277- chunkStartSnippet . includes ( "_sentryDebugIdIdentifier" ) ||
278- chunkEndSnippet . includes ( "//# debugId=" )
279- ) {
280- return null ; // Debug ID already present, skip injection
281- }
304+ return null ; // returning null means not modifying the chunk at all
305+ }
306+
307+ // Skip empty chunks and HTML facade chunks (Vite MPA)
308+ if ( shouldSkipCodeInjection ( code , chunk . facadeModuleId ) ) {
309+ return null ;
310+ }
282311
283- const debugId = stringToUUID ( code ) ; // generate a deterministic debug ID
284- const codeToInject = getDebugIdSnippet ( debugId ) ;
312+ // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI)
313+ const chunkStartSnippet = code . slice ( 0 , 6000 ) ;
314+ const chunkEndSnippet = code . slice ( - 500 ) ;
285315
286- const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
316+ if (
317+ chunkStartSnippet . includes ( "_sentryDebugIdIdentifier" ) ||
318+ chunkEndSnippet . includes ( "//# debugId=" )
319+ ) {
320+ return null ; // Debug ID already present, skip injection
321+ }
287322
288- const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
323+ const debugId = stringToUUID ( code ) ; // generate a deterministic debug ID
324+ const codeToInject = getDebugIdSnippet ( debugId ) ;
289325
290- if ( match ) {
291- // Add injected code after any comments or "use strict" at the beginning of the bundle.
292- ms . appendLeft ( match . length , codeToInject ) ;
293- } else {
294- // ms.replace() doesn't work when there is an empty string match (which happens if
295- // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
296- // need this special case here.
297- ms . prepend ( codeToInject ) ;
298- }
326+ const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
299327
300- return {
301- code : ms . toString ( ) ,
302- map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
303- } ;
328+ const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
329+
330+ if ( match ) {
331+ // Add injected code after any comments or "use strict" at the beginning of the bundle.
332+ ms . appendLeft ( match . length , codeToInject ) ;
304333 } else {
305- return null ; // returning null means not modifying the chunk at all
334+ // ms.replace() doesn't work when there is an empty string match (which happens if
335+ // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
336+ // need this special case here.
337+ ms . prepend ( codeToInject ) ;
306338 }
339+
340+ return {
341+ code : ms . toString ( ) ,
342+ map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
343+ } ;
307344 } ,
308345 } ;
309346}
@@ -312,34 +349,39 @@ export function createRollupModuleMetadataInjectionHooks(injectionCode: string):
312349 renderChunk : RenderChunkHook ;
313350} {
314351 return {
315- renderChunk ( code : string , chunk : { fileName : string } ) {
352+ renderChunk ( code : string , chunk : { fileName : string ; facadeModuleId ?: string | null } ) {
316353 if (
317354 // chunks could be any file (html, md, ...)
318- [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) =>
355+ ! [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) =>
319356 stripQueryAndHashFromPath ( chunk . fileName ) . endsWith ( ending )
320357 )
321358 ) {
322- const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
359+ return null ; // returning null means not modifying the chunk at all
360+ }
323361
324- const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
362+ // Skip empty chunks and HTML facade chunks (Vite MPA)
363+ if ( shouldSkipCodeInjection ( code , chunk . facadeModuleId ) ) {
364+ return null ;
365+ }
325366
326- if ( match ) {
327- // Add injected code after any comments or "use strict" at the beginning of the bundle.
328- ms . appendLeft ( match . length , injectionCode ) ;
329- } else {
330- // ms.replace() doesn't work when there is an empty string match (which happens if
331- // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
332- // need this special case here.
333- ms . prepend ( injectionCode ) ;
334- }
367+ const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
335368
336- return {
337- code : ms . toString ( ) ,
338- map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
339- } ;
369+ const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
370+
371+ if ( match ) {
372+ // Add injected code after any comments or "use strict" at the beginning of the bundle.
373+ ms . appendLeft ( match . length , injectionCode ) ;
340374 } else {
341- return null ; // returning null means not modifying the chunk at all
375+ // ms.replace() doesn't work when there is an empty string match (which happens if
376+ // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
377+ // need this special case here.
378+ ms . prepend ( injectionCode ) ;
342379 }
380+
381+ return {
382+ code : ms . toString ( ) ,
383+ map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
384+ } ;
343385 } ,
344386 } ;
345387}
0 commit comments