Загрузка данных


/* eslint-disable @typescript-eslint/no-var-requires */

const path = require('path');

require('dotenv').config({ path: '.env.local' });

const ReactRefreshWebpackPlugin = require(
  '@pmmmwh/react-refresh-webpack-plugin',
);
const CircularDependencyPlugin = require(
  'circular-dependency-plugin',
);
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ForkTSCheckerPlugin = require(
  'fork-ts-checker-webpack-plugin',
);
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require(
  'mini-css-extract-plugin',
);
const ReactRefreshTypeScript = require(
  'react-refresh-typescript',
);
const TerserPlugin = require('terser-webpack-plugin');
const { ids } = require('webpack');

const { HashedModuleIdsPlugin } = ids;

let standSwitcher;

try {
  standSwitcher = require('./dev/stands/index.js');
} catch {
  standSwitcher = undefined;
}

const PORT = 3000;
const MAX_CYCLES = 84;

let numCyclesDetected = 0;

function createCircularDependencyPlugin() {
  return new CircularDependencyPlugin({
    // exclude detection of files based on a RegExp
    exclude: /a\.js|node_modules/,
    // include specific files based on a RegExp
    // include: /dir/,
    // add errors to webpack instead of warnings
    // failOnError: true,
    // allow import cycles that include an asyncronous import,
    // e.g. via import(/* webpackMode: "weak" */ './file.js')
    allowAsyncCycles: false,
    // set the current working directory for displaying module paths
    cwd: process.cwd(),

    onStart() {
      numCyclesDetected = 0;
    },

    onDetected({ paths, compilation }) {
      numCyclesDetected += 1;

      compilation.warnings.push(
        new Error(paths.join(' -> ')),
      );
    },

    onEnd({ compilation }) {
      if (numCyclesDetected <= MAX_CYCLES) {
        return;
      }

      compilation.errors.push(
        new Error(
          `Detected ${numCyclesDetected} cycles which exceeds configured limit of ${MAX_CYCLES}`,
        ),
      );
    },
  });
}

function createCopyPatterns({
  isDevelopment,
  currentStand,
}) {
  const patterns = [{ from: 'public' }];

  if (isDevelopment) {
    patterns.push({
      from:
        standSwitcher?.getPathForCopyPlugin(
          currentStand,
        ) || 'env',
    });
  }

  return patterns;
}

function createPlugins({
  env,
  isDevelopment,
  currentStand,
}) {
  const plugins = [
    createCircularDependencyPlugin(),

    new HtmlWebpackPlugin({
      template: 'src/index.html',
      publicPath: env.staticContextPath || 'auto',
    }),

    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
    }),

    new CopyWebpackPlugin({
      patterns: createCopyPatterns({
        isDevelopment,
        currentStand,
      }),
    }),

    new HashedModuleIdsPlugin(),

    new ForkTSCheckerPlugin({}),
  ];

  if (isDevelopment) {
    plugins.push(
      new ReactRefreshWebpackPlugin({
        overlay: false,
      }),
    );
  }

  return plugins;
}

function createMinimizer() {
  return [
    new TerserPlugin({
      extractComments: false,
    }),
  ];
}

function getPackageName(module) {
  const match = module.context?.match(
    /[\\/]node_modules[\\/](.*?)([\\/]|$)/,
  );

  return match?.[1] || 'unknown';
}

function createVendorCacheGroup() {
  return {
    test: /[\\/]node_modules[\\/]/,

    name(module) {
      // получает имя, то есть node_modules/packageName/not/this/part.js
      // или node_modules/packageName
      const packageName = getPackageName(module);

      // имена npm-пакетов можно, не опасаясь проблем, использовать
      // в URL, но некоторые серверы не любят символы наподобие @
      return `npm.${packageName.replace('@', '')}`;
    },
  };
}

function createOptimization(isDevelopment) {
  const minimizer = createMinimizer();

  if (isDevelopment) {
    return {
      minimizer,
    };
  }

  return {
    runtimeChunk: 'single',

    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,

      cacheGroups: {
        vendor: createVendorCacheGroup(),
      },
    },

    minimizer,
  };
}

function createCssLoaderOptions(isDevelopment) {
  if (isDevelopment) {
    return {
      modules: {
        auto: true,
        localIdentName:
          '[local]--[hash:base64:5]',
      },
    };
  }

  return {
    modules: {
      auto: true,
      hashStrategy: 'minimal-subset',
      localIdentHashDigestLength: 5,
    },
  };
}

function getStyleLoader(env) {
  return env.production
    ? MiniCssExtractPlugin.loader
    : 'style-loader';
}

function createTypeScriptLoader(isDevelopment) {
  return {
    loader: 'ts-loader',

    options: {
      transpileOnly: true,

      getCustomTransformers: () => ({
        before: isDevelopment
          ? [ReactRefreshTypeScript()]
          : [],
      }),
    },
  };
}

