libnpmexec 4.0.8 → 4.0.9

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,76 @@
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.get(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
+ const missingFromTree = async ({ spec, tree, flatOptions }) => {
40
+ if (spec.registry && (spec.rawSpec === '' || spec.type !== 'tag')) {
41
+ // registry spec that is not a specific tag.
42
+ const nodesBySpec = tree.inventory.query('packageName', spec.name)
43
+ for (const node of nodesBySpec) {
44
+ if (spec.type === 'tag') {
45
+ // package requested by name only
46
+ return
47
+ } else if (spec.type === 'version') {
48
+ // package requested by specific version
49
+ if (node.pkgid === spec.raw) {
50
+ return
51
+ }
52
+ } else {
53
+ // package requested by version range, only remaining registry type
54
+ if (semver.satisfies(node.package.version, spec.rawSpec)) {
55
+ return
56
+ }
57
+ }
58
+ }
59
+ return await getManifest(spec, flatOptions)
60
+ } else {
61
+ // non-registry spec, or a specific tag. Look up manifest and check
62
+ // resolved to see if it's in the tree.
63
+ const manifest = await getManifest(spec, flatOptions)
64
+ const nodesByManifest = tree.inventory.query('packageName', manifest.name)
65
+ for (const node of nodesByManifest) {
66
+ if (node.package.resolved === manifest._resolved) {
67
+ // we have a package by the same name and the same resolved destination, nothing to add.
68
+ return
69
+ }
70
+ }
71
+ return manifest
72
+ }
73
+ }
25
74
 
26
75
  const exec = async (opts) => {
27
76
  const {
@@ -31,8 +80,10 @@ const exec = async (opts) => {
31
80
  localBin = resolve('./node_modules/.bin'),
32
81
  locationMsg = undefined,
33
82
  globalBin = '',
83
+ globalPath = '',
34
84
  output,
35
- packages: _packages = [],
85
+ // dereference values because we manipulate it later
86
+ packages: [...packages] = [],
36
87
  path = '.',
37
88
  runPath = '.',
38
89
  scriptShell = isWindows ? process.env.ComSpec || 'cmd' : 'sh',
@@ -40,10 +91,7 @@ const exec = async (opts) => {
40
91
  ...flatOptions
41
92
  } = opts
42
93
 
43
- // dereferences values because we manipulate it later
44
- const packages = [..._packages]
45
- const pathArr = [...PATH]
46
- const _run = () => runScript({
94
+ const run = () => runScript({
47
95
  args,
48
96
  call,
49
97
  color,
@@ -51,125 +99,101 @@ const exec = async (opts) => {
51
99
  locationMsg,
52
100
  output,
53
101
  path,
54
- pathArr,
102
+ binPaths,
55
103
  runPath,
56
104
  scriptShell,
57
105
  })
58
106
 
59
- // nothing to maybe install, skip the arborist dance
107
+ // interactive mode
60
108
  if (!call && !args.length && !packages.length) {
61
- return await _run()
109
+ return run()
62
110
  }
63
111
 
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
112
+ const needPackageCommandSwap = (args.length > 0) && (packages.length === 0)
113
+ // If they asked for a command w/o specifying a package, see if there is a
114
+ // bin that directly matches that name either globally or in the local tree.
69
115
  if (needPackageCommandSwap) {
70
- let binExists = false
71
116
  const dir = dirname(dirname(localBin))
72
- const localBinPath = await localFileExists(dir, args[0])
117
+ const localBinPath = await localFileExists(dir, args[0], '/')
73
118
  if (localBinPath) {
74
- pathArr.unshift(localBinPath)
75
- binExists = true
119
+ binPaths.push(localBinPath)
120
+ return await run()
76
121
  } else if (await fileExists(`${globalBin}/${args[0]}`)) {
77
- pathArr.unshift(globalBin)
78
- binExists = true
79
- }
80
-
81
- if (binExists) {
82
- return await _run()
122
+ binPaths.push(globalBin)
123
+ return await run()
83
124
  }
84
125
 
126
+ // We swap out args[0] with the bin from the manifest later
85
127
  packages.push(args[0])
86
128
  }
87
129
 
88
- // figure out whether we need to install stuff, or if local is fine
89
- const localArb = new Arborist({
90
- ...flatOptions,
91
- path,
92
- })
130
+ const localArb = new Arborist({ ...flatOptions, path })
93
131
  const localTree = await localArb.loadActual()
94
132
 
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
- }
133
+ // Find anything that isn't installed locally
134
+ const needInstall = []
135
+ await Promise.all(packages.map(async pkg => {
136
+ const spec = npa(pkg, path)
137
+ const manifest = await missingFromTree({ spec, tree: localTree, flatOptions })
138
+ if (manifest) {
139
+ // Package does not exist in the local tree
140
+ needInstall.push({ spec, manifest })
106
141
  }
107
- }
108
-
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
123
- }
124
- }
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
142
  }))
133
143
 
134
144
  if (needPackageCommandSwap) {
135
- args[0] = getBinFromManifest(manis[0])
136
- }
145
+ // Either we have a scoped package or the bin of our package we inferred
146
+ // from arg[0] might not be identical to the package name
147
+ const spec = npa(args[0])
148
+ let commandManifest
149
+ if (needInstall.length === 0) {
150
+ commandManifest = await getManifest(spec, flatOptions)
151
+ } else {
152
+ commandManifest = needInstall[0].manifest
153
+ }
154
+
155
+ args[0] = getBinFromManifest(commandManifest)
137
156
 
138
- // are all packages from the manifest list installed?
139
- const needInstall =
140
- manis.some(manifest => !manifest[_localManifest])
157
+ // See if the package is installed globally, and run the translated bin
158
+ const globalArb = new Arborist({ ...flatOptions, path: globalPath, global: true })
159
+ const globalTree = await globalArb.loadActual()
160
+ const globalManifest = await missingFromTree({ spec, tree: globalTree, flatOptions })
161
+ if (!globalManifest) {
162
+ binPaths.push(globalBin)
163
+ return await run()
164
+ }
165
+ }
141
166
 
142
- if (needInstall) {
167
+ const add = []
168
+ if (needInstall.length > 0) {
169
+ // Install things to the npx cache, if needed
143
170
  const { npxCache } = flatOptions
144
- const installDir = cacheInstallDir({ npxCache, packages })
171
+ if (!npxCache) {
172
+ throw new Error('Must provide a valid npxCache path')
173
+ }
174
+ const hash = crypto.createHash('sha512')
175
+ .update(packages.sort((a, b) => a.localeCompare(b, 'en')).join('\n'))
176
+ .digest('hex')
177
+ .slice(0, 16)
178
+ const installDir = resolve(npxCache, hash)
145
179
  await mkdirp(installDir)
146
- const arb = new Arborist({
180
+ const npxArb = new Arborist({
147
181
  ...flatOptions,
148
182
  path: installDir,
149
183
  })
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
184
+ const npxTree = await npxArb.loadActual()
185
+ await Promise.all(needInstall.map(async ({ spec }) => {
186
+ const manifest = await missingFromTree({ spec, tree: npxTree, flatOptions })
187
+ if (manifest) {
188
+ // Manifest is not in npxCache, we need to install it there
189
+ if (!spec.registry) {
190
+ add.push(manifest._from)
191
+ } else {
192
+ add.push(manifest._id)
193
+ }
159
194
  }
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'))
195
+ }))
171
196
 
172
- // no need to install if already present
173
197
  if (add.length) {
174
198
  if (!yes) {
175
199
  // set -n to always say no
@@ -196,15 +220,15 @@ const exec = async (opts) => {
196
220
  }
197
221
  }
198
222
  }
199
- await arb.reify({
223
+ await npxArb.reify({
200
224
  ...flatOptions,
201
225
  add,
202
226
  })
203
227
  }
204
- pathArr.unshift(resolve(installDir, 'node_modules/.bin'))
228
+ binPaths.push(resolve(installDir, 'node_modules/.bin'))
205
229
  }
206
230
 
207
- return await _run()
231
+ return await run()
208
232
  }
209
233
 
210
234
  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,11 +69,9 @@ 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',
80
76
  })
81
77
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libnpmexec",
3
- "version": "4.0.8",
3
+ "version": "4.0.9",
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