Implement asynchronous React templater
parent
db056bbe2f
commit
37daf9a628
@ -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));
|
||||
};
|
@ -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 = "<!DOCTYPE html>") {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
@ -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);
|
||||
};
|
Loading…
Reference in New Issue