@oclif/plugin-plugins
Version:
plugins plugin for oclif
351 lines (350 loc) • 14.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable no-await-in-loop */
const core_1 = require("@oclif/core");
const shelljs = require("shelljs");
const fs = require("fs");
const load_json_file_1 = require("load-json-file");
const path = require("path");
const semver_1 = require("semver");
const util_1 = require("./util");
const yarn_1 = require("./yarn");
const initPJSON = {
private: true,
oclif: { schema: 1, plugins: [] },
dependencies: {},
};
async function fileExists(filePath) {
try {
await fs.promises.access(filePath);
return true;
}
catch (_a) {
return false;
}
}
class Plugins {
constructor(config) {
this.config = config;
this.verbose = false;
this.yarn = new yarn_1.default({ config });
this.debug = require('debug')('@oclif/plugins');
}
async pjson() {
try {
const pjson = await (0, load_json_file_1.default)(this.pjsonPath);
return Object.assign(Object.assign(Object.assign({}, initPJSON), { dependencies: {} }), pjson);
}
catch (error) {
this.debug(error);
if (error.code !== 'ENOENT')
process.emitWarning(error);
return initPJSON;
}
}
async list() {
const pjson = await this.pjson();
return this.normalizePlugins(pjson.oclif.plugins);
}
async install(name, { tag = 'latest', force = false } = {}) {
try {
this.debug(`installing plugin ${name}`);
const yarnOpts = { cwd: this.config.dataDir, verbose: this.verbose };
await this.createPJSON();
let plugin;
const add = force ? ['add', '--force'] : ['add'];
if (name.includes(':')) {
// url
const url = name;
await this.yarn.exec([...add, url], yarnOpts);
name = Object.entries((await this.pjson()).dependencies || {}).find(([, u]) => u === url)[0];
plugin = await core_1.Config.load({
devPlugins: false,
userPlugins: false,
root: path.join(this.config.dataDir, 'node_modules', name),
name,
});
await this.refresh({ all: true, prod: true }, plugin.root);
this.isValidPlugin(plugin);
await this.add({ name, url, type: 'user' });
}
else {
// npm
const range = (0, semver_1.validRange)(tag);
const unfriendly = this.unfriendlyName(name);
if (unfriendly && (await this.npmHasPackage(unfriendly))) {
name = unfriendly;
}
await this.yarn.exec([...add, `${name}@${tag}`], yarnOpts);
this.debug(`loading plugin ${name}...`);
plugin = await core_1.Config.load({
devPlugins: false,
userPlugins: false,
root: path.join(this.config.dataDir, 'node_modules', name),
name,
});
this.debug(`finished loading plugin ${name} at root ${plugin.root}`);
this.isValidPlugin(plugin);
await this.refresh({ all: true, prod: true }, plugin.root);
await this.add({ name, tag: range || tag, type: 'user' });
}
return plugin;
}
catch (error) {
this.debug('error installing plugin:', error);
await this.uninstall(name).catch(error => this.debug(error));
if (String(error).includes('EACCES')) {
throw new core_1.Errors.CLIError(error, {
suggestions: [
`Plugin failed to install because of a permissions error.\nDoes your current user own the directory ${this.config.dataDir}?`,
],
});
}
throw error;
}
}
/**
* If a yarn.lock or oclif.lock exists at the root, refresh dependencies by
* rerunning yarn. If options.prod is true, only install production dependencies.
*
* As of v9 npm will always ignore the yarn.lock during `npm pack`]
* (see https://github.com/npm/cli/issues/6738). To get around this plugins can
* rename yarn.lock to oclif.lock before running `npm pack` using `oclif lock`.
*
* We still check for the existence of yarn.lock since it could be included if a plugin was
* packed using yarn or v8 of npm. Plugins installed directly from a git url will also
* have a yarn.lock.
*
* @param options {prod: boolean, all: boolean}
* @param roots string[]
* @returns Promise<void>
*/
async refresh(options, ...roots) {
const doRefresh = async (root) => {
await this.yarn.exec(options.prod ? ['--prod'] : [], {
cwd: root,
verbose: this.verbose,
});
};
const pluginRoots = [...roots];
if (options.all) {
const userPluginsRoots = this.config.getPluginsList()
.filter(p => p.type === 'user')
.map(p => p.root);
pluginRoots.push(...userPluginsRoots);
}
const deduped = [...new Set(pluginRoots)];
await Promise.all(deduped.map(async (r) => {
if (await fileExists(path.join(r, 'yarn.lock'))) {
this.debug(`yarn.lock exists at ${r}. Installing prod dependencies`);
await doRefresh(r);
}
else if (await fileExists(path.join(r, 'oclif.lock'))) {
this.debug(`oclif.lock exists at ${r}. Installing prod dependencies`);
await fs.promises.rename(path.join(r, 'oclif.lock'), path.join(r, 'yarn.lock'));
await doRefresh(r);
}
else {
this.debug(`no yarn.lock or oclif.lock exists at ${r}. Skipping dependency refresh`);
}
}));
}
async link(p) {
const c = await core_1.Config.load(path.resolve(p));
core_1.ux.action.start(`${this.config.name}: linking plugin ${c.name}`);
this.isValidPlugin(c);
// refresh will cause yarn.lock to install dependencies, including devDeps
await this.refresh({ prod: false, all: false }, c.root);
await this.add({ type: 'link', name: c.name, root: c.root });
}
async add(...plugins) {
const pjson = await this.pjson();
pjson.oclif.plugins = (0, util_1.uniq)([...(pjson.oclif.plugins || []), ...plugins]);
await this.savePJSON(pjson);
}
async remove(name) {
const pjson = await this.pjson();
if (pjson.dependencies)
delete pjson.dependencies[name];
pjson.oclif.plugins = this.normalizePlugins(pjson.oclif.plugins).filter(p => p.name !== name);
await this.savePJSON(pjson);
}
async uninstall(name) {
try {
const pjson = await this.pjson();
if ((pjson.oclif.plugins || []).find(p => typeof p === 'object' && p.type === 'user' && p.name === name)) {
await this.yarn.exec(['remove', name], {
cwd: this.config.dataDir,
verbose: this.verbose,
});
}
}
catch (error) {
core_1.ux.warn(error);
}
finally {
await this.remove(name);
}
}
async update() {
var _a;
let plugins = (await this.list()).filter((p) => p.type === 'user');
if (plugins.length === 0)
return;
core_1.ux.action.start(`${this.config.name}: Updating plugins`);
// migrate deprecated plugins
const aliases = this.config.pjson.oclif.aliases || {};
for (const [name, to] of Object.entries(aliases)) {
const plugin = plugins.find(p => p.name === name);
if (!plugin)
continue;
if (to)
await this.install(to);
await this.uninstall(name);
plugins = plugins.filter(p => p.name !== name);
}
if (plugins.find(p => Boolean(p.url))) {
await this.yarn.exec(['upgrade'], {
cwd: this.config.dataDir,
verbose: this.verbose,
});
}
const npmPlugins = plugins.filter(p => !p.url);
const jitPlugins = (_a = this.config.pjson.oclif.jitPlugins) !== null && _a !== void 0 ? _a : {};
const modifiedPlugins = [];
if (npmPlugins.length > 0) {
await this.yarn.exec(['add', ...npmPlugins.map(p => {
var _a;
// a not valid tag indicates that it's a dist-tag like 'latest'
if (!(0, semver_1.valid)(p.tag))
return `${p.name}@${p.tag}`;
if (p.tag && (0, semver_1.valid)(p.tag) && jitPlugins[p.name] && (0, semver_1.gt)(p.tag, jitPlugins[p.name])) {
// The user has installed a version of the JIT plugin that is newer than the one
// specified by the root plugin's JIT configuration. In this case, we want to
// keep the version installed by the user.
return `${p.name}@${p.tag}`;
}
const tag = (_a = jitPlugins[p.name]) !== null && _a !== void 0 ? _a : p.tag;
modifiedPlugins.push(Object.assign(Object.assign({}, p), { tag }));
return `${p.name}@${tag}`;
})], { cwd: this.config.dataDir, verbose: this.verbose });
}
await this.refresh({ all: true, prod: true });
await this.add(...modifiedPlugins);
core_1.ux.action.stop();
}
/* eslint-enable no-await-in-loop */
async hasPlugin(name) {
var _a, _b;
const list = await this.list();
const friendly = list.find(p => this.friendlyName(p.name) === this.friendlyName(name));
const unfriendly = list.find(p => this.unfriendlyName(p.name) === this.unfriendlyName(name));
const link = list.find(p => p.type === 'link' && path.resolve(p.root) === path.resolve(name));
return ((_b = (_a = friendly !== null && friendly !== void 0 ? friendly : unfriendly) !== null && _a !== void 0 ? _a : link) !== null && _b !== void 0 ? _b : false);
}
async yarnNodeVersion() {
try {
const f = await (0, load_json_file_1.default)(path.join(this.config.dataDir, 'node_modules', '.yarn-integrity'));
return f.nodeVersion;
}
catch (error) {
if (error.code !== 'ENOENT')
core_1.ux.warn(error);
}
}
unfriendlyName(name) {
if (name.includes('@'))
return;
const scope = this.config.pjson.oclif.scope;
if (!scope)
return;
return `@${scope}/plugin-${name}`;
}
async maybeUnfriendlyName(name) {
const unfriendly = this.unfriendlyName(name);
this.debug(`checking registry for expanded package name ${unfriendly}`);
if (unfriendly && (await this.npmHasPackage(unfriendly))) {
return unfriendly;
}
this.debug(`expanded package name ${unfriendly} not found, using given package name ${name}`);
return name;
}
friendlyName(name) {
const scope = this.config.pjson.oclif.scope;
if (!scope)
return name;
const match = name.match(`@${scope}/plugin-(.+)`);
if (!match)
return name;
return match[1];
}
async createPJSON() {
if (!fs.existsSync(this.pjsonPath)) {
this.debug(`creating ${this.pjsonPath} with pjson: ${JSON.stringify(initPJSON, null, 2)}`);
await this.savePJSON(initPJSON);
}
}
get pjsonPath() {
return path.join(this.config.dataDir, 'package.json');
}
async npmHasPackage(name) {
const nodeExecutable = (0, util_1.findNode)(this.config.root);
const npmCli = await (0, util_1.findNpm)();
this.debug(`Using node executable located at: ${nodeExecutable}`);
this.debug(`Using npm executable located at: ${npmCli}`);
const command = `${nodeExecutable} ${npmCli} show ${name} dist-tags`;
try {
const npmShowResult = shelljs.exec(command, {
silent: true,
async: false,
encoding: 'utf8',
});
if (npmShowResult.code !== 0) {
this.debug(npmShowResult.stderr);
return false;
}
this.debug(`Found ${name} in the registry.`);
return true;
}
catch (_a) {
throw new Error(`Could not run npm show for ${name}`);
}
}
async savePJSON(pjson) {
pjson.oclif.plugins = this.normalizePlugins(pjson.oclif.plugins);
this.debug(`saving pjson at ${this.pjsonPath}`, JSON.stringify(pjson, null, 2));
await fs.promises.mkdir(path.dirname(this.pjsonPath), { recursive: true });
await fs.promises.writeFile(this.pjsonPath, JSON.stringify(pjson, null, 2));
}
normalizePlugins(input) {
let plugins = (input || []).map(p => {
if (typeof p === 'string') {
return {
name: p,
type: 'user',
tag: 'latest',
};
}
return p;
});
plugins = (0, util_1.uniqWith)(plugins, (a, b) => a.name === b.name ||
(a.type === 'link' && b.type === 'link' && a.root === b.root));
return plugins;
}
isValidPlugin(p) {
if (p.valid)
return true;
if (!this.config.getPluginsList().find(p => p.name === '@oclif/plugin-legacy') ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore check if this plugin was loaded by `plugin-legacy`
p._base.includes('@oclif/plugin-legacy')) {
return true;
}
throw new core_1.Errors.CLIError('plugin is invalid', {
suggestions: [
'Plugin failed to install because it does not appear to be a valid CLI plugin.\nIf you are sure it is, contact the CLI developer noting this error.',
],
});
}
}
exports.default = Plugins;