When using npm Enterprise, we sometimes encounter public packages in our private registry that need to fetch resources from the public internet when being installed by a client via npm install
.
Unfortunately, this poses a problem for developers who work in an environment with limited or no access to the public internet.
In this article, we're going to look at some of the more common types of problems in this area and talk about ways we can work around them.
Note that these problems are not specific to npm Enterprise as a product, but are specific to using certain public packages in a limited-access environment. That being said, there are some things that npm (as an organization and software vendor) can do to better prevent or handle some of these problems, and we will be working towards that goal in the near future.
Typically developers will discover the problem when installing packages from their private registry. When this happens, we need to determine the type of problem it is and where in the dependency graph the problematic dependency resides.
Here are some common problem types:
-
Git repo dependency
This is when a package dependency is listed in a
package.json
file with a reference to a Git repository instead of with a semver version range. Typically these point to a particular branch or revision in a public GitHub or Bitbucket repository. They are mainly used when the package contents have not been published to the public npm registry.When the npm client encounters these, it attempts to fetch the package from the Git repository directly, which is a problem for folks who do not have network access to the repository.
-
Shrinkwrapped package
This is when the internal contents of a package contain an
npm-shrinkwrap.json
file that lists a specific version and URL to use for each mentioned package from the dependency tree.During a normal
npm install
, the npm client attempts to fetch the dependencies listed innpm-shrinkwrap.json
directly from the URLs contained in the file. This poses a problem when the client installing the shrinkwrapped package does not have access to the URLs that the shrinkwrap author has access to. -
Package with install script or node-gyp dependency
This is when a package attempts to defer some setup process until the package is installed, using a script defined in
package.json
, which typically involves building platform-specific binaries or Node add-ons on the client's machine.On a typical install, the
npm
client will find and run these scripts such that the required resources can be fetched and built in an automated way, targeting the platform that the client is running on. When the necessary resources cannot be fetched due to limited internet access, the install will fail. The package is most likely unusable until the end result of running the install script on the client's machine is achieved.
When it comes to determining the location of the problematic dependency, we can boil it down to two categories:
-
Direct dependency
A direct dependency is one that is explicitly listed in your own
package.json
file - a dependency that your project/package uses directly in code or in an npm run script. -
Transitive dependency
A transitive dependency is one that is not explicitly listed in your own
package.json
file - a dependency that comes from anywhere in the tree of your direct dependencies' dependencies.
Just as publishing a package to the public registry requires access to the public internet, most of these solutions require access to the same, at least on a temporary basis. Once the solution is in place, access to public resources can then be restricted.
Note that it's generally a good idea to use the latest version of the npm
client. To install or upgrade to the latest version, regardless of what version of Node you have installed, run npm i -g npm@latest
(and make sure npm -v
prints the version that was installed).
Let's go over the problem types in more detail.
Unfortunately, a dependency that references a Git repository (instead of a semver range for a published package) must be replaced with a published package. To do this, you'll need to first publish the Git repository as a package to your npm Enterprise registry and then fork the project with the Git dependency, replacing the dependency with the package you published. You will then need to publish the forked project and use that package as a dependency instead of the original.
Remember that it's usually a good idea to open an issue on the project with the Git dependency, asking the maintainers to politely replace the Git dependency, if possible. Generally, using Git dependencies in package.json
is discouraged and is typically only used temporarily while a maintainer waits for an upstream fix to be applied and published.
As an example, we will replace the "grunt-mocha-istanbul": "christian-bromann/grunt-mocha-istanbul"
Git dependency defined in version 4.0.4 of the webdriverio
package, assuming that webdriverio
is a direct dependency and grunt-mocha-istanbul
is a transitive dependency.
We'll tackle this in two main steps: (Step 1) forking and publishing the transitive dependency and (Step 2) forking and publishing the direct dependency.
-
Clone the project that is referenced by the Git dependency
You can optionally create a remote fork first (e.g. in GitHub or Bitbucket) and then clone your fork locally, or just clone/download the project directly from the remote repository. It's a good idea to use source control so you can keep a history of your changes, but you could also probably get away with downloading and extracting the project contents.
Example:
git clone https://github.com/christian-bromann/grunt-mocha-istanbul.git
-
Create a new branch to hold your customizations
Again, this is so you can keep a history of your changes. It's probably a good idea to include the current version of the package in the branch name, in case you need to repeat these steps when a later version is available.
Example:
cd grunt-mocha-istanbul git checkout -b myco-custom-3.0.1
-
Add your scope to the package name in
package.json
In our example, change
"grunt-mocha-istanbul"
to"@myco/grunt-mocha-istanbul"
. -
Commit your changes to your branch and publish the scoped package to your npm Enterprise registry
Assuming you have already configured
npm
to associate your scope to your private registry, publishing should be as simple asnpm publish
.Example:
git add package.json git commit -m 'add @myco scope to package name' npm publish
-
Clone the project's source code locally
Either create a remote fork first (e.g. in GitHub or Bitbucket) and clone your fork locally, or just clone/download the project directly from the original remote repository. It's a good idea to use source control so you can keep a history of your changes.
Example:
git clone https://github.com/webdriverio/webdriverio.git
-
Create a new branch to hold your customizations
This is so you can keep a history of your changes. It's probably a good idea to include the current version of the package in the branch name, in case you need to repeat these steps when a later version is available.
Example:
cd webdriverio git checkout -b myco-custom-4.0.4
-
Add your scope to the package name in
package.json
In our example, change
"webdriverio"
to"@myco/webdriverio"
. -
Replace the Git dependency with the scoped package
This means updating the reference in
package.json
, and it may mean updatingrequire()
orimport
statements too. You should basically do a find-and-replace, finding the unscoped package name and judiciously replacing it with the scoped package name.In our example, we only need to update the reference in
package.json
from"grunt-mocha-istanbul": "christian-bromann/grunt-mocha-istanbul"
to"@myco/grunt-mocha-istanbul": "^3.0.1"
. -
Commit your changes to your branch and publish the scoped package to your npm Enterprise registry
Assuming you have already configured
npm
to associate your scope to your private registry, publishing should be as simple asnpm publish
.In our example of
webdriverio
, we actually need to deal with the shrinkwrap URLs before we can publish (handled below), but under other scenarios, it may be possible to publish now.Example:
git add . git commit -m 'replace git dep with scoped fork' npm publish
-
Update your downstream project(s) to use the scoped package as a direct dependency (in
package.json
and in anyrequire()
statements)In our example, this basically means doing a find-and-replace to find references to
webdriverio
and judiciously replace them with@myco/webdriverio
. However,webdriverio
also contains annpm-shrinkwrap.json
file, which is covered in the next section.
It just so happens that our sample direct dependency above (webdriverio
) also uses an npm-shrinkwrap.json
file to pin certain dependencies to specific versions. Unfortunately the shrinkwrap file contains hardcoded URLs to the public registry, so we need a way to either ignore or fix the URLs.
A quick workaround is to install packages using the --no-shrinkwrap
flag. This will tell the npm
client to ignore any shrinkwrap files it finds in the package dependency tree and, instead, install the dependencies from package.json
in the normal fashion.
This is considered a workaround rather than a long-term solution because it's possible that installing from package.json
will install versions of dependencies that do not exactly match the ones listed in npm-shrinkwrap.json
, even though the versions of the package's direct dependencies are guaranteed to be within the declared semver range.
Example:
npm install webdriverio --no-shrinkwrap
(As noted above, webdriverio@4.0.4
also has a Git dependency, so just ignoring the shrinkwrap is not quite enough for this package.)
If you want to use the exact versions from the shrinkwrap file without using the URLs in it, you'll have to use your own custom fork of the project that contains a modified shrinkwrap file.
Here's the general idea:
(Note that steps 1-3 are identical to the fork-publish instructions for a direct dependency above. If you have already completed them, skip to step 4.)
-
Clone the project's source code locally
Either create a remote fork first (e.g. in GitHub or Bitbucket) an clone your fork locally, or just clone/download the project directly from the original remote repository. It's a good idea to use source control so you can keep a history of your changes.
Example:
git clone https://github.com/webdriverio/webdriverio.git
-
Create a new branch to hold your customizations
This is so you can keep a history of your changes. It's probably a good idea to include the current version of the package in the branch name, in case you need to repeat these steps when a later version is available.
Example:
cd webdriverio git checkout -b myco-custom-4.0.4
-
Add your scope to the package name in
package.json
In our example, change
"webdriverio"
to"@myco/webdriverio"
. -
Use
rewrite-shrinkwrap-urls
to modifynpm-shrinkwrap.json
, pointing the URLs to your npm Enterprise registryUnfortunately it's slightly more complicated than a find-and-replace since the tarball URL structure of the public registry is different than the one used for an npm Enterprise private registry.
In the example below, replace
{your-registry}
with the base URL of your private registry, e.g.https://npm-registry.myco.com
orhttp://localhost:8080
. The value you use should come from the Full URL of npm Enterprise registry setting in your Enterprise admin UI Settings page.Example:
npm install -g rewrite-shrinkwrap-urls rewrite-shrinkwrap-urls -r {your-registry} git diff npm-shrinkwrap.json
-
Commit your changes to your branch and publish the scoped package to your npm Enterprise registry
Assuming you have already configured
npm
to associate your scope to your private registry, publishing should be as simple asnpm publish
.Be mindful of any
prepublish
orpublish
scripts that may be defined inpackage.json
. You can try skipping those scripts when publishing vianpm publish --ignore-scripts
, but running the scripts may be necessary to put the package into a usable state, e.g. if source transpilation is required.Example:
git add npm-shrinkwrap.json package.json git commit -m 'add @myco scope to package name' package.json git commit -m 'rewrite shrinkwrap urls' npm-shrinkwrap.json npm publish
Note that a
prepublish
script will probably need to install the package's dependencies in order to run, in which casenpm install
will first be executed. If this happens, it should pull all dependencies in the shrinkwrap file from your registry. If any of those packages do not yet exist in your registry, you will either need to enable the Read Through Cache setting in your Enterprise instance or manually add the packages to the white-list by runningnpme add-package webdriverio
from your server's shell and answeringY
at the prompt to add dependencies. -
Update your downstream project(s) to use the scoped package as a direct dependency (in
package.json
and in anyrequire()
statements)In our example, this basically means doing a find-and-replace to find references to
webdriverio
and judiciously replace them with@myco/webdriverio
.
We at npm realize this is less than ideal, and we are currently considering ways to improve handling of shrinkwrapped packages on the server side, but a better solution is not yet available.
Some packages want or need to run some script(s) on installation in order to build platform-specific dependencies or otherwise put the package into a usable state. This approach means that a package can be distributed as platform-independent source without having to prebundle binaries or provide multiple installation options.
Unfortunately this also means that these packages typically need access to the public internet in order to fetch required resources. In these cases, there is little we can do to work around this approach other than attempt to isolate the steps of fetching the package from the registry and setting up the platform-specific resources it needs.
As a quick first attempt, you can ignore lifecycle scripts when installing packages via npm install {pkg-name} --ignore-scripts
.
Unfortunately, install scripts typically do some sort of platform-specific setup to make the package usable. Thus, you should review the install
or postinstall
scripts from the package's package.json
file and determine if you need to attempt to run them separately or somehow achieve the same result manually.
When node-gyp is involved in the setup process, the package requires platform-specific binaries to be built and plugged into the Node runtime on the client's system. In order to build the binaries, the package will typically need to fetch source header files for the Node API.
The best we can do is attempt to setup the node-gyp build toolchain manually. This requires Python and a C/C++ compiler. You can read more about this at the following locations:
General installation: https://github.com/nodejs/node-gyp#installation
Windows issues: nodejs/node-gyp#629
A good example of a package with a node-gyp dependency is node-sass
.
Once the build toolchain is in place, the package's install script may not need to fetch any external resources.