webpack.config.js 14.1 KB
Newer Older
1 2
'use strict';

3
var crypto = require('crypto');
4
var fs = require('fs');
5
var path = require('path');
6
var glob = require('glob');
7
var webpack = require('webpack');
8
var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
9
var CopyWebpackPlugin = require('copy-webpack-plugin');
10
var CompressionPlugin = require('compression-webpack-plugin');
11
var NameAllModulesPlugin = require('name-all-modules-plugin');
12
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
13
var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
14

15
var ROOT_PATH = path.resolve(__dirname, '..');
16
var IS_PRODUCTION = process.env.NODE_ENV === 'production';
17
var IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
18
var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
19
var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
20
var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
21
var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
22
var NO_COMPRESSION = process.env.NO_COMPRESSION;
23

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
// generate automatic entry points
var autoEntries = {};
var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });

// filter out entries currently imported dynamically in dispatcher.js
var dispatcher = fs.readFileSync(path.join(ROOT_PATH, 'app/assets/javascripts/dispatcher.js')).toString();
var dispatcherChunks = dispatcher.match(/(?!import\('.\/)pages\/[^']+/g);

pageEntries.forEach(( path ) => {
  let chunkPath = path.replace(/\/index\.js$/, '');
  if (!dispatcherChunks.includes(chunkPath)) {
    let chunkName = chunkPath.replace(/\//g, '.');
    autoEntries[chunkName] = './' + path;
  }
});

// report our auto-generated bundle count
var autoEntriesCount = Object.keys(autoEntries).length;
console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`);

44
var config = {
45 46 47 48
  // because sqljs requires fs.
  node: {
    fs: "empty"
  },
49
  context: path.join(ROOT_PATH, 'app/assets/javascripts'),
50
  entry: {
51
    account:              './profile/account/index.js',
52
    add_gitlab_slack_application: './add_gitlab_slack_application/index.js',
53
    balsamiq_viewer:      './blob/balsamiq_viewer.js',
54
    blob:                 './blob_edit/blob_bundle.js',
55
    boards:               './boards/boards_bundle.js',
56
    burndown_chart:       './burndown_chart/index.js',
57
    common:               './commons/index.js',
58
    common_vue:           './vue_shared/vue_resource_interceptor.js',
59
    cycle_analytics:      './cycle_analytics/cycle_analytics_bundle.js',
60
    commit_pipelines:     './commit/pipelines/pipelines_bundle.js',
61
    deploy_keys:          './deploy_keys/index.js',
62
    docs:                 './docs/docs_bundle.js',
63 64
    diff_notes:           './diff_notes/diff_notes_bundle.js',
    environments:         './environments/environments_bundle.js',
Filipa Lacerda's avatar
Filipa Lacerda committed
65
    environments_folder:  './environments/folder/environments_folder_bundle.js',
Clement Ho's avatar
Clement Ho committed
66
    epic_show:            'ee/epics/epic_show/epic_show_bundle.js',
Jarka Kadlecova's avatar
Jarka Kadlecova committed
67
    new_epic:             'ee/epics/new_epic/new_epic_bundle.js',
68
    filtered_search:      './filtered_search/filtered_search_bundle.js',
Kushal Pandya's avatar
Kushal Pandya committed
69
    geo_nodes:            'ee/geo_nodes',
70
    graphs:               './graphs/graphs_bundle.js',
71 72
    graphs_charts:        './graphs/graphs_charts.js',
    graphs_show:          './graphs/graphs_show.js',
73
    help:                 './help/help.js',
Eric Eastwood's avatar
Eric Eastwood committed
74
    issuable:             './issuable/issuable_bundle.js',
75
    issues:               './issues/issues_bundle.js',
76
    how_to_merge:         './how_to_merge.js',
77
    issue_show:           './issue_show/index.js',
78
    job_details:          './jobs/job_details_bundle.js',
James Lopez's avatar
James Lopez committed
79
    ldap_group_links:     './groups/ldap_group_links.js',
Phil Hughes's avatar
Phil Hughes committed
80
    locale:               './locale/index.js',
81
    main:                 './main.js',
82
    merge_conflicts:      './merge_conflicts/merge_conflicts_bundle.js',
83
    mirrors:              './mirrors',
84
    monitoring:           './monitoring/monitoring_bundle.js',
85
    network:              './network/network_bundle.js',
86
    notebook_viewer:      './blob/notebook_viewer.js',
87
    notes:                './notes/index.js',
88
    pdf_viewer:           './blob/pdf_viewer.js',
89
    pipelines:            './pipelines/pipelines_bundle.js',
90 91 92
    pipelines_charts:     './pipelines/pipelines_charts.js',
    pipelines_details:    './pipelines/pipeline_details_bundle.js',
    pipelines_times:      './pipelines/pipelines_times.js',
93
    profile:              './profile/profile_bundle.js',
94
    project_import_gl:    './projects/project_import_gitlab_project.js',
95
    prometheus_metrics:   './prometheus_metrics',
96
    protected_branches:   './protected_branches',
Lin Jen-Shin's avatar
Lin Jen-Shin committed
97
    ee_protected_branches: 'ee/protected_branches',
98
    protected_tags:       './protected_tags',
Lin Jen-Shin's avatar
Lin Jen-Shin committed
99
    ee_protected_tags:    'ee/protected_tags',
100
    service_desk:         './projects/settings_service_desk/service_desk_bundle.js',
101
    service_desk_issues:  './service_desk_issues/index.js',
102
    registry_list:        './registry/index.js',
Kushal Pandya's avatar
Kushal Pandya committed
103
    roadmap:              'ee/roadmap',
Tim Zallmann's avatar
Tim Zallmann committed
104
    ide:                 './ide/index.js',
Clement Ho's avatar
Clement Ho committed
105
    sidebar:              './sidebar/sidebar_bundle.js',
106
    ee_sidebar:           'ee/sidebar/sidebar_bundle.js',
107
    snippet:              './snippet/snippet_bundle.js',
108
    sketch_viewer:        './blob/sketch_viewer.js',
Phil Hughes's avatar
Phil Hughes committed
109
    stl_viewer:           './blob/stl_viewer.js',
110
    terminal:             './terminal/terminal_bundle.js',
111
    u2f:                  ['vendor/u2f'],
112
    ui_development_kit:   './ui_development_kit.js',
113
    raven:                './raven/index.js',
114
    vue_merge_request_widget: './vue_merge_request_widget/index.js',
115
    test:                 './test.js',
116
    two_factor_auth:      './two_factor_auth.js',
117
    webpack_runtime:      './webpack.js',
118 119 120 121 122
  },

  output: {
    path: path.join(ROOT_PATH, 'public/assets/webpack'),
    publicPath: '/assets/webpack/',
123 124
    filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js',
    chunkFilename: IS_PRODUCTION ? '[name].[chunkhash].chunk.js' : '[name].chunk.js',
125 126
  },

127
  module: {
128
    rules: [
129
      {
130
        test: /\.js$/,
131
        exclude: /(node_modules|vendor\/assets)/,
Mike Greiling's avatar
Mike Greiling committed
132
        loader: 'babel-loader',
Filipa Lacerda's avatar
Filipa Lacerda committed
133
      },
134 135
      {
        test: /\.vue$/,
Mike Greiling's avatar
Mike Greiling committed
136
        loader: 'vue-loader',
Filipa Lacerda's avatar
Filipa Lacerda committed
137 138 139
      },
      {
        test: /\.svg$/,
Mike Greiling's avatar
Mike Greiling committed
140 141
        loader: 'raw-loader',
      },
Sam Rose's avatar
Sam Rose committed
142
      {
143
        test: /\.(gif|png)$/,
Sam Rose's avatar
Sam Rose committed
144
        loader: 'url-loader',
145
        options: { limit: 2048 },
Sam Rose's avatar
Sam Rose committed
146
      },
147 148
      {
        test: /\_worker\.js$/,
Phil Hughes's avatar
Phil Hughes committed
149
        use: [
150
          {
Tim Zallmann's avatar
Tim Zallmann committed
151
            loader: 'worker-loader',
152
            options: {
Tim Zallmann's avatar
Tim Zallmann committed
153 154 155
              inline: true
            }
          },
Phil Hughes's avatar
Phil Hughes committed
156 157
          { loader: 'babel-loader' },
        ],
158
      },
Mike Greiling's avatar
Mike Greiling committed
159
      {
160
        test: /\.(worker(\.min)?\.js|pdf|bmpr)$/,
161 162
        exclude: /node_modules/,
        loader: 'file-loader',
163 164 165
        options: {
          name: '[name].[hash].[ext]',
        }
166
      },
167 168 169 170 171
      {
        test: /katex.css$/,
        include: /node_modules\/katex\/dist/,
        use: [
          { loader: 'style-loader' },
172
          {
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
            loader: 'css-loader',
            options: {
              name: '[name].[hash].[ext]'
            }
          },
        ],
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        include: /node_modules\/katex\/dist\/fonts/,
        loader: 'file-loader',
        options: {
          name: '[name].[hash].[ext]',
        }
      },
188 189 190 191 192 193 194 195 196 197
      {
        test: /monaco-editor\/\w+\/vs\/loader\.js$/,
        use: [
          { loader: 'exports-loader', options: 'l.global' },
          { loader: 'imports-loader', options: 'l=>{},this=>l,AMDLoader=>this,module=>undefined' },
        ],
      }
    ],

    noParse: [/monaco-editor\/\w+\/vs\//],
198
    strictExportPresence: true,
199 200
  },

201 202 203
  plugins: [
    // manifest filename must match config.webpack.manifest_filename
    // webpack-rails only needs assetsByChunkName to function properly
204 205 206 207 208 209 210 211 212 213 214 215
    new StatsWriterPlugin({
      filename: 'manifest.json',
      transform: function(data, opts) {
        var stats = opts.compiler.getStats().toJson({
          chunkModules: false,
          source: false,
          chunks: false,
          modules: false,
          assets: true
        });
        return JSON.stringify(stats, null, 2);
      }
Phil Hughes's avatar
Phil Hughes committed
216
    }),
217 218

    // prevent pikaday from including moment.js
Phil Hughes's avatar
Phil Hughes committed
219
    new webpack.IgnorePlugin(/moment/, /pikaday/),
220 221 222 223 224 225 226

    // fix legacy jQuery plugins which depend on globals
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
    }),

227
    // assign deterministic module ids
228
    new webpack.NamedModulesPlugin(),
229
    new NameAllModulesPlugin(),
230

231 232 233 234 235
    // assign deterministic chunk ids
    new webpack.NamedChunksPlugin((chunk) => {
      if (chunk.name) {
        return chunk.name;
      }
236 237 238 239 240 241 242 243 244 245

      const moduleNames = [];

      function collectModuleNames(m) {
        // handle ConcatenatedModule which does not have resource nor context set
        if (m.modules) {
          m.modules.forEach(collectModuleNames);
          return;
        }

246
        const pagesBase = path.join(ROOT_PATH, 'app/assets/javascripts/pages');
247

248
        if (m.resource.indexOf(pagesBase) === 0) {
249
          moduleNames.push(path.relative(pagesBase, m.resource)
250
            .replace(/\/index\.[a-z]+$/, '')
251 252 253
            .replace(/\//g, '__'));
        } else {
          moduleNames.push(path.relative(m.context, m.resource));
254
        }
255 256 257 258 259 260 261 262 263
      }

      chunk.forEachModule(collectModuleNames);

      const hash = crypto.createHash('sha256')
        .update(moduleNames.join('_'))
        .digest('hex');

      return `${moduleNames[0]}-${hash.substr(0, 6)}`;
264
    }),
265 266 267 268 269 270 271 272

    // create cacheable common library bundle for all vue chunks
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common_vue',
      chunks: [
        'boards',
        'commit_pipelines',
        'cycle_analytics',
Phil Hughes's avatar
Phil Hughes committed
273
        'deploy_keys',
274 275 276
        'diff_notes',
        'environments',
        'environments_folder',
277
        'filtered_search',
Alfredo Sumaran's avatar
Alfredo Sumaran committed
278
        'groups',
Eric Eastwood's avatar
Eric Eastwood committed
279
        'issuable',
280
        'issue_show',
281
        'job_details',
282
        'merge_conflicts',
283
        'monitoring',
284
        'notebook_viewer',
285
        'notes',
286
        'pdf_viewer',
287
        'pipelines',
288
        'pipelines_details',
289
        'registry_list',
Tim Zallmann's avatar
Tim Zallmann committed
290
        'ide',
291 292
        'schedule_form',
        'schedules_index',
293
        'service_desk',
294
        'sidebar',
295
        'vue_merge_request_widget',
296 297 298 299 300 301 302 303 304
      ],
      minChunks: function(module, count) {
        return module.resource && (/vue_shared/).test(module.resource);
      },
    }),

    // create cacheable common library bundle for all d3 chunks
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common_d3',
305 306
      chunks: [
        'graphs',
307
        'graphs_show',
308
        'monitoring',
309
        'users',
310
        'burndown_chart', // EE
311
      ],
312 313 314
      minChunks: function (module, count) {
        return module.resource && /d3-/.test(module.resource);
      },
315 316 317 318
    }),

    // create cacheable common library bundles
    new webpack.optimize.CommonsChunkPlugin({
319
      names: ['main', 'common', 'webpack_runtime'],
320
    }),
321

322
    // enable scope hoisting
323
    new webpack.optimize.ModuleConcatenationPlugin(),
324

325
    // copy pre-compiled vendor libraries verbatim
326 327
    new CopyWebpackPlugin([
      {
328 329 330
        from: path.join(ROOT_PATH, `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs`),
        to: 'monaco-editor/vs',
        transform: function(content, path) {
331
          if (/\.js$/.test(path) && !/worker/i.test(path) && !/typescript/i.test(path)) {
332 333 334
            return (
              '(function(){\n' +
              'var define = this.define, require = this.require;\n' +
335
              'window.define = define; window.require = require;\n' +
336 337 338 339 340 341
              content +
              '\n}.call(window.__monaco_context__ || (window.__monaco_context__ = {})));'
            );
          }
          return content;
        }
342 343
      }
    ]),
344 345 346
  ],

  resolve: {
347
    extensions: ['.js'],
348
    alias: {
349
      '~':              path.join(ROOT_PATH, 'app/assets/javascripts'),
350
      'emojis':         path.join(ROOT_PATH, 'fixtures/emojis'),
351
      'empty_states':   path.join(ROOT_PATH, 'app/views/shared/empty_states'),
352
      'icons':          path.join(ROOT_PATH, 'app/views/shared/icons'),
353
      'images':         path.join(ROOT_PATH, 'app/assets/images'),
354
      'vendor':         path.join(ROOT_PATH, 'vendor/assets/javascripts'),
355
      'vue$':           'vue/dist/vue.esm.js',
356 357 358 359 360

      'ee':              path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
      'ee_empty_states': path.join(ROOT_PATH, 'ee/app/views/shared/empty_states'),
      'ee_icons':        path.join(ROOT_PATH, 'ee/app/views/shared/icons'),
      'ee_images':       path.join(ROOT_PATH, 'ee/app/assets/images'),
361
    }
362
  }
363 364
}

365 366
config.entry = Object.assign({}, autoEntries, config.entry);

367
if (IS_PRODUCTION) {
368
  config.devtool = 'source-map';
369
  config.plugins.push(
370
    new webpack.NoEmitOnErrorsPlugin(),
371 372 373 374
    new webpack.LoaderOptionsPlugin({
      minimize: true,
      debug: false
    }),
375
    new webpack.optimize.UglifyJsPlugin({
376
      sourceMap: true
377 378 379
    }),
    new webpack.DefinePlugin({
      'process.env': { NODE_ENV: JSON.stringify('production') }
380
    })
381
  );
382

383
  // compression can require a lot of compute time and is disabled in CI
384
  if (!NO_COMPRESSION) {
385
    config.plugins.push(new CompressionPlugin());
386
  }
387 388 389
}

if (IS_DEV_SERVER) {
390
  config.devtool = 'cheap-module-eval-source-map';
391
  config.devServer = {
392
    host: DEV_SERVER_HOST,
393
    port: DEV_SERVER_PORT,
394
    disableHostCheck: true,
395 396
    headers: { 'Access-Control-Allow-Origin': '*' },
    stats: 'errors-only',
Simon Knox's avatar
Simon Knox committed
397
    hot: DEV_SERVER_LIVERELOAD,
398
    inline: DEV_SERVER_LIVERELOAD
399
  };
400 401 402 403
  config.plugins.push(
    // watch node_modules for changes if we encounter a missing module compile error
    new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
  );
Simon Knox's avatar
Simon Knox committed
404 405 406
  if (DEV_SERVER_LIVERELOAD) {
    config.plugins.push(new webpack.HotModuleReplacementPlugin());
  }
407 408
}

409 410 411 412 413 414 415 416 417 418 419 420
if (WEBPACK_REPORT) {
  config.plugins.push(
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      generateStatsFile: true,
      openAnalyzer: false,
      reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'),
      statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'),
    })
  );
}

421
module.exports = config;