libnpmexec 4.0.7 → 4.0.10

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