11'use strict' ;
22
33const fs = require ( 'fs' ) ;
4+ const vm = require ( 'vm' ) ;
45const path = require ( 'path' ) ;
56const chalk = require ( 'chalk' ) ;
67
@@ -9,6 +10,7 @@ const SimpleDOM = require('simple-dom');
910const resolve = require ( 'resolve' ) ;
1011const debug = require ( 'debug' ) ( 'fastboot:ember-app' ) ;
1112
13+ const Sandbox = require ( './sandbox' ) ;
1214const FastBootInfo = require ( './fastboot-info' ) ;
1315const Result = require ( './result' ) ;
1416const FastBootSchemaVersions = require ( './fastboot-schema-versions' ) ;
@@ -27,15 +29,15 @@ class EmberApp {
2729 * Create a new EmberApp.
2830 * @param {Object } options
2931 * @param {string } options.distPath - path to the built Ember application
30- * @param {Sandbox } [options.sandbox=VMSandbox] - sandbox to use
3132 * @param {Object } [options.sandboxGlobals] - sandbox variables that can be added or used for overrides in the sandbox.
3233 */
3334 constructor ( options ) {
34- let distPath = path . resolve ( options . distPath ) ;
35+ // TODO: make these two into builder functions
36+ this . sandboxGlobals = options . sandboxGlobals ;
37+
38+ let distPath = ( this . distPath = path . resolve ( options . distPath ) ) ;
3539 let config = this . readPackageJSON ( distPath ) ;
3640
37- this . appFilePaths = config . appFiles ;
38- this . vendorFilePaths = config . vendorFiles ;
3941 this . moduleWhitelist = config . moduleWhitelist ;
4042 this . hostWhitelist = config . hostWhitelist ;
4143 this . config = config . config ;
@@ -57,23 +59,25 @@ class EmberApp {
5759
5860 this . html = fs . readFileSync ( config . htmlFile , 'utf8' ) ;
5961
60- this . sandbox = this . buildSandbox ( distPath , options . sandbox , options . sandboxGlobals ) ;
61- this . app = this . retrieveSandboxedApp ( ) ;
62+ this . sandboxRequire = this . buildWhitelistedRequire ( this . moduleWhitelist , distPath ) ;
63+ let filePaths = [ require . resolve ( './scripts/install-source-map-support' ) ] . concat (
64+ config . vendorFiles ,
65+ config . appFiles
66+ ) ;
67+ this . scripts = buildScripts ( filePaths ) ;
68+
69+ // Ensure that the dist files can be evaluated and the `Ember.Application`
70+ // instance created.
71+ this . buildApp ( ) ;
6272 }
6373
6474 /**
6575 * @private
6676 *
6777 * Builds and initializes a new sandbox to run the Ember application in.
68- *
69- * @param {string } distPath path to the built Ember app to load
70- * @param {Sandbox } [sandboxClass=VMSandbox] sandbox class to use
71- * @param {Object } [sandboxGlobals={}] any additional variables to expose in the sandbox or override existing in the sandbox
7278 */
73- buildSandbox ( distPath , sandboxClass , sandboxGlobals ) {
74- const { config, appName } = this ;
75-
76- let sandboxRequire = this . buildWhitelistedRequire ( this . moduleWhitelist , distPath ) ;
79+ buildSandbox ( ) {
80+ const { distPath, sandboxGlobals, config, appName, sandboxRequire } = this ;
7781
7882 function fastbootConfig ( key ) {
7983 if ( ! key ) {
@@ -89,21 +93,22 @@ class EmberApp {
8993 }
9094
9195 // add any additional user provided variables or override the default globals in the sandbox
92- let globals = {
93- najax,
94- FastBoot : {
95- require : sandboxRequire ,
96- config : fastbootConfig ,
97-
98- get distPath ( ) {
99- return distPath ;
96+ let globals = Object . assign (
97+ {
98+ najax,
99+ FastBoot : {
100+ require : sandboxRequire ,
101+ config : fastbootConfig ,
102+
103+ get distPath ( ) {
104+ return distPath ;
105+ } ,
100106 } ,
101107 } ,
102- } ;
103-
104- globals = Object . assign ( globals , sandboxGlobals ) ;
108+ sandboxGlobals
109+ ) ;
105110
106- return new sandboxClass ( { globals } ) ;
111+ return new Sandbox ( globals ) ;
107112 }
108113
109114 /**
@@ -180,43 +185,31 @@ class EmberApp {
180185 }
181186
182187 /**
183- * @private
184- *
185- * Loads the app and vendor files in the sandbox (Node vm).
186- *
188+ * Perform any cleanup that is needed
187189 */
188- loadAppFiles ( ) {
189- let sandbox = this . sandbox ;
190- let appFilePaths = this . appFilePaths ;
191- let vendorFilePaths = this . vendorFilePaths ;
192-
193- sandbox . eval ( 'sourceMapSupport.install(Error);' ) ;
194-
195- debug ( 'evaluating app and vendor files' ) ;
196-
197- vendorFilePaths . forEach ( function ( vendorFilePath ) {
198- debug ( 'evaluating vendor file %s' , vendorFilePath ) ;
199- let vendorFile = fs . readFileSync ( vendorFilePath , 'utf8' ) ;
200- sandbox . eval ( vendorFile , vendorFilePath ) ;
201- } ) ;
202- debug ( 'vendor file evaluated' ) ;
203-
204- appFilePaths . forEach ( function ( appFilePath ) {
205- debug ( 'evaluating app file %s' , appFilePath ) ;
206- let appFile = fs . readFileSync ( appFilePath , 'utf8' ) ;
207- sandbox . eval ( appFile , appFilePath ) ;
208- } ) ;
209- debug ( 'app files evaluated' ) ;
190+ destroy ( ) {
191+ // TODO: expose as public api (through the top level) so that we can
192+ // cleanup pre-warmed visits
210193 }
211194
212195 /**
213196 * @private
214197 *
215- * Create the ember application in the sandbox.
198+ * Creates a new `Application`
216199 *
200+ * @returns {Ember.Application } instance
217201 */
218- createEmberApp ( ) {
219- let sandbox = this . sandbox ;
202+ buildApp ( ) {
203+ let sandbox = this . buildSandbox ( ) ;
204+
205+ debug ( 'adding files to sandbox' ) ;
206+
207+ for ( let script of this . scripts ) {
208+ debug ( 'evaluating file %s' , script ) ;
209+ sandbox . runScript ( script ) ;
210+ }
211+
212+ debug ( 'files evaluated' ) ;
220213
221214 // Retrieve the application factory from within the sandbox
222215 let AppFactory = sandbox . run ( function ( ctx ) {
@@ -230,48 +223,12 @@ class EmberApp {
230223 ) ;
231224 }
232225
233- // Otherwise, return a new `Ember.Application` instance
234- return AppFactory [ 'default' ] ( ) ;
235- }
236-
237- /**
238- * @private
239- *
240- * Initializes the sandbox by evaluating the Ember app's JavaScript
241- * code, then retrieves the application factory from the sandbox and creates a new
242- * `Ember.Application`.
243- *
244- * @returns {Ember.Application } the Ember application from the sandbox
245- */
246- retrieveSandboxedApp ( ) {
247- this . loadAppFiles ( ) ;
248-
249- return this . createEmberApp ( ) ;
250- }
251-
252- /**
253- * Destroys the app and its sandbox.
254- */
255- destroy ( ) {
256- if ( this . app ) {
257- this . app . destroy ( ) ;
258- }
226+ debug ( 'creating application' ) ;
259227
260- this . sandbox = null ;
261- }
228+ // Otherwise, return a new `Ember.Application` instance
229+ let app = AppFactory [ 'default' ] ( ) ;
262230
263- /**
264- * @private
265- *
266- * Creates a new `ApplicationInstance` from the sandboxed `Application`.
267- *
268- * @returns {Promise<Ember.ApplicationInstance> } instance
269- */
270- buildAppInstance ( ) {
271- return this . app . boot ( ) . then ( function ( app ) {
272- debug ( 'building instance' ) ;
273- return app . buildInstance ( ) ;
274- } ) ;
231+ return app ;
275232 }
276233
277234 /**
@@ -292,20 +249,20 @@ class EmberApp {
292249 * @param {Object } result
293250 * @return {Promise<instance> } instance
294251 */
295- visitRoute ( path , fastbootInfo , bootOptions , result ) {
296- let instance ;
297-
298- return this . buildAppInstance ( )
299- . then ( appInstance => {
300- instance = appInstance ;
301- result . instance = instance ;
302- registerFastBootInfo ( fastbootInfo , instance ) ;
303-
304- return instance . boot ( bootOptions ) ;
305- } )
306- . then ( ( ) => instance . visit ( path , bootOptions ) )
307- . then ( ( ) => fastbootInfo . deferredPromise )
308- . then ( ( ) => instance ) ;
252+ async visitRoute ( path , fastbootInfo , bootOptions , result ) {
253+ let app = await this . buildApp ( ) ;
254+ result . applicationInstance = app ;
255+
256+ await app . boot ( ) ;
257+
258+ let instance = await app . buildInstance ( ) ;
259+ result . applicationInstanceInstance = instance ;
260+
261+ registerFastBootInfo ( fastbootInfo , instance ) ;
262+
263+ await instance . boot ( bootOptions ) ;
264+ await instance . visit ( path , bootOptions ) ;
265+ await fastbootInfo . deferredPromise ;
309266 }
310267
311268 /**
@@ -330,7 +287,7 @@ class EmberApp {
330287 * @param {ClientResponse }
331288 * @returns {Promise<Result> } result
332289 */
333- visit ( path , options ) {
290+ async visit ( path , options ) {
334291 let req = options . request ;
335292 let res = options . response ;
336293 let html = options . html || this . html ;
@@ -345,42 +302,44 @@ class EmberApp {
345302 } ) ;
346303
347304 let doc = bootOptions . document ;
305+ let result = new Result ( doc , html , fastbootInfo ) ;
348306
349- let result = new Result ( {
350- doc : doc ,
351- html : html ,
352- fastbootInfo : fastbootInfo ,
353- } ) ;
354-
307+ // TODO: Use Promise.race here
355308 let destroyAppInstanceTimer ;
356309 if ( destroyAppInstanceInMs > 0 ) {
357310 // start a timer to destroy the appInstance forcefully in the given ms.
358311 // This is a failure mechanism so that node process doesn't get wedged if the `visit` never completes.
359312 destroyAppInstanceTimer = setTimeout ( function ( ) {
360- if ( result . _destroyAppInstance ( ) ) {
313+ if ( result . _destroy ( ) ) {
361314 result . error = new Error (
362315 'App instance was forcefully destroyed in ' + destroyAppInstanceInMs + 'ms'
363316 ) ;
364317 }
365318 } , destroyAppInstanceInMs ) ;
366319 }
367320
368- return this . visitRoute ( path , fastbootInfo , bootOptions , result )
369- . then ( ( ) => {
370- if ( ! disableShoebox ) {
371- // if shoebox is not disabled, then create the shoebox and send API data
372- createShoebox ( doc , fastbootInfo ) ;
373- }
374- } )
375- . catch ( error => ( result . error = error ) )
376- . then ( ( ) => result . _finalize ( ) )
377- . finally ( ( ) => {
378- if ( result . _destroyAppInstance ( ) ) {
379- if ( destroyAppInstanceTimer ) {
380- clearTimeout ( destroyAppInstanceTimer ) ;
381- }
382- }
383- } ) ;
321+ try {
322+ await this . visitRoute ( path , fastbootInfo , bootOptions , result ) ;
323+
324+ if ( ! disableShoebox ) {
325+ // if shoebox is not disabled, then create the shoebox and send API data
326+ createShoebox ( doc , fastbootInfo ) ;
327+ }
328+
329+ result . _finalize ( ) ;
330+ } catch ( error ) {
331+ // eslint-disable-next-line require-atomic-updates
332+ result . error = error ;
333+ } finally {
334+ // ensure we invoke `Ember.Application.destroy()` and
335+ // `Ember.ApplicationInstance.destroy()`, but use `result._destroy()` so
336+ // that the `result` object's internal `this.isDestroyed` flag is correct
337+ result . _destroy ( ) ;
338+
339+ clearTimeout ( destroyAppInstanceTimer ) ;
340+ }
341+
342+ return result ;
384343 }
385344
386345 /**
@@ -455,14 +414,14 @@ class EmberApp {
455414 } ) ;
456415
457416 return {
458- appFiles : appFiles ,
459- vendorFiles : vendorFiles ,
417+ appFiles,
418+ vendorFiles,
460419 htmlFile : path . join ( distPath , manifest . htmlFile ) ,
461420 moduleWhitelist : pkg . fastboot . moduleWhitelist ,
462421 hostWhitelist : pkg . fastboot . hostWhitelist ,
463- config : config ,
464- appName : appName ,
465- schemaVersion : schemaVersion ,
422+ config,
423+ appName,
424+ schemaVersion,
466425 } ;
467426 }
468427
@@ -553,4 +512,11 @@ function registerFastBootInfo(info, instance) {
553512 info . register ( instance ) ;
554513}
555514
515+ function buildScripts ( filePaths ) {
516+ return filePaths . filter ( Boolean ) . map ( filePath => {
517+ let source = fs . readFileSync ( filePath , { encoding : 'utf8' } ) ;
518+
519+ return new vm . Script ( source , { filename : filePath } ) ;
520+ } ) ;
521+ }
556522module . exports = EmberApp ;
0 commit comments