224

What is the most correct way to install npm packages in nested sub folders?

my-app
  /my-sub-module
  package.json
package.json

What is the best way to have packages in /my-sub-module be installed automatically when npm install run in my-app?

Bogdan Bogdanov
  • 1,707
  • 2
  • 20
  • 31
WHITECOLOR
  • 24,996
  • 37
  • 121
  • 181

15 Answers15

360

I prefer using post-install, if you know the names of the nested subdir. In package.json:

"scripts": {
  "postinstall": "cd nested_dir && npm install",
  ...
}
Tunaki
  • 132,869
  • 46
  • 340
  • 423
Scott
  • 4,211
  • 3
  • 17
  • 20
111

Per @Scott's answer, the install|postinstall script is the simplest way as long as sub-directory names are known. This is how I run it for multiple sub dirs. For example, pretend we have api/, web/ and shared/ sub-projects in a monorepo root:

// In monorepo root package.json
{
...
 "scripts": {
    "postinstall": "(cd api && npm install); (cd web && npm install); (cd shared && npm install)"
  },
}

On Windows, replace the ; between the parentesis with &&.

// In monorepo root package.json
{
...
 "scripts": {
    "postinstall": "(cd api && npm install) && (cd web && npm install) && (cd shared && npm install)"
  },
}
beaver
  • 523
  • 1
  • 9
  • 20
demisx
  • 7,217
  • 4
  • 45
  • 43
  • 1
    will those npm installs happen one after eachother or all at the same time? – Suisse Nov 21 '19 at 22:16
  • 20
    Good use of `( )` to create subshells and avoiding `cd api && npm install && cd ..`. – Cameron Hudson Jan 31 '20 at 01:08
  • 9
    I get this error when running `npm install` at the top-level: `"(cd was unexpected at this time."` – Mr. Polywhirl May 16 '20 at 22:33
  • 3
    My directories are nested at the same depth as each other relative to their parent directory so in my case I had to do it like this: `"(cd api && npm install); (cd ../web && npm install);...` including the `../` to make sure it changes the directory to the right one. I thought the parentheses would run the command in the same working directory context independently from each other as @CameronHudson said but for some reason, it didn't work for me... – JohnD Aug 20 '21 at 08:57
  • As a super lazy approach (I admit): `"postinstall": "find . -maxdepth 1 -mindepth 1 -type d -not -path './.*' | xargs -Idest yarn --cwd dest install"` – Alireza Mohamadi Apr 27 '22 at 08:28
60

Use Case 1: If you want be able to run npm commands from within each subdirectory (where each package.json is), you will need to use postinstall.

As I often use npm-run-all anyway, I use it to keep it nice and short (the part in the postinstall):

{
    "install:demo": "cd projects/demo && npm install",
    "install:design": "cd projects/design && npm install",
    "install:utils": "cd projects/utils && npm install",

    "postinstall": "run-p install:*"
}

This has the added benefit that I can install all at once, or individually. If you don't need this or don't want npm-run-all as a dependency, check out demisx's answer (using subshells in postinstall).

