21

I have a script in my package.json:

{
  "scripts": {
    "start": "source run-nvm.sh && ..."
  }
}

But running source run-nvm.sh && ... is different to yarn start (or npm run start).

Why? It creates a subshell. So I can't change environment of the original shell, I can't export constants for it or manipulate the state of nvm (I can't change node version for the parent shell)

So the real question

Can I execute yarn/npm script by not creating a subshell? (and use the current shell)

OR

How to source shell script with npm scripts?

Eventually, you may try to change the discourse by asking: "why don't you just source run-nvm.sh && yarn start" but I don't want to just add some custom scripts and complexity, I want it to be automatically executed on yarn start / npm start (to change node version automatically)

And the real problem

It works currently (the script changes the version of node and runs the app) but since it's a subshell it does not save the state of nvm. So on every yarn start it's using default version initially, then changes the version, then starts the app, so it adds ~3-4 seconds for the yarn start command for the version change. While it shouldn't set the version every time, but should set it just once, for the first time.

Jerry Green
  • 1,244
  • 1
  • 15
  • 25
  • You can source your environment and run command in the same start syntax, "start": "source run-nvm.sh; cmd to run ; second cmd ; etc" the other commands will be run in the same shell. or you can source the environment before running yarn as you say. I think no other option except to set your environment variables yourself without sourcing an external command. – GaryB Aug 11 '20 at 02:57
  • 1
    @GaryB, yeah, I suppose the closest solution here is to use something other than npm/yarn, which will behave slightly different for npm scripts, and won’t create a subshell. It is a bit dangerous though, and will require trust from all libs under all the scripts. I initially got here because I wanted to use `nvm use` automatically. I ended up using a plugin for vscode for nvm, so it simply runs `nvm use` in my project folder in built-in terminal, so whatever project is opened - nvm switches to proper nodejs automatically. Not an ideal solution, because it’s only applied to vscode, but alright – Jerry Green Aug 12 '20 at 03:52
  • I know it was explicitly not asked for, however, bash solution: `[ "$(node -v)" != "v10.24.1" ] && source run-nvm.sh && yarn start || yarn start` – Michael Dimmitt Jun 06 '21 at 17:28
  • you could put all of this in a bash script. start.sh and run it with `bash ./start`, also you could grep from package.json to get the node version. Instead of hardcoding like I did. – Michael Dimmitt Jun 06 '21 at 17:29

3 Answers3

8

Total guess, but try

{
 "scripts": {
    "start": "bash -c 'source run-nvm.sh && ...'"
  }
}
Michael Dimmitt
  • 826
  • 10
  • 23
  • 1
    The problem that npm and yarn create a subshell on their own level, so no, - whatever command you use in "scripts", it’s wrong answer. I think if it’s not just some historical reason and “it just happen so” (which is quite possible), then it should be some security measure, because your scripts will gain access to all your env variables, not just some default ones, - this will give npm scripts access to your email for example (if you ever used npm login, to publish packages), because npm stores such information there (idk why), and there’s more other quite sensible info. – Jerry Green Aug 24 '20 at 14:04
4

I faced this problem today and I found a simple solution that works for me.

  1. Create a env file with the vars

    # cat > .env << EOF
    PORT=8080
    DB_HOST=my-db.host
    DB_PORT=3306
    DB_USER=mysql
    DB_PASS=123456
    EOF
    
  2. Create a new entry on the package.json file

    {
      [...]
      "start": "export $(cat .env | egrep -v '#|^$' | xargs) && node production-server/server.js",
      [...]
    }
    
  3. Start the app as usual

    # npm run start
    
    > myapp@1.0.2 start /usr/local/myapp
    > export $(cat .env | egrep -v '#|^$' | xargs) && 
    node production-server/server.js
    
    > Ready on http://localhost:8080
    

Thats all.

If you want to know what export $(cat .env | egrep -v '#|^$' | xargs) does, keep reading.

export $(cat .env | egrep -v '#|^$' | xargs)
   |      |           |                 |
   |      |           |                 transform the output in "PORT=8080 DB_HOST=my-db.host DB_PORT=3306 DB_USER=mysql DB_PASS=123456"
   |      |           |
   |      |           filter lines starting with comment or blank line
   |      |
   |      cat the .env file
   |
   save the env on subshell before start the node
Charles Santos
  • 739
  • 9
  • 12
1

Spotted your comment about using a vscode plugin If all your looking for is to change your nvm version for all terminals when the version differes on one of your projects then check out my overkill answer.

I will probably put all of the logic in a bash script and just share the link.

Usually version of node is stored in the package.json and causing an incorrect version of node or npm to cause a failure.

// package.json

 "engines": {
    "node": ">=10.0.0"
  },
  "scripts": {

Usually version logic is in bashrc sourced by the shell in the $HOME/.bashrc file. when new window windows are created they re-trigger nvm use.

Two possibilities:
both use script: script-to-replace-nvm-version.sh
# Which replaces in bashrc the "nvm use " line with "nvm use $correctProjectVersion"

  1. Use start script in package json to run script-to-replace-nvm-version

  2. Use PROMPT_COMMAND to run script-to-replace-nvm-version:

a check is used here to make sure we are on an npm project.

be careful with PROMPT_COMMAND as it will run every time before the ps1 renders.

 PROMPT_COMMAND="[ -f ./package.json ] && $HOME/.scriptsourcedbybashrc.sh"

Script Summary: Nvm is usually sourced with the particular version in $HOME/.bashrc. Therefore we could the npm package rewrite the line containing nvm use ... and update the version. We could even prompt the user with do you wish to update?

check node version from package.json

projectVersion=$(cat ./package.json | grep 'node":' | tr -dc '0-9.')
echo $projectVersion

check node version from $HOME/.bashrc

nodeVersion=$(cat $HOME/.bashrc | grep 'nvm use' | tr -dc '0-9.')
echo $nodeVersion

check if node and package.json versions differ

# bash script, to be sourced by npm run start;

# WIP, needs a few more error checks
# such as, nvm used in more than one place in your bashrc.
if
  versions_differ_tell_and_prompt
fi
versions_differ_tell_and_prompt() {
  echo "nvm versions differ: ";
  found in project: $projectVersion;
  found in '$HOME/.bashrc': $nodeVersion;
  echo ;
  echo "ignore with any keypress or [uU] to update";
  old_stty_cfg=$(stty -g);
  stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg;
}
if echo "$answer" | grep -iq "^u" ;then
    update_file 'nvm use' "nvm use $projectVersion" && source $HOME.bashrc;
else
    echo "No changes made";
fi
update_file() {
  searchLine=$1
  replacementLine=$2
  lineNumber=$(cat $HOME/.bashrc | grep -n $searchLine | cut -f1 -d:)
  cp "$HOME/.bashrc" "$HOME/.bashrc.backup"
  sed -i "$lineNumbers/.*/$replacementLine/" "$HOME/.bashrc"
  source "$HOME/.bashrc"
}
unset searchLine
unset replacementLine
unset answer
unset old_stty_cfg
unset nodeVersion
unset projectVersion

Some other helpful links: https://github.com/md-command-line/ERRORSCREAM/issues/2

took most of the logic from here: https://github.com/MichaelDimmitt/git_check_computer/blob/master/git_check_computer.sh

correct the yarn start command to run the correct yarn or npm based on the lockfile found. https://github.com/MichaelDimmitt/know-your-package-manager

Michael Dimmitt
  • 826
  • 10
  • 23
  • Wow... I got the idea behind `PROMPT_COMMAND`, which is quite interesting concept even though very dangerous. It would be much cooler if yarn/npm itself come with nvm included or at least they tried to propagate commands to nvm, and upon seeing some newer version, they'd installed/used proper Node version automatically via nvm :D – Jerry Green Nov 07 '20 at 11:46
  • Note: PROMPT_COMMAND will only work for the current terminal so I was recommending putting that in the .bashrc – Michael Dimmitt Nov 07 '20 at 18:10