mirror of
https://github.com/WeBankFinTech/fes.js.git
synced 2025-04-06 03:59:53 +08:00
293 lines
8.9 KiB
JavaScript
293 lines
8.9 KiB
JavaScript
/**
|
||
* @copy 该文件代码大部分出自 umi,有需要请参考:
|
||
* https://github.com/umijs/umi/tree/master/packages/core
|
||
*/
|
||
|
||
import { existsSync } from 'fs';
|
||
import { extname, join } from 'path';
|
||
import {
|
||
chalk,
|
||
chokidar,
|
||
compatESModuleRequire,
|
||
deepmerge,
|
||
cleanRequireCache,
|
||
lodash,
|
||
parseRequireDeps,
|
||
winPath,
|
||
getFile
|
||
} from '@umijs/utils';
|
||
import assert from 'assert';
|
||
import joi from 'joi';
|
||
import { ServiceStage } from '../service/enums';
|
||
import {
|
||
getUserConfigWithKey,
|
||
updateUserConfigWithKey
|
||
} from './utils/configUtils';
|
||
import isEqual from './utils/isEqual';
|
||
import mergeDefault from './utils/mergeDefault';
|
||
|
||
const CONFIG_FILES = ['.fes.js'];
|
||
|
||
// TODO:
|
||
// 1. custom config file
|
||
export default class Config {
|
||
cwd;
|
||
|
||
service;
|
||
|
||
config;
|
||
|
||
localConfig;
|
||
|
||
configFile;
|
||
|
||
constructor(opts) {
|
||
this.cwd = opts.cwd || process.cwd();
|
||
this.service = opts.service;
|
||
this.localConfig = opts.localConfig;
|
||
}
|
||
|
||
async getDefaultConfig() {
|
||
const pluginIds = Object.keys(this.service.plugins);
|
||
|
||
// collect default config
|
||
const defaultConfig = pluginIds.reduce((memo, pluginId) => {
|
||
const { key, config = {} } = this.service.plugins[pluginId];
|
||
if ('default' in config) memo[key] = config.default;
|
||
return memo;
|
||
}, {});
|
||
|
||
return defaultConfig;
|
||
}
|
||
|
||
getConfig({ defaultConfig }) {
|
||
assert(
|
||
this.service.stage >= ServiceStage.pluginReady,
|
||
'Config.getConfig() failed, it should not be executed before plugin is ready.'
|
||
);
|
||
|
||
const userConfig = this.getUserConfig();
|
||
// 用于提示用户哪些 key 是未定义的
|
||
// TODO: 考虑不排除 false 的 key
|
||
const userConfigKeys = Object.keys(userConfig).filter(
|
||
key => userConfig[key] !== false
|
||
);
|
||
|
||
// get config
|
||
const pluginIds = Object.keys(this.service.plugins);
|
||
pluginIds.forEach((pluginId) => {
|
||
const { key, config = {} } = this.service.plugins[pluginId];
|
||
// recognize as key if have schema config
|
||
if (!config.schema) return;
|
||
|
||
const value = getUserConfigWithKey({
|
||
key,
|
||
userConfig
|
||
});
|
||
// 不校验 false 的值,此时已禁用插件
|
||
if (value === false) return;
|
||
|
||
// do validate
|
||
const schema = config.schema(joi);
|
||
assert(
|
||
joi.isSchema(schema),
|
||
`schema return from plugin ${pluginId} is not valid schema.`
|
||
);
|
||
const { error } = schema.validate(value);
|
||
if (error) {
|
||
const e = new Error(
|
||
`Validate config "${key}" failed, ${error.message}`
|
||
);
|
||
e.stack = error.stack;
|
||
throw e;
|
||
}
|
||
|
||
// remove key
|
||
const index = userConfigKeys.indexOf(key.split('.')[0]);
|
||
if (index !== -1) {
|
||
userConfigKeys.splice(index, 1);
|
||
}
|
||
|
||
// update userConfig with defaultConfig
|
||
if (key in defaultConfig) {
|
||
const newValue = mergeDefault({
|
||
defaultConfig: defaultConfig[key],
|
||
config: value
|
||
});
|
||
updateUserConfigWithKey({
|
||
key,
|
||
value: newValue,
|
||
userConfig
|
||
});
|
||
}
|
||
});
|
||
|
||
if (userConfigKeys.length) {
|
||
const keys = userConfigKeys.length > 1 ? 'keys' : 'key';
|
||
throw new Error(
|
||
`Invalid config ${keys}: ${userConfigKeys.join(', ')}`
|
||
);
|
||
}
|
||
|
||
return userConfig;
|
||
}
|
||
|
||
getUserConfig() {
|
||
const configFile = this.getConfigFile();
|
||
this.configFile = configFile;
|
||
if (configFile.length > 0) {
|
||
// clear require cache and set babel register
|
||
const requireDeps = configFile.reduce((memo, file) => {
|
||
memo = memo.concat(parseRequireDeps(file));
|
||
return memo;
|
||
}, []);
|
||
requireDeps.forEach(cleanRequireCache);
|
||
this.service.babelRegister.setOnlyMap({
|
||
key: 'config',
|
||
value: requireDeps
|
||
});
|
||
|
||
// require config and merge
|
||
return this.mergeConfig(...this.requireConfigs(configFile));
|
||
}
|
||
return {};
|
||
}
|
||
|
||
addAffix(file, affix) {
|
||
const ext = extname(file);
|
||
return file.replace(new RegExp(`${ext}$`), `.${affix}${ext}`);
|
||
}
|
||
|
||
requireConfigs(configFiles) {
|
||
// eslint-disable-next-line
|
||
return configFiles.map((f) => compatESModuleRequire(require(f)));
|
||
}
|
||
|
||
mergeConfig(...configs) {
|
||
let ret = {};
|
||
for (const config of configs) {
|
||
// TODO: 精细化处理,比如处理 dotted config key
|
||
ret = deepmerge(ret, config);
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
getConfigFile() {
|
||
// TODO: support custom config file
|
||
let configFile = CONFIG_FILES.find(f => existsSync(join(this.cwd, f)));
|
||
if (!configFile) return [];
|
||
configFile = winPath(configFile);
|
||
let envConfigFile;
|
||
// 潜在问题:
|
||
// .local 和 .env 的配置必须有 configFile 才有效
|
||
if (process.env.FES_ENV) {
|
||
const envConfigFileName = this.addAffix(
|
||
configFile,
|
||
process.env.FES_ENV
|
||
);
|
||
const fileNameWithoutExt = envConfigFileName.replace(
|
||
extname(envConfigFileName),
|
||
''
|
||
);
|
||
envConfigFile = getFile({
|
||
base: this.cwd,
|
||
fileNameWithoutExt,
|
||
type: 'javascript'
|
||
}).filename;
|
||
if (!envConfigFile) {
|
||
throw new Error(
|
||
`get user config failed, ${envConfigFile} does not exist, but process.env.FES_ENV is set to ${process.env.FES_ENV}.`
|
||
);
|
||
}
|
||
}
|
||
const files = [
|
||
configFile,
|
||
envConfigFile,
|
||
this.localConfig && this.addAffix(configFile, 'local')
|
||
]
|
||
.filter(f => !!f)
|
||
.map(f => join(this.cwd, f))
|
||
.filter(f => existsSync(f));
|
||
return files;
|
||
}
|
||
|
||
getWatchFilesAndDirectories() {
|
||
const fesEnv = process.env.FES_ENV;
|
||
const configFiles = lodash.clone(CONFIG_FILES);
|
||
CONFIG_FILES.forEach((f) => {
|
||
if (this.localConfig) configFiles.push(this.addAffix(f, 'local'));
|
||
if (fesEnv) configFiles.push(this.addAffix(f, fesEnv));
|
||
});
|
||
|
||
const configDir = winPath(join(this.cwd, 'config'));
|
||
|
||
const files = configFiles
|
||
.reduce((memo, f) => {
|
||
const file = winPath(join(this.cwd, f));
|
||
if (existsSync(file)) {
|
||
memo = memo.concat(parseRequireDeps(file));
|
||
} else {
|
||
memo.push(file);
|
||
}
|
||
return memo;
|
||
}, [])
|
||
.filter(f => !f.startsWith(configDir));
|
||
|
||
return [configDir].concat(files);
|
||
}
|
||
|
||
watch(opts) {
|
||
let paths = this.getWatchFilesAndDirectories();
|
||
let userConfig = opts.userConfig;
|
||
const watcher = chokidar.watch(paths, {
|
||
ignoreInitial: true,
|
||
cwd: this.cwd
|
||
});
|
||
watcher.on('all', (event, path) => {
|
||
console.log(chalk.green(`[${event}] ${path}`));
|
||
const newPaths = this.getWatchFilesAndDirectories();
|
||
const diffs = lodash.difference(newPaths, paths);
|
||
if (diffs.length) {
|
||
watcher.add(diffs);
|
||
paths = paths.concat(diffs);
|
||
}
|
||
|
||
const newUserConfig = this.getUserConfig();
|
||
const pluginChanged = [];
|
||
const valueChanged = [];
|
||
Object.keys(this.service.plugins).forEach((pluginId) => {
|
||
const { key, config = {} } = this.service.plugins[pluginId];
|
||
// recognize as key if have schema config
|
||
if (!config.schema) return;
|
||
if (!isEqual(newUserConfig[key], userConfig[key])) {
|
||
const changed = {
|
||
key,
|
||
pluginId
|
||
};
|
||
if (
|
||
newUserConfig[key] === false
|
||
|| userConfig[key] === false
|
||
) {
|
||
pluginChanged.push(changed);
|
||
} else {
|
||
valueChanged.push(changed);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (pluginChanged.length || valueChanged.length) {
|
||
opts.onChange({
|
||
userConfig: newUserConfig,
|
||
pluginChanged,
|
||
valueChanged
|
||
});
|
||
}
|
||
userConfig = newUserConfig;
|
||
});
|
||
|
||
return () => {
|
||
watcher.close();
|
||
};
|
||
}
|
||
}
|