webpack.config.js 15.8 KB
Newer Older
1
const fs = require('fs');
2 3 4
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
5
const VueLoaderPlugin = require('vue-loader/lib/plugin');
6 7
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
const CompressionPlugin = require('compression-webpack-plugin');
8
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
P
Phil Hughes 已提交
9
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
S
Stan Hu 已提交
10
const CopyWebpackPlugin = require('copy-webpack-plugin');
11
const vendorDllHash = require('./helpers/vendor_dll_hash');
12 13

const ROOT_PATH = path.resolve(__dirname, '..');
14
const VENDOR_DLL = process.env.WEBPACK_VENDOR_DLL && process.env.WEBPACK_VENDOR_DLL !== 'false';
15
const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache');
16
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
17
const IS_DEV_SERVER = process.env.WEBPACK_DEV_SERVER === 'true';
W
Winnie Hellmann 已提交
18
const IS_EE = require('./helpers/is_ee_env');
19 20
const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
21
const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
22
const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
M
Mike Greiling 已提交
23
const WEBPACK_MEMORY_TEST = process.env.WEBPACK_MEMORY_TEST;
24
const NO_COMPRESSION = process.env.NO_COMPRESSION;
25
const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS;
26

27 28
const VUE_VERSION = require('vue/package.json').version;
const VUE_LOADER_VERSION = require('vue-loader/package.json').version;
29
const WEBPACK_VERSION = require('webpack/package.json').version;
30

31 32
const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map';

33 34
let autoEntriesCount = 0;
let watchAutoEntries = [];
35
const defaultEntries = ['./main'];
36 37 38

function generateEntries() {
  // generate automatic entry points
39
  const autoEntries = {};
40
  const autoEntriesMap = {};
M
Mike Greiling 已提交
41 42 43 44
  const pageEntries = glob.sync('pages/**/index.js', {
    cwd: path.join(ROOT_PATH, 'app/assets/javascripts'),
  });
  watchAutoEntries = [path.join(ROOT_PATH, 'app/assets/javascripts/pages/')];
45 46 47

  function generateAutoEntries(path, prefix = '.') {
    const chunkPath = path.replace(/\/index\.js$/, '');
48
    const chunkName = chunkPath.replace(/\//g, '.');
49
    autoEntriesMap[chunkName] = `${prefix}/${path}`;
50
  }
51

M
Mike Greiling 已提交
52
  pageEntries.forEach(path => generateAutoEntries(path));
53

54 55 56 57 58 59 60 61
  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/'));
  }

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
  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);
  });
77

78
  const manualEntries = {
79
    default: defaultEntries,
80
    sentry: './sentry/index.js',
81 82 83 84 85
  };

  return Object.assign(manualEntries, autoEntries);
}

86 87 88 89 90 91 92 93 94
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'),
95
  jest: path.join(ROOT_PATH, 'spec/frontend'),
96 97 98

  // the following resolves files which are different between CE and EE
  ee_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
99 100 101 102 103 104

  // 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',
  ),
105 106 107 108 109
};

if (IS_EE) {
  Object.assign(alias, {
    ee: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
110
    ee_component: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
111 112 113 114
    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'),
115
    ee_jest: path.join(ROOT_PATH, 'ee/spec/frontend'),
116 117 118 119
    ee_else_ce: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
  });
}

120 121 122 123 124
let dll;

if (VENDOR_DLL && !IS_PRODUCTION) {
  const dllHash = vendorDllHash();
  const dllCachePath = path.join(ROOT_PATH, `tmp/cache/webpack-dlls/${dllHash}`);
125 126 127 128 129 130 131
  dll = {
    manifestPath: path.join(dllCachePath, 'vendor.dll.manifest.json'),
    cacheFrom: dllCachePath,
    cacheTo: path.join(ROOT_PATH, `public/assets/webpack/dll.${dllHash}/`),
    publicPath: `dll.${dllHash}/vendor.dll.bundle.js`,
    exists: null,
  };
132 133
}

