UNPKG

build-scripts

Version:
454 lines (453 loc) 21.4 kB
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; });