UNPKG

antwar

Version:

A static site engine built with React and Webpack

241 lines (194 loc) 7.14 kB
"use strict"; var _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; }; 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: _extends({}, 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: _extends({}, 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"); }