UNPKG

9.36 kBJavaScriptView Raw
1/* eslint-disable global-require, import/no-dynamic-require, no-nested-ternary */
2
3const wp = require('webpack');
4const R = require('ramda');
5const path = require('path');
6const merge = require('webpack-merge');
7const fsExtra = require('fs-extra');
8const { baseConfigFn, ssrConfig } = require('@rei/front-end-build-configs').profiles.application;
9const logger = require('./lib/logger');
10const lib = require('./lib');
11const devServer = require('./lib/dev-server');
12
13const projectPath = process.cwd();
14
15/**
16 * FEBS entry point. The module is initialized with the
17 * conf entries from bin/febs.
18 *
19 * Passed in at run or test time
20 * @param conf
21 * @param conf.env The environment (dev or prod)
22 * @param conf.logLevel The log level.
23 * @param command The command from commander.
24 * passed in at test-time
25 * @param conf.fs The file system (passed in from unit tests.)
26 */
27module.exports = function init(command, conf = {}) {
28 const febsConfigArg = conf;
29
30 // Get the build environment. (prod | dev)
31 const env = command.name
32 ? command.name() === 'prod'
33 ? 'production'
34 : 'development'
35 : conf.env; // <-- Passed in to set env during unit tests.
36
37 // Allow for in-memory fs for testing.
38 const fs = conf.fs || require('fs');
39
40 if (conf.logLevel) logger.setLogLevel(conf.logLevel);
41
42 // Get local overrides WP conf.
43 const getOverridesConf = (confOverride) => {
44 if (confOverride) return confOverride;
45
46 const overridesConfFile = path.resolve(projectPath, './webpack.overrides.conf.js');
47
48 if (fs.existsSync(overridesConfFile)) {
49 logger.info('Using local webpack.overrides.conf.js...');
50
51 const overridesConf = require(overridesConfFile);
52
53 // Warn if overriding output path
54 if (R.hasPath(['output', 'path'], overridesConf)) {
55 logger.warn('Overriding the output path may break upstream expectations of asset locations.');
56 }
57
58 return overridesConf;
59 }
60
61 return {};
62 };
63
64 const memoize = fn => R.memoizeWith(R.identity, fn);
65
66 const febsConfigPath = path.resolve(projectPath, './febs-config.json');
67 const readJson = memoize(filePath => fsExtra.readJsonSync(filePath));
68 const getFebsConfigJson = readJson.bind(null, febsConfigPath);
69
70 const getFebsConfig = (febsConfig = {}) => {
71 let febsConfigFileJSON;
72 if (fs.existsSync(febsConfigPath)) {
73 febsConfigFileJSON = getFebsConfigJson();
74 } else if (febsConfigArg && (febsConfigArg.output || febsConfigArg.entry)) {
75 febsConfigFileJSON = febsConfigArg;
76 }
77
78 if (febsConfigFileJSON) {
79 logger.info('Using local febs-config.json...');
80 logger.warn('Entries in febs-config.json will override those in webpack.overrides.conf.js.');
81 }
82
83 return R.merge(febsConfig, febsConfigFileJSON);
84 };
85
86 const isSSR = () => getFebsConfig().ssr;
87
88 const getPackageName = () => {
89 const projectPackageJson = path.join(projectPath, 'package.json');
90 return require(projectPackageJson).name;
91 };
92
93 /**
94 * Applies febs-config to the webpack configuration
95 * @param febsConfig The febs-config.json object.
96 * @param wpConf The webpack config.
97 * @returns {object} The webpack config with merged febs-config object.
98 */
99 const febsConfigMerge = (febsConfig, wpConf) => {
100 // Update the output.path to what is in febs-config
101 const newOutputPath = R.hasPath(['output', 'path'], febsConfig)
102 ? path.resolve(projectPath, febsConfig.output.path, getPackageName())
103 : wpConf.output.path;
104
105 const wpConfNewOutputPath = R.mergeDeepRight(wpConf, {
106 output: {
107 path: newOutputPath,
108 },
109 });
110
111 // If febsConfig.entry, replace wpConf.entry with it using fully qualified paths.
112 if (febsConfig.entry) {
113 const { entry } = febsConfig;
114 const newEntries = R.zipObj(
115 R.keys(entry),
116 R.values(entry).map(
117 entryArr => entryArr.map(
118 entryPath => path.resolve(projectPath, entryPath)
119 )
120 )
121 );
122
123 return R.merge(R.dissoc('entry', wpConfNewOutputPath), {
124 entry: newEntries,
125 });
126 }
127
128 return wpConfNewOutputPath;
129 };
130
131 /**
132 * Modifications needed for SSR.
133 *
134 * @param ssr
135 * @param wpConf
136 * @returns {*}
137 */
138 const addVueSSRToWebpackConfig = R.curry((ssr, wpConf) => {
139 if (!ssr) {
140 return wpConf;
141 }
142
143 const pluginsToRemove = ['ManifestPlugin', 'CleanWebpackPlugin'];
144
145 // Remove above plugins during SSR build.
146 const plugins = wpConf.plugins
147 .filter(plugin => !pluginsToRemove.includes(plugin.constructor.name));
148
149 const pluginsFiltered = R.merge(R.dissoc('plugins', wpConf), {
150 plugins,
151 });
152
153 // Add SSR config.
154 return merge.smartStrategy({
155 entry: 'replace',
156 plugins: 'append',
157 })(pluginsFiltered, ssrConfig);
158 });
159
160 /**
161 * Get the webpack config using:
162 * - webpack.base.conf.js
163 * - confOverrides
164 * - febs-config.json
165 *
166 * @param confOverride Optional conf overrides that comes in either from
167 * webpack.overrides.conf or from unit tests.
168 */
169 const getWebpackConfigBase = memoize((confOverride) => {
170 const webpackConfigBase = baseConfigFn(env);
171
172 logger.info(`Building in webpack ${webpackConfigBase.mode} mode...`);
173
174 const configsToMerge = [webpackConfigBase];
175
176 // Config for webpack-merge
177 const wpMergeConf = {
178 entry: 'replace',
179 };
180
181 // Overrides config.
182 configsToMerge.push(getOverridesConf(confOverride));
183
184 const wpConf = merge.smartStrategy(wpMergeConf)(configsToMerge);
185
186 // Ensure febs config makes the final configurable decisions
187 return febsConfigMerge(getFebsConfig(), wpConf);
188 });
189
190 /**
191 * Get the webpack config. This depends upon:
192 * - the base webpack config.
193 * - the webpack server config for optional SSR (ssr property in febs-config)
194 * - any other overrides coming in from febs-config.json.
195 * - optional overrides passed in from unit tests.
196 */
197 const getWebpackConfigFn = ssr => R.compose(
198 addVueSSRToWebpackConfig(ssr),
199 getWebpackConfigBase
200 );
201
202 /**
203 * Configure
204 * @param ssr Whether or not to include SSR webpack build config.
205 * @returns {function} A function that takes overrides argument
206 * and returns the final webpack config.
207 */
208 const getWebpackConfig = ssr => getWebpackConfigFn(ssr);
209
210 /**
211 * Create's compiler instance with appropriate environmental
212 * webpack.conf merged with the webpack.overrides/febs-config/SSR configs.
213 *
214 * @param {WebpackOptions} wpConf The final webpack config object.
215 * @return {Object} The webpack compiler instance.
216 */
217 const createWebpackCompiler = wpConf => wp(wpConf);
218
219 /**
220 * Create the webpack compiler.
221 * @param {boolean} ssr Whether or not to include Vue SSR build.
222 */
223 const createCompiler = ssr => R.compose(
224 createWebpackCompiler,
225 getWebpackConfig(ssr)
226 );
227
228 /**
229 * The webpack run callback.
230 * @param err
231 * @param stats
232 * @returns {{err: *, stats: *, exitCode: number}}
233 */
234 const webpackCompileDone = (err, stats) => {
235 // Log results
236 if (!process.env.FEBS_TEST) {
237 logger.info(stats.toString({
238 chunks: false,
239 colors: true,
240 }));
241 }
242
243 // No errors.
244 if (stats.compilation.errors && stats.compilation.errors.length === 0) {
245 return {
246 err,
247 stats,
248 exitCode: 0,
249 };
250 }
251
252 // If dev mode, do not exit as it will kill watcher.
253 if (env === 'development') {
254 return {
255 err,
256 stats,
257 exitCode: 0,
258 };
259 }
260
261 // Syntax and/or parse errors.
262 // Set error exit code to fail external build tools.
263 process.exitCode = 1;
264 return {
265 err,
266 stats,
267 exitCode: 1,
268 };
269 };
270
271 /**
272 * Runs the webpack compile either via 'run' or 'watch'.
273 * @param ssr Whether or not to run SSR build.
274 * @returns The webpack compiler instance.
275 */
276 const runCompile = (ssr) => {
277 const compilerFn = command.watch ? 'watch' : 'run';
278
279 const compiler = createCompiler(ssr)();
280
281 logger.info(`Compiling ${ssr ? 'SSR build' : 'client-side bundles'}:`);
282
283 if (!ssr) {
284 Object.keys(compiler.options.entry).forEach(e => logger.info(` ✔ ${e}`));
285 }
286
287 logger.info(`📝 Writing ${ssr ? 'vue-ssr-server-bundle.json' : 'assets'} to: ${path.relative(projectPath, compiler.outputPath)}...`);
288
289 if (compilerFn === 'run') {
290 compiler[compilerFn](webpackCompileDone);
291 } else {
292 compiler[compilerFn]({/* watch options */}, webpackCompileDone);
293 }
294 return compiler;
295 };
296
297 /**
298 * Compile function.
299 *
300 * - Creates compiler with config object
301 * - Runs via webpack run/watch methods
302 * - Handles the various WP errors.
303 *
304 * @returns {Object} Webpack compiler instance.
305 */
306 const compile = function compile() {
307 // Create client-side bundle
308 runCompile(false);
309
310 // If SSRing, create vue-ssr-server-bundle.json.
311 if (isSSR()) {
312 runCompile(true);
313 }
314 };
315
316 /**
317 * Start the webpack dev server
318 * @param wds Optionally pass in fake wds (UT only)
319 */
320 const startDevServerFn = wds => R.compose(
321 devServer.bind(null, wds),
322 createCompiler(false),
323 );
324
325 return {
326 compile,
327 createCompiler,
328 webpackCompileDone,
329 startDevServerFn,
330 getWebpackConfig,
331 addVueSSRToWebpackConfig,
332 getWebpackConfigFn,
333 febsConfigMerge,
334 };
335};