134
module.exports = {
M
Mike Greiling 已提交
135 136
  mode: IS_PRODUCTION ? 'production' : 'development',

137 138 139
  context: path.join(ROOT_PATH, 'app/assets/javascripts'),

  entry: generateEntries,
140 141 142 143

  output: {
    path: path.join(ROOT_PATH, 'public/assets/webpack'),
    publicPath: '/assets/webpack/',
144 145
    filename: IS_PRODUCTION ? '[name].[chunkhash:8].bundle.js' : '[name].bundle.js',
    chunkFilename: IS_PRODUCTION ? '[name].[chunkhash:8].chunk.js' : '[name].chunk.js',
146
    globalObject: 'this', // allow HMR and web workers to play nice
147 148
  },

149
  resolve: {
P
Phil Hughes 已提交
150
    extensions: ['.js', '.gql', '.graphql'],
151
    alias,
M
Mike Greiling 已提交
152 153
  },

M
Mike Greiling 已提交
154
  module: {
155
    strictExportPresence: true,
M
Mike Greiling 已提交
156
    rules: [
P
Phil Hughes 已提交
157 158 159 160 161
      {
        type: 'javascript/auto',
        test: /\.mjs$/,
        use: [],
      },
M
Mike Greiling 已提交
162
      {
163
        test: /\.js$/,
164
        exclude: path => /node_modules|vendor[\\/]assets/.test(path) && !/\.vue\.js/.test(path),
M
Mike Greiling 已提交
165
        loader: 'babel-loader',
166
        options: {
167
          cacheDirectory: path.join(CACHE_PATH, 'babel-loader'),
168
        },
F
Filipa Lacerda 已提交
169
      },
170 171
      {
        test: /\.vue$/,
M
Mike Greiling 已提交
172
        loader: 'vue-loader',
173 174 175 176 177 178 179 180 181
        options: {
          cacheDirectory: path.join(CACHE_PATH, 'vue-loader'),
          cacheIdentifier: [
            process.env.NODE_ENV || 'development',
            webpack.version,
            VUE_VERSION,
            VUE_LOADER_VERSION,
          ].join('|'),
        },
182
      },
P
Phil Hughes 已提交
183 184 185 186 187
      {
        test: /\.(graphql|gql)$/,
        exclude: /node_modules/,
        loader: 'graphql-tag/loader',
      },
188 189 190 191 192 193 194
      {
        test: /icons\.svg$/,
        loader: 'file-loader',
        options: {
          name: '[name].[hash:8].[ext]',
        },
      },
F
Filipa Lacerda 已提交
195 196
      {
        test: /\.svg$/,
197
        exclude: /icons\.svg$/,
M
Mike Greiling 已提交
198 199
        loader: 'raw-loader',
      },
S
Sam Rose 已提交
200
      {
201
        test: /\.(gif|png|mp4)$/,
S
Sam Rose 已提交
202
        loader: 'url-loader',
203
        options: { limit: 2048 },
S
Sam Rose 已提交
204
      },
P
Phil Hughes 已提交
205
      {
206
        test: /_worker\.js$/,
207 208 209 210
        use: [
          {
            loader: 'worker-loader',
            options: {
211
              name: '[name].[hash:8].worker.js',
212
              inline: IS_DEV_SERVER,
213 214 215 216
            },
          },
          'babel-loader',
        ],
P
Phil Hughes 已提交
217
      },
M
Mike Greiling 已提交
218
      {
219
        test: /\.(worker(\.min)?\.js|pdf|bmpr)$/,
S
Sam Rose 已提交
220 221
        exclude: /node_modules/,
        loader: 'file-loader',
222
        options: {
223
          name: '[name].[hash:8].[ext]',
M
Mike Greiling 已提交
224
        },
S
Sam Rose 已提交
225
      },
226
      {
227
        test: /.css$/,
228
        use: [
229
          'vue-style-loader',
230
          {
231 232
            loader: 'css-loader',
            options: {
233
              name: '[name].[hash:8].[ext]',
M
Mike Greiling 已提交
234
            },
235 236 237 238 239 240 241 242
          },
        ],
      },
      {
        test: /\.(eot|ttf|woff|woff2)$/,
        include: /node_modules\/katex\/dist\/fonts/,
        loader: 'file-loader',
        options: {
243
          name: '[name].[hash:8].[ext]',
M
Mike Greiling 已提交
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
  },

  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,
        },
      },
    },
M
Mike Greiling 已提交
273 274
  },

275 276 277
  plugins: [
    // manifest filename must match config.webpack.manifest_filename
    // webpack-rails only needs assetsByChunkName to function properly
278 279 280
    new StatsWriterPlugin({
      filename: 'manifest.json',
      transform: function(data, opts) {
281
        const stats = opts.compiler.getStats().toJson({
282 283 284 285
          chunkModules: false,
          source: false,
          chunks: false,
          modules: false,
M
Mike Greiling 已提交
286
          assets: true,
287
        });
288 289 290 291 292

        // tell our rails helper where to find the DLL files
        if (dll) {
          stats.dllAssets = dll.publicPath;
        }
293
        return JSON.stringify(stats, null, 2);
M
Mike Greiling 已提交
294
      },
P
Phil Hughes 已提交
295
    }),
M
Mike Greiling 已提交
296

297 298 299
    // enable vue-loader to use existing loader rules for other module types
    new VueLoaderPlugin(),

300 301 302
    // automatically configure monaco editor web workers
    new MonacoWebpackPlugin(),

M
Mike Greiling 已提交
303
    // prevent pikaday from including moment.js
P
Phil Hughes 已提交
304
    new webpack.IgnorePlugin(/moment/, /pikaday/),
M
Mike Greiling 已提交
305

306 307 308 309 310 311
    // fix legacy jQuery plugins which depend on globals
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
    }),

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
    // if DLLs are enabled, detect whether the DLL exists and create it automatically if necessary
    dll && {
      apply(compiler) {
        compiler.hooks.beforeCompile.tapAsync('DllAutoCompilePlugin', (params, callback) => {
          if (dll.exists) {
            callback();
          } else if (fs.existsSync(dll.manifestPath)) {
            console.log(`Using vendor DLL found at: ${dll.cacheFrom}`);
            dll.exists = true;
            callback();
          } else {
            console.log(
              `Warning: No vendor DLL found at: ${dll.cacheFrom}. Compiling DLL automatically.`,
            );

            const dllConfig = require('./webpack.vendor.config.js');
            const dllCompiler = webpack(dllConfig);

            dllCompiler.run((err, stats) => {
              if (err) {
                return callback(err);
              }

              const info = stats.toJson();

              if (stats.hasErrors()) {
                console.error(info.errors.join('\n\n'));
                return callback('DLL not compiled successfully.');
              }

              if (stats.hasWarnings()) {
                console.warn(info.warnings.join('\n\n'));
                console.warn('DLL compiled with warnings.');
              } else {
                console.log('DLL compiled successfully.');
              }

              dll.exists = true;
              callback();
            });
          }
        });
      },
    },

