วิธีที่ดีที่สุดในการเรียกใช้การติดตั้ง npm สำหรับโฟลเดอร์ที่ซ้อนกัน


129

วิธีใดเป็นวิธีที่ถูกต้องที่สุดในการติดตั้งnpm packagesในโฟลเดอร์ย่อยที่ซ้อนกัน

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

อะไรคือวิธีที่ดีที่สุดpackagesในการ/my-sub-moduleติดตั้งโดยอัตโนมัติเมื่อnpm installเรียกใช้my-app?


ฉันคิดว่าสิ่งที่เป็นสำนวนที่สุดคือการมีไฟล์ package.json เดียวที่ถึงโครงการของคุณ
Robert Moskal

แนวคิดอย่างหนึ่งคือการใช้สคริปต์ npm ที่รันไฟล์ bash
Davin Tryon

ไม่สามารถทำได้ด้วย modificaiton วิธีการทำงานของเส้นทางท้องถิ่น: stackoverflow.com/questions/14381898/…
Evanss

คำตอบ:


26

หากคุณต้องการรันคำสั่งเดียวเพื่อติดตั้งแพ็กเกจ npm ในโฟลเดอร์ย่อยที่ซ้อนกันคุณสามารถรันสคริปต์ผ่านnpmและเป็นหลักpackage.jsonในไดเร็กทอรีรากของคุณ npm installสคริปต์ที่จะเข้าชมทุกไดเรกทอรีย่อยและเรียกใช้

ด้านล่างนี้เป็น.jsสคริปต์ที่จะได้ผลลัพธ์ที่ต้องการ:

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

โปรดทราบว่านี่เป็นตัวอย่างที่นำมาจากบทความStrongLoopที่กล่าวถึงnode.jsโครงสร้างโครงการแบบแยกส่วนโดยเฉพาะ(รวมถึงส่วนประกอบและpackage.jsonไฟล์ที่ซ้อนกัน)

ตามที่แนะนำคุณสามารถทำสิ่งเดียวกันได้ด้วยสคริปต์ทุบตี

แก้ไข: ทำให้รหัสทำงานใน Windows


1
แม้ว่าจะซับซ้อนก็ตามขอบคุณสำหรับลิงค์บทความ
WHITECOLOR

ในขณะที่โครงสร้างตาม 'องค์ประกอบ' เป็นวิธีที่สะดวกในการตั้งค่าแอปโหนด แต่ในช่วงแรกของแอปอาจมีการใช้งานมากเกินไปที่จะแยกไฟล์ package.json แยกต่างหากเป็นต้นแนวคิดนี้มีแนวโน้มที่จะบรรลุผลเมื่อแอปเติบโตขึ้นและ คุณต้องการโมดูล / บริการแยกต่างหาก แต่ใช่แน่นอนซับซ้อนเกินไปถ้าไม่จำเป็น
snozza

3
แม้ว่าจะใช่ bash script แต่ฉันชอบวิธี nodejs ในการทำเพื่อความสะดวกในการพกพาสูงสุดระหว่าง Windows ซึ่งมี DOS shell และ Linux / Mac ซึ่งมี Unix shell
truthadjustr

270

ฉันชอบใช้หลังการติดตั้งถ้าคุณรู้จักชื่อของ subdir ที่ซ้อนกัน ในpackage.json:

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

10
แล้วหลายโฟลเดอร์ล่ะ? "cd nested_dir && npm install && cd .. & cd nested_dir2 && npm install" ??
Emre

1
@ อืมใช่ - นั่นแหละ
ผู้ชาย

2
@Scott คุณไม่สามารถใส่โฟลเดอร์ถัดไปใน package.json ข้างในเหมือนกับ"postinstall": "cd nested_dir2 && npm install"แต่ละโฟลเดอร์ได้หรือไม่?
อารอน

1
@Aron จะเกิดอะไรขึ้นถ้าคุณต้องการไดเร็กทอรีย่อยสองไดเร็กทอรีภายในชื่อพาเรนต์ไดเร็กทอรี?
อเล็กซ์

29
@ เอิ่มนั่นควรใช้งานได้ subshells อาจจะสะอาดกว่าเล็กน้อย: "(cd nested_dir && npm install); (cd nested_dir2 && npm install); ... "
Alec

49

คำตอบของ Per @ Scott สคริปต์การติดตั้ง | postinstall เป็นวิธีที่ง่ายที่สุดตราบใดที่ทราบชื่อไดเรกทอรีย่อย นี่คือวิธีที่ฉันเรียกใช้สำหรับ dirs ย่อยหลายตัว ยกตัวอย่างเช่นหลอกว่าเรามีapi/, web/และshared/โครงการย่อยในราก monorepo นี้:

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

1
โซลูชั่นที่สมบูรณ์แบบ ขอบคุณสำหรับการแบ่งปัน :-)
Rahul Soni

