Skip to content

Commit ff877c7

Browse files
authored
Merge pull request #91 from kratiahuja/li-refactor
Refactor Fastboot's visit API to take additional options
2 parents c2e9fd4 + 5e2d1f4 commit ff877c7

8 files changed

Lines changed: 162 additions & 21 deletions

File tree

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
"fs-promise": "^0.5.0",
5252
"mocha": "^2.4.5",
5353
"mocha-jshint": "^2.3.1",
54-
"request-promise": "^2.0.1",
5554
"temp": "^0.8.3"
5655
}
5756
}

src/ember-app.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,20 +190,29 @@ class EmberApp {
190190
* @param {string} path the URL path to render, like `/photos/1`
191191
* @param {Object} options
192192
* @param {string} [options.html] the HTML document to insert the rendered app into
193+
* @param {Object} [options.metadata] Per request specific data used in the app.
194+
* @param {Boolean} [options.shouldRender] whether the app should do rendering or not. If set to false, it puts the app in routing-only.
195+
* @param {Boolean} [options.disableShoebox] whether we should send the API data in the shoebox. If set to false, it will not send the API data used for rendering the app on server side in the index.html.
196+
* @param {Integer} [options.destroyAppInstanceInMs] whether to destroy the instance in the given number of ms. This is a failure mechanism to not wedge the Node process (See: https://github.com/ember-fastboot/fastboot/issues/90)
193197
* @param {ClientRequest}
198+
* @param {ClientResponse}
194199
* @returns {Promise<Result>} result
195200
*/
196201
visit(path, options) {
197202
let req = options.request;
198203
let res = options.response;
199204
let html = options.html || this.html;
205+
let disableShoebox = options.disableShoebox || false;
206+
let destroyAppInstanceInMs = options.destroyAppInstanceInMs;
200207

201-
let bootOptions = buildBootOptions();
208+
let shouldRender = (options.shouldRender !== undefined) ? options.shouldRender : true;
209+
let bootOptions = buildBootOptions(shouldRender);
202210
let fastbootInfo = new FastBootInfo(
203211
req,
204212
res,
205213
{ hostWhitelist: this.hostWhitelist, metadata: options.metadata }
206214
);
215+
207216
let doc = bootOptions.document;
208217

209218
let instance;
@@ -214,6 +223,19 @@ class EmberApp {
214223
fastbootInfo: fastbootInfo
215224
});
216225

226+
let destroyAppInstanceTimer;
227+
if (parseInt(destroyAppInstanceInMs, 10) > 0) {
228+
// start a timer to destroy the appInstance forcefully in the given ms.
229+
// This is a failure mechanism so that node process doesn't get wedged if the `visit` never completes.
230+
destroyAppInstanceTimer = setTimeout(function() {
231+
if (instance && !result.instanceDestroyed) {
232+
result.instanceDestroyed = true;
233+
result.error = new Error('App instance was forcefully destroyed in ' + destroyAppInstanceInMs + 'ms');
234+
instance.destroy();
235+
}
236+
}, destroyAppInstanceInMs);
237+
}
238+
217239
return this.buildAppInstance()
218240
.then(appInstance => {
219241
instance = appInstance;
@@ -225,12 +247,22 @@ class EmberApp {
225247
.then(() => result.instanceBooted = true)
226248
.then(() => instance.visit(path, bootOptions))
227249
.then(() => waitForApp(instance))
228-
.then(() => createShoebox(doc, fastbootInfo))
250+
.then(() => {
251+
if (!disableShoebox) {
252+
// if shoebox is not disabled, then create the shoebox and send API data
253+
createShoebox(doc, fastbootInfo);
254+
}
255+
})
229256
.catch(error => result.error = error)
230257
.then(() => result._finalize())
231258
.finally(() => {
232-
if (instance) {
259+
if (instance && !result.instanceDestroyed) {
260+
result.instanceDestroyed = true;
233261
instance.destroy();
262+
263+
if (destroyAppInstanceTimer) {
264+
clearTimeout(destroyAppInstanceTimer);
265+
}
234266
}
235267
});
236268
}
@@ -274,14 +306,15 @@ class EmberApp {
274306
* Builds an object with the options required to boot an ApplicationInstance in
275307
* FastBoot mode.
276308
*/
277-
function buildBootOptions() {
309+
function buildBootOptions(shouldRender) {
278310
let doc = new SimpleDOM.Document();
279311
let rootElement = doc.body;
280312

281313
return {
282314
isBrowser: false,
283315
document: doc,
284-
rootElement: rootElement
316+
rootElement,
317+
shouldRender
285318
};
286319
}
287320

src/fastboot-info.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ var FastBootResponse = require('./fastboot-response');
66
* A class that encapsulates information about the
77
* current HTTP request from FastBoot. This is injected
88
* on to the FastBoot service.
9+
*
10+
* @param {ClientRequest} the incoming request object
11+
* @param {ClientResponse} the response object
12+
* @param {Object} additional options passed to fastboot info
13+
* @param {Array} [options.hostWhitelist] expected hosts in your application
14+
* @param {Object} [options.metaData] per request meta data
915
*/
10-
function FastBootInfo(request, response, options = {}) {
11-
const { hostWhitelist, metadata } = options;
16+
function FastBootInfo(request, response, options) {
1217

1318
this.deferredPromise = RSVP.resolve();
19+
let { hostWhitelist, metadata } = options;
1420
if (request) {
1521
this.request = new FastBootRequest(request, hostWhitelist);
1622
}

src/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ class FastBoot {
5656
* @param {Object} options
5757
* @param {Boolean} [options.resilient] whether to reject the returned promise if there is an error during rendering. Overrides the instance's `resilient` setting
5858
* @param {string} [options.html] the HTML document to insert the rendered app into. Uses the built app's index.html by default.
59+
* @param {Object} [options.metadata] per request meta data that need to be exposed in the app.
60+
* @param {Boolean} [options.shouldRender] whether the app should do rendering or not. If set to false, it puts the app in routing-only.
61+
* @param {Boolean} [options.disableShoebox] whether we should send the API data in the shoebox. If set to false, it will not send the API data used for rendering the app on server side in the index.html.
62+
* @param {Integer} [options.destroyAppInstanceInMs] whether to destroy the instance in the given number of ms. This is a failure mechanism to not wedge the Node process (See: https://github.com/ember-fastboot/fastboot/issues/90)
5963
* @returns {Promise<Result>} result
6064
*/
6165
visit(path, options) {

src/result.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const HTMLSerializer = new SimpleDOM.HTMLSerializer(SimpleDOM.voidMap);
1111
class Result {
1212
constructor(options) {
1313
this.instanceBooted = false;
14+
this.instanceDestroyed = false;
1415
this._doc = options.doc;
1516
this._html = options.html;
1617
this._fastbootInfo = options.fastbootInfo;
@@ -45,6 +46,18 @@ class Result {
4546
return Promise.resolve(insertIntoIndexHTML(this._html, this._head, this._body));
4647
}
4748

49+
/**
50+
* Returns the serialized representation of DOM HEAD and DOM BODY
51+
*
52+
* @returns {Object} serialized version of DOM
53+
*/
54+
domContents() {
55+
return {
56+
head: this._head,
57+
body: this._body
58+
};
59+
}
60+
4861
/**
4962
* @private
5063
*

test/fastboot-info-test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ describe("FastBootInfo", function() {
99
var response;
1010
var request;
1111
var fastbootInfo;
12-
var metadata = { foo: 'bar' };
12+
var metadata = {
13+
'foo': 'bar',
14+
'baz': 'apple'
15+
};
1316

1417
beforeEach(function () {
1518
response = {};
@@ -34,6 +37,7 @@ describe("FastBootInfo", function() {
3437
expect(fastbootInfo.response).to.be.an.instanceOf(FastBootResponse);
3538
});
3639

40+
3741
it("has metadata", function() {
3842
expect(fastbootInfo.metadata).to.deep.equal(metadata);
3943
});

test/fastboot-shoebox-test.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
const expect = require('chai').expect;
44
const fs = require('fs');
55
const path = require('path');
6-
const request = require('request-promise');
7-
const TestHTTPServer = require('./helpers/test-http-server');
6+
const fixture = require('./helpers/fixture-path');
87
const alchemistRequire = require('broccoli-module-alchemist/require');
98
const FastBoot = alchemistRequire('index');
109

@@ -28,8 +27,44 @@ describe("FastBootShoebox", function() {
2827
});
2928
});
3029

31-
});
30+
it("can render the escaped shoebox HTML with shouldRender set to false", function() {
31+
var fastboot = new FastBoot({
32+
distPath: fixture('shoebox')
33+
});
3234

33-
function fixture(fixtureName) {
34-
return path.join(__dirname, '/fixtures/', fixtureName);
35-
}
35+
return fastboot.visit('/', {
36+
shouldRender: false
37+
})
38+
.then(r => r.html())
39+
.then(html => {
40+
expect(html).to.match(/<script type="fastboot\/shoebox" id="shoebox-key1">{"foo":"bar"}<\/script>/);
41+
expect(html).to.match(/<script type="fastboot\/shoebox" id="shoebox-key2">{"zip":"zap"}<\/script>/);
42+
43+
// Special characters are JSON encoded, most notably the </script sequence.
44+
expect(html).to.include('<script type="fastboot/shoebox" id="shoebox-key4">{"nastyScriptCase":"\\u003cscript\\u003ealert(\'owned\');\\u003c/script\\u003e\\u003c/script\\u003e\\u003c/script\\u003e"}</script>');
45+
46+
expect(html).to.include('<script type="fastboot/shoebox" id="shoebox-key5">{"otherUnicodeChars":"\\u0026\\u0026\\u003e\\u003e\\u003c\\u003c\\u2028\\u2028\\u2029\\u2029"}</script>');
47+
});
48+
});
49+
50+
it("cannot render the escaped shoebox HTML when disableShoebox is set to true", function() {
51+
var fastboot = new FastBoot({
52+
distPath: fixture('shoebox')
53+
});
54+
55+
return fastboot.visit('/', {
56+
disableShoebox: true
57+
})
58+
.then(r => r.html())
59+
.then(html => {
60+
expect(html).to.not.match(/<script type="fastboot\/shoebox" id="shoebox-key1">{"foo":"bar"}<\/script>/);
61+
expect(html).to.not.match(/<script type="fastboot\/shoebox" id="shoebox-key2">{"zip":"zap"}<\/script>/);
62+
63+
// Special characters are JSON encoded, most notably the </script sequence.
64+
expect(html).to.not.include('<script type="fastboot/shoebox" id="shoebox-key4">{"nastyScriptCase":"\\u003cscript\\u003ealert(\'owned\');\\u003c/script\\u003e\\u003c/script\\u003e\\u003c/script\\u003e"}</script>');
65+
66+
expect(html).to.not.include('<script type="fastboot/shoebox" id="shoebox-key5">{"otherUnicodeChars":"\\u0026\\u0026\\u003e\\u003e\\u003c\\u003c\\u2028\\u2028\\u2029\\u2029"}</script>');
67+
});
68+
});
69+
70+
});

test/fastboot-test.js

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
const expect = require('chai').expect;
44
const fs = require('fs');
55
const path = require('path');
6-
const request = require('request-promise');
7-
const TestHTTPServer = require('./helpers/test-http-server');
6+
const fixture = require('./helpers/fixture-path');
87
const alchemistRequire = require('broccoli-module-alchemist/require');
98
const FastBoot = alchemistRequire('index');
109

@@ -51,6 +50,47 @@ describe("FastBoot", function() {
5150
});
5251
});
5352

53+
it("cannot not render app HTML with shouldRender set as false", function() {
54+
var fastboot = new FastBoot({
55+
distPath: fixture('basic-app')
56+
});
57+
58+
return fastboot.visit('/', {
59+
shouldRender: false
60+
})
61+
.then(r => r.html())
62+
.then(html => {
63+
expect(html).to.not.match(/Welcome to Ember/);
64+
});
65+
});
66+
67+
it("can serialize the head and body", function() {
68+
var fastboot = new FastBoot({
69+
distPath: fixture('basic-app')
70+
});
71+
72+
return fastboot.visit('/')
73+
.then((r) => {
74+
let contents = r.domContents();
75+
76+
expect(contents.head).to.equal('');
77+
expect(contents.body).to.match(/Welcome to Ember/);
78+
});
79+
});
80+
81+
it("can forcefully destroy the app instance using destroyAppInstanceInMs", function() {
82+
var fastboot = new FastBoot({
83+
distPath: fixture('basic-app')
84+
});
85+
86+
return fastboot.visit('/', {
87+
destroyAppInstanceInMs: 5
88+
})
89+
.catch((e) => {
90+
expect(e.message).to.equal('App instance was forcefully destroyed in 5ms');
91+
});
92+
});
93+
5494
it("rejects the promise if an error occurs", function() {
5595
var fastboot = new FastBoot({
5696
distPath: fixture('rejected-promise')
@@ -59,6 +99,17 @@ describe("FastBoot", function() {
5999
return expect(fastboot.visit('/')).to.be.rejected;
60100
});
61101

102+
it("catches the error if an error occurs", function() {
103+
var fastboot = new FastBoot({
104+
distPath: fixture('rejected-promise')
105+
});
106+
107+
fastboot.visit('/')
108+
.catch(function(err) {
109+
return expect(err).to.be.not.null;
110+
});
111+
});
112+
62113
it("renders an empty page if the resilient flag is set", function() {
63114
var fastboot = new FastBoot({
64115
distPath: fixture('rejected-promise'),
@@ -188,7 +239,3 @@ describe("FastBoot", function() {
188239
});
189240

190241
});
191-
192-
function fixture(fixtureName) {
193-
return path.join(__dirname, '/fixtures/', fixtureName);
194-
}

0 commit comments

Comments
 (0)