357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
    // reference our compiled DLL modules
    dll &&
      new webpack.DllReferencePlugin({
        context: ROOT_PATH,
        manifest: dll.manifestPath,
      }),

    dll &&
      new CopyWebpackPlugin([
        {
          from: dll.cacheFrom,
          to: dll.cacheTo,
        },
      ]),

372 373
    !IS_EE &&
      new webpack.NormalModuleReplacementPlugin(/^ee_component\/(.*)\.vue/, resource => {
374 375
        resource.request = path.join(
          ROOT_PATH,
376
          'app/assets/javascripts/vue_shared/components/empty_component.js',
377
        );
378
      }),
379

S
Stan Hu 已提交
380 381 382 383 384
    new CopyWebpackPlugin([
      {
        from: path.join(ROOT_PATH, 'node_modules/pdfjs-dist/cmaps/'),
        to: path.join(ROOT_PATH, 'public/assets/webpack/cmaps/'),
      },
385 386 387 388 389
      {
        from: path.join(ROOT_PATH, 'node_modules/@sourcegraph/code-host-integration/'),
        to: path.join(ROOT_PATH, 'public/assets/webpack/sourcegraph/'),
        ignore: ['package.json'],
      },
S
shampton 已提交
390 391 392 393 394 395 396
      {
        from: path.join(
          ROOT_PATH,
          'node_modules/@gitlab/visual-review-tools/dist/visual_review_toolbar.js',
        ),
        to: path.join(ROOT_PATH, 'public/assets/webpack'),
      },
S
Stan Hu 已提交
397 398
    ]),

399 400
    // compression can require a lot of compute time and is disabled in CI
    IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(),
401

402 403 404 405 406 407 408 409
    // 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(
410
            file => file.indexOf(nodeModulesPath) !== -1,
411
          );
412

413 414 415 416 417
          // 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));
418

419 420
          // report our auto-generated bundle count
          console.log(
421
            `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`,
422 423 424 425 426 427 428
          );

          callback();
        });
      },
    },

M
Mike Greiling 已提交
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
    // 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`);

447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
          const webpackStatistics = {
            memoryUsage,
            date: Date.now(), // milliseconds
            commitSHA: process.env.CI_COMMIT_SHA,
            nodeVersion: process.versions.node,
            webpackVersion: WEBPACK_VERSION,
          };

          console.log(webpackStatistics);

          fs.writeFileSync(
            path.join(ROOT_PATH, 'webpack-dev-server.json'),
            JSON.stringify(webpackStatistics),
          );

M
Mike Greiling 已提交
462 463 464 465 466 467
          // exit in case we're running webpack-dev-server
          IS_DEV_SERVER && process.exit();
        });
      },
    },

468 469 470 471 472 473 474 475 476 477 478
    // 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'),
479 480 481
        statsOptions: {
          source: false,
        },
482
      }),
483 484

    new webpack.DefinePlugin({
485
      // This one is used to define window.gon.ee and other things properly in tests:
486
      'process.env.IS_EE': JSON.stringify(IS_EE),
487 488
      // This one is used to check against "EE" properly in application code
      IS_EE: IS_EE ? 'window.gon && window.gon.ee' : JSON.stringify(false),
489
    }),
490 491 492
  ].filter(Boolean),

  devServer: {
493
    host: DEV_SERVER_HOST,
494
    port: DEV_SERVER_PORT,
495
    disableHostCheck: true,
496 497 498 499
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': '*',
    },
500
    stats: 'errors-only',
S
Simon Knox 已提交
501
    hot: DEV_SERVER_LIVERELOAD,
M
Mike Greiling 已提交
502
    inline: DEV_SERVER_LIVERELOAD,
503
  },
504

505
  devtool: NO_SOURCEMAPS ? false : devtool,
506

507 508 509 510
  node: {
    fs: 'empty', // sqljs requires fs
    setImmediate: false,
  },
511
};