@semo/core
Version:
The core of Semo
1,213 lines • 75 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.Utils = exports.isUsingTsRunner = exports.getNodeRuntime = void 0;
const crypto_1 = __importDefault(require("crypto"));
const path_1 = __importDefault(require("path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const glob_1 = require("glob");
const table_1 = require("table");
const find_up_1 = __importDefault(require("find-up"));
const lodash_1 = __importDefault(require("lodash"));
const yaml_1 = __importDefault(require("yaml"));
const json_colorizer_1 = __importDefault(require("json-colorizer"));
const json_stringify_pretty_compact_1 = __importDefault(require("json-stringify-pretty-compact"));
const picocolors_1 = __importDefault(require("picocolors"));
const debug_1 = __importDefault(require("debug"));
const child_process_1 = require("child_process");
const object_hash_1 = __importDefault(require("object-hash"));
const get_stdin_1 = __importDefault(require("get-stdin"));
const node_cache_1 = __importDefault(require("node-cache"));
const yargs_1 = __importDefault(require("yargs"));
const yargs_2 = __importDefault(require("yargs/yargs"));
const envinfo_1 = __importDefault(require("envinfo"));
const dotenv_1 = __importDefault(require("dotenv"));
const dotenv_expand_1 = require("dotenv-expand");
const shelljs_1 = __importDefault(require("shelljs"));
const chalk_1 = __importDefault(require("chalk"));
const hook_1 = require("./hook");
const dayjs_1 = __importDefault(require("dayjs"));
// @ts-ignore
const yParser = yargs_2.default.Parser;
let cachedInstance;
const getNodeRuntime = () => {
const script = process.env.npm_lifecycle_script || '';
if (script.startsWith('tsx ')) {
return 'tsx';
}
if (script.startsWith('jiti ')) {
return 'jiti';
}
if (script.includes('ts-node')) {
return 'ts-node';
}
return 'node';
};
exports.getNodeRuntime = getNodeRuntime;
/**
* Determines if the current Node.js process is using a TypeScript runner.
*
* @returns {boolean} `true` if a TypeScript runner is detected, otherwise `false`.
*/
const isUsingTsRunner = () => {
return ((0, exports.getNodeRuntime)() === 'ts-node' ||
(0, exports.getNodeRuntime)() === 'jiti' ||
(0, exports.getNodeRuntime)() === 'tsx');
};
exports.isUsingTsRunner = isUsingTsRunner;
/**
* Get Semo internal cache instance
* @returns {NodeCache}
*/
const getInternalCache = function () {
if (!cachedInstance) {
cachedInstance = new node_cache_1.default({
useClones: false,
});
}
return cachedInstance;
};
cachedInstance = getInternalCache();
/**
* Get Semo cache instance by namespace
* @param {string} namespace
* @returns {NodeCache}
*/
const getCache = function (namespace) {
if (!namespace) {
throw Error('Namespace is necessary.');
}
let cachedNamespaceInstances;
cachedNamespaceInstances = cachedInstance.get('cachedNamespaceInstances');
if (!cachedNamespaceInstances) {
cachedNamespaceInstances = {};
}
if (!cachedNamespaceInstances[namespace]) {
cachedNamespaceInstances[namespace] = new node_cache_1.default({
useClones: false,
});
cachedInstance.set('cachedNamespaceInstances', cachedNamespaceInstances);
}
return cachedNamespaceInstances[namespace];
};
/**
* Use dotenv style
* @param expand expand dotenv
* @param options dotenv options
*/
const useDotEnv = (expand, options = {}) => {
try {
const myEnv = dotenv_1.default.config();
if (expand && !myEnv.error) {
(0, dotenv_expand_1.expand)(myEnv);
}
}
catch (e) {
// .env may not exist, it's not a serious bug
}
};
/**
* debug core
*/
const debugCore = function (...args) {
let debugCache = getInternalCache().get('debug');
if (!debugCache) {
const argv = getInternalCache().get('argv');
const scriptName = argv && argv.scriptName ? argv.scriptName : 'semo';
debugCache = (0, debug_1.default)(`${scriptName}-core`);
getInternalCache().set('debug', debugCache);
}
debugCache(...args);
return debugCache;
};
const fileExistsSyncCache = function (filePath) {
const fileCheckHistory = cachedInstance.get('fileCheckHistory') || {};
if (fileCheckHistory[filePath]) {
fileCheckHistory[filePath].count++;
return fileCheckHistory[filePath].existed;
}
const existed = fs_extra_1.default.existsSync(filePath);
fileCheckHistory[filePath] = { count: 1, existed };
cachedInstance.set('fileCheckHistory', fileCheckHistory);
return fileCheckHistory[filePath].existed;
};
/**
* Run hook in all valid plugins and return the combined results.
* Plugins implement hook in `module.exports`, could be generator function or promise function or non-function
* For non-function, it will be used as hook data directly, likely to be returned by function
* @example
* const hookReturn = await Utils.invokeHook('semo:hook')
* @param {string} hook Hook name, suggest plugin defined hook include a prefix, e.g. `prefix:hook`
* @param {string} options Options
* @param {string} options.mode Hook mode, could be `assign`, `merge`, `push`, `replace`, `group`, default is assign.
* @param {bool} options.useCache If or not use cached hook result
* @param {array} options.include set plugins to be used in invoking
* @param {array} options.exclude set plugins not to be used in invoking, same ones options.exclude take precedence
* @param {boolean} options.reload If or not clear module cache before require
*/
const invokeHook = async function (hook = null, options = { mode: 'assign' }, argv = null) {
const splitHookName = hook.split(':');
let moduler, originModuler;
if (splitHookName.length === 1) {
moduler = '';
originModuler = '';
hook = splitHookName[0];
}
else if (splitHookName.length === 2) {
moduler = splitHookName[0];
hook = splitHookName[1];
originModuler = moduler;
moduler = moduler.replace('-', '__').replace('/', '__').replace('@', '');
}
else {
throw Error('Invalid hook name');
}
argv = argv || getInternalCache().get('argv') || {};
const scriptName = argv && argv.scriptName ? argv.scriptName : 'semo';
const invokedHookCache = cachedInstance.get('invokedHookCache') || {};
hook = !hook.startsWith('hook_') ? `hook_${hook}` : hook;
options = Object.assign({
mode: 'assign',
useCache: false,
include: [],
exclude: [],
opts: {},
}, options);
try {
const cacheKey = `${hook}:${(0, object_hash_1.default)(options)}`;
if (options.useCache && invokedHookCache[cacheKey]) {
return invokedHookCache[cacheKey];
}
// Make Application supporting hook invocation
const appConfig = getApplicationConfig(argv);
const combinedConfig = getCombinedConfig(argv);
// Make Semo core supporting hook invocation
const plugins = argv.packageDirectory
? Object.assign({}, {
[scriptName]: path_1.default.resolve(argv.packageDirectory),
}, getAllPluginsMapping(argv))
: getAllPluginsMapping(argv);
if (appConfig &&
appConfig.name !== scriptName &&
appConfig.name !== argv.packageName &&
!plugins[appConfig.name] &&
appConfig.applicationDir &&
appConfig.applicationDir !== argv.packageDirectory) {
plugins.application = appConfig.applicationDir;
}
let pluginsReturn;
switch (options.mode) {
case 'push':
pluginsReturn = [];
break;
case 'replace':
pluginsReturn = undefined;
break;
case 'group':
case 'assign':
case 'merge':
default:
pluginsReturn = {};
break;
}
const hookCollected = [];
const hookIndex = [];
for (let i = 0, length = Object.keys(plugins).length; i < length; i++) {
const plugin = Object.keys(plugins)[i];
// Process include option
if (lodash_1.default.isArray(options.include) &&
options.include.length > 0 &&
!options.include.includes(plugin)) {
continue;
}
// Process exclude option
if (lodash_1.default.isArray(options.exclude) &&
options.exclude.length > 0 &&
options.exclude.includes(plugin)) {
continue;
}
try {
let pluginEntryPath; // resolve plugin hook entry file path
let hookDir; // resolve plugin hook dir
switch (plugin) {
case scriptName:
const coreRcInfo = parseRcFile(plugin, plugins[plugin]);
hookDir = coreRcInfo && coreRcInfo.hookDir ? coreRcInfo.hookDir : '';
break;
case 'application':
if (combinedConfig.hookDir) {
hookDir = combinedConfig.hookDir;
}
break;
default:
if (combinedConfig.pluginConfigs[plugin]) {
hookDir = combinedConfig.pluginConfigs[plugin].hookDir;
}
break;
}
let entryFileName = 'index.js';
if ((0, exports.isUsingTsRunner)()) {
entryFileName = 'index.ts';
}
if (hookDir &&
fileExistsSyncCache(path_1.default.resolve(plugins[plugin], hookDir, entryFileName))) {
pluginEntryPath = path_1.default.resolve(plugins[plugin], hookDir, entryFileName);
}
// pluginEntryPath resolve failed, means this plugin do not hook anything
if (!pluginEntryPath) {
continue;
}
// force clear require cache
if (options.reload && require.cache[pluginEntryPath]) {
delete require.cache[pluginEntryPath];
}
let loadedPlugin = require(pluginEntryPath);
if (lodash_1.default.isFunction(loadedPlugin)) {
loadedPlugin = await loadedPlugin(exports.Utils, argv);
}
else if (lodash_1.default.isFunction(loadedPlugin.default)) {
loadedPlugin = await loadedPlugin.default(exports.Utils, argv);
}
let forHookCollected = null;
if (loadedPlugin[hook]) {
if (!loadedPlugin[hook].getHook ||
!lodash_1.default.isFunction(loadedPlugin[hook].getHook)) {
forHookCollected = new hook_1.Hook(loadedPlugin[hook]);
}
else {
forHookCollected = loadedPlugin[hook];
}
}
if (forHookCollected) {
const loadedPluginHook = forHookCollected.getHook(originModuler);
if (lodash_1.default.isFunction(loadedPluginHook)) {
hookCollected.push(loadedPluginHook(options));
}
else {
hookCollected.push(loadedPluginHook);
}
hookIndex.push(plugin);
}
}
catch (e) {
if (!e.code || e.code !== 'MODULE_NOT_FOUND') {
throw new Error(e.stack);
}
else {
error(e.message, false);
}
}
}
const hookResolved = await Promise.all(hookCollected);
hookResolved.forEach((pluginReturn, index) => {
switch (options.mode) {
case 'group':
pluginReturn = pluginReturn || {};
const plugin = hookIndex[index];
pluginsReturn[plugin] = pluginReturn;
break;
case 'push':
pluginsReturn.push(pluginReturn);
break;
case 'replace':
pluginsReturn = pluginReturn;
break;
case 'merge':
pluginReturn = pluginReturn || {};
pluginsReturn = lodash_1.default.merge(pluginsReturn, pluginReturn);
break;
case 'assign':
default:
pluginReturn = pluginReturn || {};
pluginsReturn = Object.assign(pluginsReturn, pluginReturn);
break;
}
});
invokedHookCache[cacheKey] = pluginsReturn;
cachedInstance.set('invokedHookCache', invokedHookCache);
return pluginsReturn;
}
catch (e) {
throw new Error(e.stack);
}
};
/**
* Extend command's sub command, it give other plugins an opportunity to extend it's sub command.
* So if you want other plugins to extend your sub commands, you can use this util function to replace default `yargs.commandDir`
* @example
* exports.builder = function (yargs) {
* // The first param could be a/b/c if you want to extend subcommand's subcommand
* Utils.extendSubCommand('make', 'semo', yargs, __dirname)
* }
* @param {String} command Current command name.
* @param {String} moduler Current plugin name.
* @param {Object} yargs Yargs reference.
* @param {String} basePath Often set to `__dirname`.
*/
const extendSubCommand = function (command, moduleName, yargs, basePath) {
let argv = cachedInstance.get('argv') || {};
if (lodash_1.default.isEmpty(argv)) {
argv = yargs.getOptions().configObjects[0];
getInternalCache().set('argv', yargs.getOptions().configObjects[0]);
}
const plugins = getAllPluginsMapping(argv);
const config = getCombinedConfig(argv);
const opts = {
// try to use ts command with ts-node/register
extensions: ['ts', 'js'],
exclude: /.d.ts$/,
// Give each command an ability to disable temporarily
visit: command => {
command.middlewares = command.middlewares
? lodash_1.default.castArray(command.middlewares)
: [];
command.middlewares.unshift(async (argv) => {
if (!command.noblank) {
// Insert a blank line to terminal
console.log();
}
// Give command a plugin level config
argv.$config = {};
if (command.plugin) {
command.plugin = command.plugin.startsWith(argv.scriptName + '-plugin-')
? command.plugin.substring(argv.scriptName + '-plugin-'.length)
: command.plugin;
if (command.plugin && argv.$plugin) {
if (argv.$plugin[command.plugin]) {
argv.$config = formatRcOptions(argv.$plugin[command.plugin] || {});
}
else if (argv.$plugin[argv.scriptName + '-plugin-' + command.plugin]) {
argv.$config = formatRcOptions(argv.$plugin[argv.scriptName + '-plugin-' + command.plugin] ||
{});
}
}
}
argv.$command = command;
argv.$input = await (0, get_stdin_1.default)();
getInternalCache().set('argv', argv);
});
if (command.middleware) {
command.middlewares = command.middlewares.concat(command.middleware);
}
return command.disabled === true ? false : command;
},
};
// load default commands
const currentCommand = command.split('/').pop();
if (currentCommand &&
fileExistsSyncCache(path_1.default.resolve(basePath, currentCommand))) {
yargs.commandDir(path_1.default.resolve(basePath, currentCommand), opts);
}
// Load plugin commands
if (plugins) {
Object.keys(plugins).forEach(function (plugin) {
if (config.pluginConfigs[plugin] &&
config.pluginConfigs[plugin].extendDir) {
if (fileExistsSyncCache(path_1.default.resolve(plugins[plugin], `${config.pluginConfigs[plugin].extendDir}/${moduleName}/src/commands`, command))) {
yargs.commandDir(path_1.default.resolve(plugins[plugin], `${config.pluginConfigs[plugin].extendDir}/${moduleName}/src/commands`, command), opts);
}
}
});
}
// Load application commands
if (config.extendDir &&
fileExistsSyncCache(path_1.default.resolve(process.cwd(), `${config.extendDir}/${moduleName}/src/commands`, command))) {
yargs.commandDir(path_1.default.resolve(process.cwd(), `${config.extendDir}/${moduleName}/src/commands`, command), opts);
}
};
/**
* Get all plugins path mapping.
* Same name plugins would be overriden orderly.
* This function also influences final valid commands and configs.
*/
let configPluginLoaded = false;
let enablePluginAutoScan = true;
const getAllPluginsMapping = function (argv = {}) {
argv = argv || cachedInstance.get('argv') || {};
let pluginsRegistryCachePath;
const nodeModulesDir = find_up_1.default.sync('node_modules', {
cwd: process.cwd(),
type: 'directory',
});
if (nodeModulesDir) {
const pluginsRegistryCacheDir = path_1.default.resolve(nodeModulesDir, '.cache/semo');
fs_extra_1.default.ensureDirSync(pluginsRegistryCacheDir);
pluginsRegistryCachePath = path_1.default.resolve(pluginsRegistryCacheDir, '.semo-plugins.json');
}
if (!pluginsRegistryCachePath ||
!fileExistsSyncCache(pluginsRegistryCachePath) ||
!argv.cachePlugins) {
let plugins = cachedInstance.get('plugins') || {};
const scriptName = argv && argv.scriptName ? argv.scriptName : 'semo';
if (lodash_1.default.isEmpty(plugins) && !configPluginLoaded) {
const registerPlugins = exports.Utils.config('$plugins.register') || {};
if (!lodash_1.default.isEmpty(registerPlugins)) {
enablePluginAutoScan = false;
}
Object.keys(registerPlugins).forEach(plugin => {
let pluginPath = registerPlugins[plugin];
if (!plugin.startsWith('.') &&
plugin.indexOf(scriptName + '-plugin-') === -1) {
plugin = scriptName + '-plugin-' + plugin;
}
if (lodash_1.default.isBoolean(pluginPath) && pluginPath) {
try {
pluginPath = path_1.default.dirname(getPackagePath(plugin, [process.cwd()]));
plugins[plugin] = pluginPath;
}
catch (e) {
warn(e.message);
}
}
else if ((lodash_1.default.isString(pluginPath) && pluginPath.startsWith('/')) ||
pluginPath.startsWith('.') ||
pluginPath.startsWith('~')) {
pluginPath = getAbsolutePath(pluginPath);
plugins[plugin] = pluginPath;
}
else {
// Means not register for now
}
});
cachedInstance.set('plugins', plugins);
configPluginLoaded = true;
}
let pluginPrefix = argv.pluginPrefix || 'semo';
if (lodash_1.default.isString(pluginPrefix)) {
pluginPrefix = [pluginPrefix];
}
if (!lodash_1.default.isArray(pluginPrefix)) {
error('invalid --plugin-prefix');
}
const topPluginPattern = pluginPrefix.length > 1
? '{' + pluginPrefix.map(prefix => `${prefix}-plugin-*`).join(',') + '}'
: pluginPrefix.map(prefix => `${prefix}-plugin-*`).join(',');
const orgPluginPattern = pluginPrefix.length > 1
? '{' +
pluginPrefix.map(prefix => `@*/${prefix}-plugin-*`).join(',') +
'}'
: pluginPrefix.map(prefix => `@*/${prefix}-plugin-*`).join(',');
if (lodash_1.default.isEmpty(plugins) && enablePluginAutoScan) {
plugins = {};
// Process core plugins if needed
// Maybe core need to interact with some other plugins
(0, glob_1.sync)(topPluginPattern, {
noext: true,
cwd: path_1.default.resolve(__dirname, '../plugins'),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(__dirname, '../plugins', plugin);
});
// argv.packageDirectory not always exists, if not, plugins list will not include npm global plugins
if (!argv.disableGlobalPlugin && argv.packageDirectory) {
// process core same directory top level plugins
(0, glob_1.sync)(topPluginPattern, {
noext: true,
cwd: path_1.default.resolve(argv.packageDirectory, argv.orgMode ? '../../' : '../'),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(argv.packageDirectory, argv.orgMode ? '../../' : '../', plugin);
});
// Only local dev needed: load sibling plugins in packageDirectory parent directory
// Only for orgMode = true, if orgMode = false, the result would be same as above search
if (argv.orgMode) {
(0, glob_1.sync)(topPluginPattern, {
noext: true,
cwd: path_1.default.resolve(argv.packageDirectory, '../'),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(argv.packageDirectory, '../', plugin);
});
}
// Process core same directory org npm plugins
(0, glob_1.sync)(orgPluginPattern, {
noext: true,
cwd: path_1.default.resolve(argv.packageDirectory, argv.orgMode ? '../../' : '../'),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(argv.packageDirectory, argv.orgMode ? '../../' : '../', plugin);
});
}
if (process.env.HOME && !argv.disableHomePlugin) {
// Semo home is a special directory
if (fileExistsSyncCache(path_1.default.resolve(process.env.HOME, '.' + scriptName, `.${scriptName}rc.yml`))) {
// So home plugin directory will not be overridden by other places normally.
plugins['.' + scriptName] = path_1.default.resolve(process.env.HOME, '.' + scriptName);
}
// process home npm plugins
(0, glob_1.sync)(topPluginPattern, {
noext: true,
cwd: path_1.default.resolve(process.env.HOME, `.${scriptName}`, 'home-plugin-cache', 'node_modules'),
}).forEach(function (plugin) {
if (process.env.HOME) {
plugins[plugin] = path_1.default.resolve(process.env.HOME, `.${scriptName}`, 'home-plugin-cache', 'node_modules', plugin);
}
});
// process home npm scope plugins
(0, glob_1.sync)(orgPluginPattern, {
noext: true,
cwd: path_1.default.resolve(process.env.HOME, `.${scriptName}`, 'node_modules'),
}).forEach(function (plugin) {
if (process.env.HOME) {
plugins[plugin] = path_1.default.resolve(process.env.HOME, `.${scriptName}`, 'node_modules', plugin);
}
});
}
// process cwd(current directory) npm plugins
(0, glob_1.sync)(topPluginPattern, {
noext: true,
cwd: path_1.default.resolve(process.cwd(), 'node_modules'),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(process.cwd(), 'node_modules', plugin);
});
// process cwd(current directory) npm scope plugins
(0, glob_1.sync)(orgPluginPattern, {
noext: true,
cwd: path_1.default.resolve(process.cwd(), 'node_modules'),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(process.cwd(), 'node_modules', plugin);
});
const config = getApplicationConfig();
const pluginDirs = lodash_1.default.castArray(config.pluginDir);
pluginDirs.forEach(pluginDir => {
if (fileExistsSyncCache(pluginDir)) {
// process local plugins
(0, glob_1.sync)(topPluginPattern, {
noext: true,
cwd: path_1.default.resolve(process.cwd(), pluginDir),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(process.cwd(), pluginDir, plugin);
});
// process local npm scope plugins
(0, glob_1.sync)(orgPluginPattern, {
noext: true,
cwd: path_1.default.resolve(process.cwd(), pluginDir),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(process.cwd(), pluginDir, plugin);
});
}
});
// Process plugin project
// If project name contains `-plugin-`, then current directory should be plugin too.
if (fileExistsSyncCache(path_1.default.resolve(process.cwd(), 'package.json'))) {
const pkgConfig = require(path_1.default.resolve(process.cwd(), 'package.json'));
const matchPluginProject = pluginPrefix
.map(prefix => `${prefix}-plugin-`)
.join('|');
const regExp = new RegExp(`^(@[^/]+\/)?(${matchPluginProject})`);
if (pkgConfig.name && regExp.test(pkgConfig.name)) {
plugins[pkgConfig.name] = path_1.default.resolve(process.cwd());
}
}
cachedInstance.set('plugins', plugins);
}
// extraPluginDir plugins would not be in cache
const extraPluginDirEnvName = lodash_1.default.upperCase(scriptName) + '_PLUGIN_DIR';
if (extraPluginDirEnvName &&
process.env[extraPluginDirEnvName] &&
fileExistsSyncCache(getAbsolutePath(process.env[extraPluginDirEnvName]))) {
const envDir = getAbsolutePath(String(process.env[extraPluginDirEnvName]));
// process cwd npm plugins
(0, glob_1.sync)(topPluginPattern, {
noext: true,
cwd: path_1.default.resolve(envDir),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(envDir, plugin);
});
// process cwd npm scope plugins
(0, glob_1.sync)(orgPluginPattern, {
noext: true,
cwd: path_1.default.resolve(envDir),
}).forEach(function (plugin) {
plugins[plugin] = path_1.default.resolve(envDir, plugin);
});
}
// Second filter for registered or scanned plugins
const includePlugins = exports.Utils.config('$plugins.include') || [];
const excludePlugins = exports.Utils.config('$plugins.exclude') || [];
if (lodash_1.default.isArray(includePlugins) && includePlugins.length > 0) {
plugins = lodash_1.default.pickBy(plugins, (pluginPath, plugin) => {
if (plugin.indexOf(scriptName + '-plugin-') === 0) {
plugin = plugin.substring((scriptName + '-plugin-').length);
}
return (includePlugins.includes(plugin) ||
includePlugins.includes(scriptName + '-plugin-' + plugin));
});
}
if (lodash_1.default.isArray(excludePlugins) && excludePlugins.length > 0) {
plugins = lodash_1.default.omitBy(plugins, (pluginPath, plugin) => {
if (plugin.indexOf(scriptName + '-plugin-') === 0) {
plugin = plugin.substring((scriptName + '-plugin-').length);
}
return (excludePlugins.includes(plugin) ||
excludePlugins.includes(scriptName + '-plugin-' + plugin));
});
}
// Write plugins to pluginsRegistryCachePath
if (pluginsRegistryCachePath && argv.cachePlugins) {
fs_extra_1.default.writeFileSync(pluginsRegistryCachePath, JSON.stringify(plugins));
}
return plugins;
}
else {
console.log('use cache');
const plugins = require(pluginsRegistryCachePath);
return plugins;
}
};
/**
* Get absolute path or dir, this func will not judge if exist
*/
const getAbsolutePath = (filePath) => {
if (filePath[0] === '/')
return filePath;
if (process.env.HOME) {
if (filePath[0] === '~')
return filePath.replace(/^~/, process.env.HOME);
}
return path_1.default.resolve(filePath);
};
/**
* Get application semo config only.
*
* @param cwd
* @param opts
* opts.scriptName: set scriptName
*/
const getApplicationConfig = function (opts = {}) {
let argv = cachedInstance.get('argv') || {};
const cache = cachedInstance.get('getApplicationConfig');
if (!lodash_1.default.isEmpty(cache)) {
return cache;
}
const scriptName = opts.scriptName
? opts.scriptName
: argv && argv.scriptName
? argv.scriptName
: 'semo';
argv = Object.assign(argv, opts, { scriptName });
let applicationConfig;
const configPath = find_up_1.default.sync([`.${scriptName}rc.yml`], {
cwd: opts.cwd,
});
const nodeEnv = getNodeEnv(argv);
const configEnvPath = find_up_1.default.sync([`.${scriptName}rc.${nodeEnv}.yml`], {
cwd: opts.cwd,
});
// Load home config if exists
const homeSemoYamlRcPath = process.env.HOME
? path_1.default.resolve(process.env.HOME, `.${scriptName}`, `.${scriptName}rc.yml`)
: '';
if (homeSemoYamlRcPath && fileExistsSyncCache(homeSemoYamlRcPath)) {
try {
const rcFile = fs_extra_1.default.readFileSync(homeSemoYamlRcPath, 'utf8');
applicationConfig = formatRcOptions(yaml_1.default.parse(rcFile));
}
catch (e) {
debugCore('load rc:', e);
warn(`Global ${homeSemoYamlRcPath} config load failed!`);
applicationConfig = {};
}
}
else {
applicationConfig = {};
}
applicationConfig.applicationDir = opts.cwd
? opts.cwd
: configPath
? path_1.default.dirname(configPath)
: process.cwd();
// Inject some core config, hard coded
applicationConfig = Object.assign({}, applicationConfig, opts, {
coreCommandDir: 'lib/commands',
});
// Load application rc, if same dir with core, it's a dup process, rare case.
if (fileExistsSyncCache(path_1.default.resolve(applicationConfig.applicationDir, 'package.json'))) {
const packageInfo = require(path_1.default.resolve(applicationConfig.applicationDir, 'package.json'));
if (packageInfo.name) {
applicationConfig.name = packageInfo.name;
}
// args > package > current rc
if (packageInfo.rc) {
packageInfo.rc = formatRcOptions(packageInfo.rc);
applicationConfig = Object.assign({}, applicationConfig, packageInfo.rc);
}
if (packageInfo[scriptName]) {
packageInfo[scriptName] = formatRcOptions(packageInfo[scriptName]);
applicationConfig = Object.assign({}, applicationConfig, packageInfo[scriptName]);
}
}
// Load current directory main rc config
if (configPath) {
let semoRcInfo = {};
try {
if (configPath.endsWith('.yml')) {
const rcFile = fs_extra_1.default.readFileSync(configPath, 'utf8');
semoRcInfo = formatRcOptions(yaml_1.default.parse(rcFile));
}
else {
semoRcInfo = require(configPath);
semoRcInfo = formatRcOptions(semoRcInfo);
}
applicationConfig = lodash_1.default.merge(applicationConfig, semoRcInfo);
}
catch (e) {
debugCore('load rc:', e);
warn('application rc config load failed!');
}
}
// Load current directory env rc config
if (configEnvPath) {
let semoEnvRcInfo = {};
try {
if (configEnvPath.endsWith('.yml')) {
const rcFile = fs_extra_1.default.readFileSync(configEnvPath, 'utf8');
semoEnvRcInfo = formatRcOptions(yaml_1.default.parse(rcFile));
}
else {
semoEnvRcInfo = require(configEnvPath);
semoEnvRcInfo = formatRcOptions(semoEnvRcInfo);
}
applicationConfig = lodash_1.default.merge(applicationConfig, semoEnvRcInfo);
}
catch (e) {
debugCore('load rc:', e);
warn('application env rc config load failed!');
}
}
cachedInstance.set('getApplicationConfig', applicationConfig);
return applicationConfig;
};
/**
* Format options keys
*
* Make compatible of param cases and camel cases
*/
const formatRcOptions = opts => {
if (!lodash_1.default.isObject(opts)) {
throw new Error('Not valid rc options!');
}
Object.keys(opts)
.filter(key => key.indexOf('-') > -1 || key.indexOf('.') > -1)
.forEach(key => {
const newKey = key
.replace(/--+/g, '-')
.replace(/^-/g, '')
.replace(/-([a-z])/g, (m, p1) => p1.toUpperCase())
.replace('.', '_');
opts[newKey] = opts[key];
// delete opts[key] // sometimes we need original style
});
return opts;
};
const parseRcFile = function (plugin, pluginPath, argv = {}) {
argv = argv || cachedInstance.get('argv') || {};
const scriptName = argv && argv.scriptName ? argv.scriptName : 'semo';
const cacheKey = `parseRcFile:${plugin}:${pluginPath}`;
const cache = cachedInstance.get(cacheKey);
if (!lodash_1.default.isEmpty(cache)) {
return cache;
}
const pluginSemoYamlRcPath = path_1.default.resolve(pluginPath, `.${scriptName}rc.yml`);
const pluginPackagePath = path_1.default.resolve(pluginPath, 'package.json');
let pluginConfig;
if (fileExistsSyncCache(pluginSemoYamlRcPath)) {
try {
const rcFile = fs_extra_1.default.readFileSync(pluginSemoYamlRcPath, 'utf8');
pluginConfig = formatRcOptions(yaml_1.default.parse(rcFile));
try {
const packageConfig = require(pluginPackagePath);
pluginConfig.version = packageConfig.version;
}
catch (e) {
pluginConfig.version = '0.0.0';
}
}
catch (e) {
debugCore('load rc:', e);
warn(`Plugin ${plugin} .semorc.yml config load failed!`);
pluginConfig = {};
}
}
cachedInstance.set(cacheKey, pluginConfig);
return pluginConfig;
};
/**
* Get commbined config from whole environment.
*/
const getCombinedConfig = function (argv = {}) {
let combinedConfig = cachedInstance.get('combinedConfig') || {};
const pluginConfigs = {};
if (lodash_1.default.isEmpty(combinedConfig)) {
const plugins = getAllPluginsMapping(argv);
Object.keys(plugins).forEach(plugin => {
const pluginConfig = parseRcFile(plugin, plugins[plugin], argv);
const pluginConfigPick = lodash_1.default.pick(pluginConfig, [
'commandDir',
'extendDir',
'hookDir',
plugin,
]);
combinedConfig = lodash_1.default.merge(combinedConfig, pluginConfigPick);
pluginConfigs[plugin] = pluginConfig;
});
const applicatonConfig = getApplicationConfig();
combinedConfig = lodash_1.default.merge(combinedConfig, applicatonConfig);
combinedConfig.pluginConfigs = pluginConfigs;
cachedInstance.set('combinedConfig', combinedConfig);
}
return combinedConfig || {};
};
/**
* Print message with format and color.
* @param {mix} message Message to log
*/
const log = function (message) {
if (lodash_1.default.isArray(message) || lodash_1.default.isObject(message)) {
console.log((0, json_colorizer_1.default)((0, json_stringify_pretty_compact_1.default)(message)));
}
else {
console.log(message);
}
};
/**
* Print error message, and exit process.
* @param {mix} message Error message to log
* @param {string} label Error log label
* @param {integer} errorCode Error code
*/
const error = function (message, exit = true, errorCode = 1) {
message = lodash_1.default.isString(message) ? { message } : message;
console.log(picocolors_1.default.red(message.message));
if (exit) {
process.exit(errorCode);
}
};
/**
* Print warn message with yellow color.
* @param {mix} message Error message to log
*/
const warn = function (message, exit = false, errorCode = 0) {
message = lodash_1.default.isString(message) ? { message } : message;
console.log(picocolors_1.default.yellow(message.message));
if (exit) {
process.exit(errorCode);
}
};
/**
* Print info message with green color.
* @param {mix} message Error message to log
*/
const info = function (message, exit = false, errorCode = 0) {
message = lodash_1.default.isString(message) ? { message } : message;
console.log(picocolors_1.default.cyan(message.message));
if (exit) {
process.exit(errorCode);
}
};
/**
* Print success message with green color.
* @param {mix} message Error message to log
*/
const success = function (message, exit = false, errorCode = 0) {
message = lodash_1.default.isString(message) ? { message } : message;
console.log(picocolors_1.default.green(message.message));
if (exit) {
process.exit(errorCode);
}
};
/**
* Compute md5.
* @param {string} s
*/
const md5 = function (s) {
return crypto_1.default.createHash('md5').update(s, 'utf8').digest('hex');
};
/**
* Split input by comma and blank.
* @example
* const = Utils.splitComma('a, b , c,d')
* @param {string} input
* @returns {array} input separated by comma
*/
const splitComma = function (input) {
return splitByChar(input, ',');
};
/**
* Split input by a specific char and blank.
* @example
* const = Utils.splitByChar('a, b , c=d', '=')
* @param {string} input
* @returns {array} input separated by comma
*/
const splitByChar = function (input, char) {
const exp = new RegExp(char, 'g');
return input.replace(exp, ' ').split(/\s+/);
};
/**
* Print a simple table.
* A table style for `semo status`, if you don't like this style, can use Utils.table
* @param {array} columns Table columns
* @param {string} caption Table caption
* @param {object} borderOptions Border options
*/
const outputTable = function (columns, caption = '', borderOptions = {}) {
// table config
const config = {
drawHorizontalLine: () => {
return false;
},
columnDefault: {
paddingLeft: 2,
paddingRight: 1,
},
border: Object.assign((0, table_1.getBorderCharacters)('void'), { bodyJoin: ':' }, borderOptions),
};
if (caption) {
info(caption);
}
console.log((0, table_1.table)(columns, config));
};
/**
* Parse packages from yargs option
* @param {*} input yarns option input, could be string or array
* @returns {array} Package list
*/
const parsePackageNames = function (input) {
if (lodash_1.default.isString(input)) {
return splitComma(input);
}
if (lodash_1.default.isArray(input)) {
return lodash_1.default.flatten(input.map(item => splitComma(item)));
}
return [];
};
/**
* Load any package's package.json
* @param {string} pkg package name
* @param {array} paths search paths
*/
const getPackagePath = function (pkg = undefined, paths = []) {
const packagePath = find_up_1.default.sync('package.json', {
cwd: pkg ? path_1.default.dirname(require.resolve(pkg, { paths })) : process.cwd(),
});
return packagePath;
};
/**
* Load any package's package.json
* @param {string} pkg package name
* @param {array} paths search paths
*/
const loadPackageInfo = function (pkg = undefined, paths = []) {
const packagePath = getPackagePath(pkg, paths);
return packagePath ? require(packagePath) : {};
};
/**
* Load core package.json
*/
const loadCorePackageInfo = function () {
const packagePath = find_up_1.default.sync('package.json', {
cwd: path_1.default.resolve('../../', __dirname),
});
return packagePath ? require(packagePath) : {};
};
/**
* Execute command, because npm install running info can not be catched by shelljs, temporarily use this one
* @param {string} command Command to exec
* @param {object} options Options stdio default is [0, 1, 2]
*/
const exec = function (command, options = {}) {
debugCore('Utils.exec', { command, options });
if (!options.stdio) {
options.stdio = [0, 1, 2];
}
return (0, child_process_1.execSync)(command, options);
};
/**
* Get current node env setting
*
* You can change the node-env-key in command args or semo rc file
*/
const getNodeEnv = (argv = null) => {
argv = argv || cachedInstance.get('argv') || {};
const nodeEnvKey = argv.nodeEnvKey || argv.nodeEnv || 'NODE_ENV';
return process.env[nodeEnvKey] || 'development';
};
/**
* Shortcut for checking if or not current env is production
*/
const isProduction = () => getNodeEnv() === 'production';
/**
* Shortcut for checking if or not current env is development
*/
const isDevelopment = () => getNodeEnv() === 'development';
/**
* Sleep a while of ms
* @param {integer} ms
*/
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const delay = sleep;
/**
* Keep repl history
*
* repl.history 0.1.4 not compatile with node v12, so find other solution.
*/
const replHistory = function (repl, file) {
try {
fs_extra_1.default.statSync(file);
repl.history = fs_extra_1.default.readFileSync(file, 'utf-8').split('\n').reverse();
repl.history.shift();
repl.historyIndex = -1; // will be incremented before pop
}
catch (e) { }
const fd = fs_extra_1.default.openSync(file, 'a');
const wstream = fs_extra_1.default.createWriteStream(file, {
fd,
});
wstream.on('error', function (err) {
throw err;
});
repl.addListener('line', function (code) {
if (code && code !== '.history') {
wstream.write(code + '\n');
}
else {
repl.historyIndex++;
repl.history.pop();
}
});
process.on('exit', function () {
fs_extra_1.default.closeSync(fd);
});
repl.commands.history = {
help: 'Show the history',
action: function () {
const out = [];
repl.history.forEach(function (v) {
out.push(v);
});
repl.outputStream.write(out.reverse().join('\n') + '\n');
repl.displayPrompt();
},
};
};
/**
* Launch dispatcher
*/
const launchDispatcher = async (opts = {}) => {
// process.on('warning', e => console.warn(e.stack));
process.setMaxListeners(0);
useDotEnv(true);
const pkg = loadCorePackageInfo();
// const updateNotifier = await import('update-notifier')
// updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 24 * 7 }).notify({
// defer: false,
// isGlobal: true,
// })
const cache = getInternalCache();
// @see https://github.com/yargs/yargs/blob/main/lib/typings/yargs-parser-types.ts#L35
let parsedArgv = yParser(process.argv.slice(2), {
/** Should commands be sorted in help? */
'sort-commands': true,
/** Should unparsed flags be stored in -- or _? Default is `false` */
'populate--': true,
});
// let parsedArgvOrigin = parsedArgv;
cache.set('argv', Object.assign(parsedArgv, {
scriptName: opts.scriptName || 'semo',
})); // set argv first time
let appConfig = getApplicationConfig();
appConfig = Object.assign(appConfig, {
scriptName: opts.scriptName,
packageName: opts.packageName,
packageDirectory: opts.packageDirectory,
orgMode: opts.orgMode, // Means my package publish under npm orgnization scope
[`$${opts.scriptName || 'semo'}`]: { Utils: exports.Utils, VERSION: pkg.version },
originalArgv: process.argv.slice(2),
});
yargs_1.default.config(appConfig);
parsedArgv = lodash_1.default.merge(appConfig, parsedArgv);
cache.set('argv', parsedArgv); // set argv second time
cache.set('yargs', yargs_1.default);
const plugins = getAllPluginsMapping(parsedArgv);
const config = getCombinedConfig(parsedArgv);
const packageConfig = loadPackageInfo();
if (!parsedArgv.scriptName) {
yargs_1.default.hide('script-name').option('script-name', {
default: 'semo',
describe: 'Rename script name.',
type: 'string',
});
}
else {
if (!lodash_1.default.isString(parsedArgv.scriptName)) {
error('--script-name must be string, should be used only once.');
}
yargs_1.default.scriptName(parsedArgv.scriptName);
}
yargs_1.default.hide('plugin-prefix').option('plugin-prefix', {
default: 'semo',
describe: 'Set plugin prefix.',
});
yargs_1.default.hide('enable-core-hook').option('enable-core-hook', {
default: [],
describe: 'Enable core default disabled hook',
});
const scriptName = parsedArgv.scriptName || 'semo';
const yargsOpts = {
// try to use ts command with ts-node/register
extensions: ['ts', 'js'],
exclude: /.d.ts$/,
// Give each command an ability to disable temporarily
visit: (command, pathTofile, filename) => {
command.middlewares = command.middlewares
? lodash_1.default.castArray(command.middlewares)
: [];
command.middlewares.unshift(async (argv) => {
if (!command.noblank) {
// Insert a blank line to terminal
console.log();
}
argv.$config = {};
// Give command a plugin level config
if (command.plugin) {
command.plugin = command.plugin.startsWith(appConfig.scriptName + '-plugin-')
? command.plugin.substring(appConfig.scriptName + '-plugin-'.length)
: command.plugin;
if (command.plugin && argv.$plugin) {
if (argv.$plugin[command.plugin]) {
argv.$config = formatRcOptions(argv.$plugin[command.plugin] || {});
}
else if (argv.$plugin[appConfig.scriptName + '-plugin-' + command.plugin]) {
argv.$config = formatRcOptions(argv.$plugin[appConfig.scriptName + '-plugin-' + command.plugin] || {});
}
}
}
// argv['$' + scriptName] = { Utils: module.exports }
argv.$command = command;
argv.$input = await (0, get_stdin_1.default)();
getInternalCache().set('argv', argv);
});
if (command.middleware) {
command.middlewares = command.middlewares.concat(command.middleware);
}
return command.disabled === true ? false : command;
},
};
if (!parsedArgv.disableCoreCommand &&
opts.packageDirectory &&
packageConfig.name !== scriptName) {
// Load core commands
yargs_1.default.commandDir(path_1.default.resolve(opts.packageDirectory, appConfig.coreCommandDir), yargsOpts);
}
// Load plugin commands
if (plugins) {
Object.keys(plugins).forEach(function (plugin) {
if (config.pluginConfigs[plugin] &&
config.pluginConfigs[plugin].commandDir &&
fileExistsSyncCache(path_1.default.resolve(plugins[plugin], config.pluginConfigs[plugin].commandDir))) {
yargs_1.default.commandDir(path_1.default.resolve(plugins[plugin], config.pluginConfigs[plugin].commandDir), yargsOpts);
}
});
}
// Load application commands
if (appConfig.commandDir &&
fileExistsSyncCache(path_1.default.resolve(process.cwd(), appConfig.commandDi