@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.
Files changed (45) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +24 -0
  3. package/package.json +76 -0
  4. package/scripts/version.js +22 -0
  5. package/ssr/server/build-remote-server.js +1025 -0
  6. package/ssr/server/build-remote-server.test.js +24 -0
  7. package/ssr/server/constants.js +37 -0
  8. package/ssr/server/express.js +460 -0
  9. package/ssr/server/express.lambda.test.js +383 -0
  10. package/ssr/server/express.test.js +847 -0
  11. package/ssr/server/test_fixtures/favicon.ico +0 -0
  12. package/ssr/server/test_fixtures/loadable-stats.json +1 -0
  13. package/ssr/server/test_fixtures/localhost.pem +45 -0
  14. package/ssr/server/test_fixtures/main.js +7 -0
  15. package/ssr/server/test_fixtures/mobify.png +0 -0
  16. package/ssr/server/test_fixtures/server-renderer.js +12 -0
  17. package/ssr/server/test_fixtures/worker.js +7 -0
  18. package/ssr/server/test_fixtures/worker.js.map +1 -0
  19. package/utils/morgan-stream.js +26 -0
  20. package/utils/ssr-cache.js +177 -0
  21. package/utils/ssr-cache.test.js +64 -0
  22. package/utils/ssr-config.client.js +23 -0
  23. package/utils/ssr-config.client.test.js +25 -0
  24. package/utils/ssr-config.js +20 -0
  25. package/utils/ssr-config.server.js +88 -0
  26. package/utils/ssr-config.server.test.js +30 -0
  27. package/utils/ssr-proxying.js +804 -0
  28. package/utils/ssr-proxying.test.js +591 -0
  29. package/utils/ssr-request-processing.js +164 -0
  30. package/utils/ssr-request-processing.test.js +95 -0
  31. package/utils/ssr-server/cached-response.js +116 -0
  32. package/utils/ssr-server/configure-proxy.js +241 -0
  33. package/utils/ssr-server/metrics-sender.js +191 -0
  34. package/utils/ssr-server/outgoing-request-hook.js +139 -0
  35. package/utils/ssr-server/parse-end-parameters.js +38 -0
  36. package/utils/ssr-server/process-express-response.js +56 -0
  37. package/utils/ssr-server/process-lambda-response.js +36 -0
  38. package/utils/ssr-server/update-global-agent-options.js +42 -0
  39. package/utils/ssr-server/update-global-agent-options.test.js +28 -0
  40. package/utils/ssr-server/utils.js +119 -0
  41. package/utils/ssr-server/utils.test.js +64 -0
  42. package/utils/ssr-server/wrap-response-write.js +40 -0
  43. package/utils/ssr-server.js +115 -0
  44. package/utils/ssr-server.test.js +835 -0
  45. package/utils/ssr-shared.js +185 -0
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+
3
+ function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
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
+ /* eslint-disable @typescript-eslint/no-var-requires */
11
+
12
+ // Mock static assets (require path is relative to the 'ssr' directory)
13
+ const mockStaticAssets = {};
14
+ jest.mock('../static/assets.json', () => mockStaticAssets, {
15
+ virtual: true
16
+ });
17
+
18
+ // We use require() for the ssr-server since we have to mock a module
19
+ // that it needs.
20
+ const {
21
+ RemoteServerFactory
22
+ } = require('./build-remote-server');
23
+ const AWSMockContext = require('aws-lambda-mock-context');
24
+ const createEvent = require('@serverless/event-mocks').default;
25
+ const crypto = require('crypto');
26
+ const nock = require('nock');
27
+ const https = require('https');
28
+ const path = require('path');
29
+ const zlib = require('zlib');
30
+ const {
31
+ X_HEADERS_TO_REMOVE
32
+ } = require('../../utils/ssr-proxying');
33
+ const TEST_PORT = 3446;
34
+ const testPackageMobify = {
35
+ ssrEnabled: true,
36
+ ssrOnly: ['main.js.map', 'ssr.js', 'ssr.js.map', 'vendor.js.map'],
37
+ ssrShared: ['main.js', 'ssr-loader.js', 'vendor.js', 'worker.js'],
38
+ ssrParameters: {
39
+ proxyProtocol1: 'http',
40
+ proxyHost1: 'test.proxy.com',
41
+ proxyPath1: 'base',
42
+ proxyProtocol2: 'https',
43
+ // This is intentionally an unreachable host
44
+ proxyHost2: '0.0.0.0',
45
+ proxyPath2: 'base2'
46
+ }
47
+ };
48
+ const testFixtures = path.resolve(process.cwd(), 'src/ssr/server/test_fixtures');
49
+
50
+ /**
51
+ * An HTTPS.Agent that allows self-signed certificates
52
+ * @type {module:https.Agent}
53
+ */
54
+ const httpsAgent = new https.Agent({
55
+ rejectUnauthorized: false
56
+ });
57
+ describe('SSRServer Lambda integration', () => {
58
+ let savedEnvironment;
59
+ let server;
60
+ beforeAll(() => {
61
+ savedEnvironment = _extends({}, process.env);
62
+ });
63
+ afterAll(() => {
64
+ process.env = savedEnvironment;
65
+ });
66
+ beforeEach(() => {
67
+ // Ensure the environment is set up
68
+ process.env = {
69
+ LISTEN_ADDRESS: '',
70
+ BUNDLE_ID: 1,
71
+ DEPLOY_TARGET: 'test',
72
+ EXTERNAL_DOMAIN_NAME: 'test.com',
73
+ MOBIFY_PROPERTY_ID: 'test',
74
+ AWS_LAMBDA_FUNCTION_NAME: 'pretend-to-be-remote'
75
+ };
76
+ });
77
+ afterEach(() => {
78
+ nock.cleanAll();
79
+ if (server) {
80
+ server.close();
81
+ }
82
+ });
83
+ const fakeBinaryPayload = crypto.randomBytes(16);
84
+ const jsPayload = '// This is JavaScript';
85
+ const redirectTarget = '/webapp/wcs/stores/servlet/prod_55555_10001_048010312563_-1002?shipToCntry=AU';
86
+ const lambdaTestCases = [{
87
+ name: 'plain HTML response',
88
+ path: '/',
89
+ validate: response => {
90
+ expect(response.statusCode).toBe(200);
91
+ expect(response.isBase64Encoded).toBe(false);
92
+ const contentType = response.headers['content-type'];
93
+ expect(contentType).toBeDefined();
94
+ expect(contentType.startsWith('text/html')).toBe(true);
95
+ expect(response.headers['content-encoding']).toBeFalsy();
96
+ expect(response.body).toContain('<html>');
97
+ },
98
+ route: (req, res) => {
99
+ res.send('<html></html>');
100
+ }
101
+ }, {
102
+ name: 'binary response',
103
+ path: '/mobify.png',
104
+ validate: response => {
105
+ expect(response.statusCode).toBe(200);
106
+ expect(response.isBase64Encoded).toBe(true);
107
+ expect(response.headers['content-type']).toBe('image/png');
108
+ expect(response.headers['content-encoding']).toBeFalsy();
109
+ const data = Buffer.from(response.body, 'base64');
110
+ expect(data).toEqual(fakeBinaryPayload);
111
+ },
112
+ route: (req, res) => {
113
+ // Return a binary payload
114
+ res.status(200).set('Content-Type', 'image/png').send(fakeBinaryPayload);
115
+ }
116
+ }, {
117
+ name: 'binary gzipped response',
118
+ path: '/mobify.png.gzip',
119
+ validate: response => {
120
+ expect(response.statusCode).toBe(200);
121
+ expect(response.isBase64Encoded).toBe(true);
122
+ const headers = response.headers;
123
+ expect(headers).toBeDefined();
124
+ expect(headers['content-type']).toBe('image/png');
125
+ expect(headers['content-encoding']).toBe('gzip');
126
+ const data = Buffer.from(response.body, 'base64');
127
+ const unzipped = zlib.gunzipSync(data);
128
+ expect(unzipped).toEqual(fakeBinaryPayload);
129
+ },
130
+ route: (req, res) => {
131
+ // Return a gzipped binary payload
132
+ res.status(200).set('Content-Type', 'image/png').set('Content-Encoding', 'gzip').send(zlib.gzipSync(fakeBinaryPayload));
133
+ }
134
+ }, {
135
+ name: 'binary gzipped text response',
136
+ path: '/mobify.png.astext',
137
+ validate: response => {
138
+ expect(response.statusCode).toBe(200);
139
+ expect(response.isBase64Encoded).toBe(true);
140
+ const headers = response.headers;
141
+ expect(headers).toBeDefined();
142
+ const contentType = headers['content-type'];
143
+ expect(contentType).toBeDefined();
144
+ expect(contentType.startsWith('text/plain')).toBe(true);
145
+ expect(headers['content-encoding']).toBe('gzip');
146
+ const data = Buffer.from(response.body, 'base64');
147
+ const unzipped = zlib.gunzipSync(data);
148
+ expect(unzipped).toEqual(fakeBinaryPayload);
149
+ },
150
+ route: (req, res) => {
151
+ // Return a gzipped binary payload using a non-binary
152
+ // content type.
153
+ res.status(200).set('Content-Type', 'text/plain').set('Content-Encoding', 'gzip').send(zlib.gzipSync(fakeBinaryPayload));
154
+ }
155
+ }, {
156
+ name: 'Javascript response',
157
+ path: '/mobify.js',
158
+ validate: response => {
159
+ expect(response.statusCode).toBe(200);
160
+ expect(response.isBase64Encoded).toBe(true);
161
+ const headers = response.headers;
162
+ expect(headers).toBeDefined();
163
+ const contentType = headers['content-type'];
164
+ expect(contentType).toBeDefined();
165
+ expect(contentType.startsWith('application/javascript')).toBe(true);
166
+ expect(headers['content-encoding']).toBeFalsy();
167
+ const js = Buffer.from(response.body, 'base64').toString();
168
+ expect(js).toEqual(jsPayload);
169
+ },
170
+ route: (req, res) => {
171
+ // Return JS text
172
+ res.status(200).set('Content-Type', 'application/javascript').send(Buffer.from(jsPayload, 'utf8'));
173
+ }
174
+ }, {
175
+ name: 'proxied text response',
176
+ path: '/mobify/proxy/base/test1',
177
+ validate: response => {
178
+ // Proxying is disabled for remote execution.
179
+ expect(response.statusCode).toBe(501);
180
+ },
181
+ route: () => {
182
+ throw new Error('Should never hit this line');
183
+ }
184
+ }, {
185
+ name: 'redirect 301',
186
+ path: '/redirect301',
187
+ validate: response => {
188
+ expect(response.statusCode).toBe(301);
189
+ expect(response.isBase64Encoded).toBe(false);
190
+ expect(response.headers.location).toEqual(redirectTarget);
191
+ expect(response.headers['content-length']).toBe('0');
192
+ },
193
+ route: (req, res) => {
194
+ res.redirect(301, redirectTarget);
195
+ }
196
+ }, {
197
+ name: 'redirect 302',
198
+ path: '/redirect302',
199
+ validate: response => {
200
+ expect(response.statusCode).toBe(302);
201
+ expect(response.isBase64Encoded).toBe(false);
202
+ expect(response.headers.location).toEqual(redirectTarget);
203
+ expect(response.headers['content-length']).toBe('0');
204
+ },
205
+ route: (req, res) => {
206
+ res.redirect(redirectTarget);
207
+ }
208
+ }];
209
+ lambdaTestCases.forEach(testCase => test(`${testCase.name}`, () => {
210
+ const options = {
211
+ buildDir: testFixtures,
212
+ mainFilename: 'main-big.js',
213
+ mobify: testPackageMobify,
214
+ sslFilePath: path.join(testFixtures, 'localhost.pem'),
215
+ quiet: true,
216
+ port: TEST_PORT,
217
+ fetchAgents: {
218
+ https: httpsAgent
219
+ }
220
+ };
221
+ const {
222
+ handler,
223
+ app,
224
+ server: srv
225
+ } = RemoteServerFactory.createHandler(options, app => {
226
+ app.get('/*', testCase.route);
227
+ });
228
+ server = srv;
229
+
230
+ // Set up the mock proxy
231
+ nock('http://test.proxy.com').get('/test1').reply(200, 'success1', {
232
+ 'Content-Type': 'text/plain'
233
+ });
234
+
235
+ // Ensure that this server sends metrics and that we can
236
+ // track them.
237
+ const metrics = [];
238
+ app.metrics._CW = {
239
+ putMetricData: (params, callback) => {
240
+ metrics.push(params);
241
+ callback(null);
242
+ }
243
+ };
244
+ const metricSent = name => !!metrics.find(metric => !!metric.MetricData.find(data => data.MetricName === name));
245
+
246
+ // Set up a fake event and a fake context for the Lambda call
247
+ const event = createEvent('aws:apiGateway', {
248
+ path: testCase.path,
249
+ body: undefined
250
+ });
251
+ if (event.queryStringParameters) {
252
+ delete event.queryStringParameters;
253
+ }
254
+
255
+ // Add a fake X-Amz-Cf-Id header
256
+ event.headers['X-Amz-Cf-Id'] = '1234567';
257
+ const context = AWSMockContext({
258
+ functionName: 'SSRTest'
259
+ });
260
+ return new Promise((resolve, reject) => {
261
+ handler(event, context, (err, response) => err ? reject(err) : resolve(response));
262
+ })
263
+ // The callback function gets passed an error object and
264
+ // the API Gateway response.
265
+ .then(response => {
266
+ // We expect all metrics to have been sent
267
+ expect(app.metrics.queueLength).toBe(0);
268
+
269
+ // We're not asserting which metrics were sent, just
270
+ // checking if any were sent. As of DESKTOP-434, every
271
+ // request will send metrics.
272
+ expect(!!metrics.length).toBe(true);
273
+
274
+ // We check for some specific metrics here
275
+ expect(metricSent('LambdaCreated')).toBe(true);
276
+
277
+ // We expect a context property to have been set false
278
+ expect(context.callbackWaitsForEmptyEventLoop).toBe(false);
279
+
280
+ // Check the response
281
+ testCase.validate(response);
282
+ });
283
+ }));
284
+ test('Lambda integration strips rogue headers', () => {
285
+ const options = {
286
+ buildDir: testFixtures,
287
+ mobify: testPackageMobify,
288
+ sslFilePath: path.join(testFixtures, 'localhost.pem'),
289
+ quiet: true,
290
+ port: TEST_PORT,
291
+ fetchAgents: {
292
+ https: httpsAgent
293
+ }
294
+ };
295
+ const {
296
+ handler,
297
+ server: srv
298
+ } = RemoteServerFactory.createHandler(options, app => {
299
+ const route = (req, res) => {
300
+ // Return the request headers as JSON
301
+ res.status(200).set('Content-Type', 'application/json').send(JSON.stringify(req.headers));
302
+ };
303
+ app.get('/*', route);
304
+ });
305
+ server = srv;
306
+
307
+ // Set up a fake event and a fake context for the Lambda call
308
+ const event = createEvent('aws:apiGateway', {
309
+ path: '/headers',
310
+ body: undefined,
311
+ // Other x-headers are added by AWSServerlessExpress
312
+ headers: {
313
+ 'x-api-key': '1234567890'
314
+ }
315
+ });
316
+ if (event.queryStringParameters) {
317
+ delete event.queryStringParameters;
318
+ }
319
+ const context = AWSMockContext({
320
+ functionName: 'SSRTest'
321
+ });
322
+ const call = event => new Promise(resolve => handler(event, context, (err, response) => resolve(response)));
323
+ return call(event).then(response => {
324
+ expect(response.statusCode).toBe(200);
325
+ const decodedBody = response.isBase64Encoded ? atob(response.body) : response.body;
326
+ const reqHeaders = JSON.parse(decodedBody);
327
+ X_HEADERS_TO_REMOVE.forEach(key => expect(reqHeaders[key]).toBeUndefined());
328
+ });
329
+ });
330
+ test('Lambda reuse behaviour', () => {
331
+ const route = jest.fn((req, res) => {
332
+ res.send('<html/>');
333
+ });
334
+ const options = {
335
+ buildDir: testFixtures,
336
+ mobify: testPackageMobify,
337
+ sslFilePath: path.join(testFixtures, 'localhost.pem'),
338
+ quiet: true,
339
+ port: TEST_PORT,
340
+ fetchAgents: {
341
+ https: httpsAgent
342
+ }
343
+ };
344
+ const {
345
+ app,
346
+ handler,
347
+ server: srv
348
+ } = RemoteServerFactory.createHandler(options, app => {
349
+ app.get('/*', route);
350
+ });
351
+ const collectGarbage = jest.spyOn(app, '_collectGarbage');
352
+ const sendMetric = jest.spyOn(app, 'sendMetric');
353
+ server = srv;
354
+
355
+ // Set up a fake event and a fake context for the Lambda call
356
+ const event = createEvent('aws:apiGateway', {
357
+ path: '/',
358
+ body: undefined
359
+ });
360
+ if (event.queryStringParameters) {
361
+ delete event.queryStringParameters;
362
+ }
363
+ const context = AWSMockContext({
364
+ functionName: 'SSRTest'
365
+ });
366
+ const call = event => new Promise(resolve => handler(event, context, (err, response) => resolve(response)));
367
+ return Promise.resolve().then(() => call(event)).then(response => {
368
+ // First request - Lambda container created
369
+ expect(response.statusCode).toBe(200);
370
+ expect(collectGarbage.mock.calls).toHaveLength(0);
371
+ expect(route.mock.calls).toHaveLength(1);
372
+ expect(sendMetric).toHaveBeenCalledWith('LambdaCreated');
373
+ expect(sendMetric).not.toHaveBeenCalledWith('LambdaReused');
374
+ }).then(() => call(event)).then(response => {
375
+ // Second call - Lambda container reused
376
+ expect(response.statusCode).toBe(200);
377
+ expect(collectGarbage.mock.calls).toHaveLength(1);
378
+ expect(route.mock.calls).toHaveLength(2);
379
+ expect(sendMetric).toHaveBeenCalledWith('LambdaCreated');
380
+ expect(sendMetric).toHaveBeenCalledWith('LambdaReused');
381
+ });
382
+ });
383
+ });