Cách tốt nhất để chạy cài đặt npm cho các thư mục lồng nhau?


128

Cách chính xác nhất để cài đặt npm packagestrong các thư mục con lồng nhau là gì?

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

Cách tốt nhất để có là gì packagestrong /my-sub-moduleđược cài đặt tự động khi npm installchạy ở my-app?


Tôi nghĩ điều ngớ ngẩn nhất là có một tệp package.json duy nhất ở đầu dự án của bạn.
Robert Moskal

Một ý tưởng sẽ là sử dụng một tập lệnh npm chạy một tệp bash.
Davin Tryon

Điều này có thể không được thực hiện với một modificaiton về cách hoạt động của các đường dẫn cục bộ ?: stackoverflow.com/questions/14381898/…
Evanss

Câu trả lời:


26

Nếu bạn muốn chạy một lệnh duy nhất để cài đặt các gói npm trong các thư mục con lồng nhau, bạn có thể chạy một tập lệnh qua npmvà chính package.jsontrong thư mục gốc của mình. Tập lệnh sẽ truy cập vào mọi thư mục con và chạy npm install.

Dưới đây là một .jskịch bản sẽ đạt được kết quả mong muốn:

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

Lưu ý rằng đây là một ví dụ được lấy từ bài báo StrongLoop đề cập cụ thể node.jscấu trúc dự án mô-đun (bao gồm các thành phần và package.jsontệp được lồng vào nhau ).

Như đã đề xuất, bạn cũng có thể đạt được điều tương tự với một tập lệnh bash.

CHỈNH SỬA: Làm cho mã hoạt động trong Windows


1
Tuy nhiên, phải phức tạp, cảm ơn vì liên kết bài viết.
WHITECOLOR

Mặc dù cấu trúc dựa trên 'thành phần' là một cách khá tiện dụng để thiết lập một ứng dụng nút, nó có thể là quá mức cần thiết trong giai đoạn đầu của ứng dụng để phá vỡ các tệp package.json riêng biệt, v.v. Ý tưởng này có xu hướng thành hiện thực khi ứng dụng phát triển và bạn muốn các mô-đun / dịch vụ riêng biệt một cách hợp pháp. Nhưng có, chắc chắn là quá phức tạp nếu không cần thiết.
snozza

3
Mặc dù có một tập lệnh bash sẽ làm được, nhưng tôi thích cách thực hiện của nodejs để có tính di động tối đa giữa Windows có trình bao DOS và Linux / Mac có trình bao Unix.
truthadjustr

270

Tôi thích sử dụng post-install hơn, nếu bạn biết tên của subir lồng nhau. Trong package.json:

"scripts": {
  "postinstall": "cd nested_dir && npm install",
  ...
}

10
những gì về nhiều thư mục? "cd nested_dir && npm install && cd .. & cd nested_dir2 && npm install" ??
Emre

1
@Emre yes - chính là nó.
Chàng trai

2
@Scott, bạn không thể đặt thư mục tiếp theo vào bên trong package.json như thế nào "postinstall": "cd nested_dir2 && npm install"cho mỗi thư mục?
Aron

1
@Aron Điều gì xảy ra nếu bạn muốn có hai thư mục con trong thư mục mẹ tên?
Alec

28
@Emre Điều đó sẽ hoạt động, các biểu mẫu con có thể sạch hơn một chút: "(cd nested_dir && npm install); (cd nested_dir2 && npm install); ..."
Alec

49

Theo câu trả lời của @ Scott, tập lệnh install | postinstall là cách đơn giản nhất miễn là tên thư mục con được biết đến. Đây là cách tôi chạy nó cho nhiều dirs phụ. Ví dụ, giả vờ chúng ta có api/, web/shared/tiểu dự án trong một gốc monorepo:

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

1
Giải pháp hoàn hảo. Cảm ơn vì đã chia sẻ :-)
Rahul Soni

1
Cảm ơn vì câu trả lời. Làm việc cho tôi.
AMIC MING

5
Sử dụng tốt ( )để tạo phiếu bầu và tránh cd api && npm install && cd ...
Cameron Hudson

4
Đó phải là câu trả lời được lựa chọn!
tmos

3
Tôi gặp lỗi này khi chạy npm installở cấp cao nhất:"(cd was unexpected at this time."
Ông Polywhirl

22

Giải pháp của tôi là rất giống nhau. Pure Node.js

Tập lệnh sau kiểm tra tất cả các thư mục con (đệ quy) miễn là chúng có package.jsonvà chạy npm installtrong mỗi thư mục đó. Người ta có thể thêm ngoại lệ cho nó: các thư mục được phép không có package.json. Trong ví dụ dưới đây, một thư mục như vậy là "các gói". Người ta có thể chạy nó như một tập lệnh "cài đặt sẵn".

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

3
kịch bản của bạn là tốt. Tuy nhiên, vì mục đích cá nhân của mình, tôi muốn loại bỏ 'if condition' đầu tiên để có được 'npm install' lồng nhau sâu!
Guilherme Caraciolo

21

Chỉ để tham khảo trong trường hợp mọi người bắt gặp câu hỏi này. Bây giờ bạn có thể:

  • Thêm một package.json vào một thư mục con
  • Cài đặt thư mục con này dưới dạng liên kết tham chiếu trong gói chính .json:

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


2
Lưu ý rằng các phụ thuộc được cài đặt trong thư mục gốc. Tôi nghi ngờ rằng nếu bạn thậm chí đang xem xét mẫu này, bạn muốn các phụ thuộc của thư mục con package.json trong thư mục con.
Cody Allan Taylor

Ý anh là gì? Các phụ thuộc của gói thư mục con nằm trong package.json trong thư mục con.
Jelmer Jellema