function createModuleRules({
  env,
  isDevelopment,
}) {
  const styleLoader = getStyleLoader(env);

  return [
    {
      test: /\.d\.ts$/,
      use: 'null-loader',
    },

    {
      test: /\.(woff|woff2|eot|ttf|otf)$/i,
      type: 'asset/resource',

      generator: {
        filename: 'static/fonts/[name][ext]',
      },
    },

    {
      test: /\.[jt]sx?$/,
      exclude: /node_modules/,

      use: [
        createTypeScriptLoader(isDevelopment),
      ],
    },

    {
      test: /\.s[ac]ss$/i,

      use: [
        styleLoader,

        {
          loader: 'css-loader',
          options:
            createCssLoaderOptions(isDevelopment),
        },

        {
          loader: 'sass-loader',

          options: {
            sassOptions: {
              includePaths: ['./src/styles'],
            },
          },
        },
      ],
    },

    {
      test: /\.(png|jpe?g|gif|mp(3|4))$/i,

      use: [
        {
          loader: 'file-loader',
        },
      ],
    },

    {
      test: /\.css$/i,
      use: [
        styleLoader,
        'css-loader',
      ],
    },

    {
      test: /\.mdx$/,

      use: [
        {
          loader: 'babel-loader',

          options: {
            presets: ['@babel/preset-react'],
          },
        },

        {
          loader: '@mdx-js/loader',
        },
      ],
    },

    {
      test: /\.svg$/,
      issuer: /\.[jt]sx?$/,
      use: [
        '@svgr/webpack',
        'url-loader',
      ],
    },
  ];
}

function createResolveConfig() {
  return {
    extensions: ['.ts', '.tsx', '.js'],

    extensionAlias: {
      '.js': ['.js', '.ts'],
      '.cjs': ['.cjs', '.cts'],
      '.mjs': ['.mjs', '.mts'],
    },

    alias: {
      '@modules': path.resolve(
        __dirname,
        'src/modules',
      ),
      '@widgets': path.resolve(
        __dirname,
        'src/widgets',
      ),
      '@core': path.resolve(
        __dirname,
        'src/core',
      ),
      '@uikit': path.resolve(
        __dirname,
        'src/uikit',
      ),
      '@pages': path.resolve(
        __dirname,
        'src/pages',
      ),
      '@components': path.resolve(
        __dirname,
        'src/components',
      ),
      '@styles': path.resolve(
        __dirname,
        'src/styles',
      ),
      '@store': path.resolve(
        __dirname,
        'src/store',
      ),
      types: path.resolve(
        __dirname,
        'src/types',
      ),
      '@configs': path.resolve(
        __dirname,
        'src/configs',
      ),
      '@hooks': path.resolve(
        __dirname,
        'src/hooks',
      ),
      '@api': path.resolve(
        __dirname,
        'src/api',
      ),
      '@utils': path.resolve(
        __dirname,
        'src/utils',
      ),
      '@localisation': path.resolve(
        __dirname,
        'src/localisation',
      ),
      '@features': path.resolve(
        __dirname,
        'src/features',
      ),
      '@terminal': path.resolve(
        __dirname,
        'src/terminal',
      ),
      '@testing': path.resolve(
        __dirname,
        'src/testing',
      ),
    },
  };
}

function createProxy(localDevUrl) {
  return {
    target: '',
    secure: false,
    changeOrigin: true,
    ws: true,

    // Этот заголовок нужен для правильного редиректа обратно на локальный домен после авторизации
    headers: {
      'X-Dev-Redirect': localDevUrl,
    },
  };
}

function createDevServer({
  currentStand,
  localDevUrl,
  proxy,
}) {
  const getProxyConfig = () =>
    standSwitcher?.modifyProxy(
      currentStand,
      proxy,
    ) || proxy;

  return {
    server: 'https',
    allowedHosts: 'all',

    static: {
      directory: path.join(
        __dirname,
        'public',
      ),
    },

    compress: true,
    port: PORT,

    client: {
      overlay: false,
      progress: true,
    },

    historyApiFallback: true,

    open: {
      target: localDevUrl,
    },

    proxy: {
      '/session': getProxyConfig(),
      '/ntbagro': getProxyConfig(),
    },
  };
}

function createWebpackConfig(env = {}) {
  const isDevelopment = env.type === 'start';

  const currentStand =
    process.env.DEV_STAND ||
    env.stand ||
    standSwitcher?.getDefaultStand();

  const localDevUrl = `${
    standSwitcher?.getLocalDevUrl(
      currentStand,
    ) || 'localhost'
  }:${PORT}`;

  const proxy = createProxy(localDevUrl);

  return {
    entry: './src/index.tsx',

    devtool: isDevelopment
      ? 'source-map'
      : false,

    output: {
      filename: '[name].[contenthash].js',

      path: path.resolve(
        __dirname,
        'dist',
      ),
    },

    plugins: createPlugins({
      env,
      isDevelopment,
      currentStand,
    }),

    resolve: createResolveConfig(),

    optimization:
      createOptimization(isDevelopment),

    module: {
      rules: createModuleRules({
        env,
        isDevelopment,
      }),
    },

    devServer: createDevServer({
      currentStand,
      localDevUrl,
      proxy,
    }),
  };
}

module.exports = createWebpackConfig;