diff --git a/packages/fes-core/src/Config/index.js b/packages/fes-core/src/Config/index.js new file mode 100644 index 00000000..7ce22819 --- /dev/null +++ b/packages/fes-core/src/Config/index.js @@ -0,0 +1,286 @@ +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', 'config/config.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; + // 潜在问题: + // .local 和 .env 的配置必须有 configFile 才有效 + if (configFile) { + let envConfigFile; + 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)); + + // clear require cache and set babel register + const requireDeps = files.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(files)); + } + 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 + const configFile = CONFIG_FILES.find(f => existsSync(join(this.cwd, f))); + return configFile ? winPath(configFile) : null; + } + + 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(); + }; + } +} diff --git a/packages/fes-core/src/Service/PluginAPI.js b/packages/fes-core/src/Service/PluginAPI.js index 7954f3b6..64e2efd4 100644 --- a/packages/fes-core/src/Service/PluginAPI.js +++ b/packages/fes-core/src/Service/PluginAPI.js @@ -99,6 +99,26 @@ export default class PluginAPI { } } + registerPresets(presets) { + assert( + this.service.stage === ServiceStage.initPresets, + 'api.registerPresets() failed, it should only used in presets.', + ); + assert( + Array.isArray(presets), + 'api.registerPresets() failed, presets must be Array.', + ); + const extraPresets = presets.map(preset => (isValidPlugin(preset) + ? (preset) + : pathToObj({ + type: PluginType.preset, + path: preset, + cwd: this.service.cwd + }))); + // 插到最前面,下个 while 循环优先执行 + this.service._extraPresets.splice(0, 0, ...extraPresets); + } + registerMethod({ name, fn, diff --git a/packages/fes-core/src/Service/index.js b/packages/fes-core/src/Service/index.js new file mode 100644 index 00000000..49f7fd89 --- /dev/null +++ b/packages/fes-core/src/Service/index.js @@ -0,0 +1,522 @@ +import { join } from 'path'; +import { EventEmitter } from 'events'; +import assert from 'assert'; +import { AsyncSeriesWaterfallHook } from 'tapable'; +import { existsSync } from 'fs'; +import { BabelRegister, lodash } from '@umijs/utils'; +import { resolvePresets, pathToObj, resolvePlugins } from './utils/pluginUtils'; +import loadDotEnv from './utils/loadDotEnv'; +import isPromise from './utils/isPromise'; +import PluginAPI from './pluginAPI'; +import { + ApplyPluginsType, + ConfigChangeType, + EnableBy, + PluginType, + ServiceStage +} from './enums'; +import Config from '../config'; +import { getUserConfigWithKey } from '../config/utils/configUtils'; +import getPaths from './getPaths'; + +// TODO +// 1. duplicated key +// 2. Logger +export default class Service extends EventEmitter { + cwd; + + pkg; + + skipPluginIds = new Set(); + + // lifecycle stage + stage = ServiceStage.uninitialized; + + // registered commands + commands = {}; + + // including plugins + plugins = {}; + + // plugin methods + pluginMethods = {}; + + // initial presets and plugins from arguments, config, process.env, and package.json + initialPresets = []; + + // initial plugins from arguments, config, process.env, and package.json + initialPlugins = []; + + _extraPresets = []; + + _extraPlugins = []; + + // user config + userConfig; + + configInstance; + + config = null; + + // babel register + babelRegister; + + // hooks + hooksByPluginId = {}; + + hooks = {}; + + // paths + paths = {}; + + env; + + ApplyPluginsType = ApplyPluginsType; + + EnableBy = EnableBy; + + ConfigChangeType = ConfigChangeType; + + ServiceStage = ServiceStage; + + args; + + constructor(opts) { + super(); + this.cwd = opts.cwd || process.cwd(); + // repoDir should be the root dir of repo + this.pkg = opts.pkg || this.resolvePackage(); + this.env = opts.env || process.env.NODE_ENV; + + assert(existsSync(this.cwd), `cwd ${this.cwd} does not exist.`); + + // register babel before config parsing + this.babelRegister = new BabelRegister(); + + // load .env or .local.env + this.loadEnv(); + + // get user config without validation + this.configInstance = new Config({ + cwd: this.cwd, + service: this, + localConfig: this.env === 'development' + }); + this.userConfig = this.configInstance.getUserConfig(); + + // get paths + this.paths = getPaths({ + cwd: this.cwd, + config: this.userConfig, + env: this.env + }); + + // setup initial plugins + const baseOpts = { + pkg: this.pkg, + cwd: this.cwd + }; + this.initialPresets = resolvePresets({ + ...baseOpts, + presets: opts.presets || [], + userConfigPresets: this.userConfig.presets || [] + }); + this.initialPlugins = resolvePlugins({ + ...baseOpts, + plugins: opts.plugins || [], + userConfigPlugins: this.userConfig.plugins || [] + }); + } + + setStage(stage) { + this.stage = stage; + } + + resolvePackage() { + try { + // eslint-disable-next-line + return require(join(this.cwd, "package.json")); + } catch (e) { + return {}; + } + } + + loadEnv() { + const basePath = join(this.cwd, '.env'); + const localPath = `${basePath}.local`; + loadDotEnv(basePath); + loadDotEnv(localPath); + } + + async init() { + this.setStage(ServiceStage.init); + await this.initPresetsAndPlugins(); + + // hooksByPluginId -> hooks + // hooks is mapped with hook key, prepared for applyPlugins() + this.setStage(ServiceStage.initHooks); + Object.keys(this.hooksByPluginId).forEach((id) => { + const hooks = this.hooksByPluginId[id]; + hooks.forEach((hook) => { + const { key } = hook; + hook.pluginId = id; + this.hooks[key] = (this.hooks[key] || []).concat(hook); + }); + }); + + // plugin is totally ready + this.setStage(ServiceStage.pluginReady); + await this.applyPlugins({ + key: 'onPluginReady', + type: ApplyPluginsType.event + }); + + // get config, including: + // 1. merge default config + // 2. validate + this.setStage(ServiceStage.getConfig); + await this.setConfig(); + + // merge paths to keep the this.paths ref + this.setStage(ServiceStage.getPaths); + // config.outputPath may be modified by plugins + if (this.config.outputPath) { + this.paths.absOutputPath = join(this.cwd, this.config.outputPath); + } + const paths = await this.applyPlugins({ + key: 'modifyPaths', + type: ApplyPluginsType.modify, + initialValue: this.paths + }); + Object.keys(paths).forEach((key) => { + this.paths[key] = paths[key]; + }); + } + + async setConfig() { + const defaultConfig = await this.applyPlugins({ + key: 'modifyDefaultConfig', + type: this.ApplyPluginsType.modify, + initialValue: await this.configInstance.getDefaultConfig() + }); + this.config = await this.applyPlugins({ + key: 'modifyConfig', + type: this.ApplyPluginsType.modify, + initialValue: this.configInstance.getConfig({ + defaultConfig + }) + }); + } + + async initPresetsAndPlugins() { + this.setStage(ServiceStage.initPresets); + this._extraPlugins = []; + while (this.initialPresets.length) { + // eslint-disable-next-line + await this.initPreset(this.initialPresets.shift()); + } + + this.setStage(ServiceStage.initPlugins); + this._extraPlugins.push(...this.initialPlugins); + while (this._extraPlugins.length) { + // eslint-disable-next-line + await this.initPlugin(this._extraPlugins.shift()); + } + } + + getPluginAPI(opts) { + const pluginAPI = new PluginAPI(opts); + + // register built-in methods + [ + 'onPluginReady', + 'modifyPaths', + 'onStart', + 'modifyDefaultConfig', + 'modifyConfig' + ].forEach((name) => { + pluginAPI.registerMethod({ + name, + exitsError: false + }); + }); + + return new Proxy(pluginAPI, { + get: (target, prop) => { + // 由于 pluginMethods 需要在 register 阶段可用 + // 必须通过 proxy 的方式动态获取最新,以实现边注册边使用的效果 + if (this.pluginMethods[prop]) return this.pluginMethods[prop]; + if ( + [ + 'applyPlugins', + 'ApplyPluginsType', + 'EnableBy', + 'ConfigChangeType', + 'babelRegister', + 'stage', + 'ServiceStage', + 'paths', + 'cwd', + 'pkg', + 'userConfig', + 'config', + 'env', + 'args', + 'hasPlugins', + 'hasPresets', + 'setConfig' + ].includes(prop) + ) { + return typeof this[prop] === 'function' + ? this[prop].bind(this) + : this[prop]; + } + return target[prop]; + } + }); + } + + async applyAPI(opts) { + let ret = opts.apply()(opts.api); + if (isPromise(ret)) { + ret = await ret; + } + return ret || {}; + } + + async initPreset(preset) { + const { id, key, apply } = preset; + preset.isPreset = true; + + const api = this.getPluginAPI({ id, key, service: this }); + + // register before apply + this.registerPlugin(preset); + const { presets, plugins } = await this.applyAPI({ + api, + apply + }); + + // register extra presets and plugins + if (presets) { + assert( + Array.isArray(presets), + `presets returned from preset ${id} must be Array.`, + ); + // 插到最前面,下个 while 循环优先执行 + this._extraPresets.splice( + 0, + 0, + ...presets.map(path => pathToObj({ + type: PluginType.preset, + path, + cwd: this.cwd + })), + ); + } + + // 深度优先 + const extraPresets = lodash.clone(this._extraPresets); + this._extraPresets = []; + while (extraPresets.length) { + // eslint-disable-next-line + await this.initPreset(extraPresets.shift()); + } + + if (plugins) { + assert( + Array.isArray(plugins), + `plugins returned from preset ${id} must be Array.`, + ); + this._extraPlugins.push( + ...plugins.map(path => pathToObj({ + type: PluginType.plugin, + path, + cwd: this.cwd + })), + ); + } + } + + + async initPlugin(plugin) { + const { id, key, apply } = plugin; + + const api = this.getPluginAPI({ + id, + key, + service: this + }); + + // register before apply + this.registerPlugin(plugin); + await this.applyAPI({ + api, + apply + }); + } + + getPluginOptsWithKey(key) { + return getUserConfigWithKey({ + key, + userConfig: this.userConfig + }); + } + + registerPlugin(plugin) { + this.plugins[plugin.id] = plugin; + } + + isPluginEnable(pluginId) { + // api.skipPlugins() 的插件 + if (this.skipPluginIds.has(pluginId)) return false; + + const { key, enableBy } = this.plugins[pluginId]; + + // 手动设置为 false + if (this.userConfig[key] === false) return false; + + // 配置开启 + if (enableBy === this.EnableBy.config && !(key in this.userConfig)) { + return false; + } + + // 函数自定义开启 + if (typeof enableBy === 'function') { + return enableBy(); + } + + // 注册开启 + return true; + } + + hasPresets(presetIds) { + return presetIds.every((presetId) => { + const preset = this.plugins[presetId]; + return preset && preset.isPreset && this.isPluginEnable(presetId); + }); + } + + hasPlugins(pluginIds) { + return pluginIds.every((pluginId) => { + const plugin = this.plugins[pluginId]; + return plugin && !plugin.isPreset && this.isPluginEnable(pluginId); + }); + } + + async applyPlugins(opts) { + const hooks = this.hooks[opts.key] || []; + switch (opts.type) { + case ApplyPluginsType.add: + if ('initialValue' in opts) { + assert( + Array.isArray(opts.initialValue), + 'applyPlugins failed, opts.initialValue must be Array if opts.type is add.' + ); + } + // eslint-disable-next-line + const tAdd = new AsyncSeriesWaterfallHook(["memo"]); + for (const hook of hooks) { + if (!this.isPluginEnable(hook.pluginId)) { + continue; + } + tAdd.tapPromise( + { + name: hook.pluginId, + stage: hook.stage || 0, + // @ts-ignore + before: hook.before + }, + async (memo) => { + const items = await hook.fn(opts.args); + return memo.concat(items); + } + ); + } + return tAdd.promise(opts.initialValue || []); + case ApplyPluginsType.modify: + // eslint-disable-next-line + const tModify = new AsyncSeriesWaterfallHook(["memo"]); + for (const hook of hooks) { + if (!this.isPluginEnable(hook.pluginId)) { + continue; + } + tModify.tapPromise( + { + name: hook.pluginId, + stage: hook.stage || 0, + // @ts-ignore + before: hook.before + }, + async memo => hook.fn(memo, opts.args) + ); + } + return tModify.promise(opts.initialValue); + case ApplyPluginsType.event: + // eslint-disable-next-line + const tEvent = new AsyncSeriesWaterfallHook(["_"]); + for (const hook of hooks) { + if (!this.isPluginEnable(hook.pluginId)) { + continue; + } + tEvent.tapPromise( + { + name: hook.pluginId, + stage: hook.stage || 0, + // @ts-ignore + before: hook.before + }, + async () => { + await hook.fn(opts.args); + } + ); + } + return tEvent.promise(); + default: + throw new Error( + `applyPlugin failed, type is not defined or is not matched, got ${opts.type}.` + ); + } + } + + async run({ name, args = {} }) { + args._ = args._ || []; + // shift the command itself + if (args._[0] === name) args._.shift(); + + this.args = args; + await this.init(); + + this.setStage(ServiceStage.run); + await this.applyPlugins({ + key: 'onStart', + type: ApplyPluginsType.event, + args: { + args + } + }); + + return this.runCommand({ + name, + args + }); + } + + async runCommand({ name, args = {} }) { + assert(this.stage >= ServiceStage.init, 'service is not initialized.'); + + args._ = args._ || []; + // shift the command itself + if (args._[0] === name) args._.shift(); + + const command = typeof this.commands[name] === 'string' + ? this.commands[this.commands[name]] + : this.commands[name]; + assert(command, `run command failed, command ${name} does not exists.`); + + const { fn } = command; + return fn({ + args + }); + } +} diff --git a/packages/fes-core/src/Service/pluginAPI.js b/packages/fes-core/src/Service/pluginAPI.js new file mode 100644 index 00000000..64e2efd4 --- /dev/null +++ b/packages/fes-core/src/Service/pluginAPI.js @@ -0,0 +1,154 @@ +import assert from 'assert'; +import * as utils from '@umijs/utils'; +import { isValidPlugin, pathToObj } from './utils/pluginUtils'; +import { EnableBy, PluginType, ServiceStage } from './enums'; +import Logger from '../logger'; +// TODO +// 标准化 logger +export default class PluginAPI { + constructor(opts) { + this.id = opts.id; + this.key = opts.key; + this.service = opts.service; + this.utils = utils; + this.logger = new Logger(`fes:plugin:${this.id || this.key}`); + } + + // TODO: reversed keys + describe({ + id, + key, + config, + enableBy + } = {}) { + const { plugins } = this.service; + // this.id and this.key is generated automatically + // so we need to diff first + if (id && this.id !== id) { + if (plugins[id]) { + const name = plugins[id].isPreset ? 'preset' : 'plugin'; + throw new Error( + `api.describe() failed, ${name} ${id} is already registered by ${plugins[id].path}.`, + ); + } + plugins[id] = plugins[this.id]; + plugins[id].id = id; + delete plugins[this.id]; + this.id = id; + } + if (key && this.key !== key) { + this.key = key; + plugins[this.id].key = key; + } + + if (config) { + plugins[this.id].config = config; + } + + plugins[this.id].enableBy = enableBy || EnableBy.register; + } + + register(hook) { + assert( + hook.key && typeof hook.key === 'string', + `api.register() failed, hook.key must supplied and should be string, but got ${hook.key}.`, + ); + assert( + hook.fn && typeof hook.fn === 'function', + `api.register() failed, hook.fn must supplied and should be function, but got ${hook.fn}.`, + ); + this.service.hooksByPluginId[this.id] = ( + this.service.hooksByPluginId[this.id] || [] + ).concat(hook); + } + + registerCommand(command) { + const { name, alias } = command; + assert( + !this.service.commands[name], + `api.registerCommand() failed, the command ${name} is exists.`, + ); + this.service.commands[name] = command; + if (alias) { + this.service.commands[alias] = name; + } + } + + // 在 preset 初始化阶段放后面,在插件注册阶段放前面 + registerPlugins(plugins) { + assert( + this.service.stage === ServiceStage.initPresets + || this.service.stage === ServiceStage.initPlugins, + 'api.registerPlugins() failed, it should only be used in registering stage.', + ); + assert( + Array.isArray(plugins), + 'api.registerPlugins() failed, plugins must be Array.', + ); + const extraPlugins = plugins.map(plugin => (isValidPlugin(plugin) + ? (plugin) + : pathToObj({ + type: PluginType.plugin, + path: plugin, + cwd: this.service.cwd + }))); + if (this.service.stage === ServiceStage.initPresets) { + this.service._extraPlugins.push(...extraPlugins); + } else { + this.service._extraPlugins.splice(0, 0, ...extraPlugins); + } + } + + registerPresets(presets) { + assert( + this.service.stage === ServiceStage.initPresets, + 'api.registerPresets() failed, it should only used in presets.', + ); + assert( + Array.isArray(presets), + 'api.registerPresets() failed, presets must be Array.', + ); + const extraPresets = presets.map(preset => (isValidPlugin(preset) + ? (preset) + : pathToObj({ + type: PluginType.preset, + path: preset, + cwd: this.service.cwd + }))); + // 插到最前面,下个 while 循环优先执行 + this.service._extraPresets.splice(0, 0, ...extraPresets); + } + + registerMethod({ + name, + fn, + exitsError = true + }) { + if (this.service.pluginMethods[name]) { + if (exitsError) { + throw new Error( + `api.registerMethod() failed, method ${name} is already exist.`, + ); + } else { + return; + } + } + this.service.pluginMethods[name] = fn + // 这里不能用 arrow function,this 需指向执行此方法的 PluginAPI + // 否则 pluginId 会不会,导致不能正确 skip plugin + || function (hookFn) { + const hook = { + key: name, + ...(utils.lodash.isPlainObject(hookFn) ? hookFn : { fn: hookFn }) + }; + // @ts-ignore + this.register(hook); + }; + } + + skipPlugins(pluginIds) { + pluginIds.forEach((pluginId) => { + this.service.skipPluginIds.add(pluginId); + }); + } +} diff --git a/packages/fes-core/src/Service/utils/pluginUtils.js b/packages/fes-core/src/Service/utils/pluginUtils.js index 0c2ecf5d..d394cff7 100644 --- a/packages/fes-core/src/Service/utils/pluginUtils.js +++ b/packages/fes-core/src/Service/utils/pluginUtils.js @@ -9,27 +9,37 @@ import { lodash } from '@umijs/utils'; +import { PluginType } from '../enums'; + const RE = { - plugin: /^(@webank\/)?fes-plugin-/ + [PluginType.plugin]: /^(@webank\/)?fes-plugin-/, + [PluginType.preset]: /^(@webank\/)?fes-preset-/ }; -export function isPlugin(name) { +export function isPluginOrPreset(type, name) { const hasScope = name.charAt(0) === '@'; - const re = RE.plugin; + const re = RE[type]; if (hasScope) { return re.test(name.split('/')[1]) || re.test(name); } return re.test(name); } -export function getPlugins(opts) { +export function getPluginsOrPresets(type, opts) { + const upperCaseType = type.toUpperCase(); return [ // dependencies - ...opts.plugins, + // opts + ...((opts[type === PluginType.preset ? 'presets' : 'plugins']) || []), + // env + ...(process.env[`FES_${upperCaseType}S`] || '').split(',').filter(Boolean), ...Object.keys(opts.pkg.devDependencies || {}) .concat(Object.keys(opts.pkg.dependencies || {})) - .filter(isPlugin.bind(null)), - ...opts.userConfigPlugins + .filter(isPluginOrPreset.bind(null, type)), + // user config + ...((opts[ + type === PluginType.preset ? 'userConfigPresets' : 'userConfigPlugins' + ]) || []) ].map(path => resolve.sync(path, { basedir: opts.cwd, extensions: ['.js', '.ts'] @@ -46,14 +56,14 @@ function nameToKey(name) { .join('.'); } -function pkgNameToKey(pkgName) { +function pkgNameToKey(pkgName, type) { if (pkgName.charAt(0) === '@' && !pkgName.startsWith('@webank/')) { pkgName = pkgName.split('/')[1]; } - return nameToKey(pkgName.replace(RE.plugin, '')); + return nameToKey(pkgName.replace(RE[type], '')); } -export function pathToObj({ path, cwd }) { +export function pathToObj({ path, type, cwd }) { let pkg = null; let isPkgPlugin = false; const pkgJSONPath = pkgUp.sync({ cwd: path }); @@ -74,11 +84,11 @@ export function pathToObj({ path, cwd }) { } else { id = winPath(path); } - id = id.replace('@webank/fes-plugin-built-in/lib/plugins', '@@'); + id = id.replace('@webank/fes-preset-built-in/lib/plugins', '@@'); id = id.replace(/\.js$/, ''); const key = isPkgPlugin - ? pkgNameToKey(pkg.name) + ? pkgNameToKey(pkg.name, type) : nameToKey(basename(path, extname(path))); return { @@ -100,10 +110,22 @@ export function pathToObj({ path, cwd }) { }; } +export function resolvePresets(opts) { + const type = PluginType.preset; + const presets = [...getPluginsOrPresets(type, opts)]; + return presets.map(path => pathToObj({ + type, + path, + cwd: opts.cwd + })); +} + export function resolvePlugins(opts) { - const plugins = getPlugins(opts); + const type = PluginType.plugin; + const plugins = getPluginsOrPresets(type, opts); return plugins.map(path => pathToObj({ path, + type, cwd: opts.cwd })); }