Cara terbaik untuk menjalankan npm install untuk folder bersarang?

128

Apa cara paling benar untuk menginstal npm packagesdi sub folder bersarang?

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

Apa cara terbaik untuk telah packagesdi /my-sub-modulediinstal secara otomatis ketika npm installberjalan di my-app?

WARNA PUTIH
sumber
Saya pikir hal yang paling idiomatis adalah memiliki satu file package.json di proyek Anda.
Robert Moskal
Salah satu ide adalah menggunakan skrip npm yang menjalankan file bash.
Davin Tryon
Bisakah ini tidak dilakukan dengan modifikasi pada cara kerja jalur lokal ?: stackoverflow.com/questions/14381898/…
Evanss

Jawaban:

26

Jika Anda ingin menjalankan satu perintah untuk menginstal paket npm di subfolder bersarang, Anda dapat menjalankan skrip via npmdan main package.jsondi direktori root Anda. Skrip akan mengunjungi setiap subdirektori dan berjalannpm install .

Di bawah ini adalah .jsskrip yang akan mencapai hasil yang diinginkan:

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' })
})

Perhatikan bahwa ini adalah contoh yang diambil dari artikel StrongLoop yang secara khusus membahas node.jsstruktur proyek modular (termasuk komponen dan package.jsonfile bersarang ).

Seperti yang disarankan, Anda juga bisa mendapatkan hal yang sama dengan skrip bash.

EDIT: Membuat kode berfungsi di Windows

snozza.dll
sumber
1
Untuk rumit kan, terima kasih untuk tautan artikelnya.
WHITECOLOR
Meskipun struktur berbasis 'komponen' adalah cara yang cukup berguna untuk menyiapkan aplikasi node, mungkin berlebihan pada tahap awal aplikasi untuk memecah file package.json yang terpisah, dll. Ide tersebut cenderung membuahkan hasil ketika aplikasi tumbuh dan Anda secara sah menginginkan modul / layanan terpisah. Tapi ya, pasti ribet juga kalau gak perlu.
snozza
3
Meskipun ya skrip bash akan dilakukan, tetapi saya lebih suka cara nodejs melakukannya untuk portabilitas maksimum antara Windows yang memiliki shell DOS dan Linux / Mac yang memiliki shell Unix.
truthadjustr
270

Saya lebih suka menggunakan post-install, jika Anda tahu nama-nama subdirektori bersarang. Masuk package.json:

"scripts": {
  "postinstall": "cd nested_dir && npm install",
  ...
}
Scott
sumber
10
bagaimana dengan banyak folder? "cd nested_dir && npm instal && cd .. & cd nested_dir2 && npm instal" ??
Emre
1
@Emre ya - itu saja.
Guy
2
@Scott Tidak bisakah Anda meletakkan folder berikutnya di dalam package.json seperti "postinstall": "cd nested_dir2 && npm install"untuk setiap folder?
Aron
1
@Aron Bagaimana jika Anda menginginkan dua subdirektori dalam nama direktori induk?
Alec
29
@Emre Itu seharusnya bekerja, subkulit mungkin sedikit lebih bersih: "(cd nested_dir && npm install); (cd nested_dir2 && npm install); ..."
Alec
49

Sesuai jawaban @ Scott, script install | postinstall adalah cara termudah selama nama sub-direktori diketahui. Ini adalah bagaimana saya menjalankannya untuk beberapa sub dirs. Misalnya, anggap saja kita punya api/, web/dan shared/subproyek di root monorepo:

// In monorepo root package.json
{
...
 "scripts": {
    "postinstall": "(cd api && npm install); (cd web && npm install); (cd shared && npm install)"
  },
}
demisx
sumber
1
Solusi sempurna. Terima kasih telah berbagi :-)
Rahul Soni
1
Terima kasih atas jawabannya. Bekerja untuk saya.
AMIC MING
5
Penggunaan yang baik ( )untuk membuat subkulit dan menghindari cd api && npm install && cd ...
Cameron Hudson
4
Itu harus menjadi jawaban yang dipilih!
tmos
3
Saya mendapatkan kesalahan ini saat menjalankan npm installdi tingkat atas:"(cd was unexpected at this time."
Tn. Polywhirl
22

Solusi saya sangat mirip. Node.js murni

Skrip berikut memeriksa semua subfolder (secara rekursif) selama subfolder tersebut memiliki package.jsondan berjalan npm installdi masing-masing subfolder . Seseorang dapat menambahkan pengecualian padanya: folder tidak boleh memiliki package.json. Dalam contoh di bawah ini, salah satu folder tersebut adalah "paket". Seseorang dapat menjalankannya sebagai skrip "prainstal".

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
sumber
3
naskahmu bagus. Namun, untuk tujuan pribadi saya, saya lebih suka menghapus 'jika kondisi' pertama untuk mendapatkan 'npm install' bersarang yang dalam!
Guilherme Caraciolo
21

Hanya untuk referensi jika orang menemukan pertanyaan ini. Sekarang kamu bisa:

  • Tambahkan package.json ke subfolder
  • Instal subfolder ini sebagai link referensi di package.json utama:

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

