Run `npx` from PHP app

Hey :slight_smile:

I need to execute some npx commands from a PHP (Symfony) app.

I’m already following this guide to add node/nvm to my setup (and to build some assets at the build hook).

I tried running npx commands but I get the following error: npx: not found.
So I added this step to the build hook: npm install -g npx but then got this error when building:

W: npm ERR! code EEXIST
W: npm ERR! syscall symlink
W: npm ERR! path ../lib/node_modules/npx/index.js
W: npm ERR! dest /app/.nvm/versions/node/v12.16.1/bin/npx
W: npm ERR! errno -17
W: npm ERR! EEXIST: file already exists, symlink '../lib/node_modules/npx/index.js' -> '/app/.nvm/versions/node/v12.16.1/bin/npx'
W: npm ERR! File exists: /app/.nvm/versions/node/v12.16.1/bin/npx
W: npm ERR! Remove the existing file and try again, or run npm
W: npm ERR! with --force to overwrite files recklessly.
W:
W: npm ERR! A complete log of this run can be found in:
W: npm ERR! /mnt/cache/npm/_logs/2020-10-13T07_36_03_432Z-debug.log
W: 0

E: Error building project: Step failed with status code 239.
``
E: Error: Unable to build application, aborting.

So it looks like npx is already here, but I can’t seem to be able to use it.
Anyone can help me with that?

Thanks!

Hey!

I haven’t been able to replicate your issue.
Could you tell me what version of PHP you’re using and show me your build hook?

Thanks!

Sure, here’s a portion of my YAML:

# The name of this app. Must be unique within a project.
name: app

# The type of the application to build.
type: 'php:7.4'
build:
    flavor: composer

variables:
    php:
        "date.timezone": "Europe/Paris"
        "opcache.preload": "src/.preload.php"
        "opcache.memory_consumption": 256
        "opcache.max_accelerated_files": 20000
        "opcache.validate_timestamps": 0
        realpath_cache_size: 4096K
        realpath_cache_ttl: 600
#    env:
        # Tell Symfony to always install in production-mode.
#        APP_ENV: 'prod'
#        APP_DEBUG: 0

# The hooks that will be performed when the package is deployed.
hooks:
    build: |
        set -e
        bin/console assets:install --no-debug
        bin/console cache:clear
        curl -sS https://platform.sh/cli/installer | php
        unset NPM_CONFIG_PREFIX
        curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.35.2/install.sh | dash
        export NVM_DIR="$PLATFORM_APP_DIR/.nvm"
        [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
        nvm current
        nvm install 12.16.1
        npm install
        npm run quiet-build
    deploy: |
        set -e
        bin/console assets:install --symlink --relative public
        bin/console cache:clear
        bin/console doctrine:migrations:migrate --allow-no-migration --no-interaction
        unset NPM_CONFIG_PREFIX
        export NVM_DIR="$PLATFORM_APP_DIR/.nvm"
        [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
        nvm use 12.16.1

I still haven’t been able to replicate your npx error using a similar build hook.

If we follow what happens during your build hook once you’ve installed nvm, we have:

  • nvm current: should display “system” at this point. The node system version is 6.17.1 which comes with npm 3.10.10. npx is not available with this npm version, as it only appeared with version 5. You cannot use npx using the node version that comes by default.
  • nvm install 12.16.1: you will get the specified node version, which comes with npm 6.13.4 so npx will now be available. Trying to install npx after this step will get you into a EEXIST error as npx can already be found in the bin folder of your current node version.
  • npm run quiet-build: the quiet-build npm script should be able to make use of npx as well as everything that lives under node_modules/.bin. When running an npm script, the local node_modules/.bin directory and the npm global’s bin directory (in your case /app/.nvm/versions/node/v12.16.1/bin) are automatically added to your PATH by npm.

If this clarification doesn’t help, let me know at which step you are trying to use npx and how you invoke the command (e.g. in an npm script or directly in your build hook) so I can try to mimic your situation as good as possible.

Hey Rudy,
I agree with the steps and how you describe them:

  • nvm current displays system
  • nvm install 12.16.1 does its job
  • npm run quiet-build, too

But what I’m looking for is the ability to run npx afterwards, when the app is built and deployed.

When connecting with platform ssh and running npx -v I get -bash: npx: command not found (which makes sense because node -v displays v6.17.1). Is that normal?

How come Node “switched back” to its system version?
How can I use the node/npm/npx tools installed by nvm?

But! I now understand why npx can’t run anything, that’s because the filesystem is read-only so it can’t download packages, right? But I could theorically call it with --no-install if the packages are listed in packages.json and installed, right?

Current fix (but looks like a hack to me):

Replace npx --no-install calls with

/app/.nvm/versions/node/v12.16.1/bin/node /app/.nvm/versions/node/v12.16.1/bin/npx --no-install

Oh, if you want to be able to run npx via SSH directly in your container, then it’s a different story!

When connecting with platform ssh and running npx -v I get -bash: npx: command not found (which makes sense because node -v displays v6.17.1). Is that normal?
How come Node “switched back” to its system version?

This is normal. The different build hooks and your shell when you SSH into your container are different sessions. Therefore, environment variables set in your hooks won’t affect your environment when using SSH. Everything the nvm.sh script does has no effect anywhere else than in the hook it is being executed. After that, your environment is unaware of nvm's presence. Since node 12 has been installed via nvm, the only available version to your system at this point is the default one: 6.17.1.

How can I use the node/npm/npx tools installed by nvm?

Everything is there, but you need to set the environment variables. To do so, just add these lines in your .environment file:

# This is necessary for nvm to work.
unset NPM_CONFIG_PREFIX
# Disable npm update notifier; being a read only system it will probably annoy you.
export NO_UPDATE_NOTIFIER=1
# This loads nvm for general usage.
export NVM_DIR="$PLATFORM_APP_DIR/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

When connecting with plaftorm ssh, you should now be able to be in a situation where:

$ command -v nvm
nvm
$ node -v
12.16.1
$ which npx
/app/.nvm/versions/node/v12.16.1/bin/npx

Note: you will see a message saying npm update check failed when getting in your shell. This is due to an access right limitation when nvm wants to check for updates. You can ignore this. Just know that you will be running npm and npx 6.13.4 at this point (as opposed to 6.17.1 (as of today) in your hooks.)

I now understand why npx can’t run anything, that’s because the filesystem is read-only so it can’t download packages, right? But I could theorically call it with --no-install if the packages are listed in packages.json and installed, right?

Yes, the filesystem is read-only so this will limit what you can do with npx but you don’t have to pass the --no-install option. npx will check for the command you want to execute in the local and global bin directories before trying to download anything so any already installed packages can be run by doing npx [package] directly. Also, even with the --no-install option, trying to run a non-installed package will result in a EROFS error as npx is going to try to perform write operations anyway.