prebid-universal-creative 1.14.2 → 1.16.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 (41) hide show
  1. package/.babelrc +1 -2
  2. package/.github/workflows/issue_tracker.yml +31 -16
  3. package/README.md +21 -3
  4. package/dist/amp.js +3 -0
  5. package/dist/banner.js +3 -0
  6. package/dist/creative.js +4 -3
  7. package/dist/creative.max.js +546 -593
  8. package/dist/load-cookie-with-consent.html +1 -1
  9. package/dist/load-cookie.html +1 -1
  10. package/dist/mobile.js +3 -0
  11. package/dist/native-render.js +3 -3
  12. package/dist/native-trk.js +3 -3
  13. package/dist/native.js +3 -0
  14. package/dist/uid.js +3 -3
  15. package/dist/video.js +3 -0
  16. package/gulpfile.js +84 -45
  17. package/karma.conf.maker.js +4 -6
  18. package/package.json +80 -82
  19. package/src/ampOrMobile.js +14 -0
  20. package/src/creative.js +2 -9
  21. package/src/environment.js +62 -75
  22. package/src/legacy.js +29 -0
  23. package/src/legacyNativeRender.js +6 -0
  24. package/src/mobileAndAmpRender.js +239 -0
  25. package/src/nativeAssetManager.js +91 -57
  26. package/src/nativeORTBTrackerManager.js +2 -2
  27. package/src/nativeRender.js +2 -2
  28. package/src/nativeRenderManager.js +46 -69
  29. package/src/postscribeRender.js +10 -0
  30. package/src/renderingManager.js +106 -358
  31. package/src/utils.js +1 -11
  32. package/template/amp/dfp-creative.html +1 -1
  33. package/test/spec/environment_spec.js +4 -11
  34. package/test/spec/legacyNativeRender_spec.js +25 -0
  35. package/test/spec/mobileAndAmpRender_spec.js +316 -0
  36. package/test/spec/nativeAssetManager_spec.js +227 -79
  37. package/test/spec/nativeORTBTrackerManager_spec.js +3 -19
  38. package/test/spec/nativeRenderManager_spec.js +77 -55
  39. package/test/spec/nativeRender_spec.js +23 -0
  40. package/test/spec/renderingManager_spec.js +16 -265
  41. package/webpack.conf.js +3 -1
@@ -37,40 +37,24 @@ describe('test firing native trackers', function () {
37
37
 
38
38
 
39
39
  it('should fire impression trackers', function () {
40
- let imgUrl = 'foo.bar/event?type=img';
41
- let jsUrl = 'foo.bar/event?type=js';
42
-
43
-
44
40
  fireNativeImpressionTrackers("abc123", sendMessage);
45
41
 
46
42
  expect(sendMessage.getCall(0).args[0]).to.deep.equal({
47
- message: 'Prebid Native',
43
+ message: 'Prebid Native',
48
44
  action: 'fireNativeImpressionTrackers',
49
45
  adId: 'abc123'
50
46
  })
51
47
  });
52
48
 
53
49
  it('should fire asset clicktrackers', function () {
54
- let assetTrackers = ['foo.bar/click?id=1', 'foo.bar/click?id=2'];
55
- let mainTrackers = ['foo.bar/click?id=3'];
56
50
  let adId = "abc123";
57
- let nativeOrtb = {
58
- assets: [{
59
- id: 1,
60
- link: { clicktrackers: assetTrackers }
61
- }],
62
- link: {
63
- clicktrackers: mainTrackers
64
- }
65
- }
66
-
67
- addNativeClickTrackers(adId, nativeOrtb, sendMessage);
51
+ addNativeClickTrackers(adId, sendMessage);
68
52
  expect(sendMessage.getCall(0).args[0]).to.deep.equal({
69
53
  message: "Prebid Native",
70
54
  action: 'click',
71
55
  adId: 'abc123',
72
56
  assetId: 1
73
57
  });
74
-
58
+
75
59
  });
76
60
  });
@@ -1,8 +1,7 @@
1
- import { newNativeRenderManager } from 'src/nativeRenderManager';
2
- import * as nam from 'src/nativeAssetManager';
3
- import { expect } from 'chai';
4
- import { mocks } from 'test/helpers/mocks';
5
- import { merge } from 'lodash';
1
+ import {newNativeRenderManager} from 'src/nativeRenderManager';
2
+ import * as nam from 'src/nativeAssetManager';
3
+ import {mocks} from 'test/helpers/mocks';
4
+ import {merge} from 'lodash';
6
5
 
