Terser: run on multiple files using globs and unblock yourself from the issues

Mohamed Lamine Allal
5 min readNov 8, 2023

--

Terser, does have issues, terser is one-to-one file processing only. You need to run the command multiple times. Also, there are other issues. We will explore them and present good solutions in this article.

The problem

  • Terser support only processing-output one file at a time (no globs, or dir) ❌
    ◌ one => one
    ◌ multiple => one (bundling)
terser file1.js file2.js -o someCombiningFile.js # terser is made with bundling in mind
terser src/file.js -o dist/file.minified.js
  • What about many =to=> many (Like, typescript, eslint, babel, swc, and many others do)
  • So if you have for example a typescript project where there are multiple files. And after transpiring typescript to javascript in an dest output directory. And want to terser all of the files. How would you do it ??
  • You can check the issue on GitHub here
    ◌ No solution is intended. You have to loop yourself.
  • Other issues ❌
    ◌ Doesn’t create directories for you if doesn’t exist. If you set some output. And the dir doesn’t exist. You’ll get an error.

✨ How to solve it, use globs and gain productivity

Besides programmatic usage with some glob packages. The best is to have and use a CLI runner package that does glob expansion. Just like npm-run-all simplifies running multiple npm scripts ( I use npm-run-all in mostly all of my projects. It’s so productive)

Having Something similar for glob expansion-based execution is a great thing.

Npm scripts way

After checking existing packages. As detailed in the link above. foreach-cli is the only one in npm that offers such a thing and well (at least for now)

  • foreach-cli does support parallel execution. And does support exposing matching variables (placeholders) and their expansion so that you match files and you can then construct your command. Many commands require source and output. Many times you may need to change the output format ….
    foreach-cli seems a good option given the 4k download.
    ◌ It’s also very flexible with the path patterns it does expose. It works better than simple glob expansion.
    ◌ For terser it fully does the job well. And in general, it does the job for almost every case scenario. Mostly.

Example for terser

"scripts": {
"build": "npm-run-all -s build:*",
"build:ts": "tsc",
"build:terser": "npm-run-all -s build:terser:*",
"build:terser:clean": "rimraf dest/minified && mkdirp dest/minified",
"build:terser:terse": "foreach -g \"dest/js/**/*.js\" --concurrent -x \"mkdirp dest/minified/#{reldir} && terser #{path} -o dest/minified/#{reldir}/#{name}.min#{ext} -c \"ecma=6,toplevel\" -m \"toplevel,eval\" --mkdir\"",
}

Notes:

Quotes

  • within any npm script. If you you want to use double quotes , then you’ll have to escape them \” .
  • What about inside the already escaped quotes of foreach ?
"foreach -g \"...\" -x  \"cmd <what if here i need part of the command quotes>\""

◌ with for each. You can keep using \” and not double escape like \\\” , like with some systems.

  • What about mapping files in nested directories ? (Keeping the same structure ?)
    ◌ Normally foreach-cli had an answer for that. And that’s the reldir placeholder, that it offer. You can see me i used it below
terser #{path} -o dest/minified/#{reldir}/#{name}.min#{ext} -c \"ecma=6,toplevel\" -m \"toplevel,eval\" --mkdir\"
  • At this moment (2023–11–08). There is a bug, and it’s not working as is.
    ◌ I did create a PR fix for it. here you can check it
    ◌ Full issue details
    ◌ The PR is interesting in the part of No pure tests and how i fixed them. Check the PR description. That’s always an example of how tests can be wrong and unconsistent when they are unpure (wrongly done). And how people can miss seeing it.
    ◌ With this fix. reldir works fully as expected. You can keep track of that PR. To see when it get merged. I’ll leave a list of links at the end of this articles for you to go back to.
Illustration of reldir not working correctly as of 2023–11–08
  • terser and ensuring the directories exist
"build:terser:clean": "rimraf dest/minified && mkdirp dest/minified",
"build:terser:terse": "foreach -g \"dest/js/**/*.js\" --concurrent -x \"mkdirp dest/minified/#{reldir} && terser #{path} -o dest/minified/#{reldir}/#{name}.min#{ext} -c \"ecma=6,toplevel\" -m \"toplevel,eval\" --mkdir\"",

◌ You can see the usage of rimraf to clean all (not a necessary step. You would only care to do so. To make sure no old files get left when you change the source )
◌ And mkdirp to ensure the existance of every nested directory (the foreach command). This is a necessary step. Otherwise you will get errors.

Terser error directory not exist

◌ Most tools manage creating directories, terser is not one of them.

The programatic way

The other way, is to do it programmatically through a nodejs script. Where we would use a glob package (glob, fast-glob, …). We can have our flow. And we can be productive by having a fast accessible snippet. A base snippet for the core of glob looping and command execution, with full parallelism.

Example:

/**
* npm install fast-glob
*/
const { spawn } = require('child_process');
const fastGlob = require('fast-glob');
const path = require('path');
const fs = require('fs');
const fsp = fs.promises;

const rootDir = path.resolve(__dirname, '..');

/**
* Relative path to root
*/
function root(relPath) {
return path.resolve(rootDir, relPath);
}

/**
* Resolve alias
*/
function r(..._path) {
return path.resolve(..._path);
}

/**
* File selection
*/
(async () => {
const glob = './dest/js/**/*.js';
// const globIgnore = '';

const files = await fastGlob(root(glob));

files.forEach(async (file) => {
// Note: file is absolute (fastGlob default)
console.log(`file: ${file}`);
const fileParse = path.parse(file);
const { name, ext, dir } = fileParse;
const reldir = path.dirname(path.relative(root('dest/js'), file));

// console.log(`reldir: ${reldir}`);

const outFile = root(`dest/minified/${reldir}/${name}.min${ext}`);
console.log(`outFile: ${outFile}`);

// Pre-commands
await fsp.mkdir(path.dirname(outFile), { recursive: true });

// Commands
command = `npx terser ${file} -o ${outFile} -c "ecma=6,toplevel" -m "toplevel,eval" --mkdir`;

spawn(command, { stdio: 'inherit', shell: true });
});
})();
Vscode Snippet illustration
Snippet after being expanded

✨ Working with snippets

To work greatly with snippets, checkout the full deep productivity article i wrote about that

✨ Demonstration repo

In this repo you can check and test every piece. I did check and demonstrate all.

Related articles

--

--

Mohamed Lamine Allal
Mohamed Lamine Allal

Written by Mohamed Lamine Allal

Developper, Entrepreneur, CTO, Writer! A magic chaser! And A passionate thinker! Obsessed with Performance! And magic for life! And deep thinking!

No responses yet