Support new prerendering mode that doesn't require you to deploy node_modules to production. This is a breaking change in aspnet-prerendering, hence the major version bump. The NuGet package is back-compatible though.

This commit is contained in:
SteveSandersonMS
2016-11-25 17:39:58 +00:00
parent 9f6b0b0573
commit 17f9ecec29
9 changed files with 235 additions and 122 deletions

View File

@@ -1,4 +1,6 @@
/typings/
/node_modules/
/*.js
/*.d.ts
/**/*.js
/**/.d.ts
!/src/**/*.d.ts

View File

@@ -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",

View File

@@ -1,4 +1,4 @@
import 'es6-promise';
/// <reference path="./PrerenderingInterfaces.d.ts" />
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<RenderToStringResult>;
}
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<any>;
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<T>(promise: Promise<T>, timeoutMilliseconds: number, timeoutRejectionValue: any): Promise<T> {
@@ -119,59 +93,6 @@ function wrapWithTimeout<T>(promise: Promise<T>, timeoutMilliseconds: number, ti
});
}
function findBootModule<T>(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<any>(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<any>, domainInstance: domain.Domain) {
const originalThen = promise.then;
promise.then = (function then(resolve, reject) {

View File

@@ -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<RenderToStringResult>;
}
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<any>;
data: any; // any custom object passed through from .NET
}
interface BootModuleInfo {
moduleName: string;
exportName?: string;
webpackConfig?: string;
}

View File

@@ -1 +1,5 @@
/// <reference path="./PrerenderingInterfaces.d.ts" />
export * from './Prerendering';
export type RenderResult = RenderToStringResult | RedirectResult;