@guardian/commercial-core 4.6.0 → 4.9.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.
@@ -22,6 +22,7 @@ declare class AdSize extends Array<number> {
22
22
  readonly [1]: number;
23
23
  constructor([width, height]: [number, number]);
24
24
  toString(): AdSizeString;
25
+ toArray(): number[];
25
26
  get width(): number;
26
27
  get height(): number;
27
28
  }
@@ -21,13 +21,17 @@ exports.createAdSize = exports.slotSizeMappings = exports.getAdSize = exports.ad
21
21
  class AdSize extends Array {
22
22
  constructor([width, height]) {
23
23
  super();
24
- this.push(width, height);
24
+ this[0] = width;
25
+ this[1] = height;
25
26
  }
26
27
  toString() {
27
28
  return this.width === 0 && this.height === 0
28
29
  ? 'fluid'
29
30
  : `${this.width},${this.height}`;
30
31
  }
32
+ toArray() {
33
+ return [this[0], this[1]];
34
+ }
31
35
  get width() {
32
36
  return this[0];
33
37
  }
@@ -1,9 +1,8 @@
1
- import type { AdSize, SizeMapping } from './ad-sizes';
1
+ import type { SizeMapping } from './ad-sizes';
2
2
  declare type SlotName = 'im' | 'high-merch' | 'high-merch-lucky' | 'high-merch-paid' | 'inline' | 'mostpop' | 'comments' | 'top-above-nav' | 'carrot' | 'epic' | 'mobile-sticky';
3
3
  declare type CreateSlotOptions = {
4
4
  classes?: string;
5
5
  name?: string;
6
- sizes?: Record<string, AdSize[]>;
7
6
  };
8
7
  /**
9
8
  * Given default size mappings and additional size mappings from
@@ -1,62 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.concatSizeMappings = exports.createAdSlot = void 0;
4
- const ad_sizes_1 = require("./ad-sizes");
5
4
  const breakpoint_1 = require("./lib/breakpoint");
6
5
  const adSlotIdPrefix = 'dfp-ad--';
7
6
  const adSlotConfigs = {
8
7
  im: {
9
8
  label: false,
10
9
  refresh: false,
11
- sizeMappings: ad_sizes_1.slotSizeMappings['im'],
12
10
  },
13
11
  'high-merch': {
14
12
  label: false,
15
13
  refresh: false,
16
14
  name: 'merchandising-high',
17
- sizeMappings: ad_sizes_1.slotSizeMappings['merchandising-high'],
18
15
  },
19
16
  'high-merch-lucky': {
20
17
  label: false,
21
18
  refresh: false,
22
19
  name: 'merchandising-high-lucky',
23
- sizeMappings: ad_sizes_1.slotSizeMappings['merchandising-high-lucky'],
24
20
  },
25
21
  'high-merch-paid': {
26
22
  label: false,
27
23
  refresh: false,
28
24
  name: 'merchandising-high',
29
- sizeMappings: ad_sizes_1.slotSizeMappings['merchandising-high'],
30
- },
31
- inline: {
32
- sizeMappings: ad_sizes_1.slotSizeMappings['inline'],
33
- },
34
- mostpop: {
35
- sizeMappings: ad_sizes_1.slotSizeMappings['mostpop'],
36
- },
37
- comments: {
38
- sizeMappings: ad_sizes_1.slotSizeMappings['comments'],
39
- },
40
- 'top-above-nav': {
41
- sizeMappings: ad_sizes_1.slotSizeMappings['top-above-nav'],
42
25
  },
43
26
  carrot: {
44
27
  label: false,
45
28
  refresh: false,
46
29
  name: 'carrot',
47
- sizeMappings: ad_sizes_1.slotSizeMappings['merchandising-high'],
48
30
  },
49
31
  epic: {
50
32
  label: false,
51
33
  refresh: false,
52
34
  name: 'epic',
53
- sizeMappings: ad_sizes_1.slotSizeMappings['epic'],
54
35
  },
55
36
  'mobile-sticky': {
56
37
  label: true,
57
38
  refresh: true,
58
39
  name: 'mobile-sticky',
59
- sizeMappings: ad_sizes_1.slotSizeMappings['mobile-sticky'],
60
40
  },
61
41
  };
62
42
  /**
@@ -85,10 +65,10 @@ const createAdSlotElement = (name, attrs, classes) => {
85
65
  const adSlot = document.createElement('div');
86
66
  adSlot.id = id;
87
67
  adSlot.className = `js-ad-slot ad-slot ${classes.join(' ')}`;
88
- adSlot.setAttribute('data-link-name', `ad slot ${name}`);
89
- adSlot.setAttribute('data-name', name);
68
+ adSlot.dataset.linkName = `ad slot ${name}`;
69
+ adSlot.dataset.name = name;
90
70
  adSlot.setAttribute('aria-hidden', 'true');
91
- Object.entries(attrs).forEach(([k, v]) => adSlot.setAttribute(k, v));
71
+ Object.entries(attrs).forEach(([k, v]) => (adSlot.dataset[k] = v));
92
72
  return adSlot;
93
73
  };
94
74
  /**
@@ -112,35 +92,16 @@ const concatSizeMappings = (defaultSizeMappings, optionSizeMappings = {}) => Obj
112
92
  return sizeMappings;
113
93
  }, { ...defaultSizeMappings });
114
94
  exports.concatSizeMappings = concatSizeMappings;
115
- /**
116
- * Convert size mappings to a string that will be added to the ad slot
117
- * data attributes.
118
- *
119
- * e.g. { desktop: [[320,250], [320, 280]] } => { desktop: '320,250|320,280' }
120
- *
121
- */
122
- const covertSizeMappingsToStrings = (sizeMappings) => Object.entries(sizeMappings).reduce((result, [device, sizes]) => {
123
- result[device] = sizes.join('|');
124
- return result;
125
- }, {});
126
- /**
127
- * Prefix all object keys with data-${key}
128
- */
129
- const createDataAttributes = (attrs) => Object.entries(attrs).reduce((result, [key, value]) => {
130
- result[`data-${key}`] = value;
131
- return result;
132
- }, {});
133
95
  const createAdSlot = (name, options = {}) => {
134
- const adSlotConfig = adSlotConfigs[name];
96
+ const adSlotConfig = adSlotConfigs[name] ?? {};
135
97
  const slotName = options.name ?? adSlotConfig.name ?? name;
136
- const sizeMappings = concatSizeMappings(adSlotConfig.sizeMappings, options.sizes);
137
- const attributes = covertSizeMappingsToStrings(sizeMappings);
98
+ const dataAttributes = {};
138
99
  if (adSlotConfig.label === false) {
139
- attributes.label = 'false';
100
+ dataAttributes.label = 'false';
140
101
  }
141
102
  if (adSlotConfig.refresh === false) {
142
- attributes.refresh = 'false';
103
+ dataAttributes.refresh = 'false';
143
104
  }
144
- return createAdSlotElement(slotName, createDataAttributes(attributes), createClasses(slotName, options.classes));
105
+ return createAdSlotElement(slotName, dataAttributes, createClasses(slotName, options.classes));
145
106
  };
146
107
  exports.createAdSlot = createAdSlot;
@@ -32,6 +32,7 @@ interface EventTimerProperties {
32
32
  pageHeightVH?: number;
33
33
  gpcSignal?: number;
34
34
  lazyLoadMarginPercent?: number;
35
+ hasLabsContainer?: boolean;
35
36
  }
36
37
  export declare class EventTimer {
37
38
  private _events;
@@ -14,6 +14,7 @@ export type { SizeKeys, AdSizeString, AdSize, SizeMapping, SlotSizeMappings, Slo
14
14
  export { isAdBlockInUse } from './detect-ad-blocker';
15
15
  export { clearPermutiveSegments, getPermutiveSegments, getPermutivePFPSegments, } from './permutive';
16
16
  export { initTrackScrollDepth } from './track-scroll-depth';
17
+ export { initTrackLabsContainer } from './track-labs-container';
17
18
  export { initTrackGpcSignal } from './track-gpc-signal';
18
19
  export { buildAdsConfigWithConsent, disabledAds } from './ad-targeting-youtube';
19
20
  export { createAdSlot, concatSizeMappings } from './create-ad-slot';
package/dist/cjs/index.js CHANGED
@@ -24,7 +24,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
24
24
  return result;
25
25
  };
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.pickTargetingValues = exports.getViewportTargeting = exports.getSharedTargeting = exports.getSessionTargeting = exports.getPersonalisedTargeting = exports.getContentTargeting = exports.constants = exports.concatSizeMappings = exports.createAdSlot = exports.disabledAds = exports.buildAdsConfigWithConsent = exports.initTrackGpcSignal = exports.initTrackScrollDepth = exports.getPermutivePFPSegments = exports.getPermutiveSegments = exports.clearPermutiveSegments = exports.isAdBlockInUse = exports.isBreakpoint = exports.createAdSize = exports.slotSizeMappings = exports.getAdSize = exports.adSizes = exports.initCommercialMetrics = exports.bypassCommercialMetricsSampling = exports.EventTimer = exports.remarketing = exports.inizio = exports.twitter = exports.fbPixel = exports.permutive = exports.ias = void 0;
27
+ exports.pickTargetingValues = exports.getViewportTargeting = exports.getSharedTargeting = exports.getSessionTargeting = exports.getPersonalisedTargeting = exports.getContentTargeting = exports.constants = exports.concatSizeMappings = exports.createAdSlot = exports.disabledAds = exports.buildAdsConfigWithConsent = exports.initTrackGpcSignal = exports.initTrackLabsContainer = exports.initTrackScrollDepth = exports.getPermutivePFPSegments = exports.getPermutiveSegments = exports.clearPermutiveSegments = exports.isAdBlockInUse = exports.isBreakpoint = exports.createAdSize = exports.slotSizeMappings = exports.getAdSize = exports.adSizes = exports.initCommercialMetrics = exports.bypassCommercialMetricsSampling = exports.EventTimer = exports.remarketing = exports.inizio = exports.twitter = exports.fbPixel = exports.permutive = exports.ias = void 0;
28
28
  var ias_1 = require("./third-party-tags/ias");
29
29
  Object.defineProperty(exports, "ias", { enumerable: true, get: function () { return ias_1.ias; } });
30
30
  var permutive_1 = require("./third-party-tags/permutive");
@@ -57,6 +57,8 @@ Object.defineProperty(exports, "getPermutiveSegments", { enumerable: true, get:
57
57
  Object.defineProperty(exports, "getPermutivePFPSegments", { enumerable: true, get: function () { return permutive_2.getPermutivePFPSegments; } });
58
58
  var track_scroll_depth_1 = require("./track-scroll-depth");
59
59
  Object.defineProperty(exports, "initTrackScrollDepth", { enumerable: true, get: function () { return track_scroll_depth_1.initTrackScrollDepth; } });
60
+ var track_labs_container_1 = require("./track-labs-container");
61
+ Object.defineProperty(exports, "initTrackLabsContainer", { enumerable: true, get: function () { return track_labs_container_1.initTrackLabsContainer; } });
60
62
  var track_gpc_signal_1 = require("./track-gpc-signal");
61
63
  Object.defineProperty(exports, "initTrackGpcSignal", { enumerable: true, get: function () { return track_gpc_signal_1.initTrackGpcSignal; } });
62
64
  var ad_targeting_youtube_1 = require("./ad-targeting-youtube");
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Collect commercial metrics on:
3
+ * - whether the page contains a Guardian Labs container element (aka 'dumathoin'), and if so
4
+ * - when the element is scrolled into view
5
+ */
6
+ declare const initTrackLabsContainer: () => void;
7
+ export { initTrackLabsContainer };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initTrackLabsContainer = void 0;
4
+ const libs_1 = require("@guardian/libs");
5
+ const event_timer_1 = require("./event-timer");
6
+ /**
7
+ * Collect commercial metrics on:
8
+ * - whether the page contains a Guardian Labs container element (aka 'dumathoin'), and if so
9
+ * - when the element is scrolled into view
10
+ */
11
+ const initTrackLabsContainer = () => {
12
+ const target = document.querySelector('section.dumathoin');
13
+ if (target === null)
14
+ return;
15
+ const eventTimer = event_timer_1.EventTimer.get();
16
+ (0, libs_1.log)('commercial', 'Page has labs container');
17
+ eventTimer.properties['hasLabsContainer'] = true;
18
+ const observer = new IntersectionObserver((entries) => {
19
+ entries.map((entry) => {
20
+ if (entry.isIntersecting) {
21
+ (0, libs_1.log)('commercial', 'Labs container in view');
22
+ eventTimer.trigger('labsContainerInView');
23
+ observer.unobserve(entry.target);
24
+ }
25
+ });
26
+ });
27
+ observer.observe(target);
28
+ };
29
+ exports.initTrackLabsContainer = initTrackLabsContainer;
@@ -22,6 +22,7 @@ declare class AdSize extends Array<number> {
22
22
  readonly [1]: number;
23
23
  constructor([width, height]: [number, number]);
24
24
  toString(): AdSizeString;
25
+ toArray(): number[];
25
26
  get width(): number;
26
27
  get height(): number;
27
28
  }
@@ -18,13 +18,17 @@
18
18
  class AdSize extends Array {
19
19
  constructor([width, height]) {
20
20
  super();
21
- this.push(width, height);
21
+ this[0] = width;
22
+ this[1] = height;
22
23
  }
23
24
  toString() {
24
25
  return this.width === 0 && this.height === 0
25
26
  ? 'fluid'
26
27
  : `${this.width},${this.height}`;
27
28
  }
29
+ toArray() {
30
+ return [this[0], this[1]];
31
+ }
28
32
  get width() {
29
33
  return this[0];
30
34
  }
@@ -1,9 +1,8 @@
1
- import type { AdSize, SizeMapping } from './ad-sizes';
1
+ import type { SizeMapping } from './ad-sizes';
2
2
  declare type SlotName = 'im' | 'high-merch' | 'high-merch-lucky' | 'high-merch-paid' | 'inline' | 'mostpop' | 'comments' | 'top-above-nav' | 'carrot' | 'epic' | 'mobile-sticky';
3
3
  declare type CreateSlotOptions = {
4
4
  classes?: string;
5
5
  name?: string;
6
- sizes?: Record<string, AdSize[]>;
7
6
  };
8
7
  /**
9
8
  * Given default size mappings and additional size mappings from
@@ -1,59 +1,39 @@
1
- import { slotSizeMappings } from './ad-sizes';
2
1
  import { isBreakpoint } from './lib/breakpoint';
3
2
  const adSlotIdPrefix = 'dfp-ad--';
4
3
  const adSlotConfigs = {
5
4
  im: {
6
5
  label: false,
7
6
  refresh: false,
8
- sizeMappings: slotSizeMappings['im'],
9
7
  },
10
8
  'high-merch': {
11
9
  label: false,
12
10
  refresh: false,
13
11
  name: 'merchandising-high',
14
- sizeMappings: slotSizeMappings['merchandising-high'],
15
12
  },
16
13
  'high-merch-lucky': {
17
14
  label: false,
18
15
  refresh: false,
19
16
  name: 'merchandising-high-lucky',
20
- sizeMappings: slotSizeMappings['merchandising-high-lucky'],
21
17
  },
22
18
  'high-merch-paid': {
23
19
  label: false,
24
20
  refresh: false,
25
21
  name: 'merchandising-high',
26
- sizeMappings: slotSizeMappings['merchandising-high'],
27
- },
28
- inline: {
29
- sizeMappings: slotSizeMappings['inline'],
30
- },
31
- mostpop: {
32
- sizeMappings: slotSizeMappings['mostpop'],
33
- },
34
- comments: {
35
- sizeMappings: slotSizeMappings['comments'],
36
- },
37
- 'top-above-nav': {
38
- sizeMappings: slotSizeMappings['top-above-nav'],
39
22
  },
40
23
  carrot: {
41
24
  label: false,
42
25
  refresh: false,
43
26
  name: 'carrot',
44
- sizeMappings: slotSizeMappings['merchandising-high'],
45
27
  },
46
28
  epic: {
47
29
  label: false,
48
30
  refresh: false,
49
31
  name: 'epic',
50
- sizeMappings: slotSizeMappings['epic'],
51
32
  },
52
33
  'mobile-sticky': {
53
34
  label: true,
54
35
  refresh: true,
55
36
  name: 'mobile-sticky',
56
- sizeMappings: slotSizeMappings['mobile-sticky'],
57
37
  },
58
38
  };
59
39
  /**
@@ -82,10 +62,10 @@ const createAdSlotElement = (name, attrs, classes) => {
82
62
  const adSlot = document.createElement('div');
83
63
  adSlot.id = id;
84
64
  adSlot.className = `js-ad-slot ad-slot ${classes.join(' ')}`;
85
- adSlot.setAttribute('data-link-name', `ad slot ${name}`);
86
- adSlot.setAttribute('data-name', name);
65
+ adSlot.dataset.linkName = `ad slot ${name}`;
66
+ adSlot.dataset.name = name;
87
67
  adSlot.setAttribute('aria-hidden', 'true');
88
- Object.entries(attrs).forEach(([k, v]) => adSlot.setAttribute(k, v));
68
+ Object.entries(attrs).forEach(([k, v]) => (adSlot.dataset[k] = v));
89
69
  return adSlot;
90
70
  };
91
71
  /**
@@ -108,35 +88,16 @@ const concatSizeMappings = (defaultSizeMappings, optionSizeMappings = {}) => Obj
108
88
  }
109
89
  return sizeMappings;
110
90
  }, { ...defaultSizeMappings });
111
- /**
112
- * Convert size mappings to a string that will be added to the ad slot
113
- * data attributes.
114
- *
115
- * e.g. { desktop: [[320,250], [320, 280]] } => { desktop: '320,250|320,280' }
116
- *
117
- */
118
- const covertSizeMappingsToStrings = (sizeMappings) => Object.entries(sizeMappings).reduce((result, [device, sizes]) => {
119
- result[device] = sizes.join('|');
120
- return result;
121
- }, {});
122
- /**
123
- * Prefix all object keys with data-${key}
124
- */
125
- const createDataAttributes = (attrs) => Object.entries(attrs).reduce((result, [key, value]) => {
126
- result[`data-${key}`] = value;
127
- return result;
128
- }, {});
129
91
  const createAdSlot = (name, options = {}) => {
130
- const adSlotConfig = adSlotConfigs[name];
92
+ const adSlotConfig = adSlotConfigs[name] ?? {};
131
93
  const slotName = options.name ?? adSlotConfig.name ?? name;
132
- const sizeMappings = concatSizeMappings(adSlotConfig.sizeMappings, options.sizes);
133
- const attributes = covertSizeMappingsToStrings(sizeMappings);
94
+ const dataAttributes = {};
134
95
  if (adSlotConfig.label === false) {
135
- attributes.label = 'false';
96
+ dataAttributes.label = 'false';
136
97
  }
137
98
  if (adSlotConfig.refresh === false) {
138
- attributes.refresh = 'false';
99
+ dataAttributes.refresh = 'false';
139
100
  }
140
- return createAdSlotElement(slotName, createDataAttributes(attributes), createClasses(slotName, options.classes));
101
+ return createAdSlotElement(slotName, dataAttributes, createClasses(slotName, options.classes));
141
102
  };
142
103
  export { createAdSlot, concatSizeMappings };
@@ -32,6 +32,7 @@ interface EventTimerProperties {
32
32
  pageHeightVH?: number;
33
33
  gpcSignal?: number;
34
34
  lazyLoadMarginPercent?: number;
35
+ hasLabsContainer?: boolean;
35
36
  }
36
37
  export declare class EventTimer {
37
38
  private _events;
@@ -14,6 +14,7 @@ export type { SizeKeys, AdSizeString, AdSize, SizeMapping, SlotSizeMappings, Slo
14
14
  export { isAdBlockInUse } from './detect-ad-blocker';
15
15
  export { clearPermutiveSegments, getPermutiveSegments, getPermutivePFPSegments, } from './permutive';
16
16
  export { initTrackScrollDepth } from './track-scroll-depth';
17
+ export { initTrackLabsContainer } from './track-labs-container';
17
18
  export { initTrackGpcSignal } from './track-gpc-signal';
18
19
  export { buildAdsConfigWithConsent, disabledAds } from './ad-targeting-youtube';
19
20
  export { createAdSlot, concatSizeMappings } from './create-ad-slot';
package/dist/esm/index.js CHANGED
@@ -12,6 +12,7 @@ export { isBreakpoint } from './lib/breakpoint';
12
12
  export { isAdBlockInUse } from './detect-ad-blocker';
13
13
  export { clearPermutiveSegments, getPermutiveSegments, getPermutivePFPSegments, } from './permutive';
14
14
  export { initTrackScrollDepth } from './track-scroll-depth';
15
+ export { initTrackLabsContainer } from './track-labs-container';
15
16
  export { initTrackGpcSignal } from './track-gpc-signal';
16
17
  export { buildAdsConfigWithConsent, disabledAds } from './ad-targeting-youtube';
17
18
  export { createAdSlot, concatSizeMappings } from './create-ad-slot';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Collect commercial metrics on:
3
+ * - whether the page contains a Guardian Labs container element (aka 'dumathoin'), and if so
4
+ * - when the element is scrolled into view
5
+ */
6
+ declare const initTrackLabsContainer: () => void;
7
+ export { initTrackLabsContainer };
@@ -0,0 +1,26 @@
1
+ import { log } from '@guardian/libs';
2
+ import { EventTimer } from './event-timer';
3
+ /**
4
+ * Collect commercial metrics on:
5
+ * - whether the page contains a Guardian Labs container element (aka 'dumathoin'), and if so
6
+ * - when the element is scrolled into view
7
+ */
8
+ const initTrackLabsContainer = () => {
9
+ const target = document.querySelector('section.dumathoin');
10
+ if (target === null)
11
+ return;
12
+ const eventTimer = EventTimer.get();
13
+ log('commercial', 'Page has labs container');
14
+ eventTimer.properties['hasLabsContainer'] = true;
15
+ const observer = new IntersectionObserver((entries) => {
16
+ entries.map((entry) => {
17
+ if (entry.isIntersecting) {
18
+ log('commercial', 'Labs container in view');
19
+ eventTimer.trigger('labsContainerInView');
20
+ observer.unobserve(entry.target);
21
+ }
22
+ });
23
+ });
24
+ observer.observe(target);
25
+ };
26
+ export { initTrackLabsContainer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guardian/commercial-core",
3
- "version": "4.6.0",
3
+ "version": "4.9.0",
4
4
  "description": "Guardian advertising business logic",
5
5
  "homepage": "https://github.com/guardian/commercial-core#readme",
6
6
  "bugs": {
@@ -47,9 +47,9 @@
47
47
  "@guardian/ab-core": "^2.0.0",
48
48
  "@guardian/consent-management-platform": "^10.6.0",
49
49
  "@guardian/eslint-config-typescript": "^1.0.0",
50
- "@guardian/libs": "5.1.0",
51
- "@guardian/prettier": "^1.0.0",
52
- "@octokit/core": "^3.5.1",
50
+ "@guardian/libs": "^7.1.3",
51
+ "@guardian/prettier": "^2.1.1",
52
+ "@octokit/core": "^4.0.5",
53
53
  "@semantic-release/github": "^8.0.2",
54
54
  "@types/google.analytics": "^0.0.42",
55
55
  "@types/googletag": "^2.0.0",
@@ -57,7 +57,7 @@
57
57
  "@typescript-eslint/eslint-plugin": "^5.5.0",
58
58
  "@typescript-eslint/parser": "^5.5.0",
59
59
  "commitizen": "^4.2.4",
60
- "conventional-changelog-conventionalcommits": "^4.6.3",
60
+ "conventional-changelog-conventionalcommits": "^5.0.0",
61
61
  "cz-conventional-changelog": "^3.3.0",
62
62
  "eslint": "^8.4.1",
63
63
  "eslint-config-prettier": "^8.3.0",
@@ -82,6 +82,6 @@
82
82
  },
83
83
  "peerDependencies": {
84
84
  "@guardian/ab-core": "^2.0.0",
85
- "@guardian/libs": "^3.3.0"
85
+ "@guardian/libs": ">6"
86
86
  }
87
87
  }