@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.
- package/package.json +6 -5
- package/ssr/server/build-remote-server.js +1159 -0
- package/ssr/server/build-remote-server.test.js +41 -0
- package/ssr/server/constants.js +37 -0
- package/ssr/server/express.js +462 -0
- package/ssr/server/express.lambda.test.js +390 -0
- package/ssr/server/express.test.js +963 -0
- package/ssr/server/test_fixtures/favicon.ico +0 -0
- package/ssr/server/test_fixtures/loadable-stats.json +1 -0
- package/ssr/server/test_fixtures/localhost.pem +45 -0
- package/ssr/server/test_fixtures/main.js +7 -0
- package/ssr/server/test_fixtures/mobify.png +0 -0
- package/ssr/server/test_fixtures/server-renderer.js +12 -0
- package/ssr/server/test_fixtures/worker.js +7 -0
- package/ssr/server/test_fixtures/worker.js.map +1 -0
- package/utils/logger-factory.js +154 -0
- package/utils/logger-factory.test.js +71 -0
- package/utils/logger-instance.js +19 -0
- package/utils/middleware/index.js +16 -0
- package/utils/middleware/security.js +111 -0
- package/utils/middleware/security.test.js +110 -0
- package/utils/morgan-stream.js +28 -0
- package/utils/ssr-cache.js +177 -0
- package/utils/ssr-cache.test.js +64 -0
- package/utils/ssr-config.client.js +23 -0
- package/utils/ssr-config.client.test.js +25 -0
- package/utils/ssr-config.js +20 -0
- package/utils/ssr-config.server.js +98 -0
- package/utils/ssr-config.server.test.js +50 -0
- package/utils/ssr-namespace-paths.js +60 -0
- package/utils/ssr-namespace-paths.test.js +30 -0
- package/utils/ssr-paths.js +51 -0
- package/utils/ssr-paths.test.js +49 -0
- package/utils/ssr-proxying.js +859 -0
- package/utils/ssr-proxying.test.js +593 -0
- package/utils/ssr-request-processing.js +164 -0
- package/utils/ssr-request-processing.test.js +95 -0
- package/utils/ssr-server/cached-response.js +116 -0
- package/utils/ssr-server/configure-proxy.js +304 -0
- package/utils/ssr-server/metrics-sender.js +204 -0
- package/utils/ssr-server/outgoing-request-hook.js +139 -0
- package/utils/ssr-server/parse-end-parameters.js +38 -0
- package/utils/ssr-server/process-express-response.js +56 -0
- package/utils/ssr-server/process-lambda-response.js +43 -0
- package/utils/ssr-server/update-global-agent-options.js +41 -0
- package/utils/ssr-server/update-global-agent-options.test.js +28 -0
- package/utils/ssr-server/utils.js +124 -0
- package/utils/ssr-server/utils.test.js +69 -0
- package/utils/ssr-server/wrap-response-write.js +40 -0
- package/utils/ssr-server.js +115 -0
- package/utils/ssr-server.test.js +869 -0
- 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
|
+
}
|