UNPKG

build-scripts

Version:

scripts core

383 lines (382 loc) 15.7 kB
"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;