javascriptnpmwebpackterser

Scoping issue with terser-webpack-plugin options


I have a situation where I don't have Terser configured correctly and it's causing the compressed version of my page to break. My issue is that I have some functional declarations in my index.js. These declarations need to be accessible from Bootstrap modal windows that can be loaded in at a later time.

For example, in my index.js there is a function declared like this:

function doThis() { }

Then, say, a user opens an address form in a modal window, and a different javascript file called 'address-form.js' is loaded. In this form there is a button with an onclick handler that calls doThis(). The onclick handler lives in 'address-form.js' but is able to access doThis() in the parent index.js. The button works fine when index.js isn't compressed. But after it's compressed, I get an error saying doThis() doesn't exist.

I believe this to be a scoping issue, because I do see the doThis() declaration in index.js, but it appears to be wrapped in a bunch of parentheses. I'm not sure how to make the scope of doThis() the top-level window. The same scoping issue exists for literally hundreds of function declarations and variables in index.js, so I'm looking for a solution where I don't have to tinker too much with the gargantuan file.

Notice if I change the function declaration to an expression, it DOES seem to work:

window.doThis = function() {
}

However, because there are hundreds of vars and const and let variables in the file (in addition to dozens of function declarations), it's not really practical for me to change the scope of all of them just so the compression will work.

Here is my webpack.config.js:

const TerserPlugin = require("terser-webpack-plugin")
const glob = require('glob')
const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: glob.sync('./js/Pages/*.js').reduce((object, element) => {
    object[path.parse(element).name] = element
    return object
  }, {}),
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, './js/Pages/minified')
  },
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        test: /\.js(\?.*)?$/i,
        terserOptions: {
          mangle: false,
          compress: true,
          keep_fnames: true,
          keep_classnames: true,
          ie8: false,
          safari10: false,
          toplevel: false
        }
      })
    ]
  },
  plugins: [
    new webpack.optimize.LimitChunkCountPlugin({
      maxChunks: 1
    })
  ]
}

The command I'm running is:

webpack --mode=production --config webpack.config.js

SOLUTION

Solution as accepted below was to use terser-cli directly. I wrote a little script that runs terser on every file in a directory if anyone should find it helpful:

const fs = require('fs')
const path = require('path')
const exec = require('child_process').exec;
const Terser = require('terser')
const srcDir = '../js/Pages'
const destDir = '../js/Pages/minified'


function minifyPagesDir() {
  let srcFileNames = fs.readdirSync(srcDir).filter(path => path.match(/\.js$/)) || []
  let srcFilePaths = []
  
  let destFilePaths = srcFileNames.map((item, i) => {
    srcFilePaths[i] = `${srcDir}/${srcFileNames[i]}`
    return `${destDir}/${item}`
  })

  if (!fs.existsSync(destDir))
    fs.mkdirSync(destDir)
  
  srcFileNames.forEach((item, i) => {
    exec(
      `terser ${srcFilePaths[i]} -c -o ${destFilePaths[i]} --ecma 2021`,
      (error, stdout, stderr) => {
        if (error !== null)
            console.log(`RUHROH: ${error}`, ' stderr: ', stderr);
      }
    )

    console.log(`Minified '${srcFilePaths[i]}' to '${destFilePaths[i]}'!`)
  })
  console.log('Minification complete!')
}

minifyPagesDir()

Solution

  • I created a repro project with your configuration and verified that your problems are not caused by terser, but by webpack.

    I've used the following test file:

    function doThis() { }
    

    With minimize disabled and no minimizer set, this is the result of building the test file with webpack:

    /******/ (() => { // webpackBootstrap
    var __webpack_exports__ = {};
    function doThis() { }
    /******/ })()
    ;
    

    If we reformat it and remove the comments, it's clear that everything inside the file is dead code. Webpack is a bundler and wraps everything in an IIFE. This means that functions won't be assigned to global scope.

    (() => {
      var __webpack_exports__ = {};
      function doThis() { }
    })();
    

    Since you're not exporting anything or causing any side-effects, like assigning to window, terser will remove the content. This is correct and even without removing it, you still would get the same errors, since the function in webpack's output is not assigned to the global scope and your handlers couldn't access it. Compressing webpack's export with the terser cli results in an empty file.

    Running the terser cli directly on the test input file, results in the desired compressed output:

    function doThis(){}
    

    You said you've ran terser from the command line and had the same problems as when bundling and compressing through webpack. Did you run the source files through terser or the output of webpack? Because running the source files through terser should result in the desired output. Please try running terser foo/bar.js -c -o foo/bar.min.js and test if it solves your problem.

    If you want to use webpack, you need to either assign to window or move away from using global scope altogether.