1 | import {
|
2 | dirname as toDirname,
|
3 | resolve as resolvePath,
|
4 | } from "path";
|
5 |
|
6 | import {
|
7 | default as Rx,
|
8 | Subject,
|
9 | Observable,
|
10 | } from "rx";
|
11 |
|
12 | import {
|
13 | comp,
|
14 | map,
|
15 | filter,
|
16 | identity,
|
17 | } from "transducers-js";
|
18 |
|
19 | Rx.config.longStackSupport = true;
|
20 |
|
21 |
|
22 | const babel = require("babel-core");
|
23 |
|
24 | import {
|
25 | default as evaluateAsModule,
|
26 | } from "eval-as-module";
|
27 |
|
28 | import {
|
29 | default as React,
|
30 | Children,
|
31 | } from "react";
|
32 |
|
33 | import {
|
34 | default as webpack,
|
35 | } from "webpack";
|
36 |
|
37 | import {
|
38 | default as entryPropTypeKeyList,
|
39 | } from "./entry/entryPropTypeKeyList";
|
40 |
|
41 | const transformFile = Observable.fromNodeCallback(babel.transformFile);
|
42 |
|
43 | export const xfFilepath$ToWebpackConfig$ = comp(...[
|
44 | map(filepath$ToBabelResult$),
|
45 | map(babelResult$ToReactElement$),
|
46 | map(reactElement$ToChunkList$),
|
47 | map(chunkList$ToWebpackConfig$),
|
48 | ]);
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | export function filepath$ToBabelResult$ (filepath$) {
|
54 | return filepath$
|
55 | .selectMany(filepath => {
|
56 | return transformFile(filepath)
|
57 | .map(({code}) => ({filepath, code}));
|
58 | });
|
59 | }
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | export function babelResult$ToReactElement$ (babelResult$) {
|
65 | return babelResult$
|
66 | .map(fromBabelCodeToReactElement);
|
67 | }
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | export function reactElement$ToChunkList$ (reactElement$) {
|
73 | return reactElement$
|
74 | .selectMany(extractWebpackConfigFilepathList);
|
75 | }
|
76 |
|
77 |
|
78 |
|
79 |
|
80 | export function chunkList$ToWebpackConfig$ (chunkList$) {
|
81 | return chunkList$
|
82 | .groupBy(it => it.webpackConfigFilepath)
|
83 | .selectMany(groupedObsToWebpackConfig);
|
84 | }
|
85 |
|
86 |
|
87 |
|
88 |
|
89 | export function webpackConfig$ToWebpackCompiler$ (webpackConfig$) {
|
90 | return webpackConfig$
|
91 | .reduce((acc, {webpackConfig}) => {
|
92 |
|
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 |
|
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
|
114 | all of your webpack config files. This may cause unexpected behaviour when
|
115 | using them with webpack-dev-server. The base path serving your assets may
|
116 | change according to these commits:
|
117 | 0. https://github.com/webpack/webpack-dev-server/blob/f6b3bcb4a349540176bacc86df0df8e4109d0e3f/lib/Server.js#L53
|
118 | 1. https://github.com/webpack/webpack-dev-middleware/blob/42e5778f44939cd45fedd36d7b201b3eeb357630/middleware.js#L140
|
119 | 2. https://github.com/webpack/webpack/blob/8ff6cb5fedfc487665bb5dd8ecedf5d4ea306b51/lib/MultiCompiler.js#L51-L63
|
120 | request goes from webpack-dev-server (0.) > webpack-dev-middleware (1.) > webpack/MultiCompiler (2.)`
|
121 |
|
122 | console.warn(message);
|
123 | }
|
124 | })
|
125 |
|
126 | .map(webpackConfig => webpack(webpackConfig));
|
127 | }
|
128 |
|
129 |
|
130 |
|
131 |
|
132 | export function webpackCompiler$ToWebpackStats$ (webpackCompiler$) {
|
133 | return webpackCompiler$
|
134 | .selectMany(runWebpackCompiler)
|
135 | .selectMany(stats =>
|
136 |
|
137 | Observable.fromArray(stats.stats)
|
138 | .map(stats => ({stats, statsJson: stats.toJson()}))
|
139 | );
|
140 | }
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | export 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 |
|
154 |
|
155 | export function chunkList$ToStaticMarkup$ (chunkList$) {
|
156 | return chunkList$
|
157 | .groupBy(it => it.filepath)
|
158 | .selectMany(groupedObsToStaticMarkup);
|
159 | }
|
160 |
|
161 |
|
162 |
|
163 |
|
164 | export 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 |
|
177 |
|
178 | export 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 |
|
192 |
|
193 | export function isEntryType (type) {
|
194 | return entryPropTypeKeyList.every(key => {
|
195 | return type.propTypes && type.propTypes[key];
|
196 | });
|
197 | }
|
198 |
|
199 |
|
200 |
|
201 |
|
202 | export 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 |
|
230 |
|
231 | export 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 |
|
249 |
|
250 | export 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 |
|
261 |
|
262 | export function groupedObsToWebpackConfig (groupedObservable) {
|
263 |
|
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 |
|
291 |
|
292 | export function runWebpackCompiler (compiler) {
|
293 | return Observable.fromNodeCallback(::compiler.run)();
|
294 | }
|
295 |
|
296 |
|
297 |
|
298 |
|
299 | export 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 |
|
312 |
|
313 | export 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 |
|
333 |
|
334 | export 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 |
|
358 |
|
359 | export function groupedObsToStaticMarkup (groupedObservable) {
|
360 |
|
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 |
|