UNPKG

@ssv/aspnet-prerendering

Version:

aspnet SPA Services Node prerendering ESM compatible.

132 lines (125 loc) 6.46 kB
'use strict'; 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;