1
ขอบคุณสำหรับคำตอบ. ทำงานให้ฉัน
AMIC MING

5
ใช้( )สร้าง subshells และหลีกเลี่ยงcd api && npm install && cd ..ได้ดี
Cameron Hudson

4
นั่นน่าจะเป็นคำตอบที่เลือก!
tmos

3
ฉันได้รับข้อผิดพลาดนี้เมื่อทำงานnpm installที่ระดับบนสุด:"(cd was unexpected at this time."
Mr.Polywhirl

22

วิธีแก้ปัญหาของฉันคล้ายกันมาก โหนดบริสุทธิ์ js

สคริปต์ต่อไปนี้ตรวจสอบโฟลเดอร์ย่อยทั้งหมด (เรียกซ้ำ) ตราบเท่าที่มีpackage.jsonและเรียกใช้npm installในแต่ละโฟลเดอร์ หนึ่งสามารถเพิ่มข้อยกเว้นไป: package.jsonโฟลเดอร์ที่ได้รับอนุญาตให้ไม่ได้มี ในตัวอย่างด้านล่างโฟลเดอร์ดังกล่าวคือ "แพ็กเกจ" หนึ่งสามารถเรียกใช้เป็นสคริปต์ "ติดตั้งล่วงหน้า"

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
สคริปต์ของคุณดีมาก อย่างไรก็ตามเพื่อจุดประสงค์ส่วนตัวของฉันฉันต้องการลบ 'if condition' ตัวแรกเพื่อให้ได้ 'npm install' ที่ซ้อนกันลึก ๆ !
Guilherme Caraciolo

21

เพียงเพื่อใช้อ้างอิงในกรณีที่ผู้คนเจอคำถามนี้ ตอนนี้คุณสามารถ:

  • เพิ่ม package.json ลงในโฟลเดอร์ย่อย
  • ติดตั้งโฟลเดอร์ย่อยนี้เป็นลิงก์อ้างอิงในแพ็กเกจหลัก json:

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


2
โปรดสังเกตว่าการอ้างอิงถูกติดตั้งในโฟลเดอร์รูท ฉันสงสัยว่าถ้าคุณกำลังพิจารณารูปแบบนี้คุณต้องการการอ้างอิงของ sub-directory package.json ในไดเร็กทอรีย่อย
Cody Allan Taylor

คุณหมายถึงอะไร? การอ้างอิงสำหรับโฟลเดอร์ย่อยแพ็กเกจอยู่ใน package.json ในโฟลเดอร์ย่อย
Jelmer Jellema

(โดยใช้ npm v6.6.0 & node v8.15.0) - ตั้งค่าตัวอย่างสำหรับตัวคุณเอง mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;ตอนนี้รอ ... คุณเพิ่งติดตั้งการอ้างอิงด้วยตนเองใน "b" นั่นไม่ใช่สิ่งที่เกิดขึ้นเมื่อคุณโคลนโครงการใหม่ rm -rf node_modules ; cd .. ; npm install --save ./b. ตอนนี้แสดงรายการ node_modules จากนั้นแสดงรายการ b
Cody Allan Taylor

1
คุณหมายถึงโมดูล ใช่ node_modules สำหรับ b จะถูกติดตั้งใน a / node_modules ซึ่งสมเหตุสมผลเพราะคุณจะต้อง / รวมโมดูลเป็นส่วนหนึ่งของโค้ดหลักไม่ใช่โมดูลโหนด "จริง" ดังนั้น "ต้องใช้ ('throug2')" จะค้นหาผ่าน 2 ใน a / node_modules
Jelmer Jellema

ฉันกำลังพยายามสร้างโค้ดและต้องการโฟลเดอร์ย่อยแพ็กเกจที่พร้อมสำหรับการรันรวมถึง node_modules ของตัวเองด้วย ถ้าฉันพบวิธีแก้ปัญหาฉันจะอัปเดตอย่างแน่นอน!
ohsully

20

ใช้กรณีที่ 1 : หากคุณต้องการให้สามารถรันคำสั่ง npm จากภายในแต่ละไดเร็กทอรีย่อย (โดยที่แต่ละ package.json อยู่) คุณจะต้องใช้postinstallคุณจะต้องใช้

เนื่องจากฉันมักจะใช้npm-run-allอยู่แล้วฉันจึงใช้มันเพื่อให้มันดูดีและสั้น (ส่วนใน 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:*"
}

สิ่งนี้มีประโยชน์เพิ่มเติมที่ฉันสามารถติดตั้งทั้งหมดในครั้งเดียวหรือทีละรายการ หากคุณไม่ต้องการสิ่งนี้หรือไม่ต้องการnpm-run-allเป็นที่พึ่งพาลองดูคำตอบของ demisx (โดยใช้ subshells ใน postinstall)

