'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var _fs = require('fs'); var _os = require('os'); var _os2 = _interopRequireDefault(_os); var _boom = require('boom'); var _boom2 = _interopRequireDefault(_boom); var _miniCssExtractPlugin = require('mini-css-extract-plugin'); var _miniCssExtractPlugin2 = _interopRequireDefault(_miniCssExtractPlugin); var _terserWebpackPlugin = require('terser-webpack-plugin'); var _terserWebpackPlugin2 = _interopRequireDefault(_terserWebpackPlugin); var _webpack = require('webpack'); var _webpack2 = _interopRequireDefault(_webpack); var _Stats = require('webpack/lib/Stats'); var _Stats2 = _interopRequireDefault(_Stats); var _threadLoader = require('thread-loader'); var threadLoader = _interopRequireWildcard(_threadLoader); var _webpackMerge = require('webpack-merge'); var _webpackMerge2 = _interopRequireDefault(_webpackMerge); var _dynamic_dll_plugin = require('./dynamic_dll_plugin'); var _lodash = require('lodash'); var _utils = require('../utils'); var _public_path_placeholder = require('./public_path_placeholder'); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const BABEL_EXCLUDE_RE = [/[\/\\](webpackShims|node_modules|bower_components)[\/\\]/]; const STATS_WARNINGS_FILTER = new RegExp(['(export .* was not found in)', '|(chunk .* \\[mini-css-extract-plugin\\]\\\nConflicting order between:)'].join('')); class BaseOptimizer { constructor(opts) { this.logWithMetadata = opts.logWithMetadata || (() => null); this.uiBundles = opts.uiBundles; this.profile = opts.profile || false; switch (opts.sourceMaps) { case true: this.sourceMaps = 'source-map'; break; case 'fast': this.sourceMaps = 'cheap-module-eval-source-map'; break; default: this.sourceMaps = opts.sourceMaps || false; break; } // Run some pre loading in order to prevent // high delay when booting thread loader workers this.warmupThreadLoaderPool(); } async init() { if (this.compiler) { return this; } const compilerConfig = this.getConfig(); this.compiler = (0, _webpack2.default)(compilerConfig); // register the webpack compiler hooks // for the base optimizer this.registerCompilerHooks(); return this; } registerCompilerHooks() { this.registerCompilerDoneHook(); } registerCompilerDoneHook() { this.compiler.hooks.done.tap('base_optimizer-done', stats => { // We are not done while we have an additional // compilation pass to run // We also don't need to emit the stats if we don't have // the profile option set if (!this.profile || stats.compilation.needAdditionalPass) { return; } const path = this.uiBundles.resolvePath('stats.json'); const content = JSON.stringify(stats.toJson()); (0, _fs.writeFile)(path, content, function (err) { if (err) throw err; }); }); } warmupThreadLoaderPool() { const baseModules = ['babel-loader', BABEL_PRESET_PATH]; const nonDistributableOnlyModules = !_utils.IS_KIBANA_DISTRIBUTABLE ? ['ts-loader'] : []; threadLoader.warmup( // pool options, like passed to loader options // must match loader options to boot the correct pool this.getThreadLoaderPoolConfig(), [ // modules to load on the pool ...baseModules, ...nonDistributableOnlyModules]); } getThreadPoolCpuCount() { const cpus = _os2.default.cpus(); if (!cpus) { // sometimes this call returns undefined so we fall back to 1: https://github.com/nodejs/node/issues/19022 return 1; } return Math.max(1, Math.min(cpus.length - 1, 7)); } getThreadLoaderPoolConfig() { // Calculate the node options from the NODE_OPTIONS env var const parsedNodeOptions = process.env.NODE_OPTIONS ? process.env.NODE_OPTIONS.split(/\s/) : []; return { name: 'optimizer-thread-loader-main-pool', workers: 1, workerParallelJobs: 1, // This is a safe check in order to set // the parent node options applied from // the NODE_OPTIONS env var for every launched worker. // Otherwise, if the user sets max_old_space_size, as they // are used to, into NODE_OPTIONS, it won't affect the workers. workerNodeArgs: parsedNodeOptions, poolParallelJobs: 1, poolTimeout: this.uiBundles.isDevMode() ? Infinity : 2000 }; } getConfig() { function getStyleLoaderExtractor() { return [_miniCssExtractPlugin2.default.loader]; } function getStyleLoaders(preProcessors = [], postProcessors = []) { return [...postProcessors, { loader: 'css-loader', options: { // importLoaders needs to know the number of loaders that follow this one, // so we add 1 (for the postcss-loader) to the length of the preProcessors // array that we merge into this array importLoaders: 1 + preProcessors.length } }, { loader: 'postcss-loader', options: { config: { path: POSTCSS_CONFIG_PATH } } }, ...preProcessors]; } /** * Adds a cache loader if we're running in dev mode. The reason we're not adding * the cache-loader when running in production mode is that it creates cache * files in optimize/.cache that are not necessary for distributable versions * of Kibana and just make compressing and extracting it more difficult. */ const maybeAddCacheLoader = (cacheName, loaders) => { if (_utils.IS_KIBANA_DISTRIBUTABLE) { return loaders; } return [{ loader: 'cache-loader', options: { cacheDirectory: this.uiBundles.getCacheDirectory(cacheName) } }, ...loaders]; }; /** * Creates the selection rules for a loader that will only pass for * source files that are eligible for automatic transpilation. */ const createSourceFileResourceSelector = test => { return [{ test, exclude: BABEL_EXCLUDE_RE.concat(this.uiBundles.getWebpackNoParseRules()) }, { test, include: /[\/\\]node_modules[\/\\]x-pack[\/\\]/, exclude: /[\/\\]node_modules[\/\\]x-pack[\/\\](.+?[\/\\])*node_modules[\/\\]/ }]; }; const commonConfig = { mode: 'development', node: { fs: 'empty' }, context: (0, _utils.fromRoot)('.'), cache: true, entry: this.uiBundles.toWebpackEntries(), devtool: this.sourceMaps, profile: this.profile || false, output: { path: this.uiBundles.getWorkingDir(), filename: '[name].bundle.js', sourceMapFilename: '[file].map', publicPath: _public_path_placeholder.PUBLIC_PATH_PLACEHOLDER, devtoolModuleFilenameTemplate: '[absolute-resource-path]' }, optimization: { splitChunks: { cacheGroups: { commons: { name: 'commons', chunks: 'initial', minChunks: 2, reuseExistingChunk: true } } }, noEmitOnErrors: true }, plugins: [new _dynamic_dll_plugin.DynamicDllPlugin({ uiBundles: this.uiBundles, threadLoaderPoolConfig: this.getThreadLoaderPoolConfig(), logWithMetadata: this.logWithMetadata }), new _miniCssExtractPlugin2.default({ filename: '[name].style.css' }), // replace imports for `uiExports/*` modules with a synthetic module // created by create_ui_exports_module.js new _webpack2.default.NormalModuleReplacementPlugin(/^uiExports\//, resource => { // the map of uiExport types to module ids const extensions = this.uiBundles.getAppExtensions(); // everything following the first / in the request is // treated as a type of appExtension const type = resource.request.slice(resource.request.indexOf('/') + 1); resource.request = [ // the "val-loader" is used to execute create_ui_exports_module // and use its return value as the source for the module in the // bundle. This allows us to bypass writing to the file system require.resolve('val-loader'), '!', require.resolve('./create_ui_exports_module'), '?', // this JSON is parsed by create_ui_exports_module and determines // what require() calls it will execute within the bundle JSON.stringify({ type, modules: extensions[type] || [] })].join(''); }), ...this.uiBundles.getWebpackPluginProviders().map(provider => provider(_webpack2.default))], module: { rules: [{ test: /\.less$/, use: [...getStyleLoaderExtractor(), ...getStyleLoaders(['less-loader'], maybeAddCacheLoader('less', []))] }, { test: /\.css$/, use: [...getStyleLoaderExtractor(), ...getStyleLoaders([], maybeAddCacheLoader('css', []))] }, { test: /\.(html|tmpl)$/, loader: 'raw-loader' }, { test: /\.(png|jpg|gif|jpeg)$/, loader: ['url-loader'] }, { test: /\.(woff|woff2|ttf|eot|svg|ico)(\?|$)/, loader: 'file-loader' }, { resource: createSourceFileResourceSelector(/\.js$/), use: maybeAddCacheLoader('babel', [{ loader: 'thread-loader', options: this.getThreadLoaderPoolConfig() }, { loader: 'babel-loader', options: { babelrc: false, presets: [BABEL_PRESET_PATH] } }]) }, ...this.uiBundles.getPostLoaders().map(loader => _extends({ enforce: 'post' }, loader))], noParse: this.uiBundles.getWebpackNoParseRules() }, resolve: { extensions: ['.js', '.json'], mainFields: ['browser', 'browserify', 'main'], modules: ['webpackShims', (0, _utils.fromRoot)('webpackShims'), 'node_modules', (0, _utils.fromRoot)('node_modules')], alias: this.uiBundles.getAliases() }, performance: { // NOTE: we are disabling this as those hints // are more tailored for the final bundles result // and not for the webpack compilations performance itself hints: false } }; // when running from the distributable define an environment variable we can use // to exclude chunks of code, modules, etc. const isDistributableConfig = { plugins: [new _webpack2.default.DefinePlugin({ 'process.env': { 'IS_KIBANA_DISTRIBUTABLE': `"true"` } })] }; // when running from source transpile TypeScript automatically const getSourceConfig = () => { // dev/typescript is deleted from the distributable, so only require it if we actually need the source config const { Project } = require('../dev/typescript'); const browserProject = new Project((0, _utils.fromRoot)('tsconfig.browser.json')); return { module: { rules: [{ resource: createSourceFileResourceSelector(/\.tsx?$/), use: maybeAddCacheLoader('typescript', [{ loader: 'thread-loader', options: this.getThreadLoaderPoolConfig() }, { loader: 'ts-loader', options: { happyPackMode: true, transpileOnly: true, experimentalWatchApi: true, onlyCompileBundledFiles: true, configFile: (0, _utils.fromRoot)('tsconfig.json'), compilerOptions: _extends({}, browserProject.config.compilerOptions, { sourceMap: Boolean(this.sourceMaps) }) } }]) }] }, resolve: { extensions: ['.ts', '.tsx'] } }; }; // We need to add react-addons (and a few other bits) for enzyme to work. // https://github.com/airbnb/enzyme/blob/master/docs/guides/webpack.md const supportEnzymeConfig = { externals: { 'mocha': 'mocha', 'react/lib/ExecutionEnvironment': true, 'react/addons': true, 'react/lib/ReactContext': true } }; const watchingConfig = { plugins: [new _webpack2.default.WatchIgnorePlugin([ // When our bundle entry files are fresh they cause webpack // to think they might have changed since the watcher was // initialized, which triggers a second compilation on startup. // Since we can't reliably update these files anyway, we can // just ignore them in the watcher and prevent the extra compilation /bundles[\/\\].+\.entry\.js/])] }; // in production we set the process.env.NODE_ENV and run // the terser minimizer over our bundles const productionConfig = { mode: 'production', optimization: { minimizer: [new _terserWebpackPlugin2.default({ parallel: true, sourceMap: false, terserOptions: { compress: false, mangle: false } })] } }; return (0, _webpackMerge2.default)(commonConfig, _utils.IS_KIBANA_DISTRIBUTABLE ? isDistributableConfig : getSourceConfig(), this.uiBundles.isDevMode() ? (0, _webpackMerge2.default)(watchingConfig, supportEnzymeConfig) : productionConfig); } isFailure(stats) { if (stats.hasErrors()) { return true; } const { warnings } = stats.toJson({ all: false, warnings: true }); // 1 - when typescript doesn't do a full type check, as we have the ts-loader // configured here, it does not have enough information to determine // whether an imported name is a type or not, so when the name is then // exported, typescript has no choice but to emit the export. Fortunately, // the extraneous export should not be harmful, so we just suppress these warnings // https://github.com/TypeStrong/ts-loader#transpileonly-boolean-defaultfalse // // 2 - Mini Css Extract plugin tracks the order for each css import we have // through the project (and it's successive imports) since version 0.4.2. // In case we have the same imports more than one time with different // sequences, this plugin will throw a warning. This should not be harmful, // but the an issue was opened and can be followed on: // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/250#issuecomment-415345126 const filteredWarnings = _Stats2.default.filterWarnings(warnings, STATS_WARNINGS_FILTER); return filteredWarnings.length > 0; } failedStatsToError(stats) { const details = stats.toString((0, _lodash.defaults)({ colors: true, warningsFilter: STATS_WARNINGS_FILTER }, _Stats2.default.presetToOptions('minimal'))); return _boom2.default.internal(`Optimizations failure.\n${details.split('\n').join('\n ')}\n`, stats.toJson((0, _lodash.defaults)(_extends({ warningsFilter: STATS_WARNINGS_FILTER }, _Stats2.default.presetToOptions('detailed'))))); } } exports.default = BaseOptimizer; module.exports = exports['default'];