Use Case 2: If you will be running all npm commands from the root directory (and, for example, won't be using npm scripts in subdirectories), you could simply install each subdirectory like you would any dependecy:

npm install path/to/any/directory/with/a/package-json

In the latter case, don't be surprised that you don't find any node_modules or package-lock.json file in the sub-directories - all packages will be installed in the root node_modules, which is why you won't be able to run your npm commands (that require dependencies) from any of your subdirectories.

If you're not sure, use case 1 always works.

uɥƃnɐʌuop
  • 14,022
  • 5
  • 58
  • 61
  • 1
    It's nice to have each submodule have its own install script and then execute them all in postinstall. `run-p` is not necessary, but it's then more verbose `"postinstall": "npm run install:a && npm run install:b"` – Qwerty Jan 31 '20 at 13:52
  • 2
    Yes, you can use `&&` without `run-p`. But as you say, that's less readable. Another drawback (that run-p solves because installs run in parallel) is that if one fails, no other script is affected – uɥƃnɐʌuop Jan 31 '20 at 14:53
  • 1
    FYI if you accidentally `cd` into a wrong path that doesn't contain `package.json` it will loop install script forever. Therefore I use `"install:mymodule": "cd src/mymodule && test -f package.json && npm install"` – Viacheslav Sep 12 '20 at 10:36
  • @DonVaughn that's the point: `test -f package.json` will make `npm install` exit with `error tmp@1.0.0 install:module: cd module && test -f package.json && npm install npm ERR! Exit status 1`. Without that check it will loop forever without any clues for you to figure out where you've made a mistake. Try it yourself. – Viacheslav Sep 13 '20 at 17:58
  • @DonVaughn I suppose you understand that to correct a mistake you need to know where the mistake is? Or at least that there is a mistake at all. That's the entire purpose of my adjustment. Without it 'npm install' won't throw any errors at you. That was my last comment in this thread, sorry. – Viacheslav Sep 14 '20 at 13:38
  • use case 1 is the simplest most readable and imo best way to achieve this. thanks for that – stor314 Aug 05 '21 at 21:06
37

If you want to run a single command to install npm packages in nested subfolders, you can run a script via npm and main package.json in your root directory. The script will visit every subdirectory and run npm install.

Below is a .js script that will achieve the desired result:

var fs = require('fs');
var resolve = require('path').resolve;
var join = require('path').join;
var cp = require('child_process');
var os = require('os');
    
// get library path
var lib = resolve(__dirname, '../lib/');
    
fs.readdirSync(lib).forEach(function(mod) {
    var modPath = join(lib, mod);
    
    // ensure path has package.json
    if (!fs.existsSync(join(modPath, 'package.json'))) {
        return;
    }

    // npm binary based on OS
    var npmCmd = os.platform().startsWith('win') ? 'npm.cmd' : 'npm';

    // install folder
    cp.spawn(npmCmd, ['i'], {
        env: process.env,
        cwd: modPath,
        stdio: 'inherit'
    });
})

Note that this is an example taken from a StrongLoop article that specifically addresses a modular node.js project structure (including nested components and package.json files).

As suggested, you could also achieve the same thing with a bash script.

EDIT: Made the code work in Windows

Ronin
  • 1,688
  • 1
  • 13
  • 23
snozza
  • 2,123
  • 14
  • 17
  • 2
    To complicated right though, thanks for the article link. – WHITECOLOR Aug 02 '15 at 16:41
  • While the 'component' based structure is quite a handy way to setup a node app, it's probably overkill in the early stages of the app to break out separate package.json files etc. The idea tends to come to fruition when the app grows and you legitimately want separate modules/services. But yes, definitely too complicated if not necessary. – snozza Aug 02 '15 at 16:44
  • 3
    While yes a bash script will do, but I prefer the nodejs way of doing it for maximum portability between Windows which has a DOS shell and Linux/Mac which has the Unix shell. – daparic Oct 03 '17 at 17:01
28

Just for reference in case people come across this question. You can now:

  • Add a package.json to a subfolder
  • Install this subfolder as reference-link in the main package.json:

npm install --save path/to/my/subfolder

Jelmer Jellema
  • 1,072
  • 10
  • 16
  • 2
    Note that dependencies are installed in the root folder. I suspect that if you are even considering this pattern, you want the dependencies of the sub-directory package.json in the sub-directory. – Cody Allan Taylor Jan 15 '19 at 16:57
  • What do you mean? The dependencies for the subfolder-package are in the package.json in the subfolder. – Jelmer Jellema Jan 18 '19 at 09:17
  • (using npm v6.6.0 & node v8.15.0) - Setup up an example for yourself. `mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;` Now wait... you just *manually* installed dependencies in "b", that's not what happens when you clone a fresh project. `rm -rf node_modules ; cd .. ; npm install --save ./b`. Now list node_modules, then list b. – Cody Allan Taylor Jan 20 '19 at 03:05
  • 1
    Ah you mean the modules. Yes, the node_modules for b will be installed in a/node_modules. Which makes sense, because you will require / include the modules as part of the main code, not as a "real" node module. So a "require('throug2')" would search through2 in a/node_modules. – Jelmer Jellema Jan 21 '19 at 08:37
  • I'm trying to do code generation and want a subfolder-package which is fully prepared to run, including its own node_modules. If I find the solution, I'll make sure to update! – ohsully Mar 28 '19 at 02:36
  • Directly running npm install within that folder worked, so the post-install script is very clean. If I were to build the process into a node script, I'd recommend using `shelljs`! Clean cross-platform support for free. – ohsully Mar 28 '19 at 02:47
  • When you add this subfolder package to a project as a subfolder, the dependency modules will be added to the node_modules of the main project. If you add the subpackage parallel to the main project (i.e. they share a parent directory), the subpackage will have it's own node_modules. In that case you must run npm install manually for the subpackage. – Jelmer Jellema Apr 12 '19 at 09:49
  • This unfortunately doesn't seem to work when installed transitively. `Could not install "path/to/my/subfolder" as it is not a directory and is not a file with a name ending in .tgz, .tar.gz or .tar` – Drazen Bjelovuk Nov 11 '19 at 21:20
  • It should be a directory, containing your code and package.json file. – Jelmer Jellema Nov 13 '19 at 15:57
  • It does not install `node_modules` in the specified folder, but in the folder you are in – Matteo Jan 06 '20 at 02:33
  • You shoud run nmp install in the top directory of your project, the one with the right package.json. – Jelmer Jellema Jan 07 '20 at 10:19
28

The accepted answer works, but you can use --prefix to run npm commands in a selected location.

"postinstall": "npm --prefix ./nested_dir install"

And --prefix works for any npm command, not just install.

You can also view the current prefix with

npm prefix

And set your global install (-g) folder with

npm config set prefix "folder_path"

Maybe TMI, but you get the idea...

BJ Anderson
  • 1,096
  • 9
  • 4
27

My solution is very similar. Pure Node.js

The following script examines all subfolders (recursively) as long as they have package.json and runs npm install in each of them. One can add exceptions to it: folders allowed not having package.json. In the example below one such folder is "packages". One can run it as a "preinstall" script.

const path = require('path')
const fs = require('fs')
const child_process = require('child_process')

const root = process.cwd()
npm_install_recursive(root)

// Since this script is intended to be run as a "preinstall" command,
// it will do `npm install` automatically inside the root folder in the end.
console.log('===================================================================')
console.log(`Performing "npm install" inside root folder`)
console.log('===================================================================')

// Recurses into a folder
function npm_install_recursive(folder)
{
    const has_package_json = fs.existsSync(path.join(folder, 'package.json'))

    // Abort if there's no `package.json` in this folder and it's not a "packages" folder
    if (!has_package_json && path.basename(folder) !== 'packages')
    {
        return
    }

    // If there is `package.json` in this folder then perform `npm install`.
    //
    // Since this script is intended to be run as a "preinstall" command,
    // skip the root folder, because it will be `npm install`ed in the end.
    // Hence the `folder !== root` condition.
    //
    if (has_package_json && folder !== root)
    {
        console.log('===================================================================')
        console.log(`Performing "npm install" inside ${folder === root ? 'root folder' : './' + path.relative(root, folder)}`)
        console.log('===================================================================')

        npm_install(folder)
    }

    // Recurse into subfolders
    for (let subfolder of subfolders(folder))
    {
        npm_install_recursive(subfolder)
    }
}

// Performs `npm install`
function npm_install(where)
{
    child_process.execSync('npm install', { cwd: where, env: process.env, stdio: 'inherit' })
}

// Lists subfolders in a folder
function subfolders(folder)
{
    return fs.readdirSync(folder)
        .filter(subfolder => fs.statSync(path.join(folder, subfolder)).isDirectory())
        .filter(subfolder => subfolder !== 'node_modules' && subfolder[0] !== '.')
        .map(subfolder => path.join(folder, subfolder))
}
catamphetamine
  • 4,489
  • 30
  • 25
13

If you have find utility on your system, you could try running the following command in your application root directory:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

Basically, find all package.json files and run npm install in that directory, skipping all node_modules directories.

Moha the almighty camel
  • 4,327
  • 4
  • 30
  • 53
  • 1
    Great answer. Just a note that you can also omit additional paths with: `find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;` – Evan Moran Jul 08 '20 at 23:20
8

EDIT As mentioned by fgblomqvist in comments, npm now supports workspaces too.


Some of the answers are quite old. I think nowadays we have some new options available to setup monorepos.

  1. I would suggest using yarn workspaces:

Workspaces are a new way to set up your package architecture that’s available by default starting from Yarn 1.0. It allows you to setup multiple packages in such a way that you only need to run yarn install once to install all of them in a single pass.

  1. If you prefer or have to stay with npm, I suggest taking a look at lerna:

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

lerna works perfect with yarn workspaces too - article. I've just finished setting up a monorepo project - example.

And here is an example of a multi-package project configured to use npm + lerna - MDC Web: they run lerna bootstrap using package.json's postinstall.

Konstantin Lyakh
  • 781
  • 6
  • 15
  • 3
    Please don't use lerna, NPM has native support for "workspaces" now too: https://docs.npmjs.com/cli/v7/using-npm/workspaces/ – fgblomqvist Jun 23 '21 at 15:42
3

Adding Windows support to snozza's answer, as well as skipping of node_modules folder if present.

var fs = require('fs')
var resolve = require('path').resolve
var join = require('path').join
var cp = require('child_process')

// get library path
var lib = resolve(__dirname, '../lib/')

fs.readdirSync(lib)
  .forEach(function (mod) {
    var modPath = join(lib, mod)
    // ensure path has package.json
    if (!mod === 'node_modules' && !fs.existsSync(join(modPath, 'package.json'))) return

    // Determine OS and set command accordingly
    const cmd = /^win/.test(process.platform) ? 'npm.cmd' : 'npm';

    // install folder
    cp.spawn(cmd, ['i'], { env: process.env, cwd: modPath, stdio: 'inherit' })
})
Ghostrydr
  • 752
  • 5
  • 14
3

Inspired by the scripts provided here, I built a configurable example which:

  • can be setup to use yarn or npm
  • can be setup to determine the command to use based on lock files so that if you set it to use yarn but a directory only has a package-lock.json it will use npm for that directory (defaults to true).
  • configure logging
  • runs installations in parallel using cp.spawn
  • can do dry runs to let you see what it would do first
  • can be run as a function or auto run using env vars
    • when run as a function, optionally provide array of directories to check
  • returns a promise that resolves when completed
  • allows setting max depth to look if needed
  • knows to stop recursing if it finds a folder with yarn workspaces (configurable)
  • allows skipping directories using a comma separated env var or by passing the config an array of strings to match against or a function which receives the file name, file path, and the fs.Dirent obj and expects a boolean result.
const path = require('path');
const { promises: fs } = require('fs');
const cp = require('child_process');

// if you want to have it automatically run based upon
// process.cwd()
const AUTO_RUN = Boolean(process.env.RI_AUTO_RUN);

/**
 * Creates a config object from environment variables which can then be
 * overriden if executing via its exported function (config as second arg)
 */
const getConfig = (config = {}) => ({
  // we want to use yarn by default but RI_USE_YARN=false will
  // use npm instead
  useYarn: process.env.RI_USE_YARN !== 'false',
  // should we handle yarn workspaces?  if this is true (default)
  // then we will stop recursing if a package.json has the "workspaces"
  // property and we will allow `yarn` to do its thing.
  yarnWorkspaces: process.env.RI_YARN_WORKSPACES !== 'false',
  // if truthy, will run extra checks to see if there is a package-lock.json
  // or yarn.lock file in a given directory and use that installer if so.
  detectLockFiles: process.env.RI_DETECT_LOCK_FILES !== 'false',
  // what kind of logging should be done on the spawned processes?
  // if this exists and it is not errors it will log everything
  // otherwise it will only log stderr and spawn errors
  log: process.env.RI_LOG || 'errors',
  // max depth to recurse?
  maxDepth: process.env.RI_MAX_DEPTH || Infinity,
  // do not install at the root directory?
  ignoreRoot: Boolean(process.env.RI_IGNORE_ROOT),
  // an array (or comma separated string for env var) of directories
  // to skip while recursing. if array, can pass functions which
  // return a boolean after receiving the dir path and fs.Dirent args
  // @see https://nodejs.org/api/fs.html#fs_class_fs_dirent
  skipDirectories: process.env.RI_SKIP_DIRS
    ? process.env.RI_SKIP_DIRS.split(',').map(str => str.trim())
    : undefined,
  // just run through and log the actions that would be taken?
  dry: Boolean(process.env.RI_DRY_RUN),
  ...config
});

function handleSpawnedProcess(dir, log, proc) {
  return new Promise((resolve, reject) => {
    proc.on('error', error => {
      console.log(`
----------------
  [RI] | [ERROR] | Failed to Spawn Process
  - Path:   ${dir}
  - Reason: ${error.message}
----------------
  `);
      reject(error);
    });

    if (log) {
      proc.stderr.on('data', data => {
        console.error(`[RI] | [${dir}] | ${data}`);
      });
    }

    if (log && log !== 'errors') {
      proc.stdout.on('data', data => {
        console.log(`[RI] | [${dir}] | ${data}`);
      });
    }

    proc.on('close', code => {
      if (log && log !== 'errors') {
        console.log(`
----------------
  [RI] | [COMPLETE] | Spawned Process Closed
  - Path: ${dir}
  - Code: ${code}
----------------
        `);
      }
      if (code === 0) {
        resolve();
      } else {
        reject(
          new Error(
            `[RI] | [ERROR] | [${dir}] | failed to install with exit code ${code}`
          )
        );
      }
    });
  });
}

async function recurseDirectory(rootDir, config) {
  const {
    useYarn,
    yarnWorkspaces,
    detectLockFiles,
    log,
    maxDepth,
    ignoreRoot,
    skipDirectories,
    dry
  } = config;

  const installPromises = [];

  function install(cmd, folder, relativeDir) {
    const proc = cp.spawn(cmd, ['install'], {
      cwd: folder,
      env: process.env
    });
    installPromises.push(handleSpawnedProcess(relativeDir, log, proc));
  }

  function shouldSkipFile(filePath, file) {
    if (!file.isDirectory() || file.name === 'node_modules') {
      return true;
    }
    if (!skipDirectories) {
      return false;
    }
    return skipDirectories.some(check =>
      typeof check === 'function' ? check(filePath, file) : check === file.name
    );
  }

  async function getInstallCommand(folder) {
    let cmd = useYarn ? 'yarn' : 'npm';
    if (detectLockFiles) {
      const [hasYarnLock, hasPackageLock] = await Promise.all([
        fs
          .readFile(path.join(folder, 'yarn.lock'))
          .then(() => true)
          .catch(() => false),
        fs
          .readFile(path.join(folder, 'package-lock.json'))
          .then(() => true)
          .catch(() => false)
      ]);
      if (cmd === 'yarn' && !hasYarnLock && hasPackageLock) {
        cmd = 'npm';
      } else if (cmd === 'npm' && !hasPackageLock && hasYarnLock) {
        cmd = 'yarn';
      }
    }
    return cmd;
  }

  async function installRecursively(folder, depth = 0) {
    if (dry || (log && log !== 'errors')) {
      console.log('[RI] | Check Directory --> ', folder);
    }

    let pkg;

    if (folder !== rootDir || !ignoreRoot) {
      try {
        // Check if package.json exists, if it doesnt this will error and move on
        pkg = JSON.parse(await fs.readFile(path.join(folder, 'package.json')));
        // get the command that we should use.  if lock checking is enabled it will
        // also determine what installer to use based on the available lock files
        const cmd = await getInstallCommand(folder);
        const relativeDir = `${path.basename(rootDir)} -> ./${path.relative(
          rootDir,
          folder
        )}`;
        if (dry || (log && log !== 'errors')) {
          console.log(
            `[RI] | Performing (${cmd} install) at path "${relativeDir}"`
          );
        }
        if (!dry) {
          install(cmd, folder, relativeDir);
        }
      } catch {
        // do nothing when error caught as it simply indicates package.json likely doesnt
        // exist.
      }
    }

    if (
      depth >= maxDepth ||
      (pkg && useYarn && yarnWorkspaces && pkg.workspaces)
    ) {
      // if we have reached maxDepth or if our package.json in the current directory
      // contains yarn workspaces then we use yarn for installing then this is the last
      // directory we will attempt to install.
      return;
    }

    const files = await fs.readdir(folder, { withFileTypes: true });

    return Promise.all(
      files.map(file => {
        const filePath = path.join(folder, file.name);
        return shouldSkipFile(filePath, file)
          ? undefined
          : installRecursively(filePath, depth + 1);
      })
    );
  }

  await installRecursively(rootDir);
  await Promise.all(installPromises);
}

async function startRecursiveInstall(directories, _config) {
  const config = getConfig(_config);
  const promise = Array.isArray(directories)
    ? Promise.all(directories.map(rootDir => recurseDirectory(rootDir, config)))
    : recurseDirectory(directories, config);
  await promise;
}

if (AUTO_RUN) {
  startRecursiveInstall(process.cwd());
}

module.exports = startRecursiveInstall;


And with it being used:

const installRecursively = require('./recursive-install');

installRecursively(process.cwd(), { dry: true })
3

[For macOS, Linux users]:

I created a bash file to install all dependencies in the project and nested folder.

find . -name node_modules -prune -o -name package.json -execdir npm install \;

Explain: In the root directory, exclude the node_modules folder (even inside nested folders), find the directory that has the package.json file then run the npm install command.

In case you just want to find on specified folders (eg: abc123, def456 folder), run as below:

find ./abc123/* ./def456/* -name node_modules -prune -o -name package.json -execdir npm install \;
Long Nguyen
  • 9,898
  • 5
  • 53
  • 52
2
find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c "cd '{}' && npm install" \;
2

To run npm install on every subdirectory you can do something like:

"scripts": {
  ...
  "install:all": "for D in */; do npm install --cwd \"${D}\"; done"
}

where

install:all is just the name of the script, you can name it whatever you please

D Is the name of the directory at the current iteration

*/ Specifies where you want to look for subdirectories. directory/*/ will list all directories inside directory/ and directory/*/*/ will list all directories two levels in.

npm install -cwd install all dependencies in the given folder

You could also run several commands, for example:

for D in */; do echo \"Installing stuff on ${D}\" && npm install --cwd \"${D}\"; done

will print "Installing stuff on your_subfolder/" on every iteration.

This works for yarn too

whtlnv
  • 2,109
  • 1
  • 25
  • 26
0

Any language that can get a list of directories and run shell commands can do this for you.

I know it isn't the answer OP was going for exactly, but it's one that will always work. You need to create an array of subdirectory names, then loop over them and run npm i, or whatever command you're needing to run.

For reference, I tried npm i **/, which just installed the modules from all the subdirectories in the parent. It's unintuitive as hell, but needless to say it's not the solution you need.