build-scripts
Version:
scripts core
454 lines (453 loc) • 21.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
/* eslint-disable max-lines */
import camelCase from 'camelcase';
import assert from 'assert';
import _ from 'lodash';
import { getUserConfig, resolveConfigFile } from './utils/loadConfig.js';
import loadPkg from './utils/loadPkg.js';
import { createLogger } from './utils/logger.js';
import resolvePlugins from './utils/resolvePlugins.js';
import checkPlugin from './utils/checkPlugin.js';
import { PLUGIN_CONTEXT_KEY, VALIDATION_MAP, BUILTIN_CLI_OPTIONS, IGNORED_USE_CONFIG_KEY, USER_CONFIG_FILE } from './utils/constant.js';
const mergeConfig = (currentValue, newValue) => {
// only merge when currentValue and newValue is object and array
const isBothArray = Array.isArray(currentValue) && Array.isArray(newValue);
const isBothObject = _.isPlainObject(currentValue) && _.isPlainObject(newValue);
if (isBothArray || isBothObject) {
return _.merge(currentValue, newValue);
}
else {
return newValue;
}
};
/**
* Build Scripts Context
*
* @class Context
* @template T Task Config
* @template U Type of extendsPluginAPI
* @template K User Config
*/
class Context {
constructor(options) {
this.logger = createLogger('BUILD-SCRIPTS');
// 存放 config 配置的数组
this.configArr = [];
this.modifyConfigFns = [];
this.modifyJestConfig = [];
this.modifyConfigRegistrationCallbacks = [];
this.modifyCliRegistrationCallbacks = [];
this.eventHooks = {};
this.internalValue = {};
this.userConfigRegistration = {};
this.cliOptionRegistration = {};
this.methodRegistration = {};
this.cancelTaskNames = [];
this.runJestConfig = (jestConfig) => {
let result = jestConfig;
for (const fn of this.modifyJestConfig) {
result = fn(result);
}
return result;
};
this.getTaskConfig = () => {
return this.configArr;
};
this.setup = () => __awaiter(this, void 0, void 0, function* () {
yield this.resolveUserConfig();
yield this.resolvePlugins();
yield this.runPlugins();
yield this.runConfigModification();
yield this.validateUserConfig();
yield this.runOnGetConfigFn();
yield this.runCliOption();
// filter webpack config by cancelTaskNames
this.configArr = this.configArr.filter((config) => !this.cancelTaskNames.includes(config.name));
return this.configArr;
});
this.getAllTask = () => {
return this.configArr.map((v) => v.name);
};
this.getAllPlugin = (dataKeys = ['pluginPath', 'options', 'name']) => {
return this.plugins.map((pluginInfo) => {
// filter fn to avoid loop
return _.pick(pluginInfo, dataKeys);
});
};
this.resolveUserConfig = () => __awaiter(this, void 0, void 0, function* () {
if (!this.userConfig) {
this.configFilePath = yield resolveConfigFile(this.configFile, this.commandArgs, this.rootDir);
this.userConfig = yield getUserConfig({
rootDir: this.rootDir,
commandArgs: this.commandArgs,
pkg: this.pkg,
logger: this.logger,
configFilePath: this.configFilePath,
});
}
return this.userConfig;
});
this.resolvePlugins = () => __awaiter(this, void 0, void 0, function* () {
if (!this.plugins) {
// shallow copy of userConfig while userConfig may be modified
this.originalUserConfig = Object.assign({}, this.userConfig);
const { plugins = [], getBuiltInPlugins = () => [] } = this.options;
// run getBuiltInPlugins before resolve webpack while getBuiltInPlugins may add require hook for webpack
const builtInPlugins = [
...plugins,
...getBuiltInPlugins(this.userConfig),
];
checkPlugin(builtInPlugins); // check plugins property
this.plugins = yield resolvePlugins([
...builtInPlugins,
...(this.userConfig.plugins || []),
], {
rootDir: this.rootDir,
logger: this.logger,
});
}
return this.plugins;
});
this.applyHook = (key, opts = {}) => __awaiter(this, void 0, void 0, function* () {
const hooks = this.eventHooks[key] || [];
const preHooks = [];
const normalHooks = [];
const postHooks = [];
hooks.forEach(([fn, options]) => {
if ((options === null || options === void 0 ? void 0 : options.enforce) === 'pre') {
preHooks.push(fn);
}
else if ((options === null || options === void 0 ? void 0 : options.enforce) === 'post') {
postHooks.push(fn);
}
else {
normalHooks.push(fn);
}
});
for (const fn of [...preHooks, ...normalHooks, ...postHooks]) {
// eslint-disable-next-line no-await-in-loop
yield fn(opts);
}
});
this.registerTask = (name, config) => {
const exist = this.configArr.find((v) => v.name === name);
if (!exist) {
this.configArr.push({
name,
config,
modifyFunctions: [],
});
}
else {
throw new Error(`[Error] config '${name}' already exists!`);
}
};
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 = Object.assign(Object.assign({}, this.userConfig), { [confName]: conf.defaultValue });
}
});
};
this.onHook = (key, fn, options) => {
if (!Array.isArray(this.eventHooks[key])) {
this.eventHooks[key] = [];
}
this.eventHooks[key].push([fn, options]);
};
this.runPlugins = () => __awaiter(this, void 0, void 0, function* () {
for (const pluginInfo of this.plugins) {
const { setup, options, name: pluginName } = pluginInfo;
const pluginContext = _.pick(this, PLUGIN_CONTEXT_KEY);
const applyMethod = (methodName, ...args) => {
return this.applyMethod([methodName, pluginName], ...args);
};
const pluginAPI = _.merge({
context: pluginContext,
registerTask: this.registerTask,
getAllTask: this.getAllTask,
getAllPlugin: this.getAllPlugin,
cancelTask: this.cancelTask,
onGetConfig: this.onGetConfig,
onGetJestConfig: this.onGetJestConfig,
onHook: this.onHook,
setValue: this.setValue,
getValue: this.getValue,
registerUserConfig: this.registerUserConfig,
hasRegistration: this.hasRegistration,
registerCliOption: this.registerCliOption,
registerMethod: this.registerMethod,
applyMethod,
hasMethod: this.hasMethod,
modifyUserConfig: this.modifyUserConfig,
modifyConfigRegistration: this.modifyConfigRegistration,
modifyCliRegistration: this.modifyCliRegistration,
}, this.extendsPluginAPI || {});
// eslint-disable-next-line no-await-in-loop
yield setup(pluginAPI, options);
}
});
this.runConfigModification = () => __awaiter(this, void 0, void 0, function* () {
const callbackRegistrations = [
'modifyConfigRegistrationCallbacks',
'modifyCliRegistrationCallbacks',
];
callbackRegistrations.forEach((registrationKey) => {
const registrations = this[registrationKey];
registrations.forEach(([name, callback]) => {
const modifyAll = _.isFunction(name);
const configRegistrations = this[registrationKey === 'modifyConfigRegistrationCallbacks'
? 'userConfigRegistration'
: 'cliOptionRegistration'];
if (modifyAll) {
const modifyFunction = name;
const modifiedResult = modifyFunction(configRegistrations);
Object.keys(modifiedResult).forEach((configKey) => {
configRegistrations[configKey] = Object.assign(Object.assign({}, (configRegistrations[configKey] || {})), modifiedResult[configKey]);
});
}
else if (typeof name === 'string') {
if (!configRegistrations[name]) {
throw new Error(`Config key '${name}' is not registered`);
}
const configRegistration = configRegistrations[name];
configRegistrations[name] = Object.assign(Object.assign({}, configRegistration), callback(configRegistration));
}
});
});
});
this.validateUserConfig = () => __awaiter(this, void 0, void 0, function* () {
for (const configInfoKey in this.userConfig) {
if (IGNORED_USE_CONFIG_KEY.includes(configInfoKey)) {
continue;
}
const configInfo = this.userConfigRegistration[configInfoKey];
if (!configInfo) {
throw new Error(`[Config File] Config key '${configInfoKey}' is not supported`);
}
const { name, validation, ignoreTasks, setConfig } = configInfo;
const configValue = this.userConfig[name];
if (validation) {
let validationInfo;
if (_.isString(validation)) {
// split validation string
const supportTypes = validation.split('|');
const validateResult = supportTypes.some((supportType) => {
const fnName = VALIDATION_MAP[supportType];
if (!fnName) {
throw new Error(`validation does not support ${supportType}`);
}
return _[fnName](configValue);
});
assert(validateResult, `Config ${name} should be ${validation}, but got ${configValue}`);
}
else {
// eslint-disable-next-line no-await-in-loop
validationInfo = yield validation(configValue);
assert(validationInfo, `${name} did not pass validation, result: ${validationInfo}`);
}
}
if (setConfig) {
// eslint-disable-next-line no-await-in-loop
yield this.runSetConfig(setConfig, configValue, ignoreTasks);
}
}
});
this.runCliOption = () => __awaiter(this, void 0, void 0, function* () {
for (const cliOpt in this.commandArgs) {
// allow all jest option when run command test
if (this.command !== 'test' || cliOpt !== 'jestArgv') {
const { commands, name, setConfig, ignoreTasks } = this.cliOptionRegistration[cliOpt] || {};
if (!name || !(commands || []).includes(this.command)) {
throw new Error(`cli option '${cliOpt}' is not supported when run command '${this.command}'`);
}
if (setConfig) {
// eslint-disable-next-line no-await-in-loop
yield this.runSetConfig(setConfig, this.commandArgs[cliOpt], ignoreTasks);
}
}
}
});
this.runOnGetConfigFn = () => __awaiter(this, void 0, void 0, function* () {
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
const maybeConfig = yield func(configInfo.config);
if (maybeConfig) {
configInfo.config = maybeConfig;
}
}
}
});
this.cancelTask = (name) => {
if (this.cancelTaskNames.includes(name)) {
this.logger.info(`task ${name} has already been canceled`);
}
else {
this.cancelTaskNames.push(name);
}
};
this.registerMethod = (name, fn, options) => {
if (this.methodRegistration[name]) {
throw new Error(`[Error] method '${name}' already registered`);
}
else {
const registration = [fn, options];
this.methodRegistration[name] = registration;
}
};
this.applyMethod = (config, ...args) => {
const [methodName, pluginName] = Array.isArray(config) ? config : [config];
if (this.methodRegistration[methodName]) {
const [registerMethod, methodOptions] = this.methodRegistration[methodName];
if (methodOptions === null || methodOptions === void 0 ? void 0 : methodOptions.pluginName) {
return registerMethod(pluginName)(...args);
}
else {
return registerMethod(...args);
}
}
else {
throw new Error(`apply unknown method ${methodName}`);
}
};
this.hasMethod = (name) => {
return !!this.methodRegistration[name];
};
this.modifyUserConfig = (configKey, value, options) => {
const errorMsg = 'config plugins is not support to be modified';
const { deepmerge: mergeInDeep } = options || {};
if (typeof configKey === 'string') {
if (configKey === 'plugins') {
throw new Error(errorMsg);
}
const configPath = configKey.split('.');
const originalValue = _.get(this.userConfig, configPath);
const newValue = typeof value !== 'function' ? value : value(originalValue);
_.set(this.userConfig, configPath, mergeInDeep ? mergeConfig(originalValue, newValue) : newValue);
}
else if (typeof configKey === 'function') {
const modifiedValue = configKey(this.userConfig);
if (_.isPlainObject(modifiedValue)) {
if (Object.prototype.hasOwnProperty.call(modifiedValue, 'plugins')) {
// remove plugins while it is not support to be modified
this.logger.info('delete plugins of user config while it is not support to be modified');
delete modifiedValue.plugins;
}
Object.keys(modifiedValue).forEach((modifiedConfigKey) => {
const originalValue = this.userConfig[modifiedConfigKey];
this.userConfig = Object.assign(Object.assign({}, this.userConfig), { [modifiedConfigKey]: mergeInDeep
? mergeConfig(originalValue, modifiedValue[modifiedConfigKey])
: modifiedValue[modifiedConfigKey] });
});
}
else {
throw new Error('modifyUserConfig must return a plain object');
}
}
};
this.modifyConfigRegistration = (...args) => {
this.modifyConfigRegistrationCallbacks.push(args);
};
this.modifyCliRegistration = (...args) => {
this.modifyCliRegistrationCallbacks.push(args);
};
this.onGetConfig = (...args) => {
this.modifyConfigFns.push(args);
};
this.onGetJestConfig = (fn) => {
this.modifyJestConfig.push(fn);
};
this.setValue = (key, value) => {
this.internalValue[key] = value;
};
this.getValue = (key) => {
return this.internalValue[key];
};
this.registerUserConfig = (args) => {
this.registerConfig('userConfig', args);
};
this.hasRegistration = (name, type = 'userConfig') => {
const mappedType = type === 'cliOption' ? 'cliOptionRegistration' : 'userConfigRegistration';
return Object.keys(this[mappedType] || {}).includes(name);
};
this.registerCliOption = (args) => {
this.registerConfig('cliOption', args, (name) => {
return camelCase(name, { pascalCase: false });
});
};
const { command, configFile = USER_CONFIG_FILE, rootDir = process.cwd(), commandArgs = {}, extendsPluginAPI, } = options || {};
this.options = options;
this.command = command;
this.commandArgs = commandArgs;
this.rootDir = rootDir;
this.extendsPluginAPI = extendsPluginAPI;
this.pkg = loadPkg(rootDir, this.logger);
this.configFile = configFile;
// Register built-in command
this.registerCliOption(BUILTIN_CLI_OPTIONS);
}
runSetConfig(fn, configValue, ignoreTasks) {
return __awaiter(this, void 0, void 0, function* () {
for (const configInfo of this.configArr) {
const taskName = configInfo.name;
let ignoreConfig = false;
if (Array.isArray(ignoreTasks)) {
ignoreConfig = ignoreTasks.some((ignoreTask) => new RegExp(ignoreTask).exec(taskName));
}
if (!ignoreConfig) {
const userConfigContext = Object.assign(Object.assign({}, _.pick(this, PLUGIN_CONTEXT_KEY)), { taskName });
// eslint-disable-next-line no-await-in-loop
const maybeConfig = yield fn(configInfo.config, configValue, userConfigContext);
if (maybeConfig) {
configInfo.config = maybeConfig;
}
}
}
});
}
}
export default Context;
export const createContext = (args) => __awaiter(void 0, void 0, void 0, function* () {
const ctx = new Context(args);
yield ctx.setup();
return ctx;
});