UNPKG

9.55 kBJavaScriptView Raw
1import {
2 dirname as toDirname,
3 resolve as resolvePath,
4} from "path";
5
6import {
7 default as Rx,
8 Subject,
9 Observable,
10} from "rx";
11
12import {
13 comp,
14 map,
15 filter,
16 identity,
17} from "transducers-js";
18
19Rx.config.longStackSupport = true;
20
21// Note babel-core/index.js is NOT a ES6 module
22const babel = require("babel-core");
23
24import {
25 default as evaluateAsModule,
26} from "eval-as-module";
27
28import {
29 default as React,
30 Children,
31} from "react";
32
33import {
34 default as webpack,
35} from "webpack";
36
37import {
38 default as entryPropTypeKeyList,
39} from "./entry/entryPropTypeKeyList";
40
41const transformFile = Observable.fromNodeCallback(babel.transformFile);
42
43export const xfFilepath$ToWebpackConfig$ = comp(...[
44 map(filepath$ToBabelResult$),
45 map(babelResult$ToReactElement$),
46 map(reactElement$ToChunkList$),
47 map(chunkList$ToWebpackConfig$),
48]);
49
50/**
51 * @public
52 */
53export function filepath$ToBabelResult$ (filepath$) {
54 return filepath$
55 .selectMany(filepath => {
56 return transformFile(filepath)
57 .map(({code}) => ({filepath, code}));
58 });
59}
60
61/**
62 * @public
63 */
64export function babelResult$ToReactElement$ (babelResult$) {
65 return babelResult$
66 .map(fromBabelCodeToReactElement);
67}
68
69/**
70 * @public
71 */
72export function reactElement$ToChunkList$ (reactElement$) {
73 return reactElement$
74 .selectMany(extractWebpackConfigFilepathList);
75}
76
77/**
78 * @public
79 */
80export function chunkList$ToWebpackConfig$ (chunkList$) {
81 return chunkList$
82 .groupBy(it => it.webpackConfigFilepath)
83 .selectMany(groupedObsToWebpackConfig);
84}
85
86/**
87 * @package
88 */
89export function webpackConfig$ToWebpackCompiler$ (webpackConfig$) {
90 return webpackConfig$
91 .reduce((acc, {webpackConfig}) => {
92 // Your Client config should always be first
93 if (webpackConfig.reacthtmlpackDevServer) {
94 return [webpackConfig].concat(acc);
95 } else {
96 return acc.concat(webpackConfig);
97 }
98 }, [])
99 .first()
100 .tap(webpackConfig => {
101 const notMultipleConfig = 2 > webpackConfig.length;
102 if (notMultipleConfig) {
103 return;
104 }
105 const [{reacthtmlpackDevServer, output: {path: outputPath}}] = webpackConfig;
106 const notInDevServerMode = !reacthtmlpackDevServer;
107 if (notInDevServerMode) {
108 return;
109 }
110 // In devServer command, you have to keep all output.path the same.
111 const theyDontHaveTheSameOutputPath = webpackConfig.some(it => it.output.path !== outputPath);
112 if (theyDontHaveTheSameOutputPath) {
113 const message = `Some of your output.path is different than others in
114all of your webpack config files. This may cause unexpected behaviour when
115using them with webpack-dev-server. The base path serving your assets may
116change according to these commits:
1170. https://github.com/webpack/webpack-dev-server/blob/f6b3bcb4a349540176bacc86df0df8e4109d0e3f/lib/Server.js#L53
1181. https://github.com/webpack/webpack-dev-middleware/blob/42e5778f44939cd45fedd36d7b201b3eeb357630/middleware.js#L140
1192. https://github.com/webpack/webpack/blob/8ff6cb5fedfc487665bb5dd8ecedf5d4ea306b51/lib/MultiCompiler.js#L51-L63
120request goes from webpack-dev-server (0.) > webpack-dev-middleware (1.) > webpack/MultiCompiler (2.)`
121
122 console.warn(message);
123 }
124 })
125 // The webpackCompiler should be an instance of MultiCompiler
126 .map(webpackConfig => webpack(webpackConfig));
127}
128
129/**
130 * @package
131 */
132export function webpackCompiler$ToWebpackStats$ (webpackCompiler$) {
133 return webpackCompiler$
134 .selectMany(runWebpackCompiler)
135 .selectMany(stats =>
136 // See MultiCompiler - MultiStats
137 Observable.fromArray(stats.stats)
138 .map(stats => ({stats, statsJson: stats.toJson()}))
139 );
140}
141
142/**
143 * @public
144 */
145export function webpackConfig$ToChunkList$ (webpackConfig$) {
146 return Observable.of(webpackConfig$)
147 .map(webpackConfig$ToWebpackCompiler$)
148 .map(webpackCompiler$ToWebpackStats$)
149 .selectMany(mergeWebpackStats$ToChunkList$WithWebpackConfig$(webpackConfig$));
150}
151
152/**
153 * @public
154 */
155export function chunkList$ToStaticMarkup$ (chunkList$) {
156 return chunkList$
157 .groupBy(it => it.filepath)
158 .selectMany(groupedObsToStaticMarkup);
159}
160
161/**
162 * @package
163 */
164export function evaluateAsES2015Module (code, filepath) {
165 const cjsModule = evaluateAsModule(code, filepath);
166 if (cjsModule.exports && cjsModule.__esModule) {
167 return cjsModule.exports;
168 } else {
169 return {
170 "default": cjsModule.exports,
171 };
172 };
173}
174
175/**
176 * @private
177 */
178export function fromBabelCodeToReactElement ({filepath, code}) {
179 const ComponentModule = evaluateAsES2015Module(code, filepath);
180 const element = ComponentModule.default;
181 const doctypeHTML = ComponentModule.doctypeHTML || "<!DOCTYPE html>";
182
183 return {
184 filepath,
185 element,
186 doctypeHTML,
187 };
188}
189
190/**
191 * @private
192 */
193export function isEntryType (type) {
194 return entryPropTypeKeyList.every(key => {
195 return type.propTypes && type.propTypes[key];
196 });
197}
198
199/**
200 * @private
201 */
202export function entryWithConfigReducer (children) {
203 const acc = [];
204
205 Children.forEach(children, child => {
206 if (!React.isValidElement(child)) {
207 return;
208 }
209 if (isEntryType(child.type)) {
210 const {
211 chunkName,
212 chunkFilepath,
213 configFilepath,
214 } = child.props;
215
216 acc.push({
217 chunkName,
218 chunkFilepath,
219 configFilepath,
220 });
221 }
222 acc.push(...entryWithConfigReducer(child.props.children));
223 });
224
225 return acc;
226}
227
228/**
229 * @private
230 */
231export function extractWebpackConfigFilepathList ({filepath, element, doctypeHTML}) {
232 const entryWithConfigList = entryWithConfigReducer(element.props.children);
233
234 return Observable.fromArray(entryWithConfigList)
235 .map(({chunkName, chunkFilepath, configFilepath}) => {
236 return {
237 filepath,
238 element,
239 doctypeHTML,
240 chunkName,
241 chunkFilepath,
242 webpackConfigFilepath: resolvePath(toDirname(filepath), configFilepath),
243 };
244 });
245}
246
247/**
248 * @private
249 */
250export function toEntryReducer(acc, item) {
251 const {chunkName, chunkFilepath} = item;
252 if (!acc.entry.hasOwnProperty(chunkName)) {
253 acc.entry[chunkName] = chunkFilepath;
254 acc.chunkList.push(item);
255 }
256 return acc;
257}
258
259/**
260 * @private
261 */
262export function groupedObsToWebpackConfig (groupedObservable) {
263 // http://requirebin.com/?gist=fe2c7d8fe7083d8bcd2d
264 const {key: webpackConfigFilepath} = groupedObservable;
265
266 return groupedObservable.reduce(toEntryReducer, {entry: {}, chunkList: []})
267 .first()
268 .selectMany(function ({entry, chunkList}) {
269 return transformFile(webpackConfigFilepath)
270 .map(({code}) => {
271 const WebpackConfigModule = evaluateAsES2015Module(code, webpackConfigFilepath);
272 const webpackConfig = WebpackConfigModule.default;
273
274 return {
275 webpackConfigFilepath,
276 chunkList,
277 webpackConfig: {
278 ...webpackConfig,
279 entry: {
280 ...webpackConfig.reacthtmlpackExtraEntry,
281 ...entry,
282 },
283 },
284 }
285 });
286 });
287}
288
289/**
290 * @private
291 */
292export function runWebpackCompiler (compiler) {
293 return Observable.fromNodeCallback(::compiler.run)();
294}
295
296/**
297 * @package
298 */
299export function mergeWebpackStats$ToChunkList$WithWebpackConfig$ (webpackConfig$) {
300 return webpackStats$ => {
301 return Observable.zip(
302 webpackStats$,
303 webpackConfig$,
304 ({stats, statsJson}, {chunkList}) => ({chunkList, stats, statsJson})
305 )
306 .selectMany(chunkListWithStats);
307 };
308}
309
310/**
311 * @private
312 */
313export function chunkListWithStats ({chunkList, stats, statsJson}) {
314 return Observable.fromArray(chunkList)
315 .map((it) => {
316 const outputAssetList = [].concat(statsJson.assetsByChunkName[it.chunkName])
317 .map(assetName => {
318 return {
319 rawAsset: stats.compilation.assets[assetName],
320 publicFilepath: `${ statsJson.publicPath }${ assetName }`,
321 };
322 });
323
324 return {
325 ...it,
326 outputAssetList,
327 };
328 });
329}
330
331/**
332 * @private
333 */
334export function entryWithOutputMapper (children, outputAssetListByChunkName) {
335 return Children.map(children, child => {
336 if (!React.isValidElement(child)) {
337 return child;
338 }
339 const {
340 chunkName,
341 children,
342 } = child.props;
343
344 const extraProps = {
345 children: entryWithOutputMapper(children, outputAssetListByChunkName),
346 };
347
348 if (isEntryType(child.type)) {
349 extraProps.outputAssetList = outputAssetListByChunkName[chunkName];
350 }
351
352 return React.cloneElement(child, extraProps);
353 });
354}
355
356/**
357 * @private
358 */
359export function groupedObsToStaticMarkup (groupedObservable) {
360 // http://requirebin.com/?gist=fe2c7d8fe7083d8bcd2d
361 return groupedObservable.reduce((acc, item) => {
362 const {chunkName, outputAssetList} = item;
363
364 acc.outputAssetListByChunkName[chunkName] = outputAssetList;
365 acc.filepath = item.filepath;
366 acc.element = item.element;
367 acc.doctypeHTML = item.doctypeHTML;
368
369 return acc;
370 }, {outputAssetListByChunkName: {}})
371 .first()
372 .map(({outputAssetListByChunkName, filepath, element, doctypeHTML}) => {
373 const clonedElement = React.cloneElement(element, {
374 children: entryWithOutputMapper(element.props.children, outputAssetListByChunkName),
375 });
376
377 const reactHtmlMarkup = React.renderToStaticMarkup(clonedElement);
378 const markup = `${ doctypeHTML }${ reactHtmlMarkup }`;
379
380 return {
381 filepath,
382 markup,
383 };
384 });
385}
386