UNPKG

7.34 kBJavaScriptView Raw
1"use strict";
2
3var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
4
5const _crypto = require("crypto");
6const _fs = require("fs");
7const _path = require("path");
8
9const async = require("neo-async");
10const cheerio = require("cheerio");
11const ejs = require("ejs");
12const merge = require("webpack-merge");
13const mkdirp = require("mkdirp");
14const tmp = require("tmp");
15const touch = require("touch");
16const rimraf = require("rimraf");
17const webpack = require("webpack");
18
19const defaultAntwar = require("../config/default-antwar");
20const mergeConfiguration = require("../libs/merge-configuration");
21
22const cwd = process.cwd();
23
24module.exports = function writePages({ configurationPaths, environment, pages, outputPath, templates }, finalCb) {
25 const antwarConfiguration = mergeConfiguration(defaultAntwar(), require(configurationPaths.antwar)(environment));
26
27 async.each(pages, ({ page, path }, cb) => processPage({
28 configurationPaths,
29 antwarConfiguration,
30 page,
31 path,
32 outputPath,
33 templates
34 }, cb), finalCb);
35};
36
37function processPage({
38 configurationPaths,
39 antwarConfiguration,
40 page = "",
41 outputPath = "",
42 path = "",
43 templates = {} // page/interactive/interactiveIndex
44}, cb) {
45 const renderPage = require(_path.join(outputPath, "site.js")).renderPage;
46 const console = antwarConfiguration.console;
47
48 renderPage(page, function (err, { html, page, context }) {
49 if (err) {
50 return cb(err);
51 }
52
53 const $ = cheerio.load(html);
54 const components = $(".interactive").map((i, el) => {
55 const $el = $(el);
56 const id = $el.attr("id");
57 const props = $el.data("props");
58
59 return {
60 id,
61 name: `Interactive${i}`,
62 path: _path.join(cwd, id),
63 props: convertToJS(props)
64 };
65 }).get();
66 const jsFiles = [];
67
68 if (components.length) {
69 // XXX: Should this bail early?
70 components.forEach(component => {
71 if (!_fs.existsSync(component.path)) {
72 console.log("Failed to find", component.path);
73 }
74 });
75
76 // Calculate hash based on filename and section so we can check whether
77 // to generate a bundle at all. Use a relative path so the project directory
78 // can be moved around.
79 const filename = calculateMd5(_path.relative(cwd, path).split("/").filter(a => a).slice(0, -1).join("/") + components.map(c => c.id + "=" + c.props).join(""));
80 const interactivePath = _path.join(outputPath, `${filename}.js`);
81
82 // Attach generated file to template
83 jsFiles.push(`/${filename}.js`);
84
85 // If the bundle exists already, skip generating
86 if (!_fs.existsSync(interactivePath)) {
87 const interactiveIndexEntry = ejs.compile(templates.interactiveIndex.file)({
88 components
89 });
90 const entry = ejs.compile(templates.interactive.file)({
91 components
92 });
93
94 // Touch output so that other processes get a clue
95 touch.sync(interactivePath);
96
97 // Write to a temporary files so we can point webpack to that
98 const interactiveEntryTmpFile = tmp.fileSync();
99 const entryTmpFile = tmp.fileSync();
100
101 // XXX: convert to async
102 _fs.writeFileSync(interactiveEntryTmpFile.name, interactiveIndexEntry);
103 _fs.writeFileSync(entryTmpFile.name, entry);
104
105 const interactiveConfig = require(configurationPaths.webpack)("interactive");
106 const webpackConfig = merge(interactiveConfig, {
107 mode: "production",
108 resolve: {
109 modules: [cwd, _path.join(cwd, "node_modules")],
110 alias: generateAliases(components)
111 },
112 resolveLoader: {
113 modules: [cwd, _path.join(cwd, "node_modules")]
114 }
115 });
116
117 const interactiveIndexEntryName = `${filename}-interactive-entry`;
118
119 // Override webpack configuration to process correctly
120 webpackConfig.entry = {
121 [interactiveIndexEntryName]: interactiveEntryTmpFile.name,
122 [filename]: entryTmpFile.name
123 };
124
125 // Merge output to avoid overriding publicPath
126 webpackConfig.output = merge(interactiveConfig.output, {
127 filename: "[name].js",
128 path: outputPath,
129 publicPath: "/",
130 libraryTarget: "umd", // Needed for interactive index exports to work
131 globalObject: "this"
132 });
133
134 return webpack(webpackConfig, (err2, stats) => {
135 if (err2) {
136 return cb(err2);
137 }
138
139 if (stats.hasErrors()) {
140 return cb(stats.toString("errors-only"));
141 }
142
143 const interactiveIndexPath = _path.join(outputPath, interactiveIndexEntryName);
144 const interactiveComponents = require(interactiveIndexPath);
145 const renderErrors = [];
146
147 // Render initial HTML for each component
148 $(".interactive").each((i, el) => {
149 const $el = $(el);
150 const props = $el.data("props");
151
152 try {
153 $el.html(antwarConfiguration.render.interactive({
154 component: interactiveComponents[`Interactive${i}`],
155 props
156 }));
157 } catch (renderErr) {
158 renderErrors.push(renderErr);
159 }
160 });
161
162 if (renderErrors.length) {
163 return cb(renderErrors[0]);
164 }
165
166 rimraf.sync(interactiveIndexPath + ".*");
167
168 // Wrote a bundle, compile through ejs now
169 const data = ejs.compile(templates.page.file)({
170 htmlWebpackPlugin: {
171 options: {
172 context: _extends({}, context, page.file, templates.page, {
173 jsFiles: [...templates.page.jsFiles, ...jsFiles]
174 })
175 }
176 },
177 webpackConfig: {
178 html: $.html()
179 }
180 });
181
182 return writePage({ console, path, data, page }, cb);
183 });
184 }
185 }
186
187 // No need to go through webpack so go only through ejs
188 const data = ejs.compile(templates.page.file)({
189 htmlWebpackPlugin: {
190 options: {
191 context: _extends({}, context, page.file, templates.page, {
192 jsFiles: [...templates.page.jsFiles, ...jsFiles]
193 })
194 }
195 },
196 webpackConfig: {
197 html
198 }
199 });
200
201 return writePage({ console, path, data }, cb);
202 });
203}
204
205function convertToJS(props) {
206 let ret = "";
207
208 Object.keys(props).forEach(prop => {
209 const v = props[prop];
210
211 ret += `${prop}: ${JSON.stringify(v)},`;
212 });
213
214 return `{${ret}}`;
215}
216
217function generateAliases(components) {
218 const ret = {};
219
220 components.forEach(({ id, path }) => {
221 ret[id] = path;
222 });
223
224 return ret;
225}
226
227function writePage({ console, path, data }, cb) {
228 mkdirp(_path.dirname(path), function (err) {
229 if (err) {
230 return cb(err);
231 }
232
233 return _fs.writeFile(path, data, function (err2) {
234 if (err2) {
235 return cb(err2);
236 }
237
238 console.log("Finished writing page", path);
239
240 return cb();
241 });
242 });
243}
244
245function calculateMd5(input) {
246 return _crypto.createHash("md5").update(input).digest("hex");
247}
\No newline at end of file