Home > front end >  How to bundle (minify) a Kotlin React app for deployment?
How to bundle (minify) a Kotlin React app for deployment?

Time:04-03

The app is created with the default template for Kotlin React apps:

  • uses KTS-based Gradle build scripts;
  • Kotlin JS plugin 1.6.10;
  • Kotlin wrappers for React 17.0.2.

When using ./gradlew browserProductionWebpack without any additional tweaks, it generates a build/distributions directory with:

  • all resources (without any modifications);
  • index.html (without any modifications);
  • Kotlin sources compiled into one minified .js file.

What I want is to:

  • add some hash to the generated .js file;
  • minify the index.html file and refer the hashed .js file in it;
  • minify all resources (.json localization files).

Please prompt me some possible direction to do it. Looking to webpack configuration by adding corresponding scripts into webpack.config.d, but no luck yet: tried adding required dependencies into build.gradle.kts, i.e.:

implementation(devNpm("terser-webpack-plugin", "5.3.1"))
implementation(devNpm("html-webpack-plugin", "5.5.0"))

and describing webpack scripts:

const TerserPlugin = require("terser-webpack-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  optimization: {
      minimizer: [
        new TerserPlugin(),
        new HtmlWebpackPlugin({
          minify: {
            removeAttributeQuotes: true,
            collapseWhitespace: true,
            removeComments: true,
          },
        }),
      ],
  }
}

Any hint will be appreciated.

CodePudding user response:

A couple of things to put into consideration first.

  1. If some flexible bundling configuration is needed, most likely it won't be possible to use Kotlin-wrapped (Gradle) solutions. After checking the org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig it turns out that there is only a limited set of things can be configured with it.

  2. JS-based webpack configs require a little bit of reverse engineering to find out what is generated from the Kotlin/Gradle side and how to extend it.

  3. For simple configurations (IMHO) there is almost no need in tweaking bundling options while using the Kotlin JS plugin.

Let's start from initial webpack configs. In my case (see the short environment description in the question above) they appear in ./build/js/packages/<project-name>/webpack.config.js. These original configs will also include all contents from JS files we create inside the ./webpack.config.d folder.

Some webpack configurations require external JS dependencies. We need to declare them in the dependencies block of build.gradle.kts. In my case they are represented with:

// Bundling.
implementation(devNpm("html-webpack-plugin", "5.5.0"))
implementation(devNpm("uglifyjs-webpack-plugin", "2.2.0"))
implementation(devNpm("terser-webpack-plugin", "5.3.1"))
implementation(devNpm("copy-webpack-plugin", "9.1.0" )) // newer versions don't work correctly with npm and Yarn
implementation(devNpm("node-json-minify", "3.0.0"))

I also dropped all commonWebpackConfigs from build.gradle.kts as they are going to be performed manually on the JS level.

All webpack JS configs (inside the ./webpack.config.d folder) are divided into 3 files:

common.js (dev server configuration for both dev and production builds):

// All route paths should fallback to the index page to make SPA's routes processed correctly.
const devServer = config.devServer = config.devServer || {};
devServer.historyApiFallback = true;

development.js:

// All configs inside of this file will be enabled only in the development mode.
// To check the outputs of this config, see ../build/processedResources/js/main
if (config.mode == "development") {

    const HtmlWebpackPlugin = require("html-webpack-plugin");

    // Pointing to the template to be used as a base and injecting the JS sources path into.
    config.plugins.push(new HtmlWebpackPlugin({ template: "./kotlin/index.html" }));

}

and production.js:

// All configs inside of this file will be enabled only in the production mode.
// The result webpack configurations file will be generated inside ../build/js/packages/<project-name>
// To check the outputs of this config, see ../build/distributions
if (config.mode == "production") {

    const HtmlWebpackPlugin     = require("html-webpack-plugin"),
          UglifyJsWebpackPlugin = require("uglifyjs-webpack-plugin"),
          TerserWebpackPlugin   = require("terser-webpack-plugin"),
          CopyWebpackPlugin     = require("copy-webpack-plugin"),
          NodeJsonMinify        = require("node-json-minify");

    // Where to output and how to name JS sources.
    // Using hashes for correct caching.
    // The index.html will be updated correspondingly to refer the compiled JS sources.
    config.output.filename = "js/[name].[contenthash].js";

    // Making sure optimization and minimizer configs exist, or accessing its properties can crash otherwise.
    config.optimization = config.optimization || {};
    const minimizer = config.optimization.minimizer = config.optimization.minimizer || [];

    // Minifying HTML.
    minimizer.push(new HtmlWebpackPlugin({
        template: "./kotlin/index.html",
        minify: {
            removeAttributeQuotes: true,
            collapseWhitespace: true,
            removeComments: true,
        },
    }));

    // Minifying and obfuscating JS.
    minimizer.push(new UglifyJsWebpackPlugin({
        parallel: true,   // speeds up the compilation
        sourceMap: false, // help to match obfuscated functions with their origins, not needed for now
        uglifyOptions: {
            compress: {
                drop_console: true, // removing console calls
            }
        }
    }));

    // Additional JS minification.
    minimizer.push(new TerserWebpackPlugin({
        extractComments: true // excluding all comments (mostly licence-related ones) into a separate file
    }));

    // Minifying JSON locales.
    config.plugins.push(new CopyWebpackPlugin({
        patterns: [
            {
                context: "./kotlin",
                from: "./locales/**/*.json",
                to: "[path][name][ext]",
                transform: content => NodeJsonMinify(content.toString())
            }
        ]
    }));

}

I use styled components, so no CSS configs are provided. In other things these configs do almost the same minification as being done out-of-the-box without any additional configs. The differences are:

  • JS sources use a hash in their name: it is referenced correctly from the index page HTML template;
  • HTML template is minified;
  • locales (just simple JSON files) are minified.

It can look like an overhead slightly because as mentioned in the beginning, it does almost the same with minimal differences from the out-of-the-box configs. But as advantage we're getting more flexible configs which can be tweaked easier further.

  • Related