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

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