codekingpro/portable-devtools
114k
1const { readdir } = require('node:fs/promises')
2const { resolve, join } = require('node:path')
3const { log } = require('proc-log')
4const runScript = require('@npmcli/run-script')
5const pacote = require('pacote')
6const checks = require('npm-install-checks')
7const reifyFinish = require('../utils/reify-finish.js')
8const ArboristWorkspaceCmd = require('../arborist-cmd.js')
9
10class Install extends ArboristWorkspaceCmd {
11 static description = 'Install a package'
12 static name = 'install'
13
14 // These are in the order they will show up in when running "-h" If adding to this list, consider adding also to ci.js
15 static params = [
16 'save',
17 'save-exact',
18 'global',
19 'install-strategy',
20 'legacy-bundling',
21 'global-style',
22 'omit',
23 'include',
24 'strict-peer-deps',
25 'prefer-dedupe',
26 'package-lock',
27 'package-lock-only',
28 'foreground-scripts',
29 'ignore-scripts',
30 'allow-git',
31 'audit',
32 'before',
33 'min-release-age',
34 'bin-links',
35 'fund',
36 'dry-run',
37 'cpu',
38 'os',
39 'libc',
40 ...super.params,
41 ]
42
43 static usage = ['[<package-spec> ...]']
44
45 static async completion (opts) {
46 const { partialWord } = opts
47 // install can complete to a folder with a package.json, or any package.
48 // if it has a slash, then it's gotta be a folder
49 // if it starts with https?://, then just give up, because it's a url
50 if (/^https?:\/\//.test(partialWord)) {
51 // do not complete to URLs
52 return []
53 }
54
55 if (/\//.test(partialWord)) {
56 // Complete fully to folder if there is exactly one match and it is a folder containing a package.json file.
57 // If that is not the case we return 0 matches, which will trigger the default bash complete.
58 const lastSlashIdx = partialWord.lastIndexOf('/')
59 const partialName = partialWord.slice(lastSlashIdx + 1)
60 const partialPath = partialWord.slice(0, lastSlashIdx) || '/'
61
62 const isDirMatch = async sibling => {
63 if (sibling.slice(0, partialName.length) !== partialName) {
64 return false
65 }
66
67 try {
68 const contents = await readdir(join(partialPath, sibling))
69 const result = (contents.indexOf('package.json') !== -1)
70 return result
71 } catch {
72 return false
73 }
74 }
75
76 try {
77 const siblings = await readdir(partialPath)
78 const matches = []
79 for (const sibling of siblings) {
80 if (await isDirMatch(sibling)) {
81 matches.push(sibling)
82 }
83 }
84 if (matches.length === 1) {
85 return [join(partialPath, matches[0])]
86 }
87 // no matches
88 return []
89 } catch {
90 return [] // invalid dir: no matching
91 }
92 }
93 // Note: there used to be registry completion here, but it stopped making sense somewhere around 50,000 packages on the registry
94 }
95
96 async exec (args) {
97 // the /path/to/node_modules/..
98 const globalTop = resolve(this.npm.globalDir, '..')
99 const ignoreScripts = this.npm.config.get('ignore-scripts')
100 const isGlobalInstall = this.npm.global
101 const where = isGlobalInstall ? globalTop : this.npm.prefix
102 const forced = this.npm.config.get('force')
103 const scriptShell = this.npm.config.get('script-shell') || undefined
104
105 // be very strict about engines when trying to update npm itself
106 const npmInstall = args.find(arg => arg.startsWith('npm@') || arg === 'npm')
107 if (isGlobalInstall && npmInstall) {
108 const npmOptions = this.npm.flatOptions
109 const npmManifest = await pacote.manifest(npmInstall, npmOptions)
110 try {
111 checks.checkEngine(npmManifest, npmManifest.version, process.version)
112 } catch (e) {
113 if (forced) {
114 log.warn(
115 'install',
116 `Forcing global npm install with incompatible version ${npmManifest.version} into node ${process.version}`
117 )
118 } else {
119 throw e
120 }
121 }
122 }
123
124 // don't try to install the prefix into itself
125 args = args.filter(a => resolve(a) !== this.npm.prefix)
126
127 // `npm i -g` => "install this package globally"
128 if (isGlobalInstall && !args.length) {
129 args = ['.']
130 }
131
132 // throw usage error if trying to install empty package name to global space, e.g: `npm i -g ""`
133 if (where === globalTop && !args.every(Boolean)) {
134 throw this.usageError()
135 }
136
137 const Arborist = require('@npmcli/arborist')
138 const opts = {
139 ...this.npm.flatOptions,
140 auditLevel: null,
141 path: where,
142 add: args,
143 workspaces: this.workspaceNames,
144 }
145 const arb = new Arborist(opts)
146 await arb.reify(opts)
147
148 if (!args.length && !isGlobalInstall && !ignoreScripts) {
149 const scripts = [
150 'preinstall',
151 'install',
152 'postinstall',
153 'prepublish', // XXX(npm9) should we remove this finally??
154 'preprepare',
155 'prepare',
156 'postprepare',
157 ]
158 for (const event of scripts) {
159 await runScript({
160 path: where,
161 args: [],
162 scriptShell,
163 stdio: 'inherit',
164 event,
165 })
166 }
167 }
168 await reifyFinish(this.npm, arb)
169 }
170}
171
172module.exports = Install
173 