import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin' ; import AssetsPlugin from 'assets-webpack-plugin' ; import { CleanWebpackPlugin } from 'clean-webpack-plugin' ; import CopyPlugin from 'copy-webpack-plugin' ; import { readFileSync } from 'fs' ; import HtmlWebpackPlugin from 'html-webpack-plugin' ; import MomentLocalesPlugin from 'moment-locales-webpack-plugin' ; import MomentTimezoneDataPlugin from 'moment-timezone-data-webpack-plugin' ; import { join , resolve } from 'path' ; import ReactRefreshTypeScript from 'react-refresh-typescript' ; import { Configuration , DefinePlugin , LoaderOptionsPlugin , optimize , WebpackPluginInstance } from 'webpack' ; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' ; import 'webpack-dev-server' ; import loadBuildConfig , { ANDROID , IOS , WEB } from 'build-configs' ; import { config as translationsConfig } from 'translations' ; delete process . env . TS_NODE_PROJECT ; const currentYear = new Date ( ) . getFullYear ( ) ; const SHORT_COMMIT_SHA_LENGTH = 8 ; const MAX_BUNDLE_SIZE = 1.64 * 2 ** 20 ; const MAX_ASSET_SIZE = 2.1 * 2 ** 20 ; const readJson = ( path ) => JSON . parse ( readFileSync ( path , 'utf8' ) ) ; const readVersionName = ( ) => { const versionFile = readJson ( resolve ( __dirname , '../../version.json' ) ) ; return versionFile . versionName ; } ; const generateManifest = ( content , buildConfigName ) => { const manifest = JSON . parse ( content . toString ( ) ) ; const androidBuildConfig = loadBuildConfig ( buildConfigName , ANDROID ) ; const iOSBuildConfig = loadBuildConfig ( buildConfigName , IOS ) ; const webBuildConfig = loadBuildConfig ( buildConfigName , WEB ) ; manifest . version = readVersionName ( ) ; manifest . homepage_url = webBuildConfig . aboutUrls . default ; manifest . theme_color = webBuildConfig . lightTheme . colors . themeColor ; manifest . name = webBuildConfig . appName ; manifest . description = webBuildConfig . appDescription ; manifest . related_applications = [ { platform : 'play' , id : androidBuildConfig . applicationId , url : `https://play.google.com/store/apps/details?id=${ androidBuildConfig . applicationId }` } , { platform : 'itunes' , url : `https://apps.apple.com/de/app/${ iOSBuildConfig . itunesAppName }/id${ iOSBuildConfig . appleId }` } ] ; manifest . short_name = manifest . name ; return JSON . stringify ( manifest , null , 2 ) ; } ; const createConfig = ( env = { } ) : Configuration => { const { config_name : buildConfigName , commit_sha : passedCommitSha , version_name : passedVersionName , dev_server : devServer } = env ; if ( ! buildConfigName ) { throw new Error ( 'Please specify a build config name' ) ; } const buildConfig = loadBuildConfig ( buildConfigName , WEB ) ; const NODE_ENV = devServer ? '"development"' : '"production"' ; process . env . NODE_ENV = NODE_ENV ; const versionName = passedVersionName || readVersionName ( ) ; const shortCommitSha = passedCommitSha ?. substring ( 0 , SHORT_COMMIT_SHA_LENGTH ) || 'Commit SHA unknown' ; console . log ( 'Used config: ' , buildConfigName ) ; console . log ( 'Configured as running in dev server: ' , ! devServer ) ; console . log ( 'Version name: ' , versionName ) ; console . log ( 'Commit SHA ' , shortCommitSha ) ; const configAssets = resolve ( __dirname , `../node_modules/build-configs/${ buildConfigName }/assets` ) ; const nodeModules = resolve ( __dirname , '../node_modules' ) ; const rootNodeModules = resolve ( __dirname , '../../node_modules' ) ; const wwwDirectory = resolve ( __dirname , '../www' ) ; const distDirectory = resolve ( __dirname , `../dist/${ buildConfigName }` ) ; const srcDirectory = resolve ( __dirname , '../src' ) ; const bundleReportDirectory = resolve ( __dirname , '../reports/bundle' ) ; const manifestPreset = resolve ( __dirname , 'manifest.json' ) ; const plugins = [ ] ; if ( devServer ) { plugins . push ( new ReactRefreshPlugin ( ) ) ; } const polyfills = [ 'whatwg-fetch' , 'url-polyfill' ] ; const config = { mode : devServer ? 'development' : 'production' , resolve : { extensions : [ '.tsx' , '.ts' , '.js' ] , modules : [ nodeModules , rootNodeModules ] , alias : { 'mapbox-gl' : 'maplibre-gl' } } , context : srcDirectory , entry : [ '!!style-loader!css-loader!normalize.css/normalize.css' , ... polyfills , './index.tsx' ] , output : { path : distDirectory , publicPath : '/' , filename : devServer ? '[name].js?[contenthash]' : '[name].[contenthash].js' , chunkFilename : devServer ? '[id].js?[chunkhash]' : '[id].[chunkhash].js' , sourcePrefix : '  ' } , optimization : { usedExports : true , runtimeChunk : 'single' } , devtool : 'source-map' , devServer : { static : { directory : distDirectory } , compress : true , port : 9000 , host : '0.0.0.0' , hot : true , http2 : false , historyApiFallback : true } , stats : 'minimal' , performance : { hints : ! devServer ? 'error' : false , maxEntrypointSize : MAX_BUNDLE_SIZE , maxAssetSize : MAX_ASSET_SIZE } , plugins : [ new BundleAnalyzerPlugin ( { analyzerMode : devServer ? 'disabled' : 'static' , generateStatsFile : ! devServer , openAnalyzer : false , reportFilename : join ( bundleReportDirectory , 'report.html' ) , statsFilename : join ( bundleReportDirectory , 'stats.json' ) } ) , new CleanWebpackPlugin ( ) , new HtmlWebpackPlugin ( { title : buildConfig . appName , template : 'index.ejs' , templateParameters : { config : buildConfig } } ) , new CopyPlugin ( { patterns : [ { from : wwwDirectory , to : distDirectory } , { from : configAssets , to : distDirectory } , { from : manifestPreset , to : distDirectory , transform : ( content ) => generateManifest ( content , buildConfigName ) } ] } ) , new DefinePlugin ( { 'process.env.NODE_ENV' : NODE_ENV , __VERSION_NAME__ : JSON . stringify ( versionName ) , __COMMIT_SHA__ : JSON . stringify ( shortCommitSha ) , __BUILD_CONFIG_NAME__ : JSON . stringify ( buildConfigName ) , __BUILD_CONFIG__ : JSON . stringify ( buildConfig ) } ) , new AssetsPlugin ( { path : distDirectory , filename : 'assets.json' , prettyPrint : true } ) , new LoaderOptionsPlugin ( { debug : devServer , minimize : ! devServer } ) , new MomentTimezoneDataPlugin ( { startYear : currentYear , endYear : currentYear + 2 } ) , new MomentLocalesPlugin ( { localesToKeep : translationsConfig . getSupportedLanguageTags ( ) , ignoreInvalidLocales : true } ) , ... plugins ] , module : { rules : [ { test : /\.tsx?$/ , exclude : /node_modules/ , use : [ { loader : 'ts-loader' , options : { transpileOnly : true , ... ( devServer && { getCustomTransformers : ( ) => ( { before : [ ReactRefreshTypeScript ( ) ] } ) } ) } } ] } , { test : /\.html$/ , use : [ { loader : 'html-loader' , options : { minimize : true } } ] } , { test : /\.css$/ , include : /node_modules/ , use : [ 'style-loader' , 'css-loader' ] } , { test : /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/ , type : 'javascript/auto' , use : [ { loader : 'url-loader' , options : { limit : 10000 } } , { loader : 'img-loader' , options : { enabled : ! devServer , gifsicle : { interlaced : false } , mozjpeg : { progressive : true , arithmetic : false } , optipng : false , pngquant : { floyd : 0.5 , speed : 2 } , svgo : { plugins : [ { removeTitle : true } , { convertPathData : false } ] } } } ] } , { test : /\.(eot|ttf|wav|mp3)$/ , type : 'assets/resource' } ] } } ; if ( ! devServer && optimize ) { config . plugins ?. push ( new optimize . AggressiveMergingPlugin ( ) ) ; } return config ; } ; export default createConfig ;