Use node to enhance an npm script
Here's a way that I like to enhance a minimal npm script by using node.
Given this folder structure:
..
└── projects
├── Aces
│ └── translations
│ └── en-US.json
├── Clubs
│ └── translations
│ └── en-US.json
├── Hearts
│ └── translations
│ └── en-US.json
└── Spades
└── translations
└── en-US.json
I've got an npm script that uses the formatjs cli to extract translations for each of the folders in the projects folder. Here it is, from package.json:
{
"scripts": {
"i18n:extract": "formatjs extract"
}
}
Now, to run this, numerous flags & options have to be provided. Here's how the extract script is executed:
npm run i18n:extract -- 'projects/Hearts/**/*.ts*' --out-file projects/Hearts/translations/en-US.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'
I'd like to remove the hassle of:
- remembering the options/flags order
- having to pass specific folder names
- remembering the
id-interpolation-patternflag and value
Now, let's take a look at how we can improve this by prompting to ask which project we want to run the script for.
First, we'll update the script in package.json to use node to execute a custom file:
{
"scripts": {
"i18n:extract": "node i18n-extract.js"
}
}
and install 2 devDependencies:
npm i -D execa prompts
Getting a list of project names #
Now, we need a way to get a list of the projects in the projects/ directory. In a new file at the root of the project (let's call it utilities.js):
const fs = require('fs');
const path = require('path');
const projectsPath = __dirname + '/projects';
module.exports = {
projectNames: fs
.readdirSync(projectsPath)
.filter((fileOrDir) =>
fs.statSync(path.join(projectsPath, fileOrDir)).isDirectory()
),
};
This script exports an object with a key of projectNames, which is a node fs (filesystem) process that crawls the projectsPath directory & filters out everything that is not a directory - leaving us with an array of strings that are the names of the folders in the projects directory.
i18n-extract.js #
Within a new i18n-extract.js file, we can use our new utilities file to prompt the user for the project they want to work with:
const prompts = require('prompts');
const { projectNames } = require('./utilities');
const projectPrompt = prompts([
{
type: 'autocomplete',
name: 'project',
message: 'Choose project:',
choices: projectNames.map((choice) => ({ title: choice, value: choice })),
},
]);
(async () => {
const { project } = await projectPrompt;
console.log(`Selected project: ${project}`);
})();
At this point, the selected project name is available in the project constant. Here's what we've got:

Now, let's replace the hand typed formatjs extract bash command:
(async () => {
const { project } = await projectPrompt;
try {
await execa( // 1
'formatjs', // 2
[
'extract', // 3
`'projects/${project}/**/*.ts*' --out-file projects/${project}/translations/en-US.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'`, // 4
],
{ shell: true } // 5
).stdout.pipe(process.stdout); // 6
} catch (error) {
console.error('error: ', error);
}
})();
The actual script itself isn't super relevant for this post, but here are some notes:
execais used to make executing in a child process nice & easyformatjsis the binary to execute.execafinds the file automatically (it lives in./node_modules/.bin)- The values in the array are arguments to pass to the command in #2
- Interpolates the
projectvariable, using the value the user chose in the prompt - We need to pass
shell: trueas an option, otherwise the node script won't spawn the correct process stdoutis piped through, so we can see any status codes or messages
That's pretty much it! Hope it helps. Here's the final i18n-extract.js file, altogether:
const prompts = require('prompts');
const { projectNames } = require('./utilities');
const projectPrompt = prompts([
{
type: 'autocomplete',
name: 'project',
message: 'Choose project:',
choices: projectNames.map((choice) => ({ title: choice, value: choice })),
},
]);
(async () => {
const { project } = await projectPrompt;
try {
await execa(
'formatjs',
[
'extract',
`'projects/${project}/**/*.ts*' --out-file projects/${project}/translations/en-US.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'`,
],
{ shell: true }
).stdout.pipe(process.stdout);
} catch (error) {
console.error('error: ', error);
}
})();
- Previous: Stimulus Toggle Utility
- Next: Seeding database with cy.exec