Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => {
"sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => {
"sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => {
",
"sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => {
"sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => {
"sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => {
",
"sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => {
"sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => {
"sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"],
["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => {
",
"sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"],
["release","finalize","CURRENT_SHA"],
["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],
",
}
`);
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
...sourcemaps,
assets: sourcemaps?.assets ?? getSourcemapsAssetsGlobs(config),
filesToDeleteAfterUpload: sourcemaps?.filesToDeleteAfterUpload ?? updatedFilesToDeleteAfterUpload,
deleteSourcemapsAfterUpload: false,
},
bundleSizeOptimizations: {
...bundleSizeOptimizations,
Expand Down
4 changes: 4 additions & 0 deletions packages/astro/test/integration/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ describe('sentryAstro integration', () => {
'./dist/**/_astro/**/*.map',
'./dist/**/.prerender/**/*.map',
],
deleteSourcemapsAfterUpload: false,
},
_metaOptions: {
telemetry: {
Expand Down Expand Up @@ -154,6 +155,7 @@ describe('sentryAstro integration', () => {
'./dist/**/_astro/**/*.map',
'./dist/**/.prerender/**/*.map',
],
deleteSourcemapsAfterUpload: false,
},
_metaOptions: {
telemetry: {
Expand Down Expand Up @@ -198,6 +200,7 @@ describe('sentryAstro integration', () => {
'./dist/**/_astro/**/*.map',
'./dist/**/.prerender/**/*.map',
],
deleteSourcemapsAfterUpload: false,
},
_metaOptions: {
telemetry: {
Expand Down Expand Up @@ -242,6 +245,7 @@ describe('sentryAstro integration', () => {
'./dist/**/_astro/**/*.map',
'./dist/**/.prerender/**/*.map',
],
deleteSourcemapsAfterUpload: false,
},
_metaOptions: {
telemetry: {
Expand Down
54 changes: 45 additions & 9 deletions packages/bundler-plugins/src/rollup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as path from 'node:path';
import { createRequire } from 'node:module';
import { finalizeRolldownDebugIds, getDebugIdForChunk, hasExistingDebugID } from './debug-id-injection';
import { stampDebugIds, type OutputBundle } from './debug-id-stamping';
import { createSourceMapHandling, type OutputDestinationOptions } from './source-map-handling';

// The subset of Rollup's `TransformResult` that this plugin's `transform`
// hook actually returns. Defined locally instead of imported from `rollup`
Expand Down Expand Up @@ -81,10 +82,14 @@ function getViteParseAstAsync(): Promise<ViteParseAstAsync | null> {
}

/**
* @ignore - this is the internal plugin factory function only used for the Vite plugin!
* @ignore - internal plugin factory shared by the Rollup and Vite plugins.
*
* `main` carries injection, release creation and the upload. `post` runs last in `generateBundle`,
* copies chunks and their source maps into the upload staging folder and drops the map assets
* from the bundle, so the bundler never writes them.
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function _rollupPluginInternal(
export function _createRollupPluginInternals(
userOptions: Options = {},
buildTool: 'rollup' | 'vite',
buildToolMajorVersion?: string,
Expand All @@ -105,7 +110,10 @@ export function _rollupPluginInternal(

if (options.disable) {
return {
name: 'sentry-noop-plugin',
main: {
name: 'sentry-noop-plugin',
},
post: undefined,
};
}

Expand All @@ -116,6 +124,7 @@ export function _rollupPluginInternal(
const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts();
const upload = createDebugIdUploadFunction({ sentryBuildPluginManager });
const sourcemapsEnabled = options.sourcemaps?.disable !== true;
const sourceMapHandling = createSourceMapHandling(sentryBuildPluginManager);
const releaseInjectionCode = new CodeInjection();
const chunkInjectionCode = new CodeInjection();

Expand Down Expand Up @@ -256,9 +265,11 @@ export function _rollupPluginInternal(
}

async function writeBundle(
outputOptions: { dir?: string; file?: string },
outputOptions: OutputDestinationOptions,
bundle: { [fileName: string]: unknown },
): Promise<void> {
const staging = sourceMapHandling.takeStaging(outputOptions);

try {
await sentryBuildPluginManager.createRelease();

Expand All @@ -274,13 +285,15 @@ export function _rollupPluginInternal(
'/**/*.cjs.map',
].map(q => `${q}?(\\?*)?(#*)`); // We want to allow query and hash strings at the end of files
const buildArtifacts = await globFiles(JS_AND_MAP_PATTERNS, { root: outputDir });
await upload(buildArtifacts);
await upload(buildArtifacts, { staging });
} else if (outputOptions.file) {
await upload([outputOptions.file]);
await upload([outputOptions.file], { staging });
} else {
const buildArtifacts = Object.keys(bundle).map(asset => path.join(path.resolve(), asset));
await upload(buildArtifacts);
await upload(buildArtifacts, { staging });
}
} else {
await staging?.discard();
}
} finally {
freeGlobalDependencyOnBuildArtifacts();
Expand All @@ -297,21 +310,44 @@ export function _rollupPluginInternal(
}
: transform;

return {
const main = {
name,
buildStart,
...(shouldTransform ? { transform: transformHook } : {}),
// Vite warns about `rollupOptions.output.sourcemap`, so the Vite plugin
// decides in its `config` hook instead.
...(buildTool === 'rollup' ? { outputOptions: sourceMapHandling.outputOptions } : {}),
renderChunk,
generateBundle: { order: 'pre' as const, handler: generateBundle },
writeBundle,
};

const post = {
name: `sentry-${buildTool}-upload-staging`,
generateBundle: { order: 'post' as const, handler: sourceMapHandling.stageSourceMaps },
};

return { main, post };
}

/**
* @ignore - this is the internal plugin factory function only used for the Vite plugin!
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function _rollupPluginInternal(
userOptions: Options = {},
buildTool: 'rollup' | 'vite',
buildToolMajorVersion?: string,
) {
return _createRollupPluginInternals(userOptions, buildTool, buildToolMajorVersion).main;
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any
export function sentryRollupPlugin(userOptions: Options = {}): any {
const { main, post } = _createRollupPluginInternals(userOptions, 'rollup');
// We return an array here so we don't break backwards compatibility with what
// unplugin used to return
return [_rollupPluginInternal(userOptions, 'rollup')];
return post ? [main, post] : [main];
}

export type { Options as SentryRollupPluginOptions } from '../core';
118 changes: 118 additions & 0 deletions packages/bundler-plugins/src/rollup/source-map-handling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as path from 'node:path';
import type { SentryBuildPluginManager, SourceMapDecision, SourceMapKind, UploadStaging } from '../core';
import { isJsFile } from '../core';
import type { OutputBundle } from './debug-id-stamping';

export type RollupSourceMapSetting = boolean | 'inline' | 'hidden' | undefined;

export type OutputDestinationOptions = { dir?: string; file?: string };

/** Maps Rollup's (and Vite's) `sourcemap` setting onto the plugin's source map kinds. */
export function sourceMapKindFromRollupSetting(setting: RollupSourceMapSetting): SourceMapKind {
switch (setting) {
case undefined:
return 'unset';
case false:
return 'off';
case true:
return 'visible';
default:
return setting;
}
}

/** The directory an output writes to. Decisions and stagings are keyed by it. */
export function getOutputDestination(outputOptions: OutputDestinationOptions): string {
return path.resolve(outputOptions.dir ?? path.dirname(outputOptions.file ?? '.'));
}

export type SourceMapHandling = {
/** Rollup's `outputOptions` hook: decides for the output and switches it to hidden source maps when needed. */
outputOptions: (
rawOutputOptions: OutputDestinationOptions & { sourcemap?: RollupSourceMapSetting },
) => (OutputDestinationOptions & { sourcemap?: RollupSourceMapSetting }) | null;
/** Records the decision for an output destination, for bundlers that decide outside `outputOptions`. */
setDecision: (destination: string, decision: SourceMapDecision) => void;
/**
* `generateBundle` with `order: 'post'`: copies each chunk and its source map into a staging folder
* and drops the map asset from the bundle, so the bundler never writes it.
*/
stageSourceMaps: (outputOptions: OutputDestinationOptions, bundle: OutputBundle, isWrite?: boolean) => Promise<void>;
/** Hands the staging of this output to `writeBundle`, first in first out. */
takeStaging: (outputOptions: OutputDestinationOptions) => UploadStaging | undefined;
};

/**
* Source map decisions and upload stagings of one plugin instance. Keyed by output destination
* because one instance can serve several outputs, and watch mode or several `write()` calls
* rebuild the same one.
*/
export function createSourceMapHandling(sentryBuildPluginManager: SentryBuildPluginManager): SourceMapHandling {
const decisions = new Map<string, SourceMapDecision>();
const pendingStagings = new Map<string, UploadStaging[]>();

return {
outputOptions: rawOutputOptions => {
const decision = sentryBuildPluginManager.sourceMaps.decide(
sourceMapKindFromRollupSetting(rawOutputOptions.sourcemap),
);
decisions.set(getOutputDestination(rawOutputOptions), decision);

return decision.enableHidden ? { ...rawOutputOptions, sourcemap: 'hidden' } : null;
},

setDecision: (destination, decision) => {
decisions.set(destination, decision);
},

stageSourceMaps: async (outputOptions, bundle, isWrite) => {
// `generate()` never reaches `writeBundle`, so nothing would consume the staging.
if (isWrite === false) {
return;
}

const destination = getOutputDestination(outputOptions);
if (!decisions.get(destination)?.deleteAfterUpload) {
return;
}

const staging = await sentryBuildPluginManager.createUploadStaging();

for (const output of Object.values(bundle)) {
if (output.type !== 'chunk' || !isJsFile(output.fileName)) {
continue;
}

const mapFileName = output.sourcemapFileName ?? `${output.fileName}.map`;
const mapAsset = bundle[mapFileName];
if (mapAsset?.type !== 'asset' || typeof mapAsset.source !== 'string') {
continue;
}

const result = await staging.stage({
code: output.code,
mapSource: mapAsset.source,
mapDir: path.join(destination, path.dirname(output.fileName)),
fileName: output.fileName,
});

if (result === 'staged') {
// oxlint-disable-next-line typescript/no-dynamic-delete -- Rollup drops an asset when its key is deleted from the bundle
delete bundle[mapFileName];
} else {
sentryBuildPluginManager.logger.warn(
`Could not prepare ${output.fileName} for upload, so its source map ${mapFileName} stays in the build output.`,
);
}
}

const stagings = pendingStagings.get(destination) ?? [];
stagings.push(staging);
pendingStagings.set(destination, stagings);
},

takeStaging: outputOptions => {
return pendingStagings.get(getOutputDestination(outputOptions))?.shift();
},
};
}
Loading
Loading