UNPKG

@kui-shell/core

Version:

An Electron-based shell for cloud-native development

469 lines (467 loc) • 16.9 kB
"use strict"; 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;