UNPKG

17.1 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const path = require("path");
4const assert = require("assert");
5const fs = require("fs-extra");
6const _ = require("lodash");
7const camelCase = require("camelcase");
8const log = require("../utils/log");
9const JSON5 = require("json5");
10const PKG_FILE = 'package.json';
11const USER_CONFIG_FILE = 'build.json';
12const PLUGIN_CONTEXT_KEY = [
13 'command',
14 'commandArgs',
15 'rootDir',
16 'userConfig',
17 'pkg',
18 'webpack',
19];
20const VALIDATION_MAP = {
21 string: 'isString',
22 number: 'isNumber',
23 array: 'isArray',
24 object: 'isObject',
25 boolean: 'isBoolean',
26};
27const BUILTIN_CLI_OPTIONS = [
28 { name: 'port', commands: ['start'] },
29 { name: 'host', commands: ['start'] },
30 { name: 'disableAsk', commands: ['start'] },
31 { name: 'config', commands: ['start', 'build', 'test'] },
32];
33class Context {
34 constructor({ command, rootDir = process.cwd(), args = {}, plugins = [], getBuiltInPlugins = () => [], }) {
35 this.registerConfig = (type, args, parseName) => {
36 const registerKey = `${type}Registration`;
37 if (!this[registerKey]) {
38 throw new Error(`unknown register type: ${type}, use available types (userConfig or cliOption) instead`);
39 }
40 const configArr = _.isArray(args) ? args : [args];
41 configArr.forEach((conf) => {
42 const confName = parseName ? parseName(conf.name) : conf.name;
43 if (this[registerKey][confName]) {
44 throw new Error(`${conf.name} already registered in ${type}`);
45 }
46 this[registerKey][confName] = conf;
47 // set default userConfig
48 if (type === 'userConfig'
49 && _.isUndefined(this.userConfig[confName])
50 && Object.prototype.hasOwnProperty.call(conf, 'defaultValue')) {
51 this.userConfig[confName] = conf.defaultValue;
52 }
53 });
54 };
55 this.getProjectFile = (fileName) => {
56 const configPath = path.resolve(this.rootDir, fileName);
57 let config = {};
58 if (fs.existsSync(configPath)) {
59 try {
60 config = fs.readJsonSync(configPath);
61 }
62 catch (err) {
63 log.info('CONFIG', `Fail to load config file ${configPath}, use empty object`);
64 }
65 }
66 return config;
67 };
68 this.getUserConfig = () => {
69 const { config } = this.commandArgs;
70 let configPath = '';
71 if (config) {
72 configPath = path.isAbsolute(config) ? config : path.resolve(this.rootDir, config);
73 }
74 else {
75 configPath = path.resolve(this.rootDir, USER_CONFIG_FILE);
76 }
77 let userConfig = {
78 plugins: [],
79 };
80 const isJsFile = path.extname(configPath) === '.js';
81 if (fs.existsSync(configPath)) {
82 try {
83 userConfig = isJsFile ? require(configPath) : JSON5.parse(fs.readFileSync(configPath, 'utf-8')); // read build.json
84 }
85 catch (err) {
86 log.info('CONFIG', `Fail to load config file ${configPath}, use default config instead`);
87 log.error('CONFIG', (err.stack || err.toString()));
88 process.exit(1);
89 }
90 }
91 return this.mergeModeConfig(userConfig);
92 };
93 this.mergeModeConfig = (userConfig) => {
94 const { mode } = this.commandArgs;
95 // modify userConfig by userConfig.modeConfig
96 if (userConfig.modeConfig && mode && userConfig.modeConfig[mode]) {
97 const { plugins, ...basicConfig } = userConfig.modeConfig[mode];
98 const userPlugins = [...userConfig.plugins];
99 if (Array.isArray(plugins)) {
100 const pluginKeys = userPlugins.map((pluginInfo) => {
101 return Array.isArray(pluginInfo) ? pluginInfo[0] : pluginInfo;
102 });
103 plugins.forEach((pluginInfo) => {
104 const [pluginName] = Array.isArray(pluginInfo) ? pluginInfo : [pluginInfo];
105 const pluginIndex = pluginKeys.indexOf(pluginName);
106 if (pluginIndex > -1) {
107 // overwrite plugin info by modeConfig
108 userPlugins[pluginIndex] = pluginInfo;
109 }
110 else {
111 // push new plugin added by modeConfig
112 userPlugins.push(pluginInfo);
113 }
114 });
115 }
116 return { ...userConfig, ...basicConfig, plugins: userPlugins };
117 }
118 return userConfig;
119 };
120 this.resolvePlugins = (builtInPlugins) => {
121 const userPlugins = [...builtInPlugins, ...(this.userConfig.plugins || [])].map((pluginInfo) => {
122 let fn;
123 if (_.isFunction(pluginInfo)) {
124 return {
125 fn: pluginInfo,
126 options: {},
127 };
128 }
129 const plugins = Array.isArray(pluginInfo) ? pluginInfo : [pluginInfo, undefined];
130 const pluginPath = require.resolve(plugins[0], { paths: [this.rootDir] });
131 const options = plugins[1];
132 try {
133 fn = require(pluginPath); // eslint-disable-line
134 }
135 catch (err) {
136 log.error('CONFIG', `Fail to load plugin ${pluginPath}`);
137 log.error('CONFIG', (err.stack || err.toString()));
138 process.exit(1);
139 }
140 return {
141 name: plugins[0],
142 pluginPath,
143 fn: fn.default || fn || (() => { }),
144 options,
145 };
146 });
147 return userPlugins;
148 };
149 this.getAllPlugin = (dataKeys = ['pluginPath', 'options', 'name']) => {
150 return this.plugins.map((pluginInfo) => {
151 // filter fn to avoid loop
152 return _.pick(pluginInfo, dataKeys);
153 });
154 };
155 this.registerTask = (name, chainConfig) => {
156 const exist = this.configArr.find((v) => v.name === name);
157 if (!exist) {
158 this.configArr.push({
159 name,
160 chainConfig,
161 modifyFunctions: [],
162 });
163 }
164 else {
165 throw new Error(`[Error] config '${name}' already exists!`);
166 }
167 };
168 this.registerMethod = (name, fn) => {
169 if (this.methodRegistration[name]) {
170 throw new Error(`[Error] method '${name}' already registered`);
171 }
172 else {
173 this.methodRegistration[name] = fn;
174 }
175 };
176 this.applyMethod = (name, ...args) => {
177 if (this.methodRegistration[name]) {
178 return this.methodRegistration[name](...args);
179 }
180 else {
181 return new Error(`apply unkown method ${name}`);
182 }
183 };
184 this.modifyUserConfig = (configKey, value) => {
185 const errorMsg = 'config plugins is not support to be modified';
186 if (typeof configKey === 'string') {
187 if (configKey === 'plugins') {
188 throw new Error(errorMsg);
189 }
190 this.userConfig[configKey] = value;
191 }
192 else if (typeof configKey === 'function') {
193 const modifiedValue = configKey(this.userConfig);
194 if (_.isPlainObject(modifiedValue)) {
195 if (Object.prototype.hasOwnProperty.call(modifiedValue, 'plugins')) {
196 log.warn('[waring]', errorMsg);
197 }
198 delete modifiedValue.plugins;
199 this.userConfig = { ...this.userConfig, ...modifiedValue };
200 }
201 else {
202 throw new Error(`modifyUserConfig must return a plain object`);
203 }
204 }
205 };
206 this.getAllTask = () => {
207 return this.configArr.map(v => v.name);
208 };
209 this.onGetWebpackConfig = (...args) => {
210 this.modifyConfigFns.push(args);
211 };
212 this.onGetJestConfig = (fn) => {
213 this.modifyJestConfig.push(fn);
214 };
215 this.runJestConfig = (jestConfig) => {
216 let result = jestConfig;
217 for (const fn of this.modifyJestConfig) {
218 result = fn(result);
219 }
220 return result;
221 };
222 this.onHook = (key, fn) => {
223 if (!Array.isArray(this.eventHooks[key])) {
224 this.eventHooks[key] = [];
225 }
226 this.eventHooks[key].push(fn);
227 };
228 this.applyHook = async (key, opts = {}) => {
229 const hooks = this.eventHooks[key] || [];
230 for (const fn of hooks) {
231 // eslint-disable-next-line no-await-in-loop
232 await fn(opts);
233 }
234 };
235 this.setValue = (key, value) => {
236 this.internalValue[key] = value;
237 };
238 this.getValue = (key) => {
239 return this.internalValue[key];
240 };
241 this.registerUserConfig = (args) => {
242 this.registerConfig('userConfig', args);
243 };
244 this.registerCliOption = (args) => {
245 this.registerConfig('cliOption', args, (name) => {
246 return camelCase(name, { pascalCase: false });
247 });
248 };
249 this.runPlugins = async () => {
250 for (const pluginInfo of this.plugins) {
251 const { fn, options } = pluginInfo;
252 const pluginContext = _.pick(this, PLUGIN_CONTEXT_KEY);
253 const pluginAPI = {
254 log,
255 context: pluginContext,
256 registerTask: this.registerTask,
257 getAllTask: this.getAllTask,
258 getAllPlugin: this.getAllPlugin,
259 onGetWebpackConfig: this.onGetWebpackConfig,
260 onGetJestConfig: this.onGetJestConfig,
261 onHook: this.onHook,
262 setValue: this.setValue,
263 getValue: this.getValue,
264 registerUserConfig: this.registerUserConfig,
265 registerCliOption: this.registerCliOption,
266 registerMethod: this.registerMethod,
267 applyMethod: this.applyMethod,
268 modifyUserConfig: this.modifyUserConfig,
269 };
270 // eslint-disable-next-line no-await-in-loop
271 await fn(pluginAPI, options);
272 }
273 };
274 this.checkPluginValue = (plugins) => {
275 let flag;
276 if (!_.isArray(plugins)) {
277 flag = false;
278 }
279 else {
280 flag = plugins.every(v => {
281 let correct = _.isArray(v) || _.isString(v) || _.isFunction(v);
282 if (correct && _.isArray(v)) {
283 correct = _.isString(v[0]);
284 }
285 return correct;
286 });
287 }
288 if (!flag) {
289 throw new Error('plugins did not pass validation');
290 }
291 };
292 this.runUserConfig = async () => {
293 for (const configInfoKey in this.userConfig) {
294 if (!['plugins', 'customWebpack'].includes(configInfoKey)) {
295 const configInfo = this.userConfigRegistration[configInfoKey];
296 if (!configInfo) {
297 throw new Error(`[Config File] Config key '${configInfoKey}' is not supported`);
298 }
299 const { name, validation } = configInfo;
300 const configValue = this.userConfig[name];
301 if (validation) {
302 let validationInfo;
303 if (_.isString(validation)) {
304 const fnName = VALIDATION_MAP[validation];
305 if (!fnName) {
306 throw new Error(`validation does not support ${validation}`);
307 }
308 assert(_[VALIDATION_MAP[validation]](configValue), `Config ${name} should be ${validation}, but got ${configValue}`);
309 }
310 else {
311 // eslint-disable-next-line no-await-in-loop
312 validationInfo = await validation(configValue);
313 assert(validationInfo, `${name} did not pass validation, result: ${validationInfo}`);
314 }
315 }
316 if (configInfo.configWebpack) {
317 // eslint-disable-next-line no-await-in-loop
318 await this.runConfigWebpack(configInfo.configWebpack, configValue);
319 }
320 }
321 }
322 };
323 this.runCliOption = async () => {
324 for (const cliOpt in this.commandArgs) {
325 // allow all jest option when run command test
326 if (this.command !== 'test' || cliOpt !== 'jestArgv') {
327 const { commands, name, configWebpack } = this.cliOptionRegistration[cliOpt] || {};
328 if (!name || !(commands || []).includes(this.command)) {
329 throw new Error(`cli option '${cliOpt}' is not supported when run command '${this.command}'`);
330 }
331 if (configWebpack) {
332 // eslint-disable-next-line no-await-in-loop
333 await this.runConfigWebpack(configWebpack, this.commandArgs[cliOpt]);
334 }
335 }
336 }
337 };
338 this.runWebpackFunctions = async () => {
339 this.modifyConfigFns.forEach(([name, func]) => {
340 const isAll = _.isFunction(name);
341 if (isAll) { // modify all
342 this.configArr.forEach(config => {
343 config.modifyFunctions.push(name);
344 });
345 }
346 else { // modify named config
347 this.configArr.forEach(config => {
348 if (config.name === name) {
349 config.modifyFunctions.push(func);
350 }
351 });
352 }
353 });
354 for (const configInfo of this.configArr) {
355 for (const func of configInfo.modifyFunctions) {
356 // eslint-disable-next-line no-await-in-loop
357 await func(configInfo.chainConfig);
358 }
359 }
360 };
361 this.setUp = async () => {
362 await this.runPlugins();
363 await this.runUserConfig();
364 await this.runWebpackFunctions();
365 await this.runCliOption();
366 return this.configArr;
367 };
368 this.command = command;
369 this.commandArgs = args;
370 this.rootDir = rootDir;
371 /**
372 * config array
373 * {
374 * name,
375 * chainConfig,
376 * webpackFunctions,
377 * }
378 */
379 this.configArr = [];
380 this.modifyConfigFns = [];
381 this.modifyJestConfig = [];
382 this.eventHooks = {}; // lifecycle functions
383 this.internalValue = {}; // internal value shared between plugins
384 this.userConfigRegistration = {};
385 this.cliOptionRegistration = {};
386 this.methodRegistration = {};
387 this.pkg = this.getProjectFile(PKG_FILE);
388 this.userConfig = this.getUserConfig();
389 // custom webpack
390 const webpackPath = this.userConfig.customWebpack ? require.resolve('webpack', { paths: [this.rootDir] }) : 'webpack';
391 this.webpack = require(webpackPath);
392 // register buildin options
393 this.registerCliOption(BUILTIN_CLI_OPTIONS);
394 const builtInPlugins = [...plugins, ...getBuiltInPlugins(this.userConfig)];
395 this.checkPluginValue(builtInPlugins); // check plugins property
396 this.plugins = this.resolvePlugins(builtInPlugins);
397 }
398 async runConfigWebpack(fn, configValue) {
399 for (const webpackConfigInfo of this.configArr) {
400 const userConfigContext = {
401 ..._.pick(this, PLUGIN_CONTEXT_KEY),
402 taskName: webpackConfigInfo.name,
403 };
404 // eslint-disable-next-line no-await-in-loop
405 await fn(webpackConfigInfo.chainConfig, configValue, userConfigContext);
406 }
407 }
408}
409exports.default = Context;