Does package.json support compound variables? - npm

A project that respects the semver directory structure for build artefacts is beginning soon, and package.json or .nmprc would seem to be the right place to define this metadata. This is some preliminary code that demonstrates how the goal is intended to be achieved:
{
"name": "com.vendor.product"
, "version": "0.0.0"
, "directories": {
"build": "./out"
}
, "main": "./${npm_directories_build}/${npm_package_name}/${npm_package_version}/${npm_package_name}.js"
, "exports": "${npm_package_main}"
, "scripts": {
"echo": "echo\"${npm_package_exports}\""
}
}
I expected
npm run echo
to print the compound variable result to standard output,
./out/com.vendor.product/0.0.0/com.vendor.product.js
but instead, it prints the literal text
${npm_package_export}
I attempted to use array variables in .npmrc
outpath[]=./out
outpath[]=/${npm_package_name}
outpath[]=/${npm_package_version}
But
...
{
"echo": "echo \"${npm_config_outpath}\""
}
Simply prints an empty newline
It was expected that package.json supports compound variables, but this assumption is now in question. I have checked documentation, but either I am missing something or such is not defined. Long hand repetition of the same data is to be avoided (e.g. multiple references to package variables in order to make a single path). It is intended for package name and version to always dictate the location of the build files in a reliable and predictable manner.
If compound variables are not supported, could you clarify how .npmrc array variables actually work? Failing that, could you recommend an alternative method to achieve the same ends? Many thanks!
Searched documentation:
https://docs.npmjs.com/misc/config
https://docs.npmjs.com/files/npmrc
https://docs.npmjs.com/configuring-npm/npmrc.html
https://docs.npmjs.com/files/package.json#config
http://doc.codingdict.com/npm-ref/misc/config.html#config-settings
https://github.com/npm/ini

Short Answer:
"Does package.json support compound variables?"
Unfortunately no, not for the way you are wanting to use them. It only has package json vars which can be used in npm scripts only. For example on *Nix defining the echo script as:
"echo": "echo $npm_package_version"
or on Windows defining it as:
"echo": "echo %npm_package_version%"
will print the version e.g. 0.0.0.
Note: cross-env provides a solution for a single syntax that works cross-platform.
You certainly cannot include parameter substitution ${...} elsewhere in package.json fields except for the scripts section.
Additional info:
Regarding your subsequent comment:
How array values defined in .npmrc can be used in package.json
AFAIK I don't think you can. For example let's say we;
Save this contrived .npmrc in the root of the project directory.
.npmrc
quux[]="one"
quux[]="two"
quux[]="three"
foo=foobar
Then cd to the project directory and run the following command to print all environment variables:
npm run env
As you can see, the npm_config_foo=foobar environment variable has been added by npm. However for the quux array there is no npm_config_quux=[ ... ] environment variable added.
So, in npm scripts using package.json vars the following does work:
"echo": "echo $npm_config_foo"
However the following, (for referencing the array), does not - simply because it does not exist;
"echo": "echo $npm_config_quux"
The ini node.js package:
Maybe consider investigating the ini node.js package that npm utilizes for parsing .npmrc files. For example:
If you run the following command to install the package in your project:
npm i -D ini
Then define the npm echo script as per this:
"scripts": {
"echo": "node -e \"var fs = require('fs'), ini = require('ini'); var config = ini.parse(fs.readFileSync('./.npmrc', 'utf-8')); console.log(config.quux)\""
}
Note it uses the nodejs command line option -e to evaluate the JavaScript code. It essentially executes the following:
var fs = require('fs'),
ini = require('ini');
var config = ini.parse(fs.readFileSync('./.npmrc', 'utf-8'));
console.log(config.quux);
Then given the contrived .npmrc file that I mentioned previously when running:
npm run echo
it will print:
[ 'one', 'two', 'three' ]

Related

Is there a way to get the name of the npm script passed to the command specified by that script?

