diff options
Diffstat (limited to 'config/webpack.config.js')
-rw-r--r-- | config/webpack.config.js | 363 |
1 files changed, 213 insertions, 150 deletions
diff --git a/config/webpack.config.js b/config/webpack.config.js index 2e8c94655c1..b74d9dde494 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -1,77 +1,60 @@ -'use strict'; - -var fs = require('fs'); -var path = require('path'); -var webpack = require('webpack'); -var StatsPlugin = require('stats-webpack-plugin'); -var CompressionPlugin = require('compression-webpack-plugin'); -var NameAllModulesPlugin = require('name-all-modules-plugin'); -var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; -var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); - -var ROOT_PATH = path.resolve(__dirname, '..'); -var IS_PRODUCTION = process.env.NODE_ENV === 'production'; -var IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1; -var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; -var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; -var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; -var WEBPACK_REPORT = process.env.WEBPACK_REPORT; -var NO_COMPRESSION = process.env.NO_COMPRESSION; - -var config = { - // because sqljs requires fs. - node: { - fs: "empty" - }, +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); +const webpack = require('webpack'); +const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const CompressionPlugin = require('compression-webpack-plugin'); +const NameAllModulesPlugin = require('name-all-modules-plugin'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); + +const ROOT_PATH = path.resolve(__dirname, '..'); +const IS_PRODUCTION = process.env.NODE_ENV === 'production'; +const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1; +const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; +const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; +const DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; +const WEBPACK_REPORT = process.env.WEBPACK_REPORT; +const NO_COMPRESSION = process.env.NO_COMPRESSION; + +let autoEntriesCount = 0; +let watchAutoEntries = []; + +function generateEntries() { + // generate automatic entry points + const autoEntries = {}; + const pageEntries = glob.sync('pages/**/index.js', { + cwd: path.join(ROOT_PATH, 'app/assets/javascripts'), + }); + watchAutoEntries = [path.join(ROOT_PATH, 'app/assets/javascripts/pages/')]; + + function generateAutoEntries(path, prefix = '.') { + const chunkPath = path.replace(/\/index\.js$/, ''); + const chunkName = chunkPath.replace(/\//g, '.'); + autoEntries[chunkName] = `${prefix}/${path}`; + } + + pageEntries.forEach(path => generateAutoEntries(path)); + + autoEntriesCount = Object.keys(autoEntries).length; + + const manualEntries = { + common: './commons/index.js', + main: './main.js', + raven: './raven/index.js', + webpack_runtime: './webpack.js', + ide: './ide/index.js', + }; + + return Object.assign(manualEntries, autoEntries); +} + +const config = { context: path.join(ROOT_PATH, 'app/assets/javascripts'), - entry: { - balsamiq_viewer: './blob/balsamiq_viewer.js', - blob: './blob_edit/blob_bundle.js', - boards: './boards/boards_bundle.js', - common: './commons/index.js', - common_vue: ['vue', './vue_shared/common_vue.js'], - common_d3: ['d3'], - cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', - commit_pipelines: './commit/pipelines/pipelines_bundle.js', - deploy_keys: './deploy_keys/index.js', - diff_notes: './diff_notes/diff_notes_bundle.js', - environments: './environments/environments_bundle.js', - environments_folder: './environments/folder/environments_folder_bundle.js', - filtered_search: './filtered_search/filtered_search_bundle.js', - graphs: './graphs/graphs_bundle.js', - group: './group.js', - groups: './groups/index.js', - groups_list: './groups_list.js', - issue_show: './issue_show/index.js', - integrations: './integrations', - job_details: './jobs/job_details_bundle.js', - locale: './locale/index.js', - main: './main.js', - merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', - monitoring: './monitoring/monitoring_bundle.js', - network: './network/network_bundle.js', - notebook_viewer: './blob/notebook_viewer.js', - pdf_viewer: './blob/pdf_viewer.js', - pipelines: './pipelines/pipelines_bundle.js', - pipelines_details: './pipelines/pipeline_details_bundle.js', - profile: './profile/profile_bundle.js', - prometheus_metrics: './prometheus_metrics', - protected_branches: './protected_branches/protected_branches_bundle.js', - protected_tags: './protected_tags', - sidebar: './sidebar/sidebar_bundle.js', - schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js', - schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js', - snippet: './snippet/snippet_bundle.js', - sketch_viewer: './blob/sketch_viewer.js', - stl_viewer: './blob/stl_viewer.js', - terminal: './terminal/terminal_bundle.js', - u2f: ['vendor/u2f'], - users: './users/users_bundle.js', - raven: './raven/index.js', - vue_merge_request_widget: './vue_merge_request_widget/index.js', - test: './test.js', - peek: './peek.js', - }, + + entry: generateEntries, output: { path: path.join(ROOT_PATH, 'public/assets/webpack'), @@ -101,26 +84,74 @@ var config = { options: { limit: 2048 }, }, { - test: /\.(worker\.js|pdf|bmpr)$/, + test: /\_worker\.js$/, + use: [ + { + loader: 'worker-loader', + options: { + inline: true, + }, + }, + { loader: 'babel-loader' }, + ], + }, + { + test: /\.(worker(\.min)?\.js|pdf|bmpr)$/, exclude: /node_modules/, loader: 'file-loader', + options: { + name: '[name].[hash].[ext]', + }, }, { - test: /locale\/\w+\/(.*)\.js$/, - loader: 'exports-loader?locales', + test: /katex.css$/, + include: /node_modules\/katex\/dist/, + use: [ + { loader: 'style-loader' }, + { + 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]', + }, + }, + { + 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\//], + strictExportPresence: true, }, plugins: [ // manifest filename must match config.webpack.manifest_filename // webpack-rails only needs assetsByChunkName to function properly - new StatsPlugin('manifest.json', { - chunkModules: false, - source: false, - chunks: false, - modules: false, - assets: true + new StatsWriterPlugin({ + filename: 'manifest.json', + transform: function(data, opts) { + const stats = opts.compiler.getStats().toJson({ + chunkModules: false, + source: false, + chunks: false, + modules: false, + assets: true, + }); + return JSON.stringify(stats, null, 2); + }, }), // prevent pikaday from including moment.js @@ -137,75 +168,92 @@ var config = { new NameAllModulesPlugin(), // assign deterministic chunk ids - new webpack.NamedChunksPlugin((chunk) => { + new webpack.NamedChunksPlugin(chunk => { if (chunk.name) { return chunk.name; } - return chunk.modules.map((m) => { - var chunkPath = m.request.split('!').pop(); - return path.relative(m.context, chunkPath); - }).join('_'); - }), - // create cacheable common library bundle for all vue chunks - new webpack.optimize.CommonsChunkPlugin({ - name: 'common_vue', - chunks: [ - 'boards', - 'commit_pipelines', - 'cycle_analytics', - 'deploy_keys', - 'diff_notes', - 'environments', - 'environments_folder', - 'filtered_search', - 'groups', - 'issue_show', - 'job_details', - 'merge_conflicts', - 'notebook_viewer', - 'pdf_viewer', - 'pipelines', - 'pipelines_details', - 'schedule_form', - 'schedules_index', - 'sidebar', - 'vue_merge_request_widget', - ], - minChunks: function(module, count) { - return module.resource && (/vue_shared/).test(module.resource); - }, - }), + const moduleNames = []; - // create cacheable common library bundle for all d3 chunks - new webpack.optimize.CommonsChunkPlugin({ - name: 'common_d3', - chunks: [ - 'graphs', - 'users', - 'monitoring', - ], + function collectModuleNames(m) { + // handle ConcatenatedModule which does not have resource nor context set + if (m.modules) { + m.modules.forEach(collectModuleNames); + return; + } + + const pagesBase = path.join(ROOT_PATH, 'app/assets/javascripts/pages'); + + if (m.resource.indexOf(pagesBase) === 0) { + moduleNames.push( + path + .relative(pagesBase, m.resource) + .replace(/\/index\.[a-z]+$/, '') + .replace(/\//g, '__') + ); + } else { + moduleNames.push(path.relative(m.context, m.resource)); + } + } + + chunk.forEachModule(collectModuleNames); + + const hash = crypto + .createHash('sha256') + .update(moduleNames.join('_')) + .digest('hex'); + + return `${moduleNames[0]}-${hash.substr(0, 6)}`; }), // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ - names: ['main', 'locale', 'common', 'runtime'], + names: ['main', 'common', 'webpack_runtime'], }), + + // copy pre-compiled vendor libraries verbatim + new CopyWebpackPlugin([ + { + from: path.join( + ROOT_PATH, + `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs` + ), + to: 'monaco-editor/vs', + transform: function(content, path) { + if (/\.js$/.test(path) && !/worker/i.test(path) && !/typescript/i.test(path)) { + return ( + '(function(){\n' + + 'var define = this.define, require = this.require;\n' + + 'window.define = define; window.require = require;\n' + + content + + '\n}.call(window.__monaco_context__ || (window.__monaco_context__ = {})));' + ); + } + return content; + }, + }, + ]), ], resolve: { extensions: ['.js'], 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', - } - } -} + '~': 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'), + }, + }, + + // sqljs requires fs + node: { + fs: 'empty', + }, +}; if (IS_PRODUCTION) { config.devtool = 'source-map'; @@ -213,24 +261,20 @@ if (IS_PRODUCTION) { new webpack.NoEmitOnErrorsPlugin(), new webpack.LoaderOptionsPlugin({ minimize: true, - debug: false + debug: false, }), + new webpack.optimize.ModuleConcatenationPlugin(), new webpack.optimize.UglifyJsPlugin({ - sourceMap: true + sourceMap: true, }), new webpack.DefinePlugin({ - 'process.env': { NODE_ENV: JSON.stringify('production') } + 'process.env': { NODE_ENV: JSON.stringify('production') }, }) ); - // zopfli requires a lot of compute time and is disabled in CI + // compression can require a lot of compute time and is disabled in CI if (!NO_COMPRESSION) { - // gracefully fall back to gzip if `node-zopfli` is unavailable (e.g. in CentOS 6) - try { - config.plugins.push(new CompressionPlugin({ algorithm: 'zopfli' })); - } catch(err) { - config.plugins.push(new CompressionPlugin({ algorithm: 'gzip' })); - } + config.plugins.push(new CompressionPlugin()); } } @@ -239,15 +283,34 @@ if (IS_DEV_SERVER) { config.devServer = { host: DEV_SERVER_HOST, port: DEV_SERVER_PORT, + disableHostCheck: true, headers: { 'Access-Control-Allow-Origin': '*' }, stats: 'errors-only', hot: DEV_SERVER_LIVERELOAD, - inline: DEV_SERVER_LIVERELOAD + inline: DEV_SERVER_LIVERELOAD, }; - config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath; config.plugins.push( // watch node_modules for changes if we encounter a missing module compile error - new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')) + new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')), + + // watch for changes to our automatic entry point modules + { + apply(compiler) { + compiler.plugin('emit', (compilation, callback) => { + compilation.contextDependencies = [ + ...compilation.contextDependencies, + ...watchAutoEntries, + ]; + + // report our auto-generated bundle count + console.log( + `${autoEntriesCount} entries from '/pages' automatically added to webpack output.` + ); + + callback(); + }); + }, + } ); if (DEV_SERVER_LIVERELOAD) { config.plugins.push(new webpack.HotModuleReplacementPlugin()); |