UNPKG

6.3 kBJavaScriptView Raw
1import { existsSync, readFileSync } from 'fs';
2import { join, resolve } from 'path';
3import assert from 'assert';
4import stripJsonComments from 'strip-json-comments';
5import didyoumean from 'didyoumean';
6import chalk from 'chalk';
7import isEqual from 'lodash.isequal';
8import isPlainObject from 'is-plain-object';
9import { clearConsole } from '../reactDevUtils';
10import { watch, unwatch } from './watch';
11import getPlugins from './getPlugins';
12
13const debug = require('debug')('af-webpack:getUserConfig');
14
15const plugins = getPlugins();
16const pluginNames = plugins.map(p => p.name);
17const pluginsMapByName = plugins.reduce((memo, p) => {
18 memo[p.name] = p;
19 return memo;
20}, {});
21
22let devServer = null;
23const USER_CONFIGS = 'USER_CONFIGS';
24
25function throwError(msg) {
26 printError(msg);
27 throw new Error(msg);
28}
29
30function printError(messages) {
31 if (devServer) {
32 devServer.sockWrite(
33 devServer.sockets,
34 'errors',
35 typeof messages === 'string' ? [messages] : messages,
36 );
37 }
38}
39
40function reload() {
41 devServer.sockWrite(devServer.sockets, 'content-changed');
42}
43
44function restart(why) {
45 clearConsole();
46 console.log(chalk.green(`Since ${why}, try to restart the server`));
47 unwatch();
48 devServer.close();
49 process.send({ type: 'RESTART' });
50}
51
52function merge(oldObj, newObj) {
53 for (const key in newObj) {
54 if (Array.isArray(newObj[key]) && Array.isArray(oldObj[key])) {
55 oldObj[key] = oldObj[key].concat(newObj[key]);
56 } else if (isPlainObject(newObj[key]) && isPlainObject(oldObj[key])) {
57 oldObj[key] = Object.assign(oldObj[key], newObj[key]);
58 } else {
59 oldObj[key] = newObj[key];
60 }
61 }
62}
63
64function replaceNpmVariables(value, pkg) {
65 if (typeof value === 'string') {
66 return value
67 .replace('$npm_package_name', pkg.name)
68 .replace('$npm_package_version', pkg.version);
69 } else {
70 return value;
71 }
72}
73
74export default function getUserConfig(opts = {}) {
75 const {
76 cwd = process.cwd(),
77 configFile = '.webpackrc',
78 disabledConfigs = [],
79 preprocessor,
80 } = opts;
81
82 // TODO: 支持数组的形式?
83
84 // Read config from configFile and `${configFile}.js`
85 const rcFile = resolve(cwd, configFile);
86 const jsRCFile = resolve(cwd, `${configFile}.js`);
87
88 assert(
89 !(existsSync(rcFile) && existsSync(jsRCFile)),
90 `${configFile} file and ${configFile}.js file can not exist at the same time.`,
91 );
92
93 let config = {};
94 if (existsSync(rcFile)) {
95 config = JSON.parse(stripJsonComments(readFileSync(rcFile, 'utf-8')));
96 }
97 if (existsSync(jsRCFile)) {
98 // no cache
99 delete require.cache[jsRCFile];
100 config = require(jsRCFile); // eslint-disable-line
101 if (config.default) {
102 config = config.default;
103 }
104 }
105 if (typeof preprocessor === 'function') {
106 config = preprocessor(config);
107 }
108
109 // Context for validate function
110 const context = {
111 cwd,
112 };
113
114 // Validate
115 let errorMsg = null;
116 Object.keys(config).forEach(key => {
117 // 禁用项
118 if (disabledConfigs.includes(key)) {
119 errorMsg = `Configuration item ${key} is disabled, please remove it.`;
120 }
121 // 非法的项
122 if (!pluginNames.includes(key)) {
123 const guess = didyoumean(key, pluginNames);
124 const affix = guess ? `do you meen ${guess} ?` : 'please remove it.';
125 errorMsg = `Configuration item ${key} is not valid, ${affix}`;
126 } else {
127 // run config plugin's validate
128 const plugin = pluginsMapByName[key];
129 if (plugin.validate) {
130 try {
131 plugin.validate.call(context, config[key]);
132 } catch (e) {
133 errorMsg = e.message;
134 }
135 }
136 }
137 });
138
139 // 确保不管校验是否出错,下次 watch 判断时能拿到正确的值
140 if (errorMsg) {
141 if (/* from watch */ opts.setConfig) {
142 opts.setConfig(config);
143 }
144 throwError(errorMsg);
145 }
146
147 // Merge config with current env
148 if (config.env) {
149 if (config.env[process.env.NODE_ENV]) {
150 merge(config, config.env[process.env.NODE_ENV]);
151 }
152 delete config.env;
153 }
154
155 // Replace npm variables
156 const pkgFile = resolve(cwd, 'package.json');
157 if (Object.keys(config).length && existsSync(pkgFile)) {
158 const pkg = JSON.parse(readFileSync(pkgFile, 'utf-8'));
159 config = Object.keys(config).reduce((memo, key) => {
160 memo[key] = replaceNpmVariables(config[key], pkg);
161 return memo;
162 }, {});
163 }
164
165 let configFailed = false;
166 function watchConfigsAndRun(_devServer, watchOpts = {}) {
167 devServer = _devServer;
168
169 const watcher = watchConfigs(opts);
170 if (watcher) {
171 watcher.on('all', () => {
172 try {
173 if (watchOpts.beforeChange) {
174 watchOpts.beforeChange();
175 }
176
177 const { config: newConfig } = getUserConfig({
178 ...opts,
179 setConfig(newConfig) {
180 config = newConfig;
181 },
182 });
183
184 // 从失败中恢复过来,需要 reload 一次
185 if (configFailed) {
186 configFailed = false;
187 reload();
188 }
189
190 // 比较,然后执行 onChange
191 for (const plugin of plugins) {
192 const { name, onChange } = plugin;
193
194 if (!isEqual(newConfig[name], config[name])) {
195 debug(
196 `Config ${name} changed, from ${JSON.stringify(
197 config[name],
198 )} to ${JSON.stringify(newConfig[name])}`,
199 );
200 (onChange || restart.bind(null, `${name} changed`)).call(null, {
201 name,
202 val: config[name],
203 newVal: newConfig[name],
204 config,
205 newConfig,
206 });
207 }
208 }
209 } catch (e) {
210 configFailed = true;
211 console.error(chalk.red(`Watch handler failed, since ${e.message}`));
212 console.error(e);
213 }
214 });
215 }
216 }
217
218 debug(`UserConfig: ${JSON.stringify(config)}`);
219
220 return { config, watch: watchConfigsAndRun };
221}
222
223export function watchConfigs(opts = {}) {
224 const { cwd = process.cwd(), configFile = '.webpackrc' } = opts;
225
226 const rcFile = resolve(cwd, configFile);
227 const jsRCFile = resolve(cwd, `${configFile}.js`);
228
229 return watch(USER_CONFIGS, [rcFile, jsRCFile]);
230}
231
232export function unwatchConfigs() {
233 unwatch(USER_CONFIGS);
234}