95

Given that a project's lib/ dir shouldn't be checked into Git because the files it contains are derived files (from the build process). When installing a package from the project's github (during development for example) the lib/ dir will not exist, so if the package's package.json's main field points to (for example) lib/index.js, the package cannot be compiled when imported because these files do not exist within the repository and therefore within the package installed into node_modules. This means the package needs to built (just as it would be before release), only this time locally so that the lib directory (or whatever other files are generated during the build process) are added to the module's directory.

Assuming there is a build script within the package.json file's scripts field, can the package be configured to run this automatically in the situation where it is installed from github only? If not, what is the the best approach to ensuring it is built when installed from github?

There are now prepublish, prepublishOnly and prepare lifecycle hooks, but none provide an answer to this problem because they don't allow any way to differentiate between the source of the install. In short, yes they allow you to build on install, but they don't allow you to build on install from github only. There is not only no reason to force a build when people install from npm, but more importantly, development dependencies will not be installed (for example babel which is critical to the build).

I am aware of one strategy to deal with this problem:

  • Fork / branch the repo
  • build locally
  • remove the lib/ dir from .gitignore and check it in.
  • install module from your fork/branch
  • When you are ready for a PR / rebase add lib/ dir to .gitignore and remove dir from git.

But that is far from ideal. I guess this could be automated with a githook though. So every you push to master the project also builds and pushes to a separate branch.

There is a closed issue on NPM's Github with no resolution - just lots of people who want a solution. From this issue it is clear that using prepare is not the answer.

My usecase is that I am developing a module that is used in a number of other projects. I want to consume the latest version of the module without having to push out a release to NPM whenever I update the codebase - I would rather push out fewer releases when I am ready, but I still need to consume whatever the latest version of the lib is on Github.

Note: I also reached out to NPM's support regarding this issue and I'll add their response if I receive one.

