webpack.config.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. /*eslint-env node*/
  2. /*eslint no-var:0 import/no-nodejs-modules:0 */
  3. var path = require('path'),
  4. fs = require('fs'),
  5. webpack = require('webpack'),
  6. ExtractTextPlugin = require('extract-text-webpack-plugin'),
  7. LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
  8. var staticPrefix = 'src/sentry/static/sentry',
  9. distPath = path.join(__dirname, staticPrefix, 'dist');
  10. // this is set by setup.py sdist
  11. if (process.env.SENTRY_STATIC_DIST_PATH) {
  12. distPath = process.env.SENTRY_STATIC_DIST_PATH;
  13. }
  14. var IS_PRODUCTION = process.env.NODE_ENV === 'production';
  15. var IS_TEST = process.env.NODE_ENV === 'TEST' || process.env.TEST_SUITE;
  16. var WEBPACK_DEV_PORT = process.env.WEBPACK_DEV_PORT;
  17. var SENTRY_DEVSERVER_PORT = process.env.SENTRY_DEVSERVER_PORT;
  18. var USE_HOT_MODULE_RELOAD = !IS_PRODUCTION && WEBPACK_DEV_PORT && SENTRY_DEVSERVER_PORT;
  19. var babelConfig = JSON.parse(fs.readFileSync(path.join(__dirname, '.babelrc')));
  20. babelConfig.cacheDirectory = true;
  21. // only extract po files if we need to
  22. if (process.env.SENTRY_EXTRACT_TRANSLATIONS === '1') {
  23. babelConfig.plugins.push([
  24. 'babel-gettext-extractor',
  25. {
  26. fileName: 'build/javascript.po',
  27. baseDirectory: path.join(__dirname, 'src/sentry'),
  28. functionNames: {
  29. gettext: ['msgid'],
  30. ngettext: ['msgid', 'msgid_plural', 'count'],
  31. gettextComponentTemplate: ['msgid'],
  32. t: ['msgid'],
  33. tn: ['msgid', 'msgid_plural', 'count'],
  34. tct: ['msgid']
  35. }
  36. }
  37. ]);
  38. }
  39. var appEntry = {
  40. app: ['app'],
  41. vendor: [
  42. 'babel-polyfill',
  43. // Yes this is included in prod builds, but has no effect on render and build size in prod
  44. 'react-hot-loader/patch',
  45. 'bootstrap/js/dropdown',
  46. 'bootstrap/js/tab',
  47. 'bootstrap/js/tooltip',
  48. 'bootstrap/js/alert',
  49. 'crypto-js/md5',
  50. 'jed',
  51. 'jquery',
  52. 'marked',
  53. 'moment',
  54. 'moment-timezone',
  55. 'raven-js',
  56. 'react',
  57. 'react-dom',
  58. 'react-dom/server',
  59. 'react-document-title',
  60. 'react-router',
  61. 'react-bootstrap/lib/Modal',
  62. 'react-sparklines',
  63. 'reflux',
  64. 'select2',
  65. 'vendor/simple-slider/simple-slider',
  66. 'ios-device-list'
  67. ]
  68. };
  69. // dynamically iterate over locale files and add to `entry` appConfig
  70. var localeCatalogPath = path.join(__dirname, 'src', 'sentry', 'locale', 'catalogs.json');
  71. var localeCatalog = JSON.parse(fs.readFileSync(localeCatalogPath, 'utf8'));
  72. var localeEntries = [];
  73. localeCatalog.supported_locales.forEach(function(locale) {
  74. if (locale === 'en') return;
  75. // Django locale names are "zh_CN", moment's are "zh-cn"
  76. var normalizedLocale = locale.toLowerCase().replace('_', '-');
  77. appEntry['locale/' + normalizedLocale] = [
  78. 'moment/locale/' + normalizedLocale,
  79. 'sentry-locale/' + locale + '/LC_MESSAGES/django.po' // relative to static/sentry
  80. ];
  81. localeEntries.push('locale/' + normalizedLocale);
  82. });
  83. /**
  84. * Main Webpack config for Sentry React SPA.
  85. */
  86. var appConfig = {
  87. entry: appEntry,
  88. context: path.join(__dirname, staticPrefix),
  89. module: {
  90. rules: [
  91. {
  92. test: /\.jsx?$/,
  93. loader: 'babel-loader',
  94. include: path.join(__dirname, staticPrefix),
  95. exclude: /(vendor|node_modules|dist)/,
  96. query: babelConfig
  97. },
  98. {
  99. test: /\.po$/,
  100. loader: 'po-catalog-loader',
  101. query: {
  102. referenceExtensions: ['.js', '.jsx'],
  103. domain: 'sentry'
  104. }
  105. },
  106. {
  107. test: /\.json$/,
  108. loader: 'json-loader'
  109. },
  110. // loader for dynamic styles imported into components (embedded as js)
  111. {
  112. test: /\.less$/,
  113. use: [
  114. {
  115. loader: 'style-loader'
  116. },
  117. {
  118. loader: 'css-loader',
  119. options: {
  120. minimize: IS_PRODUCTION
  121. }
  122. },
  123. {
  124. loader: 'less-loader'
  125. }
  126. ]
  127. },
  128. {
  129. test: /\.(woff|woff2|ttf|eot|svg|png|gif|ico|jpg)($|\?)/,
  130. loader: 'file-loader?name=' + '[name].[ext]'
  131. }
  132. ],
  133. noParse: [
  134. // don't parse known, pre-built javascript files (improves webpack perf)
  135. /dist\/jquery\.js/,
  136. /jed\/jed\.js/,
  137. /marked\/lib\/marked\.js/
  138. ]
  139. },
  140. plugins: [
  141. new LodashModuleReplacementPlugin({
  142. collections: true,
  143. currying: true, // these are enabled to support lodash/fp/ features
  144. flattening: true // used by a dependency of react-mentions
  145. }),
  146. new webpack.optimize.CommonsChunkPlugin({
  147. names: localeEntries.concat(['vendor']) // 'vendor' must be last entry
  148. }),
  149. new webpack.ProvidePlugin({
  150. $: 'jquery',
  151. jQuery: 'jquery',
  152. 'window.jQuery': 'jquery',
  153. 'root.jQuery': 'jquery',
  154. Raven: 'raven-js'
  155. }),
  156. new ExtractTextPlugin('[name].css'),
  157. new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), // ignore moment.js locale files
  158. new webpack.DefinePlugin({
  159. 'process.env': {
  160. NODE_ENV: JSON.stringify(process.env.NODE_ENV),
  161. IS_PERCY: JSON.stringify(
  162. process.env.CI && !!process.env.PERCY_TOKEN && !!process.env.TRAVIS
  163. )
  164. }
  165. }),
  166. // restrict translation files pulled into dist/app.js to only those specified
  167. // in locale/catalogs.json
  168. new webpack.ContextReplacementPlugin(
  169. /locale$/,
  170. path.join(__dirname, 'src', 'sentry', 'locale', path.sep),
  171. true,
  172. new RegExp('(' + localeCatalog.supported_locales.join('|') + ')\/.*\\.po$')
  173. )
  174. ],
  175. resolve: {
  176. alias: {
  177. 'sentry-locale': path.join(__dirname, 'src', 'sentry', 'locale'),
  178. 'integration-docs-platforms': IS_TEST
  179. ? path.join(__dirname, 'tests/fixtures/_platforms.json')
  180. : path.join(__dirname, 'src/sentry/integration-docs/_platforms.json')
  181. },
  182. modules: [path.join(__dirname, staticPrefix), 'node_modules'],
  183. extensions: ['.less', '.jsx', '.js', '.json']
  184. },
  185. output: {
  186. path: distPath,
  187. filename: '[name].js',
  188. libraryTarget: 'var',
  189. library: 'exports',
  190. sourceMapFilename: '[name].js.map'
  191. },
  192. devtool: IS_PRODUCTION ? '#source-map' : '#cheap-source-map'
  193. };
  194. /**
  195. * Webpack entry for password strength checker
  196. */
  197. var pwConfig = {
  198. entry: {
  199. pwstrength: './index'
  200. },
  201. context: path.resolve(path.join(__dirname, staticPrefix), 'js', 'pwstrength'),
  202. module: {},
  203. plugins: [],
  204. resolve: {
  205. modules: [path.join(__dirname, staticPrefix), 'node_modules'],
  206. extensions: ['.js']
  207. },
  208. output: {
  209. path: distPath,
  210. filename: '[name].js',
  211. libraryTarget: 'window',
  212. library: 'sentrypw',
  213. sourceMapFilename: '[name].js.map'
  214. },
  215. devtool: IS_PRODUCTION ? '#source-map' : '#cheap-source-map'
  216. };
  217. /**
  218. * Legacy CSS Webpack appConfig for Django-powered views.
  219. * This generates a single "sentry.css" file that imports ALL component styles
  220. * for use on Django-powered pages.
  221. */
  222. var legacyCssConfig = {
  223. entry: {
  224. sentry: 'less/sentry.less'
  225. },
  226. context: path.join(__dirname, staticPrefix),
  227. output: {
  228. path: distPath,
  229. filename: '[name].css'
  230. },
  231. plugins: [new ExtractTextPlugin('[name].css')],
  232. resolve: {
  233. extensions: ['.less', '.js'],
  234. modules: [path.join(__dirname, staticPrefix), 'node_modules']
  235. },
  236. module: {
  237. rules: [
  238. {
  239. test: /\.less$/,
  240. include: path.join(__dirname, staticPrefix),
  241. use: ExtractTextPlugin.extract({
  242. fallback: 'style-loader?sourceMap=false',
  243. use: [
  244. {
  245. loader: 'css-loader',
  246. options: {
  247. minimize: IS_PRODUCTION
  248. }
  249. },
  250. 'less-loader'
  251. ]
  252. })
  253. },
  254. {
  255. test: /\.(woff|woff2|ttf|eot|svg|png|gif|ico|jpg)($|\?)/,
  256. loader: 'file-loader?name=' + '[name].[ext]'
  257. }
  258. ]
  259. }
  260. };
  261. // Dev only! Hot module reloading
  262. if (USE_HOT_MODULE_RELOAD) {
  263. // Otherwise with hot reloads we get module ID number
  264. appConfig.plugins.push(new webpack.NamedModulesPlugin());
  265. // HMR
  266. appConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
  267. appConfig.devServer = {
  268. headers: {
  269. 'Access-Control-Allow-Origin': '*',
  270. 'Access-Control-Allow-Credentials': 'true'
  271. },
  272. // Required for getsentry
  273. disableHostCheck: true,
  274. contentBase: './src/sentry/static/sentry',
  275. hot: true,
  276. // If below is false, will reload on errors
  277. hotOnly: true,
  278. port: WEBPACK_DEV_PORT
  279. };
  280. // Required, without this we get this on updates:
  281. // [HMR] Update failed: SyntaxError: Unexpected token < in JSON at position 12
  282. appConfig.output.publicPath = 'http://localhost:' + WEBPACK_DEV_PORT + '/';
  283. }
  284. var minificationPlugins = [
  285. // This compression-webpack-plugin generates pre-compressed files
  286. // ending in .gz, to be picked up and served by our internal static media
  287. // server as well as nginx when paired with the gzip_static module.
  288. new (require('compression-webpack-plugin'))({
  289. algorithm: function(buffer, options, callback) {
  290. require('zlib').gzip(buffer, callback);
  291. },
  292. regExp: /\.(js|map|css|svg|html|txt|ico|eot|ttf)$/
  293. }),
  294. // Disable annoying UglifyJS warnings that pollute Travis log output
  295. // NOTE: This breaks -p in webpack 2. Must call webpack w/ NODE_ENV=production for minification.
  296. new webpack.optimize.UglifyJsPlugin({
  297. compress: {
  298. warnings: false
  299. },
  300. // https://github.com/webpack/webpack/blob/951a7603d279c93c936e4b8b801a355dc3e26292/bin/convert-argv.js#L442
  301. sourceMap: appConfig.devtool &&
  302. (appConfig.devtool.indexOf('sourcemap') >= 0 ||
  303. appConfig.devtool.indexOf('source-map') >= 0)
  304. })
  305. ];
  306. if (IS_PRODUCTION) {
  307. // NOTE: can't do plugins.push(Array) because webpack/webpack#2217
  308. minificationPlugins.forEach(function(plugin) {
  309. appConfig.plugins.push(plugin);
  310. pwConfig.plugins.push(plugin);
  311. legacyCssConfig.plugins.push(plugin);
  312. });
  313. }
  314. module.exports = [appConfig, legacyCssConfig, pwConfig];