How can a postinstall script be restricted to run only on macOS?
I have a shell script inside my React native library and it needs to be started when the npm install has completed.
This works great with postinstall but the problem is that Windows can't execute the shell script.
"scripts": {
"postinstall": "./iospatch.sh"
},
I need a way to limit that, to only run on macOS.
I tried with this library but it didn't work for my case
https://www.npmjs.com/package/cross-os
For cross-platform consider redefining your npm script as follows. This ensures that the shell script (.sh) is run only, via the postinstall npm script, when installing your package on macOS.
"scripts": {
"postinstall": "node -e \"process.platform === 'darwin' && require('child_process').spawn('sh', ['./iospatch.sh'], { stdio: 'inherit'})\""
}
Explanation:
The node -e \"...\" part utilizes the Node.js command line option -e to evaluate the inline JavaScript as follows:
process.platform === 'darwin' utilizes the process.platform property to identify the operating system platform. If it equals darwin then it's macOS.
The last part on the right-hand side of the && operator;
require('child_process').spawn('sh', ['./iospatch.sh'], { stdio: 'inherit'})
is executed only if the expression on the left-hand side of the && operator is true, i.e. it only runs if the platform is macOS.
That part of the code essentially utilizes the child_process.spawn() method to invoke your .sh file. The stdio option is set to inherit to configure the pipes for stdin, stdout, stderr in the child process.
Also note the command passed to child_process.spawn() is sh and the argument is the file path to the shell script, i.e. ['./iospatch.sh']. We do this to avoid having to set file permissions on macOS so that it can execute iospatch.sh.
Related
I am working on a project with a windows machine, and I have a few npm scripts like this:
"start" : "./foo/bar"
When I try to run npm run start I get this error:
.\foo\bar is not recognized as an internal or external command,
operable program or batch file.
I noticed the forward slash has been flipped to a backslash for windows, but also if I run this command on its own the bash terminal will interrupt them as 'escapes' and return:
bash: .foobar: command not found
The file runs ok in the terminal if I use ./foo/bar or .\\foo\\bar but not if I use these in the npm script.
What can I do to have this working in Windows? Furthermore is there a way to write it to be compatible for Win/Mac/Linux?
It works when you first do a cd with normal slashes (npm/nodejs seems to resolve this depending on the OS), then you only have to specify the file.
"scripts": {
"not-working": "scripts/another-folder/foo.cmd",
"working": "cd scripts/another-folder && foo.cmd"
},
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
In an NPM project, I'd like to have a commit for each build version. This will allow me to go back to the current build version, fix a bug, without having to go through all the QA of a new version.
We can commit using npm scripts like this (see this answer):
package.json
"scripts": {
"git": "git add . && git commit -m",
}
Then invoke the script by running:
npm run git -- "Message of the commit"
I'd like to automate it to run after npm run build. For this purpose we can create a new command.
package.json
"scripts": {
"buildAndCommit": "npm run build && git add . && git commit -m",
}
This could be run using npm run buildAndCommit -- "commit for a new build"
The only thing left is that I'd like to identify this commit as one that could be linked to a commit. Is it possible to start the message automatically with "BUILD -" and to add to that the unique message which is passed in the command line? Something like:
package.json
"scripts": {
"buildAndCommit": "npm run build && git add . && git commit -'Build' + $uniqueMessageFromTheCommandLine`",
}
If it is not possible to template the string in package.json, how could I achieve it using a command line script? (Powershell is my command line tool).
Running on *nix platforms
On a *nix platform npm utilizes sh by default to execute the npm script(s). In this scenario you can simply use a shell function and reference the git message argument passed via the CLI using the $1 positional parameter.
Your npm script would be redefined like this:
"scripts": {
"build": "...",
"buildAndCommit": "func() { npm run build && git add . && git commit -m \"BUILD - $1\"; }; func"
}
Cross platform
Unfortunately, via Windows Powershell the solution is not quite as simple and terse.
When using Powershell npm utilizes cmd by default to execute npm script(s). Likewise npm utilizes cmd by default via other Windows consoles too, such as Command Prompt.
One way to achieve your requirement is to invoke a node.js via your npm script. The following provides two different methods that are essentially the same. Either will run successfully cross-platform (in your case via Powershell).
Method A - Using a separate node.js script
Create the following node.js script. Let's name the file script.js and save it in the root of the project directory, i.e. in the same directory where package.json resides.
script.js
const execSync = require('child_process').execSync;
const mssg = 'BUILD - ' + process.argv[2];
execSync('npm run build && git add . && git commit -m \"' + mssg + '\"', { stdio:[0, 1, 2] });
Explanation
The node.js builtin process.argv captures the argument at index two, i.e. the git commit message, that was provided via the CLI. The git commit message is concatenated with with the substring BUILD - to form the desired commit message. The resultant string is assigned to the variable mssg.
We then utilize the node.js builtin execSync() to execute your given npm script. As you can see, the value of the mssg variable is used as the git commit message.
The stdio option is utilized to ensure the correct configuration of the pipes, i.e. stdin, stdout, 'stderr', are established between the parent and child process.
Define your npm script named buildAndCommit as follows:
package.json
"scripts": {
"build": "...",
"buildAndCommit": "node script"
}
Above node invokes script.js.
Method B - Inline the node.js script in npm script
Alternatively, the aforementioned node.js script (i.e. script.js) can be provided inline in your npm script instead - therefore negating the use of a separate .js file.
package.json
"scripts": {
"build": "...",
"buildAndCommit": "node -e \"const mssg = 'BUILD - ' + process.argv[1]; require('child_process').execSync('npm run build && git add . && git commit -m \\\"' + mssg + '\\\"', { stdio:[0, 1, 2] })\""
}
This utilizes the same code from Method A albeit it slightly refactored. The notable differences are:
The nodejs command line option -e is utilized to evaluate the inline JavaScript.
process.argv this time will capture the argument, i.e. the git commit message, at index one in the array of arguments.
Additional escaping of the double quotes is necessary, i.e. \\\"
Running the npm script
Using either Method A or Method B run the command via your CLI as desired: For instance:
$ npm run buildAndCommit -- "commit for a new build"
This will produce the following git commit message:
BUILD - commit for a new build
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.
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.