ใช้กรณีที่ 2 : หากคุณจะรันคำสั่ง npm ทั้งหมดจากไดเร็กทอรีรูท (และตัวอย่างเช่นจะไม่ใช้สคริปต์ npm ในไดเร็กทอรีย่อย) คุณสามารถติดตั้งไดเร็กทอรีย่อยแต่ละไดเร็กทอรีเช่นเดียวกับการอ้างอิงใด ๆ :

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

ในกรณีหลังอย่าแปลกใจที่คุณไม่พบไฟล์node_modulesหรือpackage-lock.jsonไฟล์ใด ๆในไดเร็กทอรีย่อยแพ็กเกจทั้งหมดจะถูกติดตั้งในรูnode_modulesทซึ่งเป็นสาเหตุที่คุณไม่สามารถรันคำสั่ง npm ของคุณได้ (นั่นคือ ต้องการการอ้างอิง) จากไดเร็กทอรีย่อยใด ๆ ของคุณ

หากคุณไม่แน่ใจให้ใช้กรณีที่ 1 ได้ผลเสมอ


เป็นเรื่องดีที่โมดูลย่อยแต่ละโมดูลมีสคริปต์การติดตั้งของตัวเองจากนั้นเรียกใช้งานทั้งหมดใน postinstall run-pไม่จำเป็น แต่มันละเอียดกว่านั้น"postinstall": "npm run install:a && npm run install:b"
Qwerty

ใช่คุณสามารถใช้โดยไม่ต้อง&& run-pแต่อย่างที่บอกว่าอ่านได้น้อย ข้อเสียเปรียบอีกประการหนึ่ง (run-p แก้ไขได้เนื่องจากการติดตั้งทำงานแบบขนาน) คือหากล้มเหลวสคริปต์อื่น ๆ จะไม่ได้รับผลกระทบ
Don Vaughn

3

การเพิ่มการรองรับ Windows ในคำตอบของ snozzaรวมถึงการข้ามnode_modulesโฟลเดอร์หากมี

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

คุณสามารถทำได้ ฉันได้อัปเดตโซลูชันของฉันเพื่อข้ามโฟลเดอร์ node_modules
Ghostrydr

2

ได้รับแรงบันดาลใจจากสคริปต์ที่ให้ไว้ที่นี่ฉันได้สร้างตัวอย่างที่กำหนดค่าได้ซึ่ง:

  • สามารถตั้งค่าเพื่อใช้งานyarnหรือnpm
  • สามารถตั้งค่าเพื่อกำหนดคำสั่งที่จะใช้ตามไฟล์ล็อคดังนั้นหากคุณตั้งค่าให้ใช้yarnแต่ไดเร็กทอรีมีเพียงไดเร็กทอรีเท่านั้นที่package-lock.jsonจะใช้npmสำหรับไดเร็กทอรีนั้น (ค่าเริ่มต้นคือ true)
  • กำหนดค่าการบันทึก
  • รันการติดตั้งแบบขนานโดยใช้ cp.spawn
  • สามารถวิ่งแห้งเพื่อให้คุณเห็นว่าจะทำอะไรก่อน
  • สามารถเรียกใช้เป็นฟังก์ชันหรือเรียกใช้อัตโนมัติโดยใช้ env vars
    • เมื่อรันเป็นฟังก์ชันให้เลือกอาร์เรย์ของไดเร็กทอรีเพื่อตรวจสอบ
  • ส่งคืนสัญญาที่แก้ไขเมื่อเสร็จสิ้น
  • อนุญาตให้ตั้งค่าความลึกสูงสุดเพื่อดูหากจำเป็น
  • รู้ว่าจะหยุดการเรียกซ้ำหากพบโฟลเดอร์ที่มีไฟล์ yarn workspaces (กำหนดค่าได้)
  • อนุญาตให้ข้ามไดเร็กทอรีโดยใช้ env var ที่คั่นด้วยเครื่องหมายจุลภาคหรือโดยการส่ง config อาร์เรย์ของสตริงเพื่อจับคู่หรือฟังก์ชั่นที่รับชื่อไฟล์พา ธ ไฟล์และ fs.Dirent obj และคาดหวังผลลัพธ์บูลีน
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;

และด้วยการใช้:

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

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

1

หากคุณมีfindยูทิลิตี้ในระบบของคุณคุณสามารถลองรันคำสั่งต่อไปนี้ในไดเร็กทอรีรากของแอปพลิเคชันของคุณ:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

โดยทั่วไปค้นหาpackage.jsonไฟล์ทั้งหมดและเรียกใช้npm installในไดเร็กทอรีนั้นโดยข้ามnode_modulesไดเร็กทอรีทั้งหมด


1
คำตอบที่ดี โปรดทราบว่าคุณสามารถละเว้นเส้นทางเพิ่มเติมได้ด้วย:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Evan Moran
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.