@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,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
|
+
});
|