UNPKG

10 kBJavaScriptView Raw
1import {
2 extname as extractExtname,
3 dirname as extractDirname,
4 basename as extractBasename,
5 relative as relativePathOf,
6 resolve as resolvePath,
7} from "path";
8
9import {
10 writeFile as nodeWriteFile,
11} from "fs";
12
13import {
14 Glob,
15} from "glob";
16
17import {
18 default as glob2base,
19} from "glob2base";
20
21import {
22 default as Rx,
23 Observable,
24} from "rx";
25
26import {
27 comp,
28 map,
29 filter,
30 identity,
31} from "transducers-js";
32
33import {
34 default as WebpackDevServer,
35} from "webpack-dev-server";
36
37import {
38 default as webpack,
39} from "webpack";
40
41import {
42 default as _,
43} from "lodash";
44
45import {
46 default as nodeMkdirp,
47} from "mkdirp";
48
49import {
50 xfFilepath$ToWebpackConfig$,
51
52 filepath$ToBabelResult$,
53 babelResult$ToReactElement$,
54 reactElement$ToChunkList$,
55 chunkList$ToWebpackConfig$,
56 webpackConfig$ToWebpackCompiler$,
57 webpackConfig$ToChunkList$,
58 chunkList$ToStaticMarkup$,
59 // @package
60 mergeWebpackStats$ToChunkList$WithWebpackConfig$,
61} from "./core";
62
63const mkdirp = Observable.fromNodeCallback(nodeMkdirp);
64const writeFile = Observable.fromNodeCallback(nodeWriteFile);
65
66/**
67 * @public
68 */
69export function buildToDir (destDir, srcPatternList) {
70 const {filepath$, relativePathByMatch$} = getMatchResult(srcPatternList);
71
72 const xf = comp(...[
73 xfFilepath$ToWebpackConfig$,
74 map(webpackConfig$ToChunkList$),
75 map(chunkList$ToStaticMarkup$),
76 map(createWriteStaticMarkup$ToDestDir(relativePathByMatch$, destDir)),
77 ]);
78
79 Observable.of(filepath$)
80 .transduce(xf)
81 .concatAll()
82 .subscribe(
83 ::console.log,
84 ::console.error,
85 () => { console.log("done!"); }
86 );
87}
88
89/**
90 * @public
91 */
92export function watchAndBuildToDir (destDir, srcPatternList) {
93 const {filepath$, relativePathByMatch$} = getMatchResult(srcPatternList);
94
95 const xf = comp(...[
96 map(chunkList$ToStaticMarkup$),
97 map(createWriteStaticMarkup$ToDestDir(relativePathByMatch$, destDir)),
98 ]);
99
100 Observable.of(filepath$)
101 .transduce(xfFilepath$ToWebpackConfig$)
102 .selectMany(webpackConfig$ => {
103 // Why selectMany? Because watch could be repeative.
104 // Instead of wrapping one value, now a series of values are emitted.
105 return Observable.of(webpackConfig$)
106 .map(webpackConfig$ToWebpackCompiler$)
107 .combineLatest(
108 webpackConfig$.count(),
109 (webpackCompiler$, count) => ({webpackCompiler$, count})
110 )
111 .selectMany(({webpackCompiler$, count}) => {
112 return Observable.of(webpackCompiler$)
113 .map(watchMultiCompiler$ToChildrenStats$)
114 .selectMany(identity)
115 .scan((acc, it) => {
116 acc = [...acc];
117 const {index, ...rest} = it;
118
119 acc[index] = rest;
120
121 return acc;
122 }, new Array(count))
123 .takeWhile(acc => acc.every(identity))
124 .map(acc => Observable.fromArray(acc));
125 })
126 .map(mergeWebpackStats$ToChunkList$WithWebpackConfig$(webpackConfig$))
127 })
128 .transduce(xf)
129 .concatAll()
130 .subscribe(
131 ::console.log,
132 ::console.error,
133 () => { console.log("done!"); }
134 );
135}
136
137/**
138 * @public
139 */
140export function devServer (relativeDevServerConfigFilepath, destDir, srcPatternList) {
141 const devServerConfigFilepath = resolvePath(process.cwd(), relativeDevServerConfigFilepath);
142
143 const {filepath$, relativePathByMatch$} = getMatchResult(srcPatternList);
144
145 const xf = comp(...[
146 map(chunkList$ToStaticMarkup$),
147 map(createWriteStaticMarkup$ToDestDir(relativePathByMatch$, destDir)),
148 ]);
149
150 Observable.of(filepath$)
151 .transduce(xfFilepath$ToWebpackConfig$)
152 .selectMany(webpackConfig$ => {
153 // Why selectMany? Because devServer is watching and could be repeative.
154 // Instead of wrapping one value, now a series of values are emitted.
155 return Observable.of(webpackConfig$)
156 .map(addDevServerToEntryMapperCreator(devServerConfigFilepath))
157 .map(webpackConfig$ToWebpackCompiler$)
158 .combineLatest(
159 webpackConfig$.count(),
160 (webpackCompiler$, count) => ({webpackCompiler$, count})
161 )
162 .selectMany(({webpackCompiler$, count}) => {
163 return Observable.of(webpackCompiler$)
164 .selectMany(startDevServerWithMultiCompiler$ToChildrenStats$MapperCreator(devServerConfigFilepath));
165 })
166 .map(mergeWebpackStats$ToChunkList$WithWebpackConfig$(webpackConfig$))
167 })
168 .transduce(xf)
169 .concatAll()
170 .subscribe(
171 ::console.log,
172 ::console.error,
173 () => { console.log("done!"); }
174 );
175}
176
177/**
178 * @private
179 */
180export function getMatchResult (srcPatternList) {
181 const matchResult$ = Observable.fromArray(srcPatternList)
182 .selectMany(srcPatternToMatchResult)
183 .reduce(matchResultToMatchesFilepathReducer, {matches: [], relativePathByMatch: {}})
184 .first();
185
186 const filepath$ = matchResult$
187 .selectMany(({matches}) => Observable.fromArray(matches));
188
189 const relativePathByMatch$ = matchResult$
190 .map(({relativePathByMatch}) => relativePathByMatch);
191
192 return {
193 filepath$,
194 relativePathByMatch$,
195 };
196}
197
198/**
199 * @private
200 */
201export function srcPatternToMatchResult (srcPattern) {
202 const globber = new Glob(srcPattern);
203 const base = glob2base(globber);
204
205 return Rx.Observable.create(function (observer) {
206 function callback (matches) {
207 observer.onNext({
208 base,
209 matches,
210 });
211 observer.onCompleted();
212 };
213
214 globber.once("end", callback);
215
216 return globber.removeListener.bind(globber, "end", callback);
217 });
218}
219
220/**
221 * @private
222 */
223export function matchResultToMatchesFilepathReducer (acc, {base, matches}) {
224 acc.matches.push(...matches);
225 matches.forEach(match => {
226
227 const filepath = replaceWithHtmlExt(match);
228 acc.relativePathByMatch[match] = relativePathOf(base, filepath);
229 });
230
231 return acc;
232}
233
234/**
235 * @private
236 */
237export function replaceWithHtmlExt (filepath) {
238 const dirpath = extractDirname(filepath);
239
240 let basename = extractBasename(filepath);
241
242 while (true) {
243 const ext = extractExtname(basename);
244 if (ext) {
245 basename = extractBasename(basename, ext);
246 } else {
247 return resolvePath(dirpath, `${ basename }.html`);
248 }
249 }
250}
251
252/**
253 * @private
254 */
255export function createWriteStaticMarkup$ToDestDir (relativePathByMatch$, destDir) {
256 return staticMarkup$ => {
257 return staticMarkup$
258 .combineLatest(relativePathByMatch$,
259 ({filepath, markup}, relativePathByMatch) => {
260 const relativePath = relativePathByMatch[filepath];
261
262 return {
263 filepath: resolvePath(destDir, relativePath),
264 markup,
265 };
266 }
267 )
268 .selectMany(({filepath, markup}) => {
269 return mkdirp(extractDirname(filepath))
270 .selectMany(() => {
271 return writeFile(filepath, markup);
272 });
273 });
274 };
275}
276
277/**
278 * @private
279 */
280export function watchMultiCompiler$ToChildrenStats$ (webpackCompiler$) {
281 // return Observable.create(observer => {
282 // function callback (err, stats) {
283 // if (err) {
284 // observer.onError(err);
285 // } else {
286 // observer.onNext(stats);
287 // }
288 // }
289 // const watcher = webpackCompiler.watch({}, callback);
290 // return watcher.close.bind(watcher);
291 // });
292 // We cannot use the above code because we want every results in a sub compiler.
293 // This is an issue of implementation details of webpack
294 return webpackCompiler$
295 .selectMany(webpackCompiler => Observable.fromArray(webpackCompiler.compilers))
296 .selectMany((compiler, index) => {
297
298 return Observable.create(observer => {
299 function callback (err, stats) {
300 if (err) {
301 observer.onError(err);
302 } else {
303 observer.onNext({
304 index,
305 stats,
306 statsJson: stats.toJson(),
307 });
308 }
309 }
310
311 const watcher = compiler.watch({}, callback);
312
313 return watcher.close.bind(watcher);
314 });
315 });
316}
317
318/**
319 * @private
320 */
321export function addDevServerToEntryMapperCreator (devServerConfigFilepath) {
322 return (webpackConfig$) => {
323 return webpackConfig$
324 .map(it => {
325 if (it.webpackConfigFilepath === devServerConfigFilepath) {
326 const {webpackConfig} = it;
327 const {devServer} = webpackConfig;
328
329 const inlineDevServerChunkList = [
330 require.resolve("webpack-dev-server/client/") + `?http://${ devServer.host }:${ devServer.port }`,
331 "webpack/hot/dev-server",
332 ];
333
334 return {
335 ...it,
336 webpackConfig: {
337 ...webpackConfig,
338 reacthtmlpackDevServer: true,
339 entry: _.mapValues(webpackConfig.entry, filepathList =>
340 inlineDevServerChunkList.concat(filepathList)
341 ),
342 plugins: [
343 ...webpackConfig.plugins,
344 new webpack.HotModuleReplacementPlugin(),
345 ],
346 },
347 };
348 } else {
349 return it;
350 }
351 });
352 };
353}
354
355/**
356 * @private
357 */
358export function startDevServerWithMultiCompiler$ToChildrenStats$MapperCreator(devServerConfigFilepath) {
359 return (webpackCompiler$) => {
360 return webpackCompiler$
361 .selectMany(webpackCompiler => {
362 const [devServer] = webpackCompiler
363 .compilers
364 .filter(compiler => compiler.options.reacthtmlpackDevServer)
365 .map(compiler => compiler.options.devServer);
366 const wDS = new WebpackDevServer(webpackCompiler, devServer);
367
368 return Observable.create(observer => {
369 wDS.listen(devServer.port, devServer.host, (err) => {
370 if (err) {
371 observer.onError(err);
372 }
373 });
374
375 webpackCompiler.plugin("done", multiStats => {
376 observer.onNext(
377 Observable.fromArray(multiStats.stats)
378 .map(stats => ({stats, statsJson: stats.toJson()}))
379 );
380 });
381 });
382 });
383 };
384}