UNPKG

suitest-js-api

Version:

Suitest is a test automation and device manipulation tool for living room devices and web browsers.

277 lines (245 loc) 8.25 kB
// TODO: add test coverage for file const {pickNonNil} = require('../utils/common'); const {hideOwnArgs} = require('./processArgs'); const suitest = require('../../index'); const fs = require('fs'); const SuitestError = require('../utils/SuitestError'); const { invalidUserConfig, circularDependencyError, } = require('../texts'); const yaml = require('js-yaml'); const JSON5 = require('json5'); const ini = require('ini'); const path = require('path'); const PRESETS = 'presets'; const APP_NAME = 'suitest'; const ETC_DIR = '/etc'; const IS_WINDOWS = process.platform === 'win32'; const HOME_DIR = IS_WINDOWS ? process.env.USERPROFILE : process.env.HOME; const CONFIG_FORMATS = [ '.js', '.json', '.yaml', '.yml', '.json5', '.ini', ]; /** * @description default directories to search. Logic the same as in rc * @type { * { * path: string, * filename: string, * deepSearch: boolean, * isGeneral: boolean * } [] * } */ const DEFAULT_PATHS = [ {path: process.cwd(), filename: `.${APP_NAME}rc`, deepSearch: true, isGeneral: true}, {path: path.join(HOME_DIR, '.config', APP_NAME), filename: 'config', deepSearch: false, isGeneral: true}, {path: path.join(HOME_DIR, '.config'), filename: APP_NAME, deepSearch: false, isGeneral: true}, {path: path.join(HOME_DIR, `.${APP_NAME}`), filename: 'config', deepSearch: false, isGeneral: true}, {path: HOME_DIR, filename: `.${APP_NAME}rc`, deepSearch: false, isGeneral: true}, {path: path.join(ETC_DIR, APP_NAME), filename: 'config', deepSearch: false, isGeneral: false}, {path: ETC_DIR, filename: `${APP_NAME}rc`, deepSearch: false, isGeneral: false}, ]; /** * @description Reads file from path and returns file as an object. Supports json, yaml, json5 and ini formats. * @param {String} filePath path to file */ function readConfigFile(filePath) { const extension = path.extname(filePath); if (extension === '.js') { if (!path.isAbsolute(filePath)) { // ensure correct handling of relative paths, we want to prevent searching in __dirname filePath = path.join(process.cwd(), filePath); } return require(filePath); } const fileContent = fs.readFileSync(filePath).toString('utf-8'); switch (extension) { case '.yaml': case '.yml': return yaml.load(fileContent); case '.json5': return JSON5.parse(fileContent); case '.ini': return ini.parse(fileContent); case '.json': return JSON.parse(fileContent); default: try { return JSON.parse(fileContent); } catch (error) { return ini.parse(fileContent); } } } /** * @description Recursive search and merging for Extends configs * @param {object} defaultConfigObject main configuration file object * @param {String} extendPath extend path of other configuration file to merge with * @param {String} filePath currently in read configuration file path * @param {Array} foundPaths array of extends paths. Used to verify circular dependency */ function findExtendConfigs(defaultConfigObject, extendPath, filePath, foundPaths) { const pathToConfig = path.join(path.dirname(filePath), extendPath); if (foundPaths.includes(pathToConfig)) { throw new SuitestError(circularDependencyError(pathToConfig)); } const additionalConfigFile = readConfigFile(pathToConfig); const mainConfigFile = {...additionalConfigFile}; Object.keys(defaultConfigObject).forEach((objKey) => { if (objKey === PRESETS) { mainConfigFile.presets = {...mainConfigFile[objKey], ...defaultConfigObject[objKey]}; } else { mainConfigFile[objKey] = defaultConfigObject[objKey]; } }); if ('extends' in additionalConfigFile) { return findExtendConfigs( mainConfigFile, additionalConfigFile.extends, pathToConfig, [...foundPaths, pathToConfig], ); } return mainConfigFile; } /** * @description Search for configuration files. * @param {String} pathToSearch path to directory to search * @param {String} filename base filename without extension */ function findConfig(pathToSearch, filename) { if (!fs.existsSync(pathToSearch)) { return; } const files = fs.readdirSync(pathToSearch); const file = files.find(file => { const {name, ext} = path.parse(file); return name === filename && (CONFIG_FORMATS.includes(ext) || !ext); }); return file ? path.join(pathToSearch, file) : undefined; } /** * @description Search for configuration files up to the root. * @param {String} pathToSearch path to directory to search * @param {String} filename base filename without extension */ function findConfigUpToRoot(pathToSearch, filename) { const foundConfigFile = findConfig(pathToSearch, filename); if (foundConfigFile || path.parse(pathToSearch).root === pathToSearch) { return foundConfigFile; } return findConfigUpToRoot(path.join(pathToSearch, '../'), filename); } /** * Read `.suitestrc` launcher config file. * Also, searches for default RC paths. * If file not found, return empty object. * Supports json, json5, js, yaml, yml, ini formats. * Searches for 'extends' property for other config file and if presents merge them. * cli arguments are not parsed. * If file found, but json invalid, throw error. * @param {string} [pathToConfig] expicit path to configuration file * @returns {Object} */ function readRcConfig(pathToConfig) { let mainConfigFilePath = ''; if (pathToConfig) { mainConfigFilePath = pathToConfig; } else { const defaultConfigurations = DEFAULT_PATHS .filter(defaultConfig => IS_WINDOWS ? defaultConfig.isGeneral : true); for (const defaultConfig of defaultConfigurations) { if ( fs.existsSync(defaultConfig.path) && fs.lstatSync(defaultConfig.path).isDirectory() ) { mainConfigFilePath = ( defaultConfig.deepSearch ? findConfigUpToRoot : findConfig)(defaultConfig.path, defaultConfig.filename); if (mainConfigFilePath) { break; } } } } if (!mainConfigFilePath) { return {}; } const configFile = readConfigFile(mainConfigFilePath); if ('extends' in configFile) { return { ...findExtendConfigs( configFile, configFile.extends, mainConfigFilePath, [mainConfigFilePath], ), config: mainConfigFilePath, }; } return { ...configFile, config: mainConfigFilePath, }; } /** * Read config file provided by user. * @param {string} path - path to config file * @throws {SuitestError} * @returns {Object} - parsed config file */ function readUserConfig(path) { try { return readConfigFile(path); } catch (error) { throw new SuitestError(invalidUserConfig(path, error.message), error.code); } } /** * Compose config from rc file, user config file and cli args * @param {Object} argv - yargs cli args object * @returns {{ownArgs: Object, userCommandArgs: string[]}} */ const composeConfig = (argv) => { if (argv.configFile) { console.warn('Warning: You are using deprecated argument --config-file. Please use --override-config-file or --base-config-file instead.'); if (argv.baseConfigFile || argv.overrideConfigFile) { throw new SuitestError('Combination of deprecated --config-file with either --base-config-file or --override-config-file is not allowed'); } } const rcConfig = readRcConfig(argv.baseConfigFile || argv.configFile); if (rcConfig.configFile) { console.warn('Warning: You are using deprecated option configFile. Please use overrideConfigFile instead.'); } if ((argv.configFile || rcConfig.configFile) && (argv.baseConfigFile || argv.overrideConfigFile || rcConfig.overrideConfigFile)) { throw new SuitestError('Combination of deprecated configFile with --base-config-file or --override-config-file or overrideConfigFile is not allowed'); } const configFilePath = argv.overrideConfigFile || rcConfig.overrideConfigFile || argv.configFile || rcConfig.configFile; const userConfig = configFilePath ? readUserConfig(configFilePath) : {}; const ownArgs = { ...pickNonNil(suitest.configuration.overridableFields, rcConfig), ...pickNonNil(suitest.configuration.overridableFields, userConfig), ...pickNonNil(suitest.configuration.overridableFields, argv), }; const userCommandArgs = hideOwnArgs(); return { ownArgs, userCommandArgs, }; }; module.exports = { readConfigFile, findExtendConfigs, composeConfig, readRcConfig, readUserConfig, };