webpack.config.js 12.6 KB
Newer Older
1 2 3
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
4
const VueLoaderPlugin = require('vue-loader/lib/plugin');
5 6
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
const CompressionPlugin = require('compression-webpack-plugin');
7
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
Phil Hughes's avatar
Phil Hughes committed
8
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
9
const CopyWebpackPlugin = require('copy-webpack-plugin');
10 11

const ROOT_PATH = path.resolve(__dirname, '..');
12
const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache');
13
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
14
const IS_DEV_SERVER = process.env.WEBPACK_DEV_SERVER === 'true';
15
const IS_EE = require('./helpers/is_ee_env');
16 17
const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
18
const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
19
const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
20
const WEBPACK_MEMORY_TEST = process.env.WEBPACK_MEMORY_TEST;
21
const NO_COMPRESSION = process.env.NO_COMPRESSION;
22
const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS;
23

24 25 26
const VUE_VERSION = require('vue/package.json').version;
const VUE_LOADER_VERSION = require('vue-loader/package.json').version;

27 28
const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map';

29 30
let autoEntriesCount = 0;
let watchAutoEntries = [];
31
const defaultEntries = ['./main'];
32 33 34

function generateEntries() {
  // generate automatic entry points
35
  const autoEntries = {};
36
  const autoEntriesMap = {};
Mike Greiling's avatar
Mike Greiling committed
37 38 39 40
  const pageEntries = glob.sync('pages/**/index.js', {
    cwd: path.join(ROOT_PATH, 'app/assets/javascripts'),
  });
  watchAutoEntries = [path.join(ROOT_PATH, 'app/assets/javascripts/pages/')];
41 42 43

  function generateAutoEntries(path, prefix = '.') {
    const chunkPath = path.replace(/\/index\.js$/, '');
44
    const chunkName = chunkPath.replace(/\//g, '.');
45
    autoEntriesMap[chunkName] = `${prefix}/${path}`;
46
  }
47

Mike Greiling's avatar
Mike Greiling committed
48
  pageEntries.forEach(path => generateAutoEntries(path));
49

50 51 52 53 54 55 56 57
  if (IS_EE) {
    const eePageEntries = glob.sync('pages/**/index.js', {
      cwd: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
    });
    eePageEntries.forEach(path => generateAutoEntries(path, 'ee'));
    watchAutoEntries.push(path.join(ROOT_PATH, 'ee/app/assets/javascripts/pages/'));
  }

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
  const autoEntryKeys = Object.keys(autoEntriesMap);
  autoEntriesCount = autoEntryKeys.length;

  // import ancestor entrypoints within their children
  autoEntryKeys.forEach(entry => {
    const entryPaths = [autoEntriesMap[entry]];
    const segments = entry.split('.');
    while (segments.pop()) {
      const ancestor = segments.join('.');
      if (autoEntryKeys.includes(ancestor)) {
        entryPaths.unshift(autoEntriesMap[ancestor]);
      }
    }
    autoEntries[entry] = defaultEntries.concat(entryPaths);
  });
73

74
  const manualEntries = {
75
    default: defaultEntries,
Mike Greiling's avatar
Mike Greiling committed
76
    raven: './raven/index.js',
77 78 79 80 81
  };

  return Object.assign(manualEntries, autoEntries);
}

82 83 84 85 86 87 88 89 90 91 92 93
const alias = {
  '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
  emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
  empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),
  icons: path.join(ROOT_PATH, 'app/views/shared/icons'),
  images: path.join(ROOT_PATH, 'app/assets/images'),
  vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
  vue$: 'vue/dist/vue.esm.js',
  spec: path.join(ROOT_PATH, 'spec/javascripts'),

  // the following resolves files which are different between CE and EE
  ee_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
94 95 96 97 98 99

  // override loader path for icons.svg so we do not duplicate this asset
  '@gitlab/svgs/dist/icons.svg': path.join(
    ROOT_PATH,
    'app/assets/javascripts/lib/utils/icons_path.js',
  ),
100 101 102 103 104 105 106 107 108 109 110 111 112
};

if (IS_EE) {
  Object.assign(alias, {
    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'),
    ee_spec: path.join(ROOT_PATH, 'ee/spec/javascripts'),
    ee_else_ce: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
  });
}

