@fesjs/compiler
Version:
@fesjs/compiler
492 lines (461 loc) • 14.6 kB
JavaScript
"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(` /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;