Skip to content

feat(webpack): Add sentry/webpack-plugin/webpack5 export for webpack 5.1+ and compatible environments #715

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

Merged
merged 4 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions packages/webpack-plugin/README_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pnpm add @sentry/webpack-plugin --save-dev
```js
// webpack.config.js
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
// for webpack 5.1 and webpack compatible environments
// const { sentryWebpackPlugin } = require("@sentry/webpack-plugin/webpack5");

module.exports = {
// ... other config above ...
Expand Down
15 changes: 12 additions & 3 deletions packages/webpack-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,25 @@
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
},
"./webpack5": {
"import": "./dist/esm/webpack5.mjs",
"require": "./dist/cjs/webpack5.js",
"types": "./dist/types/webpack5.d.ts"
}
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.mjs",
"types": "dist/types/index.d.ts",
"scripts": {
"build": "rimraf ./out && run-p build:rollup build:types",
"build": "rimraf ./dist && run-p build:rollup build:types",
"build:watch": "run-p build:rollup:watch build:types:watch",
"build:rollup": "rollup --config rollup.config.js",
"build:rollup:watch": "rollup --config rollup.config.js --watch --no-watch.clearScreen",
"build:rollup": "run-p build:rollup:default build:rollup:webpack5",
"build:rollup:default": "rollup --config rollup.config.default.js",
"build:rollup:webpack5": "rollup --config rollup.config.webpack5.js",
"build:rollup:watch": "run-p build:rollup:default:watch build:rollup:webpack5:watch",
"build:rollup:default:watch": "rollup --config rollup.config.default.js --watch --no-watch.clearScreen",
"build:rollup:webpack5:watch": "rollup --config rollup.config.webpack5.js --watch --no-watch.clearScreen",
"build:types": "tsc --project types.tsconfig.json",
"build:types:watch": "tsc --project types.tsconfig.json --watch --preserveWatchOutput",
"check:types": "run-p check:types:src check:types:test",
Expand Down
48 changes: 48 additions & 0 deletions packages/webpack-plugin/rollup.config.webpack5.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import resolve from "@rollup/plugin-node-resolve";
import babel from "@rollup/plugin-babel";
import packageJson from "./package.json";
import modulePackage from "module";

const input = ["src/webpack5.ts"];

const extensions = [".ts"];

export default {
input,
external: [...Object.keys(packageJson.dependencies), ...modulePackage.builtinModules, "webpack"],
onwarn: (warning) => {
if (warning.code === "CIRCULAR_DEPENDENCY") {
// Circular dependencies are usually not a big deal for us so let's just warn about them
console.warn(warning.message);
return;
}
// Warnings are usually high-consequence for us so let's throw to catch them
throw new Error(warning.message);
},
plugins: [
resolve({
extensions,
rootDir: "./src",
preferBuiltins: true,
}),
babel({
extensions,
babelHelpers: "bundled",
include: ["src/**/*"],
}),
],
output: [
{
file: packageJson.exports["./webpack5"].import,
format: "esm",
exports: "named",
sourcemap: true,
},
{
file: packageJson.exports["./webpack5"].require,
format: "cjs",
exports: "named",
sourcemap: true,
},
],
};
202 changes: 6 additions & 196 deletions packages/webpack-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,208 +1,18 @@
import {
getDebugIdSnippet,
Options,
sentryUnpluginFactory,
stringToUUID,
SentrySDKBuildFlags,
createComponentNameAnnotateHooks,
Logger,
} from "@sentry/bundler-plugin-core";
import * as path from "path";
import { UnpluginOptions } from "unplugin";
import { v4 as uuidv4 } from "uuid";
import { SentryWebpackPluginOptions, sentryWebpackUnpluginFactory } from "./plugin";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore webpack is a peer dep
import * as webpack4or5 from "webpack";

interface BannerPluginCallbackArg {
chunk?: {
hash?: string;
contentHash?: {
javascript?: string;
};
};
}
const BannerPlugin = webpack4or5?.BannerPlugin || webpack4or5?.default?.BannerPlugin;

function webpackReleaseInjectionPlugin(injectionCode: string): UnpluginOptions {
return {
name: "sentry-webpack-release-injection-plugin",
webpack(compiler) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore webpack version compatibility shenanigans
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const BannerPlugin =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore webpack version compatibility shenanigans
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
compiler?.webpack?.BannerPlugin ||
webpack4or5?.BannerPlugin ||
webpack4or5?.default?.BannerPlugin;
compiler.options.plugins = compiler.options.plugins || [];
compiler.options.plugins.push(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
new BannerPlugin({
raw: true,
include: /\.(js|ts|jsx|tsx|mjs|cjs)(\?[^?]*)?(#[^#]*)?$/,
banner: injectionCode,
})
);
},
};
}
const DefinePlugin = webpack4or5?.DefinePlugin || webpack4or5?.default?.DefinePlugin;

function webpackComponentNameAnnotatePlugin(ignoredComponents?: string[]): UnpluginOptions {
return {
name: "sentry-webpack-component-name-annotate-plugin",
enforce: "pre",
// Webpack needs this hook for loader logic, so the plugin is not run on unsupported file types
transformInclude(id) {
return id.endsWith(".tsx") || id.endsWith(".jsx");
},
transform: createComponentNameAnnotateHooks(ignoredComponents).transform,
};
}

function webpackBundleSizeOptimizationsPlugin(
replacementValues: SentrySDKBuildFlags
): UnpluginOptions {
return {
name: "sentry-webpack-bundle-size-optimizations-plugin",
webpack(compiler) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore webpack version compatibility shenanigans
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const DefinePlugin =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore webpack version compatibility shenanigans
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
compiler?.webpack?.DefinePlugin ||
webpack4or5?.DefinePlugin ||
webpack4or5?.default?.DefinePlugin;
compiler.options.plugins = compiler.options.plugins || [];
compiler.options.plugins.push(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
new DefinePlugin({
...replacementValues,
})
);
},
};
}

function webpackDebugIdInjectionPlugin(): UnpluginOptions {
return {
name: "sentry-webpack-debug-id-injection-plugin",
webpack(compiler) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore webpack version compatibility shenanigans
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const BannerPlugin =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore webpack version compatibility shenanigans
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
compiler?.webpack?.BannerPlugin ||
webpack4or5?.BannerPlugin ||
webpack4or5?.default?.BannerPlugin;
compiler.options.plugins = compiler.options.plugins || [];
compiler.options.plugins.push(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
new BannerPlugin({
raw: true,
include: /\.(js|ts|jsx|tsx|mjs|cjs)(\?[^?]*)?(#[^#]*)?$/,
banner: (arg?: BannerPluginCallbackArg) => {
const hash = arg?.chunk?.contentHash?.javascript ?? arg?.chunk?.hash;
const debugId = hash ? stringToUUID(hash) : uuidv4();
return getDebugIdSnippet(debugId);
},
})
);
},
};
}

function webpackDebugIdUploadPlugin(
upload: (buildArtifacts: string[]) => Promise<void>,
logger: Logger,
forceExitOnBuildCompletion?: boolean
): UnpluginOptions {
const pluginName = "sentry-webpack-debug-id-upload-plugin";
return {
name: pluginName,
webpack(compiler) {
compiler.hooks.afterEmit.tapAsync(pluginName, (compilation, callback: () => void) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const outputPath = (compilation.outputOptions.path as string | undefined) ?? path.resolve();
const buildArtifacts = Object.keys(compilation.assets as Record<string, unknown>).map(
(asset) => path.join(outputPath, asset)
);
void upload(buildArtifacts).then(() => {
callback();
});
});

if (forceExitOnBuildCompletion && compiler.options.mode === "production") {
compiler.hooks.done.tap(pluginName, () => {
setTimeout(() => {
logger.debug("Exiting process after debug file upload");
process.exit(0);
});
});
}
},
};
}

function webpackModuleMetadataInjectionPlugin(injectionCode: string): UnpluginOptions {
return {
name: "sentry-webpack-module-metadata-injection-plugin",
webpack(compiler) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore webpack version compatibility shenanigans
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const BannerPlugin =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore webpack version compatibility shenanigans
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
compiler?.webpack?.BannerPlugin ||
webpack4or5?.BannerPlugin ||
webpack4or5?.default?.BannerPlugin;
compiler.options.plugins = compiler.options.plugins || [];
compiler.options.plugins.push(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
new BannerPlugin({
raw: true,
include: /\.(js|ts|jsx|tsx|mjs|cjs)(\?[^?]*)?(#[^#]*)?$/,
banner: injectionCode,
})
);
},
};
}

const sentryUnplugin = sentryUnpluginFactory({
releaseInjectionPlugin: webpackReleaseInjectionPlugin,
componentNameAnnotatePlugin: webpackComponentNameAnnotatePlugin,
moduleMetadataInjectionPlugin: webpackModuleMetadataInjectionPlugin,
debugIdInjectionPlugin: webpackDebugIdInjectionPlugin,
debugIdUploadPlugin: webpackDebugIdUploadPlugin,
bundleSizeOptimizationsPlugin: webpackBundleSizeOptimizationsPlugin,
const sentryUnplugin = sentryWebpackUnpluginFactory({
BannerPlugin,
DefinePlugin,
});

type SentryWebpackPluginOptions = Options & {
_experiments?: Options["_experiments"] & {
/**
* If enabled, the webpack plugin will exit the build process after the build completes.
* Use this with caution, as it will terminate the process.
*
* More information: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/345
*
* @default false
*/
forceExitOnBuildCompletion?: boolean;
};
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any =
sentryUnplugin.webpack;
Expand Down
Loading
Loading