@oclif/core
Version:
base library for oclif CLIs
375 lines (374 loc) • 16.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Plugin = void 0;
const node_path_1 = require("node:path");
const node_util_1 = require("node:util");
const tinyglobby_1 = require("tinyglobby");
const cache_1 = __importDefault(require("../cache"));
const errors_1 = require("../errors");
const module_loader_1 = require("../module-loader");
const performance_1 = require("../performance");
const symbols_1 = require("../symbols");
const cache_command_1 = require("../util/cache-command");
const find_root_1 = require("../util/find-root");
const fs_1 = require("../util/fs");
const read_pjson_1 = require("../util/read-pjson");
const util_1 = require("../util/util");
const ts_path_1 = require("./ts-path");
const util_2 = require("./util");
const _pjson = cache_1.default.getInstance().get('@oclif/core');
function topicsToArray(input, base) {
if (!input)
return [];
base = base ? `${base}:` : '';
if (Array.isArray(input)) {
return [...input, input.flatMap((t) => topicsToArray(t.subtopics, `${base}${t.name}`))];
}
return Object.keys(input).flatMap((k) => {
input[k].name = k;
return [{ ...input[k], name: `${base}${k}` }, ...topicsToArray(input[k].subtopics, `${base}${input[k].name}`)];
});
}
const cachedCommandCanBeUsed = (manifest, id) => Boolean(manifest?.commands[id] && 'isESM' in manifest.commands[id] && 'relativePath' in manifest.commands[id]);
const searchForCommandClass = (cmd) => {
if (typeof cmd.run === 'function')
return cmd;
if (cmd.default && cmd.default.run)
return cmd.default;
return Object.values(cmd).find((cmd) => typeof cmd.run === 'function');
};
const ensureCommandClass = (cmd) => {
if (cmd && typeof cmd.run === 'function')
return cmd;
};
const GLOB_PATTERNS = [
'**/*.+(js|cjs|mjs|ts|tsx|mts|cts)',
'!**/*.+(d.ts|test.ts|test.js|spec.ts|spec.js|d.mts|d.cts)?(x)',
];
function processCommandIds(files) {
return files.map((file) => {
const p = (0, node_path_1.parse)(file);
const topics = p.dir.split('/');
const command = p.name !== 'index' && p.name;
const id = [...topics, command].filter(Boolean).join(':');
return id === '' ? symbols_1.SINGLE_COMMAND_CLI_SYMBOL : id;
});
}
function determineCommandDiscoveryOptions(commandDiscovery) {
if (!commandDiscovery)
return;
if (typeof commandDiscovery === 'string') {
return { globPatterns: GLOB_PATTERNS, strategy: 'pattern', target: commandDiscovery };
}
if (!commandDiscovery.target)
throw new errors_1.CLIError('`oclif.commandDiscovery.target` is required.');
if (!commandDiscovery.strategy)
throw new errors_1.CLIError('`oclif.commandDiscovery.strategy` is required.');
if (commandDiscovery.strategy === 'explicit' && !commandDiscovery.identifier) {
commandDiscovery.identifier = 'default';
}
return commandDiscovery;
}
function determineHookOptions(hook) {
if (typeof hook === 'string')
return { identifier: 'default', target: hook };
if (!hook.identifier)
return { ...hook, identifier: 'default' };
return hook;
}
class Plugin {
options;
_base = `${_pjson.name}@${_pjson.version}`;
_debug = (0, util_2.makeDebug)();
alias;
alreadyLoaded = false;
children = [];
commandIDs = [];
// This will be initialized in the _manifest() method, which gets called in the load() method.
commands;
commandsDir;
hasManifest = false;
hooks;
isRoot = false;
manifest;
moduleType;
name;
parent;
pjson;
root;
tag;
type;
valid = false;
version;
commandCache;
commandDiscoveryOpts;
flexibleTaxonomy;
constructor(options) {
this.options = options;
}
get topics() {
return topicsToArray(this.pjson.oclif.topics || {});
}
async findCommand(id, opts = {}) {
const marker = performance_1.Performance.mark(performance_1.OCLIF_MARKER_OWNER, `plugin.findCommand#${this.name}.${id}`, {
id,
plugin: this.name,
});
const fetch = async () => {
if (this.commandDiscoveryOpts?.strategy === 'pattern') {
const commandsDir = await this.getCommandsDir();
if (!commandsDir)
return;
let module;
let isESM;
let filePath;
try {
;
({ filePath, isESM, module } = cachedCommandCanBeUsed(this.manifest, id)
? await (0, module_loader_1.loadWithDataFromManifest)(this.manifest.commands[id], this.root)
: await (0, module_loader_1.loadWithData)(this, (0, node_path_1.join)(commandsDir ?? this.pjson.oclif.commands, ...id.split(':'))));
this._debug(isESM ? '(import)' : '(require)', filePath);
}
catch (error) {
if (!opts.must && error.code === 'MODULE_NOT_FOUND')
return;
throw error;
}
const cmd = searchForCommandClass(module);
if (!cmd)
return;
cmd.id = id;
cmd.plugin = this;
cmd.isESM = isESM;
cmd.relativePath = (0, node_path_1.relative)(this.root, filePath || '').split(node_path_1.sep);
return cmd;
}
if (this.commandDiscoveryOpts?.strategy === 'single' || this.commandDiscoveryOpts?.strategy === 'explicit') {
const commandCache = await this.loadCommandsFromTarget();
const cmd = ensureCommandClass(commandCache?.[id]);
if (!cmd)
return;
cmd.id = id;
cmd.plugin = this;
return cmd;
}
};
const cmd = await fetch();
if (!cmd && opts.must)
(0, errors_1.error)(`command ${id} not found`);
marker?.stop();
return cmd;
}
// eslint-disable-next-line complexity
async load() {
this.type = this.options.type ?? 'core';
this.tag = this.options.tag;
this.isRoot = this.options.isRoot ?? false;
if (this.options.parent)
this.parent = this.options.parent;
// Linked plugins already have a root so there's no need to search for it.
// However there could be child plugins nested inside the linked plugin, in which
// case we still need to search for the child plugin's root.
const root = this.options.pjson && this.options.isRoot
? this.options.root
: this.type === 'link' && !this.parent
? this.options.root
: await (0, find_root_1.findRoot)(this.options.name, this.options.root);
if (!root)
throw new errors_1.CLIError(`could not find package.json with ${(0, node_util_1.inspect)(this.options)}`);
this.root = root;
this._debug(`loading ${this.type} plugin from ${root}`);
this.pjson = this.options.pjson ?? (await (0, read_pjson_1.readPjson)(root));
this.flexibleTaxonomy = this.options?.flexibleTaxonomy || this.pjson.oclif?.flexibleTaxonomy || false;
this.moduleType = this.pjson.type === 'module' ? 'module' : 'commonjs';
this.name = this.pjson.name;
this.alias = this.options.name ?? this.pjson.name;
if (!this.name)
throw new errors_1.CLIError(`no name in package.json (${root})`);
this._debug = (0, util_2.makeDebug)(this.name);
this.version = this.pjson.version;
if (this.pjson.oclif) {
this.valid = true;
}
else {
this.pjson.oclif = this.pjson['cli-engine'] || {};
}
this.hooks = Object.fromEntries(Object.entries(this.pjson.oclif.hooks ?? {}).map(([k, v]) => [
k,
(0, util_1.castArray)(v).map((v) => determineHookOptions(v)),
]));
this.commandDiscoveryOpts = determineCommandDiscoveryOptions(this.pjson.oclif?.commands);
this._debug('command discovery options', this.commandDiscoveryOpts);
this.manifest = await this._manifest();
this.commands = Object.entries(this.manifest.commands)
.map(([id, c]) => ({
...c,
load: async () => this.findCommand(id, { must: true }),
pluginAlias: this.alias,
pluginType: c.pluginType === 'jit' ? 'jit' : this.type,
}))
.sort((a, b) => a.id.localeCompare(b.id));
}
async _manifest() {
const ignoreManifest = Boolean(this.options.ignoreManifest);
const errorOnManifestCreate = Boolean(this.options.errorOnManifestCreate);
const respectNoCacheDefault = Boolean(this.options.respectNoCacheDefault);
const readManifest = async (dotfile = false) => {
try {
const p = (0, node_path_1.join)(this.root, `${dotfile ? '.' : ''}oclif.manifest.json`);
const manifest = await (0, fs_1.readJson)(p);
if (!process.env.OCLIF_NEXT_VERSION && manifest.version.split('-')[0] !== this.version.split('-')[0]) {
process.emitWarning(`Mismatched version in ${this.name} plugin manifest. Expected: ${this.version} Received: ${manifest.version}\nThis usually means you have an oclif.manifest.json file that should be deleted in development. This file should be automatically generated when publishing.`);
}
else {
this._debug('using manifest from', p);
this.hasManifest = true;
return manifest;
}
}
catch (error) {
if (error.code === 'ENOENT') {
if (!dotfile)
return readManifest(true);
}
else {
this.warn(error, 'readManifest');
}
}
};
const marker = performance_1.Performance.mark(performance_1.OCLIF_MARKER_OWNER, `plugin.manifest#${this.name}`, { plugin: this.name });
if (!ignoreManifest) {
const manifest = await readManifest();
if (manifest) {
marker?.addDetails({ commandCount: Object.keys(manifest.commands).length, fromCache: true });
marker?.stop();
this.commandIDs = Object.keys(manifest.commands);
return manifest;
}
}
this.commandIDs = await this.getCommandIDs();
const manifest = {
commands: (await Promise.all(this.commandIDs.map(async (id) => {
try {
const found = await this.findCommand(id, { must: true });
const cached = await (0, cache_command_1.cacheCommand)(found, this, respectNoCacheDefault);
// Ensure that id is set to the id being processed
// This is necessary because the id is set by findCommand but if there
// are multiple instances of a Command, then the id will be set to the
// last one found.
cached.id = id;
if (this.flexibleTaxonomy) {
const permutations = (0, util_2.getCommandIdPermutations)(id);
const aliasPermutations = cached.aliases.flatMap((a) => (0, util_2.getCommandIdPermutations)(a));
return [id, { ...cached, aliasPermutations, permutations }];
}
return [id, cached];
}
catch (error) {
const scope = `findCommand (${id})`;
if (Boolean(errorOnManifestCreate) === false)
this.warn(error, scope);
else
throw this.addErrorScope(error, scope);
}
})))
// eslint-disable-next-line unicorn/prefer-native-coercion-functions
.filter((f) => Boolean(f))
.reduce((commands, [id, c]) => {
commands[id] = c;
return commands;
}, {}),
version: this.version,
};
marker?.addDetails({ commandCount: Object.keys(manifest.commands).length, fromCache: false });
marker?.stop();
return manifest;
}
addErrorScope(err, scope) {
err.name = err.name ?? (0, node_util_1.inspect)(err).trim();
err.detail = (0, util_1.compact)([
err.detail,
`module: ${this._base}`,
scope && `task: ${scope}`,
`plugin: ${this.name}`,
`root: ${this.root}`,
...(err.code ? [`code: ${err.code}`] : []),
...(err.message ? [`message: ${err.message}`] : []),
'See more details with DEBUG=*',
]).join('\n');
return err;
}
async getCommandIDs() {
const marker = performance_1.Performance.mark(performance_1.OCLIF_MARKER_OWNER, `plugin.getCommandIDs#${this.name}`, { plugin: this.name });
let ids;
switch (this.commandDiscoveryOpts?.strategy) {
case 'explicit': {
ids = (await this.getCommandIdsFromTarget()) ?? [];
break;
}
case 'pattern': {
ids = await this.getCommandIdsFromPattern();
break;
}
case 'single': {
ids = (await this.getCommandIdsFromTarget()) ?? [];
break;
}
default: {
ids = [];
}
}
this._debug('found commands', ids);
marker?.addDetails({ count: ids.length });
marker?.stop();
return ids;
}
async getCommandIdsFromPattern() {
const commandsDir = await this.getCommandsDir();
if (!commandsDir)
return [];
this._debug(`loading IDs from ${commandsDir}`);
const files = await (0, tinyglobby_1.glob)(this.commandDiscoveryOpts?.globPatterns ?? GLOB_PATTERNS, { cwd: commandsDir });
return processCommandIds(files);
}
async getCommandIdsFromTarget() {
const commandsFromExport = await this.loadCommandsFromTarget();
if (commandsFromExport) {
return Object.entries((await this.loadCommandsFromTarget()) ?? [])
.filter(([, cmd]) => ensureCommandClass(cmd))
.map(([id]) => id);
}
}
async getCommandsDir() {
if (this.commandsDir)
return this.commandsDir;
this.commandsDir = await (0, ts_path_1.tsPath)(this.root, this.commandDiscoveryOpts?.target, this);
return this.commandsDir;
}
async loadCommandsFromTarget() {
if (this.commandCache)
return this.commandCache;
if (this.commandDiscoveryOpts?.strategy === 'explicit' && this.commandDiscoveryOpts.target) {
const filePath = await (0, ts_path_1.tsPath)(this.root, this.commandDiscoveryOpts.target, this);
const module = await (0, module_loader_1.load)(this, filePath);
this.commandCache = module[this.commandDiscoveryOpts?.identifier ?? 'default'] ?? {};
return this.commandCache;
}
if (this.commandDiscoveryOpts?.strategy === 'single' && this.commandDiscoveryOpts.target) {
const filePath = await (0, ts_path_1.tsPath)(this.root, this.commandDiscoveryOpts?.target ?? this.root, this);
const module = await (0, module_loader_1.load)(this, filePath);
this.commandCache = { [symbols_1.SINGLE_COMMAND_CLI_SYMBOL]: searchForCommandClass(module) };
return this.commandCache;
}
}
warn(err, scope) {
if (typeof err === 'string')
err = new Error(err);
const warning = this.addErrorScope(err, scope);
process.emitWarning(warning.name, warning);
}
}
exports.Plugin = Plugin;