11// -----------------------------------------------------------------------------------------
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 = requireIfInstalled ( '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- try {
19- return babelRcText ? JSON . parse ( babelRcText ) : { } ;
20- } catch ( ex ) {
21- ex . message = 'Error while parsing babelrc JSON: ' + ex . message ;
22- throw ex ;
23- }
24- }
25-
26- function loadViaTypeScript ( module , filename ) {
27- if ( ! ts ) {
28- throw new Error ( 'Can\'t load .ts/.tsx files because the \'ntypescript\' package isn\'t installed.\nModule requested: ' + module ) ;
29- }
30-
31- // First perform a minimal transpilation from TS code to ES2015. This is very fast (doesn't involve type checking)
32- // and is unlikely to need any special compiler options
33- var src = fs . readFileSync ( filename , 'utf8' ) ;
34- var compilerOptions = { jsx : ts . JsxEmit . Preserve , module : ts . ModuleKind . ES2015 , target : ts . ScriptTarget . ES6 , emitDecoratorMetadata : true } ;
35- var es6Code = ts . transpile ( src , compilerOptions , 'test.tsx' , /* diagnostics */ [ ] ) ;
36-
37- // Second, process the ES2015 via Babel. We have to do this (instead of going directly from TS to ES5) because
38- // TypeScript's ES5 output isn't exactly compatible with Node-style CommonJS modules. The main issue is with
39- // resolving default exports - https://github.com/Microsoft/TypeScript/issues/2719
40- var es5Code = babelCore . transform ( es6Code , resolveBabelOptions ( filename ) ) . code ;
41- return module . _compile ( es5Code , filename ) ;
42- }
43-
44- function loadViaBabel ( module , filename ) {
45- // Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are.
46- // The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict
47- // mode when the developer didn't expect that (e.g., current versions of underscore.js can't be loaded in strict mode).
48- var useBabel = filename . indexOf ( 'node_modules' ) < 0 ;
49- if ( useBabel ) {
50- var transformedFile = babelCore . transformFileSync ( filename , resolveBabelOptions ( filename ) ) ;
51- return module . _compile ( transformedFile . code , filename ) ;
52- } else {
53- return origJsLoader . apply ( this , arguments ) ;
54- }
55- }
2+ // Loading via Webpack
3+ // This is optional. You don't have to use Webpack. But if you are doing, it's extremely convenient
4+ // to be able to load your boot module via Webpack compilation, so you can use whatever source language
5+ // you like (e.g., TypeScript), and so that loader plugins (e.g., require('./mystyles.less')) work in
6+ // exactly the same way on the server as you do on the client.
7+ // If you don't use Webpack, then it's up to you to define a plain-JS boot module that in turn loads
8+ // whatever other files you need (e.g., using some other compiler/bundler API, or maybe just having
9+ // already precompiled to plain JS files on disk).
10+ function loadViaWebpackNoCache ( webpackConfigPath , modulePath ) {
11+ var ExternalsPlugin = require ( 'webpack-externals-plugin' ) ;
12+ var requireFromString = require ( 'require-from-string' ) ;
13+ var MemoryFS = require ( 'memory-fs' ) ;
14+ var webpack = require ( 'webpack' ) ;
15+
16+ return new Promise ( function ( resolve , reject ) {
17+ // Load the Webpack config and make alterations needed for loading the output into Node
18+ var webpackConfig = require ( webpackConfigPath ) ;
19+ webpackConfig . entry = modulePath ;
20+ webpackConfig . target = 'node' ;
21+ webpackConfig . output = { path : '/' , filename : 'webpack-output.js' , libraryTarget : 'commonjs' } ;
22+
23+ // In Node, we want anything under /node_modules/ to be loaded natively and not bundled into the output
24+ // (partly because it's faster, but also because otherwise there'd be different instances of modules
25+ // depending on how they were loaded, which could lead to errors)
26+ webpackConfig . plugins = webpackConfig . plugins || [ ] ;
27+ webpackConfig . plugins . push ( new ExternalsPlugin ( { type : 'commonjs' , include : / n o d e _ m o d u l e s / } ) ) ;
28+
29+ // The CommonsChunkPlugin is not compatible with a CommonJS environment like Node, nor is it needed in that case
30+ webpackConfig . plugins = webpackConfig . plugins . filter ( function ( plugin ) {
31+ return ! ( plugin instanceof webpack . optimize . CommonsChunkPlugin ) ;
32+ } ) ;
5633
57- function requireIfInstalled ( packageName ) {
58- return isPackageInstalled ( packageName ) ? require ( packageName ) : null ;
34+ // Create a compiler instance that stores its output in memory, then load its output
35+ var compiler = webpack ( webpackConfig ) ;
36+ compiler . outputFileSystem = new MemoryFS ( ) ;
37+ compiler . run ( function ( err , stats ) {
38+ if ( err ) {
39+ reject ( err ) ;
40+ } else {
41+ var fileContent = compiler . outputFileSystem . readFileSync ( '/webpack-output.js' , 'utf8' ) ;
42+ var moduleInstance = requireFromString ( fileContent ) ;
43+ resolve ( moduleInstance ) ;
44+ }
45+ } ) ;
46+ } ) ;
5947}
6048
61- function isPackageInstalled ( packageName ) {
62- try {
63- require . resolve ( packageName ) ;
64- return true ;
65- } catch ( e ) {
66- return false ;
49+ // Ensure we only go through the compile process once per [config, module] pair
50+ var loadViaWebpackPromisesCache = { } ;
51+ function loadViaWebpack ( webpackConfigPath , modulePath , callback ) {
52+ var cacheKey = JSON . stringify ( webpackConfigPath ) + JSON . stringify ( modulePath ) ;
53+ if ( ! ( cacheKey in loadViaWebpackPromisesCache ) ) {
54+ loadViaWebpackPromisesCache [ cacheKey ] = loadViaWebpackNoCache ( webpackConfigPath , modulePath ) ;
6755 }
56+ loadViaWebpackPromisesCache [ cacheKey ] . then ( function ( result ) {
57+ callback ( null , result ) ;
58+ } , function ( error ) {
59+ callback ( error ) ;
60+ } )
6861}
6962
70- function register ( ) {
71- require . extensions [ '.js' ] = loadViaBabel ;
72- require . extensions [ '.jsx' ] = loadViaBabel ;
73- require . extensions [ '.ts' ] = loadViaTypeScript ;
74- require . extensions [ '.tsx' ] = loadViaTypeScript ;
75- } ;
76-
77- register ( ) ;
78-
7963// -----------------------------------------------------------------------------------------
8064// Rendering
8165
@@ -85,64 +69,94 @@ var domain = require('domain');
8569var domainTask = require ( 'domain-task' ) ;
8670var baseUrl = require ( 'domain-task/fetch' ) . baseUrl ;
8771
88- function findBootFunc ( bootModulePath , bootModuleExport ) {
89- var resolvedPath = path . resolve ( process . cwd ( ) , bootModulePath ) ;
90- var bootFunc = require ( resolvedPath ) ;
91- if ( bootModuleExport ) {
92- bootFunc = bootFunc [ bootModuleExport ] ;
93- } else if ( typeof bootFunc !== 'function' ) {
94- bootFunc = bootFunc . default ; // TypeScript sometimes uses this name for default exports
95- }
96- if ( typeof bootFunc !== 'function' ) {
97- if ( bootModuleExport ) {
98- throw new Error ( 'The module at ' + bootModulePath + ' has no function export named ' + bootModuleExport + '.' ) ;
99- } else {
100- throw new Error ( 'The module at ' + bootModulePath + ' does not export a default function, and you have not specified which export to invoke.' ) ;
101- }
72+ function findBootModule ( bootModule , callback ) {
73+ var bootModuleNameFullPath = path . resolve ( process . cwd ( ) , bootModule . moduleName ) ;
74+ if ( bootModule . webpackConfig ) {
75+ var webpackConfigFullPath = path . resolve ( process . cwd ( ) , bootModule . webpackConfig ) ;
76+ loadViaWebpack ( webpackConfigFullPath , bootModuleNameFullPath , callback ) ;
77+ } else {
78+ callback ( null , require ( bootModuleNameFullPath ) ) ;
10279 }
103-
104- return bootFunc ;
10580}
10681
107- function renderToString ( callback , bootModulePath , bootModuleExport , absoluteRequestUrl , requestPathAndQuery ) {
108- var bootFunc = findBootFunc ( bootModulePath , bootModuleExport ) ;
109-
110- // Prepare a promise that will represent the completion of all domain tasks in this execution context.
111- // The boot code will wait for this before performing its final render.
112- var domainTaskCompletionPromiseResolve ;
113- var domainTaskCompletionPromise = new Promise ( function ( resolve , reject ) {
114- domainTaskCompletionPromiseResolve = resolve ;
115- } ) ;
116- var params = {
117- location : url . parse ( requestPathAndQuery ) ,
118- url : requestPathAndQuery ,
119- absoluteUrl : absoluteRequestUrl ,
120- domainTasks : domainTaskCompletionPromise
121- } ;
122-
123- // Open a new domain that can track all the async tasks involved in the app's execution
124- domainTask . run ( function ( ) {
125- // Workaround for Node bug where native Promise continuations lose their domain context
126- // (https://github.com/nodejs/node-v0.x-archive/issues/8648)
127- bindPromiseContinuationsToDomain ( domainTaskCompletionPromise , domain . active ) ;
82+ function findBootFunc ( bootModule , callback ) {
83+ // First try to load the module (possibly via Webpack)
84+ findBootModule ( bootModule , function ( findBootModuleError , foundBootModule ) {
85+ if ( findBootModuleError ) {
86+ callback ( findBootModuleError ) ;
87+ return ;
88+ }
12889
129- // Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
130- baseUrl ( absoluteRequestUrl ) ;
90+ // Now try to pick out the function they want us to invoke
91+ var bootFunc ;
92+ if ( bootModule . exportName ) {
93+ // Explicitly-named export
94+ bootFunc = foundBootModule [ bootModule . exportName ] ;
95+ } else if ( typeof foundBootModule !== 'function' ) {
96+ // TypeScript-style default export
97+ bootFunc = foundBootModule . default ;
98+ } else {
99+ // Native default export
100+ bootFunc = foundBootModule ;
101+ }
131102
132- // Actually perform the rendering
133- bootFunc ( params ) . then ( function ( successResult ) {
134- callback ( null , { html : successResult . html , globals : successResult . globals } ) ;
135- } , function ( error ) {
136- callback ( error , null ) ;
137- } ) ;
138- } , function allDomainTasksCompleted ( error ) {
139- // There are no more ongoing domain tasks (typically data access operations), so we can resolve
140- // the domain tasks promise which notifies the boot code that it can do its final render.
141- if ( error ) {
142- callback ( error , null ) ;
103+ // Validate the result
104+ if ( typeof bootFunc !== 'function' ) {
105+ if ( bootModule . exportName ) {
106+ callback ( new Error ( 'The module at ' + bootModule . moduleName + ' has no function export named ' + bootModule . exportName + '.' ) ) ;
107+ } else {
108+ callback ( new Error ( 'The module at ' + bootModule . moduleName + ' does not export a default function, and you have not specified which export to invoke.' ) ) ;
109+ }
143110 } else {
144- domainTaskCompletionPromiseResolve ( ) ;
111+ callback ( null , bootFunc ) ;
112+ }
113+ } ) ;
114+ }
115+
116+ function renderToString ( callback , bootModule , absoluteRequestUrl , requestPathAndQuery ) {
117+ findBootFunc ( bootModule , function ( findBootFuncError , bootFunc ) {
118+ if ( findBootFuncError ) {
119+ callback ( findBootFuncError ) ;
120+ return ;
145121 }
122+
123+ // Prepare a promise that will represent the completion of all domain tasks in this execution context.
124+ // The boot code will wait for this before performing its final render.
125+ var domainTaskCompletionPromiseResolve ;
126+ var domainTaskCompletionPromise = new Promise ( function ( resolve , reject ) {
127+ domainTaskCompletionPromiseResolve = resolve ;
128+ } ) ;
129+ var params = {
130+ location : url . parse ( requestPathAndQuery ) ,
131+ url : requestPathAndQuery ,
132+ absoluteUrl : absoluteRequestUrl ,
133+ domainTasks : domainTaskCompletionPromise
134+ } ;
135+
136+ // Open a new domain that can track all the async tasks involved in the app's execution
137+ domainTask . run ( function ( ) {
138+ // Workaround for Node bug where native Promise continuations lose their domain context
139+ // (https://github.com/nodejs/node-v0.x-archive/issues/8648)
140+ bindPromiseContinuationsToDomain ( domainTaskCompletionPromise , domain . active ) ;
141+
142+ // Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
143+ baseUrl ( absoluteRequestUrl ) ;
144+
145+ // Actually perform the rendering
146+ bootFunc ( params ) . then ( function ( successResult ) {
147+ callback ( null , { html : successResult . html , globals : successResult . globals } ) ;
148+ } , function ( error ) {
149+ callback ( error , null ) ;
150+ } ) ;
151+ } , function allDomainTasksCompleted ( error ) {
152+ // There are no more ongoing domain tasks (typically data access operations), so we can resolve
153+ // the domain tasks promise which notifies the boot code that it can do its final render.
154+ if ( error ) {
155+ callback ( error , null ) ;
156+ } else {
157+ domainTaskCompletionPromiseResolve ( ) ;
158+ }
159+ } ) ;
146160 } ) ;
147161}
148162
0 commit comments