@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.
Files changed (54) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +35 -0
  3. package/bin/pwa-kit-dev.js +461 -0
  4. package/configs/babel/babel-config.js +34 -0
  5. package/configs/eslint/README.md +21 -0
  6. package/configs/eslint/eslint-config.js +11 -0
  7. package/configs/eslint/index.js +11 -0
  8. package/configs/eslint/no-react.js +18 -0
  9. package/configs/eslint/partials/base.js +38 -0
  10. package/configs/eslint/partials/jest.js +24 -0
  11. package/configs/eslint/partials/react.js +29 -0
  12. package/configs/eslint/partials/typescript-permit-any.js +31 -0
  13. package/configs/eslint/partials/typescript.js +17 -0
  14. package/configs/eslint/recommended.js +20 -0
  15. package/configs/eslint/safe-types.js +20 -0
  16. package/configs/jest/jest-babel-transform.js +19 -0
  17. package/configs/jest/jest.config.js +33 -0
  18. package/configs/jest/mocks/fileMock.js +9 -0
  19. package/configs/jest/mocks/styleMock.js +9 -0
  20. package/configs/jest/mocks/svgMock.js +11 -0
  21. package/configs/webpack/config-names.js +24 -0
  22. package/configs/webpack/config.js +425 -0
  23. package/configs/webpack/overrides-plugin.js +120 -0
  24. package/configs/webpack/plugins.js +92 -0
  25. package/package.json +150 -0
  26. package/scripts/version.js +22 -0
  27. package/ssr/server/build-dev-server.js +443 -0
  28. package/ssr/server/build-dev-server.test.js +635 -0
  29. package/ssr/server/loading-screen/css/main.css +272 -0
  30. package/ssr/server/loading-screen/css/normalize.css +349 -0
  31. package/ssr/server/loading-screen/img/cloud-1.svg +1 -0
  32. package/ssr/server/loading-screen/img/cloud-2.svg +1 -0
  33. package/ssr/server/loading-screen/img/cloud-3.svg +1 -0
  34. package/ssr/server/loading-screen/img/cloud.svg +1 -0
  35. package/ssr/server/loading-screen/img/codey-arm.svg +1 -0
  36. package/ssr/server/loading-screen/img/codey-bear.svg +1 -0
  37. package/ssr/server/loading-screen/img/codey-bg.svg +1 -0
  38. package/ssr/server/loading-screen/img/codey-cloud.svg +1 -0
  39. package/ssr/server/loading-screen/img/codey-search.svg +1 -0
  40. package/ssr/server/loading-screen/img/codey.svg +1 -0
  41. package/ssr/server/loading-screen/img/codeyCarry.svg +1 -0
  42. package/ssr/server/loading-screen/img/devDocumentation.svg +1 -0
  43. package/ssr/server/loading-screen/img/devGithub.svg +1 -0
  44. package/ssr/server/loading-screen/img/devTrailhead.svg +1 -0
  45. package/ssr/server/loading-screen/img/logo.svg +1 -0
  46. package/ssr/server/loading-screen/img/slds_spinner_brand_9EA9F1.gif +0 -0
  47. package/ssr/server/loading-screen/index.html +130 -0
  48. package/ssr/server/test_fixtures/app/main.js +6 -0
  49. package/ssr/server/test_fixtures/app/static/favicon.ico +0 -0
  50. package/ssr/server/test_fixtures/localhost.pem +45 -0
  51. package/utils/script-utils.js +312 -0
  52. package/utils/script-utils.test.js +282 -0
  53. package/utils/test-fixtures/minimal-built-app/ssr.js +9 -0
  54. 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
+ });