libnpmexec 4.0.8 → 4.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,25 @@
1
1
  const { resolve } = require('path')
2
- const { promisify } = require('util')
3
- const stat = promisify(require('fs').stat)
2
+ const fs = require('@npmcli/fs')
4
3
  const walkUp = require('walk-up-path')
5
4
 
6
- const fileExists = (file) => stat(file)
7
- .then((res) => res.isFile())
8
- .catch(() => false)
9
-
10
- const localFileExists = async (dir, binName, root = '/') => {
11
- root = resolve(root).toLowerCase()
5
+ const fileExists = async (file) => {
6
+ try {
7
+ const res = await fs.stat(file)
8
+ return res.isFile()
9
+ } catch {
10
+ return false
11
+ }
12
+ }
12
13
 
13
- for (const path of walkUp(resolve(dir))) {
14
+ const localFileExists = async (dir, binName, root) => {
15
+ for (const path of walkUp(dir)) {
14
16
  const binDir = resolve(path, 'node_modules', '.bin')
15
17
 
16
18
  if (await fileExists(resolve(binDir, binName))) {
17
19
  return binDir
18
20
  }
19
21
 
20
- if (path.toLowerCase() === root) {
22
+ if (path.toLowerCase() === resolve(root).toLowerCase()) {
21
23
  return false
22
24
  }
23
25
  }
package/lib/index.js CHANGED
@@ -1,27 +1,81 @@
1
- const { delimiter, dirname, resolve } = require('path')
1
+ 'use strict'
2
+
2
3
  const { promisify } = require('util')
3
- const read = promisify(require('read'))
4
4
 
5
5
  const Arborist = require('@npmcli/arborist')
6
6
  const ciDetect = require('@npmcli/ci-detect')
7
+ const crypto = require('crypto')
7
8
  const log = require('proc-log')
8
- const npmlog = require('npmlog')
9
9
  const mkdirp = require('mkdirp-infer-owner')
10
10
  const npa = require('npm-package-arg')
11
+ const npmlog = require('npmlog')
11
12
  const pacote = require('pacote')
13
+ const read = promisify(require('read'))
14
+ const semver = require('semver')
12
15
 
13
- const cacheInstallDir = require('./cache-install-dir.js')
14
16
  const { fileExists, localFileExists } = require('./file-exists.js')
15
17
  const getBinFromManifest = require('./get-bin-from-manifest.js')
16
18
  const noTTY = require('./no-tty.js')
17
19
  const runScript = require('./run-script.js')
18
20
  const isWindows = require('./is-windows.js')
19
- const _localManifest = Symbol('localManifest')
20
21
 
21
- /* istanbul ignore next */
22
- const PATH = (
23
- process.env.PATH || process.env.Path || process.env.path
24
- ).split(delimiter)
22
+ const { dirname, resolve } = require('path')
23
+
24
+ const binPaths = []
25
+
26
+ // when checking the local tree we look up manifests, cache those results by
27
+ // spec.raw so we don't have to fetch again when we check npxCache
28
+ const manifests = new Map()
29
+
30
+ const getManifest = async (spec, flatOptions) => {
31
+ if (!manifests.has(spec.raw)) {
32
+ const manifest = await pacote.manifest(spec, { ...flatOptions, preferOnline: true })
33
+ manifests.set(spec.raw, manifest)
34
+ }
35
+ return manifests.get(spec.raw)
36
+ }
37
+
38
+ // Returns the required manifest if the spec is missing from the tree
39
+ // Returns the found node if it is in the tree
40
+ const missingFromTree = async ({ spec, tree, flatOptions }) => {
41
+ if (spec.registry && (spec.rawSpec === '' || spec.type !== 'tag')) {
42
+ // registry spec that is not a specific tag.
43
+ const nodesBySpec = tree.inventory.query('packageName', spec.name)
44
+ for (const node of nodesBySpec) {
45
+ if (spec.type === 'tag') {
46
+ // package requested by name only
47
+ return { node }
48
+ } else if (spec.type === 'version') {
49
+ // package requested by specific version
50
+ if (node.pkgid === spec.raw) {
51
+ return { node }
52
+ }
53
+ } else {
54
+ // package requested by version range, only remaining registry type
55
+ if (semver.satisfies(node.package.version, spec.rawSpec)) {
56
+ return { node }
57
+ }
58
+ }
59
+ }
60
+ const manifest = await getManifest(spec, flatOptions)
61
+ return { manifest }
62
+ } else {
63
+ // non-registry spec, or a specific tag. Look up manifest and check
64
+ // resolved to see if it's in the tree.
65
+ const manifest = await getManifest(spec, flatOptions)
66
+ if (spec.type === 'directory') {
67
+ return { manifest }
68
+ }
69
+ const nodesByManifest = tree.inventory.query('packageName', manifest.name)
70
+ for (const node of nodesByManifest) {
71
+ if (node.package.resolved === manifest._resolved) {
72
+ // we have a package by the same name and the same resolved destination, nothing to add.
73
+ return { node }
74
+ }
75
+ }
76
+ return { manifest }
77
+ }
78
+ }
25
79
 
26
80
  const exec = async (opts) => {
27
81
  const {
@@ -31,19 +85,18 @@ const exec = async (opts) => {
31
85
  localBin = resolve('./node_modules/.bin'),
32
86
  locationMsg = undefined,
33
87
  globalBin = '',
88
+ globalPath,
34
89
  output,
35
- packages: _packages = [],
90
+ // dereference values because we manipulate it later
91
+ packages: [...packages] = [],
36
92
  path = '.',
37
93
  runPath = '.',
38
94
  scriptShell = isWindows ? process.env.ComSpec || 'cmd' : 'sh',
39
- yes = undefined,
40
95
  ...flatOptions
41
96
  } = opts
42
97
 
43
- // dereferences values because we manipulate it later
44
- const packages = [..._packages]
45
- const pathArr = [...PATH]
46
- const _run = () => runScript({
98
+ let yes = opts.yes
99
+ const run = () => runScript({
47
100
  args,
48
101
  call,
49
102
  color,
@@ -51,125 +104,125 @@ const exec = async (opts) => {
51
104
  locationMsg,
52
105
  output,
53
106
  path,
54
- pathArr,
107
+ binPaths,
55
108
  runPath,
56
109
  scriptShell,
57
110
  })
58
111
 
59
- // nothing to maybe install, skip the arborist dance
112
+ // interactive mode
60
113
  if (!call && !args.length && !packages.length) {
61
- return await _run()
114
+ return run()
62
115
  }
63
116
 
64
- const needPackageCommandSwap = args.length && !packages.length
65
- // if there's an argument and no package has been explicitly asked for
66
- // check the local and global bin paths for a binary named the same as
67
- // the argument and run it if it exists, otherwise fall through to
68
- // the behavior of treating the single argument as a package name
117
+ const needPackageCommandSwap = (args.length > 0) && (packages.length === 0)
118
+ // If they asked for a command w/o specifying a package, see if there is a
119
+ // bin that directly matches that name either globally or in the local tree.
69
120
  if (needPackageCommandSwap) {
70
- let binExists = false
71
121
  const dir = dirname(dirname(localBin))
72
- const localBinPath = await localFileExists(dir, args[0])
122
+ const localBinPath = await localFileExists(dir, args[0], '/')
73
123
  if (localBinPath) {
74
- pathArr.unshift(localBinPath)
75
- binExists = true
76
- } else if (await fileExists(`${globalBin}/${args[0]}`)) {
77
- pathArr.unshift(globalBin)
78
- binExists = true
79
- }
80
-
81
- if (binExists) {
82
- return await _run()
124
+ binPaths.push(localBinPath)
125
+ return await run()
126
+ } else if (globalPath && await fileExists(`${globalBin}/${args[0]}`)) {
127
+ binPaths.push(globalBin)
128
+ return await run()
83
129
  }
84
130
 
131
+ // We swap out args[0] with the bin from the manifest later
85
132
  packages.push(args[0])
86
133
  }
87
134
 
88
- // figure out whether we need to install stuff, or if local is fine
89
- const localArb = new Arborist({
90
- ...flatOptions,
91
- path,
92
- })
93
- const localTree = await localArb.loadActual()
94
-
95
- const getLocalManifest = ({ tree, name }) => {
96
- // look up the package name in the current tree inventory,
97
- // if it's found then return that normalized pkg data
98
- const [node] = tree.inventory.query('packageName', name)
99
-
100
- if (node) {
101
- return {
102
- _id: node.pkgid,
103
- ...node.package,
104
- [_localManifest]: true,
105
- }
135
+ // Resolve any directory specs so that the npx directory is unique to the
136
+ // resolved directory, not the potentially relative one (i.e. "npx .")
137
+ for (const i in packages) {
138
+ const pkg = packages[i]
139
+ const spec = npa(pkg)
140
+ if (spec.type === 'directory') {
141
+ packages[i] = spec.fetchSpec
106
142
  }
107
143
  }
108
144
 
109
- // If we do `npm exec foo`, and have a `foo` locally, then we'll
110
- // always use that, so we don't really need to fetch the manifest.
111
- // So: run npa on each packages entry, and if it is a name with a
112
- // rawSpec==='', then try to find that node name in the tree inventory
113
- // and only pacote fetch if that fails.
114
- const manis = await Promise.all(packages.map(async p => {
115
- const spec = npa(p, path)
116
- if (spec.type === 'tag' && spec.rawSpec === '') {
117
- const localManifest = getLocalManifest({
118
- tree: localTree,
119
- name: spec.name,
120
- })
121
- if (localManifest) {
122
- return localManifest
145
+ const localArb = new Arborist({ ...flatOptions, path })
146
+ const localTree = await localArb.loadActual()
147
+
148
+ // Find anything that isn't installed locally
149
+ const needInstall = []
150
+ let commandManifest
151
+ await Promise.all(packages.map(async (pkg, i) => {
152
+ const spec = npa(pkg, path)
153
+ const { manifest, node } = await missingFromTree({ spec, tree: localTree, flatOptions })
154
+ if (manifest) {
155
+ // Package does not exist in the local tree
156
+ needInstall.push({ spec, manifest })
157
+ if (i === 0) {
158
+ commandManifest = manifest
123
159
  }
160
+ } else if (i === 0) {
161
+ // The node.package has enough to look up the bin
162
+ commandManifest = node.package
124
163
  }
125
- // Force preferOnline to true so we are making sure to pull in the latest
126
- // This is especially useful if the user didn't give us a version, and
127
- // they expect to be running @latest
128
- return await pacote.manifest(p, {
129
- ...flatOptions,
130
- preferOnline: true,
131
- })
132
164
  }))
133
165
 
134
166
  if (needPackageCommandSwap) {
135
- args[0] = getBinFromManifest(manis[0])
136
- }
167
+ const spec = npa(args[0])
137
168
 
138
- // are all packages from the manifest list installed?
139
- const needInstall =
140
- manis.some(manifest => !manifest[_localManifest])
169
+ if (spec.type === 'directory') {
170
+ yes = true
171
+ }
141
172
 
142
- if (needInstall) {
173
+ args[0] = getBinFromManifest(commandManifest)
174
+
175
+ if (needInstall.length > 0 && globalPath) {
176
+ // See if the package is installed globally, and run the translated bin
177
+ const globalArb = new Arborist({ ...flatOptions, path: globalPath, global: true })
178
+ const globalTree = await globalArb.loadActual()
179
+ const { manifest: globalManifest } =
180
+ await missingFromTree({ spec, tree: globalTree, flatOptions })
181
+ if (!globalManifest && await fileExists(`${globalBin}/${args[0]}`)) {
182
+ binPaths.push(globalBin)
183
+ return await run()
184
+ }
185
+ }
186
+ }
187
+
188
+ const add = []
189
+ if (needInstall.length > 0) {
190
+ // Install things to the npx cache, if needed
143
191
  const { npxCache } = flatOptions
144
- const installDir = cacheInstallDir({ npxCache, packages })
192
+ if (!npxCache) {
193
+ throw new Error('Must provide a valid npxCache path')
194
+ }
195
+ const hash = crypto.createHash('sha512')
196
+ .update(packages.map(p => {
197
+ // Keeps the npx directory unique to the resolved directory, not the
198
+ // potentially relative one (i.e. "npx .")
199
+ const spec = npa(p)
200
+ if (spec.type === 'directory') {
201
+ return spec.fetchSpec
202
+ }
203
+ return p
204
+ }).sort((a, b) => a.localeCompare(b, 'en')).join('\n'))
205
+ .digest('hex')
206
+ .slice(0, 16)
207
+ const installDir = resolve(npxCache, hash)
145
208
  await mkdirp(installDir)
146
- const arb = new Arborist({
209
+ const npxArb = new Arborist({
147
210
  ...flatOptions,
148
211
  path: installDir,
149
212
  })
150
- const tree = await arb.loadActual()
151
-
152
- // inspect the npx-space installed tree to check if the package is already
153
- // there, if that's the case also check that it's version matches the same
154
- // version expected by the user requested pkg returned by pacote.manifest
155
- const filterMissingPackagesFromInstallDir = (mani) => {
156
- const localManifest = getLocalManifest({ tree, name: mani.name })
157
- if (localManifest) {
158
- return localManifest.version !== mani.version
213
+ const npxTree = await npxArb.loadActual()
214
+ await Promise.all(needInstall.map(async ({ spec }) => {
215
+ const { manifest } = await missingFromTree({ spec, tree: npxTree, flatOptions })
216
+ if (manifest) {
217
+ // Manifest is not in npxCache, we need to install it there
218
+ if (!spec.registry) {
219
+ add.push(manifest._from)
220
+ } else {
221
+ add.push(manifest._id)
222
+ }
159
223
  }
160
- return true
161
- }
162
-
163
- // at this point, we have to ensure that we get the exact same
164
- // version, because it's something that has only ever been installed
165
- // by npm exec in the cache install directory
166
- const add = manis
167
- .filter(mani => !mani[_localManifest])
168
- .filter(filterMissingPackagesFromInstallDir)
169
- .map(mani => mani._id || mani._from)
170
- .sort((a, b) => a.localeCompare(b, 'en'))
224
+ }))
171
225
 
172
- // no need to install if already present
173
226
  if (add.length) {
174
227
  if (!yes) {
175
228
  // set -n to always say no
@@ -196,15 +249,15 @@ const exec = async (opts) => {
196
249
  }
197
250
  }
198
251
  }
199
- await arb.reify({
252
+ await npxArb.reify({
200
253
  ...flatOptions,
201
254
  add,
202
255
  })
203
256
  }
204
- pathArr.unshift(resolve(installDir, 'node_modules/.bin'))
257
+ binPaths.push(resolve(installDir, 'node_modules/.bin'))
205
258
  }
206
259
 
207
- return await _run()
260
+ return await run()
208
261
  }
209
262
 
210
263
  module.exports = exec
package/lib/run-script.js CHANGED
@@ -1,5 +1,3 @@
1
- const { delimiter } = require('path')
2
-
3
1
  const chalk = require('chalk')
4
2
  const ciDetect = require('@npmcli/ci-detect')
5
3
  const runScript = require('@npmcli/run-script')
@@ -22,7 +20,7 @@ const run = async ({
22
20
  locationMsg,
23
21
  output = () => {},
24
22
  path,
25
- pathArr,
23
+ binPaths,
26
24
  runPath,
27
25
  scriptShell,
28
26
  }) => {
@@ -71,12 +69,11 @@ const run = async ({
71
69
  // we always run in cwd, not --prefix
72
70
  path: runPath,
73
71
  stdioString: true,
72
+ binPaths,
74
73
  event: 'npx',
75
74
  args,
76
- env: {
77
- PATH: pathArr.join(delimiter),
78
- },
79
75
  stdio: 'inherit',
76
+ scriptShell,
80
77
  })
81
78
  } finally {
82
79
  npmlog.enableProgress()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libnpmexec",
3
- "version": "4.0.8",
3
+ "version": "4.0.11",
4
4
  "files": [
5
5
  "bin/",
6
6
  "lib/"
@@ -57,7 +57,8 @@
57
57
  "dependencies": {
58
58
  "@npmcli/arborist": "^5.0.0",
59
59
  "@npmcli/ci-detect": "^2.0.0",
60
- "@npmcli/run-script": "^4.1.3",
60
+ "@npmcli/fs": "^2.1.1",
61
+ "@npmcli/run-script": "^4.2.0",
61
62
  "chalk": "^4.1.0",
62
63
  "mkdirp-infer-owner": "^2.0.0",
63
64
  "npm-package-arg": "^9.0.1",
@@ -66,6 +67,7 @@
66
67
  "proc-log": "^2.0.0",
67
68
  "read": "^1.0.7",
68
69
  "read-package-json-fast": "^2.0.2",
70
+ "semver": "^7.3.7",
69
71
  "walk-up-path": "^1.0.0"
70
72
  },
71
73
  "templateOSS": {
@@ -1,20 +0,0 @@
1
- const crypto = require('crypto')
2
-
3
- const { resolve } = require('path')
4
-
5
- const cacheInstallDir = ({ npxCache, packages }) => {
6
- if (!npxCache) {
7
- throw new Error('Must provide a valid npxCache path')
8
- }
9
-
10
- // only packages not found in ${prefix}/node_modules
11
- return resolve(npxCache, getHash(packages))
12
- }
13
-
14
- const getHash = (packages) =>
15
- crypto.createHash('sha512')
16
- .update(packages.sort((a, b) => a.localeCompare(b, 'en')).join('\n'))
17
- .digest('hex')
18
- .slice(0, 16)
19
-
20
- module.exports = cacheInstallDir