Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
index.js316 linesDownload Raw Back to lib
1'use strict'
2
3const { dirname, join, resolve } = require('node:path')
4const crypto = require('node:crypto')
5const { mkdir } = require('node:fs/promises')
6const Arborist = require('@npmcli/arborist')
7const ciInfo = require('ci-info')
8const { log, input } = require('proc-log')
9const npa = require('npm-package-arg')
10const pacote = require('pacote')
11const { read } = require('read')
12const semver = require('semver')
13const PackageJson = require('@npmcli/package-json')
14const { fileExists, localFileExists } = require('./file-exists.js')
15const getBinFromManifest = require('./get-bin-from-manifest.js')
16const noTTY = require('./no-tty.js')
17const runScript = require('./run-script.js')
18const isWindows = require('./is-windows.js')
19const withLock = require('./with-lock.js')
20
21const binPaths = []
22
23// when checking the local tree we look up manifests, cache those results by
24// spec.raw so we don't have to fetch again when we check npxCache
25const manifests = new Map()
26
27const getManifest = async (spec, flatOptions) => {
28  if (!manifests.has(spec.raw)) {
29    const manifest = await pacote.manifest(spec, {
30      ...flatOptions,
31      preferOnline: true,
32      Arborist,
33      _isRoot: true,
34    })
35    manifests.set(spec.raw, manifest)
36  }
37  return manifests.get(spec.raw)
38}
39
40// Returns the required manifest if the spec is missing from the tree
41// Returns the found node if it is in the tree
42const missingFromTree = async ({ spec, tree, flatOptions, isNpxTree, shallow }) => {
43  // If asking for a spec by name only (spec.raw === spec.name):
44  //  - In local or global mode go with anything in the tree that matches
45  //  - If looking in the npx cache check if a newer version is available
46  const npxByNameOnly = isNpxTree && spec.name === spec.raw
47  // If they gave a range and not a tag we still need to check if it's outdated.
48  if (spec.registry && spec.type !== 'tag' && !npxByNameOnly) {
49    // registry spec that is not a specific tag.
50    const nodesBySpec = tree.inventory.query('packageName', spec.name)
51    for (const node of nodesBySpec) {
52      // continue if node is not a top level node
53      if (shallow && node.depth) {
54        continue
55      }
56      if (spec.rawSpec === '*') {
57        return { node }
58      }
59      // package requested by specific version
60      if (spec.type === 'version' && (node.pkgid === spec.raw)) {
61        return { node }
62      }
63      // package requested by version range, only remaining registry type
64      // the npx tree shouldn't be ok w/ an outdated version
65      if (!isNpxTree && semver.satisfies(node.package.version, spec.rawSpec)) {
66        return { node }
67      }
68    }
69    const manifest = await getManifest(spec, flatOptions)
70    return { manifest }
71  } else {
72    // non-registry spec, or a specific tag, or name only in npx tree.  Look up
73    // manifest and check resolved to see if it's in the tree.
74    const manifest = await getManifest(spec, flatOptions)
75    if (spec.type === 'directory') {
76      return { manifest }
77    }
78    const nodesByManifest = tree.inventory.query('packageName', manifest.name)
79    for (const node of nodesByManifest) {
80      if (node.package.resolved === manifest._resolved) {
81        // we have a package by the same name and the same resolved destination, nothing to add.
82        return { node }
83      }
84    }
85    return { manifest }
86  }
87}
88
89// see if the package.json at `path` has an entry that matches `cmd`
90const hasPkgBin = (path, cmd, flatOptions) =>
91  pacote.manifest(path, flatOptions)
92    .then(manifest => manifest?.bin?.[cmd]).catch(() => null)
93
94const exec = async (opts) => {
95  const {
96    args = [],
97    call = '',
98    localBin = resolve('./node_modules/.bin'),
99    locationMsg = undefined,
100    globalBin = '',
101    globalPath,
102    // dereference values because we manipulate it later
103    packages: [...packages] = [],
104    path = '.',
105    runPath = '.',
106    scriptShell = isWindows ? process.env.ComSpec || 'cmd' : 'sh',
107    ...flatOptions
108  } = opts
109
110  let pkgPaths = opts.pkgPath
111  if (typeof pkgPaths === 'string') {
112    pkgPaths = [pkgPaths]
113  }
114  if (!pkgPaths) {
115    pkgPaths = ['.']
116  }
117  let yes = opts.yes
118  const run = () => runScript({
119    args,
120    call,
121    flatOptions,
122    locationMsg,
123    path,
124    binPaths,
125    runPath,
126    scriptShell,
127  })
128
129  // interactive mode
130  if (!call && !args.length && !packages.length) {
131    return run()
132  }
133
134  // Look in the local tree too
135  pkgPaths.push(path)
136
137  let needPackageCommandSwap = (args.length > 0) && (packages.length === 0)
138  // If they asked for a command w/o specifying a package, see if there is a
139  // bin that directly matches that name:
140  // - in any local packages (pkgPaths can have workspaces in them or just the root)
141  // - in the local tree (path)
142  // - globally
143  if (needPackageCommandSwap) {
144    // Local packages and local tree
145    for (const p of pkgPaths) {
146      if (await hasPkgBin(p, args[0], flatOptions)) {
147        // we have to install the local package into the npx cache so that its
148        // bin links get set up
149        flatOptions.installLinks = false
150        // args[0] will exist when the package is installed
151        packages.push(p)
152        yes = true
153        needPackageCommandSwap = false
154        break
155      }
156    }
157    if (needPackageCommandSwap) {
158      // no bin entry in local packages or in tree, now we look for binPaths
159      const dir = dirname(dirname(localBin))
160      const localBinPath = await localFileExists(dir, args[0], '/')
161      if (localBinPath) {
162        binPaths.push(localBinPath)
163        return await run()
164      } else if (globalPath && await fileExists(`${globalBin}/${args[0]}`)) {
165        binPaths.push(globalBin)
166        return await run()
167      }
168      // We swap out args[0] with the bin from the manifest later
169      packages.push(args[0])
170    }
171  }
172
173  // Resolve any directory specs so that the npx directory is unique to the
174  // resolved directory, not the potentially relative one (i.e. "npx .")
175  for (const i in packages) {
176    const pkg = packages[i]
177    const spec = npa(pkg)
178    if (spec.type === 'directory') {
179      packages[i] = spec.fetchSpec
180    }
181  }
182
183  const localArb = new Arborist({ ...flatOptions, path })
184  const localTree = await localArb.loadActual()
185
186  // Find anything that isn't installed locally
187  const needInstall = []
188  let commandManifest
189  await Promise.all(packages.map(async (pkg, i) => {
190    const spec = npa(pkg, path)
191    const { manifest, node } = await missingFromTree({ spec, tree: localTree, flatOptions })
192    if (manifest) {
193      // Package does not exist in the local tree
194      needInstall.push({ spec, manifest })
195      if (i === 0) {
196        commandManifest = manifest
197      }
198    } else if (i === 0) {
199      // The node.package has enough to look up the bin
200      commandManifest = node.package
201    }
202  }))
203
204  if (needPackageCommandSwap) {
205    const spec = npa(args[0])
206
207    if (spec.type === 'directory') {
208      yes = true
209    }
210
211    args[0] = getBinFromManifest(commandManifest)
212
213    if (needInstall.length > 0 && globalPath) {
214      // See if the package is installed globally. If it is, run the translated bin
215      const globalArb = new Arborist({ ...flatOptions, path: globalPath, global: true })
216      const globalTree = await globalArb.loadActual().catch(() => {
217        log.verbose(`Could not read global path ${globalPath}, ignoring`)
218        return null
219      })
220      if (globalTree) {
221        const { manifest: globalManifest } =
222          await missingFromTree({ spec, tree: globalTree, flatOptions, shallow: true })
223        if (!globalManifest && await fileExists(`${globalBin}/${args[0]}`)) {
224          binPaths.push(globalBin)
225          return await run()
226        }
227      }
228    }
229  }
230
231  const add = []
232  if (needInstall.length > 0) {
233    // Install things to the npx cache, if needed
234    const { npxCache } = flatOptions
235    if (!npxCache) {
236      throw new Error('Must provide a valid npxCache path')
237    }
238    const hash = crypto.createHash('sha512')
239      .update(packages.map(p => {
240        // Keeps the npx directory unique to the resolved directory, not the
241        // potentially relative one (i.e. "npx .")
242        const spec = npa(p)
243        if (spec.type === 'directory') {
244          return spec.fetchSpec
245        }
246        return p
247      }).sort((a, b) => a.localeCompare(b, 'en')).join('\n'))
248      .digest('hex')
249      .slice(0, 16)
250    const installDir = resolve(npxCache, hash)
251    await mkdir(installDir, { recursive: true })
252    const npxArb = new Arborist({
253      ...flatOptions,
254      path: installDir,
255    })
256    const lockPath = join(installDir, 'concurrency.lock')
257    const npxTree = await withLock(lockPath, () => npxArb.loadActual())
258    await Promise.all(needInstall.map(async ({ spec }) => {
259      const { manifest } = await missingFromTree({
260        spec,
261        tree: npxTree,
262        flatOptions,
263        isNpxTree: true,
264      })
265      if (manifest) {
266        // Manifest is not in npxCache, we need to install it there
267        if (!spec.registry) {
268          add.push(manifest._from)
269        } else {
270          add.push(manifest._id)
271        }
272      }
273    }))
274
275    if (add.length) {
276      if (!yes) {
277        const addList = add.map(a => `${a.replace(/@$/, '')}`)
278
279        // set -n to always say no
280        if (yes === false) {
281          // Error message lists missing package(s) when process is canceled
282          /* eslint-disable-next-line max-len */
283          throw new Error(`npx canceled due to missing packages and no YES option: ${JSON.stringify(addList)}`)
284        }
285
286        if (noTTY() || ciInfo.isCI) {
287          /* eslint-disable-next-line max-len */
288          log.warn('exec', `The following package${add.length === 1 ? ' was' : 's were'} not found and will be installed: ${addList.join(', ')}`)
289        } else {
290          const confirm = await input.read(() => read({
291            /* eslint-disable-next-line max-len */
292            prompt: `Need to install the following packages:\n${addList.join('\n')}\nOk to proceed? `,
293            default: 'y',
294          }))
295          if (confirm.trim().toLowerCase().charAt(0) !== 'y') {
296            throw new Error('canceled')
297          }
298        }
299      }
300      await withLock(lockPath, () => npxArb.reify({
301        ...flatOptions,
302        save: true,
303        add,
304      }))
305    }
306    binPaths.push(resolve(installDir, 'node_modules/.bin'))
307    const pkgJson = await PackageJson.load(installDir)
308    pkgJson.update({ _npx: { packages } })
309    await pkgJson.save()
310  }
311
312  return await run()
313}
314
315module.exports = exec
316 
codekingpro/portable-devtools · Team Ai