Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 20 additions & 159 deletions packages/bundler-plugin-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import SentryCli from "@sentry/cli";
import { logger } from "@sentry/utils";
import * as fs from "fs";
import { glob } from "glob";
import MagicString, { SourceMap } from "magic-string";
import * as path from "path";
import { createUnplugin, TransformResult, UnpluginInstance, UnpluginOptions } from "unplugin";
import { createSentryBuildPluginManager } from "./build-plugin-manager";
import { createDebugIdUploadFunction } from "./debug-id-upload";
Expand All @@ -16,10 +14,8 @@ import { Options, SentrySDKBuildFlags } from "./types";
import {
CodeInjection,
containsOnlyImports,
generateGlobalInjectorCode,
generateReleaseInjectorCode,
generateModuleMetadataInjectorCode,
replaceBooleanFlagsInCode,
stringToUUID,
stripQueryAndHashFromPath,
} from "./utils";

Expand Down Expand Up @@ -121,7 +117,7 @@ export function sentryUnpluginFactory({
"No release name provided. Will not inject release. Please set the `release.name` option to identify your release."
);
} else {
const code = generateGlobalInjectorCode({
const code = generateReleaseInjectorCode({
release: options.release.name,
injectBuildInformation: options._experiments.injectBuildInformation || false,
});
Expand Down Expand Up @@ -230,33 +226,15 @@ export function sentryCliBinaryExists(): boolean {

// We need to be careful not to inject the snippet before any `"use strict";`s.
// As an additional complication `"use strict";`s may come after any number of comments.
const COMMENT_USE_STRICT_REGEX =
export const COMMENT_USE_STRICT_REGEX =
// Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files.
/^(?:\s*|\/\*(?:.|\r|\n)*?\*\/|\/\/.*[\n\r])*(?:"[^"]*";|'[^']*';)?/;

/**
* Simplified `renderChunk` hook type from Rollup.
* We can't reference the type directly because the Vite plugin complains
* about type mismatches
*/
type RenderChunkHook = (
code: string,
chunk: {
fileName: string;
facadeModuleId?: string | null;
},
outputOptions?: unknown,
meta?: { magicString?: MagicString }
) => {
code: string;
readonly 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 {
export function isJsFile(fileName: string): boolean {
const cleanFileName = stripQueryAndHashFromPath(fileName);
return [".js", ".mjs", ".cjs"].some((ext) => cleanFileName.endsWith(ext));
}
Expand All @@ -275,7 +253,10 @@ function isJsFile(fileName: string): boolean {
* @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 {
export 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;
Expand All @@ -289,137 +270,17 @@ function shouldSkipCodeInjection(code: string, facadeModuleId: string | null | u
return false;
}

export function createRollupBundleSizeOptimizationHooks(replacementValues: SentrySDKBuildFlags): {
transform: UnpluginOptions["transform"];
} {
return {
transform(code: string) {
return replaceBooleanFlagsInCode(code, replacementValues);
},
};
}

export function createRollupInjectionHooks(
injectionCode: CodeInjection,
debugIds: boolean
): {
renderChunk: RenderChunkHook;
} {
return {
renderChunk(
code: string,
chunk: { fileName: string; facadeModuleId?: string | null },
_?: unknown,
meta?: { magicString?: MagicString }
) {
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;
}

const codeToInject = injectionCode.clone();

if (debugIds) {
// 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=")
)
) {
const debugId = stringToUUID(code); // generate a deterministic debug ID
codeToInject.append(getDebugIdSnippet(debugId));
}
}

const ms = meta?.magicString || new MagicString(code, { filename: chunk.fileName });
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, codeToInject.code());
} 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.code());
}

// Rolldown can pass a native MagicString instance in meta.magicString
// https://rolldown.rs/in-depth/native-magic-string#usage-examples
if (ms?.constructor?.name === "BindingMagicString") {
// Rolldown docs say to return the magic string instance directly in this case
return { code: ms as unknown as string };
}

return {
code: ms.toString(),
get map() {
return ms.generateMap({
file: chunk.fileName,
hires: "boundary",
});
},
};
},
};
}

export function createRollupDebugIdUploadHooks(
upload: (buildArtifacts: string[]) => Promise<void>,
_logger: Logger,
createDependencyOnBuildArtifacts: () => () => void
): {
writeBundle: (
outputOptions: { dir?: string; file?: string },
bundle: { [fileName: string]: unknown }
) => Promise<void>;
} {
const freeGlobalDependencyOnDebugIdSourcemapArtifacts = createDependencyOnBuildArtifacts();
return {
async writeBundle(
outputOptions: { dir?: string; file?: string },
bundle: { [fileName: string]: unknown }
) {
try {
if (outputOptions.dir) {
const outputDir = outputOptions.dir;
const buildArtifacts = await glob(
[
"/**/*.js",
"/**/*.mjs",
"/**/*.cjs",
"/**/*.js.map",
"/**/*.mjs.map",
"/**/*.cjs.map",
].map((q) => `${q}?(\\?*)?(#*)`), // We want to allow query and hashes strings at the end of files
{
root: outputDir,
absolute: true,
nodir: true,
}
);
await upload(buildArtifacts);
} else if (outputOptions.file) {
await upload([outputOptions.file]);
} else {
const buildArtifacts = Object.keys(bundle).map((asset) =>
path.join(path.resolve(), asset)
);
await upload(buildArtifacts);
}
} finally {
freeGlobalDependencyOnDebugIdSourcemapArtifacts();
}
},
};
export function globFiles(outputDir: string): Promise<string[]> {
return glob(
["/**/*.js", "/**/*.mjs", "/**/*.cjs", "/**/*.js.map", "/**/*.mjs.map", "/**/*.cjs.map"].map(
(q) => `${q}?(\\?*)?(#*)`
), // We want to allow query and hashes strings at the end of files
{
root: outputDir,
absolute: true,
nodir: true,
}
);
}

export function createComponentNameAnnotateHooks(
Expand Down Expand Up @@ -497,7 +358,7 @@ export {
CodeInjection,
replaceBooleanFlagsInCode,
stringToUUID,
generateGlobalInjectorCode,
generateReleaseInjectorCode,
generateModuleMetadataInjectorCode,
} from "./utils";
export { createSentryBuildPluginManager } from "./build-plugin-manager";
Expand Down
2 changes: 1 addition & 1 deletion packages/bundler-plugin-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export function determineReleaseName(): string | undefined {
* Generates code for the global injector which is responsible for setting the global
* `SENTRY_RELEASE` & `SENTRY_BUILD_INFO` variables.
*/
export function generateGlobalInjectorCode({
export function generateReleaseInjectorCode({
release,
injectBuildInformation,
}: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`generateGlobalInjectorCode generates code with release 1`] = `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e.SENTRY_RELEASE={id:\\"1.2.3\\"};}catch(e){}}();"`;

exports[`generateGlobalInjectorCode generates code with release and build information 1`] = `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e.SENTRY_RELEASE={id:\\"1.2.3\\"};e.SENTRY_BUILD_INFO={\\"deps\\":[\\"myDep\\",\\"rollup\\"],\\"depsVersions\\":{\\"rollup\\":3},\\"nodeVersion\\":18};}catch(e){}}();"`;

exports[`generateModuleMetadataInjectorCode generates code with empty metadata object 1`] = `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n<arguments.length;n++){var a=arguments[n];if(null!=a)for(var t in a)a.hasOwnProperty(t)&&(e[t]=a[t])}return e}({},e._sentryModuleMetadata[(new e.Error).stack],{});}catch(e){}}();"`;

exports[`generateModuleMetadataInjectorCode generates code with metadata object 1`] = `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n<arguments.length;n++){var a=arguments[n];if(null!=a)for(var t in a)a.hasOwnProperty(t)&&(e[t]=a[t])}return e}({},e._sentryModuleMetadata[(new e.Error).stack],{\\"file1.js\\":{\\"foo\\":\\"bar\\"},\\"file2.js\\":{\\"bar\\":\\"baz\\"}});}catch(e){}}();"`;

exports[`generateReleaseInjectorCode generates code with release 1`] = `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e.SENTRY_RELEASE={id:\\"1.2.3\\"};}catch(e){}}();"`;

exports[`generateReleaseInjectorCode generates code with release and build information 1`] = `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e.SENTRY_RELEASE={id:\\"1.2.3\\"};e.SENTRY_BUILD_INFO={\\"deps\\":[\\"myDep\\",\\"rollup\\"],\\"depsVersions\\":{\\"rollup\\":3},\\"nodeVersion\\":18};}catch(e){}}();"`;
Loading
Loading