forked from getsentry/sentry
-
Notifications
You must be signed in to change notification settings - Fork 0
/
webpack.config.ts
626 lines (568 loc) · 20.1 KB
/
webpack.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
/* eslint-env node */
/* eslint import/no-nodejs-modules:0 */
import fs from 'fs';
import path from 'path';
import CompressionPlugin from 'compression-webpack-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import webpack from 'webpack';
import {Configuration as DevServerConfig} from 'webpack-dev-server';
import FixStyleOnlyEntriesPlugin from 'webpack-remove-empty-scripts';
import IntegrationDocsFetchPlugin from './build-utils/integration-docs-fetch-plugin';
import LastBuiltPlugin from './build-utils/last-built-plugin';
import SentryInstrumentation from './build-utils/sentry-instrumentation';
import babelConfig from './babel.config';
/**
* Merges the devServer config into the webpack config
*
* See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/43232
*/
interface Configuration extends webpack.Configuration {
devServer?: DevServerConfig;
}
const {env} = process;
/**
* Environment configuration
*/
const IS_PRODUCTION = env.NODE_ENV === 'production';
const IS_TEST = env.NODE_ENV === 'test' || !!env.TEST_SUITE;
const IS_STORYBOOK = env.STORYBOOK_BUILD === '1';
// This is used to stop rendering dynamic content for tests/snapshots
// We want it in the case where we are running tests and it is in CI,
// this should not happen in local
const IS_CI = !!env.CI;
// We intentionally build in production mode for acceptance tests, so we explicitly use an env var to
// say that the bundle will be used in acceptance tests. This affects webpack plugins and components
// with dynamic data that render differently statically in tests.
//
// Note, cannot assume it is an acceptance test if `IS_CI` is true, as our image builds has the
// `CI` env var set.
const IS_ACCEPTANCE_TEST = !!env.IS_ACCEPTANCE_TEST;
const IS_DEPLOY_PREVIEW = !!env.NOW_GITHUB_DEPLOYMENT;
const IS_UI_DEV_ONLY = !!env.SENTRY_UI_DEV_ONLY;
const DEV_MODE = !(IS_PRODUCTION || IS_CI);
const WEBPACK_MODE: Configuration['mode'] = IS_PRODUCTION ? 'production' : 'development';
/**
* Environment variables that are used by other tooling and should
* not be user configurable.
*/
// Ports used by webpack dev server to proxy to backend and webpack
const SENTRY_BACKEND_PORT = env.SENTRY_BACKEND_PORT;
const SENTRY_WEBPACK_PROXY_HOST = env.SENTRY_WEBPACK_PROXY_HOST;
const SENTRY_WEBPACK_PROXY_PORT = env.SENTRY_WEBPACK_PROXY_PORT;
// Used by sentry devserver runner to force using webpack-dev-server
const FORCE_WEBPACK_DEV_SERVER = !!env.FORCE_WEBPACK_DEV_SERVER;
const HAS_WEBPACK_DEV_SERVER_CONFIG =
!!SENTRY_BACKEND_PORT && !!SENTRY_WEBPACK_PROXY_PORT;
/**
* User/tooling configurable environment variables
*/
const NO_DEV_SERVER = !!env.NO_DEV_SERVER; // Do not run webpack dev server
const SHOULD_FORK_TS = DEV_MODE && !env.NO_TS_FORK; // Do not run fork-ts plugin (or if not dev env)
const SHOULD_HOT_MODULE_RELOAD = DEV_MODE && !!env.SENTRY_UI_HOT_RELOAD;
// Deploy previews are built using zeit. We can check if we're in zeit's
// build process by checking the existence of the PULL_REQUEST env var.
const DEPLOY_PREVIEW_CONFIG = IS_DEPLOY_PREVIEW && {
branch: env.NOW_GITHUB_COMMIT_REF,
commitSha: env.NOW_GITHUB_COMMIT_SHA,
githubOrg: env.NOW_GITHUB_COMMIT_ORG,
githubRepo: env.NOW_GITHUB_COMMIT_REPO,
};
// When deploy previews are enabled always enable experimental SPA mode --
// deploy previews are served standalone. Otherwise fallback to the environment
// configuration.
const SENTRY_EXPERIMENTAL_SPA =
!DEPLOY_PREVIEW_CONFIG && !IS_UI_DEV_ONLY ? !!env.SENTRY_EXPERIMENTAL_SPA : true;
// We should only read from the SENTRY_SPA_DSN env variable if SENTRY_EXPERIMENTAL_SPA
// is true. This is to make sure we can validate that the experimental SPA mode is
// working properly.
const SENTRY_SPA_DSN = SENTRY_EXPERIMENTAL_SPA ? env.SENTRY_SPA_DSN : undefined;
// this is the path to the django "sentry" app, we output the webpack build here to `dist`
// so that `django collectstatic` and so that we can serve the post-webpack bundles
const sentryDjangoAppPath = path.join(__dirname, 'src/sentry/static/sentry');
const distPath = env.SENTRY_STATIC_DIST_PATH || path.join(sentryDjangoAppPath, 'dist');
const staticPrefix = path.join(__dirname, 'static');
/**
* Locale file extraction build step
*/
if (env.SENTRY_EXTRACT_TRANSLATIONS === '1') {
babelConfig.plugins?.push([
'module:babel-gettext-extractor',
{
fileName: 'build/javascript.po',
baseDirectory: path.join(__dirname, 'src/sentry'),
functionNames: {
gettext: ['msgid'],
ngettext: ['msgid', 'msgid_plural', 'count'],
gettextComponentTemplate: ['msgid'],
t: ['msgid'],
tn: ['msgid', 'msgid_plural', 'count'],
tct: ['msgid'],
},
},
]);
}
/**
* Locale compilation and optimizations.
*
* Locales are code-split from the app and vendor chunk into separate chunks
* that will be loaded by layout.html depending on the users configured locale.
*
* Code splitting happens using the splitChunks plugin, configured under the
* `optimization` key of the webpack module. We create chunk (cache) groups for
* each of our supported locales and extract the PO files and moment.js locale
* files into each chunk.
*
* A plugin is used to remove the locale chunks from the app entry's chunk
* dependency list, so that our compiled bundle does not expect that *all*
* locale chunks must be loaded
*/
const localeCatalogPath = path.join(
__dirname,
'src',
'sentry',
'locale',
'catalogs.json'
);
type LocaleCatalog = {
supported_locales: string[];
};
const localeCatalog: LocaleCatalog = JSON.parse(
fs.readFileSync(localeCatalogPath, 'utf8')
);
// Translates a locale name to a language code.
//
// * po files are kept in a directory represented by the locale name [0]
// * moment.js locales are stored as language code files
//
// [0] https://docs.djangoproject.com/en/2.1/topics/i18n/#term-locale-name
const localeToLanguage = (locale: string) => locale.toLowerCase().replace('_', '-');
const supportedLocales = localeCatalog.supported_locales;
const supportedLanguages = supportedLocales.map(localeToLanguage);
type CacheGroups = Exclude<
NonNullable<Configuration['optimization']>['splitChunks'],
false | undefined
>['cacheGroups'];
type CacheGroupTest = (
module: webpack.Module,
context: Parameters<webpack.optimize.SplitChunksPlugin['options']['getCacheGroups']>[1]
) => boolean;
// A mapping of chunk groups used for locale code splitting
const localeChunkGroups: CacheGroups = {};
supportedLocales
// No need to split the english locale out as it will be completely empty and
// is not included in the django layout.html.
.filter(l => l !== 'en')
.forEach(locale => {
const language = localeToLanguage(locale);
const group = `locale/${language}`;
// List of module path tests to group into locale chunks
const localeGroupTests = [
new RegExp(`locale\\/${locale}\\/.*\\.po$`),
new RegExp(`moment\\/locale\\/${language}\\.js$`),
];
// module test taken from [0] and modified to support testing against
// multiple expressions.
//
// [0] https://github.com/webpack/webpack/blob/7a6a71f1e9349f86833de12a673805621f0fc6f6/lib/optimize/SplitChunksPlugin.js#L309-L320
const groupTest: CacheGroupTest = (module, {chunkGraph}) =>
localeGroupTests.some(pattern =>
pattern.test(module?.nameForCondition?.() ?? '')
? true
: chunkGraph.getModuleChunks(module).some(c => c.name && pattern.test(c.name))
);
// We are defining a chunk that combines the django language files with
// moment's locales as if you want one, you will want the other.
//
// In the application code you will still need to import via their module
// paths and not the chunk name
localeChunkGroups[group] = {
chunks: 'async',
name: group,
test: groupTest,
enforce: true,
};
});
const babelOptions = {...babelConfig, cacheDirectory: true};
const babelLoaderConfig = {
loader: 'babel-loader',
options: babelOptions,
};
/**
* Main Webpack config for Sentry React SPA.
*/
let appConfig: Configuration = {
mode: WEBPACK_MODE,
entry: {
/**
* Main Sentry SPA
*
* The order here matters for `getsentry`
*/
app: ['app/utils/statics-setup', 'app'],
/**
* Pipeline View for integrations
*/
pipeline: ['app/utils/statics-setup', 'app/views/integrationPipeline'],
/**
* Legacy CSS Webpack appConfig for Django-powered views.
* This generates a single "sentry.css" file that imports ALL component styles
* for use on Django-powered pages.
*/
sentry: 'less/sentry.less',
},
context: staticPrefix,
module: {
/**
* XXX: Modifying the order/contents of these rules may break `getsentry`
* Please remember to test it.
*/
rules: [
{
test: /\.[tj]sx?$/,
include: [staticPrefix],
exclude: /(vendor|node_modules|dist)/,
use: babelLoaderConfig,
},
{
test: /\.po$/,
use: {
loader: 'po-catalog-loader',
options: {
referenceExtensions: ['.js', '.jsx'],
domain: 'sentry',
},
},
},
{
test: /\.pegjs/,
use: {loader: 'pegjs-loader'},
},
{
test: /\.css/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/,
include: [staticPrefix],
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: 'auto',
},
},
'css-loader',
'less-loader',
],
},
{
test: /\.(woff|woff2|ttf|eot|svg|png|gif|ico|jpg|mp4)($|\?)/,
type: 'asset',
},
],
noParse: [
// don't parse known, pre-built javascript files (improves webpack perf)
/dist\/jquery\.js/,
/jed\/jed\.js/,
/marked\/lib\/marked\.js/,
/terser\/dist\/bundle\.min\.js/,
],
},
plugins: [
// Do not bundle moment's locale files as we will lazy load them using
// dynamic imports in the application code
new webpack.IgnorePlugin({
contextRegExp: /moment$/,
resourceRegExp: /^\.\/locale$/,
}),
/**
* jQuery must be provided in the global scope specifically and only for
* bootstrap, as it will not import jQuery itself.
*
* We discourage the use of global jQuery through eslint rules
*/
new webpack.ProvidePlugin({
jQuery: 'jquery',
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
}),
/**
* Extract CSS into separate files.
*/
new MiniCssExtractPlugin({
// We want the sentry css file to be unversioned for frontend-only deploys
// We will cache using `Cache-Control` headers
filename: 'entrypoints/[name].css',
}),
/**
* Defines environment specific flags.
*/
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(env.NODE_ENV),
IS_ACCEPTANCE_TEST: JSON.stringify(IS_ACCEPTANCE_TEST),
DEPLOY_PREVIEW_CONFIG: JSON.stringify(DEPLOY_PREVIEW_CONFIG),
EXPERIMENTAL_SPA: JSON.stringify(SENTRY_EXPERIMENTAL_SPA),
SPA_DSN: JSON.stringify(SENTRY_SPA_DSN),
},
}),
/**
* This removes empty js files for style only entries (e.g. sentry.less)
*/
new FixStyleOnlyEntriesPlugin({verbose: false}),
/**
* Adds build time measurement instrumentation, which will be reported back
* to sentry
*/
new SentryInstrumentation(),
...(SHOULD_FORK_TS
? [
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.resolve(__dirname, './config/tsconfig.build.json'),
configOverwrite: {
compilerOptions: {incremental: true},
},
},
logger: {devServer: false},
}),
]
: []),
/**
* Restrict translation files that are pulled in through app/translations.jsx
* and through moment/locale/* to only those which we create bundles for via
* locale/catalogs.json.
*
* Without this, webpack will still output all of the unused locale files despite
* the application never loading any of them.
*/
new webpack.ContextReplacementPlugin(
/sentry-locale$/,
path.join(__dirname, 'src', 'sentry', 'locale', path.sep),
true,
new RegExp(`(${supportedLocales.join('|')})/.*\\.po$`)
),
new webpack.ContextReplacementPlugin(
/moment\/locale/,
new RegExp(`(${supportedLanguages.join('|')})\\.js$`)
),
],
resolve: {
alias: {
app: path.join(staticPrefix, 'app'),
'sentry-images': path.join(staticPrefix, 'images'),
'sentry-logos': path.join(sentryDjangoAppPath, 'images', 'logos'),
'sentry-fonts': path.join(staticPrefix, 'fonts'),
// Aliasing this for getsentry's build, otherwise `less/select2` will not be able
// to be resolved
less: path.join(staticPrefix, 'less'),
'sentry-test': path.join(__dirname, 'tests', 'js', 'sentry-test'),
'sentry-locale': path.join(__dirname, 'src', 'sentry', 'locale'),
'ios-device-list': path.join(
__dirname,
'node_modules',
'ios-device-list',
'dist',
'ios-device-list.min.js'
),
},
fallback: {
vm: false,
stream: false,
crypto: require.resolve('crypto-browserify'),
// `yarn why` says this is only needed in dev deps
string_decoder: false,
},
modules: ['node_modules'],
extensions: ['.jsx', '.js', '.json', '.ts', '.tsx', '.less'],
},
output: {
clean: true, // Clean the output directory before emit.
path: distPath,
publicPath: '',
filename: 'entrypoints/[name].js',
chunkFilename: 'chunks/[name].[contenthash].js',
sourceMapFilename: 'sourcemaps/[name].[contenthash].js.map',
assetModuleFilename: 'assets/[name].[contenthash][ext]',
},
optimization: {
chunkIds: 'named',
moduleIds: 'named',
splitChunks: {
// Only affect async chunks, otherwise webpack could potentially split our initial chunks
// Which means the app will not load because we'd need these additional chunks to be loaded in our
// django template.
chunks: 'async',
maxInitialRequests: 10, // (default: 30)
maxAsyncRequests: 10, // (default: 30)
cacheGroups: {
...localeChunkGroups,
},
},
// This only runs in production mode
// Grabbed this example from https://github.com/webpack-contrib/css-minimizer-webpack-plugin
minimizer: ['...', new CssMinimizerPlugin()],
},
devtool: IS_PRODUCTION ? 'source-map' : 'eval-cheap-module-source-map',
};
if (IS_TEST || IS_ACCEPTANCE_TEST || IS_STORYBOOK) {
appConfig.resolve!.alias!['integration-docs-platforms'] = path.join(
__dirname,
'tests/fixtures/integration-docs/_platforms.json'
);
} else {
const plugin = new IntegrationDocsFetchPlugin({basePath: __dirname});
appConfig.plugins?.push(plugin);
appConfig.resolve!.alias!['integration-docs-platforms'] = plugin.modulePath;
}
if (IS_ACCEPTANCE_TEST) {
appConfig.plugins?.push(new LastBuiltPlugin({basePath: __dirname}));
}
// Dev only! Hot module reloading
if (
FORCE_WEBPACK_DEV_SERVER ||
(HAS_WEBPACK_DEV_SERVER_CONFIG && !NO_DEV_SERVER) ||
IS_UI_DEV_ONLY
) {
if (SHOULD_HOT_MODULE_RELOAD) {
// Hot reload react components on save
// We include the library here as to not break docker/google cloud builds
// since we do not install devDeps there.
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
appConfig.plugins?.push(new ReactRefreshWebpackPlugin());
}
appConfig.devServer = {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
},
// Required for getsentry
disableHostCheck: true,
contentBase: './src/sentry/static/sentry',
host: SENTRY_WEBPACK_PROXY_HOST,
hot: true,
// If below is false, will reload on errors
hotOnly: true,
port: Number(SENTRY_WEBPACK_PROXY_PORT),
stats: 'errors-only',
overlay: false,
watchOptions: {
ignored: ['node_modules'],
},
};
if (!IS_UI_DEV_ONLY) {
// This proxies to local backend server
const backendAddress = `http://localhost:${SENTRY_BACKEND_PORT}/`;
const relayAddress = 'http://127.0.0.1:7899';
appConfig.devServer = {
...appConfig.devServer,
publicPath: '/_static/dist/sentry',
// syntax for matching is using https://www.npmjs.com/package/micromatch
proxy: {
'/api/store/**': relayAddress,
'/api/{1..9}*({0..9})/**': relayAddress,
'/api/0/relays/outcomes/': relayAddress,
'!/_static/dist/sentry/**': backendAddress,
},
};
}
}
// XXX(epurkhiser): Sentry (development) can be run in an experimental
// pure-SPA mode, where ONLY /api* requests are proxied directly to the API
// backend (in this case, sentry.io), otherwise ALL requests are rewritten
// to a development index.html -- thus, completely separating the frontend
// from serving any pages through the backend.
//
// THIS IS EXPERIMENTAL and has limitations (e.g. you can't use SSO)
//
// Various sentry pages still rely on django to serve html views.
if (IS_UI_DEV_ONLY) {
// Try and load certificates from mkcert if available. Use $ yarn mkcert-localhost
const certPath = path.join(__dirname, 'config');
const https = !fs.existsSync(path.join(certPath, 'localhost.pem'))
? true
: {
key: fs.readFileSync(path.join(certPath, 'localhost-key.pem')),
cert: fs.readFileSync(path.join(certPath, 'localhost.pem')),
};
appConfig.devServer = {
...appConfig.devServer,
compress: true,
https,
publicPath: '/_assets/',
proxy: [
{
context: ['/api/', '/avatar/', '/organization-avatar/'],
target: 'https://sentry.io',
secure: false,
changeOrigin: true,
headers: {
Referer: 'https://sentry.io/',
},
},
],
historyApiFallback: {
rewrites: [{from: /^\/.*$/, to: '/_assets/index.html'}],
},
};
}
if (IS_UI_DEV_ONLY || IS_DEPLOY_PREVIEW) {
appConfig.output!.publicPath = '/_assets/';
/**
* Generate a index.html file used for running the app in pure client mode.
* This is currently used for PR deploy previews, where only the frontend
* is deployed.
*/
const HtmlWebpackPlugin = require('html-webpack-plugin');
appConfig.plugins?.push(
new HtmlWebpackPlugin({
// Local dev vs vercel slightly differs...
...(IS_UI_DEV_ONLY
? {devServer: `https://localhost:${SENTRY_WEBPACK_PROXY_PORT}`}
: {}),
favicon: path.resolve(sentryDjangoAppPath, 'images', 'favicon_dev.png'),
template: path.resolve(staticPrefix, 'index.ejs'),
mobile: true,
excludeChunks: ['pipeline'],
title: 'Sentry',
})
);
}
const minificationPlugins = [
// This compression-webpack-plugin generates pre-compressed files
// ending in .gz, to be picked up and served by our internal static media
// server as well as nginx when paired with the gzip_static module.
//
// TODO(ts): The current @types/compression-webpack-plugin is still targeting
// webpack@4, for now we just as any it.
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|map|css|svg|html|txt|ico|eot|ttf)$/,
}) as any,
// NOTE: In production mode webpack will automatically minify javascript
// using the TerserWebpackPlugin.
];
if (IS_PRODUCTION) {
// NOTE: can't do plugins.push(Array) because webpack/webpack#2217
minificationPlugins.forEach(plugin => appConfig.plugins?.push(plugin));
}
// Cache webpack builds
if (env.WEBPACK_CACHE_PATH) {
appConfig.cache = {
type: 'filesystem',
cacheLocation: path.resolve(__dirname, env.WEBPACK_CACHE_PATH),
buildDependencies: {
// This makes all dependencies of this file - build dependencies
config: [__filename],
// By default webpack and loaders are build dependencies
},
};
}
if (env.MEASURE) {
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
appConfig = smp.wrap(appConfig);
}
export default appConfig;