Skip to content

Commit 4e4a481

Browse files
authored
feat: add plugins:inspect (#237)
* feat: add plugins:inspect * chore: code review
1 parent 7a6fbf1 commit 4e4a481

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed

src/commands/plugins/inspect.ts

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import * as path from 'path'
2+
import {Command, flags} from '@oclif/command'
3+
import {Plugin} from '@oclif/config'
4+
import * as chalk from 'chalk'
5+
import {exec} from 'child_process'
6+
import * as fs from 'fs-extra'
7+
8+
import Plugins from '../../plugins'
9+
import {sortBy} from '../../util'
10+
11+
const TAB = ' '
12+
13+
type Dependencies = {
14+
[key: string]: unknown;
15+
from?: string;
16+
version?: string;
17+
name?: string;
18+
dependencies: {
19+
[key: string]: Dependencies;
20+
};
21+
}
22+
23+
export default class PluginsInspect extends Command {
24+
static description = 'displays installation properties of a plugin';
25+
26+
static usage = 'plugins:inspect PLUGIN...';
27+
28+
static examples = [
29+
'$ <%= config.bin %> plugins:inspect <%- config.pjson.oclif.examplePlugin || "myplugin" %> ',
30+
];
31+
32+
static strict = false;
33+
34+
static args = [
35+
{name: 'plugin', description: 'plugin to inspect', required: true, default: '.'},
36+
];
37+
38+
static flags = {
39+
help: flags.help({char: 'h'}),
40+
verbose: flags.boolean({char: 'v'}),
41+
};
42+
43+
plugins = new Plugins(this.config);
44+
45+
allDeps: Dependencies = {dependencies: {}};
46+
47+
// In this case we want these operations to happen
48+
// sequentially so the `no-await-in-loop` rule is ugnored
49+
/* eslint-disable no-await-in-loop */
50+
async run() {
51+
this.allDeps = await this.npmList(this.config.root, 3)
52+
const {flags, argv} = this.parse(PluginsInspect)
53+
if (flags.verbose) this.plugins.verbose = true
54+
const aliases = this.config.pjson.oclif.aliases || {}
55+
for (let name of argv) {
56+
if (name === '.') {
57+
const pkgJson = JSON.parse(await fs.readFile('package.json', 'utf-8'))
58+
name = pkgJson.name
59+
}
60+
if (aliases[name] === null) this.error(`${name} is blocked`)
61+
name = aliases[name] || name
62+
const pluginName = await this.parsePluginName(name)
63+
64+
try {
65+
await this.inspect(pluginName)
66+
} catch (error) {
67+
this.log(chalk.bold.red('failed'))
68+
throw error
69+
}
70+
}
71+
}
72+
/* eslint-enable no-await-in-loop */
73+
74+
async parsePluginName(input: string): Promise<string> {
75+
if (input.includes('@') && input.includes('/')) {
76+
input = input.slice(1)
77+
const [name] = input.split('@')
78+
return '@' + name
79+
}
80+
const [splitName] = input.split('@')
81+
const name = await this.plugins.maybeUnfriendlyName(splitName)
82+
return name
83+
}
84+
85+
findPlugin(pluginName: string): Plugin {
86+
const pluginConfig = this.config.plugins.find(plg => plg.name === pluginName)
87+
88+
if (pluginConfig) return pluginConfig as Plugin
89+
throw new Error(`${pluginName} not installed`)
90+
}
91+
92+
async inspect(pluginName: string) {
93+
const plugin = this.findPlugin(pluginName)
94+
this.log(chalk.bold.cyan(plugin.name))
95+
96+
this.log(`${TAB}version: ${plugin.version}`)
97+
if (plugin.tag) this.log(`${TAB}tag: ${plugin.tag}`)
98+
if (plugin.pjson.homepage) this.log(`${TAB}homepage: ${plugin.pjson.homepage}`)
99+
this.log(`${TAB}location: ${plugin.root}`)
100+
101+
this.log(`${TAB}commands:`)
102+
const commands = sortBy(plugin.commandIDs, c => c)
103+
commands.forEach(cmd => this.log(`${TAB.repeat(2)}${cmd}`))
104+
105+
const dependencies = plugin.root.includes(this.config.root) ?
106+
this.findDepInTree(plugin).dependencies :
107+
(await this.npmList(plugin.root)).dependencies
108+
109+
this.log(`${TAB}dependencies:`)
110+
const deps = sortBy(Object.keys(dependencies), d => d)
111+
for (const dep of deps) {
112+
// eslint-disable-next-line no-await-in-loop
113+
const version = dependencies[dep].version || await this.findDepInSharedModules(plugin, dep)
114+
const from = dependencies[dep].from ?
115+
dependencies[dep].from!.split('@').reverse()[0] :
116+
null
117+
118+
if (from) this.log(`${TAB.repeat(2)}${dep}: ${from} => ${version}`)
119+
else this.log(`${TAB.repeat(2)}${dep}: ${version}`)
120+
}
121+
}
122+
123+
async findDepInSharedModules(plugin: Plugin, dependency: string): Promise<string> {
124+
const sharedModulePath = path.join(plugin.root.replace(plugin.name, ''), ...dependency.split('/'), 'package.json')
125+
const pkgJson = JSON.parse(await fs.readFile(sharedModulePath, 'utf-8'))
126+
return pkgJson.version as string
127+
}
128+
129+
findDepInTree(plugin: Plugin): Dependencies {
130+
if (plugin.name === this.allDeps.name) return this.allDeps
131+
const plugins = [plugin.name]
132+
let p = plugin
133+
while (p.parent) {
134+
plugins.push(p.parent.name)
135+
p = p.parent
136+
}
137+
138+
let dependencies = this.allDeps
139+
for (const plg of plugins.reverse()) {
140+
dependencies = dependencies.dependencies[plg]
141+
}
142+
return dependencies
143+
}
144+
145+
async npmList(cwd: string, depth = 0): Promise<Dependencies> {
146+
return new Promise((resolve, reject) => {
147+
exec(`npm list --json --depth ${depth}`, {
148+
cwd,
149+
encoding: 'utf-8',
150+
maxBuffer: 2048 * 2048,
151+
}, (error, stdout) => {
152+
if (error) {
153+
try {
154+
const parsed = JSON.parse(stdout)
155+
if (parsed) resolve(parsed)
156+
} catch {
157+
reject(new Error(`Could not get dependencies for ${cwd}`))
158+
}
159+
} else {
160+
resolve(JSON.parse(stdout))
161+
}
162+
})
163+
})
164+
}
165+
}

0 commit comments

Comments
 (0)