2021-03-10 15:11:04 +08:00

293 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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();
};
}
}