UNPKG

@semo/core

Version:

The core of Semo

1,213 lines 75 kB
"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