@salesforce/pwa-kit-dev 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 +35 -0
- package/bin/pwa-kit-dev.js +461 -0
- package/configs/babel/babel-config.js +34 -0
- package/configs/eslint/README.md +21 -0
- package/configs/eslint/eslint-config.js +11 -0
- package/configs/eslint/index.js +11 -0
- package/configs/eslint/no-react.js +18 -0
- package/configs/eslint/partials/base.js +38 -0
- package/configs/eslint/partials/jest.js +24 -0
- package/configs/eslint/partials/react.js +29 -0
- package/configs/eslint/partials/typescript-permit-any.js +31 -0
- package/configs/eslint/partials/typescript.js +17 -0
- package/configs/eslint/recommended.js +20 -0
- package/configs/eslint/safe-types.js +20 -0
- package/configs/jest/jest-babel-transform.js +19 -0
- package/configs/jest/jest.config.js +33 -0
- package/configs/jest/mocks/fileMock.js +9 -0
- package/configs/jest/mocks/styleMock.js +9 -0
- package/configs/jest/mocks/svgMock.js +11 -0
- package/configs/webpack/config-names.js +24 -0
- package/configs/webpack/config.js +425 -0
- package/configs/webpack/overrides-plugin.js +120 -0
- package/configs/webpack/plugins.js +92 -0
- package/package.json +150 -0
- package/scripts/version.js +22 -0
- package/ssr/server/build-dev-server.js +443 -0
- package/ssr/server/build-dev-server.test.js +635 -0
- package/ssr/server/loading-screen/css/main.css +272 -0
- package/ssr/server/loading-screen/css/normalize.css +349 -0
- package/ssr/server/loading-screen/img/cloud-1.svg +1 -0
- package/ssr/server/loading-screen/img/cloud-2.svg +1 -0
- package/ssr/server/loading-screen/img/cloud-3.svg +1 -0
- package/ssr/server/loading-screen/img/cloud.svg +1 -0
- package/ssr/server/loading-screen/img/codey-arm.svg +1 -0
- package/ssr/server/loading-screen/img/codey-bear.svg +1 -0
- package/ssr/server/loading-screen/img/codey-bg.svg +1 -0
- package/ssr/server/loading-screen/img/codey-cloud.svg +1 -0
- package/ssr/server/loading-screen/img/codey-search.svg +1 -0
- package/ssr/server/loading-screen/img/codey.svg +1 -0
- package/ssr/server/loading-screen/img/codeyCarry.svg +1 -0
- package/ssr/server/loading-screen/img/devDocumentation.svg +1 -0
- package/ssr/server/loading-screen/img/devGithub.svg +1 -0
- package/ssr/server/loading-screen/img/devTrailhead.svg +1 -0
- package/ssr/server/loading-screen/img/logo.svg +1 -0
- package/ssr/server/loading-screen/img/slds_spinner_brand_9EA9F1.gif +0 -0
- package/ssr/server/loading-screen/index.html +130 -0
- package/ssr/server/test_fixtures/app/main.js +6 -0
- package/ssr/server/test_fixtures/app/static/favicon.ico +0 -0
- package/ssr/server/test_fixtures/localhost.pem +45 -0
- package/utils/script-utils.js +312 -0
- package/utils/script-utils.test.js +282 -0
- package/utils/test-fixtures/minimal-built-app/ssr.js +9 -0
- package/utils/test-fixtures/minimal-built-app/static/favicon.ico +0 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _constants = require("@salesforce/pwa-kit-runtime/ssr/server/constants");
|
|
4
|
+
var _ssrProxying = require("@salesforce/pwa-kit-runtime/utils/ssr-proxying");
|
|
5
|
+
var _express = require("@salesforce/pwa-kit-runtime/ssr/server/express");
|
|
6
|
+
var _nodeFetch = _interopRequireDefault(require("node-fetch"));
|
|
7
|
+
var _supertest = _interopRequireDefault(require("supertest"));
|
|
8
|
+
var _buildDevServer = require("./build-dev-server");
|
|
9
|
+
var _os = _interopRequireDefault(require("os"));
|
|
10
|
+
var _path = _interopRequireDefault(require("path"));
|
|
11
|
+
var _http = _interopRequireDefault(require("http"));
|
|
12
|
+
var _https = _interopRequireDefault(require("https"));
|
|
13
|
+
var _nock = _interopRequireDefault(require("nock"));
|
|
14
|
+
var _zlib = _interopRequireDefault(require("zlib"));
|
|
15
|
+
var _fsExtra = _interopRequireDefault(require("fs-extra"));
|
|
16
|
+
var _rimraf = _interopRequireDefault(require("rimraf"));
|
|
17
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
18
|
+
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); }
|
|
19
|
+
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
|
20
|
+
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
|
21
|
+
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; }
|
|
22
|
+
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; }
|
|
23
|
+
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; }
|
|
24
|
+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
25
|
+
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); } /*
|
|
26
|
+
* Copyright (c) 2022, Salesforce, Inc.
|
|
27
|
+
* All rights reserved.
|
|
28
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
29
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
30
|
+
*/
|
|
31
|
+
const TEST_PORT = 3444;
|
|
32
|
+
const testFixtures = _path.default.resolve(__dirname, 'test_fixtures');
|
|
33
|
+
|
|
34
|
+
// Mocks methods on the DevServerFactory to skip setting
|
|
35
|
+
// up Webpack's dev middleware – a massive simplification
|
|
36
|
+
// for testing.
|
|
37
|
+
const NoWebpackDevServerFactory = _objectSpread(_objectSpread({}, _buildDevServer.DevServerFactory), {}, {
|
|
38
|
+
_addSDKInternalHandlers() {
|
|
39
|
+
// Override default implementation with no-op
|
|
40
|
+
},
|
|
41
|
+
_getRequestProcessor() {
|
|
42
|
+
// Override default implementation with no-op
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const httpAgent = new _http.default.Agent({});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An HTTPS.Agent that allows self-signed certificates
|
|
49
|
+
* @type {module:https.Agent}
|
|
50
|
+
*/
|
|
51
|
+
const httpsAgent = new _https.default.Agent({
|
|
52
|
+
rejectUnauthorized: false
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fetch and ignore self-signed certificate errors.
|
|
57
|
+
*/
|
|
58
|
+
const insecureFetch = (url, opts) => {
|
|
59
|
+
return (0, _nodeFetch.default)(url, _objectSpread(_objectSpread({}, opts), {}, {
|
|
60
|
+
agent: _parsedURL => _parsedURL.protocol === 'https:' ? httpsAgent : httpAgent
|
|
61
|
+
}));
|
|
62
|
+
};
|
|
63
|
+
const opts = (overrides = {}) => {
|
|
64
|
+
const defaults = {
|
|
65
|
+
buildDir: _path.default.join(testFixtures, 'build'),
|
|
66
|
+
mobify: {
|
|
67
|
+
ssrEnabled: true,
|
|
68
|
+
ssrOnly: ['main.js.map', 'ssr.js', 'ssr.js.map'],
|
|
69
|
+
ssrShared: ['main.js', 'ssr-loader.js', 'worker.js'],
|
|
70
|
+
ssrParameters: {
|
|
71
|
+
proxyConfigs: [{
|
|
72
|
+
protocol: 'https',
|
|
73
|
+
host: 'test.proxy.com',
|
|
74
|
+
path: 'base'
|
|
75
|
+
}, {
|
|
76
|
+
protocol: 'https',
|
|
77
|
+
// This is intentionally an unreachable host
|
|
78
|
+
host: '0.0.0.0',
|
|
79
|
+
path: 'base2'
|
|
80
|
+
}, {
|
|
81
|
+
protocol: 'https',
|
|
82
|
+
host: 'test.proxy.com',
|
|
83
|
+
path: 'base3',
|
|
84
|
+
caching: true
|
|
85
|
+
}]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
quiet: true,
|
|
89
|
+
port: TEST_PORT,
|
|
90
|
+
protocol: 'http',
|
|
91
|
+
sslFilePath: _path.default.join(testFixtures, 'localhost.pem')
|
|
92
|
+
};
|
|
93
|
+
return _objectSpread(_objectSpread({}, defaults), overrides);
|
|
94
|
+
};
|
|
95
|
+
describe('DevServer error handlers', () => {
|
|
96
|
+
const expectServerErrorHandled = (error, times) => {
|
|
97
|
+
const proc = {
|
|
98
|
+
exit: jest.fn()
|
|
99
|
+
};
|
|
100
|
+
const devserver = {
|
|
101
|
+
close: jest.fn()
|
|
102
|
+
};
|
|
103
|
+
const handler = (0, _buildDevServer.makeErrorHandler)(proc, devserver, jest.fn());
|
|
104
|
+
handler({
|
|
105
|
+
code: error
|
|
106
|
+
});
|
|
107
|
+
expect(devserver.close).toHaveBeenCalledTimes(times);
|
|
108
|
+
};
|
|
109
|
+
test('should exit the current process if the requested port is in use', () => {
|
|
110
|
+
expectServerErrorHandled('EADDRINUSE', 1);
|
|
111
|
+
});
|
|
112
|
+
test('should ignore errors other than EADDRINUSE', () => {
|
|
113
|
+
expectServerErrorHandled('EACCES', 0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('DevServer startup', () => {
|
|
117
|
+
test('_createApp creates an express app', () => {
|
|
118
|
+
const app = NoWebpackDevServerFactory._createApp(opts());
|
|
119
|
+
expect(app.options.defaultCacheControl).toEqual(_constants.NO_CACHE);
|
|
120
|
+
});
|
|
121
|
+
test(`_createApp validates missing or invalid field "protocol"`, () => {
|
|
122
|
+
expect(() => NoWebpackDevServerFactory._createApp(opts({
|
|
123
|
+
protocol: 'ssl'
|
|
124
|
+
}))).toThrow();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe('DevServer loading page', () => {
|
|
128
|
+
test('should redirect to the loading screen with an HTTP 302', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
129
|
+
const options = opts();
|
|
130
|
+
const app = NoWebpackDevServerFactory._createApp(options);
|
|
131
|
+
app.use('/', _buildDevServer.DevServerFactory._redirectToLoadingScreen);
|
|
132
|
+
return (0, _supertest.default)(app).get('/').expect(302) // Expecting the 302 temporary redirect (not 301)
|
|
133
|
+
.then(response => {
|
|
134
|
+
expect(response.headers.location).toBe('/__mrt/loading-screen/index.html?loading=1');
|
|
135
|
+
});
|
|
136
|
+
}));
|
|
137
|
+
});
|
|
138
|
+
describe('DevServer request processor support', () => {
|
|
139
|
+
const helloWorld = '<div>hello world</div>';
|
|
140
|
+
let route;
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
route = jest.fn().mockImplementation((req, res) => {
|
|
143
|
+
res.send(helloWorld);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
route = undefined;
|
|
148
|
+
});
|
|
149
|
+
test('SSRServer supports the request-processor and request class', () => {
|
|
150
|
+
const ServerFactory = _objectSpread(_objectSpread({}, NoWebpackDevServerFactory), {
|
|
151
|
+
_getRequestProcessor() {
|
|
152
|
+
return {
|
|
153
|
+
processRequest: ({
|
|
154
|
+
getRequestClass,
|
|
155
|
+
setRequestClass
|
|
156
|
+
}) => {
|
|
157
|
+
console.log(`getRequestClass returns ${getRequestClass()}`);
|
|
158
|
+
setRequestClass('bot');
|
|
159
|
+
return {
|
|
160
|
+
path: '/altered',
|
|
161
|
+
querystring: 'foo=bar'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
const app = ServerFactory._createApp(opts());
|
|
168
|
+
app.get('/*', route);
|
|
169
|
+
return (0, _supertest.default)(app).get('/').expect(200).then(response => {
|
|
170
|
+
const requestClass = response.headers[_ssrProxying.X_MOBIFY_REQUEST_CLASS];
|
|
171
|
+
expect(requestClass).toBe('bot');
|
|
172
|
+
expect(route).toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
test('SSRServer handles no request processor', () => {
|
|
176
|
+
const ServerFactory = _objectSpread(_objectSpread({}, NoWebpackDevServerFactory), {
|
|
177
|
+
_getRequestProcessor() {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
const options = opts();
|
|
182
|
+
const app = ServerFactory._createApp(options);
|
|
183
|
+
app.get('/*', route);
|
|
184
|
+
return (0, _supertest.default)(app).get('/').expect(200).then(response => {
|
|
185
|
+
expect(response.headers[_ssrProxying.X_MOBIFY_REQUEST_CLASS]).toBeUndefined();
|
|
186
|
+
expect(route).toHaveBeenCalled();
|
|
187
|
+
expect(response.text).toEqual(helloWorld);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
test('SSRServer handles a broken request processor', () => {
|
|
191
|
+
// This is a broken because processRequest is required to return
|
|
192
|
+
// {path, querystring}, but returns undefined
|
|
193
|
+
|
|
194
|
+
const ServerFactory = _objectSpread(_objectSpread({}, NoWebpackDevServerFactory), {
|
|
195
|
+
_getRequestProcessor() {
|
|
196
|
+
return {
|
|
197
|
+
processRequest: () => {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
const app = ServerFactory._createApp(opts());
|
|
204
|
+
app.get('/*', route);
|
|
205
|
+
return (0, _supertest.default)(app).get('/').expect(500).then(() => {
|
|
206
|
+
expect(route).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe('DevServer listening on http/https protocol', () => {
|
|
211
|
+
let server;
|
|
212
|
+
let originalEnv;
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
originalEnv = _extends({}, process.env);
|
|
215
|
+
});
|
|
216
|
+
afterEach(() => {
|
|
217
|
+
if (server) {
|
|
218
|
+
server.close();
|
|
219
|
+
}
|
|
220
|
+
process.env = originalEnv;
|
|
221
|
+
});
|
|
222
|
+
const cases = [{
|
|
223
|
+
options: {
|
|
224
|
+
protocol: 'http'
|
|
225
|
+
},
|
|
226
|
+
env: {},
|
|
227
|
+
name: 'listens on http (set in options)'
|
|
228
|
+
}, {
|
|
229
|
+
options: {
|
|
230
|
+
protocol: 'https'
|
|
231
|
+
},
|
|
232
|
+
env: {},
|
|
233
|
+
name: 'listens on https (set in options)'
|
|
234
|
+
}, {
|
|
235
|
+
options: {},
|
|
236
|
+
env: {
|
|
237
|
+
DEV_SERVER_PROTOCOL: 'http'
|
|
238
|
+
},
|
|
239
|
+
name: 'listens on http (set in env var)'
|
|
240
|
+
}, {
|
|
241
|
+
options: {},
|
|
242
|
+
env: {
|
|
243
|
+
DEV_SERVER_PROTOCOL: 'https'
|
|
244
|
+
},
|
|
245
|
+
name: 'listens on https (set in env var)'
|
|
246
|
+
}];
|
|
247
|
+
cases.forEach(({
|
|
248
|
+
options,
|
|
249
|
+
env,
|
|
250
|
+
name
|
|
251
|
+
}) => {
|
|
252
|
+
const protocol = options.protocol || env.DEV_SERVER_PROTOCOL;
|
|
253
|
+
test(`${name}`, () => {
|
|
254
|
+
process.env = _objectSpread(_objectSpread({}, process.env), env);
|
|
255
|
+
const {
|
|
256
|
+
server: _server
|
|
257
|
+
} = NoWebpackDevServerFactory.createHandler(opts(options), app => {
|
|
258
|
+
app.get('/*', (req, res) => {
|
|
259
|
+
res.send('<div>hello world</div>');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
server = _server;
|
|
263
|
+
return insecureFetch(`${protocol}://localhost:${TEST_PORT}`).then(response => {
|
|
264
|
+
expect(response.ok).toBe(true);
|
|
265
|
+
return Promise.resolve();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
test('SSRServer proxying handles empty path', () => {
|
|
271
|
+
// This tests a specific fault that occurred when making a proxy request to mobify/proxy/base/.
|
|
272
|
+
const location = '/another/path';
|
|
273
|
+
|
|
274
|
+
// Create a mock response from the proxied backend
|
|
275
|
+
const nockRedirect = (0, _nock.default)('https://test.proxy.com').get('/').reply(301, '', {
|
|
276
|
+
Location: location
|
|
277
|
+
});
|
|
278
|
+
const options = opts();
|
|
279
|
+
|
|
280
|
+
// We expect the Express app to rewrite redirect responses
|
|
281
|
+
const rewritten = `${options.protocol}://localhost:${options.port}/mobify/proxy/base${location}`;
|
|
282
|
+
const app = NoWebpackDevServerFactory._createApp(options);
|
|
283
|
+
return (0, _supertest.default)(app).get('/mobify/proxy/base/').expect(301).expect('Location', rewritten)
|
|
284
|
+
// Expected to hit the backend
|
|
285
|
+
.then(() => expect(nockRedirect.isDone()).toBe(true));
|
|
286
|
+
});
|
|
287
|
+
describe('DevServer proxying', () => {
|
|
288
|
+
afterEach(() => {
|
|
289
|
+
_nock.default.cleanAll();
|
|
290
|
+
});
|
|
291
|
+
test('rewrites redirects', () => {
|
|
292
|
+
// Example:
|
|
293
|
+
// - You're running a proxy server for example.com at localhost/mobify/base
|
|
294
|
+
// - You request localhost/mobify/base which hits example.com and returns a redirect to example.com/found
|
|
295
|
+
// - The proxy must rewrite that redirect to localhost/mobify/base/found
|
|
296
|
+
|
|
297
|
+
// Create a mock response from the proxied backend
|
|
298
|
+
const location = '/another/path';
|
|
299
|
+
const nockRedirect = (0, _nock.default)('https://test.proxy.com').get('/test/path').reply(301, '', {
|
|
300
|
+
Location: location
|
|
301
|
+
});
|
|
302
|
+
const options = opts();
|
|
303
|
+
|
|
304
|
+
// We expect the Express app to rewrite redirect responses
|
|
305
|
+
const rewritten = `${options.protocol}://localhost:${options.port}/mobify/proxy/base${location}`;
|
|
306
|
+
const app = NoWebpackDevServerFactory._createApp(options);
|
|
307
|
+
return (0, _supertest.default)(app).get('/mobify/proxy/base/test/path').expect(301).expect('Location', rewritten)
|
|
308
|
+
// Expected to hit the backend
|
|
309
|
+
.then(() => expect(nockRedirect.isDone()).toBe(true));
|
|
310
|
+
});
|
|
311
|
+
test('rewrites headers', () => {
|
|
312
|
+
// Use nock to mock out a host to which we proxy
|
|
313
|
+
const requestHeaders = [];
|
|
314
|
+
const responseHeaders = {
|
|
315
|
+
'Set-Cookie': 'xyz=456'
|
|
316
|
+
};
|
|
317
|
+
const targetPath = '/test/path3?abc=123';
|
|
318
|
+
const nockResponse = (0, _nock.default)('https://test.proxy.com').get(targetPath).reply(200, function () {
|
|
319
|
+
requestHeaders.push(this.req.headers);
|
|
320
|
+
}, responseHeaders);
|
|
321
|
+
const app = NoWebpackDevServerFactory._createApp(opts());
|
|
322
|
+
const path = `/mobify/proxy/base${targetPath}`;
|
|
323
|
+
const outgoingHeaders = {
|
|
324
|
+
Host: 'localhost:4567',
|
|
325
|
+
Origin: 'https://localhost:4567',
|
|
326
|
+
Cookie: 'abc=123',
|
|
327
|
+
'x-multi-value': 'abc, def'
|
|
328
|
+
};
|
|
329
|
+
return (0, _supertest.default)(app).get(path).set(outgoingHeaders).then(response => {
|
|
330
|
+
// Expected that proxy request would be fetched
|
|
331
|
+
expect(nockResponse.isDone()).toBe(true);
|
|
332
|
+
|
|
333
|
+
// We expect a 200 (that nock returned)
|
|
334
|
+
expect(response.status).toBe(200);
|
|
335
|
+
|
|
336
|
+
// We expect that we got a copy of the request headers
|
|
337
|
+
expect(requestHeaders).toHaveLength(1);
|
|
338
|
+
|
|
339
|
+
// Verify that the request headers were rewritten
|
|
340
|
+
const headers = requestHeaders[0];
|
|
341
|
+
expect(headers.host).toBe('test.proxy.com');
|
|
342
|
+
expect(headers.origin).toBe('https://test.proxy.com');
|
|
343
|
+
|
|
344
|
+
// Verify that the cookie and multi-value headers are
|
|
345
|
+
// correctly preserved.
|
|
346
|
+
expect(headers.cookie).toBe('abc=123');
|
|
347
|
+
const multi = headers['x-multi-value'];
|
|
348
|
+
expect(multi).toBe('abc, def');
|
|
349
|
+
|
|
350
|
+
// Verify that the response contains a Set-Cookie
|
|
351
|
+
const setCookie = response.headers['set-cookie'];
|
|
352
|
+
expect(setCookie).toHaveLength(1);
|
|
353
|
+
expect(setCookie[0]).toBe('xyz=456');
|
|
354
|
+
|
|
355
|
+
// Verify that the x-proxy-request-url header is present in
|
|
356
|
+
// the response
|
|
357
|
+
const requestUrl = response.headers[_ssrProxying.X_PROXY_REQUEST_URL];
|
|
358
|
+
expect(requestUrl).toBe(`https://test.proxy.com${targetPath}`);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
test('restricts methods', () => {
|
|
362
|
+
// Use nock to mock out a host to which we proxy, though we
|
|
363
|
+
// do not expect the request to be made.
|
|
364
|
+
const nockResponse = (0, _nock.default)('https://test.proxy.com').get('/test/path3').reply(200, 'OK');
|
|
365
|
+
const app = NoWebpackDevServerFactory._createApp(opts());
|
|
366
|
+
const path = '/mobify/caching/base3/test/path3';
|
|
367
|
+
return (0, _supertest.default)(app).put(path).then(response => {
|
|
368
|
+
// Expected that proxy request would not be fetched
|
|
369
|
+
expect(nockResponse.isDone()).toBe(false);
|
|
370
|
+
expect(response.status).toBe(405);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
test('filters headers', () => {
|
|
374
|
+
// Use nock to mock out a host to which we proxy
|
|
375
|
+
const nockResponse = (0, _nock.default)('https://test.proxy.com').get('/test/path3').reply(200, function () {
|
|
376
|
+
const headers = this.req.headers;
|
|
377
|
+
expect('x-mobify-access-key' in headers).toBe(false);
|
|
378
|
+
expect('cache-control' in headers).toBe(false);
|
|
379
|
+
expect('cookie' in headers).toBe(false);
|
|
380
|
+
expect(headers['accept-language']).toBe('en');
|
|
381
|
+
expect(headers['accept-encoding']).toBe('gzip');
|
|
382
|
+
|
|
383
|
+
// This value is fixed
|
|
384
|
+
expect(headers['user-agent']).toBe('Amazon CloudFront');
|
|
385
|
+
return 'Success';
|
|
386
|
+
});
|
|
387
|
+
const app = NoWebpackDevServerFactory._createApp(opts());
|
|
388
|
+
const path = '/mobify/caching/base3/test/path3';
|
|
389
|
+
return (0, _supertest.default)(app).get(path).set({
|
|
390
|
+
// These headers are disallowed and should be removed
|
|
391
|
+
'x-mobify-access-key': '12345',
|
|
392
|
+
'cache-control': 'no-cache',
|
|
393
|
+
cookie: 'abc=123',
|
|
394
|
+
// These headers are allowed
|
|
395
|
+
'accept-encoding': 'gzip',
|
|
396
|
+
'accept-language': 'en'
|
|
397
|
+
}).then(response => {
|
|
398
|
+
// Expected that proxy request would be fetched
|
|
399
|
+
expect(nockResponse.isDone()).toBe(true);
|
|
400
|
+
expect(response.status).toBe(200);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
test('handles error', () => {
|
|
404
|
+
const app = NoWebpackDevServerFactory._createApp(opts());
|
|
405
|
+
return (0, _supertest.default)(app).get('/mobify/proxy/base2/test/path').expect(500);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
describe('DevServer persistent caching support', () => {
|
|
409
|
+
const namespace = 'test';
|
|
410
|
+
const keyFromURL = url => encodeURIComponent(url);
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* A cache decorator for a route function that uses the percent-encoded req.url
|
|
414
|
+
* as keys for all cache entries (this makes testing easier).
|
|
415
|
+
*/
|
|
416
|
+
const cachedRoute = route => (req, res) => {
|
|
417
|
+
const shouldCache = !req.query.noCache;
|
|
418
|
+
const cacheArgs = {
|
|
419
|
+
req,
|
|
420
|
+
res,
|
|
421
|
+
namespace,
|
|
422
|
+
key: keyFromURL(req.url)
|
|
423
|
+
};
|
|
424
|
+
const shouldCacheResponse = (req, res) => res.statusCode >= 200 && res.statusCode < 300;
|
|
425
|
+
return Promise.resolve().then(() => (0, _express.getResponseFromCache)(cacheArgs)).then(entry => {
|
|
426
|
+
if (entry.found) {
|
|
427
|
+
(0, _express.sendCachedResponse)(entry);
|
|
428
|
+
} else {
|
|
429
|
+
if (shouldCache) {
|
|
430
|
+
(0, _express.cacheResponseWhenDone)(_objectSpread({
|
|
431
|
+
shouldCacheResponse
|
|
432
|
+
}, cacheArgs));
|
|
433
|
+
}
|
|
434
|
+
return route(req, res);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* A test route that returns different content types based on query params.
|
|
441
|
+
*/
|
|
442
|
+
const routeImplementation = (req, res) => {
|
|
443
|
+
const status = parseInt(req.query.status || 200);
|
|
444
|
+
switch (req.query.type) {
|
|
445
|
+
case 'precompressed':
|
|
446
|
+
res.status(status);
|
|
447
|
+
res.setHeader('content-type', 'application/javascript');
|
|
448
|
+
res.setHeader('content-encoding', 'gzip');
|
|
449
|
+
res.send(_zlib.default.gzipSync(_fsExtra.default.readFileSync(_path.default.join(testFixtures, 'app', 'main.js'))));
|
|
450
|
+
break;
|
|
451
|
+
case 'compressed-responses-test':
|
|
452
|
+
// The "compression" middleware only compresses responses that are
|
|
453
|
+
// "compressable". So we must set the `content-type` to a known
|
|
454
|
+
// "compressible" type.
|
|
455
|
+
res.setHeader('content-type', 'text/html');
|
|
456
|
+
res.write('<div>Hello Compression</div>');
|
|
457
|
+
res.end();
|
|
458
|
+
break;
|
|
459
|
+
default:
|
|
460
|
+
throw new Error('Unhandled case');
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
let app, route;
|
|
464
|
+
beforeEach(() => {
|
|
465
|
+
route = jest.fn().mockImplementation(routeImplementation);
|
|
466
|
+
const withCaching = cachedRoute(route);
|
|
467
|
+
app = NoWebpackDevServerFactory._createApp(opts());
|
|
468
|
+
app.get('/*', withCaching);
|
|
469
|
+
});
|
|
470
|
+
afterEach(() => {
|
|
471
|
+
app.applicationCache.close();
|
|
472
|
+
app = null;
|
|
473
|
+
route = null;
|
|
474
|
+
});
|
|
475
|
+
test('No caching of compressed responses', () => {
|
|
476
|
+
// ADN-118 reported that a cached response was correctly sent
|
|
477
|
+
// the first time, but was corrupted the second time. This
|
|
478
|
+
// test is specific to that issue.
|
|
479
|
+
const url = '/?type=compressed-responses-test';
|
|
480
|
+
const expected = '<div>Hello Compression</div>';
|
|
481
|
+
return Promise.resolve().then(() => (0, _supertest.default)(app).get(url)).then(res => app._requestMonitor._waitForResponses().then(() => res)).then(res => {
|
|
482
|
+
expect(res.status).toBe(200);
|
|
483
|
+
expect(res.headers['x-mobify-from-cache']).toBe('false');
|
|
484
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
485
|
+
expect(res.text).toEqual(expected);
|
|
486
|
+
}).then(() => app.applicationCache.get({
|
|
487
|
+
key: keyFromURL(url),
|
|
488
|
+
namespace
|
|
489
|
+
})).then(entry => expect(entry.found).toBe(false)).then(() => (0, _supertest.default)(app).get(url)).then(res => app._requestMonitor._waitForResponses().then(() => res)).then(res => {
|
|
490
|
+
expect(res.status).toBe(200);
|
|
491
|
+
expect(res.headers['x-mobify-from-cache']).toBe('false');
|
|
492
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
493
|
+
expect(res.text).toEqual(expected);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
test('Compressed responses are not re-compressed', () => {
|
|
497
|
+
const url = '/?type=precompressed';
|
|
498
|
+
return (0, _supertest.default)(app).get(url).then(res => app._requestMonitor._waitForResponses().then(() => res)).then(res => {
|
|
499
|
+
expect(res.status).toBe(200);
|
|
500
|
+
expect(res.headers['x-mobify-from-cache']).toBe('false');
|
|
501
|
+
expect(res.headers['content-encoding']).toBe('gzip');
|
|
502
|
+
}).then(() => app.applicationCache.get({
|
|
503
|
+
key: keyFromURL(url),
|
|
504
|
+
namespace
|
|
505
|
+
})).then(entry => {
|
|
506
|
+
expect(entry.found).toBe(false);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
describe('DevServer helpers', () => {
|
|
511
|
+
test('Local asset headers', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
512
|
+
const tmpDir = yield _fsExtra.default.mkdtemp(_path.default.join(_os.default.tmpdir(), 'pwa-kit-'));
|
|
513
|
+
const tmpFile = _path.default.join(tmpDir, 'local-asset-headers-test.svg');
|
|
514
|
+
yield _fsExtra.default.ensureFile(tmpFile);
|
|
515
|
+
const now = new Date();
|
|
516
|
+
yield _fsExtra.default.utimes(tmpFile, now, now);
|
|
517
|
+
const res = new Map(); // Don't need a full Response, just `.set` functionality
|
|
518
|
+
(0, _buildDevServer.setLocalAssetHeaders)(res, tmpFile);
|
|
519
|
+
expect([...res]).toEqual([['content-type', 'image/svg+xml'], ['date', now.toUTCString()], ['last-modified', now.toUTCString()], ['etag', now.getTime()], ['cache-control', 'max-age=0, nocache, nostore, must-revalidate']]);
|
|
520
|
+
}));
|
|
521
|
+
});
|
|
522
|
+
describe('DevServer rendering', () => {
|
|
523
|
+
test('uses hot server middleware when ready', () => {
|
|
524
|
+
const req = {
|
|
525
|
+
app: {
|
|
526
|
+
__webpackReady: jest.fn().mockReturnValue(true),
|
|
527
|
+
__hotServerMiddleware: jest.fn()
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
const res = {};
|
|
531
|
+
const next = jest.fn();
|
|
532
|
+
NoWebpackDevServerFactory.render(req, res, next);
|
|
533
|
+
expect(req.app.__hotServerMiddleware).toHaveBeenCalledWith(req, res, next);
|
|
534
|
+
});
|
|
535
|
+
test('redirects to loading screen when not ready', () => {
|
|
536
|
+
const TestFactory = _objectSpread(_objectSpread({}, NoWebpackDevServerFactory), {}, {
|
|
537
|
+
_redirectToLoadingScreen: jest.fn()
|
|
538
|
+
});
|
|
539
|
+
const req = {
|
|
540
|
+
app: {
|
|
541
|
+
__webpackReady: jest.fn().mockReturnValue(false)
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const res = {};
|
|
545
|
+
const next = jest.fn();
|
|
546
|
+
TestFactory.render(req, res, next);
|
|
547
|
+
expect(TestFactory._redirectToLoadingScreen).toHaveBeenCalledWith(req, res, next);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
describe('DevServer service worker', () => {
|
|
551
|
+
let tmpDir;
|
|
552
|
+
beforeEach( /*#__PURE__*/_asyncToGenerator(function* () {
|
|
553
|
+
tmpDir = yield _fsExtra.default.mkdtemp(_path.default.join(_os.default.tmpdir(), 'pwa-kit-test-'));
|
|
554
|
+
}));
|
|
555
|
+
afterEach(() => {
|
|
556
|
+
_rimraf.default.sync(tmpDir);
|
|
557
|
+
});
|
|
558
|
+
const createApp = () => {
|
|
559
|
+
const app = NoWebpackDevServerFactory._createApp(opts());
|
|
560
|
+
// This isn't ideal! We need a way to test the dev middleware
|
|
561
|
+
// including the on demand webpack compiler. However, the webpack config and
|
|
562
|
+
// the Dev server assumes the code runs at the root of a project.
|
|
563
|
+
// When we run the tests, we are not in a project.
|
|
564
|
+
// We have a /test_fixtures project, but Jest does not support process.chdir(),
|
|
565
|
+
// nor mocking process.cwd(), so we mock the dev middleware for now.
|
|
566
|
+
// TODO: create a proper testing fixture project and run the tests in the isolated
|
|
567
|
+
// project environment.
|
|
568
|
+
return _extends(app, {
|
|
569
|
+
__devMiddleware: {
|
|
570
|
+
waitUntilValid: cb => cb(),
|
|
571
|
+
context: {
|
|
572
|
+
outputFileSystem: _fsExtra.default,
|
|
573
|
+
stats: {
|
|
574
|
+
toJson: () => ({
|
|
575
|
+
children: {
|
|
576
|
+
find: () => ({
|
|
577
|
+
outputPath: tmpDir
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
__webpackReady: () => true
|
|
585
|
+
});
|
|
586
|
+
};
|
|
587
|
+
const cases = [{
|
|
588
|
+
file: 'worker.js',
|
|
589
|
+
content: '// a service worker',
|
|
590
|
+
name: 'Should serve the service worker',
|
|
591
|
+
requestPath: '/worker.js'
|
|
592
|
+
}, {
|
|
593
|
+
file: 'worker.js.map',
|
|
594
|
+
content: '{}',
|
|
595
|
+
name: 'Should serve the service worker source map',
|
|
596
|
+
requestPath: '/worker.js.map'
|
|
597
|
+
}];
|
|
598
|
+
cases.forEach(({
|
|
599
|
+
file,
|
|
600
|
+
content,
|
|
601
|
+
name,
|
|
602
|
+
requestPath
|
|
603
|
+
}) => {
|
|
604
|
+
test(`${name}`, /*#__PURE__*/_asyncToGenerator(function* () {
|
|
605
|
+
const updatedFile = _path.default.resolve(tmpDir, file);
|
|
606
|
+
yield _fsExtra.default.writeFile(updatedFile, content);
|
|
607
|
+
const app = createApp();
|
|
608
|
+
app.get('/worker.js(.map)?', NoWebpackDevServerFactory.serveServiceWorker);
|
|
609
|
+
yield (0, _supertest.default)(app).get(requestPath).expect(200).then(res => expect(res.text).toEqual(content));
|
|
610
|
+
}));
|
|
611
|
+
test(`${name} (and handle 404s correctly)`, () => {
|
|
612
|
+
const app = createApp();
|
|
613
|
+
app.get('/worker.js(.map)?', NoWebpackDevServerFactory.serveServiceWorker);
|
|
614
|
+
return (0, _supertest.default)(app).get(requestPath).expect(404);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
describe('DevServer serveStaticFile', () => {
|
|
619
|
+
test('should serve static files', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
620
|
+
const options = opts({
|
|
621
|
+
projectDir: testFixtures
|
|
622
|
+
});
|
|
623
|
+
const app = NoWebpackDevServerFactory._createApp(options);
|
|
624
|
+
app.use('/test', NoWebpackDevServerFactory.serveStaticFile('static/favicon.ico'));
|
|
625
|
+
return (0, _supertest.default)(app).get('/test').expect(200);
|
|
626
|
+
}));
|
|
627
|
+
test('should return 404 if static file does not exist', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
628
|
+
const options = opts({
|
|
629
|
+
projectDir: testFixtures
|
|
630
|
+
});
|
|
631
|
+
const app = NoWebpackDevServerFactory._createApp(options);
|
|
632
|
+
app.use('/test', NoWebpackDevServerFactory.serveStaticFile('static/IDoNotExist.ico'));
|
|
633
|
+
return (0, _supertest.default)(app).get('/test').expect(404);
|
|
634
|
+
}));
|
|
635
|
+
});
|