UNPKG

@fesjs/compiler

Version:
492 lines (461 loc) 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _path = require("path"); var _events = require("events"); var _assert = _interopRequireDefault(require("assert")); var _fs = require("fs"); var _tapable = require("tapable"); var _utils = require("@fesjs/utils"); var _commander = require("commander"); var _config = _interopRequireDefault(require("../config")); var _configUtils = require("../config/utils/configUtils"); var _pluginUtils = require("./utils/pluginUtils"); var _loadDotEnv = _interopRequireDefault(require("./utils/loadDotEnv")); var _isPromise = _interopRequireDefault(require("./utils/isPromise")); var _babelRegister = _interopRequireDefault(require("./babelRegister")); var _pluginAPI = _interopRequireDefault(require("./pluginAPI")); var _enums = require("./enums"); var _getPaths = _interopRequireDefault(require("./getPaths")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * @copy 该文件代码大部分出自 umi,有需要请参考: * https://github.com/umijs/umi/tree/master/packages/core */ // TODO // 1. duplicated key class Service extends _events.EventEmitter { cwd; pkg; skipPluginIds = new Set(); // lifecycle stage stage = _enums.ServiceStage.uninitialized; // registered commands commands = {}; // including plugins plugins = {}; // 构建 builder = {}; // 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 = _enums.ApplyPluginsType; EnableBy = _enums.EnableBy; ConfigChangeType = _enums.ConfigChangeType; ServiceStage = _enums.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; this.fesPkg = opts.fesPkg || {}; (0, _assert.default)((0, _fs.existsSync)(this.cwd), `cwd ${this.cwd} does not exist.`); // register babel before config parsing this.babelRegister = new _babelRegister.default(); // load .env or .local.env this.loadEnv(); // get user config without validation this.configInstance = new _config.default({ cwd: this.cwd, service: this, localConfig: this.env === 'development' }); this.userConfig = this.configInstance.getUserConfig(); // get paths this.paths = (0, _getPaths.default)({ cwd: this.cwd, config: this.userConfig, env: this.env }); this.program = this.initCommand(); // setup initial plugins const baseOpts = { pkg: this.pkg, cwd: this.cwd }; this.initialPresets = (0, _pluginUtils.resolvePresets)({ ...baseOpts, presets: opts.presets || [], userConfigPresets: this.userConfig.presets || [], builder: this.userConfig.builder }); this.initialPlugins = (0, _pluginUtils.resolvePlugins)({ ...baseOpts, plugins: opts.plugins || [], userConfigPlugins: this.userConfig.plugins || [], builder: this.userConfig.builder }); } setStage(stage) { this.stage = stage; } resolvePackage() { try { // eslint-disable-next-line return require((0, _path.join)(this.cwd, "package.json")); } catch (e) { return {}; } } loadEnv() { const basePath = (0, _path.join)(this.cwd, '.env'); const localPath = `${basePath}.local`; (0, _loadDotEnv.default)(basePath); if (process.env.FES_ENV) { (0, _loadDotEnv.default)(`${basePath}.${process.env.FES_ENV}`); } (0, _loadDotEnv.default)(localPath); } async init() { this.setStage(_enums.ServiceStage.init); await this.initPresetsAndPlugins(); // hooksByPluginId -> hooks // hooks is mapped with hook key, prepared for applyPlugins() this.setStage(_enums.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(_enums.ServiceStage.pluginReady); await this.applyPlugins({ key: 'onPluginReady', type: _enums.ApplyPluginsType.event }); // get config, including: // 1. merge default config // 2. validate this.setStage(_enums.ServiceStage.getConfig); await this.setConfig(); // merge paths to keep the this.paths ref this.setStage(_enums.ServiceStage.getPaths); // config.outputPath may be modified by plugins if (this.config.outputPath) { this.paths.absOutputPath = (0, _path.join)(this.cwd, this.config.outputPath); } const paths = await this.applyPlugins({ key: 'modifyPaths', type: _enums.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(_enums.ServiceStage.initPresets); this._extraPlugins = []; while (this.initialPresets.length) { // eslint-disable-next-line await this.initPreset(this.initialPresets.shift()); } this.setStage(_enums.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.default(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', 'configInstance', 'userConfig', 'config', 'env', 'args', 'hasPlugins', 'hasPresets', 'setConfig', 'builder'].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 ((0, _isPromise.default)(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) { (0, _assert.default)(Array.isArray(presets), `presets returned from preset ${id} must be Array.`); // 插到最前面,下个 while 循环优先执行 this._extraPresets.splice(0, 0, ...presets.map(path => (0, _pluginUtils.pathToObj)({ type: _enums.PluginType.preset, path, cwd: this.cwd }))); } // 深度优先 const extraPresets = _utils.lodash.clone(this._extraPresets); this._extraPresets = []; while (extraPresets.length) { // eslint-disable-next-line await this.initPreset(extraPresets.shift()); } if (plugins) { (0, _assert.default)(Array.isArray(plugins), `plugins returned from preset ${id} must be Array.`); this._extraPlugins.push(...plugins.map(path => (0, _pluginUtils.pathToObj)({ type: _enums.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 (0, _configUtils.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 _enums.ApplyPluginsType.add: if ('initialValue' in opts) { (0, _assert.default)(Array.isArray(opts.initialValue), 'applyPlugins failed, opts.initialValue must be Array if opts.type is add.'); } // eslint-disable-next-line const tAdd = new _tapable.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 _enums.ApplyPluginsType.modify: // eslint-disable-next-line const tModify = new _tapable.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 _enums.ApplyPluginsType.event: // eslint-disable-next-line const tEvent = new _tapable.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}.`); } } initCommand() { const command = new _commander.Command(); command.usage('<command> [options]').version(`@fesjs/fes ${this.fesPkg.version}`, '-v, --vers', 'output the current version').description(_utils.chalk.cyan('一个好用的前端应用解决方案')); return command; } async run({ rawArgv = {}, args = {} }) { await this.init(); this.setStage(_enums.ServiceStage.run); await this.applyPlugins({ key: 'onStart', type: _enums.ApplyPluginsType.event, args: { args } }); return this.runCommand({ rawArgv, args }); } async runCommand({ rawArgv = {}, args = {} }) { (0, _assert.default)(this.stage >= _enums.ServiceStage.init, 'service is not initialized.'); Object.keys(this.commands).forEach(command => { const commandOptionConfig = this.commands[command]; const program = this.program; let c = program.command(command).description(commandOptionConfig.description); if (Array.isArray(commandOptionConfig.options)) { commandOptionConfig.options.forEach(config => { const option = new _commander.Option(config.name, config.description); if (config.default) { option.default(config.default); } if (config.choices) { option.choices(config.choices); } c = c.addOption(option); }); } if (commandOptionConfig.fn) { c.action(async () => { await commandOptionConfig.fn({ rawArgv, args, options: c.opts(), program }); }); } }); return this.parseCommand(); } async parseCommand() { this.program.on('--help', () => { console.log(); console.log(` Run ${_utils.chalk.cyan('fes <command> --help')} for detailed usage of given command.`); console.log(); }); this.program.commands.forEach(c => c.on('--help', () => console.log())); return this.program.parseAsync(process.argv); } } exports.default = Service;