@@ -206,44 +206,80 @@ 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 file is a JavaScript file based on its extension.
218+ * Handles query strings and hashes in the filename.
219+ */
220+ function isJsFile ( fileName : string ) : boolean {
221+ const cleanFileName = stripQueryAndHashFromPath ( fileName ) ;
222+ return [ ".js" , ".mjs" , ".cjs" ] . some ( ( ext ) => cleanFileName . endsWith ( ext ) ) ;
223+ }
224+
225+ /**
226+ * Checks if a chunk should be skipped for code injection
227+ *
228+ * This is necessary to handle Vite's MPA (multi-page application) mode where
229+ * HTML entry points create "facade" chunks that should not contain injected code.
230+ * See: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829
231+ *
232+ * @param code - The chunk's code content
233+ * @param facadeModuleId - The facade module ID (if any) - HTML files create facade chunks
234+ * @returns true if the chunk should be skipped
235+ */
236+ function shouldSkipCodeInjection ( code : string , facadeModuleId : string | null | undefined ) : boolean {
237+ // Skip empty chunks - these are placeholder chunks that should be optimized away
238+ if ( code . trim ( ) . length === 0 ) {
239+ return true ;
240+ }
241+
242+ // Skip HTML facade chunks
243+ // They only contain import statements and should not have Sentry code injected
244+ if ( facadeModuleId && stripQueryAndHashFromPath ( facadeModuleId ) . endsWith ( ".html" ) ) {
245+ return true ;
246+ }
247+
248+ return false ;
249+ }
250+
215251export function createRollupReleaseInjectionHooks ( injectionCode : string ) : {
216252 renderChunk : RenderChunkHook ;
217253} {
218254 return {
219- renderChunk ( code : string , chunk : { fileName : string } ) {
220- if (
221- // chunks could be any file (html, md, ...)
222- [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) =>
223- stripQueryAndHashFromPath ( chunk . fileName ) . endsWith ( ending )
224- )
225- ) {
226- const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
255+ renderChunk ( code : string , chunk : { fileName : string ; facadeModuleId ?: string | null } ) {
256+ if ( ! isJsFile ( chunk . fileName ) ) {
257+ return null ; // returning null means not modifying the chunk at all
258+ }
227259
228- const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
260+ // Skip empty chunks and HTML facade chunks (Vite MPA)
261+ if ( shouldSkipCodeInjection ( code , chunk . facadeModuleId ) ) {
262+ return null ;
263+ }
229264
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- }
265+ const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
239266
240- return {
241- code : ms . toString ( ) ,
242- map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
243- } ;
267+ const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
268+
269+ if ( match ) {
270+ // Add injected code after any comments or "use strict" at the beginning of the bundle.
271+ ms . appendLeft ( match . length , injectionCode ) ;
244272 } else {
245- return null ; // returning null means not modifying the chunk at all
273+ // ms.replace() doesn't work when there is an empty string match (which happens if
274+ // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
275+ // need this special case here.
276+ ms . prepend ( injectionCode ) ;
246277 }
278+
279+ return {
280+ code : ms . toString ( ) ,
281+ map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
282+ } ;
247283 } ,
248284 } ;
249285}
@@ -262,48 +298,48 @@ export function createRollupDebugIdInjectionHooks(): {
262298 renderChunk : RenderChunkHook ;
263299} {
264300 return {
265- renderChunk ( code : string , chunk : { fileName : string } ) {
301+ renderChunk ( code : string , chunk : { fileName : string ; facadeModuleId ?: string | null } ) {
302+ if ( ! isJsFile ( chunk . fileName ) ) {
303+ return null ; // returning null means not modifying the chunk at all
304+ }
305+
306+ // Skip empty chunks and HTML facade chunks (Vite MPA)
307+ if ( shouldSkipCodeInjection ( code , chunk . facadeModuleId ) ) {
308+ return null ;
309+ }
310+
311+ // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI)
312+ const chunkStartSnippet = code . slice ( 0 , 6000 ) ;
313+ const chunkEndSnippet = code . slice ( - 500 ) ;
314+
266315 if (
267- // chunks could be any file (html, md, ...)
268- [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) =>
269- stripQueryAndHashFromPath ( chunk . fileName ) . endsWith ( ending )
270- )
316+ chunkStartSnippet . includes ( "_sentryDebugIdIdentifier" ) ||
317+ chunkEndSnippet . includes ( "//# debugId=" )
271318 ) {
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- }
282-
283- const debugId = stringToUUID ( code ) ; // generate a deterministic debug ID
284- const codeToInject = getDebugIdSnippet ( debugId ) ;
319+ return null ; // Debug ID already present, skip injection
320+ }
285321
286- const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
322+ const debugId = stringToUUID ( code ) ; // generate a deterministic debug ID
323+ const codeToInject = getDebugIdSnippet ( debugId ) ;
287324
288- const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
325+ const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
289326
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- }
327+ const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
299328
300- return {
301- code : ms . toString ( ) ,
302- map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
303- } ;
329+ if ( match ) {
330+ // Add injected code after any comments or "use strict" at the beginning of the bundle.
331+ ms . appendLeft ( match . length , codeToInject ) ;
304332 } else {
305- return null ; // returning null means not modifying the chunk at all
333+ // ms.replace() doesn't work when there is an empty string match (which happens if
334+ // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
335+ // need this special case here.
336+ ms . prepend ( codeToInject ) ;
306337 }
338+
339+ return {
340+ code : ms . toString ( ) ,
341+ map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
342+ } ;
307343 } ,
308344 } ;
309345}
@@ -312,34 +348,34 @@ export function createRollupModuleMetadataInjectionHooks(injectionCode: string):
312348 renderChunk : RenderChunkHook ;
313349} {
314350 return {
315- renderChunk ( code : string , chunk : { fileName : string } ) {
316- if (
317- // chunks could be any file (html, md, ...)
318- [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) =>
319- stripQueryAndHashFromPath ( chunk . fileName ) . endsWith ( ending )
320- )
321- ) {
322- const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
351+ renderChunk ( code : string , chunk : { fileName : string ; facadeModuleId ?: string | null } ) {
352+ if ( ! isJsFile ( chunk . fileName ) ) {
353+ return null ; // returning null means not modifying the chunk at all
354+ }
323355
324- const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
356+ // Skip empty chunks and HTML facade chunks (Vite MPA)
357+ if ( shouldSkipCodeInjection ( code , chunk . facadeModuleId ) ) {
358+ return null ;
359+ }
325360
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- }
361+ const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
335362
336- return {
337- code : ms . toString ( ) ,
338- map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
339- } ;
363+ const match = code . match ( COMMENT_USE_STRICT_REGEX ) ?. [ 0 ] ;
364+
365+ if ( match ) {
366+ // Add injected code after any comments or "use strict" at the beginning of the bundle.
367+ ms . appendLeft ( match . length , injectionCode ) ;
340368 } else {
341- return null ; // returning null means not modifying the chunk at all
369+ // ms.replace() doesn't work when there is an empty string match (which happens if
370+ // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
371+ // need this special case here.
372+ ms . prepend ( injectionCode ) ;
342373 }
374+
375+ return {
376+ code : ms . toString ( ) ,
377+ map : ms . generateMap ( { file : chunk . fileName , hires : "boundary" } ) ,
378+ } ;
343379 } ,
344380 } ;
345381}
0 commit comments