ci.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. const reifyFinish = require('../utils/reify-finish.js')
  2. const runScript = require('@npmcli/run-script')
  3. const fs = require('node:fs/promises')
  4. const path = require('node:path')
  5. const { log, time } = require('proc-log')
  6. const validateLockfile = require('../utils/validate-lockfile.js')
  7. const ArboristWorkspaceCmd = require('../arborist-cmd.js')
  8. const getWorkspaces = require('../utils/get-workspaces.js')
  9. class CI extends ArboristWorkspaceCmd {
  10. static description = 'Clean install a project'
  11. static name = 'ci'
  12. // These are in the order they will show up in when running "-h"
  13. static params = [
  14. 'install-strategy',
  15. 'legacy-bundling',
  16. 'global-style',
  17. 'omit',
  18. 'include',
  19. 'strict-peer-deps',
  20. 'foreground-scripts',
  21. 'ignore-scripts',
  22. 'allow-git',
  23. 'audit',
  24. 'bin-links',
  25. 'fund',
  26. 'dry-run',
  27. ...super.params,
  28. ]
  29. async exec () {
  30. if (this.npm.global) {
  31. throw Object.assign(new Error('`npm ci` does not work for global packages'), {
  32. code: 'ECIGLOBAL',
  33. })
  34. }
  35. const dryRun = this.npm.config.get('dry-run')
  36. const ignoreScripts = this.npm.config.get('ignore-scripts')
  37. const where = this.npm.prefix
  38. const Arborist = require('@npmcli/arborist')
  39. const opts = {
  40. ...this.npm.flatOptions,
  41. packageLock: true, // npm ci should never skip lock files
  42. path: where,
  43. save: false, // npm ci should never modify the lockfile or package.json
  44. workspaces: this.workspaceNames,
  45. }
  46. // generate an inventory from the virtual tree in the lockfile
  47. const virtualArb = new Arborist(opts)
  48. try {
  49. await virtualArb.loadVirtual()
  50. } catch (err) {
  51. log.verbose('loadVirtual', err.stack)
  52. const msg =
  53. 'The `npm ci` command can only install with an existing package-lock.json or\n' +
  54. 'npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 or\n' +
  55. 'later to generate a package-lock.json file, then try again.'
  56. throw this.usageError(msg)
  57. }
  58. const virtualInventory = new Map(virtualArb.virtualTree.inventory)
  59. // Now we make our real Arborist.
  60. // We need a new one because the virtual tree fromt the lockfile can have extraneous dependencies in it that won't install on this platform
  61. const arb = new Arborist(opts)
  62. await arb.buildIdealTree()
  63. // Verifies that the packages from the ideal tree will match the same versions that are present in the virtual tree (lock file).
  64. const errors = validateLockfile(virtualInventory, arb.idealTree.inventory)
  65. if (errors.length) {
  66. throw this.usageError(
  67. '`npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. ' +
  68. 'Please update your lock file with `npm install` before continuing.\n\n' +
  69. errors.join('\n')
  70. )
  71. }
  72. if (!dryRun) {
  73. const workspacePaths = await getWorkspaces([], {
  74. path: this.npm.localPrefix,
  75. includeWorkspaceRoot: true,
  76. })
  77. // Only remove node_modules after we've successfully loaded the virtual tree and validated the lockfile
  78. await time.start('npm-ci:rm', async () => {
  79. return await Promise.all([...workspacePaths.values()].map(async modulePath => {
  80. const fullPath = path.join(modulePath, 'node_modules')
  81. // get the list of entries so we can skip the glob for performance
  82. const entries = await fs.readdir(fullPath, null).catch(() => [])
  83. return Promise.all(entries.map(folder => {
  84. return fs.rm(path.join(fullPath, folder), { force: true, recursive: true })
  85. }))
  86. }))
  87. })
  88. }
  89. await arb.reify(opts)
  90. // run the same set of scripts that `npm install` runs.
  91. if (!ignoreScripts) {
  92. const scripts = [
  93. 'preinstall',
  94. 'install',
  95. 'postinstall',
  96. 'prepublish', // XXX should we remove this finally??
  97. 'preprepare',
  98. 'prepare',
  99. 'postprepare',
  100. ]
  101. const scriptShell = this.npm.config.get('script-shell') || undefined
  102. for (const event of scripts) {
  103. await runScript({
  104. path: where,
  105. args: [],
  106. scriptShell,
  107. stdio: 'inherit',
  108. event,
  109. })
  110. }
  111. }
  112. await reifyFinish(this.npm, arb)
  113. }
  114. }
  115. module.exports = CI