From ea449954844f21abbf984e09e421f0e03485a535 Mon Sep 17 00:00:00 2001 From: Gar Date: Mon, 1 Aug 2022 08:50:28 -0700 Subject: [PATCH] fix: properly find locally/globally/npxCache packages Lots of bugfixes here, we properly parse ranges and versions, and we also now work with git repos and gists, and know when they are already installed. --- docs/content/commands/npm-exec.md | 2 +- docs/content/using-npm/config.md | 2 +- lib/commands/exec.js | 2 + lib/utils/config/definitions.js | 2 +- .../lib/utils/config/definitions.js.test.cjs | 2 +- .../libnpmexec/lib/cache-install-dir.js | 20 - workspaces/libnpmexec/lib/file-exists.js | 22 +- workspaces/libnpmexec/lib/index.js | 216 ++++++----- .../libnpmexec/test/cache-install-dir.js | 12 - workspaces/libnpmexec/test/index.js | 353 +++++++++++++++++- .../content/ruyadorno/create-index.json | 2 +- 11 files changed, 480 insertions(+), 155 deletions(-) delete mode 100644 workspaces/libnpmexec/lib/cache-install-dir.js delete mode 100644 workspaces/libnpmexec/test/cache-install-dir.js diff --git a/docs/content/commands/npm-exec.md b/docs/content/commands/npm-exec.md index 8ccfa75c73386..3d8de1ea54ad6 100644 --- a/docs/content/commands/npm-exec.md +++ b/docs/content/commands/npm-exec.md @@ -127,7 +127,7 @@ $ npm exec -- foo@latest bar --package=@npmcli/foo * Default: * Type: String (can be set multiple times) -The package to install for [`npm exec`](/commands/npm-exec) +The package or packages to install for [`npm exec`](/commands/npm-exec) diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md index 4689d340b5878..cd13237f34dd3 100644 --- a/docs/content/using-npm/config.md +++ b/docs/content/using-npm/config.md @@ -1244,7 +1244,7 @@ Directory in which `npm pack` will save tarballs. * Default: * Type: String (can be set multiple times) -The package to install for [`npm exec`](/commands/npm-exec) +The package or packages to install for [`npm exec`](/commands/npm-exec) diff --git a/lib/commands/exec.js b/lib/commands/exec.js index d9a686cc9a3be..0bbf7b3fa0b72 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -49,8 +49,10 @@ class Exec extends BaseCommand { static isShellout = true async exec (_args, { locationMsg, runPath } = {}) { + // This is where libnpmexec will look for locally installed packages const path = this.npm.localPrefix + // This is where libnpmexec will actually run the scripts from if (!runPath) { runPath = process.cwd() } diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index 04576df0abf8e..a132c8456b0fa 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -1470,7 +1470,7 @@ define('package', { hint: '', type: [String, Array], description: ` - The package to install for [\`npm exec\`](/commands/npm-exec) + The package or packages to install for [\`npm exec\`](/commands/npm-exec) `, flatten, }) diff --git a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs index 324118f1de1b1..024ad345a5d32 100644 --- a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs @@ -1316,7 +1316,7 @@ exports[`test/lib/utils/config/definitions.js TAP > config description for packa * Default: * Type: String (can be set multiple times) -The package to install for [\`npm exec\`](/commands/npm-exec) +The package or packages to install for [\`npm exec\`](/commands/npm-exec) ` exports[`test/lib/utils/config/definitions.js TAP > config description for package-lock 1`] = ` diff --git a/workspaces/libnpmexec/lib/cache-install-dir.js b/workspaces/libnpmexec/lib/cache-install-dir.js deleted file mode 100644 index 77466938762d0..0000000000000 --- a/workspaces/libnpmexec/lib/cache-install-dir.js +++ /dev/null @@ -1,20 +0,0 @@ -const crypto = require('crypto') - -const { resolve } = require('path') - -const cacheInstallDir = ({ npxCache, packages }) => { - if (!npxCache) { - throw new Error('Must provide a valid npxCache path') - } - - // only packages not found in ${prefix}/node_modules - return resolve(npxCache, getHash(packages)) -} - -const getHash = (packages) => - crypto.createHash('sha512') - .update(packages.sort((a, b) => a.localeCompare(b, 'en')).join('\n')) - .digest('hex') - .slice(0, 16) - -module.exports = cacheInstallDir diff --git a/workspaces/libnpmexec/lib/file-exists.js b/workspaces/libnpmexec/lib/file-exists.js index f89cfc217d61d..e5cd474dac388 100644 --- a/workspaces/libnpmexec/lib/file-exists.js +++ b/workspaces/libnpmexec/lib/file-exists.js @@ -1,23 +1,25 @@ const { resolve } = require('path') -const { promisify } = require('util') -const stat = promisify(require('fs').stat) +const fs = require('@npmcli/fs') const walkUp = require('walk-up-path') -const fileExists = (file) => stat(file) - .then((res) => res.isFile()) - .catch(() => false) - -const localFileExists = async (dir, binName, root = '/') => { - root = resolve(root).toLowerCase() +const fileExists = async (file) => { + try { + const res = await fs.stat(file) + return res.isFile() + } catch { + return false + } +} - for (const path of walkUp(resolve(dir))) { +const localFileExists = async (dir, binName, root) => { + for (const path of walkUp(dir)) { const binDir = resolve(path, 'node_modules', '.bin') if (await fileExists(resolve(binDir, binName))) { return binDir } - if (path.toLowerCase() === root) { + if (path.toLowerCase() === resolve(root).toLowerCase()) { return false } } diff --git a/workspaces/libnpmexec/lib/index.js b/workspaces/libnpmexec/lib/index.js index dfe7560120702..227dc2cdda1ac 100644 --- a/workspaces/libnpmexec/lib/index.js +++ b/workspaces/libnpmexec/lib/index.js @@ -1,27 +1,74 @@ -const { delimiter, dirname, resolve } = require('path') +'use strict' + const { promisify } = require('util') -const read = promisify(require('read')) const Arborist = require('@npmcli/arborist') const ciDetect = require('@npmcli/ci-detect') +const crypto = require('crypto') const log = require('proc-log') -const npmlog = require('npmlog') const mkdirp = require('mkdirp-infer-owner') const npa = require('npm-package-arg') +const npmlog = require('npmlog') const pacote = require('pacote') +const read = promisify(require('read')) +const semver = require('semver') -const cacheInstallDir = require('./cache-install-dir.js') const { fileExists, localFileExists } = require('./file-exists.js') const getBinFromManifest = require('./get-bin-from-manifest.js') const noTTY = require('./no-tty.js') const runScript = require('./run-script.js') const isWindows = require('./is-windows.js') -const _localManifest = Symbol('localManifest') -/* istanbul ignore next */ -const PATH = ( - process.env.PATH || process.env.Path || process.env.path -).split(delimiter) +const { delimiter, dirname, resolve } = require('path') + +const pathArr = process.env.PATH.split(delimiter) + +// when checking the local tree we look up manifests, cache those results by +// spec.raw so we don't have to fetch again when we check npxCache +const manifests = new Map() + +// Returns the required manifest if the spec is missing from the tree +const missingFromTree = async ({ spec, tree, pacoteOpts }) => { + if (spec.registry && (spec.rawSpec === '' || spec.type !== 'tag')) { + // registry spec that is not a specific tag. + const nodesBySpec = tree.inventory.query('packageName', spec.name) + for (const node of nodesBySpec) { + if (spec.type === 'tag') { + // package requested by name only + return + } else if (spec.type === 'version') { + // package requested by specific version + if (node.pkgid === spec.raw) { + return + } + } else { + // package requested by version range, only remaining registry type + if (semver.satisfies(node.package.version, spec.rawSpec)) { + return + } + } + } + if (!manifests.get(spec.raw)) { + manifests.set(spec.raw, await pacote.manifest(spec, pacoteOpts)) + } + return manifests.get(spec.raw) + } else { + // non-registry spec, or a specific tag. Look up manifest and check + // resolved to see if it's in the tree. + if (!manifests.get(spec.raw)) { + manifests.set(spec.raw, await pacote.manifest(spec, pacoteOpts)) + } + const manifest = manifests.get(spec.raw) + const nodesByManifest = tree.inventory.query('packageName', manifest.name) + for (const node of nodesByManifest) { + if (node.package.resolved === manifest._resolved) { + // we have a package by the same name and the same resolved destination, nothing to add. + return + } + } + return manifest + } +} const exec = async (opts) => { const { @@ -32,7 +79,8 @@ const exec = async (opts) => { locationMsg = undefined, globalBin = '', output, - packages: _packages = [], + // dereference values because we manipulate it later + packages: [...packages] = [], path = '.', runPath = '.', scriptShell = isWindows ? process.env.ComSpec || 'cmd' : 'sh', @@ -40,10 +88,7 @@ const exec = async (opts) => { ...flatOptions } = opts - // dereferences values because we manipulate it later - const packages = [..._packages] - const pathArr = [...PATH] - const _run = () => runScript({ + const run = () => runScript({ args, call, color, @@ -56,120 +101,87 @@ const exec = async (opts) => { scriptShell, }) - // nothing to maybe install, skip the arborist dance + // interactive mode if (!call && !args.length && !packages.length) { - return await _run() + return run() } - const needPackageCommandSwap = args.length && !packages.length - // if there's an argument and no package has been explicitly asked for - // check the local and global bin paths for a binary named the same as - // the argument and run it if it exists, otherwise fall through to - // the behavior of treating the single argument as a package name + const pacoteOpts = { ...flatOptions, perferOnline: true } + + const needPackageCommandSwap = (args.length > 0) && (packages.length === 0) if (needPackageCommandSwap) { - let binExists = false const dir = dirname(dirname(localBin)) - const localBinPath = await localFileExists(dir, args[0]) + const localBinPath = await localFileExists(dir, args[0], '/') if (localBinPath) { - pathArr.unshift(localBinPath) - binExists = true + // @npmcli/run-script adds local bin to $PATH itself + return await run() } else if (await fileExists(`${globalBin}/${args[0]}`)) { pathArr.unshift(globalBin) - binExists = true - } - - if (binExists) { - return await _run() + return await run() } + // We swap out args[0] with the bin from the manifest later packages.push(args[0]) } - // figure out whether we need to install stuff, or if local is fine - const localArb = new Arborist({ - ...flatOptions, - path, - }) + const localArb = new Arborist({ ...flatOptions, path }) const localTree = await localArb.loadActual() - const getLocalManifest = ({ tree, name }) => { - // look up the package name in the current tree inventory, - // if it's found then return that normalized pkg data - const [node] = tree.inventory.query('packageName', name) - - if (node) { - return { - _id: node.pkgid, - ...node.package, - [_localManifest]: true, - } - } - } - - // If we do `npm exec foo`, and have a `foo` locally, then we'll - // always use that, so we don't really need to fetch the manifest. - // So: run npa on each packages entry, and if it is a name with a - // rawSpec==='', then try to find that node name in the tree inventory - // and only pacote fetch if that fails. - const manis = await Promise.all(packages.map(async p => { - const spec = npa(p, path) - if (spec.type === 'tag' && spec.rawSpec === '') { - const localManifest = getLocalManifest({ - tree: localTree, - name: spec.name, - }) - if (localManifest) { - return localManifest - } + // Find anything that isn't installed locally + const needInstall = [] + await Promise.all(packages.map(async pkg => { + const spec = npa(pkg, path) + const manifest = await missingFromTree({ spec, tree: localTree, pacoteOpts }) + if (manifest) { + needInstall.push({ spec, manifest }) } - // Force preferOnline to true so we are making sure to pull in the latest - // This is especially useful if the user didn't give us a version, and - // they expect to be running @latest - return await pacote.manifest(p, { - ...flatOptions, - preferOnline: true, - }) })) if (needPackageCommandSwap) { - args[0] = getBinFromManifest(manis[0]) + // Either we have a scoped package or the bin of our package we inferred + // from arg[0] is not identical to the package name + let commandManifest + if (needInstall.length === 0) { + commandManifest = await pacote.manifest(args[0], { + ...flatOptions, + preferOnline: true, + }) + } else { + commandManifest = needInstall[0].manifest + } + args[0] = getBinFromManifest(commandManifest) } - // are all packages from the manifest list installed? - const needInstall = - manis.some(manifest => !manifest[_localManifest]) - - if (needInstall) { + const add = [] + if (needInstall.length > 0) { + // Install things to the npx cache, if needed const { npxCache } = flatOptions - const installDir = cacheInstallDir({ npxCache, packages }) + if (!npxCache) { + throw new Error('Must provide a valid npxCache path') + } + const hash = crypto.createHash('sha512') + .update(packages.sort((a, b) => a.localeCompare(b, 'en')).join('\n')) + .digest('hex') + .slice(0, 16) + const installDir = resolve(npxCache, hash) await mkdirp(installDir) - const arb = new Arborist({ + const npxArb = new Arborist({ ...flatOptions, path: installDir, }) - const tree = await arb.loadActual() - - // inspect the npx-space installed tree to check if the package is already - // there, if that's the case also check that it's version matches the same - // version expected by the user requested pkg returned by pacote.manifest - const filterMissingPackagesFromInstallDir = (mani) => { - const localManifest = getLocalManifest({ tree, name: mani.name }) - if (localManifest) { - return localManifest.version !== mani.version + const npxTree = await npxArb.loadActual() + await Promise.all(needInstall.map(async ({ spec }) => { + const manifest = await missingFromTree({ spec, tree: npxTree, pacoteOpts }) + if (manifest) { + // Manifest is not in npxCache, we need to install it there + if (!spec.registry) { + add.push(manifest._from) + } else { + add.push(manifest._id) + } } - return true - } - - // at this point, we have to ensure that we get the exact same - // version, because it's something that has only ever been installed - // by npm exec in the cache install directory - const add = manis - .filter(mani => !mani[_localManifest]) - .filter(filterMissingPackagesFromInstallDir) - .map(mani => mani._id || mani._from) - .sort((a, b) => a.localeCompare(b, 'en')) + })) - // no need to install if already present if (add.length) { if (!yes) { // set -n to always say no @@ -196,7 +208,7 @@ const exec = async (opts) => { } } } - await arb.reify({ + await npxArb.reify({ ...flatOptions, add, }) @@ -204,7 +216,7 @@ const exec = async (opts) => { pathArr.unshift(resolve(installDir, 'node_modules/.bin')) } - return await _run() + return await run() } module.exports = exec diff --git a/workspaces/libnpmexec/test/cache-install-dir.js b/workspaces/libnpmexec/test/cache-install-dir.js deleted file mode 100644 index 9a7aee6d7c7e3..0000000000000 --- a/workspaces/libnpmexec/test/cache-install-dir.js +++ /dev/null @@ -1,12 +0,0 @@ -const t = require('tap') - -const cacheInstallDir = require('../lib/cache-install-dir.js') - -t.test('invalid npxCache path', t => { - t.throws( - () => cacheInstallDir({}), - /Must provide a valid npxCache path/, - 'should throw invalid path error' - ) - t.end() -}) diff --git a/workspaces/libnpmexec/test/index.js b/workspaces/libnpmexec/test/index.js index d42947e83687c..0eae95910848e 100644 --- a/workspaces/libnpmexec/test/index.js +++ b/workspaces/libnpmexec/test/index.js @@ -66,7 +66,7 @@ require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`, t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') }) -t.test('local pkg, must not fetch manifest for avail pkg', async t => { +t.test('locally available pkg - by name', async t => { const pkg = { name: '@ruyadorno/create-index', version: '2.0.0', @@ -121,6 +121,186 @@ t.test('local pkg, must not fetch manifest for avail pkg', async t => { t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') }) +t.test('locally available pkg - by version', async t => { + const pkg = { + name: '@ruyadorno/create-index', + version: '1.0.0', + bin: { + 'create-index': './index.js', + }, + } + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '.bin': {}, + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify(pkg), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('resfile', 'LOCAL PKG')`, + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^1.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + const executable = + resolve(path, 'node_modules/@ruyadorno/create-index/index.js') + fs.chmodSync(executable, 0o775) + + await binLinks({ + path: resolve(path, 'node_modules/@ruyadorno/create-index'), + pkg, + }) + + await libexec({ + ...baseOpts, + cache, + npxCache, + args: ['@ruyadorno/create-index@1.0.0'], + path, + runPath, + }) + + const res = fs.readFileSync(resolve(path, 'resfile')).toString() + t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') +}) + +t.test('locally available pkg - by range', async t => { + const pkg = { + name: '@ruyadorno/create-index', + version: '2.0.0', + bin: { + 'create-index': './index.js', + }, + } + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '.bin': {}, + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify(pkg), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`, + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^2.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + const executable = + resolve(path, 'node_modules/@ruyadorno/create-index/index.js') + fs.chmodSync(executable, 0o775) + + await binLinks({ + path: resolve(path, 'node_modules/@ruyadorno/create-index'), + pkg, + }) + + await libexec({ + ...baseOpts, + cache, + npxCache, + packages: ['@ruyadorno/create-index@^2.0.0'], + call: 'create-index resfile', + path, + runPath, + }) + + const res = fs.readFileSync(resolve(path, 'resfile')).toString() + t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') +}) + +t.test('locally available pkg - by tag', async t => { + const pkg = { + name: '@ruyadorno/create-index', + version: '1.0.0', + bin: { + 'create-index': './index.js', + }, + } + + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '.bin': {}, + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify(pkg), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`, + }, + }, + '.package-lock.json': JSON.stringify({ + name: 'lock', + lockfileVersion: 3, + requires: true, + packages: { + 'node_modules/@ruyadorno/create-index': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/@ruyadorno/create-index/-/create-index-1.0.0.tgz', + bin: { + 'create-index': 'create-index.js', + }, + }, + }, + + }), + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^1.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + const executable = + resolve(path, 'node_modules/@ruyadorno/create-index/index.js') + fs.chmodSync(executable, 0o775) + + await binLinks({ + path: resolve(path, 'node_modules/@ruyadorno/create-index'), + pkg, + }) + + await libexec({ + ...baseOpts, + cache, + npxCache, + packages: ['@ruyadorno/create-index@latest'], + call: 'create-index resfile', + path, + runPath, + }) + + const res = fs.readFileSync(resolve(path, 'resfile')).toString() + t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') +}) + t.test('multiple local pkgs', async t => { const foo = { name: '@ruyadorno/create-foo', @@ -197,6 +377,35 @@ t.test('multiple local pkgs', async t => { t.equal(resBar, 'bar', 'should run local pkg bin script') }) +t.test('no npxCache', async t => { + const path = t.testdir({ + cache: {}, + a: { + 'package.json': JSON.stringify({ + name: 'a', + bin: { + a: './index.js', + }, + }), + 'index.js': `#!/usr/bin/env node +require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`, + }, + }) + const runPath = path + const cache = resolve(path, 'cache') + + const executable = resolve(path, 'a/index.js') + fs.chmodSync(executable, 0o775) + + await t.rejects(libexec({ + ...baseOpts, + args: [`file:${resolve(path, 'a')}`, 'resfile'], + cache, + path, + runPath, + }), /Must provide a valid npxCache path/) +}) + t.test('local file system path', async t => { const path = t.testdir({ cache: {}, @@ -276,7 +485,7 @@ t.test('global space pkg', async t => { t.equal(res, 'GLOBAL PKG', 'should run local pkg bin script') }) -t.test('run from registry', async t => { +t.test('run from registry - no local packages', async t => { const testdir = t.testdir({ cache: {}, npxCache: {}, @@ -305,6 +514,132 @@ t.test('run from registry', async t => { t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg') }) +t.test('run from registry - local version mismatch', async t => { + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify({ + name: '@ruyadorno/create-index', + version: '2.0.0', + }), + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^2.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + await libexec({ + ...baseOpts, + args: ['@ruyadorno/create-index@1.0.0'], + cache, + npxCache, + path, + runPath, + }) + + t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg from registry') +}) + +t.test('run from registry - local range mismatch', async t => { + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify({ + name: '@ruyadorno/create-index', + version: '2.0.0', + }), + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^2.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + await libexec({ + ...baseOpts, + args: ['@ruyadorno/create-index@^1.0.0'], + cache, + npxCache, + path, + runPath, + }) + + t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg from registry') +}) + +t.test('run from registry - local tag mismatch', async t => { + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify({ + name: '@ruyadorno/create-index', + version: '2.0.0', + }), + }, + }, + '.package-lock.json': JSON.stringify({ + name: 'lock', + lockfileVersion: 3, + requires: true, + packages: { + 'node_modules/@ruyadorno/create-index': { + version: '2.0.0', + resolved: 'https://registry.npmjs.org/@ruyadorno/create-index/-/create-index-2.0.0.tgz', + bin: { + 'create-index': 'create-index.js', + }, + }, + }, + + }), + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^2.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + await libexec({ + ...baseOpts, + args: ['@ruyadorno/create-index@latest'], + cache, + npxCache, + path, + runPath, + }) + + t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg from registry') +}) + t.test('avoid install when exec from registry an available pkg', async t => { const testdir = t.testdir({ cache: {}, @@ -656,9 +991,15 @@ t.test('no prompt if CI, multiple packages', async t => { 'proc-log': { warn (title, msg) { t.equal(title, 'exec', 'should warn exec title') - const expected = 'The following packages were not found and will be ' + - 'installed: @ruyadorno/create-index@1.0.0, @ruyadorno/create-test@1.0.0' - t.equal(msg, expected, 'should warn installing pkg') + // this message is nondeterministic as it queries manifests so we just + // test the constituent parts + t.match( + msg, + 'The following packages were not found and will be installed:', + 'should warn installing packages' + ) + t.match(msg, '@ruyadorno/create-index@1.0.0', 'includes package being installed') + t.match(msg, '@ruyadorno/create-test@1.0.0', 'includes package being installed') }, }, }) @@ -675,7 +1016,7 @@ t.test('no prompt if CI, multiple packages', async t => { }) }) -t.test('sane defaults', async t => { +t.test('defaults', async t => { const testdir = t.testdir({ cache: {}, npxCache: {}, diff --git a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json index 1e85cbbabec73..6118dec7dfd87 100644 --- a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json +++ b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json @@ -78,4 +78,4 @@ "readmeFilename": "README.md", "_cached": false, "_contentLength": 0 -} \ No newline at end of file +}