build-scripts
Version:
scripts core
383 lines (382 loc) • 15.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const assert = require("assert");
const fs = require("fs-extra");
const _ = require("lodash");
const camelCase = require("camelcase");
const log = require("../utils/log");
const JSON5 = require("json5");
const PKG_FILE = 'package.json';
const USER_CONFIG_FILE = 'build.json';
const PLUGIN_CONTEXT_KEY = [
'command',
'commandArgs',
'rootDir',
'userConfig',
'pkg',
'webpack',
];
const VALIDATION_MAP = {
string: 'isString',
number: 'isNumber',
array: 'isArray',
object: 'isObject',
boolean: 'isBoolean',
};
const BUILTIN_CLI_OPTIONS = [
{ name: 'port', commands: ['start'] },
{ name: 'host', commands: ['start'] },
{ name: 'disableAsk', commands: ['start'] },
{ name: 'config', commands: ['start', 'build', 'test'] },
];
class Context {
constructor({ command, rootDir = process.cwd(), args = {}, plugins = [], getBuiltInPlugins = () => [], }) {
this.registerConfig = (type, args, parseName) => {
const registerKey = `${type}Registration`;
if (!this[registerKey]) {
throw new Error(`unknown register type: ${type}, use available types (userConfig or cliOption) instead`);
}
const configArr = _.isArray(args) ? args : [args];
configArr.forEach((conf) => {
const confName = parseName ? parseName(conf.name) : conf.name;
if (this[registerKey][confName]) {
throw new Error(`${conf.name} already registered in ${type}`);
}
this[registerKey][confName] = conf;
// set default userConfig
if (type === 'userConfig'
&& _.isUndefined(this.userConfig[confName])
&& Object.prototype.hasOwnProperty.call(conf, 'defaultValue')) {
this.userConfig[confName] = conf.defaultValue;
}
});
};
this.getProjectFile = (fileName) => {
const configPath = path.resolve(this.rootDir, fileName);
let config = {};
if (fs.existsSync(configPath)) {
try {
config = fs.readJsonSync(configPath);
}
catch (err) {
log.info('CONFIG', `Fail to load config file ${configPath}, use empty object`);
}
}
return config;
};
this.getUserConfig = () => {
const { config } = this.commandArgs;
let configPath = '';
if (config) {
configPath = path.isAbsolute(config) ? config : path.resolve(this.rootDir, config);
}
else {
configPath = path.resolve(this.rootDir, USER_CONFIG_FILE);
}
let userConfig = {
plugins: [],
};
const isJsFile = path.extname(configPath) === '.js';
if (fs.existsSync(configPath)) {
try {
userConfig = isJsFile ? require(configPath) : JSON5.parse(fs.readFileSync(configPath, 'utf-8')); // read build.json
}
catch (err) {
log.info('CONFIG', `Fail to load config file ${configPath}, use default config instead`);
log.error('CONFIG', (err.stack || err.toString()));
process.exit(1);
}
}
return userConfig;
};
this.resolvePlugins = (builtInPlugins) => {
const userPlugins = [...builtInPlugins, ...(this.userConfig.plugins || [])].map((pluginInfo) => {
let fn;
if (_.isFunction(pluginInfo)) {
return {
fn: pluginInfo,
options: {},
};
}
const plugins = Array.isArray(pluginInfo) ? pluginInfo : [pluginInfo, undefined];
const pluginPath = require.resolve(plugins[0], { paths: [this.rootDir] });
const options = plugins[1];
try {
fn = require(pluginPath); // eslint-disable-line
}
catch (err) {
log.error('CONFIG', `Fail to load plugin ${pluginPath}`);
log.error('CONFIG', (err.stack || err.toString()));
process.exit(1);
}
return {
name: plugins[0],
pluginPath,
fn: fn.default || fn || (() => { }),
options,
};
});
return userPlugins;
};
this.getAllPlugin = (dataKeys = ['pluginPath', 'options', 'name']) => {
return this.plugins.map((pluginInfo) => {
// filter fn to avoid loop
return _.pick(pluginInfo, dataKeys);
});
};
this.registerTask = (name, chainConfig) => {
const exist = this.configArr.find((v) => v.name === name);
if (!exist) {
this.configArr.push({
name,
chainConfig,
modifyFunctions: [],
});
}
else {
throw new Error(`[Error] config '${name}' already exists!`);
}
};
this.registerMethod = (name, fn) => {
if (this.methodRegistration[name]) {
throw new Error(`[Error] method '${name}' already registered`);
}
else {
this.methodRegistration[name] = fn;
}
};
this.applyMethod = (name, ...args) => {
if (this.methodRegistration[name]) {
return this.methodRegistration[name](...args);
}
else {
return new Error(`apply unkown method ${name}`);
}
};
this.modifyUserConfig = (configKey, value) => {
const errorMsg = 'config plugins is not support to be modified';
if (typeof configKey === 'string') {
if (configKey === 'plugins') {
throw new Error(errorMsg);
}
this.userConfig[configKey] = value;
}
else if (typeof configKey === 'function') {
const modifiedValue = configKey(this.userConfig);
if (_.isPlainObject(modifiedValue)) {
if (Object.prototype.hasOwnProperty.call(modifiedValue, 'plugins')) {
log.warn('[waring]', errorMsg);
}
delete modifiedValue.plugins;
this.userConfig = { ...this.userConfig, ...modifiedValue };
}
else {
throw new Error(`modifyUserConfig must return a plain object`);
}
}
};
this.getAllTask = () => {
return this.configArr.map(v => v.name);
};
this.onGetWebpackConfig = (...args) => {
this.modifyConfigFns.push(args);
};
this.onGetJestConfig = (fn) => {
this.modifyJestConfig.push(fn);
};
this.runJestConfig = (jestConfig) => {
let result = jestConfig;
for (const fn of this.modifyJestConfig) {
result = fn(result);
}
return result;
};
this.onHook = (key, fn) => {
if (!Array.isArray(this.eventHooks[key])) {
this.eventHooks[key] = [];
}
this.eventHooks[key].push(fn);
};
this.applyHook = async (key, opts = {}) => {
const hooks = this.eventHooks[key] || [];
for (const fn of hooks) {
// eslint-disable-next-line no-await-in-loop
await fn(opts);
}
};
this.setValue = (key, value) => {
this.internalValue[key] = value;
};
this.getValue = (key) => {
return this.internalValue[key];
};
this.registerUserConfig = (args) => {
this.registerConfig('userConfig', args);
};
this.registerCliOption = (args) => {
this.registerConfig('cliOption', args, (name) => {
return camelCase(name, { pascalCase: false });
});
};
this.runPlugins = async () => {
for (const pluginInfo of this.plugins) {
const { fn, options } = pluginInfo;
const pluginContext = _.pick(this, PLUGIN_CONTEXT_KEY);
const pluginAPI = {
log,
context: pluginContext,
registerTask: this.registerTask,
getAllTask: this.getAllTask,
getAllPlugin: this.getAllPlugin,
onGetWebpackConfig: this.onGetWebpackConfig,
onGetJestConfig: this.onGetJestConfig,
onHook: this.onHook,
setValue: this.setValue,
getValue: this.getValue,
registerUserConfig: this.registerUserConfig,
registerCliOption: this.registerCliOption,
registerMethod: this.registerMethod,
applyMethod: this.applyMethod,
modifyUserConfig: this.modifyUserConfig,
};
// eslint-disable-next-line no-await-in-loop
await fn(pluginAPI, options);
}
};
this.checkPluginValue = (plugins) => {
let flag;
if (!_.isArray(plugins)) {
flag = false;
}
else {
flag = plugins.every(v => {
let correct = _.isArray(v) || _.isString(v) || _.isFunction(v);
if (correct && _.isArray(v)) {
correct = _.isString(v[0]);
}
return correct;
});
}
if (!flag) {
throw new Error('plugins did not pass validation');
}
};
this.runUserConfig = async () => {
for (const configInfoKey in this.userConfig) {
if (!['plugins', 'customWebpack'].includes(configInfoKey)) {
const configInfo = this.userConfigRegistration[configInfoKey];
if (!configInfo) {
throw new Error(`[Config File] Config key '${configInfoKey}' is not supported`);
}
const { name, validation } = configInfo;
const configValue = this.userConfig[name];
if (validation) {
let validationInfo;
if (_.isString(validation)) {
const fnName = VALIDATION_MAP[validation];
if (!fnName) {
throw new Error(`validation does not support ${validation}`);
}
assert(_[VALIDATION_MAP[validation]](configValue), `Config ${name} should be ${validation}, but got ${configValue}`);
}
else {
// eslint-disable-next-line no-await-in-loop
validationInfo = await validation(configValue);
assert(validationInfo, `${name} did not pass validation, result: ${validationInfo}`);
}
}
if (configInfo.configWebpack) {
// eslint-disable-next-line no-await-in-loop
await this.runConfigWebpack(configInfo.configWebpack, configValue);
}
}
}
};
this.runCliOption = async () => {
for (const cliOpt in this.commandArgs) {
// allow all jest option when run command test
if (this.command !== 'test' || cliOpt !== 'jestArgv') {
const { commands, name, configWebpack } = this.cliOptionRegistration[cliOpt] || {};
if (!name || !(commands || []).includes(this.command)) {
throw new Error(`cli option '${cliOpt}' is not supported when run command '${this.command}'`);
}
if (configWebpack) {
// eslint-disable-next-line no-await-in-loop
await this.runConfigWebpack(configWebpack, this.commandArgs[cliOpt]);
}
}
}
};
this.runWebpackFunctions = async () => {
this.modifyConfigFns.forEach(([name, func]) => {
const isAll = _.isFunction(name);
if (isAll) { // modify all
this.configArr.forEach(config => {
config.modifyFunctions.push(name);
});
}
else { // modify named config
this.configArr.forEach(config => {
if (config.name === name) {
config.modifyFunctions.push(func);
}
});
}
});
for (const configInfo of this.configArr) {
for (const func of configInfo.modifyFunctions) {
// eslint-disable-next-line no-await-in-loop
await func(configInfo.chainConfig);
}
}
};
this.setUp = async () => {
await this.runPlugins();
await this.runUserConfig();
await this.runWebpackFunctions();
await this.runCliOption();
return this.configArr;
};
this.command = command;
this.commandArgs = args;
this.rootDir = rootDir;
/**
* config array
* {
* name,
* chainConfig,
* webpackFunctions,
* }
*/
this.configArr = [];
this.modifyConfigFns = [];
this.modifyJestConfig = [];
this.eventHooks = {}; // lifecycle functions
this.internalValue = {}; // internal value shared between plugins
this.userConfigRegistration = {};
this.cliOptionRegistration = {};
this.methodRegistration = {};
this.pkg = this.getProjectFile(PKG_FILE);
this.userConfig = this.getUserConfig();
// custom webpack
const webpackPath = this.userConfig.customWebpack ? require.resolve('webpack', { paths: [this.rootDir] }) : 'webpack';
this.webpack = require(webpackPath);
// register buildin options
this.registerCliOption(BUILTIN_CLI_OPTIONS);
const builtInPlugins = [...plugins, ...getBuiltInPlugins(this.userConfig)];
this.checkPluginValue(builtInPlugins); // check plugins property
this.plugins = this.resolvePlugins(builtInPlugins);
}
async runConfigWebpack(fn, configValue) {
for (const webpackConfigInfo of this.configArr) {
const userConfigContext = {
..._.pick(this, PLUGIN_CONTEXT_KEY),
taskName: webpackConfigInfo.name,
};
// eslint-disable-next-line no-await-in-loop
await fn(webpackConfigInfo.chainConfig, configValue, userConfigContext);
}
}
}
exports.default = Context;