nukeop/nuclear

View on GitHub
packages/app/webpack.config.ts

Summary

Maintainability
C
1 day
Test Coverage
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
import 'webpack-dev-server';

const BUILD_DIR = path.resolve(__dirname, '../../dist');
const APP_DIR = path.resolve(__dirname, 'app');
const RESOURCES_DIR = path.resolve(__dirname, 'resources');

const UI_DIR = path.resolve(__dirname, 'node_modules', '@nuclear', 'ui');
const VENDOR_DIR = path.resolve(__dirname, '..', '..', 'node_modules');

const buildIncludedPaths = () => {
  const paths: string[] = [];
  const modules = ['core', 'i18n', 'ui'];
  const srcs = ['src', 'lib', 'index.js', 'index.ts'];
  modules.forEach((module) => {
    srcs.forEach((src) => {
      paths.push(
        path.resolve(
          __dirname, 
          '..', '..',
          'node_modules', '@nuclear', module, src
        )
      );
    });
  });
  return paths;
};
const NUCLEAR_MODULES = buildIncludedPaths();

module.exports = (env) => {
  const IS_PROD = env.NODE_ENV === 'production';
  const IS_DEV = env.NODE_ENV === 'development';

  const entry = IS_PROD
    ? path.resolve(APP_DIR, 'index.js')
    : [
      'react-hot-loader/patch',
      'webpack/hot/dev-server.js',
      'webpack-dev-server/client/index.js?hot=true&live-reload=true',
      path.resolve(APP_DIR, 'index.js')
    ];
  const output: webpack.Configuration['output'] = {
    path: BUILD_DIR,
    filename: '[name].[contenthash].js',
    sourceMapFilename: '[name].[contenthash].map',
    chunkFilename: '[id].[contenthash].js'
  };
  const optimization: webpack.Configuration['optimization'] = {
    moduleIds: 'named' as const,
    minimize: false
  };
  const jsxRule: webpack.RuleSetRule = {
    test: /\.(js|jsx|tsx|ts)$/,
    loader: 'babel-loader',
    options: {
      cacheDirectory: true,
      presets: [
        [
          '@babel/preset-env',
          {
            targets: {
              electron: '4.2'
            }
          }
        ],
        '@babel/preset-react',
        '@babel/preset-typescript'
      ],
      plugins: [
        ['@babel/plugin-proposal-decorators', { legacy: true }],
        '@babel/plugin-proposal-class-properties',
        '@babel/plugin-proposal-object-rest-spread'
      ],
      ignore: [/node_modules\/(?!@nuclear).*/]
    }
  };
  const contentSecurity =
    'connect-src *; style-src \'unsafe-inline\' https:; font-src https: data:; img-src https: data: file:;';
  const plugins: webpack.Configuration['plugins'] = [
    new HtmlWebpackPlugin({
      meta: {
        charset: {
          charset: 'UTF-8'
        },
        'Content-Security-Policy': {
          'http-equiv': 'Content-Security-Policy',
          content: IS_DEV
            ? `${contentSecurity} script-src 'unsafe-eval' 'unsafe-inline' localhost:8080`
            : contentSecurity
        }
      },
      template: path.resolve(__dirname, 'index.html'),
      minify: {
        html5: true,
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true
      },
      inject: true
    }),
    new webpack.DefinePlugin(
      Object.entries(
        dotenv.parse(fs.readFileSync(path.resolve(__dirname, '../../.env')))
      ).reduce(
        (acc, [key, value]) => ({
          ...acc,
          [`process.env.${key}`]: JSON.stringify(value)
        }),
        {}
      )
    ),
    new webpack.ContextReplacementPlugin(
      /\/(ytpl|ytsr|youtube-ext|bandcamp-scraper|http-cookie-agent|deasync)\//,
      false
    ),
    new webpack.ContextReplacementPlugin(
      /deasync/,
      path.resolve(__dirname, 'node_modules', 'deasync', 'index.js')
    )
  ];

  if (IS_PROD) {
    jsxRule.loader = 'ts-loader';
    jsxRule.options = {
      allowTsInNodeModules: true
    };
    jsxRule.include = [APP_DIR, ...NUCLEAR_MODULES];
    jsxRule.exclude = [
      /node_modules\/electron-timber\/preload\.js/,
      /node_modules\/(?!@nuclear).*/
    ];
    optimization.splitChunks = {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    };
  } else {
    output.publicPath = '/';
    jsxRule.exclude = /node_modules\/(?!@nuclear).*/;
  }

  const config: webpack.Configuration = {
    entry,
    output,
    devtool: 'source-map',
    stats: 'errors-only',
    mode: IS_PROD ? 'production' : 'development',
    optimization,
    resolve: {
      extensions: ['.*', '.js', '.ts', '.jsx', '.tsx', '.json'],
      alias: {
        react: path.resolve(__dirname, '..', '..', 'node_modules', 'react'),
        'styled-component': path.resolve(
          __dirname,
          'node_modules/styled-component'
        )
      },
      fallback: {
        fs: false
      },
      symlinks: false
    },
    externals: ['bufferutil', 'utf-8-validate'],
    module: {
      rules: [
        jsxRule,
        {
          test: /.scss$/,
          use: [
            'style-loader',
            {
              loader: 'css-loader',
              options: {
                importLoaders: 1,
                modules: {
                  localIdentName: '[local]'
                }
              }
            },
            'sass-loader'
          ]
        },
        {
          test: /\.css/,
          use: [
            'style-loader',
            {
              loader: 'css-loader',
              options: {
                url: (url) => {
                  if (url.includes('charset=utf-8;;')) {
                    return false;
                  }
                  return true;
                }
              }
            }
          ]
        },
        {
          test: /\.(png|jpg|gif)$/,
          loader: 'url-loader',
          include: [RESOURCES_DIR, APP_DIR, UI_DIR, VENDOR_DIR]
        },
        {
          test: /\.(ttf|eot|woff|woff2|svg)$/,
          loader: 'url-loader',
          include: [UI_DIR, APP_DIR, VENDOR_DIR]
        },
        {
          test: /\.svg$/,
          loader: 'svg-inline-loader'
        },
        {
          test: /node_modules[/\\](iconv-lite)[/\\].+/,
          resolve: {
            aliasFields: ['main']
          }
        }
      ]
    },
    plugins,
    target: 'electron-renderer'
  };

  if (IS_DEV) {
    config.devServer = {
      hot: true,
      static: {
        publicPath: '/'
      },
      allowedHosts: 'all'
    };
  }

  return config;
};