@salesforce/pwa-kit-runtime 3.0.0-preview.0
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/LICENSE +14 -0
- package/README.md +24 -0
- package/package.json +76 -0
- package/scripts/version.js +22 -0
- package/ssr/server/build-remote-server.js +1025 -0
- package/ssr/server/build-remote-server.test.js +24 -0
- package/ssr/server/constants.js +37 -0
- package/ssr/server/express.js +460 -0
- package/ssr/server/express.lambda.test.js +383 -0
- package/ssr/server/express.test.js +847 -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/morgan-stream.js +26 -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 +88 -0
- package/utils/ssr-config.server.test.js +30 -0
- package/utils/ssr-proxying.js +804 -0
- package/utils/ssr-proxying.test.js +591 -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 +241 -0
- package/utils/ssr-server/metrics-sender.js +191 -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 +36 -0
- package/utils/ssr-server/update-global-agent-options.js +42 -0
- package/utils/ssr-server/update-global-agent-options.test.js +28 -0
- package/utils/ssr-server/utils.js +119 -0
- package/utils/ssr-server/utils.test.js +64 -0
- package/utils/ssr-server/wrap-response-write.js +40 -0
- package/utils/ssr-server.js +115 -0
- package/utils/ssr-server.test.js +835 -0
- package/utils/ssr-shared.js +185 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _buildRemoteServer = require("./build-remote-server");
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2021, salesforce.com, inc.
|
|
6
|
+
* All rights reserved.
|
|
7
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
8
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
describe('the once function', () => {
|
|
12
|
+
test('should prevent a function being called more than once', () => {
|
|
13
|
+
const fn = jest.fn(() => ({
|
|
14
|
+
test: 'test'
|
|
15
|
+
}));
|
|
16
|
+
const wrapped = (0, _buildRemoteServer.once)(fn);
|
|
17
|
+
expect(fn.mock.calls).toHaveLength(0);
|
|
18
|
+
const v1 = wrapped();
|
|
19
|
+
expect(fn.mock.calls).toHaveLength(1);
|
|
20
|
+
const v2 = wrapped();
|
|
21
|
+
expect(fn.mock.calls).toHaveLength(1);
|
|
22
|
+
expect(v1).toBe(v2); // The exact same instance
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.X_ORIGINAL_CONTENT_TYPE = exports.X_MOBIFY_QUERYSTRING = exports.X_MOBIFY_FROM_CACHE = exports.STATIC_ASSETS = exports.SET_COOKIE = exports.PROXY_PATH_PREFIX = exports.NO_CACHE = exports.CONTENT_TYPE = exports.CONTENT_ENCODING = exports.CACHE_CONTROL = exports.BUILD = exports.APPLICATION_OCTET_STREAM = void 0;
|
|
7
|
+
/*
|
|
8
|
+
* Copyright (c) 2021, salesforce.com, inc.
|
|
9
|
+
* All rights reserved.
|
|
10
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
11
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
12
|
+
*/
|
|
13
|
+
const APPLICATION_OCTET_STREAM = 'application/octet-stream';
|
|
14
|
+
exports.APPLICATION_OCTET_STREAM = APPLICATION_OCTET_STREAM;
|
|
15
|
+
const BUILD = 'build';
|
|
16
|
+
exports.BUILD = BUILD;
|
|
17
|
+
const STATIC_ASSETS = 'static_assets';
|
|
18
|
+
exports.STATIC_ASSETS = STATIC_ASSETS;
|
|
19
|
+
const PROXY_PATH_PREFIX = '/mobify/proxy';
|
|
20
|
+
// All these values MUST be lower case
|
|
21
|
+
exports.PROXY_PATH_PREFIX = PROXY_PATH_PREFIX;
|
|
22
|
+
const CONTENT_TYPE = 'content-type';
|
|
23
|
+
exports.CONTENT_TYPE = CONTENT_TYPE;
|
|
24
|
+
const CONTENT_ENCODING = 'content-encoding';
|
|
25
|
+
exports.CONTENT_ENCODING = CONTENT_ENCODING;
|
|
26
|
+
const X_ORIGINAL_CONTENT_TYPE = 'x-original-content-type';
|
|
27
|
+
exports.X_ORIGINAL_CONTENT_TYPE = X_ORIGINAL_CONTENT_TYPE;
|
|
28
|
+
const X_MOBIFY_QUERYSTRING = 'x-mobify-querystring';
|
|
29
|
+
exports.X_MOBIFY_QUERYSTRING = X_MOBIFY_QUERYSTRING;
|
|
30
|
+
const X_MOBIFY_FROM_CACHE = 'x-mobify-from-cache';
|
|
31
|
+
exports.X_MOBIFY_FROM_CACHE = X_MOBIFY_FROM_CACHE;
|
|
32
|
+
const SET_COOKIE = 'set-cookie';
|
|
33
|
+
exports.SET_COOKIE = SET_COOKIE;
|
|
34
|
+
const CACHE_CONTROL = 'cache-control';
|
|
35
|
+
exports.CACHE_CONTROL = CACHE_CONTROL;
|
|
36
|
+
const NO_CACHE = 'max-age=0, nocache, nostore, must-revalidate';
|
|
37
|
+
exports.NO_CACHE = NO_CACHE;
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.sendCachedResponse = exports.respondFromBundle = exports.getRuntime = exports.getResponseFromCache = exports.generateCacheKey = exports.cacheResponseWhenDone = exports.RESOLVED_PROMISE = void 0;
|
|
7
|
+
var _url = _interopRequireDefault(require("url"));
|
|
8
|
+
var _ssrServer = require("../../utils/ssr-server");
|
|
9
|
+
var _constants = require("./constants");
|
|
10
|
+
var _ssrProxying = require("../../utils/ssr-proxying");
|
|
11
|
+
var _buildRemoteServer = require("./build-remote-server");
|
|
12
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
13
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
14
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
15
|
+
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
16
|
+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
17
|
+
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
|
|
18
|
+
* Copyright (c) 2022, Salesforce, Inc.
|
|
19
|
+
* All rights reserved.
|
|
20
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
21
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
22
|
+
*/ /**
|
|
23
|
+
* @module progressive-web-sdk/ssr/server/express
|
|
24
|
+
*/
|
|
25
|
+
const RESOLVED_PROMISE = Promise.resolve();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Use properties of the request, such as URL and querystring, to generate
|
|
29
|
+
* a cache key string suitable for passing to sendResponseFromCache or
|
|
30
|
+
* storeResponseInCache.
|
|
31
|
+
*
|
|
32
|
+
* This method is provided as a convenience: you do not have to use it,
|
|
33
|
+
* but it should cover the most common use cases. It is the default
|
|
34
|
+
* cache key generator used by sendResponseFromCache and
|
|
35
|
+
* storeResponseInCache. If you override the cache key function
|
|
36
|
+
* in the options for storeResponseInCache, you may call this function
|
|
37
|
+
* and then further modify the returned key.
|
|
38
|
+
*
|
|
39
|
+
* The cache key is based on the request's path (case-independent) and
|
|
40
|
+
* querystring (case-dependent). The order of parameters in the querystring
|
|
41
|
+
* is important: if the order changes, the cache key changes. This is done
|
|
42
|
+
* because the order of query parameters is important for some systems.
|
|
43
|
+
*
|
|
44
|
+
* To allow for simple extension of the default algorithm, the optional
|
|
45
|
+
* 'options.extras' parameter may be used to pass an array of strings that
|
|
46
|
+
* will also be used (in the order they are passed) to build the key.
|
|
47
|
+
* Undefined values are allowed in the extras array.
|
|
48
|
+
*
|
|
49
|
+
* By default, the key generation does NOT consider the Accept,
|
|
50
|
+
* Accept-Charset or Accept-Language request header values. If it's
|
|
51
|
+
* appropriate to include these, the caller should add their values
|
|
52
|
+
* (or values based on them) to the options.extras array.
|
|
53
|
+
*
|
|
54
|
+
* By default, method will generate different cache keys for requests with
|
|
55
|
+
* different request classes (effectively, the value of the request-class
|
|
56
|
+
* string is included in 'extras'). To suppress this, pass true for
|
|
57
|
+
* options.ignoreRequestClass
|
|
58
|
+
*
|
|
59
|
+
* @param req {IncomingMessage} the request to generate the key for.
|
|
60
|
+
* @param [options] {Object} values that affect the cache key generation.
|
|
61
|
+
* @param [options.extras] {Array<String|undefined>} extra string values
|
|
62
|
+
* to be included in the key.
|
|
63
|
+
* @param [options.ignoreRequestClass] {Boolean} set this to true to suppress
|
|
64
|
+
* automatic variation of the key by request class.
|
|
65
|
+
* @returns {String} the generated key.
|
|
66
|
+
*
|
|
67
|
+
* @private
|
|
68
|
+
*/
|
|
69
|
+
exports.RESOLVED_PROMISE = RESOLVED_PROMISE;
|
|
70
|
+
const generateCacheKey = (req, options = {}) => {
|
|
71
|
+
let {
|
|
72
|
+
pathname,
|
|
73
|
+
query
|
|
74
|
+
} = _url.default.parse(req.url);
|
|
75
|
+
|
|
76
|
+
// remove the trailing slash
|
|
77
|
+
if (pathname.charAt(pathname.length - 1) === '/') {
|
|
78
|
+
pathname = pathname.substring(0, pathname.length - 1);
|
|
79
|
+
}
|
|
80
|
+
const elements = [];
|
|
81
|
+
if (query) {
|
|
82
|
+
const filteredQueryStrings = query.split('&').filter(querystring => !/^mobify_devicetype=/.test(querystring));
|
|
83
|
+
elements.push(...filteredQueryStrings);
|
|
84
|
+
}
|
|
85
|
+
if (!options.ignoreRequestClass) {
|
|
86
|
+
const requestClass = req.get(_ssrProxying.X_MOBIFY_REQUEST_CLASS);
|
|
87
|
+
if (requestClass) {
|
|
88
|
+
elements.push(`class=${requestClass}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (options.extras) {
|
|
92
|
+
options.extras.forEach((extra, index) => elements.push(`ex${index}=${extra}`));
|
|
93
|
+
}
|
|
94
|
+
return pathname.toLowerCase() + '/' + (0, _ssrServer.getHashForString)(elements.join('-'));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Internal handler that is called on completion of a response
|
|
99
|
+
* that is to be cached.
|
|
100
|
+
*
|
|
101
|
+
* @param req {http.IncomingMessage} the request
|
|
102
|
+
* @param res {http.ServerResponse} the response
|
|
103
|
+
* @returns {Promise} resolved when caching is done (also
|
|
104
|
+
* stored in locals.responseCaching.promise
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
exports.generateCacheKey = generateCacheKey;
|
|
108
|
+
const storeResponseInCache = (req, res) => {
|
|
109
|
+
const locals = res.locals;
|
|
110
|
+
const caching = locals.responseCaching;
|
|
111
|
+
const metadata = {
|
|
112
|
+
status: res.statusCode,
|
|
113
|
+
headers: res.getHeaders()
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ADN-118 When the response is created, we intercept the data
|
|
117
|
+
// as it's written, and store it so that we can cache it here.
|
|
118
|
+
// However, ExpressJS will apply compression *after* we store
|
|
119
|
+
// the data, but will add a content-encoding header *before*
|
|
120
|
+
// this method is called. If we store the headers unchanged,
|
|
121
|
+
// we'll cache a response with an uncompressed body, but
|
|
122
|
+
// a content-encoding header. We therefore remove the content-
|
|
123
|
+
// encoding at this point, so that the response is stored
|
|
124
|
+
// in a consistent way.
|
|
125
|
+
// The exception is if the contentEncodingSet flag is set on
|
|
126
|
+
// the response. If it's truthy, then project code set a
|
|
127
|
+
// content-encoding before the Express compression code was
|
|
128
|
+
// called; in that case, we must leave the content-encoding
|
|
129
|
+
// unchanged.
|
|
130
|
+
if (!locals.contentEncodingSet) {
|
|
131
|
+
delete metadata.headers[_constants.CONTENT_ENCODING];
|
|
132
|
+
}
|
|
133
|
+
const cacheControl = (0, _ssrServer.parseCacheControl)(res.get('cache-control'));
|
|
134
|
+
const expiration = parseInt(caching.expiration || cacheControl['s-maxage'] || cacheControl['max-age'] || 7 * 24 * 3600);
|
|
135
|
+
|
|
136
|
+
// Return a Promise that will be resolved when caching is complete.
|
|
137
|
+
let dataToCache;
|
|
138
|
+
/* istanbul ignore else */
|
|
139
|
+
if (caching.chunks.length) {
|
|
140
|
+
// Concat the body into a single buffer.\
|
|
141
|
+
// caching.chunks will be an Array of Buffer
|
|
142
|
+
// values, and may be empty.
|
|
143
|
+
dataToCache = Buffer.concat(caching.chunks);
|
|
144
|
+
}
|
|
145
|
+
return req.app.applicationCache.put({
|
|
146
|
+
key: caching.cacheKey,
|
|
147
|
+
namespace: caching.cacheNamespace,
|
|
148
|
+
data: dataToCache,
|
|
149
|
+
metadata,
|
|
150
|
+
expiration: expiration * 1000 // in mS
|
|
151
|
+
})
|
|
152
|
+
// If an error occurs,we don't want to prevent the
|
|
153
|
+
// response being sent, so we just log.
|
|
154
|
+
.catch(err => {
|
|
155
|
+
console.warn(`Unexpected error in cache put: ${err}`);
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Configure a response so that it will be cached when it has been sent.
|
|
161
|
+
* Caching ExpressJS responses requires intercepting of all the header
|
|
162
|
+
* and body setting calls on it, which may occur at any point in the
|
|
163
|
+
* response lifecycle, so this call must be made before the response
|
|
164
|
+
* is generated.
|
|
165
|
+
*
|
|
166
|
+
* If no key is provided, it's generated by generateCacheKey.
|
|
167
|
+
* Project code may call generateCacheKey with extra options to affect
|
|
168
|
+
* the key, or may use custom key generation logic. If code has
|
|
169
|
+
* previously called getResponseFromCache, the key and namespace are
|
|
170
|
+
* available as properties on the CachedResponse instance returned
|
|
171
|
+
* from that method.
|
|
172
|
+
*
|
|
173
|
+
* When caching response, the cache expiration time is set by
|
|
174
|
+
* the expiration parameter. The cache expiration time may be
|
|
175
|
+
* different to the response expiration time as set by the cache-control
|
|
176
|
+
* header. See the documentation for the 'expiration' parameter for
|
|
177
|
+
* details.
|
|
178
|
+
*
|
|
179
|
+
* @param req {express.request}
|
|
180
|
+
* @param res {express.response}
|
|
181
|
+
* @param [expiration] {Number} the number of seconds
|
|
182
|
+
* that a cached response should persist in the cache. If this is
|
|
183
|
+
* not provided, then the expiration time is taken from the
|
|
184
|
+
* Cache-Control header; the s-maxage value is used if available,
|
|
185
|
+
* then the max-age value. If no value can be found in the
|
|
186
|
+
* Cache-Control header, the default expiration time is
|
|
187
|
+
* one week.
|
|
188
|
+
* @param [key] {String} the key to use - if this is not supplied,
|
|
189
|
+
* generateCacheKey will be called to derive the key.
|
|
190
|
+
* @param [namespace] {String|undefined} the cache namespace to use.
|
|
191
|
+
* @param [shouldCacheResponse] {Function} an optional callback that is passed a
|
|
192
|
+
* Response after it has been sent but immediately before it is stored in
|
|
193
|
+
* the cache, and can control whether or not caching actually takes place.
|
|
194
|
+
* The function takes the request and response as parameters and should
|
|
195
|
+
* return true if the response should be cached, false if not.
|
|
196
|
+
* @private
|
|
197
|
+
*/
|
|
198
|
+
const cacheResponseWhenDone = ({
|
|
199
|
+
req,
|
|
200
|
+
res,
|
|
201
|
+
expiration,
|
|
202
|
+
key,
|
|
203
|
+
namespace,
|
|
204
|
+
shouldCacheResponse
|
|
205
|
+
}) => {
|
|
206
|
+
const locals = res.locals;
|
|
207
|
+
const caching = locals.responseCaching;
|
|
208
|
+
|
|
209
|
+
// If we have a key passed in, use that.
|
|
210
|
+
// If we have a key already generated by getResponseFromCache, use that.
|
|
211
|
+
// Otherwise generate the key from the request
|
|
212
|
+
/* istanbul ignore next */
|
|
213
|
+
caching.cacheKey = key || caching.cacheKey || generateCacheKey(req);
|
|
214
|
+
|
|
215
|
+
// Save values that will be needed when we store the response
|
|
216
|
+
caching.cacheNamespace = namespace;
|
|
217
|
+
caching.expiration = expiration;
|
|
218
|
+
|
|
219
|
+
// Set a flag that we use to detect a double call to end().
|
|
220
|
+
// Because we delay the actual call to end() until after
|
|
221
|
+
// caching is complete, we also delay when the response's 'finished'
|
|
222
|
+
// flag becomes true, and when the 'finished' event is emitted.
|
|
223
|
+
// This means that code may call end() more than once. We need
|
|
224
|
+
// to ignore any second call.
|
|
225
|
+
caching.endCalled = false;
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
Headers can be retrieved at any point, so there's no need to
|
|
229
|
+
intercept them. They're still present after the response ends
|
|
230
|
+
(contrary to some StackOverflow responses).
|
|
231
|
+
The response body can be sent in multiple chunks, at any time,
|
|
232
|
+
so we need a way to store references to those chunks so that
|
|
233
|
+
we can access the whole body to store it in the cache.
|
|
234
|
+
We patch the write() method on the response (which is a subclass of
|
|
235
|
+
node's ServerResponse, implementing the stream.Writeable interface)
|
|
236
|
+
and record the chunks as they are sent.
|
|
237
|
+
*/
|
|
238
|
+
(0, _ssrServer.wrapResponseWrite)(res);
|
|
239
|
+
|
|
240
|
+
/*
|
|
241
|
+
Patch the end() method of the response to call _storeResponseInCache.
|
|
242
|
+
We use this patching method instead of firing on the 'finished' event
|
|
243
|
+
because we want to guarantee that caching is complete before we
|
|
244
|
+
send the event. If we use the event, then caching may happen after
|
|
245
|
+
the event is complete, but in a Lambda environment processing is
|
|
246
|
+
halted once the event is sent, so caching might not occur.
|
|
247
|
+
*/
|
|
248
|
+
const originalEnd = res.end;
|
|
249
|
+
res.end = (...params) => {
|
|
250
|
+
// Check the cached flag - in some cases, end() may be
|
|
251
|
+
// called twice and we want to ignore the second call.
|
|
252
|
+
if (caching.endCalled) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
caching.endCalled = true;
|
|
256
|
+
|
|
257
|
+
// Handle any data writing that must be done before end()
|
|
258
|
+
// is called.
|
|
259
|
+
const {
|
|
260
|
+
data,
|
|
261
|
+
encoding,
|
|
262
|
+
callback
|
|
263
|
+
} = (0, _ssrServer.parseEndParameters)(params);
|
|
264
|
+
if (data) {
|
|
265
|
+
// We ignore the return value from write(), because we
|
|
266
|
+
// don't care whether the data is queued in user memory
|
|
267
|
+
// or is accepted by the OS, as long as we call write()
|
|
268
|
+
// before we call end()
|
|
269
|
+
res.write(data, encoding);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The response has been sent. If there is a shouldCacheResponse
|
|
273
|
+
// callback, we call it to decide whether to cache or not.
|
|
274
|
+
if (shouldCacheResponse) {
|
|
275
|
+
if (!shouldCacheResponse(req, res)) {
|
|
276
|
+
(0, _ssrServer.localDevLog)(`Req ${locals.requestId}: not caching response for ${req.url}`);
|
|
277
|
+
return originalEnd.call(res, callback);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// We know that all the data has been written, so we
|
|
282
|
+
// can now store the response in the cache and call
|
|
283
|
+
// end() on it.
|
|
284
|
+
req.app.applicationCache._cacheDeletePromise.then(() => {
|
|
285
|
+
(0, _ssrServer.localDevLog)(`Req ${locals.requestId}: caching response for ${req.url}`);
|
|
286
|
+
return storeResponseInCache(req, res);
|
|
287
|
+
}).finally(() => {
|
|
288
|
+
originalEnd.call(res, callback);
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Given a CachedResponse that represents a response from the
|
|
295
|
+
* cache, send it. Once this method has been called, the response
|
|
296
|
+
* is sent and can no longer be modified. If this method is
|
|
297
|
+
* called from the requestHook, the caller should return, and
|
|
298
|
+
* should not call next()
|
|
299
|
+
*
|
|
300
|
+
* @param cached {CachedResponse} the cached response to send
|
|
301
|
+
* @private
|
|
302
|
+
*/
|
|
303
|
+
exports.cacheResponseWhenDone = cacheResponseWhenDone;
|
|
304
|
+
const sendCachedResponse = cached => {
|
|
305
|
+
if (!(cached && cached.found)) {
|
|
306
|
+
throw new Error(`Cannot send a non-cached CachedResponse`);
|
|
307
|
+
}
|
|
308
|
+
cached._send();
|
|
309
|
+
cached._res.end();
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Look up a cached response for the given request in the persistent cache
|
|
314
|
+
* and return a CachedResponse that represents what was found.
|
|
315
|
+
*
|
|
316
|
+
* This method would generally be called in the requestHook. The caller
|
|
317
|
+
* should check the result of resolving the Promise returned by this
|
|
318
|
+
* method. The returned object's 'found' property is true if a response
|
|
319
|
+
* was found, 'false' if no response was found.
|
|
320
|
+
*
|
|
321
|
+
* The CachedResponse instance returned has details of any cached response
|
|
322
|
+
* found, and project code can then choose whether to send it or not. For
|
|
323
|
+
* example, the headers may be checked. To send that cached response, call
|
|
324
|
+
* sendCachedResponse with it.
|
|
325
|
+
*
|
|
326
|
+
* If there is no cached response found, or the project code does not
|
|
327
|
+
* choose to send it, then the code can also choose whether the
|
|
328
|
+
* response generated by the server should be cached. If so, it
|
|
329
|
+
* should call cacheResponseWhenDone.
|
|
330
|
+
*
|
|
331
|
+
* If no key is provided, it's generated by generateCacheKey.
|
|
332
|
+
* Project code may call generateCacheKey with extra options to affect
|
|
333
|
+
* the key, or may use custom key generation logic.
|
|
334
|
+
*
|
|
335
|
+
* By default, all cache entries occupy the same namespace, so responses
|
|
336
|
+
* cached for a given URL/querystring/headers by one version of the UPWA
|
|
337
|
+
* may be retrieved and used by other, later versions. If this is not
|
|
338
|
+
* the required behaviour, the options parameter may be used to pass a
|
|
339
|
+
* 'namespace' value. The same cache key may be used in different
|
|
340
|
+
* namespaces to cache different responses. For example, passing the
|
|
341
|
+
* bundle id as the namespace will result in each publish bundle starting
|
|
342
|
+
* with a cache that is effectively per-bundle. The namespace value
|
|
343
|
+
* may be any string, or an array of strings.
|
|
344
|
+
*
|
|
345
|
+
* @param req {express.request}
|
|
346
|
+
* @param res {express.response}
|
|
347
|
+
* @param [key] {String} the key to use - if this is not supplied,
|
|
348
|
+
* generateCacheKey will be called to derive the key.
|
|
349
|
+
* @param [namespace] {String|undefined} the cache namespace to use.
|
|
350
|
+
* @returns {Promise<CachedResponse>} resolves to a CachedResponse
|
|
351
|
+
* that represents the result of the cache lookup.
|
|
352
|
+
* @private
|
|
353
|
+
*/
|
|
354
|
+
exports.sendCachedResponse = sendCachedResponse;
|
|
355
|
+
const getResponseFromCache = ({
|
|
356
|
+
req,
|
|
357
|
+
res,
|
|
358
|
+
namespace,
|
|
359
|
+
key
|
|
360
|
+
}) => {
|
|
361
|
+
/* istanbul ignore next */
|
|
362
|
+
const locals = res.locals;
|
|
363
|
+
const workingKey = key || generateCacheKey(req);
|
|
364
|
+
|
|
365
|
+
// Save the key as the default for caching
|
|
366
|
+
locals.responseCaching.cacheKey = workingKey;
|
|
367
|
+
|
|
368
|
+
// Return a Promise that handles the asynchronous cache lookup
|
|
369
|
+
return req.app.applicationCache.get({
|
|
370
|
+
key: workingKey,
|
|
371
|
+
namespace
|
|
372
|
+
}).then(entry => {
|
|
373
|
+
(0, _ssrServer.localDevLog)(`Req ${locals.requestId}: ${entry.found ? 'Found' : 'Did not find'} cached response for ${req.url}`);
|
|
374
|
+
if (!entry.found) {
|
|
375
|
+
res.setHeader(_constants.X_MOBIFY_FROM_CACHE, 'false');
|
|
376
|
+
}
|
|
377
|
+
return new _ssrServer.CachedResponse({
|
|
378
|
+
entry,
|
|
379
|
+
req,
|
|
380
|
+
res
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Provided for use by requestHook overrides.
|
|
387
|
+
*
|
|
388
|
+
* Call this to return a res that is a redirect to a bundle asset.
|
|
389
|
+
* Be careful with res caching - 301 responses can be cached. You
|
|
390
|
+
* can call res.set to set the 'Cache-Control' header before
|
|
391
|
+
* calling this function.
|
|
392
|
+
*
|
|
393
|
+
* This function returns a Promise that resolves when the res
|
|
394
|
+
* has been sent. The caller does not need to wait on this Promise.
|
|
395
|
+
*
|
|
396
|
+
* @param {Object} options
|
|
397
|
+
* @param {Request} options.req - the ExpressJS request object
|
|
398
|
+
* @param {Response} options.res - the ExpressJS res object
|
|
399
|
+
* @param {String} [options.path] - the path to the bundle asset (relative
|
|
400
|
+
* to the bundle root/build directory). If this is falsy, then
|
|
401
|
+
* request.path is used (i.e. '/robots.txt' would be the path for
|
|
402
|
+
* 'robots.txt' at the top level of the build directory).
|
|
403
|
+
* @param {Number} [options.redirect] a 301 or 302 status code, which
|
|
404
|
+
* will be used to respond with a redirect to the bundle asset.
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
exports.getResponseFromCache = getResponseFromCache;
|
|
408
|
+
const respondFromBundle = ({
|
|
409
|
+
req,
|
|
410
|
+
res,
|
|
411
|
+
path,
|
|
412
|
+
redirect = 301
|
|
413
|
+
}) => {
|
|
414
|
+
// The path *may* start with a slash
|
|
415
|
+
const workingPath = path || req.path;
|
|
416
|
+
|
|
417
|
+
// Validate redirect
|
|
418
|
+
const workingRedirect = Number.parseInt(redirect);
|
|
419
|
+
/* istanbul ignore next */
|
|
420
|
+
if (workingRedirect < 301 || workingRedirect > 307) {
|
|
421
|
+
throw new Error('The redirect parameter must be a number between 301 and 307 inclusive');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// assetPath will not start with a slash
|
|
425
|
+
/* istanbul ignore next */
|
|
426
|
+
const assetPath = workingPath.startsWith('/') ? workingPath.slice(1) : workingPath;
|
|
427
|
+
|
|
428
|
+
// This is the relative or absolute location of the asset via the
|
|
429
|
+
// /mobify/bundle path
|
|
430
|
+
const location = `${(0, _ssrServer.getBundleBaseUrl)()}${assetPath}`;
|
|
431
|
+
(0, _ssrServer.localDevLog)(`Req ${res.locals.requestId}: redirecting ${assetPath} to ${location} (${workingRedirect})`);
|
|
432
|
+
res.redirect(workingRedirect, location);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get the appropriate runtime object for the current environment (remote or development)
|
|
437
|
+
* @returns Shallow of the runtime object with bound methods
|
|
438
|
+
*/
|
|
439
|
+
exports.respondFromBundle = respondFromBundle;
|
|
440
|
+
const getRuntime = () => {
|
|
441
|
+
const runtime = (0, _ssrServer.isRemote)() ? _buildRemoteServer.RemoteServerFactory :
|
|
442
|
+
// The dev server is for development only, and should not be deployed to production.
|
|
443
|
+
// To avoid deploying the dev server (and all of its dependencies) to production, it exists
|
|
444
|
+
// as an optional peer dependency to this package. The unusual `require` statement is needed
|
|
445
|
+
// to bypass webpack and ensure that the dev server does not get bundled.
|
|
446
|
+
eval('require').main.require('@salesforce/pwa-kit-dev/ssr/server/build-dev-server').DevServerFactory;
|
|
447
|
+
|
|
448
|
+
// The runtime is a JavaScript object.
|
|
449
|
+
// Sometimes the runtime APIs are invoked directly as express middlewares.
|
|
450
|
+
// In order to make sure the "this" keyword always have the correct context,
|
|
451
|
+
// we bind every single method to have the context of the object itself
|
|
452
|
+
const boundRuntime = _objectSpread({}, runtime);
|
|
453
|
+
for (const property of Object.keys(boundRuntime)) {
|
|
454
|
+
if (typeof boundRuntime[property] === 'function') {
|
|
455
|
+
boundRuntime[property] = boundRuntime[property].bind(boundRuntime);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return boundRuntime;
|
|
459
|
+
};
|
|
460
|
+
exports.getRuntime = getRuntime;
|