|
| 1 | +import 'es6-promise'; |
| 2 | +import * as url from 'url'; |
| 3 | +import * as path from 'path'; |
| 4 | +import * as domain from 'domain'; |
| 5 | +import { run as domainTaskRun } from 'domain-task/main'; |
| 6 | +import { baseUrl } from 'domain-task/fetch'; |
| 7 | + |
| 8 | +export interface RenderToStringCallback { |
| 9 | + (error: any, result: RenderToStringResult): void; |
| 10 | +} |
| 11 | + |
| 12 | +export interface RenderToStringResult { |
| 13 | + html: string; |
| 14 | + globals: { [key: string]: any }; |
| 15 | +} |
| 16 | + |
| 17 | +export interface BootFunc { |
| 18 | + (params: BootFuncParams): Promise<RenderToStringResult>; |
| 19 | +} |
| 20 | + |
| 21 | +export interface BootFuncParams { |
| 22 | + location: url.Url; |
| 23 | + url: string; |
| 24 | + absoluteUrl: string; |
| 25 | + domainTasks: Promise<any>; |
| 26 | +} |
| 27 | + |
| 28 | +export interface BootModuleInfo { |
| 29 | + moduleName: string; |
| 30 | + exportName?: string; |
| 31 | + webpackConfig?: string; |
| 32 | +} |
| 33 | + |
| 34 | +export function renderToString(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string) { |
| 35 | + findBootFunc(applicationBasePath, bootModule, (findBootFuncError, bootFunc) => { |
| 36 | + if (findBootFuncError) { |
| 37 | + callback(findBootFuncError, null); |
| 38 | + return; |
| 39 | + } |
| 40 | + |
| 41 | + // Prepare a promise that will represent the completion of all domain tasks in this execution context. |
| 42 | + // The boot code will wait for this before performing its final render. |
| 43 | + let domainTaskCompletionPromiseResolve; |
| 44 | + const domainTaskCompletionPromise = new Promise((resolve, reject) => { |
| 45 | + domainTaskCompletionPromiseResolve = resolve; |
| 46 | + }); |
| 47 | + const params: BootFuncParams = { |
| 48 | + location: url.parse(requestPathAndQuery), |
| 49 | + url: requestPathAndQuery, |
| 50 | + absoluteUrl: absoluteRequestUrl, |
| 51 | + domainTasks: domainTaskCompletionPromise |
| 52 | + }; |
| 53 | + |
| 54 | + // Open a new domain that can track all the async tasks involved in the app's execution |
| 55 | + domainTaskRun(/* code to run */ () => { |
| 56 | + // Workaround for Node bug where native Promise continuations lose their domain context |
| 57 | + // (https://github.com/nodejs/node-v0.x-archive/issues/8648) |
| 58 | + // The domain.active property is set by the domain-context module |
| 59 | + bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain['active']); |
| 60 | + |
| 61 | + // Make the base URL available to the 'domain-tasks/fetch' helper within this execution context |
| 62 | + baseUrl(absoluteRequestUrl); |
| 63 | + |
| 64 | + // Actually perform the rendering |
| 65 | + bootFunc(params).then(successResult => { |
| 66 | + callback(null, { html: successResult.html, globals: successResult.globals }); |
| 67 | + }, error => { |
| 68 | + callback(error, null); |
| 69 | + }); |
| 70 | + }, /* completion callback */ errorOrNothing => { |
| 71 | + if (errorOrNothing) { |
| 72 | + callback(errorOrNothing, null); |
| 73 | + } else { |
| 74 | + // There are no more ongoing domain tasks (typically data access operations), so we can resolve |
| 75 | + // the domain tasks promise which notifies the boot code that it can do its final render. |
| 76 | + domainTaskCompletionPromiseResolve(); |
| 77 | + } |
| 78 | + }); |
| 79 | + }); |
| 80 | +} |
| 81 | + |
| 82 | +function findBootModule<T>(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, foundModule: T) => void) { |
| 83 | + const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName); |
| 84 | + if (bootModule.webpackConfig) { |
| 85 | + const webpackConfigFullPath = path.resolve(applicationBasePath, bootModule.webpackConfig); |
| 86 | + |
| 87 | + let aspNetWebpackModule: any; |
| 88 | + try { |
| 89 | + aspNetWebpackModule = require('aspnet-webpack'); |
| 90 | + } catch (ex) { |
| 91 | + callback('To load your boot module via webpack (i.e., if you specify a \'webpackConfig\' option), you must install the \'aspnet-webpack\' NPM package.', null); |
| 92 | + return; |
| 93 | + } |
| 94 | + |
| 95 | + aspNetWebpackModule.loadViaWebpack(webpackConfigFullPath, bootModuleNameFullPath, callback); |
| 96 | + } else { |
| 97 | + callback(null, require(bootModuleNameFullPath)); |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +function findBootFunc(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, bootFunc: BootFunc) => void) { |
| 102 | + // First try to load the module (possibly via Webpack) |
| 103 | + findBootModule<any>(applicationBasePath, bootModule, (findBootModuleError, foundBootModule) => { |
| 104 | + if (findBootModuleError) { |
| 105 | + callback(findBootModuleError, null); |
| 106 | + return; |
| 107 | + } |
| 108 | + |
| 109 | + // Now try to pick out the function they want us to invoke |
| 110 | + let bootFunc: BootFunc; |
| 111 | + if (bootModule.exportName) { |
| 112 | + // Explicitly-named export |
| 113 | + bootFunc = foundBootModule[bootModule.exportName]; |
| 114 | + } else if (typeof foundBootModule !== 'function') { |
| 115 | + // TypeScript-style default export |
| 116 | + bootFunc = foundBootModule.default; |
| 117 | + } else { |
| 118 | + // Native default export |
| 119 | + bootFunc = foundBootModule; |
| 120 | + } |
| 121 | + |
| 122 | + // Validate the result |
| 123 | + if (typeof bootFunc !== 'function') { |
| 124 | + if (bootModule.exportName) { |
| 125 | + callback(`The module at ${ bootModule.moduleName } has no function export named ${ bootModule.exportName }.`, null); |
| 126 | + } else { |
| 127 | + callback(`The module at ${ bootModule.moduleName } does not export a default function, and you have not specified which export to invoke.`, null); |
| 128 | + } |
| 129 | + } else { |
| 130 | + callback(null, bootFunc); |
| 131 | + } |
| 132 | + }); |
| 133 | +} |
| 134 | + |
| 135 | +function bindPromiseContinuationsToDomain(promise: Promise<any>, domainInstance: domain.Domain) { |
| 136 | + const originalThen = promise.then; |
| 137 | + promise.then = function then(resolve, reject) { |
| 138 | + if (typeof resolve === 'function') { |
| 139 | + resolve = domainInstance.bind(resolve); |
| 140 | + } |
| 141 | + |
| 142 | + if (typeof reject === 'function') { |
| 143 | + reject = domainInstance.bind(reject); |
| 144 | + } |
| 145 | + |
| 146 | + return originalThen.call(this, resolve, reject); |
| 147 | + }; |
| 148 | +} |
0 commit comments