Undistraction
  • 42,754
  • 56
  • 195
  • 331
  • I'm exploring the same problem. In my case, I have a patched fork of react/react-sketchapp. The only solution seems to be to: "cd node_modules/react-sketchapp && npm install && npm run prepublishOnly". This could be added to my own package.json as a post install step, but it would run whenever a package is installed, not only when needed (after installing/updating react-sketchapp) – Dave Meehan Jan 17 '18 at 16:45
  • There is a npm package called "npm-git-install" that proposes a solution but I tried it and it installed the dependency repo in a /var/... temp directory and creates a symlink that seemed not to work. It then prevented me from uninstalling, so I had to do some manual editing and reversion of package-lock.json to resolve. – Dave Meehan Jan 17 '18 at 16:49
  • Possible duplicate: https://stackoverflow.com/questions/40528053/npm-install-and-build-of-forked-github-repo – Dave Meehan Jan 17 '18 at 16:52
  • @DaveMeehan Thanks. I think using `prepublishOnly` is potentially really problematic and probably best discounted as a solution. I tried `npm-git-install` and found it unreliable. Its so strange there is no solution to this. It must be a common problem. – Undistraction Jan 17 '18 at 20:16
  • The original npm issue: https://github.com/npm/npm/issues/3055 – Dave Meehan Jan 17 '18 at 22:36
  • I'm guessing that package authors don't want to deal with the minor edge case of supporting forked build steps. It can be dealt with by the developer installing packages from GitHub on a case by case basis, running a script that looks for missing build products and issues the appropriate commands. – Dave Meehan Jan 17 '18 at 22:39
  • @DaveMeehan this must be such a common use-case: there must be many people developing a lib that want to use it in other projects without a full release. I think this deficiency probably encourages people to push out lots of unnecessary releases just to get access to their own updated lib from other projects. Anyway. I've added the issue to the question. – Undistraction Jan 18 '18 at 10:01
  • Does it work for you to use a local repo rather than the GitHub repo? i.e. clone your fork of the npm module from Github and then install that as the dependency in your main project. You wouldn't be able to push your main project and have others work on it without them knowing about the local repo and matching it location, which is the reason why I needed to switch to using the GitHub source for my needs. – Dave Meehan Jan 18 '18 at 14:02
  • @DaveMeehan That's definitely one way to handle it, but it still involves unignoring the dir containing the derived files, building, checking in built files, updating every change and remembering not remove the derived files before PR. – Undistraction Jan 18 '18 at 19:49
  • you can create [private repo](https://docs.npmjs.com/private-modules/intro) where you push dev branch and then during build process via condition PRODUCTION use right one.. – bigless Jan 19 '18 at 05:58
  • If this is for local development, is [npm link](https://docs.npmjs.com/cli/link) an option? – carpeliam Jan 21 '18 at 19:39
  • 1
    The presumption for even wanting to have a different build step for git vs npm seems wrong to me. The whole idea of running a build after installing from git is _specifically_ to generate the same assets that are already built during npm-publish. The reason there's no easy way to do a thing after git-clone is because you shouldn't want to. TL;DR: `prepare` is the right script. It will build the same assets for the package tarball during publish, and when installed as a git dep. – jasonkarns Apr 20 '19 at 16:17
  • Can you elaborate on why `prepare` doesn't work? The issue you link to doesn't reference `prepare` in a way that suggests it is problematic (beside bugs that have been fixed). This works fine for me in every case I've tried to use it as long as `package.json` is setup correctly. The only difficulty I've found is when we didn't realize that `.gitignore` was messing with our builds. – Cameron Tacklind Aug 14 '19 at 23:31

4 Answers4

38

prepare is the correct way, but might seem broken

If you have a repository with source files but a "build" step is necessary to use it,
prepare does exactly what you want in all cases (as of npm 4).

prepare: Run both BEFORE the package is packed and published, on local npm install without any arguments, and when installing git dependencies.

You can even put your build dependencies into devDependencies and they will be installed before prepare is executed.

Here is an example of a package of mine that uses this method.


Problems with .gitignore - prepare will seem broken

There is one issue with this option that gets many people. When preparing a dependency, Npm and Yarn will keep only the files that are listed in the files section of package.json.

One might see that files defaults to all files being included and think they're done. What is easily missed is that:

  • .npmignore mostly overrides the files directive and,
  • if .npmignore does not exist, .gitignore is used instead.

So, if you have your built files listed in .gitignore, like a sane person, and don't do anything else, prepare will seem broken.

If you fix files to only include the built files or add an empty .npmignore, you're all set.

My recommendation is to set files (or, by inversion, .npmignore) such that the only files actually published are those needed by users of the published package. Imho, there is no need to include uncompiled sources in published packages.

Cameron Tacklind
  • 5,764
  • 1
  • 36
  • 45
  • except it runs the build 2 times - https://github.com/npm/cli/blob/36682d4482cddee0acc55e8d75b3bee6e78fff37/lib/pack.js#L294 – Kamil Tomšík Oct 16 '19 at 20:02
  • @KamilTomšík I don't think that is an accurate statement. The code you reference looks to only build once. I've also tried testing this directly and see no evidence of the build running twice. – Cameron Tacklind Oct 21 '19 at 19:52
  • I know it does npm install (silently) during the `prepareGitDep` phase, but you might be right that the package might then not be built again if you do it only in prepare phase, but unfortunately for my case, it does because I do both typescript compilation and native module compilation which has to be done after install, and hence it's done two times (first in prepareGitDep during npm install and second when the packaged result is installed) - try installing this https://github.com/cztomsik/graffiti (it takes forever) – Kamil Tomšík Oct 22 '19 at 20:12
36

Edit: Detecting if the package is being installed from git repo

I didn't understand the question properly. Below are things that I wrote but are a bit off-topic. For now if you want to run build.js only when installing from repo:

Files in repo:

 .gitignore
 .npmignore
 ThisIsNpmPackage
 build.js
 package.json

The .gitginore:

ThisIsNpmPackage

The .npmignore:

!ThisIsNpmPackage

In the package.json:

"scripts": {
    "install": "( [ ! -f ./ThisIsNpmPackage ] && [ ! -f ./AlreadyInstalled ] && echo \"\" > ./AlreadyInstalled && npm install . && node ./build.js ) || echo \"SKIP: NON GIT SOURCE\""
}

The idea is to make file ThisIsNpmPackage available on the repo, but not in npm package.

Install hook it's just a piece of bashy script to check if ThisIsNpmPackage exists. If yes then we execute npm install . (this will ensure we have devDependencies. File AlreadyInstalled is generated to prevent infinite looping (npm install will recursively invoke install hook)

When publishing I do git push and npm publish
Note that npm publish can be automated via CI tools - githooks

This little hack with file ThisIsNpmPackage makes the source detection available.

Results of invoking npm install dumb-package:

"SKIP: NON-GIT SOURCE"

And executing npm install https://github.com/styczynski/dumb-package

Files will be built

The issues

The main issues we are facing here are the following ones:

  • Have to do npm publish ... everytime

    Sometimes it's too much pain to fix a small bug, then push to the repo and forget to publish on npm. When I was working with a microservices-based project that has about 5 standalone subprojects divided into modules the problem that I found an issue, fixed it and forget to publish in every place I had to was really annoying.

  • Don't want to push lib into the repo, because it's derived from sources

  • Rebasing and merging is even more annoying.

  • No mess with .gitgnore

    Heck, I know that problem when you have a troublesome files that you have to include inside repo but never modify them, or sometimes remove? That's just sick.

Edit: npm hooks

As @Roy Tinker mentioned there exist ability for a package to execute a command when installed.
It can be achieved via npm hooks.

"install": "npm run build"

And we execute the:

npm install https://github.com/<user>/<package>

Edit:
OP question from comments:

But this will run an install for everyone downloading the module from npm right? This is hugely problematic given that dev dependencies will not be installed for anyone downloading the module from npm. The libs used to build the app - babel etc will not be installed.

Note: But if you want a specific version of the package (production/dev) with or without dev dependencies you can install it via:

npm install --only=dev

The --only={prod[uction]|dev[elopment]} argument will cause either only devDependencies or only non-devDependencies to be installed regardless of the NODE_ENV.

A better solution, in my opinion, is to use:

npm install <git remote url>

And then inside package.json specify:

"scripts": {
    "prepare": "npm run build"
}

If the package being installed contains a prepare script, its dependencies and devDependencies will be installed, and the prepare script will be run, before the package is packaged and installed.

Example:

npm install git+https://isaacs@github.com/npm/npm.git

Read the npm docs there: npm install

Edit: proxy module (advanced technique)

It's kind of bad practice, but good to know.

Sometimes (as in case of Electron framework you need to install other external packages or resources/modules depending on various conditions).

In these cases the proxy idea is used:

  • You make a module that behaves like installer and installs all depending things you want

In your case prepare script will be enough, but I leave this option, because it may be sometimes helpful.

The idea is that you write a module and write a install kook for it:

"scripts": {
    "install": "<do the install>"
}

In this scenario you can place there:

npm install . && npm run build

Which install all devDependencies anyway (as beforementioned prepare case do), but it's a bit of hacking.

If you want do the real hacking there:

 "scripts": {
    "install": "curl -L -J -O \"<some_url>\""
 }

which manually download files using -nix command curl

It should be avoided but it's an option in case of the module that has huge binary files for each platform and you don't want to install them all.

Like in case of Electron where you have compiled binaries (each for the separate platform)

So you want people to make install package not install package-linux or package-window etc.

So you provide custom install script in the package.json

{
  ...
  "scripts": {
     "install": "node ./install_platform_dep.js"
  }
}

Then when installing module the install_platform_dep.js script will be executed. Inside install_platform_dep.js you place:

// For Windows...
if(process.platform === 'win32') {
    // Trigger Windows module installation
    exec('npm install fancy-module-windows', (err, stdout, stderr) => {
         // Some error handling...
    }
} else if ... // Process other OS'es

And this in purely manual way installs everything.

Note: Once again this approach is usable with platform-depending modules and if you use that it's probably design issue with your code.

Build on CI

What comes to my mind is the solution that I used really for a long time (automatic building with CI services).

Most of the CI services' main purpose is to test/build/publish your code when pushing to the branch or doing other actions with the repo.

The idea is that you provide settings file (like travis.yml or .gitlab-ci.yml) and the tools take care of the rest.

If you really don't want to include the lib into the project, just trust CI to do everything:

  • Githook will trigger building on commit (on a branch or any other - it's just a matter of configs)
  • CI will build your files then pass them to the test phase and publish

Now i'm working on Gitlab on my own project doing (as a part of hobby) some webpage. The Gitlab configuration that builds the project looks like this:

image: tetraweb/php

cache:
  untracked: true
  paths:
    - public_html/
    - node_modules/

before_script:
  - apt-get update

stages:
  - build
  - test
  - deploy
  
build_product:
  stage: build
  script:
    - npm run test

build_product:
  stage: build
  script:
    - npm run build
  
deploy_product:
  stage: deploy
  script:
    - npm run deploy

When I merge into the main branch the following events happen:

  • CI runs build stage
  • If the build succeeds then test stage is launched
  • If test phase is ok then finally the stage deploy is triggered

The script is the list of unix commands to be executed.

You can specify any Docker image in the config, so use in fact any Unix version you want with some (or not) preinstalled tools.

There is a package deploy-to-git which deploys artefacts to the desired repo branch.

Or here (for Travis CI) the piece of config that publishes artefacts to the repo:

travis-publish-to-git

(I used it by myself)

Then, of course, you can let CI run:

npm publish .

Because CI executes Unix commands then it can (at least a bunch of CI providers there):

  • Publish tags (release tag maybe?)
  • Trigger script to update version of the project in all READMEs and everywhere
  • Send you a notification if all phases succeeded

So what I do:
I commit, push and let the tools do everything else I want.
In the meantime, I make other changes and after one to ten minutes get update report by mail.

There is plenty of CI provider there:

Here I attach another example of my other project (.travis.yml):

language: generic
install:
    - npm install
script:
    - chmod u+x ./utest.sh 
    - chmod u+x ./self_test/autodetection_cli/someprogram.sh
    - cd self_test && bash ../utest.sh --ttools stime --tno-spinner

If you set up CI to push and publish your package you can be always sure to use the latest cutting-edge version of your code without worrying about eh I have to run also this command now... problem.

I recommend you to choose one of the CI providers out there.
The best ones offer you hundreds of abilities!

When you get used to automatically doing publish, test and build phases you will see how it helps to enjoy the life!
Then to start another project with automatic scripts just copy the configs!

Summary

In my opinion npm prepare script is an option.
You can also maybe want to try others.

Each of the described methods has it's drawbacks and can be used depending on what you want to achieve.
I just want to provide some alternatives hope some of them will fit your problem!

Community
  • 1
  • 1
Piotr Styczyński
  • 1,391
  • 10
  • 19
  • Thanks for the additional explanation. I can see how this solves the problem of dev dependencies being installed, but unless I'm missing something this still involves `build` being run for everyone who downloads the package, whether from npm or github. I'm looking for build to happen _only_ on install from Github, and `prepare` doesn't achieve this. `npm install example` or npm install {githubURL} will both result in `prepare` and therefore `build` being run. I've added this clarification to my question. – Undistraction Jan 25 '18 at 11:49
  • Oh I see now I understand. So maybe this is an option: Add a file (content does not matter) `ThisIsNpmPackage` to your repo? Add `ThisIsNpmPackage` to `.gitignore` and `!ThisIsNpmPackage` to `.npmignore` then `"install": "node ./build.js"` in build script detect if file `ThisIsNpmPackage` is present if it's present then you installed package from npm then in case of non-npm package do `exec('npm install .')` and proceed ... – Piotr Styczyński Jan 25 '18 at 14:07
  • I provided example repo with setup (described there^) Please see the new edit - Edit: Detecting if package is being installed from git repo – Piotr Styczyński Jan 25 '18 at 15:02
  • @Undistraction I hope it will help :) – Piotr Styczyński Jan 25 '18 at 15:04
  • Although I'm not convinced this is the best possible solution (at least I hope that there is a perfect solution out there), you absolutely deserve the bounty for your efforts. Thank you. – Undistraction Jan 25 '18 at 19:27
  • Thanks! :D I did all my best @Undistraction – Piotr Styczyński Jan 26 '18 at 13:59
  • Do you mean continuous integration rather than circular integration? – Tim Krins Jan 14 '19 at 09:41
  • The `prepare` is the right way. You just need to watch out for the `.npmignore`/`.gitignore` issue. – Cameron Tacklind Sep 07 '19 at 01:04
6

Assuming there is a build script within the package.json file's scripts field, can the package be configured to run this automatically in this situation?

Yes. There are 2 things you need to do:

  1. Make sure your system uses npm or yarn to install the package from GitHub. If this package is a dependency of another package, you can use the GitHub URL in place of the version number in package.json. Otherwise, the following command will work:

    npm install https://github.com/YourUser/your-package
    

    You can add /tags/v1.0.0 or whatever to the end of the URL if you're after a specific tag or branch.

  2. Add the following to the scripts in your module's package.json:

    "install": "npm run build"
    

install is a hook that the package manager executes after installing the module. (preinstall and postinstall also -- see documentation).

Documentation: https://docs.npmjs.com/misc/scripts

Roy Tinker
  • 10,044
  • 4
  • 41
  • 58
  • @Undistraction, Yes, but at this point you _have_ jacked into the matrix. Other problems can be solved separately -- perhaps just move the required dependencies from `devDependencies` to `dependencies`. Or maybe your build script could run its own `npm install` -- which will install dev dependencies. – Roy Tinker Jan 25 '18 at 17:14
  • `prepare` script is the correct way. This solution will rebuild already built versions. – Cameron Tacklind May 03 '20 at 22:10
  • `prepare` didnt work for me, `install` did on the other hand. what should be the value of `prepare` if I'm using yarn? – bfahm Jan 05 '23 at 16:08
3

EDITED 2

It's a great question. It's too bad there isn't a recognized reliable solution, but the following seems to work.

Create a .buildme marker file, and commit to git.

In package.json:

  "files": ["lib"],
  "scripts": {
    "build": "echo DO WHAT YOU NEED TO BUILD",
    "prepack": "[ ! -f .buildme ] || npm run build",
    "preinstall": "[ ! -f .buildme ] || npm run build"
  },

Here are the things to note.

  1. The special .buildme marker file should be excluded from the npm package with either the "files" key, or via .npmignore.

  2. The prepack hook runs when you publish (prepublishOnly might also work, but it's nice that with prepack, npm pack will produce a correct tarball).

  3. When installing from npm, preinstall runs, but does nothing because .buildme is missing (thanks to the the [ ! -f .buildme ] clause).

  4. When installing from github, .buildme does exist. On npm6, prepack hook runs the build (and produces a package without .buildme), and preinstall does nothing. On yarn 1.12, preinstall does the build.

  5. If you install an updated version from github, preinstall will run again, and will build again.

NOTE: When installing from github, it's up to the person installing to have enough of your package's devDependencies already installed for the build to work. (This solution doesn't attempt to auto-install devDependencies.)

That's it. It seems to work with varios combinations of npm 6 and yarn 1.12.

DS.
  • 22,632
  • 6
  • 47
  • 54
  • 1
    This is a clever solution. Unfortunately it relies on `bash` which is generally not available on Windows systems. There is an official way to do this with the `prepare` script. – Cameron Tacklind May 03 '20 at 22:09