/**
 * Copyright (C) 2018-2026  The Software Heritage developers
 * See the AUTHORS file at the top-level directory of this distribution
 * License: GNU Affero General Public License version 3, or any later version
 * See top-level LICENSE file for more information
 */

// webpack configuration for compiling static assets in development mode

// import required node modules and webpack plugins
const chalk = require('chalk');
const {execSync} = require('child_process');
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const BundleTracker = require('webpack-bundle-tracker');
const RobotstxtPlugin = require('robotstxt-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin').CleanWebpackPlugin;
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const FixSwhSourceMapsPlugin = require('./webpack-plugins/fix-swh-source-maps-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const GenerateWebLabelsPlugin = require('./webpack-plugins/generate-weblabels-webpack-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const DumpHighlightjsLanguagesDataPlugin = require('./webpack-plugins/dump-highlightjs-languages-data-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const StylelintPlugin = require('stylelint-webpack-plugin');

// are we running webpack-dev-server ?
const isDevServer = process.argv.find(v => v.includes('serve')) !== undefined;
// webpack-dev-server configuration
const devServerPort = 3000;
const devServerPublicPath = 'http://localhost:' + devServerPort + '/static/';
// set publicPath according if we are using webpack-dev-server to serve
// our assets or not
const publicPath = isDevServer ? devServerPublicPath : '/static/';

const repoRootPath = path.resolve(__dirname, '../..');
const nodeModules = path.resolve(repoRootPath, 'node_modules');

let outputPath;
try {
  // handle editable install of swh.web package as compiled assets must
  // be outputted in the swh.web.static sub-package but it is located in
  // a non trivial location in that case
  const swhWebPythonModulePath = execSync(
    'python -c "from importlib.metadata import files;' +
    'pth_file = [p for p in files(\'swh.web\') if str(p).endswith(\'.pth\')];' +
    'swh_web_package_path = pth_file[0].read_text()[:-1];' +
    'assert swh_web_package_path.startswith(\'/\');' +
    'print(swh_web_package_path, end=\'\')"',
    {cwd: __dirname, stdio: ['pipe', 'pipe', null]}
  );
  outputPath = path.resolve(swhWebPythonModulePath.toString(), './swh/web/static');
} catch (_) {
  // assets should be outputted in swh/web/static folder otherwise
  outputPath = path.resolve('./swh/web/static/');
}

// collect all bundles we want to produce with webpack,
// bundles will be generated by scanning swh-web django applications:
//   * if swh/web/<app>/assets/index.js exists, bundle <app> is generated
//   * if swh/web/<app>/assets/<bundle>/index.js exists, bundle <bundle> is generated
var bundles = {};
const appsDir = path.join(repoRootPath, 'swh/web');
fs.readdirSync(appsDir).forEach(app => {
  const appAssetsDir = path.join(appsDir, app, 'assets');
  if (fs.existsSync(appAssetsDir)) {
    const appAssetsIndex = path.join(appAssetsDir, 'index.js');
    if (fs.existsSync(appAssetsIndex)) {
      bundles[app] = [path.join(app, 'assets', 'index.js')];
    } else {
      fs.readdirSync(appAssetsDir).forEach(appBundle => {
        const appBundleIndex = path.join(appAssetsDir, appBundle, 'index.js');
        if (fs.existsSync(appBundleIndex)) {
          bundles[appBundle] = [path.join(app, 'assets', appBundle, 'index.js')];
        }
      });
    }
  }
});

const mathjaxExtensionsLicenses = {};

const mathjaxTexExtensionsPath = path.join(nodeModules, 'mathjax/input/tex/extensions');
const mathjaxMMLExtensionsPath = path.join(nodeModules, 'mathjax/input/mml/extensions');

for (const mathjaxExtData of [
  {'extsType': 'tex', 'extsPath': mathjaxTexExtensionsPath},
  {'extsType': 'mml', 'extsPath': mathjaxMMLExtensionsPath}]) {
  fs.readdirSync(mathjaxExtData['extsPath']).forEach(extension => {
    if (extension.endsWith('.js')) {
      mathjaxExtensionsLicenses[`js/mathjax/input/${mathjaxExtData['extsType']}/extensions/${extension}`] = [
        {
          'id': `mathjax/input/${mathjaxExtData['extsType']}/extensions/${extension}`,
          'path': `./node_modules/mathjax/input/${mathjaxExtData['extsType']}/extensions/${extension}`,
          'spdxLicenseExpression': 'Apache-2.0',
          'licenseFilePath': './node_modules/mathjax/LICENSE'
        }
      ];
    }
  });
}

// common loaders for css related assets (css, sass)
const cssLoaders = [
  MiniCssExtractPlugin.loader,
  {
    loader: 'css-loader',
    options: {
      sourceMap: true
    }
  },
  {
    loader: 'postcss-loader',
    options: {
      sourceMap: true,
      postcssOptions: {
        plugins: [
          // automatically add vendor prefixes to css rules
          'autoprefixer',
          'postcss-normalize',
          ['postcss-reporter', {
            clearReportedMessages: true
          }]
        ]
      }
    }
  }
];

// webpack development configuration
module.exports = {
  // use caching to speedup incremental builds
  cache: {
    type: 'filesystem'
  },
  // set mode to development
  mode: 'development',
  // workaround for https://github.com/webpack/webpack-dev-server/issues/2758
  target: process.env.NODE_ENV === 'development' ? 'web' : 'browserslist',
  // use eval source maps when using webpack-dev-server for quick debugging,
  // otherwise generate source map files (more expensive)
  devtool: isDevServer ? 'eval' : 'source-map',
  // webpack-dev-server configuration
  devServer: {
    client: {
      logging: 'warn',
      overlay: {
        warnings: true,
        errors: true
      },
      progress: true
    },
    devMiddleware: {
      publicPath: devServerPublicPath,
      stats: 'errors-only'
    },
    setupMiddlewares: (middlewares) => {
      return middlewares.filter(middleware => middleware.name !== 'cross-origin-header-check');
    },
    host: '0.0.0.0',
    port: devServerPort,
    // enable to serve static assets not managed by webpack
    static: {
      directory: path.resolve('./'),
      watch: false
    },
    // we do not use hot reloading here (as a framework like React needs to be used in order to fully benefit from that feature)
    // and prefer to fully reload the frontend application in the browser instead
    hot: false,
    historyApiFallback: true,
    headers: {
      'Access-Control-Allow-Origin': '*'
    },
    watchFiles: {
      paths: ['assets/**/*', 'swh/web/**/*'],
      options: {
        ignored: /.*.sqlite3.*/
      }
    }
  },
  // set entries to the bundles we want to produce
  entry: bundles,
  // assets output configuration
  output: {
    path: outputPath,
    filename: 'js/[name].[contenthash].js',
    chunkFilename: 'js/[name].[contenthash].js',
    publicPath: publicPath,
    // each bundle will be compiled as a umd module with its own namespace
    // in order to easily use them in django templates
    library: ['swh', '[name]'],
    libraryTarget: 'umd'
  },
  // module resolving configuration
  resolve: {
    // configure base paths for resolving modules with webpack
    modules: [
      'node_modules',
      path.resolve(repoRootPath, 'assets/src'),
      path.resolve(repoRootPath, 'swh/web')
    ]
  },
  stats: 'errors-warnings',
  snapshot: {
    // fix webpack warning related to missing package.json file
    managedPaths: [/^highlightjs-/]
  },
  // module import configuration
  module: {
    rules: [
      {
      // Use babel-loader in order to use es6 syntax in js files
      // but also advanced js features like async/await syntax.
      // All code get transpiled to es5 in order to be executed
      // in a large majority of browsers.
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                // use env babel presets to benefit from es6 syntax
                ['@babel/preset-env', {
                  // Do not transform es6 module syntax to another module type
                  // in order to benefit from dead code elimination (aka tree shaking)
                  // when running webpack in production mode
                  'loose': true,
                  'modules': false
                }]
              ],
              plugins: [
                // use babel transform-runtime plugin in order to use aync/await syntax
                '@babel/plugin-transform-runtime',
                // use other babel plugins to benefit from advanced js features (es2017)
                '@babel/plugin-syntax-dynamic-import',
                // polyfill globalThis in old browsers
                'babel-plugin-transform-globalthis'
              ],
              env: {
                test: {
                  plugins: ['istanbul']
                }
              }
            }
          }]
      },
      {
        test: /\.ejs$/,
        use: [{
          loader: 'ejs-compiled-loader',
          options: {
            htmlmin: true,
            htmlminOptions: {
              removeComments: true
            }
          }
        }]
      },
      // css import configuration:
      //  - first process it with postcss
      //  - then extract it to a dedicated file associated to each bundle
      {
        test: /\.css$/,
        use: cssLoaders
      },
      // sass import configuration:
      //  - generate css with sass-loader
      //  - process it with postcss
      //  - then extract it to a dedicated file associated to each bundle
      {
        test: /\.scss$/,
        use: cssLoaders.concat([
          {
            loader: 'resolve-url-loader',
            options: {
              sourceMap: true
            }
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
              implementation: require('sass'),
              sassOptions: {
                quietDeps: true,
                silenceDeprecations: ['import', 'global-builtin']
              }
            }
          }
        ])
      },
      // web fonts import configuration
      {
        test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name][ext][query]'
        }
      }, {
        test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name][ext][query]'
        }
      }, {
        test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name][ext][query]'
        }
      }, {
        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name][ext][query]'
        }
      }, {
        test: /\.otf(\?v=\d+\.\d+\.\d+)?$/,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name][ext][query]'
        }
      }, {
        test: /\.png$/,
        type: 'asset/resource',
        generator: {
          filename: 'img/thirdParty/[name][ext][query]'
        }
      },
      {
        test: /\.ya?ml$/,
        type: 'json',
        use: {
          loader: 'yaml-loader',
          options: {
            asJSON: true
          }
        }
      }
    ],
    // tell webpack to not parse already minified files to speedup build process
    noParse: [path.resolve(nodeModules, 'mathjax/tex-mml-chtml.js')]
  },
  // webpack plugins
  plugins: [
    // cleanup previously generated assets
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: [
        '**/*', '!xml', '!xml/*', '!img', '!img/*',
        '!img/logos', '!img/logos/*', '!img/icons',
        '!img/icons/*', '!json', '!json/*',
        '!security.txt'
      ]
    }),
    // needed in order to use django_webpack_loader
    new BundleTracker({
      path: outputPath,
      filename: 'webpack-stats.json'
    }),
    // for generating the robots.txt file
    new RobotstxtPlugin({
      policy: [{
        userAgent: '*',
        disallow: ['/api/', '/browse/snapshot/*/log/', '/browse/revision/*/log/']
      }]
    }),
    // for extracting all stylesheets in separate css files
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css',
      chunkFilename: 'css/[name].[contenthash].css'
    }),
    // fix generated asset sourcemaps to workaround a Firefox issue
    new FixSwhSourceMapsPlugin(),
    // define some global variables accessible from js code
    new webpack.DefinePlugin({
      __STATIC__: JSON.stringify(publicPath)
    }),
    // needed in order to use pdf.js
    new webpack.IgnorePlugin({resourceRegExp: /^\.\/pdf.worker.js$/}),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.resolve(nodeModules, 'pdfjs-dist/legacy/build/pdf.worker.min.mjs'),
          to: path.resolve(outputPath, 'js/pdf.worker.min.js')
        },
        {
          from: path.resolve(nodeModules, '@mathjax/mathjax-newcm-font/chtml/woff2/**'),
          to: path.resolve(outputPath, 'fonts/[name][ext]')
        },
        {
          from: path.resolve(nodeModules, 'mathjax/input/tex/extensions/'),
          to: path.resolve(outputPath, 'js/mathjax/input/tex/extensions/')
        },
        {
          from: path.resolve(nodeModules, 'mathjax/input/mml/extensions/'),
          to: path.resolve(outputPath, 'js/mathjax/input/mml/extensions/')
        },
        {
          from: path.resolve(nodeModules, 'mathjax/sre/'),
          to: path.resolve(outputPath, 'js/mathjax/sre/')
        }
      ]
    }),
    new GenerateWebLabelsPlugin({
      outputType: 'json',
      exclude: ['mini-css-extract-plugin'],
      srcReplace: {
        './node_modules/admin-lte/dist/js/adminlte.min.js':
        './node_modules/admin-lte/dist/js/adminlte.js'
      },
      licenseOverride: {
        './assets/src/thirdparty/jquery.tabSlideOut/jquery.tabSlideOut.js': {
          'spdxLicenseExpression': 'GPL-3.0',
          'licenseFilePath': './assets/src/thirdparty/jquery.tabSlideOut/LICENSE'
        },
        './node_modules/highlightjs-chapel/dist/chapel.min.js': {
          'spdxLicenseExpression': 'BSD-3-Clause',
          'licenseFilePath': './node_modules/highlightjs-chapel/LICENSE'
        },
        './node_modules/highlightjs-lang/dist/lang.min.js': {
          'spdxLicenseExpression': 'MIT',
          'licenseFilePath': './node_modules/highlightjs-lang/LICENSE'
        },
        './node_modules/highlightjs-mirc/mirc.js': {
          'spdxLicenseExpression': 'MIT',
          'licenseFilePath': './node_modules/highlightjs-mirc/LICENSE'
        },
        './node_modules/highlightjs-never/dist/never.min.js': {
          'spdxLicenseExpression': 'MIT',
          'licenseFilePath': './node_modules/highlightjs-never/LICENSE'
        },
        './node_modules/highlightjs-bicep/src/highlightjs/dist/bicep.es.min.js': {
          'spdxLicenseExpression': 'MIT',
          'licenseFilePath': './node_modules/highlightjs-bicep/LICENSE'
        }
      },
      additionalScripts: Object.assign(
        {
          'js/pdf.worker.min.js': [
            {
              'id': 'pdfjs-dist/legacy/build/pdf.worker.mjs',
              'path': './node_modules/pdfjs-dist/build/pdf.worker.mjs',
              'spdxLicenseExpression': 'Apache-2.0',
              'licenseFilePath': './node_modules/pdfjs-dist/LICENSE'

            }
          ],
          '/jsreverse/': [
            {
              'id': 'jsreverse',
              'path': '/jsreverse/',
              'spdxLicenseExpression': 'AGPL-3.0-or-later',
              'licenseFilePath': './LICENSE'
            }
          ],
          'https://piwik.inria.fr/matomo.js': [
            {
              'id': 'matomo.js',
              'path': 'https://github.com/matomo-org/matomo/blob/master/js/piwik.js',
              'spdxLicenseExpression': 'BSD-3-Clause',
              'licenseFilePath': 'https://github.com/matomo-org/matomo/blob/master/js/LICENSE.txt'
            }
          ],
          ...mathjaxExtensionsLicenses
        }
      )
    }),
    new ProgressBarPlugin({
      format: chalk.cyan.bold('webpack build of swh-web assets') + ' [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)',
      width: 50
    }),
    new DumpHighlightjsLanguagesDataPlugin(),
    // Process all js files with eslint for consistent code style
    // and avoid bad js development practices.
    new ESLintPlugin({
      configType: 'flat',
      overrideConfigFile: path.join(repoRootPath, 'eslint.config.js'),
      cache: true,
      failOnError: !isDevServer
    }),
    // lint swh-web stylesheets
    new StylelintPlugin({
      config: {
        extends: 'stylelint-config-standard',
        rules: {
          'font-family-no-missing-generic-family-keyword': null,
          'no-descending-specificity': null,
          'selector-class-pattern': null,
          'media-feature-range-notation': 'prefix'
        },
        ignoreFiles: ['node_modules/**/*.css',
                      'node_modules/**/*.scss',
                      '.tox*/**/*.css',
                      '.tox*/**/*.scss',
                      'swh/web/tests/resources/**/*.css',
                      'swh/web/tests/resources/**/*.scss',
                      'build/**/*.css',
                      'build/**/*.scss',
                      'cypress/**/*.css',
                      'swh/web/**/*.scss',
                      'assets/src/thirdparty/**/*.css',
                      'docs/**/*.css',
                      '.cypress_cache/**/*.css',
                      'swh/web/static/css/*.css']
      }
    })
  ],
  // webpack optimizations
  optimization: {
    // ensure the vendors bundle gets emitted in a single chunk
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          test: 'vendors',
          chunks: 'all',
          name: 'vendors',
          enforce: true
        }
      }
    }
  },
  // disable webpack warnings about bundle sizes
  performance: {
    hints: false
  }
};