With npm or yarn, is it possible for the script specified by an npm script to know the name of the npm script itself? For example:
"scripts": {
"foo": "echo Original command: $0",
"bar": "echo Original command: $0"
}
I'd like the result of those two scripts to be something like:
Original command: yarn run foo
Original command: yarn run bar
But all I actually get is: Original command: /bin/sh.
And in case it makes a difference, it's just the name of the script I need, not the yarn run part, so output like Original command: foo would be fine.
NPM adds the npm_lifecycle_event environment variable. It's similar to package.json vars.
*Nix (Linux, macOS, ... )
On *nix platforms npm utilizes sh as the default shell for running npm scripts, therefore your scripts can be defined as:
"scripts": {
"foo": "echo The script run was: $npm_lifecycle_event",
"bar": "echo The script run was: $npm_lifecycle_event"
}
Note: The dollar prefix $ to reference the variable.
Windows:
On Windows npm utilizes cmd.exe as the default shell for running npm scripts, therefore your scripts can be defined as:
"scripts": {
"foo": "echo The script run was: %npm_lifecycle_event%",
"bar": "echo The script run was: %npm_lifecycle_event%"
}
Note: The leading and trailing percentage sign % used to reference the variable.
Cross-platform (Linux, macOS, Windows, ... )
For cross-platform you can either:
Utilize cross-var to enable a single syntax, i.e. using the dollar sign prefix $ as per the *nix syntax.
Or, utilize the node.js command line option -p to evaluate and print the result of the following inline JavaScript:
"scripts": {
"foo": "node -e \"console.log('The script run was:', process.env.npm_lifecycle_event)\"",
"bar": "node -e \"console.log('The script run was:', process.env.npm_lifecycle_event)\""
}
Note In this example we:
Access the npm_lifecycle_event environment variable using the node.js process.env property.
Utilize console.log (instead of echo) to print the result to stdout

Assign a random number (UUID) value to a environment variable in npm script

I'd like to add a UUID argument when calling my npm script. Each time the script is called, I'd like to generate a new number. It should look like:
"build": "cross-env UUID=unique_number ng build"
The only thing I need is generating the unique_number here. I tried to use the uuid package but I don't know how to fire it in the script and pass the number as the argument.
tl;dr As you're question shows the use of cross-var I've assumed a cross-platform solution is required. In which case refer to the Solution A. However refer to either Solution B or C if my assumption is incorrect.
Solution A: Cross Platform (Windows/Linux/macOS...)
Fo a cross platform solution, (i.e. one that runs successfully on Windows, Linux, and macOS...), you'll need to utilize nodejs to achieve your requirement. There are a couple of different ways to approach this as described in the following two sub-sections titled:
Using an external nodejs (.js) file
Inlining your JavaScript in package.json.
Note both approaches are effectively the same
Using an external nodejs (.js) file
Create a nodejs utility script. Let's name the file run-ng-build.js and save it in the root of your project directory, i.e. in the same directory where package.json currently resides:
run-ng-build.js
const uuid = require('uuid/v1');
const execSync = require('child_process').execSync;
process.env.UUID = uuid();
execSync('ng build', { stdio: [0, 1, 2]} );
In the scripts section of your package.json replace your current build script with the following:
package.json
"scripts": {
"build": "node run-ng-build"
}
Explanation:
In run-ng-build.js we require the uuid package and the nodejs built-in execSync().
To create the environment variable named UUID we utilize the nodejs builtin process.env, and assign a uuid value to it by invoking uuid().
We then invoke the ng build command using execSync.
The options.stdio option configures the pipes between the parent and child process - [0, 1, 2] effectively inherit's stdin, stdout, and stderr.
Inlining your JavaScript in package.json.
Alternatively, you can inline your nodejs/JavaScript code in the scripts section of your package.json.
In the scripts section of your package.json replace your current build script with the following instead:
package.json
"scripts": {
"build": "node -e \"process.env.UUID = require('uuid/v1')(); require('child_process').execSync('ng build', { stdio: [0, 1, 2]} );\""
}
Explanation:
This is effectively the same as the aforementioned solution that utilized a separate .js file, however the use of a separate nodejs script/file is now redundant.
The nodejs command line option -e is utilized to evaluate the inline JavaScript.
Important The cross-env package is redundant utilizing either of the two aforementioned solutions. To uninstall it run: npm un -D cross-env via your CLI.
Solution B: *Nix platforms only (Linux/MacOS...)
For *nix platforms only it becomes very terse, you can just define your build script in package.json as follows:
package.json
"scripts": {
"build": "cross-env UUID=$(uuid) ng build"
}
This utilizes a Bash feature known as command substitution, i.e. $(uuid). However, if *nix is the only platform you need to support, then cross-env is really not necessary. Use the built-in export feature instead. For instance:
package.json
"scripts": {
"build": "export UUID=$(uuid) && ng build"
}
Solution C: Windows only (cmd.exe)
On Windows (only) running via Command Prompt or PowerShell you can do the following:
package.json
"scripts": {
"build": "FOR /F %U IN ('uuid') DO cross-env UUID=%~U node -e \"process.env.UUID = require('uuid/v1')(); require('child_process').execSync('ng buuld', { stdio: [0, 1, 2] });\""
}
This is similar to the first example shown in Solution B however command substitution is achieved (very) differently in cmd.exe. See this answer for further explanation.