7
6
  const renderingMocks = {
8
7
  getWindowObject: function() {
@@ -15,10 +14,6 @@ const renderingMocks = {
15
14
  }
16
15
  };
17
16
 
18
- function trimPort(url) {
19
- return ((/:\d+/).test(url)) ? url.substring(0, url.lastIndexOf(':')) : url;
20
- }
21
-
22
17
  describe('nativeRenderManager', function () {
23
18
  describe('load renderNativeAd', function () {
24
19
  let mockWin;
@@ -55,56 +50,83 @@ describe('nativeRenderManager', function () {
55
50
  assetManagerStub.restore();
56
51
  });
57
52
 
58
- it('should verify the postMessage for impression trackers was executed', function() {
59
- mockWin.document.getElementsByClassName = () => [{
60
- attributes: {
61
- pbAdId: {
62
- value: 'ad123'
63
- }
64
- },
65
- addEventListener: (type, listener, capture) => {
66
- },
67
- }];
68
- let nativeTracker = new newNativeRenderManager(mockWin);
69
- nativeTracker.renderNativeAd(tagData);
70
-
71
- expect(mockWin.parent.postMessage.callCount).to.equal(1);
72
- let postMessageTargetDomain = mockWin.parent.postMessage.args[0][1];
73
- let postMessageContents = mockWin.parent.postMessage.args[0][0];
74
- let rawPostMessage = JSON.parse(postMessageContents);
75
-
76
- expect(rawPostMessage.message).to.exist.and.to.equal("Prebid Native");
77
- expect(rawPostMessage.adId).to.exist.and.to.equal("ad123");
78
- expect(rawPostMessage.action).to.not.exist;
79
- expect(trimPort(postMessageTargetDomain)).to.equal(tagData.pubUrl);
80
- });
53
+ describe('should fire event', () => {
54
+ let recvMessages;
55
+ function mockMessenger() {
56
+ return recvMessages.push.bind(recvMessages);
57
+ }
81
58
 
82
- it('should verify the postMessages for the impression and click trackers were executed', function() {
83
- mockWin.document.getElementsByClassName = () => [{
84
- attributes: {
85
- pbAdId: {
86
- value: 'ad123'
59
+ beforeEach(() => {
60
+ recvMessages = [];
61
+ });
62
+ it('AD_RENDER_SUCCEEDED', () => {
63
+ const mockAssetMgr = function () {
64
+ return {
65
+ loadAssets(_, fn) {
66
+ fn();
67
+ }
87
68
  }
88
- },
89
- addEventListener: ((type, listener, capture) => {
90
- listener({
69
+ }
70
+ const rdr = newNativeRenderManager(mockWin, mockMessenger, mockAssetMgr);
71
+ rdr.renderNativeAd(mockWin.document, tagData);
72
+ sinon.assert.match(recvMessages[0], {
73
+ message: 'Prebid Event',
74
+ adId: 'ad123',
75
+ event: 'adRenderSucceeded',
76
+ })
77
+ })
78
+ describe('AD_RENDER_FAILED', () => {
79
+ it('on exceptions', () => {
80
+ const rdr = newNativeRenderManager(mockWin, mockMessenger);
81
+ rdr.renderNativeAd(mockWin.document, Object.defineProperties({...tagData}, {
82
+ rendererUrl: {
83
+ get() {
84
+ throw new Error('err');
85
+ }
86
+ }
87
+ }));
88
+ sinon.assert.match(recvMessages[0], {
89
+ message: 'Prebid Event',
90
+ adId: 'ad123',
91
+ event: 'adRenderFailed',
92
+ info: {
93
+ reason: 'exception',
94
+ message: 'err'
95
+ }
96
+ })
97
+ });
98
+ it('on missing adId',() => {
99
+ const rdr = newNativeRenderManager(mockWin, mockMessenger);
100
+ rdr.renderNativeAd(mockWin.document, {...tagData, adId: undefined});
101
+ sinon.assert.match(recvMessages[0], {
102
+ message: 'Prebid Event',
103
+ event: 'adRenderFailed',
104
+ info: {
105
+ reason: 'missingDocOrAdid',
106
+ }
107
+ })
108
+ });
109
+ it('on rendering errors', () => {
110
+ const mockAssetMgr = function () {
111
+ return {
112
+ loadAssets(_1, _2, err) {
113
+ err(new Error('err'))
114
+ }
115
+ }
116
+ }
117
+ const rdr = newNativeRenderManager(mockWin, mockMessenger, mockAssetMgr);
118
+ rdr.renderNativeAd(mockWin.document, tagData);
119
+ sinon.assert.match(recvMessages[0], {
120
+ message: 'Prebid Event',
121
+ event: 'adRenderFailed',
122
+ info: {
123
+ reason: 'exception',
124
+ message: 'err'
125
+ }
91
126
  })
92
127
  })
93
- }];
94
-
95
- let nativeTracker = new newNativeRenderManager(mockWin);
96
- nativeTracker.renderNativeAd(tagData);
97
-
98
- expect(mockWin.parent.postMessage.callCount).to.equal(2);
99
-
100
- let postMessageTargetDomain = mockWin.parent.postMessage.args[0][1];
101
- let postMessageContents = mockWin.parent.postMessage.args[1][0];
102
- let rawPostMessage = JSON.parse(postMessageContents);
103
-
104
- expect(rawPostMessage.message).to.exist.and.to.equal("Prebid Native");
105
- expect(rawPostMessage.adId).to.exist.and.to.equal("ad123");
106
- expect(rawPostMessage.action).to.exist.and.to.equal('click');
107
- expect(trimPort(postMessageTargetDomain)).to.equal(tagData.pubUrl);
128
+ })
108
129
  });
109
130
  });
131
+
110
132
  });
@@ -0,0 +1,23 @@
1
+ import '../../src/nativeRender';
2
+
3
+ describe('nativeRender', () => {
4
+
5
+ after(() => {
6
+ delete window.ucTag;
7
+ })
8
+
9
+ it('should accept 2 arguments', () => {
10
+ expect(window.ucTag.renderAd).to.exist;
11
+ //expect exactly two arguments by this function
12
+ expect(window.ucTag.renderAd.length).to.equal(2);
13
+
14
+ // this function with two arguments and see it NOT throwing
15
+ const renderAd = window.ucTag.renderAd.bind(this, document, {
16
+ pubUrl: 'http://prebidjs.com',
17
+ adId: 'abc123',
18
+ replaceAllAssets: true
19
+ })
20
+ expect(renderAd).to.not.throw();
21
+
22
+ })
23
+ })
@@ -1,4 +1,4 @@
1
- import { newRenderingManager } from 'src/renderingManager';
1
+ import { renderCrossDomain, renderLegacy } from 'src/renderingManager';
2
2
  import * as utils from 'src/utils';
3
3
  import * as domHelper from 'src/domHelper';
4
4
  import { expect } from 'chai';
@@ -70,251 +70,12 @@ describe('renderingManager', function() {
70
70
  xhr.restore();
71
71
  });
72
72
 
73
- describe('mobile creative', function() {
74
- let writeHtmlSpy;
75
- let sendRequestSpy;
76
- let triggerPixelSpy;
77
- let mockWin;
78
-
79
- before(function() {
80
- writeHtmlSpy = sinon.spy(utils, 'writeAdHtml');
81
- sendRequestSpy = sinon.spy(utils, 'sendRequest');
82
- triggerPixelSpy = sinon.spy(utils, 'triggerPixel');
83
- mockWin = merge(mocks.createFakeWindow('http://example.com'), renderingMocks().getWindowObject());
84
- });
85
-
86
- afterEach(function() {
87
- writeHtmlSpy.resetHistory();
88
- sendRequestSpy.resetHistory();
89
- triggerPixelSpy.resetHistory();
90
- });
91
-
92
- after(function() {
93
- writeHtmlSpy.restore();
94
- sendRequestSpy.restore();
95
- triggerPixelSpy.restore();
96
- });
97
-
98
- const env = {
99
- isMobileApp: () => true,
100
- isSafeFrame: () => false
101
- };
102
-
103
- it('should render mobile app creative', function() {
104
- const renderObject = newRenderingManager(mockWin, env);
105
- let ucTagData = {
106
- cacheHost: 'example.com',
107
- cachePath: '/path',
108
- uuid: '123',
109
- size: '300x250'
110
- };
111
-
112
- renderObject.renderAd(mockWin.document, ucTagData);
113
-
114
- let response = {
115
- width: 300,
116
- height: 250,
117
- crid: 123,
118
- adm: 'ad-markup',
119
- wurl: 'https://test.prebidcache.wurl'
120
- };
121
- requests[0].respond(200, {}, JSON.stringify(response));
122
- expect(writeHtmlSpy.callCount).to.equal(1);
123
- expect(sendRequestSpy.args[0][0]).to.equal('https://example.com/path?uuid=123');
124
- });
125
-
126
- it('should render mobile app creative with missing cache wurl', function() {
127
- const renderObject = newRenderingManager(mockWin, env);
128
- let ucTagData = {
129
- cacheHost: 'example.com',
130
- cachePath: '/path',
131
- uuid: '123',
132
- size: '300x250'
133
- };
134
-
135
- renderObject.renderAd(mockWin.document, ucTagData);
136
-
137
- let response = {
138
- width: 300,
139
- height: 250,
140
- crid: 123,
141
- adm: 'ad-markup'
142
- };
143
- requests[0].respond(200, {}, JSON.stringify(response));
144
- expect(writeHtmlSpy.callCount).to.equal(1);
145
- expect(sendRequestSpy.args[0][0]).to.equal('https://example.com/path?uuid=123');
146
- });
147
-
148
- it('should render mobile app creative using default cacheHost and cachePath', function() {
149
- const renderObject = newRenderingManager(mockWin, env);
150
- let ucTagData = {
151
- uuid: '123',
152
- size: '300x250'
153
- };
154
-
155
- renderObject.renderAd(mockWin.document, ucTagData);
156
-
157
- let response = {
158
- width: 300,
159
- height: 250,
160
- crid: 123,
161
- adm: 'ad-markup'
162
- };
163
- requests[0].respond(200, {}, JSON.stringify(response));
164
- expect(writeHtmlSpy.callCount).to.equal(1);
165
- expect(sendRequestSpy.args[0][0]).to.equal('https://prebid.adnxs.com/pbc/v1/cache?uuid=123');
166
- });
167
-
168
- // it('should catch errors from creative', function (done) {
169
- // window.addEventListener('error', e => {
170
- // done(e.error);
171
- // });
172
-
173
- // const consoleErrorSpy = sinon.spy(console, 'error');
174
-
175
- // const renderObject = newRenderingManager(mockWin, env);
176
- // let ucTagData = {
177
- // cacheHost: 'example.com',
178
- // cachePath: '/path',
179
- // uuid: '123',
180
- // size: '300x250'
181
- // };
182
-
183
- // renderObject.renderAd(mockWin.document, ucTagData);
184
-
185
- // let response = {
186
- // width: 300,
187
- // height: 250,
188
- // crid: 123,
189
- // adm: '<script src="notExistingScript.js"></script>'
190
- // };
191
- // requests[0].respond(200, {}, JSON.stringify(response));
192
-
193
- // setTimeout(()=>{
194
- // expect(consoleErrorSpy.callCount).to.equal(1);
195
- // done();
196
- // }, 10);
197
- // });
198
- });
199
-
200
- describe('amp creative', function() {
201
- let writeHtmlSpy;
202
- let sendRequestSpy;
203
- let triggerPixelSpy;
204
- let mockWin;
205
-
206
- before(function() {
207
- writeHtmlSpy = sinon.spy(utils, 'writeAdHtml');
208
- sendRequestSpy = sinon.spy(utils, 'sendRequest');
209
- triggerPixelSpy = sinon.spy(utils, 'triggerPixel');
210
- mockWin = merge(mocks.createFakeWindow('http://example.com'), renderingMocks().getWindowObject());
211
- });
212
-
213
- afterEach(function() {
214
- writeHtmlSpy.resetHistory();
215
- sendRequestSpy.resetHistory();
216
- triggerPixelSpy.resetHistory();
217
- });
218
-
219
- after(function() {
220
- writeHtmlSpy.restore();
221
- sendRequestSpy.restore();
222
- triggerPixelSpy.restore();
223
- });
224
-
225
- const env = {
226
- isMobileApp: () => false,
227
- isAmp: () => true,
228
- isSafeFrame: () => true
229
- };
230
-
231
- it('should render amp creative', function() {
232
- const renderObject = newRenderingManager(mockWin, env);
233
-
234
- let ucTagData = {
235
- cacheHost: 'example.com',
236
- cachePath: '/path',
237
- uuid: '123',
238
- size: '300x250',
239
- hbPb: '10.00'
240
- };
241
-
242
- renderObject.renderAd(mockWin.document, ucTagData);
243
-
244
- let response = {
245
- width: 300,
246
- height: 250,
247
- crid: 123,
248
- adm: 'ad-markup${AUCTION_PRICE}',
249
- wurl: 'https://test.prebidcache.wurl'
250
- };
251
- requests[0].respond(200, {}, JSON.stringify(response));
252
- expect(writeHtmlSpy.args[0][0]).to.equal('<!--Creative 123 served by Prebid.js Header Bidding-->ad-markup10.00');
253
- expect(sendRequestSpy.args[0][0]).to.equal('https://example.com/path?uuid=123');
254
- expect(triggerPixelSpy.args[0][0]).to.equal('https://test.prebidcache.wurl');
255
- });
256
-
257
- it('should replace AUCTION_PRICE with response.price over hbPb', function() {
258
- const renderObject = newRenderingManager(mockWin, env);
259
-
260
- let ucTagData = {
261
- cacheHost: 'example.com',
262
- cachePath: '/path',
263
- uuid: '123',
264
- size: '300x250',
265
- hbPb: '10.00'
266
- };
267
-
268
- renderObject.renderAd(mockWin.document, ucTagData);
269
-
270
- let response = {
271
- width: 300,
272
- height: 250,
273
- crid: 123,
274
- price: 12.50,
275
- adm: 'ad-markup${AUCTION_PRICE}',
276
- wurl: 'https://test.prebidcache.wurl'
277
- };
278
- requests[0].respond(200, {}, JSON.stringify(response));
279
- expect(writeHtmlSpy.args[0][0]).to.equal('<!--Creative 123 served by Prebid.js Header Bidding-->ad-markup12.5');
280
- expect(sendRequestSpy.args[0][0]).to.equal('https://example.com/path?uuid=123');
281
- expect(triggerPixelSpy.args[0][0]).to.equal('https://test.prebidcache.wurl');
282
- });
283
-
284
- it('should replace AUCTION_PRICE with with empty value when neither price nor hbPb exist', function() {
285
- const renderObject = newRenderingManager(mockWin, env);
286
-
287
- let ucTagData = {
288
- cacheHost: 'example.com',
289
- cachePath: '/path',
290
- uuid: '123',
291
- size: '300x250'
292
- };
293
-
294
- renderObject.renderAd(mockWin.document, ucTagData);
295
-
296
- let response = {
297
- width: 300,
298
- height: 250,
299
- crid: 123,
300
- adm: 'ad-markup${AUCTION_PRICE}',
301
- wurl: 'https://test.prebidcache.wurl'
302
- };
303
- requests[0].respond(200, {}, JSON.stringify(response));
304
- expect(writeHtmlSpy.args[0][0]).to.equal('<!--Creative 123 served by Prebid.js Header Bidding-->ad-markup');
305
- expect(sendRequestSpy.args[0][0]).to.equal('https://example.com/path?uuid=123');
306
- expect(triggerPixelSpy.args[0][0]).to.equal('https://test.prebidcache.wurl');
307
- });
308
- });
309
-
310
73
  describe('cross domain creative', function() {
311
74
  const ORIGIN = 'http://example.com';
312
75
  let parseStub;
313
76
  let iframeStub;
314
77
  let triggerPixelSpy;
315
78
  let mockWin;
316
- let env;
317
- let renderObject;
318
79
  let ucTagData;
319
80
  let mockIframe;
320
81
  let eventSource;
@@ -329,22 +90,18 @@ describe('renderingManager', function() {
329
90
  host: 'example.com'
330
91
  });
331
92
  mockWin = merge(mocks.createFakeWindow(ORIGIN), renderingMocks().getWindowObject());
332
- env = {
333
- isMobileApp: () => false,
334
- isAmp: () => false,
335
- canLocatePrebid: () => false
336
- };
337
- renderObject = newRenderingManager(mockWin, env);
338
93
  ucTagData = {
339
94
  adId: '123',
340
95
  adServerDomain: 'mypub.com',
341
96
  pubUrl: ORIGIN,
342
97
  };
343
- renderObject.renderAd(mockWin.document, ucTagData);
98
+ eventSource = null;
99
+
100
+ renderCrossDomain(mockWin, ucTagData.adId, ucTagData.adServerDomain, ucTagData.pubUrl);
344
101
 
345
102
  });
346
103
 
347
- afterEach(function() {
104
+ afterEach(function () {
348
105
  parseStub.restore();
349
106
  iframeStub.restore();
350
107
  triggerPixelSpy.restore();
@@ -353,17 +110,17 @@ describe('renderingManager', function() {
353
110
  function mockPrebidResponse(msg) {
354
111
  mockWin.postMessage({
355
112
  origin: ORIGIN,
356
- message: JSON.stringify(Object.assign({message: 'Prebid Response'}, msg))
113
+ message: JSON.stringify(Object.assign({ message: 'Prebid Response' }, msg))
357
114
  });
358
115
  }
359
116
 
360
- it('should render cross domain creative', function() {
117
+ it("should render cross domain creative", function () {
361
118
  mockPrebidResponse({
362
- ad: 'ad',
119
+ ad: "ad",
363
120
  adUrl: ORIGIN,
364
- adId: '123',
121
+ adId: "123",
365
122
  width: 300,
366
- height: 250
123
+ height: 250,
367
124
  });
368
125
  expect(mockIframe.contentDocument.write.args[0][0]).to.equal("ad");
369
126
  });
@@ -390,13 +147,13 @@ describe('renderingManager', function() {
390
147
  info: {
391
148
  reason: 'preventWritingOnMainDocument'
392
149
  }
393
- })
150
+ });
394
151
  });
395
152
 
396
153
  it('on ads that have no markup or adUrl', () => {
397
154
  mockPrebidResponse({
398
155
  adId: '123',
399
- })
156
+ });
400
157
  expectEventMessage({
401
158
  adId: '123',
402
159
  event: RENDER_FAILED,
@@ -408,7 +165,7 @@ describe('renderingManager', function() {
408
165
 
409
166
  it('on exceptions', () => {
410
167
  iframeStub.callsFake(() => {
411
- throw new Error()
168
+ throw new Error();
412
169
  });
413
170
  mockPrebidResponse({
414
171
  adId: '123',
@@ -422,7 +179,7 @@ describe('renderingManager', function() {
422
179
  reason: 'exception'
423
180
  }
424
181
  });
425
- })
182
+ });
426
183
  });
427
184
  describe('should post AD_RENDER_SUCCEEDED', () => {
428
185
  it('on ad with markup', () => {
@@ -452,18 +209,12 @@ describe('renderingManager', function() {
452
209
  describe('legacy creative', function() {
453
210
  it('should render legacy creative', function() {
454
211
  const mockWin = merge(mocks.createFakeWindow('http://example.com'), renderingMocks().getWindowObject());
455
- const env = {
456
- isMobileApp: () => false,
457
- isAmp: () => false,
458
- canLocatePrebid: () => true
459
- };
460
- const renderObject = newRenderingManager(mockWin, env);
461
-
462
212
  let ucTagData = {
463
213
  adId: '123'
464
214
  };
215
+ window.parent = mockWin;
465
216
 
466
- renderObject.renderAd(mockWin.document, ucTagData);
217
+ renderLegacy(mockWin.document, ucTagData.adId);
467
218
  expect(mockWin.parent.$$PREBID_GLOBAL$$.renderAd.callCount).to.equal(1);
468
219
  });
469
220
  });
package/webpack.conf.js CHANGED
@@ -1,6 +1,7 @@
1
1
  var creative = require('./package.json');
2
2
  var StringReplacePlugin = require('string-replace-webpack-plugin');
3
3
  var path = require('path');
4
+ const ShakePlugin = require('webpack-common-shake').Plugin;
4
5
 
5
6
  module.exports = {
6
7
  devtool: 'source-map',
@@ -42,5 +43,6 @@ module.exports = {
42
43
  })
43
44
  }
44
45
  ]
45
- }
46
+ },
47
+ plugins: [new ShakePlugin()],
46
48
  };