This repository was archived by the owner on Aug 9, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 49
/
Copy pathmain.js
321 lines (273 loc) · 13 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
'use strict';
var url = require('url');
var angularcontext = require('angularcontext');
var ngoverrides = require('./ngoverrides.js');
var escapeHtml = require('escape-html');
function sendError(error, response) {
console.error(error.stack);
response.writeHead(
500,
{
'Content-Type': 'text/plain'
}
);
response.end(
error.stack
);
}
function makeRunInContext(serverScripts, angularModules, prepContext, template) {
return function (func) {
var context = angularcontext.Context(template);
context.runFiles(
serverScripts,
function (success, error) {
if (error) {
context.dispose();
func(undefined, error);
}
prepContext(
context,
function () {
ngoverrides.registerModule(context);
var angular = context.getAngular();
var modules = angular.copy(angularModules);
modules.unshift('angularjs-server');
modules.unshift('ng');
var $injector = context.injector(modules);
// Although the called module will primarily use the injector, we
// also give it indirect access to the angular object so it can
// make use of Angular's "global" functions.
$injector.angular = angular;
// The caller must call this when it's finished in order to free the context.
$injector.close = function () {
context.dispose();
};
func($injector);
}
);
}
);
};
}
function middlewareWithAngular(func, serverScripts, angularModules, prepContext, template) {
var runInContext = makeRunInContext(serverScripts, angularModules, prepContext, template);
return function (request, response, next) {
// need to tell the context its absolute base URL so that relative paths will work as expected.
var contextUrl = request.protocol + '://' + request.get('host') + request.url;
runInContext(
function ($injector, error) {
if (error) {
sendError(error, response);
return;
}
// Set window.location.href so that things that go directly to that layer will
// work as expected.
// However, we leave the context's *base* URL set to the default file:// URL,
// since otherwise we can't make xmlhttprequest requests to the filesystem.
var window = $injector.get('$window');
if (window.location) {
window.location.href = contextUrl;
}
// Tell the context about our current request, so $location will work.
var reqContext = $injector.get('serverRequestContext');
reqContext.setRequest(request);
// Override the end() method so that we can automatically dispose the
// context when the request ends.
var previousEnd = response.end;
response.end = function () {
previousEnd.apply(this, arguments);
$injector.close();
};
func(request, response, next, $injector);
}
);
};
}
function makeHtmlGenerator(serverScripts, prepContext, clientScripts, template, modules) {
// starts as an array, and then flattened into a string below
var initShim = [];
initShim.push('<script>');
initShim.push('document.write(');
// FIXME: this fails if the template contains a </script> tag,
// because JSON.stringify doesn't escape that.
initShim.push(JSON.stringify(template));
initShim.push(');');
for (var i = 0; i < clientScripts.length; i++) {
var scriptUrl = url.resolve('/static/', clientScripts[i]);
initShim.push('document.write(\'<script src="');
initShim.push(escapeHtml(scriptUrl));
initShim.push('"></scr\'+\'ipt>\');');
}
initShim.push('</script>');
initShim = initShim.join('');
// Try to envelop the rest of the document in a script, so the browser won't render it.
// This is separated from the initShim because we wait until we have rendered the HTML snapshot
// before we'll return it, so the browser won't block rendering waiting for the end of this
// never-ending script tag.
var hideShim = '<script>document.write("<script type=\'dummy/x-hide-document\'>")</script>';
return middlewareWithAngular(
function (request, response, next, injector) {
// bootstrap the injector against the root element
injector.bootstrap();
var $rootScope = injector.get('$rootScope');
var $httpBackend = injector.get('$httpBackend');
var $route = injector.get('$route');
var element = injector.get('$rootElement');
var reqContext = injector.get('serverRequestContext');
var path = reqContext.location.path();
var search = reqContext.location.search();
//Fixing trailing slash issue - when navigating from home to any city (ex: boston) and reload the page, somehow '\' is added to the end of location.href, which breaks the routing.
//This code will remove the trailing slash
//Taken from https://github.com/angular-ui/ui-router/issues/50#issuecomment-64895625
var re = /(.+)(\/+)(\?.*)?$/
if(re.test(path)) {
path = path.replace(re, '$1$3')
}
//End of fix
var matchedRoute = $route.getByPath(path, search);
if (matchedRoute) {
// just in case the app depends on this event to do some cleanup/initialization
$route.current = matchedRoute;
$rootScope.$broadcast(
'$routeChangeStart',
matchedRoute,
undefined
);
$rootScope.$digest();
// making a JSON-friendly route has the side-effect of waiting for all of
// the route locals to resolve, which we're depending on here.
// Right now this actually flattens the promises in the route locals as a side-effect,
// which may or may not be a problem. Keeping it this way for now because at least it
// means the controller running on the server will see the same thing as the controller
// running on the client when we're using server-defined routes.
matchedRoute.jsonFriendly().then(
function (retRoute) {
// JSON stringification will fail if the resolve function produces
// something we can't serialize as JSON.
var jsonRetRoute;
try {
jsonRetRoute = JSON.stringify(retRoute);
}
catch (e) {
response.writeHead(500);
response.end('Route data could not be rendered as JSON');
return;
}
var preResolvedHtml = [
'<script>var initialRoute = ',
// FIXME: this fails if the template contains a </script> tag,
// because JSON.stringify doesn't escape that.
jsonRetRoute,
'</script>'
].join('');
// Return the part of the response that browsers care about as soon as it's
// ready. Then we'll generate the robot-oriented snapshot below.
// Of course this doesn't really help any when the client is getting this
// data from a reverse-proxy cache in front of the app, but it allows the
// page to start rendering sooner if we're talking directly to a browser.
response.set('Content-Type', 'text/html; charset=utf-8');
response.writeHead(200);
response.write(preResolvedHtml + initShim);
response.write('\n');
$rootScope.$on(
'$viewContentLoaded',
function () {
$httpBackend.notifyWhenNoOpenRequests(
function () {
$rootScope.$digest();
var container = injector.angular.element('<div></div>');
container.append(element);
// We remove all scripts from the snapshot, because the snapshot
// is intended for browsers that don't support JS and also this
// avoids us accidentally terminating our "hideShim" script that
// makes the HTML snapshot to JS-aware browsers.
container.find('script').remove();
var staticSnapshot = container.html();
container = null;
response.end(hideShim + staticSnapshot);
}
);
}
);
$rootScope.$broadcast(
'$routeChangeSuccess',
matchedRoute,
undefined
);
}
);
}
else {
// TODO: render the 'otherwise' route as the 404 response body.
response.writeHead(404);
response.end('Not found');
}
},
serverScripts,
modules,
prepContext,
template
);
}
function makeSdrApi(serverScripts, prepContext, modules) {
return middlewareWithAngular(
function (request, response, next, injector) {
var reqContext = injector.get('serverRequestContext');
var path = reqContext.location.path();
var search = reqContext.location.search();
var $route = injector.get('$route');
var matchedRoute = $route.getByPath(path, search);
if (matchedRoute) {
matchedRoute.jsonFriendly().then(
function (retRoute) {
var ret = {};
ret.route = retRoute;
// JSON stringification will fail if the resolve function produces
// something we can't serialize as JSON.
var jsonRet;
try {
jsonRet = JSON.stringify(ret);
}
catch (e) {
response.writeHead(500);
response.end('Route data could not be rendered as JSON');
return;
}
response.writeHead(200);
response.end(jsonRet);
},
function (error) {
response.writeHead(200);
response.end('{}');
}
);
}
else {
response.writeHead(200);
response.end('{}');
}
},
serverScripts,
modules,
prepContext,
// don't actually need a template for the SDR API, but
// need to provide enough DOM that AngularJS can find the document and the body.
'<html><head></head><body></body></html>'
);
}
function makeInstance(options) {
var template = options.template;
var serverScripts = options.serverScripts;
var prepContext = options.prepContext || function (context, callback) { callback(); };
var clientScripts = options.clientScripts || [];
var angularModules = options.angularModules || [];
return {
htmlGenerator: makeHtmlGenerator(serverScripts, prepContext, clientScripts, template, angularModules),
sdrApi: makeSdrApi(serverScripts, prepContext, angularModules),
wrapMiddlewareWithAngular: function (func) {
return middlewareWithAngular(func, serverScripts, angularModules, prepContext, template);
},
runInContext: makeRunInContext(serverScripts, angularModules, prepContext, template)
};
}
exports.Server = makeInstance;