@sitecore-jss/sitecore-jss-rendering-host
Version:
This module is provided as a part of Sitecore JavaScript Rendering SDK (JSS). It contains the rendering host implementation.
136 lines (135 loc) • 4.44 kB
JavaScript
import zlib from 'zlib'; // node.js standard lib
// may be used by a "standalone" JSS rendering host / express server.
export const ssrMiddleware = ({ appInvocationInfoResolver, }) => {
if (!appInvocationInfoResolver) {
throw new Error('No AppInvocationInfo resolver was provided for SSR middleware');
}
return (req, res) => {
let callback;
readRequestBodyAsJson(req)
.then((bodyJson) => {
if (!bodyJson) {
throw new Error(`Request body was not JSON: ${req.url}`);
}
const invocationInfo = appInvocationInfoResolver(bodyJson, req, res);
callback = invocationInfo.renderFunctionCallback || getDefaultAppRendererCallback(res);
invocationInfo.renderFunction(callback, ...invocationInfo.renderFunctionArgs);
})
.catch((err) => {
console.error(err);
callback(err, null);
});
};
};
// todo: add hook for modifying html / response before end
/**
* @param {ServerResponse} res
*/
export function getDefaultAppRendererCallback(res) {
const callback = (errorValue, successValue) => {
if (errorValue) {
respondWithError(res, errorValue);
}
else if (typeof successValue !== 'string') {
// Arbitrary object/number/etc - JSON-serialize it
let successValueJson = {};
try {
successValueJson = JSON.stringify(successValue);
}
catch (ex) {
// JSON serialization error - pass it back to http caller.
respondWithError(res, ex);
return;
}
res.setHeader('Content-Type', 'application/json');
res.end(successValueJson);
}
else {
// String - can bypass JSON-serialization altogether
res.setHeader('Content-Type', 'text/plain');
res.end(successValue);
}
};
return callback;
}
/**
* @param {IncomingMessage} request
*/
export function readRequestBodyAsJson(request) {
const dataWriter = { output: Buffer.from('') };
request.on('data', onReadableStreamDataHandler(dataWriter));
return new Promise((resolve, reject) => {
request.on('end', () => {
const contentEncoding = request.headers['content-encoding'];
extractJsonFromStreamData(dataWriter.output, contentEncoding)
.then((json) => resolve(json))
.catch((err) => reject(err));
});
});
}
/**
* @param {ServerResponse} res
* @param {Error} errorValue
*/
export function respondWithError(res, errorValue) {
console.error(errorValue);
res.statusCode = 500;
res.end(JSON.stringify({
errorMessage: errorValue.message || errorValue,
errorDetails: errorValue.stack || null,
}));
}
/**
* @param {object} dataWriter
* @param {Buffer} dataWriter.output
*/
export function onReadableStreamDataHandler(dataWriter) {
return (data) => {
if (Buffer.isBuffer(data)) {
dataWriter.output = Buffer.concat([dataWriter.output, data]); // append raw buffer
}
else {
dataWriter.output = Buffer.concat([dataWriter.output, Buffer.from(data)]);
}
};
}
/**
* @param {Buffer} data
* @param {string} [contentEncoding]
*/
export function extractJsonFromStreamData(data, contentEncoding) {
let responseString;
if (contentEncoding &&
(contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) {
responseString = new Promise((resolve, reject) => {
zlib.unzip(data, (error, result) => {
if (error) {
reject(error);
}
if (result) {
resolve(result.toString('utf-8'));
}
});
});
}
else {
responseString = Promise.resolve(data.toString('utf-8'));
}
return responseString.then(tryParseJson);
}
/**
* @param {string} jsonString
*/
export function tryParseJson(jsonString) {
try {
const json = JSON.parse(jsonString);
// handle non-exception-throwing cases
if (json && typeof json === 'object' && json !== null) {
return json;
}
}
catch (e) {
console.error(`error parsing json string '${jsonString}'`, e);
}
return null;
}