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