@kui-shell/core
Version:
An Electron-based shell for cloud-native development
469 lines (467 loc) • 16.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.scanForModules = exports.generatePrescanModel = exports.assemble = void 0;
var _debug = _interopRequireDefault(require("debug"));
var _path = require("path");
var _plugins = require("./plugins");
var _commandTree = require("../core/command-tree");
var _errors = require("../models/errors");
var _Theme = require("../webapp/themes/Theme");
var _tree = require("../commands/tree");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/*
* Copyright 2017 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const debug = (0, _debug.default)('core/plugins/scanner');
debug('loading');
/**
* A data structure to facilitate computation of the Prescan model
*
*/
class ScanCache {
constructor(registrar) {
/** map from command to plugin *name* that defines it */
this.commandToPlugin = {};
/** topological sort of the plugins (order they resolved) */
this.topological = {};
/** some plugins override the behavior of others */
this.overrides = {};
/** as we scan for plugins, we'll memoize their usage models */
this.usage = {};
// TODO document
this.flat = [];
this.isSubtreeSynonym = {};
this.isSynonym = {};
this.registrar = registrar;
}
}
/**
* Override the standard ImplForPlugins with some extra state that we
* need for scanning.
*
*/
class CommandRegistrarForScan extends _commandTree.ImplForPlugins {
constructor(plugin, scanCache) {
super(plugin);
this.scanCache = scanCache;
/** map to plugin *name* */
this.cmdToPlugin = {};
}
subtreeSynonym(route, master) {
if (route !== master.route) {
this.scanCache.isSubtreeSynonym[route] = true;
this.scanCache.isSubtreeSynonym[master.route] = true;
return super.subtreeSynonym(route, master, {});
}
}
listen(route, handler, options) {
this.cmdToPlugin[route] = this.plugin;
return super.listen(route, handler, options);
}
subtree(route, options) {
return super.subtree(route, Object.assign({
listen: this.listen.bind(this)
}, options));
}
synonym(route, handler, master, options) {
this.cmdToPlugin[route] = this.plugin;
this.scanCache.isSynonym[route] = true;
return super.synonym(route, handler, master, options);
}
}
/**
* Load one plugin for the given plugin route, located in the given pluginPath on the local filesystem
*
*/
const loadPlugin = (route, pluginPath, scanCache) => __awaiter(void 0, void 0, void 0, function* () {
debug('loadPlugin %s', route);
// create a CommandRegistrar instance for this one plugin load
const ctree = new CommandRegistrarForScan(route, scanCache);
const pluginLoaderRef = yield Promise.resolve().then(() => require(`${pluginPath}`));
function isDirect(loader) {
return typeof loader === 'function';
}
const pluginLoader = isDirect(pluginLoaderRef) ? pluginLoaderRef : pluginLoaderRef.default;
if (typeof pluginLoader === 'function') {
// invoke the plugin loader
scanCache.registrar[route] = yield pluginLoader(ctree, {});
// generate a mapping from commands (e.g. "/git/status" which is
// hosted by the bash-like plugin) to plugin (e.g. "bash-like"),
// which services that command
const {
cmdToPlugin
} = ctree;
for (const k in cmdToPlugin) {
if (scanCache.commandToPlugin[k]) {
debug('override', k, cmdToPlugin[k], scanCache.commandToPlugin[k]);
scanCache.overrides[k] = cmdToPlugin[k];
} else {
debug('not override', k, cmdToPlugin[k]);
}
scanCache.commandToPlugin[k] = cmdToPlugin[k];
}
}
});
/**
* Attempt to load the plugins during a plugin scan
*
* @param lastError so we don't repeat making the same mistake 100 times!
*
*/
const topologicalSortForScan = (scanCache, pluginPaths, iter, lastError, lastErrorAlreadyEmitted) => __awaiter(void 0, void 0, void 0, function* () {
debug('topologicalSortForScan', iter);
if (iter >= 100) {
debug('unable to resolve plugins');
if (lastError) {
if (!lastErrorAlreadyEmitted) {
throw lastError;
} // fallthrough intentional
throw new Error('Unable to resolve plugins');
}
}
let nUnresolved = 0;
const unresolved = [];
for (const route in pluginPaths) {
debug('resolving %s', route);
try {
const module = {
route,
path: pluginPaths[route]
};
yield loadPlugin(route, pluginPaths[route], scanCache);
scanCache.flat.push(module);
delete pluginPaths[route];
} catch (err) {
const notFound = err.message.indexOf('Module not found') >= 0 || err.message.indexOf('Cannot find module') >= 0;
if ((!notFound || iter > 10) && lastError && lastError.message !== err.message) {
//
// note how we do not print the error if any of three
// conditions hold (but we still continue iterating)
//
// 1. Module not found errors; these are why we have to iterate, because of inter-module dependencies
//
// 2. don't print errors for the first 10 iterations; only print errors if they persist
//
// 3. if this error is the same as the last; no sense in repeating ourselves
//
debug('not retrying');
console.error(err);
lastErrorAlreadyEmitted = true;
}
//
// let's try again; here we are implementing a fixpoint
// computation by iterating till convergence; this fixpoint
// gives us the topological sort without having to bother
// computing the topological sort!
//
debug('retry on', err);
lastError = err;
nUnresolved++;
unresolved.push(route);
}
}
if (nUnresolved > 0) {
debug('nUnresolved', nUnresolved, unresolved);
return topologicalSortForScan(scanCache, pluginPaths, iter + 1, lastError, lastErrorAlreadyEmitted);
} else {
debug('topologicalSortForScan done');
}
});
const scanForModules = (dir, quiet = false, filter = () => true) => __awaiter(void 0, void 0, void 0, function* () {
debug('scanForModules %s', dir);
const fs = yield Promise.resolve().then(() => require('fs'));
const path = yield Promise.resolve().then(() => require('path'));
const colors = yield Promise.resolve().then(() => require('colors/safe'));
try {
const plugins = {};
const preloads = {};
const themeSets = [];
const doScan = ({
modules,
moduleDir,
parentPath,
lookForPlugin = true,
lookForPrescan = true
}) => {
debug('doScan', modules);
modules.forEach(module => {
const modulePath = path.join(moduleDir, module);
const name = (parentPath ? `${parentPath}/` : '') + module;
try {
const themePath = path.join(modulePath, 'theme.json');
if (fs.existsSync(themePath)) {
const themeSet = require(themePath);
const nThemes = !themeSet || !themeSet.themes ? 0 : themeSet.themes.length;
if (nThemes > 0) {
// set these to have the latest apiVersion unless otherwise stated
// this decision will be serialized out to the prescan persisted model
themeSet.themes.forEach(_ => {
if (!_.apiVersion) {
_.apiVersion = _Theme.apiVersion;
}
});
const msg = colors.bold(colors.rainbow('themes')) + colors.dim(` x${nThemes}`);
console.log(colors.green(' \u2713 ') + msg + '\t' + path.basename(module));
// add this plugin's themes onto the full list, across all plugins
themeSets.push({
themes: themeSet.themes,
plugin: module
});
}
}
} catch (err) {
console.error('Error registering theme', err);
}
if (module.charAt(0) === '@') {
// support for @owner/repo style modules; see shell issue #260
return doScan({
modules: fs.readdirSync(modulePath),
moduleDir: modulePath,
parentPath: module
});
}
function lookFor(filename, destMap, colorFn) {
const pluginPath = path.join(moduleDir, module, filename);
debug('lookFor', filename, pluginPath);
/** report a successful find to the console */
const ok = () => {
const parent = parentPath ? `${parentPath}/` : '';
console.log(colors.green(' \u2713 ') + colorFn(filename.replace(/\..*$/, '')) + '\t' + parent + path.basename(module));
};
if (fs.existsSync(pluginPath)) {
if (!quiet) {
debug('found', name);
ok();
}
destMap[name] = pluginPath;
} else {
const backupPluginPath = path.join(modulePath, 'dist', filename);
debug('lookFor2', filename, backupPluginPath);
if (fs.existsSync(backupPluginPath)) {
if (!quiet) {
debug('found2', name);
ok();
}
destMap[name] = backupPluginPath;
} else {
// support for javascript-coded plugins (monorepo)
const backupPluginPath = path.join(modulePath, 'src/plugin', filename);
debug('lookFor3', filename, backupPluginPath);
if (fs.existsSync(backupPluginPath)) {
if (!quiet) {
debug('found3', name);
ok();
}
destMap[name] = backupPluginPath;
} else {
// support for javascript-coded plugins (external client)
const backupPluginPath = path.join(modulePath, 'plugin', filename);
debug('lookFor4', filename, backupPluginPath);
if (fs.existsSync(backupPluginPath)) {
if (!quiet) {
debug('found4', name);
ok();
}
destMap[name] = backupPluginPath;
// console.error('Skipping plugin, because it does not have a plugin.js', module)
}
}
}
}
if (fs.statSync(modulePath).isDirectory()) {
const subDirs = fs.readdirSync(modulePath).filter(_ => !/(^m?dist)|(bin)|(web)|(src)|(samples)|(i18n)|(tests)|(node_modules)$/.test(_) && fs.existsSync(path.join(modulePath, _)) &&
// see https://github.com/kubernetes-sigs/kui/issues/7326
fs.statSync(path.join(modulePath, _)).isDirectory());
if (subDirs.length > 0) {
doScan({
modules: subDirs,
moduleDir: modulePath,
parentPath: module,
lookForPlugin: filename === 'plugin.js',
lookForPrescan: filename === 'preload.js'
});
}
}
}
if (lookForPlugin) {
lookFor('plugin.js', plugins, colors.bold);
}
if (lookForPrescan) {
lookFor('preload.js', preloads, colors.dim);
}
});
};
// scan the app/plugins/modules directory
const moduleDir = dir; // path.join(dir, 'modules')
debug('moduleDir', moduleDir);
doScan({
modules: fs.readdirSync(moduleDir).filter(filter),
moduleDir
});
return {
plugins,
preloads,
themeSets
};
} catch (err) {
if ((0, _errors.isCodedError)(err) && err.code !== 'ENOENT') {
console.error('Error scanning for external plugins', err);
}
return {};
}
});
/**
* Scan for plugins hosted by the client itself, rather than npm
* installed from somewhere else
*
*/
exports.scanForModules = scanForModules;
function clientHosted(opts) {
return __awaiter(this, void 0, void 0, function* () {
const pluginRootAbsolute = process.env.PLUGIN_ROOT || opts.pluginRoot || (0, _path.join)(__dirname, _plugins.pluginRoot); // filesystem path for the plugins
debug('pluginRootAbsolute', pluginRootAbsolute);
const topOfScan = opts.pluginRoot || pluginRootAbsolute;
debug('using clientHosted scan', topOfScan);
return scanForModules(topOfScan);
});
}
/**
* Scan for plugins that clients incorporate via `npm install`
*
*/
function clientRequired() {
return __awaiter(this, void 0, void 0, function* () {
const topOfScan = (0, _path.dirname)(require.resolve('@kui-shell/prescan.json'));
debug('using clientRequired scan', topOfScan);
return scanForModules(topOfScan, false, filename => /^plugin-/.test(filename) || /^client$/.test(filename));
});
}
/**
* Look for plugins by scanning the local filesystem
*
*/
const resolveFromLocalFilesystem = (scanCache, opts = {}) => __awaiter(void 0, void 0, void 0, function* () {
debug('resolveFromLocalFilesystem');
const {
plugins,
preloads,
themeSets
} = opts.externalOnly ? yield clientHosted(opts) : yield clientRequired();
debug('availablePlugins %s', JSON.stringify(plugins));
// then, we load the plugins
yield topologicalSortForScan(scanCache, plugins, 0);
return {
preloads,
themeSets
};
});
/**
* Generate a prescan model
*
*/
const generatePrescanModel = (registrar, opts) => __awaiter(void 0, void 0, void 0, function* () {
debug('generatePrescanModel', opts);
// this will store the state of a scan
const scanCache = new ScanCache(registrar);
// do the scan, which will populate that StateCache instance
const {
preloads,
themeSets
} = yield resolveFromLocalFilesystem(scanCache, opts);
const disambiguator = {};
for (const route in scanCache.commandToPlugin) {
const A = route.split('/');
for (let idx = 1; idx < A.length; idx++) {
const cmd = `/${A.slice(idx).join('/')}`;
if (!disambiguator[cmd] && (route === cmd || !scanCache.commandToPlugin[route])) {
// this is, so far, an unambiguous resolution, and we don't
// have a specific resolution for this cmd
disambiguator[cmd] = route;
scanCache.commandToPlugin[cmd] = scanCache.commandToPlugin[route];
} else if (disambiguator[cmd]) {
// a conflict. is it a subtree-synonym conflcit? if so ignore the conflict
const subtree = route.substring(0, route.lastIndexOf('/'));
if (!scanCache.isSubtreeSynonym[subtree]) {
if (disambiguator[cmd] === cmd) {
// rule in favor of what we have
} else if (route === cmd) {
// rule in favor of the new one
disambiguator[cmd] = route;
scanCache.commandToPlugin[cmd] = scanCache.commandToPlugin[route];
} else {
// collision, remove the previous disambiguator
disambiguator[cmd] = true;
delete scanCache.commandToPlugin[cmd];
}
}
}
}
}
return {
preloads: Object.keys(preloads).map(route => ({
route,
path: preloads[route]
})),
themeSets,
commandToPlugin: scanCache.commandToPlugin,
topological: scanCache.topological,
flat: scanCache.flat,
overrides: scanCache.overrides,
usage: scanCache.usage,
disambiguator: undefined,
catchalls: (0, _tree.getModel)().catchalls,
docs: undefined // assembler.ts will fill this in
};
});
/**
* Assemble the plugins for faster loading
*
*/
exports.generatePrescanModel = generatePrescanModel;
const assemble = (registrar, opts) => {
return generatePrescanModel(registrar, Object.assign({
assembly: true
}, opts));
};
exports.assemble = assemble;