antwar
Version:
A static site engine built with React and Webpack
285 lines (239 loc) • 7.31 kB
JavaScript
const _crypto = require("crypto");
const _fs = require("fs");
const _path = require("path");
const async = require("neo-async");
const cheerio = require("cheerio");
const ejs = require("ejs");
const merge = require("webpack-merge");
const mkdirp = require("mkdirp");
const tmp = require("tmp");
const touch = require("touch");
const rimraf = require("rimraf");
const webpack = require("webpack");
const defaultAntwar = require("../config/default-antwar");
const mergeConfiguration = require("deepmerge");
const cwd = process.cwd();
module.exports = function writePages(
{ configurationPaths, environment, pages, outputPath, templates },
finalCb
) {
const antwarConfiguration = mergeConfiguration(
defaultAntwar(),
require(configurationPaths.antwar)(environment)
);
async.each(
pages,
({ page, path }, cb) =>
processPage(
{
configurationPaths,
antwarConfiguration,
page,
path,
outputPath,
templates,
},
cb
),
finalCb
);
};
function processPage(
{
configurationPaths,
antwarConfiguration,
page = "",
outputPath = "",
path = "",
templates = {}, // page/interactive/interactiveIndex
},
cb
) {
const renderPage = require(_path.join(outputPath, "site.js")).renderPage;
const console = antwarConfiguration.console;
renderPage(page, function(err, { html, page, context } = {}) {
if (err) {
return cb(err);
}
const $ = cheerio.load(html);
const components = $(".interactive")
.map((i, el) => {
const $el = $(el);
const id = $el.attr("id");
const props = $el.data("props");
return {
id,
name: `Interactive${i}`,
path: _path.join(cwd, id),
props: convertToJS(props),
};
})
.get();
const jsFiles = [];
if (components.length) {
// XXX: Should this bail early?
components.forEach(component => {
if (!_fs.existsSync(component.path)) {
console.log("Failed to find", component.path);
}
});
// Calculate hash based on filename and section to avoid ENAMETOOLONG.
// TODO: See if this check could be skipped entirely.
const filename = calculateMd5(
_path
.relative(cwd, path)
.split("/")
.filter(a => a)
.slice(0, -1)
.join("/") + components.map(c => c.id + "=" + c.props).join("")
);
const interactivePath = _path.join(outputPath, `${filename}.js`);
// Attach generated file to template
jsFiles.push(`/${filename}.js`);
const interactiveIndexEntry = ejs.compile(
templates.interactiveIndex.file
)({
components,
});
const entry = ejs.compile(templates.interactive.file)({
components,
});
// Touch output so that other processes get a clue
touch.sync(interactivePath);
// Write to a temporary files so we can point webpack to that
const interactiveEntryTmpFile = tmp.fileSync();
const entryTmpFile = tmp.fileSync();
// XXX: convert to async
_fs.writeFileSync(interactiveEntryTmpFile.name, interactiveIndexEntry);
_fs.writeFileSync(entryTmpFile.name, entry);
const interactiveConfig = require(configurationPaths.webpack)(
"interactive"
);
const webpackConfig = merge(interactiveConfig, {
mode: "production",
resolve: {
modules: [cwd, _path.join(cwd, "node_modules")],
alias: generateAliases(components),
},
resolveLoader: {
modules: [cwd, _path.join(cwd, "node_modules")],
},
});
const interactiveIndexEntryName = `${filename}-interactive-entry`;
// Override webpack configuration to process correctly
webpackConfig.entry = {
[interactiveIndexEntryName]: interactiveEntryTmpFile.name,
[filename]: entryTmpFile.name,
};
// Merge output to avoid overriding publicPath
webpackConfig.output = merge(interactiveConfig.output, {
filename: "[name].js",
path: outputPath,
publicPath: "/",
libraryTarget: "umd", // Needed for interactive index exports to work
globalObject: "this",
});
return webpack(webpackConfig, (err2, stats) => {
if (err2) {
return cb(err2);
}
if (stats.hasErrors()) {
return cb(stats.toString("errors-only"));
}
const assets = stats.compilation.assets;
const cssFiles = Object.keys(assets)
.map(asset => {
if (_path.extname(asset) === ".css") {
return assets[asset].existsAt;
}
return null;
})
.filter(a => a)
.map(cssFile => "/" + _path.basename(cssFile));
const interactiveIndexPath = _path.join(
outputPath,
interactiveIndexEntryName
);
const interactiveComponents = require(interactiveIndexPath);
const renderErrors = [];
// Render initial HTML for each component
$(".interactive").each((i, el) => {
const $el = $(el);
const props = $el.data("props");
try {
$el.html(
antwarConfiguration.render.interactive({
component: interactiveComponents[`Interactive${i}`],
props,
})
);
} catch (renderErr) {
renderErrors.push(renderErr);
}
});
if (renderErrors.length) {
return cb(renderErrors[0]);
}
rimraf.sync(interactiveIndexPath + ".*");
// Wrote a bundle, compile through ejs now
const data = ejs.compile(templates.page.file)({
context: {
...context,
...page.file,
...templates.page,
cssFiles: [...templates.page.cssFiles, ...cssFiles],
jsFiles: [...templates.page.jsFiles, ...jsFiles],
html: $.html(),
},
});
return writePage({ console, path, data, page }, cb);
});
}
// No need to go through webpack so go only through ejs
const data = ejs.compile(templates.page.file)({
context: {
...context,
...page.file,
...templates.page,
jsFiles: [...templates.page.jsFiles, ...jsFiles],
html,
},
});
return writePage({ console, path, data }, cb);
});
}
function convertToJS(props) {
let ret = "";
Object.keys(props).forEach(prop => {
const v = props[prop];
ret += `${prop}: ${JSON.stringify(v)},`;
});
return `{${ret}}`;
}
function generateAliases(components) {
const ret = {};
components.forEach(({ id, path }) => {
ret[id] = path;
});
return ret;
}
function writePage({ console, path, data }, cb) {
mkdirp(_path.dirname(path), function(err) {
if (err) {
return cb(err);
}
return _fs.writeFile(path, data, function(err2) {
if (err2) {
return cb(err2);
}
console.log("Finished writing page", path);
return cb();
});
});
}
function calculateMd5(input) {
return _crypto
.createHash("md5")
.update(input)
.digest("hex");
}