113
module.exports = {
Mike Greiling's avatar
Mike Greiling committed
114 115
  mode: IS_PRODUCTION ? 'production' : 'development',

116 117 118
  context: path.join(ROOT_PATH, 'app/assets/javascripts'),

  entry: generateEntries,
119 120 121 122

  output: {
    path: path.join(ROOT_PATH, 'public/assets/webpack'),
    publicPath: '/assets/webpack/',
123 124
    filename: IS_PRODUCTION ? '[name].[chunkhash:8].bundle.js' : '[name].bundle.js',
    chunkFilename: IS_PRODUCTION ? '[name].[chunkhash:8].chunk.js' : '[name].chunk.js',
125
    globalObject: 'this', // allow HMR and web workers to play nice
126 127
  },

128
  resolve: {
129
    extensions: ['.js', '.gql', '.graphql'],
130
    alias,
Mike Greiling's avatar
Mike Greiling committed
131 132
  },

133
  module: {
134
    strictExportPresence: true,
Mike Greiling's avatar
Mike Greiling committed
135
    rules: [
136 137 138 139 140
      {
        type: 'javascript/auto',
        test: /\.mjs$/,
        use: [],
      },
141
      {
142
        test: /\.js$/,
143
        exclude: path => /node_modules|vendor[\\/]assets/.test(path) && !/\.vue\.js/.test(path),
Mike Greiling's avatar
Mike Greiling committed
144
        loader: 'babel-loader',
145
        options: {
146
          cacheDirectory: path.join(CACHE_PATH, 'babel-loader'),
147
        },
Filipa Lacerda's avatar
Filipa Lacerda committed
148
      },
149 150
      {
        test: /\.vue$/,
Mike Greiling's avatar
Mike Greiling committed
151
        loader: 'vue-loader',
152 153 154 155 156 157 158 159 160
        options: {
          cacheDirectory: path.join(CACHE_PATH, 'vue-loader'),
          cacheIdentifier: [
            process.env.NODE_ENV || 'development',
            webpack.version,
            VUE_VERSION,
            VUE_LOADER_VERSION,
          ].join('|'),
        },
161
      },
162 163 164 165 166
      {
        test: /\.(graphql|gql)$/,
        exclude: /node_modules/,
        loader: 'graphql-tag/loader',
      },
167 168 169 170 171 172 173
      {
        test: /icons\.svg$/,
        loader: 'file-loader',
        options: {
          name: '[name].[hash:8].[ext]',
        },
      },
Filipa Lacerda's avatar
Filipa Lacerda committed
174 175
      {
        test: /\.svg$/,
176
        exclude: /icons\.svg$/,
Mike Greiling's avatar
Mike Greiling committed
177 178
        loader: 'raw-loader',
      },
Sam Rose's avatar
Sam Rose committed
179
      {
180
        test: /\.(gif|png)$/,
Sam Rose's avatar
Sam Rose committed
181
        loader: 'url-loader',
182
        options: { limit: 2048 },
Sam Rose's avatar
Sam Rose committed
183
      },
Phil Hughes's avatar
Phil Hughes committed
184 185
      {
        test: /\_worker\.js$/,
186 187 188 189
        use: [
          {
            loader: 'worker-loader',
            options: {
190
              name: '[name].[hash:8].worker.js',
191
              inline: IS_DEV_SERVER,
192 193 194 195
            },
          },
          'babel-loader',
        ],
Phil Hughes's avatar
Phil Hughes committed
196
      },
Mike Greiling's avatar
Mike Greiling committed
197
      {
198
        test: /\.(worker(\.min)?\.js|pdf|bmpr)$/,
199 200
        exclude: /node_modules/,
        loader: 'file-loader',
201
        options: {
202
          name: '[name].[hash:8].[ext]',
Mike Greiling's avatar
Mike Greiling committed
203
        },
204
      },
205
      {
206
        test: /.css$/,
207
        use: [
208
          'vue-style-loader',
209
          {
210 211
            loader: 'css-loader',
            options: {
212
              name: '[name].[hash:8].[ext]',
Mike Greiling's avatar
Mike Greiling committed
213
            },
214 215 216 217 218 219 220 221
          },
        ],
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        include: /node_modules\/katex\/dist\/fonts/,
        loader: 'file-loader',
        options: {
222
          name: '[name].[hash:8].[ext]',
Mike Greiling's avatar
Mike Greiling committed
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
  },

  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      maxInitialRequests: 4,
      cacheGroups: {
        default: false,
        common: () => ({
          priority: 20,
          name: 'main',
          chunks: 'initial',
          minChunks: autoEntriesCount * 0.9,
        }),
        vendors: {
          priority: 10,
          chunks: 'async',
          test: /[\\/](node_modules|vendor[\\/]assets[\\/]javascripts)[\\/]/,
        },
        commons: {
          chunks: 'all',
          minChunks: 2,
          reuseExistingChunk: true,
        },
      },
    },
252 253
  },

254 255 256
  plugins: [
    // manifest filename must match config.webpack.manifest_filename
    // webpack-rails only needs assetsByChunkName to function properly
257 258 259
    new StatsWriterPlugin({
      filename: 'manifest.json',
      transform: function(data, opts) {
260
        const stats = opts.compiler.getStats().toJson({
261 262 263 264
          chunkModules: false,
          source: false,
          chunks: false,
          modules: false,
Mike Greiling's avatar
Mike Greiling committed
265
          assets: true,
266 267
        });
        return JSON.stringify(stats, null, 2);
Mike Greiling's avatar
Mike Greiling committed
268
      },
Phil Hughes's avatar
Phil Hughes committed
269
    }),
Mike Greiling's avatar
Mike Greiling committed
270

271 272 273
    // enable vue-loader to use existing loader rules for other module types
    new VueLoaderPlugin(),

274 275 276
    // automatically configure monaco editor web workers
    new MonacoWebpackPlugin(),

Mike Greiling's avatar
Mike Greiling committed
277
    // prevent pikaday from including moment.js
Phil Hughes's avatar
Phil Hughes committed
278
    new webpack.IgnorePlugin(/moment/, /pikaday/),
Mike Greiling's avatar
Mike Greiling committed
279

280 281 282 283 284 285
    // fix legacy jQuery plugins which depend on globals
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
    }),

286 287 288 289 290 291
    new webpack.NormalModuleReplacementPlugin(/^ee_component\/(.*)\.vue/, function(resource) {
      if (Object.keys(module.exports.resolve.alias).indexOf('ee') >= 0) {
        resource.request = resource.request.replace(/^ee_component/, 'ee');
      } else {
        resource.request = path.join(
          ROOT_PATH,
292
          'app/assets/javascripts/vue_shared/components/empty_component.js',
293 294 295 296
        );
      }
    }),

297 298 299 300 301
    new CopyWebpackPlugin([
      {
        from: path.join(ROOT_PATH, 'node_modules/pdfjs-dist/cmaps/'),
        to: path.join(ROOT_PATH, 'public/assets/webpack/cmaps/'),
      },
302 303 304 305 306 307 308
      {
        from: path.join(
          ROOT_PATH,
          'node_modules/@gitlab/visual-review-tools/dist/visual_review_toolbar.js',
        ),
        to: path.join(ROOT_PATH, 'public/assets/webpack'),
      },
309 310
    ]),

311 312
    // compression can require a lot of compute time and is disabled in CI
    IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(),
313

314 315 316 317 318 319 320 321
    // WatchForChangesPlugin
    // TODO: publish this as a separate plugin
    IS_DEV_SERVER && {
      apply(compiler) {
        compiler.hooks.emit.tapAsync('WatchForChangesPlugin', (compilation, callback) => {
          const missingDeps = Array.from(compilation.missingDependencies);
          const nodeModulesPath = path.join(ROOT_PATH, 'node_modules');
          const hasMissingNodeModules = missingDeps.some(
322
            file => file.indexOf(nodeModulesPath) !== -1,
323
          );
324

325 326 327 328 329
          // watch for changes to missing node_modules
          if (hasMissingNodeModules) compilation.contextDependencies.add(nodeModulesPath);

          // watch for changes to automatic entrypoints
          watchAutoEntries.forEach(watchPath => compilation.contextDependencies.add(watchPath));
330

331 332
          // report our auto-generated bundle count
          console.log(
333
            `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`,
334 335 336 337 338 339 340
          );

          callback();
        });
      },
    },

341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
    // output the in-memory heap size upon compilation and exit
    WEBPACK_MEMORY_TEST && {
      apply(compiler) {
        compiler.hooks.emit.tapAsync('ReportMemoryConsumptionPlugin', (compilation, callback) => {
          console.log('Assets compiled...');
          if (global.gc) {
            console.log('Running garbage collection...');
            global.gc();
          } else {
            console.error(
              "WARNING: you must use the --expose-gc node option to accurately measure webpack's heap size",
            );
          }
          const memoryUsage = process.memoryUsage().heapUsed;
          const toMB = bytes => Math.floor(bytes / 1024 / 1024);

          console.log(`Webpack heap size: ${toMB(memoryUsage)} MB`);

          // exit in case we're running webpack-dev-server
          IS_DEV_SERVER && process.exit();
        });
      },
    },

365 366 367 368 369 370 371 372 373 374 375
    // enable HMR only in webpack-dev-server
    DEV_SERVER_LIVERELOAD && new webpack.HotModuleReplacementPlugin(),

    // optionally generate webpack bundle analysis
    WEBPACK_REPORT &&
      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'),
376 377 378
        statsOptions: {
          source: false,
        },
379
      }),
380 381

    new webpack.DefinePlugin({
382
      // This one is used to define window.gon.ee and other things properly in tests:
383
      'process.env.IS_EE': JSON.stringify(IS_EE),
384 385
      // This one is used to check against "EE" properly in application code
      IS_EE: IS_EE ? 'window.gon && window.gon.ee' : JSON.stringify(false),
386
    }),
387 388 389
  ].filter(Boolean),

  devServer: {
390
    host: DEV_SERVER_HOST,
391
    port: DEV_SERVER_PORT,
392
    disableHostCheck: true,
393 394 395 396
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': '*',
    },
397
    stats: 'errors-only',
Simon Knox's avatar
Simon Knox committed
398
    hot: DEV_SERVER_LIVERELOAD,
Mike Greiling's avatar
Mike Greiling committed
399
    inline: DEV_SERVER_LIVERELOAD,
400
  },
401

402
  devtool: NO_SOURCEMAPS ? false : devtool,
403

404 405 406 407
  node: {
    fs: 'empty', // sqljs requires fs
    setImmediate: false,
  },
408
};