@salesforce/pwa-kit-runtime 3.8.0-preview.0-basepath → 3.8.0-preview.2-basepath

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/package.json +6 -5
  2. package/ssr/server/build-remote-server.js +1159 -0
  3. package/ssr/server/build-remote-server.test.js +41 -0
  4. package/ssr/server/constants.js +37 -0
  5. package/ssr/server/express.js +462 -0
  6. package/ssr/server/express.lambda.test.js +390 -0
  7. package/ssr/server/express.test.js +963 -0
  8. package/ssr/server/test_fixtures/favicon.ico +0 -0
  9. package/ssr/server/test_fixtures/loadable-stats.json +1 -0
  10. package/ssr/server/test_fixtures/localhost.pem +45 -0
  11. package/ssr/server/test_fixtures/main.js +7 -0
  12. package/ssr/server/test_fixtures/mobify.png +0 -0
  13. package/ssr/server/test_fixtures/server-renderer.js +12 -0
  14. package/ssr/server/test_fixtures/worker.js +7 -0
  15. package/ssr/server/test_fixtures/worker.js.map +1 -0
  16. package/utils/logger-factory.js +154 -0
  17. package/utils/logger-factory.test.js +71 -0
  18. package/utils/logger-instance.js +19 -0
  19. package/utils/middleware/index.js +16 -0
  20. package/utils/middleware/security.js +111 -0
  21. package/utils/middleware/security.test.js +110 -0
  22. package/utils/morgan-stream.js +28 -0
  23. package/utils/ssr-cache.js +177 -0
  24. package/utils/ssr-cache.test.js +64 -0
  25. package/utils/ssr-config.client.js +23 -0
  26. package/utils/ssr-config.client.test.js +25 -0
  27. package/utils/ssr-config.js +20 -0
  28. package/utils/ssr-config.server.js +98 -0
  29. package/utils/ssr-config.server.test.js +50 -0
  30. package/utils/ssr-namespace-paths.js +60 -0
  31. package/utils/ssr-namespace-paths.test.js +30 -0
  32. package/utils/ssr-paths.js +51 -0
  33. package/utils/ssr-paths.test.js +49 -0
  34. package/utils/ssr-proxying.js +859 -0
  35. package/utils/ssr-proxying.test.js +593 -0
  36. package/utils/ssr-request-processing.js +164 -0
  37. package/utils/ssr-request-processing.test.js +95 -0
  38. package/utils/ssr-server/cached-response.js +116 -0
  39. package/utils/ssr-server/configure-proxy.js +304 -0
  40. package/utils/ssr-server/metrics-sender.js +204 -0
  41. package/utils/ssr-server/outgoing-request-hook.js +139 -0
  42. package/utils/ssr-server/parse-end-parameters.js +38 -0
  43. package/utils/ssr-server/process-express-response.js +56 -0
  44. package/utils/ssr-server/process-lambda-response.js +43 -0
  45. package/utils/ssr-server/update-global-agent-options.js +41 -0
  46. package/utils/ssr-server/update-global-agent-options.test.js +28 -0
  47. package/utils/ssr-server/utils.js +124 -0
  48. package/utils/ssr-server/utils.test.js +69 -0
  49. package/utils/ssr-server/wrap-response-write.js +40 -0
  50. package/utils/ssr-server.js +115 -0
  51. package/utils/ssr-server.test.js +869 -0
  52. package/utils/ssr-shared.js +183 -0
