|
| 1 | +// ----------------------------------------------------------------------------------------- |
| 2 | +// Prepare the Node environment to support loading .jsx/.ts/.tsx files without needing precompilation, |
| 3 | +// since that's such a common scenario. In the future, this might become a config option. |
| 4 | +// This is bundled in with the actual prerendering logic below just to simplify the initialization |
| 5 | +// logic (we can't have cross-file imports, because these files don't exist on disk until the |
| 6 | +// StringAsTempFile utility puts them there temporarily). |
| 7 | + |
| 8 | +// TODO: Consider some general method for checking if you have all the necessary NPM modules installed, |
| 9 | +// and if not, giving an error that tells you what command to execute to install the missing ones. |
| 10 | +var fs = require('fs'); |
| 11 | +var ts = require('ntypescript'); |
| 12 | +var babelCore = require('babel-core'); |
| 13 | +var resolveBabelRc = require('babel-loader/lib/resolve-rc'); // If this ever breaks, we can easily scan up the directory hierarchy ourselves |
| 14 | +var origJsLoader = require.extensions['.js']; |
| 15 | + |
| 16 | +function resolveBabelOptions(relativeToFilename) { |
| 17 | + var babelRcText = resolveBabelRc(relativeToFilename); |
| 18 | + return babelRcText ? JSON.parse(babelRcText) : {}; |
| 19 | +} |
| 20 | + |
| 21 | +function loadViaTypeScript(module, filename) { |
| 22 | + // First perform a minimal transpilation from TS code to ES2015. This is very fast (doesn't involve type checking) |
| 23 | + // and is unlikely to need any special compiler options |
| 24 | + var src = fs.readFileSync(filename, 'utf8'); |
| 25 | + var compilerOptions = { jsx: ts.JsxEmit.Preserve, module: ts.ModuleKind.ES2015, target: ts.ScriptTarget.ES6 }; |
| 26 | + var es6Code = ts.transpile(src, compilerOptions, 'test.tsx', /* diagnostics */ []); |
| 27 | + |
| 28 | + // Second, process the ES2015 via Babel. We have to do this (instead of going directly from TS to ES5) because |
| 29 | + // TypeScript's ES5 output isn't exactly compatible with Node-style CommonJS modules. The main issue is with |
| 30 | + // resolving default exports - https://github.com/Microsoft/TypeScript/issues/2719 |
| 31 | + var es5Code = babelCore.transform(es6Code, resolveBabelOptions(filename)).code; |
| 32 | + return module._compile(es5Code, filename); |
| 33 | +} |
| 34 | + |
| 35 | +function loadViaBabel(module, filename) { |
| 36 | + // Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are. |
| 37 | + // The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict |
| 38 | + // mode when the developer didn't expect that (e.g., current versions of underscore.js can't be loaded in strict mode). |
| 39 | + var useBabel = filename.indexOf('node_modules') < 0; |
| 40 | + if (useBabel) { |
| 41 | + var transformedFile = babelCore.transformFileSync(filename, resolveBabelOptions(filename)); |
| 42 | + return module._compile(transformedFile.code, filename); |
| 43 | + } else { |
| 44 | + return origJsLoader.apply(this, arguments); |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +function register() { |
| 49 | + require.extensions['.js'] = loadViaBabel; |
| 50 | + require.extensions['.jsx'] = loadViaBabel; |
| 51 | + require.extensions['.ts'] = loadViaTypeScript; |
| 52 | + require.extensions['.tsx'] = loadViaTypeScript; |
| 53 | +}; |
| 54 | + |
| 55 | +register(); |
| 56 | + |
| 57 | +// ----------------------------------------------------------------------------------------- |
| 58 | +// Rendering |
| 59 | + |
| 60 | +var url = require('url'); |
| 61 | +var path = require('path'); |
| 62 | +var domain = require('domain'); |
| 63 | +var domainTask = require('domain-task'); |
| 64 | +var baseUrl = require('domain-task/fetch').baseUrl; |
| 65 | + |
| 66 | +function findBootFunc(bootModulePath, bootModuleExport) { |
| 67 | + var resolvedPath = path.resolve(process.cwd(), bootModulePath); |
| 68 | + var bootFunc = require(resolvedPath); |
| 69 | + if (bootModuleExport) { |
| 70 | + bootFunc = bootFunc[bootModuleExport]; |
| 71 | + } else if (typeof bootFunc !== 'function') { |
| 72 | + bootFunc = bootFunc.default; // TypeScript sometimes uses this name for default exports |
| 73 | + } |
| 74 | + if (typeof bootFunc !== 'function') { |
| 75 | + if (bootModuleExport) { |
| 76 | + throw new Error('The module at ' + bootModulePath + ' has no function export named ' + bootModuleExport + '.'); |
| 77 | + } else { |
| 78 | + throw new Error('The module at ' + bootModulePath + ' does not export a default function, and you have not specified which export to invoke.'); |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + return bootFunc; |
| 83 | +} |
| 84 | + |
| 85 | +function renderToString(callback, bootModulePath, bootModuleExport, absoluteRequestUrl, requestPathAndQuery) { |
| 86 | + var bootFunc = findBootFunc(bootModulePath, bootModuleExport); |
| 87 | + |
| 88 | + // Prepare a promise that will represent the completion of all domain tasks in this execution context. |
| 89 | + // The boot code will wait for this before performing its final render. |
| 90 | + var domainTaskCompletionPromiseResolve; |
| 91 | + var domainTaskCompletionPromise = new Promise(function (resolve, reject) { |
| 92 | + domainTaskCompletionPromiseResolve = resolve; |
| 93 | + }); |
| 94 | + var params = { |
| 95 | + location: url.parse(requestPathAndQuery), |
| 96 | + url: requestPathAndQuery, |
| 97 | + domainTasks: domainTaskCompletionPromise |
| 98 | + }; |
| 99 | + |
| 100 | + // Open a new domain that can track all the async tasks involved in the app's execution |
| 101 | + domainTask.run(function() { |
| 102 | + // Workaround for Node bug where native Promise continuations lose their domain context |
| 103 | + // (https://github.com/nodejs/node-v0.x-archive/issues/8648) |
| 104 | + bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain.active); |
| 105 | + |
| 106 | + // Make the base URL available to the 'domain-tasks/fetch' helper within this execution context |
| 107 | + baseUrl(absoluteRequestUrl); |
| 108 | + |
| 109 | + // Actually perform the rendering |
| 110 | + bootFunc(params).then(function(successResult) { |
| 111 | + callback(null, { html: successResult.html, globals: successResult.globals }); |
| 112 | + }, function(error) { |
| 113 | + callback(error, null); |
| 114 | + }); |
| 115 | + }, function allDomainTasksCompleted(error) { |
| 116 | + // There are no more ongoing domain tasks (typically data access operations), so we can resolve |
| 117 | + // the domain tasks promise which notifies the boot code that it can do its final render. |
| 118 | + if (error) { |
| 119 | + callback(error, null); |
| 120 | + } else { |
| 121 | + domainTaskCompletionPromiseResolve(); |
| 122 | + } |
| 123 | + }); |
| 124 | +} |
| 125 | + |
| 126 | +function bindPromiseContinuationsToDomain(promise, domainInstance) { |
| 127 | + var originalThen = promise.then; |
| 128 | + promise.then = function then(resolve, reject) { |
| 129 | + if (typeof resolve === 'function') { resolve = domainInstance.bind(resolve); } |
| 130 | + if (typeof reject === 'function') { reject = domainInstance.bind(reject); } |
| 131 | + return originalThen.call(this, resolve, reject); |
| 132 | + }; |
| 133 | +} |
| 134 | + |
| 135 | +module.exports.renderToString = renderToString; |
0 commit comments