diff --git a/src/Microsoft.AspNetCore.SpaServices/Content/Node/prerenderer.js b/src/Microsoft.AspNetCore.SpaServices/Content/Node/prerenderer.js
index d5ebd7e..4be014b 100644
--- a/src/Microsoft.AspNetCore.SpaServices/Content/Node/prerenderer.js
+++ b/src/Microsoft.AspNetCore.SpaServices/Content/Node/prerenderer.js
@@ -52,29 +52,105 @@
/***/ function(module, exports, __webpack_require__) {
"use strict";
- // Pass through the invocation to the 'aspnet-prerendering' package, verifying that it can be loaded
- function renderToString(callback) {
- var aspNetPrerendering;
+ var path = __webpack_require__(2);
+ // Separate declaration and export just to add type checking on function signature
+ exports.renderToString = renderToStringImpl;
+ // This function is invoked by .NET code (via NodeServices). Its job is to hand off execution to the application's
+ // prerendering boot function. It can operate in two modes:
+ // [1] Legacy mode
+ // This is for backward compatibility with projects created with templates older than the generator version 0.6.0.
+ // In this mode, we don't really do anything here - we just load the 'aspnet-prerendering' NPM module (which must
+ // exist in node_modules, and must be v1.x (not v2+)), and pass through all the parameters to it. Code in
+ // 'aspnet-prerendering' v1.x will locate the boot function and invoke it.
+ // The drawback to this mode is that, for it to work, you have to deploy node_modules to production.
+ // [2] Current mode
+ // This is for projects created with the Yeoman generator 0.6.0+ (or projects manually updated). In this mode,
+ // we don't invoke 'require' at runtime at all. All our dependencies are bundled into the NuGet package, so you
+ // don't have to deploy node_modules to production.
+ // To determine whether we're in mode [1] or [2], the code locates your prerendering boot function, and checks whether
+ // a certain flag is attached to the function instance.
+ function renderToStringImpl(callback, applicationBasePath, bootModule, absoluteRequestUrl, requestPathAndQuery, customDataParameter, overrideTimeoutMilliseconds) {
try {
- aspNetPrerendering = __webpack_require__(2);
+ var renderToStringFunc = findRenderToStringFunc(applicationBasePath, bootModule);
+ var isNotLegacyMode = renderToStringFunc && renderToStringFunc['isServerRenderer'];
+ if (isNotLegacyMode) {
+ // Current (non-legacy) mode - we invoke the exported function directly (instead of going through aspnet-prerendering)
+ // It's type-safe to just apply the incoming args to this function, because we already type-checked that it's a RenderToStringFunc,
+ // just like renderToStringImpl itself is.
+ renderToStringFunc.apply(null, arguments);
+ }
+ else {
+ // Legacy mode - just hand off execution to 'aspnet-prerendering' v1.x, which must exist in node_modules at runtime
+ renderToStringFunc = __webpack_require__(3).renderToString;
+ if (renderToStringFunc) {
+ renderToStringFunc(callback, applicationBasePath, bootModule, absoluteRequestUrl, requestPathAndQuery, customDataParameter, overrideTimeoutMilliseconds);
+ }
+ else {
+ callback('If you use aspnet-prerendering >= 2.0.0, you must update your server-side boot module to call createServerRenderer. '
+ + 'Either update your boot module code, or revert to aspnet-prerendering version 1.x');
+ }
+ }
}
catch (ex) {
- // Developers sometimes have trouble with badly-configured Node installations, where it's unable
- // to find node_modules. Or they accidentally fail to deploy node_modules, or even to run 'npm install'.
- // Make sure such errors are reported back to the .NET part of the app.
- callback('Prerendering failed because of an error while loading \'aspnet-prerendering\'. Error was: '
+ // Make sure loading errors are reported back to the .NET part of the app
+ callback('Prerendering failed because of error: '
+ ex.stack
+ '\nCurrent directory is: '
+ process.cwd());
- return;
}
- return aspNetPrerendering.renderToString.apply(this, arguments);
}
- exports.renderToString = renderToString;
+ ;
+ function findBootModule(applicationBasePath, bootModule) {
+ var bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName);
+ if (bootModule.webpackConfig) {
+ // If you're using asp-prerender-webpack-config, you're definitely in legacy mode
+ return null;
+ }
+ else {
+ return require(bootModuleNameFullPath);
+ }
+ }
+ function findRenderToStringFunc(applicationBasePath, bootModule) {
+ // First try to load the module
+ var foundBootModule = findBootModule(applicationBasePath, bootModule);
+ if (foundBootModule === null) {
+ return null; // Must be legacy mode
+ }
+ // Now try to pick out the function they want us to invoke
+ var renderToStringFunc;
+ if (bootModule.exportName) {
+ // Explicitly-named export
+ renderToStringFunc = foundBootModule[bootModule.exportName];
+ }
+ else if (typeof foundBootModule !== 'function') {
+ // TypeScript-style default export
+ renderToStringFunc = foundBootModule.default;
+ }
+ else {
+ // Native default export
+ renderToStringFunc = foundBootModule;
+ }
+ // Validate the result
+ if (typeof renderToStringFunc !== 'function') {
+ if (bootModule.exportName) {
+ throw new Error("The module at " + bootModule.moduleName + " has no function export named " + bootModule.exportName + ".");
+ }
+ else {
+ throw new Error("The module at " + bootModule.moduleName + " does not export a default function, and you have not specified which export to invoke.");
+ }
+ }
+ return renderToStringFunc;
+ }
/***/ },
/* 2 */
+/***/ function(module, exports) {
+
+ module.exports = require("path");
+
+/***/ },
+/* 3 */
/***/ function(module, exports) {
module.exports = require("aspnet-prerendering");
diff --git a/src/Microsoft.AspNetCore.SpaServices/Content/Node/webpack-dev-middleware.js b/src/Microsoft.AspNetCore.SpaServices/Content/Node/webpack-dev-middleware.js
index dbabebe..f5385d4 100644
--- a/src/Microsoft.AspNetCore.SpaServices/Content/Node/webpack-dev-middleware.js
+++ b/src/Microsoft.AspNetCore.SpaServices/Content/Node/webpack-dev-middleware.js
@@ -44,13 +44,14 @@
/* 0 */
/***/ function(module, exports, __webpack_require__) {
- module.exports = __webpack_require__(3);
+ module.exports = __webpack_require__(4);
/***/ },
/* 1 */,
/* 2 */,
-/* 3 */
+/* 3 */,
+/* 4 */
/***/ function(module, exports, __webpack_require__) {
"use strict";
@@ -58,7 +59,7 @@
function createWebpackDevServer(callback) {
var aspNetWebpack;
try {
- aspNetWebpack = __webpack_require__(4);
+ aspNetWebpack = __webpack_require__(5);
}
catch (ex) {
// Developers sometimes have trouble with badly-configured Node installations, where it's unable
@@ -76,7 +77,7 @@
/***/ },
-/* 4 */
+/* 5 */
/***/ function(module, exports) {
module.exports = require("aspnet-webpack");
diff --git a/src/Microsoft.AspNetCore.SpaServices/TypeScript/Prerenderer.ts b/src/Microsoft.AspNetCore.SpaServices/TypeScript/Prerenderer.ts
index a837f98..3b6d5d7 100644
--- a/src/Microsoft.AspNetCore.SpaServices/TypeScript/Prerenderer.ts
+++ b/src/Microsoft.AspNetCore.SpaServices/TypeScript/Prerenderer.ts
@@ -1,20 +1,94 @@
-// Pass through the invocation to the 'aspnet-prerendering' package, verifying that it can be loaded
-export function renderToString(callback) {
- let aspNetPrerendering;
+///
+import * as url from 'url';
+import * as path from 'path';
+declare var __non_webpack_require__;
+
+// Separate declaration and export just to add type checking on function signature
+export const renderToString: RenderToStringFunc = renderToStringImpl;
+
+// This function is invoked by .NET code (via NodeServices). Its job is to hand off execution to the application's
+// prerendering boot function. It can operate in two modes:
+// [1] Legacy mode
+// This is for backward compatibility with projects created with templates older than the generator version 0.6.0.
+// In this mode, we don't really do anything here - we just load the 'aspnet-prerendering' NPM module (which must
+// exist in node_modules, and must be v1.x (not v2+)), and pass through all the parameters to it. Code in
+// 'aspnet-prerendering' v1.x will locate the boot function and invoke it.
+// The drawback to this mode is that, for it to work, you have to deploy node_modules to production.
+// [2] Current mode
+// This is for projects created with the Yeoman generator 0.6.0+ (or projects manually updated). In this mode,
+// we don't invoke 'require' at runtime at all. All our dependencies are bundled into the NuGet package, so you
+// don't have to deploy node_modules to production.
+// To determine whether we're in mode [1] or [2], the code locates your prerendering boot function, and checks whether
+// a certain flag is attached to the function instance.
+function renderToStringImpl(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number) {
try {
- aspNetPrerendering = require('aspnet-prerendering');
+ let renderToStringFunc = findRenderToStringFunc(applicationBasePath, bootModule);
+ const isNotLegacyMode = renderToStringFunc && renderToStringFunc['isServerRenderer'];
+
+ if (isNotLegacyMode) {
+ // Current (non-legacy) mode - we invoke the exported function directly (instead of going through aspnet-prerendering)
+ // It's type-safe to just apply the incoming args to this function, because we already type-checked that it's a RenderToStringFunc,
+ // just like renderToStringImpl itself is.
+ renderToStringFunc.apply(null, arguments);
+ } else {
+ // Legacy mode - just hand off execution to 'aspnet-prerendering' v1.x, which must exist in node_modules at runtime
+ renderToStringFunc = require('aspnet-prerendering').renderToString;
+ if (renderToStringFunc) {
+ renderToStringFunc(callback, applicationBasePath, bootModule, absoluteRequestUrl, requestPathAndQuery, customDataParameter, overrideTimeoutMilliseconds);
+ } else {
+ callback('If you use aspnet-prerendering >= 2.0.0, you must update your server-side boot module to call createServerRenderer. '
+ + 'Either update your boot module code, or revert to aspnet-prerendering version 1.x');
+ }
+ }
} catch (ex) {
- // Developers sometimes have trouble with badly-configured Node installations, where it's unable
- // to find node_modules. Or they accidentally fail to deploy node_modules, or even to run 'npm install'.
- // Make sure such errors are reported back to the .NET part of the app.
+ // Make sure loading errors are reported back to the .NET part of the app
callback(
- 'Prerendering failed because of an error while loading \'aspnet-prerendering\'. Error was: '
+ 'Prerendering failed because of error: '
+ ex.stack
+ '\nCurrent directory is: '
+ process.cwd()
);
- return;
+ }
+};
+
+function findBootModule(applicationBasePath: string, bootModule: BootModuleInfo): any {
+ const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName);
+ if (bootModule.webpackConfig) {
+ // If you're using asp-prerender-webpack-config, you're definitely in legacy mode
+ return null;
+ } else {
+ return __non_webpack_require__(bootModuleNameFullPath);
+ }
+}
+
+function findRenderToStringFunc(applicationBasePath: string, bootModule: BootModuleInfo): RenderToStringFunc {
+ // First try to load the module
+ const foundBootModule = findBootModule(applicationBasePath, bootModule);
+ if (foundBootModule === null) {
+ return null; // Must be legacy mode
}
- return aspNetPrerendering.renderToString.apply(this, arguments);
+ // Now try to pick out the function they want us to invoke
+ let renderToStringFunc: RenderToStringFunc;
+ if (bootModule.exportName) {
+ // Explicitly-named export
+ renderToStringFunc = foundBootModule[bootModule.exportName];
+ } else if (typeof foundBootModule !== 'function') {
+ // TypeScript-style default export
+ renderToStringFunc = foundBootModule.default;
+ } else {
+ // Native default export
+ renderToStringFunc = foundBootModule;
+ }
+
+ // Validate the result
+ if (typeof renderToStringFunc !== 'function') {
+ if (bootModule.exportName) {
+ throw new Error(`The module at ${ bootModule.moduleName } has no function export named ${ bootModule.exportName }.`);
+ } else {
+ throw new Error(`The module at ${ bootModule.moduleName } does not export a default function, and you have not specified which export to invoke.`);
+ }
+ }
+
+ return renderToStringFunc;
}
diff --git a/src/Microsoft.AspNetCore.SpaServices/TypeScript/tsconfig.json b/src/Microsoft.AspNetCore.SpaServices/TypeScript/tsconfig.json
index 896fc88..433cde0 100644
--- a/src/Microsoft.AspNetCore.SpaServices/TypeScript/tsconfig.json
+++ b/src/Microsoft.AspNetCore.SpaServices/TypeScript/tsconfig.json
@@ -3,7 +3,8 @@
"target": "es3",
"module": "commonjs",
"moduleResolution": "node",
- "types": ["node"]
+ "types": ["node"],
+ "lib": ["es2015"]
},
"exclude": [
"node_modules"
diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/.gitignore b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/.gitignore
index a1df9a5..477bc9d 100644
--- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/.gitignore
+++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/.gitignore
@@ -1,4 +1,6 @@
/typings/
/node_modules/
-/*.js
-/*.d.ts
+/**/*.js
+
+/**/.d.ts
+!/src/**/*.d.ts
diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json
index 83e0259..301d6a2 100644
--- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json
+++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json
@@ -1,6 +1,6 @@
{
"name": "aspnet-prerendering",
- "version": "1.0.7",
+ "version": "2.0.0",
"description": "Helpers for server-side rendering of JavaScript applications in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
"main": "index.js",
"scripts": {
@@ -17,8 +17,7 @@
"url": "https://github.com/aspnet/JavaScriptServices.git"
},
"dependencies": {
- "domain-task": "^2.0.1",
- "es6-promise": "^3.1.2"
+ "domain-task": "^2.0.1"
},
"devDependencies": {
"@types/node": "^6.0.42",
diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts
index e9d604e..9afd211 100644
--- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts
+++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts
@@ -1,4 +1,4 @@
-import 'es6-promise';
+///
import * as url from 'url';
import * as path from 'path';
import * as domain from 'domain';
@@ -7,41 +7,8 @@ import { baseUrl } from 'domain-task/fetch';
const defaultTimeoutMilliseconds = 30 * 1000;
-export interface RenderToStringCallback {
- (error: any, result: RenderToStringResult): void;
-}
-
-export interface RenderToStringResult {
- html: string;
- globals: { [key: string]: any };
-}
-
-export interface BootFunc {
- (params: BootFuncParams): Promise;
-}
-
-export interface BootFuncParams {
- location: url.Url; // e.g., Location object containing information '/some/path'
- origin: string; // e.g., 'https://example.com:1234'
- url: string; // e.g., '/some/path'
- absoluteUrl: string; // e.g., 'https://example.com:1234/some/path'
- domainTasks: Promise;
- data: any; // any custom object passed through from .NET
-}
-
-export interface BootModuleInfo {
- moduleName: string;
- exportName?: string;
- webpackConfig?: string;
-}
-
-export function renderToString(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number) {
- findBootFunc(applicationBasePath, bootModule, (findBootFuncError, bootFunc) => {
- if (findBootFuncError) {
- callback(findBootFuncError, null);
- return;
- }
-
+export function createServerRenderer(bootFunc: BootFunc): RenderToStringFunc {
+ const resultFunc = (callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number) => {
// 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;
@@ -76,7 +43,7 @@ export function renderToString(callback: RenderToStringCallback, applicationBase
}
const timeoutMilliseconds = overrideTimeoutMilliseconds || defaultTimeoutMilliseconds; // e.g., pass -1 to override as 'never time out'
const bootFuncPromiseWithTimeout = timeoutMilliseconds > 0
- ? wrapWithTimeout(bootFuncPromise, timeoutMilliseconds,
+ ? 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.')
@@ -97,7 +64,14 @@ export function renderToString(callback: RenderToStringCallback, applicationBase
domainTaskCompletionPromiseResolve();
}
});
- });
+ };
+
+ // 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: Promise, timeoutMilliseconds: number, timeoutRejectionValue: any): Promise {
@@ -119,59 +93,6 @@ function wrapWithTimeout(promise: Promise, timeoutMilliseconds: number, ti
});
}
-function findBootModule(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, foundModule: T) => void) {
- const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName);
- if (bootModule.webpackConfig) {
- const webpackConfigFullPath = path.resolve(applicationBasePath, bootModule.webpackConfig);
-
- let aspNetWebpackModule: any;
- try {
- aspNetWebpackModule = require('aspnet-webpack');
- } catch (ex) {
- callback('To load your boot module via webpack (i.e., if you specify a \'webpackConfig\' option), you must install the \'aspnet-webpack\' NPM package. Error encountered while loading \'aspnet-webpack\': ' + ex.stack, null);
- return;
- }
-
- aspNetWebpackModule.loadViaWebpack(webpackConfigFullPath, bootModuleNameFullPath, callback);
- } else {
- callback(null, require(bootModuleNameFullPath));
- }
-}
-
-function findBootFunc(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, bootFunc: BootFunc) => void) {
- // First try to load the module (possibly via Webpack)
- findBootModule(applicationBasePath, bootModule, (findBootModuleError, foundBootModule) => {
- if (findBootModuleError) {
- callback(findBootModuleError, null);
- return;
- }
-
- // Now try to pick out the function they want us to invoke
- let bootFunc: BootFunc;
- if (bootModule.exportName) {
- // Explicitly-named export
- bootFunc = foundBootModule[bootModule.exportName];
- } else if (typeof foundBootModule !== 'function') {
- // TypeScript-style default export
- bootFunc = foundBootModule.default;
- } else {
- // Native default export
- bootFunc = foundBootModule;
- }
-
- // Validate the result
- if (typeof bootFunc !== 'function') {
- if (bootModule.exportName) {
- callback(`The module at ${ bootModule.moduleName } has no function export named ${ bootModule.exportName }.`, null);
- } else {
- callback(`The module at ${ bootModule.moduleName } does not export a default function, and you have not specified which export to invoke.`, null);
- }
- } else {
- callback(null, bootFunc);
- }
- });
-}
-
function bindPromiseContinuationsToDomain(promise: Promise, domainInstance: domain.Domain) {
const originalThen = promise.then;
promise.then = (function then(resolve, reject) {
diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/PrerenderingInterfaces.d.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/PrerenderingInterfaces.d.ts
new file mode 100644
index 0000000..c919646
--- /dev/null
+++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/PrerenderingInterfaces.d.ts
@@ -0,0 +1,35 @@
+interface RenderToStringFunc {
+ (callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number): void;
+}
+
+interface RenderToStringCallback {
+ (error: any, result?: RenderToStringResult): void;
+}
+
+interface RenderToStringResult {
+ html: string;
+ globals?: { [key: string]: any };
+}
+
+interface RedirectResult {
+ redirectUrl: string;
+}
+
+interface BootFunc {
+ (params: BootFuncParams): Promise;
+}
+
+interface BootFuncParams {
+ location: any; // e.g., Location object containing information '/some/path'
+ origin: string; // e.g., 'https://example.com:1234'
+ url: string; // e.g., '/some/path'
+ absoluteUrl: string; // e.g., 'https://example.com:1234/some/path'
+ domainTasks: Promise;
+ data: any; // any custom object passed through from .NET
+}
+
+interface BootModuleInfo {
+ moduleName: string;
+ exportName?: string;
+ webpackConfig?: string;
+}
diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/index.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/index.ts
index aaff576..6df3ad2 100644
--- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/index.ts
+++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/index.ts
@@ -1 +1,5 @@
+///
+
export * from './Prerendering';
+
+export type RenderResult = RenderToStringResult | RedirectResult;