1const t = require('tap') 2const { join, resolve, basename, extname } = require('path') 3const fs = require('fs/promises') 4const localeCompare = require('@isaacs/string-locale-compare')('en') 5const docs = require('@npmcli/docs') 6 7const { load: loadMockNpm } = require('../fixtures/mock-npm.js') 8const { definitions } = require('@npmcli/config/lib/definitions') 9const cmdList = require('../../lib/utils/cmd-list.js') 10const pkg = require('../../package.json') 11const { cleanCwd } = require('../fixtures/clean-snapshot.js') 12 13t.test('command list', async t => { 14 for (const [key, value] of Object.entries(cmdList)) { 15 t.matchSnapshot(value, key) 16 } 17}) 18 19t.test('shorthands', async t => { 20 t.matchSnapshot(docs.shorthands(docs.TAGS.SHORTHANDS, {}), 'docs') 21}) 22 23t.test('config', async t => { 24 const keys = Object.keys(definitions) 25 const flat = Object.entries(definitions).filter(([_, d]) => d.flatten).map(([k]) => k) 26 const notFlat = keys.filter(k => !flat.includes(k)) 27 t.matchSnapshot(keys, 'all keys') 28 t.matchSnapshot(flat, 'keys that are flattened') 29 t.matchSnapshot(notFlat, 'keys that are not flattened') 30 t.matchSnapshot(docs.config(docs.TAGS.CONFIG, {}), 'all definitions') 31}) 32 33t.test('flat options', async t => { 34 t.cleanSnapshot = (s) => cleanCwd(s) 35 .split(cleanCwd(process.execPath)).join('{NODE}') 36 37 const { npm } = await loadMockNpm(t, { 38 command: 'version', 39 exec: true, 40 npm: ({ other }) => ({ 41 npmRoot: other, 42 }), 43 otherDirs: { 44 'package.json': JSON.stringify({ version: '1.1.1' }), 45 }, 46 globals: { 47 'process.stdout.isTTY': false, 48 'process.stderr.isTTY': false, 49 'process.env': { 50 EDITOR: '{EDITOR}', 51 SHELL: '{SHELL}', 52 }, 53 'process.version': '2.2.2', 54 'process.platform': '{PLATFORM}', 55 'process.arch': '{ARCH}', 56 }, 57 mocks: { 58 'ci-info': { isCI: true, name: '{CI}' }, 59 // currently the config version is read from npmRoot and the Npm 60 // constructor version is read from the root package.json file. This 61 // snapshot is meant to explicitly show that they are read from different 62 // places. In the future we should probably only read it from npmRoot, 63 // which would require more changes to ensure nothing depends on it being 64 // a static prop on the constructor 65 '{ROOT}/package.json': { version: '3.3.3' }, 66 }, 67 }) 68 69 t.matchSnapshot(npm.flatOptions, 'full flat options object') 70}) 71 72t.test('basic usage', async t => { 73 // snapshot basic usage without commands since all the command snapshots 74 // are generated in the following test 75 const { npm } = await loadMockNpm(t, { 76 mocks: { 77 '{LIB}/utils/cmd-list.js': { commands: [] }, 78 }, 79 config: { userconfig: '/some/config/file/.npmrc' }, 80 globals: { process: { platform: 'posix' } }, 81 }) 82 83 t.cleanSnapshot = str => str 84 .replace(npm.npmRoot, '{BASEDIR}') 85 .replace(npm.config.get('userconfig'), '{USERCONFIG}') 86 .split(pkg.version).join('{VERSION}') 87 88 t.matchSnapshot(npm.usage) 89}) 90 91t.test('usage', async t => { 92 const readdir = async (dir, ext) => { 93 const files = await fs.readdir(dir) 94 return files.filter(f => extname(f) === ext).map(f => basename(f, ext)) 95 } 96 97 const fsCommands = await readdir(resolve(__dirname, '../../lib/commands'), '.js') 98 const docsCommands = await readdir(join(docs.paths.content, 'commands'), docs.DOC_EXT) 99 const bareCommands = ['npm', 'npx'] 100 101 // XXX: These extra commands exist as js files but not as docs pages 102 const allDocs = docsCommands.concat(['get', 'set', 'll']).map(n => n.replace('npm-', '')) 103 104 // ensure that the list of js files in commands, docs files, and the command list 105 // are all in sync. eg, this will error if a command is removed but not its docs file 106 t.strictSame( 107 fsCommands.sort(localeCompare), 108 cmdList.commands, 109 'command list and fs are the same' 110 ) 111 t.strictSame( 112 allDocs.filter(f => !bareCommands.includes(f)).sort(localeCompare), 113 cmdList.commands, 114 'command list and docs files are the same' 115 ) 116 117 // use the list of files from the docs since those include the special cases 118 // for the bare npm and npx usage 119 for (const cmd of allDocs) { 120 t.test(cmd, async t => { 121 let output = null 122 if (!bareCommands.includes(cmd)) { 123 const mock = await loadMockNpm(t, { command: cmd }) 124 output = mock[cmd].usage 125 } 126 127 const usage = docs.usage(docs.TAGS.USAGE, { path: cmd }) 128 const params = docs.params(docs.TAGS.CONFIG, { path: cmd }) 129 .split('\n') 130 .filter(l => l.startsWith('#### ')) 131 .join('\n') || 'NO PARAMS' 132 133 t.matchSnapshot([output, usage, params].filter(Boolean).join('\n\n')) 134 }) 135 } 136}) 137