mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-22 17:47:53 +00:00
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:
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
/// <reference path="../npm/aspnet-prerendering/src/PrerenderingInterfaces.d.ts" />
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"target": "es3",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"types": ["node"]
|
||||
"types": ["node"],
|
||||
"lib": ["es2015"]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/typings/
|
||||
/node_modules/
|
||||
/*.js
|
||||
/*.d.ts
|
||||
/**/*.js
|
||||
|
||||
/**/.d.ts
|
||||
!/src/**/*.d.ts
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
35
src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/PrerenderingInterfaces.d.ts
vendored
Normal file
35
src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/PrerenderingInterfaces.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
/// <reference path="./PrerenderingInterfaces.d.ts" />
|
||||
|
||||
export * from './Prerendering';
|
||||
|
||||
export type RenderResult = RenderToStringResult | RedirectResult;
|
||||
|
||||
Reference in New Issue
Block a user