@@ -0,0 +1,1159 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.once = exports.RemoteServerFactory = exports.REMOTE_REQUIRED_ENV_VARS = void 0;
7
+ var _path = _interopRequireDefault(require("path"));
8
+ var _constants = require("./constants");
9
+ var _ssrServer = require("../../utils/ssr-server");
10
+ var _dns = _interopRequireDefault(require("dns"));
11
+ var _express = _interopRequireDefault(require("express"));
12
+ var _ssrCache = require("../../utils/ssr-cache");
13
+ var _mergeDescriptors = _interopRequireDefault(require("merge-descriptors"));
14
+ var _url = _interopRequireDefault(require("url"));
15
+ var _ssrProxying = require("../../utils/ssr-proxying");
16
+ var _assert = _interopRequireDefault(require("assert"));
17
+ var _semver = _interopRequireDefault(require("semver"));
18
+ var _package = _interopRequireDefault(require("../../package.json"));
19
+ var _fs = _interopRequireDefault(require("fs"));
20
+ var _express2 = require("./express");
21
+ var _http = _interopRequireDefault(require("http"));
22
+ var _https = _interopRequireDefault(require("https"));
23
+ var _ssrShared = require("../../utils/ssr-shared");
24
+ var _ssrPaths = require("../../utils/ssr-paths");
25
+ var _configureProxy = require("../../utils/ssr-server/configure-proxy");
26
+ var _awsServerlessExpress = _interopRequireDefault(require("aws-serverless-express"));
27
+ var _morgan = _interopRequireDefault(require("morgan"));
28
+ var _loggerInstance = _interopRequireDefault(require("../../utils/logger-instance"));
29
+ var _httpProxyMiddleware = require("http-proxy-middleware");
30
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
31
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
32
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
33
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
34
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
35
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
36
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } /*
37
+ * Copyright (c) 2022, Salesforce, Inc.
38
+ * All rights reserved.
39
+ * SPDX-License-Identifier: BSD-3-Clause
40
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
41
+ */
42
+ /**
43
+ * An Array of mime-types (Content-Type values) that are considered
44
+ * as binary by awsServerlessExpress when processing responses.
45
+ * We intentionally exclude all text/* values since we assume UTF8
46
+ * encoding and there's no reason to bulk up the response by base64
47
+ * encoding the result.
48
+ *
49
+ * We can use '*' in these types as a wildcard - see
50
+ * https://www.npmjs.com/package/type-is#type--typeisismediatype-types
51
+ *
52
+ * @private
53
+ */
54
+ const binaryMimeTypes = ['application/*', 'audio/*', 'font/*', 'image/*', 'video/*'];
55
+
56
+ /**
57
+ * Environment variables that must be set for the Express app to run remotely.
58
+ *
59
+ * @private
60
+ */
61
+ const REMOTE_REQUIRED_ENV_VARS = exports.REMOTE_REQUIRED_ENV_VARS = ['BUNDLE_ID', 'DEPLOY_TARGET', 'EXTERNAL_DOMAIN_NAME', 'MOBIFY_PROPERTY_ID'];
62
+ const METRIC_DIMENSIONS = {
63
+ Project: process.env.MOBIFY_PROPERTY_ID,
64
+ Target: process.env.DEPLOY_TARGET
65
+ };
66
+
67
+ /**
68
+ * @private
69
+ */
70
+ const RemoteServerFactory = exports.RemoteServerFactory = {
71
+ /**
72
+ * @private
73
+ */
74
+ _configure(options) {
75
+ /**
76
+ * Not all of these options are documented. Some exist to allow for
77
+ * testing, or to handle non-standard projects.
78
+ */
79
+ const defaults = {
80
+ // For test only – allow the project dir to be overridden.
81
+ projectDir: process.cwd(),
82
+ // Absolute path to the build directory
83
+ buildDir: _path.default.resolve(process.cwd(), _constants.BUILD),
84
+ // The cache time for SSR'd pages (defaults to 600 seconds)
85
+ defaultCacheTimeSeconds: 600,
86
+ // The port that the local dev server listens on
87
+ port: 3443,
88
+ // The protocol that the local dev server listens on
89
+ protocol: 'https',
90
+ // Whether or not to use a keep alive agent for proxy connections.
91
+ proxyKeepAliveAgent: true,
92
+ // Quiet flag (suppresses output if true)
93
+ quiet: false,
94
+ // Suppress SSL checks - can be used for local dev server
95
+ // test code. Undocumented at present because there should
96
+ // be no use-case for SDK users to set this.
97
+ strictSSL: true,
98
+ mobify: undefined,
99
+ // Toggle cookies being passed and set
100
+ localAllowCookies: false,
101
+ // Toggle for setting up the custom SLAS private client secret handler
102
+ useSLASPrivateClient: false,
103
+ // A regex for identifying which SLAS endpoints the custom SLAS private
104
+ // client secret handler will inject an Authorization header.
105
+ // Do not modify unless a project wants to customize additional SLAS
106
+ // endpoints that we currently do not support (ie. /oauth2/passwordless/token)
107
+ applySLASPrivateClientToEndpoints: /\/oauth2\/token/
108
+ };
109
+ options = _extends({}, defaults, options);
110
+ (0, _ssrServer.setQuiet)(options.quiet || process.env.SSR_QUIET);
111
+
112
+ // Set the protocol for the Express app listener - defaults to https on remote
113
+ options.protocol = this._getProtocol(options);
114
+
115
+ // Local dev server doesn't cache by default
116
+ options.defaultCacheControl = this._getDefaultCacheControl(options);
117
+
118
+ // Ensure this is a boolean, and is always true for a remote server.
119
+ options.strictSSL = this._strictSSL(options);
120
+
121
+ // This is the external HOSTNAME under which we are serving the page.
122
+ // The EXTERNAL_DOMAIN_NAME value technically only applies to remote
123
+ // operation, but we allow it to be used for a local dev server also.
124
+ options.appHostname = process.env.EXTERNAL_DOMAIN_NAME || `localhost:${options.port}`;
125
+ options.devServerHostName = process.env.LISTEN_ADDRESS || `localhost:${options.port}`;
126
+
127
+ // This is the ORIGIN under which we are serving the page.
128
+ // because it's an origin, it does not end with a slash.
129
+ options.appOrigin = process.env.APP_ORIGIN = `${options.protocol}://${options.appHostname}`;
130
+
131
+ // Toggle cookies being passed and set. Can be overridden locally,
132
+ // always uses MRT_ALLOW_COOKIES env remotely
133
+ options.allowCookies = this._getAllowCookies(options);
134
+
135
+ // For test only – configure the SLAS private client secret proxy endpoint
136
+ options.slasHostName = this._getSlasEndpoint(options);
137
+ options.slasTarget = options.slasTarget || `https://${options.slasHostName}`;
138
+ return options;
139
+ },
140
+ /**
141
+ * @private
142
+ */
143
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
144
+ _logStartupMessage(options) {
145
+ // Hook for the DevServer
146
+ },
147
+ /**
148
+ * @private
149
+ */
150
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
151
+ _getAllowCookies(options) {
152
+ return 'MRT_ALLOW_COOKIES' in process.env ? process.env.MRT_ALLOW_COOKIES == 'true' : false;
153
+ },
154
+ /**
155
+ * @private
156
+ */
157
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
158
+ _getProtocol(options) {
159
+ return 'https';
160
+ },
161
+ /**
162
+ * @private
163
+ */
164
+ _getDefaultCacheControl(options) {
165
+ return `max-age=${options.defaultCacheTimeSeconds}, s-maxage=${options.defaultCacheTimeSeconds}`;
166
+ },
167
+ /**
168
+ * @private
169
+ */
170
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
171
+ _strictSSL(options) {
172
+ return true;
173
+ },
174
+ /**
175
+ * @private
176
+ */
177
+ _isBundleOrProxyPath(url) {
178
+ return url.startsWith((0, _ssrPaths.getProxyPath)()) || url.startsWith((0, _ssrPaths.getBundlePath)());
179
+ },
180
+ /**
181
+ * @private
182
+ */
183
+ _getSlasEndpoint(options) {
184
+ var _options$mobify, _options$mobify$app, _options$mobify$app$c, _options$mobify$app$c2;
185
+ if (!options.useSLASPrivateClient) return undefined;
186
+ const shortCode = (_options$mobify = options.mobify) === null || _options$mobify === void 0 ? void 0 : (_options$mobify$app = _options$mobify.app) === null || _options$mobify$app === void 0 ? void 0 : (_options$mobify$app$c = _options$mobify$app.commerceAPI) === null || _options$mobify$app$c === void 0 ? void 0 : (_options$mobify$app$c2 = _options$mobify$app$c.parameters) === null || _options$mobify$app$c2 === void 0 ? void 0 : _options$mobify$app$c2.shortCode;
187
+ return `${shortCode}.api.commercecloud.salesforce.com`;
188
+ },
189
+ /**
190
+ * @private
191
+ */
192
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
193
+ _setCompression(app) {
194
+ // Let the CDN do it
195
+ },
196
+ /**
197
+ * @private
198
+ */
199
+
200
+ _setupLogging(app) {
201
+ const morganLoggerFormat = function (tokens, req, res) {
202
+ const contentLength = tokens.res(req, res, 'content-length');
203
+ return [`(${res.locals.requestId})`, tokens.method(req, res), tokens.url(req, res), tokens.status(req, res), tokens['response-time'](req, res), 'ms', contentLength && `- ${contentLength}`].join(' ');
204
+ };
205
+
206
+ // Morgan stream for logging status codes less than 400
207
+ app.use((0, _morgan.default)(morganLoggerFormat, {
208
+ skip: function (req, res) {
209
+ return res.statusCode >= 400;
210
+ },
211
+ stream: {
212
+ write: message => {
213
+ _loggerInstance.default.info(message, {
214
+ namespace: 'httprequest'
215
+ });
216
+ }
217
+ }
218
+ }));
219
+
220
+ // Morgan stream for logging status codes 400 and above
221
+ app.use((0, _morgan.default)(morganLoggerFormat, {
222
+ skip: function (req, res) {
223
+ return res.statusCode < 400;
224
+ },
225
+ stream: {
226
+ write: message => {
227
+ _loggerInstance.default.error(message, {
228
+ namespace: 'httprequest'
229
+ });
230
+ }
231
+ }
232
+ }));
233
+ },
234
+ /**
235
+ * Passing the correlation Id from MRT to locals
236
+ * @private
237
+ */
238
+ _setRequestId(app) {
239
+ app.use((req, res, next) => {
240
+ const correlationId = req.headers['x-correlation-id'];
241
+ const requestId = correlationId ? correlationId : req.headers['x-apigateway-event'];
242
+ if (!requestId) {
243
+ _loggerInstance.default.error('Both x-correlation-id and x-apigateway-event headers are missing', {
244
+ namespace: '_setRequestId'
245
+ });
246
+ next();
247
+ return;
248
+ }
249
+ res.locals.requestId = requestId;
250
+ next();
251
+ });
252
+ },
253
+ /**
254
+ * @private
255
+ */
256
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
257
+ _setupMetricsFlushing(app) {
258
+ // Hook for the dev-server
259
+ },
260
+ /**
261
+ * @private
262
+ */
263
+ _updatePackageMobify(options) {
264
+ (0, _ssrShared.updatePackageMobify)(options.mobify);
265
+ },
266
+ /**
267
+ * @private
268
+ */
269
+ _configureProxyConfigs(options) {
270
+ (0, _ssrServer.configureProxyConfigs)(options.appHostname, options.protocol);
271
+ },
272
+ /**
273
+ * @private
274
+ */
275
+ _createApp(options) {
276
+ options = this._configure(options);
277
+ this._logStartupMessage(options);
278
+
279
+ // To gain a small speed increase in the event that this
280
+ // server needs to make a proxy request back to itself,
281
+ // we kick off a DNS lookup for the appHostname. We don't
282
+ // wait for it to complete, or care if it fails, so the
283
+ // callback is a no-op.
284
+ _dns.default.lookup(options.appHostname, () => null);
285
+ this._validateConfiguration(options);
286
+ this._updatePackageMobify(options);
287
+ this._configureProxyConfigs(options);
288
+ const app = this._createExpressApp(options);
289
+
290
+ // Do this first – we want compression applied to
291
+ // everything when it's enabled at all.
292
+ this._setCompression(app);
293
+ this._setRequestId(app);
294
+ // this._addEventContext(app)
295
+ // Ordering of the next two calls are vital - we don't
296
+ // want request-processors applied to development views.
297
+ this._addSDKInternalHandlers(app);
298
+ this._setupSSRRequestProcessorMiddleware(app);
299
+ this._setupLogging(app);
300
+ this._setupMetricsFlushing(app);
301
+ this._setupHealthcheck(app);
302
+ this._setupProxying(app, options);
303
+ this._setupSlasPrivateClientProxy(app, options);
304
+
305
+ // Beyond this point, we know that this is not a proxy request
306
+ // and not a bundle request, so we can apply specific
307
+ // processing.
308
+ this._setupCommonMiddleware(app, options);
309
+ this._addStaticAssetServing(app);
310
+ this._addDevServerGarbageCollection(app);
311
+ return app;
312
+ },
313
+ /**
314
+ * @private
315
+ */
316
+ _createExpressApp(options) {
317
+ const app = (0, _express.default)();
318
+ app.disable('x-powered-by');
319
+ const mixin = {
320
+ options,
321
+ _collectGarbage() {
322
+ // Do global.gc in a separate 'then' handler so
323
+ // that all major variables are out of scope and
324
+ // eligible for garbage collection.
325
+
326
+ /* istanbul ignore next */
327
+ let gcTime = 0;
328
+ /* istanbul ignore next */
329
+ if (global.gc) {
330
+ const start = Date.now();
331
+ global.gc();
332
+ gcTime = Date.now() - start;
333
+ }
334
+ this.sendMetric('GCTime', gcTime, 'Milliseconds');
335
+ },
336
+ _requestMonitor: new RequestMonitor(),
337
+ metrics: _ssrServer.MetricsSender.getSender(),
338
+ /**
339
+ * Send a metric with fixed dimensions. See MetricsSender.send for more details.
340
+ *
341
+ * @private
342
+ * @param name {String} metric name
343
+ * @param [value] {Number} metric value (defaults to 1)
344
+ * @param [unit] {String} defaults to 'Count'
345
+ * @param [dimensions] {Object} optional extra dimensions
346
+ */
347
+ sendMetric(name, value = 1, unit = 'Count', dimensions) {
348
+ this.metrics.send([{
349
+ name,
350
+ value,
351
+ timestamp: Date.now(),
352
+ unit,
353
+ dimensions: _extends({}, dimensions || {}, METRIC_DIMENSIONS)
354
+ }]);
355
+ },
356
+ get applicationCache() {
357
+ if (!this._applicationCache) {
358
+ this._applicationCache = new _ssrCache.PersistentCache();
359
+ }
360
+ return this._applicationCache;
361
+ }
362
+ };
363
+ (0, _mergeDescriptors.default)(app, mixin);
364
+ return app;
365
+ },
366
+ /**
367
+ * @private
368
+ */
369
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
370
+ _addSDKInternalHandlers(app) {
371
+ // This method is used by the dev server, but is not needed here.
372
+ },
373
+ /**
374
+ * @private
375
+ */
376
+ _setupSSRRequestProcessorMiddleware(app) {
377
+ // Attach this middleware as early as possible. It does timing
378
+ // and applies some early processing that must occur before
379
+ // anything else.
380
+
381
+ /**
382
+ * Incoming request processing.
383
+ *
384
+ * For the local dev server, if there is a request processor, use it to
385
+ * process all non-proxy, non-bundle requests, in the same way that
386
+ * CloudFront will do for a deployed bundle.
387
+ *
388
+ * If there is an x-querystring header in the incoming request, use
389
+ * that as the definitive querystring.
390
+ *
391
+ * @param req {express.req} the incoming request - modified in-place
392
+ * @param res {express.res} the response object
393
+ * @private
394
+ */
395
+ const processIncomingRequest = (req, res) => {
396
+ const options = req.app.options;
397
+ // If the request is for a proxy or bundle path, do nothing
398
+ if (this._isBundleOrProxyPath(req.originalUrl)) {
399
+ return;
400
+ }
401
+
402
+ // Apply the request processor
403
+ // `this` is bound to the calling context, usually RemoteServerFactory
404
+ const requestProcessor = this._getRequestProcessor(req);
405
+ const parsed = _url.default.parse(req.url);
406
+ const originalQuerystring = parsed.query;
407
+ let updatedQuerystring = originalQuerystring;
408
+ let updatedPath = req.path;
409
+
410
+ // If there's an x-querystring header, use that as the definitive
411
+ // querystring. This header is used in production, not in local dev,
412
+ // but we always handle it here to allow for testing.
413
+ const xQueryString = req.headers[_constants.X_MOBIFY_QUERYSTRING];
414
+ if (xQueryString) {
415
+ updatedQuerystring = xQueryString;
416
+ // Hide the header from any other code
417
+ delete req.headers[_constants.X_MOBIFY_QUERYSTRING];
418
+ }
419
+ if (requestProcessor) {
420
+ // Allow the processor to handle this request. Because this code
421
+ // runs only in the local development server, we intentionally do
422
+ // not swallow errors - we want them to happen and show up on the
423
+ // console because that's how developers can test the processor.
424
+ const headers = new _ssrProxying.Headers(req.headers, 'http');
425
+ const processed = requestProcessor.processRequest({
426
+ headers,
427
+ path: req.path,
428
+ querystring: updatedQuerystring,
429
+ getRequestClass: () => headers.getHeader(_ssrProxying.X_MOBIFY_REQUEST_CLASS),
430
+ setRequestClass: value => headers.setHeader(_ssrProxying.X_MOBIFY_REQUEST_CLASS, value),
431
+ // This matches the set of parameters passed in the
432
+ // Lambda@Edge context.
433
+ parameters: {
434
+ deployTarget: `${process.env.DEPLOY_TARGET || 'local'}`,
435
+ appHostname: options.appHostname,
436
+ proxyConfigs: _ssrShared.proxyConfigs
437
+ }
438
+ });
439
+
440
+ // Aid debugging by checking the return value
441
+ (0, _assert.default)(processed && 'path' in processed && 'querystring' in processed, 'Expected processRequest to return an object with ' + '"path" and "querystring" properties, ' + `but got ${JSON.stringify(processed, null, 2)}`);
442
+
443
+ // Update the request.
444
+ updatedQuerystring = processed.querystring;
445
+ updatedPath = processed.path;
446
+ if (headers.modified) {
447
+ req.headers = headers.toObject();
448
+ }
449
+ }
450
+
451
+ // Update the request.
452
+ if (updatedQuerystring !== originalQuerystring) {
453
+ // Update the string in the parsed URL
454
+ parsed.search = updatedQuerystring ? `?${updatedQuerystring}` : '';
455
+
456
+ // Let Express re-parse the parameters
457
+ if (updatedQuerystring) {
458
+ const queryStringParser = req.app.set('query parser fn');
459
+ req.query = queryStringParser(updatedQuerystring);
460
+ } else {
461
+ req.query = {};
462
+ }
463
+ }
464
+ parsed.pathname = updatedPath;
465
+
466
+ // This will update the request's URL with the new path
467
+ // and querystring.
468
+ req.url = _url.default.format(parsed);
469
+
470
+ // Get the request class and store it for general use. We
471
+ // must do this AFTER the request-processor, because that's
472
+ // what may set the request class.
473
+ res.locals.requestClass = req.headers[_ssrProxying.X_MOBIFY_REQUEST_CLASS];
474
+ };
475
+ const ssrRequestProcessorMiddleware = (req, res, next) => {
476
+ const locals = res.locals;
477
+ locals.requestStart = Date.now();
478
+ locals.afterResponseCalled = false;
479
+ locals.responseCaching = {};
480
+ locals.originalUrl = req.originalUrl;
481
+
482
+ // Track this response
483
+ req.app._requestMonitor._responseStarted(res);
484
+
485
+ // If the path is /, we enforce that the only methods
486
+ // allowed are GET, HEAD or OPTIONS. This is a restriction
487
+ // imposed by API Gateway: we enforce it here so that the
488
+ // local dev server has the same behaviour.
489
+ if (req.path === '/' && !['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
490
+ res.sendStatus(405);
491
+ return;
492
+ }
493
+
494
+ // Apply custom query parameter parsing.
495
+ processIncomingRequest(req, res);
496
+ const afterResponse = () => {
497
+ /* istanbul ignore else */
498
+ if (!locals.afterResponseCalled) {
499
+ locals.afterResponseCalled = true;
500
+ // Emit timing unless the request is for a proxy
501
+ // or bundle path. We don't want to emit metrics
502
+ // for those requests. We test req.originalUrl
503
+ // because it is consistently available across
504
+ // different types of the 'req' object, and will
505
+ // always contain the original full path.
506
+ /* istanbul ignore else */
507
+ if (!this._isBundleOrProxyPath(req.originalUrl)) {
508
+ req.app.sendMetric('RequestTime', Date.now() - locals.requestStart, 'Milliseconds');
509
+ // We count 4xx and 5xx as errors, everything else is
510
+ // a success. 404 is a special case.
511
+ let metricName = 'RequestSuccess';
512
+ if (res.statusCode === 404) {
513
+ metricName = 'RequestFailed404';
514
+ } else if (res.statusCode >= 400 && res.statusCode <= 499) {
515
+ metricName = 'RequestFailed400';
516
+ } else if (res.statusCode >= 500 && res.statusCode <= 599) {
517
+ metricName = 'RequestFailed500';
518
+ }
519
+ req.app.sendMetric(metricName);
520
+ }
521
+ }
522
+ };
523
+
524
+ // Attach event listeners to the Response (we need to attach
525
+ // both to handle all possible cases)
526
+ res.on('finish', afterResponse);
527
+ res.on('close', afterResponse);
528
+
529
+ // Strip out API Gateway headers from the incoming request. We
530
+ // do that now so that the rest of the code don't have to deal
531
+ // with these headers, which can be large and may be accidentally
532
+ // forwarded to other servers.
533
+ _ssrProxying.X_HEADERS_TO_REMOVE_ORIGIN.forEach(key => {
534
+ delete req.headers[key];
535
+ });
536
+
537
+ // Hand off to the next middleware
538
+ next();
539
+ };
540
+ app.use(ssrRequestProcessorMiddleware);
541
+ },
542
+ /**
543
+ * @private
544
+ */
545
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
546
+ _setupProxying(app, options) {
547
+ app.all(`${(0, _ssrPaths.getProxyPath)()}/*`, (_, res) => {
548
+ return res.status(501).json({
549
+ message: 'Environment proxies are not set: https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/proxying-requests.html'
550
+ });
551
+ });
552
+ },
553
+ /**
554
+ * @private
555
+ */
556
+ _handleMissingSlasPrivateEnvVar(app) {
557
+ app.use((0, _ssrPaths.getSlasPrivateProxyPath)(), (_, res) => {
558
+ return res.status(501).json({
559
+ message: 'Environment variable PWA_KIT_SLAS_CLIENT_SECRET not set: Please set this environment variable to proceed.'
560
+ });
561
+ });
562
+ },
563
+ /**
564
+ * @private
565
+ */
566
+ _setupSlasPrivateClientProxy(app, options) {
567
+ var _options$mobify2, _options$mobify2$app, _options$mobify2$app$, _options$mobify2$app$2;
568
+ if (!options.useSLASPrivateClient) {
569
+ return;
570
+ }
571
+ (0, _ssrServer.localDevLog)(`Proxying ${(0, _ssrPaths.getSlasPrivateProxyPath)()} to ${options.slasTarget}`);
572
+ const clientId = (_options$mobify2 = options.mobify) === null || _options$mobify2 === void 0 ? void 0 : (_options$mobify2$app = _options$mobify2.app) === null || _options$mobify2$app === void 0 ? void 0 : (_options$mobify2$app$ = _options$mobify2$app.commerceAPI) === null || _options$mobify2$app$ === void 0 ? void 0 : (_options$mobify2$app$2 = _options$mobify2$app$.parameters) === null || _options$mobify2$app$2 === void 0 ? void 0 : _options$mobify2$app$2.clientId;
573
+ const clientSecret = process.env.PWA_KIT_SLAS_CLIENT_SECRET;
574
+ if (!clientSecret) {
575
+ this._handleMissingSlasPrivateEnvVar(app, (0, _ssrPaths.getSlasPrivateProxyPath)());
576
+ return;
577
+ }
578
+ const encodedSlasCredentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
579
+ app.use((0, _ssrPaths.getSlasPrivateProxyPath)(), (0, _httpProxyMiddleware.createProxyMiddleware)({
580
+ target: options.slasTarget,
581
+ changeOrigin: true,
582
+ pathRewrite: {
583
+ [(0, _ssrPaths.getSlasPrivateProxyPath)()]: ''
584
+ },
585
+ onProxyReq: (proxyRequest, incomingRequest) => {
586
+ var _incomingRequest$path;
587
+ (0, _configureProxy.applyProxyRequestHeaders)({
588
+ proxyRequest,
589
+ incomingRequest,
590
+ proxyPath: (0, _ssrPaths.getSlasPrivateProxyPath)(),
591
+ targetHost: options.slasHostName,
592
+ targetProtocol: 'https'
593
+ });
594
+
595
+ // We pattern match and add client secrets only to endpoints that
596
+ // match the regex specified by options.applySLASPrivateClientToEndpoints.
597
+ // By default, this regex matches only calls to SLAS /oauth2/token
598
+ // (see option defaults at the top of this file).
599
+ // Other SLAS endpoints, ie. SLAS authenticate (/oauth2/login) and
600
+ // SLAS logout (/oauth2/logout), use the Authorization header for a different
601
+ // purpose so we don't want to overwrite the header for those calls.
602
+ if ((_incomingRequest$path = incomingRequest.path) !== null && _incomingRequest$path !== void 0 && _incomingRequest$path.match(options.applySLASPrivateClientToEndpoints)) {
603
+ proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`);
604
+ }
605
+ },
606
+ onProxyRes: (proxyRes, req) => {
607
+ if (proxyRes.statusCode && proxyRes.statusCode >= 400) {
608
+ _loggerInstance.default.error(`Failed to proxy SLAS Private Client request - ${proxyRes.statusCode}`, {
609
+ namespace: '_setupSlasPrivateClientProxy',
610
+ additionalProperties: {
611
+ statusCode: proxyRes.statusCode
612
+ }
613
+ });
614
+ _loggerInstance.default.error(`Please make sure you have enabled the SLAS Private Client Proxy in your ssr.js and set the correct environment variable PWA_KIT_SLAS_CLIENT_SECRET.`, {
615
+ namespace: '_setupSlasPrivateClientProxy'
616
+ });
617
+ _loggerInstance.default.error(`SLAS Private Client Proxy Request URL - ${req.protocol}://${req.get('host')}${req.originalUrl}`, {
618
+ namespace: '_setupSlasPrivateClientProxy',
619
+ additionalProperties: {
620
+ protocol: req.protocol,
621
+ originalUrl: req.originalUrl
622
+ }
623
+ });
624
+ }
625
+ }
626
+ }));
627
+ },
628
+ /**
629
+ * @private
630
+ */
631
+ _setupHealthcheck(app) {
632
+ app.get(`${(0, _ssrPaths.getHealthCheckPath)()}`, (_, res) => res.set('cache-control', _constants.NO_CACHE).sendStatus(200).end());
633
+ },
634
+ /**
635
+ * @private
636
+ */
637
+ _setupCommonMiddleware(app, options) {
638
+ app.use(prepNonProxyRequest);
639
+
640
+ // Apply the SSR middleware to any subsequent routes that we expect users
641
+ // to add in their projects, like in any regular Express app.
642
+ app.use(ssrMiddleware);
643
+ app.use(errorHandlerMiddleware);
644
+ applyPatches(options);
645
+ },
646
+ /**
647
+ * @private
648
+ */
649
+ _validateConfiguration(options) {
650
+ // Check that we are running under a compatible version of node
651
+ /* istanbul ignore next */
652
+ const requiredNode = new _semver.default.Range(_package.default.engines.node);
653
+ /* istanbul ignore next */
654
+ if (!_semver.default.satisfies(process.versions.node,
655
+ // A string like '8.10.0'
656
+ requiredNode)) {
657
+ /* istanbul ignore next */
658
+ console.warn(`Warning: You are using Node ${process.versions.node}. ` + `Your app may not work as expected when deployed to Managed ` + `Runtime servers which are compatible with Node ${requiredNode}`);
659
+ }
660
+
661
+ // Verify the remote environment
662
+ if ((0, _ssrServer.isRemote)()) {
663
+ const notFound = [];
664
+ REMOTE_REQUIRED_ENV_VARS.forEach(envVar => {
665
+ if (!process.env[envVar]) {
666
+ notFound.push(envVar);
667
+ }
668
+ });
669
+ if (notFound.length) {
670
+ throw new Error(`SSR server cannot initialize: missing environment values: ${notFound.join(', ')}`);
671
+ }
672
+ }
673
+ if (['http', 'https'].indexOf(options.protocol) < 0) {
674
+ throw new Error(`Invalid local development server protocol ${options.protocol}. ` + `Valid protocols are http and https.`);
675
+ }
676
+ if (!options.buildDir) {
677
+ throw new Error('The buildDir option passed to the SSR server must be a non-empty string');
678
+ }
679
+
680
+ // Fix up the path in case we were passed a relative one
681
+ options.buildDir = _path.default.resolve(process.cwd(), options.buildDir);
682
+ if (!(options.mobify instanceof Object)) {
683
+ throw new Error('The mobify option passed to the SSR server must be an object');
684
+ }
685
+ const {
686
+ sslFilePath
687
+ } = options;
688
+ if (!(0, _ssrServer.isRemote)() && sslFilePath && (!sslFilePath.endsWith('.pem') || !_fs.default.existsSync(sslFilePath))) {
689
+ throw new Error('The sslFilePath option passed to the SSR server constructor ' + 'must be a path to an SSL certificate file ' + 'in PEM format, whose name ends with ".pem". ' + 'See the "cert" and "key" options on ' + 'https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options');
690
+ }
691
+ if (!options.strictSSL) {
692
+ console.warn('The SSR Server has _strictSSL turned off for https requests');
693
+ }
694
+ },
695
+ /**
696
+ * @private
697
+ */
698
+ _addStaticAssetServing() {
699
+ // Handled by the CDN on remote
700
+ },
701
+ /**
702
+ * @private
703
+ */
704
+ _addDevServerGarbageCollection() {
705
+ // This is a hook for the dev-server. The remote-server
706
+ // does GC in a way that is awkward to extract. See _createHandler.
707
+ },
708
+ /**
709
+ * Serve the service worker at `req.path`
710
+ *
711
+ * For best results, serve the service worker at the root of the site and
712
+ * it must not be a redirect. We set a long value for s-maxage (to allow CDN
713
+ * caching), plus a strong etag (for CDN-only revalidation), and to set
714
+ * maxage to 0 to prevent browser caching.
715
+ *
716
+ * See https://developer.chrome.com/blog/fresher-sw/ for details on
717
+ * efficiently serving service workers.
718
+ *
719
+ */
720
+ serveServiceWorker(req, res) {
721
+ const options = req.app.options;
722
+ // We apply this cache-control to all responses (200 and 404)
723
+ res.set(_constants.CACHE_CONTROL,
724
+ // The CDN can cache for 24 hours. The browser may not cache
725
+ // the file.
726
+ 's-maxage=86400, max-age=0');
727
+ const workerFilePath = _path.default.join(options.buildDir, req.path);
728
+
729
+ // If there is no file, send a 404
730
+ if (!_fs.default.existsSync(workerFilePath)) {
731
+ res.status(404).send();
732
+ return;
733
+ }
734
+ const content = _fs.default.readFileSync(workerFilePath, {
735
+ encoding: 'utf8'
736
+ });
737
+
738
+ // Serve the file, with a strong ETag
739
+ res.set('etag', (0, _ssrServer.getHashForString)(content));
740
+ res.set(_constants.CONTENT_TYPE, 'application/javascript');
741
+ res.send(content);
742
+ },
743
+ /**
744
+ * Serve static files from the app's build directory and set default
745
+ * cache-control headers.
746
+ * @since v2.0.0
747
+ *
748
+ * @param {String} filePath - the location of the static file relative to the build directory
749
+ * @param {Object} opts - the options object to pass to the original `sendFile` method
750
+ */
751
+ serveStaticFile(filePath, opts = {}) {
752
+ return (req, res) => {
753
+ const baseDir = req.app.options.buildDir;
754
+ return this._serveStaticFile(req, res, baseDir, filePath, opts);
755
+ };
756
+ },
757
+ /**
758
+ * @private
759
+ */
760
+ _serveStaticFile(req, res, baseDir, filePath, opts = {}) {
761
+ const options = req.app.options;
762
+ const file = _path.default.resolve(baseDir, filePath);
763
+ res.sendFile(file, _objectSpread({
764
+ headers: {
765
+ [_constants.CACHE_CONTROL]: options.defaultCacheControl
766
+ }
767
+ }, opts));
768
+ },
769
+ /**
770
+ * Server side rendering entry.
771
+ *
772
+ * @since v2.0.0
773
+ *
774
+ * This is a wrapper around the Express `res.sendFile` method.
775
+ *
776
+ * @param {Object} req - the req object
777
+ * @param {Object} req - the res object
778
+ * @param {function} next - the callback function for middleware chain
779
+ */
780
+ render(req, res, next) {
781
+ const app = req.app;
782
+ if (!app.__renderer) {
783
+ // See - https://www.npmjs.com/package/webpack-hot-server-middleware#usage
784
+ const {
785
+ buildDir
786
+ } = app.options;
787
+ const _require = eval('require');
788
+ const serverRenderer = _require(_path.default.join(buildDir, 'server-renderer.js')).default;
789
+ const stats = _require(_path.default.join(buildDir, 'loadable-stats.json'));
790
+ app.__renderer = serverRenderer(stats);
791
+ }
792
+ app.__renderer(req, res, next);
793
+ },
794
+ /**
795
+ * Builds a Lambda handler function from an Express app.
796
+ *
797
+ * See: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html
798
+ *
799
+ * @param app {Express} - an Express App
800
+ * @private
801
+ */
802
+ _createHandler(app) {
803
+ // This flag is initially false, and is set true on the first request
804
+ // handled by a Lambda. If it is true on entry to the handler function,
805
+ // it indicates that the Lambda container has been reused.
806
+ let lambdaContainerReused = false;
807
+ const server = _awsServerlessExpress.default.createServer(app, null, binaryMimeTypes);
808
+ const handler = (event, context, callback) => {
809
+ // We don't want to wait for an empty event loop once the response
810
+ // has been sent. Setting this to false will "send the response
811
+ // right away when the callback executes", but any pending events
812
+ // may be executed if the Lambda container is then reused for
813
+ // another invocation (which we expect will happen under all
814
+ // but very low load). This means two things:
815
+ // 1. Any code that we have *after* the callback MAY be executed
816
+ // if the Lambda container is reused, but there's no guarantee
817
+ // it will be.
818
+ // 2. There is no way to have code do cleanup work (such as sending
819
+ // metrics) after the response is sent to the browser. We have
820
+ // to accept that doing such work delays the response.
821
+ // It would be good if we could set this to true and do work like sending
822
+ // metrics after calling the callback, but that doesn't work - API Gateway
823
+ // will wait for the Lambda invocation to complete before sending
824
+ // the response to the browser.
825
+ context.callbackWaitsForEmptyEventLoop = false;
826
+ if (lambdaContainerReused) {
827
+ // DESKTOP-434 If this Lambda container is being reused,
828
+ // clean up memory now, so that we start with low usage.
829
+ // These regular GC calls take about 80-100 mS each, as opposed
830
+ // to forced GC calls, which occur randomly and can take several
831
+ // hundred mS.
832
+ app._collectGarbage();
833
+ app.sendMetric('LambdaReused');
834
+ } else {
835
+ // This is the first use of this container, so set the
836
+ // reused flag for next time.
837
+ lambdaContainerReused = true;
838
+ app.sendMetric('LambdaCreated');
839
+ }
840
+
841
+ // Proxy the request through to the server. When the response
842
+ // is done, context.succeed will be called with the response
843
+ // data.
844
+ _awsServerlessExpress.default.proxy(server, event,
845
+ // The incoming event
846
+ context,
847
+ // The event context
848
+ 'CALLBACK',
849
+ // How the proxy signals completion
850
+ (err, response) => {
851
+ // The 'response' parameter here is NOT the same response
852
+ // object handled by ExpressJS code. The awsServerlessExpress
853
+ // middleware works by sending an http.Request to the Express
854
+ // server and parsing the HTTP response that it returns.
855
+ // Wait util all pending metrics have been sent, and any pending
856
+ // response caching to complete. We have to do this now, before
857
+ // sending the response; there's no way to do it afterwards
858
+ // because the Lambda container is frozen inside the callback.
859
+
860
+ // We return this Promise, but the awsServerlessExpress object
861
+ // doesn't make any use of it.
862
+ return app._requestMonitor._waitForResponses().then(() => app.metrics.flush())
863
+ // Now call the Lambda callback to complete the response
864
+ .then(() => callback(err, (0, _ssrServer.processLambdaResponse)(response, event)))
865
+ // DON'T add any then() handlers here, after the callback.
866
+ // They won't be called after the response is sent, but they
867
+ // *might* be called if the Lambda container running this code
868
+ // is reused, which can lead to odd and unpredictable
869
+ // behaviour.
870
+ ;
871
+ });
872
+ };
873
+ return {
874
+ handler,
875
+ server,
876
+ app
877
+ };
878
+ },
879
+ /**
880
+ * Create an SSR (Server-Side Rendering) Server.
881
+ *
882
+ * @constructor
883
+ * @param {Object} options
884
+ * @param {String} [options.buildDir] - The build directory path, either as an
885
+ * absolute path, or relative to the current working directory. Defaults
886
+ * to 'build'.
887
+ * @param {Number} [options.defaultCacheTimeSeconds=600] - The cache time
888
+ * for rendered pages and assets (not used in local development mode).
889
+ * @param {Object} options.mobify - The 'mobify' object from the project's
890
+ * package.json file, containing the SSR parameters.
891
+ * @param {Number} [options.port=3443] - the localhost port on which the local
892
+ * development Express app listens.
893
+ * @param {String} [options.protocol='https'] - the protocol on which the development
894
+ * Express app listens.
895
+ * @param {Boolean} [options.proxyKeepAliveAgent] - This boolean value indicates
896
+ * whether or not we are using a keep alive agent for proxy connections. Defaults
897
+ * to 'true'. NOTE: This keep alive agent will only be used on remote.
898
+ * @param {String} options.sslFilePath - the absolute path to a PEM format
899
+ * certificate file to be used by the local development server. This should
900
+ * contain both the certificate and the private key.
901
+ * @param {function} customizeApp - a callback that takes an express app
902
+ * as an argument. Use this to customize the server.
903
+ * @param {Boolean} [options.allowCookies] - This boolean value indicates
904
+ * whether or not we strip cookies from requests and block setting of cookies. Defaults
905
+ * to 'false'.
906
+ */
907
+ createHandler(options, customizeApp) {
908
+ process.on('unhandledRejection', _ssrServer.catchAndLog);
909
+ const app = this._createApp(options);
910
+ customizeApp(app);
911
+ return this._createHandler(app);
912
+ },
913
+ /**
914
+ * @private
915
+ */
916
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
917
+ _getRequestProcessor(req) {
918
+ return null;
919
+ }
920
+ };
921
+
922
+ /**
923
+ * ExpressJS middleware that processes any non-proxy request passing
924
+ * through the Express app.
925
+ *
926
+ * If allowCookies is false, strips Cookie headers from incoming requests, and
927
+ * configures the Response so that it cannot have cookies set on it.
928
+ *
929
+ * Sets the Host header to the application host.
930
+ * If there's an Origin header, rewrites it to be the application
931
+ * Origin.
932
+ *
933
+ * This function should not be called for proxied requests, which
934
+ * MAY allow use of cookies.
935
+ *
936
+ * @private
937
+ */
938
+ const prepNonProxyRequest = (req, res, next) => {
939
+ const options = req.app.options;
940
+ if (!options.allowCookies) {
941
+ // Strip cookies from the request
942
+ delete req.headers.cookie;
943
+ // In an Express Response, all cookie setting ends up
944
+ // calling setHeader, so we override that to allow us
945
+ // to intercept and discard cookie setting.
946
+ const setHeader = Object.getPrototypeOf(res).setHeader;
947
+ const remote = (0, _ssrServer.isRemote)();
948
+ res.setHeader = function (header, value) {
949
+ /* istanbul ignore else */
950
+ if (header && header.toLowerCase() !== _constants.SET_COOKIE && value) {
951
+ setHeader.call(this, header, value);
952
+ } /* istanbul ignore else */else if (!remote) {
953
+ _loggerInstance.default.warn(`Req ${res.locals.requestId}: ` + `Cookies cannot be set on responses sent from ` + `the SSR Server. Discarding "Set-Cookie: ${value}"`, {
954
+ namespace: 'RemoteServerFactory.prepNonProxyRequest'
955
+ });
956
+ }
957
+ };
958
+ }
959
+
960
+ // Set the Host header
961
+ req.headers.host = options.appHostname;
962
+
963
+ // Replace any Origin header
964
+ if (req.headers.origin) {
965
+ req.headers.origin = options.appOrigin;
966
+ }
967
+ next();
968
+ };
969
+
970
+ /**
971
+ * Express Middleware applied to requests that require rendering of a response.
972
+ *
973
+ * @private
974
+ */
975
+ const ssrMiddleware = (req, res, next) => {
976
+ setDefaultHeaders(req, res);
977
+ const renderStartTime = Date.now();
978
+ const done = () => {
979
+ const elapsedRenderTime = Date.now() - renderStartTime;
980
+ req.app.sendMetric('RenderTime', elapsedRenderTime, 'Milliseconds');
981
+ };
982
+ res.on('finish', done);
983
+ res.on('close', done);
984
+ next();
985
+ };
986
+
987
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
988
+ const errorHandlerMiddleware = (err, req, res, next) => {
989
+ (0, _ssrServer.catchAndLog)(err);
990
+ req.app.sendMetric('RenderErrors');
991
+ res.sendStatus(500);
992
+ };
993
+
994
+ /**
995
+ * Wrap the function fn in such a way that it will be called at most once. Subsequent
996
+ * calls will always return the same value.
997
+ *
998
+ * @private
999
+ */
1000
+ const once = fn => {
1001
+ let result;
1002
+ return (...args) => {
1003
+ if (fn) {
1004
+ result = fn(...args);
1005
+ fn = null;
1006
+ }
1007
+ return result;
1008
+ };
1009
+ };
1010
+ exports.once = once;
1011
+ const applyPatches = once(options => {
1012
+ // If we're running remotely, we also override the send()
1013
+ // method for ExpressJS's Response class (which is actually
1014
+ // a function). See responseSend for details.
1015
+ if ((0, _ssrServer.isRemote)()) {
1016
+ // http.ServerResponse.prototype
1017
+ const expressResponse = _express.default.response;
1018
+ expressResponse.send = (0, _ssrServer.responseSend)(expressResponse.send);
1019
+ }
1020
+
1021
+ // Patch the http.request/get and https.request/get
1022
+ // functions to allow us to intercept them (since
1023
+ // there are multiple ways to make requests in Node).
1024
+ _http.default.request = (0, _ssrServer.outgoingRequestHook)(_http.default.request, options);
1025
+ _http.default.get = (0, _ssrServer.outgoingRequestHook)(_http.default.get, options);
1026
+ _https.default.request = (0, _ssrServer.outgoingRequestHook)(_https.default.request, options);
1027
+ _https.default.get = (0, _ssrServer.outgoingRequestHook)(_https.default.get, options);
1028
+
1029
+ // Patch the ExpressJS Response class's redirect function to suppress
1030
+ // the creation of a body (DESKTOP-485). Including the body may
1031
+ // trigger a parsing error in aws-serverless-express.
1032
+ _express.default.response.redirect = function (status, url) {
1033
+ let workingStatus = status;
1034
+ let workingUrl = url;
1035
+ if (typeof status === 'string') {
1036
+ workingUrl = status;
1037
+ workingStatus = 302;
1038
+ }
1039
+
1040
+ // Duplicate behaviour in node_modules/express/lib/response.js
1041
+ const address = this.location(workingUrl).get('Location');
1042
+
1043
+ // Send a minimal response with just a status and location
1044
+ this.status(workingStatus).location(address).end();
1045
+ };
1046
+
1047
+ // Patch the whatwg-encoding decode function so that it will accept plain
1048
+ // JS strings and return them as-is.
1049
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1050
+ const whatWGEncoding = require('whatwg-encoding');
1051
+ const originalDecode = whatWGEncoding.decode;
1052
+ whatWGEncoding.decode = (buffer, fallbackEncodingName) => {
1053
+ /* istanbul ignore else */
1054
+ if (typeof buffer === 'string') {
1055
+ return buffer;
1056
+ }
1057
+ /* istanbul ignore next */
1058
+ return originalDecode(buffer, fallbackEncodingName);
1059
+ };
1060
+
1061
+ // The 'http-proxy-middleware' module pollutes global by adding a lodash
1062
+ // reference to it as '_' (this global reference is not used). We kill
1063
+ // that reference here because otherwise it can hold on to references
1064
+ // to other objects.
1065
+ /* istanbul ignore next */
1066
+ if (global._) {
1067
+ delete global._;
1068
+ }
1069
+ });
1070
+
1071
+ /**
1072
+ * Set default headers on a response. The arguments to this function
1073
+ * are the same as those for the responseHook function.
1074
+ *
1075
+ * @private
1076
+ */
1077
+ const setDefaultHeaders = (req, res) => {
1078
+ const requestClass = res.locals.requestClass;
1079
+ if (requestClass) {
1080
+ res.set(_ssrProxying.X_MOBIFY_REQUEST_CLASS, requestClass);
1081
+ }
1082
+ };
1083
+
1084
+ /**
1085
+ * Tracks in-flight requests.
1086
+ *
1087
+ * @private
1088
+ */
1089
+ class RequestMonitor {
1090
+ constructor() {
1091
+ this._pendingResponses = {
1092
+ ids: [],
1093
+ promise: null,
1094
+ resolve: null
1095
+ };
1096
+ }
1097
+ /**
1098
+ * Returns a Promise that wil resolve when the server has completed
1099
+ * handling of any current requests. If the server is idle, returns
1100
+ * a Promise that is already resolved.
1101
+ *
1102
+ * This method is safe to use at any point. If there are any responses
1103
+ * in progress when it's called, then the returned Promise will not
1104
+ * resolve until all responses complete, even if those responses start
1105
+ * after this method is called.
1106
+ *
1107
+ * @private
1108
+ * @returns {Promise}
1109
+ */
1110
+ _waitForResponses() {
1111
+ const pending = this._pendingResponses;
1112
+ if (pending.ids.length === 0) {
1113
+ return _express2.RESOLVED_PROMISE;
1114
+ }
1115
+ if (!pending.promise) {
1116
+ pending.promise = new Promise(resolve => {
1117
+ pending.resolve = resolve;
1118
+ });
1119
+ }
1120
+ return pending.promise;
1121
+ }
1122
+
1123
+ /**
1124
+ * Works with waitForCompletion: when invoked by the initial request
1125
+ * handler, adds the id of the response to the set of pending ids.
1126
+ * @param res {express.response} the response that has started
1127
+ * @private
1128
+ */
1129
+ _responseStarted(res) {
1130
+ this._pendingResponses.ids.push(res.locals.requestId);
1131
+ const finish = () => this._responseFinished(res);
1132
+ // We hook both the 'finished' and 'close' events, so that
1133
+ // we properly complete any responses that fail (in which case
1134
+ // 'close' will fire, but 'finish' may not).
1135
+ res.once('finish', finish);
1136
+ res.once('close', finish);
1137
+ }
1138
+
1139
+ /**
1140
+ * Works with waitForCompletion: when invoked by the 'finish' event of
1141
+ * any response, removes the response from the list of those pending
1142
+ * (using the id, so that multiple calls to this method will work
1143
+ * correctly). If there are no more responses pending, resolves any
1144
+ * Promise that is waiting for responses to complete.
1145
+ * @param res {express.response} the response that has finished
1146
+ * @private
1147
+ */
1148
+ _responseFinished(res) {
1149
+ const pending = this._pendingResponses;
1150
+ if (pending.ids.length) {
1151
+ const requestId = res.locals.requestId;
1152
+ pending.ids = pending.ids.filter(id => id !== requestId);
1153
+ if (pending.ids.length === 0 && pending.resolve) {
1154
+ pending.resolve();
1155
+ pending.resolve = pending.promise = null;
1156
+ }
1157
+ }
1158
+ }
1159
+ }