(sử dụng npm v6.6.0 & node v8.15.0) - Thiết lập một ví dụ cho chính bạn. mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;Bây giờ, hãy đợi ... bạn chỉ cần cài đặt thủ công các phụ thuộc trong "b", đó không phải là điều xảy ra khi bạn sao chép một dự án mới. rm -rf node_modules ; cd .. ; npm install --save ./b. Bây giờ liệt kê node_modules, sau đó liệt kê b.
Cody Allan Taylor

1
Ah bạn có nghĩa là các mô-đun. Có, node_modules cho b sẽ được cài đặt trong a / node_modules. Điều này có ý nghĩa, bởi vì bạn sẽ yêu cầu / bao gồm các mô-đun như một phần của mã chính, không phải như một mô-đun nút "thực". Vì vậy, một "request ('thnking2')" sẽ tìm kiếm qua2 trong a / node_modules.
Jelmer Jellema

Tôi đang cố gắng tạo mã và muốn một gói thư mục con được chuẩn bị đầy đủ để chạy, bao gồm node_modules của riêng nó. Nếu tôi tìm thấy giải pháp, tôi sẽ đảm bảo cập nhật!
ohsully

19

Trường hợp sử dụng 1 : Nếu bạn muốn có thể chạy các lệnh npm từ bên trong mỗi thư mục con (trong đó mỗi package.json là), bạn sẽ cần phải sử dụng postinstall.

Vì tôi vẫn thường sử dụng npm-run-all, tôi sử dụng nó để giữ cho nó đẹp và ngắn gọn (phần trong 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:*"
}

Điều này có thêm lợi ích mà tôi có thể cài đặt tất cả cùng một lúc hoặc riêng lẻ. Nếu bạn không cần điều này hoặc không muốn npm-run-allnhư một phụ thuộc, hãy xem câu trả lời của demisx (sử dụng biểu quyết con trong postinstall).

Trường hợp sử dụng 2 : Nếu bạn sẽ chạy tất cả các lệnh npm từ thư mục gốc (và, ví dụ: sẽ không sử dụng các tập lệnh npm trong các thư mục con), bạn có thể chỉ cần cài đặt từng thư mục con giống như bất kỳ tùy chọn nào:

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

Trong trường hợp thứ hai, đừng ngạc nhiên khi bạn không tìm thấy bất kỳ node_moduleshoặc package-lock.jsontệp nào trong các thư mục con - tất cả các gói sẽ được cài đặt trong thư mục gốc node_modules, đó là lý do tại sao bạn sẽ không thể chạy các lệnh npm của mình (đó yêu cầu phụ thuộc) từ bất kỳ thư mục con nào của bạn.

Nếu bạn không chắc chắn, trường hợp sử dụng 1 luôn hoạt động.


Thật tuyệt khi mỗi mô-đun con có tập lệnh cài đặt riêng và sau đó thực thi tất cả chúng trong postinstall. run-pkhông cần thiết, nhưng sau đó dài dòng hơn"postinstall": "npm run install:a && npm run install:b"
Qwerty

Có, bạn có thể sử dụng &&mà không cần run-p. Nhưng như bạn nói, nó ít đọc hơn. Một nhược điểm khác (run-p giải quyết được vì các cài đặt chạy song song) là nếu một lỗi không thành công thì không có tập lệnh nào khác bị ảnh hưởng
Don Vaughn

3

Thêm hỗ trợ của Windows vào câu trả lời của snozza , cũng như bỏ qua node_modulesthư mục nếu có.

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

Bạn chắc chắn có thể. Tôi đã cập nhật giải pháp của mình để bỏ qua thư mục node_modules.
Ghostrydr

2

Lấy cảm hứng từ các tập lệnh được cung cấp ở đây, tôi đã xây dựng một ví dụ có thể định cấu hình:

  • có thể được thiết lập để sử dụng yarnhoặcnpm
  • có thể được thiết lập để xác định lệnh sử dụng dựa trên các tệp khóa để nếu bạn đặt nó để sử dụng yarnnhưng một thư mục chỉ có một package-lock.jsonthì nó sẽ sử dụngnpm cho thư mục đó (mặc định là true).
  • cấu hình ghi nhật ký
  • chạy các cài đặt song song bằng cách sử dụng cp.spawn
  • có thể chạy khô để cho bạn xem nó sẽ làm gì trước
  • có thể được chạy dưới dạng một hàm hoặc chạy tự động bằng cách sử dụng env vars
    • khi chạy dưới dạng một hàm, tùy chọn cung cấp mảng thư mục để kiểm tra
  • trả về một lời hứa sẽ giải quyết khi hoàn thành
  • cho phép thiết lập độ sâu tối đa để xem nếu cần
  • biết dừng đệ quy nếu nó tìm thấy một thư mục có yarn workspaces (có thể định cấu hình)
  • cho phép bỏ qua các thư mục bằng cách sử dụng env var được phân tách bằng dấu phẩy hoặc bằng cách chuyển cấu hình một mảng các chuỗi để khớp với hoặc một hàm nhận tên tệp, đường dẫn tệp, và fs.Dirent obj và mong đợi một kết quả 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;

Và với nó đang được sử dụng:

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

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

1

Nếu bạn có findtiện ích trên hệ thống của mình, bạn có thể thử chạy lệnh sau trong thư mục gốc của ứng dụng:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

Về cơ bản, hãy tìm tất cả package.jsoncác tệp và chạy npm installtrong thư mục đó, bỏ qua tất cả các node_modulesthư mục.


1
Câu trả lời chính xác. Chỉ cần lưu ý rằng bạn cũng có thể bỏ qua các đường dẫn bổ sung với:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Evan Moran
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.