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 @@ -15,6 +15,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 @@ -15,6 +15,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 @@ -14,6 +14,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
131 changes: 129 additions & 2 deletions packages/bundler-plugins/src/webpack/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* oxlint-disable max-lines */

import type { Options } from '../core/index';
import type { Options, SourceMapDecision, SourceMapKind, UploadStaging } from '../core/index';
import {
createSentryBuildPluginManager,
generateReleaseInjectorCode,
Expand Down Expand Up @@ -71,6 +71,7 @@ type WebpackCompiler = {
options: {
plugins?: unknown[];
mode?: string;
devtool?: string | false;
module?: {
rules?: unknown[];
};
Expand All @@ -88,12 +89,16 @@ type WebpackCompiler = {
done: {
tap: (name: string, callback: () => void) => void;
};
failed?: {
tap: (name: string, callback: () => void) => void;
};
};
webpack?: {
DefinePlugin?: PluginClass;
Compilation?: {
PROCESS_ASSETS_STAGE_ADDITIONS?: number;
PROCESS_ASSETS_STAGE_DEV_TOOLING?: number;
PROCESS_ASSETS_STAGE_REPORT?: number;
};
sources?: {
RawSource?: WebpackRawSource;
Expand All @@ -116,16 +121,44 @@ type WebpackCompilation = {
getAssets: () => WebpackAsset[];
getAsset: (name: string) => WebpackAsset | undefined;
updateAsset: (name: string, source: WebpackSource) => void;
deleteAsset: (name: string) => void;
hooks: {
processAssets: {
tap: (
options: { name: string; stage: number },
callback: (assets: Record<string, WebpackSource>) => void,
) => void;
tapPromise: (options: { name: string; stage: number }, callback: () => Promise<void>) => void;
};
};
};

/**
* Maps webpack's `devtool` onto the plugin's source map kinds. A user who sets `devtool: false`
* and adds a `SourceMapDevToolPlugin` manages source maps themselves.
*/
function sourceMapKindFromDevtool(compiler: WebpackCompiler): SourceMapKind | undefined {
const devtool = compiler.options.devtool;

if (devtool === undefined) {
return 'unset';
}
if (devtool === false) {
const hasSourceMapPlugin = compiler.options.plugins?.some(
plugin =>
(plugin as { constructor?: { name?: string } } | undefined)?.constructor?.name === 'SourceMapDevToolPlugin',
);
return hasSourceMapPlugin ? undefined : 'off';
}
if (devtool.includes('inline') || devtool.includes('eval')) {
return 'inline';
}
if (devtool.includes('hidden')) {
return 'hidden';
}
return 'visible';
}

type WebpackModule = {
version?: string;
default?: { version?: string };
Expand Down Expand Up @@ -171,6 +204,51 @@ function addDebugIdsToAssets(compilation: WebpackCompilation, RawSource: Webpack
}
}

/**
* Copies each JS asset and its source map into the upload staging folder and drops the source map
* assets (and their compressed variants) from the compilation, so webpack never emits them.
*/
async function stageSourceMaps(
compilation: WebpackCompilation,
staging: UploadStaging,
logger: { warn: (message: string) => void },
): Promise<void> {
const outputPath = compilation.outputOptions.path ?? path.resolve();

for (const asset of compilation.getAssets()) {
if (!isJsFile(asset.name)) {
continue;
}

const relatedSourceMap = asset.info.related?.sourceMap;
const sourceMapName = typeof relatedSourceMap === 'string' ? relatedSourceMap : `${asset.name}.map`;
const sourceMapAsset = compilation.getAsset(sourceMapName);
if (!sourceMapAsset) {
continue;
}

const result = await staging.stage({
code: asset.source.source().toString(),
mapSource: sourceMapAsset.source.source().toString(),
mapDir: path.join(outputPath, path.dirname(sourceMapName)),
fileName: asset.name,
});

if (result !== 'staged') {
logger.warn(
`Could not prepare ${asset.name} for upload, so its source map ${sourceMapName} stays in the build output.`,
);
continue;
}

for (const candidate of compilation.getAssets()) {
if (candidate.name === sourceMapName || candidate.name.startsWith(`${sourceMapName}.`)) {
compilation.deleteAsset(candidate.name);
}
}
}
}

function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) {
const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, {
loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? '[sentry-webpack-plugin]',
Expand Down Expand Up @@ -231,6 +309,21 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {})

const transformReplace = Object.keys(replacementValues).length > 0;

function decideSourceMaps(compiler: WebpackCompiler): SourceMapDecision {
const kind = compiler.options.mode === 'development' ? undefined : sourceMapKindFromDevtool(compiler);
if (kind === undefined) {
return { enableHidden: false, deleteAfterUpload: false };
}

const decision = sentryBuildPluginManager.sourceMaps.decide(kind);

if (decision.enableHidden) {
compiler.options.devtool = kind === 'visible' ? `hidden-${compiler.options.devtool}` : 'hidden-source-map';
}

return decision;
}

function addCodeInjection(compiler: WebpackCompiler): void {
if (releaseInjectionCode.isEmpty() && chunkInjectionCode.isEmpty() && !sourcemapsEnabled) {
return;
Expand Down Expand Up @@ -299,6 +392,35 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {})

const { DefinePlugin } = compiler.webpack ?? {};

// One plugin instance can serve several compilers (`MultiCompiler`) and
// a compiler several compilations (watch mode), so nothing is shared.
const sourceMapDecision = decideSourceMaps(compiler);
const pendingStagings = new Map<WebpackCompilation, UploadStaging>();

if (sourceMapDecision.deleteAfterUpload) {
// After `RealContentHashPlugin` rewrote the hash placeholders, so the staged
// JS equals the emitted file and `[contenthash]` names are final.
const stage = (compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_REPORT ?? 5000) + 1;

compiler.hooks.thisCompilation.tap('sentry-webpack-plugin', compilation => {
compilation.hooks.processAssets.tapPromise({ name: 'sentry-webpack-plugin', stage }, async () => {
const staging = await sentryBuildPluginManager.createUploadStaging();
pendingStagings.set(compilation, staging);
await stageSourceMaps(compilation, staging, logger);
});
});

// A compilation that fails before emitting never reaches `afterEmit`.
const discardPendingStagings = (): void => {
for (const staging of pendingStagings.values()) {
void staging.discard();
}
pendingStagings.clear();
};
compiler.hooks.done.tap('sentry-webpack-plugin', discardPendingStagings);
compiler.hooks.failed?.tap('sentry-webpack-plugin', discardPendingStagings);
}

// BannerPlugin prepends executable code before directive prologues.
addCodeInjection(compiler);

Expand Down Expand Up @@ -354,13 +476,18 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {})
const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts();
const upload = createDebugIdUploadFunction({ sentryBuildPluginManager });

const staging = pendingStagings.get(compilation);
pendingStagings.delete(compilation);

const run = async (): Promise<void> => {
try {
await sentryBuildPluginManager.createRelease();
if (sourcemapsEnabled && options.sourcemaps?.disable !== 'disable-upload') {
const outputPath = compilation.outputOptions.path ?? path.resolve();
const buildArtifacts = Object.keys(compilation.assets).map(asset => path.join(outputPath, asset));
await upload(buildArtifacts);
await upload(buildArtifacts, { staging });
} else {
await staging?.discard();
}
} finally {
freeGlobalDependencyOnBuildArtifacts();
Expand Down
Loading
Loading