Jelmer Jellema
sumber
2
Perhatikan bahwa dependensi diinstal di folder root. Saya menduga bahwa jika Anda bahkan mempertimbangkan pola ini, Anda menginginkan dependensi dari sub-direktori package.json di sub-direktori.
Cody Allan Taylor
Maksud kamu apa? Dependensi untuk subfolder-paket ada di package.json di subfolder.
Jelmer Jellema
(menggunakan npm v6.6.0 & node v8.15.0) - Siapkan contoh untuk Anda sendiri. mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;Sekarang tunggu ... Anda baru saja menginstal dependensi secara manual di "b", bukan itu yang terjadi saat Anda mengkloning proyek baru. rm -rf node_modules ; cd .. ; npm install --save ./b. Sekarang daftar node_modules, lalu daftar b.
Cody Allan Taylor
1
Ah maksudmu modulnya. Ya, node_modules untuk b akan diinstal di / node_modules. Yang masuk akal, karena Anda akan memerlukan / menyertakan modul sebagai bagian dari kode utama, bukan sebagai modul node "nyata". Jadi "memerlukan ('throug2')" akan mencari melalui2 di / node_modules.
Jelmer Jellema
Saya mencoba melakukan pembuatan kode dan menginginkan paket subfolder yang sepenuhnya siap untuk dijalankan, termasuk node_modules-nya sendiri. Jika saya menemukan solusinya, saya akan memastikan untuk memperbarui!
ohsully
19

Kasus Penggunaan 1 : Jika Anda ingin dapat menjalankan perintah npm dari dalam setiap subdirektori (di mana setiap package.json berada), Anda perlu menggunakanpostinstall .

Seperti yang sering saya gunakan npm-run-all, saya menggunakannya agar tetap bagus dan singkat (bagian dalam 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:*"
}

Ini memiliki manfaat tambahan yang dapat saya instal sekaligus, atau satu per satu. Jika Anda tidak membutuhkan ini atau tidak ingin npm-run-allsebagai dependensi, lihat jawaban demisx (menggunakan subkulit di postinstall).

Kasus Penggunaan 2 : Jika Anda akan menjalankan semua perintah npm dari direktori root (dan, misalnya, tidak akan menggunakan skrip npm di subdirektori), Anda cukup menginstal setiap subdirektori seperti yang Anda lakukan pada dependecy:

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

Dalam kasus terakhir, jangan kaget bahwa Anda tidak menemukan node_modulesatau package-lock.jsonfile apa pun di sub-direktori - semua paket akan diinstal di root node_modules, itulah sebabnya Anda tidak akan dapat menjalankan perintah npm Anda (itu membutuhkan ketergantungan) dari salah satu subdirektori Anda.

Jika Anda tidak yakin, use case 1 selalu berhasil.

Don Vaughn
sumber
Sangat menyenangkan jika setiap submodule memiliki skrip instal sendiri dan kemudian menjalankan semuanya di postinstall. run-ptidak perlu, tapi lebih bertele"postinstall": "npm run install:a && npm run install:b"
Qwerty
Ya, Anda dapat menggunakan &&tanpa run-p. Tapi seperti yang Anda katakan, itu kurang terbaca. Kelemahan lain (run-p terpecahkan karena pemasangan berjalan paralel) adalah jika salah satu gagal, tidak ada skrip lain yang terpengaruh
Don Vaughn
3

Menambahkan dukungan Windows ke jawaban snozza , serta melewatkannode_modules folder jika ada.

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
sumber
Anda pasti bisa. Saya telah memperbarui solusi saya untuk melewati folder node_modules.
Ghostrydr
2

Terinspirasi oleh skrip yang disediakan di sini, saya membuat contoh yang dapat dikonfigurasi yang:

  • dapat diatur untuk digunakan yarnataunpm
  • dapat diatur untuk menentukan perintah yang akan digunakan berdasarkan file kunci sehingga jika Anda mengaturnya untuk digunakan yarntetapi direktori hanya memiliki package-lock.jsonitu akan digunakan npmuntuk direktori itu (default ke true).
  • konfigurasikan logging
  • menjalankan instalasi secara paralel menggunakan cp.spawn
  • dapat melakukan dry run agar Anda dapat melihat apa yang akan dilakukannya pertama kali
  • dapat dijalankan sebagai fungsi atau dijalankan secara otomatis menggunakan env vars
    • ketika dijalankan sebagai fungsi, secara opsional menyediakan larik direktori untuk diperiksa
  • mengembalikan janji yang diselesaikan saat selesai
  • memungkinkan pengaturan kedalaman maksimal untuk dilihat jika diperlukan
  • tahu untuk berhenti berulang jika menemukan folder dengan yarn workspaces (dapat dikonfigurasi)
  • memungkinkan melewatkan direktori menggunakan env var yang dipisahkan koma atau dengan meneruskan konfigurasi array string untuk dicocokkan atau fungsi yang menerima nama file, jalur file, dan objek fs.Dirent dan mengharapkan hasil boolean.
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;

Dan dengan itu digunakan:

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

installRecursively(process.cwd(), { dry: true })
Braden Rockwell Napier
sumber
1

Jika Anda memiliki findutilitas di sistem Anda, Anda dapat mencoba menjalankan perintah berikut di direktori root aplikasi Anda:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

Pada dasarnya, temukan semua package.jsonfile dan jalankan npm installdi direktori itu, lewati semua node_modulesdirektori.

Moha si unta yang maha kuasa
sumber
1
Jawaban yang bagus. Sekadar catatan bahwa Anda juga dapat menghilangkan jalur tambahan dengan:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Evan Moran