From 37daf9a6287be296e1d3d5dd7f35f467a6543ae6 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Sun, 21 Apr 2019 18:30:22 +0200 Subject: [PATCH] Implement asynchronous React templater --- .../clear-require-cache.js | 30 +++++++ src/express-async-react/index.js | 79 +++++++++++++++++++ src/express-async-react/register-babel.js | 20 +++++ 3 files changed, 129 insertions(+) create mode 100644 src/express-async-react/clear-require-cache.js create mode 100644 src/express-async-react/index.js create mode 100644 src/express-async-react/register-babel.js diff --git a/src/express-async-react/clear-require-cache.js b/src/express-async-react/clear-require-cache.js new file mode 100644 index 0000000..218a2fb --- /dev/null +++ b/src/express-async-react/clear-require-cache.js @@ -0,0 +1,30 @@ +"use strict"; + +const escapeStringRegexp = require("escape-string-regexp"); +const assureArray = require("assure-array"); + +let viewPathRegexes = new Map(); + +function generateRegex(paths) { + let pathRegexes = assureArray(paths) + .map((path) => `^${escapeStringRegexp(path)}`) + .join("|"); + + return new RegExp(pathRegexes); +} + +function clearCacheByRegex(regex) { + for (let [key, entry] in Object.entries(regex)) { + if (regex.test(entry.filename) === true) { + delete require.cache[key]; + } + } +} + +module.exports = function clearRequireCache(viewPaths) { + if (!viewPathRegexes.has(viewPaths)) { + viewPathRegexes.set(viewPathRegexes, generateRegex(viewPaths)); + } + + clearCacheByRegex(viewPathRegexes.get(viewPathRegexes)); +}; \ No newline at end of file diff --git a/src/express-async-react/index.js b/src/express-async-react/index.js new file mode 100644 index 0000000..8b7f699 --- /dev/null +++ b/src/express-async-react/index.js @@ -0,0 +1,79 @@ +"use strict"; + +const Promise = require("bluebird"); +const React = require("react"); +const ReactDOMServer = require("react-dom/server"); +const defaultValue = require("default-value"); +const dotty = require("dotty"); + +const registerBabel = require("./register-babel"); +const clearRequireCache = require("./clear-require-cache"); + +/* FILEBUG: Express does not copy symbol-keyed properties from app.locals, but it probably should? */ +// let ExpressReact = Symbol("ExpressReact"); +let ExpressReact = "__EXPRESS_REACT_SETTINGS"; +let LocalsContext = React.createContext({}); + +function componentAtPath(moduleRoot, componentPath) { + if (componentPath == null) { + return moduleRoot; + } else { + return dotty.get(moduleRoot, componentPath); + } +} + +function renderComponent(component, locals, doctype = "") { + let tree = React.createElement(LocalsContext.Provider, {value: locals}, + React.createElement(component, locals) + ); + + try { + /* TODO: Expose internals? Like koa-react */ + return doctype + ReactDOMServer.renderToStaticMarkup(tree); + } catch (err) { + if (/Element type is invalid:/.test(err.message)) { + throw new Error(`Expected a React component, but got '${component}' - maybe you forgot to specify a componentPath?`); + } else { + throw err; + } + } +} + +module.exports = { + Settings: ExpressReact, + LocalsContext: LocalsContext, + createEngine: function createEngine({ prepare } = {}) { + let babelRegistered = false; + + return function asyncRender(filename, options, callback) { + return Promise.try(() => { + let viewPaths = options.settings.views; + + if (!babelRegistered) { + registerBabel(viewPaths); + babelRegistered = true; + } + + let {componentPath} = defaultValue(options[ExpressReact], {}); + + return Promise.try(() => { + let templateFile = require(filename); + let moduleRoot = defaultValue(templateFile.exports, templateFile); + + return Promise.try(() => { + if (prepare != null) { + return prepare(moduleRoot, options); + } + }).then((result) => { + let mergedOptions = Object.assign({}, options, result); + return renderComponent(componentAtPath(moduleRoot, componentPath), mergedOptions); + }); + }).finally(() => { + if (options.settings.env === "development") { + clearRequireCache(viewPaths); + } + }); + }).asCallback(callback); + } + } +}; \ No newline at end of file diff --git a/src/express-async-react/register-babel.js b/src/express-async-react/register-babel.js new file mode 100644 index 0000000..ed1f28f --- /dev/null +++ b/src/express-async-react/register-babel.js @@ -0,0 +1,20 @@ +"use strict"; + +const assureArray = require("assure-array"); +const babelRegister = require("@babel/register"); + +module.exports = function registerBabel(viewPaths, options = {}) { + let babelOptions = Object.assign({ + only: assureArray(viewPaths), + presets: [ + "@babel/preset-react", + ["@babel/preset-env", { + targets: { + node: "current" + } + }] + ] + }, options); + + babelRegister(babelOptions); +}; \ No newline at end of file