dalao-proxy
Version:
An expandable HTTP proxy based on the plug-in system for frontend developers with request caching request mock and development!
377 lines (317 loc) • 10.9 kB
JavaScript
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const EventEmitter = require('events');
const _ = require('lodash');
const pwd = process.cwd();
const defaultConfig = _.cloneDeep(require('../../config'));
const defaults = require('../../config/defaults');
defaultConfig.defaults = defaults;
const { register } = require('../plugin');
const {
custom_assign,
pathCompareFactory,
transformPath,
joinUrl,
addHttpProtocol,
splitTargetAndPath,
getType,
rewriteString,
} = require('../utils');
const parseEmitter = new EventEmitter();
exports.emitter = parseEmitter;
function cleanRequireCache(fileName) {
const id = fileName;
const cache = require.cache;
if (cache[id]) {
const mod = cache[id];
module.children = module.children.filter(m => m !== mod);
cache[id] = null;
delete cache[id];
}
}
/**
* Resolve the absolute path of config file
* @param {String} configFilePath
* @returns {String}
*/
function resolveConfigPath(configFilePath) {
let filePath;
// if specific
if (configFilePath) {
filePath = path.resolve(pwd, configFilePath);
}
else {
filePath = path.resolve(defaultConfig.configFileName);
}
try {
filePath = require.resolve(filePath);
return filePath;
} catch (error) {
return null;
}
}
/**
* Parse config file in JSON and JS type into an object
* @param {String} filePath
* @returns {Object}
*/
function parseFile(filePath) {
const resolvedPath = resolveConfigPath(filePath);
try {
if (resolvedPath) {
cleanRequireCache(resolvedPath);
const fileConfig = require(resolvedPath);
return {
path: resolvedPath,
rawConfig: fileConfig,
config: mergeConfig(defaultConfig, fileConfig)
};
}
else {
return {
path: null,
rawConfig: null,
config: defaultConfig
};
}
} catch (error) {
console.error(chalk.red(` > An error occurred (${error.message}) while parsing config file.`))
return {
path: resolvedPath,
rawConfig: null,
config: defaultConfig
};
}
}
/**
* Parse value of `config` from command line
* @returns {String}
*/
function parsePathFromArgv() {
const argvs = process.argv;
let i = 0;
for (let argv of argvs) {
let matched;
if (matched = argv.match(/^(?:--config|-C)(?:=(.+))?/)) {
let theValue;
if (matched[1]) {
theValue = matched[1];
}
else {
theValue = argvs[i + 1];
}
if (!/^--?/.test(theValue)) {
return theValue;
}
}
i++;
}
}
/**
* Merge base config and file config
* @param {Object} fileConfig
* @returns {Object}
*/
function mergeConfig(baseConfig, fileConfig) {
if (!fileConfig) return baseConfig;
// * merge strategy fields
const EXTRA_FIELDS = {
obj: ['headers', 'proxyTable'],
arr: ['plugins']
};
const EXTRA_FIELDS_ALL = [...EXTRA_FIELDS.obj, ...EXTRA_FIELDS.arr];
// merge plain fields
const baseConfig_plain = _.omit(baseConfig, EXTRA_FIELDS_ALL);
const fileConfig_plain = _.omit(fileConfig, EXTRA_FIELDS_ALL);
const mergedConfig_plain = _.assignWith({}, baseConfig_plain, fileConfig_plain, custom_assign);
// Object.keys(mergedConfig_plain).forEach(config => {
// const checkFn = CheckFunctions[config];
// checkFn && checkFn(mergedConfig_plain[config]);
// });
// merge extra fields
const baseConfig_extra_obj = _.pick(baseConfig, EXTRA_FIELDS.obj);
const baseConfig_extra_arr = _.pick(baseConfig, EXTRA_FIELDS.arr);
const fileConfig_extra_obj = _.pick(fileConfig, EXTRA_FIELDS.obj);
const fileConfig_extra_arr = _.pick(fileConfig, EXTRA_FIELDS.arr);
Object.keys(fileConfig_extra_obj).forEach(key => fileConfig_extra_obj[key] = fileConfig_extra_obj[key] || {});
Object.keys(fileConfig_extra_arr).forEach(key => fileConfig_extra_arr[key] = fileConfig_extra_arr[key] || []);
const mergedConfig_extra_obj = _.merge({}, baseConfig_extra_obj, fileConfig_extra_obj);
const mergedConfig_extra_arr = {};
EXTRA_FIELDS.arr.forEach(field => {
const baseConfigField = baseConfig_extra_arr[field] || [];
const fileConfigField = fileConfig_extra_arr[field] || [];
mergedConfig_extra_arr[field] = [...new Set([...baseConfigField, ...fileConfigField])];
});
// other plain field need to be replaced
const mergedFileConfig = _.merge({}, mergedConfig_extra_obj, mergedConfig_extra_arr, mergedConfig_plain)
return mergedFileConfig;
}
/**
* @param {import('../plugin').Plugin[]} plugins
*/
function mergePluginsConfig(targetConfig, plugins) {
plugins.forEach(plugin => {
if (Array.isArray(plugin.setting.optionsField)) {
plugin.setting.optionsField.forEach(field => {
targetConfig[field] = plugin.config[field];
});
}
else {
const field = plugin.setting.optionsField;
targetConfig[field] = plugin.config;
}
});
}
/**
* Parse each router in Route Table
* @param {Object} config
* @return {CliTable}
*/
function parseRouter(config) {
const {
target,
changeOrigin,
proxyTable,
} = config;
const Table = require('cli-table3');
const outputTable = new Table({
head: [chalk.yellow('Proxy'), chalk.yellow('To'), chalk.white('Path Rewrite'), chalk.green('Result')]
});
const proxyPaths = Object.keys(proxyTable).sort(pathCompareFactory(1));
proxyPaths.forEach(proxyPath => {
// if (!CheckFunctions.proxyTable.proxyPath(proxyPath)) return;
const router = proxyTable[proxyPath];
/**
* assign localValue
* if no value provided, replace with global/default value
* [ localKey, defaultValue, checkFunction ]
*/
[
['path', '/'],
['target', target],
['changeOrigin', changeOrigin],
['pathRewrite', {}],
// ['hostRewrite', config.defaults.route.hostRewrite],
['headers', {}],
].forEach(pair => {
checkRouteConfig(router, pair);
});
const defaultsHostRewrite = defaults.route.hostRewrite;
router.target = addHttpProtocol(router.target);
router.target = rewriteString(addHttpProtocol(router.target), defaultsHostRewrite);
outputTable.push(resolveRouteProxyMap(proxyPath, router));
});
return outputTable;
}
/**
* Resolve single router configuration
* @param {RouterObject} router
* @param {String} localPath
* @param {any} defaultValue
* @return {any} resolvedValue
*/
function checkRouteConfig(router, [localKey, defaultValue]) {
const route = router[localKey];
if (_.isUndefined(route)) {
router[localKey] = defaultValue;
}
else if (getType(route) === 'Object') {
router[localKey] = {
...defaultValue,
...route
}
}
// checkFn && checkFn(router[localKey]);
}
/**
* Resolve route proxy map
* @param {String} proxyPath original route path
* @param {Object} router router config
*/
function resolveRouteProxyMap(proxyPath, router) {
const {
path: overwritePath,
target: overwriteTarget,
pathRewrite: overwritePathRewrite,
hostRewrite: overwriteHostRewrite,
} = router;
function pathRewriteToString(pathRewriteMap) {
if (_.isEmpty(pathRewriteMap)) {
return '-';
}
else {
const Table = require('cli-table3');
const rewriteMapTable = new Table({
chars: {
'top': '', 'top-mid': '', 'top-left': '', 'top-right': ''
, 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': ''
, 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': ''
, 'right': '', 'right-mid': '', 'middle': ' '
},
style: { 'padding-left': 0 }
});
Object.keys(pathRewriteMap).forEach(path => {
rewriteMapTable.push([`'${path}'`, chalk.yellow('->'), `'${pathRewriteMap[path]}'`]);
});
return rewriteMapTable.toString();
}
}
function resolveProxyRoute() {
const { target: overwriteTarget_target, path: overwriteTarget_path } = splitTargetAndPath(overwriteTarget);
const modeReg = /^~(\*?)\s/;
let proxyedPath = joinUrl(overwriteTarget_path, overwritePath, proxyPath.replace(modeReg, ''));
proxyedPath = transformPath(overwriteTarget_target + proxyedPath, overwriteHostRewrite, overwritePathRewrite);
return proxyedPath;
}
return [
// Proxy
proxyPath,
// To
joinUrl(splitTargetAndPath(overwriteTarget)['path'], overwritePath),
// Path Rewrite
pathRewriteToString(overwritePathRewrite),
// Result
resolveProxyRoute(),
];
}
/**
* Main Config Parser
* user arguments setting > user file setting > base internal setting
* @param {Commander} command
*/
exports.parse = function parse(command) {
let runtimeConfig = {};
const argsConfig = Object.assign({}, command.context.options);
argsConfig.configFileName = argsConfig.config;
delete argsConfig.config;
const { path: filePath, config: fileConfig } = parseFile(argsConfig.configFileName);
// replace fileConfig by argsConfig
runtimeConfig = _.assignWith({}, fileConfig, argsConfig, custom_assign);
mergePluginsConfig(runtimeConfig, command.context.plugins);
register._trigger('config:process', runtimeConfig, value => {
runtimeConfig = value;
});
const output = {
routeTable: parseRouter(runtimeConfig)
};
register._trigger('output', output, value => {
command.context.output = value;
});
const currentCommand = command.context.command;
if (currentCommand && fs.existsSync(filePath) && runtimeConfig.watch) {
fs.unwatchFile(filePath);
fs.watchFile(filePath, function () {
parseEmitter.emit('config:triggerParse:fileChange');
console.log(chalk.yellow('> dalao find your config file has changed, reloading...'));
});
}
// emit event to reload proxy server
parseEmitter.emit('config:parsed', {
path: filePath,
config: runtimeConfig
});
};
exports.parseFile = parseFile;
exports.parsePathFromArgv = parsePathFromArgv;
exports.mergePluginsConfig = mergePluginsConfig;