-
Notifications
You must be signed in to change notification settings - Fork 51
fix(vite): Skip code injection for HTML facade chunks #830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ab98b4f
647fae9
2347b6d
841bd5d
a91cefc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -206,44 +206,80 @@ type RenderChunkHook = ( | |
| code: string, | ||
| chunk: { | ||
| fileName: string; | ||
| facadeModuleId?: string | null; | ||
| } | ||
| ) => { | ||
| code: string; | ||
| map: SourceMap; | ||
| } | null; | ||
|
|
||
| /** | ||
| * Checks if a file is a JavaScript file based on its extension. | ||
| * Handles query strings and hashes in the filename. | ||
| */ | ||
| function isJsFile(fileName: string): boolean { | ||
| const cleanFileName = stripQueryAndHashFromPath(fileName); | ||
| return [".js", ".mjs", ".cjs"].some((ext) => cleanFileName.endsWith(ext)); | ||
| } | ||
|
|
||
| /** | ||
| * Checks if a chunk should be skipped for code injection | ||
| * | ||
| * This is necessary to handle Vite's MPA (multi-page application) mode where | ||
| * HTML entry points create "facade" chunks that should not contain injected code. | ||
| * See: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829 | ||
| * | ||
| * @param code - The chunk's code content | ||
| * @param facadeModuleId - The facade module ID (if any) - HTML files create facade chunks | ||
| * @returns true if the chunk should be skipped | ||
| */ | ||
| function shouldSkipCodeInjection(code: string, facadeModuleId: string | null | undefined): boolean { | ||
| // Skip empty chunks - these are placeholder chunks that should be optimized away | ||
| if (code.trim().length === 0) { | ||
| return true; | ||
| } | ||
|
|
||
| // Skip HTML facade chunks | ||
| // They only contain import statements and should not have Sentry code injected | ||
| if (facadeModuleId && stripQueryAndHashFromPath(facadeModuleId).endsWith(".html")) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| export function createRollupReleaseInjectionHooks(injectionCode: string): { | ||
| renderChunk: RenderChunkHook; | ||
| } { | ||
| return { | ||
| renderChunk(code: string, chunk: { fileName: string }) { | ||
| if ( | ||
| // chunks could be any file (html, md, ...) | ||
| [".js", ".mjs", ".cjs"].some((ending) => | ||
| stripQueryAndHashFromPath(chunk.fileName).endsWith(ending) | ||
| ) | ||
| ) { | ||
| const ms = new MagicString(code, { filename: chunk.fileName }); | ||
| renderChunk(code: string, chunk: { fileName: string; facadeModuleId?: string | null }) { | ||
| if (!isJsFile(chunk.fileName)) { | ||
| return null; // returning null means not modifying the chunk at all | ||
| } | ||
|
|
||
| const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; | ||
| // Skip empty chunks and HTML facade chunks (Vite MPA) | ||
| if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) { | ||
| return null; | ||
| } | ||
|
|
||
| if (match) { | ||
| // Add injected code after any comments or "use strict" at the beginning of the bundle. | ||
| ms.appendLeft(match.length, injectionCode); | ||
| } else { | ||
| // ms.replace() doesn't work when there is an empty string match (which happens if | ||
| // there is neither, a comment, nor a "use strict" at the top of the chunk) so we | ||
| // need this special case here. | ||
| ms.prepend(injectionCode); | ||
| } | ||
| const ms = new MagicString(code, { filename: chunk.fileName }); | ||
|
|
||
| return { | ||
| code: ms.toString(), | ||
| map: ms.generateMap({ file: chunk.fileName, hires: "boundary" }), | ||
| }; | ||
| const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; | ||
|
|
||
| if (match) { | ||
| // Add injected code after any comments or "use strict" at the beginning of the bundle. | ||
| ms.appendLeft(match.length, injectionCode); | ||
| } else { | ||
| return null; // returning null means not modifying the chunk at all | ||
| // ms.replace() doesn't work when there is an empty string match (which happens if | ||
| // there is neither, a comment, nor a "use strict" at the top of the chunk) so we | ||
| // need this special case here. | ||
| ms.prepend(injectionCode); | ||
| } | ||
|
|
||
| return { | ||
| code: ms.toString(), | ||
| map: ms.generateMap({ file: chunk.fileName, hires: "boundary" }), | ||
| }; | ||
| }, | ||
| }; | ||
| } | ||
|
|
@@ -262,48 +298,48 @@ export function createRollupDebugIdInjectionHooks(): { | |
| renderChunk: RenderChunkHook; | ||
| } { | ||
| return { | ||
| renderChunk(code: string, chunk: { fileName: string }) { | ||
| renderChunk(code: string, chunk: { fileName: string; facadeModuleId?: string | null }) { | ||
| if (!isJsFile(chunk.fileName)) { | ||
| return null; // returning null means not modifying the chunk at all | ||
| } | ||
|
|
||
| // Skip empty chunks and HTML facade chunks (Vite MPA) | ||
| if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) { | ||
| return null; | ||
| } | ||
|
|
||
| // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI) | ||
| const chunkStartSnippet = code.slice(0, 6000); | ||
| const chunkEndSnippet = code.slice(-500); | ||
|
|
||
| if ( | ||
| // chunks could be any file (html, md, ...) | ||
| [".js", ".mjs", ".cjs"].some((ending) => | ||
| stripQueryAndHashFromPath(chunk.fileName).endsWith(ending) | ||
| ) | ||
| chunkStartSnippet.includes("_sentryDebugIdIdentifier") || | ||
| chunkEndSnippet.includes("//# debugId=") | ||
| ) { | ||
| // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI) | ||
| const chunkStartSnippet = code.slice(0, 6000); | ||
| const chunkEndSnippet = code.slice(-500); | ||
|
|
||
| if ( | ||
| chunkStartSnippet.includes("_sentryDebugIdIdentifier") || | ||
| chunkEndSnippet.includes("//# debugId=") | ||
| ) { | ||
| return null; // Debug ID already present, skip injection | ||
| } | ||
|
|
||
| const debugId = stringToUUID(code); // generate a deterministic debug ID | ||
| const codeToInject = getDebugIdSnippet(debugId); | ||
| return null; // Debug ID already present, skip injection | ||
| } | ||
|
|
||
| const ms = new MagicString(code, { filename: chunk.fileName }); | ||
| const debugId = stringToUUID(code); // generate a deterministic debug ID | ||
| const codeToInject = getDebugIdSnippet(debugId); | ||
|
|
||
| const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; | ||
| const ms = new MagicString(code, { filename: chunk.fileName }); | ||
|
|
||
| if (match) { | ||
| // Add injected code after any comments or "use strict" at the beginning of the bundle. | ||
| ms.appendLeft(match.length, codeToInject); | ||
| } else { | ||
| // ms.replace() doesn't work when there is an empty string match (which happens if | ||
| // there is neither, a comment, nor a "use strict" at the top of the chunk) so we | ||
| // need this special case here. | ||
| ms.prepend(codeToInject); | ||
| } | ||
| const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; | ||
|
||
|
|
||
| return { | ||
| code: ms.toString(), | ||
| map: ms.generateMap({ file: chunk.fileName, hires: "boundary" }), | ||
| }; | ||
| if (match) { | ||
| // Add injected code after any comments or "use strict" at the beginning of the bundle. | ||
| ms.appendLeft(match.length, codeToInject); | ||
| } else { | ||
| return null; // returning null means not modifying the chunk at all | ||
| // ms.replace() doesn't work when there is an empty string match (which happens if | ||
| // there is neither, a comment, nor a "use strict" at the top of the chunk) so we | ||
| // need this special case here. | ||
| ms.prepend(codeToInject); | ||
| } | ||
|
|
||
| return { | ||
| code: ms.toString(), | ||
| map: ms.generateMap({ file: chunk.fileName, hires: "boundary" }), | ||
| }; | ||
| }, | ||
| }; | ||
| } | ||
|
|
@@ -312,34 +348,34 @@ export function createRollupModuleMetadataInjectionHooks(injectionCode: string): | |
| renderChunk: RenderChunkHook; | ||
| } { | ||
| return { | ||
| renderChunk(code: string, chunk: { fileName: string }) { | ||
| if ( | ||
| // chunks could be any file (html, md, ...) | ||
| [".js", ".mjs", ".cjs"].some((ending) => | ||
| stripQueryAndHashFromPath(chunk.fileName).endsWith(ending) | ||
| ) | ||
| ) { | ||
| const ms = new MagicString(code, { filename: chunk.fileName }); | ||
| renderChunk(code: string, chunk: { fileName: string; facadeModuleId?: string | null }) { | ||
| if (!isJsFile(chunk.fileName)) { | ||
| return null; // returning null means not modifying the chunk at all | ||
| } | ||
|
|
||
| const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; | ||
| // Skip empty chunks and HTML facade chunks (Vite MPA) | ||
| if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) { | ||
| return null; | ||
| } | ||
|
|
||
| if (match) { | ||
| // Add injected code after any comments or "use strict" at the beginning of the bundle. | ||
| ms.appendLeft(match.length, injectionCode); | ||
| } else { | ||
| // ms.replace() doesn't work when there is an empty string match (which happens if | ||
| // there is neither, a comment, nor a "use strict" at the top of the chunk) so we | ||
| // need this special case here. | ||
| ms.prepend(injectionCode); | ||
| } | ||
| const ms = new MagicString(code, { filename: chunk.fileName }); | ||
|
|
||
| return { | ||
| code: ms.toString(), | ||
| map: ms.generateMap({ file: chunk.fileName, hires: "boundary" }), | ||
| }; | ||
| const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; | ||
|
||
|
|
||
| if (match) { | ||
| // Add injected code after any comments or "use strict" at the beginning of the bundle. | ||
| ms.appendLeft(match.length, injectionCode); | ||
| } else { | ||
| return null; // returning null means not modifying the chunk at all | ||
| // ms.replace() doesn't work when there is an empty string match (which happens if | ||
| // there is neither, a comment, nor a "use strict" at the top of the chunk) so we | ||
| // need this special case here. | ||
| ms.prepend(injectionCode); | ||
| } | ||
|
|
||
| return { | ||
| code: ms.toString(), | ||
| map: ms.generateMap({ file: chunk.fileName, hires: "boundary" }), | ||
| }; | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { sentryVitePlugin } from "@sentry/vite-plugin"; | ||
| import * as path from "path"; | ||
| import * as vite from "vite"; | ||
|
|
||
| const inputDir = path.join(__dirname, "input"); | ||
|
|
||
| void vite.build({ | ||
| clearScreen: false, | ||
| root: inputDir, | ||
| build: { | ||
| sourcemap: true, | ||
| outDir: path.join(__dirname, "out", "with-plugin"), | ||
| emptyOutDir: true, | ||
| rollupOptions: { | ||
| input: { | ||
| index: path.join(inputDir, "index.html"), | ||
| page1: path.join(inputDir, "page1.html"), | ||
| page2: path.join(inputDir, "page2.html"), | ||
| }, | ||
| }, | ||
| }, | ||
| plugins: [ | ||
| sentryVitePlugin({ | ||
| telemetry: false, | ||
| // Empty options - the issue says options don't affect the results | ||
| }), | ||
| ], | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import * as path from "path"; | ||
| import * as vite from "vite"; | ||
|
|
||
| const inputDir = path.join(__dirname, "input"); | ||
|
|
||
| void vite.build({ | ||
| clearScreen: false, | ||
| root: inputDir, | ||
| build: { | ||
| sourcemap: true, | ||
| outDir: path.join(__dirname, "out", "without-plugin"), | ||
| emptyOutDir: true, | ||
| rollupOptions: { | ||
| input: { | ||
| index: path.join(inputDir, "index.html"), | ||
| page1: path.join(inputDir, "page1.html"), | ||
| page2: path.join(inputDir, "page2.html"), | ||
| }, | ||
| }, | ||
| }, | ||
| plugins: [], | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Index Page</title> | ||
| </head> | ||
| <body> | ||
| <h1>Index Page - No Scripts</h1> | ||
| <!-- This page has no scripts --> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Page 1</title> | ||
| </head> | ||
| <body> | ||
| <h1>Page 1 - With Shared Module</h1> | ||
| <script type="module" src="./shared-module.js"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Page 2</title> | ||
| </head> | ||
| <body> | ||
| <h1>Page 2 - With Shared Module</h1> | ||
| <script type="module" src="./shared-module.js"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // This is a shared module that is used by multiple HTML pages | ||
| export function greet(name) { | ||
| // eslint-disable-next-line no-console | ||
| console.log(`Hello, ${String(name)}!`); | ||
| } | ||
|
|
||
| export const VERSION = "1.0.0"; | ||
|
|
||
| // Side effect: greet on load | ||
| greet("World"); |
Uh oh!
There was an error while loading. Please reload this page.