@ssv/aspnet-prerendering
Version:
aspnet SPA Services Node prerendering ESM compatible.
132 lines (125 loc) • 6.46 kB
JavaScript
var url = require('url');
var domain = require('domain');
var main = require('domain-task/main');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var url__namespace = /*#__PURE__*/_interopNamespaceDefault(url);
var domain__namespace = /*#__PURE__*/_interopNamespaceDefault(domain);
/* eslint-disable @typescript-eslint/no-explicit-any */
const defaultTimeoutMilliseconds = 30 * 1000;
// REF: based on https://github.com/aspnet/JavaScriptServices/blob/master/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts
function createServerRenderer(bootFunc) {
const resultFunc = (applicationBasePath, bootModule, absoluteRequestUrl, requestPathAndQuery, customDataParameter, overrideTimeoutMilliseconds, requestPathBase) => {
let renderPromiseResolve;
let renderPromiseReject;
const renderPromise = new Promise((resolve, reject) => {
renderPromiseResolve = resolve;
renderPromiseReject = reject;
});
// Prepare a promise that will represent the completion of all domain tasks in this execution context.
// The boot code will wait for this before performing its final render.
let domainTaskCompletionPromiseResolve;
const domainTaskCompletionPromise = new Promise(resolve => {
domainTaskCompletionPromiseResolve = resolve;
});
const parsedAbsoluteRequestUrl = url__namespace.parse(absoluteRequestUrl);
const params = {
// It's helpful for boot funcs to receive the query as a key-value object, so parse it here
// e.g., react-redux-router requires location.query to be a key-value object for consistency with client-side behaviour
location: url__namespace.parse(requestPathAndQuery, /* parseQueryString */ true),
origin: `${parsedAbsoluteRequestUrl.protocol}//${parsedAbsoluteRequestUrl.host}`,
url: requestPathAndQuery,
baseUrl: `${requestPathBase || ""}/`,
absoluteUrl: absoluteRequestUrl,
domainTasks: domainTaskCompletionPromise,
data: customDataParameter
};
const absoluteBaseUrl = params.origin + params.baseUrl; // Should be same value as page's <base href>
// Open a new domain that can track all the async tasks involved in the app's execution
main.run(/* code to run */ () => {
// Workaround for Node bug where native Promise continuations lose their domain context
// (https://github.com/nodejs/node-v0.x-archive/issues/8648)
// The domain.active property is set by the domain-context module
bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain__namespace.active);
// Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
main.baseUrl(absoluteBaseUrl);
// Begin rendering, and apply a timeout
const bootFuncPromise = bootFunc(params);
if (!bootFuncPromise || typeof bootFuncPromise.then !== "function") {
renderPromiseReject(`Prerendering failed because the boot function in ${bootModule.moduleName} did not return a promise.`);
return;
}
const timeoutMilliseconds = overrideTimeoutMilliseconds || defaultTimeoutMilliseconds; // e.g., pass -1 to override as 'never time out'
const bootFuncPromiseWithTimeout = timeoutMilliseconds > 0
? wrapWithTimeout(bootFuncPromise, timeoutMilliseconds, `Prerendering timed out after ${timeoutMilliseconds}ms because the boot function in '${bootModule.moduleName}' `
+ "returned a promise that did not resolve or reject. Make sure that your boot function always resolves or "
+ "rejects its promise. You can change the timeout value using the 'asp-prerender-timeout' tag helper.")
: bootFuncPromise;
// Actually perform the rendering
bootFuncPromiseWithTimeout.then(successResult => {
renderPromiseResolve(successResult);
}, error => {
renderPromiseReject(error);
});
}, /* completion callback */ /* completion callback */ errorOrNothing => {
if (errorOrNothing) {
renderPromiseReject(errorOrNothing);
}
else {
// There are no more ongoing domain tasks (typically data access operations), so we can resolve
// the domain tasks promise which notifies the boot code that it can do its final render.
domainTaskCompletionPromiseResolve();
}
});
return renderPromise;
};
// Indicate to the prerendering code bundled into Microsoft.AspNetCore.SpaServices that this is a serverside rendering
// function, so it can be invoked directly. This flag exists only so that, in its absence, we can run some different
// backward-compatibility logic.
resultFunc.isServerRenderer = true;
return resultFunc;
}
function wrapWithTimeout(promise, timeoutMilliseconds, timeoutRejectionValue) {
return new Promise((resolve, reject) => {
const timeoutTimer = setTimeout(() => {
reject(timeoutRejectionValue);
}, timeoutMilliseconds);
promise.then(resolvedValue => {
clearTimeout(timeoutTimer);
resolve(resolvedValue);
}, rejectedValue => {
clearTimeout(timeoutTimer);
reject(rejectedValue);
});
});
}
function bindPromiseContinuationsToDomain(promise, domainInstance) {
const originalThen = promise.then;
promise.then = (function then(resolve, reject) {
if (typeof resolve === "function") {
resolve = domainInstance.bind(resolve);
}
if (typeof reject === "function") {
reject = domainInstance.bind(reject);
}
return originalThen.call(this, resolve, reject);
});
}
const VERSION = "0.0.0-PLACEHOLDER";
exports.VERSION = VERSION;
exports.createServerRenderer = createServerRenderer;
;