1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const path = require("path");
|
4 | const assert = require("assert");
|
5 | const fs = require("fs-extra");
|
6 | const _ = require("lodash");
|
7 | const camelCase = require("camelcase");
|
8 | const log = require("../utils/log");
|
9 | const JSON5 = require("json5");
|
10 | const PKG_FILE = 'package.json';
|
11 | const USER_CONFIG_FILE = 'build.json';
|
12 | const PLUGIN_CONTEXT_KEY = [
|
13 | 'command',
|
14 | 'commandArgs',
|
15 | 'rootDir',
|
16 | 'userConfig',
|
17 | 'pkg',
|
18 | 'webpack',
|
19 | ];
|
20 | const VALIDATION_MAP = {
|
21 | string: 'isString',
|
22 | number: 'isNumber',
|
23 | array: 'isArray',
|
24 | object: 'isObject',
|
25 | boolean: 'isBoolean',
|
26 | };
|
27 | const 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 | ];
|
33 | class 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 |
|
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'));
|
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 |
|
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 |
|
108 | userPlugins[pluginIndex] = pluginInfo;
|
109 | }
|
110 | else {
|
111 |
|
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);
|
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 |
|
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 |
|
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 |
|
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 |
|
312 | validationInfo = await validation(configValue);
|
313 | assert(validationInfo, `${name} did not pass validation, result: ${validationInfo}`);
|
314 | }
|
315 | }
|
316 | if (configInfo.configWebpack) {
|
317 |
|
318 | await this.runConfigWebpack(configInfo.configWebpack, configValue);
|
319 | }
|
320 | }
|
321 | }
|
322 | };
|
323 | this.runCliOption = async () => {
|
324 | for (const cliOpt in this.commandArgs) {
|
325 |
|
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 |
|
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) {
|
342 | this.configArr.forEach(config => {
|
343 | config.modifyFunctions.push(name);
|
344 | });
|
345 | }
|
346 | else {
|
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 |
|
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 |
|
373 |
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 | this.configArr = [];
|
380 | this.modifyConfigFns = [];
|
381 | this.modifyJestConfig = [];
|
382 | this.eventHooks = {};
|
383 | this.internalValue = {};
|
384 | this.userConfigRegistration = {};
|
385 | this.cliOptionRegistration = {};
|
386 | this.methodRegistration = {};
|
387 | this.pkg = this.getProjectFile(PKG_FILE);
|
388 | this.userConfig = this.getUserConfig();
|
389 |
|
390 | const webpackPath = this.userConfig.customWebpack ? require.resolve('webpack', { paths: [this.rootDir] }) : 'webpack';
|
391 | this.webpack = require(webpackPath);
|
392 |
|
393 | this.registerCliOption(BUILTIN_CLI_OPTIONS);
|
394 | const builtInPlugins = [...plugins, ...getBuiltInPlugins(this.userConfig)];
|
395 | this.checkPluginValue(builtInPlugins);
|
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 |
|
405 | await fn(webpackConfigInfo.chainConfig, configValue, userConfigContext);
|
406 | }
|
407 | }
|
408 | }
|
409 | exports.default = Context;
|