Terser: run on multiple files using globs and unblock yourself from the issues
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 toterser
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
- Build your base to handle such an abstract problem for
terser
and for any other tool that is similar
◌ Npm scripts — running commands with globs expansion for each file
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.
◌ Forterser
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 ?)
◌ Normallyforeach-cli
had an answer for that. And that’s thereldir
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.
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.
◌ 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 });
});
})();
✨ 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.