Pass parameter to npm script and then gulp-task [duplicate]

I have package.json scripts with the following structure:
"scripts": {
"watch:build": "tsc --watch",
"watch:server": "nodemon ./src/app.js --watch 'app'",
"build": "tsc && gulp do_something",
"start": "npm-run-all clean build --parallel watch:build",
"watch:server --print-label"
}
I would like to start the application as npm run start with_argument
and pass it to build script, to perform actions in the gulp task based on that argument.
I read a lot of tutorial and how to articles, but without result. It is possible somehow to pass argument from one script to another(which starts a gulp task).
Thanks in advance!
npm-run-all provides a its own custom mechanism for handling arguments by utilizing placeholders in npm-scripts, as stated in the Argument Placeholders section of its documentation found here.
npm-script:
Given your current npm-script named start you'll need to redefine it as follows:
"scripts": {
...
"start": "npm-run-all clean \"build -- {#}\" --parallel watch:build --"
...
}
Notes:
-- {#} must be added after build.1
build -- {#} must be wrapped in escaped double quotes \"...\"
-- must also be added after last script invocation, namely: watch:build
gulpfile.js
To obtain the arguments passed via the CLI inside your gulpfile.js you''ll need to utilize nodes process.argv
For the purpose of demonstration lets say our gulpfile.js is as follows:
var gulp = require('gulp');
var args = process.argv.splice(3, process.argv.length - 3);
gulp.task('doSomething', function() {
// For testing purposes...
if (args.indexOf('--foo') > -1) {
console.log('--foo was passed via the CLI.')
}
if (args.indexOf('--quux') > -1) {
console.log('--quux was passed via the CLI.')
}
});
Notes:
The first three items in nodes process.argv are:
The path to the executable running the JavaScript file.
The path of the JavaScript file being executed.
The name of the gulp task, i.e. doSomething
However, we're only interested in the elements from the fourth item in the array onwards - as these will be the arguments passed via the CLI. The line which reads:
var args = process.argv.splice(3, process.argv.length - 3);
creates an args variable and assigns an Array containing each argument passed via the CLI, i.e. we omit the first three aforementioned items in point 1 above using the Arrays splice() method.
Running your start script:
You invoke your start script via your CLI as follows:
$ npm start -- --foo --quux
Note You must provide the -- which precedes npm start before providing your own arguments.
Output:
Using the contrived gulpfile.js above, in combination with your current scripts defined in your package.json, and of course the necessary changes made to your start script. When you run:
$ npm start -- --foo --quux
you will see the following printed to the console:
--foo was passed via the CLI.
--quux was passed via the CLI.
Running:
$ npm start -- --quux
you will see just the following printed to the console:
--quux was passed via the CLI.
And of course, running:
$ npm start
Does not print either of the messages defined in gulpfile.js.
Footnotes:
1 -- {#} can be replaced with -- {1} if you only intend to pass one argument. However, -- {#} handles multiple arguments, so it's fine to use it for one argument too.

Cross-platform way to pass environment variables as arguments to npm scripts

From both Windows or Linux, I want a way to pass args to a npm script, but have them be injected as environment variables
From the command line, I'd start my project in this fashion:
npm run start -- --env=dev --host=localhost --port=1234
To consume my cli args & inject them as env variables regardless of platform, I used the cross-env npm package :
package.json
"scripts": {
"start": "cross-env env=%env% host=%host% port=%port% my-app"
},
I understand the above is invalid syntax, but can that start script somehow consume the args I pass instead of forwarding them on to my-app?
Unfortunately npm does not, nor intends to, provide a builtin feature which allows arguments to be passed to the middle of a npm script (as stated here). Arguments can only be passed to the end of a script.
For Linux and macOS you can utilize bash functions in npm-scripts to pass arguments to the middle of a script, as per my answer here. However Windows will choke at such a solution.
As cross-platform compatibility is a requirement, consider moving the logic currently in your start script into a separate nodejs utility script. The nodejs script can then be invoked via the npm-script named start.
The following describes how to achieve your requirement in a cross-platform compatible way.
1. Custom nodejs utility script.
Create a nodejs script as follows. Let's name the script start.js and save it in the root of your project directory, i.e. at the same level where your package.json file currently resides.
const execSync = require('child_process').execSync;
const args = process.argv.splice(2, process.argv.length - 2)
.map(arg => arg.replace(/^--/, ''));
execSync(`cross-env ${args.join(' ')} my-app`, {stdio:[0, 1, 2]});
Explanation:
In the first line we require nodes builtin execSync(). We'll utilize this to run cross-env and set the environment variables.
Nodes builtin process.argv obtains the arguments passed via the command-line. The first two items in nodes process.argv are:
The path to the executable running the JavaScript file.
The path of the JavaScript file being executed.
However, we're only interested in the elements from the third item in the array onwards - as these will be your arguments passed via the CLI. These lines which read;
const args = process.argv.splice(2, process.argv.length - 2)
.map(arg => arg.replace(/^--/, ''));
create an args variable and assigns an Array containing each argument passed via the CLI. The first two aforementioned items in point 2 are omitted from the array using the splice() method. In the map() method we remove the -- prefix from each argument.
The last line reading:
execSync(`cross-env ${args.join(' ')} my-app`, {stdio:[0, 1, 2]});
invokes cross-env and places the arguments as a string using Template Literals and the Array join() method. The stdio part configures the pipes for stdin, stdout, stderr in the child process.
Note: If your targeting older versions of node that don't support Template Literals then you can substitute this line with the following instead. This handles string concatenation using the + operator:
execSync('cross-env ' + args.join(' ') + ' my-app', {stdio:[0, 1, 2]});
Similarly, if ES6 arrow functions aren't supported change the map() to use standard functions. For instance:
.map(function(arg) {
return arg.replace(/^--/, '');
});
2. package.json script.
Redefine your start script in package.json as follows:
"scripts": {
"start": "node start"
},
Here we are asking node to invoke the start.js script.
Note If you prefer to save the start.js file in a directory location which is different to the aforementioned root of your project directory, then you'll need to define the path to start.js as necessary. The path should be relative to package.json. For instance:
"scripts": {
"start": "node ./the/path/to/start"
},
3. Running the npm-script.
The npm start script can be invoked via your CLI by running:
$ npm start -- --env=dev --host=localhost --port=1234
The run part, i.e. npm run start ... is not required when invoking the npm's start script.

npm run: how to pass parameter and replace placeholder by param in script call

I want to to define a scripts entry in my package.json to simplify building for several environments.
In the script execution I need to replace $1 (or whatever syntax I need for a placeholder) by a parameter I would pass to npm run build-script like for example --env=prod or even simpler, --prod. How can I do that? Other questions and answers I found here didn't help me solve the problem.
"scripts": {
"build-for": "ng build --output-path=../../web/admin-v2 --env=$1 --base-href=\"/admin-v2/\""
}
I often resort to creating a utility node script for this kind of scenario and invoke it via the scripts section of package.json.
build-for.js
var nodeCLI = require('shelljs-nodecli');
var env = '--env=foo'; // <-- Default env flag/value when no arg provided.
if (process.argv.indexOf('--prod') !== -1) {
env = '--env=prod';
}
// Check for other possible env values
if (process.argv.indexOf('--quux') !== -1) {
env = '--env=quux';
}
// Run the ng build command
nodeCLI.exec('ng', 'build', '--output-path=../../web/admin-v2', env, '--base-href=\"/admin-v2/\"');
build-for.js utilizes nodes process.argv to ascertain the argument/flag passed via the CLI and then invokes the ng command (the one currently defined in your package.json) using shelljs-nodecli.
npm i -D shelljs-nodecli
Lets assume that build-for.js is saved to a hidden folder named .scripts in your projects root directory; then your scripts section of package.json will be defined as follows:
package.json
{
...
"scripts": {
"build-for": "node ./.scripts/build-for"
},
...
}
Running the script
Invoke the npm script by running:
npm run build-for -- --prod
Note the special -- before the argument --prod must be included as explained here
As of npm#2.0.0, you can use custom arguments when executing scripts. The special option -- is used by getopt to delimit the end of the options. npm will pass all the arguments after the -- directly to your script:
Given the logic curently in the build-for.js - when no arguments are passed, for example:
npm run build-for
...the env flag will be set to --env=foo
Running the following:
npm run build-for -- --quux
...will result in the env flag will be set to --env=quux
Caveat
I haven't fully tested build-for.js, so you may find that you don't need to escape the double-quotes in this part '--base-href=\"/admin-v2/\"' of the following command (nodeCLI may handle that for you.):
// Run the ng build command
nodeCLI.exec('ng', 'build', '--output-path=../../web/admin-v2', env, '--base-href=\"/admin-v2/\"');
Not exactly what you're looking for but you can use environment variables & provide them inline:
package.json script:
"<script>": "echo ${ENV1} ${ENV2}"
run like so:
ENV1=a ENV2=b npm run <script>
$ npm run <script>
a b